diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index daba164a4a00ce..c67bb491910b77 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -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 ( d.x0); + const xMax = d3.max(buckets, d => d.x); + const tickFormat = scaleUtc() + .domain([xMin, xMax]) + .tickFormat(); + return (
@@ -79,7 +88,11 @@ export function ErrorDistribution({ distribution, title }: Props) { 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) => diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx index 10eb4659ea6951..64350a57416473 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx @@ -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, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js index 50c94fe88e6ad2..d8f7815d25be6f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -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} /> { + nodes={buckets.map(bucket => { return { ...bucket, xCenter: (bucket.x0 + bucket.x) / 2 diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts b/x-pack/legacy/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts new file mode 100644 index 00000000000000..ab4c599dfcf756 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts @@ -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() + ]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/helper/timezone.ts b/x-pack/legacy/plugins/apm/public/components/shared/charts/helper/timezone.ts new file mode 100644 index 00000000000000..1a6be1a76ea63d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/helper/timezone.ts @@ -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]; +};