From e25936648cf826aca19436ebc0e46331eaa16c6d Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 15 Dec 2019 05:26:17 -0800 Subject: [PATCH] Optimize tooltip event handler (#6827) * Optimize tooltip event handler * Address review comments * Additional cleanup --- src/core/core.interaction.js | 134 ++++++++++++++++++--------- src/scales/scale.time.js | 10 ++ test/specs/core.interaction.tests.js | 4 +- 3 files changed, 103 insertions(+), 45 deletions(-) diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index ad1ec4717..d829b006b 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -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}); } diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 3ad274833..4741bcfcb 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -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 */ diff --git a/test/specs/core.interaction.tests.js b/test/specs/core.interaction.tests.js index d640e8665..86eabf44e 100644 --- a/test/specs/core.interaction.tests.js +++ b/test/specs/core.interaction.tests.js @@ -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([]); }); }); -- 2.47.2