chartjs/Chart.js

View on GitHub
src/core/core.config.js

Summary

Maintainability
B
4 hrs
Test Coverage
File `core.config.js` has 310 lines of code (exceeds 250 allowed). Consider refactoring.
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 idMatchesAxis(id) {
if (id === 'x' || id === 'y' || id === 'r') {
return id;
}
}
 
function axisFromPosition(position) {
if (position === 'top' || position === 'bottom') {
return 'x';
}
if (position === 'left' || position === 'right') {
return 'y';
}
}
 
export function determineAxis(id, ...scaleOptions) {
if (idMatchesAxis(id)) {
return id;
}
for (const opts of scaleOptions) {
const axis = opts.axis
|| axisFromPosition(opts.position)
|| id.length > 1 && idMatchesAxis(id[0].toLowerCase());
if (axis) {
return axis;
}
}
throw new Error(`Cannot determine type of '${id}' axis. Please provide 'axis' or 'position' option.`);
}
 
function getAxisFromDataset(id, axis, dataset) {
if (dataset[axis + 'AxisID'] === id) {
return {axis};
}
}
 
function retrieveAxisFromDatasets(id, config) {
if (config.data && config.data.datasets) {
const boundDs = config.data.datasets.filter((d) => d.xAxisID === id || d.yAxisID === id);
if (boundDs.length) {
return getAxisFromDataset(id, 'x', boundDs[0]) || getAxisFromDataset(id, 'y', boundDs[0]);
}
}
return {};
}
 
Function `mergeScaleConfig` has 34 lines of code (exceeds 25 allowed). Consider refactoring.
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, retrieveAxisFromDatasets(id, config), defaults.scales[scaleConf.type]);
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).some((key) => isFunction(value[key]));
 
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;
}