Chart.js
Chart.js copied to clipboard
Non-focused dataset state
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.
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();
}
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();
},`
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:
- 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 - It requires configuring the hover mode accordingly (i.e. mode: 'dataset'), which is an annoying coupling for me (I am using
indexmode currently for other purposes, such as tooltip)