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)', () => {

Reply via email to