diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index 952ec88f51b..58dd50da996 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -378,6 +378,8 @@ Plugins will be called at the following times * End of initialization * Start of update * After the chart scales have calculated +* Start of datasets update +* End of datasets update * End of update (before render occurs) * Start of draw * End of draw @@ -396,9 +398,11 @@ Plugins should derive from Chart.PluginBase and implement the following interfac beforeUpdate: function(chartInstance) { }, afterScaleUpdate: function(chartInstance) { } + beforeDatasetsUpdate: function(chartInstance) { } + afterDatasetsUpdate: function(chartInstance) { } afterUpdate: function(chartInstance) { }, - // This is called at the start of a render. It is only called once, even if the animation will run for a number of frames. Use beforeDraw or afterDraw + // This is called at the start of a render. It is only called once, even if the animation will run for a number of frames. Use beforeDraw or afterDraw // to do something on each animation frame beforeRender: function(chartInstance) { }, @@ -406,8 +410,8 @@ Plugins should derive from Chart.PluginBase and implement the following interfac beforeDraw: function(chartInstance, easing) { }, afterDraw: function(chartInstance, easing) { }, // Before the datasets are drawn but after scales are drawn - beforeDatasetDraw: function(chartInstance, easing) { }, - afterDatasetDraw: function(chartInstance, easing) { }, + beforeDatasetsDraw: function(chartInstance, easing) { }, + afterDatasetsDraw: function(chartInstance, easing) { }, destroy: function(chartInstance) { } } diff --git a/src/chart.js b/src/chart.js index f992a9e1c45..75bd4753841 100644 --- a/src/chart.js +++ b/src/chart.js @@ -1,3 +1,6 @@ +/** + * @namespace Chart + */ var Chart = require('./core/core.js')(); require('./core/core.helpers')(Chart); diff --git a/src/core/core.controller.js b/src/core/core.controller.js index db521c607eb..2d0148b7749 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -13,7 +13,10 @@ module.exports = function(Chart) { // Controllers available for dataset visualization eg. bar, line, slice, etc. Chart.controllers = {}; - // The main controller of a chart + /** + * @class Chart.Controller + * The main controller of a chart. + */ Chart.Controller = function(instance) { this.chart = instance; @@ -40,12 +43,12 @@ module.exports = function(Chart) { return this; }; - helpers.extend(Chart.Controller.prototype, { + helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller */ { initialize: function initialize() { var me = this; // Before init plugin notification - Chart.pluginService.notifyPlugins('beforeInit', [me]); + Chart.plugins.notify('beforeInit', [me]); me.bindEvents(); @@ -60,7 +63,7 @@ module.exports = function(Chart) { me.update(); // After init plugin notification - Chart.pluginService.notifyPlugins('afterInit', [me]); + Chart.plugins.notify('afterInit', [me]); return me; }, @@ -83,7 +86,7 @@ module.exports = function(Chart) { var newWidth = helpers.getMaximumWidth(canvas); var aspectRatio = chart.aspectRatio; var newHeight = (me.options.maintainAspectRatio && isNaN(aspectRatio) === false && isFinite(aspectRatio) && aspectRatio !== 0) ? newWidth / aspectRatio : helpers.getMaximumHeight(canvas); - + var sizeChanged = chart.width !== newWidth || chart.height !== newHeight; if (!sizeChanged) { @@ -97,7 +100,7 @@ module.exports = function(Chart) { // Notify any plugins about the resize var newSize = { width: newWidth, height: newHeight }; - Chart.pluginService.notifyPlugins('resize', [me, newSize]); + Chart.plugins.notify('resize', [me, newSize]); // Notify of resize if (me.options.onResize) { @@ -225,7 +228,7 @@ module.exports = function(Chart) { update: function update(animationDuration, lazy) { var me = this; - Chart.pluginService.notifyPlugins('beforeUpdate', [me]); + Chart.plugins.notify('beforeUpdate', [me]); // In case the entire data object changed me.tooltip._data = me.data; @@ -241,27 +244,65 @@ module.exports = function(Chart) { Chart.layoutService.update(me, me.chart.width, me.chart.height); // Apply changes to the dataets that require the scales to have been calculated i.e BorderColor chages - Chart.pluginService.notifyPlugins('afterScaleUpdate', [me]); + Chart.plugins.notify('afterScaleUpdate', [me]); // Can only reset the new controllers after the scales have been updated helpers.each(newControllers, function(controller) { controller.reset(); }); - // This will loop through any data and do the appropriate element update for the type - helpers.each(me.data.datasets, function(dataset, datasetIndex) { - me.getDatasetMeta(datasetIndex).controller.update(); - }, me); + me.updateDatasets(); // Do this before render so that any plugins that need final scale updates can use it - Chart.pluginService.notifyPlugins('afterUpdate', [me]); + Chart.plugins.notify('afterUpdate', [me]); me.render(animationDuration, lazy); }, + /** + * @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 + */ + + /** + * @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 + */ + updateDatasets: function() { + var me = this; + var i, ilen; + + if (Chart.plugins.notify('beforeDatasetsUpdate', [ me ])) { + for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + me.getDatasetMeta(i).controller.update(); + } + + Chart.plugins.notify('afterDatasetsUpdate', [ me ]); + } + }, + render: function render(duration, lazy) { var me = this; - Chart.pluginService.notifyPlugins('beforeRender', [me]); + Chart.plugins.notify('beforeRender', [me]); var animationOptions = me.options.animation; if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) { @@ -297,7 +338,7 @@ module.exports = function(Chart) { var easingDecimal = ease || 1; me.clear(); - Chart.pluginService.notifyPlugins('beforeDraw', [me, easingDecimal]); + Chart.plugins.notify('beforeDraw', [me, easingDecimal]); // Draw all the scales helpers.each(me.boxes, function(box) { @@ -307,7 +348,7 @@ module.exports = function(Chart) { me.scale.draw(); } - Chart.pluginService.notifyPlugins('beforeDatasetDraw', [me, easingDecimal]); + Chart.plugins.notify('beforeDatasetsDraw', [me, easingDecimal]); // Draw each dataset via its respective controller (reversed to support proper line stacking) helpers.each(me.data.datasets, function(dataset, datasetIndex) { @@ -316,12 +357,12 @@ module.exports = function(Chart) { } }, me, true); - Chart.pluginService.notifyPlugins('afterDatasetDraw', [me, easingDecimal]); + Chart.plugins.notify('afterDatasetsDraw', [me, easingDecimal]); // Finally draw the tooltip me.tooltip.transition(easingDecimal).draw(); - Chart.pluginService.notifyPlugins('afterDraw', [me, easingDecimal]); + Chart.plugins.notify('afterDraw', [me, easingDecimal]); }, // Get the single element that was clicked on @@ -470,7 +511,7 @@ module.exports = function(Chart) { canvas.style.width = me.chart.originalCanvasStyleWidth; canvas.style.height = me.chart.originalCanvasStyleHeight; - Chart.pluginService.notifyPlugins('destroy', [me]); + Chart.plugins.notify('destroy', [me]); delete Chart.instances[me.id]; }, diff --git a/src/core/core.legend.js b/src/core/core.legend.js index b3d392c05fc..78c89c55925 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -420,7 +420,7 @@ module.exports = function(Chart) { }); // Register the legend plugin - Chart.pluginService.register({ + Chart.plugins.register({ beforeInit: function(chartInstance) { var opts = chartInstance.options; var legendOpts = opts.legend; diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js index 31be4d48b4a..d6f95c4d117 100644 --- a/src/core/core.plugin.js +++ b/src/core/core.plugin.js @@ -1,42 +1,102 @@ "use strict"; module.exports = function(Chart) { - var helpers = Chart.helpers; - - // Plugins are stored here - Chart.plugins = []; - Chart.pluginService = { - // Register a new plugin - register: function(plugin) { - var p = Chart.plugins; - if (p.indexOf(plugin) === -1) { - p.push(plugin); - } + + var noop = Chart.helpers.noop; + + /** + * The plugin service singleton + * @namespace Chart.plugins + * @since 2.1.0 + */ + Chart.plugins = { + _plugins: [], + + /** + * Registers the given plugin(s) if not already registered. + * @param {Array|Object} plugins plugin instance(s). + */ + register: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + if (p.indexOf(plugin) === -1) { + p.push(plugin); + } + }); }, - // Remove a registered plugin - remove: function(plugin) { - var p = Chart.plugins; - var idx = p.indexOf(plugin); - if (idx !== -1) { - p.splice(idx, 1); - } + /** + * Unregisters the given plugin(s) only if registered. + * @param {Array|Object} plugins plugin instance(s). + */ + unregister: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + var idx = p.indexOf(plugin); + if (idx !== -1) { + p.splice(idx, 1); + } + }); + }, + + /** + * Remove all registered p^lugins. + * @since 2.1.5 + */ + clear: function() { + this._plugins = []; }, - // Iterate over all plugins - notifyPlugins: function(method, args, scope) { - helpers.each(Chart.plugins, function(plugin) { - if (plugin[method] && typeof plugin[method] === 'function') { - plugin[method].apply(scope, args); + /** + * Returns the number of registered plugins? + * @returns {Number} + * @since 2.1.5 + */ + count: function() { + return this._plugins.length; + }, + + /** + * Returns all registered plugin intances. + * @returns {Array} array of plugin objects. + * @since 2.1.5 + */ + getAll: function() { + return this._plugins; + }, + + /** + * Calls registered plugins on the specified extension, 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 {String} extension the name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {Array} [args] extra arguments to apply to the extension call. + * @returns {Boolean} false if any of the plugins return false, else returns true. + */ + notify: function(extension, args) { + var plugins = this._plugins; + var ilen = plugins.length; + var i, plugin; + + for (i=0; i