-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Add axis domain references to shapes, annotations, and layout images #5014
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
67d5f8e
83a7452
f9e05bc
f4c3a60
36667f5
ab7b973
c6005d9
6235f3a
10cbd78
b546658
40a639c
7373e1c
13f7203
b3258f6
e5139b0
547deaf
31b22be
c1ca50a
e139ab0
f4b37fd
6bb7b82
d92c4da
f5c5a17
2225eb5
23d8888
50f75ee
aa316e8
37597ce
0289e6f
360d322
474f7d9
0891c0f
0bb16db
696f8df
2e4ed19
cf6e040
6577c57
4402dc7
896fbde
647eef2
2f625a7
9c68c67
57c25bd
dda98a7
4972a2b
e2b440f
167da28
2b29d19
416cc3b
1d06c4f
fda38fc
a588f1e
e405e5b
63cbc29
4bb72c1
55300b7
adbc6a5
5d4cf74
0f204ad
c328243
e12dcb4
117b3ed
68c807b
60e053d
f9d9c35
3896b08
af7103f
35ff5a7
9f0d331
11a8d3a
e651d57
27d8c43
0f1eae6
83834f8
c9103df
9249684
f9d4b03
e6c6235
44862fa
95e328f
00edf56
93284df
6e26db3
156b73b
fd07290
bd2b34d
09635ba
6510415
aa2d6eb
4203088
b8dc27f
099bf52
f3531dc
93890d0
6eba309
2d25857
35ab9da
759d37b
d70f4e9
9055503
e458c86
df0560c
676d6c5
69acd55
a54d354
8b63e38
7d2129c
1c51d72
71b0cc4
a31a94a
472c135
5d586dd
7f5300c
dbf1bd4
35712fe
a3d561c
dcc27f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,8 @@ build/* | |
|
||
npm-debug.log* | ||
*.sublime* | ||
*~ | ||
tags | ||
|
||
.* | ||
!.circleci | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,34 @@ var ARROWPATHS = require('./arrow_paths'); | |
var fontAttrs = require('../../plots/font_attributes'); | ||
var cartesianConstants = require('../../plots/cartesian/constants'); | ||
var templatedArray = require('../../plot_api/plot_template').templatedArray; | ||
var axisPlaceableObjs = require('../../constants/axis_placeable_objects'); | ||
|
||
function arrowAxisRefDescription(axis) { | ||
return [ | ||
'In order for absolute positioning of the arrow to work, *a' + axis + | ||
'ref* must be exactly the same as *' + axis + 'ref*, otherwise *a' + axis + | ||
'ref* will revert to *pixel* (explained next).', | ||
'For relative positioning, *a' + axis + 'ref* can be set to *pixel*,', | ||
'in which case the *a' + axis + '* value is specified in pixels', | ||
'relative to *' + axis + '*.', | ||
'Absolute positioning is useful', | ||
'for trendline annotations which should continue to indicate', | ||
'the correct trend when zoomed. Relative positioning is useful', | ||
'for specifying the text offset for an annotated point.' | ||
].join(' '); | ||
} | ||
|
||
function arrowCoordinateDescription(axis, lower, upper) { | ||
return [ | ||
'Sets the', axis, 'component of the arrow tail about the arrow head.', | ||
'If `a' + axis + 'ref` is `pixel`, a positive (negative)', | ||
'component corresponds to an arrow pointing', | ||
'from', upper, 'to', lower, '(' + lower, 'to', upper + ').', | ||
'If `a' + axis + 'ref` is not `pixel` and is exactly the same as `' + axis + 'ref`,', | ||
'this is an absolute value on that axis,', | ||
'like `' + axis + '`, specified in the same coordinates as `' + axis + 'ref`.' | ||
].join(' '); | ||
} | ||
|
||
module.exports = templatedArray('annotation', { | ||
visible: { | ||
|
@@ -254,25 +281,15 @@ module.exports = templatedArray('annotation', { | |
role: 'info', | ||
editType: 'calc+arraydraw', | ||
description: [ | ||
'Sets the x component of the arrow tail about the arrow head.', | ||
'If `axref` is `pixel`, a positive (negative) ', | ||
'component corresponds to an arrow pointing', | ||
'from right to left (left to right).', | ||
'If `axref` is an axis, this is an absolute value on that axis,', | ||
'like `x`, NOT a relative value.' | ||
arrowCoordinateDescription('x', 'left', 'right') | ||
].join(' ') | ||
}, | ||
ay: { | ||
valType: 'any', | ||
role: 'info', | ||
editType: 'calc+arraydraw', | ||
description: [ | ||
'Sets the y component of the arrow tail about the arrow head.', | ||
'If `ayref` is `pixel`, a positive (negative) ', | ||
'component corresponds to an arrow pointing', | ||
'from bottom to top (top to bottom).', | ||
'If `ayref` is an axis, this is an absolute value on that axis,', | ||
'like `y`, NOT a relative value.' | ||
arrowCoordinateDescription('y', 'top', 'bottom') | ||
].join(' ') | ||
}, | ||
axref: { | ||
|
@@ -285,12 +302,10 @@ module.exports = templatedArray('annotation', { | |
role: 'info', | ||
editType: 'calc', | ||
description: [ | ||
'Indicates in what terms the tail of the annotation (ax,ay) ', | ||
'is specified. If `pixel`, `ax` is a relative offset in pixels ', | ||
'from `x`. If set to an x axis id (e.g. *x* or *x2*), `ax` is ', | ||
'specified in the same terms as that axis. This is useful ', | ||
'for trendline annotations which should continue to indicate ', | ||
'the correct trend when zoomed.' | ||
'Indicates in what coordinates the tail of the', | ||
'annotation (ax,ay) is specified.', | ||
axisPlaceableObjs.axisRefDescription('ax', 'left', 'right'), | ||
arrowAxisRefDescription('x') | ||
].join(' ') | ||
}, | ||
ayref: { | ||
|
@@ -303,12 +318,10 @@ module.exports = templatedArray('annotation', { | |
role: 'info', | ||
editType: 'calc', | ||
description: [ | ||
'Indicates in what terms the tail of the annotation (ax,ay) ', | ||
'is specified. If `pixel`, `ay` is a relative offset in pixels ', | ||
'from `y`. If set to a y axis id (e.g. *y* or *y2*), `ay` is ', | ||
'specified in the same terms as that axis. This is useful ', | ||
'for trendline annotations which should continue to indicate ', | ||
'the correct trend when zoomed.' | ||
'Indicates in what coordinates the tail of the', | ||
'annotation (ax,ay) is specified.', | ||
axisPlaceableObjs.axisRefDescription('ay', 'bottom', 'top'), | ||
arrowAxisRefDescription('y') | ||
].join(' ') | ||
}, | ||
// positioning | ||
|
@@ -322,11 +335,7 @@ module.exports = templatedArray('annotation', { | |
editType: 'calc', | ||
description: [ | ||
'Sets the annotation\'s x coordinate axis.', | ||
'If set to an x axis id (e.g. *x* or *x2*), the `x` position', | ||
'refers to an x coordinate', | ||
'If set to *paper*, the `x` position refers to the distance from', | ||
'the left side of the plotting area in normalized coordinates', | ||
'where 0 (1) corresponds to the left (right) side.' | ||
axisPlaceableObjs.axisRefDescription('x', 'left', 'right'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In addition to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes we can, but There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool! Thanks for the info. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (sorry wrong review item) |
||
].join(' ') | ||
}, | ||
x: { | ||
|
@@ -385,11 +394,7 @@ module.exports = templatedArray('annotation', { | |
editType: 'calc', | ||
description: [ | ||
'Sets the annotation\'s y coordinate axis.', | ||
'If set to an y axis id (e.g. *y* or *y2*), the `y` position', | ||
'refers to an y coordinate', | ||
'If set to *paper*, the `y` position refers to the distance from', | ||
'the bottom of the plotting area in normalized coordinates', | ||
'where 0 (1) corresponds to the bottom (top).' | ||
axisPlaceableObjs.axisRefDescription('y', 'bottom', 'top'), | ||
].join(' ') | ||
}, | ||
y: { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,6 +73,31 @@ function drawOne(gd, index) { | |
drawRaw(gd, options, index, false, xa, ya); | ||
} | ||
|
||
// Convert pixels to the coordinates relevant for the axis referred to. For | ||
// example, for paper it would convert to a value normalized by the dimension of | ||
// the plot. | ||
// axDomainRef: if true and axa defined, draws relative to axis domain, | ||
// otherwise draws relative to data (if axa defined) or paper (if not). | ||
function shiftPosition(axa, dAx, axLetter, gs, options) { | ||
var optAx = options[axLetter]; | ||
var axRef = options[axLetter + 'ref']; | ||
var vertical = axLetter.indexOf('y') !== -1; | ||
var axDomainRef = Axes.getRefType(axRef) === 'domain'; | ||
var gsDim = vertical ? gs.h : gs.w; | ||
if(axa) { | ||
if(axDomainRef) { | ||
// here optAx normalized to length of axis (e.g., normally in range | ||
// 0 to 1). But dAx is in pixels. So we normalize dAx to length of | ||
// axis before doing the math. | ||
return optAx + (vertical ? -dAx : dAx) / axa._length; | ||
} else { | ||
return axa.p2r(axa.r2p(optAx) + dAx); | ||
} | ||
} else { | ||
return optAx + (vertical ? -dAx : dAx) / gsDim; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To improve performance, we may consider refactoring: var delta = vertical ? -dAx : dAx;
return !axa ? optAx + delta / gsDim :
axDomainRef ?
optAx + delta / axa._length : // here optAx normalized to length of axis (e.g., normally in range 0 to 1). But dAx is in pixels. So we normalize dAx to length of axis before doing the math.
axa.p2r(axa.r2p(optAx) + dAx); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think inline-ifs are good if they make the code clearer, but here I would not say they do that haha :-) |
||
} | ||
|
||
/** | ||
* drawRaw: draw a single annotation, potentially with modifications | ||
* | ||
|
@@ -296,13 +321,14 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { | |
var alignPosition; | ||
var autoAlignFraction; | ||
var textShift; | ||
var axRefType = Axes.getRefType(axRef); | ||
|
||
/* | ||
* calculate the *primary* pixel position | ||
* which is the arrowhead if there is one, | ||
* otherwise the text anchor point | ||
*/ | ||
if(ax) { | ||
if(ax && (axRefType !== 'domain')) { | ||
// check if annotation is off screen, to bypass DOM manipulations | ||
var posFraction = ax.r2fraction(options[axLetter]); | ||
if(posFraction < 0 || posFraction > 1) { | ||
|
@@ -318,12 +344,17 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { | |
basePx = ax._offset + ax.r2p(options[axLetter]); | ||
autoAlignFraction = 0.5; | ||
} else { | ||
var axRefTypeEqDomain = axRefType === 'domain'; | ||
if(axLetter === 'x') { | ||
alignPosition = options[axLetter]; | ||
basePx = gs.l + gs.w * alignPosition; | ||
basePx = axRefTypeEqDomain ? | ||
ax._offset + ax._length * alignPosition : | ||
basePx = gs.l + gs.w * alignPosition; | ||
} else { | ||
alignPosition = 1 - options[axLetter]; | ||
basePx = gs.t + gs.h * alignPosition; | ||
basePx = axRefTypeEqDomain ? | ||
ax._offset + ax._length * alignPosition : | ||
basePx = gs.t + gs.h * alignPosition; | ||
} | ||
autoAlignFraction = options.showarrow ? 0.5 : alignPosition; | ||
} | ||
|
@@ -340,8 +371,29 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { | |
annSizeFromHeight * shiftFraction(0.5, options.yanchor); | ||
|
||
if(tailRef === axRef) { | ||
posPx.tail = ax._offset + ax.r2p(arrowLength); | ||
// tail is data-referenced: autorange pads the text in px from the tail | ||
// In the case tailRefType is 'domain' or 'paper', the arrow's | ||
// position is set absolutely, which is consistent with how | ||
// it behaves when its position is set in data ('range') | ||
// coordinates. | ||
var tailRefType = Axes.getRefType(tailRef); | ||
if(tailRefType === 'domain') { | ||
if(axLetter === 'y') { | ||
arrowLength = 1 - arrowLength; | ||
} | ||
archmoj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
posPx.tail = ax._offset + ax._length * arrowLength; | ||
} else if(tailRefType === 'paper') { | ||
if(axLetter === 'y') { | ||
arrowLength = 1 - arrowLength; | ||
posPx.tail = gs.t + gs.h * arrowLength; | ||
} else { | ||
posPx.tail = gs.l + gs.w * arrowLength; | ||
} | ||
} else { | ||
// assumed tailRef is range or paper referenced | ||
posPx.tail = ax._offset + ax.r2p(arrowLength); | ||
} | ||
// tail is range- or domain-referenced: autorange pads the | ||
// text in px from the tail | ||
textPadShift = textShift; | ||
} else { | ||
posPx.tail = basePx + arrowLength; | ||
|
@@ -562,19 +614,20 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { | |
var ycenter = annxy0[1] + dy; | ||
annTextGroupInner.call(Drawing.setTranslate, xcenter, ycenter); | ||
|
||
modifyItem('x', xa ? | ||
xa.p2r(xa.r2p(options.x) + dx) : | ||
(options.x + (dx / gs.w))); | ||
modifyItem('y', ya ? | ||
ya.p2r(ya.r2p(options.y) + dy) : | ||
(options.y - (dy / gs.h))); | ||
modifyItem('x', | ||
shiftPosition(xa, dx, 'x', gs, options)); | ||
modifyItem('y', | ||
shiftPosition(ya, dy, 'y', gs, options)); | ||
|
||
// for these 2 calls to shiftPosition, it is assumed xa, ya are | ||
// defined, so gsDim will not be used, but we put it in | ||
// anyways for consistency | ||
if(options.axref === options.xref) { | ||
modifyItem('ax', xa.p2r(xa.r2p(options.ax) + dx)); | ||
modifyItem('ax', shiftPosition(xa, dx, 'ax', gs, options)); | ||
} | ||
|
||
if(options.ayref === options.yref) { | ||
modifyItem('ay', ya.p2r(ya.r2p(options.ay) + dy)); | ||
modifyItem('ay', shiftPosition(ya, dy, 'ay', gs, options)); | ||
} | ||
|
||
arrowGroup.attr('transform', 'translate(' + dx + ',' + dy + ')'); | ||
|
@@ -609,14 +662,17 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { | |
moveFn: function(dx, dy) { | ||
var csr = 'pointer'; | ||
if(options.showarrow) { | ||
// for these 2 calls to shiftPosition, it is assumed xa, ya are | ||
// defined, so gsDim will not be used, but we put it in | ||
// anyways for consistency | ||
if(options.axref === options.xref) { | ||
modifyItem('ax', xa.p2r(xa.r2p(options.ax) + dx)); | ||
modifyItem('ax', shiftPosition(xa, dx, 'ax', gs, options)); | ||
} else { | ||
modifyItem('ax', options.ax + dx); | ||
} | ||
|
||
if(options.ayref === options.yref) { | ||
modifyItem('ay', ya.p2r(ya.r2p(options.ay) + dy)); | ||
modifyItem('ay', shiftPosition(ya, dy, 'ay', gs.w, options)); | ||
} else { | ||
modifyItem('ay', options.ay + dy); | ||
} | ||
|
@@ -625,7 +681,9 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { | |
} else if(!subplotId) { | ||
var xUpdate, yUpdate; | ||
if(xa) { | ||
xUpdate = xa.p2r(xa.r2p(options.x) + dx); | ||
// shiftPosition will not execute code where xa was | ||
// undefined, so we use to calculate xUpdate too | ||
xUpdate = shiftPosition(xa, dx, 'x', gs, options); | ||
} else { | ||
var widthFraction = options._xsize / gs.w; | ||
var xLeft = options.x + (options._xshift - options.xshift) / gs.w - widthFraction / 2; | ||
|
@@ -635,7 +693,9 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { | |
} | ||
|
||
if(ya) { | ||
yUpdate = ya.p2r(ya.r2p(options.y) + dy); | ||
// shiftPosition will not execute code where ya was | ||
// undefined, so we use to calculate yUpdate too | ||
yUpdate = shiftPosition(ya, dy, 'y', gs, options); | ||
} else { | ||
var heightFraction = options._ysize / gs.h; | ||
var yBottom = options.y - (options._yshift + options.yshift) / gs.h - heightFraction / 2; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you elaborate why we should add these two?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some text editors suffix backup files with
~
and use the filetags
to store information so you can jump to function definitions. But taggers don't work that well with Javascript it seems, so we could get rid of thetags
.