From 456e46b0f60ee1e8441198f1bb997e67c6a250a9 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 19 Sep 2024 09:47:19 +0200 Subject: [PATCH] Add getAnnotations function to plugin and remove _getState private one (#892) * add getAnnotation function * add function for coverage just for interaction * add test case on getannotations * add doc --- docs/.vuepress/config.ts | 1 + docs/guide/developers.md | 33 ++++++++++++++++++++++++++++++ docs/guide/integration.md | 2 +- src/annotation.js | 11 ++++++++-- src/events.js | 5 ++--- src/interaction.js | 38 +++++++++++++++++------------------ test/index.js | 3 ++- test/integration/ts/basic.ts | 2 ++ test/specs/annotation.spec.js | 28 ++++++++++++++++++++++++++ test/specs/box.spec.js | 11 +++++----- test/specs/ellipse.spec.js | 11 +++++----- test/specs/label.spec.js | 11 +++++----- test/specs/line.spec.js | 22 +++++++++++--------- test/specs/point.spec.js | 11 +++++----- test/specs/polygon.spec.js | 11 +++++----- test/utils.js | 6 +++++- types/index.d.ts | 7 +++++-- types/tests/exports.ts | 2 ++ 18 files changed, 151 insertions(+), 64 deletions(-) create mode 100644 docs/guide/developers.md diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index 2727f9157..395c89dba 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -134,6 +134,7 @@ export default defineConfig({ 'types/polygon' ] }, + 'developers', { title: 'Migration', collapsable: true, diff --git a/docs/guide/developers.md b/docs/guide/developers.md new file mode 100644 index 000000000..5664e7e66 --- /dev/null +++ b/docs/guide/developers.md @@ -0,0 +1,33 @@ +# Developers + +## Access to the annotation elements + +The annotation plugin uses Chart.js elements to draw the annotation requested by the user. The following APIs allows the user to get the created annotation elements to use in callbacks or for other purposes. +The APIs are available in the annotation plugin instance. + +#### Script Tag + +```html + +``` + +#### Bundlers (Webpack, Rollup, etc.) + +```javascript +// get annotation plugin instance +import annotationPlugin from 'chartjs-plugin-annotation'; +``` + +### `.getAnnotations(chart: Chart): AnnotationElement[]` + +It provides all annotation elements configured by the plugin options, even if the annotations are not visible. + +```javascript +const myLineChart = new Chart(ctx, config); +// get all annotation elements +const elements = annotationPlugin.getAnnotations(myLineChart); +``` + \ No newline at end of file diff --git a/docs/guide/integration.md b/docs/guide/integration.md index f46f83bc6..3dde63da1 100644 --- a/docs/guide/integration.md +++ b/docs/guide/integration.md @@ -10,7 +10,7 @@ title: Integration ``` diff --git a/src/annotation.js b/src/annotation.js index 006416329..2cfe325cd 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -4,6 +4,7 @@ import {handleEvent, eventHooks, updateListeners} from './events'; import {invokeHook, elementHooks, updateHooks} from './hooks'; import {adjustScaleRange, verifyScaleOptions} from './scale'; import {updateElements, resolveType, isIndexable} from './elements'; +import {getElements} from './interaction'; import {annotationTypes} from './types'; import {requireVersion} from './helpers'; import {version} from '../package.json'; @@ -101,8 +102,14 @@ export default { chartStates.delete(chart); }, - _getState(chart) { - return chartStates.get(chart); + getAnnotations(chart) { + const state = chartStates.get(chart); + return state ? state.elements : []; + }, + + // only for testing + _getAnnotationElementsAtEventForMode(visibleElements, event, options) { + return getElements(visibleElements, event, options); }, defaults: { diff --git a/src/events.js b/src/events.js index 67bd5e9d3..f10f6bd8b 100644 --- a/src/events.js +++ b/src/events.js @@ -19,7 +19,6 @@ export const eventHooks = moveHooks.concat('click'); export function updateListeners(chart, state, options) { state.listened = loadHooks(options, eventHooks, state.listeners); state.moveListened = false; - state._getElements = getElements; // for testing moveHooks.forEach(hook => { if (isFunction(options[hook])) { @@ -71,7 +70,7 @@ function handleMoveEvents(state, event, options) { let elements; if (event.type === 'mousemove') { - elements = getElements(state, event, options.interaction); + elements = getElements(state.visibleElements, event, options.interaction); } else { elements = []; } @@ -96,7 +95,7 @@ function dispatchMoveEvents({state, event}, hook, elements, checkElements) { function handleClickEvents(state, event, options) { const listeners = state.listeners; - const elements = getElements(state, event, options.interaction); + const elements = getElements(state.visibleElements, event, options.interaction); let changed; for (const element of elements) { changed = dispatchEvent(element.options.click || listeners.click, element, event) || changed; diff --git a/src/interaction.js b/src/interaction.js index 63b753ee6..188615a7e 100644 --- a/src/interaction.js +++ b/src/interaction.js @@ -9,58 +9,58 @@ const interaction = { modes: { /** * Point mode returns all elements that hit test based on the event position - * @param {Object} state - the state of the plugin + * @param {AnnotationElement[]} visibleElements - annotation elements which are visible * @param {ChartEvent} event - the event we are find things at * @return {AnnotationElement[]} - elements that are found */ - point(state, event) { - return filterElements(state, event, {intersect: true}); + point(visibleElements, event) { + return filterElements(visibleElements, event, {intersect: true}); }, /** * Nearest mode returns the element closest to the event position - * @param {Object} state - the state of the plugin + * @param {AnnotationElement[]} visibleElements - annotation elements which are visible * @param {ChartEvent} event - the event we are find things at * @param {Object} options - interaction options to use * @return {AnnotationElement[]} - elements that are found (only 1 element) */ - nearest(state, event, options) { - return getNearestItem(state, event, options); + nearest(visibleElements, event, options) { + return getNearestItem(visibleElements, event, options); }, /** * x mode returns the elements that hit-test at the current x coordinate - * @param {Object} state - the state of the plugin + * @param {AnnotationElement[]} visibleElements - annotation elements which are visible * @param {ChartEvent} event - the event we are find things at * @param {Object} options - interaction options to use * @return {AnnotationElement[]} - elements that are found */ - x(state, event, options) { - return filterElements(state, event, {intersect: options.intersect, axis: 'x'}); + x(visibleElements, event, options) { + return filterElements(visibleElements, event, {intersect: options.intersect, axis: 'x'}); }, /** * y mode returns the elements that hit-test at the current y coordinate - * @param {Object} state - the state of the plugin + * @param {AnnotationElement[]} visibleElements - annotation elements which are visible * @param {ChartEvent} event - the event we are find things at * @param {Object} options - interaction options to use * @return {AnnotationElement[]} - elements that are found */ - y(state, event, options) { - return filterElements(state, event, {intersect: options.intersect, axis: 'y'}); + y(visibleElements, event, options) { + return filterElements(visibleElements, event, {intersect: options.intersect, axis: 'y'}); } } }; /** * Returns all elements that hit test based on the event position - * @param {Object} state - the state of the plugin + * @param {AnnotationElement[]} visibleElements - annotation elements which are visible * @param {ChartEvent} event - the event we are find things at * @param {Object} options - interaction options to use * @return {AnnotationElement[]} - elements that are found */ -export function getElements(state, event, options) { +export function getElements(visibleElements, event, options) { const mode = interaction.modes[options.mode] || interaction.modes.nearest; - return mode(state, event, options); + return mode(visibleElements, event, options); } function inRangeByAxis(element, event, axis) { @@ -79,14 +79,14 @@ function getPointByAxis(event, center, axis) { return center; } -function filterElements(state, event, options) { - return state.visibleElements.filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, options.axis)); +function filterElements(visibleElements, event, options) { + return visibleElements.filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, options.axis)); } -function getNearestItem(state, event, options) { +function getNearestItem(visibleElements, event, options) { let minDistance = Number.POSITIVE_INFINITY; - return filterElements(state, event, options) + return filterElements(visibleElements, event, options) .reduce((nearestItems, element) => { const center = element.getCenterPoint(); const evenPoint = getPointByAxis(event, center, options.axis); diff --git a/test/index.js b/test/index.js index 968895b8e..75690c33e 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,6 @@ import {acquireChart, addMatchers, releaseCharts, specsFromFixtures, triggerMouseEvent, afterEvent} from 'chartjs-test-utils'; import {testEvents, eventPoint0, getCenterPoint} from './events'; -import {createCanvas, getAnnotationElements, scatterChart, stringifyObject, interactionData, getQuadraticXY, getQuadraticAngle, drawStar} from './utils'; +import {createCanvas, getAnnotationElements, getAnnotationInteractedElements, scatterChart, stringifyObject, interactionData, getQuadraticXY, getQuadraticAngle, drawStar} from './utils'; import * as helpers from '../src/helpers'; window.helpers = helpers; @@ -14,6 +14,7 @@ window.getCenterPoint = getCenterPoint; window.createCanvas = createCanvas; window.drawStar = drawStar; window.getAnnotationElements = getAnnotationElements; +window.getAnnotationInteractedElements = getAnnotationInteractedElements; window.scatterChart = scatterChart; window.stringifyObject = stringifyObject; window.interactionData = interactionData; diff --git a/test/integration/ts/basic.ts b/test/integration/ts/basic.ts index 8ed682633..763fd4a2e 100644 --- a/test/integration/ts/basic.ts +++ b/test/integration/ts/basic.ts @@ -32,3 +32,5 @@ const chart = new Chart('id', { }, plugins: [Annotation] }); + +const elements = Annotation.getAnnotations(chart); diff --git a/test/specs/annotation.spec.js b/test/specs/annotation.spec.js index 90c9c5dc0..2afd6d960 100644 --- a/test/specs/annotation.spec.js +++ b/test/specs/annotation.spec.js @@ -142,6 +142,34 @@ describe('Annotation plugin', function() { console.warn = origWarn; }); + it('should return the right amount of annotations elements', function() { + const types = ['box', 'ellipse', 'label', 'line', 'point', 'polygon']; + const annotations = types.map(function(type) { + return { + type, + display: () => type.startsWith('l'), + xMin: 2, + yMin: 2, + xMax: 8, + yMax: 8 + }; + }); + + const chart = acquireChart({ + type: 'line', + options: { + plugins: { + annotation: { + annotations + } + } + } + }); + + expect(window.getAnnotationElements(chart).length).toBe(types.length); + expect(window.getAnnotationElements(undefined).length).toBe(0); + }); + describe('Annotation option resolution', function() { it('should resolve from plugin common options', function() { const chart = acquireChart({ diff --git a/test/specs/box.spec.js b/test/specs/box.spec.js index 8b698f7e3..0951c7229 100644 --- a/test/specs/box.spec.js +++ b/test/specs/box.spec.js @@ -153,10 +153,11 @@ describe('Box annotation', function() { }; const chart = window.scatterChart(10, 10, {outer, inner}); - const state = window['chartjs-plugin-annotation']._getState(chart); + const elements = window.getAnnotationElements(chart); + const visible = elements.filter(el => !el.skip && el.options.display); const interactionOpts = {}; - const outerEl = window.getAnnotationElements(chart)[0]; - const innerEl = window.getAnnotationElements(chart)[1]; + const outerEl = elements[0]; + const innerEl = elements[1]; it('should return the right amount of annotation elements', function() { for (const interaction of window.interactionData) { @@ -177,8 +178,8 @@ describe('Box annotation', function() { for (let i = 0; i < points.length; i++) { const point = points[i]; const elementsCount = elementsCounts[i]; - const elements = state._getElements(state, point, interactionOpts); - expect(elements.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}}`).toEqual(elementsCount); + const els = window.getAnnotationInteractedElements(visible, point, interactionOpts); + expect(els.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}}`).toEqual(elementsCount); } }); } diff --git a/test/specs/ellipse.spec.js b/test/specs/ellipse.spec.js index 63d91e034..5c7f7f64d 100644 --- a/test/specs/ellipse.spec.js +++ b/test/specs/ellipse.spec.js @@ -105,10 +105,11 @@ describe('Ellipse annotation', function() { }; const chart = window.scatterChart(10, 10, {outer, inner}); - const state = window['chartjs-plugin-annotation']._getState(chart); + const elements = window.getAnnotationElements(chart); + const visible = elements.filter(el => !el.skip && el.options.display); const interactionOpts = {}; - const outerEl = window.getAnnotationElements(chart)[0]; - const innerEl = window.getAnnotationElements(chart)[1]; + const outerEl = elements[0]; + const innerEl = elements[1]; it('should return the right amount of annotation elements', function() { for (const interaction of window.interactionData) { @@ -130,8 +131,8 @@ describe('Ellipse annotation', function() { const point = points[i]; const elementsCount = elementsCounts[i]; const {x, y} = rotated(point, point.el.getCenterPoint(), rotation / 180 * Math.PI); - const elements = state._getElements(state, {x, y}, interactionOpts); - expect(elements.length).withContext(`with rotation ${rotation}, interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${x.toFixed(1)}, y: ${y.toFixed(1)}`).toEqual(elementsCount); + const els = window.getAnnotationInteractedElements(visible, {x, y}, interactionOpts, true); + expect(els.length).withContext(`with rotation ${rotation}, interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${x.toFixed(1)}, y: ${y.toFixed(1)}`).toEqual(elementsCount); } }); } diff --git a/test/specs/label.spec.js b/test/specs/label.spec.js index d21fd7fc2..4210ad5bb 100644 --- a/test/specs/label.spec.js +++ b/test/specs/label.spec.js @@ -131,10 +131,11 @@ describe('Label annotation', function() { }; const chart = window.scatterChart(10, 10, {outer, inner}); - const state = window['chartjs-plugin-annotation']._getState(chart); + const elements = window.getAnnotationElements(chart); + const visible = elements.filter(el => !el.skip && el.options.display); const interactionOpts = {}; - const outerEl = window.getAnnotationElements(chart)[0]; - const innerEl = window.getAnnotationElements(chart)[1]; + const outerEl = elements[0]; + const innerEl = elements[1]; it('should return the right amount of annotation elements', function() { for (const interaction of window.interactionData) { @@ -155,8 +156,8 @@ describe('Label annotation', function() { for (let i = 0; i < points.length; i++) { const point = points[i]; const elementsCount = elementsCounts[i]; - const elements = state._getElements(state, point, interactionOpts); - expect(elements.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); + const els = window.getAnnotationInteractedElements(visible, point, interactionOpts); + expect(els.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); } }); } diff --git a/test/specs/line.spec.js b/test/specs/line.spec.js index fdbfd292a..fca1e5016 100644 --- a/test/specs/line.spec.js +++ b/test/specs/line.spec.js @@ -224,12 +224,13 @@ describe('Line annotation', function() { }; const chart = window.scatterChart(10, 10, {outer, inner}); - const state = window['chartjs-plugin-annotation']._getState(chart); + const elements = window.getAnnotationElements(chart); + const visible = elements.filter(el => !el.skip && el.options.display); const interactionOpts = {}; - const outerEl = window.getAnnotationElements(chart)[0]; + const outerEl = elements[0]; const outCenter = outerEl.getCenterPoint(); const outHBordeWidth = outerEl.options.borderWidth / 2; - const innerEl = window.getAnnotationElements(chart)[1]; + const innerEl = elements[1]; const inCenter = outerEl.getCenterPoint(); const inHBordeWidth = innerEl.options.borderWidth / 2; @@ -252,8 +253,8 @@ describe('Line annotation', function() { for (let i = 0; i < points.length; i++) { const point = points[i]; const elementsCount = elementsCounts[i]; - const elements = state._getElements(state, point, interactionOpts); - expect(elements.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); + const els = window.getAnnotationInteractedElements(visible, point, interactionOpts); + expect(els.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); } }); } @@ -291,11 +292,12 @@ describe('Line annotation', function() { }; const chart = window.scatterChart(10, 10, {outer, inner}); - const state = window['chartjs-plugin-annotation']._getState(chart); + const elements = window.getAnnotationElements(chart); + const visible = elements.filter(el => !el.skip && el.options.display); const interactionOpts = {}; - const outerEl = window.getAnnotationElements(chart)[0]; + const outerEl = elements[0]; const outCenter = outerEl.getCenterPoint(); - const innerEl = window.getAnnotationElements(chart)[1]; + const innerEl = elements[1]; it('should return the right amount of annotation elements', function() { for (const interaction of window.interactionData) { @@ -316,8 +318,8 @@ describe('Line annotation', function() { for (let i = 0; i < points.length; i++) { const point = points[i]; const elementsCount = elementsCounts[i]; - const elements = state._getElements(state, point, interactionOpts); - expect(elements.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); + const els = window.getAnnotationInteractedElements(visible, point, interactionOpts); + expect(els.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); } }); } diff --git a/test/specs/point.spec.js b/test/specs/point.spec.js index 9da097e65..87efce5b0 100644 --- a/test/specs/point.spec.js +++ b/test/specs/point.spec.js @@ -172,10 +172,11 @@ describe('Point annotation', function() { }; const chart = window.scatterChart(10, 10, {outer, inner}); - const state = window['chartjs-plugin-annotation']._getState(chart); + const elements = window.getAnnotationElements(chart); + const visible = elements.filter(el => !el.skip && el.options.display); const interactionOpts = {}; - const outerEl = window.getAnnotationElements(chart)[0]; - const innerEl = window.getAnnotationElements(chart)[1]; + const outerEl = elements[0]; + const innerEl = elements[1]; it('should return the right amount of annotation elements', function() { for (const interaction of window.interactionData) { @@ -196,8 +197,8 @@ describe('Point annotation', function() { for (let i = 0; i < points.length; i++) { const point = points[i]; const elementsCount = elementsCounts[i]; - const elements = state._getElements(state, point, interactionOpts); - expect(elements.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); + const els = window.getAnnotationInteractedElements(visible, point, interactionOpts); + expect(els.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); } }); } diff --git a/test/specs/polygon.spec.js b/test/specs/polygon.spec.js index 52aae340e..71fb927b7 100644 --- a/test/specs/polygon.spec.js +++ b/test/specs/polygon.spec.js @@ -260,10 +260,11 @@ describe('Polygon annotation', function() { }; const chart = window.scatterChart(10, 10, {outer, inner}); - const state = window['chartjs-plugin-annotation']._getState(chart); + const elements = window.getAnnotationElements(chart); + const visible = elements.filter(el => !el.skip && el.options.display); const interactionOpts = {}; - const outerEl = window.getAnnotationElements(chart)[0]; - const innerEl = window.getAnnotationElements(chart)[1]; + const outerEl = elements[0]; + const innerEl = elements[1]; it('should return the right amount of annotation elements', function() { for (const interaction of window.interactionData) { @@ -285,8 +286,8 @@ describe('Polygon annotation', function() { const point = points[i]; const elementsCount = elementsCounts[i]; const {x, y} = rotated(point, point.el.getCenterPoint(), rotation / 180 * Math.PI); - const elements = state._getElements(state, {x, y}, interactionOpts); - expect(elements.length).withContext(`with rotation ${rotation}, interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); + const els = window.getAnnotationInteractedElements(visible, {x, y}, interactionOpts); + expect(els.length).withContext(`with rotation ${rotation}, interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); } }); } diff --git a/test/utils.js b/test/utils.js index 8439ee0fa..db7439114 100644 --- a/test/utils.js +++ b/test/utils.js @@ -46,7 +46,11 @@ export function drawStar(ctx, x, y, radius, spikes, inset) { } export function getAnnotationElements(chart) { - return window['chartjs-plugin-annotation']._getState(chart).elements; + return window['chartjs-plugin-annotation'].getAnnotations(chart); +} + +export function getAnnotationInteractedElements(visibleElements, event, options) { + return window['chartjs-plugin-annotation']._getAnnotationElementsAtEventForMode(visibleElements, event, options); } export function scatterChart(xMax, yMax, annotations) { diff --git a/types/index.d.ts b/types/index.d.ts index a713ce1e1..15db88b4e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,5 +1,6 @@ -import { ChartType, Plugin } from 'chart.js'; +import { Chart, ChartType, Plugin } from 'chart.js'; import { AnnotationPluginOptions, BoxAnnotationOptions, EllipseAnnotationOptions, LabelAnnotationOptions, LineAnnotationOptions, PointAnnotationOptions, PolygonAnnotationOptions } from './options'; +import { AnnotationElement } from './element'; declare module 'chart.js' { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -17,7 +18,9 @@ declare module 'chart.js' { } } -declare const Annotation: Plugin; +declare const Annotation: Plugin & { + getAnnotations(chart: Chart): AnnotationElement[]; +}; export default Annotation; diff --git a/types/tests/exports.ts b/types/tests/exports.ts index 5cacbc570..ccc5aca9b 100644 --- a/types/tests/exports.ts +++ b/types/tests/exports.ts @@ -48,3 +48,5 @@ const chart = new Chart('id', { }, plugins: [Annotation] }); + +const elements = Annotation.getAnnotations(chart);