Make all `before` hooks cancellable (except `beforeInit`), meaning that if any plugin return explicitly `false`, the current action is not performed. Ensure that `init` hooks are called before `update` hooks and add associated calling order unit tests. Deprecate `Chart.PluginBase` in favor of `IPlugin` (no more an inheritable class) and document plugin hooks (also rename `extension` by `hook`).
* Before an animation is started
* When an event occurs on the canvas (mousemove, click, etc). This requires the `options.events` property handled
-Plugins should derive from Chart.PluginBase and implement the following interface
+Plugins should implement the `IPlugin` interface:
```javascript
{
beforeInit: function(chartInstance) { },
module.exports = function(Chart) {
var helpers = Chart.helpers;
+ var plugins = Chart.plugins;
var platform = Chart.platform;
// Create a dictionary of chart types, to allow for extension of existing types
}
me.initialize();
+ me.update();
return me;
};
- helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller */ {
+ helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller.prototype */ {
initialize: function() {
var me = this;
// Before init plugin notification
- Chart.plugins.notify(me, 'beforeInit');
+ plugins.notify(me, 'beforeInit');
helpers.retinaScale(me.chart);
me.ensureScalesHaveIDs();
me.buildScales();
me.initToolTip();
- me.update();
// After init plugin notification
- Chart.plugins.notify(me, 'afterInit');
+ plugins.notify(me, 'afterInit');
return me;
},
if (!silent) {
// Notify any plugins about the resize
var newSize = {width: newWidth, height: newHeight};
- Chart.plugins.notify(me, 'resize', [newSize]);
+ plugins.notify(me, 'resize', [newSize]);
// Notify of resize
if (me.options.onResize) {
/**
* Reset the elements of all datasets
- * @method resetElements
* @private
*/
resetElements: function() {
/**
* Resets the chart back to it's state before the initial animation
- * @method reset
*/
reset: function() {
this.resetElements();
this.tooltip.initialize();
},
-
update: function(animationDuration, lazy) {
var me = this;
updateConfig(me);
- Chart.plugins.notify(me, 'beforeUpdate');
+
+ if (plugins.notify(me, 'beforeUpdate') === false) {
+ return;
+ }
// In case the entire data object changed
me.tooltip._data = me.data;
me.getDatasetMeta(datasetIndex).controller.buildOrUpdateElements();
}, me);
- Chart.layoutService.update(me, me.chart.width, me.chart.height);
-
- // Apply changes to the datasets that require the scales to have been calculated i.e BorderColor changes
- Chart.plugins.notify(me, 'afterScaleUpdate');
+ me.updateLayout();
// Can only reset the new controllers after the scales have been updated
helpers.each(newControllers, function(controller) {
me.updateDatasets();
// Do this before render so that any plugins that need final scale updates can use it
- Chart.plugins.notify(me, 'afterUpdate');
+ plugins.notify(me, 'afterUpdate');
if (me._bufferedRender) {
me._bufferedRequest = {
},
/**
- * @method beforeDatasetsUpdate
- * @description Called before all datasets are updated. If a plugin returns false,
- * the datasets update will be cancelled until another chart update is triggered.
- * @param {Object} instance the chart instance being updated.
- * @returns {Boolean} false to cancel the datasets update.
- * @memberof Chart.PluginBase
- * @since version 2.1.5
- * @instance
+ * Updates the chart layout unless a plugin returns `false` to the `beforeLayout`
+ * hook, in which case, plugins will not be called on `afterLayout`.
+ * @private
*/
+ updateLayout: function() {
+ var me = this;
- /**
- * @method afterDatasetsUpdate
- * @description Called after all datasets have been updated. Note that this
- * extension will not be called if the datasets update has been cancelled.
- * @param {Object} instance the chart instance being updated.
- * @memberof Chart.PluginBase
- * @since version 2.1.5
- * @instance
- */
+ if (plugins.notify(me, 'beforeLayout') === false) {
+ return;
+ }
+
+ Chart.layoutService.update(this, this.chart.width, this.chart.height);
+
+ /**
+ * Provided for backward compatibility, use `afterLayout` instead.
+ * @method IPlugin#afterScaleUpdate
+ * @deprecated since version 2.5.0
+ * @todo remove at version 3
+ */
+ plugins.notify(me, 'afterScaleUpdate');
+ plugins.notify(me, 'afterLayout');
+ },
/**
- * Updates all datasets unless a plugin returns false to the beforeDatasetsUpdate
- * extension, in which case no datasets will be updated and the afterDatasetsUpdate
- * notification will be skipped.
- * @protected
- * @instance
+ * Updates all datasets unless a plugin returns `false` to the `beforeDatasetsUpdate`
+ * hook, in which case, plugins will not be called on `afterDatasetsUpdate`.
+ * @private
*/
updateDatasets: function() {
var me = this;
- var i, ilen;
- if (Chart.plugins.notify(me, 'beforeDatasetsUpdate')) {
- for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) {
- me.getDatasetMeta(i).controller.update();
- }
+ if (plugins.notify(me, 'beforeDatasetsUpdate') === false) {
+ return;
+ }
- Chart.plugins.notify(me, 'afterDatasetsUpdate');
+ for (var i = 0, ilen = me.data.datasets.length; i < ilen; ++i) {
+ me.getDatasetMeta(i).controller.update();
}
+
+ plugins.notify(me, 'afterDatasetsUpdate');
},
render: function(duration, lazy) {
var me = this;
- Chart.plugins.notify(me, 'beforeRender');
+
+ if (plugins.notify(me, 'beforeRender') === false) {
+ return;
+ }
var animationOptions = me.options.animation;
+ var onComplete = function() {
+ plugins.notify(me, 'afterRender');
+ var callback = animationOptions && animationOptions.onComplete;
+ if (callback && callback.call) {
+ callback.call(me);
+ }
+ };
+
if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) {
var animation = new Chart.Animation();
animation.numSteps = (duration || animationOptions.duration) / 16.66; // 60 fps
// user events
animation.onAnimationProgress = animationOptions.onProgress;
- animation.onAnimationComplete = animationOptions.onComplete;
+ animation.onAnimationComplete = onComplete;
Chart.animationService.addAnimation(me, animation, duration, lazy);
} else {
me.draw();
- if (animationOptions && animationOptions.onComplete && animationOptions.onComplete.call) {
- animationOptions.onComplete.call(me);
- }
+ onComplete();
}
+
return me;
},
var easingDecimal = ease || 1;
me.clear();
- Chart.plugins.notify(me, 'beforeDraw', [easingDecimal]);
+ plugins.notify(me, 'beforeDraw', [easingDecimal]);
// Draw all the scales
helpers.each(me.boxes, function(box) {
box.draw(me.chartArea);
}, me);
+
if (me.scale) {
me.scale.draw();
}
- Chart.plugins.notify(me, 'beforeDatasetsDraw', [easingDecimal]);
+ me.drawDatasets(easingDecimal);
+
+ // Finally draw the tooltip
+ me.tooltip.transition(easingDecimal).draw();
+
+ plugins.notify(me, 'afterDraw', [easingDecimal]);
+ },
+
+ /**
+ * Draws all datasets unless a plugin returns `false` to the `beforeDatasetsDraw`
+ * hook, in which case, plugins will not be called on `afterDatasetsDraw`.
+ * @private
+ */
+ drawDatasets: function(easingValue) {
+ var me = this;
+
+ if (plugins.notify(me, 'beforeDatasetsDraw', [easingValue]) === false) {
+ return;
+ }
// Draw each dataset via its respective controller (reversed to support proper line stacking)
helpers.each(me.data.datasets, function(dataset, datasetIndex) {
if (me.isDatasetVisible(datasetIndex)) {
- me.getDatasetMeta(datasetIndex).controller.draw(ease);
+ me.getDatasetMeta(datasetIndex).controller.draw(easingValue);
}
}, me, true);
- Chart.plugins.notify(me, 'afterDatasetsDraw', [easingDecimal]);
-
- // Finally draw the tooltip
- me.tooltip.transition(easingDecimal).draw();
-
- Chart.plugins.notify(me, 'afterDraw', [easingDecimal]);
+ plugins.notify(me, 'afterDatasetsDraw', [easingValue]);
},
// Get the single element that was clicked on
me.chart.ctx = null;
}
- Chart.plugins.notify(me, 'destroy');
+ plugins.notify(me, 'destroy');
delete Chart.instances[me.id];
},
var changed = me.handleEvent(e);
changed |= tooltip && tooltip.handleEvent(e);
- changed |= Chart.plugins.notify(me, 'onEvent', [e]);
+ changed |= plugins.notify(me, 'onEvent', [e]);
var bufferedRequest = me._bufferedRequest;
if (bufferedRequest) {
module.exports = function(Chart) {
var helpers = Chart.helpers;
- var noop = helpers.noop;
Chart.defaults.global.plugins = {};
},
/**
- * Calls enabled plugins for chart, on the specified extension and with the given args.
+ * Calls enabled plugins for `chart` on the specified hook and with the given args.
* This method immediately returns as soon as a plugin explicitly returns false. The
* returned value can be used, for instance, to interrupt the current action.
- * @param {Object} chart chart instance for which plugins should be called.
- * @param {String} extension the name of the plugin method to call (e.g. 'beforeUpdate').
- * @param {Array} [args] extra arguments to apply to the extension call.
+ * @param {Object} chart - The chart instance for which plugins should be called.
+ * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate').
+ * @param {Array} [args] - Extra arguments to apply to the hook call.
* @returns {Boolean} false if any of the plugins return false, else returns true.
*/
- notify: function(chart, extension, args) {
+ notify: function(chart, hook, args) {
var descriptors = this.descriptors(chart);
var ilen = descriptors.length;
var i, descriptor, plugin, params, method;
for (i=0; i<ilen; ++i) {
descriptor = descriptors[i];
plugin = descriptor.plugin;
- method = plugin[extension];
+ method = plugin[hook];
if (typeof method === 'function') {
params = [chart].concat(args || []);
params.push(descriptor.options);
};
/**
- * Plugin extension methods.
- * @interface Chart.PluginBase
+ * Plugin extension hooks.
+ * @interface IPlugin
* @since 2.1.0
*/
- Chart.PluginBase = helpers.inherits({
- // Called at start of chart init
- beforeInit: noop,
-
- // Called at end of chart init
- afterInit: noop,
-
- // Called at start of update
- beforeUpdate: noop,
-
- // Called at end of update
- afterUpdate: noop,
-
- // Called at start of draw
- beforeDraw: noop,
-
- // Called at end of draw
- afterDraw: noop,
-
- // Called during destroy
- destroy: noop
- });
+ /**
+ * @method IPlugin#beforeInit
+ * @desc Called before initializing `chart`.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ */
+ /**
+ * @method IPlugin#afterInit
+ * @desc Called after `chart` has been initialized and before the first update.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ */
+ /**
+ * @method IPlugin#beforeUpdate
+ * @desc Called before updating `chart`. If any plugin returns `false`, the update
+ * is cancelled (and thus subsequent render(s)) until another `update` is triggered.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ * @returns {Boolean} `false` to cancel the chart update.
+ */
+ /**
+ * @method IPlugin#afterUpdate
+ * @desc Called after `chart` has been updated and before rendering. Note that this
+ * hook will not be called if the chart update has been previously cancelled.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ */
+ /**
+ * @method IPlugin#beforeDatasetsUpdate
+ * @desc Called before updating the `chart` datasets. If any plugin returns `false`,
+ * the datasets update is cancelled until another `update` is triggered.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ * @returns {Boolean} false to cancel the datasets update.
+ * @since version 2.1.5
+ */
+ /**
+ * @method IPlugin#afterDatasetsUpdate
+ * @desc Called after the `chart` datasets have been updated. Note that this hook
+ * will not be called if the datasets update has been previously cancelled.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ * @since version 2.1.5
+ */
+ /**
+ * @method IPlugin#beforeLayout
+ * @desc Called before laying out `chart`. If any plugin returns `false`,
+ * the layout update is cancelled until another `update` is triggered.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ * @returns {Boolean} `false` to cancel the chart layout.
+ */
+ /**
+ * @method IPlugin#afterLayout
+ * @desc Called after the `chart` has been layed out. Note that this hook will not
+ * be called if the layout update has been previously cancelled.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ */
+ /**
+ * @method IPlugin#beforeRender
+ * @desc Called before rendering `chart`. If any plugin returns `false`,
+ * the rendering is cancelled until another `render` is triggered.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ * @returns {Boolean} `false` to cancel the chart rendering.
+ */
+ /**
+ * @method IPlugin#afterRender
+ * @desc Called after the `chart` has been fully rendered (and animation completed). Note
+ * that this hook will not be called if the rendering has been previously cancelled.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ */
+ /**
+ * @method IPlugin#beforeDraw
+ * @desc Called before drawing `chart` at every animation frame specified by the given
+ * easing value. If any plugin returns `false`, the frame drawing is cancelled until
+ * another `render` is triggered.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Number} easingValue - The current animation value, between 0.0 and 1.0.
+ * @param {Object} options - The plugin options.
+ * @returns {Boolean} `false` to cancel the chart drawing.
+ */
+ /**
+ * @method IPlugin#afterDraw
+ * @desc Called after the `chart` has been drawn for the specific easing value. Note
+ * that this hook will not be called if the drawing has been previously cancelled.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Number} easingValue - The current animation value, between 0.0 and 1.0.
+ * @param {Object} options - The plugin options.
+ */
+ /**
+ * @method IPlugin#beforeDatasetsDraw
+ * @desc Called before drawing the `chart` datasets. If any plugin returns `false`,
+ * the datasets drawing is cancelled until another `render` is triggered.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Number} easingValue - The current animation value, between 0.0 and 1.0.
+ * @param {Object} options - The plugin options.
+ * @returns {Boolean} `false` to cancel the chart datasets drawing.
+ */
+ /**
+ * @method IPlugin#afterDatasetsDraw
+ * @desc Called after the `chart` datasets have been drawn. Note that this hook
+ * will not be called if the datasets drawing has been previously cancelled.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Number} easingValue - The current animation value, between 0.0 and 1.0.
+ * @param {Object} options - The plugin options.
+ */
+ /**
+ * @method IPlugin#resize
+ * @desc Called after the chart as been resized.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Number} size - The new canvas display size (eq. canvas.style width & height).
+ * @param {Object} options - The plugin options.
+ */
+ /**
+ * @method IPlugin#destroy
+ * @desc Called after the chart as been destroyed.
+ * @param {Chart.Controller} chart - The chart instance.
+ * @param {Object} options - The plugin options.
+ */
/**
* Provided for backward compatibility, use Chart.plugins instead
* @namespace Chart.pluginService
* @deprecated since version 2.1.5
- * TODO remove me at version 3
+ * @todo remove at version 3
+ * @private
*/
Chart.pluginService = Chart.plugins;
+
+ /**
+ * Provided for backward compatibility, inheriting from Chart.PlugingBase has no
+ * effect, instead simply create/register plugins via plain JavaScript objects.
+ * @interface Chart.PluginBase
+ * @deprecated since version 2.5.0
+ * @todo remove at version 3
+ * @private
+ */
+ Chart.PluginBase = helpers.inherits({});
};
expect(chart.tooltip._options).toEqual(jasmine.objectContaining(newTooltipConfig));
});
});
+
+ describe('plugin.extensions', function() {
+ it ('should notify plugin in correct order', function(done) {
+ var plugin = this.plugin = {id: 'foobar'};
+ var sequence = [];
+ var hooks = {
+ init: [
+ 'beforeInit',
+ 'afterInit'
+ ],
+ update: [
+ 'beforeUpdate',
+ 'beforeLayout',
+ 'afterLayout',
+ 'beforeDatasetsUpdate',
+ 'afterDatasetsUpdate',
+ 'afterUpdate',
+ ],
+ render: [
+ 'beforeRender',
+ 'beforeDraw',
+ 'beforeDatasetsDraw',
+ 'afterDatasetsDraw',
+ 'afterDraw',
+ 'afterRender',
+ ],
+ resize: [
+ 'resize'
+ ],
+ destroy: [
+ 'destroy'
+ ]
+ };
+
+ Object.keys(hooks).forEach(function(group) {
+ hooks[group].forEach(function(name) {
+ plugin[name] = function() {
+ sequence.push(name);
+ };
+ });
+ });
+
+ var chart = window.acquireChart({
+ plugins: [plugin],
+ options: {
+ responsive: true
+ }
+ }, {
+ wrapper: {
+ style: 'width: 300px'
+ }
+ });
+
+ chart.chart.canvas.parentNode.style.width = '400px';
+ waitForResize(chart, function() {
+ chart.destroy();
+
+ expect(sequence).toEqual([].concat(
+ hooks.init,
+ hooks.update,
+ hooks.render,
+ hooks.resize,
+ hooks.update,
+ hooks.render,
+ hooks.destroy
+ ));
+
+ done();
+ });
+ });
+ });
});