Skip to content

Commit 40e3419

Browse files
committed
[BUGFIX] fix tooltip of timeseries chart when timezone changed
1 parent c0650a9 commit 40e3419

3 files changed

Lines changed: 138 additions & 5 deletions

File tree

timeserieschart/src/TimeSeriesChartBase.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
DEFAULT_TOOLTIP_CONFIG,
4848
EChart,
4949
enableDataZoom,
50+
formatWithTimeZone,
5051
getClosestTimestamp,
5152
getFormattedAxis,
5253
getFormattedAxisLabel,
@@ -60,6 +61,7 @@ import {
6061
useTimeZone,
6162
ZoomEventData,
6263
} from '@perses-dev/components';
64+
import { createTimezoneAwareAxisFormatter } from './utils/timezone-formatter';
6365
import { DatasetOption } from 'echarts/types/dist/shared';
6466

6567
use([
@@ -128,6 +130,10 @@ export const TimeSeriesChartBase = forwardRef<ChartInstance, TimeChartProps>(fun
128130
const [isDragging, setIsDragging] = useState(false);
129131
const [startX, setStartX] = useState(0);
130132
const { timeZone } = useTimeZone();
133+
134+
const getTimezoneAwareAxisFormatter = (rangeMs: number) =>
135+
createTimezoneAwareAxisFormatter(rangeMs, timeZone);
136+
131137
let timeScale: TimeScale;
132138
if (timeScaleProp === undefined) {
133139
const commonTimeScale = getCommonTimeScale(data);
@@ -204,11 +210,10 @@ export const TimeSeriesChartBase = forwardRef<ChartInstance, TimeChartProps>(fun
204210
// Utilizes ECharts dataset so raw data is separate from series option style properties
205211
// https://apache.github.io/echarts-handbook/en/concepts/dataset/
206212
const dataset: DatasetOption[] = [];
207-
const isLocalTimeZone = timeZone === 'local';
208213
data.map((d, index) => {
209214
const values = d.values.map(([timestamp, value]) => {
210215
const val: string | number = value === null ? '-' : value; // echarts use '-' to represent null data
211-
return [isLocalTimeZone ? timestamp : toZonedTime(timestamp, timeZone), val];
216+
return [timestamp, val];
212217
});
213218
dataset.push({ id: index, source: [...values], dimensions: ['time', 'value'] });
214219
});
@@ -221,11 +226,11 @@ export const TimeSeriesChartBase = forwardRef<ChartInstance, TimeChartProps>(fun
221226
series: updatedSeriesMapping,
222227
xAxis: {
223228
type: 'time',
224-
min: isLocalTimeZone ? timeScale.startMs : toZonedTime(timeScale.startMs, timeZone),
225-
max: isLocalTimeZone ? timeScale.endMs : toZonedTime(timeScale.endMs, timeZone),
229+
min: timeScale.startMs,
230+
max: timeScale.endMs,
226231
axisLabel: {
227232
hideOverlap: true,
228-
formatter: getFormattedAxisLabel(timeScale.rangeMs ?? 0),
233+
formatter: getTimezoneAwareAxisFormatter(timeScale.rangeMs ?? 0),
229234
},
230235
axisPointer: {
231236
snap: false, // important so shared crosshair does not lag
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
import { createTimezoneAwareAxisFormatter } from './timezone-formatter';
15+
16+
// Mock formatWithTimeZone since it's from @perses-dev/components
17+
jest.mock('@perses-dev/components', () => ({
18+
formatWithTimeZone: jest.fn((date: Date, format: string, timeZone: string) => {
19+
// Simple mock that returns format pattern with timezone
20+
return `${format}[${timeZone}]`;
21+
}),
22+
}));
23+
24+
describe('createTimezoneAwareAxisFormatter', () => {
25+
const testTimestamp = 1640995200000; // 2022-01-01 00:00:00 UTC
26+
const timeZone = 'America/New_York';
27+
28+
it('should format for ranges > 5 years with year format', () => {
29+
const formatter = createTimezoneAwareAxisFormatter(6 * 365 * 24 * 60 * 60 * 1000, timeZone);
30+
const result = formatter(testTimestamp);
31+
expect(result).toBe('yyyy[America/New_York]');
32+
});
33+
34+
it('should format for ranges > 2 years with month-year format', () => {
35+
const formatter = createTimezoneAwareAxisFormatter(3 * 365 * 24 * 60 * 60 * 1000, timeZone);
36+
const result = formatter(testTimestamp);
37+
expect(result).toBe('MMM yyyy[America/New_York]');
38+
});
39+
40+
it('should format for ranges between 10 days and 6 months with day-month format', () => {
41+
const formatter = createTimezoneAwareAxisFormatter(30 * 24 * 60 * 60 * 1000, timeZone); // 30 days
42+
const result = formatter(testTimestamp);
43+
expect(result).toBe('dd.MM[America/New_York]');
44+
});
45+
46+
it('should format for ranges between 2-10 days with day-month-time format', () => {
47+
const formatter = createTimezoneAwareAxisFormatter(5 * 24 * 60 * 60 * 1000, timeZone); // 5 days
48+
const result = formatter(testTimestamp);
49+
expect(result).toBe('dd.MM HH:mm[America/New_York]');
50+
});
51+
52+
it('should format for ranges <= 2 days with time format', () => {
53+
const formatter = createTimezoneAwareAxisFormatter(6 * 60 * 60 * 1000, timeZone); // 6 hours
54+
const result = formatter(testTimestamp);
55+
expect(result).toBe('HH:mm[America/New_York]');
56+
});
57+
58+
it('should handle different timezones', () => {
59+
const formatter = createTimezoneAwareAxisFormatter(6 * 60 * 60 * 1000, 'Europe/Prague');
60+
const result = formatter(testTimestamp);
61+
expect(result).toBe('HH:mm[Europe/Prague]');
62+
});
63+
64+
it('should handle edge case at exactly 5 years', () => {
65+
const fiveYears = 5 * 365 * 24 * 60 * 60 * 1000;
66+
const formatter = createTimezoneAwareAxisFormatter(fiveYears, timeZone);
67+
const result = formatter(testTimestamp);
68+
// Should use MMM yyyy format (not > 5 years)
69+
expect(result).toBe('MMM yyyy[America/New_York]');
70+
});
71+
72+
it('should handle edge case at exactly 2 days', () => {
73+
const twoDays = 2 * 24 * 60 * 60 * 1000;
74+
const formatter = createTimezoneAwareAxisFormatter(twoDays, timeZone);
75+
const result = formatter(testTimestamp);
76+
// Should use HH:mm format (not > 2 days)
77+
expect(result).toBe('HH:mm[America/New_York]');
78+
});
79+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright The Perses Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
import { formatWithTimeZone } from '@perses-dev/components';
15+
16+
const DAY_MS = 86400000;
17+
const MONTH_MS = 2629440000;
18+
const YEAR_MS = 31536000000;
19+
20+
/**
21+
* Creates a timezone-aware axis formatter function for different time ranges
22+
*/
23+
export function createTimezoneAwareAxisFormatter(rangeMs: number, timeZone: string) {
24+
return function (value: number): string {
25+
const timeStamp = new Date(Number(value));
26+
27+
// more than 5 years
28+
if (rangeMs > YEAR_MS * 5) {
29+
return formatWithTimeZone(timeStamp, 'yyyy', timeZone);
30+
}
31+
32+
// more than 2 years
33+
if (rangeMs > YEAR_MS * 2) {
34+
return formatWithTimeZone(timeStamp, 'MMM yyyy', timeZone);
35+
}
36+
37+
// between 10 days to 6 months
38+
if (rangeMs > DAY_MS * 10 && rangeMs < MONTH_MS * 6) {
39+
return formatWithTimeZone(timeStamp, 'dd.MM', timeZone);
40+
}
41+
42+
// between 2 and 10 days
43+
if (rangeMs > DAY_MS * 2 && rangeMs <= DAY_MS * 10) {
44+
return formatWithTimeZone(timeStamp, 'dd.MM HH:mm', timeZone);
45+
}
46+
47+
return formatWithTimeZone(timeStamp, 'HH:mm', timeZone);
48+
};
49+
}

0 commit comments

Comments
 (0)