Skip to content

Commit c4efa80

Browse files
authored
Merge pull request #1041 from plotly/merge-container-arrays-2
Improved animation merging for layout and traces
2 parents 6b2c16a + e085a53 commit c4efa80

File tree

5 files changed

+270
-26
lines changed

5 files changed

+270
-26
lines changed

src/lib/index.js

+61-8
Original file line numberDiff line numberDiff line change
@@ -589,18 +589,36 @@ lib.objectFromPath = function(path, value) {
589589
/**
590590
* Iterate through an object in-place, converting dotted properties to objects.
591591
*
592-
* @example
593-
* lib.expandObjectPaths({'nested.test.path': 'value'});
594-
* // returns { nested: { test: {path: 'value'}}}
592+
* Examples:
593+
*
594+
* lib.expandObjectPaths({'nested.test.path': 'value'});
595+
* => { nested: { test: {path: 'value'}}}
596+
*
597+
* It also handles array notation, e.g.:
598+
*
599+
* lib.expandObjectPaths({'foo[1].bar': 'value'});
600+
* => { foo: [null, {bar: value}] }
601+
*
602+
* It handles merges the results when two properties are specified in parallel:
603+
*
604+
* lib.expandObjectPaths({'foo[1].bar': 10, 'foo[0].bar': 20});
605+
* => { foo: [{bar: 10}, {bar: 20}] }
606+
*
607+
* It does NOT, however, merge mulitple mutliply-nested arrays::
608+
*
609+
* lib.expandObjectPaths({'marker[1].range[1]': 5, 'marker[1].range[0]': 4})
610+
* => { marker: [null, {range: 4}] }
595611
*/
596612

597-
// Store this to avoid recompiling regex on every prop since this may happen many
598-
// many times for animations.
599-
// TODO: Premature optimization? Remove?
600-
var dottedPropertyRegex = /^([^\.]*)\../;
613+
// Store this to avoid recompiling regex on *every* prop since this may happen many
614+
// many times for animations. Could maybe be inside the function. Not sure about
615+
// scoping vs. recompilation tradeoff, but at least it's not just inlining it into
616+
// the inner loop.
617+
var dottedPropertyRegex = /^([^\[\.]+)\.(.+)?/;
618+
var indexedPropertyRegex = /^([^\.]+)\[([0-9]+)\](\.)?(.+)?/;
601619

602620
lib.expandObjectPaths = function(data) {
603-
var match, key, prop, datum;
621+
var match, key, prop, datum, idx, dest, trailingPath;
604622
if(typeof data === 'object' && !Array.isArray(data)) {
605623
for(key in data) {
606624
if(data.hasOwnProperty(key)) {
@@ -611,12 +629,47 @@ lib.expandObjectPaths = function(data) {
611629
delete data[key];
612630

613631
data[prop] = lib.extendDeepNoArrays(data[prop] || {}, lib.objectFromPath(key, lib.expandObjectPaths(datum))[prop]);
632+
} else if((match = key.match(indexedPropertyRegex))) {
633+
datum = data[key];
634+
635+
prop = match[1];
636+
idx = parseInt(match[2]);
637+
638+
delete data[key];
639+
640+
data[prop] = data[prop] || [];
641+
642+
if(match[3] === '.') {
643+
// This is the case where theere are subsequent properties into which
644+
// we must recurse, e.g. transforms[0].value
645+
trailingPath = match[4];
646+
dest = data[prop][idx] = data[prop][idx] || {};
647+
648+
// NB: Extend deep no arrays prevents this from working on multiple
649+
// nested properties in the same object, e.g.
650+
//
651+
// {
652+
// foo[0].bar[1].range
653+
// foo[0].bar[0].range
654+
// }
655+
//
656+
// In this case, the extendDeepNoArrays will overwrite one array with
657+
// the other, so that both properties *will not* be present in the
658+
// result. Fixing this would require a more intelligent tracking
659+
// of changes and merging than extendDeepNoArrays currently accomplishes.
660+
lib.extendDeepNoArrays(dest, lib.objectFromPath(trailingPath, lib.expandObjectPaths(datum)));
661+
} else {
662+
// This is the case where this property is the end of the line,
663+
// e.g. xaxis.range[0]
664+
data[prop][idx] = lib.expandObjectPaths(datum);
665+
}
614666
} else {
615667
data[key] = lib.expandObjectPaths(data[key]);
616668
}
617669
}
618670
}
619671
}
672+
620673
return data;
621674
};
622675

src/plots/plots.js

+89-17
Original file line numberDiff line numberDiff line change
@@ -1299,7 +1299,7 @@ plots.modifyFrames = function(gd, operations) {
12991299
*/
13001300
plots.computeFrame = function(gd, frameName) {
13011301
var frameLookup = gd._transitionData._frameHash;
1302-
var i, traceIndices, traceIndex, expandedObj, destIndex, copy;
1302+
var i, traceIndices, traceIndex, destIndex;
13031303

13041304
var framePtr = frameLookup[frameName];
13051305

@@ -1326,9 +1326,7 @@ plots.computeFrame = function(gd, frameName) {
13261326
// Merge, starting with the last and ending with the desired frame:
13271327
while((framePtr = frameStack.pop())) {
13281328
if(framePtr.layout) {
1329-
copy = Lib.extendDeepNoArrays({}, framePtr.layout);
1330-
expandedObj = Lib.expandObjectPaths(copy);
1331-
result.layout = Lib.extendDeepNoArrays(result.layout || {}, expandedObj);
1329+
result.layout = plots.extendLayout(result.layout, framePtr.layout);
13321330
}
13331331

13341332
if(framePtr.data) {
@@ -1363,16 +1361,99 @@ plots.computeFrame = function(gd, frameName) {
13631361
result.traces[destIndex] = traceIndex;
13641362
}
13651363

1366-
copy = Lib.extendDeepNoArrays({}, framePtr.data[i]);
1367-
expandedObj = Lib.expandObjectPaths(copy);
1368-
result.data[destIndex] = Lib.extendDeepNoArrays(result.data[destIndex] || {}, expandedObj);
1364+
result.data[destIndex] = plots.extendTrace(result.data[destIndex], framePtr.data[i]);
13691365
}
13701366
}
13711367
}
13721368

13731369
return result;
13741370
};
13751371

1372+
/**
1373+
* Extend an object, treating container arrays very differently by extracting
1374+
* their contents and merging them separately.
1375+
*
1376+
* This exists so that we can extendDeepNoArrays and avoid stepping into data
1377+
* arrays without knowledge of the plot schema, but so that we may also manually
1378+
* recurse into known container arrays, such as transforms.
1379+
*
1380+
* See extendTrace and extendLayout below for usage.
1381+
*/
1382+
plots.extendObjectWithContainers = function(dest, src, containerPaths) {
1383+
var containerProp, containerVal, i, j, srcProp, destProp, srcContainer, destContainer;
1384+
var copy = Lib.extendDeepNoArrays({}, src || {});
1385+
var expandedObj = Lib.expandObjectPaths(copy);
1386+
var containerObj = {};
1387+
1388+
// Step through and extract any container properties. Otherwise extendDeepNoArrays
1389+
// will clobber any existing properties with an empty array and then supplyDefaults
1390+
// will reset everything to defaults.
1391+
if(containerPaths && containerPaths.length) {
1392+
for(i = 0; i < containerPaths.length; i++) {
1393+
containerProp = Lib.nestedProperty(expandedObj, containerPaths[i]);
1394+
containerVal = containerProp.get();
1395+
containerProp.set(null);
1396+
Lib.nestedProperty(containerObj, containerPaths[i]).set(containerVal);
1397+
}
1398+
}
1399+
1400+
dest = Lib.extendDeepNoArrays(dest || {}, expandedObj);
1401+
1402+
if(containerPaths && containerPaths.length) {
1403+
for(i = 0; i < containerPaths.length; i++) {
1404+
srcProp = Lib.nestedProperty(containerObj, containerPaths[i]);
1405+
srcContainer = srcProp.get();
1406+
1407+
if(!srcContainer) continue;
1408+
1409+
destProp = Lib.nestedProperty(dest, containerPaths[i]);
1410+
1411+
destContainer = destProp.get();
1412+
if(!Array.isArray(destContainer)) {
1413+
destContainer = [];
1414+
destProp.set(destContainer);
1415+
}
1416+
1417+
for(j = 0; j < srcContainer.length; j++) {
1418+
destContainer[j] = plots.extendObjectWithContainers(destContainer[j], srcContainer[j]);
1419+
}
1420+
}
1421+
}
1422+
1423+
return dest;
1424+
};
1425+
1426+
/*
1427+
* Extend a trace definition. This method:
1428+
*
1429+
* 1. directly transfers any array references
1430+
* 2. manually recurses into container arrays like transforms
1431+
*
1432+
* The result is the original object reference with the new contents merged in.
1433+
*/
1434+
plots.extendTrace = function(destTrace, srcTrace) {
1435+
return plots.extendObjectWithContainers(destTrace, srcTrace, ['transforms']);
1436+
};
1437+
1438+
/*
1439+
* Extend a layout definition. This method:
1440+
*
1441+
* 1. directly transfers any array references (not critically important for
1442+
* layout since there aren't really data arrays)
1443+
* 2. manually recurses into container arrays like annotations
1444+
*
1445+
* The result is the original object reference with the new contents merged in.
1446+
*/
1447+
plots.extendLayout = function(destLayout, srcLayout) {
1448+
return plots.extendObjectWithContainers(destLayout, srcLayout, [
1449+
'annotations',
1450+
'shapes',
1451+
'images',
1452+
'sliders',
1453+
'updatemenus'
1454+
]);
1455+
};
1456+
13761457
/**
13771458
* Transition to a set of new data and layout properties
13781459
*
@@ -1411,16 +1492,7 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts)
14111492

14121493
transitionedTraces.push(traceIdx);
14131494

1414-
// This is a multi-step process. First clone w/o arrays so that
1415-
// we're not modifying the original:
1416-
var update = Lib.extendDeepNoArrays({}, data[i]);
1417-
1418-
// Then expand object paths since we don't obey object-overwrite
1419-
// semantics here:
1420-
update = Lib.expandObjectPaths(update);
1421-
1422-
// Finally apply the update (without copying arrays, of course):
1423-
Lib.extendDeepNoArrays(gd.data[traceIndices[i]], update);
1495+
gd.data[traceIndices[i]] = plots.extendTrace(gd.data[traceIndices[i]], data[i]);
14241496
}
14251497

14261498
// Follow the same procedure. Clone it so we don't mangle the input, then

test/jasmine/tests/animate_test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('Test animate API', function() {
8484
});
8585

8686
Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() {
87-
Plotly.addFrames(gd, mockCopy.frames);
87+
return Plotly.addFrames(gd, mockCopy.frames);
8888
}).then(done);
8989
});
9090

test/jasmine/tests/lib_test.js

+84
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,90 @@ describe('Test lib.js:', function() {
537537
expect(input).toBe(expanded);
538538
expect(origArray).toBe(newArray);
539539
});
540+
541+
it('expands bracketed array notation', function() {
542+
var input = {'marker[1]': {color: 'red'}};
543+
var expected = {marker: [undefined, {color: 'red'}]};
544+
expect(Lib.expandObjectPaths(input)).toEqual(expected);
545+
});
546+
547+
it('expands nested arrays', function() {
548+
var input = {'marker[1].range[1]': 5};
549+
var expected = {marker: [undefined, {range: [undefined, 5]}]};
550+
var computed = Lib.expandObjectPaths(input);
551+
expect(computed).toEqual(expected);
552+
});
553+
554+
it('expands bracketed array with more nested attributes', function() {
555+
var input = {'marker[1]': {'color.alpha': 2}};
556+
var expected = {marker: [undefined, {color: {alpha: 2}}]};
557+
var computed = Lib.expandObjectPaths(input);
558+
expect(computed).toEqual(expected);
559+
});
560+
561+
it('expands bracketed array notation without further nesting', function() {
562+
var input = {'marker[1]': 8};
563+
var expected = {marker: [undefined, 8]};
564+
var computed = Lib.expandObjectPaths(input);
565+
expect(computed).toEqual(expected);
566+
});
567+
568+
it('expands bracketed array notation with further nesting', function() {
569+
var input = {'marker[1].size': 8};
570+
var expected = {marker: [undefined, {size: 8}]};
571+
var computed = Lib.expandObjectPaths(input);
572+
expect(computed).toEqual(expected);
573+
});
574+
575+
it('expands bracketed array notation with further nesting', function() {
576+
var input = {'marker[1].size.magnitude': 8};
577+
var expected = {marker: [undefined, {size: {magnitude: 8}}]};
578+
var computed = Lib.expandObjectPaths(input);
579+
expect(computed).toEqual(expected);
580+
});
581+
582+
it('combines changes with single array nesting', function() {
583+
var input = {'marker[1].foo': 5, 'marker[0].foo': 4};
584+
var expected = {marker: [{foo: 4}, {foo: 5}]};
585+
var computed = Lib.expandObjectPaths(input);
586+
expect(computed).toEqual(expected);
587+
});
588+
589+
// TODO: This test is unimplemented since it's a currently-unused corner case.
590+
// Getting the test to pass requires some extension (pun?) to extendDeepNoArrays
591+
// that's intelligent enough to only selectively merge *some* arrays, in particular
592+
// not data arrays but yes on arrays that were previously expanded. This is a bit
593+
// tricky to get to work just right and currently doesn't have any known use since
594+
// container arrays are not multiply nested.
595+
//
596+
// Additional notes on what works or what doesn't work. This case does *not* work
597+
// because the two nested arrays that would result from the expansion need to be
598+
// deep merged.
599+
//
600+
// Lib.expandObjectPaths({'marker.range[0]': 5, 'marker.range[1]': 2})
601+
//
602+
// // => {marker: {range: [null, 2]}}
603+
//
604+
// This case *does* work becuase the array merging does not require a deep extend:
605+
//
606+
// Lib.expandObjectPaths({'range[0]': 5, 'range[1]': 2}
607+
//
608+
// // => {range: [5, 2]}
609+
//
610+
// Finally note that this case works fine becuase there's no merge necessary:
611+
//
612+
// Lib.expandObjectPaths({'marker.range[1]': 2})
613+
//
614+
// // => {marker: {range: [null, 2]}}
615+
//
616+
/*
617+
it('combines changes', function() {
618+
var input = {'marker[1].range[1]': 5, 'marker[1].range[0]': 4};
619+
var expected = {marker: [undefined, {range: [4, 5]}]};
620+
var computed = Lib.expandObjectPaths(input);
621+
expect(computed).toEqual(expected);
622+
});
623+
*/
540624
});
541625

542626
describe('coerce', function() {

test/jasmine/tests/transition_test.js

+35
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,41 @@ function runTests(transitionDuration) {
6565
}).catch(fail).then(done);
6666
});
6767

68+
it('transitions a transform', function(done) {
69+
Plotly.restyle(gd, {
70+
'transforms[0]': {
71+
enabled: true,
72+
type: 'filter',
73+
operation: '<',
74+
filtersrc: 'x',
75+
value: 10
76+
}
77+
}, [0]).then(function() {
78+
expect(gd._fullData[0].transforms).toEqual([{
79+
enabled: true,
80+
type: 'filter',
81+
operation: '<',
82+
filtersrc: 'x',
83+
value: 10
84+
}]);
85+
86+
return Plots.transition(gd, [{
87+
'transforms[0].operation': '>'
88+
}], null, [0],
89+
{redraw: true, duration: transitionDuration},
90+
{duration: transitionDuration, easing: 'cubic-in-out'}
91+
);
92+
}).then(function() {
93+
expect(gd._fullData[0].transforms).toEqual([{
94+
enabled: true,
95+
type: 'filter',
96+
operation: '>',
97+
filtersrc: 'x',
98+
value: 10
99+
}]);
100+
}).catch(fail).then(done);
101+
});
102+
68103
// This doesn't really test anything that the above tests don't cover, but it combines
69104
// the behavior and attempts to ensure chaining and events happen in the correct order.
70105
it('transitions may be chained', function(done) {

0 commit comments

Comments
 (0)