Skip to content

Adding the ability to specify the tail of an annotation arrow in abso… #610

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

Merged
merged 20 commits into from
Jun 20, 2016
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ed8295a
Adding the ability to specify the tail of an annotation arrow in abso…
Jun 3, 2016
a2e9c72
Adding the ability to specify the tail of an annotation arrow in abso…
Jun 3, 2016
8fcb9dd
Merge remote-tracking branch 'origin/trendline' into trendline
Jun 5, 2016
a7e0e88
Fixing text and editing.
Jun 7, 2016
722a1ca
code review feedback
Jun 7, 2016
a1da755
Adding the ability to specify the tail of an annotation arrow in abso…
Jun 7, 2016
a2ecd65
Adding the ability to specify the tail of an annotation arrow in abso…
Jun 7, 2016
8697050
Merge remote-tracking branch 'origin/trendline' into trendline
Jun 7, 2016
49d9c7e
Trying to fix tests for absolutetail.
Jun 7, 2016
91dd08a
Removing these unit tests because they are of dubious value relative …
Jun 7, 2016
dbdcaac
fixing the unit tests for annotations and adding them back.
Jun 7, 2016
253a3fa
fixing timezone problem in annotations test.
Jun 7, 2016
4aa569d
fixing lint for annotation tests.
Jun 7, 2016
a008354
Switching absolutetail to axref, ayref based on code review feedback.…
Jun 14, 2016
925360f
Merge remote-tracking branch 'upstream/master' into trendline
Jun 14, 2016
3a827a6
Merge remote-tracking branch 'origin/absolutetail' into absolutetail
Jun 14, 2016
d07b676
somehow I lost this change in my screw up with the branches.
Jun 14, 2016
143328d
Not hardcoding axis references but using regex and coerce. Purposeful…
Jun 14, 2016
e83b947
Fixing bug where annotation would be marked as offscreen if the head …
Jun 16, 2016
9358a56
Fixing a bug where the x,y of the arrow would be changed to stay on t…
Jun 20, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions src/components/annotations/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,31 @@ module.exports = {
role: 'style',
description: 'Sets the width (in px) of annotation arrow.'
},
absolutetail: {
valType: 'boolean',
dflt: false,
role: 'style',
description: [
'Indicates if the tail of this arrow is a point in ',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better, more customizable and more plotly.js-esque to have this functionality be set with a pair of attributes, one for each axis?

I'm thinking about:

{
  axref: 'x',  // or 'pixel' (or 'paper' down the road) similar to 'xref' but for the arrow tail
  ayref: 'y'  
}

@mdfederici But this solution might have too many degrees of freedom for most cases. Can you think of a use case where a user would want the x arrow coordinates in pixel and the y in data coordinates (or vice-versa)?

'the coordinate system vs a relative offset in pixels.',
'This is useful for trendline annotations which should ',
'continue to indicate the correct trend when zoomed.',
'If *true*, `ax` is a value on the x axis and `ay` is ',
'a value on the y axis.',
'If *false*, `ax` and `ay` assume their normal offset ',
'roles.'
].join(' ')
},
ax: {
valType: 'number',
dflt: -10,
role: 'info',
description: [
'Sets the x component of the arrow tail about the arrow head.',
'A positive (negative) component corresponds to an arrow pointing',
'from right to left (left to right)'
'If `absolutetail` is false, a positive (negative) ',
'component corresponds to an arrow pointing',
'from right to left (left to right).',
'If `absolutetail` is true, this is a value on the x axis.'
].join(' ')
},
ay: {
Expand All @@ -147,8 +164,10 @@ module.exports = {
role: 'info',
description: [
'Sets the y component of the arrow tail about the arrow head.',
'A positive (negative) component corresponds to an arrow pointing',
'from bottom to top (top to bottom)'
'If `absolutetail` is false, a positive (negative) ',
'component corresponds to an arrow pointing',
'from bottom to top (top to bottom).',
'If `absolutetail` is true, this is a value on the y axis.'
].join(' ')
},
// positioning
Expand Down
71 changes: 57 additions & 14 deletions src/components/annotations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function handleAnnotationDefaults(annIn, fullLayout) {
coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2);
coerce('ax');
coerce('ay');
coerce('absolutetail');

// if you have one part of arrow length you should have both
Lib.noneOrAll(annIn, annOut, ['ax', 'ay']);
Expand Down Expand Up @@ -89,6 +90,11 @@ function handleAnnotationDefaults(annIn, fullLayout) {
if(ax.type === 'date') {
newval = Lib.dateTime2ms(annIn[axLetter]);
if(newval !== false) annIn[axLetter] = newval;

if(annIn.absolutetail) {
var newvalB = Lib.dateTime2ms(annIn['a' + axLetter]);
if(newvalB !== false) annIn['a' + axLetter] = newvalB;
}
}
else if((ax._categories || []).length) {
newval = ax._categories.indexOf(annIn[axLetter]);
Expand Down Expand Up @@ -450,13 +456,17 @@ annotations.draw = function(gd, index, opt, value) {
}

var alignShift = 0;
if(options.showarrow) {
alignShift = options['a' + axLetter];
}
else {
alignShift = annSize * shiftFraction(alignPosition, anchor);
if(options.absolutetail) {
annPosPx['aa' + axLetter] = ax._offset + ax.l2p(options['a' + axLetter]);
} else {
if(options.showarrow) {
alignShift = options['a' + axLetter];
}
else {
alignShift = annSize * shiftFraction(alignPosition, anchor);
}
annPosPx[axLetter] += alignShift;
}
annPosPx[axLetter] += alignShift;

// save the current axis type for later log/linear changes
options['_' + axLetter + 'type'] = ax && ax.type;
Expand All @@ -476,8 +486,13 @@ annotations.draw = function(gd, index, opt, value) {
// make sure the arrowhead (if there is one)
// and the annotation center are visible
if(options.showarrow) {
arrowX = Lib.constrain(annPosPx.x - options.ax, 1, fullLayout.width - 1);
arrowY = Lib.constrain(annPosPx.y - options.ay, 1, fullLayout.height - 1);
if(options.absolutetail) {
arrowX = Lib.constrain(annPosPx.x, 1, fullLayout.width - 1);
arrowY = Lib.constrain(annPosPx.y, 1, fullLayout.height - 1);
} else {
arrowX = Lib.constrain(annPosPx.x - options.ax, 1, fullLayout.width - 1);
arrowY = Lib.constrain(annPosPx.y - options.ay, 1, fullLayout.height - 1);
}
}
annPosPx.x = Lib.constrain(annPosPx.x, 1, fullLayout.width - 1);
annPosPx.y = Lib.constrain(annPosPx.y, 1, fullLayout.height - 1);
Expand All @@ -496,8 +511,15 @@ annotations.draw = function(gd, index, opt, value) {
annbg.call(Drawing.setRect, borderwidth / 2, borderwidth / 2,
outerwidth - borderwidth, outerheight - borderwidth);

var annX = Math.round(annPosPx.x - outerwidth / 2),
var annX = 0, annY = 0;
if(options.absolutetail) {
annX = Math.round(annPosPx.aax - outerwidth / 2);
annY = Math.round(annPosPx.aay - outerheight / 2);
} else {
annX = Math.round(annPosPx.x - outerwidth / 2);
annY = Math.round(annPosPx.y - outerheight / 2);
}

ann.call(Lib.setTranslate, annX, annY);

var annbase = 'annotations[' + index + ']';
Expand All @@ -515,11 +537,18 @@ annotations.draw = function(gd, index, opt, value) {
// looks like there may be a cross-browser solution, see
// http://stackoverflow.com/questions/5364980/
// how-to-get-the-width-of-an-svg-tspan-element
var arrowX0 = annPosPx.x + dx,
arrowY0 = annPosPx.y + dy,
var arrowX0, arrowY0;

if(options.absolutetail) {
arrowX0 = annPosPx.aax + dx;
arrowY0 = annPosPx.aay + dy;
} else {
arrowX0 = annPosPx.x + dx;
arrowY0 = annPosPx.y + dy;
}

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

if(options.absolutetail) {
update[annbase + '.ax'] = xa ?
(options.ax + dx / xa._m) :
((arrowX + dx - gs.l) / gs.w);
update[annbase + '.ay'] = ya ?
(options.ay + dy / ya._m) :
(1 - ((arrowY + dy - gs.t) / gs.h));
}

anng.attr({
transform: 'rotate(' + textangle + ',' +
xcenter + ',' + ycenter + ')'
Expand Down Expand Up @@ -660,8 +698,13 @@ annotations.draw = function(gd, index, opt, value) {
ann.call(Lib.setTranslate, x0 + dx, y0 + dy);
var csr = 'pointer';
if(options.showarrow) {
update[annbase + '.ax'] = options.ax + dx;
update[annbase + '.ay'] = options.ay + dy;
if(options.absolutetail) {
update[annbase + '.ax'] = xa.p2l(xa.l2p(options.ax) + dx);
update[annbase + '.ay'] = ya.p2l(ya.l2p(options.ay) + dy);
} else {
update[annbase + '.ax'] = options.ax + dx;
update[annbase + '.ay'] = options.ay + dy;
}
drawArrow(dx, dy);
}
else {
Expand Down
Binary file modified test/image/baselines/annotations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion test/image/mocks/annotations.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"bordercolor":"rgb(255, 0, 0)","borderwidth":4,"bgcolor":"rgba(255,255,0,0.5)",
"font":{"color":"rgb(0, 0, 255)","size":20},
"arrowcolor":"rgb(166, 28, 0)","borderpad":3,"textangle":50,"x":5,"y":1
}
},
{"text":"","showarrow":true,"borderwidth":1.2,"arrowhead":2,"absolutetail":true,"x":5,"y":5,"ax":4,"ay":3}
]
}
}
31 changes: 31 additions & 0 deletions test/jasmine/tests/annotations_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require('@src/plotly');
var Annotations = require('@src/components/annotations');
var Dates = require('@src/lib/dates');

describe('Test annotations', function() {
'use strict';

describe('supplyLayoutDefaults', function() {
it('should default to not use absolute arrow tail', function() {
var annotationDefaults = {};
annotationDefaults._has = function() { return false; };

Annotations.supplyLayoutDefaults({ annotations: [{ showarrow: true, arrowhead: 2}] }, annotationDefaults);

expect(annotationDefaults.annotations[0].absolutetail).toBe(false);
});

it('should convert ax/ay date coordinates to milliseconds if absolutetail is true', function() {
var annotationOut = { xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] }};
annotationOut._has = function() { return false; };

var annotationIn = {
annotations: [{ showarrow: true, absolutetail: true, x: '2008-07-01', ax: '2004-07-01', y: 0, ay: 50}]
};

Annotations.supplyLayoutDefaults(annotationIn, annotationOut);

expect(annotationIn.annotations[0].ax).toEqual(Dates.dateTime2ms('2004-07-01'));
});
});
});