[APM] x-axis labels on Error occurrences chart are incorrect based on Kibana timezone (#55686)

* adjusting x-axis to use kibana timezone

* adjusting x-axis to use kibana timezone

* refactoring
This commit is contained in:
Cauê Marcondes 2020-01-29 13:29:35 +00:00 committed by GitHub
parent d8adb395b1
commit 1838d76af6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 20 deletions

View file

@ -6,11 +6,14 @@
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { scaleUtc } from 'd3-scale';
import d3 from 'd3';
import React from 'react';
import { asRelativeDateTimeRange } from '../../../../utils/formatters';
import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs';
// @ts-ignore
import Histogram from '../../../shared/charts/Histogram';
import { EmptyMessage } from '../../../shared/EmptyMessage';
import { asRelativeDateTimeRange } from '../../../../utils/formatters';
interface IBucket {
key: number;
@ -61,7 +64,7 @@ export function ErrorDistribution({ distribution, title }: Props) {
distribution.bucketSize
);
if (distribution.noHits) {
if (!buckets || distribution.noHits) {
return (
<EmptyMessage
heading={i18n.translate('xpack.apm.errorGroupDetails.noErrorsLabel', {
@ -71,6 +74,12 @@ export function ErrorDistribution({ distribution, title }: Props) {
);
}
const xMin = d3.min(buckets, d => d.x0);
const xMax = d3.max(buckets, d => d.x);
const tickFormat = scaleUtc()
.domain([xMin, xMax])
.tickFormat();
return (
<div>
<EuiTitle size="xs">
@ -79,7 +88,11 @@ export function ErrorDistribution({ distribution, title }: Props) {
<Histogram
tooltipHeader={tooltipHeader}
verticalLineHover={(bucket: FormattedBucket) => bucket.x}
xType="time"
xType="time-utc"
formatX={(value: Date) => {
const time = value.getTime();
return tickFormat(new Date(time - getTimezoneOffsetInMs(time)));
}}
buckets={buckets}
bucketSize={distribution.bucketSize}
formatYShort={(value: number) =>

View file

@ -13,7 +13,7 @@ import React from 'react';
import { TimeSeries, Coordinate } from '../../../../../typings/timeseries';
import { unit } from '../../../../style/variables';
import { getTimezoneOffsetInMs } from './getTimezoneOffsetInMs';
import { getDomainTZ, getTimeTicksTZ } from '../helper/timezone';
const XY_HEIGHT = unit * 16;
const XY_MARGIN = {
@ -73,7 +73,6 @@ export function getPlotValues(
);
const xMin = d3.min(flattenedCoordinates, d => d.x);
const xMax = d3.max(flattenedCoordinates, d => d.x);
if (yMax === 'max') {
@ -83,9 +82,7 @@ export function getPlotValues(
yMin = d3.min(flattenedCoordinates, d => d.y ?? 0);
}
const [xMinZone, xMaxZone] = [xMin, xMax].map(x => {
return x - getTimezoneOffsetInMs(x);
});
const [xMinZone, xMaxZone] = getDomainTZ(xMin, xMax);
const xScale = getXScale(xMin, xMax, width);
const yScale = getYScale(yMin, yMax);
@ -97,15 +94,11 @@ export function getPlotValues(
// d3 will determine the exact number of ticks based on the selected range
const xTickTotal = Math.floor(width / 100);
const xTickValues = d3.time.scale
.utc()
.domain([xMinZone, xMaxZone])
.range([0, width])
.ticks(xTickTotal)
.map(x => {
const time = x.getTime();
return new Date(time + getTimezoneOffsetInMs(time));
});
const xTickValues = getTimeTicksTZ({
domain: [xMinZone, xMaxZone],
totalTicks: xTickTotal,
width
});
return {
x: xScale,

View file

@ -25,6 +25,7 @@ import { unit } from '../../../../style/variables';
import Tooltip from '../Tooltip';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { tint } from 'polished';
import { getTimeTicksTZ, getDomainTZ } from '../helper/timezone';
const XY_HEIGHT = unit * 10;
const XY_MARGIN = {
@ -104,6 +105,9 @@ export class HistogramInner extends PureComponent {
return null;
}
const isTimeSeries =
this.props.xType === 'time' || this.props.xType === 'time-utc';
const xMin = d3.min(buckets, d => d.x0);
const xMax = d3.max(buckets, d => d.x);
const yMin = 0;
@ -120,11 +124,18 @@ export class HistogramInner extends PureComponent {
.range([XY_HEIGHT, 0])
.nice();
const [xMinZone, xMaxZone] = getDomainTZ(xMin, xMax);
const xTickValues = isTimeSeries
? getTimeTicksTZ({
domain: [xMinZone, xMaxZone],
totalTicks: X_TICK_TOTAL,
width: XY_WIDTH
})
: undefined;
const xDomain = x.domain();
const yDomain = y.domain();
const yTickValues = [0, yDomain[1] / 2, yDomain[1]];
const isTimeSeries =
this.props.xType === 'time' || this.props.xType === 'time-utc';
const shouldShowTooltip =
hoveredBucket.x > 0 && (hoveredBucket.y > 0 || isTimeSeries);
@ -150,6 +161,7 @@ export class HistogramInner extends PureComponent {
tickSizeInner={0}
tickTotal={X_TICK_TOTAL}
tickFormat={formatX}
tickValues={xTickValues}
/>
<YAxis
tickSize={0}
@ -213,7 +225,7 @@ export class HistogramInner extends PureComponent {
[XY_MARGIN.left, XY_MARGIN.top],
[XY_WIDTH, XY_HEIGHT]
]}
nodes={this.props.buckets.map(bucket => {
nodes={buckets.map(bucket => {
return {
...bucket,
xCenter: (bucket.x0 + bucket.x) / 2

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment-timezone';
import { getDomainTZ, getTimeTicksTZ } from '../timezone';
describe('Timezone helper', () => {
let originalTimezone: moment.MomentZone | null;
const min = new Date('Tue Jan 28 2020 05:36:00 GMT+0100').valueOf();
const max = new Date('Wed Jan 29 2020 07:12:00 GMT+0100').valueOf();
afterAll(() => {
moment.tz.setDefault(originalTimezone ? originalTimezone.name : '');
});
describe('getTimeTicksTZ', () => {
it('returns ticks when in Ameca/New_York timezone', () => {
moment.tz.setDefault('America/New_York');
expect(
getTimeTicksTZ({ domain: [min, max], totalTicks: 8, width: 1138 })
).toEqual([
new Date('2020-01-28T11:00:00.000Z'),
new Date('2020-01-28T14:00:00.000Z'),
new Date('2020-01-28T17:00:00.000Z'),
new Date('2020-01-28T20:00:00.000Z'),
new Date('2020-01-28T23:00:00.000Z'),
new Date('2020-01-29T02:00:00.000Z'),
new Date('2020-01-29T05:00:00.000Z'),
new Date('2020-01-29T08:00:00.000Z'),
new Date('2020-01-29T11:00:00.000Z')
]);
});
it('returns ticks when in Europe/Amsterdam timezone', () => {
moment.tz.setDefault('Europe/Amsterdam');
expect(
getTimeTicksTZ({ domain: [min, max], totalTicks: 8, width: 1138 })
).toEqual([
new Date('2020-01-28T05:00:00.000Z'),
new Date('2020-01-28T08:00:00.000Z'),
new Date('2020-01-28T11:00:00.000Z'),
new Date('2020-01-28T14:00:00.000Z'),
new Date('2020-01-28T17:00:00.000Z'),
new Date('2020-01-28T20:00:00.000Z'),
new Date('2020-01-28T23:00:00.000Z'),
new Date('2020-01-29T02:00:00.000Z'),
new Date('2020-01-29T05:00:00.000Z')
]);
});
});
describe('getDomainTZ', () => {
it('returns domain when in Ameca/New_York timezone', () => {
moment.tz.setDefault('America/New_York');
expect(getDomainTZ(min, max)).toEqual([
new Date('Tue Jan 28 2020 00:36:00 GMT+0100').valueOf(),
new Date('Wed Jan 29 2020 02:12:00 GMT+0100').valueOf()
]);
});
it('returns domain when in Europe/Amsterdam timezone', () => {
moment.tz.setDefault('Europe/Amsterdam');
expect(getDomainTZ(min, max)).toEqual([
new Date('Tue Jan 28 2020 06:36:00 GMT+0100').valueOf(),
new Date('Wed Jan 29 2020 08:12:00 GMT+0100').valueOf()
]);
});
});
});

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import d3 from 'd3';
import { getTimezoneOffsetInMs } from '../CustomPlot/getTimezoneOffsetInMs';
interface Params {
domain: [number, number];
totalTicks: number;
width: number;
}
export const getTimeTicksTZ = ({ domain, totalTicks, width }: Params) =>
d3.time.scale
.utc()
.domain(domain)
.range([0, width])
.ticks(totalTicks)
.map(x => {
const time = x.getTime();
return new Date(time + getTimezoneOffsetInMs(time));
});
export const getDomainTZ = (min: number, max: number): [number, number] => {
const [xMinZone, xMaxZone] = [min, max].map(
time => time - getTimezoneOffsetInMs(time)
);
return [xMinZone, xMaxZone];
};