Files
Chart.js/src/core/core.datasetController.js
Jukka Kurkela ce74eb76a1 Convert axis options from arrays to objects (#6773)
* Convert axis options from arrays to objects

* Updated all chart type defaults
* Throw errors when axis type or position are not specified
* Avoid raising unnecessary errors when merging options into the default configs

* Fix additional tests

* Ensure scale defaults are set if type is not explicitly defined

* Another step

* Include `scale` as `firstIDs.r`

* update docs

* Update for buildOrUpdateScales

* Update migration guide

* Add test back
2019-11-21 18:46:49 -05:00

1027 lines
24 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;
}
});
});
}
function scaleClip(scale, allowedOverflow) {
var opts = scale && scale.options || {};
var reverse = opts.reverse;
var min = opts.min === undefined ? allowedOverflow : 0;
var 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;
}
var x = scaleClip(xScale, allowedOverflow);
var y = scaleClip(yScale, allowedOverflow);
return {
top: y.end,
right: x.end,
bottom: y.start,
left: x.start
};
}
function toClip(value) {
var 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) {
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;
}
function getSortedDatasetIndices(chart, filterVisible) {
var keys = [];
var metasets = chart._getSortedDatasetMetas(filterVisible);
var i, ilen;
for (i = 0, ilen = metasets.length; i < ilen; ++i) {
keys.push(metasets[i].index);
}
return keys;
}
function applyStack(stack, value, dsIndex, allOther) {
var keys = stack.keys;
var 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.sign(value) === helpers.sign(otherValue))) {
value += otherValue;
}
}
return value;
}
function convertObjectDataToArray(data) {
var keys = Object.keys(data);
var adata = [];
var i, ilen, key;
for (i = 0, ilen = keys.length; i < ilen; ++i) {
key = keys[i];
adata.push({
x: key,
y: data[key]
});
}
return adata;
}
function isStacked(scale, meta) {
var 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) {
var {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 = controller.chart;
const meta = controller._cachedMeta;
const stacks = chart._stacks || (chart._stacks = {}); // map structure is {stackKey: {datasetIndex: value}}
const xScale = meta.xScale;
const yScale = meta.yScale;
const xId = xScale.id;
const yId = yScale.id;
const xStacked = isStacked(xScale, meta);
const yStacked = isStacked(yScale, meta);
const xKey = yStacked && getStackKey(xScale, yScale, meta);
const yKey = xStacked && getStackKey(yScale, xScale, meta);
const ilen = parsed.length;
const datasetIndex = meta.index;
let stack;
for (let i = 0; i < ilen; ++i) {
const item = parsed[i];
const x = item[xId];
const y = item[yId];
const itemStacks = item._stacks || (item._stacks = {});
if (yStacked) {
stack = itemStacks[yId] = getOrCreateStack(stacks, xKey, x);
stack[datasetIndex] = y;
}
if (xStacked) {
stack = itemStacks[xId] = getOrCreateStack(stacks, yKey, y);
stack[datasetIndex] = x;
}
}
}
function getFirstScaleId(chart, axis) {
const scales = chart.scales;
return Object.keys(scales).filter(key => {
return scales[key].axis === axis;
}).shift();
}
// 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) {
const me = this;
let meta;
me.chart = chart;
me._ctx = chart.ctx;
me.index = datasetIndex;
me._cachedMeta = meta = me.getMeta();
me._type = meta.type;
me.linkScales();
meta._stacked = isStacked(meta.vScale, meta);
me.addElements();
},
updateIndex: function(datasetIndex) {
this.index = datasetIndex;
},
linkScales: function() {
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: 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._cachedMeta.yAxisID;
},
/**
* @private
*/
_getIndexScaleId: function() {
return this._cachedMeta.xAxisID;
},
/**
* @private
*/
_getValueScale: function() {
return this.getScaleForId(this._getValueScaleId());
},
/**
* @private
*/
_getIndexScale: function() {
return this.getScaleForId(this._getIndexScaleId());
},
/**
* @private
*/
_getOtherScale: function(scale) {
const meta = this._cachedMeta;
return scale === meta.iScale
? meta.vScale
: meta.iScale;
},
reset: function() {
this._update(true);
},
/**
* @private
*/
destroy: function() {
if (this._data) {
unlistenArrayEvents(this._data, this);
}
},
createElement: function(type) {
return type && new type();
},
/**
* @private
*/
_dataCheck: 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 (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: function() {
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: function() {
var me = this;
var meta = me._cachedMeta;
var metaData = meta.data;
var i, ilen, data;
me._dataCheck();
data = me._data;
for (i = 0, ilen = data.length; i < ilen; ++i) {
metaData[i] = metaData[i] || me.createElement(me.dataElementType);
}
meta.dataset = meta.dataset || me.createElement(me.datasetElementType);
},
buildOrUpdateElements: function() {
const me = this;
const dataChanged = me._dataCheck();
const labelsChanged = me._labelCheck();
const scaleChanged = me._scaleCheck();
const meta = me._cachedMeta;
// make sure cached _stacked status is current
meta._stacked = isStacked(meta.vScale, meta);
// 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);
},
/**
* Returns the merged user-supplied and default dataset-level options
* @private
*/
_configure: function() {
const me = this;
me._config = helpers.merge({}, [
me.chart.options.datasets[me._type],
me.getDataset(),
], {
merger: function(key, target, source) {
if (key !== 'data') {
helpers._merger(key, target, source);
}
}
});
},
/**
* @private
*/
_parse: function(start, count) {
const me = this;
const meta = me._cachedMeta;
const data = me._data;
const iScale = meta.iScale;
const vScale = meta.vScale;
const stacked = isStacked(iScale, meta) || isStacked(vScale, meta);
let i, ilen, parsed;
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, ilen = parsed.length; i < ilen; ++i) {
meta.data[start + i]._parsed = parsed[i];
}
if (stacked) {
updateStacks(me, parsed);
}
iScale._invalidateCaches();
if (vScale !== iScale) {
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}
* @private
*/
_parsePrimitiveData: function(meta, data, start, count) {
const iScale = meta.iScale;
const vScale = meta.vScale;
const labels = iScale._getLabels();
const singleScale = iScale === vScale;
const parsed = [];
let i, ilen, item;
for (i = start, ilen = start + count; i < ilen; ++i) {
item = {};
item[iScale.id] = singleScale || iScale._parse(labels[i], i);
item[vScale.id] = vScale._parse(data[i], i);
parsed.push(item);
}
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: {xScale0: 0, yScale0: 1}
* @private
*/
_parseArrayData: function(meta, data, start, count) {
const xScale = meta.xScale;
const yScale = meta.yScale;
const parsed = [];
let i, ilen, item, arr;
for (i = start, ilen = start + count; i < ilen; ++i) {
arr = data[i];
item = {};
item[xScale.id] = xScale._parse(arr[0], i);
item[yScale.id] = yScale._parse(arr[1], i);
parsed.push(item);
}
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'}}
* @private
*/
_parseObjectData: function(meta, data, start, count) {
const xScale = meta.xScale;
const yScale = meta.yScale;
const parsed = [];
let i, ilen, item, obj;
for (i = start, ilen = start + count; i < ilen; ++i) {
obj = data[i];
item = {};
item[xScale.id] = xScale._parseObject(obj, 'x', i);
item[yScale.id] = yScale._parseObject(obj, 'y', i);
parsed.push(item);
}
return parsed;
},
/**
* @private
*/
_getParsed: function(index) {
const data = this._cachedMeta.data;
if (index < 0 || index >= data.length) {
return;
}
return data[index]._parsed;
},
/**
* @private
*/
_applyStack: function(scale, parsed) {
const chart = this.chart;
const meta = this._cachedMeta;
const value = parsed[scale.id];
const stack = {
keys: getSortedDatasetIndices(chart, true),
values: parsed._stacks[scale.id]
};
return applyStack(stack, value, meta.index);
},
/**
* @private
*/
_getMinMax: function(scale, canStack) {
const chart = this.chart;
const meta = this._cachedMeta;
const metaData = meta.data;
const ilen = metaData.length;
const stacked = canStack && meta._stacked;
const indices = getSortedDatasetIndices(chart, true);
const otherScale = this._getOtherScale(scale);
let max = Number.NEGATIVE_INFINITY;
let {min: otherMin, max: otherMax} = getUserBounds(otherScale);
let i, item, value, parsed, stack, min, minPositive, otherValue;
min = minPositive = Number.POSITIVE_INFINITY;
for (i = 0; i < ilen; ++i) {
item = metaData[i];
parsed = item._parsed;
value = parsed[scale.id];
otherValue = parsed[otherScale.id];
if (item.hidden || isNaN(value) ||
otherMin > otherValue || otherMax < otherValue) {
continue;
}
if (stacked) {
stack = {
keys: indices,
values: parsed._stacks[scale.id]
};
value = applyStack(stack, value, meta.index, true);
}
min = Math.min(min, value);
max = Math.max(max, value);
if (value > 0) {
minPositive = Math.min(minPositive, value);
}
}
return {
min: min,
max: max,
minPositive: minPositive
};
},
/**
* @private
*/
_getAllParsedValues: function(scale) {
const meta = this._cachedMeta;
const metaData = meta.data;
const values = [];
let i, ilen, value;
for (i = 0, ilen = metaData.length; i < ilen; ++i) {
value = metaData[i]._parsed[scale.id];
if (!isNaN(value)) {
values.push(value);
}
}
return values;
},
/**
* @private
*/
_cacheScaleStackStatus: function() {
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: function() {
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;
},
/**
* @private
*/
_getMaxOverflow: function() {
return false;
},
/**
* @private
*/
_getLabelAndValue: function(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.id]) : '',
value: vScale ? '' + vScale.getLabelForValue(parsed[vScale.id]) : ''
};
},
/**
* @private
*/
_update: function(reset) {
const me = this;
const meta = me._cachedMeta;
me._configure();
me._cachedDataOpts = null;
me.update(reset);
meta._clip = toClip(helpers.valueOrDefault(me._config.clip, defaultClip(meta.xScale, meta.yScale, me._getMaxOverflow())));
me._cacheScaleStackStatus();
},
update: helpers.noop,
transition: function(easingValue) {
const meta = this._cachedMeta;
const elements = meta.data || [];
const ilen = elements.length;
let i = 0;
for (; i < ilen; ++i) {
elements[i].transition(easingValue);
}
if (meta.dataset) {
meta.dataset.transition(easingValue);
}
},
draw: function() {
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);
}
},
/**
* 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) {
const me = this;
const meta = me._cachedMeta;
const dataset = meta.dataset;
let style;
me._configure();
if (dataset && index === undefined) {
style = me._resolveDatasetElementOptions();
} else {
index = index || 0;
style = me._resolveDataElementOptions(index);
}
if (style.fill === false || style.fill === null) {
style.backgroundColor = style.borderColor;
}
return style;
},
/**
* @private
*/
_resolveDatasetElementOptions: function(active) {
const me = this;
const chart = me.chart;
const datasetOpts = me._config;
const options = chart.options.elements[me.datasetElementType.prototype._type] || {};
const elementOptions = me._datasetElementOptions;
const values = {};
const context = {
chart,
dataset: me.getDataset(),
datasetIndex: me.index,
active
};
let i, ilen, key, readKey;
for (i = 0, ilen = elementOptions.length; i < ilen; ++i) {
key = elementOptions[i];
readKey = active ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key;
values[key] = resolve([
datasetOpts[readKey],
options[readKey]
], context);
}
return values;
},
/**
* @private
*/
_resolveDataElementOptions: function(index) {
const me = this;
const cached = me._cachedDataOpts;
if (cached) {
return cached;
}
const chart = me.chart;
const datasetOpts = me._config;
const options = chart.options.elements[me.dataElementType.prototype._type] || {};
const elementOptions = me._dataElementOptions;
const values = {};
const context = {
chart: chart,
dataIndex: index,
dataset: me.getDataset(),
datasetIndex: me.index
};
const info = {cacheable: true};
let keys, i, ilen, key;
if (helpers.isArray(elementOptions)) {
for (i = 0, ilen = elementOptions.length; i < ilen; ++i) {
key = elementOptions[i];
values[key] = resolve([
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([
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, datasetIndex, index) {
const dataset = this.chart.data.datasets[datasetIndex];
const model = element._model;
const getHoverColor = helpers.getHoverColor;
element.$previousStyle = {
backgroundColor: model.backgroundColor,
borderColor: model.borderColor,
borderWidth: model.borderWidth
};
model.backgroundColor = resolve([dataset.hoverBackgroundColor, getHoverColor(model.backgroundColor)], undefined, index);
model.borderColor = resolve([dataset.hoverBorderColor, getHoverColor(model.borderColor)], undefined, index);
model.borderWidth = resolve([dataset.hoverBorderWidth, model.borderWidth], undefined, index);
},
/**
* @private
*/
_removeDatasetHoverStyle: function() {
const element = this._cachedMeta.dataset;
if (element) {
this.removeHoverStyle(element);
}
},
/**
* @private
*/
_setDatasetHoverStyle: function() {
const element = this._cachedMeta.dataset;
const prev = {};
let i, ilen, key, keys, hoverOptions, model;
if (!element) {
return;
}
model = element._model;
hoverOptions = this._resolveDatasetElementOptions(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(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);
} else if (numData < numMeta) {
meta.data.splice(numData, numMeta - numData);
me._parse(0, numData);
} else if (changed) {
me._parse(0, numData);
}
},
/**
* @private
*/
insertElements: function(start, count) {
const me = this;
const elements = [];
const data = me._cachedMeta.data;
let i;
for (i = start; i < start + count; ++i) {
elements.push(me.createElement(me.dataElementType));
}
data.splice(start, 0, ...elements);
me._parse(start, count);
me.updateElements(data, start, count);
},
/**
* @private
*/
onDataPush: function() {
const count = arguments.length;
this.insertElements(this.getDataset().data.length - count, count);
},
/**
* @private
*/
onDataPop: function() {
this._cachedMeta.data.pop();
},
/**
* @private
*/
onDataShift: function() {
this._cachedMeta.data.shift();
},
/**
* @private
*/
onDataSplice: function(start, count) {
this._cachedMeta.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;