Skip to content

Commit 05b0ec9

Browse files
authored
Merge pull request #2577 from plotly/transform-react
Transform react
2 parents 4ed586a + 0414147 commit 05b0ec9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+1264
-489
lines changed

src/components/colorscale/calc.js

+49-24
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,47 @@ var flipScale = require('./flip_scale');
1616

1717

1818
module.exports = function calc(trace, vals, containerStr, cLetter) {
19-
var container, inputContainer;
19+
var container = trace;
20+
var inputContainer = trace._input;
21+
var fullInputContainer = trace._fullInput;
22+
23+
// set by traces with groupby transforms
24+
var updateStyle = trace.updateStyle;
25+
26+
function doUpdate(attr, inputVal, fullVal) {
27+
if(fullVal === undefined) fullVal = inputVal;
28+
29+
if(updateStyle) {
30+
updateStyle(trace._input, containerStr ? (containerStr + '.' + attr) : attr, inputVal);
31+
}
32+
else {
33+
inputContainer[attr] = inputVal;
34+
}
35+
36+
container[attr] = fullVal;
37+
if(fullInputContainer && (trace !== trace._fullInput)) {
38+
if(updateStyle) {
39+
updateStyle(trace._fullInput, containerStr ? (containerStr + '.' + attr) : attr, fullVal);
40+
}
41+
else {
42+
fullInputContainer[attr] = fullVal;
43+
}
44+
}
45+
}
2046

2147
if(containerStr) {
22-
container = Lib.nestedProperty(trace, containerStr).get();
23-
inputContainer = Lib.nestedProperty(trace._input, containerStr).get();
24-
}
25-
else {
26-
container = trace;
27-
inputContainer = trace._input;
48+
container = Lib.nestedProperty(container, containerStr).get();
49+
inputContainer = Lib.nestedProperty(inputContainer, containerStr).get();
50+
fullInputContainer = Lib.nestedProperty(fullInputContainer, containerStr).get() || {};
2851
}
2952

30-
var autoAttr = cLetter + 'auto',
31-
minAttr = cLetter + 'min',
32-
maxAttr = cLetter + 'max',
33-
auto = container[autoAttr],
34-
min = container[minAttr],
35-
max = container[maxAttr],
36-
scl = container.colorscale;
53+
var autoAttr = cLetter + 'auto';
54+
var minAttr = cLetter + 'min';
55+
var maxAttr = cLetter + 'max';
56+
var auto = container[autoAttr];
57+
var min = container[minAttr];
58+
var max = container[maxAttr];
59+
var scl = container.colorscale;
3760

3861
if(auto !== false || min === undefined) {
3962
min = Lib.aggNums(Math.min, null, vals);
@@ -48,11 +71,8 @@ module.exports = function calc(trace, vals, containerStr, cLetter) {
4871
max += 0.5;
4972
}
5073

51-
container[minAttr] = min;
52-
container[maxAttr] = max;
53-
54-
inputContainer[minAttr] = min;
55-
inputContainer[maxAttr] = max;
74+
doUpdate(minAttr, min);
75+
doUpdate(maxAttr, max);
5676

5777
/*
5878
* If auto was explicitly false but min or max was missing,
@@ -61,17 +81,22 @@ module.exports = function calc(trace, vals, containerStr, cLetter) {
6181
* Otherwise make sure the trace still looks auto as far as later
6282
* changes are concerned.
6383
*/
64-
inputContainer[autoAttr] = (auto !== false ||
65-
(min === undefined && max === undefined));
84+
doUpdate(autoAttr, (auto !== false || (min === undefined && max === undefined)));
6685

6786
if(container.autocolorscale) {
6887
if(min * max < 0) scl = scales.RdBu;
6988
else if(min >= 0) scl = scales.Reds;
7089
else scl = scales.Blues;
7190

7291
// reversescale is handled at the containerOut level
73-
inputContainer.colorscale = scl;
74-
if(container.reversescale) scl = flipScale(scl);
75-
container.colorscale = scl;
92+
doUpdate('colorscale', scl, container.reversescale ? flipScale(scl) : scl);
93+
94+
// We pushed a colorscale back to input, which will change the default autocolorscale next time
95+
// to avoid spurious redraws from Plotly.react, update resulting autocolorscale now
96+
// This is a conscious decision so that changing the data later does not unexpectedly
97+
// give you a new colorscale
98+
if(!inputContainer.autocolorscale) {
99+
doUpdate('autocolorscale', false);
100+
}
76101
}
77102
};

src/lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ lib.ensureArray = require('./ensure_array');
3030
var isArrayModule = require('./is_array');
3131
lib.isTypedArray = isArrayModule.isTypedArray;
3232
lib.isArrayOrTypedArray = isArrayModule.isArrayOrTypedArray;
33+
lib.isArray1D = isArrayModule.isArray1D;
3334

3435
var coerceModule = require('./coerce');
3536
lib.valObjectMeta = coerceModule.valObjectMeta;

src/lib/is_array.js

+22-4
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,28 @@ var dv = (typeof DataView === 'undefined') ?
1818
function() {} :
1919
DataView;
2020

21-
exports.isTypedArray = function(a) {
21+
function isTypedArray(a) {
2222
return ab.isView(a) && !(a instanceof dv);
23-
};
23+
}
24+
25+
function isArrayOrTypedArray(a) {
26+
return Array.isArray(a) || isTypedArray(a);
27+
}
28+
29+
/*
30+
* Test whether an input object is 1D.
31+
*
32+
* Assumes we already know the object is an array.
33+
*
34+
* Looks only at the first element, if the dimensionality is
35+
* not consistent we won't figure that out here.
36+
*/
37+
function isArray1D(a) {
38+
return !isArrayOrTypedArray(a[0]);
39+
}
2440

25-
exports.isArrayOrTypedArray = function(a) {
26-
return Array.isArray(a) || exports.isTypedArray(a);
41+
module.exports = {
42+
isTypedArray: isTypedArray,
43+
isArrayOrTypedArray: isArrayOrTypedArray,
44+
isArray1D: isArray1D
2745
};

src/lib/keyed_container.js

+20-6
Original file line numberDiff line numberDiff line change
@@ -33,32 +33,44 @@ var UNSET = 4;
3333
module.exports = function keyedContainer(baseObj, path, keyName, valueName) {
3434
keyName = keyName || 'name';
3535
valueName = valueName || 'value';
36-
var i, arr;
36+
var i, arr, baseProp;
3737
var changeTypes = {};
3838

39-
if(path && path.length) { arr = nestedProperty(baseObj, path).get();
39+
if(path && path.length) {
40+
baseProp = nestedProperty(baseObj, path);
41+
arr = baseProp.get();
4042
} else {
4143
arr = baseObj;
4244
}
4345

4446
path = path || '';
45-
arr = arr || [];
4647

4748
// Construct an index:
4849
var indexLookup = {};
49-
for(i = 0; i < arr.length; i++) {
50-
indexLookup[arr[i][keyName]] = i;
50+
if(arr) {
51+
for(i = 0; i < arr.length; i++) {
52+
indexLookup[arr[i][keyName]] = i;
53+
}
5154
}
5255

5356
var isSimpleValueProp = SIMPLE_PROPERTY_REGEX.test(valueName);
5457

5558
var obj = {
56-
// NB: this does not actually modify the baseObj
5759
set: function(name, value) {
5860
var changeType = value === null ? UNSET : NONE;
5961

62+
// create the base array if necessary
63+
if(!arr) {
64+
if(!baseProp || changeType === UNSET) return;
65+
66+
arr = [];
67+
baseProp.set(arr);
68+
}
69+
6070
var idx = indexLookup[name];
6171
if(idx === undefined) {
72+
if(changeType === UNSET) return;
73+
6274
changeType = changeType | BOTH;
6375
idx = arr.length;
6476
indexLookup[name] = idx;
@@ -86,6 +98,8 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) {
8698
return obj;
8799
},
88100
get: function(name) {
101+
if(!arr) return;
102+
89103
var idx = indexLookup[name];
90104

91105
if(idx === undefined) {

src/lib/nested_property.js

+22-79
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
var isNumeric = require('fast-isnumeric');
1313
var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray;
14-
var isPlainObject = require('./is_plain_object');
15-
var containerArrayMatch = require('../plot_api/container_array_match');
1614

1715
/**
1816
* convert a string s (such as 'xaxis.range[0]')
@@ -115,44 +113,21 @@ function npGet(cont, parts) {
115113
}
116114

117115
/*
118-
* Can this value be deleted? We can delete any empty object (null, undefined, [], {})
119-
* EXCEPT empty data arrays, {} inside an array, or anything INSIDE an *args* array.
116+
* Can this value be deleted? We can delete `undefined`, and `null` except INSIDE an
117+
* *args* array.
120118
*
121-
* Info arrays can be safely deleted, but not deleting them has no ill effects other
122-
* than leaving a trace or layout object with some cruft in it.
119+
* Previously we also deleted some `{}` and `[]`, in order to try and make set/unset
120+
* a net noop; but this causes far more complication than it's worth, and still had
121+
* lots of exceptions. See https://github.com/plotly/plotly.js/issues/1410
123122
*
124-
* Deleting data arrays can change the meaning of the object, as `[]` means there is
125-
* data for this attribute, it's just empty right now while `undefined` means the data
126-
* should be filled in with defaults to match other data arrays.
127-
*
128-
* `{}` inside an array means "the default object" which is clearly different from
129-
* popping it off the end of the array, or setting it `undefined` inside the array.
130-
*
131-
* *args* arrays get passed directly to API methods and we should respect precisely
132-
* what the user has put there - although if the whole *args* array is empty it's fine
133-
* to delete that.
134-
*
135-
* So we do some simple tests here to find known non-data arrays but don't worry too
136-
* much about not deleting some arrays that would actually be safe to delete.
123+
* *args* arrays get passed directly to API methods and we should respect null if
124+
* the user put it there, but otherwise null is deleted as we use it as code
125+
* in restyle/relayout/update for "delete this value" whereas undefined means
126+
* "ignore this edit"
137127
*/
138-
var INFO_PATTERNS = /(^|\.)((domain|range)(\.[xy])?|args|parallels)$/;
139128
var ARGS_PATTERN = /(^|\.)args\[/;
140129
function isDeletable(val, propStr) {
141-
if(!emptyObj(val) ||
142-
(isPlainObject(val) && propStr.charAt(propStr.length - 1) === ']') ||
143-
(propStr.match(ARGS_PATTERN) && val !== undefined)
144-
) {
145-
return false;
146-
}
147-
if(!isArrayOrTypedArray(val)) return true;
148-
149-
if(propStr.match(INFO_PATTERNS)) return true;
150-
151-
var match = containerArrayMatch(propStr);
152-
// if propStr matches the container array itself, index is an empty string
153-
// otherwise we've matched something inside the container array, which may
154-
// still be a data array.
155-
return match && (match.index === '');
130+
return (val === undefined) || (val === null && !propStr.match(ARGS_PATTERN));
156131
}
157132

158133
function npSet(cont, parts, propStr) {
@@ -194,8 +169,18 @@ function npSet(cont, parts, propStr) {
194169
}
195170

196171
if(toDelete) {
197-
if(i === parts.length - 1) delete curCont[parts[i]];
198-
pruneContainers(containerLevels);
172+
if(i === parts.length - 1) {
173+
delete curCont[parts[i]];
174+
175+
// The one bit of pruning we still do: drop `undefined` from the end of arrays.
176+
// In case someone has already unset previous items, continue until we hit a
177+
// non-undefined value.
178+
if(Array.isArray(curCont) && +parts[i] === curCont.length - 1) {
179+
while(curCont.length && curCont[curCont.length - 1] === undefined) {
180+
curCont.pop();
181+
}
182+
}
183+
}
199184
}
200185
else curCont[parts[i]] = val;
201186
};
@@ -249,48 +234,6 @@ function checkNewContainer(container, part, nextPart, toDelete) {
249234
return true;
250235
}
251236

252-
function pruneContainers(containerLevels) {
253-
var i,
254-
j,
255-
curCont,
256-
propPart,
257-
keys,
258-
remainingKeys;
259-
for(i = containerLevels.length - 1; i >= 0; i--) {
260-
curCont = containerLevels[i][0];
261-
propPart = containerLevels[i][1];
262-
263-
remainingKeys = false;
264-
if(isArrayOrTypedArray(curCont)) {
265-
for(j = curCont.length - 1; j >= 0; j--) {
266-
if(isDeletable(curCont[j], joinPropStr(propPart, j))) {
267-
if(remainingKeys) curCont[j] = undefined;
268-
else curCont.pop();
269-
}
270-
else remainingKeys = true;
271-
}
272-
}
273-
else if(typeof curCont === 'object' && curCont !== null) {
274-
keys = Object.keys(curCont);
275-
remainingKeys = false;
276-
for(j = keys.length - 1; j >= 0; j--) {
277-
if(isDeletable(curCont[keys[j]], joinPropStr(propPart, keys[j]))) {
278-
delete curCont[keys[j]];
279-
}
280-
else remainingKeys = true;
281-
}
282-
}
283-
if(remainingKeys) return;
284-
}
285-
}
286-
287-
function emptyObj(obj) {
288-
if(obj === undefined || obj === null) return true;
289-
if(typeof obj !== 'object') return false; // any plain value
290-
if(isArrayOrTypedArray(obj)) return !obj.length; // []
291-
return !Object.keys(obj).length; // {}
292-
}
293-
294237
function badContainer(container, propStr, propParts) {
295238
return {
296239
set: function() { throw 'bad container'; },

src/plot_api/plot_api.js

+10-9
Original file line numberDiff line numberDiff line change
@@ -2267,7 +2267,10 @@ exports.react = function(gd, data, layout, config) {
22672267
gd.layout = layout || {};
22682268
helpers.cleanLayout(gd.layout);
22692269

2270-
Plots.supplyDefaults(gd);
2270+
// "true" skips updating calcdata and remapping arrays from calcTransforms,
2271+
// which supplyDefaults usually does at the end, but we may need to NOT do
2272+
// if the diff (which we haven't determined yet) says we'll recalc
2273+
Plots.supplyDefaults(gd, {skipUpdateCalc: true});
22712274

22722275
var newFullData = gd._fullData;
22732276
var newFullLayout = gd._fullLayout;
@@ -2289,6 +2292,9 @@ exports.react = function(gd, data, layout, config) {
22892292

22902293
// clear calcdata if required
22912294
if(restyleFlags.calc || relayoutFlags.calc) gd.calcdata = undefined;
2295+
// otherwise do the calcdata updates and calcTransform array remaps that we skipped earlier
2296+
else Plots.supplyDefaultsUpdateCalc(gd.calcdata, newFullData);
2297+
22922298
if(relayoutFlags.margins) helpers.clearAxisAutomargins(gd);
22932299

22942300
// Note: what restyle/relayout use impliedEdits and clearAxisTypes for
@@ -2397,7 +2403,7 @@ function diffData(gd, oldFullData, newFullData, immutable) {
23972403
Axes.getFromId(gd, trace.xaxis).autorange ||
23982404
Axes.getFromId(gd, trace.yaxis).autorange
23992405
) : false;
2400-
getDiffFlags(oldFullData[i], trace, [], diffOpts);
2406+
getDiffFlags(oldFullData[i]._fullInput, trace, [], diffOpts);
24012407
}
24022408

24032409
if(flags.calc || flags.plot || flags.calcIfAutorange) {
@@ -2461,13 +2467,6 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
24612467
return valObject.valType === 'data_array' || valObject.arrayOk;
24622468
}
24632469

2464-
// for transforms: look at _fullInput rather than the transform result, which often
2465-
// contains generated arrays.
2466-
var newFullInput = newContainer._fullInput;
2467-
var oldFullInput = oldContainer._fullInput;
2468-
if(newFullInput && newFullInput !== newContainer) newContainer = newFullInput;
2469-
if(oldFullInput && oldFullInput !== oldContainer) oldContainer = oldFullInput;
2470-
24712470
for(key in oldContainer) {
24722471
// short-circuit based on previous calls or previous keys that already maximized the pathway
24732472
if(flags.calc) return;
@@ -2494,6 +2493,8 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
24942493
// in case type changed, we may not even *have* a valObject.
24952494
if(!valObject) continue;
24962495

2496+
if(valObject._compareAsJSON && JSON.stringify(oldVal) === JSON.stringify(newVal)) continue;
2497+
24972498
var valType = valObject.valType;
24982499
var i;
24992500

0 commit comments

Comments
 (0)