Skip to content

Commit 44722bf

Browse files
authored
Add beforeDraw and afterDraw hooks to the annotations (#744)
* Add `beforeDraw` and `afterDraw` hooks to the annotations * adds test cases * adds types * adds doc * improves tests on hooks invocations * adds sample * Add element diagrams to the annotation types guide * apply review * fix CC
1 parent 60a2ceb commit 44722bf

File tree

24 files changed

+955
-24
lines changed

24 files changed

+955
-24
lines changed

docs/.vuepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ module.exports = {
182182
'line/image',
183183
'line/datasetBars',
184184
'line/animation',
185+
'line/hook',
185186
]
186187
},
187188
{

docs/guide/configuration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,14 @@ The following options are available for all annotation types. These options can
8080
:::
8181

8282
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).
83+
84+
## Hooks
85+
86+
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.
87+
88+
These hooks enable some user customizations on the annotations.
89+
90+
| Name | Type | Notes
91+
| ---- | ---- | ----
92+
| `beforeDraw` | `(context) => void ` | Called before that the annotation is being drawn.
93+
| `afterDraw` | `(context) => void` | Called after the annotation has been drawn.

docs/samples/line/hook.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Outside of chart
2+
3+
```js chart-editor
4+
// <block:setup:2>
5+
const DATA_COUNT = 8;
6+
const MIN = 10;
7+
const MAX = 100;
8+
9+
Utils.srand(8);
10+
11+
const labels = [];
12+
for (let i = 0; i < DATA_COUNT; ++i) {
13+
labels.push('' + i);
14+
}
15+
16+
const numberCfg = {count: DATA_COUNT, min: MIN, max: MAX};
17+
18+
const data = {
19+
labels: labels,
20+
datasets: [{
21+
data: Utils.numbers(numberCfg)
22+
}]
23+
};
24+
// </block:setup>
25+
26+
// <block:annotation:1>
27+
const annotation = {
28+
type: 'line',
29+
borderColor: 'black',
30+
borderWidth: 3,
31+
scaleID: 'y',
32+
value: 55,
33+
beforeDraw: drawExtraLine
34+
};
35+
// </block:annotation>
36+
37+
// <block:utils:3>
38+
function drawExtraLine(context) {
39+
const ctx = context.chart.ctx;
40+
const width = context.chart.canvas.width;
41+
const {x, y, x2, y2, options} = context.element;
42+
ctx.save();
43+
ctx.lineWidth = options.borderWidth;
44+
ctx.strokeStyle = options.borderColor;
45+
ctx.setLineDash([6, 6]);
46+
ctx.lineDashOffset = options.borderDashOffset;
47+
ctx.beginPath();
48+
ctx.moveTo(0, y);
49+
ctx.lineTo(x, y);
50+
ctx.moveTo(x2, y2);
51+
ctx.lineTo(width, y);
52+
ctx.stroke();
53+
ctx.restore();
54+
return true;
55+
}
56+
// </block:utils>
57+
58+
/* <block:config:0> */
59+
const config = {
60+
type: 'line',
61+
data,
62+
options: {
63+
layout: {
64+
padding: {
65+
right: 50
66+
}
67+
},
68+
scales: {
69+
y: {
70+
stacked: true
71+
}
72+
},
73+
plugins: {
74+
annotation: {
75+
clip: false,
76+
annotations: {
77+
annotation
78+
}
79+
}
80+
}
81+
}
82+
};
83+
/* </block:config> */
84+
85+
const actions = [
86+
{
87+
name: 'Randomize',
88+
handler: function(chart) {
89+
chart.data.datasets.forEach(function(dataset, i) {
90+
dataset.data = dataset.data.map(() => Utils.rand(MIN, MAX));
91+
});
92+
chart.update();
93+
}
94+
},
95+
{
96+
name: 'Add data',
97+
handler: function(chart) {
98+
chart.data.labels.push(chart.data.labels.length);
99+
chart.data.datasets.forEach(function(dataset, i) {
100+
dataset.data.push(Utils.rand(MIN, MAX));
101+
});
102+
chart.update();
103+
}
104+
},
105+
{
106+
name: 'Remove data',
107+
handler: function(chart) {
108+
chart.data.labels.shift();
109+
chart.data.datasets.forEach(function(dataset, i) {
110+
dataset.data.shift();
111+
});
112+
chart.update();
113+
}
114+
}
115+
];
116+
117+
module.exports = {
118+
actions: actions,
119+
config: config,
120+
};
121+
```

src/annotation.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import {Chart} from 'chart.js';
22
import {clipArea, unclipArea, isObject, isArray} from 'chart.js/helpers';
3-
import {handleEvent, hooks, updateListeners} from './events';
3+
import {handleEvent, eventHooks, updateListeners} from './events';
4+
import {invokeHook, elementHooks, updateHooks} from './hooks';
45
import {adjustScaleRange, verifyScaleOptions} from './scale';
56
import {updateElements, resolveType} from './elements';
67
import {annotationTypes} from './types';
78
import {requireVersion} from './helpers';
89
import {version} from '../package.json';
910

1011
const chartStates = new Map();
12+
const hooks = eventHooks.concat(elementHooks);
1113

1214
export default {
1315
id: 'annotation',
@@ -34,6 +36,8 @@ export default {
3436
listeners: {},
3537
listened: false,
3638
moveListened: false,
39+
hooks: {},
40+
hooked: false,
3741
hovered: []
3842
});
3943
},
@@ -67,6 +71,7 @@ export default {
6771
updateListeners(chart, state, options);
6872
updateElements(chart, state, options, args.mode);
6973
state.visibleElements = state.elements.filter(el => !el.skip && el.options.display);
74+
updateHooks(chart, state, options);
7075
},
7176

7277
beforeDatasetsDraw(chart, _args, options) {
@@ -142,16 +147,15 @@ export default {
142147

143148
function draw(chart, caller, clip) {
144149
const {ctx, chartArea} = chart;
145-
const {visibleElements} = chartStates.get(chart);
150+
const state = chartStates.get(chart);
146151

147152
if (clip) {
148153
clipArea(ctx, chartArea);
149154
}
150155

151-
const drawableElements = getDrawableElements(visibleElements, caller).sort((a, b) => a.options.z - b.options.z);
152-
153-
for (const element of drawableElements) {
154-
element.draw(chart.ctx, chartArea);
156+
const drawableElements = getDrawableElements(state.visibleElements, caller).sort((a, b) => a.element.options.z - b.element.options.z);
157+
for (const item of drawableElements) {
158+
drawElement(ctx, chartArea, state, item);
155159
}
156160

157161
if (clip) {
@@ -163,15 +167,26 @@ function getDrawableElements(elements, caller) {
163167
const drawableElements = [];
164168
for (const el of elements) {
165169
if (el.options.drawTime === caller) {
166-
drawableElements.push(el);
170+
drawableElements.push({element: el, main: true});
167171
}
168172
if (el.elements && el.elements.length) {
169173
for (const sub of el.elements) {
170174
if (sub.options.display && sub.options.drawTime === caller) {
171-
drawableElements.push(sub);
175+
drawableElements.push({element: sub});
172176
}
173177
}
174178
}
175179
}
176180
return drawableElements;
177181
}
182+
183+
function drawElement(ctx, chartArea, state, item) {
184+
const el = item.element;
185+
if (item.main) {
186+
invokeHook(state, el, 'beforeDraw');
187+
el.draw(ctx, chartArea);
188+
invokeHook(state, el, 'afterDraw');
189+
} else {
190+
el.draw(ctx, chartArea);
191+
}
192+
}

src/elements.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import {Animations} from 'chart.js';
22
import {isObject, defined} from 'chart.js/helpers';
3-
import {hooks} from './events';
3+
import {eventHooks} from './events';
4+
import {elementHooks} from './hooks';
45
import {annotationTypes} from './types';
56

67
const directUpdater = {
78
update: Object.assign
89
};
910

11+
const hooks = eventHooks.concat(elementHooks);
12+
1013
/**
1114
* @typedef { import("chart.js").Chart } Chart
1215
* @typedef { import("chart.js").UpdateMode } UpdateMode

src/events.js

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import {defined, callback} from 'chart.js/helpers';
1+
import {callback} from 'chart.js/helpers';
22
import {getElements} from './interaction';
3+
import {loadHooks} from './helpers';
34

45
const moveHooks = ['enter', 'leave'];
56

@@ -8,26 +9,18 @@ const moveHooks = ['enter', 'leave'];
89
* @typedef { import('../../types/options').AnnotationPluginOptions } AnnotationPluginOptions
910
*/
1011

11-
export const hooks = moveHooks.concat('click');
12+
export const eventHooks = moveHooks.concat('click');
1213

1314
/**
1415
* @param {Chart} chart
1516
* @param {Object} state
1617
* @param {AnnotationPluginOptions} options
1718
*/
1819
export function updateListeners(chart, state, options) {
19-
state.listened = false;
20+
state.listened = loadHooks(options, eventHooks, state.listeners);
2021
state.moveListened = false;
2122
state._getElements = getElements; // for testing
2223

23-
hooks.forEach(hook => {
24-
if (typeof options[hook] === 'function') {
25-
state.listened = true;
26-
state.listeners[hook] = options[hook];
27-
} else if (defined(state.listeners[hook])) {
28-
delete state.listeners[hook];
29-
}
30-
});
3124
moveHooks.forEach(hook => {
3225
if (typeof options[hook] === 'function') {
3326
state.moveListened = true;

src/helpers/helpers.options.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,22 @@ export function toPosition(value) {
8585
export function isBoundToPoint(options) {
8686
return options && (defined(options.xValue) || defined(options.yValue));
8787
}
88+
89+
/**
90+
* @param {Object} options
91+
* @param {Array} hooks
92+
* @param {Object} hooksContainer
93+
* @returns {boolean}
94+
*/
95+
export function loadHooks(options, hooks, hooksContainer) {
96+
let activated = false;
97+
hooks.forEach(hook => {
98+
if (typeof options[hook] === 'function') {
99+
activated = true;
100+
hooksContainer[hook] = options[hook];
101+
} else if (defined(hooksContainer[hook])) {
102+
delete hooksContainer[hook];
103+
}
104+
});
105+
return activated;
106+
}

src/hooks.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {callback} from 'chart.js/helpers';
2+
import {loadHooks} from './helpers';
3+
4+
/**
5+
* @typedef { import("chart.js").Chart } Chart
6+
* @typedef { import('../../types/options').AnnotationPluginOptions } AnnotationPluginOptions
7+
* @typedef { import('../../types/element').AnnotationElement } AnnotationElement
8+
*/
9+
10+
export const elementHooks = ['afterDraw', 'beforeDraw'];
11+
12+
/**
13+
* @param {Chart} chart
14+
* @param {Object} state
15+
* @param {AnnotationPluginOptions} options
16+
*/
17+
export function updateHooks(chart, state, options) {
18+
const visibleElements = state.visibleElements;
19+
state.hooked = loadHooks(options, elementHooks, state.hooks);
20+
21+
if (!state.hooked) {
22+
visibleElements.forEach(scope => {
23+
if (!state.hooked) {
24+
elementHooks.forEach(hook => {
25+
if (typeof scope.options[hook] === 'function') {
26+
state.hooked = true;
27+
}
28+
});
29+
}
30+
});
31+
}
32+
}
33+
34+
/**
35+
* @param {Object} state
36+
* @param {AnnotationElement} element
37+
* @param {string} hook
38+
*/
39+
export function invokeHook(state, element, hook) {
40+
if (state.hooked) {
41+
const callbackHook = element.options[hook] || state.hooks[hook];
42+
return callback(callbackHook, [element.$context]);
43+
}
44+
}

0 commit comments

Comments
 (0)