'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 = style.borderColor; } 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;