Skip to content

Commit d736910

Browse files
authored
Merge pull request #802 from plotly/animate-api-take-4
Animate API
2 parents cecf090 + 4e88184 commit d736910

37 files changed

+3526
-342
lines changed

Diff for: src/components/color/index.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ color.stroke = function(s, c) {
7474

7575
color.fill = function(s, c) {
7676
var tc = tinycolor(c);
77-
s.style({'fill': color.tinyRGB(tc), 'fill-opacity': tc.getAlpha()});
77+
s.style({
78+
'fill': color.tinyRGB(tc),
79+
'fill-opacity': tc.getAlpha()
80+
});
7881
};
7982

8083
// search container for colors with the deprecated rgb(fractions) format

Diff for: src/components/drawing/index.js

+93-59
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,26 @@ drawing.setRect = function(s, x, y, w, h) {
4646
s.call(drawing.setPosition, x, y).call(drawing.setSize, w, h);
4747
};
4848

49-
drawing.translatePoints = function(s, xa, ya) {
50-
s.each(function(d) {
51-
// put xp and yp into d if pixel scaling is already done
52-
var x = d.xp || xa.c2p(d.x),
53-
y = d.yp || ya.c2p(d.y),
54-
p = d3.select(this);
55-
if(isNumeric(x) && isNumeric(y)) {
56-
// for multiline text this works better
57-
if(this.nodeName === 'text') p.attr('x', x).attr('y', y);
58-
else p.attr('transform', 'translate(' + x + ',' + y + ')');
49+
drawing.translatePoint = function(d, sel, xa, ya) {
50+
// put xp and yp into d if pixel scaling is already done
51+
var x = d.xp || xa.c2p(d.x),
52+
y = d.yp || ya.c2p(d.y);
53+
54+
if(isNumeric(x) && isNumeric(y)) {
55+
// for multiline text this works better
56+
if(this.nodeName === 'text') {
57+
sel.node().attr('x', x).attr('y', y);
58+
} else {
59+
sel.attr('transform', 'translate(' + x + ',' + y + ')');
5960
}
60-
else p.remove();
61+
}
62+
else sel.remove();
63+
};
64+
65+
drawing.translatePoints = function(s, xa, ya, trace) {
66+
s.each(function(d) {
67+
var sel = d3.select(this);
68+
drawing.translatePoint(d, sel, xa, ya, trace);
6169
});
6270
};
6371

@@ -80,6 +88,16 @@ drawing.crispRound = function(td, lineWidth, dflt) {
8088
return Math.round(lineWidth);
8189
};
8290

91+
drawing.singleLineStyle = function(d, s, lw, lc, ld) {
92+
s.style('fill', 'none');
93+
var line = (((d || [])[0] || {}).trace || {}).line || {},
94+
lw1 = lw || line.width||0,
95+
dash = ld || line.dash || '';
96+
97+
Color.stroke(s, lc || line.color);
98+
drawing.dashLine(s, dash, lw1);
99+
};
100+
83101
drawing.lineGroupStyle = function(s, lw, lc, ld) {
84102
s.style('fill', 'none')
85103
.each(function(d) {
@@ -175,18 +193,13 @@ drawing.symbolNumber = function(v) {
175193
return Math.floor(Math.max(v, 0));
176194
};
177195

178-
drawing.pointStyle = function(s, trace) {
179-
if(!s.size()) return;
180-
181-
var marker = trace.marker,
182-
markerLine = marker.line;
183-
196+
function singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerLine) {
184197
// only scatter & box plots get marker path and opacity
185198
// bars, histograms don't
186199
if(Registry.traceIs(trace, 'symbols')) {
187200
var sizeFn = makeBubbleSizeFn(trace);
188201

189-
s.attr('d', function(d) {
202+
sel.attr('d', function(d) {
190203
var r;
191204

192205
// handle multi-trace graph edit case
@@ -212,54 +225,75 @@ drawing.pointStyle = function(s, trace) {
212225
return (d.mo + 1 || marker.opacity + 1) - 1;
213226
});
214227
}
228+
229+
// 'so' is suspected outliers, for box plots
230+
var fillColor,
231+
lineColor,
232+
lineWidth;
233+
if(d.so) {
234+
lineWidth = markerLine.outlierwidth;
235+
lineColor = markerLine.outliercolor;
236+
fillColor = marker.outliercolor;
237+
}
238+
else {
239+
lineWidth = (d.mlw + 1 || markerLine.width + 1 ||
240+
// TODO: we need the latter for legends... can we get rid of it?
241+
(d.trace ? d.trace.marker.line.width : 0) + 1) - 1;
242+
243+
if('mlc' in d) lineColor = d.mlcc = lineScale(d.mlc);
244+
// weird case: array wasn't long enough to apply to every point
245+
else if(Array.isArray(markerLine.color)) lineColor = Color.defaultLine;
246+
else lineColor = markerLine.color;
247+
248+
if('mc' in d) fillColor = d.mcc = markerScale(d.mc);
249+
else if(Array.isArray(marker.color)) fillColor = Color.defaultLine;
250+
else fillColor = marker.color || 'rgba(0,0,0,0)';
251+
}
252+
253+
if(d.om) {
254+
// open markers can't have zero linewidth, default to 1px,
255+
// and use fill color as stroke color
256+
sel.call(Color.stroke, fillColor)
257+
.style({
258+
'stroke-width': (lineWidth || 1) + 'px',
259+
fill: 'none'
260+
});
261+
}
262+
else {
263+
sel.style('stroke-width', lineWidth + 'px')
264+
.call(Color.fill, fillColor);
265+
if(lineWidth) {
266+
sel.call(Color.stroke, lineColor);
267+
}
268+
}
269+
}
270+
271+
drawing.singlePointStyle = function(d, sel, trace) {
272+
var marker = trace.marker,
273+
markerLine = marker.line;
274+
215275
// allow array marker and marker line colors to be
216276
// scaled by given max and min to colorscales
217277
var markerIn = (trace._input || {}).marker || {},
218278
markerScale = drawing.tryColorscale(marker, markerIn, ''),
219279
lineScale = drawing.tryColorscale(marker, markerIn, 'line.');
220280

221-
s.each(function(d) {
222-
// 'so' is suspected outliers, for box plots
223-
var fillColor,
224-
lineColor,
225-
lineWidth;
226-
if(d.so) {
227-
lineWidth = markerLine.outlierwidth;
228-
lineColor = markerLine.outliercolor;
229-
fillColor = marker.outliercolor;
230-
}
231-
else {
232-
lineWidth = (d.mlw + 1 || markerLine.width + 1 ||
233-
// TODO: we need the latter for legends... can we get rid of it?
234-
(d.trace ? d.trace.marker.line.width : 0) + 1) - 1;
235-
236-
if('mlc' in d) lineColor = d.mlcc = lineScale(d.mlc);
237-
// weird case: array wasn't long enough to apply to every point
238-
else if(Array.isArray(markerLine.color)) lineColor = Color.defaultLine;
239-
else lineColor = markerLine.color;
240-
241-
if('mc' in d) fillColor = d.mcc = markerScale(d.mc);
242-
else if(Array.isArray(marker.color)) fillColor = Color.defaultLine;
243-
else fillColor = marker.color || 'rgba(0,0,0,0)';
244-
}
281+
singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerLine);
245282

246-
var p = d3.select(this);
247-
if(d.om) {
248-
// open markers can't have zero linewidth, default to 1px,
249-
// and use fill color as stroke color
250-
p.call(Color.stroke, fillColor)
251-
.style({
252-
'stroke-width': (lineWidth || 1) + 'px',
253-
fill: 'none'
254-
});
255-
}
256-
else {
257-
p.style('stroke-width', lineWidth + 'px')
258-
.call(Color.fill, fillColor);
259-
if(lineWidth) {
260-
p.call(Color.stroke, lineColor);
261-
}
262-
}
283+
};
284+
285+
drawing.pointStyle = function(s, trace) {
286+
if(!s.size()) return;
287+
288+
// allow array marker and marker line colors to be
289+
// scaled by given max and min to colorscales
290+
var marker = trace.marker;
291+
var markerIn = (trace._input || {}).marker || {},
292+
markerScale = drawing.tryColorscale(marker, markerIn, ''),
293+
lineScale = drawing.tryColorscale(marker, markerIn, 'line.');
294+
295+
s.each(function(d) {
296+
drawing.singlePointStyle(d, d3.select(this), trace, markerScale, lineScale);
263297
});
264298
};
265299

Diff for: src/components/errorbars/plot.js

+53-11
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
var d3 = require('d3');
1313
var isNumeric = require('fast-isnumeric');
1414

15-
var Lib = require('../../lib');
1615
var subTypes = require('../../traces/scatter/subtypes');
1716

18-
19-
module.exports = function plot(traces, plotinfo) {
17+
module.exports = function plot(traces, plotinfo, transitionOpts) {
18+
var isNew;
2019
var xa = plotinfo.x(),
2120
ya = plotinfo.y();
2221

22+
var hasAnimation = transitionOpts && transitionOpts.duration > 0;
23+
2324
traces.each(function(d) {
2425
var trace = d[0].trace,
2526
// || {} is in case the trace (specifically scatterternary)
@@ -29,6 +30,12 @@ module.exports = function plot(traces, plotinfo) {
2930
xObj = trace.error_x || {},
3031
yObj = trace.error_y || {};
3132

33+
var keyFunc;
34+
35+
if(trace.ids) {
36+
keyFunc = function(d) {return d.id;};
37+
}
38+
3239
var sparse = (
3340
subTypes.hasMarkers(trace) &&
3441
trace.marker.maxdisplayed > 0
@@ -37,11 +44,21 @@ module.exports = function plot(traces, plotinfo) {
3744
if(!yObj.visible && !xObj.visible) return;
3845

3946
var errorbars = d3.select(this).selectAll('g.errorbar')
40-
.data(Lib.identity);
47+
.data(d, keyFunc);
48+
49+
errorbars.exit().remove();
4150

42-
errorbars.enter().append('g')
51+
errorbars.style('opacity', 1);
52+
53+
var enter = errorbars.enter().append('g')
4354
.classed('errorbar', true);
4455

56+
if(hasAnimation) {
57+
enter.style('opacity', 0).transition()
58+
.duration(transitionOpts.duration)
59+
.style('opacity', 1);
60+
}
61+
4562
errorbars.each(function(d) {
4663
var errorbar = d3.select(this);
4764
var coords = errorCoords(d, xa, ya);
@@ -59,11 +76,24 @@ module.exports = function plot(traces, plotinfo) {
5976
coords.yh + 'h' + (2 * yw) + // hat
6077
'm-' + yw + ',0V' + coords.ys; // bar
6178

79+
6280
if(!coords.noYS) path += 'm-' + yw + ',0h' + (2 * yw); // shoe
6381

64-
errorbar.append('path')
65-
.classed('yerror', true)
66-
.attr('d', path);
82+
var yerror = errorbar.select('path.yerror');
83+
84+
isNew = !yerror.size();
85+
86+
if(isNew) {
87+
yerror = errorbar.append('path')
88+
.classed('yerror', true);
89+
} else if(hasAnimation) {
90+
yerror = yerror
91+
.transition()
92+
.duration(transitionOpts.duration)
93+
.ease(transitionOpts.easing);
94+
}
95+
96+
yerror.attr('d', path);
6797
}
6898

6999
if(xObj.visible && isNumeric(coords.y) &&
@@ -77,9 +107,21 @@ module.exports = function plot(traces, plotinfo) {
77107

78108
if(!coords.noXS) path += 'm0,-' + xw + 'v' + (2 * xw); // shoe
79109

80-
errorbar.append('path')
81-
.classed('xerror', true)
82-
.attr('d', path);
110+
var xerror = errorbar.select('path.xerror');
111+
112+
isNew = !xerror.size();
113+
114+
if(isNew) {
115+
xerror = errorbar.append('path')
116+
.classed('xerror', true);
117+
} else if(hasAnimation) {
118+
xerror = xerror
119+
.transition()
120+
.duration(transitionOpts.duration)
121+
.ease(transitionOpts.easing);
122+
}
123+
124+
xerror.attr('d', path);
83125
}
84126
});
85127
});

Diff for: src/components/updatemenus/attributes.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ var buttonsAttrs = {
1717

1818
method: {
1919
valType: 'enumerated',
20-
values: ['restyle', 'relayout'],
20+
values: ['restyle', 'relayout', 'animate'],
2121
dflt: 'restyle',
2222
role: 'info',
2323
description: [

Diff for: src/core.js

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ exports.register = require('./plot_api/register');
4343
exports.toImage = require('./plot_api/to_image');
4444
exports.downloadImage = require('./snapshot/download');
4545
exports.validate = require('./plot_api/validate');
46+
exports.addFrames = Plotly.addFrames;
47+
exports.deleteFrames = Plotly.deleteFrames;
48+
exports.animate = Plotly.animate;
4649

4750
// scatter is the only trace included by default
4851
exports.register(require('./traces/scatter'));

Diff for: src/lib/index.js

+34
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,40 @@ lib.objectFromPath = function(path, value) {
577577
return obj;
578578
};
579579

580+
/**
581+
* Iterate through an object in-place, converting dotted properties to objects.
582+
*
583+
* @example
584+
* lib.expandObjectPaths({'nested.test.path': 'value'});
585+
* // returns { nested: { test: {path: 'value'}}}
586+
*/
587+
588+
// Store this to avoid recompiling regex on every prop since this may happen many
589+
// many times for animations.
590+
// TODO: Premature optimization? Remove?
591+
var dottedPropertyRegex = /^([^\.]*)\../;
592+
593+
lib.expandObjectPaths = function(data) {
594+
var match, key, prop, datum;
595+
if(typeof data === 'object' && !Array.isArray(data)) {
596+
for(key in data) {
597+
if(data.hasOwnProperty(key)) {
598+
if((match = key.match(dottedPropertyRegex))) {
599+
datum = data[key];
600+
prop = match[1];
601+
602+
delete data[key];
603+
604+
data[prop] = lib.extendDeepNoArrays(data[prop] || {}, lib.objectFromPath(key, lib.expandObjectPaths(datum))[prop]);
605+
} else {
606+
data[key] = lib.expandObjectPaths(data[key]);
607+
}
608+
}
609+
}
610+
}
611+
return data;
612+
};
613+
580614
/**
581615
* Converts value to string separated by the provided separators.
582616
*

0 commit comments

Comments
 (0)