From e3a7d7544b2ee0160acc3ad8da85db6affef46c3 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Mon, 30 Jan 2023 22:41:37 +0100 Subject: [PATCH 1/7] Enable options on annotations to apply automatically when hovered --- src/annotation.js | 16 +++++++++-- src/elements.js | 71 ++++++++++++++++++++++++++++++----------------- src/hover.js | 52 ++++++++++++++++++++++++++++++++++ test/utils.js | 3 ++ 4 files changed, 115 insertions(+), 27 deletions(-) create mode 100644 src/hover.js diff --git a/src/annotation.js b/src/annotation.js index 9d8fec04d..07740a0bf 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -1,6 +1,7 @@ import {Chart} from 'chart.js'; import {clipArea, unclipArea, isObject, isArray} from 'chart.js/helpers'; import {handleEvent, eventHooks, updateListeners} from './events'; +import {handleHoverElements} from './hover'; import {invokeHook, elementHooks, updateHooks} from './hooks'; import {adjustScaleRange, verifyScaleOptions} from './scale'; import {updateElements, resolveType} from './elements'; @@ -38,7 +39,8 @@ export default { moveListened: false, hooks: {}, hooked: false, - hovered: [] + hovered: [], + highlighted: [] }); }, @@ -92,7 +94,8 @@ export default { beforeEvent(chart, args, options) { const state = chartStates.get(chart); - if (handleEvent(state, args.event, options)) { + const hovered = handleHoverElements(chart, state, args.event, options); + if (handleEvent(state, args.event, options) || hovered) { args.changed = true; } }, @@ -118,6 +121,12 @@ export default { axis: undefined, intersect: undefined }, + hover: { + enabled: true, + mode: undefined, + axis: undefined, + intersect: undefined + }, common: { drawTime: 'afterDatasetsDraw', label: { @@ -135,6 +144,9 @@ export default { interaction: { _fallback: true }, + hover: { + _fallback: true + }, common: { label: { _fallback: true diff --git a/src/elements.js b/src/elements.js index ba996cc53..8466204dd 100644 --- a/src/elements.js +++ b/src/elements.js @@ -1,5 +1,5 @@ import {Animations} from 'chart.js'; -import {isObject, defined} from 'chart.js/helpers'; +import {isObject, defined, _capitalize} from 'chart.js/helpers'; import {eventHooks} from './events'; import {elementHooks} from './hooks'; import {annotationTypes} from './types'; @@ -14,6 +14,7 @@ const hooks = eventHooks.concat(elementHooks); * @typedef { import("chart.js").Chart } Chart * @typedef { import("chart.js").UpdateMode } UpdateMode * @typedef { import('../../types/options').AnnotationPluginOptions } AnnotationPluginOptions + * @typedef { import('../../types/element').AnnotationElement } AnnotationElement */ /** @@ -44,30 +45,49 @@ export function updateElements(chart, state, options, mode) { for (let i = 0; i < annotations.length; i++) { const annotationOptions = annotations[i]; const element = getOrCreateElement(elements, i, annotationOptions.type); - const resolver = annotationOptions.setContext(getContext(chart, element, annotationOptions)); - const properties = element.resolveElementProperties(chart, resolver); + updateElement(chart, element, annotationOptions, animations); + } +} + +/** + * @param {Chart} chart + * @param {Object} state + * @param {AnnotationPluginOptions} options + * @param {AnnotationElement[]} elements + */ +export function hoverElements(chart, state, options, elements) { + const animations = resolveAnimations(chart, options.animations, 'active'); - properties.skip = toSkip(properties); + elements.forEach(function(el) { + const index = state.elements.indexOf(el); + updateElement(chart, el, state.annotations[index], animations); + }); +} - if ('elements' in properties) { - updateSubElements(element, properties, resolver, animations); - // Remove the sub-element definitions from properties, so the actual elements - // are not overwritten by their definitions - delete properties.elements; - } +function updateElement(chart, element, options, animations) { + const resolver = options.setContext(getContext(chart, element, options)); + const properties = element.resolveElementProperties(chart, resolver); - if (!defined(element.x)) { - // If the element is newly created, assing the properties directly - to - // make them readily awailable to any scriptable options. If we do not do this, - // the properties retruned by `resolveElementProperties` are available only - // after options resolution. - Object.assign(element, properties); - } + properties.skip = toSkip(properties); - properties.options = resolveAnnotationOptions(resolver); + if ('elements' in properties) { + updateSubElements(element, properties, resolver, animations); + // Remove the sub-element definitions from properties, so the actual elements + // are not overwritten by their definitions + delete properties.elements; + } - animations.update(element, properties); + if (!defined(element.x)) { + // If the element is newly created, assing the properties directly - to + // make them readily awailable to any scriptable options. If we do not do this, + // the properties retruned by `resolveElementProperties` are available only + // after options resolution. + Object.assign(element, properties); } + + properties.options = resolveAnnotationOptions(resolver, element.active); + + animations.update(element, properties); } function toSkip(properties) { @@ -106,27 +126,28 @@ function getOrCreateElement(elements, index, type, initProperties) { return element; } -function resolveAnnotationOptions(resolver) { +function resolveAnnotationOptions(resolver, active) { const elementClass = annotationTypes[resolveType(resolver.type)]; const result = {}; result.id = resolver.id; result.type = resolver.type; result.drawTime = resolver.drawTime; Object.assign(result, - resolveObj(resolver, elementClass.defaults), - resolveObj(resolver, elementClass.defaultRoutes)); + resolveObj(resolver, elementClass.defaults, active), + resolveObj(resolver, elementClass.defaultRoutes, active)); for (const hook of hooks) { result[hook] = resolver[hook]; } return result; } -function resolveObj(resolver, defs) { +function resolveObj(resolver, defs, active) { const result = {}; for (const prop of Object.keys(defs)) { const optDefs = defs[prop]; - const value = resolver[prop]; - result[prop] = isObject(optDefs) ? resolveObj(value, optDefs) : value; + const hoverProp = active ? 'hover' + _capitalize(prop) : prop; + const value = resolver[hoverProp] || resolver[prop]; + result[prop] = isObject(optDefs) ? resolveObj(value, optDefs, active) : value; } return result; } diff --git a/src/hover.js b/src/hover.js new file mode 100644 index 000000000..4c26898ef --- /dev/null +++ b/src/hover.js @@ -0,0 +1,52 @@ +import {getElements} from './interaction'; +import {hoverElements} from './elements'; + +/** + * @typedef { import("chart.js").Chart } Chart + * @typedef { import('../../types/options').AnnotationPluginOptions } AnnotationPluginOptions + */ + +/** + * @param {Chart} chart + * @param {Object} state + * @param {ChartEvent} event + * @param {AnnotationPluginOptions} options + * @return {boolean|undefined} + */ +export function handleHoverElements(chart, state, event, options) { + if (options.hover.enabled) { + switch (event.type) { + case 'mousemove': + case 'mouseout': + return handleMoveEvents(chart, state, event, options); + default: + } + } +} + +function handleMoveEvents(chart, state, event, options) { + let elements; + + if (event.type === 'mousemove') { + elements = getElements(state, event, options.hover); + } else { + elements = []; + } + + if (!state.highlighted.length && !elements.length) { + return false; + } + + const unhovered = state.highlighted.filter((el) => !elements.includes(el)); + setActive(unhovered, false); + state.highlighted = elements; + setActive(state.highlighted, true); + hoverElements(chart, state, options, unhovered.concat(state.highlighted)); + return true; +} + +function setActive(elements, active) { + elements.forEach(function(el) { + el.active = active; + }); +} diff --git a/test/utils.js b/test/utils.js index 3baa7e7fc..a43bc5b70 100644 --- a/test/utils.js +++ b/test/utils.js @@ -56,6 +56,9 @@ export function scatterChart(xMax, yMax, annotations) { plugins: { legend: false, annotation: { + hover: { + enabled: false + }, annotations } } From 12ab472b380e6e6c367093397b386467eadca8c8 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Mon, 30 Jan 2023 22:46:47 +0100 Subject: [PATCH 2/7] apply active to sub-elements --- src/elements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements.js b/src/elements.js index 8466204dd..8aa5196e1 100644 --- a/src/elements.js +++ b/src/elements.js @@ -109,7 +109,7 @@ function updateSubElements(mainElement, {elements, initProperties}, resolver, an const properties = definition.properties; const subElement = getOrCreateElement(subElements, i, definition.type, initProperties); const subResolver = resolver[definition.optionScope].override(definition); - properties.options = resolveAnnotationOptions(subResolver); + properties.options = resolveAnnotationOptions(subResolver, mainElement.active); animations.update(subElement, properties); } } From 65bba50d9ee411559bd5cb0aa2810a2c08054ee2 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Tue, 31 Jan 2023 12:09:34 +0100 Subject: [PATCH 3/7] sets prefixes --- src/elements.js | 50 ++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/elements.js b/src/elements.js index 8aa5196e1..43ac8fee2 100644 --- a/src/elements.js +++ b/src/elements.js @@ -1,5 +1,5 @@ import {Animations} from 'chart.js'; -import {isObject, defined, _capitalize} from 'chart.js/helpers'; +import {isObject, defined} from 'chart.js/helpers'; import {eventHooks} from './events'; import {elementHooks} from './hooks'; import {annotationTypes} from './types'; @@ -9,6 +9,7 @@ const directUpdater = { }; const hooks = eventHooks.concat(elementHooks); +const getPrefixes = (el) => el.active ? ['hover', ''] : ['']; /** * @typedef { import("chart.js").Chart } Chart @@ -30,6 +31,21 @@ export function resolveType(type = 'line') { return 'line'; } +/** + * @param {Chart} chart + * @param {Object} state + * @param {AnnotationPluginOptions} options + * @param {AnnotationElement[]} elements + */ +export function hoverElements(chart, state, options, elements) { + const animations = resolveAnimations(chart, options.animations, 'active'); + + elements.forEach(function(el) { + const index = state.elements.indexOf(el); + updateElement(chart, el, state.annotations[index], animations); + }); +} + /** * @param {Chart} chart * @param {Object} state @@ -49,23 +65,8 @@ export function updateElements(chart, state, options, mode) { } } -/** - * @param {Chart} chart - * @param {Object} state - * @param {AnnotationPluginOptions} options - * @param {AnnotationElement[]} elements - */ -export function hoverElements(chart, state, options, elements) { - const animations = resolveAnimations(chart, options.animations, 'active'); - - elements.forEach(function(el) { - const index = state.elements.indexOf(el); - updateElement(chart, el, state.annotations[index], animations); - }); -} - function updateElement(chart, element, options, animations) { - const resolver = options.setContext(getContext(chart, element, options)); + const resolver = options.setContext(getContext(chart, element, options), getPrefixes(element)); const properties = element.resolveElementProperties(chart, resolver); properties.skip = toSkip(properties); @@ -85,7 +86,7 @@ function updateElement(chart, element, options, animations) { Object.assign(element, properties); } - properties.options = resolveAnnotationOptions(resolver, element.active); + properties.options = resolveAnnotationOptions(resolver); animations.update(element, properties); } @@ -108,8 +109,8 @@ function updateSubElements(mainElement, {elements, initProperties}, resolver, an const definition = elements[i]; const properties = definition.properties; const subElement = getOrCreateElement(subElements, i, definition.type, initProperties); - const subResolver = resolver[definition.optionScope].override(definition); - properties.options = resolveAnnotationOptions(subResolver, mainElement.active); + const subResolver = resolver[definition.optionScope].override(definition, getPrefixes(mainElement)); + properties.options = resolveAnnotationOptions(subResolver); animations.update(subElement, properties); } } @@ -126,15 +127,15 @@ function getOrCreateElement(elements, index, type, initProperties) { return element; } -function resolveAnnotationOptions(resolver, active) { +function resolveAnnotationOptions(resolver) { const elementClass = annotationTypes[resolveType(resolver.type)]; const result = {}; result.id = resolver.id; result.type = resolver.type; result.drawTime = resolver.drawTime; Object.assign(result, - resolveObj(resolver, elementClass.defaults, active), - resolveObj(resolver, elementClass.defaultRoutes, active)); + resolveObj(resolver, elementClass.defaults), + resolveObj(resolver, elementClass.defaultRoutes)); for (const hook of hooks) { result[hook] = resolver[hook]; } @@ -145,8 +146,7 @@ function resolveObj(resolver, defs, active) { const result = {}; for (const prop of Object.keys(defs)) { const optDefs = defs[prop]; - const hoverProp = active ? 'hover' + _capitalize(prop) : prop; - const value = resolver[hoverProp] || resolver[prop]; + const value = resolver[prop]; result[prop] = isObject(optDefs) ? resolveObj(value, optDefs, active) : value; } return result; From 528c45ef5cc5e46036a6222ed1e281d555ecdd3e Mon Sep 17 00:00:00 2001 From: stockiNail Date: Tue, 31 Jan 2023 12:44:42 +0100 Subject: [PATCH 4/7] renames active elements --- src/annotation.js | 6 +++--- src/hover.js | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/annotation.js b/src/annotation.js index 07740a0bf..451debfbd 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -1,7 +1,7 @@ import {Chart} from 'chart.js'; import {clipArea, unclipArea, isObject, isArray} from 'chart.js/helpers'; import {handleEvent, eventHooks, updateListeners} from './events'; -import {handleHoverElements} from './hover'; +import {handleActiveElements} from './hover'; import {invokeHook, elementHooks, updateHooks} from './hooks'; import {adjustScaleRange, verifyScaleOptions} from './scale'; import {updateElements, resolveType} from './elements'; @@ -40,7 +40,7 @@ export default { hooks: {}, hooked: false, hovered: [], - highlighted: [] + activeElements: [] }); }, @@ -94,7 +94,7 @@ export default { beforeEvent(chart, args, options) { const state = chartStates.get(chart); - const hovered = handleHoverElements(chart, state, args.event, options); + const hovered = handleActiveElements(chart, state, args.event, options); if (handleEvent(state, args.event, options) || hovered) { args.changed = true; } diff --git a/src/hover.js b/src/hover.js index 4c26898ef..1de944d52 100644 --- a/src/hover.js +++ b/src/hover.js @@ -13,7 +13,7 @@ import {hoverElements} from './elements'; * @param {AnnotationPluginOptions} options * @return {boolean|undefined} */ -export function handleHoverElements(chart, state, event, options) { +export function handleActiveElements(chart, state, event, options) { if (options.hover.enabled) { switch (event.type) { case 'mousemove': @@ -33,15 +33,15 @@ function handleMoveEvents(chart, state, event, options) { elements = []; } - if (!state.highlighted.length && !elements.length) { + if (!state.activeElements.length && !elements.length) { return false; } - const unhovered = state.highlighted.filter((el) => !elements.includes(el)); + const unhovered = state.activeElements.filter((el) => !elements.includes(el)); setActive(unhovered, false); - state.highlighted = elements; - setActive(state.highlighted, true); - hoverElements(chart, state, options, unhovered.concat(state.highlighted)); + state.activeElements = elements; + setActive(elements, true); + hoverElements(chart, state, options, unhovered.concat(elements)); return true; } From c35006e69e008ba6c1bf4ad753019b5090215ab3 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Tue, 31 Jan 2023 13:26:56 +0100 Subject: [PATCH 5/7] changes fallback of hover and some names --- src/annotation.js | 2 +- src/elements.js | 2 +- src/hover.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/annotation.js b/src/annotation.js index 451debfbd..ab895a939 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -145,7 +145,7 @@ export default { _fallback: true }, hover: { - _fallback: true + _fallback: 'interaction' }, common: { label: { diff --git a/src/elements.js b/src/elements.js index 43ac8fee2..284ef6dd0 100644 --- a/src/elements.js +++ b/src/elements.js @@ -37,7 +37,7 @@ export function resolveType(type = 'line') { * @param {AnnotationPluginOptions} options * @param {AnnotationElement[]} elements */ -export function hoverElements(chart, state, options, elements) { +export function updateActiveElements(chart, state, options, elements) { const animations = resolveAnimations(chart, options.animations, 'active'); elements.forEach(function(el) { diff --git a/src/hover.js b/src/hover.js index 1de944d52..db126208f 100644 --- a/src/hover.js +++ b/src/hover.js @@ -1,5 +1,5 @@ import {getElements} from './interaction'; -import {hoverElements} from './elements'; +import {updateActiveElements} from './elements'; /** * @typedef { import("chart.js").Chart } Chart @@ -41,7 +41,7 @@ function handleMoveEvents(chart, state, event, options) { setActive(unhovered, false); state.activeElements = elements; setActive(elements, true); - hoverElements(chart, state, options, unhovered.concat(elements)); + updateActiveElements(chart, state, options, unhovered.concat(elements)); return true; } From eec18cb2987a475302111650b6f304d2b96f81e1 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Wed, 1 Feb 2023 11:57:42 +0100 Subject: [PATCH 6/7] improves update elements invocation logic --- src/hover.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/hover.js b/src/hover.js index db126208f..cdf664ce4 100644 --- a/src/hover.js +++ b/src/hover.js @@ -40,13 +40,19 @@ function handleMoveEvents(chart, state, event, options) { const unhovered = state.activeElements.filter((el) => !elements.includes(el)); setActive(unhovered, false); state.activeElements = elements; - setActive(elements, true); - updateActiveElements(chart, state, options, unhovered.concat(elements)); + const newHovered = elements.filter((el) => !el.active); + if (!unhovered.length && !newHovered.length) { + return false; + } + setActive(newHovered, true); + updateActiveElements(chart, state, options, unhovered.concat(newHovered)); return true; } function setActive(elements, active) { - elements.forEach(function(el) { + const result = active ? elements.filter((el) => !el.active) : elements; + result.forEach(function(el) { el.active = active; }); + return result; } From 3eb90dc781eb1894d1d380c2ad73b71d41f11d95 Mon Sep 17 00:00:00 2001 From: stockiNail Date: Wed, 1 Feb 2023 17:54:53 +0100 Subject: [PATCH 7/7] fixes CC --- src/hover.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/hover.js b/src/hover.js index cdf664ce4..264e49bb2 100644 --- a/src/hover.js +++ b/src/hover.js @@ -33,7 +33,7 @@ function handleMoveEvents(chart, state, event, options) { elements = []; } - if (!state.activeElements.length && !elements.length) { + if (empty(state.activeElements, elements)) { return false; } @@ -41,7 +41,7 @@ function handleMoveEvents(chart, state, event, options) { setActive(unhovered, false); state.activeElements = elements; const newHovered = elements.filter((el) => !el.active); - if (!unhovered.length && !newHovered.length) { + if (empty(unhovered, newHovered)) { return false; } setActive(newHovered, true); @@ -49,6 +49,10 @@ function handleMoveEvents(chart, state, event, options) { return true; } +function empty(arr1, arr2) { + return !arr1.length && !arr2.length; +} + function setActive(elements, active) { const result = active ? elements.filter((el) => !el.active) : elements; result.forEach(function(el) {