Move config handling to a dedicated script (#7939)

* Split core.config.js out of core.controller.js

* Remove side effects from config.js

* Turn config into a class

* Refactor config merging as includeDefaults
This commit is contained in:
Jukka Kurkela
2020-10-24 18:37:37 +03:00
committed by GitHub
parent 58d1911d05
commit 6dbb7e7446
3 changed files with 204 additions and 179 deletions

181
src/core/core.config.js Normal file
View File

@@ -0,0 +1,181 @@
/* eslint-disable import/no-namespace, import/namespace */
import defaults from './core.defaults';
import {mergeIf, merge, _merger} from '../helpers/helpers.core';
export function getIndexAxis(type, options) {
const typeDefaults = defaults[type] || {};
const datasetDefaults = typeDefaults.datasets || {};
const typeOptions = options[type] || {};
const datasetOptions = typeOptions.datasets || {};
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;
}
return scaleOptions.axis || axisFromPosition(scaleOptions.position) || id.charAt(0).toLowerCase();
}
function mergeScaleConfig(config, options) {
options = options || {};
const chartDefaults = defaults[config.type] || {scales: {}};
const configScales = options.scales || {};
const chartIndexAxis = getIndexAxis(config.type, options);
const firstIDs = Object.create(null);
const scales = Object.create(null);
// First figure out first scale id's per axis.
Object.keys(configScales).forEach(id => {
const scaleConf = configScales[id];
const axis = determineAxis(id, scaleConf);
const defaultId = getDefaultScaleIDFromAxis(axis, chartIndexAxis);
firstIDs[axis] = firstIDs[axis] || id;
scales[id] = mergeIf(Object.create(null), [{axis}, scaleConf, chartDefaults.scales[axis], chartDefaults.scales[defaultId]]);
});
// Backward compatibility
if (options.scale) {
scales[options.scale.id || 'r'] = mergeIf(Object.create(null), [{axis: 'r'}, options.scale, chartDefaults.scales.r]);
firstIDs.r = firstIDs.r || options.scale.id || 'r';
}
// 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 = defaults[type] || {};
const defaultScaleOptions = datasetDefaults.scales || {};
Object.keys(defaultScaleOptions).forEach(defaultID => {
const axis = getAxisFromDefaultScaleID(defaultID, indexAxis);
const id = dataset[axis + 'AxisID'] || firstIDs[axis] || 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;
}
/**
* Recursively merge the given config objects as the root options by handling
* default scale options for the `scales` and `scale` properties, then returns
* a deep copy of the result, thus doesn't alter inputs.
*/
function mergeConfig(...args/* config objects ... */) {
return merge(Object.create(null), args, {
merger(key, target, source, options) {
if (key !== 'scales' && key !== 'scale') {
_merger(key, target, source, options);
}
}
});
}
function includeDefaults(options, type) {
return mergeConfig(
defaults,
defaults[type],
options || {});
}
function initConfig(config) {
config = config || {};
// Do NOT use mergeConfig for the data object because this method merges arrays
// and so would change references to labels and datasets, preventing data updates.
const data = config.data = config.data || {datasets: [], labels: []};
data.datasets = data.datasets || [];
data.labels = data.labels || [];
const scaleConfig = mergeScaleConfig(config, config.options);
const options = config.options = includeDefaults(config.options, config.type);
options.hover = merge(Object.create(null), [
defaults.interaction,
defaults.hover,
options.interaction,
options.hover
]);
options.scales = scaleConfig;
options.title = (options.title !== false) && merge(Object.create(null), [
defaults.plugins.title,
options.title
]);
options.tooltips = (options.tooltips !== false) && merge(Object.create(null), [
defaults.interaction,
defaults.plugins.tooltip,
options.interaction,
options.tooltips
]);
return config;
}
export default class Config {
constructor(config) {
this._config = initConfig(config);
}
get type() {
return this._config.type;
}
get data() {
return this._config.data;
}
set data(data) {
this._config.data = data;
}
get options() {
return this._config.options;
}
get plugins() {
return this._config.plugins;
}
update(options) {
const config = this._config;
const scaleConfig = mergeScaleConfig(config, options);
options = includeDefaults(options, config.type);
options.scales = scaleConfig;
config.options = options;
}
}

View File

@@ -6,8 +6,9 @@ import layouts from './core.layouts';
import {BasicPlatform, DomPlatform} from '../platform';
import PluginService from './core.plugins';
import registry from './core.registry';
import Config, {determineAxis, getIndexAxis} from './core.config';
import {retinaScale} from '../helpers/helpers.dom';
import {mergeIf, merge, _merger, each, callback as callCallback, uid, valueOrDefault, _elementsEqual} from '../helpers/helpers.core';
import {each, callback as callCallback, uid, valueOrDefault, _elementsEqual} from '../helpers/helpers.core';
import {clear as canvasClear, clipArea, unclipArea, _isPointInArea} from '../helpers/helpers.canvas';
// @ts-ignore
import {version} from '../../package.json';
@@ -16,174 +17,11 @@ import {version} from '../../package.json';
* @typedef { import("../platform/platform.base").IEvent } IEvent
*/
function getIndexAxis(type, options) {
const typeDefaults = defaults[type] || {};
const datasetDefaults = typeDefaults.datasets || {};
const typeOptions = options[type] || {};
const datasetOptions = typeOptions.datasets || {};
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 mergeScaleConfig(config, options) {
options = options || {};
const chartDefaults = defaults[config.type] || {scales: {}};
const configScales = options.scales || {};
const chartIndexAxis = getIndexAxis(config.type, options);
const firstIDs = Object.create(null);
const scales = Object.create(null);
// First figure out first scale id's per axis.
Object.keys(configScales).forEach(id => {
const scaleConf = configScales[id];
const axis = determineAxis(id, scaleConf);
const defaultId = getDefaultScaleIDFromAxis(axis, chartIndexAxis);
firstIDs[axis] = firstIDs[axis] || id;
scales[id] = mergeIf(Object.create(null), [{axis}, scaleConf, chartDefaults.scales[axis], chartDefaults.scales[defaultId]]);
});
// Backward compatibility
if (options.scale) {
scales[options.scale.id || 'r'] = mergeIf(Object.create(null), [{axis: 'r'}, options.scale, chartDefaults.scales.r]);
firstIDs.r = firstIDs.r || options.scale.id || 'r';
}
// 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 = defaults[type] || {};
const defaultScaleOptions = datasetDefaults.scales || {};
Object.keys(defaultScaleOptions).forEach(defaultID => {
const axis = getAxisFromDefaultScaleID(defaultID, indexAxis);
const id = dataset[axis + 'AxisID'] || firstIDs[axis] || 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;
}
/**
* Recursively merge the given config objects as the root options by handling
* default scale options for the `scales` and `scale` properties, then returns
* a deep copy of the result, thus doesn't alter inputs.
*/
function mergeConfig(...args/* config objects ... */) {
return merge(Object.create(null), args, {
merger(key, target, source, options) {
if (key !== 'scales' && key !== 'scale') {
_merger(key, target, source, options);
}
}
});
}
function initConfig(config) {
config = config || {};
// Do NOT use mergeConfig for the data object because this method merges arrays
// and so would change references to labels and datasets, preventing data updates.
const data = config.data = config.data || {datasets: [], labels: []};
data.datasets = data.datasets || [];
data.labels = data.labels || [];
const scaleConfig = mergeScaleConfig(config, config.options);
const options = config.options = mergeConfig(
defaults,
defaults[config.type],
config.options || {});
options.hover = merge(Object.create(null), [
defaults.interaction,
defaults.hover,
options.interaction,
options.hover
]);
options.scales = scaleConfig;
options.title = (options.title !== false) && merge(Object.create(null), [
defaults.plugins.title,
options.title
]);
options.tooltips = (options.tooltips !== false) && merge(Object.create(null), [
defaults.interaction,
defaults.plugins.tooltip,
options.interaction,
options.tooltips
]);
return config;
}
function isAnimationDisabled(config) {
return !config.animation;
}
function updateConfig(chart) {
let newOptions = chart.options;
each(chart.scales, (scale) => {
layouts.removeBox(chart, scale);
});
const scaleConfig = mergeScaleConfig(chart.config, newOptions);
newOptions = mergeConfig(
defaults,
defaults[chart.config.type],
newOptions);
chart.options = chart.config.options = newOptions;
chart.options.scales = scaleConfig;
chart._animationsDisabled = isAnimationDisabled(newOptions);
}
const KNOWN_POSITIONS = ['top', 'bottom', 'left', 'right', 'chartArea'];
function positionIsHorizontal(position, axis) {
return position === 'top' || position === 'bottom' || (KNOWN_POSITIONS.indexOf(position) === -1 && axis === 'x');
}
function axisFromPosition(position) {
if (position === 'top' || position === 'bottom') {
return 'x';
}
if (position === 'left' || position === 'right') {
return 'y';
}
}
function determineAxis(id, scaleOptions) {
if (id === 'x' || id === 'y' || id === 'r') {
return id;
}
return scaleOptions.axis || axisFromPosition(scaleOptions.position) || id.charAt(0).toLowerCase();
}
function compare2Level(l1, l2) {
return function(a, b) {
return a[l1] === b[l1]
@@ -235,7 +73,7 @@ class Chart {
constructor(item, config) {
const me = this;
config = initConfig(config);
this.config = config = new Config(config);
const initialCanvas = getCanvas(item);
const existingChart = Chart.getChart(initialCanvas);
if (existingChart) {
@@ -255,7 +93,6 @@ class Chart {
this.id = uid();
this.ctx = context;
this.canvas = canvas;
this.config = config;
this.width = width;
this.height = height;
this.aspectRatio = height ? width / height : null;
@@ -266,7 +103,6 @@ class Chart {
this.boxes = [];
this.currentDevicePixelRatio = undefined;
this.chartArea = undefined;
this.data = undefined;
this._active = [];
this._lastEvent = undefined;
/** @type {{attach?: function, detach?: function, resize?: function}} */
@@ -279,20 +115,11 @@ class Chart {
this.$proxies = {};
this._hiddenIndices = {};
this.attached = false;
this._animationsDisabled = undefined;
// Add the chart instance to the global namespace
Chart.instances[me.id] = me;
// Define alias to the config data: `chart.data === chart.config.data`
Object.defineProperty(me, 'data', {
get() {
return me.config.data;
},
set(value) {
me.config.data = value;
}
});
if (!context || !canvas) {
// The given item is not a compatible context2d element, let's return before finalizing
// the chart initialization but after setting basic chart / controller properties that
@@ -311,6 +138,14 @@ class Chart {
}
}
get data() {
return this.config.data;
}
set data(data) {
this.config.data = data;
}
/**
* @private
*/
@@ -592,7 +427,13 @@ class Chart {
me._updating = true;
updateConfig(me);
each(me.scales, (scale) => {
layouts.removeBox(me, scale);
});
me.config.update(me.options);
me.options = me.config.options;
me._animationsDisabled = !me.options.animation;
me.ensureScalesHaveIDs();
me.buildOrUpdateScales();

View File

@@ -50,7 +50,7 @@ export default class PluginService {
return this._cache;
}
const config = (chart && chart.config) || {};
const config = chart && chart.config;
const options = (config.options && config.options.plugins) || {};
const plugins = allPlugins(config);
const descriptors = createDescriptors(plugins, options);
@@ -61,6 +61,9 @@ export default class PluginService {
}
}
/**
* @param {import("./core.config").default} config
*/
function allPlugins(config) {
const plugins = [];
const keys = Object.keys(registry.plugins.items);