import helpers from '../helpers/index'; import Animations from './core.animations'; /** * @typedef { import("./core.controller").default } Chart * @typedef { import("./core.scale").default } Scale */ const resolve = helpers.options.resolve; const 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((key) => { const method = '_onData' + key.charAt(0).toUpperCase() + key.slice(1); const base = array[key]; Object.defineProperty(array, key, { configurable: true, enumerable: false, value(...args) { const res = base.apply(this, args); array._chartjs.listeners.forEach((object) => { if (typeof object[method] === 'function') { object[method](...args); } }); return res; } }); }); } function scaleClip(scale, allowedOverflow) { const opts = scale && scale.options || {}; const reverse = opts.reverse; const min = opts.min === undefined ? allowedOverflow : 0; const max = opts.max === undefined ? allowedOverflow : 0; return { start: reverse ? max : min, end: reverse ? min : max }; } function defaultClip(xScale, yScale, allowedOverflow) { if (allowedOverflow === false) { return false; } const x = scaleClip(xScale, allowedOverflow); const y = scaleClip(yScale, allowedOverflow); return { top: y.end, right: x.end, bottom: y.start, left: x.start }; } function toClip(value) { let t, r, b, l; if (helpers.isObject(value)) { t = value.top; r = value.right; b = value.bottom; l = value.left; } else { t = r = b = l = value; } return { top: t, right: r, bottom: b, left: l }; } /** * 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) { const stub = array._chartjs; if (!stub) { return; } const listeners = stub.listeners; const index = listeners.indexOf(listener); if (index !== -1) { listeners.splice(index, 1); } if (listeners.length > 0) { return; } arrayEvents.forEach((key) => { delete array[key]; }); delete array._chartjs; } function getSortedDatasetIndices(chart, filterVisible) { const keys = []; const metasets = chart._getSortedDatasetMetas(filterVisible); let i, ilen; for (i = 0, ilen = metasets.length; i < ilen; ++i) { keys.push(metasets[i].index); } return keys; } function applyStack(stack, value, dsIndex, allOther) { const keys = stack.keys; let i, ilen, datasetIndex, otherValue; for (i = 0, ilen = keys.length; i < ilen; ++i) { datasetIndex = +keys[i]; if (datasetIndex === dsIndex) { if (allOther) { continue; } break; } otherValue = stack.values[datasetIndex]; if (!isNaN(otherValue) && (value === 0 || helpers.math.sign(value) === helpers.math.sign(otherValue))) { value += otherValue; } } return value; } function convertObjectDataToArray(data) { const keys = Object.keys(data); const adata = new Array(keys.length); let i, ilen, key; for (i = 0, ilen = keys.length; i < ilen; ++i) { key = keys[i]; adata[i] = { x: key, y: data[key] }; } return adata; } function isStacked(scale, meta) { const stacked = scale && scale.options.stacked; return stacked || (stacked === undefined && meta.stack !== undefined); } function getStackKey(indexScale, valueScale, meta) { return indexScale.id + '.' + valueScale.id + '.' + meta.stack + '.' + meta.type; } function getUserBounds(scale) { const {min, max, minDefined, maxDefined} = scale.getUserBounds(); return { min: minDefined ? min : Number.NEGATIVE_INFINITY, max: maxDefined ? max : Number.POSITIVE_INFINITY }; } function getOrCreateStack(stacks, stackKey, indexValue) { const subStack = stacks[stackKey] || (stacks[stackKey] = {}); return subStack[indexValue] || (subStack[indexValue] = {}); } function updateStacks(controller, parsed) { const {chart, _cachedMeta: meta} = controller; const stacks = chart._stacks || (chart._stacks = {}); // map structure is {stackKey: {datasetIndex: value}} const {iScale, vScale, index: datasetIndex} = meta; const iAxis = iScale.axis; const vAxis = vScale.axis; const key = getStackKey(iScale, vScale, meta); const ilen = parsed.length; let stack; for (let i = 0; i < ilen; ++i) { const item = parsed[i]; const {[iAxis]: index, [vAxis]: value} = item; const itemStacks = item._stacks || (item._stacks = {}); stack = itemStacks[vAxis] = getOrCreateStack(stacks, key, index); stack[datasetIndex] = value; } } function getFirstScaleId(chart, axis) { const scales = chart.scales; return Object.keys(scales).filter(key => scales[key].axis === axis).shift(); } export default class DatasetController { static extend = helpers.inherits; /** * @param {Chart} chart * @param {number} datasetIndex */ constructor(chart, datasetIndex) { this.chart = chart; this._ctx = chart.ctx; this.index = datasetIndex; this._cachedAnimations = {}; this._cachedDataOpts = {}; this._cachedMeta = this.getMeta(); this._type = this._cachedMeta.type; this._config = undefined; this._parsing = false; this._data = undefined; this._dataCopy = undefined; this._objectData = undefined; this._labels = undefined; this._scaleStacked = {}; this.initialize(); } initialize() { const me = this; const meta = me._cachedMeta; me.configure(); me.linkScales(); meta._stacked = isStacked(meta.vScale, meta); me.addElements(); } updateIndex(datasetIndex) { this.index = datasetIndex; } linkScales() { const me = this; const chart = me.chart; const meta = me._cachedMeta; const dataset = me.getDataset(); const xid = meta.xAxisID = dataset.xAxisID || getFirstScaleId(chart, 'x'); const yid = meta.yAxisID = dataset.yAxisID || getFirstScaleId(chart, 'y'); const rid = meta.rAxisID = dataset.rAxisID || getFirstScaleId(chart, 'r'); meta.xScale = me.getScaleForId(xid); meta.yScale = me.getScaleForId(yid); meta.rScale = me.getScaleForId(rid); meta.iScale = me._getIndexScale(); meta.vScale = me._getValueScale(); } getDataset() { return this.chart.data.datasets[this.index]; } getMeta() { return this.chart.getDatasetMeta(this.index); } /** * @param {string} scaleID * @return {Scale} */ getScaleForId(scaleID) { return this.chart.scales[scaleID]; } /** * @protected */ getValueScaleId() { return this._cachedMeta.yAxisID; } /** * @protected */ getIndexScaleId() { return this._cachedMeta.xAxisID; } /** * @private */ _getValueScale() { return this.getScaleForId(this.getValueScaleId()); } /** * @private */ _getIndexScale() { return this.getScaleForId(this.getIndexScaleId()); } /** * @private */ _getOtherScale(scale) { const meta = this._cachedMeta; return scale === meta.iScale ? meta.vScale : meta.iScale; } reset() { this._update('reset'); } /** * @private */ _destroy() { if (this._data) { unlistenArrayEvents(this._data, this); } } /** * @private */ _dataCheck() { const me = this; const dataset = me.getDataset(); const 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 (helpers.isObject(data)) { // Object data is currently monitored for replacement only if (me._objectData === data) { return false; } me._data = convertObjectDataToArray(data); me._objectData = data; } else { if (me._data === data && helpers.arrayEquals(data, me._dataCopy)) { return false; } if (me._data) { // This case happens when the user replaced the data array instance. unlistenArrayEvents(me._data, me); } // Store a copy to detect direct modifications. // Note: This is suboptimal, but better than always parsing the data me._dataCopy = data.slice(0); if (data && Object.isExtensible(data)) { listenArrayEvents(data, me); } me._data = data; } return true; } /** * @private */ _labelCheck() { const me = this; const iScale = me._cachedMeta.iScale; const labels = iScale ? iScale.getLabels() : me.chart.data.labels; if (me._labels === labels) { return false; } me._labels = labels; return true; } addElements() { const me = this; const meta = me._cachedMeta; me._dataCheck(); const data = me._data; const metaData = meta.data = new Array(data.length); for (let i = 0, ilen = data.length; i < ilen; ++i) { metaData[i] = new me.dataElementType(); } if (me.datasetElementType) { meta.dataset = new me.datasetElementType(); } } buildOrUpdateElements() { const me = this; const dataChanged = me._dataCheck(); const labelsChanged = me._labelCheck(); const scaleChanged = me._scaleCheck(); const meta = me._cachedMeta; const dataset = me.getDataset(); let stackChanged = false; // make sure cached _stacked status is current meta._stacked = isStacked(meta.vScale, meta); // detect change in stack option if (meta.stack !== dataset.stack) { stackChanged = true; // remove values from old stack meta._parsed.forEach((parsed) => { delete parsed._stacks[meta.vScale.id][meta.index]; }); meta.stack = dataset.stack; } // 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(dataChanged || labelsChanged || scaleChanged || stackChanged); // if stack changed, update stack values for the whole dataset if (stackChanged) { updateStacks(me, meta._parsed); } } /** * Merges user-supplied and default dataset-level options * @private */ configure() { const me = this; me._config = helpers.merge({}, [ me.chart.options[me._type].datasets, me.getDataset(), ], { merger(key, target, source) { if (key !== 'data') { helpers._merger(key, target, source); } } }); me._parsing = resolve([me._config.parsing, me.chart.options.parsing, true]); } /** * @param {number} start * @param {number} count */ parse(start, count) { const me = this; const {_cachedMeta: meta, _data: data} = me; const {iScale, vScale, _stacked} = meta; const iAxis = iScale.axis; let sorted = true; let i, parsed, cur, prev; if (start > 0) { sorted = meta._sorted; prev = meta._parsed[start - 1]; } if (me._parsing === false) { meta._parsed = data; meta._sorted = true; } else { if (helpers.isArray(data[start])) { parsed = me.parseArrayData(meta, data, start, count); } else if (helpers.isObject(data[start])) { parsed = me.parseObjectData(meta, data, start, count); } else { parsed = me.parsePrimitiveData(meta, data, start, count); } for (i = 0; i < count; ++i) { meta._parsed[i + start] = cur = parsed[i]; if (sorted) { if (prev && cur[iAxis] < prev[iAxis]) { sorted = false; } prev = cur; } } meta._sorted = sorted; } if (_stacked) { updateStacks(me, parsed); } iScale.invalidateCaches(); vScale.invalidateCaches(); } /** * Parse array of primitive values * @param {object} meta - dataset meta * @param {array} data - data array. Example [1,3,4] * @param {number} start - start index * @param {number} count - number of items to parse * @returns {object} parsed item - item containing index and a parsed value * for each scale id. * Example: {xScale0: 0, yScale0: 1} * @protected */ parsePrimitiveData(meta, data, start, count) { const {iScale, vScale} = meta; const iAxis = iScale.axis; const vAxis = vScale.axis; const labels = iScale.getLabels(); const singleScale = iScale === vScale; const parsed = new Array(count); let i, ilen, index; for (i = 0, ilen = count; i < ilen; ++i) { index = i + start; parsed[i] = { [iAxis]: singleScale || iScale.parse(labels[index], index), [vAxis]: vScale.parse(data[index], index) }; } return parsed; } /** * Parse array of arrays * @param {object} meta - dataset meta * @param {array} data - data array. Example [[1,2],[3,4]] * @param {number} start - start index * @param {number} count - number of items to parse * @returns {object} parsed item - item containing index and a parsed value * for each scale id. * Example: {x: 0, y: 1} * @protected */ parseArrayData(meta, data, start, count) { const {xScale, yScale} = meta; const parsed = new Array(count); let i, ilen, index, item; for (i = 0, ilen = count; i < ilen; ++i) { index = i + start; item = data[index]; parsed[i] = { x: xScale.parse(item[0], index), y: yScale.parse(item[1], index) }; } return parsed; } /** * Parse array of objects * @param {object} meta - dataset meta * @param {array} data - data array. Example [{x:1, y:5}, {x:2, y:10}] * @param {number} start - start index * @param {number} count - number of items to parse * @returns {object} parsed item - item containing index and a parsed value * for each scale id. _custom is optional * Example: {xScale0: 0, yScale0: 1, _custom: {r: 10, foo: 'bar'}} * @protected */ parseObjectData(meta, data, start, count) { const {xScale, yScale} = meta; const parsed = new Array(count); let i, ilen, index, item; for (i = 0, ilen = count; i < ilen; ++i) { index = i + start; item = data[index]; parsed[i] = { x: xScale.parseObject(item, 'x', index), y: yScale.parseObject(item, 'y', index) }; } return parsed; } /** * @protected */ getParsed(index) { return this._cachedMeta._parsed[index]; } /** * @protected */ applyStack(scale, parsed) { const chart = this.chart; const meta = this._cachedMeta; const value = parsed[scale.axis]; const stack = { keys: getSortedDatasetIndices(chart, true), values: parsed._stacks[scale.axis] }; return applyStack(stack, value, meta.index); } /** * @protected */ getMinMax(scale, canStack) { const meta = this._cachedMeta; const _parsed = meta._parsed; const sorted = meta._sorted && scale === meta.iScale; const ilen = _parsed.length; const otherScale = this._getOtherScale(scale); const stack = canStack && meta._stacked && {keys: getSortedDatasetIndices(this.chart, true), values: null}; let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; const {min: otherMin, max: otherMax} = getUserBounds(otherScale); let i, value, parsed, otherValue; function _compute() { if (stack) { stack.values = parsed._stacks[scale.axis]; // Need to consider individual stack values for data range, // in addition to the stacked value min = Math.min(min, value); max = Math.max(max, value); value = applyStack(stack, value, meta.index, true); } min = Math.min(min, value); max = Math.max(max, value); } function _skip() { parsed = _parsed[i]; value = parsed[scale.axis]; otherValue = parsed[otherScale.axis]; return (isNaN(value) || otherMin > otherValue || otherMax < otherValue); } for (i = 0; i < ilen; ++i) { if (_skip()) { continue; } _compute(); if (sorted) { break; } } if (sorted) { for (i = ilen - 1; i >= 0; --i) { if (_skip()) { continue; } _compute(); break; } } return {min, max}; } getAllParsedValues(scale) { const parsed = this._cachedMeta._parsed; const values = []; let i, ilen, value; for (i = 0, ilen = parsed.length; i < ilen; ++i) { value = parsed[i][scale.axis]; if (!isNaN(value)) { values.push(value); } } return values; } /** * @private */ _cacheScaleStackStatus() { const me = this; const meta = me._cachedMeta; const iScale = meta.iScale; const vScale = meta.vScale; const cache = me._scaleStacked = {}; if (iScale && vScale) { cache[iScale.id] = iScale.options.stacked; cache[vScale.id] = vScale.options.stacked; } } /** * @private */ _scaleCheck() { const me = this; const meta = me._cachedMeta; const iScale = meta.iScale; const vScale = meta.vScale; const cache = me._scaleStacked; return !cache || !iScale || !vScale || cache[iScale.id] !== iScale.options.stacked || cache[vScale.id] !== vScale.options.stacked; } /** * @return {number|boolean} * @protected */ getMaxOverflow() { return false; } /** * @protected */ getLabelAndValue(index) { const me = this; const meta = me._cachedMeta; const iScale = meta.iScale; const vScale = meta.vScale; const parsed = me.getParsed(index); return { label: iScale ? '' + iScale.getLabelForValue(parsed[iScale.axis]) : '', value: vScale ? '' + vScale.getLabelForValue(parsed[vScale.axis]) : '' }; } /** * @private */ _update(mode) { const me = this; const meta = me._cachedMeta; me.configure(); me._cachedAnimations = {}; me._cachedDataOpts = {}; me.update(mode); meta._clip = toClip(helpers.valueOrDefault(me._config.clip, defaultClip(meta.xScale, meta.yScale, me.getMaxOverflow()))); me._cacheScaleStackStatus(); } /** * @param {string} mode */ update(mode) {} // eslint-disable-line no-unused-vars draw() { const ctx = this._ctx; const meta = this._cachedMeta; const elements = meta.data || []; const ilen = elements.length; let i = 0; if (meta.dataset) { meta.dataset.draw(ctx); } for (; i < ilen; ++i) { elements[i].draw(ctx); } } /** * @private */ _addAutomaticHoverColors(index, options) { const me = this; const getHoverColor = helpers.getHoverColor; const normalOptions = me.getStyle(index); const missingColors = Object.keys(normalOptions).filter(key => key.indexOf('Color') !== -1 && !(key in options)); let i = missingColors.length - 1; let color; for (; i >= 0; i--) { color = missingColors[i]; options[color] = getHoverColor(normalOptions[color]); } } /** * 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 * @param {boolean} [active] - true if hover * @return {object} style object */ getStyle(index, active) { const me = this; const meta = me._cachedMeta; const dataset = meta.dataset; if (!me._config) { me.configure(); } const options = dataset && index === undefined ? me.resolveDatasetElementOptions(active) : me.resolveDataElementOptions(index || 0, active && 'active'); if (active) { me._addAutomaticHoverColors(index, options); } return options; } /** * @private */ _getContext(index, active) { return { chart: this.chart, dataIndex: index, dataset: this.getDataset(), datasetIndex: this.index, active }; } /** * @protected */ resolveDatasetElementOptions(active) { const me = this; const chart = me.chart; const datasetOpts = me._config; // @ts-ignore const options = chart.options.elements[me.datasetElementType._type] || {}; const elementOptions = me.datasetElementOptions; const values = {}; const context = me._getContext(undefined, active); let i, ilen, key, readKey, value; for (i = 0, ilen = elementOptions.length; i < ilen; ++i) { key = elementOptions[i]; readKey = active ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key; value = resolve([ datasetOpts[readKey], options[readKey] ], context); if (value !== undefined) { values[key] = value; } } return values; } /** * @protected */ resolveDataElementOptions(index, mode) { const me = this; const active = mode === 'active'; const cached = me._cachedDataOpts; if (cached[mode]) { return cached[mode]; } const chart = me.chart; const datasetOpts = me._config; // @ts-ignore const options = chart.options.elements[me.dataElementType._type] || {}; const elementOptions = me.dataElementOptions; const values = {}; const context = me._getContext(index, active); const info = {cacheable: !active}; let keys, i, ilen, key, value, readKey; if (helpers.isArray(elementOptions)) { for (i = 0, ilen = elementOptions.length; i < ilen; ++i) { key = elementOptions[i]; readKey = active ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key; value = resolve([ datasetOpts[readKey], options[readKey] ], context, index, info); if (value !== undefined) { values[key] = value; } } } else { keys = Object.keys(elementOptions); for (i = 0, ilen = keys.length; i < ilen; ++i) { key = keys[i]; readKey = active ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key; value = resolve([ datasetOpts[elementOptions[readKey]], datasetOpts[readKey], options[readKey] ], context, index, info); if (value !== undefined) { values[key] = value; } } } if (info.cacheable) { // `$shared` indicades this set of options can be shared between multiple elements. // Sharing is used to reduce number of properties to change during animation. values.$shared = true; // We cache options by `mode`, which can be 'active' for example. This enables us // to have the 'active' element options and 'default' options to switch between // when interacting. cached[mode] = values; } return values; } /** * @private */ _resolveAnimations(index, mode, active) { const me = this; const chart = me.chart; const cached = me._cachedAnimations; mode = mode || 'default'; if (cached[mode]) { return cached[mode]; } const info = {cacheable: true}; const context = me._getContext(index, active); const datasetAnim = resolve([me._config.animation], context, index, info); const chartAnim = resolve([chart.options.animation], context, index, info); let config = helpers.mergeIf({}, [datasetAnim, chartAnim]); if (config[mode]) { config = Object.assign({}, config, config[mode]); } const animations = new Animations(chart, config); if (info.cacheable) { cached[mode] = animations && Object.freeze(animations); } return animations; } /** * Utility for checking if the options are shared and should be animated separately. * @protected */ getSharedOptions(mode, el, options) { if (!mode) { // store element option sharing status for usage in interactions this._sharedOptions = options && options.$shared; } if (mode !== 'reset' && options && options.$shared && el && el.options && el.options.$shared) { return {target: el.options, options}; } } /** * Utility for determining if `options` should be included in the updated properties * @protected */ includeOptions(mode, sharedOptions) { if (mode === 'hide' || mode === 'show') { return true; } return mode !== 'resize' && !sharedOptions; } /** * Utility for updating an element with new properties, using animations when appropriate. * @protected */ updateElement(element, index, properties, mode) { if (mode === 'reset' || mode === 'none') { Object.assign(element, properties); } else { this._resolveAnimations(index, mode).update(element, properties); } } /** * Utility to animate the shared options, that are potentially affecting multiple elements. * @protected */ updateSharedOptions(sharedOptions, mode) { if (sharedOptions) { this._resolveAnimations(undefined, mode).update(sharedOptions.target, sharedOptions.options); } } /** * @private */ _setStyle(element, index, mode, active) { element.active = active; this._resolveAnimations(index, mode, active).update(element, {options: this.getStyle(index, active)}); } removeHoverStyle(element, datasetIndex, index) { this._setStyle(element, index, 'active', false); } setHoverStyle(element, datasetIndex, index) { this._setStyle(element, index, 'active', true); } /** * @private */ _removeDatasetHoverStyle() { const element = this._cachedMeta.dataset; if (element) { this._setStyle(element, undefined, 'active', false); } } /** * @private */ _setDatasetHoverStyle() { const element = this._cachedMeta.dataset; if (element) { this._setStyle(element, undefined, 'active', true); } } /** * @private */ _resyncElements(changed) { const me = this; const meta = me._cachedMeta; const numMeta = meta.data.length; const numData = me._data.length; if (numData > numMeta) { me._insertElements(numMeta, numData - numMeta); if (changed && numMeta) { // _insertElements parses the new elements. The old ones might need parsing too. me.parse(0, numMeta); } } else if (numData < numMeta) { meta.data.splice(numData, numMeta - numData); meta._parsed.splice(numData, numMeta - numData); me.parse(0, numData); } else if (changed) { me.parse(0, numData); } } /** * @private */ _insertElements(start, count) { const me = this; const elements = new Array(count); const meta = me._cachedMeta; const data = meta.data; let i; for (i = 0; i < count; ++i) { elements[i] = new me.dataElementType(); } data.splice(start, 0, ...elements); if (me._parsing) { meta._parsed.splice(start, 0, ...new Array(count)); } me.parse(start, count); me.updateElements(elements, start, 'reset'); } updateElements(element, start, mode) {} // eslint-disable-line no-unused-vars /** * @private */ _removeElements(start, count) { const me = this; if (me._parsing) { me._cachedMeta._parsed.splice(start, count); } me._cachedMeta.data.splice(start, count); } /** * @private */ _onDataPush() { const count = arguments.length; this._insertElements(this.getDataset().data.length - count, count); } /** * @private */ _onDataPop() { this._removeElements(this._cachedMeta.data.length - 1, 1); } /** * @private */ _onDataShift() { this._removeElements(0, 1); } /** * @private */ _onDataSplice(start, count) { this._removeElements(start, count); this._insertElements(start, arguments.length - 2); } /** * @private */ _onDataUnshift() { this._insertElements(0, arguments.length); } } /** * Element type used to generate a meta dataset (e.g. Chart.element.Line). */ DatasetController.prototype.datasetElementType = null; /** * Element type used to generate a meta data (e.g. Chart.element.Point). */ DatasetController.prototype.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. * @type {string[]} */ DatasetController.prototype.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. * @type {string[]|object} */ DatasetController.prototype.dataElementOptions = [ 'backgroundColor', 'borderColor', 'borderWidth', 'pointStyle' ];