mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-10 10:16:50 +01:00
Support "r" axis for non-intersecting interaction (#9919)
* Support "r" axis for non-intersecting interaction * Extract some interaction functionality * Remove whitespace and semicolons * WIP: add interaction test * Update documentation * Fix test * Add another test * Update axis params * Add additional axis check to binary search * Update axis type
This commit is contained in:
@@ -6,7 +6,7 @@ Namespace: `options.interaction`, the global interaction configuration is at `Ch
|
||||
| ---- | ---- | ------- | -----------
|
||||
| `mode` | `string` | `'nearest'` | Sets which elements appear in the interaction. See [Interaction Modes](#modes) for details.
|
||||
| `intersect` | `boolean` | `true` | if true, the interaction mode only applies when the mouse position intersects an item on the chart.
|
||||
| `axis` | `string` | `'x'` | Can be set to `'x'`, `'y'`, or `'xy'` to define which directions are used in calculating distances. Defaults to `'x'` for `'index'` mode and `'xy'` in `dataset` and `'nearest'` modes.
|
||||
| `axis` | `string` | `'x'` | Can be set to `'x'`, `'y'`, `'xy'` or `'r'` to define which directions are used in calculating distances. Defaults to `'x'` for `'index'` mode and `'xy'` in `dataset` and `'nearest'` modes.
|
||||
|
||||
By default, these options apply to both the hover and tooltip interactions. The same options can be set in the `options.hover` namespace, in which case they will only affect the hover interaction. Similarly, the options can be set in the `options.plugins.tooltip` namespace to independently configure the tooltip interactions.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -49,7 +50,7 @@ function evaluateAllVisibleItems(chart, handler) {
|
||||
/**
|
||||
* 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
|
||||
@@ -57,7 +58,7 @@ function evaluateAllVisibleItems(chart, handler) {
|
||||
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);
|
||||
@@ -81,7 +82,7 @@ function binarySearch(metaset, axis, value, intersect) {
|
||||
/**
|
||||
* 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
|
||||
@@ -104,7 +105,7 @@ function optimizedEvaluateItems(chart, axis, position, handler, intersect) {
|
||||
/**
|
||||
* 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;
|
||||
@@ -121,7 +122,7 @@ function getDistanceMetricForAxis(axis) {
|
||||
* 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
|
||||
*/
|
||||
@@ -142,6 +143,70 @@ function getIntersectItems(chart, position, axis, useFinalPosition) {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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} [useFinalPosition] - use the elements animation target instead of current position
|
||||
* @return {InteractionItem[]} the nearest items
|
||||
*/
|
||||
function getNearestRadialItems(chart, position, axis, useFinalPosition) {
|
||||
let 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});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const pointInArea = _isPointInArea(center, chart.chartArea, chart._minPadding);
|
||||
if (!pointInArea && !inRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -152,35 +217,13 @@ function getIntersectItems(chart, position, axis, useFinalPosition) {
|
||||
* @return {InteractionItem[]} the nearest items
|
||||
*/
|
||||
function getNearestItems(chart, position, axis, intersect, useFinalPosition) {
|
||||
const distanceMetric = getDistanceMetricForAxis(axis);
|
||||
let minDistance = Number.POSITIVE_INFINITY;
|
||||
let items = [];
|
||||
|
||||
if (!_isPointInArea(position, chart.chartArea, chart._minPadding)) {
|
||||
return items;
|
||||
return [];
|
||||
}
|
||||
|
||||
const evaluationFunc = function(element, datasetIndex, index) {
|
||||
if (intersect && !element.inRange(position.x, position.y, useFinalPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const center = element.getCenterPoint(useFinalPosition);
|
||||
if (!_isPointInArea(center, chart.chartArea, chart._minPadding) && !element.inRange(position.x, position.y, useFinalPosition)) {
|
||||
return;
|
||||
}
|
||||
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});
|
||||
}
|
||||
};
|
||||
|
||||
optimizedEvaluateItems(chart, axis, position, evaluationFunc);
|
||||
return items;
|
||||
return axis === 'r' && !intersect
|
||||
? getNearestRadialItems(chart, position, axis, useFinalPosition)
|
||||
: getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition);
|
||||
}
|
||||
|
||||
function getAxisItems(chart, e, options, useFinalPosition) {
|
||||
|
||||
@@ -325,7 +325,7 @@ describe('Core.Interaction', function() {
|
||||
describe('nearest mode', function() {
|
||||
describe('intersect: false', function() {
|
||||
beforeEach(function() {
|
||||
this.chart = window.acquireChart({
|
||||
this.lineChart = window.acquireChart({
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
@@ -344,11 +344,27 @@ describe('Core.Interaction', function() {
|
||||
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,
|
||||
@@ -364,7 +380,7 @@ describe('Core.Interaction', function() {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -390,7 +406,7 @@ describe('Core.Interaction', function() {
|
||||
|
||||
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);
|
||||
|
||||
@@ -414,7 +430,7 @@ describe('Core.Interaction', function() {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -440,7 +456,7 @@ describe('Core.Interaction', function() {
|
||||
|
||||
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
|
||||
@@ -463,7 +479,7 @@ describe('Core.Interaction', function() {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -486,6 +502,40 @@ describe('Core.Interaction', function() {
|
||||
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() {
|
||||
|
||||
4
types/index.esm.d.ts
vendored
4
types/index.esm.d.ts
vendored
@@ -1391,9 +1391,9 @@ export interface CoreInteractionOptions {
|
||||
intersect: boolean;
|
||||
|
||||
/**
|
||||
* Can be set to 'x', 'y', or 'xy' to define which directions are used in calculating distances. Defaults to 'x' for 'index' mode and 'xy' in dataset and 'nearest' modes.
|
||||
* Can be set to 'x', 'y', 'xy' or 'r' to define which directions are used in calculating distances. Defaults to 'x' for 'index' mode and 'xy' in dataset and 'nearest' modes.
|
||||
*/
|
||||
axis: 'x' | 'y' | 'xy';
|
||||
axis: 'x' | 'y' | 'xy' | 'r';
|
||||
}
|
||||
|
||||
export interface CoreChartOptions<TType extends ChartType> extends ParsingOptions, AnimationOptions<TType> {
|
||||
|
||||
Reference in New Issue
Block a user