]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Plugin hooks and jsdoc enhancements
authorSimon Brunel <simonbrunel@users.noreply.github.com>
Sun, 22 Jan 2017 19:13:40 +0000 (20:13 +0100)
committerEvert Timberg <evert.timberg+github@gmail.com>
Sat, 28 Jan 2017 00:32:35 +0000 (19:32 -0500)
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`).

docs/09-Advanced.md
src/core/core.controller.js
src/core/core.plugin.js
test/core.controller.tests.js

index 4c677c904d4144f6d46ba23c3cc4215dea31e9bf..d3d69d9afc953dfbc22bb7297b7edbf835c5a487 100644 (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) { },
index 1d2366152ec6350f1bdd51e710215cbcf2c94d29..e6c19659387192c1f33f288ccaf2e96c86ae5002 100644 (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;
 
-               /**
-                * @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
@@ -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]);
+                       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
@@ -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) {
index 657c85a35455fd0622c9e65bd993ef795e7cc6e3..f89b412d2e14bfabc576b79a7ae7ad42f2325ef8 100644 (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({});
 };
index 0e8956158123b83eb6ffc13a8e5a3295e045baf9..da4b77d1d214e5b3b2c60918136ffcdb08339a9c 100644 (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();
+                       });
+               });
+       });
 });