From ebcaff15c2bd8aa74b1d8c1d082aee783f700f1b Mon Sep 17 00:00:00 2001 From: Yiwen Wang Date: Wed, 25 May 2022 18:25:27 +0800 Subject: [PATCH] Add option to include invisible points (#10362) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * 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 🌊 Co-authored-by: Jacco van den Berg <39033624+LeeLenaleee@users.noreply.github.com> --- docs/configuration/interactions.md | 1 + src/core/core.defaults.js | 3 +- src/core/core.interaction.js | 37 +++++++++++++++---------- src/platform/platform.dom.js | 2 +- test/specs/core.interaction.tests.js | 41 ++++++++++++++++++++++++++++ types/index.esm.d.ts | 7 +++++ 6 files changed, 74 insertions(+), 17 deletions(-) diff --git a/docs/configuration/interactions.md b/docs/configuration/interactions.md index 2fe77f45c..59a45875c 100644 --- a/docs/configuration/interactions.md +++ b/docs/configuration/interactions.md @@ -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. diff --git a/src/core/core.defaults.js b/src/core/core.defaults.js index 9817e7aba..eb1124fa7 100644 --- a/src/core/core.defaults.js +++ b/src/core/core.defaults.js @@ -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; diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index 4a8df47fa..5424c862a 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -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); }, /** diff --git a/src/platform/platform.dom.js b/src/platform/platform.dom.js index 7e7ae1e33..502886d07 100644 --- a/src/platform/platform.dom.js +++ b/src/platform/platform.dom.js @@ -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; diff --git a/test/specs/core.interaction.tests.js b/test/specs/core.interaction.tests.js index 34ee6438c..bfd95ae35 100644 --- a/test/specs/core.interaction.tests.js +++ b/test/specs/core.interaction.tests.js @@ -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); + }); }); }); diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index e19439e68..9c00dc5a5 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -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 extends ParsingOptions, AnimationOptions { -- 2.47.2