From 0fedabf2f787be9f4c7c77f87e83d3e80c181f80 Mon Sep 17 00:00:00 2001 From: Andreas Gerstmayr Date: Mon, 29 Jun 2026 13:34:21 +0200 Subject: [PATCH 1/2] [ENHANCEMENT] scatterchart: make axis and tooltip timezone aware Signed-off-by: Andreas Gerstmayr --- scatterchart/src/Scatterplot.tsx | 35 ++++++++++---- scatterchart/src/utils/timezone-formatter.ts | 49 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 scatterchart/src/utils/timezone-formatter.ts diff --git a/scatterchart/src/Scatterplot.tsx b/scatterchart/src/Scatterplot.tsx index b49d187ab..efba503f8 100644 --- a/scatterchart/src/Scatterplot.tsx +++ b/scatterchart/src/Scatterplot.tsx @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ReactElement, useMemo } from 'react'; -import { EChart, formatValue, OnEventsType, useChartsTheme } from '@perses-dev/components'; +import { ReactElement, useCallback, useMemo } from 'react'; +import { EChart, formatValue, OnEventsType, useChartsTheme, useTimeZone } from '@perses-dev/components'; import { use, EChartsCoreOption } from 'echarts/core'; import { ScatterChart as EChartsScatterChart } from 'echarts/charts'; import { @@ -32,6 +32,7 @@ import { useTimeRange, } from '@perses-dev/plugin-system'; import { EChartTraceValue } from './ScatterChartPanel'; +import { createTimezoneAwareAxisFormatter } from './utils/timezone-formatter'; use([ DatasetComponent, @@ -44,6 +45,16 @@ use([ CanvasRenderer, ]); +const DATE_FORMAT_OPTIONS: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + fractionalSecondDigits: 3, +}; + export interface ScatterplotProps { width: number; height: number; @@ -51,18 +62,22 @@ export interface ScatterplotProps { link?: string; } -const DATE_FORMATTER = new Intl.DateTimeFormat(undefined, { - dateStyle: 'long', - timeStyle: 'medium', -}).format; - export function Scatterplot(props: ScatterplotProps): ReactElement { const { width, height, options, link: linkTemplate } = props; const chartsTheme = useChartsTheme(); const { absoluteTimeRange } = useTimeRange(); + const { timeZone, dateFormatOptionsWithUserTimeZone } = useTimeZone(); + + const dateFormatter = useMemo(() => { + const dateFormatOptions = dateFormatOptionsWithUserTimeZone(DATE_FORMAT_OPTIONS); + return new Intl.DateTimeFormat(undefined, dateFormatOptions).format; + }, [dateFormatOptionsWithUserTimeZone]); const variableValues = useAllVariableValues(); const { navigate } = useRouterContext(); + const rangeMs = absoluteTimeRange.end.valueOf() - absoluteTimeRange.start.valueOf(); + const getAxisFormatter = useCallback(() => createTimezoneAwareAxisFormatter(rangeMs, timeZone), [rangeMs, timeZone]); + // Apache EChart Options Docs: https://echarts.apache.org/en/option.html const eChartOptions: EChartsCoreOption = { dataset: options.dataset, @@ -78,6 +93,10 @@ export function Scatterplot(props: ScatterplotProps): ReactElement { type: 'time', min: absoluteTimeRange.start, max: absoluteTimeRange.end, + axisLabel: { + hideOverlap: true, + formatter: getAxisFormatter(), + }, }, yAxis: { scale: true, @@ -103,7 +122,7 @@ export function Scatterplot(props: ScatterplotProps): ReactElement { return [ `Service name: ${data.rootServiceName}
`, `Span name: ${data.rootTraceName}
`, - `Time: ${DATE_FORMATTER(data.startTime)}
`, + `Time: ${dateFormatter(data.startTime)}
`, `Duration: ${formatValue(data.durationMs, { unit: 'milliseconds' })}
`, `Span count: ${data.spanCount} (${data.errorCount} errors)
`, ].join(''); diff --git a/scatterchart/src/utils/timezone-formatter.ts b/scatterchart/src/utils/timezone-formatter.ts new file mode 100644 index 000000000..71a8e7e89 --- /dev/null +++ b/scatterchart/src/utils/timezone-formatter.ts @@ -0,0 +1,49 @@ +// Copyright The Perses Authors +// Licensed 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 { formatWithTimeZone } from '@perses-dev/components'; + +const DAY_MS = 86400000; +const MONTH_MS = 2629440000; +const YEAR_MS = 31536000000; + +/** + * Creates a timezone-aware axis formatter function for different time ranges + */ +export function createTimezoneAwareAxisFormatter(rangeMs: number, timeZone: string) { + return function (value: number): string { + const timeStamp = new Date(Number(value)); + + // more than 5 years + if (rangeMs > YEAR_MS * 5) { + return formatWithTimeZone(timeStamp, 'yyyy', timeZone); + } + + // more than 2 years + if (rangeMs > YEAR_MS * 2) { + return formatWithTimeZone(timeStamp, 'MMM yyyy', timeZone); + } + + // between 10 days to 6 months + if (rangeMs > DAY_MS * 10 && rangeMs < MONTH_MS * 6) { + return formatWithTimeZone(timeStamp, 'dd.MM', timeZone); + } + + // between 2 and 10 days + if (rangeMs > DAY_MS * 2 && rangeMs <= DAY_MS * 10) { + return formatWithTimeZone(timeStamp, 'dd.MM HH:mm', timeZone); + } + + return formatWithTimeZone(timeStamp, 'HH:mm', timeZone); + }; +} From 20c7aad23e5f3cfd09f855e7d52c3cba4031ceb5 Mon Sep 17 00:00:00 2001 From: Andreas Gerstmayr Date: Tue, 30 Jun 2026 13:53:42 +0200 Subject: [PATCH 2/2] fix axis formatting between 2 years to 6 months range Signed-off-by: Andreas Gerstmayr --- scatterchart/src/utils/timezone-formatter.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scatterchart/src/utils/timezone-formatter.ts b/scatterchart/src/utils/timezone-formatter.ts index 71a8e7e89..019a31c6e 100644 --- a/scatterchart/src/utils/timezone-formatter.ts +++ b/scatterchart/src/utils/timezone-formatter.ts @@ -14,7 +14,6 @@ import { formatWithTimeZone } from '@perses-dev/components'; const DAY_MS = 86400000; -const MONTH_MS = 2629440000; const YEAR_MS = 31536000000; /** @@ -29,21 +28,22 @@ export function createTimezoneAwareAxisFormatter(rangeMs: number, timeZone: stri return formatWithTimeZone(timeStamp, 'yyyy', timeZone); } - // more than 2 years - if (rangeMs > YEAR_MS * 2) { + // more than 6 months + if (rangeMs > DAY_MS * 180) { return formatWithTimeZone(timeStamp, 'MMM yyyy', timeZone); } - // between 10 days to 6 months - if (rangeMs > DAY_MS * 10 && rangeMs < MONTH_MS * 6) { + // more than 10 days + if (rangeMs > DAY_MS * 10) { return formatWithTimeZone(timeStamp, 'dd.MM', timeZone); } - // between 2 and 10 days - if (rangeMs > DAY_MS * 2 && rangeMs <= DAY_MS * 10) { + // more than 2 days + if (rangeMs > DAY_MS * 2) { return formatWithTimeZone(timeStamp, 'dd.MM HH:mm', timeZone); } + // less or equal 2 days return formatWithTimeZone(timeStamp, 'HH:mm', timeZone); }; }