]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Interaction functions (#10046)
authorJosh Kelley <joshkel@gmail.com>
Thu, 24 Mar 2022 13:02:30 +0000 (09:02 -0400)
committerGitHub <noreply@github.com>
Thu, 24 Mar 2022 13:02:30 +0000 (09:02 -0400)
* Refactor get...Items functions to take events rather than positions

To work toward exposing something like the get...Items functions.

* Switch getAxisItems to use optimizedEvaluateItems

optimizedEvaluateItems falls back to evaluating all items for unsorted items, and sorting / optimizing ought to be okay, so this ought to be equivalent.

* Performance

* Consolidate getRelativePosition

helpers.dom.js's getRelativePosition already had logic to handle ChartEvent vs. Event (as demonstrated by the `native` check within `getCanvasPosition`), so it's redundant for core.interaction.js to have its own `native` check.

Update `getRelativePosition` to use the same `native` check as core.interaction.js's version.  As best as I can tell, the ChartEvent's x and y are populated from `getRelativePosition`, so the previous `getCanvasPosition` was effectively just duplicating `getRelativePosition'`s work.  I added a test to verify this; it depends on a local, not-yet-submitted change in chartjs-test-utils' `triggerMouseEvent` to return the mouse event that it triggers.

* Add an API to refactor duplicate isPointInArea

* Rename and update JSDoc to prepare for making this public

* Give functions a consistent, generic interface

* Export functions for discussion

* Code review feedback

Add a non-null assertion, as requested in code review.

Add JSDoc to clarify that `getCanvasPosition` now expects a native `Event`, not a `ChartEvent`.  Add `@ts-ignore`; `getCanvasPosition` relied on some loose conversions between `Event`, `TouchEvent`, and `Touch` that would require several changes to make TypeScript happy.

* Code review feedback

Return the event directly, to speed up the code a bit.  Add JSDoc to help communicate its intent.  Update various comments.

* Finalize exports; add docs and TypeScript

* Update src/helpers/helpers.dom.js

* Update src/helpers/helpers.dom.js

Only thing needed actually is the update of chartjs-test-utils to 0.4.0

* Bump chartjs-test-utils dependency

To get supporting work from https://github.com/chartjs/chartjs-test-utils/pull/19

Co-authored-by: Jukka Kurkela <jukka.kurkela@gmail.com>
docs/configuration/interactions.md
package-lock.json
package.json
src/core/core.controller.js
src/core/core.interaction.js
src/helpers/helpers.canvas.js
src/helpers/helpers.dom.js
test/specs/helpers.dom.tests.js
types/index.esm.d.ts

index abb111b54d4a368fa57196aa2f7d61962cd9b748..2fe77f45caf97550520dcf3d485aeb7a1c0c2a95 100644 (file)
@@ -220,3 +220,56 @@ const chart = new Chart(ctx, {
     }
 });
 ```
+
+## Custom Interaction Modes
+
+New modes can be defined by adding functions to the `Chart.Interaction.modes` map.  You can use the `Chart.Interaction.evaluateInteractionItems` function to help implement these.
+
+Example:
+
+```javascript
+import { Interaction } from 'chart.js';
+import { getRelativePosition } from 'chart.js/helpers';
+
+/**
+ * Custom interaction mode
+ * @function Interaction.modes.myCustomMode
+ * @param {Chart} chart - the chart we are returning items from
+ * @param {Event} e - the event we are find things at
+ * @param {InteractionOptions} options - options to use
+ * @param {boolean} [useFinalPosition] - use final element position (animation target)
+ * @return {InteractionItem[]} - items that are found
+ */
+Interaction.modes.myCustomMode = function(chart, e, options, useFinalPosition) {
+  const position = getRelativePosition(e, chart);
+
+  const items = [];
+  Interaction.evaluateInteractionItems(chart, 'x', position, (element, datasetIndex, index) => {
+    if (element.inXRange(position.x, useFinalPosition) && myCustomLogic(element)) {
+      items.push({element, datasetIndex, index});
+    }
+  });
+  return items;
+};
+
+// Then, to use it...
+new Chart.js(ctx, {
+    type: 'line',
+    data: data,
+    options: {
+        interaction: {
+            mode: 'myCustomMode'
+        }
+    }
+})
+```
+
+If you're using TypeScript, you'll also need to register the new mode:
+
+```typescript
+declare module 'chart.js' {
+  interface InteractionModeMap {
+    myCustomMode: InteractionModeFunction;
+  }
+}
+```
\ No newline at end of file
index 3aed0d3a97de1dbea1b23543b9320f81c4d46273..e502546aceaca6f661cf97d6f01bfc3b3afae38b 100644 (file)
@@ -22,7 +22,7 @@
                 "@vuepress/plugin-html-redirect": "^0.1.2",
                 "chartjs-adapter-luxon": "^1.0.0",
                 "chartjs-adapter-moment": "^1.0.0",
-                "chartjs-test-utils": "^0.3.1",
+                "chartjs-test-utils": "^0.4.0",
                 "concurrently": "^6.0.1",
                 "coveralls": "^3.1.0",
                 "cross-env": "^7.0.3",
             }
         },
         "node_modules/chartjs-test-utils": {
-            "version": "0.3.1",
-            "resolved": "https://registry.npmjs.org/chartjs-test-utils/-/chartjs-test-utils-0.3.1.tgz",
-            "integrity": "sha512-QsRYLWOedYGsloDvJsByPNUK44TOiqnxQEO5FOrOm9SguEl5WmJDCOIdd/1ePLOX4gGRClXBDVxD7o1SJY+nWA==",
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/chartjs-test-utils/-/chartjs-test-utils-0.4.0.tgz",
+            "integrity": "sha512-hT7weEZeWDVduSflHMpoNYW4arxVNp3+u7iZW91P6+zTYLHqgtv1gB/K0wiMqForXvw7IsDWuMF2iEvh3WT1mg==",
             "dev": true,
             "dependencies": {
+                "pixelmatch": "^5.2.1"
+            },
+            "peerDependencies": {
                 "jasmine": "^3.6.4",
                 "karma": "^6.1.1",
-                "karma-jasmine": "^4.0.1",
-                "pixelmatch": "^5.2.1"
+                "karma-jasmine": "^4.0.1"
             }
         },
         "node_modules/chokidar": {
             "requires": {}
         },
         "chartjs-test-utils": {
-            "version": "0.3.1",
-            "resolved": "https://registry.npmjs.org/chartjs-test-utils/-/chartjs-test-utils-0.3.1.tgz",
-            "integrity": "sha512-QsRYLWOedYGsloDvJsByPNUK44TOiqnxQEO5FOrOm9SguEl5WmJDCOIdd/1ePLOX4gGRClXBDVxD7o1SJY+nWA==",
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/chartjs-test-utils/-/chartjs-test-utils-0.4.0.tgz",
+            "integrity": "sha512-hT7weEZeWDVduSflHMpoNYW4arxVNp3+u7iZW91P6+zTYLHqgtv1gB/K0wiMqForXvw7IsDWuMF2iEvh3WT1mg==",
             "dev": true,
             "requires": {
-                "jasmine": "^3.6.4",
-                "karma": "^6.1.1",
-                "karma-jasmine": "^4.0.1",
                 "pixelmatch": "^5.2.1"
             }
         },
index 3861f0495b55b1fae176ae2de377c37da6941794..6dcee6ced44ea72340280d5e3fe6f961981db438 100644 (file)
@@ -62,7 +62,7 @@
         "@vuepress/plugin-html-redirect": "^0.1.2",
         "chartjs-adapter-luxon": "^1.0.0",
         "chartjs-adapter-moment": "^1.0.0",
-        "chartjs-test-utils": "^0.3.1",
+        "chartjs-test-utils": "^0.4.0",
         "concurrently": "^6.0.1",
         "coveralls": "^3.1.0",
         "cross-env": "^7.0.3",
index 6b0bd2f71da0567bbe846a4f2f285897f8a49241..17923d90abad35544e4b478bfec0033575ddfa0f 100644 (file)
@@ -15,6 +15,7 @@ import {debounce} from '../helpers/helpers.extras';
 
 /**
  * @typedef { import('../../types/index.esm').ChartEvent } ChartEvent
+ * @typedef { import("../../types/index.esm").Point } Point
  */
 
 const KNOWN_POSITIONS = ['top', 'bottom', 'left', 'right', 'chartArea'];
@@ -792,6 +793,15 @@ class Chart {
     this.notifyPlugins('afterDatasetDraw', args);
   }
 
+  /**
+   * Checks whether the given point is in the chart area.
+   * @param {Point} point - in relative coordinates (see, e.g., getRelativePosition)
+   * @returns {boolean}
+   */
+  isPointInArea(point) {
+    return _isPointInArea(point, this.chartArea, this._minPadding);
+  }
+
   getElementsAtEventForMode(e, mode, options, useFinalPosition) {
     const method = Interaction.modes[mode];
     if (typeof method === 'function') {
@@ -1134,7 +1144,7 @@ class Chart {
       event: e,
       replay,
       cancelable: true,
-      inChartArea: _isPointInArea(e, this.chartArea, this._minPadding)
+      inChartArea: this.isPointInArea(e)
     };
     const eventFilter = (plugin) => (plugin.options.events || this.options.events).includes(e.native.type);
 
index 662703d7f7b406df20bee52c3c1a05e182291009..2e18acc9d7eea2499073ada6c9073aa0eb4a3101 100644 (file)
@@ -1,6 +1,5 @@
-import {_isPointInArea} from '../helpers/helpers.canvas';
 import {_lookupByKey, _rlookupByKey} from '../helpers/helpers.collection';
-import {getRelativePosition as helpersGetRelativePosition} from '../helpers/helpers.dom';
+import {getRelativePosition} from '../helpers/helpers.dom';
 import {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math';
 
 /**
@@ -8,45 +7,9 @@ import {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math';
  * @typedef { import("../../types/index.esm").ChartEvent } ChartEvent
  * @typedef {{axis?: string, intersect?: boolean}} InteractionOptions
  * @typedef {{datasetIndex: number, index: number, element: import("./core.element").default}} InteractionItem
+ * @typedef { import("../../types/index.esm").Point } Point
  */
 
-/**
- * Helper function to get relative position for an event
- * @param {Event|ChartEvent} e - The event to get the position for
- * @param {Chart} chart - The chart
- * @returns {object} the event position
- */
-function getRelativePosition(e, chart) {
-  if ('native' in e) {
-    return {
-      x: e.x,
-      y: e.y
-    };
-  }
-
-  return helpersGetRelativePosition(e, chart);
-}
-
-/**
- * Helper function to traverse all of the visible elements in the chart
- * @param {Chart} chart - the chart
- * @param {function} handler - the callback to execute for each visible item
- */
-function evaluateAllVisibleItems(chart, handler) {
-  const metasets = chart.getSortedVisibleDatasetMetas();
-  let index, data, element;
-
-  for (let i = 0, ilen = metasets.length; i < ilen; ++i) {
-    ({index, data} = metasets[i]);
-    for (let j = 0, jlen = data.length; j < jlen; ++j) {
-      element = data[j];
-      if (!element.skip) {
-        handler(element, index, j);
-      }
-    }
-  }
-}
-
 /**
  * Helper function to do binary search when possible
  * @param {object} metaset - the dataset meta
@@ -80,14 +43,14 @@ function binarySearch(metaset, axis, value, intersect) {
 }
 
 /**
- * Helper function to get items using binary search, when the data is sorted.
+ * Helper function to select candidate elements for interaction
  * @param {Chart} chart - the chart
  * @param {string} axis - the axis mode. x|y|xy|r
- * @param {object} position - the point to be nearest to
+ * @param {Point} position - the point to be nearest to, in relative coordinates
  * @param {function} handler - the callback to execute for each visible item
  * @param {boolean} [intersect] - consider intersecting items
  */
-function optimizedEvaluateItems(chart, axis, position, handler, intersect) {
+function evaluateInteractionItems(chart, axis, position, handler, intersect) {
   const metasets = chart.getSortedVisibleDatasetMetas();
   const value = position[axis];
   for (let i = 0, ilen = metasets.length; i < ilen; ++i) {
@@ -121,7 +84,7 @@ function getDistanceMetricForAxis(axis) {
 /**
  * Helper function to get the items that intersect the event position
  * @param {Chart} chart - the chart
- * @param {object} position - the point to be nearest to
+ * @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
  * @return {InteractionItem[]} the nearest items
@@ -129,7 +92,7 @@ function getDistanceMetricForAxis(axis) {
 function getIntersectItems(chart, position, axis, useFinalPosition) {
   const items = [];
 
-  if (!_isPointInArea(position, chart.chartArea, chart._minPadding)) {
+  if (!chart.isPointInArea(position)) {
     return items;
   }
 
@@ -139,16 +102,16 @@ function getIntersectItems(chart, position, axis, useFinalPosition) {
     }
   };
 
-  optimizedEvaluateItems(chart, axis, position, evaluationFunc, true);
+  evaluateInteractionItems(chart, axis, position, evaluationFunc, true);
   return items;
 }
 
 /**
  * Helper function to get the items nearest to the event position for a radial chart
  * @param {Chart} chart - the chart to look at elements from
- * @param {object} position - the point to be nearest to
+ * @param {Point} position - the point to be nearest to, in relative coordinates
  * @param {string} axis - the axes along which to measure distance
- * @param {boolean} [useFinalPosition] - use the elements animation target instead of current position
+ * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position
  * @return {InteractionItem[]} the nearest items
  */
 function getNearestRadialItems(chart, position, axis, useFinalPosition) {
@@ -163,17 +126,17 @@ function getNearestRadialItems(chart, position, axis, useFinalPosition) {
     }
   }
 
-  optimizedEvaluateItems(chart, axis, position, evaluationFunc);
+  evaluateInteractionItems(chart, axis, position, evaluationFunc);
   return items;
 }
 
 /**
  * Helper function to get the items nearest to the event position for a cartesian chart
  * @param {Chart} chart - the chart to look at elements from
- * @param {object} position - the point to be nearest to
+ * @param {Point} position - the point to be nearest to, in relative coordinates
  * @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 elements animation target instead of current position
+ * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position
  * @return {InteractionItem[]} the nearest items
  */
 function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition) {
@@ -188,7 +151,7 @@ function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosi
     }
 
     const center = element.getCenterPoint(useFinalPosition);
-    const pointInArea = _isPointInArea(center, chart.chartArea, chart._minPadding);
+    const pointInArea = chart.isPointInArea(center);
     if (!pointInArea && !inRange) {
       return;
     }
@@ -203,21 +166,21 @@ function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosi
     }
   }
 
-  optimizedEvaluateItems(chart, axis, position, evaluationFunc);
+  evaluateInteractionItems(chart, axis, position, evaluationFunc);
   return items;
 }
 
 /**
  * Helper function to get the items nearest to the event position considering all visible items in the chart
  * @param {Chart} chart - the chart to look at elements from
- * @param {object} position - the point to be nearest to
+ * @param {Point} position - the point to be nearest to, in relative coordinates
  * @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 elements animation target instead of current position
+ * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position
  * @return {InteractionItem[]} the nearest items
  */
 function getNearestItems(chart, position, axis, intersect, useFinalPosition) {
-  if (!_isPointInArea(position, chart.chartArea, chart._minPadding)) {
+  if (!chart.isPointInArea(position)) {
     return [];
   }
 
@@ -226,26 +189,30 @@ function getNearestItems(chart, position, axis, intersect, useFinalPosition) {
     : getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition);
 }
 
-function getAxisItems(chart, e, options, useFinalPosition) {
-  const position = getRelativePosition(e, chart);
+/**
+ * Helper function to get the items matching along the given X or Y axis
+ * @param {Chart} chart - the chart to look at elements from
+ * @param {Point} position - the point to be nearest to, in relative coordinates
+ * @param {string} axis - the axis to match
+ * @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
+ * @return {InteractionItem[]} the nearest items
+ */
+function getAxisItems(chart, position, axis, intersect, useFinalPosition) {
   const items = [];
-  const axis = options.axis;
   const rangeMethod = axis === 'x' ? 'inXRange' : 'inYRange';
   let intersectsItem = false;
 
-  evaluateAllVisibleItems(chart, (element, datasetIndex, index) => {
+  evaluateInteractionItems(chart, axis, position, (element, datasetIndex, index) => {
     if (element[rangeMethod](position[axis], useFinalPosition)) {
       items.push({element, datasetIndex, index});
-    }
-
-    if (element.inRange(position.x, position.y, useFinalPosition)) {
-      intersectsItem = true;
+      intersectsItem = intersectsItem || element.inRange(position.x, position.y, useFinalPosition);
     }
   });
 
   // If we want to trigger on an intersect and we don't have any items
   // that intersect the position, return nothing
-  if (options.intersect && !intersectsItem) {
+  if (intersect && !intersectsItem) {
     return [];
   }
   return items;
@@ -256,6 +223,9 @@ function getAxisItems(chart, e, options, useFinalPosition) {
  * @namespace Chart.Interaction
  */
 export default {
+  // Part of the public API to facilitate developers creating their own modes
+  evaluateInteractionItems,
+
   // Helper function for different modes
   modes: {
     /**
@@ -365,7 +335,8 @@ export default {
                 * @return {InteractionItem[]} - items that are found
                 */
     x(chart, e, options, useFinalPosition) {
-      return getAxisItems(chart, e, {axis: 'x', intersect: options.intersect}, useFinalPosition);
+      const position = getRelativePosition(e, chart);
+      return getAxisItems(chart, position, 'x', options.intersect, useFinalPosition);
     },
 
     /**
@@ -378,7 +349,8 @@ export default {
                 * @return {InteractionItem[]} - items that are found
                 */
     y(chart, e, options, useFinalPosition) {
-      return getAxisItems(chart, e, {axis: 'y', intersect: options.intersect}, useFinalPosition);
+      const position = getRelativePosition(e, chart);
+      return getAxisItems(chart, position, 'y', options.intersect, useFinalPosition);
     }
   }
 };
index d6290f2ec0c8e09ffccaf1c9775d63df2f164c2f..54ce2cd54f6a1a39c863fe080d0df978bba16f02 100644 (file)
@@ -2,7 +2,11 @@ import {isArray, isNullOrUndef} from './helpers.core';
 import {PI, TAU, HALF_PI, QUARTER_PI, TWO_THIRDS_PI, RAD_PER_DEG} from './helpers.math';
 
 /**
- * @typedef { import("../core/core.controller").default } Chart
+ * Note: typedefs are auto-exported, so use a made-up `canvas` namespace where
+ * necessary to avoid duplicates with `export * from './helpers`; see
+ * https://github.com/microsoft/TypeScript/issues/46011
+ * @typedef { import("../core/core.controller").default } canvas.Chart
+ * @typedef { import("../../types/index.esm").Point } Point
  */
 
 /**
@@ -94,7 +98,7 @@ export function _longestText(ctx, font, arrayOfThings, cache) {
 
 /**
  * Returns the aligned pixel value to avoid anti-aliasing blur
- * @param {Chart} chart - The chart instance.
+ * @param {canvas.Chart} chart - The chart instance.
  * @param {number} pixel - A pixel value.
  * @param {number} width - The width of the element.
  * @returns {number} The aligned pixel value.
@@ -242,7 +246,7 @@ export function drawPoint(ctx, options, x, y) {
 
 /**
  * Returns true if the point is inside the rectangle
- * @param {object} point - The point to test
+ * @param {Point} point - The point to test
  * @param {object} area - The rectangle
  * @param {number} [margin] - allowed margin
  * @returns {boolean}
index 8fe1f3f9b2b19b9a6c404298dc348ad5be83b3df..c4f54e0093677b9ec87ddb449afa35aa6dfad3bf 100644 (file)
@@ -1,5 +1,13 @@
 import {INFINITY} from './helpers.math';
 
+/**
+ * Note: typedefs are auto-exported, so use a made-up `dom` namespace where
+ * necessary to avoid duplicates with `export * from './helpers`; see
+ * https://github.com/microsoft/TypeScript/issues/46011
+ * @typedef { import("../core/core.controller").default } dom.Chart
+ * @typedef { import('../../types/index.esm').ChartEvent } ChartEvent
+ */
+
 /**
  * @private
  */
@@ -59,8 +67,13 @@ function getPositionedStyle(styles, style, suffix) {
 
 const useOffsetPos = (x, y, target) => (x > 0 || y > 0) && (!target || !target.shadowRoot);
 
-function getCanvasPosition(evt, canvas) {
-  const e = evt.native || evt;
+/**
+ * @param {Event} e
+ * @param {HTMLCanvasElement} canvas
+ * @returns {{x: number, y: number, box: boolean}}
+ */
+function getCanvasPosition(e, canvas) {
+  // @ts-ignore
   const touches = e.touches;
   const source = touches && touches.length ? touches[0] : e;
   const {offsetX, offsetY} = source;
@@ -78,7 +91,17 @@ function getCanvasPosition(evt, canvas) {
   return {x, y, box};
 }
 
+/**
+ * Gets an event's x, y coordinates, relative to the chart area
+ * @param {Event|ChartEvent} evt
+ * @param {dom.Chart} chart
+ * @returns {{x: number, y: number}}
+ */
 export function getRelativePosition(evt, chart) {
+  if ('native' in evt) {
+    return evt;
+  }
+
   const {canvas, currentDevicePixelRatio} = chart;
   const style = getComputedStyle(canvas);
   const borderBox = style.boxSizing === 'border-box';
index fd0d0de14c781c1f9c18bf34e3342bc626c939b4..24dbe81b68aa959bd05bf27507bb208672159d86 100644 (file)
@@ -427,5 +427,30 @@ describe('DOM helpers tests', function() {
       expect(dataX).not.toEqual(NaN);
       expect(dataY).not.toEqual(NaN);
     });
+
+    it('Should give consistent results for native and chart events', async function() {
+      let chartPosition = null;
+      const chart = window.acquireChart(
+        {
+          type: 'bar',
+          data: {
+            datasets: [{
+              data: [{x: 'first', y: 10}, {x: 'second', y: 5}, {x: 'third', y: 15}]
+            }]
+          },
+          options: {
+            onHover: (chartEvent) => {
+              chartPosition = Chart.helpers.getRelativePosition(chartEvent, chart);
+            }
+          }
+        });
+
+      const point = chart.getDatasetMeta(0).data[1];
+      const nativeEvent = await jasmine.triggerMouseEvent(chart, 'mousemove', point);
+      const nativePosition = Chart.helpers.getRelativePosition(nativeEvent, chart);
+
+      expect(chartPosition).not.toBeNull();
+      expect(nativePosition).toEqual({x: chartPosition.x, y: chartPosition.y});
+    });
   });
 });
index 29cdd0cf4c8d14c5deaf03cb2a98b384494b6d77..d4ea9145453a5b70bf6cbd070342755921197616 100644 (file)
@@ -514,6 +514,7 @@ export declare class Chart<
   render(): void;
   draw(): void;
 
+  isPointInArea(point: Point): boolean;
   getElementsAtEventForMode(e: Event, mode: string, options: InteractionOptions, useFinalPosition: boolean): InteractionItem[];
 
   getSortedVisibleDatasetMetas(): ChartMeta[];
@@ -750,6 +751,17 @@ export type InteractionMode = keyof InteractionModeMap;
 
 export const Interaction: {
   modes: InteractionModeMap;
+
+  /**
+   * Helper function to select candidate elements for interaction
+   */
+  evaluateInteractionItems(
+    chart: Chart,
+    axis: InteractionAxis,
+    position: Point,
+    handler: (element: Element & VisualElement, datasetIndex: number, index: number) => void,
+    intersect?: boolean
+  ): InteractionItem[];
 };
 
 export const layouts: {
@@ -1404,6 +1416,8 @@ export interface ChartComponent {
   afterUnregister?(): void;
 }
 
+export type InteractionAxis = 'x' | 'y' | 'xy' | 'r';
+
 export interface CoreInteractionOptions {
   /**
    * Sets which elements appear in the tooltip. See Interaction Modes for details.
@@ -1417,9 +1431,9 @@ export interface CoreInteractionOptions {
   intersect: boolean;
 
   /**
-   * 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.
+   * Defines which directions are used in calculating distances. Defaults to 'x' for 'index' mode and 'xy' in dataset and 'nearest' modes.
    */
-  axis: 'x' | 'y' | 'xy' | 'r';
+  axis: InteractionAxis;
 }
 
 export interface CoreChartOptions<TType extends ChartType> extends ParsingOptions, AnimationOptions<TType> {