mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
Optimize tooltip event handler (#6827)
* Optimize tooltip event handler * Address review comments * Additional cleanup
This commit is contained in:
parent
3093562b33
commit
e25936648c
@ -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;
|
||||
|
||||
@ -40,52 +40,36 @@ function parseVisibleItems(chart, handler) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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 getIntersectItems(chart, position) {
|
||||
var elements = [];
|
||||
|
||||
parseVisibleItems(chart, function(element, datasetIndex, index) {
|
||||
if (element.inRange(position.x, position.y)) {
|
||||
elements.push({element, datasetIndex, index});
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {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 = [];
|
||||
|
||||
parseVisibleItems(chart, function(element, datasetIndex, index) {
|
||||
if (intersect && !element.inRange(position.x, position.y)) {
|
||||
return;
|
||||
const index = iScale.getIndexForPixel(position[axis]);
|
||||
if (!helpers.isNumber(index)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var center = element.getCenterPoint();
|
||||
var distance = distanceMetric(position, center);
|
||||
if (distance < minDistance) {
|
||||
nearestItems = [{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});
|
||||
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 nearestItems;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,16 +78,78 @@ function getNearestItems(chart, position, intersect, distanceMetric) {
|
||||
* @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 useX = axis.indexOf('x') !== -1;
|
||||
const useY = axis.indexOf('y') !== -1;
|
||||
|
||||
return function(pt1, pt2) {
|
||||
var deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0;
|
||||
var deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0;
|
||||
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, axis) {
|
||||
const items = [];
|
||||
|
||||
const evaluationFunc = function(element, datasetIndex, index) {
|
||||
if (element.inRange(position.x, position.y)) {
|
||||
items.push({element, datasetIndex, index});
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
* @return {ChartElement[]} the nearest items
|
||||
*/
|
||||
function getNearestItems(chart, position, axis, intersect) {
|
||||
const distanceMetric = getDistanceMetricForAxis(axis);
|
||||
let minDistance = Number.POSITIVE_INFINITY;
|
||||
let items = [];
|
||||
|
||||
const evaluationFunc = function(element, datasetIndex, index) {
|
||||
if (intersect && !element.inRange(position.x, position.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const center = element.getCenterPoint();
|
||||
const distance = distanceMetric(position, center);
|
||||
if (distance < minDistance) {
|
||||
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
|
||||
items.push({element, datasetIndex, index});
|
||||
}
|
||||
};
|
||||
|
||||
const optimized = evaluateItemsAtIndex(chart, axis, position, evaluationFunc);
|
||||
if (optimized) {
|
||||
return items;
|
||||
}
|
||||
|
||||
evaluateAllVisibleItems(chart, evaluationFunc);
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface IInteractionOptions
|
||||
*/
|
||||
@ -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});
|
||||
}
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user