Skip to content

Commit e84d4b9

Browse files
committed
close #1410 - yes, stop pruning in nestedProperty
1 parent cf4c9c3 commit e84d4b9

File tree

4 files changed

+45
-93
lines changed

4 files changed

+45
-93
lines changed

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'; },

test/jasmine/tests/legend_test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -973,8 +973,8 @@ describe('legend interaction', function() {
973973
}).then(function() {
974974
// Verify the group names have been cleaned up:
975975
expect(gd.data[1].transforms[0].styles).toEqual([
976-
{target: 3},
977-
{target: 4}
976+
{target: 3, value: {}},
977+
{target: 4, value: {}}
978978
]);
979979
}).catch(fail).then(done);
980980
});

test/jasmine/tests/lib_test.js

+15-9
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ describe('Test lib.js:', function() {
328328
expect(obj).toEqual({a: false, b: '', c: 0, d: NaN});
329329
});
330330

331-
it('should not remove data arrays or empty objects inside container arrays', function() {
331+
it('should not remove arrays or empty objects inside container arrays', function() {
332332
var obj = {
333333
annotations: [{a: [1, 2, 3]}],
334334
c: [1, 2, 3],
@@ -351,11 +351,17 @@ describe('Test lib.js:', function() {
351351
propS.set(null);
352352

353353
// 'a' and 'c' are both potentially data arrays so we need to keep them
354-
expect(obj).toEqual({annotations: [{a: []}], c: []});
354+
expect(obj).toEqual({
355+
annotations: [{a: []}],
356+
c: [],
357+
domain: [],
358+
range: [],
359+
shapes: []
360+
});
355361
});
356362

357363

358-
it('should allow empty object sub-containers only in arrays', function() {
364+
it('should allow empty object sub-containers', function() {
359365
var obj = {},
360366
prop = np(obj, 'a[1].b.c'),
361367
// we never set a value into a[0] so it doesn't even get {}
@@ -370,15 +376,15 @@ describe('Test lib.js:', function() {
370376

371377
prop.set(null);
372378
expect(prop.get()).toBe(undefined);
373-
expect(obj).toEqual({a: [undefined, {}]});
379+
expect(obj).toEqual({a: [undefined, {b: {}}]});
374380
});
375381

376382
it('does not prune inside `args` arrays', function() {
377383
var obj = {},
378384
args = np(obj, 'args');
379385

380386
args.set([]);
381-
expect(obj.args).toBeUndefined();
387+
expect(obj.args).toEqual([]);
382388

383389
args.set([null]);
384390
expect(obj.args).toEqual([null]);
@@ -1950,7 +1956,7 @@ describe('Test lib.js:', function() {
19501956

19511957
expect(container).toEqual({styles: [
19521958
{foo: 'name4', bar: {value: 'value1'}},
1953-
{foo: 'name2'},
1959+
{foo: 'name2', bar: {}},
19541960
{foo: 'name3', bar: {value: 'value3'}}
19551961
]});
19561962

@@ -1971,7 +1977,7 @@ describe('Test lib.js:', function() {
19711977

19721978
carr.remove('name');
19731979

1974-
expect(container.styles).toEqual([{foo: 'name', extra: 'data'}]);
1980+
expect(container.styles).toEqual([{foo: 'name', bar: {}, extra: 'data'}]);
19751981

19761982
expect(carr.constructUpdate()).toEqual({
19771983
'styles[0].bar.value': null,
@@ -2006,7 +2012,7 @@ describe('Test lib.js:', function() {
20062012

20072013
expect(container.styles).toEqual([
20082014
{foo: 'name1', bar: {extra: 'data'}},
2009-
{foo: 'name2'},
2015+
{foo: 'name2', bar: {}},
20102016
{foo: 'name3', bar: {value: 'value3', extra: 'data'}},
20112017
]);
20122018

@@ -2029,7 +2035,7 @@ describe('Test lib.js:', function() {
20292035
carr.remove('name1');
20302036

20312037
expect(container.styles).toEqual([
2032-
{foo: 'name1'},
2038+
{foo: 'name1', bar: {}},
20332039
{foo: 'name2', bar: {value: 'value2', extra: 'data2'}},
20342040
]);
20352041

test/jasmine/tests/sliders_test.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,20 @@ describe('sliders defaults', function() {
100100
method: 'relayout',
101101
label: 'Label #1',
102102
value: 'label-1',
103-
execute: true
103+
execute: true,
104+
args: []
104105
}, {
105106
method: 'update',
106107
label: 'Label #2',
107108
value: 'Label #2',
108-
execute: true
109+
execute: true,
110+
args: []
109111
}, {
110112
method: 'animate',
111113
label: 'step-2',
112114
value: 'lacks-label',
113-
execute: true
115+
execute: true,
116+
args: []
114117
}]);
115118
});
116119

0 commit comments

Comments
 (0)