mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
Rewrite the clone and merge helpers (#4422)
The `clone` method now accepts any type of input but also recursively perform a deep copy of the array items. Rewrite the `configMerge` and `scaleMerge` helpers which now rely on a new generic and customizable `merge` method, that one accepts a target object in which multiple sources are deep copied. Note that the target (first argument) is not cloned and will be modified after calling `merge(target, sources)`. Add a `mergeIf` helper which merge the source properties only if they do not exist in the target object.
This commit is contained in:
parent
6f317135a3
commit
225bfd36f3
@ -8,19 +8,7 @@ module.exports = function(Chart) {
|
||||
var helpers = Chart.helpers;
|
||||
|
||||
// -- Basic js utility methods
|
||||
helpers.clone = function(obj) {
|
||||
var objClone = {};
|
||||
helpers.each(obj, function(value, key) {
|
||||
if (helpers.isArray(value)) {
|
||||
objClone[key] = value.slice(0);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
objClone[key] = helpers.clone(value);
|
||||
} else {
|
||||
objClone[key] = value;
|
||||
}
|
||||
});
|
||||
return objClone;
|
||||
};
|
||||
|
||||
helpers.extend = function(base) {
|
||||
var setFn = function(value, key) {
|
||||
base[key] = value;
|
||||
@ -30,75 +18,60 @@ module.exports = function(Chart) {
|
||||
}
|
||||
return base;
|
||||
};
|
||||
// Need a special merge function to chart configs since they are now grouped
|
||||
helpers.configMerge = function(_base) {
|
||||
var base = helpers.clone(_base);
|
||||
helpers.each(Array.prototype.slice.call(arguments, 1), function(extension) {
|
||||
helpers.each(extension, function(value, key) {
|
||||
var baseHasProperty = base.hasOwnProperty(key);
|
||||
var baseVal = baseHasProperty ? base[key] : {};
|
||||
|
||||
helpers.configMerge = function(/* objects ... */) {
|
||||
return helpers.merge(helpers.clone(arguments[0]), [].slice.call(arguments, 1), {
|
||||
merger: function(key, target, source, options) {
|
||||
var tval = target[key] || {};
|
||||
var sval = source[key];
|
||||
|
||||
if (key === 'scales') {
|
||||
// Scale config merging is complex. Add our own function here for that
|
||||
base[key] = helpers.scaleMerge(baseVal, value);
|
||||
// scale config merging is complex. Add our own function here for that
|
||||
target[key] = helpers.scaleMerge(tval, sval);
|
||||
} else if (key === 'scale') {
|
||||
// Used in polar area & radar charts since there is only one scale
|
||||
base[key] = helpers.configMerge(baseVal, Chart.scaleService.getScaleDefaults(value.type), value);
|
||||
} else if (baseHasProperty
|
||||
&& typeof baseVal === 'object'
|
||||
&& !helpers.isArray(baseVal)
|
||||
&& baseVal !== null
|
||||
&& typeof value === 'object'
|
||||
&& !helpers.isArray(value)) {
|
||||
// If we are overwriting an object with an object, do a merge of the properties.
|
||||
base[key] = helpers.configMerge(baseVal, value);
|
||||
// used in polar area & radar charts since there is only one scale
|
||||
target[key] = helpers.merge(tval, [Chart.scaleService.getScaleDefaults(sval.type), sval]);
|
||||
} else {
|
||||
// can just overwrite the value in this case
|
||||
base[key] = value;
|
||||
helpers._merger(key, target, source, options);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return base;
|
||||
};
|
||||
helpers.scaleMerge = function(_base, extension) {
|
||||
var base = helpers.clone(_base);
|
||||
|
||||
helpers.each(extension, function(value, key) {
|
||||
if (key === 'xAxes' || key === 'yAxes') {
|
||||
// These properties are arrays of items
|
||||
if (base.hasOwnProperty(key)) {
|
||||
helpers.each(value, function(valueObj, index) {
|
||||
var axisType = helpers.valueOrDefault(valueObj.type, key === 'xAxes' ? 'category' : 'linear');
|
||||
var axisDefaults = Chart.scaleService.getScaleDefaults(axisType);
|
||||
if (index >= base[key].length || !base[key][index].type) {
|
||||
base[key].push(helpers.configMerge(axisDefaults, valueObj));
|
||||
} else if (valueObj.type && valueObj.type !== base[key][index].type) {
|
||||
// Type changed. Bring in the new defaults before we bring in valueObj so that valueObj can override the correct scale defaults
|
||||
base[key][index] = helpers.configMerge(base[key][index], axisDefaults, valueObj);
|
||||
} else {
|
||||
// Type is the same
|
||||
base[key][index] = helpers.configMerge(base[key][index], valueObj);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
base[key] = [];
|
||||
helpers.each(value, function(valueObj) {
|
||||
var axisType = helpers.valueOrDefault(valueObj.type, key === 'xAxes' ? 'category' : 'linear');
|
||||
base[key].push(helpers.configMerge(Chart.scaleService.getScaleDefaults(axisType), valueObj));
|
||||
});
|
||||
}
|
||||
} else if (base.hasOwnProperty(key) && typeof base[key] === 'object' && base[key] !== null && typeof value === 'object') {
|
||||
// If we are overwriting an object with an object, do a merge of the properties.
|
||||
base[key] = helpers.configMerge(base[key], value);
|
||||
|
||||
} else {
|
||||
// can just overwrite the value in this case
|
||||
base[key] = value;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return base;
|
||||
helpers.scaleMerge = function(/* objects ... */) {
|
||||
return helpers.merge(helpers.clone(arguments[0]), [].slice.call(arguments, 1), {
|
||||
merger: function(key, target, source, options) {
|
||||
if (key === 'xAxes' || key === 'yAxes') {
|
||||
var slen = source[key].length;
|
||||
var i, type, scale, defaults;
|
||||
|
||||
if (!target[key]) {
|
||||
target[key] = [];
|
||||
}
|
||||
|
||||
for (i = 0; i < slen; ++i) {
|
||||
scale = source[key][i];
|
||||
type = helpers.valueOrDefault(scale.type, key === 'xAxes'? 'category' : 'linear');
|
||||
defaults = Chart.scaleService.getScaleDefaults(type);
|
||||
|
||||
if (i >= target[key].length) {
|
||||
target[key].push({});
|
||||
}
|
||||
|
||||
if (!target[key][i].type || (scale.type && scale.type !== target[key][i].type)) {
|
||||
// new/untyped scale or type changed: let's apply the new defaults
|
||||
// then merge source scale to correctly overwrite the defaults.
|
||||
helpers.merge(target[key][i], [defaults, scale]);
|
||||
} else {
|
||||
// scales type are the same
|
||||
helpers.merge(target[key][i], scale);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
helpers._merger(key, target, source, options);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
helpers.where = function(collection, filterCallback) {
|
||||
|
||||
@ -22,7 +22,7 @@ module.exports = function(Chart) {
|
||||
},
|
||||
getScaleDefaults: function(type) {
|
||||
// Return the scale defaults merged with the global settings so that we always use the latest ones
|
||||
return this.defaults.hasOwnProperty(type) ? helpers.scaleMerge(Chart.defaults.scale, this.defaults[type]) : {};
|
||||
return this.defaults.hasOwnProperty(type) ? helpers.merge({}, [Chart.defaults.scale, this.defaults[type]]) : {};
|
||||
},
|
||||
updateScaleDefaults: function(type, additions) {
|
||||
var defaults = this.defaults;
|
||||
|
||||
@ -147,6 +147,110 @@ module.exports = function(Chart) {
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a deep copy of `source` without keeping references on objects and arrays.
|
||||
* @param {*} source - The value to clone.
|
||||
* @returns {*}
|
||||
*/
|
||||
clone: function(source) {
|
||||
if (helpers.isArray(source)) {
|
||||
return source.map(helpers.clone);
|
||||
}
|
||||
|
||||
if (helpers.isObject(source)) {
|
||||
var target = {};
|
||||
var keys = Object.keys(source);
|
||||
var klen = keys.length;
|
||||
var k = 0;
|
||||
|
||||
for (; k<klen; ++k) {
|
||||
target[keys[k]] = helpers.clone(source[keys[k]]);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
return source;
|
||||
},
|
||||
|
||||
/**
|
||||
* The default merger when Chart.helpers.merge is called without merger option.
|
||||
* Note(SB): this method is also used by configMerge and scaleMerge as fallback.
|
||||
* @private
|
||||
*/
|
||||
_merger: function(key, target, source, options) {
|
||||
var tval = target[key];
|
||||
var sval = source[key];
|
||||
|
||||
if (helpers.isObject(tval) && helpers.isObject(sval)) {
|
||||
helpers.merge(tval, sval, options);
|
||||
} else {
|
||||
target[key] = helpers.clone(sval);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Merges source[key] in target[key] only if target[key] is undefined.
|
||||
* @private
|
||||
*/
|
||||
_mergerIf: function(key, target, source) {
|
||||
var tval = target[key];
|
||||
var sval = source[key];
|
||||
|
||||
if (helpers.isObject(tval) && helpers.isObject(sval)) {
|
||||
helpers.mergeIf(tval, sval);
|
||||
} else if (!target.hasOwnProperty(key)) {
|
||||
target[key] = helpers.clone(sval);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Recursively deep copies `source` properties into `target` with the given `options`.
|
||||
* IMPORTANT: `target` is not cloned and will be updated with `source` properties.
|
||||
* @param {Object} target - The target object in which all sources are merged into.
|
||||
* @param {Object|Array(Object)} source - Object(s) to merge into `target`.
|
||||
* @param {Object} [options] - Merging options:
|
||||
* @param {Function} [options.merger] - The merge method (key, target, source, options)
|
||||
* @returns {Object} The `target` object.
|
||||
*/
|
||||
merge: function(target, source, options) {
|
||||
var sources = helpers.isArray(source)? source : [source];
|
||||
var ilen = sources.length;
|
||||
var merge, i, keys, klen, k;
|
||||
|
||||
if (!helpers.isObject(target)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
merge = options.merger || helpers._merger;
|
||||
|
||||
for (i=0; i<ilen; ++i) {
|
||||
source = sources[i];
|
||||
if (!helpers.isObject(source)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
keys = Object.keys(source);
|
||||
for (k=0, klen = keys.length; k<klen; ++k) {
|
||||
merge(keys[k], target, source, options);
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
},
|
||||
|
||||
/**
|
||||
* Recursively deep copies `source` properties into `target` *only* if not defined in target.
|
||||
* IMPORTANT: `target` is not cloned and will be updated with `source` properties.
|
||||
* @param {Object} target - The target object in which all sources are merged into.
|
||||
* @param {Object|Array(Object)} source - Object(s) to merge into `target`.
|
||||
* @returns {Object} The `target` object.
|
||||
*/
|
||||
mergeIf: function(target, source) {
|
||||
return helpers.merge(target, source, {merger: helpers._mergerIf});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -524,7 +524,7 @@ module.exports = function(Chart) {
|
||||
var legend = chart.legend;
|
||||
|
||||
if (legendOpts) {
|
||||
legendOpts = helpers.configMerge(Chart.defaults.global.legend, legendOpts);
|
||||
helpers.mergeIf(legendOpts, Chart.defaults.global.legend);
|
||||
|
||||
if (legend) {
|
||||
layout.configure(chart, legend, legendOpts);
|
||||
|
||||
@ -224,7 +224,7 @@ module.exports = function(Chart) {
|
||||
var titleBlock = chart.titleBlock;
|
||||
|
||||
if (titleOpts) {
|
||||
titleOpts = helpers.configMerge(Chart.defaults.global.title, titleOpts);
|
||||
helpers.mergeIf(titleOpts, Chart.defaults.global.title);
|
||||
|
||||
if (titleBlock) {
|
||||
layout.configure(chart, titleBlock, titleOpts);
|
||||
|
||||
@ -6,25 +6,6 @@ describe('Core helper tests', function() {
|
||||
helpers = window.Chart.helpers;
|
||||
});
|
||||
|
||||
it('should clone an object', function() {
|
||||
var testData = {
|
||||
myProp1: 'abc',
|
||||
myProp2: ['a', 'b'],
|
||||
myProp3: {
|
||||
myProp4: 5,
|
||||
myProp5: [1, 2]
|
||||
}
|
||||
};
|
||||
|
||||
var clone = helpers.clone(testData);
|
||||
expect(clone).toEqual(testData);
|
||||
expect(clone).not.toBe(testData);
|
||||
|
||||
expect(clone.myProp2).not.toBe(testData.myProp2);
|
||||
expect(clone.myProp3).not.toBe(testData.myProp3);
|
||||
expect(clone.myProp3.myProp5).not.toBe(testData.myProp3.myProp5);
|
||||
});
|
||||
|
||||
it('should extend an object', function() {
|
||||
var original = {
|
||||
myProp1: 'abc',
|
||||
|
||||
@ -236,4 +236,137 @@ describe('Chart.helpers.core', function() {
|
||||
expect(helpers.arrayEquals([o0, o1, o2], [o0, o1, o2])).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clone', function() {
|
||||
it('should clone primitive values', function() {
|
||||
expect(helpers.clone()).toBe(undefined);
|
||||
expect(helpers.clone(null)).toBe(null);
|
||||
expect(helpers.clone(true)).toBe(true);
|
||||
expect(helpers.clone(42)).toBe(42);
|
||||
expect(helpers.clone('foo')).toBe('foo');
|
||||
});
|
||||
it('should perform a deep copy of arrays', function() {
|
||||
var o0 = {a: 42};
|
||||
var o1 = {s: 's'};
|
||||
var a0 = ['bar'];
|
||||
var a1 = [a0, o0, 2];
|
||||
var f0 = function() {};
|
||||
var input = [a1, o1, f0, 42, 'foo'];
|
||||
var output = helpers.clone(input);
|
||||
|
||||
expect(output).toEqual(input);
|
||||
expect(output).not.toBe(input);
|
||||
expect(output[0]).not.toBe(a1);
|
||||
expect(output[0][0]).not.toBe(a0);
|
||||
expect(output[1]).not.toBe(o1);
|
||||
});
|
||||
it('should perform a deep copy of objects', function() {
|
||||
var a0 = ['bar'];
|
||||
var a1 = [1, 2, 3];
|
||||
var o0 = {a: a1, i: 42};
|
||||
var f0 = function() {};
|
||||
var input = {o: o0, a: a0, f: f0, s: 'foo', i: 42};
|
||||
var output = helpers.clone(input);
|
||||
|
||||
expect(output).toEqual(input);
|
||||
expect(output).not.toBe(input);
|
||||
expect(output.o).not.toBe(o0);
|
||||
expect(output.o.a).not.toBe(a1);
|
||||
expect(output.a).not.toBe(a0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('merge', function() {
|
||||
it('should update target and return it', function() {
|
||||
var target = {a: 1};
|
||||
var result = helpers.merge(target, {a: 2, b: 'foo'});
|
||||
expect(target).toEqual({a: 2, b: 'foo'});
|
||||
expect(target).toBe(result);
|
||||
});
|
||||
it('should return target if not an object', function() {
|
||||
expect(helpers.merge(undefined, {a: 42})).toEqual(undefined);
|
||||
expect(helpers.merge(null, {a: 42})).toEqual(null);
|
||||
expect(helpers.merge('foo', {a: 42})).toEqual('foo');
|
||||
expect(helpers.merge(['foo', 'bar'], {a: 42})).toEqual(['foo', 'bar']);
|
||||
});
|
||||
it('should ignore sources which are not objects', function() {
|
||||
expect(helpers.merge({a: 42})).toEqual({a: 42});
|
||||
expect(helpers.merge({a: 42}, null)).toEqual({a: 42});
|
||||
expect(helpers.merge({a: 42}, 42)).toEqual({a: 42});
|
||||
});
|
||||
it('should recursively overwrite target with source properties', function() {
|
||||
expect(helpers.merge({a: {b: 1}}, {a: {c: 2}})).toEqual({a: {b: 1, c: 2}});
|
||||
expect(helpers.merge({a: {b: 1}}, {a: {b: 2}})).toEqual({a: {b: 2}});
|
||||
expect(helpers.merge({a: [1, 2]}, {a: [3, 4]})).toEqual({a: [3, 4]});
|
||||
expect(helpers.merge({a: 42}, {a: {b: 0}})).toEqual({a: {b: 0}});
|
||||
expect(helpers.merge({a: 42}, {a: null})).toEqual({a: null});
|
||||
expect(helpers.merge({a: 42}, {a: undefined})).toEqual({a: undefined});
|
||||
});
|
||||
it('should merge multiple sources in the correct order', function() {
|
||||
var t0 = {a: {b: 1, c: [1, 2]}};
|
||||
var s0 = {a: {d: 3}, e: {f: 4}};
|
||||
var s1 = {a: {b: 5}};
|
||||
var s2 = {a: {c: [6, 7]}, e: 'foo'};
|
||||
|
||||
expect(helpers.merge(t0, [s0, s1, s2])).toEqual({a: {b: 5, c: [6, 7], d: 3}, e: 'foo'});
|
||||
});
|
||||
it('should deep copy merged values from sources', function() {
|
||||
var a0 = ['foo'];
|
||||
var a1 = [1, 2, 3];
|
||||
var o0 = {a: a1, i: 42};
|
||||
var output = helpers.merge({}, {a: a0, o: o0});
|
||||
|
||||
expect(output).toEqual({a: a0, o: o0});
|
||||
expect(output.a).not.toBe(a0);
|
||||
expect(output.o).not.toBe(o0);
|
||||
expect(output.o.a).not.toBe(a1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeIf', function() {
|
||||
it('should update target and return it', function() {
|
||||
var target = {a: 1};
|
||||
var result = helpers.mergeIf(target, {a: 2, b: 'foo'});
|
||||
expect(target).toEqual({a: 1, b: 'foo'});
|
||||
expect(target).toBe(result);
|
||||
});
|
||||
it('should return target if not an object', function() {
|
||||
expect(helpers.mergeIf(undefined, {a: 42})).toEqual(undefined);
|
||||
expect(helpers.mergeIf(null, {a: 42})).toEqual(null);
|
||||
expect(helpers.mergeIf('foo', {a: 42})).toEqual('foo');
|
||||
expect(helpers.mergeIf(['foo', 'bar'], {a: 42})).toEqual(['foo', 'bar']);
|
||||
});
|
||||
it('should ignore sources which are not objects', function() {
|
||||
expect(helpers.mergeIf({a: 42})).toEqual({a: 42});
|
||||
expect(helpers.mergeIf({a: 42}, null)).toEqual({a: 42});
|
||||
expect(helpers.mergeIf({a: 42}, 42)).toEqual({a: 42});
|
||||
});
|
||||
it('should recursively copy source properties in target only if they do not exist in target', function() {
|
||||
expect(helpers.mergeIf({a: {b: 1}}, {a: {c: 2}})).toEqual({a: {b: 1, c: 2}});
|
||||
expect(helpers.mergeIf({a: {b: 1}}, {a: {b: 2}})).toEqual({a: {b: 1}});
|
||||
expect(helpers.mergeIf({a: [1, 2]}, {a: [3, 4]})).toEqual({a: [1, 2]});
|
||||
expect(helpers.mergeIf({a: 0}, {a: {b: 2}})).toEqual({a: 0});
|
||||
expect(helpers.mergeIf({a: null}, {a: 42})).toEqual({a: null});
|
||||
expect(helpers.mergeIf({a: undefined}, {a: 42})).toEqual({a: undefined});
|
||||
});
|
||||
it('should merge multiple sources in the correct order', function() {
|
||||
var t0 = {a: {b: 1, c: [1, 2]}};
|
||||
var s0 = {a: {d: 3}, e: {f: 4}};
|
||||
var s1 = {a: {b: 5}};
|
||||
var s2 = {a: {c: [6, 7]}, e: 'foo'};
|
||||
|
||||
expect(helpers.mergeIf(t0, [s0, s1, s2])).toEqual({a: {b: 1, c: [1, 2], d: 3}, e: {f: 4}});
|
||||
});
|
||||
it('should deep copy merged values from sources', function() {
|
||||
var a0 = ['foo'];
|
||||
var a1 = [1, 2, 3];
|
||||
var o0 = {a: a1, i: 42};
|
||||
var output = helpers.mergeIf({}, {a: a0, o: o0});
|
||||
|
||||
expect(output).toEqual({a: a0, o: o0});
|
||||
expect(output.a).not.toBe(a0);
|
||||
expect(output.o).not.toBe(o0);
|
||||
expect(output.o.a).not.toBe(a1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user