From 631fced750193e1d580ae3afdb83ea14abbf513f Mon Sep 17 00:00:00 2001 From: stockiNail Date: Wed, 26 Jan 2022 11:49:42 +0100 Subject: [PATCH 01/27] Enable events triggering on all affected annotations --- src/annotation.js | 3 +- src/events.js | 54 ++++++++--------------- test/specs/events.spec.js | 90 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 38 deletions(-) diff --git a/src/annotation.js b/src/annotation.js index 247baae71..52398ac03 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -46,7 +46,8 @@ export default { visibleElements: [], listeners: {}, listened: false, - moveListened: false + moveListened: false, + hovered: [] }); }, diff --git a/src/events.js b/src/events.js index 17c984140..e28fe8805 100644 --- a/src/events.js +++ b/src/events.js @@ -1,4 +1,4 @@ -import {distanceBetweenPoints, defined, callback} from 'chart.js/helpers'; +import {defined, callback} from 'chart.js/helpers'; const clickHooks = ['click', 'dblclick']; const moveHooks = ['enter', 'leave']; @@ -63,32 +63,35 @@ function handleMoveEvents(state, event) { return; } - let element; + let elements = []; if (event.type === 'mousemove') { - element = getNearestItem(state.elements, event); + // elements = getNearestItem(state.elements, event); + elements = state.visibleElements.filter((element) => element.inRange(event.x, event.y)); } const previous = state.hovered; - state.hovered = element; + state.hovered = elements; - dispatchMoveEvents(state, {previous, element}, event); + const context = {state, event}; + dispatchMoveEvents(context, 'leave', previous, elements); + dispatchMoveEvents(context, 'enter', elements, previous); } -function dispatchMoveEvents(state, elements, event) { - const {previous, element} = elements; - if (previous && previous !== element) { - dispatchEvent(previous.options.leave || state.listeners.leave, previous, event); - } - if (element && element !== previous) { - dispatchEvent(element.options.enter || state.listeners.enter, element, event); +function dispatchMoveEvents({state, event}, hook, elements, checkElements) { + if (elements.length > 0) { + for (const element of elements) { + if (checkElements.indexOf(element) < 0) { + dispatchEvent(element.options[hook] || state.listeners[hook], element, event); + } + } } } function handleClickEvents(state, event, options) { const listeners = state.listeners; - const element = getNearestItem(state.elements, event); - if (element) { + const elements = state.visibleElements.filter((element) => element.inRange(event.x, event.y)); + for (const element of elements) { const elOpts = element.options; const dblclick = elOpts.dblclick || listeners.dblclick; const click = elOpts.click || listeners.click; @@ -113,26 +116,3 @@ function handleClickEvents(state, event, options) { function dispatchEvent(handler, element, event) { callback(handler, [element.$context, event]); } - -function getNearestItem(elements, position) { - let minDistance = Number.POSITIVE_INFINITY; - - return elements - .filter((element) => element.options.display && element.inRange(position.x, position.y)) - .reduce((nearestItems, element) => { - const center = element.getCenterPoint(); - const distance = distanceBetweenPoints(position, center); - - if (distance < minDistance) { - nearestItems = [element]; - minDistance = distance; - } else if (distance === minDistance) { - // Can have multiple items at the same distance in which case we sort by size - nearestItems.push(element); - } - - return nearestItems; - }, []) - .sort((a, b) => a._index - b._index) - .slice(0, 1)[0]; // return only the top item -} diff --git a/test/specs/events.spec.js b/test/specs/events.spec.js index 7dc0753b0..8b5536b8d 100644 --- a/test/specs/events.spec.js +++ b/test/specs/events.spec.js @@ -157,4 +157,94 @@ describe('Common', function() { }); }); }); + + describe('events on overlapped annotations', function() { + const enterSpy = jasmine.createSpy('enter'); + const clickSpy = jasmine.createSpy('click'); + const leaveSpy = jasmine.createSpy('leave'); + + const chartConfig = { + type: 'scatter', + options: { + animation: false, + scales: { + x: { + display: false, + min: 0, + max: 10 + }, + y: { + display: false, + min: 0, + max: 10 + } + }, + plugins: { + legend: false, + annotation: { + enter: enterSpy, + click: clickSpy, + leave: leaveSpy, + annotations: { + large: { + type: 'box', + xMin: 2, + xMax: 8, + yMin: 2, + yMax: 8, + borderWidth: 0, + }, + small: { + type: 'box', + xMin: 4, + xMax: 6, + yMin: 4, + yMax: 6, + borderWidth: 0, + } + } + } + } + } + }; + + it('should detect all events on all annotations', function(done) { + + const chart = window.acquireChart(chartConfig); + const large = window.getAnnotationElements(chart)[0]; + const small = window.getAnnotationElements(chart)[1]; + + const event1 = {x: large.x + 1, y: large.y + large.height / 2}; + const event2 = {x: small.x + 1, y: small.y + small.height / 2}; + const click = {x: small.x + small.width / 2, y: small.y + small.height / 2}; + const event3 = {x: small.x2 + 1, y: small.y2 - small.height / 2}; + const event4 = {x: large.x2 + 1, y: large.y2 - large.height / 2}; + + window.triggerMouseEvent(chart, 'mousemove', event1); + window.afterEvent(chart, 'mousemove', function() { + expect(enterSpy.calls.count()).toBe(1); + + window.triggerMouseEvent(chart, 'mousemove', event2); + window.afterEvent(chart, 'mousemove', function() { + expect(enterSpy.calls.count()).toBe(2); + + window.triggerMouseEvent(chart, 'click', click); + window.afterEvent(chart, 'click', function() { + expect(clickSpy.calls.count()).toBe(2); + + window.triggerMouseEvent(chart, 'mousemove', event3); + window.afterEvent(chart, 'mousemove', function() { + expect(leaveSpy.calls.count()).toBe(1); + + window.triggerMouseEvent(chart, 'mousemove', event4); + window.afterEvent(chart, 'mousemove', function() { + expect(leaveSpy.calls.count()).toBe(2); + done(); + }); + }); + }); + }); + }); + }); + }); }); From fa34f486ac3d7367b5a9eb8f6e7ae1ad0403bf11 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Wed, 26 Jan 2022 11:51:23 +0100 Subject: [PATCH 02/27] removes commented code --- src/events.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/events.js b/src/events.js index e28fe8805..d8b589462 100644 --- a/src/events.js +++ b/src/events.js @@ -66,7 +66,6 @@ function handleMoveEvents(state, event) { let elements = []; if (event.type === 'mousemove') { - // elements = getNearestItem(state.elements, event); elements = state.visibleElements.filter((element) => element.inRange(event.x, event.y)); } From 46a6ee7f35ed08f98a8afd74d7c1489681749056 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Wed, 26 Jan 2022 18:17:39 +0100 Subject: [PATCH 03/27] removes useless if statements --- src/events.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/events.js b/src/events.js index d8b589462..b0e9242b1 100644 --- a/src/events.js +++ b/src/events.js @@ -78,11 +78,9 @@ function handleMoveEvents(state, event) { } function dispatchMoveEvents({state, event}, hook, elements, checkElements) { - if (elements.length > 0) { - for (const element of elements) { - if (checkElements.indexOf(element) < 0) { - dispatchEvent(element.options[hook] || state.listeners[hook], element, event); - } + for (const element of elements) { + if (checkElements.indexOf(element) < 0) { + dispatchEvent(element.options[hook] || state.listeners[hook], element, event); } } } From f0f6697eb87d8cdec4a74f5ad68b1166674cbfc9 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 27 Jan 2022 18:07:36 +0100 Subject: [PATCH 04/27] first interaction mode implementation --- src/annotation.js | 6 ++++ src/events.js | 38 ++++++++++++++++++--- test/specs/events.spec.js | 72 ++++++++++++++++++++++----------------- 3 files changed, 81 insertions(+), 35 deletions(-) diff --git a/src/annotation.js b/src/annotation.js index 52398ac03..079697802 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -121,6 +121,9 @@ export default { clip: true, dblClickSpeed: 350, // ms drawTime: 'afterDatasetsDraw', + interaction: { + mode: undefined + }, label: { drawTime: null } @@ -133,6 +136,9 @@ export default { _allKeys: false, _fallback: (prop, opts) => `elements.${annotationTypes[resolveType(opts.type)].id}`, }, + interaction: { + _fallback: () => true, + } }, additionalOptionScopes: [''] diff --git a/src/events.js b/src/events.js index b0e9242b1..64d7d5592 100644 --- a/src/events.js +++ b/src/events.js @@ -1,4 +1,4 @@ -import {defined, callback} from 'chart.js/helpers'; +import {distanceBetweenPoints, defined, callback} from 'chart.js/helpers'; const clickHooks = ['click', 'dblclick']; const moveHooks = ['enter', 'leave']; @@ -48,7 +48,7 @@ export function handleEvent(state, event, options) { switch (event.type) { case 'mousemove': case 'mouseout': - handleMoveEvents(state, event); + handleMoveEvents(state, event, options); break; case 'click': handleClickEvents(state, event, options); @@ -58,7 +58,7 @@ export function handleEvent(state, event, options) { } } -function handleMoveEvents(state, event) { +function handleMoveEvents(state, event, options) { if (!state.moveListened) { return; } @@ -66,7 +66,14 @@ function handleMoveEvents(state, event) { let elements = []; if (event.type === 'mousemove') { - elements = state.visibleElements.filter((element) => element.inRange(event.x, event.y)); + if (options.interaction.mode === 'point') { + elements = state.visibleElements.filter((element) => element.inRange(event.x, event.y)); + } else { + const element = getNearestItem(state.visibleElements, event); + if (element) { + elements.push(element); + } + } } const previous = state.hovered; @@ -113,3 +120,26 @@ function handleClickEvents(state, event, options) { function dispatchEvent(handler, element, event) { callback(handler, [element.$context, event]); } + +function getNearestItem(elements, position) { + let minDistance = Number.POSITIVE_INFINITY; + + return elements + .filter((element) => element.inRange(position.x, position.y)) + .reduce((nearestItems, element) => { + const center = element.getCenterPoint(); + const distance = distanceBetweenPoints(position, center); + + if (distance < minDistance) { + nearestItems = [element]; + minDistance = distance; + } else if (distance === minDistance) { + // Can have multiple items at the same distance in which case we sort by size + nearestItems.push(element); + } + + return nearestItems; + }, []) + .sort((a, b) => a._index - b._index) + .slice(0, 1)[0]; // return only the top item +} diff --git a/test/specs/events.spec.js b/test/specs/events.spec.js index 8b5536b8d..e9b68fb35 100644 --- a/test/specs/events.spec.js +++ b/test/specs/events.spec.js @@ -159,10 +159,6 @@ describe('Common', function() { }); describe('events on overlapped annotations', function() { - const enterSpy = jasmine.createSpy('enter'); - const clickSpy = jasmine.createSpy('click'); - const leaveSpy = jasmine.createSpy('leave'); - const chartConfig = { type: 'scatter', options: { @@ -182,9 +178,8 @@ describe('Common', function() { plugins: { legend: false, annotation: { - enter: enterSpy, - click: clickSpy, - leave: leaveSpy, + interaction: { + }, annotations: { large: { type: 'box', @@ -207,44 +202,59 @@ describe('Common', function() { } } }; + const modes = ['nearest', 'point']; + const hookCallsCount = [[1, 1, 2, 0, 1], [1, 2, 2, 1, 2]]; - it('should detect all events on all annotations', function(done) { + for (let i = 0; i < modes.length; i++) { + const mode = modes[i]; + const callsCount = hookCallsCount[i]; - const chart = window.acquireChart(chartConfig); - const large = window.getAnnotationElements(chart)[0]; - const small = window.getAnnotationElements(chart)[1]; + it(`should detect events on annotations with interaction mode ${mode}`, function(done) { + const enterSpy = jasmine.createSpy('enter'); + const clickSpy = jasmine.createSpy('click'); + const leaveSpy = jasmine.createSpy('leave'); - const event1 = {x: large.x + 1, y: large.y + large.height / 2}; - const event2 = {x: small.x + 1, y: small.y + small.height / 2}; - const click = {x: small.x + small.width / 2, y: small.y + small.height / 2}; - const event3 = {x: small.x2 + 1, y: small.y2 - small.height / 2}; - const event4 = {x: large.x2 + 1, y: large.y2 - large.height / 2}; + chartConfig.options.plugins.annotation.enter = enterSpy; + chartConfig.options.plugins.annotation.click = clickSpy; + chartConfig.options.plugins.annotation.leave = leaveSpy; + chartConfig.options.plugins.annotation.interaction.mode = mode; - window.triggerMouseEvent(chart, 'mousemove', event1); - window.afterEvent(chart, 'mousemove', function() { - expect(enterSpy.calls.count()).toBe(1); + const chart = window.acquireChart(chartConfig); + const large = window.getAnnotationElements(chart)[0]; + const small = window.getAnnotationElements(chart)[1]; - window.triggerMouseEvent(chart, 'mousemove', event2); + const event1 = {x: large.x + 1, y: large.y + large.height / 2}; + const event2 = {x: small.x + 1, y: small.y + small.height / 2}; + const click = {x: small.x + small.width / 2, y: small.y + small.height / 2}; + const event3 = {x: small.x2 + 1, y: small.y2 - small.height / 2}; + const event4 = {x: large.x2 + 1, y: large.y2 - large.height / 2}; + + window.triggerMouseEvent(chart, 'mousemove', event1); window.afterEvent(chart, 'mousemove', function() { - expect(enterSpy.calls.count()).toBe(2); + expect(enterSpy.calls.count()).toBe(callsCount[0]); - window.triggerMouseEvent(chart, 'click', click); - window.afterEvent(chart, 'click', function() { - expect(clickSpy.calls.count()).toBe(2); + window.triggerMouseEvent(chart, 'mousemove', event2); + window.afterEvent(chart, 'mousemove', function() { + expect(enterSpy.calls.count()).toBe(callsCount[1]); - window.triggerMouseEvent(chart, 'mousemove', event3); - window.afterEvent(chart, 'mousemove', function() { - expect(leaveSpy.calls.count()).toBe(1); + window.triggerMouseEvent(chart, 'click', click); + window.afterEvent(chart, 'click', function() { + expect(clickSpy.calls.count()).toBe(callsCount[2]); - window.triggerMouseEvent(chart, 'mousemove', event4); + window.triggerMouseEvent(chart, 'mousemove', event3); window.afterEvent(chart, 'mousemove', function() { - expect(leaveSpy.calls.count()).toBe(2); - done(); + expect(leaveSpy.calls.count()).toBe(callsCount[3]); + + window.triggerMouseEvent(chart, 'mousemove', event4); + window.afterEvent(chart, 'mousemove', function() { + expect(leaveSpy.calls.count()).toBe(callsCount[4]); + done(); + }); }); }); }); }); }); - }); + } }); }); From a3765c672a900a916329ab38997980997f57793a Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 27 Jan 2022 18:09:29 +0100 Subject: [PATCH 05/27] removes useless callback --- src/annotation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/annotation.js b/src/annotation.js index 079697802..5592b1b8a 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -137,7 +137,7 @@ export default { _fallback: (prop, opts) => `elements.${annotationTypes[resolveType(opts.type)].id}`, }, interaction: { - _fallback: () => true, + _fallback: true, } }, From 86136ef137a58096a6d406b73d8e76327ebc3669 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 27 Jan 2022 18:23:18 +0100 Subject: [PATCH 06/27] applies interaction mode to click events --- src/events.js | 22 +++++++++++++--------- test/specs/events.spec.js | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/events.js b/src/events.js index 64d7d5592..449339930 100644 --- a/src/events.js +++ b/src/events.js @@ -66,14 +66,7 @@ function handleMoveEvents(state, event, options) { let elements = []; if (event.type === 'mousemove') { - if (options.interaction.mode === 'point') { - elements = state.visibleElements.filter((element) => element.inRange(event.x, event.y)); - } else { - const element = getNearestItem(state.visibleElements, event); - if (element) { - elements.push(element); - } - } + elements = getItems(state, event, options); } const previous = state.hovered; @@ -94,7 +87,7 @@ function dispatchMoveEvents({state, event}, hook, elements, checkElements) { function handleClickEvents(state, event, options) { const listeners = state.listeners; - const elements = state.visibleElements.filter((element) => element.inRange(event.x, event.y)); + const elements = getItems(state, event, options); for (const element of elements) { const elOpts = element.options; const dblclick = elOpts.dblclick || listeners.dblclick; @@ -121,6 +114,17 @@ function dispatchEvent(handler, element, event) { callback(handler, [element.$context, event]); } +function getItems(state, event, options) { + if (options.interaction.mode === 'point') { + return state.visibleElements.filter((element) => element.inRange(event.x, event.y)); + } + const element = getNearestItem(state.visibleElements, event); + if (element) { + return [element]; + } + return []; +} + function getNearestItem(elements, position) { let minDistance = Number.POSITIVE_INFINITY; diff --git a/test/specs/events.spec.js b/test/specs/events.spec.js index e9b68fb35..878d64309 100644 --- a/test/specs/events.spec.js +++ b/test/specs/events.spec.js @@ -203,7 +203,7 @@ describe('Common', function() { } }; const modes = ['nearest', 'point']; - const hookCallsCount = [[1, 1, 2, 0, 1], [1, 2, 2, 1, 2]]; + const hookCallsCount = [[1, 1, 1, 0, 1], [1, 2, 2, 1, 2]]; for (let i = 0; i < modes.length; i++) { const mode = modes[i]; From ebddfedc97aea27ae6440a1409823e98ad988794 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 27 Jan 2022 21:40:36 +0100 Subject: [PATCH 07/27] applies review --- src/events.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/events.js b/src/events.js index 449339930..fb4d57bd4 100644 --- a/src/events.js +++ b/src/events.js @@ -66,7 +66,7 @@ function handleMoveEvents(state, event, options) { let elements = []; if (event.type === 'mousemove') { - elements = getItems(state, event, options); + elements = getElements(state, event, options); } const previous = state.hovered; @@ -87,7 +87,7 @@ function dispatchMoveEvents({state, event}, hook, elements, checkElements) { function handleClickEvents(state, event, options) { const listeners = state.listeners; - const elements = getItems(state, event, options); + const elements = getElements(state, event, options); for (const element of elements) { const elOpts = element.options; const dblclick = elOpts.dblclick || listeners.dblclick; @@ -114,15 +114,11 @@ function dispatchEvent(handler, element, event) { callback(handler, [element.$context, event]); } -function getItems(state, event, options) { +function getElements(state, event, options) { if (options.interaction.mode === 'point') { return state.visibleElements.filter((element) => element.inRange(event.x, event.y)); } - const element = getNearestItem(state.visibleElements, event); - if (element) { - return [element]; - } - return []; + return getNearestItem(state.visibleElements, event); } function getNearestItem(elements, position) { @@ -145,5 +141,5 @@ function getNearestItem(elements, position) { return nearestItems; }, []) .sort((a, b) => a._index - b._index) - .slice(0, 1)[0]; // return only the top item + .slice(0, 1); } From 9ece84cc2f9ba7efc601350297bf225c9e4f032d Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 27 Jan 2022 21:42:57 +0100 Subject: [PATCH 08/27] removes array creation if not needed --- src/events.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/events.js b/src/events.js index fb4d57bd4..496531a30 100644 --- a/src/events.js +++ b/src/events.js @@ -63,10 +63,12 @@ function handleMoveEvents(state, event, options) { return; } - let elements = []; + let elements; if (event.type === 'mousemove') { elements = getElements(state, event, options); + } else { + elements = []; } const previous = state.hovered; From fb61612e6aa7953809ce65e8bbc6d43fe1a084ab Mon Sep 17 00:00:00 2001 From: stockiNail Date: Mon, 31 Jan 2022 15:35:42 +0100 Subject: [PATCH 09/27] interaction for box annotation --- src/annotation.js | 4 +- src/events.js | 37 ++--------- src/interaction.js | 77 ++++++++++++++++++++++ src/types/box.js | 10 +++ test/specs/events.spec.js | 100 ----------------------------- test/specs/interaction.spec.js | 113 +++++++++++++++++++++++++++++++++ 6 files changed, 207 insertions(+), 134 deletions(-) create mode 100644 src/interaction.js create mode 100644 test/specs/interaction.spec.js diff --git a/src/annotation.js b/src/annotation.js index 5592b1b8a..e5f9cb2ac 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -122,7 +122,9 @@ export default { dblClickSpeed: 350, // ms drawTime: 'afterDatasetsDraw', interaction: { - mode: undefined + mode: undefined, + axis: undefined, + intersect: true }, label: { drawTime: null diff --git a/src/events.js b/src/events.js index 496531a30..4fb025162 100644 --- a/src/events.js +++ b/src/events.js @@ -1,4 +1,5 @@ -import {distanceBetweenPoints, defined, callback} from 'chart.js/helpers'; +import {defined, callback} from 'chart.js/helpers'; +import {getElements} from './interaction'; const clickHooks = ['click', 'dblclick']; const moveHooks = ['enter', 'leave']; @@ -66,7 +67,7 @@ function handleMoveEvents(state, event, options) { let elements; if (event.type === 'mousemove') { - elements = getElements(state, event, options); + elements = getElements(state, event, options.interaction); } else { elements = []; } @@ -89,7 +90,7 @@ function dispatchMoveEvents({state, event}, hook, elements, checkElements) { function handleClickEvents(state, event, options) { const listeners = state.listeners; - const elements = getElements(state, event, options); + const elements = getElements(state, event, options.interaction); for (const element of elements) { const elOpts = element.options; const dblclick = elOpts.dblclick || listeners.dblclick; @@ -115,33 +116,3 @@ function handleClickEvents(state, event, options) { function dispatchEvent(handler, element, event) { callback(handler, [element.$context, event]); } - -function getElements(state, event, options) { - if (options.interaction.mode === 'point') { - return state.visibleElements.filter((element) => element.inRange(event.x, event.y)); - } - return getNearestItem(state.visibleElements, event); -} - -function getNearestItem(elements, position) { - let minDistance = Number.POSITIVE_INFINITY; - - return elements - .filter((element) => element.inRange(position.x, position.y)) - .reduce((nearestItems, element) => { - const center = element.getCenterPoint(); - const distance = distanceBetweenPoints(position, center); - - if (distance < minDistance) { - nearestItems = [element]; - minDistance = distance; - } else if (distance === minDistance) { - // Can have multiple items at the same distance in which case we sort by size - nearestItems.push(element); - } - - return nearestItems; - }, []) - .sort((a, b) => a._index - b._index) - .slice(0, 1); -} diff --git a/src/interaction.js b/src/interaction.js new file mode 100644 index 000000000..e6e64f057 --- /dev/null +++ b/src/interaction.js @@ -0,0 +1,77 @@ +import {distanceBetweenPoints} from 'chart.js/helpers'; + +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 {ChartEvent} event - the event we are find things at + * @return {Element[]} - elements that are found + */ + point(state, event) { + return getIntersectItems(state, event); + }, + + /** + * Nearest mode returns the element closest to the event position + * @param {Object} state - the state of the plugin + * @param {ChartEvent} event - the event we are find things at + * @param {options} options - interaction options to use + * @return {Element[]} - elements that are found + */ + nearest(state, event, options) { + return getNearestItem(state, event, options); + }, + } +}; + +export function getElements(state, event, options) { + const mode = interaction.modes[options.mode] || interaction.modes.nearest; + return mode(state, event, options); +} + +function getIntersectItems(state, event) { + return state.visibleElements.filter((element) => element.inRange(event.x, event.y)); +} + +function inRangeByAxis(element, event, axis) { + if (axis === 'x') { + return element.inXRange(event.x); + } else if (axis === 'y') { + return element.inYRange(event.y); + } + return element.inXRange(event.x) || element.inYRange(event.y); +} + +function getPointByAxis(event, center, axis) { + if (axis === 'x') { + return {x: event.x, y: center.y}; + } else if (axis === 'y') { + return {x: center.x, y: event.y}; + } + return center; +} + +function getNearestItem(state, event, options) { + const axis = options.axis || 'xy'; + let minDistance = Number.POSITIVE_INFINITY; + + return state.visibleElements + .filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, axis)) + .reduce((nearestItems, element) => { + const center = element.getCenterPoint(); + const evenPoint = getPointByAxis(event, center, axis); + const distance = distanceBetweenPoints(event, evenPoint); + if (distance < minDistance) { + nearestItems = [element]; + minDistance = distance; + } else if (distance === minDistance) { + // Can have multiple items at the same distance in which case we sort by size + nearestItems.push(element); + } + + return nearestItems; + }, []) + .sort((a, b) => a._index - b._index) + .slice(0, 1); // return only the top item; +} diff --git a/src/types/box.js b/src/types/box.js index 319927645..cce078ddf 100644 --- a/src/types/box.js +++ b/src/types/box.js @@ -7,6 +7,16 @@ export default class BoxAnnotation extends Element { return inBoxRange(mouseX, mouseY, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), this.options.borderWidth); } + inXRange(value, useFinalPosition) { + const {x, width} = this.getProps(['x', 'width'], useFinalPosition); + return value >= x && value <= x + width; + } + + inYRange(value, useFinalPosition) { + const {y, height} = this.getProps(['y', 'height'], useFinalPosition); + return value >= y && value <= y + height; + } + getCenterPoint(useFinalPosition) { return getRectCenterPoint(this.getProps(['x', 'y', 'width', 'height'], useFinalPosition)); } diff --git a/test/specs/events.spec.js b/test/specs/events.spec.js index 878d64309..7dc0753b0 100644 --- a/test/specs/events.spec.js +++ b/test/specs/events.spec.js @@ -157,104 +157,4 @@ describe('Common', function() { }); }); }); - - describe('events on overlapped annotations', function() { - const chartConfig = { - type: 'scatter', - options: { - animation: false, - scales: { - x: { - display: false, - min: 0, - max: 10 - }, - y: { - display: false, - min: 0, - max: 10 - } - }, - plugins: { - legend: false, - annotation: { - interaction: { - }, - annotations: { - large: { - type: 'box', - xMin: 2, - xMax: 8, - yMin: 2, - yMax: 8, - borderWidth: 0, - }, - small: { - type: 'box', - xMin: 4, - xMax: 6, - yMin: 4, - yMax: 6, - borderWidth: 0, - } - } - } - } - } - }; - const modes = ['nearest', 'point']; - const hookCallsCount = [[1, 1, 1, 0, 1], [1, 2, 2, 1, 2]]; - - for (let i = 0; i < modes.length; i++) { - const mode = modes[i]; - const callsCount = hookCallsCount[i]; - - it(`should detect events on annotations with interaction mode ${mode}`, function(done) { - const enterSpy = jasmine.createSpy('enter'); - const clickSpy = jasmine.createSpy('click'); - const leaveSpy = jasmine.createSpy('leave'); - - chartConfig.options.plugins.annotation.enter = enterSpy; - chartConfig.options.plugins.annotation.click = clickSpy; - chartConfig.options.plugins.annotation.leave = leaveSpy; - chartConfig.options.plugins.annotation.interaction.mode = mode; - - const chart = window.acquireChart(chartConfig); - const large = window.getAnnotationElements(chart)[0]; - const small = window.getAnnotationElements(chart)[1]; - - const event1 = {x: large.x + 1, y: large.y + large.height / 2}; - const event2 = {x: small.x + 1, y: small.y + small.height / 2}; - const click = {x: small.x + small.width / 2, y: small.y + small.height / 2}; - const event3 = {x: small.x2 + 1, y: small.y2 - small.height / 2}; - const event4 = {x: large.x2 + 1, y: large.y2 - large.height / 2}; - - window.triggerMouseEvent(chart, 'mousemove', event1); - window.afterEvent(chart, 'mousemove', function() { - expect(enterSpy.calls.count()).toBe(callsCount[0]); - - window.triggerMouseEvent(chart, 'mousemove', event2); - window.afterEvent(chart, 'mousemove', function() { - expect(enterSpy.calls.count()).toBe(callsCount[1]); - - window.triggerMouseEvent(chart, 'click', click); - window.afterEvent(chart, 'click', function() { - expect(clickSpy.calls.count()).toBe(callsCount[2]); - - window.triggerMouseEvent(chart, 'mousemove', event3); - window.afterEvent(chart, 'mousemove', function() { - expect(leaveSpy.calls.count()).toBe(callsCount[3]); - - window.triggerMouseEvent(chart, 'mousemove', event4); - window.afterEvent(chart, 'mousemove', function() { - expect(leaveSpy.calls.count()).toBe(callsCount[4]); - done(); - }); - }); - }); - }); - }); - }); - } - }); }); diff --git a/test/specs/interaction.spec.js b/test/specs/interaction.spec.js new file mode 100644 index 000000000..9316aff10 --- /dev/null +++ b/test/specs/interaction.spec.js @@ -0,0 +1,113 @@ +describe('Interaction', function() { + + describe('on overlapped annotations', function() { + const chartConfig = { + type: 'scatter', + options: { + animation: false, + scales: { + x: { + display: false, + min: 0, + max: 10 + }, + y: { + display: false, + min: 0, + max: 10 + } + }, + plugins: { + legend: false, + annotation: { + interaction: { + }, + annotations: { + large: { + type: 'box', + xMin: 2, + xMax: 8, + yMin: 2, + yMax: 8, + borderWidth: 0, + }, + small: { + type: 'box', + xMin: 4.5, + xMax: 6, + yMin: 4.5, + yMax: 6, + borderWidth: 0, + } + } + } + } + } + }; + const interactions = [{ + mode: 'nearest', + axes: { + xy: [1, 1, 1, 0, 1] + } + }, { + mode: 'point', + axes: { + xy: [1, 2, 2, 1, 2] + } + }]; + + for (const interaction of interactions) { + const mode = interaction.mode; + for (const axis of Object.keys(interaction.axes)) { + const callsCount = interaction.axes[axis]; + it(`should detect events on annotations with interaction mode ${mode}, axis ${axis}`, function(done) { + const enterSpy = jasmine.createSpy('enter'); + const clickSpy = jasmine.createSpy('click'); + const leaveSpy = jasmine.createSpy('leave'); + + chartConfig.options.plugins.annotation.enter = enterSpy; + chartConfig.options.plugins.annotation.click = clickSpy; + chartConfig.options.plugins.annotation.leave = leaveSpy; + chartConfig.options.plugins.annotation.interaction.mode = mode; + chartConfig.options.plugins.annotation.interaction.axis = axis; + + const chart = window.acquireChart(chartConfig); + const large = window.getAnnotationElements(chart)[0]; + const small = window.getAnnotationElements(chart)[1]; + + const event1 = {x: large.x + 1, y: large.y + large.height / 2}; + const event2 = {x: small.x + 1, y: small.y + small.height / 2}; + const click = {x: small.x + small.width / 2, y: small.y + small.height / 2}; + const event3 = {x: small.x2 + 1, y: small.y2 - small.height / 2}; + const event4 = {x: large.x2 + 1, y: large.y2 - large.height / 2}; + + window.triggerMouseEvent(chart, 'mousemove', event1); + window.afterEvent(chart, 'mousemove', function() { + expect(enterSpy.calls.count()).toBe(callsCount[0]); + + window.triggerMouseEvent(chart, 'mousemove', event2); + window.afterEvent(chart, 'mousemove', function() { + expect(enterSpy.calls.count()).toBe(callsCount[1]); + + window.triggerMouseEvent(chart, 'click', click); + window.afterEvent(chart, 'click', function() { + expect(clickSpy.calls.count()).toBe(callsCount[2]); + + window.triggerMouseEvent(chart, 'mousemove', event3); + window.afterEvent(chart, 'mousemove', function() { + expect(leaveSpy.calls.count()).toBe(callsCount[3]); + + window.triggerMouseEvent(chart, 'mousemove', event4); + window.afterEvent(chart, 'mousemove', function() { + expect(leaveSpy.calls.count()).toBe(callsCount[4]); + done(); + }); + }); + }); + }); + }); + }); + } + } + }); +}); From f64c524744c08f4541a42ab566609544996e13fc Mon Sep 17 00:00:00 2001 From: stockiNail Date: Mon, 31 Jan 2022 18:18:19 +0100 Subject: [PATCH 10/27] adds test cases to box.spec --- src/events.js | 1 + test/specs/box.spec.js | 126 +++++++++++++++++++++++++++++++++ test/specs/interaction.spec.js | 113 ----------------------------- 3 files changed, 127 insertions(+), 113 deletions(-) delete mode 100644 test/specs/interaction.spec.js diff --git a/src/events.js b/src/events.js index 4fb025162..57a1ee1ca 100644 --- a/src/events.js +++ b/src/events.js @@ -8,6 +8,7 @@ export const hooks = clickHooks.concat(moveHooks); export function updateListeners(chart, state, options) { state.listened = false; state.moveListened = false; + state._getElements = getElements; hooks.forEach(hook => { if (typeof options[hook] === 'function') { diff --git a/test/specs/box.spec.js b/test/specs/box.spec.js index 3084c3b7d..1f7e3ef86 100644 --- a/test/specs/box.spec.js +++ b/test/specs/box.spec.js @@ -68,4 +68,130 @@ describe('Box annotation', function() { }); }); }); + + describe('interaction', function() { + const chartConfig = { + type: 'scatter', + options: { + animation: false, + scales: { + x: { + display: false, + min: 0, + max: 10 + }, + y: { + display: false, + min: 0, + max: 10 + } + }, + plugins: { + legend: false, + annotation: { + interaction: { + intersect: true, + }, + annotations: { + large: { + type: 'box', + xMin: 2, + xMax: 8, + yMin: 2, + yMax: 8, + borderWidth: 0, + }, + small: { + type: 'box', + xMin: 4.5, + xMax: 6, + yMin: 4.5, + yMax: 6, + borderWidth: 0, + } + } + } + } + } + }; + + const chart = window.acquireChart(chartConfig); + const state = window['chartjs-plugin-annotation']._getState(chart); + const interactionOpts = chartConfig.options.plugins.annotation.interaction; + const large = window.getAnnotationElements(chart)[0]; + const small = window.getAnnotationElements(chart)[1]; + + const interactions = [{ + mode: 'point', + axes: { + xy: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 0] + } + }, + x: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 0] + } + }, + y: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 0] + } + } + }, + }, { + mode: 'nearest', + axes: { + xy: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 1, 1] + } + }, + x: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 0, 1] + } + }, + y: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 1, 0] + } + } + } + }]; + + it('should return the right amount of annotation elements', function() { + for (const interaction of interactions) { + const mode = interaction.mode; + interactionOpts.mode = mode; + for (const axis of Object.keys(interaction.axes)) { + interactionOpts.axis = axis; + [true, false].forEach(function(intersect) { + interactionOpts.intersect = intersect; + const elementsCounts = interaction.axes[axis].intersect[intersect]; + const points = [{x: large.x + 1, y: large.y + large.height / 2, what: 'enter large'}, + {x: small.x + 1, y: small.y + small.height / 2, what: 'enter small'}, + {x: small.x + small.width / 2, y: small.y + small.height / 2, what: 'click center of small'}, + {x: small.x2 + 1, y: small.y2 - small.height / 2, what: 'leave small'}, + {x: large.x2 + 1, y: large.y2 - large.height / 2, what: 'leave large'}, + {x: large.x + 1, y: large.y - 1, what: 'outside of elements'}]; + + 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}, ${point.what}`).toEqual(elementsCount); + } + }); + } + } + }); + }); }); diff --git a/test/specs/interaction.spec.js b/test/specs/interaction.spec.js deleted file mode 100644 index 9316aff10..000000000 --- a/test/specs/interaction.spec.js +++ /dev/null @@ -1,113 +0,0 @@ -describe('Interaction', function() { - - describe('on overlapped annotations', function() { - const chartConfig = { - type: 'scatter', - options: { - animation: false, - scales: { - x: { - display: false, - min: 0, - max: 10 - }, - y: { - display: false, - min: 0, - max: 10 - } - }, - plugins: { - legend: false, - annotation: { - interaction: { - }, - annotations: { - large: { - type: 'box', - xMin: 2, - xMax: 8, - yMin: 2, - yMax: 8, - borderWidth: 0, - }, - small: { - type: 'box', - xMin: 4.5, - xMax: 6, - yMin: 4.5, - yMax: 6, - borderWidth: 0, - } - } - } - } - } - }; - const interactions = [{ - mode: 'nearest', - axes: { - xy: [1, 1, 1, 0, 1] - } - }, { - mode: 'point', - axes: { - xy: [1, 2, 2, 1, 2] - } - }]; - - for (const interaction of interactions) { - const mode = interaction.mode; - for (const axis of Object.keys(interaction.axes)) { - const callsCount = interaction.axes[axis]; - it(`should detect events on annotations with interaction mode ${mode}, axis ${axis}`, function(done) { - const enterSpy = jasmine.createSpy('enter'); - const clickSpy = jasmine.createSpy('click'); - const leaveSpy = jasmine.createSpy('leave'); - - chartConfig.options.plugins.annotation.enter = enterSpy; - chartConfig.options.plugins.annotation.click = clickSpy; - chartConfig.options.plugins.annotation.leave = leaveSpy; - chartConfig.options.plugins.annotation.interaction.mode = mode; - chartConfig.options.plugins.annotation.interaction.axis = axis; - - const chart = window.acquireChart(chartConfig); - const large = window.getAnnotationElements(chart)[0]; - const small = window.getAnnotationElements(chart)[1]; - - const event1 = {x: large.x + 1, y: large.y + large.height / 2}; - const event2 = {x: small.x + 1, y: small.y + small.height / 2}; - const click = {x: small.x + small.width / 2, y: small.y + small.height / 2}; - const event3 = {x: small.x2 + 1, y: small.y2 - small.height / 2}; - const event4 = {x: large.x2 + 1, y: large.y2 - large.height / 2}; - - window.triggerMouseEvent(chart, 'mousemove', event1); - window.afterEvent(chart, 'mousemove', function() { - expect(enterSpy.calls.count()).toBe(callsCount[0]); - - window.triggerMouseEvent(chart, 'mousemove', event2); - window.afterEvent(chart, 'mousemove', function() { - expect(enterSpy.calls.count()).toBe(callsCount[1]); - - window.triggerMouseEvent(chart, 'click', click); - window.afterEvent(chart, 'click', function() { - expect(clickSpy.calls.count()).toBe(callsCount[2]); - - window.triggerMouseEvent(chart, 'mousemove', event3); - window.afterEvent(chart, 'mousemove', function() { - expect(leaveSpy.calls.count()).toBe(callsCount[3]); - - window.triggerMouseEvent(chart, 'mousemove', event4); - window.afterEvent(chart, 'mousemove', function() { - expect(leaveSpy.calls.count()).toBe(callsCount[4]); - done(); - }); - }); - }); - }); - }); - }); - } - } - }); -}); From 8078441d0825537b64960105fdc2e083098e859c Mon Sep 17 00:00:00 2001 From: stockiNail Date: Tue, 1 Feb 2022 12:09:42 +0100 Subject: [PATCH 11/27] adds ellipse to interaction management --- src/interaction.js | 15 ++++-- src/types/box.js | 10 ++-- src/types/ellipse.js | 16 +++++- test/specs/box.spec.js | 78 +++++++++------------------ test/specs/ellipse.spec.js | 107 ++++++++++++++++++++++++++++++++++++- 5 files changed, 163 insertions(+), 63 deletions(-) diff --git a/src/interaction.js b/src/interaction.js index e6e64f057..da5c3a442 100644 --- a/src/interaction.js +++ b/src/interaction.js @@ -17,7 +17,7 @@ const interaction = { * @param {Object} state - the state of the plugin * @param {ChartEvent} event - the event we are find things at * @param {options} options - interaction options to use - * @return {Element[]} - elements that are found + * @return {Element[]} - elements that are found (only 1 element) */ nearest(state, event, options) { return getNearestItem(state, event, options); @@ -25,6 +25,13 @@ const interaction = { } }; +/** + * Returns all elements that hit test based on the event position + * @param {Object} state - the state of the plugin + * @param {ChartEvent} event - the event we are find things at + * @param {options} options - interaction options to use + * @return {Element[]} - elements that are found + */ export function getElements(state, event, options) { const mode = interaction.modes[options.mode] || interaction.modes.nearest; return mode(state, event, options); @@ -36,11 +43,11 @@ function getIntersectItems(state, event) { function inRangeByAxis(element, event, axis) { if (axis === 'x') { - return element.inXRange(event.x); + return element.inXRange(event.x, event.y); } else if (axis === 'y') { - return element.inYRange(event.y); + return element.inYRange(event.x, event.y); } - return element.inXRange(event.x) || element.inYRange(event.y); + return element.inXRange(event.x, event.y) || element.inYRange(event.x, event.y); } function getPointByAxis(event, center, axis) { diff --git a/src/types/box.js b/src/types/box.js index cce078ddf..f6a9a2316 100644 --- a/src/types/box.js +++ b/src/types/box.js @@ -7,14 +7,16 @@ export default class BoxAnnotation extends Element { return inBoxRange(mouseX, mouseY, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), this.options.borderWidth); } - inXRange(value, useFinalPosition) { + inXRange(mouseX, mouseY, useFinalPosition) { const {x, width} = this.getProps(['x', 'width'], useFinalPosition); - return value >= x && value <= x + width; + const hBorderWidth = this.options.borderWidth / 2; + return mouseX >= x - hBorderWidth && mouseX <= x + width + hBorderWidth; } - inYRange(value, useFinalPosition) { + inYRange(mouseX, mouseY, useFinalPosition) { const {y, height} = this.getProps(['y', 'height'], useFinalPosition); - return value >= y && value <= y + height; + const hBorderWidth = this.options.borderWidth / 2; + return mouseY >= y - hBorderWidth && mouseY <= y + height + hBorderWidth; } getCenterPoint(useFinalPosition) { diff --git a/src/types/ellipse.js b/src/types/ellipse.js index 408c289ae..f50db4a5b 100644 --- a/src/types/ellipse.js +++ b/src/types/ellipse.js @@ -1,6 +1,6 @@ import {Element} from 'chart.js'; import {PI, toRadians} from 'chart.js/helpers'; -import {getRectCenterPoint, getChartRect, setBorderStyle, setShadowStyle} from '../helpers'; +import {getRectCenterPoint, getChartRect, setBorderStyle, setShadowStyle, rotated} from '../helpers'; export default class EllipseAnnotation extends Element { @@ -8,6 +8,20 @@ export default class EllipseAnnotation extends Element { return pointInEllipse({x: mouseX, y: mouseY}, this.getProps(['width', 'height'], useFinalPosition), this.options.rotation, this.options.borderWidth); } + inXRange(mouseX, mouseY, useFinalPosition) { + const {x, x2} = this.getProps(['x', 'x2'], useFinalPosition); + const rotValue = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(), toRadians(-this.options.rotation)); + const hBorderWidth = this.options.borderWidth / 2; + return rotValue.x >= x - hBorderWidth && rotValue.x <= x2 + hBorderWidth; + } + + inYRange(mouseX, mouseY, useFinalPosition) { + const {y, y2} = this.getProps(['y', 'y2'], useFinalPosition); + const rotValue = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); + const hBorderWidth = this.options.borderWidth / 2; + return rotValue.y >= y - hBorderWidth && rotValue.y <= y2 + hBorderWidth; + } + getCenterPoint(useFinalPosition) { return getRectCenterPoint(this.getProps(['x', 'y', 'width', 'height'], useFinalPosition)); } diff --git a/test/specs/box.spec.js b/test/specs/box.spec.js index 1f7e3ef86..68453e087 100644 --- a/test/specs/box.spec.js +++ b/test/specs/box.spec.js @@ -70,56 +70,28 @@ describe('Box annotation', function() { }); describe('interaction', function() { - const chartConfig = { - type: 'scatter', - options: { - animation: false, - scales: { - x: { - display: false, - min: 0, - max: 10 - }, - y: { - display: false, - min: 0, - max: 10 - } - }, - plugins: { - legend: false, - annotation: { - interaction: { - intersect: true, - }, - annotations: { - large: { - type: 'box', - xMin: 2, - xMax: 8, - yMin: 2, - yMax: 8, - borderWidth: 0, - }, - small: { - type: 'box', - xMin: 4.5, - xMax: 6, - yMin: 4.5, - yMax: 6, - borderWidth: 0, - } - } - } - } - } + const outer = { + type: 'box', + xMin: 2, + xMax: 8, + yMin: 2, + yMax: 8, + borderWidth: 0, + }; + const inner = { + type: 'box', + xMin: 4.5, + xMax: 6, + yMin: 4.5, + yMax: 6, + borderWidth: 0, }; - const chart = window.acquireChart(chartConfig); + const chart = window.scatterChart(10, 10, {outer, inner}); const state = window['chartjs-plugin-annotation']._getState(chart); - const interactionOpts = chartConfig.options.plugins.annotation.interaction; - const large = window.getAnnotationElements(chart)[0]; - const small = window.getAnnotationElements(chart)[1]; + const interactionOpts = {}; + const outerEl = window.getAnnotationElements(chart)[0]; + const innerEl = window.getAnnotationElements(chart)[1]; const interactions = [{ mode: 'point', @@ -176,12 +148,12 @@ describe('Box annotation', function() { [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; - const points = [{x: large.x + 1, y: large.y + large.height / 2, what: 'enter large'}, - {x: small.x + 1, y: small.y + small.height / 2, what: 'enter small'}, - {x: small.x + small.width / 2, y: small.y + small.height / 2, what: 'click center of small'}, - {x: small.x2 + 1, y: small.y2 - small.height / 2, what: 'leave small'}, - {x: large.x2 + 1, y: large.y2 - large.height / 2, what: 'leave large'}, - {x: large.x + 1, y: large.y - 1, what: 'outside of elements'}]; + const points = [{x: outerEl.x + 1, y: outerEl.y + outerEl.height / 2, what: 'enter outer'}, + {x: innerEl.x + 1, y: innerEl.y + innerEl.height / 2, what: 'enter inner'}, + {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2, what: 'click center of inner'}, + {x: innerEl.x2 + 1, y: innerEl.y2 - innerEl.height / 2, what: 'leave inner'}, + {x: outerEl.x2 + 1, y: outerEl.y2 - outerEl.height / 2, what: 'leave outer'}, + {x: outerEl.x + 1, y: outerEl.y - 1, what: 'outside of elements'}]; for (let i = 0; i < points.length; i++) { const point = points[i]; diff --git a/test/specs/ellipse.spec.js b/test/specs/ellipse.spec.js index 83a51d3b7..9ff59855c 100644 --- a/test/specs/ellipse.spec.js +++ b/test/specs/ellipse.spec.js @@ -1,8 +1,9 @@ describe('Ellipse annotation', function() { describe('auto', jasmine.fixtures('ellipse')); + const rotated = window.helpers.rotated; + describe('inRange', function() { - const rotated = window.helpers.rotated; for (const rotation of [0, 45, 90, 135, 180, 225, 270, 315]) { const annotation = { @@ -71,4 +72,108 @@ describe('Ellipse annotation', function() { } }); }); + + describe('interaction', function() { + + for (const rotation of [0, 45, 90, 135, 180, 225, 270, 315]) { + const outer = { + type: 'ellipse', + xMin: 3, + xMax: 7, + yMin: 3, + yMax: 7, + borderWidth: 0, + rotation + }; + const inner = { + type: 'ellipse', + xMin: 4.5, + xMax: 6, + yMin: 4.5, + yMax: 6, + borderWidth: 0, + rotation + }; + + const chart = window.scatterChart(10, 10, {outer, inner}); + const state = window['chartjs-plugin-annotation']._getState(chart); + const interactionOpts = {}; + const outerEl = window.getAnnotationElements(chart)[0]; + const innerEl = window.getAnnotationElements(chart)[1]; + + const interactions = [{ + mode: 'point', + axes: { + xy: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 0], + } + }, + x: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 0], + } + }, + y: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 0], + } + } + }, + }, { + mode: 'nearest', + axes: { + xy: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 1, 1] + } + }, + x: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 0, 1] + } + }, + y: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 1, 0] + } + } + }, + }]; + + it('should return the right amount of annotation elements', function() { + for (const interaction of interactions) { + const mode = interaction.mode; + interactionOpts.mode = mode; + for (const axis of Object.keys(interaction.axes)) { + interactionOpts.axis = axis; + [true, false].forEach(function(intersect) { + interactionOpts.intersect = intersect; + const elementsCounts = interaction.axes[axis].intersect[intersect]; + const points = [{x: outerEl.x + 1, y: outerEl.y + outerEl.height / 2, what: 'enter outer', el: outerEl}, + {x: innerEl.x + 1, y: innerEl.y + innerEl.height / 2, what: 'enter inner', el: innerEl}, + {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2, what: 'click center of inner', el: innerEl}, + {x: innerEl.x2 + 1, y: innerEl.y2 - innerEl.height / 2, what: 'leave inner', el: innerEl}, + {x: outerEl.x2 + 1, y: outerEl.y2 - outerEl.height / 2, what: 'leave outer', el: outerEl}, + {x: outerEl.x + 1, y: outerEl.y - 1, what: 'outside of elements', el: outerEl}]; + + for (let i = 0; i < points.length; i++) { + 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}, ${point.what}`).toEqual(elementsCount); + } + }); + } + } + }); + } + }); }); From 0c47496992b8dbf735a617e09f57ef598fe8814e Mon Sep 17 00:00:00 2001 From: stockiNail Date: Tue, 1 Feb 2022 15:16:37 +0100 Subject: [PATCH 12/27] adds label to interaction management --- src/helpers/helpers.core.js | 2 ++ src/interaction.js | 10 +++---- src/types/ellipse.js | 6 ++--- src/types/label.js | 12 +++++++++ src/types/line.js | 9 +++---- test/index.js | 3 ++- test/specs/box.spec.js | 52 +++-------------------------------- test/specs/ellipse.spec.js | 52 +++-------------------------------- test/specs/label.spec.js | 54 +++++++++++++++++++++++++++++++++++++ test/utils.js | 46 +++++++++++++++++++++++++++++++ 10 files changed, 134 insertions(+), 112 deletions(-) diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index 5af154f0a..242d2d397 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -1,3 +1,5 @@ +export const EPSILON = 0.001; + export const clamp = (x, from, to) => Math.min(to, Math.max(from, x)); export function clampAll(obj, from, to) { diff --git a/src/interaction.js b/src/interaction.js index da5c3a442..37d296055 100644 --- a/src/interaction.js +++ b/src/interaction.js @@ -38,16 +38,16 @@ export function getElements(state, event, options) { } function getIntersectItems(state, event) { - return state.visibleElements.filter((element) => element.inRange(event.x, event.y)); + return state.visibleElements.filter((element) => element.inRange(event.x, event.y, true)); } function inRangeByAxis(element, event, axis) { if (axis === 'x') { - return element.inXRange(event.x, event.y); + return element.inXRange(event.x, event.y, true); } else if (axis === 'y') { - return element.inYRange(event.x, event.y); + return element.inYRange(event.x, event.y, true); } - return element.inXRange(event.x, event.y) || element.inYRange(event.x, event.y); + return element.inXRange(event.x, event.y, true) || element.inYRange(event.x, event.y, true); } function getPointByAxis(event, center, axis) { @@ -64,7 +64,7 @@ function getNearestItem(state, event, options) { let minDistance = Number.POSITIVE_INFINITY; return state.visibleElements - .filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, axis)) + .filter((element) => options.intersect ? element.inRange(event.x, event.y, true) : inRangeByAxis(element, event, axis)) .reduce((nearestItems, element) => { const center = element.getCenterPoint(); const evenPoint = getPointByAxis(event, center, axis); diff --git a/src/types/ellipse.js b/src/types/ellipse.js index f50db4a5b..d7e048b22 100644 --- a/src/types/ellipse.js +++ b/src/types/ellipse.js @@ -1,6 +1,6 @@ import {Element} from 'chart.js'; import {PI, toRadians} from 'chart.js/helpers'; -import {getRectCenterPoint, getChartRect, setBorderStyle, setShadowStyle, rotated} from '../helpers'; +import {EPSILON, getRectCenterPoint, getChartRect, setBorderStyle, setShadowStyle, rotated} from '../helpers'; export default class EllipseAnnotation extends Element { @@ -10,9 +10,9 @@ export default class EllipseAnnotation extends Element { inXRange(mouseX, mouseY, useFinalPosition) { const {x, x2} = this.getProps(['x', 'x2'], useFinalPosition); - const rotValue = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(), toRadians(-this.options.rotation)); + const rotValue = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); const hBorderWidth = this.options.borderWidth / 2; - return rotValue.x >= x - hBorderWidth && rotValue.x <= x2 + hBorderWidth; + return rotValue.x >= x - hBorderWidth - EPSILON && rotValue.x <= x2 + hBorderWidth + EPSILON; } inYRange(mouseX, mouseY, useFinalPosition) { diff --git a/src/types/label.js b/src/types/label.js index bb491e418..a9e037c29 100644 --- a/src/types/label.js +++ b/src/types/label.js @@ -8,6 +8,18 @@ export default class LabelAnnotation extends Element { return inBoxRange(mouseX, mouseY, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), this.options.borderWidth); } + inXRange(mouseX, mouseY, useFinalPosition) { + const {x, width} = this.getProps(['x', 'width'], useFinalPosition); + const hBorderWidth = this.options.borderWidth / 2; + return mouseX >= x - hBorderWidth && mouseX <= x + width + hBorderWidth; + } + + inYRange(mouseX, mouseY, useFinalPosition) { + const {y, height} = this.getProps(['y', 'height'], useFinalPosition); + const hBorderWidth = this.options.borderWidth / 2; + return mouseY >= y - hBorderWidth && mouseY <= y + height + hBorderWidth; + } + getCenterPoint(useFinalPosition) { return getRectCenterPoint(this.getProps(['x', 'y', 'width', 'height'], useFinalPosition)); } diff --git a/src/types/line.js b/src/types/line.js index 24e90fab1..e51923eea 100644 --- a/src/types/line.js +++ b/src/types/line.js @@ -1,12 +1,11 @@ import {Element} from 'chart.js'; import {PI, toRadians, toPadding} from 'chart.js/helpers'; -import {clamp, scaleValue, rotated, drawBox, drawLabel, measureLabelSize, getRelativePosition, setBorderStyle, setShadowStyle} from '../helpers'; +import {EPSILON, clamp, scaleValue, rotated, drawBox, drawLabel, measureLabelSize, getRelativePosition, setBorderStyle, setShadowStyle} from '../helpers'; const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)}); const interpolateX = (y, p1, p2) => pointInLine(p1, p2, Math.abs((y - p1.y) / (p2.y - p1.y))).x; const interpolateY = (x, p1, p2) => pointInLine(p1, p2, Math.abs((x - p1.x) / (p2.x - p1.x))).y; const sqr = v => v * v; -const defaultEpsilon = 0.001; function isLineInArea({x, y, x2, y2}, {top, right, bottom, left}) { return !( @@ -46,7 +45,7 @@ function limitLineToArea(p1, p2, area) { export default class LineAnnotation extends Element { // TODO: make private in v2 - intersects(x, y, epsilon = defaultEpsilon, useFinalPosition) { + intersects(x, y, epsilon = EPSILON, useFinalPosition) { // Adapted from https://stackoverflow.com/a/6853926/25507 const {x: x1, y: y1, x2, y2} = this.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition); const dx = x2 - x1; @@ -91,8 +90,8 @@ export default class LineAnnotation extends Element { const hBorderWidth = this.options.label.borderWidth / 2 || 0; const w2 = labelWidth / 2 + hBorderWidth; const h2 = labelHeight / 2 + hBorderWidth; - return x >= labelX - w2 - defaultEpsilon && x <= labelX + w2 + defaultEpsilon && - y >= labelY - h2 - defaultEpsilon && y <= labelY + h2 + defaultEpsilon; + return x >= labelX - w2 - EPSILON && x <= labelX + w2 + EPSILON && + y >= labelY - h2 - EPSILON && y <= labelY + h2 + EPSILON; } inRange(mouseX, mouseY, useFinalPosition) { diff --git a/test/index.js b/test/index.js index 12e734e7d..47a0dcd4c 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} from './utils'; +import {createCanvas, getAnnotationElements, scatterChart, stringifyObject, interactionData} from './utils'; import * as helpers from '../src/helpers'; window.helpers = helpers; @@ -15,6 +15,7 @@ window.createCanvas = createCanvas; window.getAnnotationElements = getAnnotationElements; window.scatterChart = scatterChart; window.stringifyObject = stringifyObject; +window.interactionData = interactionData; jasmine.fixtures = specsFromFixtures; diff --git a/test/specs/box.spec.js b/test/specs/box.spec.js index 68453e087..122ea7c59 100644 --- a/test/specs/box.spec.js +++ b/test/specs/box.spec.js @@ -93,54 +93,8 @@ describe('Box annotation', function() { const outerEl = window.getAnnotationElements(chart)[0]; const innerEl = window.getAnnotationElements(chart)[1]; - const interactions = [{ - mode: 'point', - axes: { - xy: { - intersect: { - true: [1, 2, 2, 1, 0, 0], - false: [1, 2, 2, 1, 0, 0] - } - }, - x: { - intersect: { - true: [1, 2, 2, 1, 0, 0], - false: [1, 2, 2, 1, 0, 0] - } - }, - y: { - intersect: { - true: [1, 2, 2, 1, 0, 0], - false: [1, 2, 2, 1, 0, 0] - } - } - }, - }, { - mode: 'nearest', - axes: { - xy: { - intersect: { - true: [1, 1, 1, 1, 0, 0], - false: [1, 1, 1, 1, 1, 1] - } - }, - x: { - intersect: { - true: [1, 1, 1, 1, 0, 0], - false: [1, 1, 1, 1, 0, 1] - } - }, - y: { - intersect: { - true: [1, 1, 1, 1, 0, 0], - false: [1, 1, 1, 1, 1, 0] - } - } - } - }]; - it('should return the right amount of annotation elements', function() { - for (const interaction of interactions) { + for (const interaction of window.interactionData) { const mode = interaction.mode; interactionOpts.mode = mode; for (const axis of Object.keys(interaction.axes)) { @@ -148,8 +102,8 @@ describe('Box annotation', function() { [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; - const points = [{x: outerEl.x + 1, y: outerEl.y + outerEl.height / 2, what: 'enter outer'}, - {x: innerEl.x + 1, y: innerEl.y + innerEl.height / 2, what: 'enter inner'}, + const points = [{x: outerEl.x, y: outerEl.y + outerEl.height / 2, what: 'enter outer'}, + {x: innerEl.x, y: innerEl.y + innerEl.height / 2, what: 'enter inner'}, {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2, what: 'click center of inner'}, {x: innerEl.x2 + 1, y: innerEl.y2 - innerEl.height / 2, what: 'leave inner'}, {x: outerEl.x2 + 1, y: outerEl.y2 - outerEl.height / 2, what: 'leave outer'}, diff --git a/test/specs/ellipse.spec.js b/test/specs/ellipse.spec.js index 9ff59855c..d5430e2b0 100644 --- a/test/specs/ellipse.spec.js +++ b/test/specs/ellipse.spec.js @@ -101,54 +101,8 @@ describe('Ellipse annotation', function() { const outerEl = window.getAnnotationElements(chart)[0]; const innerEl = window.getAnnotationElements(chart)[1]; - const interactions = [{ - mode: 'point', - axes: { - xy: { - intersect: { - true: [1, 2, 2, 1, 0, 0], - false: [1, 2, 2, 1, 0, 0], - } - }, - x: { - intersect: { - true: [1, 2, 2, 1, 0, 0], - false: [1, 2, 2, 1, 0, 0], - } - }, - y: { - intersect: { - true: [1, 2, 2, 1, 0, 0], - false: [1, 2, 2, 1, 0, 0], - } - } - }, - }, { - mode: 'nearest', - axes: { - xy: { - intersect: { - true: [1, 1, 1, 1, 0, 0], - false: [1, 1, 1, 1, 1, 1] - } - }, - x: { - intersect: { - true: [1, 1, 1, 1, 0, 0], - false: [1, 1, 1, 1, 0, 1] - } - }, - y: { - intersect: { - true: [1, 1, 1, 1, 0, 0], - false: [1, 1, 1, 1, 1, 0] - } - } - }, - }]; - it('should return the right amount of annotation elements', function() { - for (const interaction of interactions) { + for (const interaction of window.interactionData) { const mode = interaction.mode; interactionOpts.mode = mode; for (const axis of Object.keys(interaction.axes)) { @@ -156,8 +110,8 @@ describe('Ellipse annotation', function() { [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; - const points = [{x: outerEl.x + 1, y: outerEl.y + outerEl.height / 2, what: 'enter outer', el: outerEl}, - {x: innerEl.x + 1, y: innerEl.y + innerEl.height / 2, what: 'enter inner', el: innerEl}, + const points = [{x: outerEl.x, y: outerEl.y + outerEl.height / 2, what: 'enter outer', el: outerEl}, + {x: innerEl.x, y: innerEl.y + innerEl.height / 2, what: 'enter inner', el: innerEl}, {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2, what: 'click center of inner', el: innerEl}, {x: innerEl.x2 + 1, y: innerEl.y2 - innerEl.height / 2, what: 'leave inner', el: innerEl}, {x: outerEl.x2 + 1, y: outerEl.y2 - outerEl.height / 2, what: 'leave outer', el: outerEl}, diff --git a/test/specs/label.spec.js b/test/specs/label.spec.js index 8edb68778..e6348758f 100644 --- a/test/specs/label.spec.js +++ b/test/specs/label.spec.js @@ -44,4 +44,58 @@ describe('Label annotation', function() { } }); }); + + describe('interaction', function() { + const outer = { + type: 'label', + xMin: 3, + xMax: 7, + yMin: 3, + yMax: 7, + content: ['outer label row 1', 'outer label row 2', 'outer label row 3'], + backgroundColor: 'transparent', + borderWidth: 0 + }; + const inner = { + type: 'label', + xMin: 4, + xMax: 6, + yMin: 4, + yMax: 6, + borderWidth: 0 + }; + + const chart = window.scatterChart(10, 10, {outer, inner}); + const state = window['chartjs-plugin-annotation']._getState(chart); + const interactionOpts = {}; + const outerEl = window.getAnnotationElements(chart)[0]; + const innerEl = window.getAnnotationElements(chart)[1]; + + it('should return the right amount of annotation elements', function() { + for (const interaction of window.interactionData) { + const mode = interaction.mode; + interactionOpts.mode = mode; + for (const axis of Object.keys(interaction.axes)) { + interactionOpts.axis = axis; + [true].forEach(function(intersect) { + interactionOpts.intersect = intersect; + const elementsCounts = interaction.axes[axis].intersect[intersect]; + const points = [{x: outerEl.x, y: outerEl.y + outerEl.height / 2, what: 'enter outer'}, + {x: innerEl.x, y: innerEl.y + innerEl.height / 2, what: 'enter inner'}, + {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2, what: 'click center of inner'}, + {x: innerEl.x + innerEl.width + 1, y: innerEl.y + innerEl.height / 2, what: 'leave inner'}, + {x: outerEl.x + outerEl.width + 1, y: outerEl.y + outerEl.height / 2, what: 'leave outer'}, + {x: outerEl.x + 1, y: outerEl.y - 1, what: 'outside of elements'}]; + + 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}, ${point.what}`).toEqual(elementsCount); + } + }); + } + } + }); + }); }); diff --git a/test/utils.js b/test/utils.js index 2008dc220..a5b049f8d 100644 --- a/test/utils.js +++ b/test/utils.js @@ -53,3 +53,49 @@ function keepInf(key, value) { export function stringifyObject(obj) { return JSON.stringify(obj, keepInf).replaceAll('"', '').replaceAll(':', ': ').replaceAll(',', ', '); } + +export const interactionData = [{ + mode: 'point', + axes: { + xy: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 0] + } + }, + x: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 0] + } + }, + y: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 0] + } + } + }, +}, { + mode: 'nearest', + axes: { + xy: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 1, 1] + } + }, + x: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 0, 1] + } + }, + y: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 1, 0] + } + } + } +}]; From 3f314c96b0f9758ca067c868870a5759ab0c2d34 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Tue, 1 Feb 2022 16:17:52 +0100 Subject: [PATCH 13/27] adds point to interaction management --- src/types/point.js | 12 ++++++++++ test/specs/label.spec.js | 12 ++++++---- test/specs/point.spec.js | 50 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/types/point.js b/src/types/point.js index 4eb7d8250..38a43c666 100644 --- a/src/types/point.js +++ b/src/types/point.js @@ -9,6 +9,18 @@ export default class PointAnnotation extends Element { return inPointRange({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), width / 2, this.options.borderWidth); } + inXRange(mouseX, mouseY, useFinalPosition) { + const {x, width} = this.getProps(['x', 'width'], useFinalPosition); + const hBorderWidth = this.options.borderWidth / 2; + return mouseX >= x - width / 2 - hBorderWidth && mouseX <= x + width / 2 + hBorderWidth; + } + + inYRange(mouseX, mouseY, useFinalPosition) { + const {y, height} = this.getProps(['y', 'height'], useFinalPosition); + const hBorderWidth = this.options.borderWidth / 2; + return mouseY >= y - height / 2 - hBorderWidth && mouseY <= y + height / 2 + hBorderWidth; + } + getCenterPoint(useFinalPosition) { return getElementCenterPoint(this, useFinalPosition); } diff --git a/test/specs/label.spec.js b/test/specs/label.spec.js index e6348758f..744f60266 100644 --- a/test/specs/label.spec.js +++ b/test/specs/label.spec.js @@ -48,10 +48,10 @@ describe('Label annotation', function() { describe('interaction', function() { const outer = { type: 'label', - xMin: 3, - xMax: 7, - yMin: 3, - yMax: 7, + xMin: 2, + xMax: 8, + yMin: 2, + yMax: 8, content: ['outer label row 1', 'outer label row 2', 'outer label row 3'], backgroundColor: 'transparent', borderWidth: 0 @@ -62,6 +62,8 @@ describe('Label annotation', function() { xMax: 6, yMin: 4, yMax: 6, + content: ['inner label 1', 'inner label 2'], + backgroundColor: 'transparent', borderWidth: 0 }; @@ -77,7 +79,7 @@ describe('Label annotation', function() { interactionOpts.mode = mode; for (const axis of Object.keys(interaction.axes)) { interactionOpts.axis = axis; - [true].forEach(function(intersect) { + [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; const points = [{x: outerEl.x, y: outerEl.y + outerEl.height / 2, what: 'enter outer'}, diff --git a/test/specs/point.spec.js b/test/specs/point.spec.js index 1d612cc50..abc7803f4 100644 --- a/test/specs/point.spec.js +++ b/test/specs/point.spec.js @@ -74,4 +74,54 @@ describe('Point annotation', function() { }); }); }); + + describe('interaction', function() { + const outer = { + type: 'point', + xValue: 5, + yValue: 5, + radius: 40, + borderWidth: 0 + }; + const inner = { + type: 'point', + xValue: 5, + yValue: 5, + radius: 20, + borderWidth: 0 + }; + + const chart = window.scatterChart(10, 10, {outer, inner}); + const state = window['chartjs-plugin-annotation']._getState(chart); + const interactionOpts = {}; + const outerEl = window.getAnnotationElements(chart)[0]; + const innerEl = window.getAnnotationElements(chart)[1]; + + it('should return the right amount of annotation elements', function() { + for (const interaction of window.interactionData) { + const mode = interaction.mode; + interactionOpts.mode = mode; + for (const axis of Object.keys(interaction.axes)) { + interactionOpts.axis = axis; + [true, false].forEach(function(intersect) { + interactionOpts.intersect = intersect; + const elementsCounts = interaction.axes[axis].intersect[intersect]; + const points = [{x: outerEl.x - outerEl.width / 2, y: outerEl.y, what: 'enter outer'}, + {x: innerEl.x - innerEl.width / 2, y: innerEl.y, what: 'enter inner'}, + {x: innerEl.x, y: innerEl.y, what: 'click center of inner'}, + {x: innerEl.x + innerEl.width / 2 + 1, y: innerEl.y, what: 'leave inner'}, + {x: outerEl.x + outerEl.width / 2 + 1, y: outerEl.y, what: 'leave outer'}, + {x: outerEl.x + 1, y: outerEl.y - outerEl.height / 2 - 1, what: 'outside of elements'}]; + + 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}, ${point.what}`).toEqual(elementsCount); + } + }); + } + } + }); + }); }); From d4f2af562785cba5658c72adebffbc7ffd396f49 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Tue, 1 Feb 2022 17:50:08 +0100 Subject: [PATCH 14/27] adds polygon to interaction management --- src/types/polygon.js | 21 ++++++++++++-- test/specs/box.spec.js | 4 +-- test/specs/polygon.spec.js | 59 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/src/types/polygon.js b/src/types/polygon.js index 11421f0d1..013c3c46c 100644 --- a/src/types/polygon.js +++ b/src/types/polygon.js @@ -1,12 +1,28 @@ import {Element} from 'chart.js'; -import {PI, RAD_PER_DEG} from 'chart.js/helpers'; -import {setBorderStyle, resolvePointPosition, getElementCenterPoint, setShadowStyle} from '../helpers'; +import {PI, RAD_PER_DEG, toRadians} from 'chart.js/helpers'; +import {setBorderStyle, resolvePointPosition, getElementCenterPoint, setShadowStyle, rotated} from '../helpers'; export default class PolygonAnnotation extends Element { inRange(mouseX, mouseY, useFinalPosition) { return this.options.radius >= 0.1 && this.elements.length > 1 && pointIsInPolygon(this.elements, mouseX, mouseY, useFinalPosition); } + inXRange(mouseX, mouseY, useFinalPosition) { + const rotValue = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); + const allX = this.elements.map((point) => point.bX); + const x = Math.min(...allX); + const x2 = Math.max(...allX); + return rotValue.x >= x && rotValue.x <= x2; + } + + inYRange(mouseX, mouseY, useFinalPosition) { + const rotValue = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); + const allY = this.elements.map((point) => point.bY); + const y = Math.min(...allY); + const y2 = Math.max(...allY); + return rotValue.y >= y && rotValue.y <= y2; + } + getCenterPoint(useFinalPosition) { return getElementCenterPoint(this, useFinalPosition); } @@ -100,7 +116,6 @@ PolygonAnnotation.defaultRoutes = { backgroundColor: 'color' }; - function pointIsInPolygon(points, x, y, useFinalPosition) { let isInside = false; let A = points[points.length - 1].getProps(['bX', 'bY'], useFinalPosition); diff --git a/test/specs/box.spec.js b/test/specs/box.spec.js index 122ea7c59..4158741dc 100644 --- a/test/specs/box.spec.js +++ b/test/specs/box.spec.js @@ -76,7 +76,7 @@ describe('Box annotation', function() { xMax: 8, yMin: 2, yMax: 8, - borderWidth: 0, + borderWidth: 0 }; const inner = { type: 'box', @@ -84,7 +84,7 @@ describe('Box annotation', function() { xMax: 6, yMin: 4.5, yMax: 6, - borderWidth: 0, + borderWidth: 0 }; const chart = window.scatterChart(10, 10, {outer, inner}); diff --git a/test/specs/polygon.spec.js b/test/specs/polygon.spec.js index 1612bc621..ebca963fd 100644 --- a/test/specs/polygon.spec.js +++ b/test/specs/polygon.spec.js @@ -112,4 +112,63 @@ describe('Polygon annotation', function() { }); }); }); + + describe('interaction', function() { + const rotated = window.helpers.rotated; + + for (const rotation of [0, 90, 180, 270]) { + const outer = { + type: 'polygon', + xValue: 5, + yValue: 5, + radius: 50, + sides: 4, + borderWidth: 1, + rotation + }; + const inner = { + type: 'polygon', + xValue: 5, + yValue: 5, + radius: 25, + sides: 4, + borderWidth: 1, + rotation + }; + + const chart = window.scatterChart(10, 10, {outer, inner}); + const state = window['chartjs-plugin-annotation']._getState(chart); + const interactionOpts = {}; + const outerEl = window.getAnnotationElements(chart)[0]; + const innerEl = window.getAnnotationElements(chart)[1]; + + it('should return the right amount of annotation elements', function() { + for (const interaction of window.interactionData) { + const mode = interaction.mode; + interactionOpts.mode = mode; + for (const axis of Object.keys(interaction.axes)) { + interactionOpts.axis = axis; + [true, false].forEach(function(intersect) { + interactionOpts.intersect = intersect; + const elementsCounts = interaction.axes[axis].intersect[intersect]; + const points = [{x: outerEl.x - outerEl.width / 2, y: outerEl.y, what: 'enter outer', el: outerEl}, + {x: innerEl.x - innerEl.width / 2, y: innerEl.y, what: 'enter inner', el: innerEl}, + {x: innerEl.x, y: innerEl.y, what: 'click center of inner', el: innerEl}, + {x: innerEl.x + innerEl.width / 2 + 1, y: innerEl.y, what: 'leave inner', el: innerEl}, + {x: outerEl.x + outerEl.width / 2 + 1, y: outerEl.y, what: 'leave outer', el: outerEl}, + {x: outerEl.x + 1, y: outerEl.y - outerEl.height / 2 - 1, what: 'outside of elements', el: outerEl}]; + + for (let i = 0; i < points.length; i++) { + 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}, ${point.what}`).toEqual(elementsCount); + } + }); + } + } + }); + } + }); }); From 308bd906981e4b6dddc00daf745c40d1bdd14eb4 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Wed, 2 Feb 2022 10:51:55 +0100 Subject: [PATCH 15/27] should fix CC on box --- src/types/box.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/types/box.js b/src/types/box.js index f6a9a2316..7de281593 100644 --- a/src/types/box.js +++ b/src/types/box.js @@ -8,15 +8,13 @@ export default class BoxAnnotation extends Element { } inXRange(mouseX, mouseY, useFinalPosition) { - const {x, width} = this.getProps(['x', 'width'], useFinalPosition); - const hBorderWidth = this.options.borderWidth / 2; - return mouseX >= x - hBorderWidth && mouseX <= x + width + hBorderWidth; + const {x: start, width: size} = this.getProps(['x', 'width'], useFinalPosition); + return inRangeByCoord(mouseX, start, size, this.options.borderWidth); } inYRange(mouseX, mouseY, useFinalPosition) { - const {y, height} = this.getProps(['y', 'height'], useFinalPosition); - const hBorderWidth = this.options.borderWidth / 2; - return mouseY >= y - hBorderWidth && mouseY <= y + height + hBorderWidth; + const {y: start, height: size} = this.getProps(['y', 'height'], useFinalPosition); + return inRangeByCoord(mouseY, start, size, this.options.borderWidth); } getCenterPoint(useFinalPosition) { @@ -114,6 +112,11 @@ BoxAnnotation.descriptors = { } }; +function inRangeByCoord(value, start, size, borderWidth) { + const hBorderWidth = borderWidth / 2; + return value >= start - hBorderWidth && value <= start + size + hBorderWidth; +} + function calculateX(box, labelSize, position, padding) { const {x: start, x2: end, width: size, options} = box; const {xAdjust: adjust, borderWidth} = options.label; From 159f15555bca754d5b2f74dac50cc076ab18f1eb Mon Sep 17 00:00:00 2001 From: stockiNail Date: Wed, 2 Feb 2022 16:16:09 +0100 Subject: [PATCH 16/27] removes inX/YRange going to a unique method inRange --- src/helpers/helpers.core.js | 16 ++++++++++------ src/interaction.js | 12 +++++------- src/types/box.js | 18 ++---------------- src/types/ellipse.js | 27 +++++++++++---------------- src/types/label.js | 16 ++-------------- src/types/point.js | 24 +++++++++--------------- src/types/polygon.js | 26 +++++++++----------------- 7 files changed, 48 insertions(+), 91 deletions(-) diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index 242d2d397..6221201dd 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -17,12 +17,16 @@ export function inPointRange(point, center, radius, borderWidth) { return (Math.pow(point.x - center.x, 2) + Math.pow(point.y - center.y, 2)) <= Math.pow(radius + hBorderWidth, 2); } -export function inBoxRange(mouseX, mouseY, {x, y, width, height}, borderWidth) { - const hBorderWidth = borderWidth / 2 || 0; - return mouseX >= x - hBorderWidth && - mouseX <= x + width + hBorderWidth && - mouseY >= y - hBorderWidth && - mouseY <= y + height + hBorderWidth; +export function inBoxRange({mouseX, mouseY}, {x, y, width, height}, axis, borderWidth) { + const hBorderWidth = borderWidth / 2; + const inRangeX = mouseX >= x - hBorderWidth && mouseX <= x + width + hBorderWidth; + const inRangeY = mouseY >= y - hBorderWidth && mouseY <= y + height + hBorderWidth; + if (axis === 'x') { + return inRangeX; + } else if (axis === 'y') { + return inRangeY; + } + return inRangeX && inRangeY; } export function getElementCenterPoint(element, useFinalPosition) { diff --git a/src/interaction.js b/src/interaction.js index 37d296055..29ae24068 100644 --- a/src/interaction.js +++ b/src/interaction.js @@ -38,16 +38,14 @@ export function getElements(state, event, options) { } function getIntersectItems(state, event) { - return state.visibleElements.filter((element) => element.inRange(event.x, event.y, true)); + return state.visibleElements.filter((element) => element.inRange(event.x, event.y)); } function inRangeByAxis(element, event, axis) { - if (axis === 'x') { - return element.inXRange(event.x, event.y, true); - } else if (axis === 'y') { - return element.inYRange(event.x, event.y, true); + if (axis !== 'x' && axis !== 'y') { + return element.inRange(event.x, event.y, 'x', true) || element.inRange(event.x, event.y, 'y', true); } - return element.inXRange(event.x, event.y, true) || element.inYRange(event.x, event.y, true); + return element.inRange(event.x, event.y, axis, true); } function getPointByAxis(event, center, axis) { @@ -64,7 +62,7 @@ function getNearestItem(state, event, options) { let minDistance = Number.POSITIVE_INFINITY; return state.visibleElements - .filter((element) => options.intersect ? element.inRange(event.x, event.y, true) : inRangeByAxis(element, event, axis)) + .filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, axis)) .reduce((nearestItems, element) => { const center = element.getCenterPoint(); const evenPoint = getPointByAxis(event, center, axis); diff --git a/src/types/box.js b/src/types/box.js index 7de281593..e3e6240b1 100644 --- a/src/types/box.js +++ b/src/types/box.js @@ -3,18 +3,9 @@ import {toPadding} from 'chart.js/helpers'; import {drawBox, drawLabel, getRelativePosition, measureLabelSize, getRectCenterPoint, getChartRect, toPosition, inBoxRange} from '../helpers'; export default class BoxAnnotation extends Element { - inRange(mouseX, mouseY, useFinalPosition) { - return inBoxRange(mouseX, mouseY, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), this.options.borderWidth); - } - - inXRange(mouseX, mouseY, useFinalPosition) { - const {x: start, width: size} = this.getProps(['x', 'width'], useFinalPosition); - return inRangeByCoord(mouseX, start, size, this.options.borderWidth); - } - inYRange(mouseX, mouseY, useFinalPosition) { - const {y: start, height: size} = this.getProps(['y', 'height'], useFinalPosition); - return inRangeByCoord(mouseY, start, size, this.options.borderWidth); + inRange(mouseX, mouseY, axis, useFinalPosition) { + return inBoxRange({mouseX, mouseY}, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), axis, this.options.borderWidth); } getCenterPoint(useFinalPosition) { @@ -112,11 +103,6 @@ BoxAnnotation.descriptors = { } }; -function inRangeByCoord(value, start, size, borderWidth) { - const hBorderWidth = borderWidth / 2; - return value >= start - hBorderWidth && value <= start + size + hBorderWidth; -} - function calculateX(box, labelSize, position, padding) { const {x: start, x2: end, width: size, options} = box; const {xAdjust: adjust, borderWidth} = options.label; diff --git a/src/types/ellipse.js b/src/types/ellipse.js index d7e048b22..27e0406b4 100644 --- a/src/types/ellipse.js +++ b/src/types/ellipse.js @@ -4,22 +4,17 @@ import {EPSILON, getRectCenterPoint, getChartRect, setBorderStyle, setShadowStyl export default class EllipseAnnotation extends Element { - inRange(mouseX, mouseY, useFinalPosition) { - return pointInEllipse({x: mouseX, y: mouseY}, this.getProps(['width', 'height'], useFinalPosition), this.options.rotation, this.options.borderWidth); - } - - inXRange(mouseX, mouseY, useFinalPosition) { - const {x, x2} = this.getProps(['x', 'x2'], useFinalPosition); - const rotValue = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); - const hBorderWidth = this.options.borderWidth / 2; - return rotValue.x >= x - hBorderWidth - EPSILON && rotValue.x <= x2 + hBorderWidth + EPSILON; - } - - inYRange(mouseX, mouseY, useFinalPosition) { - const {y, y2} = this.getProps(['y', 'y2'], useFinalPosition); - const rotValue = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); - const hBorderWidth = this.options.borderWidth / 2; - return rotValue.y >= y - hBorderWidth && rotValue.y <= y2 + hBorderWidth; + inRange(mouseX, mouseY, axis, useFinalPosition) { + const {x, y, x2, y2} = this.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition); + const rotation = this.options.rotation; + const borderWidth = this.options.borderWidth; + if (axis !== 'x' && axis !== 'y') { + return pointInEllipse({x: mouseX, y: mouseY}, this, rotation, borderWidth); + } + const hBorderWidth = borderWidth / 2; + const limit = axis === 'y' ? {start: y, end: y2} : {start: x, end: x2}; + const rotatedPoint = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-rotation)); + return rotatedPoint[axis] >= limit.start - hBorderWidth - EPSILON && rotatedPoint[axis] <= limit.end + hBorderWidth + EPSILON; } getCenterPoint(useFinalPosition) { diff --git a/src/types/label.js b/src/types/label.js index a9e037c29..539c6b19e 100644 --- a/src/types/label.js +++ b/src/types/label.js @@ -4,20 +4,8 @@ import {Element} from 'chart.js'; export default class LabelAnnotation extends Element { - inRange(mouseX, mouseY, useFinalPosition) { - return inBoxRange(mouseX, mouseY, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), this.options.borderWidth); - } - - inXRange(mouseX, mouseY, useFinalPosition) { - const {x, width} = this.getProps(['x', 'width'], useFinalPosition); - const hBorderWidth = this.options.borderWidth / 2; - return mouseX >= x - hBorderWidth && mouseX <= x + width + hBorderWidth; - } - - inYRange(mouseX, mouseY, useFinalPosition) { - const {y, height} = this.getProps(['y', 'height'], useFinalPosition); - const hBorderWidth = this.options.borderWidth / 2; - return mouseY >= y - hBorderWidth && mouseY <= y + height + hBorderWidth; + inRange(mouseX, mouseY, axis, useFinalPosition) { + return inBoxRange({mouseX, mouseY}, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), axis, this.options.borderWidth); } getCenterPoint(useFinalPosition) { diff --git a/src/types/point.js b/src/types/point.js index 38a43c666..5dcdf7b15 100644 --- a/src/types/point.js +++ b/src/types/point.js @@ -4,21 +4,15 @@ import {inPointRange, getElementCenterPoint, resolvePointPosition, setBorderStyl export default class PointAnnotation extends Element { - inRange(mouseX, mouseY, useFinalPosition) { - const {width} = this.getProps(['width'], useFinalPosition); - return inPointRange({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), width / 2, this.options.borderWidth); - } - - inXRange(mouseX, mouseY, useFinalPosition) { - const {x, width} = this.getProps(['x', 'width'], useFinalPosition); - const hBorderWidth = this.options.borderWidth / 2; - return mouseX >= x - width / 2 - hBorderWidth && mouseX <= x + width / 2 + hBorderWidth; - } - - inYRange(mouseX, mouseY, useFinalPosition) { - const {y, height} = this.getProps(['y', 'height'], useFinalPosition); - const hBorderWidth = this.options.borderWidth / 2; - return mouseY >= y - height / 2 - hBorderWidth && mouseY <= y + height / 2 + hBorderWidth; + inRange(mouseX, mouseY, axis, useFinalPosition) { + const {x, y, width, height} = this.getProps(['x', 'y', 'width', 'height'], useFinalPosition); + const borderWidth = this.options.borderWidth; + if (axis !== 'x' && axis !== 'y') { + return inPointRange({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), width / 2, borderWidth); + } + const hBorderWidth = borderWidth / 2; + const limit = axis === 'y' ? {start: y, size: height, value: mouseY} : {start: x, size: width, value: mouseX}; + return limit.value >= limit.start - limit.size / 2 - hBorderWidth && limit.value <= limit.start + limit.size / 2 + hBorderWidth; } getCenterPoint(useFinalPosition) { diff --git a/src/types/polygon.js b/src/types/polygon.js index 013c3c46c..ec1868491 100644 --- a/src/types/polygon.js +++ b/src/types/polygon.js @@ -3,24 +3,16 @@ import {PI, RAD_PER_DEG, toRadians} from 'chart.js/helpers'; import {setBorderStyle, resolvePointPosition, getElementCenterPoint, setShadowStyle, rotated} from '../helpers'; export default class PolygonAnnotation extends Element { - inRange(mouseX, mouseY, useFinalPosition) { - return this.options.radius >= 0.1 && this.elements.length > 1 && pointIsInPolygon(this.elements, mouseX, mouseY, useFinalPosition); - } - - inXRange(mouseX, mouseY, useFinalPosition) { - const rotValue = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); - const allX = this.elements.map((point) => point.bX); - const x = Math.min(...allX); - const x2 = Math.max(...allX); - return rotValue.x >= x && rotValue.x <= x2; - } - inYRange(mouseX, mouseY, useFinalPosition) { - const rotValue = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); - const allY = this.elements.map((point) => point.bY); - const y = Math.min(...allY); - const y2 = Math.max(...allY); - return rotValue.y >= y && rotValue.y <= y2; + inRange(mouseX, mouseY, axis, useFinalPosition) { + if (axis !== 'x' && axis !== 'y') { + return this.options.radius >= 0.1 && this.elements.length > 1 && pointIsInPolygon(this.elements, mouseX, mouseY, useFinalPosition); + } + const rotatedPoint = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); + const axisPoints = this.elements.map((point) => axis === 'y' ? point.bY : point.bX); + const start = Math.min(...axisPoints); + const end = Math.max(...axisPoints); + return rotatedPoint[axis] >= start && rotatedPoint[axis] <= end; } getCenterPoint(useFinalPosition) { From dd90732b00c9a64a5fe204c2886892f8ef805ac6 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Wed, 2 Feb 2022 20:59:24 +0100 Subject: [PATCH 17/27] adds line to interaction management --- src/annotation.js | 2 +- src/helpers/helpers.core.js | 2 +- src/interaction.js | 9 ++- src/types/line.js | 24 ++++++-- test/specs/line.spec.js | 120 ++++++++++++++++++++++++++++++++++++ test/utils.js | 40 ++++++++++++ 6 files changed, 184 insertions(+), 13 deletions(-) diff --git a/src/annotation.js b/src/annotation.js index 463cf49eb..56299820f 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -124,7 +124,7 @@ export default { interaction: { mode: undefined, axis: undefined, - intersect: true + intersect: undefined }, label: { drawTime: null diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index 6221201dd..172f16604 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -13,7 +13,7 @@ export function inPointRange(point, center, radius, borderWidth) { if (!point || !center || radius <= 0) { return false; } - const hBorderWidth = borderWidth / 2 || 0; + const hBorderWidth = borderWidth / 2; return (Math.pow(point.x - center.x, 2) + Math.pow(point.y - center.y, 2)) <= Math.pow(radius + hBorderWidth, 2); } diff --git a/src/interaction.js b/src/interaction.js index 29ae24068..c14f8591e 100644 --- a/src/interaction.js +++ b/src/interaction.js @@ -16,7 +16,7 @@ const interaction = { * Nearest mode returns the element closest to the event position * @param {Object} state - the state of the plugin * @param {ChartEvent} event - the event we are find things at - * @param {options} options - interaction options to use + * @param {Object} options - interaction options to use * @return {Element[]} - elements that are found (only 1 element) */ nearest(state, event, options) { @@ -29,7 +29,7 @@ const interaction = { * Returns all elements that hit test based on the event position * @param {Object} state - the state of the plugin * @param {ChartEvent} event - the event we are find things at - * @param {options} options - interaction options to use + * @param {Object} options - interaction options to use * @return {Element[]} - elements that are found */ export function getElements(state, event, options) { @@ -58,14 +58,13 @@ function getPointByAxis(event, center, axis) { } function getNearestItem(state, event, options) { - const axis = options.axis || 'xy'; let minDistance = Number.POSITIVE_INFINITY; return state.visibleElements - .filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, axis)) + .filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, options.axis)) .reduce((nearestItems, element) => { const center = element.getCenterPoint(); - const evenPoint = getPointByAxis(event, center, axis); + const evenPoint = getPointByAxis(event, center, options.axis); const distance = distanceBetweenPoints(event, evenPoint); if (distance < minDistance) { nearestItems = [element]; diff --git a/src/types/line.js b/src/types/line.js index e51923eea..a3f5894fb 100644 --- a/src/types/line.js +++ b/src/types/line.js @@ -81,7 +81,7 @@ export default class LineAnnotation extends Element { } // TODO: make private in v2 - isOnLabel(mouseX, mouseY, useFinalPosition) { + isOnLabel(mouseX, mouseY, useFinalPosition, axis) { if (!this.labelIsVisible(useFinalPosition)) { return false; } @@ -90,13 +90,25 @@ export default class LineAnnotation extends Element { const hBorderWidth = this.options.label.borderWidth / 2 || 0; const w2 = labelWidth / 2 + hBorderWidth; const h2 = labelHeight / 2 + hBorderWidth; - return x >= labelX - w2 - EPSILON && x <= labelX + w2 + EPSILON && - y >= labelY - h2 - EPSILON && y <= labelY + h2 + EPSILON; + const inRangeX = x >= labelX - w2 - EPSILON && x <= labelX + w2 + EPSILON; + const inRangeY = y >= labelY - h2 - EPSILON && y <= labelY + h2 + EPSILON; + if (axis === 'x') { + return inRangeX; + } else if (axis === 'y') { + return inRangeY; + } + return inRangeX && inRangeY; } - inRange(mouseX, mouseY, useFinalPosition) { - const epsilon = sqr(this.options.borderWidth / 2); - return this.intersects(mouseX, mouseY, epsilon, useFinalPosition) || this.isOnLabel(mouseX, mouseY, useFinalPosition); + inRange(mouseX, mouseY, axis, useFinalPosition) { + const hBorderWidth = this.options.borderWidth / 2; + if (axis !== 'x' && axis !== 'y') { + const epsilon = sqr(hBorderWidth); + return this.intersects(mouseX, mouseY, epsilon, useFinalPosition) || this.isOnLabel(mouseX, mouseY, useFinalPosition); + } + const {x, y, x2, y2} = this.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition); + const limit = axis === 'y' ? {start: Math.min(y, y2), end: Math.max(y, y2), value: mouseY} : {start: Math.min(x, x2), end: Math.max(x, x2), value: mouseX}; + return (limit.value >= limit.start - hBorderWidth && limit.value <= limit.end + hBorderWidth) || this.isOnLabel(mouseX, mouseY, useFinalPosition, axis); } getCenterPoint() { diff --git a/test/specs/line.spec.js b/test/specs/line.spec.js index 841f4f04c..1d647af80 100644 --- a/test/specs/line.spec.js +++ b/test/specs/line.spec.js @@ -99,4 +99,124 @@ describe('Line annotation', function() { }); } }); + + describe('interaction', function() { + const outer = { + type: 'line', + xMin: 5, + xMax: 5, + yMin: 2, + yMax: 8, + borderWidth: 20 + }; + const inner = { + type: 'line', + xMin: 5, + xMax: 5, + yMin: 2, + yMax: 8, + borderWidth: 8 + }; + + const chart = window.scatterChart(10, 10, {outer, inner}); + const state = window['chartjs-plugin-annotation']._getState(chart); + const interactionOpts = {}; + const outerEl = window.getAnnotationElements(chart)[0]; + const outCenter = outerEl.getCenterPoint(); + const outHBordeWidth = outerEl.options.borderWidth / 2; + const innerEl = window.getAnnotationElements(chart)[1]; + const inCenter = outerEl.getCenterPoint(); + const inHBordeWidth = innerEl.options.borderWidth / 2; + + it('should return the right amount of annotation elements', function() { + for (const interaction of window.interactionData) { + const mode = interaction.mode; + interactionOpts.mode = mode; + for (const axis of Object.keys(interaction.axes)) { + interactionOpts.axis = axis; + [true, false].forEach(function(intersect) { + interactionOpts.intersect = intersect; + const elementsCounts = interaction.axes[axis].intersect[intersect]; + const points = [{x: outCenter.x - outHBordeWidth, y: outCenter.y, what: 'enter outer'}, + {x: inCenter.x - inHBordeWidth, y: inCenter.y, what: 'enter inner'}, + {x: inCenter.x, y: inCenter.y, what: 'click center of inner'}, + {x: inCenter.x + inHBordeWidth + 1, y: inCenter.y, what: 'leave inner'}, + {x: outCenter.x + outHBordeWidth + 1, y: outCenter.y, what: 'leave outer'}, + {x: outCenter.x - outHBordeWidth + 1, y: outCenter.y - outerEl.height / 2 - outHBordeWidth - 1, what: 'outside of elements'}]; + + 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}, ${point.what}`).toEqual(elementsCount); + } + }); + } + } + }); + }); + + describe('with label interaction', function() { + const outer = { + type: 'line', + xMin: 5, + xMax: 5, + yMin: 2, + yMax: 8, + borderWidth: 0, + label: { + enabled: true, + content: ['outer label row 1', 'outer label row 2', 'outer label row 3'], + borderWidth: 0 + } + }; + const inner = { + type: 'line', + xMin: 5, + xMax: 5, + yMin: 2, + yMax: 8, + borderWidth: 0, + label: { + enabled: true, + content: ['inner label 1', 'inner label 2'], + borderWidth: 0 + } + }; + + const chart = window.scatterChart(10, 10, {outer, inner}); + const state = window['chartjs-plugin-annotation']._getState(chart); + const interactionOpts = {}; + const outerEl = window.getAnnotationElements(chart)[0]; + const outCenter = outerEl.getCenterPoint(); + const innerEl = window.getAnnotationElements(chart)[1]; + + it('should return the right amount of annotation elements', function() { + for (const interaction of window.interactionData) { + const mode = interaction.mode; + interactionOpts.mode = mode; + for (const axis of Object.keys(interaction.axes)) { + interactionOpts.axis = axis; + [true, false].forEach(function(intersect) { + interactionOpts.intersect = intersect; + const elementsCounts = interaction.axes[axis].intersect[intersect]; + const points = [{x: outerEl.labelX - outerEl.labelWidth / 2, y: outerEl.labelY, what: 'enter outer'}, + {x: innerEl.labelX - innerEl.labelWidth / 2, y: innerEl.labelY, what: 'enter inner'}, + {x: innerEl.labelX, y: innerEl.labelY, what: 'click center of inner'}, + {x: innerEl.labelX + innerEl.labelWidth / 2 + 1, y: innerEl.labelY, what: 'leave inner'}, + {x: outerEl.labelX + outerEl.labelWidth / 2 + 1, y: outerEl.labelY, what: 'leave outer'}, + {x: outerEl.labelX - outerEl.labelWidth / 2 + 1, y: outCenter.y - outerEl.height / 2 - 1, what: 'outside of elements'}]; + + 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}, ${point.what}`).toEqual(elementsCount); + } + }); + } + } + }); + }); + }); diff --git a/test/utils.js b/test/utils.js index a5b049f8d..279f9b55c 100644 --- a/test/utils.js +++ b/test/utils.js @@ -74,6 +74,12 @@ export const interactionData = [{ true: [1, 2, 2, 1, 0, 0], false: [1, 2, 2, 1, 0, 0] } + }, + r: { // not supported, use xy + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 0] + } } }, }, { @@ -96,6 +102,40 @@ export const interactionData = [{ true: [1, 1, 1, 1, 0, 0], false: [1, 1, 1, 1, 1, 0] } + }, + r: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 1, 1] + } + } + } +}, { + mode: 'dataset', // not supported, use nearest + axes: { + xy: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 1, 1] + } + }, + x: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 0, 1] + } + }, + y: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 1, 0] + } + }, + r: { + intersect: { + true: [1, 1, 1, 1, 0, 0], + false: [1, 1, 1, 1, 1, 1] + } } } }]; From 5227c1f45331343f03a69033c73a8f400e41abfb Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 3 Feb 2022 09:33:26 +0100 Subject: [PATCH 18/27] adds mode x and y --- src/events.js | 2 +- src/interaction.js | 27 ++++++++++++++++++++++----- test/specs/point.spec.js | 2 +- test/specs/polygon.spec.js | 2 +- test/utils.js | 24 ++++++++++++++++++++++-- 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/events.js b/src/events.js index 57a1ee1ca..f59b9083f 100644 --- a/src/events.js +++ b/src/events.js @@ -8,7 +8,7 @@ export const hooks = clickHooks.concat(moveHooks); export function updateListeners(chart, state, options) { state.listened = false; state.moveListened = false; - state._getElements = getElements; + state._getElements = getElements; // for testing hooks.forEach(hook => { if (typeof options[hook] === 'function') { diff --git a/src/interaction.js b/src/interaction.js index c14f8591e..f2fb5353c 100644 --- a/src/interaction.js +++ b/src/interaction.js @@ -9,7 +9,7 @@ const interaction = { * @return {Element[]} - elements that are found */ point(state, event) { - return getIntersectItems(state, event); + return state.visibleElements.filter((element) => element.inRange(event.x, event.y)); }, /** @@ -22,6 +22,27 @@ const interaction = { nearest(state, event, options) { return getNearestItem(state, event, options); }, + /** + * x mode returns the elements that hit-test at the current x coordinate + * @param {Object} state - the state of the plugin + * @param {ChartEvent} event - the event we are find things at + * @param {Object} options - interaction options to use + * @return {Element[]} - elements that are found + */ + x(state, event, options) { + return state.visibleElements.filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, 'x')); + }, + + /** + * y mode returns the elements that hit-test at the current y coordinate + * @param {Object} state - the state of the plugin + * @param {ChartEvent} event - the event we are find things at + * @param {Object} options - interaction options to use + * @return {Element[]} - elements that are found + */ + y(state, event, options) { + return state.visibleElements.filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, 'y')); + } } }; @@ -37,10 +58,6 @@ export function getElements(state, event, options) { return mode(state, event, options); } -function getIntersectItems(state, event) { - return state.visibleElements.filter((element) => element.inRange(event.x, event.y)); -} - function inRangeByAxis(element, event, axis) { if (axis !== 'x' && axis !== 'y') { return element.inRange(event.x, event.y, 'x', true) || element.inRange(event.x, event.y, 'y', true); diff --git a/test/specs/point.spec.js b/test/specs/point.spec.js index abc7803f4..04354565d 100644 --- a/test/specs/point.spec.js +++ b/test/specs/point.spec.js @@ -111,7 +111,7 @@ describe('Point annotation', function() { {x: innerEl.x, y: innerEl.y, what: 'click center of inner'}, {x: innerEl.x + innerEl.width / 2 + 1, y: innerEl.y, what: 'leave inner'}, {x: outerEl.x + outerEl.width / 2 + 1, y: outerEl.y, what: 'leave outer'}, - {x: outerEl.x + 1, y: outerEl.y - outerEl.height / 2 - 1, what: 'outside of elements'}]; + {x: outerEl.x - outerEl.width / 2 + 1, y: outerEl.y - outerEl.height / 2 - 1, what: 'outside of elements'}]; for (let i = 0; i < points.length; i++) { const point = points[i]; diff --git a/test/specs/polygon.spec.js b/test/specs/polygon.spec.js index ebca963fd..2e951465f 100644 --- a/test/specs/polygon.spec.js +++ b/test/specs/polygon.spec.js @@ -156,7 +156,7 @@ describe('Polygon annotation', function() { {x: innerEl.x, y: innerEl.y, what: 'click center of inner', el: innerEl}, {x: innerEl.x + innerEl.width / 2 + 1, y: innerEl.y, what: 'leave inner', el: innerEl}, {x: outerEl.x + outerEl.width / 2 + 1, y: outerEl.y, what: 'leave outer', el: outerEl}, - {x: outerEl.x + 1, y: outerEl.y - outerEl.height / 2 - 1, what: 'outside of elements', el: outerEl}]; + {x: outerEl.x - outerEl.width / 2 + 1, y: outerEl.y - outerEl.height / 2 - 1, what: 'outside of elements', el: outerEl}]; for (let i = 0; i < points.length; i++) { const point = points[i]; diff --git a/test/utils.js b/test/utils.js index 279f9b55c..bc1ac5e99 100644 --- a/test/utils.js +++ b/test/utils.js @@ -103,13 +103,33 @@ export const interactionData = [{ false: [1, 1, 1, 1, 1, 0] } }, - r: { + r: { // not supported, use xy intersect: { true: [1, 1, 1, 1, 0, 0], false: [1, 1, 1, 1, 1, 1] } } } +}, { + mode: 'x', + axes: { + x: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [1, 2, 2, 1, 0, 1] + } + } + } +}, { + mode: 'y', + axes: { + y: { + intersect: { + true: [1, 2, 2, 1, 0, 0], + false: [2, 2, 2, 2, 2, 0] + } + } + } }, { mode: 'dataset', // not supported, use nearest axes: { @@ -131,7 +151,7 @@ export const interactionData = [{ false: [1, 1, 1, 1, 1, 0] } }, - r: { + r: { // not supported, use xy intersect: { true: [1, 1, 1, 1, 0, 0], false: [1, 1, 1, 1, 1, 1] From 8b6c5c7e89e2048d0f0f39c9a83815e9b9f0e193 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 3 Feb 2022 09:35:32 +0100 Subject: [PATCH 19/27] removes tabs --- src/interaction.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/interaction.js b/src/interaction.js index f2fb5353c..b4079276c 100644 --- a/src/interaction.js +++ b/src/interaction.js @@ -23,23 +23,23 @@ const interaction = { return getNearestItem(state, event, options); }, /** - * x mode returns the elements that hit-test at the current x coordinate + * x mode returns the elements that hit-test at the current x coordinate * @param {Object} state - the state of the plugin * @param {ChartEvent} event - the event we are find things at * @param {Object} options - interaction options to use * @return {Element[]} - elements that are found - */ + */ x(state, event, options) { return state.visibleElements.filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, 'x')); }, /** - * y mode returns the elements that hit-test at the current y coordinate + * y mode returns the elements that hit-test at the current y coordinate * @param {Object} state - the state of the plugin * @param {ChartEvent} event - the event we are find things at * @param {Object} options - interaction options to use * @return {Element[]} - elements that are found - */ + */ y(state, event, options) { return state.visibleElements.filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, 'y')); } From 56a555c15963e5e1bfe4ff910ce283902a5b07af Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 3 Feb 2022 11:00:23 +0100 Subject: [PATCH 20/27] fixes similar code and adds point to the context of interaction tests --- src/interaction.js | 13 ++++++++----- test/specs/box.spec.js | 14 +++++++------- test/specs/ellipse.spec.js | 14 +++++++------- test/specs/label.spec.js | 14 +++++++------- test/specs/line.spec.js | 28 ++++++++++++++-------------- test/specs/point.spec.js | 14 +++++++------- test/specs/polygon.spec.js | 14 +++++++------- 7 files changed, 57 insertions(+), 54 deletions(-) diff --git a/src/interaction.js b/src/interaction.js index b4079276c..51a87b4ea 100644 --- a/src/interaction.js +++ b/src/interaction.js @@ -9,7 +9,7 @@ const interaction = { * @return {Element[]} - elements that are found */ point(state, event) { - return state.visibleElements.filter((element) => element.inRange(event.x, event.y)); + return filterElements(state, event, {intersect: true}); }, /** @@ -30,7 +30,7 @@ const interaction = { * @return {Element[]} - elements that are found */ x(state, event, options) { - return state.visibleElements.filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, 'x')); + return filterElements(state, event, {intersect: options.intersect, axis: 'x'}); }, /** @@ -41,7 +41,7 @@ const interaction = { * @return {Element[]} - elements that are found */ y(state, event, options) { - return state.visibleElements.filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, 'y')); + return filterElements(state, event, {intersect: options.intersect, axis: 'y'}); } } }; @@ -74,11 +74,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 getNearestItem(state, event, options) { let minDistance = Number.POSITIVE_INFINITY; - return state.visibleElements - .filter((element) => options.intersect ? element.inRange(event.x, event.y) : inRangeByAxis(element, event, options.axis)) + return filterElements(state, event, options) .reduce((nearestItems, element) => { const center = element.getCenterPoint(); const evenPoint = getPointByAxis(event, center, options.axis); diff --git a/test/specs/box.spec.js b/test/specs/box.spec.js index 4158741dc..1e33b0f8d 100644 --- a/test/specs/box.spec.js +++ b/test/specs/box.spec.js @@ -102,18 +102,18 @@ describe('Box annotation', function() { [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; - const points = [{x: outerEl.x, y: outerEl.y + outerEl.height / 2, what: 'enter outer'}, - {x: innerEl.x, y: innerEl.y + innerEl.height / 2, what: 'enter inner'}, - {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2, what: 'click center of inner'}, - {x: innerEl.x2 + 1, y: innerEl.y2 - innerEl.height / 2, what: 'leave inner'}, - {x: outerEl.x2 + 1, y: outerEl.y2 - outerEl.height / 2, what: 'leave outer'}, - {x: outerEl.x + 1, y: outerEl.y - 1, what: 'outside of elements'}]; + const points = [{x: outerEl.x, y: outerEl.y + outerEl.height / 2}, + {x: innerEl.x, y: innerEl.y + innerEl.height / 2}, + {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2}, + {x: innerEl.x2 + 1, y: innerEl.y2 - innerEl.height / 2}, + {x: outerEl.x2 + 1, y: outerEl.y2 - outerEl.height / 2}, + {x: outerEl.x + 1, y: outerEl.y - 1}]; 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}, ${point.what}`).toEqual(elementsCount); + expect(elements.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 d5430e2b0..81bee2a27 100644 --- a/test/specs/ellipse.spec.js +++ b/test/specs/ellipse.spec.js @@ -110,19 +110,19 @@ describe('Ellipse annotation', function() { [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; - const points = [{x: outerEl.x, y: outerEl.y + outerEl.height / 2, what: 'enter outer', el: outerEl}, - {x: innerEl.x, y: innerEl.y + innerEl.height / 2, what: 'enter inner', el: innerEl}, - {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2, what: 'click center of inner', el: innerEl}, - {x: innerEl.x2 + 1, y: innerEl.y2 - innerEl.height / 2, what: 'leave inner', el: innerEl}, - {x: outerEl.x2 + 1, y: outerEl.y2 - outerEl.height / 2, what: 'leave outer', el: outerEl}, - {x: outerEl.x + 1, y: outerEl.y - 1, what: 'outside of elements', el: outerEl}]; + const points = [{x: outerEl.x, y: outerEl.y + outerEl.height / 2, el: outerEl}, + {x: innerEl.x, y: innerEl.y + innerEl.height / 2, el: innerEl}, + {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2, el: innerEl}, + {x: innerEl.x2 + 1, y: innerEl.y2 - innerEl.height / 2, el: innerEl}, + {x: outerEl.x2 + 1, y: outerEl.y2 - outerEl.height / 2, el: outerEl}, + {x: outerEl.x + 1, y: outerEl.y - 1, el: outerEl}]; for (let i = 0; i < points.length; i++) { 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}, ${point.what}`).toEqual(elementsCount); + expect(elements.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 744f60266..faa1fad76 100644 --- a/test/specs/label.spec.js +++ b/test/specs/label.spec.js @@ -82,18 +82,18 @@ describe('Label annotation', function() { [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; - const points = [{x: outerEl.x, y: outerEl.y + outerEl.height / 2, what: 'enter outer'}, - {x: innerEl.x, y: innerEl.y + innerEl.height / 2, what: 'enter inner'}, - {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2, what: 'click center of inner'}, - {x: innerEl.x + innerEl.width + 1, y: innerEl.y + innerEl.height / 2, what: 'leave inner'}, - {x: outerEl.x + outerEl.width + 1, y: outerEl.y + outerEl.height / 2, what: 'leave outer'}, - {x: outerEl.x + 1, y: outerEl.y - 1, what: 'outside of elements'}]; + const points = [{x: outerEl.x, y: outerEl.y + outerEl.height / 2}, + {x: innerEl.x, y: innerEl.y + innerEl.height / 2}, + {x: innerEl.x + innerEl.width / 2, y: innerEl.y + innerEl.height / 2}, + {x: innerEl.x + innerEl.width + 1, y: innerEl.y + innerEl.height / 2}, + {x: outerEl.x + outerEl.width + 1, y: outerEl.y + outerEl.height / 2}, + {x: outerEl.x + 1, y: outerEl.y - 1}]; 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}, ${point.what}`).toEqual(elementsCount); + expect(elements.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 1d647af80..de4286304 100644 --- a/test/specs/line.spec.js +++ b/test/specs/line.spec.js @@ -137,18 +137,18 @@ describe('Line annotation', function() { [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; - const points = [{x: outCenter.x - outHBordeWidth, y: outCenter.y, what: 'enter outer'}, - {x: inCenter.x - inHBordeWidth, y: inCenter.y, what: 'enter inner'}, - {x: inCenter.x, y: inCenter.y, what: 'click center of inner'}, - {x: inCenter.x + inHBordeWidth + 1, y: inCenter.y, what: 'leave inner'}, - {x: outCenter.x + outHBordeWidth + 1, y: outCenter.y, what: 'leave outer'}, - {x: outCenter.x - outHBordeWidth + 1, y: outCenter.y - outerEl.height / 2 - outHBordeWidth - 1, what: 'outside of elements'}]; + const points = [{x: outCenter.x - outHBordeWidth, y: outCenter.y}, + {x: inCenter.x - inHBordeWidth, y: inCenter.y}, + {x: inCenter.x, y: inCenter.y}, + {x: inCenter.x + inHBordeWidth + 1, y: inCenter.y}, + {x: outCenter.x + outHBordeWidth + 1, y: outCenter.y}, + {x: outCenter.x - outHBordeWidth + 1, y: outCenter.y - outerEl.height / 2 - outHBordeWidth - 1}]; 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}, ${point.what}`).toEqual(elementsCount); + expect(elements.length).withContext(`with interaction mode ${mode}, axis ${axis}, intersect ${intersect}, {x: ${point.x.toFixed(1)}, y: ${point.y.toFixed(1)}`).toEqual(elementsCount); } }); } @@ -200,18 +200,18 @@ describe('Line annotation', function() { [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; - const points = [{x: outerEl.labelX - outerEl.labelWidth / 2, y: outerEl.labelY, what: 'enter outer'}, - {x: innerEl.labelX - innerEl.labelWidth / 2, y: innerEl.labelY, what: 'enter inner'}, - {x: innerEl.labelX, y: innerEl.labelY, what: 'click center of inner'}, - {x: innerEl.labelX + innerEl.labelWidth / 2 + 1, y: innerEl.labelY, what: 'leave inner'}, - {x: outerEl.labelX + outerEl.labelWidth / 2 + 1, y: outerEl.labelY, what: 'leave outer'}, - {x: outerEl.labelX - outerEl.labelWidth / 2 + 1, y: outCenter.y - outerEl.height / 2 - 1, what: 'outside of elements'}]; + const points = [{x: outerEl.labelX - outerEl.labelWidth / 2, y: outerEl.labelY}, + {x: innerEl.labelX - innerEl.labelWidth / 2, y: innerEl.labelY}, + {x: innerEl.labelX, y: innerEl.labelY}, + {x: innerEl.labelX + innerEl.labelWidth / 2 + 1, y: innerEl.labelY}, + {x: outerEl.labelX + outerEl.labelWidth / 2 + 1, y: outerEl.labelY}, + {x: outerEl.labelX - outerEl.labelWidth / 2 + 1, y: outCenter.y - outerEl.height / 2 - 1}]; 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}, ${point.what}`).toEqual(elementsCount); + expect(elements.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 04354565d..c88253085 100644 --- a/test/specs/point.spec.js +++ b/test/specs/point.spec.js @@ -106,18 +106,18 @@ describe('Point annotation', function() { [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; - const points = [{x: outerEl.x - outerEl.width / 2, y: outerEl.y, what: 'enter outer'}, - {x: innerEl.x - innerEl.width / 2, y: innerEl.y, what: 'enter inner'}, - {x: innerEl.x, y: innerEl.y, what: 'click center of inner'}, - {x: innerEl.x + innerEl.width / 2 + 1, y: innerEl.y, what: 'leave inner'}, - {x: outerEl.x + outerEl.width / 2 + 1, y: outerEl.y, what: 'leave outer'}, - {x: outerEl.x - outerEl.width / 2 + 1, y: outerEl.y - outerEl.height / 2 - 1, what: 'outside of elements'}]; + const points = [{x: outerEl.x - outerEl.width / 2, y: outerEl.y}, + {x: innerEl.x - innerEl.width / 2, y: innerEl.y}, + {x: innerEl.x, y: innerEl.y}, + {x: innerEl.x + innerEl.width / 2 + 1, y: innerEl.y}, + {x: outerEl.x + outerEl.width / 2 + 1, y: outerEl.y}, + {x: outerEl.x - outerEl.width / 2 + 1, y: outerEl.y - outerEl.height / 2 - 1}]; 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}, ${point.what}`).toEqual(elementsCount); + expect(elements.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 2e951465f..cbc5e3d75 100644 --- a/test/specs/polygon.spec.js +++ b/test/specs/polygon.spec.js @@ -151,19 +151,19 @@ describe('Polygon annotation', function() { [true, false].forEach(function(intersect) { interactionOpts.intersect = intersect; const elementsCounts = interaction.axes[axis].intersect[intersect]; - const points = [{x: outerEl.x - outerEl.width / 2, y: outerEl.y, what: 'enter outer', el: outerEl}, - {x: innerEl.x - innerEl.width / 2, y: innerEl.y, what: 'enter inner', el: innerEl}, - {x: innerEl.x, y: innerEl.y, what: 'click center of inner', el: innerEl}, - {x: innerEl.x + innerEl.width / 2 + 1, y: innerEl.y, what: 'leave inner', el: innerEl}, - {x: outerEl.x + outerEl.width / 2 + 1, y: outerEl.y, what: 'leave outer', el: outerEl}, - {x: outerEl.x - outerEl.width / 2 + 1, y: outerEl.y - outerEl.height / 2 - 1, what: 'outside of elements', el: outerEl}]; + const points = [{x: outerEl.x - outerEl.width / 2, y: outerEl.y, el: outerEl}, + {x: innerEl.x - innerEl.width / 2, y: innerEl.y, el: innerEl}, + {x: innerEl.x, y: innerEl.y, el: innerEl}, + {x: innerEl.x + innerEl.width / 2 + 1, y: innerEl.y, el: innerEl}, + {x: outerEl.x + outerEl.width / 2 + 1, y: outerEl.y, el: outerEl}, + {x: outerEl.x - outerEl.width / 2 + 1, y: outerEl.y - outerEl.height / 2 - 1, el: outerEl}]; for (let i = 0; i < points.length; i++) { 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}, ${point.what}`).toEqual(elementsCount); + 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); } }); } From 83a8e59e4c21c57f3b806ce278785322d8d27c23 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 3 Feb 2022 11:19:44 +0100 Subject: [PATCH 21/27] adds types --- types/options.d.ts | 5 +++-- types/tests/exports.ts | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/types/options.d.ts b/types/options.d.ts index 65ead6615..09659f281 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -1,4 +1,4 @@ -import { Color, PointStyle, BorderRadius } from 'chart.js'; +import { Color, PointStyle, BorderRadius, CoreInteractionOptions } from 'chart.js'; import { AnnotationEvents, PartialEventContext } from './events'; import { LabelOptions, BoxLabelOptions, LabelTypeOptions } from './label'; @@ -157,8 +157,9 @@ interface PolygonAnnotationOptions extends CoreAnnotationOptions, AnnotationPoin export interface AnnotationPluginOptions extends AnnotationEvents { annotations: AnnotationOptions[] | Record, + animations?: Record, clip?: boolean, dblClickSpeed?: Scriptable, drawTime?: Scriptable, - animations?: Record, + interaction?: CoreInteractionOptions } diff --git a/types/tests/exports.ts b/types/tests/exports.ts index 726ef6257..324117691 100644 --- a/types/tests/exports.ts +++ b/types/tests/exports.ts @@ -16,6 +16,11 @@ const chart = new Chart('id', { plugins: { annotation: { clip: false, + interaction: { + mode: 'nearest', + axis: 'xy', + intersect: true + }, annotations: [{ type: 'line', label: { From c2fdd06795919f7a42a16086eb7089da010fda68 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 3 Feb 2022 11:23:26 +0100 Subject: [PATCH 22/27] orders the options in types for annotation node --- types/options.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/options.d.ts b/types/options.d.ts index 09659f281..c891cc384 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -156,8 +156,8 @@ interface PolygonAnnotationOptions extends CoreAnnotationOptions, AnnotationPoin } export interface AnnotationPluginOptions extends AnnotationEvents { - annotations: AnnotationOptions[] | Record, animations?: Record, + annotations: AnnotationOptions[] | Record, clip?: boolean, dblClickSpeed?: Scriptable, drawTime?: Scriptable, From ebe10350ca5573698c5ab6714f0aa7ca2dd8f214 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 3 Feb 2022 11:55:33 +0100 Subject: [PATCH 23/27] adds documentation --- docs/guide/configuration.md | 1 + docs/guide/options.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index c9ac4ae1c..58600732b 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -12,6 +12,7 @@ The following options are available at the top level. They apply to all annotati | `clip` | `boolean` | No | `true` | Are the annotations clipped to the chartArea. | `dblClickSpeed` | `number` | Yes | `350` | Time to detect a double click in ms. | `drawTime` | `string` | Yes | `'afterDatasetsDraw'` | See [drawTime](options#draw-time). +| [`interaction`](options#interaction) | `Object` | No | `options.interaction` | To configure which events trigger chart interactions :::warning diff --git a/docs/guide/options.md b/docs/guide/options.md index 3b97291df..13c85f875 100644 --- a/docs/guide/options.md +++ b/docs/guide/options.md @@ -16,6 +16,18 @@ Paddings use the same format as [chart.js](https://www.chartjs.org/docs/master/g Point styles use the same format as [chart.js](https://www.chartjs.org/docs/master/configuration/elements.html#point-styles). +## Interaction + +Interaction uses the same format as [chart.js](https://www.chartjs.org/docs/latest/configuration/interactions.html#interactions). + +:::warning + +Interaction `index` and `dataset` modes are not supported by the plugin. If set, the plugin will use `nearest` mode. + +Interaction `r` axis is not supported by the plugin. If set, the plugin will use `xy` mode. + +::: + ## Scriptable Options As with most options in chart.js, the annotation plugin options are scriptable. This means that a function can be passed which returns the value as needed. In the example below, the annotation is hidden when the screen is less than 1000px wide. From 03536c2e6b0467f8e1278878de5681f442f03eb0 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Thu, 3 Feb 2022 12:33:00 +0100 Subject: [PATCH 24/27] fixes typo on doc --- docs/guide/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 58600732b..7007dfa86 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -12,7 +12,7 @@ The following options are available at the top level. They apply to all annotati | `clip` | `boolean` | No | `true` | Are the annotations clipped to the chartArea. | `dblClickSpeed` | `number` | Yes | `350` | Time to detect a double click in ms. | `drawTime` | `string` | Yes | `'afterDatasetsDraw'` | See [drawTime](options#draw-time). -| [`interaction`](options#interaction) | `Object` | No | `options.interaction` | To configure which events trigger chart interactions +| [`interaction`](options#interaction) | `Object` | No | `options.interaction` | To configure which events trigger plugin interactions :::warning From 049a5ba3b347adc932bc1079b524d7cf95d97643 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Fri, 11 Feb 2022 00:10:51 +0100 Subject: [PATCH 25/27] fixes CC for function with 5 arguments (exceeds 4 allowed) --- src/helpers/helpers.core.js | 6 +++--- src/types/box.js | 2 +- src/types/label.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index 73eb2e810..895a8a1e9 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -17,10 +17,10 @@ export function inPointRange(point, center, radius, borderWidth) { return (Math.pow(point.x - center.x, 2) + Math.pow(point.y - center.y, 2)) <= Math.pow(radius + hBorderWidth, 2); } -export function inBoxRange(mouseX, mouseY, {x, y, width, height}, axis, borderWidth) { +export function inBoxRange(point, {x, y, width, height}, axis, borderWidth) { const hBorderWidth = borderWidth / 2; - const inRangeX = mouseX >= x - hBorderWidth - EPSILON && mouseX <= x + width + hBorderWidth + EPSILON; - const inRangeY = mouseY >= y - hBorderWidth - EPSILON && mouseY <= y + height + hBorderWidth + EPSILON; + const inRangeX = point.x >= x - hBorderWidth - EPSILON && point.x <= x + width + hBorderWidth + EPSILON; + const inRangeY = point.y >= y - hBorderWidth - EPSILON && point.y <= y + height + hBorderWidth + EPSILON; if (axis === 'x') { return inRangeX; } else if (axis === 'y') { diff --git a/src/types/box.js b/src/types/box.js index 1ce4651b2..e2dd6a56d 100644 --- a/src/types/box.js +++ b/src/types/box.js @@ -6,7 +6,7 @@ export default class BoxAnnotation extends Element { inRange(mouseX, mouseY, axis, useFinalPosition) { const {x, y} = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); - return inBoxRange(x, y, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), axis, this.options.borderWidth); + return inBoxRange({x, y}, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), axis, this.options.borderWidth); } getCenterPoint(useFinalPosition) { diff --git a/src/types/label.js b/src/types/label.js index 010b52db2..edb7bc66e 100644 --- a/src/types/label.js +++ b/src/types/label.js @@ -6,7 +6,7 @@ export default class LabelAnnotation extends Element { inRange(mouseX, mouseY, axis, useFinalPosition) { const {x, y} = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation)); - return inBoxRange(x, y, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), axis, this.options.borderWidth); + return inBoxRange({x, y}, this.getProps(['x', 'y', 'width', 'height'], useFinalPosition), axis, this.options.borderWidth); } getCenterPoint(useFinalPosition) { From a307dbb956adcd36640b4b0a5242c5199b8b86d0 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Mon, 4 Apr 2022 15:13:53 +0200 Subject: [PATCH 26/27] adds note about the breaking change using interaction --- docs/guide/migrationV2.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/guide/migrationV2.md b/docs/guide/migrationV2.md index 47e0ef6d8..ec1e133ad 100644 --- a/docs/guide/migrationV2.md +++ b/docs/guide/migrationV2.md @@ -9,3 +9,9 @@ A number of changes were made to the configuration options passed to the plugin * `xScaleID` option default has been changed, now set to `undefined`. If the option is missing, the plugin will try to use the first scale of the chart, configured as `'x'` axis. If more than one scale has been defined in the chart as `'x'` axis, the option is mandatory to select the right scale. * `yScaleID` option default has been changed, now set to `undefined`. If the option is missing, the plugin will try to use the first scale of the chart, configured as `'y'` axis. If more than one scale has been defined in the chart as `'y'` axis, the option is mandatory to select the right scale. * When [stacked scales](https://www.chartjs.org/docs/latest/axes/cartesian/#common-options-to-all-cartesian-axes) are used, instead of the whole chart area, the designated scale area is used as fallback for `xMin`, `xMax`, `yMin`, `yMax`, `xValue` or `yValue` options. + +## Events + +`chartjs-plugin-annotation` plugin version 2 introduces the `interaction` options, to configure which events trigger annotation interactions. By default, the plugin uses the [chart interaction configuration](https://www.chartjs.org/docs/latest/configuration/interactions.html#interactions). + + * When [scatter charts](https://www.chartjs.org/docs/latest/charts/scatter.html) are used, the interaction default `mode` in Chart.js is `point`, while, in the previous plugin version, the default was `nearest`. From a4026de1c69106d3862d6298d6b6cfcb76acd2c9 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Mon, 4 Apr 2022 18:26:18 +0200 Subject: [PATCH 27/27] Update docs/guide/migrationV2.md Co-authored-by: Jukka Kurkela --- docs/guide/migrationV2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/migrationV2.md b/docs/guide/migrationV2.md index ec1e133ad..eeaae0fba 100644 --- a/docs/guide/migrationV2.md +++ b/docs/guide/migrationV2.md @@ -12,6 +12,6 @@ A number of changes were made to the configuration options passed to the plugin ## Events -`chartjs-plugin-annotation` plugin version 2 introduces the `interaction` options, to configure which events trigger annotation interactions. By default, the plugin uses the [chart interaction configuration](https://www.chartjs.org/docs/latest/configuration/interactions.html#interactions). +`chartjs-plugin-annotation` plugin version 2 introduces the [`interaction`](options#interaction) options, to configure which events trigger annotation interactions. By default, the plugin uses the [chart interaction configuration](https://www.chartjs.org/docs/latest/configuration/interactions.html#interactions). * When [scatter charts](https://www.chartjs.org/docs/latest/charts/scatter.html) are used, the interaction default `mode` in Chart.js is `point`, while, in the previous plugin version, the default was `nearest`.