diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 301aad415..abef8faad 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -182,6 +182,7 @@ module.exports = { 'line/image', 'line/datasetBars', 'line/animation', + 'line/hook', ] }, { diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index be6d25828..1d133ebaa 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -80,3 +80,14 @@ The following options are available for all annotation types. These options can ::: If the event callbacks explicitly returns `true`, the chart will re-render automatically after processing the event completely. This is important when there are the annotations that require re-draws (for instance, after a change of a rendering options). + +## Hooks + +The following hooks are available for all annotation types. These hooks can be specified per annotation, or at the top level which apply to all annotations. + +These hooks enable some user customizations on the annotations. + +| Name | Type | Notes +| ---- | ---- | ---- +| `beforeDraw` | `(context) => void ` | Called before that the annotation is being drawn. +| `afterDraw` | `(context) => void` | Called after the annotation has been drawn. diff --git a/docs/samples/line/hook.md b/docs/samples/line/hook.md new file mode 100644 index 000000000..fbed23a2c --- /dev/null +++ b/docs/samples/line/hook.md @@ -0,0 +1,121 @@ +# Outside of chart + +```js chart-editor +// +const DATA_COUNT = 8; +const MIN = 10; +const MAX = 100; + +Utils.srand(8); + +const labels = []; +for (let i = 0; i < DATA_COUNT; ++i) { + labels.push('' + i); +} + +const numberCfg = {count: DATA_COUNT, min: MIN, max: MAX}; + +const data = { + labels: labels, + datasets: [{ + data: Utils.numbers(numberCfg) + }] +}; +// + +// +const annotation = { + type: 'line', + borderColor: 'black', + borderWidth: 3, + scaleID: 'y', + value: 55, + beforeDraw: drawExtraLine +}; +// + +// +function drawExtraLine(context) { + const ctx = context.chart.ctx; + const width = context.chart.canvas.width; + const {x, y, x2, y2, options} = context.element; + ctx.save(); + ctx.lineWidth = options.borderWidth; + ctx.strokeStyle = options.borderColor; + ctx.setLineDash([6, 6]); + ctx.lineDashOffset = options.borderDashOffset; + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(x, y); + ctx.moveTo(x2, y2); + ctx.lineTo(width, y); + ctx.stroke(); + ctx.restore(); + return true; +} +// + +/* */ +const config = { + type: 'line', + data, + options: { + layout: { + padding: { + right: 50 + } + }, + scales: { + y: { + stacked: true + } + }, + plugins: { + annotation: { + clip: false, + annotations: { + annotation + } + } + } + } +}; +/* */ + +const actions = [ + { + name: 'Randomize', + handler: function(chart) { + chart.data.datasets.forEach(function(dataset, i) { + dataset.data = dataset.data.map(() => Utils.rand(MIN, MAX)); + }); + chart.update(); + } + }, + { + name: 'Add data', + handler: function(chart) { + chart.data.labels.push(chart.data.labels.length); + chart.data.datasets.forEach(function(dataset, i) { + dataset.data.push(Utils.rand(MIN, MAX)); + }); + chart.update(); + } + }, + { + name: 'Remove data', + handler: function(chart) { + chart.data.labels.shift(); + chart.data.datasets.forEach(function(dataset, i) { + dataset.data.shift(); + }); + chart.update(); + } + } +]; + +module.exports = { + actions: actions, + config: config, +}; +``` diff --git a/src/annotation.js b/src/annotation.js index cccccb353..9d8fec04d 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, hooks, updateListeners} from './events'; +import {handleEvent, eventHooks, updateListeners} from './events'; +import {invokeHook, elementHooks, updateHooks} from './hooks'; import {adjustScaleRange, verifyScaleOptions} from './scale'; import {updateElements, resolveType} from './elements'; import {annotationTypes} from './types'; @@ -8,6 +9,7 @@ import {requireVersion} from './helpers'; import {version} from '../package.json'; const chartStates = new Map(); +const hooks = eventHooks.concat(elementHooks); export default { id: 'annotation', @@ -34,6 +36,8 @@ export default { listeners: {}, listened: false, moveListened: false, + hooks: {}, + hooked: false, hovered: [] }); }, @@ -67,6 +71,7 @@ export default { updateListeners(chart, state, options); updateElements(chart, state, options, args.mode); state.visibleElements = state.elements.filter(el => !el.skip && el.options.display); + updateHooks(chart, state, options); }, beforeDatasetsDraw(chart, _args, options) { @@ -142,16 +147,15 @@ export default { function draw(chart, caller, clip) { const {ctx, chartArea} = chart; - const {visibleElements} = chartStates.get(chart); + const state = chartStates.get(chart); if (clip) { clipArea(ctx, chartArea); } - const drawableElements = getDrawableElements(visibleElements, caller).sort((a, b) => a.options.z - b.options.z); - - for (const element of drawableElements) { - element.draw(chart.ctx, chartArea); + const drawableElements = getDrawableElements(state.visibleElements, caller).sort((a, b) => a.element.options.z - b.element.options.z); + for (const item of drawableElements) { + drawElement(ctx, chartArea, state, item); } if (clip) { @@ -163,15 +167,26 @@ function getDrawableElements(elements, caller) { const drawableElements = []; for (const el of elements) { if (el.options.drawTime === caller) { - drawableElements.push(el); + drawableElements.push({element: el, main: true}); } if (el.elements && el.elements.length) { for (const sub of el.elements) { if (sub.options.display && sub.options.drawTime === caller) { - drawableElements.push(sub); + drawableElements.push({element: sub}); } } } } return drawableElements; } + +function drawElement(ctx, chartArea, state, item) { + const el = item.element; + if (item.main) { + invokeHook(state, el, 'beforeDraw'); + el.draw(ctx, chartArea); + invokeHook(state, el, 'afterDraw'); + } else { + el.draw(ctx, chartArea); + } +} diff --git a/src/elements.js b/src/elements.js index 5d296f7ab..ba996cc53 100644 --- a/src/elements.js +++ b/src/elements.js @@ -1,12 +1,15 @@ import {Animations} from 'chart.js'; import {isObject, defined} from 'chart.js/helpers'; -import {hooks} from './events'; +import {eventHooks} from './events'; +import {elementHooks} from './hooks'; import {annotationTypes} from './types'; const directUpdater = { update: Object.assign }; +const hooks = eventHooks.concat(elementHooks); + /** * @typedef { import("chart.js").Chart } Chart * @typedef { import("chart.js").UpdateMode } UpdateMode diff --git a/src/events.js b/src/events.js index 57052c9f9..3bda88b5c 100644 --- a/src/events.js +++ b/src/events.js @@ -1,5 +1,6 @@ -import {defined, callback} from 'chart.js/helpers'; +import {callback} from 'chart.js/helpers'; import {getElements} from './interaction'; +import {loadHooks} from './helpers'; const moveHooks = ['enter', 'leave']; @@ -8,7 +9,7 @@ const moveHooks = ['enter', 'leave']; * @typedef { import('../../types/options').AnnotationPluginOptions } AnnotationPluginOptions */ -export const hooks = moveHooks.concat('click'); +export const eventHooks = moveHooks.concat('click'); /** * @param {Chart} chart @@ -16,18 +17,10 @@ export const hooks = moveHooks.concat('click'); * @param {AnnotationPluginOptions} options */ export function updateListeners(chart, state, options) { - state.listened = false; + state.listened = loadHooks(options, eventHooks, state.listeners); state.moveListened = false; state._getElements = getElements; // for testing - hooks.forEach(hook => { - if (typeof options[hook] === 'function') { - state.listened = true; - state.listeners[hook] = options[hook]; - } else if (defined(state.listeners[hook])) { - delete state.listeners[hook]; - } - }); moveHooks.forEach(hook => { if (typeof options[hook] === 'function') { state.moveListened = true; diff --git a/src/helpers/helpers.options.js b/src/helpers/helpers.options.js index b800d02e0..ab892e2a1 100644 --- a/src/helpers/helpers.options.js +++ b/src/helpers/helpers.options.js @@ -85,3 +85,22 @@ export function toPosition(value) { export function isBoundToPoint(options) { return options && (defined(options.xValue) || defined(options.yValue)); } + +/** + * @param {Object} options + * @param {Array} hooks + * @param {Object} hooksContainer + * @returns {boolean} + */ +export function loadHooks(options, hooks, hooksContainer) { + let activated = false; + hooks.forEach(hook => { + if (typeof options[hook] === 'function') { + activated = true; + hooksContainer[hook] = options[hook]; + } else if (defined(hooksContainer[hook])) { + delete hooksContainer[hook]; + } + }); + return activated; +} diff --git a/src/hooks.js b/src/hooks.js new file mode 100644 index 000000000..5f42b8037 --- /dev/null +++ b/src/hooks.js @@ -0,0 +1,44 @@ +import {callback} from 'chart.js/helpers'; +import {loadHooks} from './helpers'; + +/** + * @typedef { import("chart.js").Chart } Chart + * @typedef { import('../../types/options').AnnotationPluginOptions } AnnotationPluginOptions + * @typedef { import('../../types/element').AnnotationElement } AnnotationElement + */ + +export const elementHooks = ['afterDraw', 'beforeDraw']; + +/** + * @param {Chart} chart + * @param {Object} state + * @param {AnnotationPluginOptions} options + */ +export function updateHooks(chart, state, options) { + const visibleElements = state.visibleElements; + state.hooked = loadHooks(options, elementHooks, state.hooks); + + if (!state.hooked) { + visibleElements.forEach(scope => { + if (!state.hooked) { + elementHooks.forEach(hook => { + if (typeof scope.options[hook] === 'function') { + state.hooked = true; + } + }); + } + }); + } +} + +/** + * @param {Object} state + * @param {AnnotationElement} element + * @param {string} hook + */ +export function invokeHook(state, element, hook) { + if (state.hooked) { + const callbackHook = element.options[hook] || state.hooks[hook]; + return callback(callbackHook, [element.$context]); + } +} diff --git a/test/fixtures/box/hooks.js b/test/fixtures/box/hooks.js new file mode 100644 index 000000000..f72ba5dc9 --- /dev/null +++ b/test/fixtures/box/hooks.js @@ -0,0 +1,124 @@ +module.exports = { + config: { + type: 'scatter', + options: { + scales: { + x: { + display: true, + min: -10, + max: 10, + ticks: { + display: false + } + }, + y: { + display: true, + min: -10, + max: 10, + ticks: { + display: false + } + } + }, + plugins: { + legend: false, + annotation: { + drawTime: 'afterDraw', + annotations: { + box1: { + type: 'box', + xScaleID: 'x', + yScaleID: 'y', + xMin: -9, + yMin: 9, + xMax: -1, + yMax: 1, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x + 10, element.y + 10, 10, 5, 3); + } + }, + box2: { + type: 'box', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: 9, + xMax: 9, + yMax: 1, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + label: { + enabled: true, + content: 'p: 25%,75%', + position: { + x: '25%', + y: '75%' + } + }, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x + element.width - 10, element.y + 10, 10, 5, 3); + } + }, + box3: { + type: 'box', + xScaleID: 'x', + yScaleID: 'y', + xMin: -9, + yMin: -1, + xMax: -1, + yMax: -9, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + label: { + enabled: true, + content: 'p: 50%,50%', + position: { + x: '50%', + y: '50%' + } + }, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x + 10, element.y + element.height - 10, 10, 5, 3); + } + }, + box4: { + type: 'box', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: -1, + xMax: 9, + yMax: -9, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + label: { + enabled: true, + content: 'p: 100%,0%', + position: { + x: '100%', + y: '0%' + } + }, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x + element.width - 10, element.y + element.height - 10, 10, 5, 3); + } + } + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/box/hooks.png b/test/fixtures/box/hooks.png new file mode 100644 index 000000000..6a7d63f28 Binary files /dev/null and b/test/fixtures/box/hooks.png differ diff --git a/test/fixtures/ellipse/hooks.js b/test/fixtures/ellipse/hooks.js new file mode 100644 index 000000000..fb94c491b --- /dev/null +++ b/test/fixtures/ellipse/hooks.js @@ -0,0 +1,101 @@ +module.exports = { + config: { + type: 'scatter', + options: { + scales: { + x: { + display: true, + min: -10, + max: 10, + ticks: { + display: false + } + }, + y: { + display: true, + min: -10, + max: 10, + ticks: { + display: false + } + } + }, + plugins: { + legend: false, + annotation: { + drawTime: 'afterDraw', + annotations: { + ellipse1: { + type: 'ellipse', + xScaleID: 'x', + yScaleID: 'y', + xMin: -9, + yMin: 7, + xMax: -2, + yMax: 3, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x + 10, element.y + 10, 10, 5, 3); + } + }, + ellipse2: { + type: 'ellipse', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: 7, + xMax: 9, + yMax: 3, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x + element.width - 10, element.y + 10, 10, 5, 3); + } + }, + ellipse3: { + type: 'ellipse', + xScaleID: 'x', + yScaleID: 'y', + xMin: -9, + yMin: -7, + xMax: -1, + yMax: -3, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + rotation: 45, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x + 10, element.y + element.height - 10, 10, 5, 3); + } + }, + ellipse4: { + type: 'ellipse', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: -1, + xMax: 9, + yMax: -9, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x + element.width - 10, element.y + element.height - 10, 10, 5, 3); + } + } + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/ellipse/hooks.png b/test/fixtures/ellipse/hooks.png new file mode 100644 index 000000000..038206f08 Binary files /dev/null and b/test/fixtures/ellipse/hooks.png differ diff --git a/test/fixtures/label/hooks.js b/test/fixtures/label/hooks.js new file mode 100644 index 000000000..40efd1241 --- /dev/null +++ b/test/fixtures/label/hooks.js @@ -0,0 +1,134 @@ +module.exports = { + config: { + type: 'scatter', + options: { + scales: { + x: { + display: true, + ticks: { + display: false + }, + min: -10, + max: 10 + }, + y: { + display: true, + ticks: { + display: false + }, + min: -10, + max: 10 + } + }, + plugins: { + legend: false, + annotation: { + drawTime: 'afterDraw', + annotations: { + label1: { + type: 'label', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: 1, + xMax: 8, + yMax: 8, + backgroundColor: 'rgba(33, 101, 171, 0.5)', + borderColor: 'rgb(33, 101, 171)', + borderWidth: 1, + content: ['This is my text', 'and this is the second row of my text'], + padding: { + x: 10, + y: 10 + }, + position: { + y: 'end' + }, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x + 10, element.y + 10, 5, 5, 3); + } + }, + point1: { + type: 'point', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: 1, + xMax: 8, + yMax: 8, + radius: 10, + backgroundColor: 'red', + borderColor: 'black', + borderWidth: 1 + }, + box1: { + type: 'box', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: 1, + xMax: 8, + yMax: 8, + backgroundColor: 'transparent', + borderColor: 'red', + borderWidth: 1, + }, + label2: { + type: 'label', + xScaleID: 'x', + yScaleID: 'y', + xMin: -8, + yMin: -8, + xMax: 1, + yMax: 1, + backgroundColor: 'rgba(33, 101, 171, 0.5)', + borderColor: 'rgb(33, 101, 171)', + borderWidth: 1, + content: ['This is my text', 'and this is the second row of my text'], + padding: { + x: 10, + y: 10 + }, + position: { + y: 'end' + }, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x + element.width - 10, element.y + 10, 5, 5, 3); + } + }, + point2: { + type: 'point', + xScaleID: 'x', + yScaleID: 'y', + xMin: -8, + yMin: -8, + xMax: 1, + yMax: 1, + radius: 10, + backgroundColor: 'red', + borderColor: 'black', + borderWidth: 1 + }, + box2: { + type: 'box', + xScaleID: 'x', + yScaleID: 'y', + xMin: -8, + yMin: -8, + xMax: 1, + yMax: 1, + backgroundColor: 'transparent', + borderColor: 'red', + borderWidth: 1, + } + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/label/hooks.png b/test/fixtures/label/hooks.png new file mode 100644 index 000000000..b875a52cc Binary files /dev/null and b/test/fixtures/label/hooks.png differ diff --git a/test/fixtures/line/hooks.js b/test/fixtures/line/hooks.js new file mode 100644 index 000000000..cd105e671 --- /dev/null +++ b/test/fixtures/line/hooks.js @@ -0,0 +1,71 @@ +module.exports = { + config: { + type: 'scatter', + options: { + layout: { + padding: { + right: 50, + left: 50 + } + }, + scales: { + x: { + display: true, + min: 0, + max: 100, + ticks: { + display: false + } + }, + y: { + display: true, + min: 0, + max: 100, + ticks: { + display: false + } + } + }, + plugins: { + annotation: { + clip: false, + annotations: { + line1: { + type: 'line', + scaleID: 'y', + value: 50, + borderColor: 'black', + borderWidth: 5, + label: { + rotation: 'auto', + borderRadius: 10, + borderWidth: 3, + content: 'afterDraw hook', + display: true + }, + afterDraw(context) { + const ctx = context.chart.ctx; + const {x, y, x2, y2, options} = context.element; + ctx.save(); + ctx.lineWidth = options.borderWidth; + ctx.strokeStyle = options.borderColor; + ctx.setLineDash([6, 6]); + ctx.lineDashOffset = options.borderDashOffset; + ctx.beginPath(); + ctx.moveTo(x - 50, y); + ctx.lineTo(x, y); + ctx.moveTo(x2, y2); + ctx.lineTo(x2 + 50, y); + ctx.stroke(); + ctx.restore(); + } + }, + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/line/hooks.png b/test/fixtures/line/hooks.png new file mode 100644 index 000000000..bb2bea33b Binary files /dev/null and b/test/fixtures/line/hooks.png differ diff --git a/test/fixtures/point/hooks.js b/test/fixtures/point/hooks.js new file mode 100644 index 000000000..67660b259 --- /dev/null +++ b/test/fixtures/point/hooks.js @@ -0,0 +1,104 @@ +module.exports = { + config: { + type: 'scatter', + options: { + scales: { + x: { + display: true, + min: -10, + max: 10, + ticks: { + display: false + } + }, + y: { + display: true, + min: -10, + max: 10, + ticks: { + display: false + } + } + }, + plugins: { + legend: false, + annotation: { + drawTime: 'afterDraw', + annotations: { + point1: { + type: 'point', + xScaleID: 'x', + yScaleID: 'y', + xMin: -9, + yMin: 9, + xMax: -1, + yMax: 1, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + radius: 50, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.centerX - element.options.radius, element.y + element.options.radius, 10, 5, 3); + } + }, + point2: { + type: 'point', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: 9, + xMax: 9, + yMax: 1, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + radius: 50, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.centerX + element.options.radius, element.y + element.options.radius, 10, 5, 3); + } + }, + point3: { + type: 'point', + xScaleID: 'x', + yScaleID: 'y', + xMin: -9, + yMin: -1, + xMax: -1, + yMax: -9, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + radius: 50, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x, element.y, 10, 5, 3); + } + }, + point4: { + type: 'point', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: -1, + xMax: 9, + yMax: -9, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + radius: 50, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.centerX, element.centerY, 10, 5, 3); + } + } + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/point/hooks.png b/test/fixtures/point/hooks.png new file mode 100644 index 000000000..1484473fa Binary files /dev/null and b/test/fixtures/point/hooks.png differ diff --git a/test/fixtures/polygon/hooks.js b/test/fixtures/polygon/hooks.js new file mode 100644 index 000000000..aeb8bda27 --- /dev/null +++ b/test/fixtures/polygon/hooks.js @@ -0,0 +1,104 @@ +module.exports = { + config: { + type: 'scatter', + options: { + scales: { + x: { + display: true, + min: -10, + max: 10, + ticks: { + display: false + } + }, + y: { + display: true, + min: -10, + max: 10, + ticks: { + display: false + } + } + }, + plugins: { + legend: false, + annotation: { + drawTime: 'afterDraw', + annotations: { + polygon1: { + type: 'polygon', + xScaleID: 'x', + yScaleID: 'y', + xMin: -9, + yMin: 9, + xMax: -1, + yMax: 1, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + radius: 50, + sides: 5, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.centerX - element.options.radius, element.y + element.options.radius, 10, 5, 3); + } + }, + polygon2: { + type: 'polygon', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: 9, + xMax: 9, + yMax: 1, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + radius: 50, + sides: 8, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.centerX + element.options.radius, element.y + element.options.radius, 10, 5, 3); + } + }, + polygon3: { + type: 'polygon', + xScaleID: 'x', + yScaleID: 'y', + xMin: -9, + yMin: -1, + xMax: -1, + yMax: -9, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + radius: 50, + sides: 10, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.x, element.y, 10, 5, 3); + } + }, + polygon4: { + type: 'polygon', + xScaleID: 'x', + yScaleID: 'y', + xMin: 1, + yMin: -1, + xMax: 9, + yMax: -9, + backgroundColor: 'white', + borderColor: 'red', + borderWidth: 2, + radius: 50, + afterDraw(ctx) { + const {chart, element} = ctx; + window.drawStar(chart.ctx, element.centerX, element.centerY, 10, 5, 3); + } + } + } + } + } + } + } +}; diff --git a/test/fixtures/polygon/hooks.png b/test/fixtures/polygon/hooks.png new file mode 100644 index 000000000..b406650da Binary files /dev/null and b/test/fixtures/polygon/hooks.png differ diff --git a/test/index.js b/test/index.js index ab97b1888..025e7470a 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,6 @@ import {acquireChart, addMatchers, releaseCharts, specsFromFixtures, triggerMouseEvent, afterEvent} from 'chartjs-test-utils'; import {testEvents, eventPoint0, getCenterPoint} from './events'; -import {createCanvas, getAnnotationElements, scatterChart, stringifyObject, interactionData} from './utils'; +import {createCanvas, getAnnotationElements, scatterChart, stringifyObject, interactionData, drawStar} from './utils'; import * as helpers from '../src/helpers'; window.helpers = helpers; @@ -12,6 +12,7 @@ window.testEvents = testEvents; window.eventPoint0 = eventPoint0; window.getCenterPoint = getCenterPoint; window.createCanvas = createCanvas; +window.drawStar = drawStar; window.getAnnotationElements = getAnnotationElements; window.scatterChart = scatterChart; window.stringifyObject = stringifyObject; diff --git a/test/specs/hooks.spec.js b/test/specs/hooks.spec.js new file mode 100644 index 000000000..3173ceff0 --- /dev/null +++ b/test/specs/hooks.spec.js @@ -0,0 +1,63 @@ +describe('Hooks', 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: { + } + } + }, + }; + + ['box', 'ellipse', 'label', 'line', 'point', 'polygon'].forEach(function(type) { + const options = { + type, + xMin: 2, + yMin: 2, + xMax: 8, + yMax: 8, + label: { + display: true, + content: 'test' + } + }; + + const pluginOpts = chartConfig.options.plugins.annotation; + + [pluginOpts, options].forEach(function(targetOptions) { + + it(`should detect hooks invocations on ${type}`, function() { + targetOptions.beforeDraw = function({element}) { + element.invocations = (element.invocations || 0) + 1; + element.count = (element.count || 0) + 1; + }; + targetOptions.afterDraw = function({element}) { + expect(element.invocations).toBe(1); + element.invocations--; + expect(element.count).toBe(1); + element.count++; + }; + pluginOpts.annotations = [options]; + const chart = window.acquireChart(chartConfig); + const element = window.getAnnotationElements(chart)[0]; + expect(element.invocations).toBe(0); + expect(element.count).toBe(2); + }); + }); + }); +}); diff --git a/test/utils.js b/test/utils.js index bc1ac5e99..3baa7e7fc 100644 --- a/test/utils.js +++ b/test/utils.js @@ -15,6 +15,23 @@ export function createCanvas() { return canvas; } +// https://stackoverflow.com/questions/25837158/how-to-draw-a-star-by-using-canvas-html5 +export function drawStar(ctx, x, y, radius, spikes, inset) { + ctx.save(); + ctx.beginPath(); + ctx.translate(x, y); + ctx.moveTo(0, 0 - radius); + for (let i = 0; i < spikes; i++) { + ctx.rotate(Math.PI / spikes); + ctx.lineTo(0, 0 - (radius * inset)); + ctx.rotate(Math.PI / spikes); + ctx.lineTo(0, 0 - radius); + } + ctx.closePath(); + ctx.fill(); + ctx.restore(); +} + export function getAnnotationElements(chart) { return window['chartjs-plugin-annotation']._getState(chart).elements; } diff --git a/types/options.d.ts b/types/options.d.ts index 13fbd2c4b..a23adbb32 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -1,5 +1,5 @@ import { Color, PointStyle, BorderRadius, CoreInteractionOptions } from 'chart.js'; -import { AnnotationEvents, PartialEventContext } from './events'; +import { AnnotationEvents, PartialEventContext, EventContext } from './events'; import { LabelOptions, BoxLabelOptions, LabelTypeOptions } from './label'; export type DrawTime = 'afterDraw' | 'afterDatasetsDraw' | 'beforeDraw' | 'beforeDatasetsDraw'; @@ -17,6 +17,11 @@ export type AnnotationType = keyof AnnotationTypeRegistry; export type AnnotationOptions = { [key in TYPE]: { type: key } & AnnotationTypeRegistry[key] }[TYPE] +interface AnnotationHooks { + beforeDraw?(context: EventContext): void, + afterDraw?(context: EventContext): void +} + export type Scriptable = T | ((ctx: TContext, options: AnnotationOptions) => T); export type ScaleValue = number | string; @@ -28,7 +33,7 @@ interface ShadowOptions { shadowOffsetY?: Scriptable } -export interface CoreAnnotationOptions extends AnnotationEvents, ShadowOptions{ +export interface CoreAnnotationOptions extends AnnotationEvents, ShadowOptions, AnnotationHooks { adjustScaleRange?: Scriptable, borderColor?: Scriptable, borderDash?: Scriptable, @@ -134,7 +139,7 @@ interface PolygonAnnotationOptions extends CoreAnnotationOptions, AnnotationPoin yAdjust?: Scriptable, } -export interface AnnotationPluginOptions extends AnnotationEvents { +export interface AnnotationPluginOptions extends AnnotationEvents, AnnotationHooks { animations?: Record, annotations: AnnotationOptions[] | Record, clip?: boolean,