mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-09 17:56:51 +01:00
576 lines
13 KiB
JavaScript
576 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
var helpers = require('../helpers/index');
|
|
|
|
var resolve = helpers.options.resolve;
|
|
|
|
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)
|
|
var DatasetController = function(chart, datasetIndex) {
|
|
this.initialize(chart, datasetIndex);
|
|
};
|
|
|
|
helpers.extend(DatasetController.prototype, {
|
|
|
|
/**
|
|
* Element type used to generate a meta dataset (e.g. Chart.element.Line).
|
|
* @type {Chart.core.element}
|
|
*/
|
|
datasetElementType: null,
|
|
|
|
/**
|
|
* Element type used to generate a meta data (e.g. Chart.element.Point).
|
|
* @type {Chart.core.element}
|
|
*/
|
|
dataElementType: null,
|
|
|
|
/**
|
|
* Dataset element option keys to be resolved in _resolveDatasetElementOptions.
|
|
* A derived controller may override this to resolve controller-specific options.
|
|
* The keys defined here are for backward compatibility for legend styles.
|
|
* @private
|
|
*/
|
|
_datasetElementOptions: [
|
|
'backgroundColor',
|
|
'borderCapStyle',
|
|
'borderColor',
|
|
'borderDash',
|
|
'borderDashOffset',
|
|
'borderJoinStyle',
|
|
'borderWidth'
|
|
],
|
|
|
|
/**
|
|
* Data element option keys to be resolved in _resolveDataElementOptions.
|
|
* A derived controller may override this to resolve controller-specific options.
|
|
* The keys defined here are for backward compatibility for legend styles.
|
|
* @private
|
|
*/
|
|
_dataElementOptions: [
|
|
'backgroundColor',
|
|
'borderColor',
|
|
'borderWidth',
|
|
'pointStyle'
|
|
],
|
|
|
|
initialize: function(chart, datasetIndex) {
|
|
var me = this;
|
|
me.chart = chart;
|
|
me.index = datasetIndex;
|
|
me.linkScales();
|
|
me.addElements();
|
|
me._type = me.getMeta().type;
|
|
},
|
|
|
|
updateIndex: function(datasetIndex) {
|
|
this.index = datasetIndex;
|
|
},
|
|
|
|
linkScales: function() {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var chart = me.chart;
|
|
var scales = chart.scales;
|
|
var dataset = me.getDataset();
|
|
var scalesOpts = chart.options.scales;
|
|
|
|
if (meta.xAxisID === null || !(meta.xAxisID in scales) || dataset.xAxisID) {
|
|
meta.xAxisID = dataset.xAxisID || scalesOpts.xAxes[0].id;
|
|
}
|
|
if (meta.yAxisID === null || !(meta.yAxisID in scales) || dataset.yAxisID) {
|
|
meta.yAxisID = dataset.yAxisID || scalesOpts.yAxes[0].id;
|
|
}
|
|
},
|
|
|
|
getDataset: function() {
|
|
return this.chart.data.datasets[this.index];
|
|
},
|
|
|
|
getMeta: function() {
|
|
return this.chart.getDatasetMeta(this.index);
|
|
},
|
|
|
|
getScaleForId: function(scaleID) {
|
|
return this.chart.scales[scaleID];
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getValueScaleId: function() {
|
|
return this.getMeta().yAxisID;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getIndexScaleId: function() {
|
|
return this.getMeta().xAxisID;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getValueScale: function() {
|
|
return this.getScaleForId(this._getValueScaleId());
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getIndexScale: function() {
|
|
return this.getScaleForId(this._getIndexScaleId());
|
|
},
|
|
|
|
reset: function() {
|
|
this._update(true);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
destroy: function() {
|
|
if (this._data) {
|
|
unlistenArrayEvents(this._data, this);
|
|
}
|
|
},
|
|
|
|
createMetaDataset: function() {
|
|
var me = this;
|
|
var type = me.datasetElementType;
|
|
return type && new type({
|
|
_chart: me.chart,
|
|
_datasetIndex: me.index
|
|
});
|
|
},
|
|
|
|
createMetaData: function(index) {
|
|
var me = this;
|
|
var type = me.dataElementType;
|
|
return type && new type({
|
|
_chart: me.chart,
|
|
_datasetIndex: me.index,
|
|
_index: index
|
|
});
|
|
},
|
|
|
|
addElements: function() {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var data = me.getDataset().data || [];
|
|
var metaData = meta.data;
|
|
var i, ilen;
|
|
|
|
for (i = 0, ilen = data.length; i < ilen; ++i) {
|
|
metaData[i] = metaData[i] || me.createMetaData(i);
|
|
}
|
|
|
|
meta.dataset = meta.dataset || me.createMetaDataset();
|
|
},
|
|
|
|
addElementAndReset: function(index) {
|
|
var element = this.createMetaData(index);
|
|
this.getMeta().data.splice(index, 0, element);
|
|
this.updateElement(element, index, true);
|
|
},
|
|
|
|
buildOrUpdateElements: function() {
|
|
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);
|
|
}
|
|
|
|
if (data && Object.isExtensible(data)) {
|
|
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();
|
|
},
|
|
|
|
/**
|
|
* Returns the merged user-supplied and default dataset-level options
|
|
* @private
|
|
*/
|
|
_configure: function() {
|
|
var me = this;
|
|
me._config = helpers.merge({}, [
|
|
me.chart.options.datasets[me._type],
|
|
me.getDataset(),
|
|
], {
|
|
merger: function(key, target, source) {
|
|
if (key !== '_meta' && key !== 'data') {
|
|
helpers._merger(key, target, source);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
_update: function(reset) {
|
|
var me = this;
|
|
me._configure();
|
|
me._cachedDataOpts = null;
|
|
me.update(reset);
|
|
},
|
|
|
|
update: helpers.noop,
|
|
|
|
transition: function(easingValue) {
|
|
var meta = this.getMeta();
|
|
var elements = meta.data || [];
|
|
var ilen = elements.length;
|
|
var i = 0;
|
|
|
|
for (; i < ilen; ++i) {
|
|
elements[i].transition(easingValue);
|
|
}
|
|
|
|
if (meta.dataset) {
|
|
meta.dataset.transition(easingValue);
|
|
}
|
|
},
|
|
|
|
draw: function() {
|
|
var meta = this.getMeta();
|
|
var elements = meta.data || [];
|
|
var ilen = elements.length;
|
|
var i = 0;
|
|
|
|
if (meta.dataset) {
|
|
meta.dataset.draw();
|
|
}
|
|
|
|
for (; i < ilen; ++i) {
|
|
elements[i].draw();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns a set of predefined style properties that should be used to represent the dataset
|
|
* or the data if the index is specified
|
|
* @param {number} index - data index
|
|
* @return {IStyleInterface} style object
|
|
*/
|
|
getStyle: function(index) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var dataset = meta.dataset;
|
|
var style;
|
|
|
|
me._configure();
|
|
if (dataset && index === undefined) {
|
|
style = me._resolveDatasetElementOptions(dataset || {});
|
|
} else {
|
|
index = index || 0;
|
|
style = me._resolveDataElementOptions(meta.data[index] || {}, index);
|
|
}
|
|
|
|
if (style.fill === false || style.fill === null) {
|
|
style.backgroundColor = 'rgba(0,0,0,0)';
|
|
}
|
|
|
|
return style;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_resolveDatasetElementOptions: function(element, hover) {
|
|
var me = this;
|
|
var chart = me.chart;
|
|
var datasetOpts = me._config;
|
|
var custom = element.custom || {};
|
|
var options = chart.options.elements[me.datasetElementType.prototype._type] || {};
|
|
var elementOptions = me._datasetElementOptions;
|
|
var values = {};
|
|
var i, ilen, key, readKey;
|
|
|
|
// Scriptable options
|
|
var context = {
|
|
chart: chart,
|
|
dataset: me.getDataset(),
|
|
datasetIndex: me.index,
|
|
hover: hover
|
|
};
|
|
|
|
for (i = 0, ilen = elementOptions.length; i < ilen; ++i) {
|
|
key = elementOptions[i];
|
|
readKey = hover ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key;
|
|
values[key] = resolve([
|
|
custom[readKey],
|
|
datasetOpts[readKey],
|
|
options[readKey]
|
|
], context);
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_resolveDataElementOptions: function(element, index) {
|
|
var me = this;
|
|
var custom = element && element.custom;
|
|
var cached = me._cachedDataOpts;
|
|
if (cached && !custom) {
|
|
return cached;
|
|
}
|
|
var chart = me.chart;
|
|
var datasetOpts = me._config;
|
|
var options = chart.options.elements[me.dataElementType.prototype._type] || {};
|
|
var elementOptions = me._dataElementOptions;
|
|
var values = {};
|
|
|
|
// Scriptable options
|
|
var context = {
|
|
chart: chart,
|
|
dataIndex: index,
|
|
dataset: me.getDataset(),
|
|
datasetIndex: me.index
|
|
};
|
|
|
|
// `resolve` sets cacheable to `false` if any option is indexed or scripted
|
|
var info = {cacheable: !custom};
|
|
|
|
var keys, i, ilen, key;
|
|
|
|
custom = custom || {};
|
|
|
|
if (helpers.isArray(elementOptions)) {
|
|
for (i = 0, ilen = elementOptions.length; i < ilen; ++i) {
|
|
key = elementOptions[i];
|
|
values[key] = resolve([
|
|
custom[key],
|
|
datasetOpts[key],
|
|
options[key]
|
|
], context, index, info);
|
|
}
|
|
} else {
|
|
keys = Object.keys(elementOptions);
|
|
for (i = 0, ilen = keys.length; i < ilen; ++i) {
|
|
key = keys[i];
|
|
values[key] = resolve([
|
|
custom[key],
|
|
datasetOpts[elementOptions[key]],
|
|
datasetOpts[key],
|
|
options[key]
|
|
], context, index, info);
|
|
}
|
|
}
|
|
|
|
if (info.cacheable) {
|
|
me._cachedDataOpts = Object.freeze(values);
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
removeHoverStyle: function(element) {
|
|
helpers.merge(element._model, element.$previousStyle || {});
|
|
delete element.$previousStyle;
|
|
},
|
|
|
|
setHoverStyle: function(element) {
|
|
var dataset = this.chart.data.datasets[element._datasetIndex];
|
|
var index = element._index;
|
|
var custom = element.custom || {};
|
|
var model = element._model;
|
|
var getHoverColor = helpers.getHoverColor;
|
|
|
|
element.$previousStyle = {
|
|
backgroundColor: model.backgroundColor,
|
|
borderColor: model.borderColor,
|
|
borderWidth: model.borderWidth
|
|
};
|
|
|
|
model.backgroundColor = resolve([custom.hoverBackgroundColor, dataset.hoverBackgroundColor, getHoverColor(model.backgroundColor)], undefined, index);
|
|
model.borderColor = resolve([custom.hoverBorderColor, dataset.hoverBorderColor, getHoverColor(model.borderColor)], undefined, index);
|
|
model.borderWidth = resolve([custom.hoverBorderWidth, dataset.hoverBorderWidth, model.borderWidth], undefined, index);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_removeDatasetHoverStyle: function() {
|
|
var element = this.getMeta().dataset;
|
|
|
|
if (element) {
|
|
this.removeHoverStyle(element);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_setDatasetHoverStyle: function() {
|
|
var element = this.getMeta().dataset;
|
|
var prev = {};
|
|
var i, ilen, key, keys, hoverOptions, model;
|
|
|
|
if (!element) {
|
|
return;
|
|
}
|
|
|
|
model = element._model;
|
|
hoverOptions = this._resolveDatasetElementOptions(element, true);
|
|
|
|
keys = Object.keys(hoverOptions);
|
|
for (i = 0, ilen = keys.length; i < ilen; ++i) {
|
|
key = keys[i];
|
|
prev[key] = model[key];
|
|
model[key] = hoverOptions[key];
|
|
}
|
|
|
|
element.$previousStyle = prev;
|
|
},
|
|
|
|
/**
|
|
* @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() {
|
|
var count = arguments.length;
|
|
this.insertElements(this.getDataset().data.length - count, count);
|
|
},
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
});
|
|
|
|
DatasetController.extend = helpers.inherits;
|
|
|
|
module.exports = DatasetController;
|