Provide APIs to set active (hovered) and tooltip elements. (#7845)

Provide APIs to set active (hovered) and tooltip elements.

Chart.setActiveElements will set the hovered items.
Chart.tooltip.setActiveElements will set the tooltip items.
This commit is contained in:
Evert Timberg 2020-10-05 17:14:38 -04:00 committed by GitHub
parent 8d36927b29
commit a8a83d12cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 289 additions and 3 deletions

View File

@ -205,6 +205,16 @@ Sets the visibility for the given dataset to true. Updates the chart and animate
chart.show(1); // shows dataset at index 1 and does 'show' animation.
```
## setActiveElements(activeElements)
Sets the active (hovered) elements for the chart. See the "Programmatic Events" sample file to see this in action.
```javascript
chart.setActiveElements([
{datasetIndex: 0, index: 1},
]);
```
## Static: getChart(key)
Finds the chart instance from the given key. If the key is a `string`, it is interpreted as the ID of the Canvas node for the Chart. The key can also be a `CanvasRenderingContext2D` or an `HTMLDOMElement`. This will return `undefined` if no Chart is found. To be found, the chart must have previously been created.

View File

@ -0,0 +1,120 @@
<!doctype html>
<html>
<head>
<title>Programmatic Event Triggers</title>
<script src="../../dist/chart.js"></script>
<script src="../utils.js"></script>
<style>
canvas {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
</style>
</head>
<body>
<div id="container" style="width: 75%;">
<canvas id="canvas"></canvas>
</div>
<button id="hover">Trigger Hover</button>
<button id="tooltip">Trigger Tooltip</button>
<script>
var color = Chart.helpers.color;
var barChartData = {
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
datasets: [{
label: 'Dataset 1',
backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(),
borderColor: window.chartColors.red,
borderWidth: 1,
hoverBorderWidth: 5,
hoverBorderColor: 'green',
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
]
}, {
label: 'Dataset 2',
backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(),
borderColor: window.chartColors.blue,
borderWidth: 1,
hoverBorderWidth: 5,
hoverBorderColor: 'green',
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
]
}]
};
window.onload = function() {
var ctx = document.getElementById('canvas').getContext('2d');
window.myBar = new Chart(ctx, {
type: 'bar',
data: barChartData,
options: {
responsive: true,
}
});
};
document.getElementById('hover').addEventListener('click', function() {
if (window.myBar.getActiveElements().length > 0) {
window.myBar.setActiveElements([]);
} else {
window.myBar.setActiveElements(
[
{
datasetIndex: 0,
index: 0,
}, {
datasetIndex: 1,
index: 0,
}
]);
}
window.myBar.update();
});
document.getElementById('tooltip').addEventListener('click', function() {
const tooltip = window.myBar.tooltip;
if (tooltip.getActiveElements().length > 0) {
tooltip.setActiveElements([], {x: 0, y: 0});
} else {
const chartArea = window.myBar.chartArea;
tooltip.setActiveElements(
[
{
datasetIndex: 0,
index: 2,
}, {
datasetIndex: 1,
index: 2,
}
],
{
x: (chartArea.left + chartArea.right) / 2,
y: (chartArea.top + chartArea.bottom) / 2,
}
);
}
window.myBar.update();
});
</script>
</body>
</html>

View File

@ -250,6 +250,9 @@
}, {
title: 'Line Gradient',
path: 'advanced/line-gradient.html'
}, {
title: 'Programmatic Event Triggers',
path: 'advanced/programmatic-events.html'
}]
}];

View File

@ -1061,6 +1061,41 @@ class Chart {
}
}
/**
* Get active (hovered) elements
* @returns array
*/
getActiveElements() {
return this._active || [];
}
/**
* Set active (hovered) elements
* @param {array} activeElements New active data points
*/
setActiveElements(activeElements) {
const me = this;
const lastActive = me._active || [];
const active = activeElements.map(({datasetIndex, index}) => {
const meta = me.getDatasetMeta(datasetIndex);
if (!meta) {
throw new Error('No dataset found at index ' + datasetIndex);
}
return {
datasetIndex,
element: meta.data[index],
index,
};
});
const changed = !_elementsEqual(active, lastActive);
if (changed) {
me._active = active;
me._updateHoverStyles(active, lastActive);
}
}
/**
* @private
*/

View File

@ -908,6 +908,45 @@ export class Tooltip extends Element {
}
}
/**
* Get active elements in the tooltip
* @returns {Array} Array of elements that are active in the tooltip
*/
getActiveElements() {
return this._active || [];
}
/**
* Set active elements in the tooltip
* @param {array} activeElements Array of active datasetIndex/index pairs.
* @param {object} eventPosition Synthetic event position used in positioning
*/
setActiveElements(activeElements, eventPosition) {
const me = this;
const lastActive = me._active;
const active = activeElements.map(({datasetIndex, index}) => {
const meta = me._chart.getDatasetMeta(datasetIndex);
if (!meta) {
throw new Error('Cannot find a dataset at index ' + datasetIndex);
}
return {
datasetIndex,
element: meta.data[index],
index,
};
});
const changed = !_elementsEqual(lastActive, active);
const positionChanged = me._positionChanged(active, eventPosition);
if (changed || positionChanged) {
me._active = active;
me._eventPosition = eventPosition;
me.update(true);
}
}
/**
* Handle an event
* @param {IEvent} e - The event to handle
@ -932,8 +971,7 @@ export class Tooltip extends Element {
// When there are multiple items shown, but the tooltip position is nearest mode
// an update may need to be made because our position may have changed even though
// the items are the same as before.
const position = positioners[options.position].call(me, active, e);
const positionChanged = this.caretX !== position.x || this.caretY !== position.y;
const positionChanged = me._positionChanged(active, e);
// Remember Last Actives
changed = replay || !_elementsEqual(active, lastActive) || positionChanged;
@ -954,6 +992,19 @@ export class Tooltip extends Element {
return changed;
}
/**
* Determine if the active elements + event combination changes the
* tooltip position
* @param {array} active - Active elements
* @param {IEvent} e - Event that triggered the position change
* @returns {boolean} True if the position has changed
*/
_positionChanged(active, e) {
const me = this;
const position = positioners[me.options.position].call(me, active, e);
return me.caretX !== position.x || me.caretY !== position.y;
}
}
/**

View File

@ -1511,4 +1511,35 @@ describe('Chart', function() {
expect(Chart.getChart(1)).toBeUndefined();
});
});
describe('active elements', function() {
it('should set the active elements', function() {
var chart = acquireChart({
type: 'pie',
data: {
datasets: [{
data: [1, 2, 3],
borderColor: 'red',
hoverBorderColor: 'blue',
}]
}
});
const meta = chart.getDatasetMeta(0);
let props = meta.data[0].getProps(['borderColor']);
expect(props.options.borderColor).toEqual('red');
chart.setActiveElements([{
datasetIndex: 0,
index: 0,
}]);
props = meta.data[0].getProps(['borderColor']);
expect(props.options.borderColor).toEqual('blue');
const active = chart.getActiveElements();
expect(active.length).toEqual(1);
expect(active[0].element).toBe(meta.data[0]);
});
});
});

View File

@ -1490,4 +1490,25 @@ describe('Plugin.Tooltip', function() {
]));
});
});
describe('active events', function() {
it('should set the active events', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [{
label: 'Dataset 1',
data: [10, 20, 30],
pointHoverBorderColor: 'rgb(255, 0, 0)',
pointHoverBackgroundColor: 'rgb(0, 255, 0)'
}],
labels: ['Point 1', 'Point 2', 'Point 3']
},
});
const meta = chart.getDatasetMeta(0);
chart.tooltip.setActiveElements([{datasetIndex: 0, index: 0}], {x: 0, y: 0});
expect(chart.tooltip.getActiveElements()[0].element).toBe(meta.data[0]);
});
});
});

12
types/core/index.d.ts vendored
View File

@ -240,6 +240,15 @@ export interface IParsingOptions {
| false;
}
export interface ActiveDataPoint {
datasetIndex: number;
index: number;
}
export interface ActiveElement extends ActiveDataPoint {
element: Element;
}
export declare class Chart<
TYPE extends IChartType = IChartType,
DATA extends unknown[] = DefaultDataPoint<TYPE>,
@ -293,6 +302,9 @@ export declare class Chart<
hide(datasetIndex: number): void;
show(datasetIndex: number): void;
getActiveElements(): ActiveElement[];
setActiveElements(active: ActiveDataPoint[]);
destroy(): void;
toBase64Image(type?: string, quality?: any): string;
bindEvents(): void;

View File

@ -1,4 +1,4 @@
import { Chart, Element, IAnimationSpecContainer, InteractionMode, LayoutPosition, IPlugin } from '../core';
import { ActiveDataPoint, ActiveElement, Chart, Element, IAnimationSpecContainer, InteractionMode, LayoutPosition, IPlugin } from '../core';
import { Color, IChartArea, IFontSpec, Scriptable, TextAlign, IEvent, IHoverInteractionOptions } from '../core/interfaces';
import { PointStyle } from '../elements';
import { IChartData, IChartDataset } from '../interfaces';
@ -281,6 +281,9 @@ export const Tooltip: IPlugin & {
readonly positioners: {
[key: string]: (items: readonly Element[], eventPosition: { x: number; y: number }) => { x: number; y: number };
};
getActiveElements(): ActiveElement[];
setActiveElements(active: ActiveDataPoint[], eventPosition: { x: number, y: number });
};
export interface ITooltipCallbacks {