Skip to content

Commit

Permalink
[Discover] fix histogram min interval
Browse files Browse the repository at this point in the history
This commit fix the missing bars on the histogram of Discover.
Discover is passing a minInterval parameter in milliseconds that is not
the the minimum interval in milliseconds between the buckets. Calendar intervals
are not fixed and this value change depending on time range and the time zone.
To avoid patching the elastic-chart and upgrading the dependency we applied a temporary patch
to the histogram code. For the feature version this patch will be removed
and a correct minInterval computation will be done directly in elastic-charts.

fix #52152
  • Loading branch information
markov00 committed Dec 11, 2019
1 parent e162295 commit e614953
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { findMinInterval } from './histogram';
import moment from 'moment-timezone';

const ONE_HOUR = 60 * 60 * 1000;
const ONE_DAY = 24 * ONE_HOUR;
describe('DiscoverHistogram', () => {
it('shall find the min interval with a single unit, single value', () => {
const oneMs = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([oneMs], 1, 'ms', 'UTC')).toBe(1);

const oneSecond = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([oneSecond], 1, 's', 'UTC')).toBe(1000);

// all are calendar intervals
const oneMinute = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([oneMinute], 1, 'm', 'UTC')).toBe(1000 * 60);

const oneHour = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([oneHour], 1, 'h', 'UTC')).toBe(ONE_HOUR);

const oneDay = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([oneDay], 1, 'd', 'UTC')).toBe(ONE_DAY);

const oneWeek = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([oneWeek], 1, 'w', 'UTC')).toBe(7 * ONE_DAY);

const oneMonth = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([oneMonth], 1, 'M', 'UTC')).toBe(29 * ONE_DAY);

const oneYear = moment('2015-01-01T00:00:00.000Z').valueOf();
expect(findMinInterval([oneYear], 1, 'y', 'UTC')).toBe(365 * ONE_DAY);

const oneLeapYear = moment('2016-01-01T00:00:00.000Z').valueOf();
expect(findMinInterval([oneLeapYear], 1, 'y', 'UTC')).toBe(366 * ONE_DAY);
});

it('shall find the min interval with a single unit, single value DST', () => {
// if configured timeZone is UTC, there is no DST
const utcDay = moment('2019-03-31T00:00:00.000Z').valueOf();
expect(findMinInterval([utcDay], 1, 'd', 'UTC')).toBe(ONE_DAY);

const dstDay = moment.tz('2019-03-31 00:00:00.000', 'Europe/Rome').valueOf();
expect(findMinInterval([dstDay], 1, 'd', 'Europe/Rome')).toBe(ONE_DAY - ONE_HOUR);

const dstAddDay = moment.tz('2019-10-27 00:00:00.000', 'Europe/Rome').valueOf();
// the 27th has 25 hours
expect(findMinInterval([dstAddDay], 1, 'd', 'Europe/Rome')).toBe(ONE_DAY + ONE_HOUR);
});

it('shall find the min interval with a single unit, multi value DST', () => {
// if configured timeZone is UTC, there is no DST
const utcStartDay = moment('2019-03-31T00:00:00.000Z').valueOf();
const utcEndDay = moment('2019-04-01T00:00:00.000Z').valueOf();
expect(findMinInterval([utcStartDay, utcEndDay], 1, 'd', 'UTC')).toBe(ONE_DAY);

const dstStartDay = moment.tz('2019-03-31 00:00:00.000', 'Europe/Rome').valueOf();
const dstEndDay = moment.tz('2019-04-01 00:00:00.000', 'Europe/Rome').valueOf();
expect(findMinInterval([dstStartDay, dstEndDay], 1, 'd', 'Europe/Rome')).toBe(
ONE_DAY - ONE_HOUR
);

const dstAddStartDay = moment.tz('2019-10-27 00:00:00.000', 'Europe/Rome').valueOf();
const dstAddEndDay = moment.tz('2019-10-28 00:00:00.000', 'Europe/Rome').valueOf();
// the 27th has 25 hours and the 28 has 24 hours, so the min is 24
expect(findMinInterval([dstAddStartDay, dstAddEndDay], 1, 'd', 'Europe/Rome')).toBe(ONE_DAY);
});

it('shall find the min interval with a multi unit, single value', () => {
// all are fixed intervals
const fiveMs = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([fiveMs], 5, 'ms', 'UTC')).toBe(5);

const fiveSecond = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([fiveSecond], 5, 's', 'UTC')).toBe(1000 * 5);

const fiveMinute = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([fiveMinute], 5, 'm', 'UTC')).toBe(1000 * 60 * 5);

const fiveHour = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([fiveHour], 5, 'h', 'UTC')).toBe(ONE_HOUR * 5);

const fiveDay = moment('2016-02-01T00:00:00.000Z').valueOf();
expect(findMinInterval([fiveDay], 5, 'd', 'UTC')).toBe(ONE_DAY * 5);

// multiple week, month and years are not allowed see parse_es_interval.ts
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui';
import moment from 'moment-timezone';
import { unitOfTime } from 'moment';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json';
Expand Down Expand Up @@ -60,6 +61,48 @@ interface DiscoverHistogramState {
chartsTheme: EuiChartThemeType['theme'];
}

function findIntervalFromDuration(dateValue: number, esUnit: unitOfTime.Base, timeZone: string) {
const date = moment.tz(dateValue, timeZone);
const startOfDate = moment.tz(date, timeZone).startOf(esUnit);
const endOfDate = moment
.tz(date, timeZone)
.startOf(esUnit)
.add(1, esUnit);
return endOfDate.valueOf() - startOfDate.valueOf();
}

function getSingleUnitInterval(value: number, esUnit: unitOfTime.Base, timeZone: string) {
switch (esUnit) {
case 's':
return 1000;
case 'ms':
return 1;
default:
return findIntervalFromDuration(value, esUnit, timeZone);
}
}

export function findMinInterval(
xValues: number[],
esValue: number,
esUnit: string,
timeZone: string
): number {
// if the interval is a single unit we need to find the minimum fixed interval in ms
// between the available dates
if (esValue === 1) {
return xValues.reduce((minInterval, currentXvalue) => {
return Math.min(
minInterval,
getSingleUnitInterval(currentXvalue, esUnit as unitOfTime.Base, timeZone)
);
}, Number.MAX_SAFE_INTEGER);
} else {
// if it's a multiple unit interval, than it's a fixed interval directly convertible to ms
return +moment.duration(esValue, esUnit as unitOfTime.Base);
}
}

export class DiscoverHistogram extends Component<DiscoverHistogramProps, DiscoverHistogramState> {
public static propTypes = {
chartData: PropTypes.object,
Expand Down Expand Up @@ -161,7 +204,7 @@ export class DiscoverHistogram extends Component<DiscoverHistogramProps, Discove
* see https://github.com/elastic/kibana/issues/27410
* TODO: Once the Discover query has been update, we should change the below to use the new field
*/
const xInterval = chartData.ordered.interval;
const { intervalESValue, intervalESUnit, interval: xInterval } = chartData.ordered;

const xValues = chartData.xAxisOrderedValues;
const lastXValue = xValues[xValues.length - 1];
Expand All @@ -176,7 +219,7 @@ export class DiscoverHistogram extends Component<DiscoverHistogramProps, Discove
const xDomain = {
min: domainMin,
max: domainMax,
minInterval: xInterval,
minInterval: findMinInterval(xValues, intervalESValue, intervalESUnit, timeZone),
};

// Domain end of 'now' will be milliseconds behind current time, so we extend time by 1 minute and check if
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ describe('initXAxis', function () {

it('reads the date interval param from the x agg', function () {
chart.aspects.x[0].params.interval = 'P1D';
chart.aspects.x[0].params.intervalESValue = 1;
chart.aspects.x[0].params.intervalESUnit = 'd';
chart.aspects.x[0].params.date = true;
initXAxis(chart, table);
expect(chart)
Expand All @@ -117,6 +119,8 @@ describe('initXAxis', function () {

expect(moment.isDuration(chart.ordered.interval)).to.be(true);
expect(chart.ordered.interval.toISOString()).to.eql('P1D');
expect(chart.ordered.intervalESValue).to.be(1);
expect(chart.ordered.intervalESUnit).to.be('d');
});

it('reads the numeric interval param from the x agg', function () {
Expand Down
18 changes: 14 additions & 4 deletions src/legacy/ui/public/agg_response/point_series/_init_x_axis.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,19 @@ export function initXAxis(chart, table) {
chart.xAxisFormat = format;
chart.xAxisLabel = title;

if (params.interval) {
chart.ordered = {
interval: params.date ? moment.duration(params.interval) : params.interval,
};
const { interval, date } = params;
if (interval) {
if(date) {
const { intervalESUnit, intervalESValue } = params;
chart.ordered = {
interval: moment.duration(interval),
intervalESUnit: intervalESUnit,
intervalESValue: intervalESValue,
};
} else {
chart.ordered = {
interval,
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,8 @@ export const buildVislibDimensions = async (
dimensions.x.params.date = true;
const { esUnit, esValue } = xAgg.buckets.getInterval();
dimensions.x.params.interval = moment.duration(esValue, esUnit);
dimensions.x.params.intervalESValue = esValue;
dimensions.x.params.intervalESUnit = esUnit;
dimensions.x.params.format = xAgg.buckets.getScaledDateFormat();
dimensions.x.params.bounds = xAgg.buckets.getBounds();
} else if (xAgg.type.name === 'histogram') {
Expand Down

0 comments on commit e614953

Please sign in to comment.