]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Add option to include invisible points (#10362)
authorYiwen Wang <vicey@live.com>
Wed, 25 May 2022 10:25:27 +0000 (18:25 +0800)
committerGitHub <noreply@github.com>
Wed, 25 May 2022 10:25:27 +0000 (13:25 +0300)
* 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>
docs/configuration/interactions.md
src/core/core.defaults.js
src/core/core.interaction.js
src/platform/platform.dom.js
test/specs/core.interaction.tests.js
types/index.esm.d.ts

index 2fe77f45caf97550520dcf3d485aeb7a1c0c2a95..59a45875c6dd1d0053e5e8250fce274e4d130572 100644 (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.
 
index 9817e7aba55ef20936c5de5fd11724a7129d2594..eb1124fa7ae2988cb622ebebe719cec91cc86c0e 100644 (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;
index 4a8df47fa10c6dc60ae2c66fa11afa1fd363a12a..5424c862ae19bb89526966d2c0d5634c5cc1974f 100644 (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);
     },
 
     /**
index 7e7ae1e33c921668633521835a3dfb37a7f77ba9..502886d07b3d9186cba0d5254ef644c33ab50f46 100644 (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;
index 34ee6438c2794461710ab5779f0298d4c1360a64..bfd95ae352e6797b627b1a903353545dfb13b12d 100644 (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);
+    });
   });
 });
index e19439e687bebbf4b08edcaac5708dd345f3f3dd..9c00dc5a5982e3477b1d6568d02835f84bbe1ee7 100644 (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> {