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 ae10e105c2 fix(chart): enable cross-filter on bar charts without 
dimensions (#37407)
ae10e105c2 is described below

commit ae10e105c2cdbf221c0cba73b00338a7a6324df5
Author: Evan Rusackas <[email protected]>
AuthorDate: Sun Feb 1 02:14:24 2026 +0100

    fix(chart): enable cross-filter on bar charts without dimensions (#37407)
    
    Co-authored-by: Claude Opus 4.5 <[email protected]>
---
 .../src/Timeseries/EchartsTimeseries.test.tsx      | 90 ++++++++++++++++++++++
 .../src/Timeseries/EchartsTimeseries.tsx           | 77 ++++++++++++++++--
 .../src/Timeseries/transformProps.ts               |  1 +
 .../src/Timeseries/transformers.ts                 |  7 +-
 .../test/Timeseries/transformers.test.ts           | 28 +++++++
 5 files changed, 196 insertions(+), 7 deletions(-)

diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx
 
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx
index 59cfba2319..89a0269dce 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx
@@ -309,3 +309,93 @@ test('falls back to window resize listener when 
ResizeObserver is unavailable',
   addEventListenerSpy.mockRestore();
   removeEventListenerSpy.mockRestore();
 });
+
+// Test for issue #25334: Bar chart cross-filter without dimensions
+test('emits cross-filter on X-axis value when no dimensions and categorical 
X-axis', async () => {
+  const setDataMaskMock = jest.fn();
+
+  const propsWithCategoricalXAxis: TimeseriesChartTransformedProps = {
+    ...defaultProps,
+    emitCrossFilters: true,
+    setDataMask: setDataMaskMock,
+    groupby: [], // No dimensions
+    xAxis: {
+      label: 'category_column',
+      type: AxisType.Category, // Categorical X-axis
+    },
+  };
+
+  render(<EchartsTimeseries {...propsWithCategoricalXAxis} />);
+
+  // Get the click handler from the mock
+  const lastCall = mockEchart.mock.calls.at(-1);
+  expect(lastCall).toBeDefined();
+  const [props] = lastCall as [EchartsProps];
+  expect(props.eventHandlers).toBeDefined();
+  expect(props.eventHandlers?.click).toBeDefined();
+
+  // Simulate a click event with X-axis data
+  const clickHandler = props.eventHandlers?.click;
+  if (clickHandler) {
+    clickHandler({
+      seriesName: 'Sales', // This is the metric name
+      data: ['Product A', 100], // X-axis value is 'Product A'
+      name: 'Product A',
+      dataIndex: 0,
+    });
+
+    // Wait for the timer (TIMER_DURATION = 300ms)
+    await waitFor(
+      () => {
+        expect(setDataMaskMock).toHaveBeenCalled();
+      },
+      { timeout: 500 },
+    );
+
+    // Verify the cross-filter uses the X-axis column and value, not the metric
+    const dataMaskCall = setDataMaskMock.mock.calls[0][0];
+    expect(dataMaskCall.extraFormData.filters).toEqual([
+      {
+        col: 'category_column', // X-axis column
+        op: 'IN',
+        val: ['Product A'], // X-axis value, not 'Sales' (metric)
+      },
+    ]);
+  }
+});
+
+test('does not emit cross-filter when no dimensions and time-based X-axis', 
async () => {
+  const setDataMaskMock = jest.fn();
+
+  const propsWithTimeXAxis: TimeseriesChartTransformedProps = {
+    ...defaultProps,
+    emitCrossFilters: true,
+    setDataMask: setDataMaskMock,
+    groupby: [], // No dimensions
+    xAxis: {
+      label: '__timestamp',
+      type: AxisType.Time, // Time-based X-axis (not categorical)
+    },
+  };
+
+  render(<EchartsTimeseries {...propsWithTimeXAxis} />);
+
+  const lastCall = mockEchart.mock.calls.at(-1);
+  expect(lastCall).toBeDefined();
+  const [props] = lastCall as [EchartsProps];
+
+  // Simulate a click event
+  const clickHandler = props.eventHandlers?.click;
+  if (clickHandler) {
+    clickHandler({
+      seriesName: 'Sales',
+      data: [1609459200000, 100], // Timestamp
+      name: '2021-01-01',
+      dataIndex: 0,
+    });
+
+    // Wait a bit and verify setDataMask was NOT called
+    await new Promise(resolve => setTimeout(resolve, 400));
+    expect(setDataMaskMock).not.toHaveBeenCalled();
+  }
+});
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
 
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
index b14b7fb6ba..bd261ba0a2 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
@@ -154,6 +154,43 @@ export default function EchartsTimeseries({
     [groupby, labelMap, selectedValues],
   );
 
+  // Cross-filter using X-axis value when no dimensions are set (issue #25334)
+  const getXAxisCrossFilterDataMask = useCallback(
+    (xAxisValue: string | number) => {
+      const stringValue = String(xAxisValue);
+      const selected: string[] = Object.values(selectedValues);
+      let values: string[];
+      if (selected.includes(stringValue)) {
+        values = selected.filter(v => v !== stringValue);
+      } else {
+        values = [stringValue];
+      }
+      return {
+        dataMask: {
+          extraFormData: {
+            filters:
+              values.length === 0
+                ? []
+                : [
+                    {
+                      col: xAxis.label,
+                      op: 'IN' as const,
+                      val: values,
+                    },
+                  ],
+          },
+          filterState: {
+            label: values.length ? values : undefined,
+            value: values.length ? values : null,
+            selectedValues: values.length ? values : null,
+          },
+        },
+        isCurrentValueSelected: selected.includes(stringValue),
+      };
+    },
+    [selectedValues, xAxis.label],
+  );
+
   const handleChange = useCallback(
     (value: string) => {
       if (!emitCrossFilters) {
@@ -164,9 +201,25 @@ export default function EchartsTimeseries({
     [emitCrossFilters, setDataMask, getCrossFilterDataMask],
   );
 
+  // Handle cross-filter using X-axis value when no dimensions (issue #25334)
+  const handleXAxisChange = useCallback(
+    (xAxisValue: string | number) => {
+      if (!emitCrossFilters) {
+        return;
+      }
+      setDataMask(getXAxisCrossFilterDataMask(xAxisValue).dataMask);
+    },
+    [emitCrossFilters, setDataMask, getXAxisCrossFilterDataMask],
+  );
+
+  // Determine if X-axis can be used for cross-filtering (categorical axis 
without dimensions)
+  const canCrossFilterByXAxis =
+    !hasDimensions && xAxis.type === AxisType.Category;
+
   const eventHandlers: EventHandlers = {
     click: props => {
-      if (!hasDimensions) {
+      // Allow cross-filter by dimensions OR by categorical X-axis (issue 
#25334)
+      if (!hasDimensions && !canCrossFilterByXAxis) {
         return;
       }
       if (clickTimer.current) {
@@ -174,8 +227,14 @@ export default function EchartsTimeseries({
       }
       // Ensure that double-click events do not trigger single click event. So 
we put it in the timer.
       clickTimer.current = setTimeout(() => {
-        const { seriesName: name } = props;
-        handleChange(name);
+        if (hasDimensions) {
+          // Cross-filter by dimension (original behavior)
+          const { seriesName: name } = props;
+          handleChange(name);
+        } else if (canCrossFilterByXAxis && props.data?.[0] != null) {
+          // Cross-filter by X-axis value when no dimensions (issue #25334)
+          handleXAxisChange(props.data[0]);
+        }
       }, TIMER_DURATION);
     },
     mouseout: () => {
@@ -252,12 +311,18 @@ export default function EchartsTimeseries({
           });
         });
 
+        // Provide cross-filter for dimensions OR categorical X-axis (issue 
#25334)
+        let crossFilter;
+        if (hasDimensions) {
+          crossFilter = getCrossFilterDataMask(seriesName);
+        } else if (canCrossFilterByXAxis && data?.[0] != null) {
+          crossFilter = getXAxisCrossFilterDataMask(data[0]);
+        }
+
         onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
           drillToDetail: drillToDetailFilters,
           drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' },
-          crossFilter: hasDimensions
-            ? getCrossFilterDataMask(seriesName)
-            : undefined,
+          crossFilter,
         });
       }
     },
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 6ebc30fc22..7e818051c8 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -419,6 +419,7 @@ export default function transformProps(
         timeCompare: array,
         timeShiftColor,
         theme,
+        hasDimensions: (groupBy?.length ?? 0) > 0,
       },
     );
     if (transformedSeries) {
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts 
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
index 33e8fc446e..34dfcb34c4 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
@@ -196,6 +196,7 @@ export function transformSeries(
     timeCompare?: string[];
     timeShiftColor?: boolean;
     theme?: SupersetTheme;
+    hasDimensions?: boolean;
   },
 ): SeriesOption | undefined {
   const { name, data } = series;
@@ -237,8 +238,12 @@ export function transformSeries(
   const isConfidenceBand =
     forecastSeries.type === ForecastSeriesEnum.ForecastLower ||
     forecastSeries.type === ForecastSeriesEnum.ForecastUpper;
+  // When cross-filtering by X-axis (no dimensions), selectedValues contains
+  // X-axis values rather than series names, so skip series-level dimming.
   const isFiltered =
-    filterState?.selectedValues && !filterState?.selectedValues.includes(name);
+    opts.hasDimensions !== false &&
+    filterState?.selectedValues &&
+    !filterState?.selectedValues.includes(name);
   const opacity = isFiltered
     ? OpacityEnum.SemiTransparent
     : opts.lineStyle?.opacity || OpacityEnum.NonTransparent;
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 143bdf4389..bb2e25ee91 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
@@ -89,6 +89,34 @@ describe('transformSeries', () => {
     expect((result as any).itemStyle.borderType).toBeUndefined();
     expect((result as any).itemStyle.borderColor).toBeUndefined();
   });
+
+  it('should dim series when selectedValues does not include series name 
(dimension-based filtering)', () => {
+    const opts = {
+      filterState: { selectedValues: ['other-series'] },
+      hasDimensions: true,
+      seriesType: EchartsTimeseriesSeriesType.Bar,
+      timeShiftColor: false,
+    };
+
+    const result = transformSeries(series, mockColorScale, 'test-key', opts);
+
+    // OpacityEnum.SemiTransparent = 0.3
+    expect((result as any).itemStyle.opacity).toBe(0.3);
+  });
+
+  it('should not dim series when hasDimensions is false (X-axis 
cross-filtering)', () => {
+    const opts = {
+      filterState: { selectedValues: ['Product A'] },
+      hasDimensions: false,
+      seriesType: EchartsTimeseriesSeriesType.Bar,
+      timeShiftColor: false,
+    };
+
+    const result = transformSeries(series, mockColorScale, 'test-key', opts);
+
+    // OpacityEnum.NonTransparent = 1 (not dimmed)
+    expect((result as any).itemStyle.opacity).toBe(1);
+  });
 });
 
 describe('transformNegativeLabelsPosition', () => {

Reply via email to