Skip to content
42 changes: 24 additions & 18 deletions src/components/images/draw.js
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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 DownExpand 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
Copy Markdown
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 DownExpand 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Layout animations for container array'd objects by rreusser · Pull Request #1081 · plotly/plotly.js · GitHub
Skip to content
42 changes: 24 additions & 18 deletions src/components/images/draw.js
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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 DownExpand 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
Copy Markdown
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 DownExpand 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Layout animations for container array'd objects by rreusser · Pull Request #1081 · plotly/plotly.js · GitHub
Skip to content
42 changes: 24 additions & 18 deletions src/components/images/draw.js
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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 DownExpand 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
Copy Markdown
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 DownExpand 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Layout animations for container array'd objects by rreusser · Pull Request #1081 · plotly/plotly.js · GitHub
Skip to content
42 changes: 24 additions & 18 deletions src/components/images/draw.js
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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 DownExpand 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
Copy Markdown
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 DownExpand 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Layout animations for container array'd objects by rreusser · Pull Request #1081 · plotly/plotly.js · GitHub
Skip to content
42 changes: 24 additions & 18 deletions src/components/images/draw.js
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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 DownExpand 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
Copy Markdown
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 DownExpand 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Layout animations for container array'd objects by rreusser · Pull Request #1081 · plotly/plotly.js · GitHub
Skip to content
42 changes: 24 additions & 18 deletions src/components/images/draw.js
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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 DownExpand 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
Copy Markdown
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 DownExpand 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Layout animations for container array'd objects by rreusser · Pull Request #1081 · plotly/plotly.js · GitHub
Skip to content
42 changes: 24 additions & 18 deletions src/components/images/draw.js
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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 DownExpand 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
Copy Markdown
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 DownExpand 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 numberDiff line numberDiff 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
Copy Markdown
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