From 891af3b20238775a6b887ace7b6e1c1d06b075f5 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 25 Oct 2016 09:23:00 -0400 Subject: [PATCH 1/9] Change layout update to use new logic --- src/plots/plots.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 6ca52d59d4e..0535913ca15 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -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); } } @@ -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); // 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 From 726cbf0d9ec288a10292bbd7833cb52cfe767f2f Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 25 Oct 2016 09:55:51 -0400 Subject: [PATCH 2/9] Slightly improve transition redraw/interrupt behavior --- src/plots/plots.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 0535913ca15..33de8d3bccb 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1558,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); + }); + } + + // 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; @@ -1633,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); } From b20371ce39f9ec542350cee03879b5f5c2493c07 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 25 Oct 2016 10:05:05 -0400 Subject: [PATCH 3/9] The linter, again --- src/plots/plots.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 33de8d3bccb..61c6e3e1354 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1567,7 +1567,7 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts) }); if(frameOpts.redraw) { - gd._transitionData._interruptCallbacks.push(function () { + gd._transitionData._interruptCallbacks.push(function() { return Plotly.redraw(gd); }); } From c137464b3c851c51b6898f74ac06fe005503f883 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 25 Oct 2016 10:23:49 -0400 Subject: [PATCH 4/9] Add test for annotations actually moving --- test/jasmine/tests/transition_test.js | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index 86348f394e2..e604f8b2e45 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -65,6 +65,34 @@ 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 a transform', function(done) { Plotly.restyle(gd, { 'transforms[0]': { From 2bcc266addf0dd625ca454f75c43cbdf393718fb Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 25 Oct 2016 10:23:49 -0400 Subject: [PATCH 5/9] Add test for annotations actually moving --- test/jasmine/tests/transition_test.js | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index 86348f394e2..3a123571774 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -65,6 +65,34 @@ 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 a transform', function(done) { Plotly.restyle(gd, { 'transforms[0]': { From 03d19bcf6c1f77a3fb79916bf1fffc31f1621d66 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 25 Oct 2016 16:17:28 -0400 Subject: [PATCH 6/9] Tweak images to handle animation a bit better --- src/components/images/draw.js | 42 +++++++++++++++------------ test/jasmine/tests/transition_test.js | 34 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/components/images/draw.js b/src/components/images/draw.js index ad4ead9e3ce..a39a534adc2 100644 --- a/src/components/images/draw.js +++ b/src/components/images/draw.js @@ -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; @@ -88,7 +92,7 @@ module.exports = function draw(gd) { thisImage.remove(); resolve(); } - }); + }.bind(this)); gd._promises.push(imagePromise); } @@ -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); + }); }; diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index 3a123571774..fc4050282c9 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -90,7 +90,41 @@ function runTests(transitionDuration) { expect(p1[1]).not.toEqual(p2[1]); }).catch(fail).then(done); + }); + + it('transitions an image', function(done) { + var whitepx = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + var blackpx = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; + + 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: whitepx}]}).then(function() { + p1 = imagesrc(); + e1 = imageel(); + + return Plots.transition(gd, null, { + 'images[0].source': blackpx, + }, [], + {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 transform', function(done) { From 30929dc819a4c4d65ae798dcb3532a5a118f89bd Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 25 Oct 2016 16:33:42 -0400 Subject: [PATCH 7/9] Test that shapes are updated by transitions --- test/jasmine/tests/transition_test.js | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index fc4050282c9..5c0fad6c1cb 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -127,6 +127,48 @@ function runTests(transitionDuration) { }).catch(fail).then(done); }); + it('transitions a shape', function(done) { + function getPath() { + return gd._fullLayout._shapeUpperLayer.select('path').node(); + } + var p1, p2, d1, d2; + + 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'); + + 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'); + + // If object constancy is implemented, this will then be *equal*: + expect(p1).not.toBe(p2); + + expect(d1).not.toEqual(d2); + }).catch(fail).then(done); + }); + + it('transitions a transform', function(done) { Plotly.restyle(gd, { 'transforms[0]': { From b72e36d8d415da4df465902f571b6643d8ee6fe6 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 25 Oct 2016 16:39:52 -0400 Subject: [PATCH 8/9] Test more attributes of shape transitions on redraw --- test/jasmine/tests/transition_test.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index 5c0fad6c1cb..90675bc985a 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -131,7 +131,7 @@ function runTests(transitionDuration) { function getPath() { return gd._fullLayout._shapeUpperLayer.select('path').node(); } - var p1, p2, d1, d2; + var p1, p2, p3, d1, d2, d3, s1, s2, s3; Plotly.relayout(gd, { shapes: [{ @@ -149,6 +149,7 @@ function runTests(transitionDuration) { }).then(function() { p1 = getPath(); d1 = p1.getAttribute('d'); + s1 = p1.getAttribute('style'); return Plots.transition(gd, null, { 'shapes[0].x0': 1, @@ -160,11 +161,26 @@ function runTests(transitionDuration) { }).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); }); From 43b95d8b5e93ae3755a8b60af75c02d204abdb09 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 25 Oct 2016 17:16:30 -0400 Subject: [PATCH 9/9] Swap image source --- test/jasmine/tests/transition_test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index 90675bc985a..36c7ec0f045 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -93,8 +93,8 @@ function runTests(transitionDuration) { }); it('transitions an image', function(done) { - var whitepx = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; - var blackpx = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; + 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(); @@ -104,12 +104,12 @@ function runTests(transitionDuration) { } var p1, p2, e1, e2; - Plotly.relayout(gd, {images: [{x: 0, y: 0, source: whitepx}]}).then(function() { + Plotly.relayout(gd, {images: [{x: 0, y: 0, source: jsLogo}]}).then(function() { p1 = imagesrc(); e1 = imageel(); return Plots.transition(gd, null, { - 'images[0].source': blackpx, + 'images[0].source': pythonLogo, }, [], {redraw: true, duration: transitionDuration}, {duration: transitionDuration, easing: 'cubic-in-out'}