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([