This is an automated email from the ASF dual-hosted git repository.

enzomartellucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 5a134170a03 fix(chart): prevent x-axis date labels from disappearing 
when rotated (#37755)
5a134170a03 is described below

commit 5a134170a031d091b4f3fee4177b4037f5915689
Author: Enzo Martellucci <[email protected]>
AuthorDate: Thu Feb 26 18:10:44 2026 +0100

    fix(chart): prevent x-axis date labels from disappearing when rotated 
(#37755)
---
 .../src/Timeseries/transformProps.ts               | 36 ++++++--
 .../test/Timeseries/transformers.test.ts           | 96 ++++++++++++++++++++++
 2 files changed, 127 insertions(+), 5 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 095e757d707..237b3088cbe 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -607,14 +607,24 @@ export default function transformProps(
     nameGap: convertInteger(xAxisTitleMargin),
     nameLocation: 'middle',
     axisLabel: {
-      hideOverlap: true,
+      // When rotation is applied on time axes, hideOverlap can
+      // aggressively hide the last label. Rotated labels already
+      // have less overlap, so disabling hideOverlap is safe.
+      // At 0° rotation, keep hideOverlap to prevent long labels
+      // from overlapping each other, with showMaxLabel to ensure
+      // the last data point label stays visible (#37181).
+      hideOverlap: !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0),
       formatter: xAxisFormatter,
       rotate: xAxisLabelRotation,
       interval: xAxisLabelInterval,
-      ...(xAxisType === AxisType.Time && {
-        showMaxLabel: true,
-        alignMaxLabel: 'right',
-      }),
+      // 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',
+        }),
     },
     minorTick: { show: minorTicks },
     minInterval:
@@ -660,6 +670,22 @@ export default function transformProps(
     nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
   };
 
+  // Increase right padding for rotated time axis labels to prevent
+  // the last label from being clipped at the chart boundary.
+  if (
+    xAxisType === AxisType.Time &&
+    xAxisLabelRotation !== 0 &&
+    !isHorizontal
+  ) {
+    padding.right = Math.max(
+      padding.right || 0,
+      TIMESERIES_CONSTANTS.gridOffsetRight +
+        Math.ceil(
+          Math.abs(Math.sin((xAxisLabelRotation * Math.PI) / 180)) * 80,
+        ),
+    );
+  }
+
   if (isHorizontal) {
     [xAxis, yAxis] = [yAxis, xAxis];
     [padding.bottom, padding.left] = [padding.left, padding.bottom];
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts
 
b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts
index 68f48196df2..1fc9c6558da 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts
@@ -246,6 +246,37 @@ describe('optimizeBarLabelPlacement', () => {
   });
 });
 
+function buildTimeseriesChartProps(
+  overrides: Record<string, unknown> = {},
+): EchartsTimeseriesChartProps {
+  return new ChartProps({
+    formData: {
+      colorScheme: 'bnbColors',
+      datasource: '3__table',
+      granularity_sqla: 'ds',
+      metric: 'sum__num',
+      viz_type: 'my_viz',
+      ...overrides,
+    },
+    width: 800,
+    height: 600,
+    queriesData: [
+      {
+        data: [
+          { sum__num: 100, __timestamp: new Date('2026-01-01').getTime() },
+          { sum__num: 200, __timestamp: new Date('2026-04-01').getTime() },
+          { sum__num: 300, __timestamp: new Date('2026-07-01').getTime() },
+          { sum__num: 400, __timestamp: new Date('2026-10-01').getTime() },
+          { sum__num: 500, __timestamp: new Date('2026-12-01').getTime() },
+        ],
+        colnames: ['sum__num', '__timestamp'],
+        coltypes: [GenericDataType.Numeric, GenericDataType.Temporal],
+      },
+    ],
+    theme: supersetTheme,
+  }) as unknown as EchartsTimeseriesChartProps;
+}
+
 test('should configure time axis labels to show max label for last month 
visibility', () => {
   const formData = {
     colorScheme: 'bnbColors',
@@ -289,6 +320,71 @@ test('should configure time axis labels to show max label 
for last month visibil
   );
 });
 
+test('x-axis dates do not overlap and last label stays visible at 0° 
rotation', () => {
+  const result = transformProps(buildTimeseriesChartProps());
+  const { axisLabel } = result.echartOptions.xAxis as Record<string, any>;
+
+  expect(axisLabel.hideOverlap).toBe(true);
+  // showMaxLabel forces the last data point label to render even
+  // when hideOverlap is active, preventing the #37181 regression.
+  expect(axisLabel.showMaxLabel).toBe(true);
+  expect(axisLabel.alignMaxLabel).toBe('right');
+});
+
+test('last x-axis date is visible and not cut off when rotated -45°', () => {
+  const lastDataPointTimestamp = new Date('2026-12-01').getTime();
+  const result = transformProps(
+    buildTimeseriesChartProps({
+      xAxisLabelRotation: -45,
+      x_axis_time_format: '%d-%m-%Y %H:%M:%S',
+    }),
+  );
+  const { xAxis, grid } = result.echartOptions as Record<string, any>;
+  const { axisLabel } = xAxis;
+
+  // The formatter renders the last data point's date as a full string
+  const lastDateLabel = axisLabel.formatter(lastDataPointTimestamp);
+  expect(lastDateLabel).toMatch(/01-12-2026/);
+  expect(lastDateLabel).not.toBe('');
+
+  // Labels are not aggressively hidden so the last date stays visible
+  expect(axisLabel.hideOverlap).toBe(false);
+  expect(axisLabel.rotate).toBe(-45);
+  // No phantom label at a position that doesn't correspond to any bar
+  expect(axisLabel.showMaxLabel).toBeUndefined();
+  // Enough right padding so the last rotated label is not clipped
+  expect(grid.right).toBeGreaterThan(TIMESERIES_CONSTANTS.gridOffsetRight);
+});
+
+test('last x-axis date is visible and not cut off when rotated 45°', () => {
+  const lastDataPointTimestamp = new Date('2026-12-01').getTime();
+  const result = transformProps(
+    buildTimeseriesChartProps({
+      xAxisLabelRotation: 45,
+      x_axis_time_format: '%d-%m-%Y %H:%M:%S',
+    }),
+  );
+  const { xAxis, grid } = result.echartOptions as Record<string, any>;
+
+  const lastDateLabel = xAxis.axisLabel.formatter(lastDataPointTimestamp);
+  expect(lastDateLabel).toMatch(/01-12-2026/);
+  expect(lastDateLabel).not.toBe('');
+
+  expect(xAxis.axisLabel.hideOverlap).toBe(false);
+  expect(xAxis.axisLabel.rotate).toBe(45);
+  expect(grid.right).toBeGreaterThan(TIMESERIES_CONSTANTS.gridOffsetRight);
+});
+
+test('no phantom date label appears at the axis boundary', () => {
+  const result = transformProps(
+    buildTimeseriesChartProps({ xAxisLabelRotation: -45 }),
+  );
+  const { axisLabel } = result.echartOptions.xAxis as Record<string, any>;
+
+  expect(axisLabel.showMaxLabel).toBeUndefined();
+  expect(axisLabel.showMinLabel).toBeUndefined();
+});
+
 function setupGetChartPaddingMock(): jest.SpyInstance {
   // Mock getChartPadding to return the padding object as-is for easier testing
   const getChartPaddingSpy = jest.spyOn(seriesUtils, 'getChartPadding');

Reply via email to