diff --git a/src/components/annotations/annotation_defaults.js b/src/components/annotations/annotation_defaults.js index b59c0aacd59..f81436159da 100644 --- a/src/components/annotations/annotation_defaults.js +++ b/src/components/annotations/annotation_defaults.js @@ -83,6 +83,9 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op // xanchor, yanchor coerce(axLetter + 'anchor'); + + // xshift, yshift + coerce(axLetter + 'shift'); } // if you have one coordinate you should have both diff --git a/src/components/annotations/attributes.js b/src/components/annotations/attributes.js index dd07be6ce28..779f4a899e4 100644 --- a/src/components/annotations/attributes.js +++ b/src/components/annotations/attributes.js @@ -180,7 +180,9 @@ module.exports = { description: [ 'Sets a distance, in pixels, to move the arrowhead away from the', 'position it is pointing at, for example to point at the edge of', - 'a marker independent of zoom.' + 'a marker independent of zoom. Note that this shortens the arrow', + 'from the `ax` / `ay` vector, in contrast to `xshift` / `yshift`', + 'which moves everything by this amount.' ].join(' ') }, ax: { @@ -292,6 +294,15 @@ module.exports = { 'corresponds to the closest side.' ].join(' ') }, + xshift: { + valType: 'number', + dflt: 0, + role: 'style', + description: [ + 'Shifts the position of the whole annotation and arrow to the', + 'right (positive) or left (negative) by this many pixels.' + ].join(' ') + }, yref: { valType: 'enumerated', values: [ @@ -342,6 +353,15 @@ module.exports = { 'corresponds to the closest side.' ].join(' ') }, + yshift: { + valType: 'number', + dflt: 0, + role: 'style', + description: [ + 'Shifts the position of the whole annotation and arrow up', + '(positive) or down (negative) by this many pixels.' + ].join(' ') + }, clicktoshow: { valType: 'enumerated', values: [false, 'onoff', 'onout'], diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js index f68ea537c63..37646b3993a 100644 --- a/src/components/annotations/calc_autorange.js +++ b/src/components/annotations/calc_autorange.js @@ -50,12 +50,17 @@ function annAutorange(gd) { ya = Axes.getFromId(gd, ann.yref), headSize = 3 * ann.arrowsize * ann.arrowwidth || 0; + var headPlus, headMinus; + if(xa && xa.autorange) { + headPlus = headSize + ann.xshift; + headMinus = headSize - ann.xshift; + if(ann.axref === ann.xref) { // expand for the arrowhead (padded by arrowhead) Axes.expand(xa, [xa.r2c(ann.x)], { - ppadplus: headSize, - ppadminus: headSize + ppadplus: headPlus, + ppadminus: headMinus }); // again for the textbox (padded by textbox) Axes.expand(xa, [xa.r2c(ann.ax)], { @@ -65,17 +70,20 @@ function annAutorange(gd) { } else { Axes.expand(xa, [xa.r2c(ann.x)], { - ppadplus: Math.max(ann._xpadplus, headSize), - ppadminus: Math.max(ann._xpadminus, headSize) + ppadplus: Math.max(ann._xpadplus, headPlus), + ppadminus: Math.max(ann._xpadminus, headMinus) }); } } if(ya && ya.autorange) { + headPlus = headSize - ann.yshift; + headMinus = headSize + ann.yshift; + if(ann.ayref === ann.yref) { Axes.expand(ya, [ya.r2c(ann.y)], { - ppadplus: headSize, - ppadminus: headSize + ppadplus: headPlus, + ppadminus: headMinus }); Axes.expand(ya, [ya.r2c(ann.ay)], { ppadplus: ann._ypadplus, @@ -84,8 +92,8 @@ function annAutorange(gd) { } else { Axes.expand(ya, [ya.r2c(ann.y)], { - ppadplus: Math.max(ann._ypadplus, headSize), - ppadminus: Math.max(ann._ypadminus, headSize) + ppadplus: Math.max(ann._ypadplus, headPlus), + ppadminus: Math.max(ann._ypadminus, headMinus) }); } } diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index 80c5c76a19a..371af5ef0da 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -236,6 +236,7 @@ function drawOne(gd, index) { // but this one is the positive total size annSize = Math.abs(annSizeFromWidth) + Math.abs(annSizeFromHeight), anchor = options[axLetter + 'anchor'], + overallShift = options[axLetter + 'shift'] * (axLetter === 'x' ? 1 : -1), posPx = annPosPx[axLetter], basePx, textPadShift, @@ -326,6 +327,9 @@ function drawOne(gd, index) { posPx.text -= shiftMinus; } } + + posPx.tail += overallShift; + posPx.head += overallShift; } else { // with no arrow, the text rotates and *then* we put the anchor @@ -335,6 +339,10 @@ function drawOne(gd, index) { posPx.text = basePx + textShift; } + posPx.text += overallShift; + textShift += overallShift; + textPadShift += overallShift; + // padplus/minus are used by autorange options['_' + axLetter + 'padplus'] = (annSize / 2) + textPadShift; options['_' + axLetter + 'padminus'] = (annSize / 2) - textPadShift; @@ -524,21 +532,17 @@ function drawOne(gd, index) { update[annbase + '.x'] = xa ? xa.p2r(xa.r2p(options.x) + dx) : - ((headX + dx - gs.l) / gs.w); + (options.x + (dx / gs.w)); update[annbase + '.y'] = ya ? ya.p2r(ya.r2p(options.y) + dy) : - (1 - ((headY + dy - gs.t) / gs.h)); + (options.y - (dy / gs.h)); if(options.axref === options.xref) { - update[annbase + '.ax'] = xa ? - xa.p2r(xa.r2p(options.ax) + dx) : - ((headX + dx - gs.l) / gs.w); + update[annbase + '.ax'] = xa.p2r(xa.r2p(options.ax) + dx); } if(options.ayref === options.yref) { - update[annbase + '.ay'] = ya ? - ya.p2r(ya.r2p(options.ay) + dy) : - (1 - ((headY + dy - gs.t) / gs.h)); + update[annbase + '.ay'] = ya.p2r(ya.r2p(options.ay) + dy); } arrowGroup.attr('transform', 'translate(' + dx + ',' + dy + ')'); @@ -594,7 +598,8 @@ function drawOne(gd, index) { if(xa) update[annbase + '.x'] = options.x + dx / xa._m; else { var widthFraction = options._xsize / gs.w, - xLeft = options.x + options._xshift / gs.w - widthFraction / 2; + xLeft = options.x + (options._xshift - options.xshift) / gs.w - + widthFraction / 2; update[annbase + '.x'] = dragElement.align(xLeft + dx / gs.w, widthFraction, 0, 1, options.xanchor); @@ -603,7 +608,8 @@ function drawOne(gd, index) { if(ya) update[annbase + '.y'] = options.y + dy / ya._m; else { var heightFraction = options._ysize / gs.h, - yBottom = options.y - options._yshift / gs.h - heightFraction / 2; + yBottom = options.y - (options._yshift + options.yshift) / gs.h - + heightFraction / 2; update[annbase + '.y'] = dragElement.align(yBottom - dy / gs.h, heightFraction, 0, 1, options.yanchor); diff --git a/test/image/baselines/annotations-autorange.png b/test/image/baselines/annotations-autorange.png index fae1b98ca22..da920e7ed1c 100644 Binary files a/test/image/baselines/annotations-autorange.png and b/test/image/baselines/annotations-autorange.png differ diff --git a/test/image/mocks/annotations-autorange.json b/test/image/mocks/annotations-autorange.json index 3fa5453db48..ded30e807e6 100644 --- a/test/image/mocks/annotations-autorange.json +++ b/test/image/mocks/annotations-autorange.json @@ -52,49 +52,61 @@ "width":800, "margin":{"r":40,"b":100,"l":40,"t":40}, "annotations":[ - {"ay":0,"ax":50,"x":1,"y":1.5,"text":"Left"}, - {"ay":0,"ax":-50,"x":2,"y":1.5,"text":"Right"}, - {"x":1.5,"y":2,"text":"Top","ay":50,"ax":0}, - {"x":1.5,"y":1,"text":"Bottom","ay":-50,"ax":0}, + {"ay":0,"ax":50,"x":1,"y":1.5,"text":"Left", "xshift": -10, "yshift": -10}, + {"ay":0,"ax":-50,"x":2,"y":1.5,"text":"Right", "xshift": 10, "yshift": 10}, + {"x":1.5,"y":2,"text":"Top","ay":50,"ax":0, "xshift": -10, "yshift": 10}, + {"x":1.5,"y":1,"text":"Bottom","ay":-50,"ax":0, "xshift": 10, "yshift": -10}, { "xref":"x2","yref":"y2","text":"From left","y":2,"ax":-17,"ay":0,"x":"2001-01-01", - "xanchor": "right", "yanchor": "top", "textangle": 35, "bordercolor": "#444" + "xanchor": "right", "yanchor": "top", "textangle": 35, "bordercolor": "#444", + "xshift": 10 }, { "xref":"x2","yref":"y2","text":"From
right","y":2,"x":"2001-03-01","ay":0,"ax":50, - "bgcolor": "#eee", "width": 50, "height": 40, "textangle": 70 + "bgcolor": "#eee", "width": 50, "height": 40, "textangle": 70, + "xshift": -10 }, { "xref":"x2","yref":"y2","text":"From top","y":3,"ax":0,"ay":-27,"x":"2001-02-01", - "xanchor": "left", "yanchor": "bottom", "textangle": -15, "bordercolor": "#444" + "xanchor": "left", "yanchor": "bottom", "textangle": -15, "bordercolor": "#444", + "yshift": -10 }, { "xref":"x2","yref":"y2","text":"From Bottom","y":1,"ax":0,"ay":50,"x":"2001-02-01", - "bordercolor": "#444", "borderwidth": 3, "height": 30 + "bordercolor": "#444", "borderwidth": 3, "height": 30, + "yshift": 10 }, { "xref":"x3","yref":"y3","text":"Left
no
arrow","y":1.5,"x":1,"showarrow":false, "bordercolor": "#444", "bgcolor": "#eee", "width": 50, "height": 60, "textangle": 10, - "align": "right", "valign": "bottom" + "align": "right", "valign": "bottom", "xshift": 10 + }, + { + "xref":"x3","yref":"y3","text":"Right
no
arrow","y":1.5,"x":2,"showarrow":false, + "xshift": -10 }, - {"xref":"x3","yref":"y3","text":"Right
no
arrow","y":1.5,"x":2,"showarrow":false}, { "xref":"x3","yref":"y3","text":"Bottom
no
arrow","y":1,"x":1.5,"showarrow":false, "bgcolor": "#eee", "width": 30, "height": 40, "textangle":-10, - "align": "left", "valign": "top" + "align": "left", "valign": "top", "yshift": 10 + }, + { + "xref":"x3","yref":"y3","text":"Top
no
arrow","y":2,"x":1.5,"showarrow":false, + "yshift": -10 }, - {"xref":"x3","yref":"y3","text":"Top
no
arrow","y":2,"x":1.5,"showarrow":false}, { "xref": "paper", "yref": "paper", "text": "On the
bottom of the plot", "x": 0.3, "y": -0.1, "showarrow": false, "xanchor": "right", "yanchor": "top", "width": 200, "height": 60, - "bgcolor": "#eee", "bordercolor": "#444" + "bgcolor": "#eee", "bordercolor": "#444", + "xshift": -5, "yshift": 5 }, { "xref": "paper", "yref": "paper", "text": "blah blah blah blah
blah
blah
blah
blah
blah", "x": 0.3, "y": -0.25, "ax": 100, "ay": 0, "textangle": 40, "borderpad": 4, "xanchor": "left", "yanchor": "bottom", "align": "left", "valign": "top", - "width": 60, "height": 40, "bgcolor": "#eee", "bordercolor": "#444" + "width": 60, "height": 40, "bgcolor": "#eee", "bordercolor": "#444", + "xshift": -5, "yshift": 5 } ] } diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index 9a24aa600e5..3a1711daabb 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -554,9 +554,9 @@ describe('annotations autorange', function() { it('should adapt to relayout calls', function(done) { Plotly.plot(gd, mock).then(function() { assertRanges( - [0.97, 2.03], [0.97, 2.03], - ['2000-10-01 08:23:18.0583', '2001-06-05 19:20:23.301'], [-0.245, 4.245], - [0.9, 2.1], [0.86, 2.14] + [0.91, 2.09], [0.91, 2.09], + ['2000-11-13', '2001-04-21'], [-0.069, 3.917], + [0.88, 2.05], [0.92, 2.08] ); return Plotly.relayout(gd, { @@ -567,9 +567,9 @@ describe('annotations autorange', function() { }) .then(function() { assertRanges( - [1.44, 2.02], [0.97, 2.03], - ['2001-01-18 15:06:04.0449', '2001-03-27 14:01:20.8989'], [-0.245, 4.245], - [1.44, 2.1], [0.86, 2.14] + [1.44, 2.02], [0.91, 2.09], + ['2001-01-18', '2001-03-27'], [-0.069, 3.917], + [1.44, 2.1], [0.92, 2.08] ); return Plotly.relayout(gd, { @@ -581,8 +581,8 @@ describe('annotations autorange', function() { .then(function() { assertRanges( [1.44, 2.02], [0.99, 1.52], - ['2001-01-31 23:59:59.999', '2001-02-01 00:00:00.001'], [-0.245, 4.245], - [0.5, 2.5], [0.86, 2.14] + ['2001-01-31 23:59:59.999', '2001-02-01 00:00:00.001'], [-0.069, 3.917], + [0.5, 2.5], [0.92, 2.08] ); return Plotly.relayout(gd, { @@ -596,9 +596,9 @@ describe('annotations autorange', function() { }) .then(function() { assertRanges( - [0.97, 2.03], [0.97, 2.03], - ['2000-10-01 08:23:18.0583', '2001-06-05 19:20:23.301'], [-0.245, 4.245], - [0.9, 2.1], [0.86, 2.14] + [0.91, 2.09], [0.91, 2.09], + ['2000-11-13', '2001-04-21'], [-0.069, 3.917], + [0.88, 2.05], [0.92, 2.08] ); }) .catch(failTest) @@ -619,10 +619,10 @@ describe('annotations autorange', function() { }) .then(function() { assertRanges( - [-1.09, 2.09], [0.94, 3.06], + [-1.09, 2.25], [0.84, 3.06], // the other axes shouldn't change - ['2000-10-01 08:23:18.0583', '2001-06-05 19:20:23.301'], [-0.245, 4.245], - [0.9, 2.1], [0.86, 2.14] + ['2000-11-13', '2001-04-21'], [-0.069, 3.917], + [0.88, 2.05], [0.92, 2.08] ); }) .catch(failTest) @@ -961,7 +961,8 @@ describe('annotation effects', function() { showarrow: false, text: 'blah
blah blah', xref: 'paper', - yref: 'paper' + yref: 'paper', + xshift: 5, yshift: 5 }]) .then(function() { var bbox = textBox().getBoundingClientRect(); @@ -981,7 +982,8 @@ describe('annotation effects', function() { xref: 'paper', yref: 'paper', xanchor: 'left', - yanchor: 'top' + yanchor: 'top', + xshift: 5, yshift: 5 }]) .then(function() { // with offsets 0, 0 because the anchor doesn't change now @@ -999,7 +1001,8 @@ describe('annotation effects', function() { xref: 'paper', yref: 'paper', ax: 30, - ay: 30 + ay: 30, + xshift: 5, yshift: 5 }]) .then(function() { return checkDragging(arrowDrag, 0, 0, 1); @@ -1014,7 +1017,8 @@ describe('annotation effects', function() { x: 0, y: 0, showarrow: false, - text: 'blah
blah blah' + text: 'blah
blah blah', + xshift: 5, yshift: 5 }]) .then(function() { return checkDragging(textDrag, 0, 0, 100); @@ -1029,7 +1033,8 @@ describe('annotation effects', function() { y: 0, text: 'blah
blah blah', ax: 30, - ay: -30 + ay: -30, + xshift: 5, yshift: 5 }]) .then(function() { return checkDragging(arrowDrag, 0, 0, 100); @@ -1127,7 +1132,7 @@ describe('annotation effects', function() { } makePlot([ - {x: 50, y: 50, text: 'hi', width: 50, ax: 0, ay: -40}, + {x: 50, y: 50, text: 'hi', width: 50, ax: 0, ay: -40, xshift: -50, yshift: 50}, {x: 20, y: 20, text: 'bye', height: 40, showarrow: false}, {x: 80, y: 80, text: 'why?', ax: 0, ay: -40} ], {}) // turn off the default editable: true @@ -1136,7 +1141,7 @@ describe('annotation effects', function() { gd.on('plotly_clickannotation', function(evt) { clickData.push(evt); }); gdBB = gd.getBoundingClientRect(); - pos0Head = [gdBB.left + 250, gdBB.top + 250]; + pos0Head = [gdBB.left + 200, gdBB.top + 200]; pos0 = [pos0Head[0], pos0Head[1] - 40]; pos1 = [gdBB.left + 160, gdBB.top + 340]; pos2Head = [gdBB.left + 340, gdBB.top + 160];