Skip to content

Commit a2ecd65

Browse files
MFedMFed
MFed
authored and
MFed
committed
Adding the ability to specify the tail of an annotation arrow in absolute point in grid terms rather than relative pixel offset terms.
1 parent 0714747 commit a2ecd65

File tree

5 files changed

+113
-19
lines changed

5 files changed

+113
-19
lines changed

src/components/annotations/attributes.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,31 @@ module.exports = {
131131
role: 'style',
132132
description: 'Sets the width (in px) of annotation arrow.'
133133
},
134+
absolutetail: {
135+
valType: 'boolean',
136+
dflt: false,
137+
role: 'style',
138+
description: [
139+
'Indicates if the tail of this arrow is a point in ',
140+
'the coordinate system vs a relative offset in pixels.',
141+
'This is useful for trendline annotations which should ',
142+
'continue to indicate the correct trend when zoomed.',
143+
'If *true*, `ax` is a value on the x axis and `ay` is ',
144+
'a value on the y axis.',
145+
'If *false*, `ax` and `ay` assume their normal offset ',
146+
'roles.'
147+
].join(' ')
148+
},
134149
ax: {
135150
valType: 'number',
136151
dflt: -10,
137152
role: 'info',
138153
description: [
139154
'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)'
155+
'If `absolutetail` is false, a positive (negative) ',
156+
'component corresponds to an arrow pointing',
157+
'from right to left (left to right).',
158+
'If `absolutetail` is true, this is a value on the x axis.'
142159
].join(' ')
143160
},
144161
ay: {
@@ -147,8 +164,10 @@ module.exports = {
147164
role: 'info',
148165
description: [
149166
'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)'
167+
'If `absolutetail` is false, a positive (negative) ',
168+
'component corresponds to an arrow pointing',
169+
'from bottom to top (top to bottom).',
170+
'If `absolutetail` is true, this is a value on the y axis.'
152171
].join(' ')
153172
},
154173
// positioning

src/components/annotations/index.js

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function handleAnnotationDefaults(annIn, fullLayout) {
5959
coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2);
6060
coerce('ax');
6161
coerce('ay');
62+
coerce('absolutetail');
6263

6364
// if you have one part of arrow length you should have both
6465
Lib.noneOrAll(annIn, annOut, ['ax', 'ay']);
@@ -89,6 +90,11 @@ function handleAnnotationDefaults(annIn, fullLayout) {
8990
if(ax.type === 'date') {
9091
newval = Lib.dateTime2ms(annIn[axLetter]);
9192
if(newval !== false) annIn[axLetter] = newval;
93+
94+
if(annIn.absolutetail) {
95+
var newvalB = Lib.dateTime2ms(annIn['a' + axLetter]);
96+
if(newvalB !== false) annIn['a' + axLetter] = newvalB;
97+
}
9298
}
9399
else if((ax._categories || []).length) {
94100
newval = ax._categories.indexOf(annIn[axLetter]);
@@ -450,13 +456,17 @@ annotations.draw = function(gd, index, opt, value) {
450456
}
451457

452458
var alignShift = 0;
453-
if(options.showarrow) {
454-
alignShift = options['a' + axLetter];
455-
}
456-
else {
457-
alignShift = annSize * shiftFraction(alignPosition, anchor);
459+
if(options.absolutetail) {
460+
annPosPx['aa' + axLetter] = ax._offset + ax.l2p(options['a' + axLetter]);
461+
} else {
462+
if(options.showarrow) {
463+
alignShift = options['a' + axLetter];
464+
}
465+
else {
466+
alignShift = annSize * shiftFraction(alignPosition, anchor);
467+
}
468+
annPosPx[axLetter] += alignShift;
458469
}
459-
annPosPx[axLetter] += alignShift;
460470

461471
// save the current axis type for later log/linear changes
462472
options['_' + axLetter + 'type'] = ax && ax.type;
@@ -476,8 +486,13 @@ annotations.draw = function(gd, index, opt, value) {
476486
// make sure the arrowhead (if there is one)
477487
// and the annotation center are visible
478488
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);
489+
if(options.absolutetail) {
490+
arrowX = Lib.constrain(annPosPx.x, 1, fullLayout.width - 1);
491+
arrowY = Lib.constrain(annPosPx.y, 1, fullLayout.height - 1);
492+
} else {
493+
arrowX = Lib.constrain(annPosPx.x - options.ax, 1, fullLayout.width - 1);
494+
arrowY = Lib.constrain(annPosPx.y - options.ay, 1, fullLayout.height - 1);
495+
}
481496
}
482497
annPosPx.x = Lib.constrain(annPosPx.x, 1, fullLayout.width - 1);
483498
annPosPx.y = Lib.constrain(annPosPx.y, 1, fullLayout.height - 1);
@@ -496,8 +511,15 @@ annotations.draw = function(gd, index, opt, value) {
496511
annbg.call(Drawing.setRect, borderwidth / 2, borderwidth / 2,
497512
outerwidth - borderwidth, outerheight - borderwidth);
498513

499-
var annX = Math.round(annPosPx.x - outerwidth / 2),
514+
var annX = 0, annY = 0;
515+
if(options.absolutetail) {
516+
annX = Math.round(annPosPx.aax - outerwidth / 2);
517+
annY = Math.round(annPosPx.aay - outerheight / 2);
518+
} else {
519+
annX = Math.round(annPosPx.x - outerwidth / 2);
500520
annY = Math.round(annPosPx.y - outerheight / 2);
521+
}
522+
501523
ann.call(Lib.setTranslate, annX, annY);
502524

503525
var annbase = 'annotations[' + index + ']';
@@ -515,11 +537,18 @@ annotations.draw = function(gd, index, opt, value) {
515537
// looks like there may be a cross-browser solution, see
516538
// http://stackoverflow.com/questions/5364980/
517539
// how-to-get-the-width-of-an-svg-tspan-element
518-
var arrowX0 = annPosPx.x + dx,
519-
arrowY0 = annPosPx.y + dy,
540+
var arrowX0, arrowY0;
541+
542+
if(options.absolutetail) {
543+
arrowX0 = annPosPx.aax + dx;
544+
arrowY0 = annPosPx.aay + dy;
545+
} else {
546+
arrowX0 = annPosPx.x + dx;
547+
arrowY0 = annPosPx.y + dy;
548+
}
520549

521550
// create transform matrix and related functions
522-
transform =
551+
var transform =
523552
Lib.rotationXYMatrix(textangle, arrowX0, arrowY0),
524553
applyTransform = Lib.apply2DTransform(transform),
525554
applyTransform2 = Lib.apply2DTransform2(transform),
@@ -618,6 +647,15 @@ annotations.draw = function(gd, index, opt, value) {
618647
(options.y + dy / ya._m) :
619648
(1 - ((arrowY + dy - gs.t) / gs.h));
620649

650+
if(options.absolutetail) {
651+
update[annbase + '.ax'] = xa ?
652+
(options.ax + dx / xa._m) :
653+
((arrowX + dx - gs.l) / gs.w);
654+
update[annbase + '.ay'] = ya ?
655+
(options.ay + dy / ya._m) :
656+
(1 - ((arrowY + dy - gs.t) / gs.h));
657+
}
658+
621659
anng.attr({
622660
transform: 'rotate(' + textangle + ',' +
623661
xcenter + ',' + ycenter + ')'
@@ -660,8 +698,13 @@ annotations.draw = function(gd, index, opt, value) {
660698
ann.call(Lib.setTranslate, x0 + dx, y0 + dy);
661699
var csr = 'pointer';
662700
if(options.showarrow) {
663-
update[annbase + '.ax'] = options.ax + dx;
664-
update[annbase + '.ay'] = options.ay + dy;
701+
if(options.absolutetail) {
702+
update[annbase + '.ax'] = xa.p2l(xa.l2p(options.ax) + dx);
703+
update[annbase + '.ay'] = ya.p2l(ya.l2p(options.ay) + dy);
704+
} else {
705+
update[annbase + '.ax'] = options.ax + dx;
706+
update[annbase + '.ay'] = options.ay + dy;
707+
}
665708
drawArrow(dx, dy);
666709
}
667710
else {

test/image/baselines/annotations.png

2.4 KB
Loading

test/image/mocks/annotations.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
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,"absolutetail":true,"x":5,"y":5,"ax":4,"ay":3}
4546
]
4647
}
4748
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
var Plots = require('@src/plots/plots');
2+
var Annotations = require('@src/components/annotations');
3+
4+
5+
describe('Test annotations', function() {
6+
'use strict';
7+
8+
describe('supplyLayoutDefaults', function() {
9+
it('should default to not use absolute arrow tail', function() {
10+
var annotationDefaults = {};
11+
annotationDefaults._has = Plots._hasPlotType.bind(annotationDefaults);
12+
13+
Annotations.supplyLayoutDefaults({ annotations: [{ showarrow: true, arrowhead: 2}] }, annotationDefaults);
14+
15+
expect(annotationDefaults.annotations[0].absolutetail).toBe(false);
16+
});
17+
18+
it('should convert ax/ay date coordinates to milliseconds if absolutetail is true', function() {
19+
var annotationOut = { xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] }};
20+
annotationOut._has = Plots._hasPlotType.bind(annotationOut);
21+
22+
var annotationIn = {
23+
annotations: [{ showarrow: true, absolutetail: true, x: '2008-07-01', ax: '2004-07-01', y: 0, ay: 50}]
24+
};
25+
26+
Annotations.supplyLayoutDefaults(annotationIn, annotationOut);
27+
28+
expect(annotationIn.annotations[0].ax).toEqual(1088654400000);
29+
});
30+
});
31+
});

0 commit comments

Comments
 (0)