This is an automated email from the ASF dual-hosted git repository. enzomartellucci pushed a commit to branch enxdev/fix/echarts-x-axis in repository https://gitbox.apache.org/repos/asf/superset.git
commit 0a06daae6c4b93d718d56fbdaa23e045c22bd84b Author: Enzo Martellucci <[email protected]> AuthorDate: Fri Feb 6 14:02:37 2026 +0100 fix(chart): prevent x-axis date labels from disappearing when rotated --- .../src/Timeseries/transformProps.ts | 28 ++++- .../test/Timeseries/transformers.test.ts | 113 +++++++++++++++------ 2 files changed, 105 insertions(+), 36 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 7acd63132b9..bde688c1186 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -606,14 +606,16 @@ 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. + hideOverlap: + !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0), formatter: xAxisFormatter, rotate: xAxisLabelRotation, interval: xAxisLabelInterval, - ...(xAxisType === AxisType.Time && { - showMaxLabel: true, - alignMaxLabel: 'right', - }), }, minorTick: { show: minorTicks }, minInterval: @@ -659,6 +661,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 f5770b6fe3f..ba1c7806362 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 @@ -227,47 +227,98 @@ describe('transformNegativeLabelsPosition', () => { }); }); -test('should configure time axis labels to show max label for last month visibility', () => { - const formData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - viz_type: 'my_viz', - }; - const queriesData = [ - { - data: [ - { sum__num: 100, __timestamp: new Date('2026-01-01').getTime() }, - { sum__num: 200, __timestamp: new Date('2026-02-01').getTime() }, - { sum__num: 300, __timestamp: new Date('2026-03-01').getTime() }, - { sum__num: 400, __timestamp: new Date('2026-04-01').getTime() }, - { sum__num: 500, __timestamp: new Date('2026-05-01').getTime() }, - ], - colnames: ['sum__num', '__timestamp'], - coltypes: [GenericDataType.Numeric, GenericDataType.Temporal], +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, }, - ]; - const chartProps = new ChartProps({ - formData, width: 800, height: 600, - queriesData, + 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('x-axis dates do not overlap when horizontal (rotation 0°)', () => { + const result = transformProps(buildTimeseriesChartProps()); + const { axisLabel } = result.echartOptions.xAxis as Record<string, any>; + expect(axisLabel.hideOverlap).toBe(true); + expect(axisLabel.showMaxLabel).toBeUndefined(); + expect(axisLabel.alignMaxLabel).toBeUndefined(); +}); + +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( - chartProps as unknown as EchartsTimeseriesChartProps, + 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); +}); - expect(result.echartOptions.xAxis).toEqual( - expect.objectContaining({ - axisLabel: expect.objectContaining({ - showMaxLabel: true, - alignMaxLabel: '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 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 {
