Skip to content

Commit b47b7f6

Browse files
committed
Group transitions to avoid race conditions
1 parent 1a9f640 commit b47b7f6

File tree

5 files changed

+150
-130
lines changed

5 files changed

+150
-130
lines changed

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

src/components/drawing/index.js

+86-96
Original file line numberDiff line numberDiff line change
@@ -46,64 +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, trace, transitionConfig, joinDirection) {
50-
var size;
51-
52-
var hasTransition = transitionConfig && (transitionConfig || {}).duration > 0;
53-
54-
if(hasTransition) {
55-
size = s.size();
49+
drawing.translatePoint = function(d, sel, xa, ya, trace, transitionConfig, joinDirection) {
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 + ')');
60+
}
5661
}
62+
else sel.remove();
63+
};
5764

65+
drawing.translatePoints = function(s, xa, ya, trace, transitionConfig, joinDirection) {
5866
s.each(function(d, i) {
59-
// put xp and yp into d if pixel scaling is already done
60-
var x = d.xp || xa.c2p(d.x),
61-
y = d.yp || ya.c2p(d.y),
62-
p = d3.select(this);
63-
if(isNumeric(x) && isNumeric(y)) {
64-
// for multiline text this works better
65-
if(this.nodeName === 'text') {
66-
p.attr('x', x).attr('y', y);
67-
} else {
68-
if(hasTransition) {
69-
var trans;
70-
if(!joinDirection) {
71-
trans = p.transition()
72-
.delay(transitionConfig.delay)
73-
.duration(transitionConfig.duration)
74-
.ease(transitionConfig.ease)
75-
.attr('transform', 'translate(' + x + ',' + y + ')');
76-
77-
if(trace) {
78-
trans.call(drawing.pointStyle, trace);
79-
}
80-
} else if(joinDirection === -1) {
81-
trans = p.style('opacity', 1)
82-
.transition()
83-
.duration(transitionConfig.duration)
84-
.ease(transitionConfig.ease)
85-
.style('opacity', 0)
86-
.remove();
87-
} else if(joinDirection === 1) {
88-
trans = p.attr('transform', 'translate(' + x + ',' + y + ')');
89-
90-
if(trace) {
91-
trans.call(drawing.pointStyle, trace);
92-
}
93-
94-
trans.style('opacity', 0)
95-
.transition()
96-
.duration(transitionConfig.duration)
97-
.ease(transitionConfig.ease)
98-
.style('opacity', 1);
99-
}
100-
101-
} else {
102-
p.attr('transform', 'translate(' + x + ',' + y + ')');
103-
}
104-
}
105-
}
106-
else p.remove();
67+
var sel = d3.select(this);
68+
drawing.translatePoint(d, sel, xa, ya, trace, transitionConfig, joinDirection);
10769
});
10870
};
10971

@@ -126,6 +88,16 @@ drawing.crispRound = function(td, lineWidth, dflt) {
12688
return Math.round(lineWidth);
12789
};
12890

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+
129101
drawing.lineGroupStyle = function(s, lw, lc, ld) {
130102
s.style('fill', 'none')
131103
.each(function(d) {
@@ -221,6 +193,64 @@ drawing.symbolNumber = function(v) {
221193
return Math.floor(Math.max(v, 0));
222194
};
223195

196+
function singlePointStyle (d, sel, trace, markerScale, lineScale, marker, markerLine) {
197+
198+
// 'so' is suspected outliers, for box plots
199+
var fillColor,
200+
lineColor,
201+
lineWidth;
202+
if(d.so) {
203+
lineWidth = markerLine.outlierwidth;
204+
lineColor = markerLine.outliercolor;
205+
fillColor = marker.outliercolor;
206+
}
207+
else {
208+
lineWidth = (d.mlw + 1 || markerLine.width + 1 ||
209+
// TODO: we need the latter for legends... can we get rid of it?
210+
(d.trace ? d.trace.marker.line.width : 0) + 1) - 1;
211+
212+
if('mlc' in d) lineColor = d.mlcc = lineScale(d.mlc);
213+
// weird case: array wasn't long enough to apply to every point
214+
else if(Array.isArray(markerLine.color)) lineColor = Color.defaultLine;
215+
else lineColor = markerLine.color;
216+
217+
if('mc' in d) fillColor = d.mcc = markerScale(d.mc);
218+
else if(Array.isArray(marker.color)) fillColor = Color.defaultLine;
219+
else fillColor = marker.color || 'rgba(0,0,0,0)';
220+
}
221+
222+
if(d.om) {
223+
// open markers can't have zero linewidth, default to 1px,
224+
// and use fill color as stroke color
225+
sel.call(Color.stroke, fillColor)
226+
.style({
227+
'stroke-width': (lineWidth || 1) + 'px',
228+
fill: 'none'
229+
});
230+
}
231+
else {
232+
sel.style('stroke-width', lineWidth + 'px')
233+
.call(Color.fill, fillColor);
234+
if(lineWidth) {
235+
sel.call(Color.stroke, lineColor);
236+
}
237+
}
238+
};
239+
240+
drawing.singlePointStyle = function(d, sel, trace) {
241+
var marker = trace.marker,
242+
markerLine = marker.line;
243+
244+
// allow array marker and marker line colors to be
245+
// scaled by given max and min to colorscales
246+
var markerIn = (trace._input || {}).marker || {},
247+
markerScale = drawing.tryColorscale(marker, markerIn, ''),
248+
lineScale = drawing.tryColorscale(marker, markerIn, 'line.');
249+
250+
singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerLine);
251+
252+
}
253+
224254
drawing.pointStyle = function(s, trace) {
225255
if(!s.size()) return;
226256

@@ -265,47 +295,7 @@ drawing.pointStyle = function(s, trace) {
265295
lineScale = drawing.tryColorscale(marker, markerIn, 'line.');
266296

267297
s.each(function(d) {
268-
// 'so' is suspected outliers, for box plots
269-
var fillColor,
270-
lineColor,
271-
lineWidth;
272-
if(d.so) {
273-
lineWidth = markerLine.outlierwidth;
274-
lineColor = markerLine.outliercolor;
275-
fillColor = marker.outliercolor;
276-
}
277-
else {
278-
lineWidth = (d.mlw + 1 || markerLine.width + 1 ||
279-
// TODO: we need the latter for legends... can we get rid of it?
280-
(d.trace ? d.trace.marker.line.width : 0) + 1) - 1;
281-
282-
if('mlc' in d) lineColor = d.mlcc = lineScale(d.mlc);
283-
// weird case: array wasn't long enough to apply to every point
284-
else if(Array.isArray(markerLine.color)) lineColor = Color.defaultLine;
285-
else lineColor = markerLine.color;
286-
287-
if('mc' in d) fillColor = d.mcc = markerScale(d.mc);
288-
else if(Array.isArray(marker.color)) fillColor = Color.defaultLine;
289-
else fillColor = marker.color || 'rgba(0,0,0,0)';
290-
}
291-
292-
var p = d3.select(this);
293-
if(d.om) {
294-
// open markers can't have zero linewidth, default to 1px,
295-
// and use fill color as stroke color
296-
p.call(Color.stroke, fillColor)
297-
.style({
298-
'stroke-width': (lineWidth || 1) + 'px',
299-
fill: 'none'
300-
});
301-
}
302-
else {
303-
p.style('stroke-width', lineWidth + 'px')
304-
.call(Color.fill, fillColor);
305-
if(lineWidth) {
306-
p.call(Color.stroke, lineColor);
307-
}
308-
}
298+
drawing.singlePointStyle(d, d3.select(this), trace, markerScale, lineScale)
309299
});
310300
};
311301

src/plot_api/plot_api.js

+25-11
Original file line numberDiff line numberDiff line change
@@ -2618,7 +2618,13 @@ Plotly.transition = function(gd, data, layout, traceIndices, transitionConfig) {
26182618
}
26192619
}
26202620

2621+
var aborted = false;
2622+
26212623
function executeTransitions() {
2624+
gd._transitionData._interruptCallbacks.push(function () {
2625+
aborted = true;
2626+
});
2627+
26222628
var traceTransitionConfig;
26232629
var hasTraceTransition = false;
26242630
var j;
@@ -2648,15 +2654,30 @@ Plotly.transition = function(gd, data, layout, traceIndices, transitionConfig) {
26482654
traceTransitionConfig = transitionConfig;
26492655
}
26502656

2651-
for(j = 0; j < basePlotModules.length; j++) {
2652-
basePlotModules[j].plot(gd, transitionedTraces, traceTransitionConfig);
2657+
// Construct callbacks that are executed on transition end. This ensures the d3 transitions
2658+
// are *complete* before anything else is done.
2659+
var numCallbacks = 0;
2660+
var numCompleted = 0;
2661+
function makeCallback () {
2662+
numCallbacks++;
2663+
return function () {
2664+
numCompleted++;
2665+
// When all are complete, perform a redraw:
2666+
if (!aborted && numCompleted === numCallbacks) {
2667+
completeTransition();
2668+
}
2669+
}
26532670
}
26542671

2655-
gd._transitionData._completionTimeout = setTimeout(completeTransition, transitionConfig.duration + transitionConfig.delay + 1000);
2672+
for(j = 0; j < basePlotModules.length; j++) {
2673+
var _config = Lib.extendFlat({onComplete: makeCallback()}, traceTransitionConfig);
2674+
basePlotModules[j].plot(gd, transitionedTraces, _config);
2675+
}
26562676

26572677
if(!hasAxisTransition && !hasTraceTransition) {
26582678
return false;
26592679
}
2680+
26602681
}
26612682

26622683
function completeTransition() {
@@ -2673,14 +2694,7 @@ Plotly.transition = function(gd, data, layout, traceIndices, transitionConfig) {
26732694
}
26742695

26752696
function interruptPreviousTransitions() {
2676-
if(gd._transitionData._completionTimeout) {
2677-
// Prevent the previous completion from occurring:
2678-
clearTimeout(gd._transitionData._completionTimeout);
2679-
gd._transitionData._completionTimeout = null;
2680-
2681-
// Interrupt an event to indicate that a transition was running:
2682-
gd.emit('plotly_interrupttransition', []);
2683-
}
2697+
gd.emit('plotly_interrupttransition', []);
26842698

26852699
flushCallbacks(gd._transitionData._cleanupCallbacks);
26862700
return executeCallbacks(gd._transitionData._interruptCallbacks);

src/traces/scatter/plot.js

+35-21
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = function plot(gd, plotinfo, cdscatter, transitionConfig) {
2929
// If transition config is provided, then it is only a partial replot and traces not
3030
// updated are removed.
3131
var isFullReplot = !transitionConfig;
32+
var hasTransition = !!transitionConfig && transitionConfig.duration > 0;
3233

3334
selection = scatterlayer.selectAll('g.trace');
3435

@@ -61,11 +62,27 @@ module.exports = function plot(gd, plotinfo, cdscatter, transitionConfig) {
6162
return idx1 > idx2 ? 1 : -1;
6263
});
6364

64-
// Must run the selection again since otherwise enters/updates get grouped together
65-
// and these get executed out of order. Except we need them in order!
66-
scatterlayer.selectAll('g.trace').each(function(d, i) {
67-
plotOne(gd, i, plotinfo, d, cdscatter, this, transitionConfig);
68-
});
65+
if (hasTransition) {
66+
var transition = d3.transition()
67+
.duration(transitionConfig.duration)
68+
.ease(transitionConfig.ease)
69+
.delay(transitionConfig.delay)
70+
.each('end', function () {
71+
transitionConfig.onComplete && transitionConfig.onComplete();
72+
});
73+
74+
transition.each(function () {
75+
// Must run the selection again since otherwise enters/updates get grouped together
76+
// and these get executed out of order. Except we need them in order!
77+
scatterlayer.selectAll('g.trace').each(function(d, i) {
78+
plotOne(gd, i, plotinfo, d, cdscatter, this, transitionConfig);
79+
});
80+
});
81+
} else {
82+
scatterlayer.selectAll('g.trace').each(function(d, i) {
83+
plotOne(gd, i, plotinfo, d, cdscatter, this, transitionConfig);
84+
});
85+
}
6986

7087
if(isFullReplot) {
7188
join.exit().remove();
@@ -127,14 +144,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
127144
var hasTransition = !!transitionConfig && transitionConfig.duration > 0;
128145

129146
function transition(selection) {
130-
if(hasTransition) {
131-
return selection.transition()
132-
.duration(transitionConfig.duration)
133-
.delay(transitionConfig.delay)
134-
.ease(transitionConfig.ease);
135-
} else {
136-
return selection;
137-
}
147+
return hasTransition ? selection.transition() : selection;
138148
}
139149

140150
var xa = plotinfo.x(),
@@ -277,8 +287,9 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
277287
.call(Drawing.lineGroupStyle))
278288
.style('opacity', 1);
279289
} else {
280-
transition(el).attr('d', thispath)
281-
.call(Drawing.lineGroupStyle);
290+
var sel = transition(el);
291+
sel.attr('d', thispath);
292+
Drawing.singleLineStyle(cdscatter, sel);
282293
}
283294
}
284295
};
@@ -376,14 +387,17 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
376387
join = selection
377388
.data(trace.marker.maxdisplayed ? visFilter : Lib.identity, getKeyFunc(trace));
378389

379-
join.enter().append('path')
380-
.classed('point', true)
381-
.call(Drawing.pointStyle, trace)
390+
var enter = join.enter().append('path')
391+
.classed('point', true);
392+
393+
enter.call(Drawing.pointStyle, trace)
382394
.call(Drawing.translatePoints, xa, ya, trace, transitionConfig, 1);
383395

384-
join.transition()
385-
.call(Drawing.translatePoints, xa, ya, trace, transitionConfig, 0)
386-
.call(Drawing.pointStyle, trace);
396+
join.transition().each(function (d) {
397+
var sel = transition(d3.select(this));
398+
Drawing.translatePoint(d, sel, xa, ya, trace, transitionConfig, 0)
399+
Drawing.singlePointStyle(d, sel, trace);
400+
});
387401

388402
if(hasTransition) {
389403
join.exit()

test/jasmine/tests/transition_test.js

-1
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,4 @@ describe('Plotly.transition', function() {
8585
Plotly.transition(gd, null, {'xaxis.range': [0.2, 0.3]}, null, {duration: 50});
8686
gd.on('plotly_endtransition', done);
8787
});
88-
8988
});

0 commit comments

Comments
 (0)