Skip to content

Commit 1cb0d5f

Browse files
committed
first cut Plotly.react transitions
- 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
1 parent 5010de0 commit 1cb0d5f

File tree

4 files changed

+440
-10
lines changed

4 files changed

+440
-10
lines changed

src/plot_api/plot_api.js

+43-8
Original file line numberDiff line numberDiff line change
@@ -2336,9 +2336,11 @@ exports.react = function(gd, data, layout, config) {
23362336
var newFullData = gd._fullData;
23372337
var newFullLayout = gd._fullLayout;
23382338
var immutable = newFullLayout.datarevision === undefined;
2339+
var transition = newFullLayout.transition;
23392340

2340-
var restyleFlags = diffData(gd, oldFullData, newFullData, immutable);
2341-
var relayoutFlags = diffLayout(gd, oldFullLayout, newFullLayout, immutable);
2341+
var relayoutFlags = diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition);
2342+
var newDataRevision = relayoutFlags.newDataRevision;
2343+
var restyleFlags = diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision);
23422344

23432345
// TODO: how to translate this part of relayout to Plotly.react?
23442346
// // Setting width or height to null must reset the graph's width / height
@@ -2368,7 +2370,19 @@ exports.react = function(gd, data, layout, config) {
23682370
seq.push(addFrames);
23692371
}
23702372

2371-
if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) {
2373+
// Transition pathway,
2374+
// only used when 'transition' is set by user and
2375+
// when at least one animatable attribute has changed,
2376+
// N.B. config changed aren't animatable
2377+
if(newFullLayout.transition && !configChanged && (restyleFlags.anim || relayoutFlags.anim)) {
2378+
Plots.doCalcdata(gd);
2379+
subroutines.doAutoRangeAndConstraints(gd);
2380+
2381+
seq.push(function() {
2382+
return Plots.transition2(gd, restyleFlags, relayoutFlags, oldFullLayout);
2383+
});
2384+
}
2385+
else if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) {
23722386
gd._fullLayout._skipDefaults = true;
23732387
seq.push(exports.plot);
23742388
}
@@ -2421,7 +2435,7 @@ exports.react = function(gd, data, layout, config) {
24212435

24222436
};
24232437

2424-
function diffData(gd, oldFullData, newFullData, immutable) {
2438+
function diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision) {
24252439
if(oldFullData.length !== newFullData.length) {
24262440
return {
24272441
fullReplot: true,
@@ -2441,10 +2455,11 @@ function diffData(gd, oldFullData, newFullData, immutable) {
24412455
getValObject: getTraceValObject,
24422456
flags: flags,
24432457
immutable: immutable,
2458+
transition: transition,
2459+
newDataRevision: newDataRevision,
24442460
gd: gd
24452461
};
24462462

2447-
24482463
var seenUIDs = {};
24492464

24502465
for(i = 0; i < oldFullData.length; i++) {
@@ -2463,7 +2478,7 @@ function diffData(gd, oldFullData, newFullData, immutable) {
24632478
return flags;
24642479
}
24652480

2466-
function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
2481+
function diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition) {
24672482
var flags = editTypes.layoutFlags();
24682483
flags.arrays = {};
24692484
flags.rangesAltered = {};
@@ -2477,6 +2492,7 @@ function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
24772492
getValObject: getLayoutValObject,
24782493
flags: flags,
24792494
immutable: immutable,
2495+
transition: transition,
24802496
gd: gd
24812497
};
24822498

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

24922508
function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
2493-
var valObject, key;
2509+
var valObject, key, astr;
24942510

24952511
var getValObject = opts.getValObject;
24962512
var flags = opts.flags;
@@ -2506,10 +2522,24 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
25062522
}
25072523
editTypes.update(flags, valObject);
25082524

2525+
// track animatable changes
2526+
if(opts.transition) {
2527+
if(flags.anim === 'all' && !valObject.anim) {
2528+
flags.anim = 'some';
2529+
} else if(!flags.anim && valObject.anim) {
2530+
flags.anim = 'all';
2531+
}
2532+
}
2533+
25092534
// track cartesian axes with altered ranges
25102535
if(AX_RANGE_RE.test(astr) || AX_AUTORANGE_RE.test(astr)) {
25112536
flags.rangesAltered[outerparts[0]] = 1;
25122537
}
2538+
2539+
// track datarevision changes
2540+
if(key === 'datarevision') {
2541+
flags.newDataRevision = 1;
2542+
}
25132543
}
25142544

25152545
function valObjectCanBeDataArray(valObject) {
@@ -2518,7 +2548,7 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
25182548

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

25232553
var oldVal = oldContainer[key];
25242554
var newVal = newContainer[key];
@@ -2614,6 +2644,11 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
26142644
if(immutable) {
26152645
flags.calc = true;
26162646
}
2647+
2648+
// look for animatable attributes when the data changed
2649+
if(immutable || opts.newDataRevision) {
2650+
changed();
2651+
}
26172652
}
26182653
else if(wasArray !== nowArray) {
26192654
flags.calc = true;

src/plots/cartesian/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ exports.layoutAttributes = require('./layout_attributes');
4545

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

48-
exports.transitionAxes = require('./transition_axes');
48+
exports.transitionAxes = require('./transition_axes').transitionAxes;
49+
exports.transitionAxes2 = require('./transition_axes').transitionAxes2;
4950

5051
exports.finalizeSubplots = function(layoutIn, layoutOut) {
5152
var subplots = layoutOut._subplots;

src/plots/cartesian/transition_axes.js

+186-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var Drawing = require('../../components/drawing');
1515
var Axes = require('./axes');
1616
var axisRegex = require('./constants').attrRegex;
1717

18-
module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCompleteCallback) {
18+
function transitionAxes(gd, newLayout, transitionOpts, makeOnCompleteCallback) {
1919
var fullLayout = gd._fullLayout;
2020
var axes = [];
2121

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

325325
return Promise.resolve();
326+
}
327+
328+
function transitionAxes2(gd, edits, transitionOpts, makeOnCompleteCallback) {
329+
var fullLayout = gd._fullLayout;
330+
331+
function ticksAndAnnotations(xa, ya) {
332+
var activeAxIds = [xa._id, ya._id];
333+
var i;
334+
335+
for(i = 0; i < activeAxIds.length; i++) {
336+
Axes.doTicksSingle(gd, activeAxIds[i], true);
337+
}
338+
339+
function redrawObjs(objArray, method, shortCircuit) {
340+
for(i = 0; i < objArray.length; i++) {
341+
var obji = objArray[i];
342+
343+
if((activeAxIds.indexOf(obji.xref) !== -1) ||
344+
(activeAxIds.indexOf(obji.yref) !== -1)) {
345+
method(gd, i);
346+
}
347+
348+
// once is enough for images (which doesn't use the `i` arg anyway)
349+
if(shortCircuit) return;
350+
}
351+
}
352+
353+
redrawObjs(fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne'));
354+
redrawObjs(fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne'));
355+
redrawObjs(fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true);
356+
}
357+
358+
function unsetSubplotTransform(plotinfo) {
359+
var xa = plotinfo.xaxis;
360+
var ya = plotinfo.yaxis;
361+
362+
fullLayout._defs.select('#' + plotinfo.clipId + '> rect')
363+
.call(Drawing.setTranslate, 0, 0)
364+
.call(Drawing.setScale, 1, 1);
365+
366+
plotinfo.plot
367+
.call(Drawing.setTranslate, xa._offset, ya._offset)
368+
.call(Drawing.setScale, 1, 1);
369+
370+
var traceGroups = plotinfo.plot.selectAll('.scatterlayer .trace');
371+
372+
// This is specifically directed at scatter traces, applying an inverse
373+
// scale to individual points to counteract the scale of the trace
374+
// as a whole:
375+
traceGroups.selectAll('.point')
376+
.call(Drawing.setPointGroupScale, 1, 1);
377+
traceGroups.selectAll('.textpoint')
378+
.call(Drawing.setTextPointsScale, 1, 1);
379+
traceGroups
380+
.call(Drawing.hideOutsideRangePoints, plotinfo);
381+
}
382+
383+
function updateSubplot(edit, progress) {
384+
var plotinfo = edit.plotinfo;
385+
var xa1 = plotinfo.xaxis;
386+
var ya1 = plotinfo.yaxis;
387+
388+
var xr0 = edit.xr0;
389+
var xr1 = edit.xr1;
390+
var xlen = xa1._length;
391+
var yr0 = edit.yr0;
392+
var yr1 = edit.yr1;
393+
var ylen = ya1._length;
394+
395+
var editX = xr0[0] !== xr1[0] || xr0[1] !== xr1[1];
396+
var editY = yr0[0] !== yr1[0] || yr0[1] !== yr1[1];
397+
var viewBox = [];
398+
399+
if(editX) {
400+
var dx0 = xr0[1] - xr0[0];
401+
var dx1 = xr1[1] - xr1[0];
402+
viewBox[0] = (xr0[0] * (1 - progress) + progress * xr1[0] - xr0[0]) / (xr0[1] - xr0[0]) * xlen;
403+
viewBox[2] = xlen * ((1 - progress) + progress * dx1 / dx0);
404+
xa1.range[0] = xr0[0] * (1 - progress) + progress * xr1[0];
405+
xa1.range[1] = xr0[1] * (1 - progress) + progress * xr1[1];
406+
} else {
407+
viewBox[0] = 0;
408+
viewBox[2] = xlen;
409+
}
410+
411+
if(editY) {
412+
var dy0 = yr0[1] - yr0[0];
413+
var dy1 = yr1[1] - yr1[0];
414+
viewBox[1] = (yr0[1] * (1 - progress) + progress * yr1[1] - yr0[1]) / (yr0[0] - yr0[1]) * ylen;
415+
viewBox[3] = ylen * ((1 - progress) + progress * dy1 / dy0);
416+
ya1.range[0] = yr0[0] * (1 - progress) + progress * yr1[0];
417+
ya1.range[1] = yr0[1] * (1 - progress) + progress * yr1[1];
418+
} else {
419+
viewBox[1] = 0;
420+
viewBox[3] = ylen;
421+
}
422+
423+
ticksAndAnnotations(plotinfo.xaxis, plotinfo.yaxis);
424+
425+
var xScaleFactor = editX ? xlen / viewBox[2] : 1;
426+
var yScaleFactor = editY ? ylen / viewBox[3] : 1;
427+
var clipDx = editX ? viewBox[0] : 0;
428+
var clipDy = editY ? viewBox[1] : 0;
429+
var fracDx = editX ? (viewBox[0] / viewBox[2] * xlen) : 0;
430+
var fracDy = editY ? (viewBox[1] / viewBox[3] * ylen) : 0;
431+
var plotDx = xa1._offset - fracDx;
432+
var plotDy = ya1._offset - fracDy;
433+
434+
plotinfo.clipRect
435+
.call(Drawing.setTranslate, clipDx, clipDy)
436+
.call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor);
437+
438+
plotinfo.plot
439+
.call(Drawing.setTranslate, plotDx, plotDy)
440+
.call(Drawing.setScale, xScaleFactor, yScaleFactor);
441+
442+
// apply an inverse scale to individual points to counteract
443+
// the scale of the trace group.
444+
Drawing.setPointGroupScale(plotinfo.zoomScalePts, 1 / xScaleFactor, 1 / yScaleFactor);
445+
Drawing.setTextPointsScale(plotinfo.zoomScaleTxt, 1 / xScaleFactor, 1 / yScaleFactor);
446+
}
447+
448+
var onComplete;
449+
if(makeOnCompleteCallback) {
450+
// This module makes the choice whether or not it notifies Plotly.transition
451+
// about completion:
452+
onComplete = makeOnCompleteCallback();
453+
}
454+
455+
function transitionComplete() {
456+
var aobj = {};
457+
var k;
458+
459+
for(k in edits) {
460+
var edit = edits[k];
461+
aobj[edit.plotinfo.xaxis._name + '.range'] = edit.xr1.slice();
462+
aobj[edit.plotinfo.yaxis._name + '.range'] = edit.yr1.slice();
463+
}
464+
465+
// Signal that this transition has completed:
466+
onComplete && onComplete();
467+
468+
return Registry.call('relayout', gd, aobj).then(function() {
469+
for(k in edits) {
470+
unsetSubplotTransform(edits[k].plotinfo);
471+
}
472+
});
473+
}
474+
475+
var t1, t2, raf;
476+
var easeFn = d3.ease(transitionOpts.easing);
477+
478+
gd._transitionData._interruptCallbacks.push(function() {
479+
window.cancelAnimationFrame(raf);
480+
raf = null;
481+
return transitionComplete();
482+
});
483+
484+
function doFrame() {
485+
t2 = Date.now();
486+
487+
var tInterp = Math.min(1, (t2 - t1) / transitionOpts.duration);
488+
var progress = easeFn(tInterp);
489+
490+
for(var k in edits) {
491+
updateSubplot(edits[k], progress);
492+
}
493+
494+
if(t2 - t1 > transitionOpts.duration) {
495+
transitionComplete();
496+
raf = window.cancelAnimationFrame(doFrame);
497+
} else {
498+
raf = window.requestAnimationFrame(doFrame);
499+
}
500+
}
501+
502+
t1 = Date.now();
503+
raf = window.requestAnimationFrame(doFrame);
504+
505+
return Promise.resolve();
506+
}
507+
508+
module.exports = {
509+
transitionAxes: transitionAxes,
510+
transitionAxes2: transitionAxes2
326511
};

0 commit comments

Comments
 (0)