Skip to content

Commit 1febdcd

Browse files
committed
Rewrote the annotation update handler, bugfixes
* Fixes a bug where calling `chartInstance.update()` broke the referential integrity of the annotations, preventing further updates * Fixes a bug where the `hovering` element flag was getting reset when calling `chartInstance.update()` from inside the `onMouseover` handler * Annotations now use an unambiguous unique ID rather than an array index to map configs to element instances * Properly hooked in the scale adjuster using the `afterDataLimits` scale option * Refactored the plugin object to its own file * Refactored helpers.js to use a consistent module format * Pointed the `main` property in `package.json` back to the `src` directory to deal with Webpack warnings * Tighter plugin code and var names
1 parent 5d14e26 commit 1febdcd

File tree

12 files changed

+400
-327
lines changed

12 files changed

+400
-327
lines changed

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# chartjs-plugin-annotation.js
22

3-
An annotation plugin for Chart.js >= 2.1.3
3+
An annotation plugin for Chart.js >= 2.4.0
44

55
Currently draws lines and boxes on the chart area.
66

@@ -13,6 +13,7 @@ To configure the annotations plugin, you can simply add new config options to yo
1313
{
1414
annotation: {
1515
annotations: [{
16+
id: 'a-line-1', // optional
1617
type: 'line',
1718
mode: 'horizontal',
1819
scaleID: 'y-axis-0',
@@ -38,7 +39,13 @@ To configure the annotations plugin, you can simply add new config options to yo
3839
// Mouse events to enable on each annotation.
3940
// Should be an array of one or more browser-supported mouse events
4041
// See https://developer.mozilla.org/en-US/docs/Web/Events
41-
events: ['click']
42+
events: ['click'],
43+
44+
// Double-click speed in ms used to distinguish single-clicks from
45+
// double-clicks whenever you need to capture both. When listening for
46+
// both click and dblclick, click events will be delayed by this
47+
// amount.
48+
dblClickSpeed: 350 // ms (default)
4249
}
4350
}
4451
```
@@ -50,6 +57,9 @@ Vertical or horizontal lines are supported.
5057
{
5158
type: 'line',
5259

60+
// optional annotation ID (must be unique)
61+
id: 'a-line-1',
62+
5363
// set to 'vertical' to draw a vertical line
5464
mode: 'horizontal',
5565

@@ -148,6 +158,9 @@ The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the bo
148158
{
149159
type: 'box',
150160

161+
// optional annotation ID (must be unique)
162+
id: 'a-box-1',
163+
151164
// ID of the X scale to bind onto
152165
xScaleID: 'x-axis-0',
153166

gulpfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ gulp.task('jshint', jshintTask);
3535
gulp.task('watch', watchTask);
3636

3737
function buildTask() {
38-
var nonBundled = browserify('./src/plugin.js')
38+
var nonBundled = browserify('./src/index.js')
3939
.ignore('chart.js')
4040
.ignore('hammerjs')
4141
.bundle()

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "Annotations for Chart.js",
44
"version": "0.5.1",
55
"license": "MIT",
6-
"main": "chartjs-plugin-annotation.js",
6+
"main": "src/index.js",
77
"repository": {
88
"type": "git",
99
"url": "https://github.com/chartjs/chartjs-plugin-annotation.git"
@@ -27,6 +27,6 @@
2727
"vinyl-source-stream": "^1.1.0"
2828
},
2929
"dependencies": {
30-
"chart.js": "^2.1.0"
30+
"chart.js": "^2.4.0"
3131
}
3232
}

samples/horizontal-line.html

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -134,32 +134,52 @@
134134
scaleID: 'y-axis-1',
135135
value: 120,
136136
borderColor: 'rgba(255, 0, 0, 0.5)',
137-
borderWidth: 2,
137+
borderWidth: 4,
138+
label: {
139+
enabled: false,
140+
content: 'Test label'
141+
},
138142
onClick: function(e) {
139-
this._model.borderColor = 'rgba(255, 0, 0, 1.0)';
140-
this.chartInstance.render(0);
141-
setTimeout((function() {
142-
this._model.borderColor = 'rgba(255, 0, 0, 0.5)';
143-
this.chartInstance.render(0);
144-
}).bind(this), 500);
143+
var element = this;
144+
element.options.borderColor = 'rgba(255, 0, 0, 1.0)';
145+
element.options.label.enabled = true;
146+
element.options.label.content = e.type;
147+
element.chartInstance.update();
148+
setTimeout(function() {
149+
element.options.borderColor = 'rgba(255, 0, 0, 0.5)';
150+
element.chartInstance.update();
151+
}, 500);
145152
},
146153
onDblclick: function(e) {
147-
this._model.borderColor = 'rgba(0, 255, 0, 1.0)';
148-
this.chartInstance.render(0);
149-
setTimeout((function() {
150-
this._model.borderColor = 'rgba(255, 0, 0, 0.5)';
151-
this.chartInstance.render(0);
152-
}).bind(this), 500);
154+
var element = this;
155+
element.options.borderColor = 'rgba(0, 255, 0, 1.0)';
156+
element.options.label.enabled = true;
157+
element.options.label.content = e.type;
158+
element.chartInstance.update();
159+
setTimeout(function() {
160+
element.options.borderColor = 'rgba(255, 0, 0, 0.5)';
161+
element.chartInstance.update();
162+
}, 500);
153163
},
154164
onMouseover: function(e) {
155-
this._model.borderWidth = 7;
156-
this.chartInstance.render(0);
157-
this.chartInstance.chart.canvas.style.cursor = 'pointer';
165+
var element = this;
166+
element.options.borderWidth = 7;
167+
element.options.label.enabled = true;
168+
element.options.label.content = e.type;
169+
element.chartInstance.update();
170+
element.chartInstance.chart.canvas.style.cursor = 'pointer';
158171
},
159172
onMouseout: function(e) {
160-
this._model.borderWidth = 5;
161-
this.chartInstance.render(0);
162-
this.chartInstance.chart.canvas.style.cursor = 'initial';
173+
var element = this;
174+
element.options.borderWidth = 4;
175+
element.options.label.enabled = true;
176+
element.options.label.content = e.type;
177+
element.chartInstance.update();
178+
setTimeout(function() {
179+
element.options.label.enabled = false;
180+
element.chartInstance.update();
181+
}, 500);
182+
element.chartInstance.chart.canvas.style.cursor = 'initial';
163183
}
164184
}]
165185
}

samples/labels.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
annotation: {
134134
events: ['click'],
135135
annotations: [{
136+
id: 'hline',
136137
type: 'line',
137138
mode: 'horizontal',
138139
scaleID: 'y-axis-1',
@@ -151,6 +152,7 @@
151152
}
152153
},
153154
{
155+
id: 'vline',
154156
type: 'line',
155157
mode: 'vertical',
156158
scaleID: 'x-axis-1',

src/annotation.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
module.exports = function(Chart) {
2+
var chartHelpers = Chart.helpers;
3+
4+
var helpers = require('./helpers.js')(Chart);
5+
var events = require('./events.js')(Chart);
6+
7+
var annotationTypes = Chart.Annotation.types;
8+
9+
function setAfterDataLimitsHook(axisOptions) {
10+
helpers.decorate(axisOptions, 'afterDataLimits', function(previous, scale) {
11+
if (previous) previous(scale);
12+
helpers.adjustScaleRange(scale);
13+
});
14+
}
15+
16+
return {
17+
beforeInit: function(chartInstance) {
18+
var chartOptions = chartInstance.options;
19+
20+
// Initialize chart instance plugin namespace
21+
var ns = chartInstance.annotation = {
22+
elements: {},
23+
options: helpers.initConfig(chartOptions.annotation || {}),
24+
onDestroy: [],
25+
firstRun: true
26+
};
27+
28+
ns[ns.options.drawTime] = function(easingDecimal) {
29+
helpers.elements(chartInstance).forEach(function(element) {
30+
element.transition(easingDecimal).draw();
31+
});
32+
};
33+
34+
// Add the annotation scale adjuster to each scale's afterDataLimits hook
35+
chartInstance.ensureScalesHaveIDs();
36+
chartHelpers.each(chartOptions.scales.xAxes, setAfterDataLimitsHook);
37+
chartHelpers.each(chartOptions.scales.yAxes, setAfterDataLimitsHook);
38+
},
39+
beforeUpdate: function(chartInstance) {
40+
var ns = chartInstance.annotation;
41+
42+
if (!ns.firstRun) {
43+
ns.options = helpers.initConfig(chartInstance.options.annotation || {});
44+
} else {
45+
ns.firstRun = false;
46+
}
47+
48+
var elementIds = [];
49+
50+
// Add new elements, or update existing ones
51+
ns.options.annotations.forEach(function(annotation) {
52+
var id = annotation.id || helpers.objectId();
53+
54+
// No element with that ID exists, and it's a valid annotation type
55+
if (!ns.elements[id] && annotationTypes[annotation.type]) {
56+
var cls = annotationTypes[annotation.type];
57+
var element = new cls({
58+
id: id,
59+
options: annotation,
60+
chartInstance: chartInstance,
61+
});
62+
element.initialize();
63+
ns.elements[id] = element;
64+
annotation.id = id;
65+
elementIds.push(id);
66+
} else if (ns.elements[id]) {
67+
// Nothing to do for update, since the element config references
68+
// the same object that exists in the chart annotation config
69+
elementIds.push(id);
70+
}
71+
});
72+
73+
// Delete removed elements
74+
Object.keys(ns.elements).forEach(function(id) {
75+
if (elementIds.indexOf(id) === -1) {
76+
ns.elements[id].destroy();
77+
delete ns.elements[id];
78+
}
79+
});
80+
},
81+
afterScaleUpdate: function(chartInstance) {
82+
helpers.elements(chartInstance).forEach(function(element) {
83+
element.configure();
84+
});
85+
},
86+
beforeDatasetsDraw: function(chartInstance, easingDecimal) {
87+
(chartInstance.annotation.beforeDatasetsDraw || helpers.noop)(easingDecimal);
88+
},
89+
afterDatasetsDraw: function(chartInstance, easingDecimal) {
90+
(chartInstance.annotation.afterDatasetsDraw || helpers.noop)(easingDecimal);
91+
},
92+
afterDraw: function(chartInstance, easingDecimal) {
93+
(chartInstance.annotation.afterDraw || helpers.noop)(easingDecimal);
94+
},
95+
afterInit: function(chartInstance) {
96+
// Detect and intercept events that happen on an annotation element
97+
var watchFor = chartInstance.annotation.options.events;
98+
if (watchFor.length > 0) {
99+
var canvas = chartInstance.chart.canvas;
100+
var eventHandler = events.dispatcher.bind(chartInstance);
101+
events.collapseHoverEvents(watchFor).forEach(function(eventName) {
102+
chartHelpers.addEvent(canvas, eventName, eventHandler);
103+
chartInstance.annotation.onDestroy.push(function() {
104+
chartHelpers.removeEvent(canvas, eventName, eventHandler);
105+
});
106+
});
107+
}
108+
},
109+
destroy: function(chartInstance) {
110+
var deregisterers = chartInstance.annotation.onDestroy;
111+
while (deregisterers.length > 0) {
112+
deregisterers.pop()();
113+
}
114+
}
115+
};
116+
};

src/events.js

Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,6 @@
1-
var helpers = require('./helpers.js');
2-
31
module.exports = function(Chart) {
42
var chartHelpers = Chart.helpers;
5-
6-
var dblClickSpeed = Chart.Annotation.dblClickSpeed || 350; // ms
7-
8-
function getEventHandlerName(eventName) {
9-
return 'on' + eventName[0].toUpperCase() + eventName.substring(1);
10-
}
11-
12-
function createMouseEvent(type, previousEvent) {
13-
try {
14-
return new MouseEvent(type, previousEvent);
15-
} catch (exception) {
16-
try {
17-
var m = document.createEvent('MouseEvent');
18-
m.initMouseEvent(
19-
type,
20-
previousEvent.canBubble,
21-
previousEvent.cancelable,
22-
previousEvent.view,
23-
previousEvent.detail,
24-
previousEvent.screenX,
25-
previousEvent.screenY,
26-
previousEvent.clientX,
27-
previousEvent.clientY,
28-
previousEvent.ctrlKey,
29-
previousEvent.altKey,
30-
previousEvent.shiftKey,
31-
previousEvent.metaKey,
32-
previousEvent.button,
33-
previousEvent.relatedTarget
34-
);
35-
return m;
36-
} catch (exception2) {
37-
var e = document.createEvent('Event');
38-
e.initEvent(
39-
type,
40-
previousEvent.canBubble,
41-
previousEvent.cancelable
42-
);
43-
return e;
44-
}
45-
}
46-
}
3+
var helpers = require('./helpers.js')(Chart);
474

485
function collapseHoverEvents(events) {
496
var hover = false;
@@ -67,34 +24,37 @@ module.exports = function(Chart) {
6724
}
6825

6926
function dispatcher(e) {
27+
var ns = this.annotation;
28+
var elements = helpers.elements(this);
7029
var position = chartHelpers.getRelativePosition(e, this.chart);
71-
var element = helpers.getNearestItems(this.annotations, position);
72-
var events = collapseHoverEvents(this.options.annotation.events);
30+
var element = helpers.getNearestItems(elements, position);
31+
var events = collapseHoverEvents(ns.options.events);
32+
var dblClickSpeed = ns.options.dblClickSpeed;
7333
var eventHandlers = [];
74-
var eventHandlerName = getEventHandlerName(e.type);
34+
var eventHandlerName = helpers.getEventHandlerName(e.type);
7535
var options = (element || {}).options;
7636

7737
// Detect hover events
7838
if (e.type === 'mousemove') {
7939
if (element && !element.hovering) {
8040
// hover started
8141
['mouseenter', 'mouseover'].forEach(function(eventName) {
82-
var eventHandlerName = getEventHandlerName(eventName);
83-
var hoverEvent = createMouseEvent(eventName, e); // recreate the event to match the handler
42+
var eventHandlerName = helpers.getEventHandlerName(eventName);
43+
var hoverEvent = helpers.createMouseEvent(eventName, e); // recreate the event to match the handler
8444
element.hovering = true;
8545
if (typeof options[eventHandlerName] === 'function') {
8646
eventHandlers.push([ options[eventHandlerName], hoverEvent, element ]);
8747
}
8848
});
8949
} else if (!element) {
9050
// hover ended
91-
this.annotations.forEach(function(element) {
51+
elements.forEach(function(element) {
9252
if (element.hovering) {
9353
element.hovering = false;
9454
var options = element.options;
9555
['mouseout', 'mouseleave'].forEach(function(eventName) {
96-
var eventHandlerName = getEventHandlerName(eventName);
97-
var hoverEvent = createMouseEvent(eventName, e); // recreate the event to match the handler
56+
var eventHandlerName = helpers.getEventHandlerName(eventName);
57+
var hoverEvent = helpers.createMouseEvent(eventName, e); // recreate the event to match the handler
9858
if (typeof options[eventHandlerName] === 'function') {
9959
eventHandlers.push([ options[eventHandlerName], hoverEvent, element ]);
10060
}

0 commit comments

Comments
 (0)