Skip to content

Commit 6b4b892

Browse files
authored
Merge pull request #2164 from apalchys/arrow_anchor
Add "arrowanchor" property to annotations
2 parents 4d8cca4 + 8e75b74 commit 6b4b892

16 files changed

+275
-95
lines changed

src/components/annotations/attributes.js

+46-3
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,25 @@ module.exports = {
173173
dflt: 1,
174174
role: 'style',
175175
editType: 'arraydraw',
176-
description: 'Sets the annotation arrow head style.'
176+
description: 'Sets the end annotation arrow head style.'
177+
},
178+
startarrowhead: {
179+
valType: 'integer',
180+
min: 0,
181+
max: ARROWPATHS.length,
182+
dflt: 1,
183+
role: 'style',
184+
editType: 'arraydraw',
185+
description: 'Sets the start annotation arrow head style.'
186+
},
187+
arrowside: {
188+
valType: 'flaglist',
189+
flags: ['end', 'start'],
190+
extras: ['none'],
191+
dflt: 'end',
192+
role: 'style',
193+
editType: 'arraydraw',
194+
description: 'Sets the annotation arrow head position.'
177195
},
178196
arrowsize: {
179197
valType: 'number',
@@ -182,7 +200,18 @@ module.exports = {
182200
role: 'style',
183201
editType: 'calcIfAutorange',
184202
description: [
185-
'Sets the size of the annotation arrow head, relative to `arrowwidth`.',
203+
'Sets the size of the end annotation arrow head, relative to `arrowwidth`.',
204+
'A value of 1 (default) gives a head about 3x as wide as the line.'
205+
].join(' ')
206+
},
207+
startarrowsize: {
208+
valType: 'number',
209+
min: 0.3,
210+
dflt: 1,
211+
role: 'style',
212+
editType: 'calcIfAutorange',
213+
description: [
214+
'Sets the size of the start annotation arrow head, relative to `arrowwidth`.',
186215
'A value of 1 (default) gives a head about 3x as wide as the line.'
187216
].join(' ')
188217
},
@@ -200,7 +229,21 @@ module.exports = {
200229
role: 'style',
201230
editType: 'calcIfAutorange',
202231
description: [
203-
'Sets a distance, in pixels, to move the arrowhead away from the',
232+
'Sets a distance, in pixels, to move the end arrowhead away from the',
233+
'position it is pointing at, for example to point at the edge of',
234+
'a marker independent of zoom. Note that this shortens the arrow',
235+
'from the `ax` / `ay` vector, in contrast to `xshift` / `yshift`',
236+
'which moves everything by this amount.'
237+
].join(' ')
238+
},
239+
startstandoff: {
240+
valType: 'number',
241+
min: 0,
242+
dflt: 0,
243+
role: 'style',
244+
editType: 'calcIfAutorange',
245+
description: [
246+
'Sets a distance, in pixels, to move the start arrowhead away from the',
204247
'position it is pointing at, for example to point at the edge of',
205248
'a marker independent of zoom. Note that this shortens the arrow',
206249
'from the `ax` / `ay` vector, in contrast to `xshift` / `yshift`',

src/components/annotations/calc_autorange.js

+19-10
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,16 @@ function annAutorange(gd) {
4848
Lib.filterVisible(fullLayout.annotations).forEach(function(ann) {
4949
var xa = Axes.getFromId(gd, ann.xref),
5050
ya = Axes.getFromId(gd, ann.yref),
51-
headSize = 3 * ann.arrowsize * ann.arrowwidth || 0;
51+
headSize = 3 * ann.arrowsize * ann.arrowwidth || 0,
52+
startHeadSize = 3 * ann.startarrowsize * ann.arrowwidth || 0;
5253

53-
var headPlus, headMinus;
54+
var headPlus, headMinus, startHeadPlus, startHeadMinus;
5455

5556
if(xa && xa.autorange) {
5657
headPlus = headSize + ann.xshift;
5758
headMinus = headSize - ann.xshift;
59+
startHeadPlus = startHeadSize + ann.xshift;
60+
startHeadMinus = startHeadSize - ann.xshift;
5861

5962
if(ann.axref === ann.xref) {
6063
// expand for the arrowhead (padded by arrowhead)
@@ -64,36 +67,42 @@ function annAutorange(gd) {
6467
});
6568
// again for the textbox (padded by textbox)
6669
Axes.expand(xa, [xa.r2c(ann.ax)], {
67-
ppadplus: ann._xpadplus,
68-
ppadminus: ann._xpadminus
70+
ppadplus: Math.max(ann._xpadplus, startHeadPlus),
71+
ppadminus: Math.max(ann._xpadminus, startHeadMinus)
6972
});
7073
}
7174
else {
75+
startHeadPlus = ann.ax ? startHeadPlus + ann.ax : startHeadPlus;
76+
startHeadMinus = ann.ax ? startHeadMinus - ann.ax : startHeadMinus;
7277
Axes.expand(xa, [xa.r2c(ann.x)], {
73-
ppadplus: Math.max(ann._xpadplus, headPlus),
74-
ppadminus: Math.max(ann._xpadminus, headMinus)
78+
ppadplus: Math.max(ann._xpadplus, headPlus, startHeadPlus),
79+
ppadminus: Math.max(ann._xpadminus, headMinus, startHeadMinus)
7580
});
7681
}
7782
}
7883

7984
if(ya && ya.autorange) {
8085
headPlus = headSize - ann.yshift;
8186
headMinus = headSize + ann.yshift;
87+
startHeadPlus = startHeadSize - ann.yshift;
88+
startHeadMinus = startHeadSize + ann.yshift;
8289

8390
if(ann.ayref === ann.yref) {
8491
Axes.expand(ya, [ya.r2c(ann.y)], {
8592
ppadplus: headPlus,
8693
ppadminus: headMinus
8794
});
8895
Axes.expand(ya, [ya.r2c(ann.ay)], {
89-
ppadplus: ann._ypadplus,
90-
ppadminus: ann._ypadminus
96+
ppadplus: Math.max(ann._ypadplus, startHeadPlus),
97+
ppadminus: Math.max(ann._ypadminus, startHeadMinus)
9198
});
9299
}
93100
else {
101+
startHeadPlus = ann.ay ? startHeadPlus + ann.ay : startHeadPlus;
102+
startHeadMinus = ann.ay ? startHeadMinus - ann.ay : startHeadMinus;
94103
Axes.expand(ya, [ya.r2c(ann.y)], {
95-
ppadplus: Math.max(ann._ypadplus, headPlus),
96-
ppadminus: Math.max(ann._ypadminus, headMinus)
104+
ppadplus: Math.max(ann._ypadplus, headPlus, startHeadPlus),
105+
ppadminus: Math.max(ann._ypadminus, headMinus, startHeadMinus)
97106
});
98107
}
99108
}

src/components/annotations/common_defaults.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,23 @@ module.exports = function handleAnnotationCommonDefaults(annIn, annOut, fullLayo
3535
if(h) coerce('valign');
3636

3737
if(showArrow) {
38+
var arrowside = coerce('arrowside');
39+
var arrowhead;
40+
var arrowsize;
41+
42+
if(arrowside.indexOf('end') !== -1) {
43+
arrowhead = coerce('arrowhead');
44+
arrowsize = coerce('arrowsize');
45+
}
46+
47+
if(arrowside.indexOf('start') !== -1) {
48+
coerce('startarrowhead', arrowhead);
49+
coerce('startarrowsize', arrowsize);
50+
}
3851
coerce('arrowcolor', borderOpacity ? annOut.bordercolor : Color.defaultLine);
39-
coerce('arrowhead');
40-
coerce('arrowsize');
4152
coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2);
4253
coerce('standoff');
54+
coerce('startstandoff');
4355

4456
}
4557

src/components/annotations/draw.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,8 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
509509
});
510510

511511
var strokewidth = options.arrowwidth,
512-
arrowColor = options.arrowcolor;
512+
arrowColor = options.arrowcolor,
513+
arrowSide = options.arrowside;
513514

514515
var arrowGroup = annGroup.append('g')
515516
.style({opacity: Color.opacity(arrowColor)})
@@ -520,7 +521,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
520521
.style('stroke-width', strokewidth + 'px')
521522
.call(Color.stroke, Color.rgb(arrowColor));
522523

523-
drawArrowHead(arrow, 'end', options);
524+
drawArrowHead(arrow, arrowSide, options);
524525

525526
// the arrow dragger is a small square right at the head, then a line to the tail,
526527
// all expanded by a stroke width of 6px plus the arrow line width

src/components/annotations/draw_arrow_head.js

+56-41
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ var ARROWPATHS = require('./arrow_paths');
2020
*
2121
* @param {d3.selection} el3: a d3-selected line or path element
2222
*
23-
* @param {string} ends: 'start', 'end', or 'start+end' for which ends get arrowheads
23+
* @param {string} ends: 'none', 'start', 'end', or 'start+end' for which ends get arrowheads
2424
*
2525
* @param {object} options: style information. Must have all the following:
26-
* @param {number} options.arrowhead: head style - see ./arrow_paths
27-
* @param {number} options.arrowsize: relative size of the head vs line width
28-
* @param {number} options.standoff: distance in px to move the arrow point from its target
26+
* @param {number} options.arrowhead: end head style - see ./arrow_paths
27+
* @param {number} options.startarrowhead: start head style - see ./arrow_paths
28+
* @param {number} options.arrowsize: relative size of the end head vs line width
29+
* @param {number} options.startarrowsize: relative size of the start head vs line width
30+
* @param {number} options.standoff: distance in px to move the end arrow point from its target
31+
* @param {number} options.startstandoff: distance in px to move the start arrow point from its target
2932
* @param {number} options.arrowwidth: width of the arrow line
3033
* @param {string} options.arrowcolor: color of the arrow line, for the head to match
3134
* Note that the opacity of this color is ignored, as it's assumed the container
@@ -35,10 +38,13 @@ var ARROWPATHS = require('./arrow_paths');
3538
module.exports = function drawArrowHead(el3, ends, options) {
3639
var el = el3.node();
3740
var headStyle = ARROWPATHS[options.arrowhead || 0];
38-
var scale = (options.arrowwidth || 1) * options.arrowsize;
41+
var startHeadStyle = ARROWPATHS[options.startarrowhead || 0];
42+
var scale = (options.arrowwidth || 1) * (options.arrowsize || 1);
43+
var startScale = (options.arrowwidth || 1) * (options.startarrowsize || 1);
3944
var doStart = ends.indexOf('start') >= 0;
4045
var doEnd = ends.indexOf('end') >= 0;
4146
var backOff = headStyle.backoff * scale + options.standoff;
47+
var startBackOff = startHeadStyle.backoff * startScale + options.startstandoff;
4248

4349
var start, end, startRot, endRot;
4450

@@ -51,6 +57,13 @@ module.exports = function drawArrowHead(el3, ends, options) {
5157

5258
startRot = Math.atan2(dy, dx);
5359
endRot = startRot + Math.PI;
60+
if(backOff && startBackOff) {
61+
if(backOff + startBackOff > Math.sqrt(dx * dx + dy * dy)) {
62+
hideLine();
63+
return;
64+
}
65+
}
66+
5467
if(backOff) {
5568
if(backOff * backOff > dx * dx + dy * dy) {
5669
hideLine();
@@ -59,16 +72,24 @@ module.exports = function drawArrowHead(el3, ends, options) {
5972
var backOffX = backOff * Math.cos(startRot),
6073
backOffY = backOff * Math.sin(startRot);
6174

62-
if(doStart) {
63-
start.x -= backOffX;
64-
start.y -= backOffY;
65-
el3.attr({x1: start.x, y1: start.y});
66-
}
67-
if(doEnd) {
68-
end.x += backOffX;
69-
end.y += backOffY;
70-
el3.attr({x2: end.x, y2: end.y});
75+
end.x += backOffX;
76+
end.y += backOffY;
77+
el3.attr({x2: end.x, y2: end.y});
78+
79+
}
80+
81+
if(startBackOff) {
82+
if(startBackOff * startBackOff > dx * dx + dy * dy) {
83+
hideLine();
84+
return;
7185
}
86+
var startBackOffX = startBackOff * Math.cos(startRot),
87+
startbackOffY = startBackOff * Math.sin(startRot);
88+
89+
start.x -= startBackOffX;
90+
start.y -= startbackOffY;
91+
el3.attr({x1: start.x, y1: start.y});
92+
7293
}
7394
}
7495
else if(el.nodeName === 'path') {
@@ -79,59 +100,53 @@ module.exports = function drawArrowHead(el3, ends, options) {
79100
// combine the two
80101
dashArray = '';
81102

82-
if(pathlen < backOff) {
103+
if(pathlen < backOff + startBackOff) {
83104
hideLine();
84105
return;
85106
}
86107

87-
if(doStart) {
88-
var start0 = el.getPointAtLength(0);
89-
var dstart = el.getPointAtLength(0.1);
90108

91-
startRot = Math.atan2(start0.y - dstart.y, start0.x - dstart.x);
92-
start = el.getPointAtLength(Math.min(backOff, pathlen));
109+
var start0 = el.getPointAtLength(0);
110+
var dstart = el.getPointAtLength(0.1);
93111

94-
if(backOff) dashArray = '0px,' + backOff + 'px,';
95-
}
112+
startRot = Math.atan2(start0.y - dstart.y, start0.x - dstart.x);
113+
start = el.getPointAtLength(Math.min(startBackOff, pathlen));
96114

97-
if(doEnd) {
98-
var end0 = el.getPointAtLength(pathlen);
99-
var dend = el.getPointAtLength(pathlen - 0.1);
115+
dashArray = '0px,' + startBackOff + 'px,';
100116

101-
endRot = Math.atan2(end0.y - dend.y, end0.x - dend.x);
102-
end = el.getPointAtLength(Math.max(0, pathlen - backOff));
117+
var end0 = el.getPointAtLength(pathlen);
118+
var dend = el.getPointAtLength(pathlen - 0.1);
103119

104-
if(backOff) {
105-
var shortening = dashArray ? 2 * backOff : backOff;
106-
dashArray += (pathlen - shortening) + 'px,' + pathlen + 'px';
107-
}
108-
}
109-
else if(dashArray) dashArray += pathlen + 'px';
120+
endRot = Math.atan2(end0.y - dend.y, end0.x - dend.x);
121+
end = el.getPointAtLength(Math.max(0, pathlen - backOff));
122+
123+
var shortening = dashArray ? startBackOff + backOff : backOff;
124+
dashArray += (pathlen - shortening) + 'px,' + pathlen + 'px';
110125

111-
if(dashArray) el3.style('stroke-dasharray', dashArray);
126+
el3.style('stroke-dasharray', dashArray);
112127
}
113128

114129
function hideLine() { el3.style('stroke-dasharray', '0px,100px'); }
115130

116-
function drawhead(p, rot) {
117-
if(!headStyle.path) return;
118-
if(headStyle.noRotate) rot = 0;
131+
function drawhead(arrowHeadStyle, p, rot, arrowScale) {
132+
if(!arrowHeadStyle.path) return;
133+
if(arrowHeadStyle.noRotate) rot = 0;
119134

120135
d3.select(el.parentNode).append('path')
121136
.attr({
122137
'class': el3.attr('class'),
123-
d: headStyle.path,
138+
d: arrowHeadStyle.path,
124139
transform:
125140
'translate(' + p.x + ',' + p.y + ')' +
126141
(rot ? 'rotate(' + (rot * 180 / Math.PI) + ')' : '') +
127-
'scale(' + scale + ')'
142+
'scale(' + arrowScale + ')'
128143
})
129144
.style({
130145
fill: Color.rgb(options.arrowcolor),
131146
'stroke-width': 0
132147
});
133148
}
134149

135-
if(doStart) drawhead(start, startRot);
136-
if(doEnd) drawhead(end, endRot);
150+
if(doStart) drawhead(startHeadStyle, start, startRot, startScale);
151+
if(doEnd) drawhead(headStyle, end, endRot, scale);
137152
};

src/components/annotations3d/attributes.js

+4
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,13 @@ module.exports = overrideAll({
7272
showarrow: annAtts.showarrow,
7373
arrowcolor: annAtts.arrowcolor,
7474
arrowhead: annAtts.arrowhead,
75+
startarrowhead: annAtts.startarrowhead,
76+
arrowside: annAtts.arrowside,
7577
arrowsize: annAtts.arrowsize,
78+
startarrowsize: annAtts.startarrowsize,
7679
arrowwidth: annAtts.arrowwidth,
7780
standoff: annAtts.standoff,
81+
startstandoff: annAtts.startstandoff,
7882
hovertext: annAtts.hovertext,
7983
hoverlabel: annAtts.hoverlabel,
8084
captureevents: annAtts.captureevents,
9.83 KB
Loading

test/image/baselines/annotations.png

1.43 KB
Loading
1.34 KB
Loading
2.9 KB
Loading

0 commit comments

Comments
 (0)