Skip to content

Layout animations for container array'd objects #1081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Oct 25, 2016
42 changes: 24 additions & 18 deletions src/components/images/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,21 @@ module.exports = function draw(gd) {
function setImage(d) {
var thisImage = d3.select(this);

if(this.img && this.img.src === d.source) {
return;
}

thisImage.attr('xmlns', xmlnsNamespaces.svg);

var imagePromise = new Promise(function(resolve) {

var img = new Image();
this.img = img;

// If not set, a `tainted canvas` error is thrown
img.setAttribute('crossOrigin', 'anonymous');
img.onerror = errorHandler;
img.onload = function() {

var canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
Expand All @@ -88,7 +92,7 @@ module.exports = function draw(gd) {
thisImage.remove();
resolve();
}
});
}.bind(this));

gd._promises.push(imagePromise);
}
Expand Down Expand Up @@ -146,29 +150,31 @@ module.exports = function draw(gd) {
}
}


// Required for updating images
function keyFunction(d, i) {
return d.source + i;
}


var imagesBelow = fullLayout._imageLowerLayer.selectAll('image')
.data(imageDataBelow, keyFunction),
.data(imageDataBelow),
imagesSubplot = fullLayout._imageSubplotLayer.selectAll('image')
.data(imageDataSubplot, keyFunction),
.data(imageDataSubplot),
imagesAbove = fullLayout._imageUpperLayer.selectAll('image')
.data(imageDataAbove, keyFunction);
.data(imageDataAbove);

imagesBelow.enter().append('image').each(setImage);
imagesSubplot.enter().append('image').each(setImage);
imagesAbove.enter().append('image').each(setImage);
imagesBelow.enter().append('image');
imagesSubplot.enter().append('image');
imagesAbove.enter().append('image');

imagesBelow.exit().remove();
imagesSubplot.exit().remove();
imagesAbove.exit().remove();

imagesBelow.each(applyAttributes);
imagesSubplot.each(applyAttributes);
imagesAbove.each(applyAttributes);
imagesBelow.each(function(d) {
setImage.bind(this)(d);
applyAttributes.bind(this)(d);
});
imagesSubplot.each(function(d) {
setImage.bind(this)(d);
applyAttributes.bind(this)(d);
});
imagesAbove.each(function(d) {
setImage.bind(this)(d);
applyAttributes.bind(this)(d);
});
};
22 changes: 18 additions & 4 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,8 @@ plots.extendObjectWithContainers = function(dest, src, containerPaths) {
for(j = 0; j < srcContainer.length; j++) {
destContainer[j] = plots.extendObjectWithContainers(destContainer[j], srcContainer[j]);
}

destProp.set(destContainer);
}
}

Expand Down Expand Up @@ -1510,7 +1512,7 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts)
delete layoutUpdate[attr].range;
}

Lib.extendDeepNoArrays(gd.layout, layoutUpdate);
plots.extendLayout(gd.layout, layoutUpdate);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, this now guarantees that shapes, images and all the other layout array container can also be animated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless there are bugs, yes. It iterates through each of those container arrays as a way of bypassing the No in extendDeepNoArrays. The option to force redraw makes non-animatable things Just Work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. Let's 🔒 this down by expanding test case

https://github.com/plotly/plotly.js/pull/1081/files#diff-2032f9a39fb6bbc9611ed5ec74c10c19R68

to transition all layout array containers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lunch, then adding it.


// Supply defaults after applying the incoming properties. Note that any attempt
// to simplify this step and reduce the amount of work resulted in the reconstruction
Expand Down Expand Up @@ -1556,10 +1558,25 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts)
gd._transitioningWithDuration = true;
}


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

if(frameOpts.redraw) {
gd._transitionData._interruptCallbacks.push(function() {
return Plotly.redraw(gd);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice use of Plotly.redraw

});
}

// Emit this and make sure it happens last:
gd._transitionData._interruptCallbacks.push(function() {
gd.emit('plotly_transitioninterrupted', []);
});

// Construct callbacks that are executed on transition end. This ensures the d3 transitions
// are *complete* before anything else is done.
var numCallbacks = 0;
Expand Down Expand Up @@ -1631,14 +1648,11 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts)
}

function interruptPreviousTransitions() {
gd.emit('plotly_transitioninterrupted', []);

// If a transition is interrupted, set this to false. At the moment, the only thing that would
// interrupt a transition is another transition, so that it will momentarily be set to true
// again, but this determines whether autorange or dragbox work, so it's for the sake of
// cleanliness:
gd._transitioning = false;
gd._transtionWithDuration = false;

return executeCallbacks(gd._transitionData._interruptCallbacks);
}
Expand Down
120 changes: 120 additions & 0 deletions test/jasmine/tests/transition_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,126 @@ function runTests(transitionDuration) {
}).catch(fail).then(done);
});

it('transitions an annotation', function(done) {
function annotationPosition() {
var g = gd._fullLayout._infolayer.select('.annotation').select('.annotation-text-g');
return [parseInt(g.attr('x')), parseInt(g.attr('y'))];
}
var p1, p2;

Plotly.relayout(gd, {annotations: [{x: 0, y: 0, text: 'test'}]}).then(function() {
p1 = annotationPosition();

return Plots.transition(gd, null, {
'annotations[0].x': 1,
'annotations[0].y': 1
}, [],
{redraw: true, duration: transitionDuration},
{duration: transitionDuration, easing: 'cubic-in-out'}
);
}).then(function() {
p2 = annotationPosition();

// Ensure both coordinates have moved, i.e. that the annotation has transitioned:
expect(p1[0]).not.toEqual(p2[0]);
expect(p1[1]).not.toEqual(p2[1]);

}).catch(fail).then(done);
});

it('transitions an image', function(done) {
var jsLogo = 'https://images.plot.ly/language-icons/api-home/js-logo.png';
var pythonLogo = 'https://images.plot.ly/language-icons/api-home/python-logo.png';

function imageel() {
return gd._fullLayout._imageUpperLayer.select('image').node();
}
function imagesrc() {
return imageel().getAttribute('href');
}
var p1, p2, e1, e2;

Plotly.relayout(gd, {images: [{x: 0, y: 0, source: jsLogo}]}).then(function() {
p1 = imagesrc();
e1 = imageel();

return Plots.transition(gd, null, {
'images[0].source': pythonLogo,
}, [],
{redraw: true, duration: transitionDuration},
{duration: transitionDuration, easing: 'cubic-in-out'}
);
}).then(function() {
p2 = imagesrc();
e2 = imageel();

// Test that the image src has changed:
expect(p1).not.toEqual(p2);

// Test that the image element identity has not:
expect(e1).toBe(e2);

}).catch(fail).then(done);
});

it('transitions a shape', function(done) {
function getPath() {
return gd._fullLayout._shapeUpperLayer.select('path').node();
}
var p1, p2, p3, d1, d2, d3, s1, s2, s3;

Plotly.relayout(gd, {
shapes: [{
type: 'circle',
xref: 'x',
yref: 'y',
x0: 0,
y0: 0,
x1: 2,
y1: 2,
opacity: 0.2,
fillcolor: 'blue',
line: {color: 'blue'}
}]
}).then(function() {
p1 = getPath();
d1 = p1.getAttribute('d');
s1 = p1.getAttribute('style');

return Plots.transition(gd, null, {
'shapes[0].x0': 1,
'shapes[0].y0': 1,
}, [],
{redraw: true, duration: transitionDuration},
{duration: transitionDuration, easing: 'cubic-in-out'}
);
}).then(function() {
p2 = getPath();
d2 = p2.getAttribute('d');
s2 = p2.getAttribute('style');

// If object constancy is implemented, this will then be *equal*:
expect(p1).not.toBe(p2);
expect(d1).not.toEqual(d2);
expect(s1).toEqual(s2);

return Plots.transition(gd, null, {
'shapes[0].color': 'red'
}, [],
{redraw: true, duration: transitionDuration},
{duration: transitionDuration, easing: 'cubic-in-out'}
);
}).then(function() {
p3 = getPath();
d3 = p3.getAttribute('d');
s3 = p3.getAttribute('d');

expect(d3).toEqual(d2);
expect(s3).not.toEqual(s2);
}).catch(fail).then(done);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!



it('transitions a transform', function(done) {
Plotly.restyle(gd, {
'transforms[0]': {
Expand Down