Chart.js icon indicating copy to clipboard operation
Chart.js copied to clipboard

Non-focused dataset state

Open alex-statsig opened this issue 3 years ago • 2 comments
trafficstars

Feature Proposal

It would be nice if hover dataset mode could support an "unfocused" state for the non-hovered items (rather than just a "hovered" state for the hovered one). In particular, it's much easier to dim every other dataset than to somehow draw extra attention to the hovered one. See the example screenshot below.

Screen Shot 2022-10-10 at 11 57 27 AM

It would also be nice if this could work in conjunction with hover.mode="index". In my case, I'd like to highlight the data points for the hovered index, and also dim out other datasets. Currently, using mode="dataset" will cause every point to be hovered. But using mode="index" allows no customization of the hovered dataset (only point).

Possible Implementation

I hacked together a plugin which uses getElementsAtEventForMode to find the active dataset, and modifies the background / border colors to dim out non-focused ones. However, it's hard to make this generic (supporting arrays, functions, values for borderColor/ backgroundColor, etc.). It also feels very hacky. I store the original background color, but I don't think it would work well if they change the color of the datasets themselves.

Essentially in afterEvent, I do something like this:

const items = chart.getElementsAtEventForMode(
            args.event.native,
            'dataset',
            { intersect: true },
            true,
          );
          let hoveredIndex = -1;
          if (items.length > 0) {
            hoveredIndex = items[0].datasetIndex;
          }

          if (hoveredIndex !== this.hoveredIndex) {
            console.log('Change to ', hoveredIndex);
            this.hoveredIndex = hoveredIndex;

            let idx = 0;
            for (const dataset of chart.data.datasets) {
              if (dataset._backgroundColor == null) {
                dataset._backgroundColor = dataset.backgroundColor;
                dataset._borderColor = dataset.borderColor;
              }
              if (idx === hoveredIndex || hoveredIndex === -1) {
                dataset.backgroundColor = dataset._backgroundColor;
                dataset.borderColor = dataset._borderColor;
              } else {
                if (dataset._backgroundColor != null) {
                  if (Array.isArray(dataset._backgroundColor)) {
                    dataset.backgroundColor = dataset._backgroundColor.map(
                      (c) => alpha(c, 0.2),
                    );
                  } else {
                    dataset.backgroundColor = alpha(
                      dataset._backgroundColor,
                      0.2,
                    );
                  }
                }
                if (dataset._borderColor != null) {
                  if (Array.isArray(dataset._borderColor)) {
                    dataset.borderColor = dataset._borderColor.map((c) =>
                      alpha(c, 0.2),
                    );
                  } else {
                    dataset.borderColor = alpha(dataset._borderColor, 0.2);
                  }
                }
              }

              idx++;
            }
            args.changed = true;
            this.chart.update();
            this.chart.draw();
          }

alex-statsig avatar Oct 10 '22 19:10 alex-statsig

for doughnut chart with single dataset

  afterDatasetsDraw(chart, args, pluginOptions){
    const {ctx, data, options, _active, chartArea: { top, bottom, left, right, width, height}, scales: {x, y}} = chart
    ctx.save()
    const dataset  = data.datasets[0]
    if (dataset._backgroundColor == null) {
      dataset._backgroundColor = dataset.backgroundColor
    }
  if(!_active.length){
    dataset.backgroundColor = dataset?._backgroundColor
  }

  if(_active.length) {
    const datasetIndexValue = _active[0].datasetIndex
    const dataIndexValue = _active[0].index
    const dataPoint = data.datasets[datasetIndexValue].data[dataIndexValue]
    dataset.backgroundColor = dataset?._backgroundColor.map((c, index) => 
      (index === dataIndexValue) ? c : alpha(c, 0.2)
    )
  }
  args.changed = true;
  chart.update();
},`

indhug19 avatar Nov 25 '22 08:11 indhug19

One other way this could be supported is using scriptable properties, i.e. borderColor: (ctx) => ctx.hovered ? regularColor : dimmedColor

There is something which almost works to support this today, which is ctx.active. There are two issues though:

  1. You cannot detect if something else is active (in my case, I want to dim the non-active ones only if something else is active, ex. ctx.isActive || !ctx.hasActive ? regularColor : dimmedColor
  2. It requires configuring the hover mode accordingly (i.e. mode: 'dataset'), which is an annoying coupling for me (I am using index mode currently for other purposes, such as tooltip)

alex-statsig avatar Jul 02 '24 21:07 alex-statsig