Skip to content

Commit 3c85d9e

Browse files
authored
Merge pull request #610 from mdfederici/absolutetail
Adding the ability to specify the tail of an annotation arrow in abso…
2 parents 2b3c344 + 9358a56 commit 3c85d9e

File tree

6 files changed

+196
-23
lines changed

6 files changed

+196
-23
lines changed

src/components/annotations/attributes.js

+42-4
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@ module.exports = {
137137
role: 'info',
138138
description: [
139139
'Sets the x component of the arrow tail about the arrow head.',
140-
'A positive (negative) component corresponds to an arrow pointing',
141-
'from right to left (left to right)'
140+
'If `axref` is `pixel`, a positive (negative) ',
141+
'component corresponds to an arrow pointing',
142+
'from right to left (left to right).',
143+
'If `axref` is an axis, this is a value on that axis.'
142144
].join(' ')
143145
},
144146
ay: {
@@ -147,8 +149,44 @@ module.exports = {
147149
role: 'info',
148150
description: [
149151
'Sets the y component of the arrow tail about the arrow head.',
150-
'A positive (negative) component corresponds to an arrow pointing',
151-
'from bottom to top (top to bottom)'
152+
'If `ayref` is `pixel`, a positive (negative) ',
153+
'component corresponds to an arrow pointing',
154+
'from bottom to top (top to bottom).',
155+
'If `ayref` is an axis, this is a value on that axis.'
156+
].join(' ')
157+
},
158+
axref: {
159+
valType: 'enumerated',
160+
dflt: 'pixel',
161+
values: [
162+
'pixel',
163+
cartesianConstants.idRegex.x.toString()
164+
],
165+
role: 'info',
166+
description: [
167+
'Indicates in what terms the tail of the annotation (ax,ay) ',
168+
'is specified. If `pixel`, `ax` is a relative offset in pixels ',
169+
'from `x`. If set to an x axis id (e.g. *x* or *x2*), `ax` is ',
170+
'specified in the same terms as that axis. This is useful ',
171+
'for trendline annotations which should continue to indicate ',
172+
'the correct trend when zoomed.'
173+
].join(' ')
174+
},
175+
ayref: {
176+
valType: 'enumerated',
177+
dflt: 'pixel',
178+
values: [
179+
'pixel',
180+
cartesianConstants.idRegex.y.toString()
181+
],
182+
role: 'info',
183+
description: [
184+
'Indicates in what terms the tail of the annotation (ax,ay) ',
185+
'is specified. If `pixel`, `ay` is a relative offset in pixels ',
186+
'from `y`. If set to a y axis id (e.g. *y* or *y2*), `ay` is ',
187+
'specified in the same terms as that axis. This is useful ',
188+
'for trendline annotations which should continue to indicate ',
189+
'the correct trend when zoomed.'
152190
].join(' ')
153191
},
154192
// positioning

src/components/annotations/index.js

+98-18
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ function handleAnnotationDefaults(annIn, fullLayout) {
5959
coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2);
6060
coerce('ax');
6161
coerce('ay');
62+
coerce('axref');
63+
coerce('ayref');
6264

6365
// if you have one part of arrow length you should have both
6466
Lib.noneOrAll(annIn, annOut, ['ax', 'ay']);
@@ -76,6 +78,10 @@ function handleAnnotationDefaults(annIn, fullLayout) {
7678
// xref, yref
7779
var axRef = Axes.coerceRef(annIn, annOut, tdMock, axLetter);
7880

81+
//todo: should be refactored in conjunction with Axes
82+
// axref, ayref
83+
var aaxRef = Axes.coerceARef(annIn, annOut, tdMock, axLetter);
84+
7985
// x, y
8086
var defaultPosition = 0.5;
8187
if(axRef !== 'paper') {
@@ -89,6 +95,11 @@ function handleAnnotationDefaults(annIn, fullLayout) {
8995
if(ax.type === 'date') {
9096
newval = Lib.dateTime2ms(annIn[axLetter]);
9197
if(newval !== false) annIn[axLetter] = newval;
98+
99+
if(aaxRef === axRef) {
100+
var newvalB = Lib.dateTime2ms(annIn['a' + axLetter]);
101+
if(newvalB !== false) annIn['a' + axLetter] = newvalB;
102+
}
92103
}
93104
else if((ax._categories || []).length) {
94105
newval = ax._categories.indexOf(annIn[axLetter]);
@@ -419,8 +430,8 @@ annotations.draw = function(gd, index, opt, value) {
419430

420431
var annotationIsOffscreen = false;
421432
['x', 'y'].forEach(function(axLetter) {
422-
var ax = Axes.getFromId(gd,
423-
options[axLetter + 'ref'] || axLetter),
433+
var axRef = options[axLetter + 'ref'] || axLetter,
434+
ax = Axes.getFromId(gd, axRef),
424435
dimAngle = (textangle + (axLetter === 'x' ? 0 : 90)) * Math.PI / 180,
425436
annSize = outerwidth * Math.abs(Math.cos(dimAngle)) +
426437
outerheight * Math.abs(Math.sin(dimAngle)),
@@ -435,8 +446,16 @@ annotations.draw = function(gd, index, opt, value) {
435446
// anyway to get its bounding box)
436447
if(!ax.autorange && ((options[axLetter] - ax.range[0]) *
437448
(options[axLetter] - ax.range[1]) > 0)) {
438-
annotationIsOffscreen = true;
439-
return;
449+
if(options['a' + axLetter + 'ref'] === axRef) {
450+
if((options['a' + axLetter] - ax.range[0]) *
451+
(options['a' + axLetter] - ax.range[1]) > 0) {
452+
annotationIsOffscreen = true;
453+
}
454+
} else {
455+
annotationIsOffscreen = true;
456+
}
457+
458+
if(annotationIsOffscreen) return;
440459
}
441460
annPosPx[axLetter] = ax._offset + ax.l2p(options[axLetter]);
442461
alignPosition = 0.5;
@@ -450,13 +469,17 @@ annotations.draw = function(gd, index, opt, value) {
450469
}
451470

452471
var alignShift = 0;
453-
if(options.showarrow) {
454-
alignShift = options['a' + axLetter];
455-
}
456-
else {
457-
alignShift = annSize * shiftFraction(alignPosition, anchor);
472+
if(options['a' + axLetter + 'ref'] === axRef) {
473+
annPosPx['aa' + axLetter] = ax._offset + ax.l2p(options['a' + axLetter]);
474+
} else {
475+
if(options.showarrow) {
476+
alignShift = options['a' + axLetter];
477+
}
478+
else {
479+
alignShift = annSize * shiftFraction(alignPosition, anchor);
480+
}
481+
annPosPx[axLetter] += alignShift;
458482
}
459-
annPosPx[axLetter] += alignShift;
460483

461484
// save the current axis type for later log/linear changes
462485
options['_' + axLetter + 'type'] = ax && ax.type;
@@ -476,8 +499,21 @@ annotations.draw = function(gd, index, opt, value) {
476499
// make sure the arrowhead (if there is one)
477500
// and the annotation center are visible
478501
if(options.showarrow) {
479-
arrowX = Lib.constrain(annPosPx.x - options.ax, 1, fullLayout.width - 1);
480-
arrowY = Lib.constrain(annPosPx.y - options.ay, 1, fullLayout.height - 1);
502+
if(options.axref === options.xref) {
503+
//we don't want to constrain if the tail is absolute
504+
//or the slope (which is meaningful) will change.
505+
arrowX = annPosPx.x;
506+
} else {
507+
arrowX = Lib.constrain(annPosPx.x - options.ax, 1, fullLayout.width - 1);
508+
}
509+
510+
if(options.ayref === options.yref) {
511+
//we don't want to constrain if the tail is absolute
512+
//or the slope (which is meaningful) will change.
513+
arrowY = annPosPx.y;
514+
} else {
515+
arrowY = Lib.constrain(annPosPx.y - options.ay, 1, fullLayout.height - 1);
516+
}
481517
}
482518
annPosPx.x = Lib.constrain(annPosPx.x, 1, fullLayout.width - 1);
483519
annPosPx.y = Lib.constrain(annPosPx.y, 1, fullLayout.height - 1);
@@ -496,8 +532,19 @@ annotations.draw = function(gd, index, opt, value) {
496532
annbg.call(Drawing.setRect, borderwidth / 2, borderwidth / 2,
497533
outerwidth - borderwidth, outerheight - borderwidth);
498534

499-
var annX = Math.round(annPosPx.x - outerwidth / 2),
535+
var annX = 0, annY = 0;
536+
if(options.axref === options.xref) {
537+
annX = Math.round(annPosPx.aax - outerwidth / 2);
538+
} else {
539+
annX = Math.round(annPosPx.x - outerwidth / 2);
540+
}
541+
542+
if(options.ayref === options.yref) {
543+
annY = Math.round(annPosPx.aay - outerheight / 2);
544+
} else {
500545
annY = Math.round(annPosPx.y - outerheight / 2);
546+
}
547+
501548
ann.call(Lib.setTranslate, annX, annY);
502549

503550
var annbase = 'annotations[' + index + ']';
@@ -515,11 +562,22 @@ annotations.draw = function(gd, index, opt, value) {
515562
// looks like there may be a cross-browser solution, see
516563
// http://stackoverflow.com/questions/5364980/
517564
// how-to-get-the-width-of-an-svg-tspan-element
518-
var arrowX0 = annPosPx.x + dx,
519-
arrowY0 = annPosPx.y + dy,
565+
var arrowX0, arrowY0;
566+
567+
if(options.axref === options.xref) {
568+
arrowX0 = annPosPx.aax + dx;
569+
} else {
570+
arrowX0 = annPosPx.x + dx;
571+
}
572+
573+
if(options.ayref === options.yref) {
574+
arrowY0 = annPosPx.aay + dy;
575+
} else {
576+
arrowY0 = annPosPx.y + dy;
577+
}
520578

521579
// create transform matrix and related functions
522-
transform =
580+
var transform =
523581
Lib.rotationXYMatrix(textangle, arrowX0, arrowY0),
524582
applyTransform = Lib.apply2DTransform(transform),
525583
applyTransform2 = Lib.apply2DTransform2(transform),
@@ -618,6 +676,18 @@ annotations.draw = function(gd, index, opt, value) {
618676
(options.y + dy / ya._m) :
619677
(1 - ((arrowY + dy - gs.t) / gs.h));
620678

679+
if(options.axref === options.xref) {
680+
update[annbase + '.ax'] = xa ?
681+
(options.ax + dx / xa._m) :
682+
((arrowX + dx - gs.l) / gs.w);
683+
}
684+
685+
if(options.ayref === options.yref) {
686+
update[annbase + '.ay'] = ya ?
687+
(options.ay + dy / ya._m) :
688+
(1 - ((arrowY + dy - gs.t) / gs.h));
689+
}
690+
621691
anng.attr({
622692
transform: 'rotate(' + textangle + ',' +
623693
xcenter + ',' + ycenter + ')'
@@ -660,8 +730,18 @@ annotations.draw = function(gd, index, opt, value) {
660730
ann.call(Lib.setTranslate, x0 + dx, y0 + dy);
661731
var csr = 'pointer';
662732
if(options.showarrow) {
663-
update[annbase + '.ax'] = options.ax + dx;
664-
update[annbase + '.ay'] = options.ay + dy;
733+
if(options.axref === options.xref) {
734+
update[annbase + '.ax'] = xa.p2l(xa.l2p(options.ax) + dx);
735+
} else {
736+
update[annbase + '.ax'] = options.ax + dx;
737+
}
738+
739+
if(options.ayref === options.yref) {
740+
update[annbase + '.ay'] = ya.p2l(ya.l2p(options.ay) + dy);
741+
} else {
742+
update[annbase + '.ay'] = options.ay + dy;
743+
}
744+
665745
drawArrow(dx, dy);
666746
}
667747
else {

src/plots/cartesian/axes.js

+20
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,26 @@ axes.coerceRef = function(containerIn, containerOut, gd, axLetter, dflt) {
5555
return Lib.coerce(containerIn, containerOut, attrDef, refAttr);
5656
};
5757

58+
//todo: duplicated per github PR 610. Should be consolidated with axes.coerceRef.
59+
// find the list of possible axes to reference with an axref or ayref attribute
60+
// and coerce it to that list
61+
axes.coerceARef = function(containerIn, containerOut, gd, axLetter, dflt) {
62+
var axlist = gd._fullLayout._has('gl2d') ? [] : axes.listIds(gd, axLetter),
63+
refAttr = 'a' + axLetter + 'ref',
64+
attrDef = {};
65+
66+
// data-ref annotations are not supported in gl2d yet
67+
68+
attrDef[refAttr] = {
69+
valType: 'enumerated',
70+
values: axlist.concat(['pixel']),
71+
dflt: dflt || 'pixel' || axlist[0]
72+
};
73+
74+
// axref, ayref
75+
return Lib.coerce(containerIn, containerOut, attrDef, refAttr);
76+
};
77+
5878
// empty out types for all axes containing these traces
5979
// so we auto-set them again
6080
axes.clearTypes = function(gd, traces) {

test/image/baselines/annotations.png

3.29 KB
Loading

test/image/mocks/annotations.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
"bordercolor":"rgb(255, 0, 0)","borderwidth":4,"bgcolor":"rgba(255,255,0,0.5)",
4242
"font":{"color":"rgb(0, 0, 255)","size":20},
4343
"arrowcolor":"rgb(166, 28, 0)","borderpad":3,"textangle":50,"x":5,"y":1
44-
}
44+
},
45+
{"text":"","showarrow":true,"borderwidth":1.2,"arrowhead":2,"axref":"x","ayref":"y","x":5,"y":3,"ax":4,"ay":5},
46+
{"text":"","showarrow":true,"borderwidth":1.2,"arrowhead":2,"axref":"x","ayref":"y","x":6,"y":2,"ax":3,"ay":3}
4547
]
4648
}
4749
}
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require('@src/plotly');
2+
var Plots = require('@src/plots/plots');
3+
var Annotations = require('@src/components/annotations');
4+
var Dates = require('@src/lib/dates');
5+
6+
describe('Test annotations', function() {
7+
'use strict';
8+
9+
describe('supplyLayoutDefaults', function() {
10+
it('should default to pixel for axref/ayref', function() {
11+
var annotationDefaults = {};
12+
annotationDefaults._has = Plots._hasPlotType.bind(annotationDefaults);
13+
14+
Annotations.supplyLayoutDefaults({ annotations: [{ showarrow: true, arrowhead: 2}] }, annotationDefaults);
15+
16+
expect(annotationDefaults.annotations[0].axref).toEqual('pixel');
17+
expect(annotationDefaults.annotations[0].ayref).toEqual('pixel');
18+
});
19+
20+
it('should convert ax/ay date coordinates to milliseconds if tail is in axis terms and axis is a date', function() {
21+
var annotationOut = { xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] }};
22+
annotationOut._has = Plots._hasPlotType.bind(annotationOut);
23+
24+
var annotationIn = {
25+
annotations: [{ showarrow: true, axref: 'x', ayref: 'y', x: '2008-07-01', ax: '2004-07-01', y: 0, ay: 50}]
26+
};
27+
28+
Annotations.supplyLayoutDefaults(annotationIn, annotationOut);
29+
30+
expect(annotationIn.annotations[0].ax).toEqual(Dates.dateTime2ms('2004-07-01'));
31+
});
32+
});
33+
});

0 commit comments

Comments
 (0)