Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom date histogram interval option #3237

Merged
merged 2 commits into from
Apr 1, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/kibana/components/agg_types/buckets/_interval_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,35 @@ define(function (require) {
},
{
display: 'Second',
val: 'second'
val: 's'
},
{
display: 'Minute',
val: 'minute'
val: 'm'
},
{
display: 'Hourly',
val: 'hour'
val: 'h'
},
{
display: 'Daily',
val: 'day'
val: 'd'
},
{
display: 'Weekly',
val: 'week'
val: 'w'
},
{
display: 'Monthly',
val: 'month'
val: 'M'
},
{
display: 'Yearly',
val: 'year'
val: 'y'
},
{
display: 'Custom',
val: 'custom'
}
];
};
Expand Down
16 changes: 14 additions & 2 deletions src/kibana/components/agg_types/buckets/date_histogram.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ define(function (require) {

var tzOffset = moment().format('Z');

function getInterval(agg) {
var interval = _.get(agg, ['params', 'interval']);
if (interval && interval.val === 'custom') interval = _.get(agg, ['params', 'customInterval']);
return interval;
}

function setBounds(agg, force) {
if (agg.buckets._alreadySet && !force) return;
agg.buckets._alreadySet = true;
Expand Down Expand Up @@ -37,7 +43,7 @@ define(function (require) {
if (buckets) return buckets;

buckets = new TimeBuckets();
buckets.setInterval(_.get(this, ['params', 'interval']));
buckets.setInterval(getInterval(this));
setBounds(this);

return buckets;
Expand Down Expand Up @@ -68,7 +74,7 @@ define(function (require) {
},
write: function (agg, output) {
setBounds(agg);
agg.buckets.setInterval(agg.params.interval);
agg.buckets.setInterval(getInterval(agg));

var interval = agg.buckets.getInterval();
output.bucketInterval = interval;
Expand All @@ -90,6 +96,12 @@ define(function (require) {
}
},

{
name: 'customInterval',
default: '2h',
write: _.noop
},

{
name: 'format'
},
Expand Down
7 changes: 7 additions & 0 deletions src/kibana/components/agg_types/controls/interval.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
name="interval">
<option value="">-- select a valid interval --</option>
</select>
<input
type="text"
ng-model="agg.params.customInterval"
ng-change="agg.write()"
ng-if="agg.params.interval.val == 'custom'"
class="form-control"
required />
<input
ng-if="!aggParam.options"
ng-model="agg.params.interval"
Expand Down
6 changes: 3 additions & 3 deletions src/kibana/components/time_buckets/time_buckets.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ define(function (require) {
var moment = require('moment');

var datemath = require('utils/datemath');
var parseInterval = require('utils/parse_interval');
var calcAuto = Private(require('components/time_buckets/calc_auto_interval'));
var calcEsInterval = Private(require('components/time_buckets/calc_es_interval'));
var tzOffset = moment().format('Z');
Expand Down Expand Up @@ -145,17 +146,16 @@ define(function (require) {

if (_.isString(interval)) {
input = interval;
interval = moment.duration(1, interval);
interval = parseInterval(interval);
if (+interval === 0) {
interval = null;
input += ' (not a valid moment unit)';
}
}

// if the value wasn't converted to a duration, and isn't
// already a duration, we have a problem
if (!moment.isDuration(interval)) {
throw new TypeError('can\'t convert input ' + input + ' to a moment.duration');
throw new TypeError('"' + input + '" is not a valid interval.');
}

this._i = interval;
Expand Down
33 changes: 33 additions & 0 deletions src/kibana/utils/parse_interval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
define(function (require) {
var _ = require('lodash');
var moment = require('moment');
var datemath = require('utils/datemath');

return function parseInterval(interval) {
// Assume interval is in the form (value)(unit), such as "1h"
var regex = new RegExp('^([0-9\\.]*)\\s*(' + datemath.units.join('|') + ')$');
var matches = regex.exec(interval), value, unit;
if (matches && matches.length) {
value = parseFloat(matches[1]) || 1;
unit = matches[2];
}

try {
interval = moment.duration(value, unit);

// There is an error with moment, where if you have a fractional interval between 0 and 1, then when you add that
// interval to an existing moment object, it will remain unchanged, which causes problems in the ordered_x_keys
// code. To counteract this, we find the first unit that doesn't result in a value between 0 and 1.
// For example, if you have '0.5d', then when calculating the x-axis series, we take the start date and begin
// adding 0.5 days until we hit the end date. However, since there is a bug in moment, when you add 0.5 days to
// the start date, you get the same exact date (instead of being ahead by 12 hours). So instead of returning
// a duration corresponding to 0.5 hours, we return a duration corresponding to 12 hours.
var selectedUnit = _.find(datemath.units, function (unit) {
return Math.abs(interval.as(unit)) >= 1;
});
return moment.duration(interval.as(selectedUnit), selectedUnit);
} catch (e) {
return null;
}
};
});
16 changes: 8 additions & 8 deletions test/unit/specs/components/agg_types/buckets/_date_histogram.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ define(function (require) {

describe('interval', function () {
it('accepts a valid interval', function () {
var output = writeInterval('day');
var output = writeInterval('d');
expect(output.params).to.have.property('interval', '1d');
});

Expand All @@ -49,22 +49,22 @@ define(function (require) {
});

it('automatically picks an interval', function () {
setTimeBounds(15, 'minutes');
setTimeBounds(15, 'm');
var output = writeInterval('auto');
expect(output.params.interval).to.be('30s');
});

it('scales up the interval if it will make too many buckets', function () {
setTimeBounds(30, 'minutes');
var output = writeInterval('second');
setTimeBounds(30, 'm');
var output = writeInterval('s');
expect(output.params.interval).to.be('10s');
expect(output.metricScaleText).to.be('second');
expect(output.metricScale).to.be(0.1);
});

it('does not scale down the interval', function () {
setTimeBounds(1, 'minutes');
var output = writeInterval('hour');
setTimeBounds(1, 'm');
var output = writeInterval('h');
expect(output.params.interval).to.be('1h');
expect(output.metricScaleText).to.be(undefined);
expect(output.metricScale).to.be(undefined);
Expand All @@ -82,15 +82,15 @@ define(function (require) {
var typeNames = test.slice();

it(typeNames.join(', ') + ' should ' + (should ? '' : 'not') + ' scale', function () {
setTimeBounds(1, 'year');
setTimeBounds(1, 'y');

var vis = paramWriter.vis;
vis.aggs.splice(0);

var histoConfig = new AggConfig(vis, {
type: aggTypes.byName.date_histogram,
schema: 'segment',
params: { interval: 'second', field: timeField }
params: { interval: 's', field: timeField }
});

vis.aggs.push(histoConfig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ define(function (require) {

init = function (interval, duration) {
interval = interval || 'auto';
if (interval === 'custom') interval = agg.params.customInterval;
duration = duration || moment.duration(15, 'minutes');
field = _.sample(indexPattern.fields.byType.date);
vis = new Vis(indexPattern, {
Expand All @@ -35,7 +36,7 @@ define(function (require) {
{
type: 'date_histogram',
schema: 'segment',
params: { field: field.name, interval: interval }
params: { field: field.name, interval: interval, customInterval: '5d' }
}
]
});
Expand Down Expand Up @@ -78,7 +79,7 @@ define(function (require) {
it('extends the filter edge to 1ms before the next bucket for all interval options', function () {
intervalOptions.forEach(function (option) {
var duration;
if (moment(1, option.val).isValid()) {
if (option.val !== 'custom' && moment(1, option.val).isValid()) {
duration = moment.duration(10, option.val);

if (+duration < 10) {
Expand Down
72 changes: 72 additions & 0 deletions test/unit/specs/utils/parse_interval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
define(function (require) {
var parseInterval = require('utils/parse_interval');

describe('parseInterval', function () {
it('should correctly parse an interval containing unit and value', function () {
var duration = parseInterval('1d');
expect(duration.as('d')).to.be(1);

duration = parseInterval('2y');
expect(duration.as('y')).to.be(2);

duration = parseInterval('5M');
expect(duration.as('M')).to.be(5);

duration = parseInterval('5m');
expect(duration.as('m')).to.be(5);

duration = parseInterval('100s');
expect(duration.as('s')).to.be(100);

duration = parseInterval('23d');
expect(duration.as('d')).to.be(23);

duration = parseInterval('52w');
expect(duration.as('w')).to.be(52);
});

it('should correctly parse fractional intervals containing unit and value', function () {
var duration = parseInterval('1.5w');
expect(duration.as('w')).to.be(1.5);

duration = parseInterval('2.35y');
expect(duration.as('y')).to.be(2.35);
});

it('should correctly bubble up intervals which are less than 1', function () {
var duration = parseInterval('0.5y');
expect(duration.as('d')).to.be(183);

duration = parseInterval('0.5d');
expect(duration.as('h')).to.be(12);
});

it('should correctly parse a unit in an interval only', function () {
var duration = parseInterval('d');
expect(duration.as('d')).to.be(1);

duration = parseInterval('m');
expect(duration.as('m')).to.be(1);

duration = parseInterval('y');
expect(duration.as('y')).to.be(1);

duration = parseInterval('M');
expect(duration.as('M')).to.be(1);
});

it('should return null for an invalid interval', function () {
var duration = parseInterval('');
expect(duration).to.not.be.ok();

duration = parseInterval(null);
expect(duration).to.not.be.ok();

duration = parseInterval('234asdf');
expect(duration).to.not.be.ok();

duration = parseInterval('ms');
expect(duration).to.not.be.ok();
});
});
});