import {_isPointInArea} from '../helpers/helpers.canvas';
import {_lookupByKey, _rlookupByKey} from '../helpers/helpers.collection';
import {getRelativePosition as helpersGetRelativePosition} from '../helpers/helpers.dom';
+import {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math';
/**
* @typedef { import("./core.controller").default } Chart
/**
* Helper function to do binary search when possible
* @param {object} metaset - the dataset meta
- * @param {string} axis - the axis mide. x|y|xy
+ * @param {string} axis - the axis mode. x|y|xy|r
* @param {number} value - the value to find
* @param {boolean} [intersect] - should the element intersect
* @returns {{lo:number, hi:number}} indices to search data array between
function binarySearch(metaset, axis, value, intersect) {
const {controller, data, _sorted} = metaset;
const iScale = controller._cachedMeta.iScale;
- if (iScale && axis === iScale.axis && _sorted && data.length) {
+ if (iScale && axis === iScale.axis && axis !== 'r' && _sorted && data.length) {
const lookupMethod = iScale._reversePixels ? _rlookupByKey : _lookupByKey;
if (!intersect) {
return lookupMethod(data, axis, value);
/**
* Helper function to get items using binary search, when the data is sorted.
* @param {Chart} chart - the chart
- * @param {string} axis - the axis mode. x|y|xy
+ * @param {string} axis - the axis mode. x|y|xy|r
* @param {object} position - the point to be nearest to
* @param {function} handler - the callback to execute for each visible item
* @param {boolean} [intersect] - consider intersecting items
/**
* Get a distance metric function for two points based on the
* axis mode setting
- * @param {string} axis - the axis mode. x|y|xy
+ * @param {string} axis - the axis mode. x|y|xy|r
*/
function getDistanceMetricForAxis(axis) {
const useX = axis.indexOf('x') !== -1;
* Helper function to get the items that intersect the event position
* @param {Chart} chart - the chart
* @param {object} position - the point to be nearest to
- * @param {string} axis - the axis mode. x|y|xy
+ * @param {string} axis - the axis mode. x|y|xy|r
* @param {boolean} [useFinalPosition] - use the element's animation target instead of current position
* @return {InteractionItem[]} the nearest items
*/
}
/**
- * Helper function to get the items nearest to the event position considering all visible items in the chart
+ * Helper function to get the items nearest to the event position for a radial chart
* @param {Chart} chart - the chart to look at elements from
* @param {object} position - the point to be nearest to
* @param {string} axis - the axes along which to measure distance
- * @param {boolean} [intersect] - if true, only consider items that intersect the position
* @param {boolean} [useFinalPosition] - use the elements animation target instead of current position
* @return {InteractionItem[]} the nearest items
*/
-function getNearestItems(chart, position, axis, intersect, useFinalPosition) {
- const distanceMetric = getDistanceMetricForAxis(axis);
- let minDistance = Number.POSITIVE_INFINITY;
+function getNearestRadialItems(chart, position, axis, useFinalPosition) {
let items = [];
- if (!_isPointInArea(position, chart.chartArea, chart._minPadding)) {
- return items;
+ function evaluationFunc(element, datasetIndex, index) {
+ const {startAngle, endAngle} = element.getProps(['startAngle', 'endAngle'], useFinalPosition);
+ const {angle} = getAngleFromPoint(element, {x: position.x, y: position.y});
+
+ if (_angleBetween(angle, startAngle, endAngle)) {
+ items.push({element, datasetIndex, index});
+ }
}
- const evaluationFunc = function(element, datasetIndex, index) {
- if (intersect && !element.inRange(position.x, position.y, useFinalPosition)) {
+ optimizedEvaluateItems(chart, axis, position, evaluationFunc);
+ return items;
+}
+
+/**
+ * Helper function to get the items nearest to the event position for a cartesian chart
+ * @param {Chart} chart - the chart to look at elements from
+ * @param {object} position - the point to be nearest to
+ * @param {string} axis - the axes along which to measure distance
+ * @param {boolean} [intersect] - if true, only consider items that intersect the position
+ * @param {boolean} [useFinalPosition] - use the elements animation target instead of current position
+ * @return {InteractionItem[]} the nearest items
+ */
+function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition) {
+ let items = [];
+ const distanceMetric = getDistanceMetricForAxis(axis);
+ let minDistance = Number.POSITIVE_INFINITY;
+
+ function evaluationFunc(element, datasetIndex, index) {
+ const inRange = element.inRange(position.x, position.y, useFinalPosition);
+ if (intersect && !inRange) {
return;
}
const center = element.getCenterPoint(useFinalPosition);
- if (!_isPointInArea(center, chart.chartArea, chart._minPadding) && !element.inRange(position.x, position.y, useFinalPosition)) {
+ const pointInArea = _isPointInArea(center, chart.chartArea, chart._minPadding);
+ if (!pointInArea && !inRange) {
return;
}
+
const distance = distanceMetric(position, center);
if (distance < minDistance) {
items = [{element, datasetIndex, index}];
// Can have multiple items at the same distance in which case we sort by size
items.push({element, datasetIndex, index});
}
- };
+ }
optimizedEvaluateItems(chart, axis, position, 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 {string} axis - the axes along which to measure distance
+ * @param {boolean} [intersect] - if true, only consider items that intersect the position
+ * @param {boolean} [useFinalPosition] - use the elements animation target instead of current position
+ * @return {InteractionItem[]} the nearest items
+ */
+function getNearestItems(chart, position, axis, intersect, useFinalPosition) {
+ if (!_isPointInArea(position, chart.chartArea, chart._minPadding)) {
+ return [];
+ }
+
+ return axis === 'r' && !intersect
+ ? getNearestRadialItems(chart, position, axis, useFinalPosition)
+ : getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition);
+}
+
function getAxisItems(chart, e, options, useFinalPosition) {
const position = getRelativePosition(e, chart);
const items = [];
describe('nearest mode', function() {
describe('intersect: false', function() {
beforeEach(function() {
- this.chart = window.acquireChart({
+ this.lineChart = window.acquireChart({
type: 'line',
data: {
datasets: [{
labels: ['Point 1', 'Point 2', 'Point 3']
}
});
+ this.polarChart = window.acquireChart({
+ type: 'polarArea',
+ data: {
+ datasets: [{
+ data: [1, 9, 5]
+ }],
+ labels: ['Point 1', 'Point 2', 'Point 3']
+ },
+ options: {
+ plugins: {
+ legend: {
+ display: false
+ },
+ },
+ }
+ });
});
describe('axis: xy', function() {
it ('should return the nearest item', function() {
- var chart = this.chart;
+ var chart = this.lineChart;
var evt = {
type: 'click',
chart: chart,
});
it ('should return all items at the same nearest distance', function() {
- var chart = this.chart;
+ var chart = this.lineChart;
var meta0 = chart.getDatasetMeta(0);
var meta1 = chart.getDatasetMeta(1);
describe('axis: x', function() {
it ('should return all items at current x', function() {
- var chart = this.chart;
+ var chart = this.lineChart;
var meta0 = chart.getDatasetMeta(0);
var meta1 = chart.getDatasetMeta(1);
});
it ('should return all items at nearest x-distance', function() {
- var chart = this.chart;
+ var chart = this.lineChart;
var meta0 = chart.getDatasetMeta(0);
var meta1 = chart.getDatasetMeta(1);
describe('axis: y', function() {
it ('should return item with value 30', function() {
- var chart = this.chart;
+ var chart = this.lineChart;
var meta0 = chart.getDatasetMeta(0);
// 'Point 1', y = 30
});
it ('should return all items at value 40', function() {
- var chart = this.chart;
+ var chart = this.lineChart;
var meta0 = chart.getDatasetMeta(0);
var meta1 = chart.getDatasetMeta(1);
expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]);
});
});
+
+ describe('axis: r', function() {
+ it ('should return item with value 9', function() {
+ var chart = this.polarChart;
+ var meta0 = chart.getDatasetMeta(0);
+
+ var evt = {
+ type: 'click',
+ chart: chart,
+ native: true, // Needed, otherwise assumed to be a DOM event
+ x: chart.width / 2,
+ y: chart.height / 2 + 5,
+ };
+
+ var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'r'}).map(item => item.element);
+ expect(elements).toEqual([meta0.data[1]]);
+ });
+
+ it ('should return item with value 1 when clicked outside of it', function() {
+ var chart = this.polarChart;
+ var meta0 = chart.getDatasetMeta(0);
+
+ var evt = {
+ type: 'click',
+ chart: chart,
+ native: true, // Needed, otherwise assumed to be a DOM event
+ x: chart.width,
+ y: 0,
+ };
+
+ var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'r', intersect: false}).map(item => item.element);
+ expect(elements).toEqual([meta0.data[0]]);
+ });
+ });
});
describe('intersect: true', function() {