You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2026/03/30 12:20:49 UTC
(superset) 08/11: fix(Timeseries): dedup x axis labels (#38733)
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 2aaf935db870274759fad9854afe1f7c688db610
Author: Mehmet Salih Yavuz <sa...@proton.me>
AuthorDate: Fri Mar 27 22:13:02 2026 +0300
fix(Timeseries): dedup x axis labels (#38733)
(cherry picked from commit f832f9b0d5e54fffb418e26115e0c548ef6f3c64)
---
.../src/Timeseries/transformProps.ts | 38 ++++++++++++++++----
.../test/Timeseries/transformProps.test.ts | 41 ++++++++++++++++++++++
2 files changed, 73 insertions(+), 6 deletions(-)
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index 237950626fb..f7d6fe86c81 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -824,6 +824,33 @@ export default function transformProps(
isHorizontal,
);
+ // When showMaxLabel is true, ECharts may render a label at the axis
+ // boundary that formats identically to the last data-point tick (e.g.
+ // "2005" appears twice with Year grain). Wrap the formatter to suppress
+ // consecutive duplicate labels.
+ const showMaxLabel =
+ xAxisType === AxisType.Time && xAxisLabelRotation === 0;
+ const deduplicatedFormatter = showMaxLabel
+ ? (() => {
+ let lastLabel: string | undefined;
+ const wrapper = (value: number | string) => {
+ const label =
+ typeof xAxisFormatter === 'function'
+ ? (xAxisFormatter as Function)(value)
+ : String(value);
+ if (label === lastLabel) {
+ return '';
+ }
+ lastLabel = label;
+ return label;
+ };
+ if (typeof xAxisFormatter === 'function' && 'id' in xAxisFormatter) {
+ (wrapper as any).id = (xAxisFormatter as any).id;
+ }
+ return wrapper;
+ })()
+ : xAxisFormatter;
+
let xAxis: any = {
type: xAxisType,
name: xAxisTitle,
@@ -837,17 +864,16 @@ export default function transformProps(
// from overlapping each other, with showMaxLabel to ensure
// the last data point label stays visible (#37181).
hideOverlap: !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0),
- formatter: xAxisFormatter,
+ formatter: deduplicatedFormatter,
rotate: xAxisLabelRotation,
interval: xAxisLabelInterval,
// Force last label on non-rotated time axes to prevent
// hideOverlap from hiding it. Skipped when rotated to
// avoid phantom labels at the axis boundary.
- ...(xAxisType === AxisType.Time &&
- xAxisLabelRotation === 0 && {
- showMaxLabel: true,
- alignMaxLabel: 'right',
- }),
+ ...(showMaxLabel && {
+ showMaxLabel: true,
+ alignMaxLabel: 'right',
+ }),
},
minorTick: { show: minorTicks },
minInterval:
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts
index 11114815338..c30491601c9 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts
@@ -28,7 +28,9 @@ import {
SqlaFormData,
TimeseriesAnnotationLayer,
ChartDataResponseResult,
+ TimeGranularity,
} from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/common';
import { EchartsTimeseriesChartProps } from '../../src/types';
import type { SeriesOption } from 'echarts';
import transformProps from '../../src/Timeseries/transformProps';
@@ -1336,6 +1338,45 @@ test('should not apply axis bounds calculation when seriesType is not Bar for ho
expect(xAxisRaw.max).toBeUndefined();
});
+test('x-axis formatter deduplicates consecutive identical labels for coarse time grains', () => {
+ const yearData = [
+ { __timestamp: Date.UTC(2003, 0, 1), sales: 100 },
+ { __timestamp: Date.UTC(2004, 0, 1), sales: 200 },
+ { __timestamp: Date.UTC(2005, 0, 1), sales: 300 },
+ ];
+
+ const chartProps = createTestChartProps({
+ formData: {
+ granularity_sqla: 'ds',
+ time_grain_sqla: TimeGranularity.YEAR,
+ xAxisTimeFormat: '%Y',
+ },
+ queriesData: [
+ createTestQueryData(yearData, {
+ colnames: ['__timestamp', 'sales'],
+ coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
+ }),
+ ],
+ });
+
+ const transformedProps = transformProps(chartProps);
+ const xAxisResult = transformedProps.echartOptions.xAxis as any;
+ const { formatter } = xAxisResult.axisLabel;
+
+ expect(typeof formatter).toBe('function');
+ expect(xAxisResult.axisLabel.showMaxLabel).toBe(true);
+
+ const label1 = formatter(Date.UTC(2003, 0, 1));
+ const label2 = formatter(Date.UTC(2004, 0, 1));
+ const label3 = formatter(Date.UTC(2005, 0, 1));
+ const label4 = formatter(Date.UTC(2005, 6, 1));
+
+ expect(label1).toBe('2003');
+ expect(label2).toBe('2004');
+ expect(label3).toBe('2005');
+ expect(label4).toBe('');
+});
+
test('should assign distinct dash patterns for multiple time offsets consistently', () => {
const queriesDataWithMultipleOffsets = [
createTestQueryData([