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');