var me = this;
// Before init plugin notification
- Chart.plugins.notify('beforeInit', [me]);
+ Chart.plugins.notify(me, 'beforeInit');
me.bindEvents();
me.update();
// After init plugin notification
- Chart.plugins.notify('afterInit', [me]);
+ Chart.plugins.notify(me, 'afterInit');
return me;
},
if (!silent) {
// Notify any plugins about the resize
var newSize = {width: newWidth, height: newHeight};
- Chart.plugins.notify('resize', [me, newSize]);
+ Chart.plugins.notify(me, 'resize', [newSize]);
// Notify of resize
if (me.options.onResize) {
var me = this;
updateConfig(me);
- Chart.plugins.notify('beforeUpdate', [me]);
+ Chart.plugins.notify(me, 'beforeUpdate');
// In case the entire data object changed
me.tooltip._data = me.data;
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('afterScaleUpdate', [me]);
+ Chart.plugins.notify(me, 'afterScaleUpdate');
// 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('afterUpdate', [me]);
+ Chart.plugins.notify(me, 'afterUpdate');
if (me._bufferedRender) {
me._bufferedRequest = {
var me = this;
var i, ilen;
- if (Chart.plugins.notify('beforeDatasetsUpdate', [me])) {
+ 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('afterDatasetsUpdate', [me]);
+ Chart.plugins.notify(me, 'afterDatasetsUpdate');
}
},
render: function(duration, lazy) {
var me = this;
- Chart.plugins.notify('beforeRender', [me]);
+ Chart.plugins.notify(me, 'beforeRender');
var animationOptions = me.options.animation;
if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) {
var easingDecimal = ease || 1;
me.clear();
- Chart.plugins.notify('beforeDraw', [me, easingDecimal]);
+ Chart.plugins.notify(me, 'beforeDraw', [easingDecimal]);
// Draw all the scales
helpers.each(me.boxes, function(box) {
me.scale.draw();
}
- Chart.plugins.notify('beforeDatasetsDraw', [me, easingDecimal]);
+ 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) {
}
}, me, true);
- Chart.plugins.notify('afterDatasetsDraw', [me, easingDecimal]);
+ Chart.plugins.notify(me, 'afterDatasetsDraw', [easingDecimal]);
// Finally draw the tooltip
me.tooltip.transition(easingDecimal).draw();
- Chart.plugins.notify('afterDraw', [me, easingDecimal]);
+ Chart.plugins.notify(me, 'afterDraw', [easingDecimal]);
},
// Get the single element that was clicked on
me.chart.ctx = null;
}
- Chart.plugins.notify('destroy', [me]);
+ Chart.plugins.notify(me, 'destroy');
delete Chart.instances[me.id];
},
module.exports = function(Chart) {
- var noop = Chart.helpers.noop;
+ var helpers = Chart.helpers;
+ var noop = helpers.noop;
+
+ Chart.defaults.global.plugins = {};
/**
* The plugin service singleton
* @since 2.1.0
*/
Chart.plugins = {
+ /**
+ * Globally registered plugins.
+ * @private
+ */
_plugins: [],
+ /**
+ * This identifier is used to invalidate the descriptors cache attached to each chart
+ * when a global plugin is registered or unregistered. In this case, the cache ID is
+ * incremented and descriptors are regenerated during following API calls.
+ * @private
+ */
+ _cacheId: 0,
+
/**
* Registers the given plugin(s) if not already registered.
* @param {Array|Object} plugins plugin instance(s).
p.push(plugin);
}
});
+
+ this._cacheId++;
},
/**
p.splice(idx, 1);
}
});
+
+ this._cacheId++;
},
/**
*/
clear: function() {
this._plugins = [];
+ this._cacheId++;
},
/**
},
/**
- * Calls registered plugins on the specified extension, with the given args. This
- * method immediately returns as soon as a plugin explicitly returns false. The
+ * Calls enabled plugins for chart, on the specified extension 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.
* @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;
+ notify: function(chart, extension, args) {
+ var descriptors = this.descriptors(chart);
+ var ilen = descriptors.length;
+ var i, descriptor, plugin, params, method;
for (i=0; i<ilen; ++i) {
- plugin = plugins[i];
- if (typeof plugin[extension] === 'function') {
- if (plugin[extension].apply(plugin, args || []) === false) {
+ descriptor = descriptors[i];
+ plugin = descriptor.plugin;
+ method = plugin[extension];
+ if (typeof method === 'function') {
+ params = [chart].concat(args || []);
+ params.push(descriptor.options);
+ if (method.apply(plugin, params) === false) {
return false;
}
}
}
return true;
+ },
+
+ /**
+ * Returns descriptors of enabled plugins for the given chart.
+ * @returns {Array} [{ plugin, options }]
+ * @private
+ */
+ descriptors: function(chart) {
+ var cache = chart._plugins || (chart._plugins = {});
+ if (cache.id === this._cacheId) {
+ return cache.descriptors;
+ }
+
+ var plugins = [];
+ var descriptors = [];
+ var config = (chart && chart.config) || {};
+ var defaults = Chart.defaults.global.plugins;
+ var options = (config.options && config.options.plugins) || {};
+
+ this._plugins.concat(config.plugins || []).forEach(function(plugin) {
+ var idx = plugins.indexOf(plugin);
+ if (idx !== -1) {
+ return;
+ }
+
+ var id = plugin.id;
+ var opts = options[id];
+ if (opts === false) {
+ return;
+ }
+
+ if (opts === true) {
+ opts = helpers.clone(defaults[id]);
+ }
+
+ plugins.push(plugin);
+ descriptors.push({
+ plugin: plugin,
+ options: opts || {}
+ });
+ });
+
+ cache.descriptors = descriptors;
+ cache.id = this._cacheId;
+ return descriptors;
}
};
* @interface Chart.PluginBase
* @since 2.1.0
*/
- Chart.PluginBase = Chart.Element.extend({
+ Chart.PluginBase = helpers.inherits({
// Called at start of chart init
beforeInit: noop,
* 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 me at version 3
*/
Chart.pluginService = Chart.plugins;
};
describe('Chart.plugins', function() {
- var oldPlugins;
-
- beforeAll(function() {
- oldPlugins = Chart.plugins.getAll();
- });
-
- afterAll(function() {
- Chart.plugins.register(oldPlugins);
+ beforeEach(function() {
+ this._plugins = Chart.plugins.getAll();
+ Chart.plugins.clear();
});
- beforeEach(function() {
+ afterEach(function() {
Chart.plugins.clear();
+ Chart.plugins.register(this._plugins);
+ delete this._plugins;
});
describe('Chart.plugins.register', function() {
});
describe('Chart.plugins.notify', function() {
- it('should call plugins with arguments', function() {
- var myplugin = {
- count: 0,
- trigger: function(chart) {
- myplugin.count = chart.count;
+ it('should call inline plugins with arguments', function() {
+ var plugin = {hook: function() {}};
+ var chart = window.acquireChart({
+ plugins: [plugin]
+ });
+
+ spyOn(plugin, 'hook');
+
+ Chart.plugins.notify(chart, 'hook', 42);
+ expect(plugin.hook.calls.count()).toBe(1);
+ expect(plugin.hook.calls.first().args[0]).toBe(chart);
+ expect(plugin.hook.calls.first().args[1]).toBe(42);
+ expect(plugin.hook.calls.first().args[2]).toEqual({});
+ });
+
+ it('should call global plugins with arguments', function() {
+ var plugin = {hook: function() {}};
+ var chart = window.acquireChart({});
+
+ spyOn(plugin, 'hook');
+
+ Chart.plugins.register(plugin);
+ Chart.plugins.notify(chart, 'hook', 42);
+ expect(plugin.hook.calls.count()).toBe(1);
+ expect(plugin.hook.calls.first().args[0]).toBe(chart);
+ expect(plugin.hook.calls.first().args[1]).toBe(42);
+ expect(plugin.hook.calls.first().args[2]).toEqual({});
+ });
+
+ it('should call plugin only once even if registered multiple times', function() {
+ var plugin = {hook: function() {}};
+ var chart = window.acquireChart({
+ plugins: [plugin, plugin]
+ });
+
+ spyOn(plugin, 'hook');
+
+ Chart.plugins.register([plugin, plugin]);
+ Chart.plugins.notify(chart, 'hook');
+ expect(plugin.hook.calls.count()).toBe(1);
+ });
+
+ it('should call plugins in the correct order (global first)', function() {
+ var results = [];
+ var chart = window.acquireChart({
+ plugins: [{
+ hook: function() {
+ results.push(1);
+ }
+ }, {
+ hook: function() {
+ results.push(2);
+ }
+ }, {
+ hook: function() {
+ results.push(3);
+ }
+ }]
+ });
+
+ Chart.plugins.register([{
+ hook: function() {
+ results.push(4);
}
- };
+ }, {
+ hook: function() {
+ results.push(5);
+ }
+ }, {
+ hook: function() {
+ results.push(6);
+ }
+ }]);
- Chart.plugins.register(myplugin);
- Chart.plugins.notify('trigger', [{count: 10}]);
- expect(myplugin.count).toBe(10);
+ var ret = Chart.plugins.notify(chart, 'hook');
+ expect(ret).toBeTruthy();
+ expect(results).toEqual([4, 5, 6, 1, 2, 3]);
});
it('should return TRUE if no plugin explicitly returns FALSE', function() {
- Chart.plugins.register({
- check: function() {}
+ var chart = window.acquireChart({
+ plugins: [{
+ hook: function() {}
+ }, {
+ hook: function() {
+ return null;
+ }
+ }, {
+ hook: function() {
+ return 0;
+ }
+ }, {
+ hook: function() {
+ return true;
+ }
+ }, {
+ hook: function() {
+ return 1;
+ }
+ }]
});
- Chart.plugins.register({
- check: function() {
- return;
- }
+
+ var plugins = chart.config.plugins;
+ plugins.forEach(function(plugin) {
+ spyOn(plugin, 'hook').and.callThrough();
});
- Chart.plugins.register({
- check: function() {
- return null;
- }
+
+ var ret = Chart.plugins.notify(chart, 'hook');
+ expect(ret).toBeTruthy();
+ plugins.forEach(function(plugin) {
+ expect(plugin.hook).toHaveBeenCalled();
});
- Chart.plugins.register({
- check: function() {
- return 42;
- }
+ });
+
+ it('should return FALSE if any plugin explicitly returns FALSE', function() {
+ var chart = window.acquireChart({
+ plugins: [{
+ hook: function() {}
+ }, {
+ hook: function() {
+ return null;
+ }
+ }, {
+ hook: function() {
+ return false;
+ }
+ }, {
+ hook: function() {
+ return 42;
+ }
+ }, {
+ hook: function() {
+ return 'bar';
+ }
+ }]
});
- var res = Chart.plugins.notify('check');
- expect(res).toBeTruthy();
+
+ var plugins = chart.config.plugins;
+ plugins.forEach(function(plugin) {
+ spyOn(plugin, 'hook').and.callThrough();
+ });
+
+ var ret = Chart.plugins.notify(chart, 'hook');
+ expect(ret).toBeFalsy();
+ expect(plugins[0].hook).toHaveBeenCalled();
+ expect(plugins[1].hook).toHaveBeenCalled();
+ expect(plugins[2].hook).toHaveBeenCalled();
+ expect(plugins[3].hook).not.toHaveBeenCalled();
+ expect(plugins[4].hook).not.toHaveBeenCalled();
});
+ });
- it('should return FALSE if no plugin explicitly returns FALSE', function() {
- Chart.plugins.register({
- check: function() {}
+ describe('config.options.plugins', function() {
+ it('should call plugins with options at last argument', function() {
+ var plugin = {id: 'foo', hook: function() {}};
+ var chart = window.acquireChart({
+ options: {
+ plugins: {
+ foo: {a: '123'},
+ }
+ }
});
- Chart.plugins.register({
- check: function() {
- return;
+
+ spyOn(plugin, 'hook');
+
+ Chart.plugins.register(plugin);
+ Chart.plugins.notify(chart, 'hook');
+ Chart.plugins.notify(chart, 'hook', ['bla']);
+ Chart.plugins.notify(chart, 'hook', ['bla', 42]);
+
+ expect(plugin.hook.calls.count()).toBe(3);
+ expect(plugin.hook.calls.argsFor(0)[1]).toEqual({a: '123'});
+ expect(plugin.hook.calls.argsFor(1)[2]).toEqual({a: '123'});
+ expect(plugin.hook.calls.argsFor(2)[3]).toEqual({a: '123'});
+ });
+
+ it('should call plugins with options associated to their identifier', function() {
+ var plugins = {
+ a: {id: 'a', hook: function() {}},
+ b: {id: 'b', hook: function() {}},
+ c: {id: 'c', hook: function() {}}
+ };
+
+ Chart.plugins.register(plugins.a);
+
+ var chart = window.acquireChart({
+ plugins: [plugins.b, plugins.c],
+ options: {
+ plugins: {
+ a: {a: '123'},
+ b: {b: '456'},
+ c: {c: '789'}
+ }
}
});
- Chart.plugins.register({
- check: function() {
- return false;
+
+ spyOn(plugins.a, 'hook');
+ spyOn(plugins.b, 'hook');
+ spyOn(plugins.c, 'hook');
+
+ Chart.plugins.notify(chart, 'hook');
+
+ expect(plugins.a.hook).toHaveBeenCalled();
+ expect(plugins.b.hook).toHaveBeenCalled();
+ expect(plugins.c.hook).toHaveBeenCalled();
+ expect(plugins.a.hook.calls.first().args[1]).toEqual({a: '123'});
+ expect(plugins.b.hook.calls.first().args[1]).toEqual({b: '456'});
+ expect(plugins.c.hook.calls.first().args[1]).toEqual({c: '789'});
+ });
+
+ it('should not called plugins when config.options.plugins.{id} is FALSE', function() {
+ var plugins = {
+ a: {id: 'a', hook: function() {}},
+ b: {id: 'b', hook: function() {}},
+ c: {id: 'c', hook: function() {}}
+ };
+
+ Chart.plugins.register(plugins.a);
+
+ var chart = window.acquireChart({
+ plugins: [plugins.b, plugins.c],
+ options: {
+ plugins: {
+ a: false,
+ b: false
+ }
}
});
- Chart.plugins.register({
- check: function() {
- return 42;
+
+ spyOn(plugins.a, 'hook');
+ spyOn(plugins.b, 'hook');
+ spyOn(plugins.c, 'hook');
+
+ Chart.plugins.notify(chart, 'hook');
+
+ expect(plugins.a.hook).not.toHaveBeenCalled();
+ expect(plugins.b.hook).not.toHaveBeenCalled();
+ expect(plugins.c.hook).toHaveBeenCalled();
+ });
+
+ it('should call plugins with default options when plugin options is TRUE', function() {
+ var plugin = {id: 'a', hook: function() {}};
+
+ Chart.defaults.global.plugins.a = {a: 42};
+ Chart.plugins.register(plugin);
+
+ var chart = window.acquireChart({
+ options: {
+ plugins: {
+ a: true
+ }
}
});
- var res = Chart.plugins.notify('check');
- expect(res).toBeFalsy();
+
+ spyOn(plugin, 'hook');
+
+ Chart.plugins.notify(chart, 'hook');
+
+ expect(plugin.hook).toHaveBeenCalled();
+ expect(plugin.hook.calls.first().args[1]).toEqual({a: 42});
+ });
+
+
+ it('should call plugins with default options if plugin config options is undefined', function() {
+ var plugin = {id: 'a', hook: function() {}};
+
+ Chart.defaults.global.plugins.a = {a: 'foobar'};
+ Chart.plugins.register(plugin);
+ spyOn(plugin, 'hook');
+
+ var chart = window.acquireChart();
+
+ Chart.plugins.notify(chart, 'hook');
+
+ expect(plugin.hook).toHaveBeenCalled();
+ expect(plugin.hook.calls.first().args[1]).toEqual({a: 'foobar'});
});
});
});