Add option to include invisible points (#10362)

* Add option to include invisible points

* Minor fixes

* Add doc for newly added option

* Fix typo

* Add test for newly added option

* Improve description of the new option

* Update docs/configuration/interactions.md

Co-authored-by: Jacco van den Berg <39033624+LeeLenaleee@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Jacco van den Berg <39033624+LeeLenaleee@users.noreply.github.com>

Co-authored-by: Yiwen Wang 🌊 <yiwwan@microsoft.com>
Co-authored-by: Jacco van den Berg <39033624+LeeLenaleee@users.noreply.github.com>
This commit is contained in:
Yiwen Wang 2022-05-25 18:25:27 +08:00 committed by GitHub
parent cf780a5db5
commit ebcaff15c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 17 deletions

View File

@ -7,6 +7,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'`, `'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.
| `includeInvisible` | `boolean` | `true` | if true, the invisible points that are outside of the chart area will also be included when evaluating interactions.
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.

View File

@ -62,7 +62,8 @@ export class Defaults {
this.indexAxis = 'x';
this.interaction = {
mode: 'nearest',
intersect: true
intersect: true,
includeInvisible: false
};
this.maintainAspectRatio = true;
this.onHover = null;

View File

@ -6,7 +6,7 @@ import {_isPointInArea} from '../helpers';
/**
* @typedef { import("./core.controller").default } Chart
* @typedef { import("../../types/index.esm").ChartEvent } ChartEvent
* @typedef {{axis?: string, intersect?: boolean}} InteractionOptions
* @typedef {{axis?: string, intersect?: boolean, includeInvisible?: boolean}} InteractionOptions
* @typedef {{datasetIndex: number, index: number, element: import("./core.element").default}} InteractionItem
* @typedef { import("../../types/index.esm").Point } Point
*/
@ -88,17 +88,18 @@ function getDistanceMetricForAxis(axis) {
* @param {Point} position - the point to be nearest to, in relative coordinates
* @param {string} axis - the axis mode. x|y|xy|r
* @param {boolean} [useFinalPosition] - use the element's animation target instead of current position
* @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area
* @return {InteractionItem[]} the nearest items
*/
function getIntersectItems(chart, position, axis, useFinalPosition) {
function getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) {
const items = [];
if (!chart.isPointInArea(position)) {
if (!includeInvisible && !chart.isPointInArea(position)) {
return items;
}
const evaluationFunc = function(element, datasetIndex, index) {
if (!_isPointInArea(element, chart.chartArea, 0)) {
if (!includeInvisible && !_isPointInArea(element, chart.chartArea, 0)) {
return;
}
if (element.inRange(position.x, position.y, useFinalPosition)) {
@ -141,9 +142,10 @@ function getNearestRadialItems(chart, position, axis, useFinalPosition) {
* @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 element's animation target instead of current position
* @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area
* @return {InteractionItem[]} the nearest items
*/
function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition) {
function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition, includeInvisible) {
let items = [];
const distanceMetric = getDistanceMetricForAxis(axis);
let minDistance = Number.POSITIVE_INFINITY;
@ -155,7 +157,7 @@ function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosi
}
const center = element.getCenterPoint(useFinalPosition);
const pointInArea = chart.isPointInArea(center);
const pointInArea = !!includeInvisible || chart.isPointInArea(center);
if (!pointInArea && !inRange) {
return;
}
@ -181,16 +183,17 @@ function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosi
* @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 element's animation target instead of current position
* @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area
* @return {InteractionItem[]} the nearest items
*/
function getNearestItems(chart, position, axis, intersect, useFinalPosition) {
if (!chart.isPointInArea(position)) {
function getNearestItems(chart, position, axis, intersect, useFinalPosition, includeInvisible) {
if (!includeInvisible && !chart.isPointInArea(position)) {
return [];
}
return axis === 'r' && !intersect
? getNearestRadialItems(chart, position, axis, useFinalPosition)
: getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition);
: getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition, includeInvisible);
}
/**
@ -247,9 +250,10 @@ export default {
const position = getRelativePosition(e, chart);
// Default axis for index mode is 'x' to match old behaviour
const axis = options.axis || 'x';
const includeInvisible = options.includeInvisible || false;
const items = options.intersect
? getIntersectItems(chart, position, axis, useFinalPosition)
: getNearestItems(chart, position, axis, false, useFinalPosition);
? getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible)
: getNearestItems(chart, position, axis, false, useFinalPosition, includeInvisible);
const elements = [];
if (!items.length) {
@ -282,9 +286,10 @@ export default {
dataset(chart, e, options, useFinalPosition) {
const position = getRelativePosition(e, chart);
const axis = options.axis || 'xy';
const includeInvisible = options.includeInvisible || false;
let items = options.intersect
? getIntersectItems(chart, position, axis, useFinalPosition) :
getNearestItems(chart, position, axis, false, useFinalPosition);
? getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) :
getNearestItems(chart, position, axis, false, useFinalPosition, includeInvisible);
if (items.length > 0) {
const datasetIndex = items[0].datasetIndex;
@ -311,7 +316,8 @@ export default {
point(chart, e, options, useFinalPosition) {
const position = getRelativePosition(e, chart);
const axis = options.axis || 'xy';
return getIntersectItems(chart, position, axis, useFinalPosition);
const includeInvisible = options.includeInvisible || false;
return getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible);
},
/**
@ -326,7 +332,8 @@ export default {
nearest(chart, e, options, useFinalPosition) {
const position = getRelativePosition(e, chart);
const axis = options.axis || 'xy';
return getNearestItems(chart, position, axis, options.intersect, useFinalPosition);
const includeInvisible = options.includeInvisible || false;
return getNearestItems(chart, position, axis, options.intersect, useFinalPosition, includeInvisible);
},
/**

View File

@ -211,7 +211,7 @@ function createResizeObserver(chart, type, listener) {
const width = entry.contentRect.width;
const height = entry.contentRect.height;
// When its container's display is set to 'none' the callback will be called with a
// size of (0, 0), which will cause the chart to lost its original height, so skip
// size of (0, 0), which will cause the chart to lose its original height, so skip
// resizing in such case.
if (width === 0 && height === 0) {
return;

View File

@ -870,5 +870,46 @@ describe('Core.Interaction', function() {
const elements = Chart.Interaction.modes.point(chart, evt, {intersect: true}).map(item => item.element);
expect(elements).not.toContain(firstElement);
});
it ('out-of-range datapoints are shown in tooltip if included', function() {
let data = [];
for (let i = 0; i < 1000; i++) {
data.push({x: i, y: i});
}
const chart = window.acquireChart({
type: 'scatter',
data: {
datasets: [{data}]
},
options: {
scales: {
x: {
min: 2
}
}
}
});
const meta0 = chart.getDatasetMeta(0);
const firstElement = meta0.data[0];
const evt = {
type: 'click',
chart: chart,
native: true, // needed otherwise it thinks its a DOM event
x: firstElement.x,
y: firstElement.y
};
const elements = Chart.Interaction.modes.point(
chart,
evt,
{
intersect: true,
includeInvisible: true
}).map(item => item.element);
expect(elements).toContain(firstElement);
});
});
});

View File

@ -701,6 +701,7 @@ export const defaults: Defaults;
export interface InteractionOptions {
axis?: string;
intersect?: boolean;
includeInvisible?: boolean;
}
export interface InteractionItem {
@ -1434,6 +1435,12 @@ export interface CoreInteractionOptions {
* Defines which directions are used in calculating distances. Defaults to 'x' for 'index' mode and 'xy' in dataset and 'nearest' modes.
*/
axis: InteractionAxis;
/**
* if true, the invisible points that are outside of the chart area will also be included when evaluating interactions.
* @default false
*/
includeInvisible: boolean;
}
export interface CoreChartOptions<TType extends ChartType> extends ParsingOptions, AnimationOptions<TType> {