Skip to content

Commit a1da755

Browse files
MFedMFed
authored andcommitted
Adding the ability to specify the tail of an annotation arrow in absolute point in grid terms rather than relative pixel offset terms. Squashing commits
1 parent ef6efd7 commit a1da755

File tree

13 files changed

+297
-274
lines changed

13 files changed

+297
-274
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: 61 additions & 18 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),
@@ -541,21 +570,22 @@ annotations.draw = function(gd, index, opt, value) {
541570
if(edges.reduce(function(a, x) {
542571
return a ^
543572
!!lineIntersect(arrowX, arrowY, arrowX + 1e6, arrowY + 1e6,
544-
x[0], x[1], x[2], x[3]);
573+
x[0], x[1], x[2], x[3]);
545574
}, false)) {
546575
// no line or arrow - so quit drawArrow now
547576
return;
548577
}
549578

550579
edges.forEach(function(x) {
551580
var p = lineIntersect(arrowX0, arrowY0, arrowX, arrowY,
552-
x[0], x[1], x[2], x[3]);
581+
x[0], x[1], x[2], x[3]);
553582
if(p) {
554583
arrowX0 = p.x;
555584
arrowY0 = p.y;
556585
}
557586
});
558587

588+
559589
var strokewidth = options.arrowwidth,
560590
arrowColor = options.arrowcolor;
561591

@@ -618,9 +648,18 @@ annotations.draw = function(gd, index, opt, value) {
618648
(options.y + dy / ya._m) :
619649
(1 - ((arrowY + dy - gs.t) / gs.h));
620650

651+
if(options.absolutetail) {
652+
update[annbase + '.ax'] = xa ?
653+
(options.ax + dx / xa._m) :
654+
((arrowX + dx - gs.l) / gs.w);
655+
update[annbase + '.ay'] = ya ?
656+
(options.ay + dy / ya._m) :
657+
(1 - ((arrowY + dy - gs.t) / gs.h));
658+
}
659+
621660
anng.attr({
622661
transform: 'rotate(' + textangle + ',' +
623-
xcenter + ',' + ycenter + ')'
662+
xcenter + ',' + ycenter + ')'
624663
});
625664
},
626665
doneFn: function(dragged) {
@@ -651,7 +690,6 @@ annotations.draw = function(gd, index, opt, value) {
651690
element: ann.node(),
652691
prepFn: function() {
653692
var pos = Lib.getTranslate(ann);
654-
655693
x0 = pos.x;
656694
y0 = pos.y;
657695
update = {};
@@ -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 {

src/components/legend/draw.js

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ module.exports = function draw(gd) {
4343
return;
4444
}
4545

46-
if(typeof gd.firstRender === 'undefined') gd.firstRender = true;
47-
else if(gd.firstRender) gd.firstRender = false;
48-
4946
var legend = fullLayout._infolayer.selectAll('g.legend')
5047
.data([0]);
5148

@@ -122,7 +119,8 @@ module.exports = function draw(gd) {
122119
.call(setupTraceToggle, gd);
123120
});
124121

125-
if(gd.firstRender) {
122+
var firstRender = legend.enter().size() !== 0;
123+
if(firstRender) {
126124
computeLegendDimensions(gd, groups, traces);
127125
expandMargin(gd);
128126
}
@@ -198,60 +196,64 @@ module.exports = function draw(gd) {
198196
// legend, background and border, scroll box and scroll bar
199197
Lib.setTranslate(legend, lx, ly);
200198

201-
bg.attr({
202-
width: legendWidth - opts.borderwidth,
203-
height: legendHeight - opts.borderwidth,
204-
x: opts.borderwidth / 2,
205-
y: opts.borderwidth / 2
206-
});
199+
var scrollBarYMax = legendHeight -
200+
constants.scrollBarHeight -
201+
2 * constants.scrollBarMargin,
202+
scrollBoxYMax = opts.height - legendHeight,
203+
scrollBarY,
204+
scrollBoxY;
207205

208-
var scrollPosition = scrollBox.attr('data-scroll') || 0;
206+
if(opts.height <= legendHeight || gd._context.staticPlot) {
207+
// if scrollbar should not be shown.
208+
bg.attr({
209+
width: legendWidth - opts.borderwidth,
210+
height: legendHeight - opts.borderwidth,
211+
x: opts.borderwidth / 2,
212+
y: opts.borderwidth / 2
213+
});
209214

210-
Lib.setTranslate(scrollBox, 0, scrollPosition);
215+
Lib.setTranslate(scrollBox, 0, 0);
211216

212-
clipPath.select('rect').attr({
213-
width: legendWidth - 2 * opts.borderwidth,
214-
height: legendHeight - 2 * opts.borderwidth,
215-
x: opts.borderwidth - scrollPosition,
216-
y: opts.borderwidth
217-
});
218-
219-
scrollBox.call(Drawing.setClipUrl, clipId);
217+
clipPath.select('rect').attr({
218+
width: legendWidth - 2 * opts.borderwidth,
219+
height: legendHeight - 2 * opts.borderwidth,
220+
x: opts.borderwidth,
221+
y: opts.borderwidth
222+
});
220223

221-
// If scrollbar should be shown.
222-
if(opts.height - legendHeight > 0 && !gd._context.staticPlot) {
224+
scrollBox.call(Drawing.setClipUrl, clipId);
225+
}
226+
else {
227+
scrollBarY = constants.scrollBarMargin,
228+
scrollBoxY = scrollBox.attr('data-scroll') || 0;
223229

224230
// increase the background and clip-path width
225231
// by the scrollbar width and margin
226232
bg.attr({
227233
width: legendWidth -
228234
2 * opts.borderwidth +
229235
constants.scrollBarWidth +
230-
constants.scrollBarMargin
236+
constants.scrollBarMargin,
237+
height: legendHeight - opts.borderwidth,
238+
x: opts.borderwidth / 2,
239+
y: opts.borderwidth / 2
231240
});
232241

233242
clipPath.select('rect').attr({
234243
width: legendWidth -
235244
2 * opts.borderwidth +
236245
constants.scrollBarWidth +
237-
constants.scrollBarMargin
246+
constants.scrollBarMargin,
247+
height: legendHeight - 2 * opts.borderwidth,
248+
x: opts.borderwidth,
249+
y: opts.borderwidth - scrollBoxY
238250
});
239251

240-
if(gd.firstRender) {
241-
// Move scrollbar to starting position
242-
scrollHandler(constants.scrollBarMargin, 0);
243-
}
244-
245-
var scrollBarYMax = legendHeight -
246-
constants.scrollBarHeight -
247-
2 * constants.scrollBarMargin,
248-
scrollBoxYMax = opts.height - legendHeight,
249-
scrollBarY = constants.scrollBarMargin,
250-
scrollBoxY = 0;
252+
scrollBox.call(Drawing.setClipUrl, clipId);
251253

252-
scrollHandler(scrollBarY, scrollBoxY);
254+
if(firstRender) scrollHandler(scrollBarY, scrollBoxY);
253255

254-
legend.on('wheel', null);
256+
legend.on('wheel', null); // to be safe, remove previous listeners
255257
legend.on('wheel', function() {
256258
scrollBoxY = Lib.constrain(
257259
scrollBox.attr('data-scroll') -
@@ -263,8 +265,10 @@ module.exports = function draw(gd) {
263265
d3.event.preventDefault();
264266
});
265267

268+
// to be safe, remove previous listeners
266269
scrollBar.on('.drag', null);
267270
scrollBox.on('.drag', null);
271+
268272
var drag = d3.behavior.drag().on('drag', function() {
269273
scrollBarY = Lib.constrain(
270274
d3.event.y - constants.scrollBarHeight / 2,
@@ -277,7 +281,6 @@ module.exports = function draw(gd) {
277281

278282
scrollBar.call(drag);
279283
scrollBox.call(drag);
280-
281284
}
282285

283286

0 commit comments

Comments
 (0)