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/src/plots/plots.js b/src/plots/plots.js index 6ca52d59d4e..61c6e3e1354 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 @@ -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); + }); + } + + // 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; @@ -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); } diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index 86348f394e2..36c7ec0f045 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -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); + }); + + it('transitions a transform', function(done) { Plotly.restyle(gd, { 'transforms[0]': {