mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-07 00:36:50 +01:00
398 lines
11 KiB
JavaScript
398 lines
11 KiB
JavaScript
import defaults, {overrides, descriptors} from './core.defaults.js';
|
|
import {mergeIf, resolveObjectKey, isArray, isFunction, valueOrDefault, isObject} from '../helpers/helpers.core.js';
|
|
import {_attachContext, _createResolver, _descriptors} from '../helpers/helpers.config.js';
|
|
|
|
export function getIndexAxis(type, options) {
|
|
const datasetDefaults = defaults.datasets[type] || {};
|
|
const datasetOptions = (options.datasets || {})[type] || {};
|
|
return datasetOptions.indexAxis || options.indexAxis || datasetDefaults.indexAxis || 'x';
|
|
}
|
|
|
|
function getAxisFromDefaultScaleID(id, indexAxis) {
|
|
let axis = id;
|
|
if (id === '_index_') {
|
|
axis = indexAxis;
|
|
} else if (id === '_value_') {
|
|
axis = indexAxis === 'x' ? 'y' : 'x';
|
|
}
|
|
return axis;
|
|
}
|
|
|
|
function getDefaultScaleIDFromAxis(axis, indexAxis) {
|
|
return axis === indexAxis ? '_index_' : '_value_';
|
|
}
|
|
|
|
function axisFromPosition(position) {
|
|
if (position === 'top' || position === 'bottom') {
|
|
return 'x';
|
|
}
|
|
if (position === 'left' || position === 'right') {
|
|
return 'y';
|
|
}
|
|
}
|
|
|
|
export function determineAxis(id, scaleOptions) {
|
|
if (id === 'x' || id === 'y' || id === 'r') {
|
|
return id;
|
|
}
|
|
|
|
id = scaleOptions.axis
|
|
|| axisFromPosition(scaleOptions.position)
|
|
|| id.length > 1 && determineAxis(id[0].toLowerCase(), scaleOptions);
|
|
|
|
if (id) {
|
|
return id;
|
|
}
|
|
|
|
throw new Error(`Cannot determine type of '${name}' axis. Please provide 'axis' or 'position' option.`);
|
|
}
|
|
|
|
function mergeScaleConfig(config, options) {
|
|
const chartDefaults = overrides[config.type] || {scales: {}};
|
|
const configScales = options.scales || {};
|
|
const chartIndexAxis = getIndexAxis(config.type, options);
|
|
const scales = Object.create(null);
|
|
|
|
// First figure out first scale id's per axis.
|
|
Object.keys(configScales).forEach(id => {
|
|
const scaleConf = configScales[id];
|
|
if (!isObject(scaleConf)) {
|
|
return console.error(`Invalid scale configuration for scale: ${id}`);
|
|
}
|
|
if (scaleConf._proxy) {
|
|
return console.warn(`Ignoring resolver passed as options for scale: ${id}`);
|
|
}
|
|
const axis = determineAxis(id, scaleConf);
|
|
const defaultId = getDefaultScaleIDFromAxis(axis, chartIndexAxis);
|
|
const defaultScaleOptions = chartDefaults.scales || {};
|
|
scales[id] = mergeIf(Object.create(null), [{axis}, scaleConf, defaultScaleOptions[axis], defaultScaleOptions[defaultId]]);
|
|
});
|
|
|
|
// Then merge dataset defaults to scale configs
|
|
config.data.datasets.forEach(dataset => {
|
|
const type = dataset.type || config.type;
|
|
const indexAxis = dataset.indexAxis || getIndexAxis(type, options);
|
|
const datasetDefaults = overrides[type] || {};
|
|
const defaultScaleOptions = datasetDefaults.scales || {};
|
|
Object.keys(defaultScaleOptions).forEach(defaultID => {
|
|
const axis = getAxisFromDefaultScaleID(defaultID, indexAxis);
|
|
const id = dataset[axis + 'AxisID'] || axis;
|
|
scales[id] = scales[id] || Object.create(null);
|
|
mergeIf(scales[id], [{axis}, configScales[id], defaultScaleOptions[defaultID]]);
|
|
});
|
|
});
|
|
|
|
// apply scale defaults, if not overridden by dataset defaults
|
|
Object.keys(scales).forEach(key => {
|
|
const scale = scales[key];
|
|
mergeIf(scale, [defaults.scales[scale.type], defaults.scale]);
|
|
});
|
|
|
|
return scales;
|
|
}
|
|
|
|
function initOptions(config) {
|
|
const options = config.options || (config.options = {});
|
|
|
|
options.plugins = valueOrDefault(options.plugins, {});
|
|
options.scales = mergeScaleConfig(config, options);
|
|
}
|
|
|
|
function initData(data) {
|
|
data = data || {};
|
|
data.datasets = data.datasets || [];
|
|
data.labels = data.labels || [];
|
|
return data;
|
|
}
|
|
|
|
function initConfig(config) {
|
|
config = config || {};
|
|
config.data = initData(config.data);
|
|
|
|
initOptions(config);
|
|
|
|
return config;
|
|
}
|
|
|
|
const keyCache = new Map();
|
|
const keysCached = new Set();
|
|
|
|
function cachedKeys(cacheKey, generate) {
|
|
let keys = keyCache.get(cacheKey);
|
|
if (!keys) {
|
|
keys = generate();
|
|
keyCache.set(cacheKey, keys);
|
|
keysCached.add(keys);
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
const addIfFound = (set, obj, key) => {
|
|
const opts = resolveObjectKey(obj, key);
|
|
if (opts !== undefined) {
|
|
set.add(opts);
|
|
}
|
|
};
|
|
|
|
export default class Config {
|
|
constructor(config) {
|
|
this._config = initConfig(config);
|
|
this._scopeCache = new Map();
|
|
this._resolverCache = new Map();
|
|
}
|
|
|
|
get platform() {
|
|
return this._config.platform;
|
|
}
|
|
|
|
get type() {
|
|
return this._config.type;
|
|
}
|
|
|
|
set type(type) {
|
|
this._config.type = type;
|
|
}
|
|
|
|
get data() {
|
|
return this._config.data;
|
|
}
|
|
|
|
set data(data) {
|
|
this._config.data = initData(data);
|
|
}
|
|
|
|
get options() {
|
|
return this._config.options;
|
|
}
|
|
|
|
set options(options) {
|
|
this._config.options = options;
|
|
}
|
|
|
|
get plugins() {
|
|
return this._config.plugins;
|
|
}
|
|
|
|
update() {
|
|
const config = this._config;
|
|
this.clearCache();
|
|
initOptions(config);
|
|
}
|
|
|
|
clearCache() {
|
|
this._scopeCache.clear();
|
|
this._resolverCache.clear();
|
|
}
|
|
|
|
/**
|
|
* Returns the option scope keys for resolving dataset options.
|
|
* These keys do not include the dataset itself, because it is not under options.
|
|
* @param {string} datasetType
|
|
* @return {string[][]}
|
|
*/
|
|
datasetScopeKeys(datasetType) {
|
|
return cachedKeys(datasetType,
|
|
() => [[
|
|
`datasets.${datasetType}`,
|
|
''
|
|
]]);
|
|
}
|
|
|
|
/**
|
|
* Returns the option scope keys for resolving dataset animation options.
|
|
* These keys do not include the dataset itself, because it is not under options.
|
|
* @param {string} datasetType
|
|
* @param {string} transition
|
|
* @return {string[][]}
|
|
*/
|
|
datasetAnimationScopeKeys(datasetType, transition) {
|
|
return cachedKeys(`${datasetType}.transition.${transition}`,
|
|
() => [
|
|
[
|
|
`datasets.${datasetType}.transitions.${transition}`,
|
|
`transitions.${transition}`,
|
|
],
|
|
// The following are used for looking up the `animations` and `animation` keys
|
|
[
|
|
`datasets.${datasetType}`,
|
|
''
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Returns the options scope keys for resolving element options that belong
|
|
* to an dataset. These keys do not include the dataset itself, because it
|
|
* is not under options.
|
|
* @param {string} datasetType
|
|
* @param {string} elementType
|
|
* @return {string[][]}
|
|
*/
|
|
datasetElementScopeKeys(datasetType, elementType) {
|
|
return cachedKeys(`${datasetType}-${elementType}`,
|
|
() => [[
|
|
`datasets.${datasetType}.elements.${elementType}`,
|
|
`datasets.${datasetType}`,
|
|
`elements.${elementType}`,
|
|
''
|
|
]]);
|
|
}
|
|
|
|
/**
|
|
* Returns the options scope keys for resolving plugin options.
|
|
* @param {{id: string, additionalOptionScopes?: string[]}} plugin
|
|
* @return {string[][]}
|
|
*/
|
|
pluginScopeKeys(plugin) {
|
|
const id = plugin.id;
|
|
const type = this.type;
|
|
return cachedKeys(`${type}-plugin-${id}`,
|
|
() => [[
|
|
`plugins.${id}`,
|
|
...plugin.additionalOptionScopes || [],
|
|
]]);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_cachedScopes(mainScope, resetCache) {
|
|
const _scopeCache = this._scopeCache;
|
|
let cache = _scopeCache.get(mainScope);
|
|
if (!cache || resetCache) {
|
|
cache = new Map();
|
|
_scopeCache.set(mainScope, cache);
|
|
}
|
|
return cache;
|
|
}
|
|
|
|
/**
|
|
* Resolves the objects from options and defaults for option value resolution.
|
|
* @param {object} mainScope - The main scope object for options
|
|
* @param {string[][]} keyLists - The arrays of keys in resolution order
|
|
* @param {boolean} [resetCache] - reset the cache for this mainScope
|
|
*/
|
|
getOptionScopes(mainScope, keyLists, resetCache) {
|
|
const {options, type} = this;
|
|
const cache = this._cachedScopes(mainScope, resetCache);
|
|
const cached = cache.get(keyLists);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
const scopes = new Set();
|
|
|
|
keyLists.forEach(keys => {
|
|
if (mainScope) {
|
|
scopes.add(mainScope);
|
|
keys.forEach(key => addIfFound(scopes, mainScope, key));
|
|
}
|
|
keys.forEach(key => addIfFound(scopes, options, key));
|
|
keys.forEach(key => addIfFound(scopes, overrides[type] || {}, key));
|
|
keys.forEach(key => addIfFound(scopes, defaults, key));
|
|
keys.forEach(key => addIfFound(scopes, descriptors, key));
|
|
});
|
|
|
|
const array = Array.from(scopes);
|
|
if (array.length === 0) {
|
|
array.push(Object.create(null));
|
|
}
|
|
if (keysCached.has(keyLists)) {
|
|
cache.set(keyLists, array);
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/**
|
|
* Returns the option scopes for resolving chart options
|
|
* @return {object[]}
|
|
*/
|
|
chartOptionScopes() {
|
|
const {options, type} = this;
|
|
|
|
return [
|
|
options,
|
|
overrides[type] || {},
|
|
defaults.datasets[type] || {}, // https://github.com/chartjs/Chart.js/issues/8531
|
|
{type},
|
|
defaults,
|
|
descriptors
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param {object[]} scopes
|
|
* @param {string[]} names
|
|
* @param {function|object} context
|
|
* @param {string[]} [prefixes]
|
|
* @return {object}
|
|
*/
|
|
resolveNamedOptions(scopes, names, context, prefixes = ['']) {
|
|
const result = {$shared: true};
|
|
const {resolver, subPrefixes} = getResolver(this._resolverCache, scopes, prefixes);
|
|
let options = resolver;
|
|
if (needContext(resolver, names)) {
|
|
result.$shared = false;
|
|
context = isFunction(context) ? context() : context;
|
|
// subResolver is passed to scriptable options. It should not resolve to hover options.
|
|
const subResolver = this.createResolver(scopes, context, subPrefixes);
|
|
options = _attachContext(resolver, context, subResolver);
|
|
}
|
|
|
|
for (const prop of names) {
|
|
result[prop] = options[prop];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @param {object[]} scopes
|
|
* @param {object} [context]
|
|
* @param {string[]} [prefixes]
|
|
* @param {{scriptable: boolean, indexable: boolean, allKeys?: boolean}} [descriptorDefaults]
|
|
*/
|
|
createResolver(scopes, context, prefixes = [''], descriptorDefaults) {
|
|
const {resolver} = getResolver(this._resolverCache, scopes, prefixes);
|
|
return isObject(context)
|
|
? _attachContext(resolver, context, undefined, descriptorDefaults)
|
|
: resolver;
|
|
}
|
|
}
|
|
|
|
function getResolver(resolverCache, scopes, prefixes) {
|
|
let cache = resolverCache.get(scopes);
|
|
if (!cache) {
|
|
cache = new Map();
|
|
resolverCache.set(scopes, cache);
|
|
}
|
|
const cacheKey = prefixes.join();
|
|
let cached = cache.get(cacheKey);
|
|
if (!cached) {
|
|
const resolver = _createResolver(scopes, prefixes);
|
|
cached = {
|
|
resolver,
|
|
subPrefixes: prefixes.filter(p => !p.toLowerCase().includes('hover'))
|
|
};
|
|
cache.set(cacheKey, cached);
|
|
}
|
|
return cached;
|
|
}
|
|
|
|
const hasFunction = value => isObject(value)
|
|
&& Object.getOwnPropertyNames(value).reduce((acc, key) => acc || isFunction(value[key]), false);
|
|
|
|
function needContext(proxy, names) {
|
|
const {isScriptable, isIndexable} = _descriptors(proxy);
|
|
|
|
for (const prop of names) {
|
|
const scriptable = isScriptable(prop);
|
|
const indexable = isIndexable(prop);
|
|
const value = (indexable || scriptable) && proxy[prop];
|
|
if ((scriptable && (isFunction(value) || hasFunction(value)))
|
|
|| (indexable && isArray(value))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|