mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
d8adb395b1
commit
1838d76af6
5 changed files with 137 additions and 20 deletions
|
@ -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) =>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue