diff --git a/src/components/images/attributes.js b/src/components/images/attributes.js new file mode 100644 index 00000000000..647333b4b91 --- /dev/null +++ b/src/components/images/attributes.js @@ -0,0 +1,158 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var cartesianConstants = require('../../plots/cartesian/constants'); + + +module.exports = { + _isLinkedToArray: true, + + source: { + valType: 'string', + role: 'info', + description: [ + 'Specifies the URL of the image to be used.', + 'The URL must be accessible from the domain where the', + 'plot code is run, and can be either relative or absolute.' + + ].join(' ') + }, + + layer: { + valType: 'enumerated', + values: ['below', 'above'], + dflt: 'above', + role: 'info', + description: [ + 'Specifies whether images are drawn below or above traces.', + 'When `xref` and `yref` are both set to `paper`,', + 'image is drawn below the entire plot area.' + ].join(' ') + }, + + width: { + valType: 'number', + role: 'info', + dflt: 0, + description: [ + 'Sets the image container width.', + 'The image will be sized based on the `position` value.', + 'When `xref` is set to `paper`, units are sized relative', + 'to the plot width.' + ].join(' ') + }, + + height: { + valType: 'number', + role: 'info', + dflt: 0, + description: [ + 'Sets the image container height.', + 'The image will be sized based on the `position` value.', + 'When `yref` is set to `paper`, units are sized relative', + 'to the plot height.' + ].join(' ') + }, + + sizing: { + valType: 'enumerated', + values: ['fill', 'contain', 'stretch'], + dflt: 'contain', + role: 'info', + description: [ + 'Specifies which dimension of the image to constrain.' + ].join(' ') + }, + + opacity: { + valType: 'number', + role: 'info', + min: 0, + max: 1, + dflt: 1, + description: 'Sets the opacity of the image.' + }, + + x: { + valType: 'number', + role: 'info', + dflt: 0, + description: [ + 'Sets the image\'s x position.', + 'When `xref` is set to `paper`, units are sized relative', + 'to the plot height.', + 'See `xref` for more info' + ].join(' ') + }, + + y: { + valType: 'number', + role: 'info', + dflt: 0, + description: [ + 'Sets the image\'s y position.', + 'When `yref` is set to `paper`, units are sized relative', + 'to the plot height.', + 'See `yref` for more info' + ].join(' ') + }, + + xanchor: { + valType: 'enumerated', + values: ['left', 'center', 'right'], + dflt: 'left', + role: 'info', + description: 'Sets the anchor for the x position' + }, + + yanchor: { + valType: 'enumerated', + values: ['top', 'middle', 'bottom'], + dflt: 'top', + role: 'info', + description: 'Sets the anchor for the y position.' + }, + + xref: { + valType: 'enumerated', + values: [ + 'paper', + cartesianConstants.idRegex.x.toString() + ], + dflt: 'paper', + role: 'info', + description: [ + 'Sets the images\'s x coordinate axis.', + 'If set to a x axis id (e.g. *x* or *x2*), the `x` position', + 'refers to an x data coordinate', + 'If set to *paper*, the `x` position refers to the distance from', + 'the left of plot in normalized coordinates', + 'where *0* (*1*) corresponds to the left (right).' + ].join(' ') + }, + + yref: { + valType: 'enumerated', + values: [ + 'paper', + cartesianConstants.idRegex.y.toString() + ], + dflt: 'paper', + role: 'info', + description: [ + 'Sets the images\'s y coordinate axis.', + 'If set to a y axis id (e.g. *y* or *y2*), the `y` position', + 'refers to a y data coordinate.', + 'If set to *paper*, the `y` position refers to the distance from', + 'the bottom of the plot in normalized coordinates', + 'where *0* (*1*) corresponds to the bottom (top).' + ].join(' ') + } +}; diff --git a/src/components/images/defaults.js b/src/components/images/defaults.js new file mode 100644 index 00000000000..61b70b8fb6d --- /dev/null +++ b/src/components/images/defaults.js @@ -0,0 +1,64 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Axes = require('../../plots/cartesian/axes'); +var Lib = require('../../lib'); +var attributes = require('./attributes'); + + +module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { + + if(!layoutIn.images || !Array.isArray(layoutIn.images)) return; + + + var containerIn = layoutIn.images, + containerOut = layoutOut.images = []; + + + for(var i = 0; i < containerIn.length; i++) { + var image = containerIn[i]; + + if(!image.source) continue; + + var defaulted = imageDefaults(containerIn[i] || {}, containerOut[i] || {}, layoutOut); + containerOut.push(defaulted); + } +}; + + +function imageDefaults(imageIn, imageOut, fullLayout) { + + imageOut = imageOut || {}; + + function coerce(attr, dflt) { + return Lib.coerce(imageIn, imageOut, attributes, attr, dflt); + } + + coerce('source'); + coerce('layer'); + coerce('x'); + coerce('y'); + coerce('xanchor'); + coerce('yanchor'); + coerce('width'); + coerce('height'); + coerce('sizing'); + coerce('opacity'); + + for(var i = 0; i < 2; i++) { + var tdMock = { _fullLayout: fullLayout }, + axLetter = ['x', 'y'][i]; + + // 'paper' is the fallback axref + Axes.coerceRef(imageIn, imageOut, tdMock, axLetter, 'paper'); + } + + return imageOut; +} diff --git a/src/components/images/draw.js b/src/components/images/draw.js new file mode 100644 index 00000000000..d90275658f6 --- /dev/null +++ b/src/components/images/draw.js @@ -0,0 +1,140 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var d3 = require('d3'); +var Drawing = require('../drawing'); +var Axes = require('../../plots/cartesian/axes'); + +module.exports = function draw(gd) { + + var fullLayout = gd._fullLayout, + imageDataAbove = [], + imageDataSubplot = [], + imageDataBelow = []; + + if(!fullLayout.images) return; + + + // Sort into top, subplot, and bottom layers + for(var i = 0; i < fullLayout.images.length; i++) { + var img = fullLayout.images[i]; + + if(img.layer === 'below' && img.xref !== 'paper' && img.yref !== 'paper') { + imageDataSubplot.push(img); + } else if(img.layer === 'above') { + imageDataAbove.push(img); + } else { + imageDataBelow.push(img); + } + } + + + var anchors = { + x: { + left: { sizing: 'xMin', offset: 0 }, + center: { sizing: 'xMid', offset: -1 / 2 }, + right: { sizing: 'xMax', offset: -1 } + }, + y: { + top: { sizing: 'YMin', offset: 0 }, + middle: { sizing: 'YMid', offset: -1 / 2 }, + bottom: { sizing: 'YMax', offset: -1 } + } + }; + + + function applyAttributes(d) { + + var thisImage = d3.select(this); + + // Axes if specified + var xref = Axes.getFromId(gd, d.xref), + yref = Axes.getFromId(gd, d.yref); + + var size = fullLayout._size, + width = xref ? Math.abs(xref.l2p(d.width) - xref.l2p(0)) : d.width * size.w, + height = yref ? Math.abs(yref.l2p(d.height) - yref.l2p(0)) : d.width * size.h; + + // Offsets for anchor positioning + var xOffset = width * anchors.x[d.xanchor].offset + size.l, + yOffset = height * anchors.y[d.yanchor].offset + size.t; + + var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing; + + // Final positions + var xPos = (xref ? xref.l2p(d.x) : d.x * width) + xOffset, + yPos = (yref ? yref.l2p(d.y) : -d.y * width) + yOffset; + + + // Construct the proper aspectRatio attribute + switch(d.sizing) { + case 'fill': + sizing += ' slice'; + break; + + case 'stretch': + sizing = 'none'; + break; + } + + thisImage.attr({ + x: xPos, + y: yPos, + width: width, + height: height, + preserveAspectRatio: sizing, + opacity: d.opacity + }); + + + // Images load async so we must add the promise to the list + var imagePromise = new Promise(function(resolve) { + + thisImage.on('load', resolve); + thisImage.on('error', function() { + thisImage.remove(); + console.log('Image with source ' + d.source + ' could not be loaded.'); + resolve(); + }); + + thisImage.attr('href', d.source); + }); + + gd._promises.push(imagePromise); + + + // Set proper clipping on images + var xId = xref ? xref._id : '', + yId = yref ? yref._id : '', + clipAxes = xId + yId; + + thisImage.call(Drawing.setClipUrl, 'clip' + fullLayout._uid + clipAxes); + } + + + var imagesBelow = fullLayout._imageLowerLayer.selectAll('image') + .data(imageDataBelow), + imagesSubplot = fullLayout._imageSubplotLayer.selectAll('image') + .data(imageDataSubplot), + imagesAbove = fullLayout._imageUpperLayer.selectAll('image') + .data(imageDataAbove); + + 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); +}; diff --git a/src/components/images/index.js b/src/components/images/index.js new file mode 100644 index 00000000000..9ea6816a08a --- /dev/null +++ b/src/components/images/index.js @@ -0,0 +1,19 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + + +var draw = require('./draw'); +var supplyLayoutDefaults = require('./defaults'); + + +module.exports = { + draw: draw, + supplyLayoutDefaults: supplyLayoutDefaults +}; diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index f0d0efeebd7..0095c7b243e 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -21,8 +21,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, axName, coun containerOut = layoutOut[axName].rangeslider = {}; function coerce(attr, dflt) { - return Lib.coerce(containerIn, containerOut, - attributes, attr, dflt); + return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); } coerce('bgcolor'); diff --git a/src/components/shapes/index.js b/src/components/shapes/index.js index a3eebd52ff9..0e493e7a287 100644 --- a/src/components/shapes/index.js +++ b/src/components/shapes/index.js @@ -98,7 +98,7 @@ shapes.drawAll = function(gd) { // Remove previous shapes before drawing new in shapes in fullLayout.shapes fullLayout._shapeUpperLayer.selectAll('path').remove(); fullLayout._shapeLowerLayer.selectAll('path').remove(); - fullLayout._subplotShapeLayer.selectAll('path').remove(); + fullLayout._shapeSubplotLayer.selectAll('path').remove(); for(var i = 0; i < fullLayout.shapes.length; i++) { shapes.draw(gd, i); @@ -356,7 +356,7 @@ function getShapeLayer(gd, index) { else if(shape.layer === 'below') { shapeLayer = (shape.xref === 'paper' && shape.yref === 'paper') ? gd._fullLayout._shapeLowerLayer : - gd._fullLayout._subplotShapeLayer; + gd._fullLayout._shapeSubplotLayer; } return shapeLayer; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 74e751b1030..c9a8f1234cf 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -24,6 +24,7 @@ var Fx = require('../plots/cartesian/graph_interact'); var Color = require('../components/color'); var Drawing = require('../components/drawing'); var ErrorBars = require('../components/errorbars'); +var Images = require('../components/images'); var Legend = require('../components/legend'); var RangeSlider = require('../components/rangeslider'); var RangeSelector = require('../components/rangeselector'); @@ -307,6 +308,7 @@ Plotly.plot = function(gd, data, layout, config) { // be set to false before these will work properly. function finalDraw() { Shapes.drawAll(gd); + Images.draw(gd); Plotly.Annotations.drawAll(gd); Legend.draw(gd); RangeSlider.draw(gd); @@ -2652,6 +2654,8 @@ function makePlotFramework(gd) { .classed('layer-below', true); fullLayout._shapeLowerLayer = layerBelow.append('g') .classed('shapelayer', true); + fullLayout._imageLowerLayer = layerBelow.append('g') + .classed('imagelayer', true); var subplots = Plotly.Axes.getSubplots(gd); if(subplots.join('') !== Object.keys(gd._fullLayout._plots || {}).join('')) { @@ -2664,8 +2668,9 @@ function makePlotFramework(gd) { fullLayout._ternarylayer = fullLayout._paper.append('g').classed('ternarylayer', true); // shape layers in subplots - fullLayout._subplotShapeLayer = fullLayout._paper - .selectAll('.shapelayer-subplot'); + var layerSubplot = fullLayout._paper.selectAll('.layer-subplot'); + fullLayout._shapeSubplotLayer = layerSubplot.selectAll('.shapelayer'); + fullLayout._imageSubplotLayer = layerSubplot.selectAll('.imagelayer'); // upper shape layer // (only for shapes to be drawn above the whole plot, including subplots) @@ -2673,6 +2678,8 @@ function makePlotFramework(gd) { .classed('layer-above', true); fullLayout._shapeUpperLayer = layerAbove.append('g') .classed('shapelayer', true); + fullLayout._imageUpperLayer = layerAbove.append('g') + .classed('imagelayer', true); // single pie layer for the whole plot fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true); @@ -2803,10 +2810,16 @@ function makeCartesianPlotFramwork(gd, subplots) { // the plot and containers for overlays plotinfo.bg = plotgroup.append('rect') .style('stroke-width', 0); - // shape layer - // (only for shapes to be drawn below a subplot) - plotinfo.shapelayer = plotgroup.append('g') - .classed('shapelayer shapelayer-subplot', true); + + // back layer for shapes and images to + // be drawn below a subplot + var backlayer = plotgroup.append('g') + .classed('layer-subplot', true); + + plotinfo.shapelayer = backlayer.append('g') + .classed('shapelayer', true); + plotinfo.imagelayer = backlayer.append('g') + .classed('imagelayer', true); plotinfo.gridlayer = plotgroup.append('g'); plotinfo.overgrid = plotgroup.append('g'); plotinfo.zerolinelayer = plotgroup.append('g'); diff --git a/src/plotly.js b/src/plotly.js index 3ae37158882..5ceb2019839 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -49,6 +49,7 @@ exports.ErrorBars = require('./components/errorbars'); exports.Annotations = require('./components/annotations'); exports.Shapes = require('./components/shapes'); exports.Legend = require('./components/legend'); +exports.Images = require('./components/images'); exports.ModeBar = require('./components/modebar'); exports.register = function register(_modules) { diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 4440e2b54d6..7278caf2d95 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -38,7 +38,7 @@ axes.getFromTrace = axisIds.getFromTrace; // find the list of possible axes to reference with an xref or yref attribute // and coerce it to that list -axes.coerceRef = function(containerIn, containerOut, gd, axLetter) { +axes.coerceRef = function(containerIn, containerOut, gd, axLetter, dflt) { var axlist = gd._fullLayout._hasGL2D ? [] : axes.listIds(gd, axLetter), refAttr = axLetter + 'ref', attrDef = {}; @@ -48,7 +48,7 @@ axes.coerceRef = function(containerIn, containerOut, gd, axLetter) { attrDef[refAttr] = { valType: 'enumerated', values: axlist.concat(['paper']), - dflt: axlist[0] || 'paper' + dflt: dflt || axlist[0] || 'paper' }; // xref, yref diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index a539e3d7e1b..6dc13c6ad01 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -520,6 +520,7 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { redrawObjs(fullLayout.annotations || [], Plotly.Annotations); redrawObjs(fullLayout.shapes || [], Plotly.Shapes); + redrawObjs(fullLayout.images || [], Plotly.Images); } function doubleClick() { diff --git a/src/plots/plots.js b/src/plots/plots.js index 271c18374cf..4c355b98617 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -750,7 +750,7 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { // TODO register these // Legend must come after traces (e.g. it depends on 'barmode') - var moduleLayoutDefaults = ['Fx', 'Annotations', 'Shapes', 'Legend']; + var moduleLayoutDefaults = ['Fx', 'Annotations', 'Shapes', 'Legend', 'Images']; for(i = 0; i < moduleLayoutDefaults.length; i++) { _module = moduleLayoutDefaults[i]; diff --git a/src/traces/heatmap/style.js b/src/traces/heatmap/style.js index 2a466860377..2d1fdb3f431 100644 --- a/src/traces/heatmap/style.js +++ b/src/traces/heatmap/style.js @@ -12,7 +12,7 @@ var d3 = require('d3'); module.exports = function style(gd) { - d3.select(gd).selectAll('image') + d3.select(gd).selectAll('.hm image') .style('opacity', function(d) { return d.trace.opacity; }); diff --git a/test/image/baselines/layout_image.png b/test/image/baselines/layout_image.png new file mode 100644 index 00000000000..6f9bf42d0f6 Binary files /dev/null and b/test/image/baselines/layout_image.png differ diff --git a/test/image/mocks/layout_image.json b/test/image/mocks/layout_image.json new file mode 100644 index 00000000000..ee6ada23d03 --- /dev/null +++ b/test/image/mocks/layout_image.json @@ -0,0 +1,60 @@ +{ + "data": [ + { + "x": [1,2,3], + "y": [1,2,3] + } + ], + "layout": { + "images": [ + { + "source": "http://www.placekitten.com/100", + "xref": "paper", + "yref": "paper", + "x": 0, + "y": 0, + "width": 0.2, + "height": 0.2, + "xanchor": "right", + "yanchor": "bottom" + }, + { + "source": "http://www.placekitten.com/300", + "xref": "x", + "yref": "y", + "x": 2.5, + "y": 2, + "width": 1, + "height": 1, + "xanchor": "center", + "yanchor": "middle", + "layer": "below" + }, + { + "source": "http://www.placekitten.com/400", + "xref": "x", + "yref": "y", + "x": 1, + "y": 3, + "width": 2, + "height": 2, + "sizing": "stretch", + "opacity": 0.4 + }, + { + "source": "http://www.placekitten.com/300", + "xref": "x", + "yref": "paper", + "x": 2.75, + "y": 0, + "width": 0.5, + "height": 1, + "opacity": 1, + "xanchor": "left", + "yanchor": "top" + } + ], + "width": 800, + "height": 500 + } +} diff --git a/test/jasmine/tests/layout_images_test.js b/test/jasmine/tests/layout_images_test.js new file mode 100644 index 00000000000..33a67b439f5 --- /dev/null +++ b/test/jasmine/tests/layout_images_test.js @@ -0,0 +1,230 @@ +var Plotly = require('@lib/index'); +var Images = require('@src/components/images'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var mouseEvent = require('../assets/mouse_event'); + +describe('Layout images', function() { + + describe('supplyLayoutDefaults', function() { + + var layoutIn, + layoutOut; + + beforeEach(function() { + layoutIn = { images: [] }; + layoutOut = {}; + }); + + it('should reject when there is no `source`', function() { + layoutIn.images[0] = { opacity: 0.5, width: 0.2, height: 0.2 }; + + Images.supplyLayoutDefaults(layoutIn, layoutOut); + + expect(layoutOut.images.length).toEqual(0); + }); + + it('should reject when not an array', function() { + layoutIn.images = { + source: 'http://www.someimagesource.com', + opacity: 0.5, + width: 0.2, + height: 0.2 + }; + + Images.supplyLayoutDefaults(layoutIn, layoutOut); + + expect(layoutOut.images).not.toBeDefined(); + }); + + it('should coerce the correct defaults', function() { + layoutIn.images[0] = { source: 'http://www.someimagesource.com' }; + + var expected = { + source: 'http://www.someimagesource.com', + layer: 'above', + x: 0, + y: 0, + xanchor: 'left', + yanchor: 'top', + width: 0, + height: 0, + sizing: 'contain', + opacity: 1, + xref: 'paper', + yref: 'paper' + }; + + Images.supplyLayoutDefaults(layoutIn, layoutOut); + + expect(layoutOut.images[0]).toEqual(expected); + }); + + }); + + describe('drawing', function() { + + var gd, + data = [{ x: [1,2,3], y: [1,2,3] }]; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + it('should draw images on the right layers', function() { + + var layer; + + Plotly.plot(gd, data, { images: [{ + source: 'imageabove', + layer: 'above' + }]}); + + layer = gd._fullLayout._imageUpperLayer; + expect(layer.length).toBe(1); + + destroyGraphDiv(); + gd = createGraphDiv(); + Plotly.plot(gd, data, { images: [{ + source: 'imagebelow', + layer: 'below' + }]}); + + layer = gd._fullLayout._imageLowerLayer; + expect(layer.length).toBe(1); + + destroyGraphDiv(); + gd = createGraphDiv(); + Plotly.plot(gd, data, { images: [{ + source: 'imagesubplot', + layer: 'below', + xref: 'x', + yref: 'y' + }]}); + + layer = gd._fullLayout._imageSubplotLayer; + expect(layer.length).toBe(1); + }); + + describe('with anchors and sizing', function() { + + function testAspectRatio(xAnchor, yAnchor, sizing, expected) { + var anchorName = xAnchor + yAnchor; + Plotly.plot(gd, data, { images: [{ + source: anchorName, + xanchor: xAnchor, + yanchor: yAnchor, + sizing: sizing + }]}); + + var image = Plotly.d3.select('[href="' + anchorName + '"]'), + parValue = image.attr('preserveAspectRatio'); + + expect(parValue).toBe(expected); + } + + it('should work for center middle', function() { + testAspectRatio('center', 'middle', undefined, 'xMidYMid'); + }); + + it('should work for left top', function() { + testAspectRatio('left', 'top', undefined, 'xMinYMin'); + }); + + it('should work for right bottom', function() { + testAspectRatio('right', 'bottom', undefined, 'xMaxYMax'); + }); + + it('should work for stretch sizing', function() { + testAspectRatio('middle', 'center', 'stretch', 'none'); + }); + + it('should work for fill sizing', function() { + testAspectRatio('invalid', 'invalid', 'fill', 'xMinYMin slice'); + }); + + }); + + }); + + describe('when the plot is dragged', function() { + var gd, + data = [{ x: [1,2,3], y: [1,2,3] }]; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + it('should not move when referencing the paper', function(done) { + var source = 'http://www.placekitten.com/200', + image = { + source: source, + xref: 'paper', + yref: 'paper', + x: 0, + y: 0, + width: 0.1, + height: 0.1 + }; + + Plotly.plot(gd, data, { + images: [image], + dragmode: 'pan', + width: 600, + height: 400 + }).then(function() { + var img = Plotly.d3.select('[href="' + source + '"]').node(), + oldPos = img.getBoundingClientRect(); + + mouseEvent('mousedown', 250, 200); + mouseEvent('mousemove', 300, 250); + + var newPos = img.getBoundingClientRect(); + + expect(newPos.left).toBe(oldPos.left); + expect(newPos.top).toBe(oldPos.top); + + mouseEvent('mouseup', 300, 250); + }).then(done); + }); + + it('should move when referencing axes', function(done) { + var source = 'http://www.placekitten.com/200', + image = { + source: source, + xref: 'x', + yref: 'y', + x: 2, + y: 2, + width: 1, + height: 1 + }; + + Plotly.plot(gd, data, { + images: [image], + dragmode: 'pan', + width: 600, + height: 400 + }).then(function() { + var img = Plotly.d3.select('[href="' + source + '"]').node(), + oldPos = img.getBoundingClientRect(); + + mouseEvent('mousedown', 250, 200); + mouseEvent('mousemove', 300, 250); + + var newPos = img.getBoundingClientRect(); + + expect(newPos.left).toBe(oldPos.left + 50); + expect(newPos.top).toBe(oldPos.top + 50); + + mouseEvent('mouseup', 300, 250); + }).then(done); + }); + + }); + +}); diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 74993cf628f..c553b55a396 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -58,7 +58,7 @@ describe('Test shapes:', function() { } function countShapeLayerNodesInSubplots() { - return d3.selectAll('.shapelayer-subplot').size(); + return d3.selectAll('.layer-subplot').size(); } function countSubplots(gd) { @@ -74,7 +74,7 @@ describe('Test shapes:', function() { } function countShapePathsInSubplots() { - return d3.selectAll('.shapelayer-subplot > path').size(); + return d3.selectAll('.layer-subplot > .shapelayer > path').size(); } describe('*shapeLowerLayer*', function() {