Skip to content

Commit 000a409

Browse files
authored
Merge pull request #1081 from plotly/annotation-animation
Layout animations for container array'd objects
2 parents 3cb7e7e + 43b95d8 commit 000a409

File tree

3 files changed

+162
-22
lines changed

3 files changed

+162
-22
lines changed

src/components/images/draw.js

+24-18
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,21 @@ module.exports = function draw(gd) {
5555
function setImage(d) {
5656
var thisImage = d3.select(this);
5757

58+
if(this.img && this.img.src === d.source) {
59+
return;
60+
}
61+
5862
thisImage.attr('xmlns', xmlnsNamespaces.svg);
5963

6064
var imagePromise = new Promise(function(resolve) {
6165

6266
var img = new Image();
67+
this.img = img;
6368

6469
// If not set, a `tainted canvas` error is thrown
6570
img.setAttribute('crossOrigin', 'anonymous');
6671
img.onerror = errorHandler;
6772
img.onload = function() {
68-
6973
var canvas = document.createElement('canvas');
7074
canvas.width = this.width;
7175
canvas.height = this.height;
@@ -88,7 +92,7 @@ module.exports = function draw(gd) {
8892
thisImage.remove();
8993
resolve();
9094
}
91-
});
95+
}.bind(this));
9296

9397
gd._promises.push(imagePromise);
9498
}
@@ -146,29 +150,31 @@ module.exports = function draw(gd) {
146150
}
147151
}
148152

149-
150-
// Required for updating images
151-
function keyFunction(d, i) {
152-
return d.source + i;
153-
}
154-
155-
156153
var imagesBelow = fullLayout._imageLowerLayer.selectAll('image')
157-
.data(imageDataBelow, keyFunction),
154+
.data(imageDataBelow),
158155
imagesSubplot = fullLayout._imageSubplotLayer.selectAll('image')
159-
.data(imageDataSubplot, keyFunction),
156+
.data(imageDataSubplot),
160157
imagesAbove = fullLayout._imageUpperLayer.selectAll('image')
161-
.data(imageDataAbove, keyFunction);
158+
.data(imageDataAbove);
162159

163-
imagesBelow.enter().append('image').each(setImage);
164-
imagesSubplot.enter().append('image').each(setImage);
165-
imagesAbove.enter().append('image').each(setImage);
160+
imagesBelow.enter().append('image');
161+
imagesSubplot.enter().append('image');
162+
imagesAbove.enter().append('image');
166163

167164
imagesBelow.exit().remove();
168165
imagesSubplot.exit().remove();
169166
imagesAbove.exit().remove();
170167

171-
imagesBelow.each(applyAttributes);
172-
imagesSubplot.each(applyAttributes);
173-
imagesAbove.each(applyAttributes);
168+
imagesBelow.each(function(d) {
169+
setImage.bind(this)(d);
170+
applyAttributes.bind(this)(d);
171+
});
172+
imagesSubplot.each(function(d) {
173+
setImage.bind(this)(d);
174+
applyAttributes.bind(this)(d);
175+
});
176+
imagesAbove.each(function(d) {
177+
setImage.bind(this)(d);
178+
applyAttributes.bind(this)(d);
179+
});
174180
};

src/plots/plots.js

+18-4
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,8 @@ plots.extendObjectWithContainers = function(dest, src, containerPaths) {
14291429
for(j = 0; j < srcContainer.length; j++) {
14301430
destContainer[j] = plots.extendObjectWithContainers(destContainer[j], srcContainer[j]);
14311431
}
1432+
1433+
destProp.set(destContainer);
14321434
}
14331435
}
14341436

@@ -1521,7 +1523,7 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts)
15211523
delete layoutUpdate[attr].range;
15221524
}
15231525

1524-
Lib.extendDeepNoArrays(gd.layout, layoutUpdate);
1526+
plots.extendLayout(gd.layout, layoutUpdate);
15251527

15261528
// Supply defaults after applying the incoming properties. Note that any attempt
15271529
// to simplify this step and reduce the amount of work resulted in the reconstruction
@@ -1567,10 +1569,25 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts)
15671569
gd._transitioningWithDuration = true;
15681570
}
15691571

1572+
1573+
// If another transition is triggered, this callback will be executed simply because it's
1574+
// in the interruptCallbacks queue. If this transition completes, it will instead flush
1575+
// that queue and forget about this callback.
15701576
gd._transitionData._interruptCallbacks.push(function() {
15711577
aborted = true;
15721578
});
15731579

1580+
if(frameOpts.redraw) {
1581+
gd._transitionData._interruptCallbacks.push(function() {
1582+
return Plotly.redraw(gd);
1583+
});
1584+
}
1585+
1586+
// Emit this and make sure it happens last:
1587+
gd._transitionData._interruptCallbacks.push(function() {
1588+
gd.emit('plotly_transitioninterrupted', []);
1589+
});
1590+
15741591
// Construct callbacks that are executed on transition end. This ensures the d3 transitions
15751592
// are *complete* before anything else is done.
15761593
var numCallbacks = 0;
@@ -1642,14 +1659,11 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts)
16421659
}
16431660

16441661
function interruptPreviousTransitions() {
1645-
gd.emit('plotly_transitioninterrupted', []);
1646-
16471662
// If a transition is interrupted, set this to false. At the moment, the only thing that would
16481663
// interrupt a transition is another transition, so that it will momentarily be set to true
16491664
// again, but this determines whether autorange or dragbox work, so it's for the sake of
16501665
// cleanliness:
16511666
gd._transitioning = false;
1652-
gd._transtionWithDuration = false;
16531667

16541668
return executeCallbacks(gd._transitionData._interruptCallbacks);
16551669
}

test/jasmine/tests/transition_test.js

+120
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,126 @@ function runTests(transitionDuration) {
6565
}).catch(fail).then(done);
6666
});
6767

68+
it('transitions an annotation', function(done) {
69+
function annotationPosition() {
70+
var g = gd._fullLayout._infolayer.select('.annotation').select('.annotation-text-g');
71+
return [parseInt(g.attr('x')), parseInt(g.attr('y'))];
72+
}
73+
var p1, p2;
74+
75+
Plotly.relayout(gd, {annotations: [{x: 0, y: 0, text: 'test'}]}).then(function() {
76+
p1 = annotationPosition();
77+
78+
return Plots.transition(gd, null, {
79+
'annotations[0].x': 1,
80+
'annotations[0].y': 1
81+
}, [],
82+
{redraw: true, duration: transitionDuration},
83+
{duration: transitionDuration, easing: 'cubic-in-out'}
84+
);
85+
}).then(function() {
86+
p2 = annotationPosition();
87+
88+
// Ensure both coordinates have moved, i.e. that the annotation has transitioned:
89+
expect(p1[0]).not.toEqual(p2[0]);
90+
expect(p1[1]).not.toEqual(p2[1]);
91+
92+
}).catch(fail).then(done);
93+
});
94+
95+
it('transitions an image', function(done) {
96+
var jsLogo = 'https://images.plot.ly/language-icons/api-home/js-logo.png';
97+
var pythonLogo = 'https://images.plot.ly/language-icons/api-home/python-logo.png';
98+
99+
function imageel() {
100+
return gd._fullLayout._imageUpperLayer.select('image').node();
101+
}
102+
function imagesrc() {
103+
return imageel().getAttribute('href');
104+
}
105+
var p1, p2, e1, e2;
106+
107+
Plotly.relayout(gd, {images: [{x: 0, y: 0, source: jsLogo}]}).then(function() {
108+
p1 = imagesrc();
109+
e1 = imageel();
110+
111+
return Plots.transition(gd, null, {
112+
'images[0].source': pythonLogo,
113+
}, [],
114+
{redraw: true, duration: transitionDuration},
115+
{duration: transitionDuration, easing: 'cubic-in-out'}
116+
);
117+
}).then(function() {
118+
p2 = imagesrc();
119+
e2 = imageel();
120+
121+
// Test that the image src has changed:
122+
expect(p1).not.toEqual(p2);
123+
124+
// Test that the image element identity has not:
125+
expect(e1).toBe(e2);
126+
127+
}).catch(fail).then(done);
128+
});
129+
130+
it('transitions a shape', function(done) {
131+
function getPath() {
132+
return gd._fullLayout._shapeUpperLayer.select('path').node();
133+
}
134+
var p1, p2, p3, d1, d2, d3, s1, s2, s3;
135+
136+
Plotly.relayout(gd, {
137+
shapes: [{
138+
type: 'circle',
139+
xref: 'x',
140+
yref: 'y',
141+
x0: 0,
142+
y0: 0,
143+
x1: 2,
144+
y1: 2,
145+
opacity: 0.2,
146+
fillcolor: 'blue',
147+
line: {color: 'blue'}
148+
}]
149+
}).then(function() {
150+
p1 = getPath();
151+
d1 = p1.getAttribute('d');
152+
s1 = p1.getAttribute('style');
153+
154+
return Plots.transition(gd, null, {
155+
'shapes[0].x0': 1,
156+
'shapes[0].y0': 1,
157+
}, [],
158+
{redraw: true, duration: transitionDuration},
159+
{duration: transitionDuration, easing: 'cubic-in-out'}
160+
);
161+
}).then(function() {
162+
p2 = getPath();
163+
d2 = p2.getAttribute('d');
164+
s2 = p2.getAttribute('style');
165+
166+
// If object constancy is implemented, this will then be *equal*:
167+
expect(p1).not.toBe(p2);
168+
expect(d1).not.toEqual(d2);
169+
expect(s1).toEqual(s2);
170+
171+
return Plots.transition(gd, null, {
172+
'shapes[0].color': 'red'
173+
}, [],
174+
{redraw: true, duration: transitionDuration},
175+
{duration: transitionDuration, easing: 'cubic-in-out'}
176+
);
177+
}).then(function() {
178+
p3 = getPath();
179+
d3 = p3.getAttribute('d');
180+
s3 = p3.getAttribute('d');
181+
182+
expect(d3).toEqual(d2);
183+
expect(s3).not.toEqual(s2);
184+
}).catch(fail).then(done);
185+
});
186+
187+
68188
it('transitions a transform', function(done) {
69189
Plotly.restyle(gd, {
70190
'transforms[0]': {

0 commit comments

Comments
 (0)