This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch fix/bar-chart-cross-filter-without-dimensions
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 09c44cf33c58fcf2d8e8c50f6dce92e1fcfcf205
Author: Evan Rusackas <[email protected]>
AuthorDate: Fri Jan 23 14:49:41 2026 -0800

    fix(chart): enable cross-filter on bar charts without dimensions
    
    When a bar chart has no dimensions set, cross-filtering was disabled
    entirely. Users expected clicking on a bar to filter by the X-axis
    category value (e.g., "Product A"), but instead the click was ignored.
    
    This change enables cross-filtering by the categorical X-axis value
    when no dimensions are configured:
    
    - Add getXAxisCrossFilterDataMask() to create filters using X-axis column
    - Add handleXAxisChange() to emit X-axis-based cross-filters
    - Modify click handler to use X-axis value when no dimensions but
      categorical X-axis exists
    - Update context menu to provide X-axis cross-filter option
    - Add tests for the new cross-filter behavior
    
    Fixes #25334
    
    Co-Authored-By: Claude Opus 4.5 <[email protected]>
---
 .../src/Timeseries/EchartsTimeseries.test.tsx      | 90 ++++++++++++++++++++++
 .../src/Timeseries/EchartsTimeseries.tsx           | 78 +++++++++++++++++--
 2 files changed, 162 insertions(+), 6 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..2706d98104 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,15 @@ 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) {
+          // Cross-filter by X-axis value when no dimensions (issue #25334)
+          const xAxisValue = props.data[0];
+          handleXAxisChange(xAxisValue);
+        }
       }, TIMER_DURATION);
     },
     mouseout: () => {
@@ -252,12 +312,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) {
+          crossFilter = getXAxisCrossFilterDataMask(data[0]);
+        }
+
         onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
           drillToDetail: drillToDetailFilters,
           drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' },
-          crossFilter: hasDimensions
-            ? getCrossFilterDataMask(seriesName)
-            : undefined,
+          crossFilter,
         });
       }
     },

Reply via email to