diff --git a/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/date_histogram.js b/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/date_histogram.js index a6b30f119908..81dfa928f7db 100644 --- a/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/date_histogram.js +++ b/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/date_histogram.js @@ -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; } diff --git a/src/kibana/apps/visualize/saved_visualizations/resp_converters/histogram.js b/src/kibana/apps/visualize/saved_visualizations/resp_converters/histogram.js index 00664e374310..2f46e0c77144 100644 --- a/src/kibana/apps/visualize/saved_visualizations/resp_converters/histogram.js +++ b/src/kibana/apps/visualize/saved_visualizations/resp_converters/histogram.js @@ -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 = {}; diff --git a/src/kibana/components/index_patterns/_field_formats.js b/src/kibana/components/index_patterns/_field_formats.js index 50b068466256..ebca655e05a9 100644 --- a/src/kibana/components/index_patterns/_field_formats.js +++ b/src/kibana/components/index_patterns/_field_formats.js @@ -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, diff --git a/src/kibana/utils/interval.js b/src/kibana/utils/interval.js index 3bad207654b7..51bfe6f3a718 100644 --- a/src/kibana/utils/interval.js +++ b/src/kibana/utils/interval.js @@ -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 }; }); \ No newline at end of file diff --git a/tasks/config/jshint.js b/tasks/config/jshint.js index 2a2bf9e68574..9d7b785984dd 100644 --- a/tasks/config/jshint.js +++ b/tasks/config/jshint.js @@ -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/*' ] } }; diff --git a/test/unit/specs/utils/interval.js b/test/unit/specs/utils/interval.js index 279bb53102d2..f48733d81731 100644 --- a/test/unit/specs/utils/interval.js +++ b/test/unit/specs/utils/interval.js @@ -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')); }); });