]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Provide APIs to set active (hovered) and tooltip elements. (#7845)
authorEvert Timberg <evert.timberg+github@gmail.com>
Mon, 5 Oct 2020 21:14:38 +0000 (17:14 -0400)
committerGitHub <noreply@github.com>
Mon, 5 Oct 2020 21:14:38 +0000 (17:14 -0400)
Provide APIs to set active (hovered) and tooltip elements.

Chart.setActiveElements will set the hovered items.
Chart.tooltip.setActiveElements will set the tooltip items.

docs/docs/developers/api.md
samples/advanced/programmatic-events.html [new file with mode: 0644]
samples/samples.js
src/core/core.controller.js
src/plugins/plugin.tooltip.js
test/specs/core.controller.tests.js
test/specs/plugin.tooltip.tests.js
types/core/index.d.ts
types/plugins/index.d.ts

index 2ce6c25d5ef97590470fccb53293f2a8c9bb8acd..175c52371b38408a8d26a1dbc0bfce6756eccb3e 100644 (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.
diff --git a/samples/advanced/programmatic-events.html b/samples/advanced/programmatic-events.html
new file mode 100644 (file)
index 0000000..d6b1dc2
--- /dev/null
@@ -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>
index 1f4fc37708f50a0c12bcd2147ad177b866545d2c..3a6bee7a16d1b360bd079623a15cff2a639b4b54 100644 (file)
                }, {
                        title: 'Line Gradient',
                        path: 'advanced/line-gradient.html'
+               }, {
+                       title: 'Programmatic Event Triggers',
+                       path: 'advanced/programmatic-events.html'
                }]
        }];
 
index a43090a091f46e1acb1d8d31ac4cb8aa3a251210..2c2ba32854f35447085013c10cf8ec7ab00b7325 100644 (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
         */
index 67299892bb2c996d03d4cd7071c07dc7e212c9b7..3e2084f85eece7998cb9c82d5f0118f09632ffe3 100644 (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;
+       }
 }
 
 /**
index ac79cab777802c65dab3a4f95d7ccc6c61f7223f..7467c08306e739e335a3cb87204262f6267e9da0 100644 (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]);
+               });
+       });
 });
index a117b2d41e4341c1ba111d422b4d50d4ee71ea24..9aa6d83444df8b548f55d5f05edba57e9ffb5a2b 100644 (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]);
+               });
+       });
 });
index 1d54f1fc13b88984847e19d951822edfdf0a7535..5936a5f11b784fc3658f4063ddf9ac726e9e22c6 100644 (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;
index fa7878071dd1c96b4a93ae2724cb11d28272f61d..efa0eda00edc17b16d1ebb8ececd913651c382cc 100644 (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 {