mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-10 10:16:50 +01:00
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.
This commit is contained in:
committed by
Evert Timberg
parent
9deebf8371
commit
1484520692
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<ilen; ++i) {
|
||||
metaData[i] = metaData[i] || me.createMetaData(meta, i);
|
||||
metaData[i] = metaData[i] || me.createMetaData(i);
|
||||
}
|
||||
|
||||
meta.dataset = meta.dataset || me.createMetaDataset();
|
||||
},
|
||||
|
||||
addElementAndReset: function(index) {
|
||||
var me = this;
|
||||
var element = me.createMetaData(index);
|
||||
me.getMeta().data.splice(index, 0, element);
|
||||
me.updateElement(element, index, true);
|
||||
var element = this.createMetaData(index);
|
||||
this.getMeta().data.splice(index, 0, element);
|
||||
this.updateElement(element, index, true);
|
||||
},
|
||||
|
||||
buildOrUpdateElements: function() {
|
||||
// Handle the number of data points changing
|
||||
var meta = this.getMeta(),
|
||||
md = meta.data,
|
||||
numData = this.getDataset().data.length,
|
||||
numMetaData = md.length;
|
||||
var me = this;
|
||||
var dataset = me.getDataset();
|
||||
var data = dataset.data || (dataset.data = []);
|
||||
|
||||
// Make sure that we handle number of datapoints changing
|
||||
if (numData < numMetaData) {
|
||||
// Remove excess bars for data points that have been removed
|
||||
md.splice(numData, numMetaData - numData);
|
||||
} else if (numData > 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<count; ++i) {
|
||||
this.addElementAndReset(start + i);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onDataPush: function() {
|
||||
this.insertElements(this.getDataset().data.length-1, arguments.length);
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onDataPop: function() {
|
||||
this.getMeta().data.pop();
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onDataShift: function() {
|
||||
this.getMeta().data.shift();
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onDataSplice: function(start, count) {
|
||||
this.getMeta().data.splice(start, count);
|
||||
this.insertElements(start, arguments.length - 2);
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onDataUnshift: function() {
|
||||
this.insertElements(0, arguments.length);
|
||||
}
|
||||
});
|
||||
|
||||
Chart.DatasetController.extend = helpers.inherits;
|
||||
|
||||
188
test/core.datasetController.tests.js
Normal file
188
test/core.datasetController.tests.js
Normal file
@@ -0,0 +1,188 @@
|
||||
describe('Chart.DatasetController', function() {
|
||||
it('should listen for dataset data insertions or removals', function() {
|
||||
var data = [0, 1, 2, 3, 4, 5];
|
||||
var chart = acquireChart({
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: data
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
var controller = chart.getDatasetMeta(0).controller;
|
||||
var methods = [
|
||||
'onDataPush',
|
||||
'onDataPop',
|
||||
'onDataShift',
|
||||
'onDataSplice',
|
||||
'onDataUnshift'
|
||||
];
|
||||
|
||||
methods.forEach(function(method) {
|
||||
spyOn(controller, method);
|
||||
});
|
||||
|
||||
data.push(6, 7, 8);
|
||||
data.push(9);
|
||||
data.pop();
|
||||
data.shift();
|
||||
data.shift();
|
||||
data.shift();
|
||||
data.splice(1, 4, 10, 11);
|
||||
data.unshift(12, 13, 14, 15);
|
||||
data.unshift(16, 17);
|
||||
|
||||
[2, 1, 3, 1, 2].forEach(function(expected, index) {
|
||||
expect(controller[methods[index]].calls.count()).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should synchronize metadata when data are inserted or removed', function() {
|
||||
var data = [0, 1, 2, 3, 4, 5];
|
||||
var chart = acquireChart({
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: data
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
var meta = chart.getDatasetMeta(0);
|
||||
var first, second, last;
|
||||
|
||||
first = meta.data[0];
|
||||
last = meta.data[5];
|
||||
data.push(6, 7, 8);
|
||||
data.push(9);
|
||||
expect(meta.data.length).toBe(10);
|
||||
expect(meta.data[0]).toBe(first);
|
||||
expect(meta.data[5]).toBe(last);
|
||||
|
||||
last = meta.data[9];
|
||||
data.pop();
|
||||
expect(meta.data.length).toBe(9);
|
||||
expect(meta.data[0]).toBe(first);
|
||||
expect(meta.data.indexOf(last)).toBe(-1);
|
||||
|
||||
last = meta.data[8];
|
||||
data.shift();
|
||||
data.shift();
|
||||
data.shift();
|
||||
expect(meta.data.length).toBe(6);
|
||||
expect(meta.data.indexOf(first)).toBe(-1);
|
||||
expect(meta.data[5]).toBe(last);
|
||||
|
||||
first = meta.data[0];
|
||||
second = meta.data[1];
|
||||
last = meta.data[5];
|
||||
data.splice(1, 4, 10, 11);
|
||||
expect(meta.data.length).toBe(4);
|
||||
expect(meta.data[0]).toBe(first);
|
||||
expect(meta.data[3]).toBe(last);
|
||||
expect(meta.data.indexOf(second)).toBe(-1);
|
||||
|
||||
data.unshift(12, 13, 14, 15);
|
||||
data.unshift(16, 17);
|
||||
expect(meta.data.length).toBe(10);
|
||||
expect(meta.data[6]).toBe(first);
|
||||
expect(meta.data[9]).toBe(last);
|
||||
});
|
||||
|
||||
it('should re-synchronize metadata when the data object reference changes', function() {
|
||||
var data0 = [0, 1, 2, 3, 4, 5];
|
||||
var data1 = [6, 7, 8];
|
||||
var chart = acquireChart({
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: data0
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
var meta = chart.getDatasetMeta(0);
|
||||
|
||||
expect(meta.data.length).toBe(6);
|
||||
|
||||
chart.data.datasets[0].data = data1;
|
||||
chart.update();
|
||||
|
||||
expect(meta.data.length).toBe(3);
|
||||
|
||||
data1.push(9, 10, 11);
|
||||
expect(meta.data.length).toBe(6);
|
||||
});
|
||||
|
||||
it('should re-synchronize metadata when data are unusually altered', function() {
|
||||
var data = [0, 1, 2, 3, 4, 5];
|
||||
var chart = acquireChart({
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: data
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
var meta = chart.getDatasetMeta(0);
|
||||
|
||||
expect(meta.data.length).toBe(6);
|
||||
|
||||
data.length = 2;
|
||||
chart.update();
|
||||
|
||||
expect(meta.data.length).toBe(2);
|
||||
|
||||
data.length = 42;
|
||||
chart.update();
|
||||
|
||||
expect(meta.data.length).toBe(42);
|
||||
});
|
||||
|
||||
it('should cleanup attached properties when the reference changes or when the chart is destroyed', function() {
|
||||
var data0 = [0, 1, 2, 3, 4, 5];
|
||||
var data1 = [6, 7, 8];
|
||||
var chart = acquireChart({
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: data0
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
var hooks = ['push', 'pop', 'shift', 'splice', 'unshift'];
|
||||
|
||||
expect(data0._chartjs).toBeDefined();
|
||||
hooks.forEach(function(hook) {
|
||||
expect(data0[hook]).not.toBe(Array.prototype[hook]);
|
||||
});
|
||||
|
||||
expect(data1._chartjs).not.toBeDefined();
|
||||
hooks.forEach(function(hook) {
|
||||
expect(data1[hook]).toBe(Array.prototype[hook]);
|
||||
});
|
||||
|
||||
chart.data.datasets[0].data = data1;
|
||||
chart.update();
|
||||
|
||||
expect(data0._chartjs).not.toBeDefined();
|
||||
hooks.forEach(function(hook) {
|
||||
expect(data0[hook]).toBe(Array.prototype[hook]);
|
||||
});
|
||||
|
||||
expect(data1._chartjs).toBeDefined();
|
||||
hooks.forEach(function(hook) {
|
||||
expect(data1[hook]).not.toBe(Array.prototype[hook]);
|
||||
});
|
||||
|
||||
chart.destroy();
|
||||
|
||||
expect(data1._chartjs).not.toBeDefined();
|
||||
hooks.forEach(function(hook) {
|
||||
expect(data1[hook]).toBe(Array.prototype[hook]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user