From 8278abef75caca0fdc89fc0499e0514abe583c1a Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 30 Apr 2026 14:57:23 -0600 Subject: [PATCH 1/6] Linting/formatting --- test/jasmine/tests/shapes_test.js | 2044 +++++++++++++++-------------- 1 file changed, 1075 insertions(+), 969 deletions(-) diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index a0a398d5f4f..60284c2a54f 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -19,11 +19,11 @@ var assertElemTopsAligned = customAssertions.assertElemTopsAligned; var assertElemInside = customAssertions.assertElemInside; // Reusable vars -var shapeTypes = [{type: 'rect'}, {type: 'circle'}, {type: 'line'}]; +var shapeTypes = [{ type: 'rect' }, { type: 'circle' }, { type: 'line' }]; var resizeDirections = ['n', 's', 'w', 'e', 'nw', 'se', 'ne', 'sw']; var resizeTypes = [ - {resizeType: 'shrink', resizeDisplayName: 'shrunken'}, - {resizeType: 'enlarge', resizeDisplayName: 'enlarged'} + { resizeType: 'shrink', resizeDisplayName: 'shrunken' }, + { resizeType: 'enlarge', resizeDisplayName: 'enlarged' } ]; var dxToShrinkWidth = { n: 0, s: 0, w: 10, e: -10, nw: 10, se: -10, ne: -10, sw: 10 }; var dyToShrinkHeight = { n: 10, s: -10, w: 0, e: 0, nw: 10, se: -10, ne: 10, sw: -10 }; @@ -38,15 +38,19 @@ function getMoveLineDragElement(index) { function getResizeLineOverStartPointElement(index) { index = index || 0; - return d3SelectAll('.shapelayer g[drag-helper][data-index="' + index + '"] circle[data-line-point="start-point"]').node(); + return d3SelectAll( + '.shapelayer g[drag-helper][data-index="' + index + '"] circle[data-line-point="start-point"]' + ).node(); } function getResizeLineOverEndPointElement(index) { index = index || 0; - return d3SelectAll('.shapelayer g[drag-helper][data-index="' + index + '"] circle[data-line-point="end-point"]').node(); + return d3SelectAll( + '.shapelayer g[drag-helper][data-index="' + index + '"] circle[data-line-point="end-point"]' + ).node(); } -describe('Test shapes defaults:', function() { +describe('Test shapes defaults:', function () { 'use strict'; function _supply(layoutIn, layoutOut) { @@ -58,8 +62,8 @@ describe('Test shapes defaults:', function() { return layoutOut.shapes; } - it('should skip non-array containers', function() { - [null, undefined, {}, 'str', 0, false, true].forEach(function(cont) { + it('should skip non-array containers', function () { + [null, undefined, {}, 'str', 0, false, true].forEach(function (cont) { var msg = '- ' + JSON.stringify(cont); var layoutIn = { shapes: cont }; var out = _supply(layoutIn); @@ -69,28 +73,30 @@ describe('Test shapes defaults:', function() { }); }); - it('should make non-object item visible: false', function() { + it('should make non-object item visible: false', function () { var shapes = [null, undefined, [], 'str', 0, false, true]; var layoutIn = { shapes: shapes }; var out = _supply(layoutIn); expect(layoutIn.shapes).toEqual(shapes); - out.forEach(function(item, i) { - expect(item).toEqual(jasmine.objectContaining({ - visible: false, - _index: i - })); + out.forEach(function (item, i) { + expect(item).toEqual( + jasmine.objectContaining({ + visible: false, + _index: i + }) + ); }); }); - it('should provide the right defaults on all axis types', function() { + it('should provide the right defaults on all axis types', function () { var fullLayout = { - xaxis: {type: 'linear', range: [0, 20], _shapeIndices: []}, - yaxis: {type: 'log', range: [1, 5], _shapeIndices: []}, - xaxis2: {type: 'date', range: ['2006-06-05', '2006-06-09'], _shapeIndices: []}, - yaxis2: {type: 'category', range: [-0.5, 7.5], _shapeIndices: []}, - _subplots: {xaxis: ['x', 'x2'], yaxis: ['y', 'y2']} + xaxis: { type: 'linear', range: [0, 20], _shapeIndices: [] }, + yaxis: { type: 'log', range: [1, 5], _shapeIndices: [] }, + xaxis2: { type: 'date', range: ['2006-06-05', '2006-06-09'], _shapeIndices: [] }, + yaxis2: { type: 'category', range: [-0.5, 7.5], _shapeIndices: [] }, + _subplots: { xaxis: ['x', 'x2'], yaxis: ['y', 'y2'] } }; Axes.setConvert(fullLayout.xaxis); @@ -98,8 +104,8 @@ describe('Test shapes defaults:', function() { Axes.setConvert(fullLayout.xaxis2); Axes.setConvert(fullLayout.yaxis2); - var shape1In = {type: 'rect'}; - var shape2In = {type: 'circle', xref: 'x2', yref: 'y2'}; + var shape1In = { type: 'rect' }; + var shape2In = { type: 'circle', xref: 'x2', yref: 'y2' }; var layoutIn = { shapes: [shape1In, shape2In] @@ -127,29 +133,31 @@ describe('Test shapes defaults:', function() { expect(shape2Out.y1).toBeWithin(5.5, 0.001); }); - it('should not coerce line.color and line.dash when line.width is zero', function() { + it('should not coerce line.color and line.dash when line.width is zero', function () { var fullLayout = { - xaxis: {type: 'linear', range: [0, 1], _shapeIndices: []}, - yaxis: {type: 'log', range: [0, 1], _shapeIndices: []}, - _subplots: {xaxis: ['x'], yaxis: ['y']} + xaxis: { type: 'linear', range: [0, 1], _shapeIndices: [] }, + yaxis: { type: 'log', range: [0, 1], _shapeIndices: [] }, + _subplots: { xaxis: ['x'], yaxis: ['y'] } }; Axes.setConvert(fullLayout.xaxis); Axes.setConvert(fullLayout.yaxis); var layoutIn = { - shapes: [{ - type: 'line', - xref: 'xaxis', - yref: 'yaxis', - x0: 0, - x1: 1, - y0: 1, - y1: 10, - line: { - width: 0 + shapes: [ + { + type: 'line', + xref: 'xaxis', + yref: 'yaxis', + x0: 0, + x1: 1, + y0: 1, + y1: 10, + line: { + width: 0 + } } - }] + ] }; var shapes = _supply(layoutIn, fullLayout); @@ -176,8 +184,7 @@ function isShapeInUpperLayer(shape) { } function isShapeInLowerLayer(shape) { - return (shape.xref === 'paper' && shape.yref === 'paper') && - !isShapeInUpperLayer(shape); + return shape.xref === 'paper' && shape.yref === 'paper' && !isShapeInUpperLayer(shape); } function isShapeInSubplot(shape) { @@ -212,13 +219,13 @@ function countShapePathsInSubplots() { return d3SelectAll('.layer-subplot > .shapelayer > .shape-group > path').size(); } -describe('Test shapes:', function() { +describe('Test shapes:', function () { 'use strict'; var mock = require('../../image/mocks/shapes.json'); var gd; - beforeEach(function(done) { + beforeEach(function (done) { gd = createGraphDiv(); var mockData = Lib.extendDeep([], mock.data); @@ -229,81 +236,69 @@ describe('Test shapes:', function() { afterEach(destroyGraphDiv); - describe('*shapeLowerLayer*', function() { - it('has one node', function() { + describe('*shapeLowerLayer*', function () { + it('has one node', function () { expect(countShapeLowerLayerNodes()).toEqual(1); }); - it('has as many *path* nodes as shapes in the lower layer', function() { - expect(countShapePathsInLowerLayer()) - .toEqual(countShapesInLowerLayer(gd)); + it('has as many *path* nodes as shapes in the lower layer', function () { + expect(countShapePathsInLowerLayer()).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(gd)); - }) - .then(done, done.fail); + 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(gd)); + }) + .then(done, done.fail); }); }); - describe('*shapeUpperLayer*', function() { - it('has one node', function() { + describe('*shapeUpperLayer*', function () { + it('has one node', function () { expect(countShapeUpperLayerNodes()).toEqual(1); }); - it('has as many *path* nodes as shapes in the upper layer', function() { - expect(countShapePathsInUpperLayer()) - .toEqual(countShapesInUpperLayer(gd)); + it('has as many *path* nodes as shapes in the upper layer', function () { + expect(countShapePathsInUpperLayer()).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(gd)); - }) - .then(done, done.fail); + 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(gd)); + }) + .then(done, done.fail); }); }); - describe('each *subplot*', function() { - it('has one *shapelayer*', function() { - expect(countShapeLayerNodesInSubplots()) - .toEqual(countSubplots(gd)); + describe('each *subplot*', function () { + it('has one *shapelayer*', function () { + expect(countShapeLayerNodesInSubplots()).toEqual(countSubplots(gd)); }); - it('has as many *path* nodes as shapes in the subplot', function() { - expect(countShapePathsInSubplots()) - .toEqual(countShapesInSubplots(gd)); + it('has as many *path* nodes as shapes in the subplot', function () { + expect(countShapePathsInSubplots()).toEqual(countShapesInSubplots(gd)); }); - it('should be able to get relayout', function(done) { - Plotly.relayout(gd, {height: 200, width: 400}) - .then(function() { - expect(countShapeLayerNodesInSubplots()) - .toEqual(countSubplots(gd)); - expect(countShapePathsInSubplots()) - .toEqual(countShapesInSubplots(gd)); - }) - .then(done, done.fail); + it('should be able to get relayout', function (done) { + Plotly.relayout(gd, { height: 200, width: 400 }) + .then(function () { + expect(countShapeLayerNodesInSubplots()).toEqual(countSubplots(gd)); + expect(countShapePathsInSubplots()).toEqual(countShapesInSubplots(gd)); + }) + .then(done, done.fail); }); }); function countShapes(gd) { - return gd.layout.shapes ? - gd.layout.shapes.length : - 0; + return gd.layout.shapes ? gd.layout.shapes.length : 0; } function getLastShape(gd) { - return gd.layout.shapes ? - gd.layout.shapes[gd.layout.shapes.length - 1] : - null; + return gd.layout.shapes ? gd.layout.shapes[gd.layout.shapes.length - 1] : null; } Lib.seedPseudoRandom(); @@ -317,106 +312,103 @@ describe('Test shapes:', function() { }; } - describe('Plotly.relayout', function() { - it('should be able to add a shape', function(done) { + describe('Plotly.relayout', function () { + it('should be able to add a shape', function (done) { var pathCount = countShapePathsInUpperLayer(); var index = countShapes(gd); var shape = getRandomShape(); Plotly.relayout(gd, 'shapes[' + index + ']', shape) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 1); + .then(function () { + expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 1); - // add a shape not at the end of the array - return Plotly.relayout(gd, 'shapes[0]', getRandomShape()); - }) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount + 2); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 2); - }) - .then(done, done.fail); + // add a shape not at the end of the array + return Plotly.relayout(gd, 'shapes[0]', getRandomShape()); + }) + .then(function () { + expect(countShapePathsInUpperLayer()).toEqual(pathCount + 2); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 2); + }) + .then(done, done.fail); }); - it('should be able to remove a shape', function(done) { + it('should be able to remove a shape', function (done) { var pathCount = countShapePathsInUpperLayer(); var index = countShapes(gd); var shape = getRandomShape(); Plotly.relayout(gd, 'shapes[' + index + ']', shape) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 1); + .then(function () { + expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 1); - return Plotly.relayout(gd, 'shapes[' + index + ']', 'remove'); - }) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount); - expect(countShapes(gd)).toEqual(index); + return Plotly.relayout(gd, 'shapes[' + index + ']', 'remove'); + }) + .then(function () { + expect(countShapePathsInUpperLayer()).toEqual(pathCount); + expect(countShapes(gd)).toEqual(index); - return Plotly.relayout(gd, 'shapes[2].visible', false); - }) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount - 1); - expect(countShapes(gd)).toEqual(index); + return Plotly.relayout(gd, 'shapes[2].visible', false); + }) + .then(function () { + expect(countShapePathsInUpperLayer()).toEqual(pathCount - 1); + expect(countShapes(gd)).toEqual(index); - return Plotly.relayout(gd, 'shapes[1]', null); - }) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(pathCount - 2); - expect(countShapes(gd)).toEqual(index - 1); - }) - .then(done, done.fail); + return Plotly.relayout(gd, 'shapes[1]', null); + }) + .then(function () { + expect(countShapePathsInUpperLayer()).toEqual(pathCount - 2); + expect(countShapes(gd)).toEqual(index - 1); + }) + .then(done, done.fail); }); - it('should be able to remove all shapes', function(done) { + it('should be able to remove all shapes', function (done) { Plotly.relayout(gd, { shapes: null }) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(0); - expect(countShapePathsInLowerLayer()).toEqual(0); - expect(countShapePathsInSubplots()).toEqual(0); - }) - .then(function() { - return Plotly.relayout(gd, {'shapes[0]': getRandomShape()}); - }) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(1); - expect(countShapePathsInLowerLayer()).toEqual(0); - expect(countShapePathsInSubplots()).toEqual(0); - expect(gd.layout.shapes.length).toBe(1); + .then(function () { + expect(countShapePathsInUpperLayer()).toEqual(0); + expect(countShapePathsInLowerLayer()).toEqual(0); + expect(countShapePathsInSubplots()).toEqual(0); + }) + .then(function () { + return Plotly.relayout(gd, { 'shapes[0]': getRandomShape() }); + }) + .then(function () { + expect(countShapePathsInUpperLayer()).toEqual(1); + expect(countShapePathsInLowerLayer()).toEqual(0); + expect(countShapePathsInSubplots()).toEqual(0); + expect(gd.layout.shapes.length).toBe(1); - return Plotly.relayout(gd, {'shapes[0]': null}); - }) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(0); - expect(countShapePathsInLowerLayer()).toEqual(0); - expect(countShapePathsInSubplots()).toEqual(0); - expect(gd.layout.shapes).toBeUndefined(); - }) - .then(done, done.fail); + return Plotly.relayout(gd, { 'shapes[0]': null }); + }) + .then(function () { + expect(countShapePathsInUpperLayer()).toEqual(0); + expect(countShapePathsInLowerLayer()).toEqual(0); + expect(countShapePathsInSubplots()).toEqual(0); + expect(gd.layout.shapes).toBeUndefined(); + }) + .then(done, done.fail); }); - it('can replace the shapes array', function(done) { + it('can replace the shapes array', function (done) { spyOn(Lib, 'warn'); - Plotly.relayout(gd, { shapes: [ - getRandomShape(), - getRandomShape() - ]}) - .then(function() { - expect(countShapePathsInUpperLayer()).toEqual(2); - expect(countShapePathsInLowerLayer()).toEqual(0); - expect(countShapePathsInSubplots()).toEqual(0); - expect(gd.layout.shapes.length).toBe(2); - expect(Lib.warn).not.toHaveBeenCalled(); - }) - .then(done, done.fail); + Plotly.relayout(gd, { shapes: [getRandomShape(), getRandomShape()] }) + .then(function () { + expect(countShapePathsInUpperLayer()).toEqual(2); + expect(countShapePathsInLowerLayer()).toEqual(0); + expect(countShapePathsInSubplots()).toEqual(0); + expect(gd.layout.shapes.length).toBe(2); + expect(Lib.warn).not.toHaveBeenCalled(); + }) + .then(done, done.fail); }); - it('should be able to update a shape layer', function(done) { + it('should be able to update a shape layer', function (done) { var index = countShapes(gd); var astr = 'shapes[' + index + ']'; var shape = getRandomShape(); @@ -426,62 +418,62 @@ describe('Test shapes:', function() { shape.xref = 'paper'; shape.yref = 'paper'; - Plotly.relayout(gd, astr, shape).then(function() { - expect(countShapePathsInLowerLayer()) - .toEqual(shapesInLowerLayer); - expect(countShapePathsInUpperLayer()) - .toEqual(shapesInUpperLayer + 1); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 1); - }) - .then(function() { - shape.layer = 'below'; - return Plotly.relayout(gd, astr + '.layer', shape.layer); - }) - .then(function() { - expect(countShapePathsInLowerLayer()) - .toEqual(shapesInLowerLayer + 1); - expect(countShapePathsInUpperLayer()) - .toEqual(shapesInUpperLayer); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 1); - }) - .then(function() { - shape.layer = 'above'; - return Plotly.relayout(gd, astr + '.layer', shape.layer); - }) - .then(function() { - expect(countShapePathsInLowerLayer()) - .toEqual(shapesInLowerLayer); - expect(countShapePathsInUpperLayer()) - .toEqual(shapesInUpperLayer + 1); - expect(getLastShape(gd)).toEqual(shape); - expect(countShapes(gd)).toEqual(index + 1); - }) - .then(done, done.fail); + Plotly.relayout(gd, astr, shape) + .then(function () { + expect(countShapePathsInLowerLayer()).toEqual(shapesInLowerLayer); + expect(countShapePathsInUpperLayer()).toEqual(shapesInUpperLayer + 1); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 1); + }) + .then(function () { + shape.layer = 'below'; + return Plotly.relayout(gd, astr + '.layer', shape.layer); + }) + .then(function () { + expect(countShapePathsInLowerLayer()).toEqual(shapesInLowerLayer + 1); + expect(countShapePathsInUpperLayer()).toEqual(shapesInUpperLayer); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 1); + }) + .then(function () { + shape.layer = 'above'; + return Plotly.relayout(gd, astr + '.layer', shape.layer); + }) + .then(function () { + expect(countShapePathsInLowerLayer()).toEqual(shapesInLowerLayer); + expect(countShapePathsInUpperLayer()).toEqual(shapesInUpperLayer + 1); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 1); + }) + .then(done, done.fail); }); }); }); -describe('shapes axis reference changes', function() { +describe('shapes axis reference changes', function () { 'use strict'; var gd; - beforeEach(function(done) { + beforeEach(function (done) { gd = createGraphDiv(); - Plotly.newPlot(gd, [ - {y: [1, 2, 3]}, - {y: [1, 2, 3], yaxis: 'y2'} - ], { - yaxis: {domain: [0, 0.4]}, - yaxis2: {domain: [0.6, 1]}, - shapes: [{ - xref: 'x', yref: 'paper', type: 'rect', - x0: 0.8, x1: 1.2, y0: 0, y1: 1, - fillcolor: '#eee', layer: 'below' - }] + Plotly.newPlot(gd, [{ y: [1, 2, 3] }, { y: [1, 2, 3], yaxis: 'y2' }], { + yaxis: { domain: [0, 0.4] }, + yaxis2: { domain: [0.6, 1] }, + shapes: [ + { + xref: 'x', + yref: 'paper', + type: 'rect', + x0: 0.8, + x1: 1.2, + y0: 0, + y1: 1, + fillcolor: '#eee', + layer: 'below' + } + ] }).then(done); }); @@ -493,7 +485,7 @@ describe('shapes axis reference changes', function() { return s; } - it('draws the right number of objects and updates clip-path correctly', function(done) { + it('draws the right number of objects and updates clip-path correctly', function (done) { expect(getShape(0).attr('clip-path') || '').toMatch(/x\)$/); Plotly.relayout(gd, { @@ -501,70 +493,88 @@ describe('shapes axis reference changes', function() { 'shapes[0].x0': 0.2, 'shapes[0].x1': 0.6 }) - .then(function() { - expect(getShape(0).attr('clip-path')).toBe(null); + .then(function () { + expect(getShape(0).attr('clip-path')).toBe(null); - return Plotly.relayout(gd, { - 'shapes[0].yref': 'y2', - 'shapes[0].y0': 1.8, - 'shapes[0].y1': 2.2, - }); - }) - .then(function() { - expect(getShape(0).attr('clip-path') || '').toMatch(/^[^x]+y2\)$/); + return Plotly.relayout(gd, { + 'shapes[0].yref': 'y2', + 'shapes[0].y0': 1.8, + 'shapes[0].y1': 2.2 + }); + }) + .then(function () { + expect(getShape(0).attr('clip-path') || '').toMatch(/^[^x]+y2\)$/); - return Plotly.relayout(gd, { - 'shapes[0].xref': 'x', - 'shapes[0].x0': 1.5, - 'shapes[0].x1': 20 - }); - }) - .then(function() { - expect(getShape(0).attr('clip-path') || '').toMatch(/xy2\)$/); - }) - .then(done, done.fail); + return Plotly.relayout(gd, { + 'shapes[0].xref': 'x', + 'shapes[0].x0': 1.5, + 'shapes[0].x1': 20 + }); + }) + .then(function () { + expect(getShape(0).attr('clip-path') || '').toMatch(/xy2\)$/); + }) + .then(done, done.fail); }); }); -describe('shapes edge cases', function() { +describe('shapes edge cases', function () { 'use strict'; var gd; - beforeEach(function() { gd = createGraphDiv(); }); + 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); - }) - .then(done, done.fail); + 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); + }) + .then(done, done.fail); }); }); -describe('shapes autosize', function() { +describe('shapes autosize', function () { var gd; afterEach(destroyGraphDiv); @@ -576,94 +586,104 @@ describe('shapes autosize', function() { expect(fullLayout.yaxis.range).toBeCloseToArray(y, PREC, msg + ' - yaxis'); } - it('should adapt to relayout calls', function(done) { + it('should adapt to relayout calls', function (done) { gd = createGraphDiv(); var mock = { data: [{}], layout: { - shapes: [{ - type: 'line', - x0: 0, - y0: 0, - x1: 1, - y1: 1 - }, { - type: 'line', - x0: 0, - y0: 0, - x1: 2, - y1: 2 - }] + shapes: [ + { + type: 'line', + x0: 0, + y0: 0, + x1: 1, + y1: 1 + }, + { + type: 'line', + x0: 0, + y0: 0, + x1: 2, + y1: 2 + } + ] } }; - Plotly.newPlot(gd, mock).then(function() { - assertRanges('base', [0, 2], [0, 2]); - return Plotly.relayout(gd, { 'shapes[1].visible': false }); - }) - .then(function() { - assertRanges('shapes[1] not visible', [0, 1], [0, 1]); + Plotly.newPlot(gd, mock) + .then(function () { + assertRanges('base', [0, 2], [0, 2]); + return Plotly.relayout(gd, { 'shapes[1].visible': false }); + }) + .then(function () { + assertRanges('shapes[1] not visible', [0, 1], [0, 1]); - return Plotly.relayout(gd, { 'shapes[1].visible': true }); - }) - .then(function() { - assertRanges('back to base', [0, 2], [0, 2]); + return Plotly.relayout(gd, { 'shapes[1].visible': true }); + }) + .then(function () { + assertRanges('back to base', [0, 2], [0, 2]); - return Plotly.relayout(gd, { 'shapes[0].x1': 3 }); - }) - .then(function() { - assertRanges('stretched shapes[0]', [0, 3], [0, 2]); - }) - .then(done, done.fail); + return Plotly.relayout(gd, { 'shapes[0].x1': 3 }); + }) + .then(function () { + assertRanges('stretched shapes[0]', [0, 3], [0, 2]); + }) + .then(done, done.fail); }); - it('should propagate axis autorange changes when axis ranges are set', function(done) { + it('should propagate axis autorange changes when axis ranges are set', function (done) { gd = createGraphDiv(); - Plotly.newPlot(gd, [{y: [1, 2]}], { - xaxis: {range: [0, 2]}, - yaxis: {range: [0, 2]}, - shapes: [{ - x0: 2, y0: 2, - x1: 3, y1: 3 - }] - }) - .then(function() { - assertRanges('set rng / narrow shape', [0, 2], [0, 2]); - return Plotly.relayout(gd, 'shapes[0].x1', 10); - }) - .then(function() { - assertRanges('set rng / large shape', [0, 2], [0, 2]); - return Plotly.relayout(gd, { - 'xaxis.autorange': true, - 'yaxis.autorange': true - }); - }) - .then(function() { - assertRanges('auto rng / large shape', [-0.61, 10], [0.86, 3]); - return Plotly.relayout(gd, 'shapes[0].x1', 3); - }) - .then(function() { - assertRanges('auto rng / small shape', [-0.18, 3], [0.86, 3]); + Plotly.newPlot(gd, [{ y: [1, 2] }], { + xaxis: { range: [0, 2] }, + yaxis: { range: [0, 2] }, + shapes: [ + { + x0: 2, + y0: 2, + x1: 3, + y1: 3 + } + ] }) - .then(done, done.fail); + .then(function () { + assertRanges('set rng / narrow shape', [0, 2], [0, 2]); + return Plotly.relayout(gd, 'shapes[0].x1', 10); + }) + .then(function () { + assertRanges('set rng / large shape', [0, 2], [0, 2]); + return Plotly.relayout(gd, { + 'xaxis.autorange': true, + 'yaxis.autorange': true + }); + }) + .then(function () { + assertRanges('auto rng / large shape', [-0.61, 10], [0.86, 3]); + return Plotly.relayout(gd, 'shapes[0].x1', 3); + }) + .then(function () { + assertRanges('auto rng / small shape', [-0.18, 3], [0.86, 3]); + }) + .then(done, done.fail); }); }); -describe('Test shapes: a plot with shapes and an overlaid axis', function() { +describe('Test shapes: a plot with shapes and an overlaid axis', function () { 'use strict'; var gd, data, layout; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); - data = [{ - y: [1934.5, 1932.3, 1930.3], - x: ['1947-01-01', '1947-04-01', '1948-07-01'], - type: 'scatter' - }]; + data = [ + { + y: [1934.5, 1932.3, 1930.3], + x: ['1947-01-01', '1947-04-01', '1948-07-01'], + type: 'scatter' + } + ]; layout = { yaxis: { @@ -676,47 +696,51 @@ describe('Test shapes: a plot with shapes and an overlaid axis', function() { side: 'right', overlaying: 'y' }, - shapes: [{ - fillcolor: '#ccc', - type: 'rect', - x0: '1947-01-01', - x1: '1947-04-01', - xref: 'x', - y0: 0, - y1: 1, - yref: 'paper', - layer: 'below' - }, { - type: 'path', - xref: 'x', - yref: 'y2', - path: 'M1947-01-01_12:00,2V4H1947-03-01Z' - }, { - type: 'rect', - xref: 'x', - yref: 'y2', - x0: '1947-02-01', - x1: '1947-03-01', - y0: 3, - y1: 5, - layer: 'below' - }, { - type: 'circle', - xref: 'x', - yref: 'y', - x0: '1947-01-15', - x1: '1947-02-15', - y0: 1931, - y1: 1934 - }] + shapes: [ + { + fillcolor: '#ccc', + type: 'rect', + x0: '1947-01-01', + x1: '1947-04-01', + xref: 'x', + y0: 0, + y1: 1, + yref: 'paper', + layer: 'below' + }, + { + type: 'path', + xref: 'x', + yref: 'y2', + path: 'M1947-01-01_12:00,2V4H1947-03-01Z' + }, + { + type: 'rect', + xref: 'x', + yref: 'y2', + x0: '1947-02-01', + x1: '1947-03-01', + y0: 3, + y1: 5, + layer: 'below' + }, + { + type: 'circle', + xref: 'x', + yref: 'y', + x0: '1947-01-15', + x1: '1947-02-15', + y0: 1931, + y1: 1934 + } + ] }; }); afterEach(destroyGraphDiv); - it('should not throw an exception', function(done) { - Plotly.newPlot(gd, data, layout) - .then(done, done.fail); + it('should not throw an exception', function (done) { + Plotly.newPlot(gd, data, layout).then(done, done.fail); }); }); @@ -735,94 +759,102 @@ function assertShapeFullyVisible(shapeElem) { assertElemInside(shapeElem, gridLayer, 'shape element fully visible'); } -describe('A path shape sized relative to data', function() { +describe('A path shape sized relative to data', function () { 'use strict'; var gd, data, layout; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); - data = [{ - x: [1, 5], - y: [1, 5], - type: 'scatter' - }]; + data = [ + { + x: [1, 5], + y: [1, 5], + type: 'scatter' + } + ]; layout = { title: { text: 'Path shape sized relative to data' }, width: 400, height: 400, - shapes: [{ - type: 'path', - xref: 'x', - yref: 'y', - xsizemode: 'data', - ysizemode: 'data', - path: 'M10,0 L2,10 L1,0 Z', - - // Hint: set those too intentionally - xanchor: '3', - yanchor: '0', - x0: 1, - x1: 3, - y0: 1, - y1: 3 - }] + shapes: [ + { + type: 'path', + xref: 'x', + yref: 'y', + xsizemode: 'data', + ysizemode: 'data', + path: 'M10,0 L2,10 L1,0 Z', + + // Hint: set those too intentionally + xanchor: '3', + yanchor: '0', + x0: 1, + x1: 3, + y0: 1, + y1: 3 + } + ] }; }); afterEach(destroyGraphDiv); - it('is expanding an auto-ranging axes', function() { + it('is expanding an auto-ranging axes', function () { Plotly.newPlot(gd, data, layout); assertShapeFullyVisible(getFirstShapeNode()); }); }); -describe('A fixed size path shape', function() { +describe('A fixed size path shape', function () { 'use strict'; var gd, data, layout; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); - data = [{ - x: [1, 5], - y: [1, 5], - type: 'scatter' - }]; + data = [ + { + x: [1, 5], + y: [1, 5], + type: 'scatter' + } + ]; layout = { title: { text: 'Fixed size path shape' }, width: 400, height: 400, - shapes: [{ - type: 'path', - xref: 'x', - yref: 'y', - xsizemode: 'pixel', - ysizemode: 'pixel', - path: 'M0,0 L30,0 L15,20 Z', - xanchor: '3', - yanchor: '0', - - // Hint: set those too intentionally - x0: 1, - x1: 3, - y0: 1, - y1: 3 - }] + shapes: [ + { + type: 'path', + xref: 'x', + yref: 'y', + xsizemode: 'pixel', + ysizemode: 'pixel', + path: 'M0,0 L30,0 L15,20 Z', + xanchor: '3', + yanchor: '0', + + // Hint: set those too intentionally + x0: 1, + x1: 3, + y0: 1, + y1: 3 + } + ] }; }); afterEach(destroyGraphDiv); - it('is defined in pixel', function() { + it('is defined in pixel', function () { Plotly.newPlot(gd, data, layout); assertShapeSize(getFirstShapeNode(), 30, 20); }); - it('is expanding auto-ranging axes', function() { + it('is expanding auto-ranging axes', function () { layout.shapes[0].xanchor = 10; layout.shapes[0].yanchor = 10; @@ -831,15 +863,14 @@ describe('A fixed size path shape', function() { assertShapeFullyVisible(getFirstShapeNode()); }); - it('is being rendered correctly when linked to a date axis', function() { - data = [{ - x: ['2018-01-01 00:00:00', - '2018-02-01 00:00:00', - '2018-03-01 00:00:00', - '2018-04-01 00:00:00'], - y: [3, 4, 2, 5], - type: 'scatter' - }]; + it('is being rendered correctly when linked to a date axis', function () { + data = [ + { + x: ['2018-01-01 00:00:00', '2018-02-01 00:00:00', '2018-03-01 00:00:00', '2018-04-01 00:00:00'], + y: [3, 4, 2, 5], + type: 'scatter' + } + ]; layout.shapes[0].xanchor = '2018-07-01 00:00:00'; layout.shapes[0].yanchor = 10; @@ -850,108 +881,114 @@ describe('A fixed size path shape', function() { assertShapeSize(shapeNode, 30, 20); }); - it('keeps its dimensions when plot is being resized', function(done) { + it('keeps its dimensions when plot is being resized', function (done) { Plotly.newPlot(gd, data, layout); assertShapeSize(getFirstShapeNode(), 30, 20); - Plotly.relayout(gd, {height: 200, width: 600}) - .then(function() { - assertShapeSize(getFirstShapeNode(), 30, 20); - }) - .then(done, done.fail); + Plotly.relayout(gd, { height: 200, width: 600 }) + .then(function () { + assertShapeSize(getFirstShapeNode(), 30, 20); + }) + .then(done, done.fail); }); - it('is draggable', function(done) { - Plotly.newPlot(gd, data, layout, {editable: true}) - .then(function() { - drag({node: getFirstShapeNode(), dpos: [50, 50]}).then(function() { - assertShapeSize(getFirstShapeNode(), 30, 20); - }) - .then(done, done.fail); - }); + it('is draggable', function (done) { + Plotly.newPlot(gd, data, layout, { editable: true }).then(function () { + drag({ node: getFirstShapeNode(), dpos: [50, 50] }) + .then(function () { + assertShapeSize(getFirstShapeNode(), 30, 20); + }) + .then(done, done.fail); + }); }); - it('being sized relative to data horizontally is getting narrower ' + - 'when being dragged to expand the x-axis', - function(done) { - layout.shapes[0].xsizemode = 'data'; - layout.shapes[0].path = 'M0,0 L2,0 L1,20 Z'; + it( + 'being sized relative to data horizontally is getting narrower ' + 'when being dragged to expand the x-axis', + function (done) { + layout.shapes[0].xsizemode = 'data'; + layout.shapes[0].path = 'M0,0 L2,0 L1,20 Z'; - Plotly.newPlot(gd, data, layout, {editable: true}) - .then(function() { + Plotly.newPlot(gd, data, layout, { editable: true }).then(function () { var shapeNodeBeforeDrag = getFirstShapeNode(); var widthBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect().width; - drag({node: shapeNodeBeforeDrag, dpos: [300, 50]}).then(function() { - var shapeNodeAfterDrag = getFirstShapeNode(); - var bbox = shapeNodeAfterDrag.getBoundingClientRect(); - expect(bbox.height).toBe(20); - expect(bbox.width).toBeLessThan(widthBeforeDrag); - assertShapeFullyVisible(shapeNodeAfterDrag); - }) - .then(done, done.fail); + drag({ node: shapeNodeBeforeDrag, dpos: [300, 50] }) + .then(function () { + var shapeNodeAfterDrag = getFirstShapeNode(); + var bbox = shapeNodeAfterDrag.getBoundingClientRect(); + expect(bbox.height).toBe(20); + expect(bbox.width).toBeLessThan(widthBeforeDrag); + assertShapeFullyVisible(shapeNodeAfterDrag); + }) + .then(done, done.fail); }); - }); + } + ); - it('being sized relative to data vertically is getting lower ' + - 'when being dragged to expand the y-axis', - function(done) { - layout.shapes[0].ysizemode = 'data'; - layout.shapes[0].path = 'M0,0 L30,0 L15,2 Z'; + it( + 'being sized relative to data vertically is getting lower ' + 'when being dragged to expand the y-axis', + function (done) { + layout.shapes[0].ysizemode = 'data'; + layout.shapes[0].path = 'M0,0 L30,0 L15,2 Z'; - Plotly.newPlot(gd, data, layout, {editable: true}) - .then(function() { + Plotly.newPlot(gd, data, layout, { editable: true }).then(function () { var shapeNodeBeforeDrag = getFirstShapeNode(); var heightBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect().height; - drag({node: shapeNodeBeforeDrag, dpos: [50, 300]}).then(function() { - var shapeNodeAfterDrag = getFirstShapeNode(); - var bbox = shapeNodeAfterDrag.getBoundingClientRect(); - expect(bbox.width).toBe(30); - expect(bbox.height).toBeLessThan(heightBeforeDrag); - assertShapeFullyVisible(shapeNodeAfterDrag); - }) - .then(done, done.fail); + drag({ node: shapeNodeBeforeDrag, dpos: [50, 300] }) + .then(function () { + var shapeNodeAfterDrag = getFirstShapeNode(); + var bbox = shapeNodeAfterDrag.getBoundingClientRect(); + expect(bbox.width).toBe(30); + expect(bbox.height).toBeLessThan(heightBeforeDrag); + assertShapeFullyVisible(shapeNodeAfterDrag); + }) + .then(done, done.fail); }); - }); + } + ); }); -describe('A fixed size shape', function() { +describe('A fixed size shape', function () { 'use strict'; var gd, data, layout; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); - data = [{ - x: [1, 5], - y: [1, 5], - type: 'scatter' - }]; + data = [ + { + x: [1, 5], + y: [1, 5], + type: 'scatter' + } + ]; layout = { title: { text: 'Fixed size shape' }, width: 400, height: 400, - shapes: [{ - type: 'rect', - xref: 'x', - yref: 'y', - xsizemode: 'pixel', - ysizemode: 'pixel', - xanchor: '3', - yanchor: '0', - x0: 3, - x1: 28, - y0: 0, - y1: -25 - }] + shapes: [ + { + type: 'rect', + xref: 'x', + yref: 'y', + xsizemode: 'pixel', + ysizemode: 'pixel', + xanchor: '3', + yanchor: '0', + x0: 3, + x1: 28, + y0: 0, + y1: -25 + } + ] }; }); afterEach(destroyGraphDiv); - it('can be positioned relative to data', function() { + it('can be positioned relative to data', function () { Plotly.newPlot(gd, data, layout); var shapeNode = getFirstShapeNode(); @@ -964,7 +1001,7 @@ describe('A fixed size shape', function() { assertElemRightTo(shapeNode, gridLine, 'Shape right to third grid line'); }); - it('can be positioned relative to the plotting area', function() { + it('can be positioned relative to the plotting area', function () { layout.shapes[0].xref = 'paper'; layout.shapes[0].yref = 'paper'; layout.shapes[0].xanchor = '1'; @@ -976,7 +1013,7 @@ describe('A fixed size shape', function() { assertElemRightTo(shapeNode, d3SelectAll('.cartesianlayer').node(), 'Shape right to plotting area'); }); - it('can be sized by pixel horizontally and relative to data vertically', function() { + it('can be sized by pixel horizontally and relative to data vertically', function () { layout.shapes[0].ysizemode = 'data'; layout.shapes[0].y0 = 1; layout.shapes[0].y1 = 5; @@ -988,7 +1025,7 @@ describe('A fixed size shape', function() { expect(bBox.width).toBe(25); }); - it('can be sized relative to data vertically and by pixel horizontally', function() { + it('can be sized relative to data vertically and by pixel horizontally', function () { layout.shapes[0].xsizemode = 'data'; layout.shapes[0].x0 = 1; layout.shapes[0].x1 = 5; @@ -1000,15 +1037,14 @@ describe('A fixed size shape', function() { expect(bBox.height).toBe(25); }); - it('is being rendered correctly when linked to a date axis', function() { - data = [{ - x: ['2018-01-01 00:00:00', - '2018-02-01 00:00:00', - '2018-03-01 00:00:00', - '2018-04-01 00:00:00'], - y: [3, 4, 2, 5], - type: 'scatter' - }]; + it('is being rendered correctly when linked to a date axis', function () { + data = [ + { + x: ['2018-01-01 00:00:00', '2018-02-01 00:00:00', '2018-03-01 00:00:00', '2018-04-01 00:00:00'], + y: [3, 4, 2, 5], + type: 'scatter' + } + ]; layout.shapes[0].xanchor = '2018-07-01 00:00:00'; layout.shapes[0].yanchor = 10; @@ -1019,82 +1055,84 @@ describe('A fixed size shape', function() { assertShapeSize(shapeNode, 25, 25); }); - it('keeps its dimensions when plot is being resized', function(done) { + it('keeps its dimensions when plot is being resized', function (done) { layout.shapes[0].yanchor = 3; // Ensure visible for debugging Plotly.newPlot(gd, data, layout); var shapeNode = getFirstShapeNode(); assertShapeSize(shapeNode, 25, 25); - Plotly.relayout(gd, {height: 200, width: 600}) - .then(function() { - var reRenderedShapeNode = getFirstShapeNode(); - assertShapeSize(reRenderedShapeNode, 25, 25); - }) - .then(done, done.fail); + Plotly.relayout(gd, { height: 200, width: 600 }) + .then(function () { + var reRenderedShapeNode = getFirstShapeNode(); + assertShapeSize(reRenderedShapeNode, 25, 25); + }) + .then(done, done.fail); }); - it('is draggable', function(done) { - Plotly.newPlot(gd, data, layout, {editable: true}) - .then(function() { - drag({node: getFirstShapeNode(), dpos: [50, 50]}).then(function() { - assertShapeSize(getFirstShapeNode(), 25, 25); - }) - .then(done, done.fail); - }); + it('is draggable', function (done) { + Plotly.newPlot(gd, data, layout, { editable: true }).then(function () { + drag({ node: getFirstShapeNode(), dpos: [50, 50] }) + .then(function () { + assertShapeSize(getFirstShapeNode(), 25, 25); + }) + .then(done, done.fail); + }); }); - it('being sized relative to data horizontally is getting narrower ' + - 'when being dragged to expand the x-axis', - function(done) { - layout.shapes[0].xsizemode = 'data'; - layout.shapes[0].x0 = 1; - layout.shapes[0].x1 = 2; + it( + 'being sized relative to data horizontally is getting narrower ' + 'when being dragged to expand the x-axis', + function (done) { + layout.shapes[0].xsizemode = 'data'; + layout.shapes[0].x0 = 1; + layout.shapes[0].x1 = 2; - Plotly.newPlot(gd, data, layout, {editable: true}) - .then(function() { + Plotly.newPlot(gd, data, layout, { editable: true }).then(function () { var shapeNodeBeforeDrag = getFirstShapeNode(); var widthBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect().width; - drag({node: shapeNodeBeforeDrag, dpos: [300, 50]}).then(function() { - var shapeNodeAfterDrag = getFirstShapeNode(); - var bbox = shapeNodeAfterDrag.getBoundingClientRect(); - expect(bbox.height).toBe(25); - expect(bbox.width).toBeLessThan(widthBeforeDrag); - assertShapeFullyVisible(shapeNodeAfterDrag); - }) - .then(done, done.fail); + drag({ node: shapeNodeBeforeDrag, dpos: [300, 50] }) + .then(function () { + var shapeNodeAfterDrag = getFirstShapeNode(); + var bbox = shapeNodeAfterDrag.getBoundingClientRect(); + expect(bbox.height).toBe(25); + expect(bbox.width).toBeLessThan(widthBeforeDrag); + assertShapeFullyVisible(shapeNodeAfterDrag); + }) + .then(done, done.fail); }); - }); + } + ); - it('being sized relative to data vertically is getting lower ' + - 'when being dragged to expand the y-axis', - function(done) { - layout.shapes[0].ysizemode = 'data'; - layout.shapes[0].y0 = 1; - layout.shapes[0].y1 = 2; + it( + 'being sized relative to data vertically is getting lower ' + 'when being dragged to expand the y-axis', + function (done) { + layout.shapes[0].ysizemode = 'data'; + layout.shapes[0].y0 = 1; + layout.shapes[0].y1 = 2; - Plotly.newPlot(gd, data, layout, {editable: true}) - .then(function() { + Plotly.newPlot(gd, data, layout, { editable: true }).then(function () { var shapeNodeBeforeDrag = getFirstShapeNode(); var heightBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect().height; - drag({node: shapeNodeBeforeDrag, dpos: [50, 300]}).then(function() { - var shapeNodeAfterDrag = getFirstShapeNode(); - var bbox = shapeNodeAfterDrag.getBoundingClientRect(); - expect(bbox.width).toBe(25); - expect(bbox.height).toBeLessThan(heightBeforeDrag); - assertShapeFullyVisible(shapeNodeAfterDrag); - }) - .then(done, done.fail); + drag({ node: shapeNodeBeforeDrag, dpos: [50, 300] }) + .then(function () { + var shapeNodeAfterDrag = getFirstShapeNode(); + var bbox = shapeNodeAfterDrag.getBoundingClientRect(); + expect(bbox.width).toBe(25); + expect(bbox.height).toBeLessThan(heightBeforeDrag); + assertShapeFullyVisible(shapeNodeAfterDrag); + }) + .then(done, done.fail); }); - }); + } + ); // Helper to combine two arrays of objects function combinations(arr1, arr2) { var combinations = []; - arr1.forEach(function(elemArr1) { - arr2.forEach(function(elemArr2) { + arr1.forEach(function (elemArr1) { + arr2.forEach(function (elemArr2) { combinations.push(Lib.extendFlat({}, elemArr1, elemArr2)); }); }); @@ -1103,53 +1141,55 @@ describe('A fixed size shape', function() { // Only rect and circle because (i) path isn't yet resizable // and (ii) line has a different resize behavior. - var shapeAndResizeTypes = combinations([{type: 'rect'}, {type: 'circle'}], resizeTypes); - shapeAndResizeTypes.forEach(function(testCase) { - describe('of type ' + testCase.type + ' can be ' + testCase.resizeDisplayName, function() { - resizeDirections.forEach(function(direction) { - it('over direction ' + direction, function(done) { + var shapeAndResizeTypes = combinations([{ type: 'rect' }, { type: 'circle' }], resizeTypes); + shapeAndResizeTypes.forEach(function (testCase) { + describe('of type ' + testCase.type + ' can be ' + testCase.resizeDisplayName, function () { + resizeDirections.forEach(function (direction) { + it('over direction ' + direction, function (done) { layout.shapes[0].type = testCase.type; - Plotly.newPlot(gd, data, layout, {editable: true}) - .then(function() { - var shapeNodeBeforeDrag = getFirstShapeNode(); - var bBoxBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect(); + Plotly.newPlot(gd, data, layout, { editable: true }).then(function () { + var shapeNodeBeforeDrag = getFirstShapeNode(); + var bBoxBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect(); - var shallShrink = testCase.resizeType === 'shrink'; - var dx = shallShrink ? dxToShrinkWidth[direction] : dxToEnlargeWidth[direction]; - var dy = shallShrink ? dyToShrinkHeight[direction] : dyToEnlargeHeight[direction]; + var shallShrink = testCase.resizeType === 'shrink'; + var dx = shallShrink ? dxToShrinkWidth[direction] : dxToEnlargeWidth[direction]; + var dy = shallShrink ? dyToShrinkHeight[direction] : dyToEnlargeHeight[direction]; - drag({node: shapeNodeBeforeDrag, dpos: [dx, dy], edge: direction}) - .then(function() { + drag({ node: shapeNodeBeforeDrag, dpos: [dx, dy], edge: direction }) + .then(function () { var shapeNodeAfterDrag = getFirstShapeNode(); var bBoxAfterDrag = shapeNodeAfterDrag.getBoundingClientRect(); var resizeFactor = shallShrink ? -1 : 1; - expect(bBoxAfterDrag.height).toBeCloseTo(bBoxBeforeDrag.height + resizeFactor * Math.abs(dy)); - expect(bBoxAfterDrag.width).toBeCloseTo(bBoxBeforeDrag.width + resizeFactor * Math.abs(dx)); + expect(bBoxAfterDrag.height).toBeCloseTo( + bBoxBeforeDrag.height + resizeFactor * Math.abs(dy) + ); + expect(bBoxAfterDrag.width).toBeCloseTo( + bBoxBeforeDrag.width + resizeFactor * Math.abs(dx) + ); assertShapeFullyVisible(shapeNodeAfterDrag); }) .then(done, done.fail); - }); + }); }); }); }); }); - describe('of type line', function() { - beforeEach(function() { + describe('of type line', function () { + beforeEach(function () { layout.shapes[0].type = 'line'; layout.shapes[0].yanchor = 3; }); - it('can be moved by dragging the middle', function(done) { - Plotly.newPlot(gd, data, layout, {editable: true}) - .then(function() { - var shapeNodeBeforeDrag = getFirstShapeNode(); - var bBoxBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect(); + it('can be moved by dragging the middle', function (done) { + Plotly.newPlot(gd, data, layout, { editable: true }).then(function () { + var shapeNodeBeforeDrag = getFirstShapeNode(); + var bBoxBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect(); - var dragSensitiveElement = getMoveLineDragElement(0); - drag({node: dragSensitiveElement, dpos: [10, -10]}) - .then(function() { + var dragSensitiveElement = getMoveLineDragElement(0); + drag({ node: dragSensitiveElement, dpos: [10, -10] }) + .then(function () { var shapeNodeAfterDrag = getFirstShapeNode(); var bBoxAfterDrag = shapeNodeAfterDrag.getBoundingClientRect(); @@ -1158,18 +1198,17 @@ describe('A fixed size shape', function() { expect(bBoxAfterDrag.top).toBe(bBoxBeforeDrag.top - 10); }) .then(done, done.fail); - }); + }); }); - it('can be resized by dragging the start point', function(done) { - Plotly.newPlot(gd, data, layout, {editable: true}) - .then(function() { - var shapeNodeBeforeDrag = getFirstShapeNode(); - var bBoxBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect(); + it('can be resized by dragging the start point', function (done) { + Plotly.newPlot(gd, data, layout, { editable: true }).then(function () { + var shapeNodeBeforeDrag = getFirstShapeNode(); + var bBoxBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect(); - var dragSensitiveElement = getResizeLineOverStartPointElement(); - drag({node: dragSensitiveElement, dpos: [50, -10]}) - .then(function() { + var dragSensitiveElement = getResizeLineOverStartPointElement(); + drag({ node: dragSensitiveElement, dpos: [50, -10] }) + .then(function () { var shapeNodeAfterDrag = getFirstShapeNode(); var bBoxAfterDrag = shapeNodeAfterDrag.getBoundingClientRect(); @@ -1180,18 +1219,17 @@ describe('A fixed size shape', function() { expect(bBoxAfterDrag.left).toBe(bBoxBeforeDrag.left + 25, 'left'); }) .then(done, done.fail); - }); + }); }); - it('can be resized by dragging the end point', function(done) { - Plotly.newPlot(gd, data, layout, {editable: true}) - .then(function() { - var shapeNodeBeforeDrag = getFirstShapeNode(); - var bBoxBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect(); + it('can be resized by dragging the end point', function (done) { + Plotly.newPlot(gd, data, layout, { editable: true }).then(function () { + var shapeNodeBeforeDrag = getFirstShapeNode(); + var bBoxBeforeDrag = shapeNodeBeforeDrag.getBoundingClientRect(); - var dragSensitiveElement = getResizeLineOverEndPointElement(); - drag({node: dragSensitiveElement, dpos: [50, -10]}) - .then(function() { + var dragSensitiveElement = getResizeLineOverEndPointElement(); + drag({ node: dragSensitiveElement, dpos: [50, -10] }) + .then(function () { var shapeNodeAfterDrag = getFirstShapeNode(); var bBoxAfterDrag = shapeNodeAfterDrag.getBoundingClientRect(); @@ -1202,105 +1240,133 @@ describe('A fixed size shape', function() { expect(bBoxAfterDrag.left).toBe(bBoxBeforeDrag.left, 'left'); }) .then(done, done.fail); - }); + }); }); }); - describe('is expanding an auto-ranging x-axis', function() { + describe('is expanding an auto-ranging x-axis', function () { var sizeVariants = [ - {x0: 5, x1: 25}, - {x0: 5, x1: -25}, - {x0: -5, x1: 25}, - {x0: -5, x1: -25} + { x0: 5, x1: 25 }, + { x0: 5, x1: -25 }, + { x0: -5, x1: 25 }, + { x0: -5, x1: -25 } ]; var shapeVariants = combinations(shapeTypes, sizeVariants); - describe('to the left', function() { - shapeVariants.forEach(function(testCase) { - it('and is fully visible when being a ' + testCase.type + - ' with x0,x1=[' + testCase.x0 + ',' + testCase.x1 + ']', - function() { - layout.shapes[0].type = testCase.type; - layout.shapes[0].xanchor = -1; - layout.shapes[0].x0 = testCase.x0; - layout.shapes[0].x1 = testCase.x1; - Plotly.newPlot(gd, data, layout); - - expect(gd.layout.xaxis.range[0]).toBeLessThanOrEqual(-1); - assertShapeFullyVisible(getFirstShapeNode()); - }); + describe('to the left', function () { + shapeVariants.forEach(function (testCase) { + it( + 'and is fully visible when being a ' + + testCase.type + + ' with x0,x1=[' + + testCase.x0 + + ',' + + testCase.x1 + + ']', + function () { + layout.shapes[0].type = testCase.type; + layout.shapes[0].xanchor = -1; + layout.shapes[0].x0 = testCase.x0; + layout.shapes[0].x1 = testCase.x1; + Plotly.newPlot(gd, data, layout); + + expect(gd.layout.xaxis.range[0]).toBeLessThanOrEqual(-1); + assertShapeFullyVisible(getFirstShapeNode()); + } + ); }); }); - describe('to the right', function() { - shapeVariants.forEach(function(testCase) { - it('and is fully visible when being a ' + testCase.type + - ' with x0,x1=[' + testCase.x0 + ',' + testCase.x1 + ']', - function() { - layout.shapes[0].type = testCase.type; - layout.shapes[0].xanchor = 10; - layout.shapes[0].x0 = testCase.x0; - layout.shapes[0].x1 = testCase.x1; - Plotly.newPlot(gd, data, layout); - - expect(gd.layout.xaxis.range[1]).toBeGreaterThanOrEqual(10); - assertShapeFullyVisible(getFirstShapeNode()); - }); + describe('to the right', function () { + shapeVariants.forEach(function (testCase) { + it( + 'and is fully visible when being a ' + + testCase.type + + ' with x0,x1=[' + + testCase.x0 + + ',' + + testCase.x1 + + ']', + function () { + layout.shapes[0].type = testCase.type; + layout.shapes[0].xanchor = 10; + layout.shapes[0].x0 = testCase.x0; + layout.shapes[0].x1 = testCase.x1; + Plotly.newPlot(gd, data, layout); + + expect(gd.layout.xaxis.range[1]).toBeGreaterThanOrEqual(10); + assertShapeFullyVisible(getFirstShapeNode()); + } + ); }); }); }); - describe('is expanding an auto-ranging y-axis', function() { + describe('is expanding an auto-ranging y-axis', function () { var sizeVariants = [ - {y0: 5, y1: 25}, - {y0: 5, y1: -25}, - {y0: -5, y1: 25}, - {y0: -5, y1: -25} + { y0: 5, y1: 25 }, + { y0: 5, y1: -25 }, + { y0: -5, y1: 25 }, + { y0: -5, y1: -25 } ]; var shapeVariants = combinations(shapeTypes, sizeVariants); - describe('to the bottom', function() { - shapeVariants.forEach(function(testCase) { - it('and is fully visible when being a ' + testCase.type + - ' with y0,y1=[' + testCase.y0 + ',' + testCase.y1 + ']', - function() { - layout.shapes[0].type = testCase.type; - layout.shapes[0].yanchor = -1; - layout.shapes[0].y0 = testCase.y0; - layout.shapes[0].y1 = testCase.y1; - Plotly.newPlot(gd, data, layout); - - expect(gd.layout.yaxis.range[0]).toBeLessThanOrEqual(-1); - assertShapeFullyVisible(getFirstShapeNode()); - }); + describe('to the bottom', function () { + shapeVariants.forEach(function (testCase) { + it( + 'and is fully visible when being a ' + + testCase.type + + ' with y0,y1=[' + + testCase.y0 + + ',' + + testCase.y1 + + ']', + function () { + layout.shapes[0].type = testCase.type; + layout.shapes[0].yanchor = -1; + layout.shapes[0].y0 = testCase.y0; + layout.shapes[0].y1 = testCase.y1; + Plotly.newPlot(gd, data, layout); + + expect(gd.layout.yaxis.range[0]).toBeLessThanOrEqual(-1); + assertShapeFullyVisible(getFirstShapeNode()); + } + ); }); }); - describe('to the top', function() { - shapeVariants.forEach(function(testCase) { - it('and is fully visible when being a ' + testCase.type + - ' with y0,y1=[' + testCase.y0 + ',' + testCase.y1 + ']', - function() { - layout.shapes[0].type = testCase.type; - layout.shapes[0].yanchor = 10; - layout.shapes[0].y0 = testCase.y0; - layout.shapes[0].y1 = testCase.y1; - Plotly.newPlot(gd, data, layout); - - expect(gd.layout.yaxis.range[1]).toBeGreaterThanOrEqual(10); - assertShapeFullyVisible(getFirstShapeNode()); - }); + describe('to the top', function () { + shapeVariants.forEach(function (testCase) { + it( + 'and is fully visible when being a ' + + testCase.type + + ' with y0,y1=[' + + testCase.y0 + + ',' + + testCase.y1 + + ']', + function () { + layout.shapes[0].type = testCase.type; + layout.shapes[0].yanchor = 10; + layout.shapes[0].y0 = testCase.y0; + layout.shapes[0].y1 = testCase.y1; + Plotly.newPlot(gd, data, layout); + + expect(gd.layout.yaxis.range[1]).toBeGreaterThanOrEqual(10); + assertShapeFullyVisible(getFirstShapeNode()); + } + ); }); }); }); }); -describe('Test shapes', function() { +describe('Test shapes', function () { 'use strict'; var gd, data, layout, config; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); data = [{}]; layout = {}; @@ -1336,32 +1402,30 @@ describe('Test shapes', function() { } ]; - testCases.forEach(function(testCase) { - it('' + testCase.title + ' should be draggable', function(done) { - setupLayout(testCase, [{type: 'line'}, {type: 'rect'}, {type: 'circle'}, {type: 'path'}]); + testCases.forEach(function (testCase) { + it('' + testCase.title + ' should be draggable', function (done) { + setupLayout(testCase, [{ type: 'line' }, { type: 'rect' }, { type: 'circle' }, { type: 'path' }]); testDragEachShape(done); }); }); - testCases.forEach(function(testCase) { - resizeDirections.forEach(function(direction) { - var testTitle = testCase.title + - ' should be resizeable over direction ' + - direction; - it('' + testTitle, function(done) { + testCases.forEach(function (testCase) { + resizeDirections.forEach(function (direction) { + var testTitle = testCase.title + ' should be resizeable over direction ' + direction; + it('' + testTitle, function (done) { // Exclude line because it has a different resize behavior - setupLayout(testCase, [{type: 'rect'}, {type: 'circle'}, {type: 'path'}]); + setupLayout(testCase, [{ type: 'rect' }, { type: 'circle' }, { type: 'path' }]); testResizeEachShape(direction, done); }); }); }); - testCases.forEach(function(testCase) { - ['start', 'end'].forEach(function(linePoint) { - var testTitle = 'Line shape ' + testCase.title + - ' should be resizable by dragging the ' + linePoint + ' point'; - it('' + testTitle, function(done) { - setupLayout(testCase, [{type: 'line'}]); + testCases.forEach(function (testCase) { + ['start', 'end'].forEach(function (linePoint) { + var testTitle = + 'Line shape ' + testCase.title + ' should be resizable by dragging the ' + linePoint + ' point'; + it('' + testTitle, function (done) { + setupLayout(testCase, [{ type: 'line' }]); testLineResize(linePoint, done); }); }); @@ -1379,22 +1443,22 @@ describe('Test shapes', function() { var y0 = yrange[0]; var y1 = yrange[1]; - if(testCase.xaxis && testCase.xaxis.type === 'log') { + if (testCase.xaxis && testCase.xaxis.type === 'log') { x0 = Math.pow(10, x0); x1 = Math.pow(10, x1); } - if(testCase.yaxis && testCase.yaxis.type === 'log') { + if (testCase.yaxis && testCase.yaxis.type === 'log') { y0 = Math.pow(10, y0); y1 = Math.pow(10, y1); } - if(testCase.xaxis && testCase.xaxis.type === 'category') { + if (testCase.xaxis && testCase.xaxis.type === 'category') { x0 = 0; x1 = 1; } - if(testCase.yaxis && testCase.yaxis.type === 'category') { + if (testCase.yaxis && testCase.yaxis.type === 'category') { y0 = 0; y1 = 1; } @@ -1403,11 +1467,11 @@ describe('Test shapes', function() { var x1y1 = x1 + ',' + y1; var x1y0 = x1 + ',' + y0; - layoutShapes.forEach(function(s) { + layoutShapes.forEach(function (s) { s.xref = xref; s.yref = yref; - if(s.type === 'path') { + if (s.type === 'path') { s.path = 'M' + x0y0 + 'L' + x1y1 + 'L' + x1y0 + 'Z'; } else { s.x0 = x0; @@ -1425,20 +1489,18 @@ describe('Test shapes', function() { var layoutShapes = gd.layout.shapes; - expect(layoutShapes.length).toBe(4); // line, rect, circle and path + expect(layoutShapes.length).toBe(4); // line, rect, circle and path - layoutShapes.forEach(function(layoutShape, index) { + layoutShapes.forEach(function (layoutShape, index) { var dx = 100; var dy = 100; - promise = promise.then(function() { - var node = layoutShape.type === 'line' ? - getMoveLineDragElement(index) : - getShapeNode(index); + promise = promise.then(function () { + var node = layoutShape.type === 'line' ? getMoveLineDragElement(index) : getShapeNode(index); expect(node).not.toBe(null); - return (layoutShape.path) ? - testPathDrag(dx, dy, layoutShape, node) : - testShapeDrag(dx, dy, layoutShape, node); + return layoutShape.path + ? testPathDrag(dx, dy, layoutShape, node) + : testShapeDrag(dx, dy, layoutShape, node); }); }); @@ -1454,20 +1516,20 @@ describe('Test shapes', function() { // Hint: line has different resize behavior. expect(layoutShapes.length).toBe(3); - layoutShapes.forEach(function(layoutShape, index) { - if(layoutShape.path) return; + layoutShapes.forEach(function (layoutShape, index) { + if (layoutShape.path) return; var dx = dxToShrinkWidth[direction]; var dy = dyToShrinkHeight[direction]; - promise = promise.then(function() { + promise = promise.then(function () { var node = getShapeNode(index); expect(node).not.toBe(null); return testShapeResize(direction, dx, dy, layoutShape, node); }); - promise = promise.then(function() { + promise = promise.then(function () { var node = getShapeNode(index); expect(node).not.toBe(null); @@ -1479,26 +1541,28 @@ describe('Test shapes', function() { } function getShapeNode(index) { - return d3SelectAll('.shapelayer .shape-group path').filter(function() { - return +this.getAttribute('data-index') === index; - }).node(); + return d3SelectAll('.shapelayer .shape-group path') + .filter(function () { + return +this.getAttribute('data-index') === index; + }) + .node(); } function testShapeDrag(dx, dy, layoutShape, node) { var xa = Axes.getFromId(gd, layoutShape.xref); var ya = Axes.getFromId(gd, layoutShape.yref); - var x2p = function(v, shift) { + var x2p = function (v, shift) { var dataToPixel = helpers.getDataToPixel(gd, xa, shift); return dataToPixel(v); }; - var y2p = function(v, shift) { + var y2p = function (v, shift) { var dataToPixel = helpers.getDataToPixel(gd, ya, shift, true); return dataToPixel(v); }; var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); - return drag({node: node, dpos: [dx, dy]}).then(function() { + return drag({ node: node, dpos: [dx, dy] }).then(function () { var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); expect(finalCoordinates.x0 - initialCoordinates.x0).toBeCloseTo(dx); @@ -1528,22 +1592,20 @@ describe('Test shapes', function() { expect(initialCoordinates.length).toBe(6); - return drag({node: node, dpos: [dx, dy]}).then(function() { + return drag({ node: node, dpos: [dx, dy] }).then(function () { var finalPath = layoutShape.path; var finalCoordinates = getPathCoordinates(finalPath, x2p, y2p); expect(finalCoordinates.length).toBe(initialCoordinates.length); - for(var i = 0; i < initialCoordinates.length; i++) { + for (var i = 0; i < initialCoordinates.length; i++) { var initialCoordinate = initialCoordinates[i]; var finalCoordinate = finalCoordinates[i]; - if(initialCoordinate.x) { - expect(finalCoordinate.x - initialCoordinate.x) - .toBeCloseTo(dx); + if (initialCoordinate.x) { + expect(finalCoordinate.x - initialCoordinate.x).toBeCloseTo(dx); } else { - expect(finalCoordinate.y - initialCoordinate.y) - .toBeCloseTo(dy); + expect(finalCoordinate.y - initialCoordinate.y).toBeCloseTo(dy); } } }); @@ -1552,46 +1614,46 @@ describe('Test shapes', function() { function testShapeResize(direction, dx, dy, layoutShape, node) { var xa = Axes.getFromId(gd, layoutShape.xref); var ya = Axes.getFromId(gd, layoutShape.yref); - var x2p = function(v, shift) { + var x2p = function (v, shift) { var dataToPixel = helpers.getDataToPixel(gd, xa, shift, false); return dataToPixel(v); }; - var y2p = function(v, shift) { + var y2p = function (v, shift) { var dataToPixel = helpers.getDataToPixel(gd, ya, shift, true); return dataToPixel(v); }; var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); - return drag({node: node, dpos: [dx, dy], edge: direction}).then(function() { + return drag({ node: node, dpos: [dx, dy], edge: direction }).then(function () { var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); var keyN, keyS, keyW, keyE; - if(initialCoordinates.y0 < initialCoordinates.y1) { - keyN = 'y0'; keyS = 'y1'; + if (initialCoordinates.y0 < initialCoordinates.y1) { + keyN = 'y0'; + keyS = 'y1'; } else { - keyN = 'y1'; keyS = 'y0'; + keyN = 'y1'; + keyS = 'y0'; } - if(initialCoordinates.x0 < initialCoordinates.x1) { - keyW = 'x0'; keyE = 'x1'; + if (initialCoordinates.x0 < initialCoordinates.x1) { + keyW = 'x0'; + keyE = 'x1'; } else { - keyW = 'x1'; keyE = 'x0'; + keyW = 'x1'; + keyE = 'x0'; } - if(~direction.indexOf('n')) { - expect(finalCoordinates[keyN] - initialCoordinates[keyN]) - .toBeCloseTo(dy); - } else if(~direction.indexOf('s')) { - expect(finalCoordinates[keyS] - initialCoordinates[keyS]) - .toBeCloseTo(dy); + if (~direction.indexOf('n')) { + expect(finalCoordinates[keyN] - initialCoordinates[keyN]).toBeCloseTo(dy); + } else if (~direction.indexOf('s')) { + expect(finalCoordinates[keyS] - initialCoordinates[keyS]).toBeCloseTo(dy); } - if(~direction.indexOf('w')) { - expect(finalCoordinates[keyW] - initialCoordinates[keyW]) - .toBeCloseTo(dx); - } else if(~direction.indexOf('e')) { - expect(finalCoordinates[keyE] - initialCoordinates[keyE]) - .toBeCloseTo(dx); + if (~direction.indexOf('w')) { + expect(finalCoordinates[keyW] - initialCoordinates[keyW]).toBeCloseTo(dx); + } else if (~direction.indexOf('e')) { + expect(finalCoordinates[keyE] - initialCoordinates[keyE]).toBeCloseTo(dx); } }); } @@ -1602,25 +1664,24 @@ describe('Test shapes', function() { var xa = Axes.getFromId(gd, layoutShape.xref); var ya = Axes.getFromId(gd, layoutShape.yref); - var x2p = function(v, shift) { + var x2p = function (v, shift) { var dataToPixel = helpers.getDataToPixel(gd, xa, shift); return dataToPixel(v); }; - var y2p = function(v, shift) { + var y2p = function (v, shift) { var dataToPixel = helpers.getDataToPixel(gd, ya, shift, true); return dataToPixel(v); }; - promise = promise.then(function() { - var dragHandle = pointToMove === 'start' ? - getResizeLineOverStartPointElement() : - getResizeLineOverEndPointElement(); + promise = promise.then(function () { + var dragHandle = + pointToMove === 'start' ? getResizeLineOverStartPointElement() : getResizeLineOverEndPointElement(); var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); - return drag({node: dragHandle, dpos: [10, 10]}).then(function() { + return drag({ node: dragHandle, dpos: [10, 10] }).then(function () { var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p); - if(pointToMove === 'start') { + if (pointToMove === 'start') { expect(finalCoordinates.x0 - initialCoordinates.x0).toBeCloseTo(10); expect(finalCoordinates.y0 - initialCoordinates.y0).toBeCloseTo(10); } else { @@ -1636,7 +1697,7 @@ describe('Test shapes', function() { function getPathCoordinates(pathString, x2p, y2p) { var coordinates = []; - pathString.match(constants.segmentRE).forEach(function(segment) { + pathString.match(constants.segmentRE).forEach(function (segment) { var paramNumber = 0; var segmentType = segment.charAt(0); var xParams = constants.paramIsX[segmentType]; @@ -1644,13 +1705,13 @@ describe('Test shapes', function() { var nParams = constants.numParams[segmentType]; var params = segment.slice(1).match(constants.paramRE); - if(params) { - params.forEach(function(param) { - if(paramNumber >= nParams) return; + if (params) { + params.forEach(function (param) { + if (paramNumber >= nParams) return; - if(xParams[paramNumber]) { + if (xParams[paramNumber]) { coordinates.push({ x: x2p(param) }); - } else if(yParams[paramNumber]) { + } else if (yParams[paramNumber]) { coordinates.push({ y: y2p(param) }); } @@ -1663,12 +1724,14 @@ describe('Test shapes', function() { } }); -describe('Test multi-axis shapes', function() { +describe('Test multi-axis shapes', function () { 'use strict'; var gd; - beforeEach(function() { gd = createGraphDiv(); }); + beforeEach(function () { + gd = createGraphDiv(); + }); afterEach(destroyGraphDiv); function getShapesWithIndex(index) { @@ -1680,254 +1743,297 @@ describe('Test multi-axis shapes', function() { return node ? node.getBoundingClientRect() : null; } - it('renders all shape types with array xref and yref values', function(done) { - Plotly.newPlot(gd, [ - {x: [1, 2], y: [1, 2]}, - {x: [1, 2], y: [1, 2], xaxis: 'x2', yaxis: 'y2'} - ], { - xaxis: {domain: [0, 0.45]}, - yaxis: {domain: [0, 0.45]}, - xaxis2: {domain: [0.55, 1], anchor: 'y2'}, - yaxis2: {domain: [0.55, 1], anchor: 'x2'}, - shapes: [ - {type: 'line', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2}, - {type: 'rect', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1.5, x1: 1.5, y0: 1.5, y1: 1.5}, - {type: 'circle', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2}, - {type: 'path', xref: ['x', 'x2'], yref: ['y', 'y2'], path: 'M1,1L2,2'} - ] - }).then(function() { - expect(getShapesWithIndex(0).size()).toBe(1); - expect(getShapesWithIndex(1).size()).toBe(1); - expect(getShapesWithIndex(2).size()).toBe(1); - expect(getShapesWithIndex(3).size()).toBe(1); - }) - .then(done, done.fail); + it('renders all shape types with array xref and yref values', function (done) { + Plotly.newPlot( + gd, + [ + { x: [1, 2], y: [1, 2] }, + { x: [1, 2], y: [1, 2], xaxis: 'x2', yaxis: 'y2' } + ], + { + xaxis: { domain: [0, 0.45] }, + yaxis: { domain: [0, 0.45] }, + xaxis2: { domain: [0.55, 1], anchor: 'y2' }, + yaxis2: { domain: [0.55, 1], anchor: 'x2' }, + shapes: [ + { type: 'line', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2 }, + { type: 'rect', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1.5, x1: 1.5, y0: 1.5, y1: 1.5 }, + { type: 'circle', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2 }, + { type: 'path', xref: ['x', 'x2'], yref: ['y', 'y2'], path: 'M1,1L2,2' } + ] + } + ) + .then(function () { + expect(getShapesWithIndex(0).size()).toBe(1); + expect(getShapesWithIndex(1).size()).toBe(1); + expect(getShapesWithIndex(2).size()).toBe(1); + expect(getShapesWithIndex(3).size()).toBe(1); + }) + .then(done, done.fail); }); - it('positions shapes correctly with side-by-side subplots', function(done) { - Plotly.newPlot(gd, [ - {x: [1, 2], y: [1, 2]}, - {x: [1, 2], y: [1, 2], xaxis: 'x2', yaxis: 'y2'} - ], { - width: 800, height: 400, - xaxis: {domain: [0, 0.45]}, - yaxis: {domain: [0, 0.45]}, - xaxis2: {domain: [0.55, 1], anchor: 'y2'}, - yaxis2: {domain: [0.55, 1], anchor: 'x2'}, - shapes: [ - {type: 'line', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2}, - {type: 'rect', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1.5, x1: 1.5, y0: 1.5, y1: 1.5}, - {type: 'circle', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2}, - {type: 'path', xref: ['x', 'x2'], yref: ['y', 'y2'], path: 'M1,1L2,2'} - ] - }).then(function() { - var xa = gd._fullLayout.xaxis; - var xa2 = gd._fullLayout.xaxis2; - var ya = gd._fullLayout.yaxis; - var ya2 = gd._fullLayout.yaxis2; - - var lineBBox = getBoundingBox(0); - expect(lineBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); - expect(lineBBox.right).toBeCloseTo(xa2.l2p(2) + xa2._offset); - expect(lineBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); - expect(lineBBox.top).toBeCloseTo(ya2.l2p(2) + ya2._offset); - - var rectBBox = getBoundingBox(1); - expect(rectBBox.left).toBeCloseTo(xa.l2p(1.5) + xa._offset); - expect(rectBBox.right).toBeCloseTo(xa2.l2p(1.5) + xa2._offset); - expect(rectBBox.bottom).toBeCloseTo(ya.l2p(1.5) + ya._offset); - expect(rectBBox.top).toBeCloseTo(ya2.l2p(1.5) + ya2._offset); - - var circleBBox = getBoundingBox(2); - expect(circleBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); - expect(circleBBox.right).toBeCloseTo(xa2.l2p(2) + xa2._offset); - expect(circleBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); - expect(circleBBox.top).toBeCloseTo(ya2.l2p(2) + ya2._offset); - - var pathBBox = getBoundingBox(3); - expect(pathBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); - expect(pathBBox.right).toBeCloseTo(xa2.l2p(2) + xa2._offset); - expect(pathBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); - expect(pathBBox.top).toBeCloseTo(ya2.l2p(2) + ya2._offset); - }) - .then(done, done.fail); + it('positions shapes correctly with side-by-side subplots', function (done) { + Plotly.newPlot( + gd, + [ + { x: [1, 2], y: [1, 2] }, + { x: [1, 2], y: [1, 2], xaxis: 'x2', yaxis: 'y2' } + ], + { + width: 800, + height: 400, + xaxis: { domain: [0, 0.45] }, + yaxis: { domain: [0, 0.45] }, + xaxis2: { domain: [0.55, 1], anchor: 'y2' }, + yaxis2: { domain: [0.55, 1], anchor: 'x2' }, + shapes: [ + { type: 'line', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2 }, + { type: 'rect', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1.5, x1: 1.5, y0: 1.5, y1: 1.5 }, + { type: 'circle', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2 }, + { type: 'path', xref: ['x', 'x2'], yref: ['y', 'y2'], path: 'M1,1L2,2' } + ] + } + ) + .then(function () { + var xa = gd._fullLayout.xaxis; + var xa2 = gd._fullLayout.xaxis2; + var ya = gd._fullLayout.yaxis; + var ya2 = gd._fullLayout.yaxis2; + + var lineBBox = getBoundingBox(0); + expect(lineBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); + expect(lineBBox.right).toBeCloseTo(xa2.l2p(2) + xa2._offset); + expect(lineBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); + expect(lineBBox.top).toBeCloseTo(ya2.l2p(2) + ya2._offset); + + var rectBBox = getBoundingBox(1); + expect(rectBBox.left).toBeCloseTo(xa.l2p(1.5) + xa._offset); + expect(rectBBox.right).toBeCloseTo(xa2.l2p(1.5) + xa2._offset); + expect(rectBBox.bottom).toBeCloseTo(ya.l2p(1.5) + ya._offset); + expect(rectBBox.top).toBeCloseTo(ya2.l2p(1.5) + ya2._offset); + + var circleBBox = getBoundingBox(2); + expect(circleBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); + expect(circleBBox.right).toBeCloseTo(xa2.l2p(2) + xa2._offset); + expect(circleBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); + expect(circleBBox.top).toBeCloseTo(ya2.l2p(2) + ya2._offset); + + var pathBBox = getBoundingBox(3); + expect(pathBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); + expect(pathBBox.right).toBeCloseTo(xa2.l2p(2) + xa2._offset); + expect(pathBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); + expect(pathBBox.top).toBeCloseTo(ya2.l2p(2) + ya2._offset); + }) + .then(done, done.fail); }); - it('positions shapes correctly with overlaid axes', function(done) { - Plotly.newPlot(gd, [ - {x: [1, 2], y: [1, 2]}, - {x: [1, 2], y: [50, 100], yaxis: 'y2'} - ], { - width: 600, height: 400, - yaxis: {range: [0, 10]}, - yaxis2: {overlaying: 'y', side: 'right', range: [0, 100]}, - shapes: [ - {type: 'line', xref: 'x', yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 20}, - {type: 'rect', xref: 'x', yref: ['y', 'y2'], x0: 1.5, x1: 1.5, y0: 1.5, y1: 50}, - {type: 'circle', xref: 'x', yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 65}, - {type: 'path', xref: 'x', yref: ['y', 'y2'], path: 'M1,1L2,90'}] - }).then(function() { - var xa = gd._fullLayout.xaxis; - var ya = gd._fullLayout.yaxis; - var ya2 = gd._fullLayout.yaxis2; - - var lineBBox = getBoundingBox(0); - expect(lineBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); - expect(lineBBox.right).toBeCloseTo(xa.l2p(2) + xa._offset); - expect(lineBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); - expect(lineBBox.top).toBeCloseTo(ya2.l2p(20) + ya2._offset); - - var rectBBox = getBoundingBox(1); - expect(rectBBox.left).toBeCloseTo(xa.l2p(1.5) + xa._offset); - expect(rectBBox.right).toBeCloseTo(xa.l2p(1.5) + xa._offset); - expect(rectBBox.bottom).toBeCloseTo(ya.l2p(1.5) + ya._offset); - expect(rectBBox.top).toBeCloseTo(ya2.l2p(50) + ya2._offset); - - var circleBBox = getBoundingBox(2); - expect(circleBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); - expect(circleBBox.right).toBeCloseTo(xa.l2p(2) + xa._offset); - expect(circleBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); - expect(circleBBox.top).toBeCloseTo(ya2.l2p(65) + ya2._offset); - - var pathBBox = getBoundingBox(3); - expect(pathBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); - expect(pathBBox.right).toBeCloseTo(xa.l2p(2) + xa._offset); - expect(pathBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); - expect(pathBBox.top).toBeCloseTo(ya2.l2p(90) + ya2._offset); - }) - .then(done, done.fail); + it('positions shapes correctly with overlaid axes', function (done) { + Plotly.newPlot( + gd, + [ + { x: [1, 2], y: [1, 2] }, + { x: [1, 2], y: [50, 100], yaxis: 'y2' } + ], + { + width: 600, + height: 400, + yaxis: { range: [0, 10] }, + yaxis2: { overlaying: 'y', side: 'right', range: [0, 100] }, + shapes: [ + { type: 'line', xref: 'x', yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 20 }, + { type: 'rect', xref: 'x', yref: ['y', 'y2'], x0: 1.5, x1: 1.5, y0: 1.5, y1: 50 }, + { type: 'circle', xref: 'x', yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 65 }, + { type: 'path', xref: 'x', yref: ['y', 'y2'], path: 'M1,1L2,90' } + ] + } + ) + .then(function () { + var xa = gd._fullLayout.xaxis; + var ya = gd._fullLayout.yaxis; + var ya2 = gd._fullLayout.yaxis2; + + var lineBBox = getBoundingBox(0); + expect(lineBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); + expect(lineBBox.right).toBeCloseTo(xa.l2p(2) + xa._offset); + expect(lineBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); + expect(lineBBox.top).toBeCloseTo(ya2.l2p(20) + ya2._offset); + + var rectBBox = getBoundingBox(1); + expect(rectBBox.left).toBeCloseTo(xa.l2p(1.5) + xa._offset); + expect(rectBBox.right).toBeCloseTo(xa.l2p(1.5) + xa._offset); + expect(rectBBox.bottom).toBeCloseTo(ya.l2p(1.5) + ya._offset); + expect(rectBBox.top).toBeCloseTo(ya2.l2p(50) + ya2._offset); + + var circleBBox = getBoundingBox(2); + expect(circleBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); + expect(circleBBox.right).toBeCloseTo(xa.l2p(2) + xa._offset); + expect(circleBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); + expect(circleBBox.top).toBeCloseTo(ya2.l2p(65) + ya2._offset); + + var pathBBox = getBoundingBox(3); + expect(pathBBox.left).toBeCloseTo(xa.l2p(1) + xa._offset); + expect(pathBBox.right).toBeCloseTo(xa.l2p(2) + xa._offset); + expect(pathBBox.bottom).toBeCloseTo(ya.l2p(1) + ya._offset); + expect(pathBBox.top).toBeCloseTo(ya2.l2p(90) + ya2._offset); + }) + .then(done, done.fail); }); - it('adjusts shape position when one referenced axis is zoomed', function(done) { - Plotly.newPlot(gd, [ - {x: [0, 4], y: [0, 4]}, - {x: [0, 4], y: [0, 4], xaxis: 'x2', yaxis: 'y2'} - ], { - width: 800, height: 400, - xaxis: {domain: [0, 0.45], range: [0, 4]}, - yaxis: {range: [0, 4]}, - xaxis2: {domain: [0.55, 1], anchor: 'y2', range: [0, 4]}, - yaxis2: {anchor: 'x2', range: [0, 4]}, - shapes: [ - {type: 'line', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2}, - {type: 'rect', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1.5, x1: 1.5, y0: 1.5, y1: 1.5}, - {type: 'circle', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2}, - {type: 'path', xref: ['x', 'x2'], yref: ['y', 'y2'], path: 'M1,1L2,2'} - ] - }).then(function() { - return Plotly.relayout(gd, 'xaxis.range', [0, 2]); - }).then(function() { - var xa = gd._fullLayout.xaxis; - var xa2 = gd._fullLayout.xaxis2; + it('adjusts shape position when one referenced axis is zoomed', function (done) { + Plotly.newPlot( + gd, + [ + { x: [0, 4], y: [0, 4] }, + { x: [0, 4], y: [0, 4], xaxis: 'x2', yaxis: 'y2' } + ], + { + width: 800, + height: 400, + xaxis: { domain: [0, 0.45], range: [0, 4] }, + yaxis: { range: [0, 4] }, + xaxis2: { domain: [0.55, 1], anchor: 'y2', range: [0, 4] }, + yaxis2: { anchor: 'x2', range: [0, 4] }, + shapes: [ + { type: 'line', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2 }, + { type: 'rect', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1.5, x1: 1.5, y0: 1.5, y1: 1.5 }, + { type: 'circle', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2 }, + { type: 'path', xref: ['x', 'x2'], yref: ['y', 'y2'], path: 'M1,1L2,2' } + ] + } + ) + .then(function () { + return Plotly.relayout(gd, 'xaxis.range', [0, 2]); + }) + .then(function () { + var xa = gd._fullLayout.xaxis; + var xa2 = gd._fullLayout.xaxis2; - var lineExpectedLeft = xa.l2p(1) + xa._offset; - var lineExpectedRight = xa2.l2p(2) + xa2._offset; - var lineBBox = getBoundingBox(0); + var lineExpectedLeft = xa.l2p(1) + xa._offset; + var lineExpectedRight = xa2.l2p(2) + xa2._offset; + var lineBBox = getBoundingBox(0); - expect(lineBBox.left).toBeCloseTo(lineExpectedLeft); - expect(lineBBox.right).toBeCloseTo(lineExpectedRight); + expect(lineBBox.left).toBeCloseTo(lineExpectedLeft); + expect(lineBBox.right).toBeCloseTo(lineExpectedRight); - var rectExpectedLeft = xa.l2p(1.5) + xa._offset; - var rectExpectedRight = xa2.l2p(1.5) + xa2._offset; - var rectBBox = getBoundingBox(1); + var rectExpectedLeft = xa.l2p(1.5) + xa._offset; + var rectExpectedRight = xa2.l2p(1.5) + xa2._offset; + var rectBBox = getBoundingBox(1); - expect(rectBBox.left).toBeCloseTo(rectExpectedLeft); - expect(rectBBox.right).toBeCloseTo(rectExpectedRight); + expect(rectBBox.left).toBeCloseTo(rectExpectedLeft); + expect(rectBBox.right).toBeCloseTo(rectExpectedRight); - var circleExpectedLeft = xa.l2p(1) + xa._offset; - var circleExpectedRight = xa2.l2p(2) + xa2._offset; - var circleBBox = getBoundingBox(2); + var circleExpectedLeft = xa.l2p(1) + xa._offset; + var circleExpectedRight = xa2.l2p(2) + xa2._offset; + var circleBBox = getBoundingBox(2); - expect(circleBBox.left).toBeCloseTo(circleExpectedLeft); - expect(circleBBox.right).toBeCloseTo(circleExpectedRight); + expect(circleBBox.left).toBeCloseTo(circleExpectedLeft); + expect(circleBBox.right).toBeCloseTo(circleExpectedRight); - var pathExpectedLeft = xa.l2p(1) + xa._offset; - var pathExpectedRight = xa2.l2p(2) + xa2._offset; - var pathBBox = getBoundingBox(3); + var pathExpectedLeft = xa.l2p(1) + xa._offset; + var pathExpectedRight = xa2.l2p(2) + xa2._offset; + var pathBBox = getBoundingBox(3); - expect(pathBBox.left).toBeCloseTo(pathExpectedLeft); - expect(pathBBox.right).toBeCloseTo(pathExpectedRight); - }) - .then(done, done.fail); + expect(pathBBox.left).toBeCloseTo(pathExpectedLeft); + expect(pathBBox.right).toBeCloseTo(pathExpectedRight); + }) + .then(done, done.fail); }); - it('updates shape positions when axis range changes via relayout', function(done) { - Plotly.newPlot(gd, [ - {x: [0, 4], y: [0, 4]}, - {x: [0, 4], y: [0, 4], xaxis: 'x2', yaxis: 'y2'} - ], { - width: 800, height: 400, - xaxis: {domain: [0, 0.45], range: [0, 4]}, - yaxis: {range: [0, 4]}, - xaxis2: {domain: [0.55, 1], anchor: 'y2', range: [0, 4]}, - yaxis2: {anchor: 'x2', range: [0, 4]}, - shapes: [ - {type: 'line', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2}, - {type: 'rect', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1.5, x1: 1.5, y0: 1.5, y1: 1.5}, - {type: 'circle', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2}, - {type: 'path', xref: ['x', 'x2'], yref: ['y', 'y2'], path: 'M1,1L2,2'} - ] - }).then(function() { - return Plotly.relayout(gd, 'yaxis.range', [1, 5]); - }).then(function() { - var ya = gd._fullLayout.yaxis; - var ya2 = gd._fullLayout.yaxis2; + it('updates shape positions when axis range changes via relayout', function (done) { + Plotly.newPlot( + gd, + [ + { x: [0, 4], y: [0, 4] }, + { x: [0, 4], y: [0, 4], xaxis: 'x2', yaxis: 'y2' } + ], + { + width: 800, + height: 400, + xaxis: { domain: [0, 0.45], range: [0, 4] }, + yaxis: { range: [0, 4] }, + xaxis2: { domain: [0.55, 1], anchor: 'y2', range: [0, 4] }, + yaxis2: { anchor: 'x2', range: [0, 4] }, + shapes: [ + { type: 'line', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2 }, + { type: 'rect', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1.5, x1: 1.5, y0: 1.5, y1: 1.5 }, + { type: 'circle', xref: ['x', 'x2'], yref: ['y', 'y2'], x0: 1, x1: 2, y0: 1, y1: 2 }, + { type: 'path', xref: ['x', 'x2'], yref: ['y', 'y2'], path: 'M1,1L2,2' } + ] + } + ) + .then(function () { + return Plotly.relayout(gd, 'yaxis.range', [1, 5]); + }) + .then(function () { + var ya = gd._fullLayout.yaxis; + var ya2 = gd._fullLayout.yaxis2; - var lineExpectedBottom = ya.l2p(1) + ya._offset; - var lineExpectedTop = ya2.l2p(2) + ya2._offset; - var lineBBox = getBoundingBox(0); + var lineExpectedBottom = ya.l2p(1) + ya._offset; + var lineExpectedTop = ya2.l2p(2) + ya2._offset; + var lineBBox = getBoundingBox(0); - expect(lineBBox.bottom).toBeCloseTo(lineExpectedBottom); - expect(lineBBox.top).toBeCloseTo(lineExpectedTop); + expect(lineBBox.bottom).toBeCloseTo(lineExpectedBottom); + expect(lineBBox.top).toBeCloseTo(lineExpectedTop); - var rectExpectedBottom = ya.l2p(1.5) + ya._offset; - var rectExpectedTop = ya2.l2p(1.5) + ya2._offset; - var rectBBox = getBoundingBox(1); + var rectExpectedBottom = ya.l2p(1.5) + ya._offset; + var rectExpectedTop = ya2.l2p(1.5) + ya2._offset; + var rectBBox = getBoundingBox(1); - expect(rectBBox.bottom).toBeCloseTo(rectExpectedBottom); - expect(rectBBox.top).toBeCloseTo(rectExpectedTop); + expect(rectBBox.bottom).toBeCloseTo(rectExpectedBottom); + expect(rectBBox.top).toBeCloseTo(rectExpectedTop); - var circleExpectedBottom = ya.l2p(1) + ya._offset; - var circleExpectedTop = ya2.l2p(2) + ya2._offset; - var circleBBox = getBoundingBox(2); + var circleExpectedBottom = ya.l2p(1) + ya._offset; + var circleExpectedTop = ya2.l2p(2) + ya2._offset; + var circleBBox = getBoundingBox(2); - expect(circleBBox.bottom).toBeCloseTo(circleExpectedBottom); - expect(circleBBox.top).toBeCloseTo(circleExpectedTop); + expect(circleBBox.bottom).toBeCloseTo(circleExpectedBottom); + expect(circleBBox.top).toBeCloseTo(circleExpectedTop); - var pathExpectedBottom = ya.l2p(1) + ya._offset; - var pathExpectedTop = ya2.l2p(2) + ya2._offset; - var pathBBox = getBoundingBox(3); + var pathExpectedBottom = ya.l2p(1) + ya._offset; + var pathExpectedTop = ya2.l2p(2) + ya2._offset; + var pathBBox = getBoundingBox(3); - expect(pathBBox.bottom).toBeCloseTo(pathExpectedBottom); - expect(pathBBox.top).toBeCloseTo(pathExpectedTop); - }) - .then(done, done.fail); + expect(pathBBox.bottom).toBeCloseTo(pathExpectedBottom); + expect(pathBBox.top).toBeCloseTo(pathExpectedTop); + }) + .then(done, done.fail); }); - it('handles autorange', function(done) { - Plotly.newPlot(gd, [ - {x: [1, 2], y: [1, 2]}, - {x: [1, 2], y: [1, 2], xaxis: 'x2', yaxis: 'y2'} - ], { - xaxis: {domain: [0, 0.45]}, - yaxis: {domain: [0, 0.45]}, - xaxis2: {domain: [0.55, 1], anchor: 'y2'}, - yaxis2: {domain: [0.55, 1], anchor: 'x2'}, - shapes: [{ - type: 'rect', - xref: ['x', 'x2'], yref: ['y', 'y2'], - x0: 0, x1: 5, y0: 0, y1: 5 - }] - }).then(function() { - expect(gd._fullLayout.xaxis.range[0]).toBeCloseTo(-0.01); - expect(gd._fullLayout.xaxis.range[1]).toBeCloseTo(2.14); - expect(gd._fullLayout.yaxis.range[0]).toBeCloseTo(-0.02); - expect(gd._fullLayout.yaxis.range[1]).toBeCloseTo(2.18); - expect(gd._fullLayout.xaxis2.range[0]).toBeCloseTo(0.72); - expect(gd._fullLayout.xaxis2.range[1]).toBeCloseTo(5.02); - expect(gd._fullLayout.yaxis2.range[0]).toBeCloseTo(0.64); - expect(gd._fullLayout.yaxis2.range[1]).toBeCloseTo(5.04); - }) - .then(done, done.fail); + it('handles autorange', function (done) { + Plotly.newPlot( + gd, + [ + { x: [1, 2], y: [1, 2] }, + { x: [1, 2], y: [1, 2], xaxis: 'x2', yaxis: 'y2' } + ], + { + xaxis: { domain: [0, 0.45] }, + yaxis: { domain: [0, 0.45] }, + xaxis2: { domain: [0.55, 1], anchor: 'y2' }, + yaxis2: { domain: [0.55, 1], anchor: 'x2' }, + shapes: [ + { + type: 'rect', + xref: ['x', 'x2'], + yref: ['y', 'y2'], + x0: 0, + x1: 5, + y0: 0, + y1: 5 + } + ] + } + ) + .then(function () { + expect(gd._fullLayout.xaxis.range[0]).toBeCloseTo(-0.01); + expect(gd._fullLayout.xaxis.range[1]).toBeCloseTo(2.14); + expect(gd._fullLayout.yaxis.range[0]).toBeCloseTo(-0.02); + expect(gd._fullLayout.yaxis.range[1]).toBeCloseTo(2.18); + expect(gd._fullLayout.xaxis2.range[0]).toBeCloseTo(0.72); + expect(gd._fullLayout.xaxis2.range[1]).toBeCloseTo(5.02); + expect(gd._fullLayout.yaxis2.range[0]).toBeCloseTo(0.64); + expect(gd._fullLayout.yaxis2.range[1]).toBeCloseTo(5.04); + }) + .then(done, done.fail); }); }); From 51e1de92929a8b956c72d6f74e0ac388934b93cd Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 1 May 2026 14:29:17 -0600 Subject: [PATCH 2/6] fix: Handle 'pixel' size mode for shape labels --- src/components/shapes/display_labels.js | 34 +++++++++++++++++-------- src/components/shapes/helpers.js | 1 + 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/components/shapes/display_labels.js b/src/components/shapes/display_labels.js index a300e3736a5..8845c335342 100644 --- a/src/components/shapes/display_labels.js +++ b/src/components/shapes/display_labels.js @@ -104,16 +104,30 @@ module.exports = function drawLabel(gd, index, options, shapeGroup) { const xRefType1 = Axes.getRefType(isArrayXref ? options.xref[1] : options.xref); const yRefType0 = Axes.getRefType(isArrayYref ? options.yref[0] : options.yref); const yRefType1 = Axes.getRefType(isArrayYref ? options.yref[1] : options.yref); - const x2p = function(v, shift, xa, xRefType) { - return helpers.getDataToPixel(gd, xa, shift, false, xRefType)(v); - }; - const y2p = function(v, shift, ya, yRefType) { - return helpers.getDataToPixel(gd, ya, shift, true, yRefType)(v); - }; - shapex0 = x2p(options.x0, options.x0shift, xa0, xRefType0); - shapex1 = x2p(options.x1, options.x1shift, xa1, xRefType1); - shapey0 = y2p(options.y0, options.y0shift, ya0, yRefType0); - shapey1 = y2p(options.y1, options.y1shift, ya1, yRefType1); + const x2p = (v, shift, xa, xRefType) => helpers.getDataToPixel(gd, xa, shift, false, xRefType)(v); + const y2p = (v, shift, ya, yRefType) => helpers.getDataToPixel(gd, ya, shift, true, yRefType)(v); + // When using pixel offset mode it's necessary to add the anchor position for the + // correct final value + if (options.xsizemode === 'pixel') { + const xAnchorPos = x2p(options.xanchor, undefined, xa0, xRefType0); + const xShift0 = helpers.getPixelShift(xa0, options.x0shift); + const xShift1 = helpers.getPixelShift(xa0, options.x1shift); + shapex0 = xAnchorPos + options.x0 + xShift0; + shapex1 = xAnchorPos + options.x1 + xShift1; + } else { + shapex0 = x2p(options.x0, options.x0shift, xa0, xRefType0); + shapex1 = x2p(options.x1, options.x1shift, xa1, xRefType1); + } + if (options.ysizemode === 'pixel') { + const yAnchorPos = y2p(options.yanchor, undefined, ya0, yRefType0); + const yShift0 = helpers.getPixelShift(ya0, options.y0shift); + const yShift1 = helpers.getPixelShift(ya0, options.y1shift); + shapey0 = yAnchorPos - options.y0 + yShift0; + shapey1 = yAnchorPos - options.y1 + yShift1; + } else { + shapey0 = y2p(options.y0, options.y0shift, ya0, yRefType0); + shapey1 = y2p(options.y1, options.y1shift, ya1, yRefType1); + } } // Handle `auto` angle diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index d27e7264d1f..8b11c2ebd11 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -352,6 +352,7 @@ function convertPath(options, x2p, y2p) { }); } +exports.getPixelShift = getPixelShift; function getPixelShift(axis, shift) { shift = shift || 0; var shiftPixels = 0; From a2a7e70a10ed687f79e002145f1d42ed0c0753f0 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 1 May 2026 14:30:17 -0600 Subject: [PATCH 3/6] Add test --- test/jasmine/tests/shapes_test.js | 133 ++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 60284c2a54f..683d3bb3e59 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -2037,3 +2037,136 @@ describe('Test multi-axis shapes', function () { .then(done, done.fail); }); }); + +describe('Shape labels with pixel sizemode', () => { + let gd; + beforeEach(() => { + gd = createGraphDiv(); + }); + afterEach(destroyGraphDiv); + + const getLabelNodes = () => d3SelectAll('.shape-label-text'); + const data = [ + { + type: 'scatter', + x: [1, 2], + y: [1, 2] + } + ]; + + it('positions labels inside shapes with xsizemode/ysizemode pixel', (done) => { + Plotly.newPlot(gd, data, { + width: 600, + height: 400, + shapes: [ + { + type: 'circle', + label: { text: 'Circle Label' }, + xref: 'x', + yref: 'y', + xsizemode: 'pixel', + ysizemode: 'pixel', + xanchor: 1.5, + yanchor: 1.5, + x0: -30, + x1: 30, + y0: -30, + y1: 30 + }, + { + type: 'rect', + label: { text: 'Rect Label' }, + xref: 'x', + yref: 'y', + xsizemode: 'pixel', + ysizemode: 'pixel', + xanchor: 1.5, + yanchor: 1.5, + x0: -40, + x1: 40, + y0: -25, + y1: 25 + } + ] + }) + .then(() => { + const labels = getLabelNodes(); + expect(labels.size()).toBe(2); + + const shapeNodes = d3SelectAll('.shapelayer .shape-group path'); + expect(shapeNodes.size()).toBe(2); + + // Each label should be positioned within or near its shape + labels.each(function (_, i) { + const labelBB = this.getBoundingClientRect(); + const shapeBB = shapeNodes[0][i].getBoundingClientRect(); + // Label center should be inside the shape bounding box + const labelCenterX = labelBB.left + labelBB.width / 2; + const labelCenterY = labelBB.top + labelBB.height / 2; + + expect(labelCenterX).toBeGreaterThan(shapeBB.left); + expect(labelCenterX).toBeLessThan(shapeBB.right); + expect(labelCenterY).toBeGreaterThan(shapeBB.top); + expect(labelCenterY).toBeLessThan(shapeBB.bottom); + }); + }) + .then(done, done.fail); + }); + + it('positions labels inside shapes with paper ref and pixel sizemode', (done) => { + Plotly.newPlot(gd, data, { + width: 600, + height: 400, + shapes: [ + { + type: 'circle', + label: { text: 'Circle Label' }, + xref: 'x', + xsizemode: 'pixel', + xanchor: 1.5, + x0: -25, + x1: 25, + yref: 'paper', + ysizemode: 'pixel', + yanchor: 0.5, + y0: -25, + y1: 25 + }, + { + type: 'rect', + label: { text: 'Rect Label' }, + xref: 'x', + xsizemode: 'pixel', + xanchor: 1.5, + x0: -30, + x1: 30, + yref: 'paper', + ysizemode: 'pixel', + yanchor: 0.5, + y0: -20, + y1: 20 + } + ] + }) + .then(() => { + const labels = getLabelNodes(); + expect(labels.size()).toBe(2); + + const shapeNodes = d3SelectAll('.shapelayer .shape-group path'); + + labels.each(function (_, i) { + const labelBB = this.getBoundingClientRect(); + const shapeBB = shapeNodes[0][i].getBoundingClientRect(); + // Label center should be inside the shape bounding box + const labelCenterX = labelBB.left + labelBB.width / 2; + const labelCenterY = labelBB.top + labelBB.height / 2; + + expect(labelCenterX).toBeGreaterThan(shapeBB.left); + expect(labelCenterX).toBeLessThan(shapeBB.right); + expect(labelCenterY).toBeGreaterThan(shapeBB.top); + expect(labelCenterY).toBeLessThan(shapeBB.bottom); + }); + }) + .then(done, done.fail); + }); +}); From 48ff5122d9654618851e6ed6b0324d657295ea9e Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 1 May 2026 14:39:18 -0600 Subject: [PATCH 4/6] Add mock and baseline image --- .../baselines/shape_label_pixel_sizemode.png | Bin 0 -> 17076 bytes .../mocks/shape_label_pixel_sizemode.json | 52 ++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 test/image/baselines/shape_label_pixel_sizemode.png create mode 100644 test/image/mocks/shape_label_pixel_sizemode.json diff --git a/test/image/baselines/shape_label_pixel_sizemode.png b/test/image/baselines/shape_label_pixel_sizemode.png new file mode 100644 index 0000000000000000000000000000000000000000..6b0c58c14f89fa3f89fa093d4fc6b9507a57e982 GIT binary patch literal 17076 zcmeIac{J7E_dm`>NQMX@b7dw)%3P+XP{t>zWg zno4N0hP@WKX zxJ&ievi(>+-x?!V<%!6Ss}Vv;e2)^2k?7{{z;M^VIHlGR3elm zgI?~-)f+DF(ln4G`q)NMcEkBTOvyP&KddHngCBs{iKhp}k^lOxmOA7gxofqrFDOdJ{c^Nyu?bRHvv^E+R8HRnq)#e%_@BAZ*QP7UchuY)5H4RVY@ZfT z+6u1%wD!gkolDWDtNk~+Y!nYYCLxdDWzb>>b?0g<`(AHMjZ0=Ws`4o~@$lD|Uc2!} zhiG3Y?fjrG?AMl?kzc^qD{YK<6cP#e_Nx-rC%K86&6yJ8E5jA6xN9&{iG8Z-pJF(# zT~3>O^I5EPj8B6*z*DL|p>}5mGrAanU+=u%Bo-F9kXxGJ`dJCrqtXkn+M0be^CmND z)r2hO5(#XJVP8@Y_45MtVby-O3QQDNTliPAB?2_R5Agm_zWnWD&va%lV zsSl;SX<;GZG28Jx&#bbIj9#80ASRUz#_;hvPb|-o8Tkk=CPeBZ7?x$@qh)=A87CY+ zIuRADUoUCfalCw`i6gU;98Oq6$?TrcR&o9c%&$RUUm`2a9iJ!>sbV_!Ev}4OAr%@F z&NM0fn)yP>9hny)eFE!(z^7E=cenE@vFr*hwt%T`y6n=xP^qax7(>#NHDJ`|mM*8+ zJ?Y;)v9w@8^usmEY*3V`J!SDFc2xDvmsK9BT(0rQMJGMy*Jwj~PjK#5J%b3>I3Sz> zk(v%OGv56m#_Oe%Bjpt*mm+;SEF;BAlVI5s6pa-KU# zWx-3w+kB5L@xT=LDbBl$2?DFN@|?k=h+8Q45r-TbDR;&Coy}&Y_~n3Q|K9H5R5-!+ z?c2Alc6{nUAKdx(xi6Dd|td@@|%maG~l`WS0PcsFX$4ty5X-+bi%ziOa=pYx7A9BGZOG{t_$^~q*OyZ`%+V@3{}>7`}tJDZAP>$ zx1869+urC0LK;se(A6YO0W5)I_nRA+cP`NnZiI5UM04+AGYD1 zx8;i{Id2ynb_I?vg}=X$RLSS%YF4?BSDEpCa%5x#b4naLI?H<74H3yMtp&VoX~dM2 zObcI+*%Fp>shP{)>DVH~3N6ebxMx@Y_wPXqWa_ch2tXg34U|kC<)*Z zT>0D_y7+rT5MF#edWZ4(#O?gmW+lQ6m+IfQE&|gNiJGnJ2jZf5gTGbfzm6Blui~p* zEPT4NvpZ<6O`c ztu_f}r-^Gy#G+v_?~S^+;Uc@I{%#$1N_gX!QVus8!^X;3Tgo;{DihkuBwFtS1vyWZ zMO_zd84{3 zFOEI67$b8Wn?qw+s`0Ma>ke2i_?jICyFNVK8k0Fng!Ce_y8+Eyi|0?zoLYtxD;KNQ zx_a+ETxn$0V@wBD_Wh}H5|0xb#%0+}$>ITbWx&c)i^#e$w2-Is7)8U^w|X-pj``2( zs&fNc&^Ew@!vzg30v=@O>3DF3pFUCyT;fe5JHL&=#UyD*;c;Pa!(r^V_{w{Er+d)M zM46hEXK&tCUMDUS3Cq3A{_UAFfV!eB;BKxrA-TDw?=Q=HTYP(uH5hUz4?Ln zhB(S#yf--ad_IJbx`e6Q?v$B`bn&hQSM8hCTdkU_gzyF`t9tdR&dK z9UTdam$1-I6Zv=q5Jb)zEZ%_*c5^jez_=msU-fjeV->^Rs=~j?&kcB@JotkZrjSkdh>||aylFBo4PEQ)S!Ozl{WkDSX8gDr zZKRVh&Urym^|hnv4Jc|rK{aJ*wV zv@xS=*rUbRHMb3-H|cT{7UQNQbH8*0U51^wxaV044|{rl!rrZ4BE$Y13kV%AaU8@< zex|6RaoVUpQ94&#Lhn5Ek#{?=`??JJ*zPeI?DJXq$u4Ghe;}EPFix*T;I%k=UE9&M zhO)O4DlPTW@wLtvX8c*Djndz6-rS#)93&x;3{DjGE@1b##N zc~9JWP}`T3+e;wo;<0+~P;soK0CJ0;#zD z=7UA8V#O|YouhOa&{$5yn%D14fRke(&_gw)_Gf=v-A6oZB+k%W2y2klX`z1gV%7#0 z90JRey+#~*?X2SzT{p~6FkL2rEiwK%iREgg<>-IjWnLKnX@{PnW?p?YfI_WZlr;|X74U()_ab(;(H{~5?t>a#N0(PYB|&inBFkn_BDlCxVm z%`WY8u|t1Gm{^%>ctvx8iR>5|zimh2Q3d_F0J*)R&40$e&hE<-wy&J0fw2;W74MvW zBy^6gZWq_7!2WHwxHG_XeAnr}Ma*y~Y6O~fA!Ckpg4jIY%K`^G{>p7?YeWjT@LE_;LnqGb;bZPU( zxc~AIjcymg4V=J6qRGYuWAFE9v^zx5;sHUNs%dr~l-Fd&OGjNQF?M%$@f+Pfp`P{S z8!XE;cx`@OE zzO^di7nyd`CGR#+l>-to6U3ln!+GlcgIurp zm9qT(M{`TZ*#p?Rs>N^MSvMw8yb3(J;s<94JQXhan47DD2P(Z%hPhnV+f~^kpI3C! zE&u#Zmv;?Q^-Qk+26lDG3aJKngVy|Xu%-)NkB9!}YL3lBtEg~Ww}~L{W%1?FQrD<+ zE*FdapZ%1n-Ri^?6l2{CG1v#WP_z#?STqwX%Iv1|++$f24vwYPJ!zLF1M>;FWoe}1RlQoHRbQ{j=6Fajqg}=X<`pfFQ zMnG4;J^iD%s3?`8M__M|ZqR&r#Gp z8l~glG~hm$;;l{`Juxk}R2*~L?F+?Z&G2}Yk$kGK9=Kz0?JHmyWvg6ru(HGzgh*Z) zU&RYAdOru>GTjN_+pU4uizX#Iq)Pvq4RXf%r8Dgb8cPXcrYyU76aklmxwt4sKRCYo z#cj1!fYsCX8VxDcCg1)Uu*P%laJ7A(bH{RE-z>}*J67)A%<284QVcciRx98=VeLI( z&VA9{+ENL5^`=|^4+&zL{Bk9}$)oFq|Cis>hZQ($ulZMw&SG<1Bx*J@K;V}v^!xo2 z14qkDqkq)A#VXQxTfFul%K7~Ij)||RH0uTj&{!l1!d$UaPSy&>sRYyrLJt1_gD?DF z20qY1l)&vMINZDUVr0aGY~W_bdDy1LE51)ZzbJX(_V3w2q!7XQU|FMg* z2um#K^J585;K!a?rAh5rDWA;iS$fhCT$mZxq_u)nH(2}{0$;RmWd3inmR&C8XQXM1 zy2UQisKjnipvOcg$YeIe94fwhiH$+?e)y(F!G%Lw^-70SCl8~eu80cCnK*nnMzq&& z)V{vi9Chne~Qn2ef!6tb(1h+7#llpK~ zv;0>bjSVoLzPEZg!hSifb-`NQA3ONJqt3oZ5yn<@{6cHgd3u8ZbSk41-k4sAMJ9?*MstN&cSI=Z&1ecgTYBk9fl;qJ%RUL>`ThbbfH zg8-P&c5yD5f6xLR(2?5us)$#>PDAJD&Lf%^3}v{~+SWLF#aiP*y&5T_8u;g&m?Ik8 z^yE~yay&Jv>Sfrcg}B~3r-DJmxarQ?vT^I#J7iV!X30&0^KFPh9aBnP1 zc4Kk)Txmi3nKKCU$8F-*?7EVLN=ix~-z)5AJsM*AdU#cI)P6I^DI!N{f)*@E!#W}1 zT8$^5zsydpp)ew)NaEdX>f^ku0E%Qb$}gLYcry?}&GgPbC4lMt^y15S2h||&0jhn`)mX6c3K8bSeVD?xQyz#5V{mso8Eb6Aa}et zKj!|T*ywm}C(DCK73z9;$V#Ov%+XlAQOV|pEZX-I-I}S+xQ3$1V2XuiR{hTs83dNu zyRdaAI_7@nAfEgez4jZ`wCXd>N^vdo+sot?;tC@lLTQJCu0&jIP>N{0p=+XH2`(ZG zg6h=den*Vdq-|8hBOV@Kn?#-Q?3fZ2etZ#;O8I-I>15BE)vMtar_Hknaz@tW?%D<*Lem>G&kQ zzLf`U+*~M?pLxM3b=AxZCL5_j;*Q5hhbfjZFi+Ot< zV0tW>MFq+Dgv5^$1zDO^9K<_x%CPre62~}tr*2P$Tr|XRk+-=WhAF5d`qDOrCbJH^ zM?QtOrF)p#y*v-rG#%&dp~W=*%iXk*2@7CgPEeOlg$Z*}xzq2;`RNbg@s9F4dg9gS zVO!?^#(}$_So;})9uEe=W?pLCi66GoQXKBLPPSj9BM2d}#$gu`F70x_yH*cH(>}>6 zQ{BllNQM=2bnvy9Jqb$;J$~UKsA)pV3iCQin@HSd*`R`Y4+xc!8Qd>SZvaH zz|isDRGwsh1-u*wo?22%gbz@5942J11ynBmCbd&b9M#H%9A6N85uf=62*=qsOj#z4 zm80+=Xy~aqsDXvdA1g;BF<>GZ?DLPEw?5d*oXZYs|5a>sTKx?FZ3{+(u3t@L!hEVy zN#p2w`mY%{WJ+BJ8#=m5hj3Ar_-g?JhY^VVDNaM7|Iwo6z+Mkpex%F_Q~xKM+_A5v zf!6GfI~eo+=O7JLDL`)Cs9f%Q{=YSY5m{1YwJRffz7m7&PsWvtaG(T1%BR|KAJ2QM%-kQDF#gK`U*UKNre*? zoRvSs;ocQQ#?3|CJbIx4YVr|yZ>+_Qp0JC+ zPiMT)JL}`rVt$5Fj+)S_GZJrEv(W6<`n!2qh&y*7bs~f!G7^3`DAld)CZ*rGqShFJ z_AJeq5n9~V+FX0`MUUwQCY{(Y?m(jo&v`!a=>SeqL5r;VWJ=<6rsKky+O4eWY;R=V z*0o#T-)!b6Z%XEz?orFSnU-%{q~4`O+`*i~fxDub;an%GG~c8|`1I;#CkF>drZ#rX zpN^{GR1kcU@%>nhrAp?tWIU_!ufEa3H7^C5bxG87^O7Hr%lUuwD^>gbes=`Lvc$;K z^PUQ~9Jo>G{_6{+1}a?%zgXnntH!Ekk;QSrEzN1HOw!w5T1x5)$eVdCbA8LL8v7!7 zNtJ|X?HKZagMB8;LV=fFsrIthaEtZoX>y}x%D9$ByPki zX2u_Oq$-$huP=#Ysz1}xR#ew<@ofxJ>ldT?j#|8SBYt#IEc^#*aVKWu3b`K#0_!5# znIwI653*s2#=jf^a5f)wllov*c?(J{{r>&?tkclPs^8&Fc9(p=oLD@$DZt^-etdRO zY1pTEP;h^e8x^0uN(U%u*-t)Ym56_zUn6_i!+QZAL`E0QkCg9AP$#dD>9f>UP0JZ9 z`2dB%ji~OURGz3}rY;bB7*3$e>SGX;Z6|=;9Xj-n52wOqO=Frvle@@l=sk8~x|C}C znJ_is0*Odv9g=6_uK-KdwmcmZbD{7TTRBh6$^DLgwd!gOj-Za5eRcG+(hBXRM0m>) zV#9{c4FxJ(MoV*I0tGV}%k@$ouzfFs&rBq<62j+De5OnowZ43J7Z&}vH2ZI-!4a-n zl4?!N_y_9euAb*rShYRHyF?U)0maFr;;SS?eLK3kDrfD@99g2hy;_>1Zu(`~gJ7%A z5_ZNFb(HXSky}iR%2Q0sQ)Z#gZ^t5GXD zzduB{Xr{5|Dl@MYrM(MMjq#%Xj_T}Xm|bC^A%r)-Z}~><_Z;^=`4xr1DZ!)?O%me6 zCy&n{Yp?v$d1$Nlo2Ta~AGeh`i~bQKDReLi!IzubGS%rmknLCmMlFr?lEQ4V=$pjP z_BCvzJ1U7e#*CW+v>My{z$YY>Fs9#P(hEwYs*X7=^7J-+hHPWPE{7Sh}J6I%Ywk)jfdM> zT{=~#p2KL7&NMqbjM^2Di}yvUrp;Zy@My(YmrYr8g~4Wx-ZC|JxP2DoG{439OnIYS zuGgt5R#EYw7C`ik>JUc1PQN;DCvds zEv+*0ylW_b6|Y?*iSj>=EvNH_dr?_02y;vLN$=7LY- zYpy5GP5Z2)l0VO$5k;huefmmpY=j!!JvF&l{KUV7{vegARlN@sWcu5pVhQG*%R|(Z3sy* zbJQzH@9`veUDa`q&`H!?%vMU=zU=nob*zV3*3vvVWf&e|#qDhiQxGqJjTEIF4&v&G z{kE%;$m*!RKoE*gof!Um=SgpaeQF#{+|BW!eU}TIQM^84$w-%?j{eTQ_dv)G61(5Q zJ$u$nDgF{l>o2fWUUSW&l3(!~eQJbxcyutaEBGc)@DIihTVZDgZO%IqdeypJrT(7^saCyn%K6x#P8j*3{x=x;H z>o9kgAn{n>y^PETDNGXD|9%u~*P;JF#kZgVYU>QU}mbahjF2yp@LPQBt*=rDe^hUaHyBg)oxTTw~ALT@`1~E zx0$5jBe{wP3-S2d*BH1+CkXc~6|vg_)+_BIHVI1$)1gxv(c>j%^<<_m&aKX)^2}B= z*-ejPm|6TOeBR@}Ia05dnmAlzlakmk{(O^Z{rl&D!=~ZDAetGvmy4TK=ntf- zGuUf>&Vi4EOV~vDywKL#szISgZR%NkVtPpVeYtI%j+t4?eUrv`oisx#Pl4`U4Q??2 zgT8S7G<^DId%$S5VZOQh=8rJ{8%R@g=$sQSL3DeiaE)ebRVNX*lgf-3S5K>~yTpZhM}IIa)ec;ZLA5aH$n%;ym&Bd$=#T_Qz$>m=1!-03bnY2d zz|tulUR@W6D}~p+p4(WY`VTkX(ytWayLrC7U+G8xnt$kD=9!aBmU2cStZ@jwh`9!C z8h;?DS+Ie(3%;`@#D~Gpia-CM2nwM%KEn1xV#f$%QUxA@VvfXVT|N~?jFm(P zLRirgDsp@5@)s$Gmm~$YAF;io6z@STOi@?@nRJixVk?2xaWl$P7)kA zXJwK4JeFg6iq8GBM7CbzMekkDaUo^ z(r~YqRrY-lULEy64kb-)<_u}QOcC)U15JCN-@rETB%*f_*#-+J*j4L$08sP^qOj2r zc@}qTk9v36d+(Z*WQDU2=k?G+O%N(^YZVC5NS-3wU=?5?Sh&N8EKfd{|cWU?qC=_A2X6&%2)J8s^b^`EqYhSztC6IXU z3W+}3)kqgyidFqr1wKJrX(X7PT|g9Me*LWQula0=QeIeLD_e8%|zabMK6# z)S#Y>kGcV9+H0OIDu`!i@jcX~wEV$TV&di6NlWuA2K6~xVgdh&zhF_3Th;smJNF7i zjXy<*cr?m7hyhC8pEdoac0ft?sfz9nWVs(g)YtcvWu89&l1%~89^{9%|Dot(L%c;0 zelf`W>|9oW55>F7Da8p5giJGD7&_`QUtRFoQwb4wWoXtA)iXb>yc3-L2j9MV#^-eO z4zVDU^&etxgy+JO*4se`F0;xj&d#|R!a|LQN!PhWw88-lu@B^GhCAn~@$+kIAWy-AXeXaw z8yhUBOt5OEIv`5R=S7@^C@e1d?mLu8pxW_cCZ2(WV0hhD0Apb)b z=jh}V+pnW!rnnNW(*nvzs^4}5uZE5+s47BxBJ22Kpd%q%iI+Z zXK@g|58r8qt6C(zfBSZruSdpdDEiffPBIxODOKV~u4(Dm+}+@ZRApzByw z`-cpblTV?qzoKS@p}zZH4XUo(0!XFAxZF1p$T#!s*|L5CpD_SwtBsaJ&d33$!@Tcw zBA8nJfEhRX3dFt~1`})U5n&K{g*OC%eUaXmli~oyN&DOp)DE8lz#(y*+F2d)*ZZDv zi&-cOLf-rY8ipY%wDzW$(aGw8EORbJ*A94;OwoOh!u|a65dakBAug%=o4YQA^d8CgJb+&;;xOhP!Y-rAbBu{sEZnNs`wDe}cZ zP~*cYz1KGYCvUFrkr7vYMXdheM9QZ)i^#1ia5{j z+yBZ&lK8N8hP~8I=9MFlvcCiOzpr#Q0yl^2l1;J0zBwQGfv35Vy65kNIi9y22f)qw zaiKLq;N!mv?D=>tiJQh?3v`xK3XzbnBi(0nURBrwusL6);(8RwPn>7Jv-r(ns56$+ zuSO>b2!&~1nO}sfCV~W=2VJ!ZBiP3&$~|ZOW|QyeEce-ZrIlS3`W>rYGBEYPQ1le& z?Ak>`Y*?euT|pYqgHRi5rIKfUvhLy4Tbnt4#2!YX(K_Ue70yrpRORa!oehD%1D$^% zA3&XtS*zrUGFpx+^6%&kD;0lM;`M<%Isjps!Frr)Uz859l73W zKw0pf2=943r&{xQ-KezU)9dE}10kA!pMS+;;b)MpMmtO>HU-7K0f@Z;KLfx3YuiSp z*0SD(0BFD;fl;kVoo`ff!1S^bhgP6f-xo><=5_SHEM6pNY9Pl!PfBR&INwW0XXj~6 zY>8Pp%~bzu{UUo~hDAmTygpMXCRitL>KNClG~8ze z^5M;701NhAn{5rIpvgelTzh#o8?B5wXBQQDT=Mev?*#!J#jUK{K0h9x^~Tja#RJOcta1uB1-ShnAM29_MXY`hJ8k;1Gz}=WL2da=vPakRh5TB@pEdWVn#KlP z?Cqmpzdk)bUhR+dV3z*URld<-O?g~`w_xBp5M>+v8b zq^C{I$83?yVPy)J*L7~Q6eD#=hz)kP^yy*M5z5}BmH*~}eU?ogSELcj{Zb;UXK!pR zh`hIkyuqHWv%8erIDgdLjgb~;a(qQXD4063cWX49`YUeDfF}x1j-{Njyv*AcT9$S@ z5lLv|U#;VCq*_ZhW6prPt2g}u05d3p=II%w^4vHTy}iAS*_fA>mOkq;Rwkxct4P?K zf?h6|$I>|i9W!?-a(L0RGnK$q1d1m1w2Zkgn6G1(L;NIhGp9N7KqDU)f_=Z+!4d(F zNN@h|&)1LO>trNLFgzl&x#Qrgv4w7kZ^7vzEb8vOq#WP`x^~@)XFX)rsD>HVl13VFd$;mG?@RH;I?i^IhU0&4$0-GAAHcY1nPP=PMV0x(2)M8s|o0; zsA9Z|RP@t_Oak&0adA*rsTgJ=CQHNMpRPurt2w{%FQmDje&|8hb*St4=Mes+@!+7V zgE{CLNanDP4D{1~a?te~=*n>h`NaM=lGaZ@{NOWWl^kcfC{NwPAvyi@A?@BH;?v&u z>rWX=wwhTK-Zd(D$%89*k5)gBFYWQy2H{CNR!A=9sXubhwgLNbsH0Ck++@cphRfie z@0QT_$Ug0G_8qGPuDFBm1~_P*#H>e*PuG#oe)`Bi>l!?#i8v#3C;IRymU(VG?gANP z=aA2jfJ8;=p?~A<4Z_zOEN+q%FHG*T~(bf&H&XP}N zl2!mdCn?F^Dhw#+ej&_ic^Z`mS?Ckv3>{*F=(E{FK=sJ@K;@0bNee(k=~VcIxb0P) z@ z-{-i2xq|a~A+*DsOGR&_9EsL9n5>5W9aRWvAL*BK;*wX1*0yuuux>2so3DP-HznV%4T)Zmh3*w7y_bUa zipQ<%)aw5B)%c zGUOVTKwKIN@;R(!t#S*3o_`@ZSD@fnQnJUWkkE5l<8TJ`cCdU1wETb{WD=@+`g|OP ze3D1vfs1&eo@V%V06ab0iz1KcWUr_{V>htLcSQ|y9l>ebk?t} zu|HSN4_gx1ryF>z~_t*_D^aI0u#Q4z3)s9u>zOP?Taxa+)XjuGyQuV8e{XYG( zAKtSDPc)b;Rhq~hK*CE3Bv(%Ly#K!2cU3~OJ5^x@JV|{{orfT&W7Xp#(lhu0XkduX^$kMrL*w1?&`#L_ zX9Iqkt|6ImV^|^^`nr@s#$x+7CDji~(T>6PtY6gttPzf`JmX=mh{mCFs zYpYXOe&*_<89e1fS@i)aF*#C~m+@%Q`7Ul^*3?{)41(*?r)A}do}rVa z5+t4}C8DdT1?&{64M4WcbKxmoP^6bgAXFon=tHDuN0QXmWV+`fW> zbmx&!AH!#GklI@M8?@gB_W$KCDgK@0^5G$aEh@1oRR=H1t`Hk$BPcBX+@ma8R* z1g~?5L4o#Cnm;VXmYqeP9XvEs^5LN+-H8cJov8T>jupu3Sy4VjJ_J7-gmIvPp_wpJ z{U6i%dNL&lU&#JdgMS&g|2T^BEYD{`45_M)3IirpIp<|4MO-LXzzRLr6%`U~p$<)cYS*HK4!{Mdt)H^^l>r9|II;V@6(zm(ZQMa=)q5#COpLL8y`KeK=q zJC7*;o2(}fbJANY2ki=wJxLAa=}|z~vO}+G!E2*ba3qqSKYM=v1?^`h5a*ffp*mvd zMeVd^kaBm7xRuI&VXQoORjPo{mhBuVahw4*^Q-~|Cv0hkrCU%_=}*PwpaM;_m3BSn zzvYge<|`d@e~MciE+P-P(0GBIK-j2l2XA4;pwe8Yl7U;Rxvl|MYqmOFo7jRsr2>Uc zMKr&}f+qwCAa`L188XPcP$0M{&09xqEyO{E2pMWmr;GcwMc8V_A%x8Fo+@#tmi znYK9h#e(MekRv%SnGxKCUtxQ1G(jo+tAXgq7=Sb@Sf%+Mnag_<2;M$2VthZ-I(2B7 zzm#!J9Rtt7`br!P-{$7Vfv1zvqi+ztJjnk0tjti3-(&XLuuJ9AYGSbDIRMeykw`A{ z3Oo1tc6V=jQR%O0Sy^RD0RRORE^bkCyGw3ao=wh6)X>c9quo(T96P6o@RhMqP>1M!%?)y zB0=PtqTW=Ly(&iNWi%vH70<*$DA!o&cY0Y24*;zSptn?gnHu!IOC^KT9p}1Zp*oSC zbQOiqRF*Dmzw zr6^GRZvqM#IRiXiLiAj4tpX+NP{rzXq61Q4{Ru2pMSv7$mLslDvG)L+;fY$1GV<}$ zray1HQvj1G6}rrSUVgKH&0-N}|D&j$pi1Py&&vk;eGx`L$BoMDkL*v>1C)C3EsX)F zXouSG0%fuKks-N1k@Ej8x@Y?TiZ^~~{2B=f>=yKQ0rno*|F5Va3H`59{l7OK1d%eV YEr5wdh_@H~8440jHEq?r%XfnRALUQ19smFU literal 0 HcmV?d00001 diff --git a/test/image/mocks/shape_label_pixel_sizemode.json b/test/image/mocks/shape_label_pixel_sizemode.json new file mode 100644 index 00000000000..bece18ba1f3 --- /dev/null +++ b/test/image/mocks/shape_label_pixel_sizemode.json @@ -0,0 +1,52 @@ +{ + "data": [{ "type": "scatter", "x": [1, 2], "y": [1, 2] }], + "layout": { + "margin": { "t": 92 }, + "shapes": [ + { + "label": { + "text": "Circle", + "textangle": 0, + "textposition": "middle center", + "xanchor": "center", + "yanchor": "middle" + }, + "showlegend": false, + "type": "circle", + "x0": -25, + "x1": 25, + "xanchor": 1, + "xref": "x", + "xsizemode": "pixel", + "y0": 25, + "y1": 75, + "yanchor": 1, + "yref": "paper", + "ysizemode": "pixel" + }, + { + "label": { + "text": "Rect", + "textangle": 0, + "textposition": "middle center", + "xanchor": "center", + "yanchor": "middle" + }, + "showlegend": false, + "type": "rectangle", + "x0": -25, + "x1": 25, + "xanchor": 2, + "xref": "x", + "xsizemode": "pixel", + "y0": 25, + "y1": 75, + "yanchor": 1, + "yref": "paper", + "ysizemode": "pixel" + } + ], + "width": 500, + "height": 500 + } +} From b277856579c8428805ab2307b9501e1650b33f45 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 1 May 2026 14:44:59 -0600 Subject: [PATCH 5/6] Add draftlog --- draftlogs/7790_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/7790_fix.md diff --git a/draftlogs/7790_fix.md b/draftlogs/7790_fix.md new file mode 100644 index 00000000000..9c1c6f47ee4 --- /dev/null +++ b/draftlogs/7790_fix.md @@ -0,0 +1 @@ +- Handle 'pixel' size mode for shape labels [[#7790](https://github.com/plotly/plotly.js/pull/7790)] From 5616f2410ea29b2b1a717226b96ab5c8c236b8c3 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 1 May 2026 16:13:34 -0600 Subject: [PATCH 6/6] Use valid type value --- test/image/mocks/shape_label_pixel_sizemode.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/image/mocks/shape_label_pixel_sizemode.json b/test/image/mocks/shape_label_pixel_sizemode.json index bece18ba1f3..066bbd2bd72 100644 --- a/test/image/mocks/shape_label_pixel_sizemode.json +++ b/test/image/mocks/shape_label_pixel_sizemode.json @@ -33,7 +33,7 @@ "yanchor": "middle" }, "showlegend": false, - "type": "rectangle", + "type": "rect", "x0": -25, "x1": 25, "xanchor": 2,