Skip to content

Commit c34d299

Browse files
authored
Merge pull request #3217 from plotly/transitions-in-react
Transitions with Plotly.react
2 parents e4b1a7e + ec4d8b9 commit c34d299

18 files changed

+1243
-308
lines changed

src/components/colorscale/attributes.js

+6
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ function code(s) {
5454
* most of these attributes already require a recalc, but the ones that do not
5555
* have editType *style* or *plot* unless you override (presumably with *calc*)
5656
*
57+
* - anim {boolean) (dflt: undefined): is 'color' animatable?
58+
*
5759
* @return {object}
5860
*/
5961
module.exports = function colorScaleAttrs(context, opts) {
@@ -109,6 +111,10 @@ module.exports = function colorScaleAttrs(context, opts) {
109111
' ' + minmaxFull + ' if set.'
110112
].join('')
111113
};
114+
115+
if(opts.anim) {
116+
attrs.color.anim = true;
117+
}
112118
}
113119

114120
attrs[auto] = {

src/components/images/defaults.js

+5
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ function imageDefaults(imageIn, imageOut, fullLayout) {
5252
var axLetter = axLetters[i];
5353
var axRef = Axes.coerceRef(imageIn, imageOut, gdMock, axLetter, 'paper');
5454

55+
if(axRef !== 'paper') {
56+
var ax = Axes.getFromId(gdMock, axRef);
57+
ax._imgIndices.push(imageOut._index);
58+
}
59+
5560
Axes.coercePosition(imageOut, gdMock, coerce, axRef, axLetter, 0);
5661
}
5762

src/plot_api/plot_api.js

+74-15
Original file line numberDiff line numberDiff line change
@@ -2738,9 +2738,11 @@ exports.react = function(gd, data, layout, config) {
27382738
var newFullData = gd._fullData;
27392739
var newFullLayout = gd._fullLayout;
27402740
var immutable = newFullLayout.datarevision === undefined;
2741+
var transition = newFullLayout.transition;
27412742

2742-
var restyleFlags = diffData(gd, oldFullData, newFullData, immutable);
2743-
var relayoutFlags = diffLayout(gd, oldFullLayout, newFullLayout, immutable);
2743+
var relayoutFlags = diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition);
2744+
var newDataRevision = relayoutFlags.newDataRevision;
2745+
var restyleFlags = diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision);
27442746

27452747
// TODO: how to translate this part of relayout to Plotly.react?
27462748
// // Setting width or height to null must reset the graph's width / height
@@ -2770,7 +2772,19 @@ exports.react = function(gd, data, layout, config) {
27702772
seq.push(addFrames);
27712773
}
27722774

2773-
if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) {
2775+
// Transition pathway,
2776+
// only used when 'transition' is set by user and
2777+
// when at least one animatable attribute has changed,
2778+
// N.B. config changed aren't animatable
2779+
if(newFullLayout.transition && !configChanged && (restyleFlags.anim || relayoutFlags.anim)) {
2780+
Plots.doCalcdata(gd);
2781+
subroutines.doAutoRangeAndConstraints(gd);
2782+
2783+
seq.push(function() {
2784+
return Plots.transitionFromReact(gd, restyleFlags, relayoutFlags, oldFullLayout);
2785+
});
2786+
}
2787+
else if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) {
27742788
gd._fullLayout._skipDefaults = true;
27752789
seq.push(exports.plot);
27762790
}
@@ -2823,8 +2837,10 @@ exports.react = function(gd, data, layout, config) {
28232837

28242838
};
28252839

2826-
function diffData(gd, oldFullData, newFullData, immutable) {
2827-
if(oldFullData.length !== newFullData.length) {
2840+
function diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision) {
2841+
var sameTraceLength = oldFullData.length === newFullData.length;
2842+
2843+
if(!transition && !sameTraceLength) {
28282844
return {
28292845
fullReplot: true,
28302846
calc: true
@@ -2833,6 +2849,9 @@ function diffData(gd, oldFullData, newFullData, immutable) {
28332849

28342850
var flags = editTypes.traceFlags();
28352851
flags.arrays = {};
2852+
flags.nChanges = 0;
2853+
flags.nChangesAnim = 0;
2854+
28362855
var i, trace;
28372856

28382857
function getTraceValObject(parts) {
@@ -2843,31 +2862,41 @@ function diffData(gd, oldFullData, newFullData, immutable) {
28432862
getValObject: getTraceValObject,
28442863
flags: flags,
28452864
immutable: immutable,
2865+
transition: transition,
2866+
newDataRevision: newDataRevision,
28462867
gd: gd
28472868
};
28482869

2849-
28502870
var seenUIDs = {};
28512871

28522872
for(i = 0; i < oldFullData.length; i++) {
2853-
trace = newFullData[i]._fullInput;
2854-
if(Plots.hasMakesDataTransform(trace)) trace = newFullData[i];
2855-
if(seenUIDs[trace.uid]) continue;
2856-
seenUIDs[trace.uid] = 1;
2873+
if(newFullData[i]) {
2874+
trace = newFullData[i]._fullInput;
2875+
if(Plots.hasMakesDataTransform(trace)) trace = newFullData[i];
2876+
if(seenUIDs[trace.uid]) continue;
2877+
seenUIDs[trace.uid] = 1;
28572878

2858-
getDiffFlags(oldFullData[i]._fullInput, trace, [], diffOpts);
2879+
getDiffFlags(oldFullData[i]._fullInput, trace, [], diffOpts);
2880+
}
28592881
}
28602882

28612883
if(flags.calc || flags.plot) {
28622884
flags.fullReplot = true;
28632885
}
28642886

2887+
if(transition && flags.nChanges && flags.nChangesAnim) {
2888+
flags.anim = (flags.nChanges === flags.nChangesAnim) && sameTraceLength ? 'all' : 'some';
2889+
}
2890+
28652891
return flags;
28662892
}
28672893

2868-
function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
2894+
function diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition) {
28692895
var flags = editTypes.layoutFlags();
28702896
flags.arrays = {};
2897+
flags.rangesAltered = {};
2898+
flags.nChanges = 0;
2899+
flags.nChangesAnim = 0;
28712900

28722901
function getLayoutValObject(parts) {
28732902
return PlotSchema.getLayoutValObject(newFullLayout, parts);
@@ -2877,6 +2906,7 @@ function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
28772906
getValObject: getLayoutValObject,
28782907
flags: flags,
28792908
immutable: immutable,
2909+
transition: transition,
28802910
gd: gd
28812911
};
28822912

@@ -2886,11 +2916,15 @@ function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
28862916
flags.layoutReplot = true;
28872917
}
28882918

2919+
if(transition && flags.nChanges && flags.nChangesAnim) {
2920+
flags.anim = flags.nChanges === flags.nChangesAnim ? 'all' : 'some';
2921+
}
2922+
28892923
return flags;
28902924
}
28912925

28922926
function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
2893-
var valObject, key;
2927+
var valObject, key, astr;
28942928

28952929
var getValObject = opts.getValObject;
28962930
var flags = opts.flags;
@@ -2905,6 +2939,25 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
29052939
return;
29062940
}
29072941
editTypes.update(flags, valObject);
2942+
2943+
if(editType !== 'none') {
2944+
flags.nChanges++;
2945+
}
2946+
2947+
// track animatable changes
2948+
if(opts.transition && valObject.anim) {
2949+
flags.nChangesAnim++;
2950+
}
2951+
2952+
// track cartesian axes with altered ranges
2953+
if(AX_RANGE_RE.test(astr) || AX_AUTORANGE_RE.test(astr)) {
2954+
flags.rangesAltered[outerparts[0]] = 1;
2955+
}
2956+
2957+
// track datarevision changes
2958+
if(key === 'datarevision') {
2959+
flags.newDataRevision = 1;
2960+
}
29082961
}
29092962

29102963
function valObjectCanBeDataArray(valObject) {
@@ -2913,10 +2966,12 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
29132966

29142967
for(key in oldContainer) {
29152968
// short-circuit based on previous calls or previous keys that already maximized the pathway
2916-
if(flags.calc) return;
2969+
if(flags.calc && !opts.transition) return;
29172970

29182971
var oldVal = oldContainer[key];
29192972
var newVal = newContainer[key];
2973+
var parts = outerparts.concat(key);
2974+
astr = parts.join('.');
29202975

29212976
if(key.charAt(0) === '_' || typeof oldVal === 'function' || oldVal === newVal) continue;
29222977

@@ -2932,7 +2987,6 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
29322987
if(key === 'range' && newContainer.autorange) continue;
29332988
if((key === 'zmin' || key === 'zmax') && newContainer.type === 'contourcarpet') continue;
29342989

2935-
var parts = outerparts.concat(key);
29362990
valObject = getValObject(parts);
29372991

29382992
// in case type changed, we may not even *have* a valObject.
@@ -3003,6 +3057,11 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
30033057
if(immutable) {
30043058
flags.calc = true;
30053059
}
3060+
3061+
// look for animatable attributes when the data changed
3062+
if(immutable || opts.newDataRevision) {
3063+
changed();
3064+
}
30063065
}
30073066
else if(wasArray !== nowArray) {
30083067
flags.calc = true;

src/plots/animation_attributes.js

+13
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ module.exports = {
6969
role: 'info',
7070
min: 0,
7171
dflt: 500,
72+
editType: 'none',
7273
description: [
7374
'The duration of the transition, in milliseconds. If equal to zero,',
7475
'updates are synchronous.'
@@ -116,7 +117,19 @@ module.exports = {
116117
'bounce-in-out'
117118
],
118119
role: 'info',
120+
editType: 'none',
119121
description: 'The easing function used for the transition'
120122
},
123+
ordering: {
124+
valType: 'enumerated',
125+
values: ['layout first', 'traces first'],
126+
dflt: 'layout first',
127+
role: 'info',
128+
editType: 'none',
129+
description: [
130+
'Determines whether the figure\'s layout or traces smoothly transitions',
131+
'during updates that make both traces and layout change.'
132+
].join(' ')
133+
}
121134
}
122135
};

src/plots/attributes.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,18 @@ module.exports = {
7474
uid: {
7575
valType: 'string',
7676
role: 'info',
77-
editType: 'plot'
77+
editType: 'plot',
78+
anim: true,
79+
description: [
80+
'Assign an id to this trace,',
81+
'Use this to provide object constancy between traces during animations',
82+
'and transitions.'
83+
].join(' ')
7884
},
7985
ids: {
8086
valType: 'data_array',
8187
editType: 'calc',
88+
anim: true,
8289
description: [
8390
'Assigns id labels to each datum.',
8491
'These ids for object constancy of data points during animation.',

src/plots/cartesian/axes.js

+33
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,39 @@ axes.cleanPosition = function(pos, gd, axRef) {
132132
return cleanPos(pos);
133133
};
134134

135+
axes.redrawComponents = function(gd, axIds) {
136+
axIds = axIds ? axIds : axes.listIds(gd);
137+
138+
var fullLayout = gd._fullLayout;
139+
140+
function _redrawOneComp(moduleName, methodName, stashName, shortCircuit) {
141+
var method = Registry.getComponentMethod(moduleName, methodName);
142+
var stash = {};
143+
144+
for(var i = 0; i < axIds.length; i++) {
145+
var ax = fullLayout[axes.id2name(axIds[i])];
146+
var indices = ax[stashName];
147+
148+
for(var j = 0; j < indices.length; j++) {
149+
var ind = indices[j];
150+
151+
if(!stash[ind]) {
152+
method(gd, ind);
153+
stash[ind] = 1;
154+
// once is enough for images (which doesn't use the `i` arg anyway)
155+
if(shortCircuit) return;
156+
}
157+
}
158+
}
159+
}
160+
161+
// annotations and shapes 'draw' method is slow,
162+
// use the finer-grained 'drawOne' method instead
163+
_redrawOneComp('annotations', 'drawOne', '_annIndices');
164+
_redrawOneComp('shapes', 'drawOne', '_shapeIndices');
165+
_redrawOneComp('images', 'draw', '_imgIndices', true);
166+
};
167+
135168
var getDataConversions = axes.getDataConversions = function(gd, trace, target, targetArray) {
136169
var ax;
137170

src/plots/cartesian/dragbox.js

+5-23
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
481481

482482
// viewbox redraw at first
483483
updateSubplots(scrollViewBox);
484-
ticksAndAnnotations(ns, ew);
484+
ticksAndAnnotations();
485485

486486
// then replot after a delay to make sure
487487
// no more scrolling is coming
@@ -513,7 +513,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
513513
if(xActive) dragAxList(xaxes, dx);
514514
if(yActive) dragAxList(yaxes, dy);
515515
updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]);
516-
ticksAndAnnotations(yActive, xActive);
516+
ticksAndAnnotations();
517517
return;
518518
}
519519

@@ -585,12 +585,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
585585
}
586586

587587
updateSubplots([x0, y0, pw - dx, ph - dy]);
588-
ticksAndAnnotations(yActive, xActive);
588+
ticksAndAnnotations();
589589
}
590590

591591
// Draw ticks and annotations (and other components) when ranges change.
592592
// Also records the ranges that have changed for use by update at the end.
593-
function ticksAndAnnotations(ns, ew) {
593+
function ticksAndAnnotations() {
594594
var activeAxIds = [];
595595
var i;
596596

@@ -618,25 +618,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
618618
updates[ax._name + '.range[1]'] = ax.range[1];
619619
}
620620

621-
function redrawObjs(objArray, method, shortCircuit) {
622-
for(i = 0; i < objArray.length; i++) {
623-
var obji = objArray[i];
624-
625-
if((ew && activeAxIds.indexOf(obji.xref) !== -1) ||
626-
(ns && activeAxIds.indexOf(obji.yref) !== -1)) {
627-
method(gd, i);
628-
// once is enough for images (which doesn't use the `i` arg anyway)
629-
if(shortCircuit) return;
630-
}
631-
}
632-
}
633-
634-
// annotations and shapes 'draw' method is slow,
635-
// use the finer-grained 'drawOne' method instead
636-
637-
redrawObjs(gd._fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne'));
638-
redrawObjs(gd._fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne'));
639-
redrawObjs(gd._fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true);
621+
Axes.redrawComponents(gd, activeAxIds);
640622
}
641623

642624
function doubleClick() {

src/plots/cartesian/layout_attributes.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,12 @@ module.exports = {
117117
valType: 'info_array',
118118
role: 'info',
119119
items: [
120-
{valType: 'any', editType: 'axrange', impliedEdits: {'^autorange': false}},
121-
{valType: 'any', editType: 'axrange', impliedEdits: {'^autorange': false}}
120+
{valType: 'any', editType: 'axrange', impliedEdits: {'^autorange': false}, anim: true},
121+
{valType: 'any', editType: 'axrange', impliedEdits: {'^autorange': false}, anim: true}
122122
],
123123
editType: 'axrange',
124124
impliedEdits: {'autorange': false},
125+
anim: true,
125126
description: [
126127
'Sets the range of this axis.',
127128
'If the axis `type` is *log*, then you must take the log of your',

src/plots/cartesian/layout_defaults.js

+1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
156156
axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; });
157157
axLayoutOut._annIndices = [];
158158
axLayoutOut._shapeIndices = [];
159+
axLayoutOut._imgIndices = [];
159160
axLayoutOut._subplotsWith = [];
160161
axLayoutOut._counterAxes = [];
161162

0 commit comments

Comments
 (0)