diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index bfc061af2..7c36396bb 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -34,19 +34,6 @@ module.exports = function(Chart) { dataElementType: Chart.elements.Point, - addElementAndReset: function(index) { - var me = this; - var options = me.chart.options; - var meta = me.getMeta(); - - Chart.DatasetController.prototype.addElementAndReset.call(me, index); - - // Make sure bezier control points are updated - if (lineEnabled(me.getDataset(), options) && meta.dataset._model.tension !== 0) { - me.updateBezierControlPoints(); - } - }, - update: function(reset) { var me = this; var meta = me.getMeta(); diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 6232165f3..cd62da476 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -24,13 +24,6 @@ module.exports = function(Chart) { linkScales: helpers.noop, - addElementAndReset: function(index) { - Chart.DatasetController.prototype.addElementAndReset.call(this, index); - - // Make sure bezier control points are updated - this.updateBezierControlPoints(); - }, - update: function(reset) { var me = this; var meta = me.getMeta(); @@ -79,7 +72,6 @@ module.exports = function(Chart) { me.updateElement(point, index, reset); }, me); - // Update bezier control points me.updateBezierControlPoints(); }, diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 52f24d2ce..b2928c6b8 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -684,10 +684,20 @@ module.exports = function(Chart) { destroy: function() { var me = this; var canvas = me.chart.canvas; + var meta, i, ilen; me.stop(); me.clear(); + // dataset controllers need to cleanup associated data + for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + meta = me.getDatasetMeta(i); + if (meta.controller) { + meta.controller.destroy(); + meta.controller = null; + } + } + if (canvas) { helpers.unbindEvents(me, me.events); helpers.removeResizeListener(canvas.parentNode); diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 29b1aacea..2faff1b6e 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -3,7 +3,77 @@ module.exports = function(Chart) { var helpers = Chart.helpers; - var noop = helpers.noop; + + var arrayEvents = ['push', 'pop', 'shift', 'splice', 'unshift']; + + /** + * Hooks the array methods that add or remove values ('push', pop', 'shift', 'splice', + * 'unshift') and notify the listener AFTER the array has been altered. Listeners are + * called on the 'onData*' callbacks (e.g. onDataPush, etc.) with same arguments. + */ + function listenArrayEvents(array, listener) { + if (array._chartjs) { + array._chartjs.listeners.push(listener); + return; + } + + Object.defineProperty(array, '_chartjs', { + configurable: true, + enumerable: false, + value: { + listeners: [listener] + } + }); + + arrayEvents.forEach(function(key) { + var method = 'onData' + key.charAt(0).toUpperCase() + key.slice(1); + var base = array[key]; + + Object.defineProperty(array, key, { + configurable: true, + enumerable: false, + value: function() { + var args = Array.prototype.slice.call(arguments); + var res = base.apply(this, args); + + helpers.each(array._chartjs.listeners, function(object) { + if (typeof object[method] === 'function') { + object[method].apply(object, args); + } + }); + + return res; + } + }); + }); + } + + /** + * Removes the given array event listener and cleanup extra attached properties (such as + * the _chartjs stub and overridden methods) if array doesn't have any more listeners. + */ + function unlistenArrayEvents(array, listener) { + var stub = array._chartjs; + if (!stub) { + return; + } + + var listeners = stub.listeners; + var index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + + if (listeners.length > 0) { + return; + } + + arrayEvents.forEach(function(key) { + delete array[key]; + }); + + delete array._chartjs; + } // Base class for all dataset controllers (line, bar, etc) Chart.DatasetController = function(chart, datasetIndex) { @@ -65,6 +135,15 @@ module.exports = function(Chart) { this.update(true); }, + /** + * @private + */ + destroy: function() { + if (this._data) { + unlistenArrayEvents(this._data, this); + } + }, + createMetaDataset: function() { var me = this; var type = me.datasetElementType; @@ -92,39 +171,42 @@ module.exports = function(Chart) { var i, ilen; for (i=0, ilen=data.length; i numMetaData) { - // Add new elements - for (var index = numMetaData; index < numData; ++index) { - this.addElementAndReset(index); + // In order to correctly handle data addition/deletion animation (an thus simulate + // real-time charts), we need to monitor these data modifications and synchronize + // the internal meta data accordingly. + if (me._data !== data) { + if (me._data) { + // This case happens when the user replaced the data array instance. + unlistenArrayEvents(me._data, me); } + + listenArrayEvents(data, me); + me._data = data; } + + // Re-sync meta data in case the user replaced the data array or if we missed + // any updates and so make sure that we handle number of datapoints changing. + me.resyncElements(); }, - update: noop, + update: helpers.noop, draw: function(ease) { var easingDecimal = ease || 1; @@ -156,8 +238,69 @@ module.exports = function(Chart) { model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : valueOrDefault(dataset.hoverBackgroundColor, index, getHoverColor(model.backgroundColor)); model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : valueOrDefault(dataset.hoverBorderColor, index, getHoverColor(model.borderColor)); model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : valueOrDefault(dataset.hoverBorderWidth, index, model.borderWidth); - } + }, + /** + * @private + */ + resyncElements: function() { + var me = this; + var meta = me.getMeta(); + var data = me.getDataset().data; + var numMeta = meta.data.length; + var numData = data.length; + + if (numData < numMeta) { + meta.data.splice(numData, numMeta - numData); + } else if (numData > numMeta) { + me.insertElements(numMeta, numData - numMeta); + } + }, + + /** + * @private + */ + insertElements: function(start, count) { + for (var i=0; i