carbon-design-system / carbon-charts

:bar_chart: :chart_with_upwards_trend:⠀Robust dataviz framework implemented using D3 & typescript
https://charts.carbondesignsystem.com
Apache License 2.0
894 stars 183 forks source link

[Enhancement]: An option to only deselect one legend item if all were selected #1481

Closed mthaak closed 1 year ago

mthaak commented 1 year ago

Contact Details

No response

Summary

By default, when all legend items are active and you click one, all items are deselected except the one you clicked. I can see why, but it's annoying if your intention was to just exclude one item from the chart.

For example, you have a donut chart with many slices and you want to show all but one. Then you need to click n - 1 legend items (where n is the number of slices) to exclude just one slice.

Justification

To save effort when you have many legend items and you want to exclude just one (or a few).

Desired UX and success metrics

Given: all items are selected image

When: the user clicks a legend item image

Then: that item gets deactivated and the slice (or other chart element depending on the chart type) transitions out image

For coherency, the checkboxes in front of the legend items need to be ticks image and not like image

The logic which need to be overriden is here.

"Must have" functionality

An option (probably under LegendOptions) to exclude only the legend item you clicked when all were selected.

Specific timeline issues / requests

I can work around it using below workaround but it's very finicky...

Available extra resources

I had to manually implement this behavior using event listeners:

  // Add an effect in which:
  //   if the user clicks a legend item
  //   and all legend items are currently enabled,
  //   then disable only the clicked legend item.
  // This behaviour deviates from Carbon Charts' default behaviour which is to
  // only enable the clicked legend item and disable the others
  useEffect(() => {
    if (
      !chartRef.current ||
      !chartRef.current.chart ||
      !chartRef.current.chart.services?.events
    )
      return;

    const events = chartRef.current.chart.services.events as Events;

    events.addEventListener(EventEnums.Legend.ITEM_CLICK, (e: CustomEvent) => {
      if (!chartRef.current) return;

      const clickedLegendItemName =
        e.detail.clickedElement._groups[0][0].__data__.name;

      const dataGroups = chartRef.current.chart.model.getDataGroups() as {
        name: string;
        status: number;
      }[];

      const allSelected = dataGroups.every((g) => g.status === 1);
      if (allSelected) {
        // Enable all but the group whose corresponding legend item was clicked
        const updatedDataGroups = dataGroups.map((g) => ({
          ...g,
          status: g.name === clickedLegendItemName ? 0 : 1, // this assumes the names to be unique
        }));

        // Perform the data groups update in the next event cycle, so it happens after any functions listening to the
        // legend item click event
        setTimeout(() => {
          if (!chartRef.current) return;

          // Dispatch legend filtering event with the status of all the dataGroups
          events.dispatchEvent(EventEnums.Legend.ITEMS_UPDATE, {
            dataGroups: updatedDataGroups,
          });

          // Update model
          chartRef.current.chart.model.set({
            dataGroups: updatedDataGroups,
          });
        }, 0);
      }
    });
  }, [chartRef.current?.chart]);
theiliad commented 1 year ago

Hi, unfortunately we can't roll this out as the legend was designed with intention to have this kind of behavior when filtering.