Skip to content

Commit

Permalink
Merge pull request #3217 from plotly/transitions-in-react
Browse files Browse the repository at this point in the history
Transitions with Plotly.react
  • Loading branch information
etpinard committed Jan 22, 2019
2 parents e4b1a7e + ec4d8b9 commit c34d299
Show file tree
Hide file tree
Showing 18 changed files with 1,243 additions and 308 deletions.
6 changes: 6 additions & 0 deletions src/components/colorscale/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ function code(s) {
* most of these attributes already require a recalc, but the ones that do not
* have editType *style* or *plot* unless you override (presumably with *calc*)
*
* - anim {boolean) (dflt: undefined): is 'color' animatable?
*
* @return {object}
*/
module.exports = function colorScaleAttrs(context, opts) {
Expand Down Expand Up @@ -109,6 +111,10 @@ module.exports = function colorScaleAttrs(context, opts) {
' ' + minmaxFull + ' if set.'
].join('')
};

if(opts.anim) {
attrs.color.anim = true;
}
}

attrs[auto] = {
Expand Down
5 changes: 5 additions & 0 deletions src/components/images/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ function imageDefaults(imageIn, imageOut, fullLayout) {
var axLetter = axLetters[i];
var axRef = Axes.coerceRef(imageIn, imageOut, gdMock, axLetter, 'paper');

if(axRef !== 'paper') {
var ax = Axes.getFromId(gdMock, axRef);
ax._imgIndices.push(imageOut._index);
}

Axes.coercePosition(imageOut, gdMock, coerce, axRef, axLetter, 0);
}

Expand Down
89 changes: 74 additions & 15 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2738,9 +2738,11 @@ exports.react = function(gd, data, layout, config) {
var newFullData = gd._fullData;
var newFullLayout = gd._fullLayout;
var immutable = newFullLayout.datarevision === undefined;
var transition = newFullLayout.transition;

var restyleFlags = diffData(gd, oldFullData, newFullData, immutable);
var relayoutFlags = diffLayout(gd, oldFullLayout, newFullLayout, immutable);
var relayoutFlags = diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition);
var newDataRevision = relayoutFlags.newDataRevision;
var restyleFlags = diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision);

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

if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) {
// Transition pathway,
// only used when 'transition' is set by user and
// when at least one animatable attribute has changed,
// N.B. config changed aren't animatable
if(newFullLayout.transition && !configChanged && (restyleFlags.anim || relayoutFlags.anim)) {
Plots.doCalcdata(gd);
subroutines.doAutoRangeAndConstraints(gd);

seq.push(function() {
return Plots.transitionFromReact(gd, restyleFlags, relayoutFlags, oldFullLayout);
});
}
else if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) {
gd._fullLayout._skipDefaults = true;
seq.push(exports.plot);
}
Expand Down Expand Up @@ -2823,8 +2837,10 @@ exports.react = function(gd, data, layout, config) {

};

function diffData(gd, oldFullData, newFullData, immutable) {
if(oldFullData.length !== newFullData.length) {
function diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision) {
var sameTraceLength = oldFullData.length === newFullData.length;

if(!transition && !sameTraceLength) {
return {
fullReplot: true,
calc: true
Expand All @@ -2833,6 +2849,9 @@ function diffData(gd, oldFullData, newFullData, immutable) {

var flags = editTypes.traceFlags();
flags.arrays = {};
flags.nChanges = 0;
flags.nChangesAnim = 0;

var i, trace;

function getTraceValObject(parts) {
Expand All @@ -2843,31 +2862,41 @@ function diffData(gd, oldFullData, newFullData, immutable) {
getValObject: getTraceValObject,
flags: flags,
immutable: immutable,
transition: transition,
newDataRevision: newDataRevision,
gd: gd
};


var seenUIDs = {};

for(i = 0; i < oldFullData.length; i++) {
trace = newFullData[i]._fullInput;
if(Plots.hasMakesDataTransform(trace)) trace = newFullData[i];
if(seenUIDs[trace.uid]) continue;
seenUIDs[trace.uid] = 1;
if(newFullData[i]) {
trace = newFullData[i]._fullInput;
if(Plots.hasMakesDataTransform(trace)) trace = newFullData[i];
if(seenUIDs[trace.uid]) continue;
seenUIDs[trace.uid] = 1;

getDiffFlags(oldFullData[i]._fullInput, trace, [], diffOpts);
getDiffFlags(oldFullData[i]._fullInput, trace, [], diffOpts);
}
}

if(flags.calc || flags.plot) {
flags.fullReplot = true;
}

if(transition && flags.nChanges && flags.nChangesAnim) {
flags.anim = (flags.nChanges === flags.nChangesAnim) && sameTraceLength ? 'all' : 'some';
}

return flags;
}

function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
function diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition) {
var flags = editTypes.layoutFlags();
flags.arrays = {};
flags.rangesAltered = {};
flags.nChanges = 0;
flags.nChangesAnim = 0;

function getLayoutValObject(parts) {
return PlotSchema.getLayoutValObject(newFullLayout, parts);
Expand All @@ -2877,6 +2906,7 @@ function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
getValObject: getLayoutValObject,
flags: flags,
immutable: immutable,
transition: transition,
gd: gd
};

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

if(transition && flags.nChanges && flags.nChangesAnim) {
flags.anim = flags.nChanges === flags.nChangesAnim ? 'all' : 'some';
}

return flags;
}

function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
var valObject, key;
var valObject, key, astr;

var getValObject = opts.getValObject;
var flags = opts.flags;
Expand All @@ -2905,6 +2939,25 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
return;
}
editTypes.update(flags, valObject);

if(editType !== 'none') {
flags.nChanges++;
}

// track animatable changes
if(opts.transition && valObject.anim) {
flags.nChangesAnim++;
}

// track cartesian axes with altered ranges
if(AX_RANGE_RE.test(astr) || AX_AUTORANGE_RE.test(astr)) {
flags.rangesAltered[outerparts[0]] = 1;
}

// track datarevision changes
if(key === 'datarevision') {
flags.newDataRevision = 1;
}
}

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

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

var oldVal = oldContainer[key];
var newVal = newContainer[key];
var parts = outerparts.concat(key);
astr = parts.join('.');

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

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

var parts = outerparts.concat(key);
valObject = getValObject(parts);

// in case type changed, we may not even *have* a valObject.
Expand Down Expand Up @@ -3003,6 +3057,11 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
if(immutable) {
flags.calc = true;
}

// look for animatable attributes when the data changed
if(immutable || opts.newDataRevision) {
changed();
}
}
else if(wasArray !== nowArray) {
flags.calc = true;
Expand Down
13 changes: 13 additions & 0 deletions src/plots/animation_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ module.exports = {
role: 'info',
min: 0,
dflt: 500,
editType: 'none',
description: [
'The duration of the transition, in milliseconds. If equal to zero,',
'updates are synchronous.'
Expand Down Expand Up @@ -116,7 +117,19 @@ module.exports = {
'bounce-in-out'
],
role: 'info',
editType: 'none',
description: 'The easing function used for the transition'
},
ordering: {
valType: 'enumerated',
values: ['layout first', 'traces first'],
dflt: 'layout first',
role: 'info',
editType: 'none',
description: [
'Determines whether the figure\'s layout or traces smoothly transitions',
'during updates that make both traces and layout change.'
].join(' ')
}
}
};
9 changes: 8 additions & 1 deletion src/plots/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,18 @@ module.exports = {
uid: {
valType: 'string',
role: 'info',
editType: 'plot'
editType: 'plot',
anim: true,
description: [
'Assign an id to this trace,',
'Use this to provide object constancy between traces during animations',
'and transitions.'
].join(' ')
},
ids: {
valType: 'data_array',
editType: 'calc',
anim: true,
description: [
'Assigns id labels to each datum.',
'These ids for object constancy of data points during animation.',
Expand Down
33 changes: 33 additions & 0 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,39 @@ axes.cleanPosition = function(pos, gd, axRef) {
return cleanPos(pos);
};

axes.redrawComponents = function(gd, axIds) {
axIds = axIds ? axIds : axes.listIds(gd);

var fullLayout = gd._fullLayout;

function _redrawOneComp(moduleName, methodName, stashName, shortCircuit) {
var method = Registry.getComponentMethod(moduleName, methodName);
var stash = {};

for(var i = 0; i < axIds.length; i++) {
var ax = fullLayout[axes.id2name(axIds[i])];
var indices = ax[stashName];

for(var j = 0; j < indices.length; j++) {
var ind = indices[j];

if(!stash[ind]) {
method(gd, ind);
stash[ind] = 1;
// once is enough for images (which doesn't use the `i` arg anyway)
if(shortCircuit) return;
}
}
}
}

// annotations and shapes 'draw' method is slow,
// use the finer-grained 'drawOne' method instead
_redrawOneComp('annotations', 'drawOne', '_annIndices');
_redrawOneComp('shapes', 'drawOne', '_shapeIndices');
_redrawOneComp('images', 'draw', '_imgIndices', true);
};

var getDataConversions = axes.getDataConversions = function(gd, trace, target, targetArray) {
var ax;

Expand Down
28 changes: 5 additions & 23 deletions src/plots/cartesian/dragbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {

// viewbox redraw at first
updateSubplots(scrollViewBox);
ticksAndAnnotations(ns, ew);
ticksAndAnnotations();

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

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

updateSubplots([x0, y0, pw - dx, ph - dy]);
ticksAndAnnotations(yActive, xActive);
ticksAndAnnotations();
}

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

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

function redrawObjs(objArray, method, shortCircuit) {
for(i = 0; i < objArray.length; i++) {
var obji = objArray[i];

if((ew && activeAxIds.indexOf(obji.xref) !== -1) ||
(ns && activeAxIds.indexOf(obji.yref) !== -1)) {
method(gd, i);
// once is enough for images (which doesn't use the `i` arg anyway)
if(shortCircuit) return;
}
}
}

// annotations and shapes 'draw' method is slow,
// use the finer-grained 'drawOne' method instead

redrawObjs(gd._fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne'));
redrawObjs(gd._fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne'));
redrawObjs(gd._fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true);
Axes.redrawComponents(gd, activeAxIds);
}

function doubleClick() {
Expand Down
5 changes: 3 additions & 2 deletions src/plots/cartesian/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,12 @@ module.exports = {
valType: 'info_array',
role: 'info',
items: [
{valType: 'any', editType: 'axrange', impliedEdits: {'^autorange': false}},
{valType: 'any', editType: 'axrange', impliedEdits: {'^autorange': false}}
{valType: 'any', editType: 'axrange', impliedEdits: {'^autorange': false}, anim: true},
{valType: 'any', editType: 'axrange', impliedEdits: {'^autorange': false}, anim: true}
],
editType: 'axrange',
impliedEdits: {'autorange': false},
anim: true,
description: [
'Sets the range of this axis.',
'If the axis `type` is *log*, then you must take the log of your',
Expand Down
1 change: 1 addition & 0 deletions src/plots/cartesian/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; });
axLayoutOut._annIndices = [];
axLayoutOut._shapeIndices = [];
axLayoutOut._imgIndices = [];
axLayoutOut._subplotsWith = [];
axLayoutOut._counterAxes = [];

Expand Down
Loading

0 comments on commit c34d299

Please sign in to comment.