Skip to content

Commit 9a88f84

Browse files
Merge pull request #5014 from plotly/issue-4958
Add axis domain references to shapes, annotations, and layout images
2 parents 6583929 + dcc27f4 commit 9a88f84

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3242
-152
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ build/*
66

77
npm-debug.log*
88
*.sublime*
9+
*~
10+
tags
911

1012
.*
1113
!.circleci

package-lock.json

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
"elliptic": "^6.5.3",
139139
"eslint": "^7.10.0",
140140
"espree": "^7.3.0",
141+
"extra-iterable": "^2.5.13",
141142
"falafel": "^2.2.4",
142143
"fs-extra": "^9.0.1",
143144
"fuse.js": "^6.4.1",

src/components/annotations/attributes.js

+39-34
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,34 @@ var ARROWPATHS = require('./arrow_paths');
1212
var fontAttrs = require('../../plots/font_attributes');
1313
var cartesianConstants = require('../../plots/cartesian/constants');
1414
var templatedArray = require('../../plot_api/plot_template').templatedArray;
15+
var axisPlaceableObjs = require('../../constants/axis_placeable_objects');
1516

17+
function arrowAxisRefDescription(axis) {
18+
return [
19+
'In order for absolute positioning of the arrow to work, *a' + axis +
20+
'ref* must be exactly the same as *' + axis + 'ref*, otherwise *a' + axis +
21+
'ref* will revert to *pixel* (explained next).',
22+
'For relative positioning, *a' + axis + 'ref* can be set to *pixel*,',
23+
'in which case the *a' + axis + '* value is specified in pixels',
24+
'relative to *' + axis + '*.',
25+
'Absolute positioning is useful',
26+
'for trendline annotations which should continue to indicate',
27+
'the correct trend when zoomed. Relative positioning is useful',
28+
'for specifying the text offset for an annotated point.'
29+
].join(' ');
30+
}
31+
32+
function arrowCoordinateDescription(axis, lower, upper) {
33+
return [
34+
'Sets the', axis, 'component of the arrow tail about the arrow head.',
35+
'If `a' + axis + 'ref` is `pixel`, a positive (negative)',
36+
'component corresponds to an arrow pointing',
37+
'from', upper, 'to', lower, '(' + lower, 'to', upper + ').',
38+
'If `a' + axis + 'ref` is not `pixel` and is exactly the same as `' + axis + 'ref`,',
39+
'this is an absolute value on that axis,',
40+
'like `' + axis + '`, specified in the same coordinates as `' + axis + 'ref`.'
41+
].join(' ');
42+
}
1643

1744
module.exports = templatedArray('annotation', {
1845
visible: {
@@ -254,25 +281,15 @@ module.exports = templatedArray('annotation', {
254281
role: 'info',
255282
editType: 'calc+arraydraw',
256283
description: [
257-
'Sets the x component of the arrow tail about the arrow head.',
258-
'If `axref` is `pixel`, a positive (negative) ',
259-
'component corresponds to an arrow pointing',
260-
'from right to left (left to right).',
261-
'If `axref` is an axis, this is an absolute value on that axis,',
262-
'like `x`, NOT a relative value.'
284+
arrowCoordinateDescription('x', 'left', 'right')
263285
].join(' ')
264286
},
265287
ay: {
266288
valType: 'any',
267289
role: 'info',
268290
editType: 'calc+arraydraw',
269291
description: [
270-
'Sets the y component of the arrow tail about the arrow head.',
271-
'If `ayref` is `pixel`, a positive (negative) ',
272-
'component corresponds to an arrow pointing',
273-
'from bottom to top (top to bottom).',
274-
'If `ayref` is an axis, this is an absolute value on that axis,',
275-
'like `y`, NOT a relative value.'
292+
arrowCoordinateDescription('y', 'top', 'bottom')
276293
].join(' ')
277294
},
278295
axref: {
@@ -285,12 +302,10 @@ module.exports = templatedArray('annotation', {
285302
role: 'info',
286303
editType: 'calc',
287304
description: [
288-
'Indicates in what terms the tail of the annotation (ax,ay) ',
289-
'is specified. If `pixel`, `ax` is a relative offset in pixels ',
290-
'from `x`. If set to an x axis id (e.g. *x* or *x2*), `ax` is ',
291-
'specified in the same terms as that axis. This is useful ',
292-
'for trendline annotations which should continue to indicate ',
293-
'the correct trend when zoomed.'
305+
'Indicates in what coordinates the tail of the',
306+
'annotation (ax,ay) is specified.',
307+
axisPlaceableObjs.axisRefDescription('ax', 'left', 'right'),
308+
arrowAxisRefDescription('x')
294309
].join(' ')
295310
},
296311
ayref: {
@@ -303,12 +318,10 @@ module.exports = templatedArray('annotation', {
303318
role: 'info',
304319
editType: 'calc',
305320
description: [
306-
'Indicates in what terms the tail of the annotation (ax,ay) ',
307-
'is specified. If `pixel`, `ay` is a relative offset in pixels ',
308-
'from `y`. If set to a y axis id (e.g. *y* or *y2*), `ay` is ',
309-
'specified in the same terms as that axis. This is useful ',
310-
'for trendline annotations which should continue to indicate ',
311-
'the correct trend when zoomed.'
321+
'Indicates in what coordinates the tail of the',
322+
'annotation (ax,ay) is specified.',
323+
axisPlaceableObjs.axisRefDescription('ay', 'bottom', 'top'),
324+
arrowAxisRefDescription('y')
312325
].join(' ')
313326
},
314327
// positioning
@@ -322,11 +335,7 @@ module.exports = templatedArray('annotation', {
322335
editType: 'calc',
323336
description: [
324337
'Sets the annotation\'s x coordinate axis.',
325-
'If set to an x axis id (e.g. *x* or *x2*), the `x` position',
326-
'refers to an x coordinate',
327-
'If set to *paper*, the `x` position refers to the distance from',
328-
'the left side of the plotting area in normalized coordinates',
329-
'where 0 (1) corresponds to the left (right) side.'
338+
axisPlaceableObjs.axisRefDescription('x', 'left', 'right'),
330339
].join(' ')
331340
},
332341
x: {
@@ -385,11 +394,7 @@ module.exports = templatedArray('annotation', {
385394
editType: 'calc',
386395
description: [
387396
'Sets the annotation\'s y coordinate axis.',
388-
'If set to an y axis id (e.g. *y* or *y2*), the `y` position',
389-
'refers to an y coordinate',
390-
'If set to *paper*, the `y` position refers to the distance from',
391-
'the bottom of the plotting area in normalized coordinates',
392-
'where 0 (1) corresponds to the bottom (top).'
397+
axisPlaceableObjs.axisRefDescription('y', 'bottom', 'top'),
393398
].join(' ')
394399
},
395400
y: {

src/components/annotations/calc_autorange.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ function annAutorange(gd) {
3434
Lib.filterVisible(fullLayout.annotations).forEach(function(ann) {
3535
var xa = Axes.getFromId(gd, ann.xref);
3636
var ya = Axes.getFromId(gd, ann.yref);
37+
var xRefType = Axes.getRefType(ann.xref);
38+
var yRefType = Axes.getRefType(ann.yref);
3739

3840
ann._extremes = {};
39-
if(xa) calcAxisExpansion(ann, xa);
40-
if(ya) calcAxisExpansion(ann, ya);
41+
if(xRefType === 'range') calcAxisExpansion(ann, xa);
42+
if(yRefType === 'range') calcAxisExpansion(ann, ya);
4143
});
4244
}
4345

src/components/annotations/defaults.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ function handleAnnotationDefaults(annIn, annOut, fullLayout) {
6060
if(showArrow) {
6161
var arrowPosAttr = 'a' + axLetter;
6262
// axref, ayref
63-
var aaxRef = Axes.coerceRef(annIn, annOut, gdMock, arrowPosAttr, 'pixel');
63+
var aaxRef = Axes.coerceRef(annIn, annOut, gdMock, arrowPosAttr, 'pixel',
64+
['pixel', 'paper']);
6465

6566
// for now the arrow can only be on the same axis or specified as pixels
6667
// TODO: sometime it might be interesting to allow it to be on *any* axis

src/components/annotations/draw.js

+77-17
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,31 @@ function drawOne(gd, index) {
7373
drawRaw(gd, options, index, false, xa, ya);
7474
}
7575

76+
// Convert pixels to the coordinates relevant for the axis referred to. For
77+
// example, for paper it would convert to a value normalized by the dimension of
78+
// the plot.
79+
// axDomainRef: if true and axa defined, draws relative to axis domain,
80+
// otherwise draws relative to data (if axa defined) or paper (if not).
81+
function shiftPosition(axa, dAx, axLetter, gs, options) {
82+
var optAx = options[axLetter];
83+
var axRef = options[axLetter + 'ref'];
84+
var vertical = axLetter.indexOf('y') !== -1;
85+
var axDomainRef = Axes.getRefType(axRef) === 'domain';
86+
var gsDim = vertical ? gs.h : gs.w;
87+
if(axa) {
88+
if(axDomainRef) {
89+
// here optAx normalized to length of axis (e.g., normally in range
90+
// 0 to 1). But dAx is in pixels. So we normalize dAx to length of
91+
// axis before doing the math.
92+
return optAx + (vertical ? -dAx : dAx) / axa._length;
93+
} else {
94+
return axa.p2r(axa.r2p(optAx) + dAx);
95+
}
96+
} else {
97+
return optAx + (vertical ? -dAx : dAx) / gsDim;
98+
}
99+
}
100+
76101
/**
77102
* drawRaw: draw a single annotation, potentially with modifications
78103
*
@@ -296,13 +321,14 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
296321
var alignPosition;
297322
var autoAlignFraction;
298323
var textShift;
324+
var axRefType = Axes.getRefType(axRef);
299325

300326
/*
301327
* calculate the *primary* pixel position
302328
* which is the arrowhead if there is one,
303329
* otherwise the text anchor point
304330
*/
305-
if(ax) {
331+
if(ax && (axRefType !== 'domain')) {
306332
// check if annotation is off screen, to bypass DOM manipulations
307333
var posFraction = ax.r2fraction(options[axLetter]);
308334
if(posFraction < 0 || posFraction > 1) {
@@ -318,12 +344,17 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
318344
basePx = ax._offset + ax.r2p(options[axLetter]);
319345
autoAlignFraction = 0.5;
320346
} else {
347+
var axRefTypeEqDomain = axRefType === 'domain';
321348
if(axLetter === 'x') {
322349
alignPosition = options[axLetter];
323-
basePx = gs.l + gs.w * alignPosition;
350+
basePx = axRefTypeEqDomain ?
351+
ax._offset + ax._length * alignPosition :
352+
basePx = gs.l + gs.w * alignPosition;
324353
} else {
325354
alignPosition = 1 - options[axLetter];
326-
basePx = gs.t + gs.h * alignPosition;
355+
basePx = axRefTypeEqDomain ?
356+
ax._offset + ax._length * alignPosition :
357+
basePx = gs.t + gs.h * alignPosition;
327358
}
328359
autoAlignFraction = options.showarrow ? 0.5 : alignPosition;
329360
}
@@ -340,8 +371,29 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
340371
annSizeFromHeight * shiftFraction(0.5, options.yanchor);
341372

342373
if(tailRef === axRef) {
343-
posPx.tail = ax._offset + ax.r2p(arrowLength);
344-
// tail is data-referenced: autorange pads the text in px from the tail
374+
// In the case tailRefType is 'domain' or 'paper', the arrow's
375+
// position is set absolutely, which is consistent with how
376+
// it behaves when its position is set in data ('range')
377+
// coordinates.
378+
var tailRefType = Axes.getRefType(tailRef);
379+
if(tailRefType === 'domain') {
380+
if(axLetter === 'y') {
381+
arrowLength = 1 - arrowLength;
382+
}
383+
posPx.tail = ax._offset + ax._length * arrowLength;
384+
} else if(tailRefType === 'paper') {
385+
if(axLetter === 'y') {
386+
arrowLength = 1 - arrowLength;
387+
posPx.tail = gs.t + gs.h * arrowLength;
388+
} else {
389+
posPx.tail = gs.l + gs.w * arrowLength;
390+
}
391+
} else {
392+
// assumed tailRef is range or paper referenced
393+
posPx.tail = ax._offset + ax.r2p(arrowLength);
394+
}
395+
// tail is range- or domain-referenced: autorange pads the
396+
// text in px from the tail
345397
textPadShift = textShift;
346398
} else {
347399
posPx.tail = basePx + arrowLength;
@@ -562,19 +614,20 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
562614
var ycenter = annxy0[1] + dy;
563615
annTextGroupInner.call(Drawing.setTranslate, xcenter, ycenter);
564616

565-
modifyItem('x', xa ?
566-
xa.p2r(xa.r2p(options.x) + dx) :
567-
(options.x + (dx / gs.w)));
568-
modifyItem('y', ya ?
569-
ya.p2r(ya.r2p(options.y) + dy) :
570-
(options.y - (dy / gs.h)));
617+
modifyItem('x',
618+
shiftPosition(xa, dx, 'x', gs, options));
619+
modifyItem('y',
620+
shiftPosition(ya, dy, 'y', gs, options));
571621

622+
// for these 2 calls to shiftPosition, it is assumed xa, ya are
623+
// defined, so gsDim will not be used, but we put it in
624+
// anyways for consistency
572625
if(options.axref === options.xref) {
573-
modifyItem('ax', xa.p2r(xa.r2p(options.ax) + dx));
626+
modifyItem('ax', shiftPosition(xa, dx, 'ax', gs, options));
574627
}
575628

576629
if(options.ayref === options.yref) {
577-
modifyItem('ay', ya.p2r(ya.r2p(options.ay) + dy));
630+
modifyItem('ay', shiftPosition(ya, dy, 'ay', gs, options));
578631
}
579632

580633
arrowGroup.attr('transform', 'translate(' + dx + ',' + dy + ')');
@@ -609,14 +662,17 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
609662
moveFn: function(dx, dy) {
610663
var csr = 'pointer';
611664
if(options.showarrow) {
665+
// for these 2 calls to shiftPosition, it is assumed xa, ya are
666+
// defined, so gsDim will not be used, but we put it in
667+
// anyways for consistency
612668
if(options.axref === options.xref) {
613-
modifyItem('ax', xa.p2r(xa.r2p(options.ax) + dx));
669+
modifyItem('ax', shiftPosition(xa, dx, 'ax', gs, options));
614670
} else {
615671
modifyItem('ax', options.ax + dx);
616672
}
617673

618674
if(options.ayref === options.yref) {
619-
modifyItem('ay', ya.p2r(ya.r2p(options.ay) + dy));
675+
modifyItem('ay', shiftPosition(ya, dy, 'ay', gs.w, options));
620676
} else {
621677
modifyItem('ay', options.ay + dy);
622678
}
@@ -625,7 +681,9 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
625681
} else if(!subplotId) {
626682
var xUpdate, yUpdate;
627683
if(xa) {
628-
xUpdate = xa.p2r(xa.r2p(options.x) + dx);
684+
// shiftPosition will not execute code where xa was
685+
// undefined, so we use to calculate xUpdate too
686+
xUpdate = shiftPosition(xa, dx, 'x', gs, options);
629687
} else {
630688
var widthFraction = options._xsize / gs.w;
631689
var xLeft = options.x + (options._xshift - options.xshift) / gs.w - widthFraction / 2;
@@ -635,7 +693,9 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
635693
}
636694

637695
if(ya) {
638-
yUpdate = ya.p2r(ya.r2p(options.y) + dy);
696+
// shiftPosition will not execute code where ya was
697+
// undefined, so we use to calculate yUpdate too
698+
yUpdate = shiftPosition(ya, dy, 'y', gs, options);
639699
} else {
640700
var heightFraction = options._ysize / gs.h;
641701
var yBottom = options.y - (options._yshift + options.yshift) / gs.h - heightFraction / 2;

0 commit comments

Comments
 (0)