Skip to content

Add beforeDraw and afterDraw hooks to the annotations #744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0ea15b3
Add `beforeDraw` and `afterDraw` hooks to the annotations
stockiNail May 21, 2022
6d86eec
adds test cases
stockiNail May 23, 2022
816b7e0
adds types
stockiNail May 23, 2022
2a6ef51
adds doc
stockiNail May 23, 2022
69439f3
improves tests on hooks invocations
stockiNail May 23, 2022
b416159
adds sample
stockiNail May 23, 2022
962ee4b
Add element diagrams to the annotation types guide
stockiNail Jun 6, 2022
05e619c
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Jun 8, 2022
22a3aa3
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Jun 8, 2022
f5186fe
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Jun 8, 2022
9715d5c
Merge remote-tracking branch 'origin/master' into elementsHooks
stockiNail Jun 9, 2022
6d87201
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Jul 21, 2022
84366b3
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Jul 22, 2022
ec3a554
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Jul 26, 2022
1324e43
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Aug 4, 2022
1f42643
Merge remote-tracking branch 'origin/master' into elementsHooks
stockiNail Aug 5, 2022
693e84d
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Aug 19, 2022
30b5941
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Aug 19, 2022
b30c95e
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Sep 28, 2022
ff2a323
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Sep 28, 2022
767bba9
apply review
stockiNail Sep 28, 2022
bb5de65
fix CC
stockiNail Sep 28, 2022
b686c7a
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Oct 28, 2022
30446eb
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Nov 15, 2022
63e01f8
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Nov 15, 2022
235712c
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Nov 17, 2022
5114654
Merge branch 'master' of https://github.com/chartjs/chartjs-plugin-an…
stockiNail Jan 9, 2023
784cc9f
Merge remote-tracking branch 'origin/master' into elementsHooks
stockiNail Jan 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ module.exports = {
'line/image',
'line/datasetBars',
'line/animation',
'line/hook',
]
},
{
Expand Down
11 changes: 11 additions & 0 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
121 changes: 121 additions & 0 deletions docs/samples/line/hook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Outside of chart

```js chart-editor
// <block:setup:2>
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)
}]
};
// </block:setup>

// <block:annotation:1>
const annotation = {
type: 'line',
borderColor: 'black',
borderWidth: 3,
scaleID: 'y',
value: 55,
beforeDraw: drawExtraLine
};
// </block:annotation>

// <block:utils:3>
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;
}
// </block:utils>

/* <block:config:0> */
const config = {
type: 'line',
data,
options: {
layout: {
padding: {
right: 50
}
},
scales: {
y: {
stacked: true
}
},
plugins: {
annotation: {
clip: false,
annotations: {
annotation
}
}
}
}
};
/* </block:config> */

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,
};
```
21 changes: 13 additions & 8 deletions src/annotation.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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 {drawElement, elementHooks, updateHooks, resetCounters} from './hooks';
import {adjustScaleRange, verifyScaleOptions} from './scale';
import {updateElements, resolveType} from './elements';
import {annotationTypes} from './types';
import {requireVersion} from './helpers';
import {version} from '../package.json';

const chartStates = new Map();
const hooks = eventHooks.concat(elementHooks);

export default {
id: 'annotation',
Expand All @@ -34,6 +36,8 @@ export default {
listeners: {},
listened: false,
moveListened: false,
hooks: {},
hooked: false,
hovered: []
});
},
Expand Down Expand Up @@ -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) {
Expand All @@ -78,6 +83,7 @@ export default {
},

beforeDraw(chart, _args, options) {
resetCounters(chartStates.get(chart));
draw(chart, 'beforeDraw', options.clip);
},

Expand Down Expand Up @@ -142,16 +148,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(chart, state, item);
}

if (clip) {
Expand All @@ -163,12 +168,12 @@ function getDrawableElements(elements, caller) {
const drawableElements = [];
for (const el of elements) {
if (el.options.drawTime === caller) {
drawableElements.push(el);
drawableElements.push({element: el});
}
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, main: el});
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/elements.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 4 additions & 11 deletions src/events.js
Original file line number Diff line number Diff line change
@@ -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'];

Expand All @@ -8,26 +9,18 @@ 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
* @param {Object} state
* @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;
Expand Down
19 changes: 19 additions & 0 deletions src/helpers/helpers.options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
74 changes: 74 additions & 0 deletions src/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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 {Chart} chart
* @param {Object} state
* @param {{element: AnnotationElement, main?: AnnotationElement}} options
*/
export function drawElement(chart, state, item) {
const {element, main} = item;
const el = main || element;
el._drawnElements += 1;
beforeDraw(state, el);
element.draw(chart.ctx);
afterDraw(state, el);
}

/**
* @param {Object} state
*/
export function resetCounters(state) {
state.visibleElements.forEach(function(el) {
el._drawnElements = 0;
const subElements = (el.elements || []).filter(item => item.options.display);
el._drawCount = subElements.length + 1;
});
}

function beforeDraw(state, el) {
if (state.hooked && el._drawnElements === 1) {
invokeHook(state, el, 'beforeDraw');
}
}

function afterDraw(state, el) {
if (state.hooked && el._drawnElements === el._drawCount) {
invokeHook(state, el, 'afterDraw');
}
}

function invokeHook(state, el, hook) {
const callbackHook = el.options[hook] || state.hooks[hook];
callback(callbackHook, [el.$context]);
}
Loading