]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Optimize tooltip event handler (#6827)
authorBen McCann <322311+benmccann@users.noreply.github.com>
Sun, 15 Dec 2019 13:26:17 +0000 (05:26 -0800)
committerEvert Timberg <evert.timberg+github@gmail.com>
Sun, 15 Dec 2019 13:26:17 +0000 (08:26 -0500)
* Optimize tooltip event handler

* Address review comments

* Additional cleanup

src/core/core.interaction.js
src/scales/scale.time.js
test/specs/core.interaction.tests.js

index ad1ec471793529808a5467ba0b12827fe3ca2ae8..d829b006b07c44e049edb571ab5d699a3365868d 100644 (file)
@@ -24,7 +24,7 @@ function getRelativePosition(e, chart) {
  * @param {Chart} chart - the chart
  * @param {function} handler - the callback to execute for each visible item
  */
-function parseVisibleItems(chart, handler) {
+function evaluateAllVisibleItems(chart, handler) {
        const metasets = chart._getSortedVisibleDatasetMetas();
        let index, data, element;
 
@@ -39,69 +39,115 @@ function parseVisibleItems(chart, handler) {
        }
 }
 
+/**
+ * Helper function to check the items at the hovered index on the index scale
+ * @param {Chart} chart - the chart
+ * @param {function} handler - the callback to execute for each visible item
+ * @return whether all scales were of a suitable type
+ */
+function evaluateItemsAtIndex(chart, axis, position, handler) {
+       const metasets = chart._getSortedVisibleDatasetMetas();
+       const indices = [];
+       for (let i = 0, ilen = metasets.length; i < ilen; ++i) {
+               const metaset = metasets[i];
+               const iScale = metaset.controller._cachedMeta.iScale;
+               if (!iScale || axis !== iScale.axis || !iScale.getIndexForPixel) {
+                       return false;
+               }
+               const index = iScale.getIndexForPixel(position[axis]);
+               if (!helpers.isNumber(index)) {
+                       return false;
+               }
+               indices.push(index);
+       }
+       // do this only after checking whether all scales are of a suitable type
+       for (let i = 0, ilen = metasets.length; i < ilen; ++i) {
+               const metaset = metasets[i];
+               const index = indices[i];
+               const element = metaset.data[index];
+               if (!element._view.skip) {
+                       handler(element, metaset.index, index);
+               }
+       }
+       return true;
+}
+
+/**
+ * Get a distance metric function for two points based on the
+ * axis mode setting
+ * @param {string} axis - the axis mode. x|y|xy
+ */
+function getDistanceMetricForAxis(axis) {
+       const useX = axis.indexOf('x') !== -1;
+       const useY = axis.indexOf('y') !== -1;
+
+       return function(pt1, pt2) {
+               const deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0;
+               const deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0;
+               return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
+       };
+}
+
 /**
  * Helper function to get the items that intersect the event position
  * @param {ChartElement[]} items - elements to filter
  * @param {object} position - the point to be nearest to
  * @return {ChartElement[]} the nearest items
  */
-function getIntersectItems(chart, position) {
-       var elements = [];
+function getIntersectItems(chart, position, axis) {
+       const items = [];
 
-       parseVisibleItems(chart, function(element, datasetIndex, index) {
+       const evaluationFunc = function(element, datasetIndex, index) {
                if (element.inRange(position.x, position.y)) {
-                       elements.push({element, datasetIndex, index});
+                       items.push({element, datasetIndex, index});
                }
-       });
+       };
 
-       return elements;
+       const optimized = evaluateItemsAtIndex(chart, axis, position, evaluationFunc);
+       if (optimized) {
+               return items;
+       }
+
+       evaluateAllVisibleItems(chart, evaluationFunc);
+       return items;
 }
 
 /**
  * Helper function to get the items nearest to the event position considering all visible items in the chart
  * @param {Chart} chart - the chart to look at elements from
  * @param {object} position - the point to be nearest to
+ * @param {function} axis - the axes along which to measure distance
  * @param {boolean} intersect - if true, only consider items that intersect the position
- * @param {function} distanceMetric - function to provide the distance between points
  * @return {ChartElement[]} the nearest items
  */
-function getNearestItems(chart, position, intersect, distanceMetric) {
-       var minDistance = Number.POSITIVE_INFINITY;
-       var nearestItems = [];
+function getNearestItems(chart, position, axis, intersect) {
+       const distanceMetric = getDistanceMetricForAxis(axis);
+       let minDistance = Number.POSITIVE_INFINITY;
+       let items = [];
 
-       parseVisibleItems(chart, function(element, datasetIndex, index) {
+       const evaluationFunc = function(element, datasetIndex, index) {
                if (intersect && !element.inRange(position.x, position.y)) {
                        return;
                }
 
-               var center = element.getCenterPoint();
-               var distance = distanceMetric(position, center);
+               const center = element.getCenterPoint();
+               const distance = distanceMetric(position, center);
                if (distance < minDistance) {
-                       nearestItems = [{element, datasetIndex, index}];
+                       items = [{element, datasetIndex, index}];
                        minDistance = distance;
                } else if (distance === minDistance) {
                        // Can have multiple items at the same distance in which case we sort by size
-                       nearestItems.push({element, datasetIndex, index});
+                       items.push({element, datasetIndex, index});
                }
-       });
-
-       return nearestItems;
-}
+       };
 
-/**
- * Get a distance metric function for two points based on the
- * axis mode setting
- * @param {string} axis - the axis mode. x|y|xy
- */
-function getDistanceMetricForAxis(axis) {
-       var useX = axis.indexOf('x') !== -1;
-       var useY = axis.indexOf('y') !== -1;
+       const optimized = evaluateItemsAtIndex(chart, axis, position, evaluationFunc);
+       if (optimized) {
+               return items;
+       }
 
-       return function(pt1, pt2) {
-               var deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0;
-               var deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0;
-               return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
-       };
+       evaluateAllVisibleItems(chart, evaluationFunc);
+       return items;
 }
 
 /**
@@ -133,8 +179,8 @@ module.exports = {
                index: function(chart, e, options) {
                        const position = getRelativePosition(e, chart);
                        // Default axis for index mode is 'x' to match old behaviour
-                       const distanceMetric = getDistanceMetricForAxis(options.axis || 'x');
-                       const items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false, distanceMetric);
+                       const axis = options.axis || 'x';
+                       const items = options.intersect ? getIntersectItems(chart, position, axis) : getNearestItems(chart, position, axis);
                        const elements = [];
 
                        if (!items.length) {
@@ -165,8 +211,8 @@ module.exports = {
                 */
                dataset: function(chart, e, options) {
                        const position = getRelativePosition(e, chart);
-                       const distanceMetric = getDistanceMetricForAxis(options.axis || 'xy');
-                       let items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false, distanceMetric);
+                       const axis = options.axis || 'xy';
+                       let items = options.intersect ? getIntersectItems(chart, position, axis) : getNearestItems(chart, position, axis);
 
                        if (items.length > 0) {
                                items = [{datasetIndex: items[0].datasetIndex}]; // when mode: 'dataset' we only need to return datasetIndex
@@ -181,11 +227,13 @@ module.exports = {
                 * @function Chart.Interaction.modes.intersect
                 * @param {Chart} chart - the chart we are returning items from
                 * @param {Event} e - the event we are find things at
+                * @param {IInteractionOptions} options - options to use
                 * @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
                 */
-               point: function(chart, e) {
+               point: function(chart, e, options) {
                        const position = getRelativePosition(e, chart);
-                       return getIntersectItems(chart, position);
+                       const axis = options.axis || 'xy';
+                       return getIntersectItems(chart, position, axis);
                },
 
                /**
@@ -198,8 +246,8 @@ module.exports = {
                 */
                nearest: function(chart, e, options) {
                        const position = getRelativePosition(e, chart);
-                       const distanceMetric = getDistanceMetricForAxis(options.axis || 'xy');
-                       return getNearestItems(chart, position, options.intersect, distanceMetric);
+                       const axis = options.axis || 'xy';
+                       return getNearestItems(chart, position, axis, options.intersect);
                },
 
                /**
@@ -215,7 +263,7 @@ module.exports = {
                        const items = [];
                        let intersectsItem = false;
 
-                       parseVisibleItems(chart, function(element, datasetIndex, index) {
+                       evaluateAllVisibleItems(chart, function(element, datasetIndex, index) {
                                if (element.inXRange(position.x)) {
                                        items.push({element, datasetIndex, index});
                                }
@@ -246,7 +294,7 @@ module.exports = {
                        const items = [];
                        let intersectsItem = false;
 
-                       parseVisibleItems(chart, function(element, datasetIndex, index) {
+                       evaluateAllVisibleItems(chart, function(element, datasetIndex, index) {
                                if (element.inYRange(position.y)) {
                                        items.push({element, datasetIndex, index});
                                }
index 3ad274833f5d0c1caa419973cff1c3baf11eb2ea..4741bcfcb2607e3097670c5900cac95794137261 100644 (file)
@@ -637,6 +637,7 @@ class TimeScale extends Scale {
                        : determineUnitForFormatting(me, ticks.length, timeOpts.minUnit, me.min, me.max));
                me._majorUnit = !tickOpts.major.enabled || me._unit === 'year' ? undefined
                        : determineMajorUnit(me._unit);
+               me._numIndices = ticks.length;
                me._table = buildLookupTable(getTimestampsForTable(me), min, max, distribution);
                me._offsets = computeOffsets(me._table, ticks, min, max, options);
 
@@ -716,6 +717,15 @@ class TimeScale extends Scale {
                return interpolate(me._table, 'pos', pos, 'time');
        }
 
+       getIndexForPixel(pixel) {
+               const me = this;
+               if (me.options.distribution !== 'series') {
+                       return null; // not implemented
+               }
+               const index = Math.round(me._numIndices * me.getDecimalForPixel(pixel));
+               return index < 0 || index >= me.numIndices ? null : index;
+       }
+
        /**
         * @private
         */
index d640e866572e41b114799a1b0e24523c5bc56e58..86eabf44e858fc629b2e044e85b835a8b695bcf1 100644 (file)
@@ -37,7 +37,7 @@ describe('Core.Interaction', function() {
                                y: point._model.y,
                        };
 
-                       var elements = Chart.Interaction.modes.point(chart, evt).map(item => item.element);
+                       var elements = Chart.Interaction.modes.point(chart, evt, {}).map(item => item.element);
                        expect(elements).toEqual([point, meta1.data[1]]);
                });
 
@@ -51,7 +51,7 @@ describe('Core.Interaction', function() {
                                y: 0
                        };
 
-                       var elements = Chart.Interaction.modes.point(chart, evt).map(item => item.element);
+                       var elements = Chart.Interaction.modes.point(chart, evt, {}).map(item => item.element);
                        expect(elements).toEqual([]);
                });
        });