Skip to content

Commit dd0b1f8

Browse files
committed
Expand the animate API
1 parent a6be166 commit dd0b1f8

File tree

7 files changed

+352
-57
lines changed

7 files changed

+352
-57
lines changed

src/plot_api/plot_api.js

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2532,7 +2532,7 @@ Plotly.relayout = function relayout(gd, astr, val) {
25322532
* @param {string id or DOM element} gd
25332533
* the id or DOM element of the graph container div
25342534
*/
2535-
Plotly.transition = function(gd, data, layout, traceIndices, transitionConfig) {
2535+
Plotly.transition = function(gd, data, layout, traceIndices, transitionConfig, onTransitioned) {
25362536
gd = getGraphDiv(gd);
25372537

25382538
var i, traceIdx;
@@ -2700,6 +2700,8 @@ Plotly.transition = function(gd, data, layout, traceIndices, transitionConfig) {
27002700
gd._transitioningWithDuration = false;
27012701

27022702
gd.emit('plotly_transitioned', []);
2703+
onTransitioned && onTransitioned();
2704+
onTransitioned = null;
27032705
});
27042706
}
27052707

@@ -2753,22 +2755,133 @@ Plotly.transition = function(gd, data, layout, traceIndices, transitionConfig) {
27532755
* @param {object} transitionConfig
27542756
* configuration for transition
27552757
*/
2756-
Plotly.animate = function(gd, frameName, transitionConfig) {
2758+
Plotly.animate = function(gd, groupNameOrFrameList, transitionConfig) {
27572759
gd = getGraphDiv(gd);
2760+
var trans = gd._transitionData;
27582761

2759-
if(!gd._transitionData._frameHash[frameName]) {
2760-
Lib.warn('animateToFrame failure: keyframe does not exist', frameName);
2761-
return Promise.reject();
2762+
// This is the queue of frames that will be animated as soon as possible. They
2763+
// are popped immediately upon the *start* of a transition:
2764+
if(!trans._frameQueue) {
2765+
trans._frameQueue = [];
2766+
}
2767+
2768+
// Since frames are popped immediately, an empty queue only means all frames have
2769+
// *started* to transition, not that the animation is complete. To solve that,
2770+
// track a separate counter that increments at the same time as frames are added
2771+
// to the queue, but decrements only when the transition is complete.
2772+
if(trans._frameWaitingCnt === undefined) {
2773+
trans._frameWaitingCnt = 0;
2774+
}
2775+
2776+
function queueFrames(frameList) {
2777+
if(frameList.length === 0) return;
2778+
2779+
for(var i = 0; i < frameList.length; i++) {
2780+
var computedFrame = Plots.computeFrame(gd, frameList[i].name);
2781+
2782+
trans._frameWaitingCnt++;
2783+
trans._frameQueue.push({
2784+
frame: computedFrame,
2785+
name: frameList[i].name,
2786+
transitionConfig: frameList[i].transitionConfig || {},
2787+
frameduration: 0,
2788+
});
2789+
}
2790+
2791+
if(!trans._animationRaf) {
2792+
beginAnimation();
2793+
}
27622794
}
27632795

2764-
var computedFrame = Plots.computeFrame(gd, frameName);
2796+
function completeAnimation() {
2797+
cancelAnimationFrame(trans._animationRaf);
2798+
trans._animationRaf = null;
2799+
}
2800+
2801+
function beginAnimation() {
2802+
gd.emit('plotly_animating');
2803+
2804+
// If no timer is running, then set last frame = long ago:
2805+
trans._lastframeat = 0;
2806+
trans._timetonext = 0;
2807+
2808+
var doFrame = function() {
2809+
// Check if we need to pop a frame:
2810+
if(Date.now() - trans._lastframeat > trans._timetonext) {
2811+
var newFrame = trans._frameQueue.shift();
2812+
2813+
var onTransitioned = function() {
2814+
trans._frameWaitingCnt--;
2815+
if(trans._frameWaitingCnt === 0) {
2816+
gd.emit('plotly_animated');
2817+
}
2818+
};
27652819

2766-
return Plotly.transition(gd,
2767-
computedFrame.data,
2768-
computedFrame.layout,
2769-
computedFrame.traceIndices,
2770-
transitionConfig
2771-
);
2820+
if(newFrame) {
2821+
trans._lastframeat = Date.now();
2822+
trans._timetonext = newFrame.transitionConfig.frameduration === undefined ? 50 : newFrame.transitionConfig.frameduration;
2823+
2824+
2825+
Plotly.transition(gd,
2826+
newFrame.frame.data,
2827+
newFrame.frame.layout,
2828+
newFrame.frame.traces,
2829+
newFrame.transitionConfig,
2830+
onTransitioned
2831+
);
2832+
}
2833+
2834+
if(trans._frameQueue.length === 0) {
2835+
completeAnimation();
2836+
return;
2837+
}
2838+
}
2839+
2840+
trans._animationRaf = requestAnimationFrame(doFrame);
2841+
};
2842+
2843+
return doFrame();
2844+
}
2845+
2846+
var counter = 0;
2847+
function setTransitionConfig(frame) {
2848+
if(Array.isArray(transitionConfig)) {
2849+
frame.transitionConfig = transitionConfig[counter];
2850+
} else {
2851+
frame.transitionConfig = transitionConfig;
2852+
}
2853+
counter++;
2854+
return frame;
2855+
}
2856+
2857+
var i, frame;
2858+
var frameList = [];
2859+
var allFrames = typeof groupNameOrFrameList === 'undefined';
2860+
if(allFrames || typeof groupNameOrFrameList === 'string') {
2861+
for(i = 0; i < trans._frames.length; i++) {
2862+
frame = trans._frames[i];
2863+
2864+
if(allFrames || frame.group === groupNameOrFrameList) {
2865+
frameList.push(setTransitionConfig({name: frame.name}));
2866+
}
2867+
}
2868+
} else if(Array.isArray(groupNameOrFrameList)) {
2869+
for(i = 0; i < groupNameOrFrameList.length; i++) {
2870+
frameList.push(setTransitionConfig({name: groupNameOrFrameList[i]}));
2871+
}
2872+
}
2873+
2874+
// Verify that all of these frames actually exist; return and reject if not:
2875+
for(i = 0; i < frameList.length; i++) {
2876+
if(!trans._frameHash[frameList[i].name]) {
2877+
Lib.warn('animate failure: frame not found: "' + frameList[i].name + '"');
2878+
return Promise.reject();
2879+
}
2880+
}
2881+
2882+
queueFrames(frameList);
2883+
2884+
return Promise.resolve();
27722885
};
27732886

27742887
/**
@@ -2780,7 +2893,7 @@ Plotly.animate = function(gd, frameName, transitionConfig) {
27802893
* - data: {array of objects} trace data
27812894
* - layout {object} layout definition
27822895
* - traces {array} trace indices
2783-
* - baseFrame {string} name of keyframe from which this keyframe gets defaults
2896+
* - baseframe {string} name of keyframe from which this keyframe gets defaults
27842897
*/
27852898
Plotly.addFrames = function(gd, frameList, indices) {
27862899
gd = getGraphDiv(gd);
@@ -2805,7 +2918,7 @@ Plotly.addFrames = function(gd, frameList, indices) {
28052918
var insertions = [];
28062919
for(i = frameList.length - 1; i >= 0; i--) {
28072920
insertions.push({
2808-
frame: frameList[i],
2921+
frame: Plots.supplyFrameDefaults(frameList[i]),
28092922
index: (indices && indices[i] !== undefined && indices[i] !== null) ? indices[i] : bigIndex + i
28102923
});
28112924
}

src/plots/frame_attributes.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = {
12+
group: {
13+
valType: 'string',
14+
role: 'info',
15+
description: [
16+
'An identifier that specifies the group to which the frame belongs,',
17+
'used by animate to select a subset of frames.'
18+
].join(' ')
19+
},
20+
name: {
21+
valType: 'string',
22+
role: 'info',
23+
description: ['A label by which to identify the frame']
24+
},
25+
traces: {
26+
valType: 'data_array',
27+
description: [
28+
'A list of trace indices that identify the respective traces in the',
29+
'data attribute'
30+
].join(' ')
31+
},
32+
baseframe: {
33+
valType: 'string',
34+
role: 'info',
35+
description: [
36+
'The name of the frame into which this frame\'s properties are merged',
37+
'before applying. This is used to unify properties and avoid needing',
38+
'to specify the same values for the same properties in multiple frames.'
39+
].join(' ')
40+
},
41+
data: {
42+
valType: 'data_array',
43+
description: [
44+
'A list of traces this frame modifies. The format is identical to the',
45+
'normal trace definition.'
46+
]
47+
},
48+
layout: {
49+
valType: 'any',
50+
description: [
51+
'Layout properties which this frame modifies. The format is identical',
52+
'to the normal layout definition.'
53+
]
54+
}
55+
};

src/plots/plots.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ var Color = require('../components/color');
1919

2020
var plots = module.exports = {};
2121
var transitionAttrs = require('./transition_attributes');
22+
var frameAttrs = require('./frame_attributes');
2223

2324
// Expose registry methods on Plots for backward-compatibility
2425
Lib.extendFlat(plots, Registry);
@@ -626,6 +627,23 @@ plots.supplyTransitionDefaults = function(config) {
626627
return configOut;
627628
};
628629

630+
plots.supplyFrameDefaults = function(frameIn) {
631+
var frameOut = {};
632+
633+
function coerce(attr, dflt) {
634+
return Lib.coerce(frameIn, frameOut, frameAttrs, attr, dflt);
635+
}
636+
637+
coerce('group');
638+
coerce('name');
639+
coerce('traces');
640+
coerce('baseframe');
641+
coerce('data');
642+
coerce('layout');
643+
644+
return frameOut;
645+
};
646+
629647
plots.supplyTraceDefaults = function(traceIn, traceIndex, layout) {
630648
var traceOut = {},
631649
defaultColor = Color.defaults[traceIndex % Color.defaults.length];
@@ -1211,7 +1229,7 @@ plots.computeFrame = function(gd, frameName) {
12111229
var frameNameStack = [framePtr.name];
12121230

12131231
// Follow frame pointers:
1214-
while((framePtr = frameLookup[framePtr.baseFrame])) {
1232+
while((framePtr = frameLookup[framePtr.baseframe])) {
12151233
// Avoid infinite loops:
12161234
if(frameNameStack.indexOf(framePtr.name) !== -1) break;
12171235

@@ -1234,7 +1252,7 @@ plots.computeFrame = function(gd, frameName) {
12341252
if(!result.data) {
12351253
result.data = [];
12361254
}
1237-
traceIndices = framePtr.traceIndices;
1255+
traceIndices = framePtr.traces;
12381256

12391257
if(!traceIndices) {
12401258
// If not defined, assume serial order starting at zero
@@ -1244,8 +1262,8 @@ plots.computeFrame = function(gd, frameName) {
12441262
}
12451263
}
12461264

1247-
if(!result.traceIndices) {
1248-
result.traceIndices = [];
1265+
if(!result.traces) {
1266+
result.traces = [];
12491267
}
12501268

12511269
for(i = 0; i < framePtr.data.length; i++) {
@@ -1256,10 +1274,10 @@ plots.computeFrame = function(gd, frameName) {
12561274
continue;
12571275
}
12581276

1259-
destIndex = result.traceIndices.indexOf(traceIndex);
1277+
destIndex = result.traces.indexOf(traceIndex);
12601278
if(destIndex === -1) {
12611279
destIndex = result.data.length;
1262-
result.traceIndices[destIndex] = traceIndex;
1280+
result.traces[destIndex] = traceIndex;
12631281
}
12641282

12651283
copy = Lib.extendDeepNoArrays({}, framePtr.data[i]);

test/image/mocks/animation.json

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,39 +40,43 @@
4040
}
4141
}, {
4242
"name": "frame0",
43+
"group": "even-frames",
4344
"data": [
4445
{"y": [0.5, 1.5, 7.5]},
4546
{"y": [4.25, 2.25, 3.05]}
4647
],
47-
"baseFrame": "base",
48-
"traceIndices": [0, 1],
48+
"baseframe": "base",
49+
"traces": [0, 1],
4950
"layout": { }
5051
}, {
5152
"name": "frame1",
53+
"group": "odd-frames",
5254
"data": [
5355
{"y": [2.1, 1, 7]},
5456
{"y": [4.5, 2.5, 3.1]}
5557
],
56-
"baseFrame": "base",
57-
"traceIndices": [0, 1],
58+
"baseframe": "base",
59+
"traces": [0, 1],
5860
"layout": { }
5961
}, {
6062
"name": "frame2",
63+
"group": "even-frames",
6164
"data": [
6265
{"y": [3.5, 0.5, 6]},
6366
{"y": [5.7, 2.7, 3.9]}
6467
],
65-
"baseFrame": "base",
66-
"traceIndices": [0, 1],
68+
"baseframe": "base",
69+
"traces": [0, 1],
6770
"layout": { }
6871
}, {
6972
"name": "frame3",
73+
"group": "odd-frames",
7074
"data": [
7175
{"y": [5.1, 0.25, 5]},
7276
{"y": [7, 2.9, 6]}
7377
],
74-
"baseFrame": "base",
75-
"traceIndices": [0, 1],
78+
"baseframe": "base",
79+
"traces": [0, 1],
7680
"layout": {
7781
"xaxis": {
7882
"range": [-1, 4]

0 commit comments

Comments
 (0)