From: Simon Brunel Date: Sat, 1 Oct 2016 13:38:19 +0000 (+0200) Subject: Better animation when adding or removing data X-Git-Tag: v2.4.0~1^2~38 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=1484520692293902c9d64d39dc3a014abc98be0f;p=thirdparty%2FChart.js.git Better animation when adding or removing data In order to simulate real-time chart updates (i.e. horizontal animation), it's necessary to distinguish a removed or added value from a simple update. The dataset controller now hooks array methods that alter the data array length to synchronize metadata accordingly. Also remove the duplicate calls of updateBezierControlPoints() for line and radar charts. --- 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); + var me = this; + var dataset = me.getDataset(); + var data = dataset.data || (dataset.data = []); + + // 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