diff --git a/src/components/images/draw.js b/src/components/images/draw.js index 52d1e6456ca..8242e72f23f 100644 --- a/src/components/images/draw.js +++ b/src/components/images/draw.js @@ -30,6 +30,15 @@ module.exports = function draw(gd) { subplot = img.xref + img.yref; var plotinfo = fullLayout._plots[subplot]; + + if(!plotinfo) { + // Fall back to _imageLowerLayer in case the requested subplot doesn't exist. + // This can happen if you reference the image to an x / y axis combination + // that doesn't have any data on it (and layer is below) + imageDataBelow.push(img); + continue; + } + if(plotinfo.mainplot) { subplot = plotinfo.mainplot.id; } diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 355b5db2935..2b757ee0a59 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -75,10 +75,17 @@ function drawOne(gd, index) { drawShape(gd._fullLayout._shapeLowerLayer); } else { - var plotinfo = gd._fullLayout._plots[options.xref + options.yref], - mainPlot = plotinfo.mainplot || plotinfo; - - drawShape(mainPlot.shapelayer); + var plotinfo = gd._fullLayout._plots[options.xref + options.yref]; + if(plotinfo) { + var mainPlot = plotinfo.mainplot || plotinfo; + drawShape(mainPlot.shapelayer); + } + else { + // Fall back to _shapeLowerLayer in case the requested subplot doesn't exist. + // This can happen if you reference the shape to an x / y axis combination + // that doesn't have any data on it (and layer is below) + drawShape(gd._fullLayout._shapeLowerLayer); + } } function drawShape(shapeLayer) { diff --git a/test/jasmine/tests/layout_images_test.js b/test/jasmine/tests/layout_images_test.js index f571a4e969c..fb0b2743732 100644 --- a/test/jasmine/tests/layout_images_test.js +++ b/test/jasmine/tests/layout_images_test.js @@ -107,7 +107,7 @@ describe('Layout images', function() { it('should draw images on the right layers', function() { Plotly.plot(gd, data, { images: [{ - source: 'imageabove', + source: jsLogo, layer: 'above' }]}); @@ -116,7 +116,7 @@ describe('Layout images', function() { destroyGraphDiv(); gd = createGraphDiv(); Plotly.plot(gd, data, { images: [{ - source: 'imagebelow', + source: jsLogo, layer: 'below' }]}); @@ -125,7 +125,7 @@ describe('Layout images', function() { destroyGraphDiv(); gd = createGraphDiv(); Plotly.plot(gd, data, { images: [{ - source: 'imagesubplot', + source: jsLogo, layer: 'below', xref: 'x', yref: 'y' @@ -134,12 +134,36 @@ describe('Layout images', function() { checkLayers(0, 0, 1); }); + it('should fall back on imageLowerLayer for below missing subplots', function() { + Plotly.newPlot(gd, [ + {x: [1, 3], y: [1, 3]}, + {x: [1, 3], y: [1, 3], xaxis: 'x2', yaxis: 'y2'} + ], { + xaxis: {domain: [0, 0.5]}, + yaxis: {domain: [0, 0.5]}, + xaxis2: {domain: [0.5, 1], anchor: 'y2'}, + yaxis2: {domain: [0.5, 1], anchor: 'x2'}, + images: [{ + source: jsLogo, + layer: 'below', + xref: 'x', + yref: 'y2' + }, { + source: jsLogo, + layer: 'below', + xref: 'x2', + yref: 'y' + }] + }); + + checkLayers(0, 2, 0); + }); + describe('with anchors and sizing', function() { function testAspectRatio(xAnchor, yAnchor, sizing, expected) { - var anchorName = xAnchor + yAnchor; Plotly.plot(gd, data, { images: [{ - source: anchorName, + source: jsLogo, xanchor: xAnchor, yanchor: yAnchor, sizing: sizing @@ -405,7 +429,7 @@ describe('images log/linear axis changes', function() { ], layout: { images: [{ - source: 'https://images.plot.ly/language-icons/api-home/python-logo.png', + source: pythonLogo, x: 1, y: 1, xref: 'x', diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 8be69644d63..dc0e197f895 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -103,75 +103,75 @@ describe('Test shapes defaults:', function() { }); }); -describe('Test shapes:', function() { - 'use strict'; +function countShapesInLowerLayer(gd) { + return gd._fullLayout.shapes.filter(isShapeInLowerLayer).length; +} - var mock = require('@mocks/shapes.json'); - var gd; +function countShapesInUpperLayer(gd) { + return gd._fullLayout.shapes.filter(isShapeInUpperLayer).length; +} - beforeEach(function(done) { - gd = createGraphDiv(); +function countShapesInSubplots(gd) { + return gd._fullLayout.shapes.filter(isShapeInSubplot).length; +} - var mockData = Lib.extendDeep([], mock.data), - mockLayout = Lib.extendDeep({}, mock.layout); +function isShapeInUpperLayer(shape) { + return shape.layer !== 'below'; +} - Plotly.plot(gd, mockData, mockLayout).then(done); - }); +function isShapeInLowerLayer(shape) { + return (shape.xref === 'paper' && shape.yref === 'paper') && + !isShapeInUpperLayer(shape); +} - afterEach(destroyGraphDiv); +function isShapeInSubplot(shape) { + return !isShapeInUpperLayer(shape) && !isShapeInLowerLayer(shape); +} - function countShapesInLowerLayer() { - return gd._fullLayout.shapes.filter(isShapeInLowerLayer).length; - } +function countShapeLowerLayerNodes() { + return d3.selectAll('.layer-below > .shapelayer').size(); +} - function countShapesInUpperLayer() { - return gd._fullLayout.shapes.filter(isShapeInUpperLayer).length; - } +function countShapeUpperLayerNodes() { + return d3.selectAll('.layer-above > .shapelayer').size(); +} - function countShapesInSubplots() { - return gd._fullLayout.shapes.filter(isShapeInSubplot).length; - } +function countShapeLayerNodesInSubplots() { + return d3.selectAll('.layer-subplot').size(); +} - function isShapeInUpperLayer(shape) { - return shape.layer !== 'below'; - } +function countSubplots(gd) { + return Object.keys(gd._fullLayout._plots || {}).length; +} - function isShapeInLowerLayer(shape) { - return (shape.xref === 'paper' && shape.yref === 'paper') && - !isShapeInUpperLayer(shape); - } +function countShapePathsInLowerLayer() { + return d3.selectAll('.layer-below > .shapelayer > path').size(); +} - function isShapeInSubplot(shape) { - return !isShapeInUpperLayer(shape) && !isShapeInLowerLayer(shape); - } +function countShapePathsInUpperLayer() { + return d3.selectAll('.layer-above > .shapelayer > path').size(); +} - function countShapeLowerLayerNodes() { - return d3.selectAll('.layer-below > .shapelayer').size(); - } +function countShapePathsInSubplots() { + return d3.selectAll('.layer-subplot > .shapelayer > path').size(); +} - function countShapeUpperLayerNodes() { - return d3.selectAll('.layer-above > .shapelayer').size(); - } +describe('Test shapes:', function() { + 'use strict'; - function countShapeLayerNodesInSubplots() { - return d3.selectAll('.layer-subplot').size(); - } + var mock = require('@mocks/shapes.json'); + var gd; - function countSubplots(gd) { - return Object.keys(gd._fullLayout._plots || {}).length; - } + beforeEach(function(done) { + gd = createGraphDiv(); - function countShapePathsInLowerLayer() { - return d3.selectAll('.layer-below > .shapelayer > path').size(); - } + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); - function countShapePathsInUpperLayer() { - return d3.selectAll('.layer-above > .shapelayer > path').size(); - } + Plotly.plot(gd, mockData, mockLayout).then(done); + }); - function countShapePathsInSubplots() { - return d3.selectAll('.layer-subplot > .shapelayer > path').size(); - } + afterEach(destroyGraphDiv); describe('*shapeLowerLayer*', function() { it('has one node', function() { @@ -180,14 +180,14 @@ describe('Test shapes:', function() { it('has as many *path* nodes as shapes in the lower layer', function() { expect(countShapePathsInLowerLayer()) - .toEqual(countShapesInLowerLayer()); + .toEqual(countShapesInLowerLayer(gd)); }); it('should be able to get relayout', function(done) { Plotly.relayout(gd, {height: 200, width: 400}).then(function() { expect(countShapeLowerLayerNodes()).toEqual(1); expect(countShapePathsInLowerLayer()) - .toEqual(countShapesInLowerLayer()); + .toEqual(countShapesInLowerLayer(gd)); }) .catch(failTest) .then(done); @@ -201,14 +201,14 @@ describe('Test shapes:', function() { it('has as many *path* nodes as shapes in the upper layer', function() { expect(countShapePathsInUpperLayer()) - .toEqual(countShapesInUpperLayer()); + .toEqual(countShapesInUpperLayer(gd)); }); it('should be able to get relayout', function(done) { Plotly.relayout(gd, {height: 200, width: 400}).then(function() { expect(countShapeUpperLayerNodes()).toEqual(1); expect(countShapePathsInUpperLayer()) - .toEqual(countShapesInUpperLayer()); + .toEqual(countShapesInUpperLayer(gd)); }) .catch(failTest) .then(done); @@ -223,7 +223,7 @@ describe('Test shapes:', function() { it('has as many *path* nodes as shapes in the subplot', function() { expect(countShapePathsInSubplots()) - .toEqual(countShapesInSubplots()); + .toEqual(countShapesInSubplots(gd)); }); it('should be able to get relayout', function(done) { @@ -231,7 +231,7 @@ describe('Test shapes:', function() { expect(countShapeLayerNodesInSubplots()) .toEqual(countSubplots(gd)); expect(countShapePathsInSubplots()) - .toEqual(countShapesInSubplots()); + .toEqual(countShapesInSubplots(gd)); }) .catch(failTest) .then(done); @@ -464,6 +464,49 @@ describe('shapes axis reference changes', function() { }); }); +describe('shapes edge cases', function() { + 'use strict'; + + var gd; + + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + beforeEach(function() { gd = createGraphDiv(); }); + + afterEach(destroyGraphDiv); + + it('falls back on shapeLowerLayer for below missing subplots', function(done) { + Plotly.newPlot(gd, [ + {x: [1, 3], y: [1, 3]}, + {x: [1, 3], y: [1, 3], xaxis: 'x2', yaxis: 'y2'} + ], { + xaxis: {domain: [0, 0.5]}, + yaxis: {domain: [0, 0.5]}, + xaxis2: {domain: [0.5, 1], anchor: 'y2'}, + yaxis2: {domain: [0.5, 1], anchor: 'x2'}, + shapes: [{ + x0: 1, x1: 2, y0: 1, y1: 2, type: 'circle', + layer: 'below', + xref: 'x', + yref: 'y2' + }, { + x0: 1, x1: 2, y0: 1, y1: 2, type: 'circle', + layer: 'below', + xref: 'x2', + yref: 'y' + }] + }).then(function() { + expect(countShapePathsInLowerLayer()).toBe(2); + expect(countShapePathsInUpperLayer()).toBe(0); + expect(countShapePathsInSubplots()).toBe(0); + }) + .catch(failTest) + .then(done); + }); +}); + describe('shapes autosize', function() { 'use strict';