Plugin hooks and jsdoc enhancements

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`).
This commit is contained in:
Simon Brunel
2017-01-22 20:13:40 +01:00
committed by Evert Timberg
parent c6fa4e5582
commit 979341ecb0
4 changed files with 301 additions and 96 deletions

View File

@@ -412,7 +412,7 @@ Plugins will be called at the following times
* 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) { },

View File

@@ -3,6 +3,7 @@
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
@@ -101,16 +102,17 @@ module.exports = function(Chart) {
}
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);
@@ -125,10 +127,9 @@ module.exports = function(Chart) {
me.ensureScalesHaveIDs();
me.buildScales();
me.initToolTip();
me.update();
// After init plugin notification
Chart.plugins.notify(me, 'afterInit');
plugins.notify(me, 'afterInit');
return me;
},
@@ -170,7 +171,7 @@ module.exports = function(Chart) {
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) {
@@ -287,7 +288,6 @@ module.exports = function(Chart) {
/**
* Reset the elements of all datasets
* @method resetElements
* @private
*/
resetElements: function() {
@@ -299,19 +299,20 @@ module.exports = function(Chart) {
/**
* 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;
@@ -324,10 +325,7 @@ module.exports = function(Chart) {
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) {
@@ -337,7 +335,7 @@ module.exports = function(Chart) {
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 = {
@@ -350,51 +348,64 @@ module.exports = function(Chart) {
},
/**
* @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;
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');
},
/**
* @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
*/
/**
* 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();
}
Chart.plugins.notify(me, 'afterDatasetsUpdate');
if (plugins.notify(me, 'beforeDatasetsUpdate') === false) {
return;
}
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
@@ -411,15 +422,14 @@ module.exports = function(Chart) {
// 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;
},
@@ -428,31 +438,45 @@ module.exports = function(Chart) {
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]);
// 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, true);
Chart.plugins.notify(me, 'afterDatasetsDraw', [easingDecimal]);
me.drawDatasets(easingDecimal);
// Finally draw the tooltip
me.tooltip.transition(easingDecimal).draw();
Chart.plugins.notify(me, 'afterDraw', [easingDecimal]);
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(easingValue);
}
}, me, true);
plugins.notify(me, 'afterDatasetsDraw', [easingValue]);
},
// Get the single element that was clicked on
@@ -551,7 +575,7 @@ module.exports = function(Chart) {
me.chart.ctx = null;
}
Chart.plugins.notify(me, 'destroy');
plugins.notify(me, 'destroy');
delete Chart.instances[me.id];
},
@@ -642,7 +666,7 @@ module.exports = function(Chart) {
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) {

View File

@@ -3,7 +3,6 @@
module.exports = function(Chart) {
var helpers = Chart.helpers;
var noop = helpers.noop;
Chart.defaults.global.plugins = {};
@@ -86,15 +85,15 @@ module.exports = function(Chart) {
},
/**
* 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;
@@ -102,7 +101,7 @@ module.exports = function(Chart) {
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);
@@ -162,38 +161,149 @@ module.exports = function(Chart) {
};
/**
* 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({});
};

View File

@@ -582,4 +582,75 @@ describe('Chart.Controller', function() {
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();
});
});
});
});