This is an automated email from the ASF dual-hosted git repository.
rusackas 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 c43effa4a32 fix(explore): show the beginning date on time-series
x-axis line charts (#42046)
c43effa4a32 is described below
commit c43effa4a321f64ce2ddde92ac80e3263740a4ed
Author: jesperct <[email protected]>
AuthorDate: Thu Jul 23 21:14:30 2026 -0300
fix(explore): show the beginning date on time-series x-axis line charts
(#42046)
---
.../src/MixedTimeseries/transformProps.ts | 17 ++++++
.../src/Timeseries/transformProps.ts | 25 +++++++-
.../test/MixedTimeseries/transformProps.test.ts | 55 +++++++++++++++++
.../test/Timeseries/transformProps.test.ts | 69 ++++++++++++++++++++++
4 files changed, 163 insertions(+), 3 deletions(-)
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
index 5e3599234a5..ee01fb6f89c 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
@@ -641,7 +641,22 @@ export default function transformProps(
const deduplicatedFormatter = showMaxLabel
? (() => {
let lastLabel: string | undefined;
+ let lastValue: number | undefined;
const wrapper = (value: number | string) => {
+ // ECharts formats the labels in repeated ascending passes. Reset the
+ // dedup state when the sequence restarts so a forced boundary label
+ // (e.g. the min date) isn't blanked by the previous pass's last
label
+ // when both format identically (e.g. a May-to-May range).
+ if (
+ typeof value === 'number' &&
+ lastValue !== undefined &&
+ value <= lastValue
+ ) {
+ lastLabel = undefined;
+ }
+ if (typeof value === 'number') {
+ lastValue = value;
+ }
const label =
typeof xAxisFormatter === 'function'
? (xAxisFormatter as Function)(value)
@@ -743,6 +758,8 @@ export default function transformProps(
...(showMaxLabel && {
showMaxLabel: true,
alignMaxLabel: 'right',
+ showMinLabel: true,
+ alignMinLabel: 'left',
}),
},
minorTick: { show: minorTicks },
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 a2ad93e9578..1493aeee897 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -883,7 +883,22 @@ export default function transformProps(
const deduplicatedFormatter = showMaxLabel
? (() => {
let lastLabel: string | undefined;
+ let lastValue: number | undefined;
const wrapper = (value: number | string) => {
+ // ECharts formats the labels in repeated ascending passes. Reset the
+ // dedup state when the sequence restarts so a forced boundary label
+ // (e.g. the min date) isn't blanked by the previous pass's last
label
+ // when both format identically (e.g. a May-to-May range).
+ if (
+ typeof value === 'number' &&
+ lastValue !== undefined &&
+ value <= lastValue
+ ) {
+ lastLabel = undefined;
+ }
+ if (typeof value === 'number') {
+ lastValue = value;
+ }
const label =
typeof xAxisFormatter === 'function'
? (xAxisFormatter as Function)(value)
@@ -921,12 +936,16 @@ export default function transformProps(
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.
+ // Force the boundary labels on non-rotated time axes so the first
+ // and last dates stay visible: hideOverlap can hide the last label,
+ // and a min date that falls between "nice" ticks otherwise renders
+ // no beginning label. Skipped when rotated to avoid phantom labels
+ // at the axis boundary.
...(showMaxLabel && {
showMaxLabel: true,
alignMaxLabel: 'right',
+ showMinLabel: true,
+ alignMinLabel: 'left',
}),
},
minorTick: { show: minorTicks },
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts
b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts
index fc189cb8358..84c1765b83b 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts
@@ -1106,3 +1106,58 @@ test('tooltip time grain wiring: chart-level time grain
drives the tooltip when
expect(result).toContain('2021');
expect(result).not.toContain('2021-01-07');
});
+
+const createTemporalMixedChartProps = (timeFormat: string) => {
+ const rows = [
+ { __timestamp: Date.UTC(2003, 4, 1), metric: 10 },
+ { __timestamp: Date.UTC(2004, 0, 1), metric: 20 },
+ { __timestamp: Date.UTC(2005, 4, 1), metric: 30 },
+ ];
+ const q = createTestQueryData(rows, {
+ colnames: ['__timestamp', 'metric'],
+ coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
+ label_map: { __timestamp: ['__timestamp'], metric: ['metric'] },
+ });
+ return createEchartsTimeseriesTestChartProps<
+ EchartsMixedTimeseriesFormData,
+ EchartsMixedTimeseriesProps
+ >({
+ ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
+ defaultQueriesData: [q, q],
+ formData: {
+ ...formData,
+ x_axis: '__timestamp',
+ metrics: ['metric'],
+ metricsB: ['metric'],
+ groupby: [],
+ groupbyB: [],
+ timeGrainSqla: TimeGranularity.MONTH,
+ xAxisTimeFormat: timeFormat,
+ },
+ queriesData: [q, q],
+ });
+};
+
+test('x-axis forces showMinLabel for time grains so the beginning date stays
visible (mixed)', () => {
+ const xAxis = transformProps(createTemporalMixedChartProps('smart_date'))
+ .echartOptions.xAxis as any;
+ expect(xAxis.axisLabel.showMinLabel).toBe(true);
+});
+
+test('x-axis dedup keeps the forced min label when the endpoints format
identically (mixed)', () => {
+ // May→May range renders "May" at both boundaries; the dedup must reset per
+ // ECharts pass so the forced min label survives the second pass.
+ const { formatter } = (
+ transformProps(createTemporalMixedChartProps('%b')).echartOptions
+ .xAxis as any
+ ).axisLabel;
+ const min = Date.UTC(2003, 4, 1);
+ const mid = Date.UTC(2004, 0, 1);
+ const max = Date.UTC(2005, 4, 1);
+
+ formatter(min);
+ formatter(mid);
+ formatter(max);
+
+ expect(formatter(min)).toBe('May');
+});
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 a219c22f56c..9f51bcfcca8 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
@@ -1517,6 +1517,45 @@ test('x-axis formatter deduplicates consecutive
identical labels for coarse time
expect(label4).toBe('');
});
+test('x-axis dedup keeps the forced min label when the endpoints format
identically', () => {
+ // A May→May range renders "May" at both boundaries. ECharts formats labels
in
+ // repeated ascending passes; the dedup must reset per pass so the forced min
+ // label isn't blanked by the previous pass's (identical) max label.
+ const data = [
+ { __timestamp: Date.UTC(2003, 4, 1), sales: 100 },
+ { __timestamp: Date.UTC(2004, 0, 1), sales: 200 },
+ { __timestamp: Date.UTC(2005, 4, 1), sales: 300 },
+ ];
+
+ const chartProps = createTestChartProps({
+ formData: {
+ granularity_sqla: 'ds',
+ timeGrainSqla: TimeGranularity.MONTH,
+ xAxisTimeFormat: '%b',
+ },
+ queriesData: [
+ createTestQueryData(data, {
+ colnames: ['__timestamp', 'sales'],
+ coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
+ }),
+ ],
+ });
+
+ const { formatter } = (transformProps(chartProps).echartOptions.xAxis as any)
+ .axisLabel;
+ const min = Date.UTC(2003, 4, 1);
+ const mid = Date.UTC(2004, 0, 1);
+ const max = Date.UTC(2005, 4, 1);
+
+ // First pass fills the dedup state, ending on the max label ("May").
+ formatter(min);
+ formatter(mid);
+ formatter(max);
+
+ // Second pass restarts at the min; it must not be blanked by the prior
"May".
+ expect(formatter(min)).toBe('May');
+});
+
test('x-axis does not force showMaxLabel when no time grain is set', () => {
const data = [
{ __timestamp: Date.UTC(2003, 0, 6), sales: 100 },
@@ -1539,6 +1578,36 @@ test('x-axis does not force showMaxLabel when no time
grain is set', () => {
const xAxisResult = transformProps(chartProps).echartOptions.xAxis as any;
expect(xAxisResult.axisLabel.showMaxLabel).not.toBe(true);
+ expect(xAxisResult.axisLabel.showMinLabel).not.toBe(true);
+});
+
+test('x-axis forces showMinLabel for time grains so the beginning date stays
visible', () => {
+ // When the first data point is not on a coarse boundary (e.g. a mid-year
+ // month), ECharts places its first label on the next "nice" tick and leaves
+ // the axis-min date unlabeled. showMinLabel forces the beginning date to
+ // render, symmetric to showMaxLabel on the trailing edge.
+ const monthData = [
+ { __timestamp: Date.UTC(2003, 4, 1), sales: 100 },
+ { __timestamp: Date.UTC(2003, 5, 1), sales: 200 },
+ { __timestamp: Date.UTC(2003, 6, 1), sales: 300 },
+ ];
+
+ const chartProps = createTestChartProps({
+ formData: {
+ granularity_sqla: 'ds',
+ timeGrainSqla: TimeGranularity.MONTH,
+ xAxisTimeFormat: 'smart_date',
+ },
+ queriesData: [
+ createTestQueryData(monthData, {
+ colnames: ['__timestamp', 'sales'],
+ coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
+ }),
+ ],
+ });
+
+ const xAxisResult = transformProps(chartProps).echartOptions.xAxis as any;
+ expect(xAxisResult.axisLabel.showMinLabel).toBe(true);
});
test('numeric x coltype routes through the number formatter (not the time
formatter)', () => {