Skip to content

Commit

Permalink
first cut Plotly.react transitions
Browse files Browse the repository at this point in the history
- temporarily add Plots.transitions2 & Cartesian.transitionAxes2
- call Plots.transition2 from Plotly.react when at one animatable
  attribute has changed AND 'layout.transition` is set by user
- 'redraw' after transition iff not all changed attributer are animatable
- handle simultaneous trace + layout updates the same way as Plotly.animate
- special handling for 'datarevision' diff'ing
  • Loading branch information
etpinard committed Nov 5, 2018
1 parent 5010de0 commit 1cb0d5f
Show file tree
Hide file tree
Showing 4 changed files with 440 additions and 10 deletions.
51 changes: 43 additions & 8 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2336,9 +2336,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 @@ -2368,7 +2370,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.transition2(gd, restyleFlags, relayoutFlags, oldFullLayout);
});
}
else if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) {
gd._fullLayout._skipDefaults = true;
seq.push(exports.plot);
}
Expand Down Expand Up @@ -2421,7 +2435,7 @@ exports.react = function(gd, data, layout, config) {

};

function diffData(gd, oldFullData, newFullData, immutable) {
function diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision) {
if(oldFullData.length !== newFullData.length) {
return {
fullReplot: true,
Expand All @@ -2441,10 +2455,11 @@ 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++) {
Expand All @@ -2463,7 +2478,7 @@ function diffData(gd, oldFullData, newFullData, immutable) {
return flags;
}

function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
function diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition) {
var flags = editTypes.layoutFlags();
flags.arrays = {};
flags.rangesAltered = {};
Expand All @@ -2477,6 +2492,7 @@ function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
getValObject: getLayoutValObject,
flags: flags,
immutable: immutable,
transition: transition,
gd: gd
};

Expand All @@ -2490,7 +2506,7 @@ function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
}

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

var getValObject = opts.getValObject;
var flags = opts.flags;
Expand All @@ -2506,10 +2522,24 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
}
editTypes.update(flags, valObject);

// track animatable changes
if(opts.transition) {
if(flags.anim === 'all' && !valObject.anim) {
flags.anim = 'some';
} else if(!flags.anim && valObject.anim) {
flags.anim = 'all';
}
}

// 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 @@ -2518,7 +2548,7 @@ 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];
Expand Down Expand Up @@ -2614,6 +2644,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
3 changes: 2 additions & 1 deletion src/plots/cartesian/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ exports.layoutAttributes = require('./layout_attributes');

exports.supplyLayoutDefaults = require('./layout_defaults');

exports.transitionAxes = require('./transition_axes');
exports.transitionAxes = require('./transition_axes').transitionAxes;
exports.transitionAxes2 = require('./transition_axes').transitionAxes2;

exports.finalizeSubplots = function(layoutIn, layoutOut) {
var subplots = layoutOut._subplots;
Expand Down
187 changes: 186 additions & 1 deletion src/plots/cartesian/transition_axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var Drawing = require('../../components/drawing');
var Axes = require('./axes');
var axisRegex = require('./constants').attrRegex;

module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCompleteCallback) {
function transitionAxes(gd, newLayout, transitionOpts, makeOnCompleteCallback) {
var fullLayout = gd._fullLayout;
var axes = [];

Expand Down Expand Up @@ -323,4 +323,189 @@ module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCo
raf = window.requestAnimationFrame(doFrame);

return Promise.resolve();
}

function transitionAxes2(gd, edits, transitionOpts, makeOnCompleteCallback) {
var fullLayout = gd._fullLayout;

function ticksAndAnnotations(xa, ya) {
var activeAxIds = [xa._id, ya._id];
var i;

for(i = 0; i < activeAxIds.length; i++) {
Axes.doTicksSingle(gd, activeAxIds[i], true);
}

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

if((activeAxIds.indexOf(obji.xref) !== -1) ||
(activeAxIds.indexOf(obji.yref) !== -1)) {
method(gd, i);
}

// once is enough for images (which doesn't use the `i` arg anyway)
if(shortCircuit) return;
}
}

redrawObjs(fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne'));
redrawObjs(fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne'));
redrawObjs(fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true);
}

function unsetSubplotTransform(plotinfo) {
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;

fullLayout._defs.select('#' + plotinfo.clipId + '> rect')
.call(Drawing.setTranslate, 0, 0)
.call(Drawing.setScale, 1, 1);

plotinfo.plot
.call(Drawing.setTranslate, xa._offset, ya._offset)
.call(Drawing.setScale, 1, 1);

var traceGroups = plotinfo.plot.selectAll('.scatterlayer .trace');

// This is specifically directed at scatter traces, applying an inverse
// scale to individual points to counteract the scale of the trace
// as a whole:
traceGroups.selectAll('.point')
.call(Drawing.setPointGroupScale, 1, 1);
traceGroups.selectAll('.textpoint')
.call(Drawing.setTextPointsScale, 1, 1);
traceGroups
.call(Drawing.hideOutsideRangePoints, plotinfo);
}

function updateSubplot(edit, progress) {
var plotinfo = edit.plotinfo;
var xa1 = plotinfo.xaxis;
var ya1 = plotinfo.yaxis;

var xr0 = edit.xr0;
var xr1 = edit.xr1;
var xlen = xa1._length;
var yr0 = edit.yr0;
var yr1 = edit.yr1;
var ylen = ya1._length;

var editX = xr0[0] !== xr1[0] || xr0[1] !== xr1[1];
var editY = yr0[0] !== yr1[0] || yr0[1] !== yr1[1];
var viewBox = [];

if(editX) {
var dx0 = xr0[1] - xr0[0];
var dx1 = xr1[1] - xr1[0];
viewBox[0] = (xr0[0] * (1 - progress) + progress * xr1[0] - xr0[0]) / (xr0[1] - xr0[0]) * xlen;
viewBox[2] = xlen * ((1 - progress) + progress * dx1 / dx0);
xa1.range[0] = xr0[0] * (1 - progress) + progress * xr1[0];
xa1.range[1] = xr0[1] * (1 - progress) + progress * xr1[1];
} else {
viewBox[0] = 0;
viewBox[2] = xlen;
}

if(editY) {
var dy0 = yr0[1] - yr0[0];
var dy1 = yr1[1] - yr1[0];
viewBox[1] = (yr0[1] * (1 - progress) + progress * yr1[1] - yr0[1]) / (yr0[0] - yr0[1]) * ylen;
viewBox[3] = ylen * ((1 - progress) + progress * dy1 / dy0);
ya1.range[0] = yr0[0] * (1 - progress) + progress * yr1[0];
ya1.range[1] = yr0[1] * (1 - progress) + progress * yr1[1];
} else {
viewBox[1] = 0;
viewBox[3] = ylen;
}

ticksAndAnnotations(plotinfo.xaxis, plotinfo.yaxis);

var xScaleFactor = editX ? xlen / viewBox[2] : 1;
var yScaleFactor = editY ? ylen / viewBox[3] : 1;
var clipDx = editX ? viewBox[0] : 0;
var clipDy = editY ? viewBox[1] : 0;
var fracDx = editX ? (viewBox[0] / viewBox[2] * xlen) : 0;
var fracDy = editY ? (viewBox[1] / viewBox[3] * ylen) : 0;
var plotDx = xa1._offset - fracDx;
var plotDy = ya1._offset - fracDy;

plotinfo.clipRect
.call(Drawing.setTranslate, clipDx, clipDy)
.call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor);

plotinfo.plot
.call(Drawing.setTranslate, plotDx, plotDy)
.call(Drawing.setScale, xScaleFactor, yScaleFactor);

// apply an inverse scale to individual points to counteract
// the scale of the trace group.
Drawing.setPointGroupScale(plotinfo.zoomScalePts, 1 / xScaleFactor, 1 / yScaleFactor);
Drawing.setTextPointsScale(plotinfo.zoomScaleTxt, 1 / xScaleFactor, 1 / yScaleFactor);
}

var onComplete;
if(makeOnCompleteCallback) {
// This module makes the choice whether or not it notifies Plotly.transition
// about completion:
onComplete = makeOnCompleteCallback();
}

function transitionComplete() {
var aobj = {};
var k;

for(k in edits) {
var edit = edits[k];
aobj[edit.plotinfo.xaxis._name + '.range'] = edit.xr1.slice();
aobj[edit.plotinfo.yaxis._name + '.range'] = edit.yr1.slice();
}

// Signal that this transition has completed:
onComplete && onComplete();

return Registry.call('relayout', gd, aobj).then(function() {
for(k in edits) {
unsetSubplotTransform(edits[k].plotinfo);
}
});
}

var t1, t2, raf;
var easeFn = d3.ease(transitionOpts.easing);

gd._transitionData._interruptCallbacks.push(function() {
window.cancelAnimationFrame(raf);
raf = null;
return transitionComplete();
});

function doFrame() {
t2 = Date.now();

var tInterp = Math.min(1, (t2 - t1) / transitionOpts.duration);
var progress = easeFn(tInterp);

for(var k in edits) {
updateSubplot(edits[k], progress);
}

if(t2 - t1 > transitionOpts.duration) {
transitionComplete();
raf = window.cancelAnimationFrame(doFrame);
} else {
raf = window.requestAnimationFrame(doFrame);
}
}

t1 = Date.now();
raf = window.requestAnimationFrame(doFrame);

return Promise.resolve();
}

module.exports = {
transitionAxes: transitionAxes,
transitionAxes2: transitionAxes2
};
Loading

0 comments on commit 1cb0d5f

Please sign in to comment.