Summary of Changes:

- rounded intervals now come with a "description" property that gives a simple human-readable description of the interval. (ie. "5 hours", "10 sec")
 - auto and scaled buckets now display the interval for the measurements
 - expanded the parsing abilities of `toMs()`
 - renamed `toMS()` to `toMs()`
 - histogram will now use the field's converter for creating the tooltip.
 - date field format added
This commit is contained in:
Spencer Alger 2014-05-27 14:16:31 -07:00
parent 60f88dae8b
commit dc19e13227
6 changed files with 150 additions and 100 deletions

View file

@ -9,7 +9,7 @@ define(function (require) {
var pickInterval = function (bounds) {
bounds || (bounds = timefilter.getBounds());
return interval.calculate(bounds.min, bounds.max, 100).interval;
return interval.calculate(bounds.min, bounds.max, 100);
};
var agg = this;
@ -67,9 +67,12 @@ define(function (require) {
write: function (selection, output) {
var bounds = timefilter.getBounds();
var auto;
if (selection.val === 'auto') {
output.aggParams.interval = pickInterval(bounds) + 'ms';
auto = pickInterval(bounds);
output.aggParams.interval = auto.interval + 'ms';
output.metricScaleText = auto.description;
return;
}
@ -77,9 +80,10 @@ define(function (require) {
var buckets = Math.ceil((bounds.max - bounds.min) / ms);
if (buckets > 150) {
// we should round these buckets out, and scale back the y values
var msPerBucket = pickInterval(bounds);
output.aggParams.interval = msPerBucket + 'ms';
output.metricScale = ms / msPerBucket;
auto = pickInterval(bounds);
output.aggParams.interval = auto.interval + 'ms';
output.metricScale = ms / auto.interval;
output.metricScaleText = selection.val || auto.description;
} else {
output.aggParams.interval = selection.val;
}

View file

@ -33,7 +33,8 @@ define(function (require) {
chart.ordered = {
date: true,
min: timeBounds.min.valueOf(),
max: timeBounds.max.valueOf()
max: timeBounds.max.valueOf(),
interval: interval.toMs(colX.aggParams.interval)
};
} else {
chart.ordered = {};
@ -56,29 +57,43 @@ define(function (require) {
// X-axis description
chart.xAxisLabel = colX.label;
if (colX.field) chart.xAxisFormatter = colX.field.format.convert;
if (aggX.name === 'date_histogram') {
chart.xAxisFormatter = function (thing) {
chart.xAxisFormatter = (function () {
var bounds = timefilter.getBounds();
return moment(thing).format(interval.calculate(
var format = interval.calculate(
moment(bounds.min.valueOf()),
moment(bounds.max.valueOf()),
rows.length)
.format);
};
}
rows.length
).format;
chart.tooltipFormatter = function (datapoint) {
if (aggX.name === 'date_histogram') {
return moment(datapoint.x).format();
}
return datapoint.x;
};
return function (thing) {
return moment(thing).format(format);
};
}());
}
else if (colX.field) chart.xAxisFormatter = colX.field.format.convert;
// Y-axis description
chart.yAxisLabel = colY.label;
if (colY.field) chart.yAxisFormatter = colY.field.format.convert;
// setup the formatter for the label
chart.tooltipFormatter = function (datapoint) {
var x = datapoint.x;
var y = datapoint.y;
if (colX.field) x = colX.field.format.convert(x);
if (colY.field) y = colY.field.format.convert(y);
if (colX.metricScaleText) {
y += ' per ' + colX.metricScaleText;
}
return x + ': ' + y;
};
var series = chart.series = [];
var seriesByLabel = {};

View file

@ -31,13 +31,14 @@ Currently, the [histogram formatter](https://github.com/elasticsearch/kibana4/bl
define(function (require) {
return function FieldFormattingService() {
var _ = require('lodash');
var moment = require('moment');
var formats = [
{
types: [
'number',
'date',
'boolean',
'date',
'ip',
'attachment',
'geo_point',
@ -53,6 +54,15 @@ define(function (require) {
}
}
},
{
types: [
'date'
],
name: 'date',
convert: function (val) {
return moment(val).format();
}
},
{
types: [
'number'
@ -75,7 +85,7 @@ define(function (require) {
formats.defaultByType = {
number: formats.byName.string,
date: formats.byName.string,
date: formats.byName.date,
boolean: formats.byName.string,
ip: formats.byName.string,
attachment: formats.byName.string,

View file

@ -19,66 +19,87 @@ define(function (require) {
from = datemath.parse(from).valueOf();
to = datemath.parse(to, true).valueOf();
rawInterval = ((to - from) / target);
return round ? roundInterval(rawInterval) : rawInterval;
var rounded = roundInterval(rawInterval);
if (!round) rounded.interval = rawInterval;
return rounded;
};
// interval should be passed in ms
// these are the rounding rules used by roundInterval()
var roundingRules = [
// bound, interval/desc, format
['500ms', '100 ms', 'hh:mm:ss.SSS'],
['5s', 'second', 'HH:mm:ss'],
['7.5s', '5 sec', 'HH:mm:ss'],
['15s', '10 sec', 'HH:mm:ss'],
['45s', '30 sec', 'HH:mm:ss'],
['3m', 'minute', 'HH:mm'],
['9m', '5 min', 'HH:mm'],
['20m', '10 min', 'HH:mm'],
['45m', '30 min', 'YYYY-MM-DD HH:mm'],
['2h', 'hour', 'YYYY-MM-DD HH:mm'],
['6h', '3 hours', 'YYYY-MM-DD HH:mm'],
['24h', '12 hours', 'YYYY-MM-DD HH:mm'],
['1w', '1 day', 'YYYY-MM-DD'],
['3w', '1 week', 'YYYY-MM-DD'],
['1y', '1 month', 'YYYY-MM'],
[null, '1 year', 'YYYY'] // default
];
var boundCache = {};
/**
* Round a millisecond interval to the closest "clean" interval,
*
* @param {ms} interval - interval in milliseconds
* @return {[type]} [description]
*/
var roundInterval = function (interval) {
switch (true) {
case (interval <= toMS('500ms')):
return {interval: toMS('100ms'), format: 'hh:mm:ss.SSS'};
case (interval <= toMS('5s')):
return {interval: toMS('1s'), format: 'HH:mm:ss'};
case (interval <= toMS('7.5s')):
return {interval: toMS('5s'), format: 'HH:mm:ss'};
case (interval <= toMS('15s')):
return {interval: toMS('10s'), format: 'HH:mm:ss'};
case (interval <= toMS('45s')):
return {interval: toMS('30s'), format: 'HH:mm:ss'};
case (interval <= toMS('3m')):
return {interval: toMS('1m'), format: 'HH:mm'};
case (interval <= toMS('9m')):
return {interval: toMS('5m'), format: 'HH:mm'};
case (interval <= toMS('20m')):
return {interval: toMS('10m'), format: 'HH:mm'};
case (interval <= toMS('45m')):
return {interval: toMS('30m'), format: 'YYYY-MM-DD HH:mm'};
case (interval <= toMS('2h')):
return {interval: toMS('1h'), format: 'YYYY-MM-DD HH:mm'};
case (interval <= toMS('6h')):
return {interval: toMS('3h'), format: 'YYYY-MM-DD HH:mm'};
case (interval <= toMS('24h')):
return {interval: toMS('12h'), format: 'YYYY-MM-DD HH:mm'};
case (interval <= toMS('1w')):
return {interval: toMS('1d'), format: 'YYYY-MM-DD'};
case (interval <= toMS('3w')):
return {interval: toMS('1w'), format: 'YYYY-MM-DD'};
case (interval < toMS('1y')):
return {interval: toMS('1M'), format: 'YYYY-MM'};
default:
return {interval: toMS('1y'), format: 'YYYY'};
}
var rule = _.find(roundingRules, function (rule, i, rules) {
var remaining = rules.length - i - 1;
// no bound? then succeed
if (!rule[0]) return true;
var bound = boundCache[rule[0]] || (boundCache[rule[0]] = toMs(rule[0]));
// check that we are below or equal to the bounds
if (remaining > 1 && interval <= bound) return true;
// the last rule before the default shouldn't include the default (which is the bound)
if (remaining === 1 && interval < bound) return true;
});
return {
description: rule[1],
interval: toMs(rule[1]),
format: rule[2]
};
};
var toMS = function (interval) {
var _p = interval.match(/([0-9.]+)([a-zA-Z]+)/);
if (_p.length !== 3) return undefined;
return moment.duration(parseFloat(_p[1]), shorthand[_p[2]]).valueOf();
};
// map of moment's short/long unit ids and elasticsearch's long unit ids
// to their value in milliseconds
var vals = _.transform([
['ms', 'milliseconds', 'millisecond'],
['s', 'seconds', 'second', 'sec'],
['m', 'minutes', 'minute', 'min'],
['h', 'hours', 'hour'],
['d', 'days', 'day'],
['w', 'weeks', 'week'],
['M', 'months', 'month'],
['quarter'],
['y', 'years', 'year']
], function (vals, units) {
var normal = moment.normalizeUnits(units[0]);
var val = moment.duration(1, normal).asMilliseconds();
[].concat(normal, units).forEach(function (unit) {
vals[unit] = val;
});
}, {});
// match any key from the vals object prececed by an optional number
var parseRE = new RegExp('^(\\d+(?:\\.\\d*)?)?\\s*(' + _.keys(vals).join('|') + ')$');
var shorthand = {
ms: 'milliseconds',
s: 'seconds',
m: 'minutes',
h: 'hours',
d: 'days',
w: 'weeks',
M: 'months',
y: 'years',
var toMs = function (expr) {
var match = expr.match(parseRE);
if (match) return parseFloat(match[1] || 1) * vals[match[2]];
};
return {
toMS: toMS,
toMs: toMs,
calculate: calculate
};
});

View file

@ -5,7 +5,8 @@ module.exports = function (config) {
files: {
src: [
'Gruntfile.js',
'<%= src %>/**/*.js',
'<%= src %>/*.js',
'<%= src %>/kibana/**/*.js',
'<%= unitTestDir %>/**/*.js',
'<%= root %>/tasks/**/*.js'
]
@ -16,8 +17,7 @@ module.exports = function (config) {
ignores: [
'node_modules/*',
'dist/*',
'sample/*',
'<%= root %>/src/bower_components/**/*'
'sample/*'
]
}
};

View file

@ -6,16 +6,16 @@ define(function (require) {
describe('interval', function () {
describe('toMS', function () {
describe('toMs', function () {
it('return the number of milliseconds represented by the string', function () {
expect(interval.toMS('1ms')).to.be(1);
expect(interval.toMS('1s')).to.be(1000);
expect(interval.toMS('1m')).to.be(60000);
expect(interval.toMS('1h')).to.be(3600000);
expect(interval.toMS('1d')).to.be(86400000);
expect(interval.toMS('1w')).to.be(604800000);
expect(interval.toMS('1M')).to.be(2592000000); // actually 30d
expect(interval.toMS('1y')).to.be(31536000000); // 1000*60*60*24*365
expect(interval.toMs('1ms')).to.be(1);
expect(interval.toMs('1s')).to.be(1000);
expect(interval.toMs('1m')).to.be(60000);
expect(interval.toMs('1h')).to.be(3600000);
expect(interval.toMs('1d')).to.be(86400000);
expect(interval.toMs('1w')).to.be(604800000);
expect(interval.toMs('1M')).to.be(2592000000); // actually 30d
expect(interval.toMs('1y')).to.be(31536000000); // 1000*60*60*24*365
});
});
@ -34,83 +34,83 @@ define(function (require) {
it('should calculate an appropriate interval for 10s', function () {
var _t = then.subtract(10, 'seconds');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('100ms'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('100ms'));
});
it('should calculate an appropriate interval for 1m', function () {
var _t = then.subtract(1, 'minute');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('1s'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('1s'));
});
it('should calculate an appropriate interval for 10m', function () {
var _t = then.subtract(10, 'minutes');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('5s'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('5s'));
});
it('should calculate an appropriate interval for 15m', function () {
var _t = then.subtract(15, 'minutes');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('10s'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('10s'));
});
it('should calculate an appropriate interval for 1h', function () {
var _t = then.subtract(1, 'hour');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('30s'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('30s'));
});
it('should calculate an appropriate interval for 90m', function () {
var _t = then.subtract(90, 'minutes');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('1m'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('1m'));
});
it('should calculate an appropriate interval for 6h', function () {
var _t = then.subtract(6, 'hours');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('5m'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('5m'));
});
it('should calculate an appropriate interval for 24h', function () {
var _t = then.subtract(24, 'hours');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('10m'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('10m'));
});
it('should calculate an appropriate interval for 3d', function () {
var _t = then.subtract(3, 'days');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('30m'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('30m'));
});
it('should calculate an appropriate interval for 1w', function () {
var _t = then.subtract(1, 'week');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('1h'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('1h'));
});
it('should calculate an appropriate interval for 2w', function () {
var _t = then.subtract(2, 'weeks');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('3h'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('3h'));
});
it('should calculate an appropriate interval for 1M', function () {
var _t = then.subtract(1, 'month');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('12h'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('12h'));
});
it('should calculate an appropriate interval for 4M', function () {
var _t = then.subtract(4, 'months');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('1d'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('1d'));
});
it('should calculate an appropriate interval for 2y', function () {
var _t = then.subtract(2, 'years');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('1w'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('1w'));
});
it('should calculate an appropriate interval for 25y', function () {
var _t = then.subtract(25, 'years');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('1M'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('1M'));
});
it('should calculate an appropriate interval for a 100y', function () {
var _t = then.subtract(100, 'years');
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMS('1y'));
expect(interval.calculate(_t, now, 100).interval).to.be(interval.toMs('1y'));
});
});