You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/01/23 22:49:55 UTC

(superset) branch fix/bar-chart-cross-filter-without-dimensions created (now 09c44cf33c)

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

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


      at 09c44cf33c fix(chart): enable cross-filter on bar charts without dimensions

This branch includes the following new commits:

     new 09c44cf33c fix(chart): enable cross-filter on bar charts without dimensions

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 01/01: fix(chart): enable cross-filter on bar charts without dimensions

Posted by ru...@apache.org.
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 <ev...@rusackas.com>
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 <no...@anthropic.com>
---
 .../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,
         });
       }
     },