Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to include invisible points #10362

Merged
merged 8 commits into from May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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