Skip to content

Commit

Permalink
Add option to include invisible points (#10362)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed May 25, 2022
1 parent cf780a5 commit ebcaff1
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 17 deletions.
1 change: 1 addition & 0 deletions docs/configuration/interactions.md
Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion src/core/core.defaults.js
Expand Up @@ -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;
Expand Down
37 changes: 22 additions & 15 deletions src/core/core.interaction.js
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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);
},

/**
Expand All @@ -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);
},

/**
Expand Down
2 changes: 1 addition & 1 deletion src/platform/platform.dom.js
Expand Up @@ -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;
Expand Down
41 changes: 41 additions & 0 deletions test/specs/core.interaction.tests.js
Expand Up @@ -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);
});
});
});
7 changes: 7 additions & 0 deletions types/index.esm.d.ts
Expand Up @@ -701,6 +701,7 @@ export const defaults: Defaults;
export interface InteractionOptions {
axis?: string;
intersect?: boolean;
includeInvisible?: boolean;
}

export interface InteractionItem {
Expand Down Expand Up @@ -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> {
Expand Down

0 comments on commit ebcaff1

Please sign in to comment.