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 all 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
46 changes: 42 additions & 4 deletions src/components/annotations/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,10 @@ module.exports = {
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 `axref` is `pixel`, a positive (negative) ',
'component corresponds to an arrow pointing',
'from right to left (left to right).',
'If `axref` is an axis, this is a value on that axis.'
].join(' ')
},
ay: {
Expand All @@ -147,8 +149,44 @@ 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 `ayref` is `pixel`, a positive (negative) ',
'component corresponds to an arrow pointing',
'from bottom to top (top to bottom).',
'If `ayref` is an axis, this is a value on that axis.'
].join(' ')
},
axref: {
valType: 'enumerated',
dflt: 'pixel',
values: [
'pixel',
cartesianConstants.idRegex.x.toString()
],
role: 'info',
description: [
'Indicates in what terms the tail of the annotation (ax,ay) ',
'is specified. If `pixel`, `ax` is a relative offset in pixels ',
'from `x`. If set to an x axis id (e.g. *x* or *x2*), `ax` is ',
'specified in the same terms as that axis. This is useful ',
'for trendline annotations which should continue to indicate ',
'the correct trend when zoomed.'
].join(' ')
},
ayref: {
valType: 'enumerated',
dflt: 'pixel',
values: [
'pixel',
cartesianConstants.idRegex.y.toString()
],
role: 'info',
description: [
'Indicates in what terms the tail of the annotation (ax,ay) ',
'is specified. If `pixel`, `ay` is a relative offset in pixels ',
'from `y`. If set to a y axis id (e.g. *y* or *y2*), `ay` is ',
'specified in the same terms as that axis. This is useful ',
'for trendline annotations which should continue to indicate ',
'the correct trend when zoomed.'
].join(' ')
},
// positioning
Expand Down
116 changes: 98 additions & 18 deletions src/components/annotations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ function handleAnnotationDefaults(annIn, fullLayout) {
coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2);
coerce('ax');
coerce('ay');
coerce('axref');
coerce('ayref');

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

//todo: should be refactored in conjunction with Axes
// axref, ayref
var aaxRef = Axes.coerceARef(annIn, annOut, tdMock, axLetter);

// x, y
var defaultPosition = 0.5;
if(axRef !== 'paper') {
Expand All @@ -89,6 +95,11 @@ function handleAnnotationDefaults(annIn, fullLayout) {
if(ax.type === 'date') {
newval = Lib.dateTime2ms(annIn[axLetter]);
if(newval !== false) annIn[axLetter] = newval;

if(aaxRef === axRef) {
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 @@ -419,8 +430,8 @@ annotations.draw = function(gd, index, opt, value) {

var annotationIsOffscreen = false;
['x', 'y'].forEach(function(axLetter) {
var ax = Axes.getFromId(gd,
options[axLetter + 'ref'] || axLetter),
var axRef = options[axLetter + 'ref'] || axLetter,
ax = Axes.getFromId(gd, axRef),
dimAngle = (textangle + (axLetter === 'x' ? 0 : 90)) * Math.PI / 180,
annSize = outerwidth * Math.abs(Math.cos(dimAngle)) +
outerheight * Math.abs(Math.sin(dimAngle)),
Expand All @@ -435,8 +446,16 @@ annotations.draw = function(gd, index, opt, value) {
// anyway to get its bounding box)
if(!ax.autorange && ((options[axLetter] - ax.range[0]) *
(options[axLetter] - ax.range[1]) > 0)) {
annotationIsOffscreen = true;
return;
if(options['a' + axLetter + 'ref'] === axRef) {
if((options['a' + axLetter] - ax.range[0]) *
(options['a' + axLetter] - ax.range[1]) > 0) {
annotationIsOffscreen = true;
}
} else {
annotationIsOffscreen = true;
}

if(annotationIsOffscreen) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm. Something isn't quite right still:

gifrecord_2016-06-17_170451

The arrow tip gets pinned to the graph's right edge.

Copy link
Author

Choose a reason for hiding this comment

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

@etpinard sigh...its happening because its coming up against the edge of the svg-container. In my testing, that didn't happen before the tail exited the chart so i didn't see it. I'm sorry about that. I believe that I've fixed it and I've added an off screen to the test to ensure it stays that way.

}
annPosPx[axLetter] = ax._offset + ax.l2p(options[axLetter]);
alignPosition = 0.5;
Expand All @@ -450,13 +469,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['a' + axLetter + 'ref'] === axRef) {
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 +499,21 @@ 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.axref === options.xref) {
//we don't want to constrain if the tail is absolute
//or the slope (which is meaningful) will change.
arrowX = annPosPx.x;
} else {
arrowX = Lib.constrain(annPosPx.x - options.ax, 1, fullLayout.width - 1);
}

if(options.ayref === options.yref) {
//we don't want to constrain if the tail is absolute
//or the slope (which is meaningful) will change.
arrowY = annPosPx.y;
Copy link
Contributor

Choose a reason for hiding this comment

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

Beautiful.

} else {
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 +532,19 @@ 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.axref === options.xref) {
annX = Math.round(annPosPx.aax - outerwidth / 2);
} else {
annX = Math.round(annPosPx.x - outerwidth / 2);
}

if(options.ayref === options.yref) {
annY = Math.round(annPosPx.aay - outerheight / 2);
} else {
annY = Math.round(annPosPx.y - outerheight / 2);
}

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

var annbase = 'annotations[' + index + ']';
Expand All @@ -515,11 +562,22 @@ 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.axref === options.xref) {
arrowX0 = annPosPx.aax + dx;
} else {
arrowX0 = annPosPx.x + dx;
}

if(options.ayref === options.yref) {
arrowY0 = annPosPx.aay + dy;
} else {
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 +676,18 @@ annotations.draw = function(gd, index, opt, value) {
(options.y + dy / ya._m) :
(1 - ((arrowY + dy - gs.t) / gs.h));

if(options.axref === options.xref) {
update[annbase + '.ax'] = xa ?
(options.ax + dx / xa._m) :
((arrowX + dx - gs.l) / gs.w);
}

if(options.ayref === options.yref) {
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 +730,18 @@ 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.axref === options.xref) {
update[annbase + '.ax'] = xa.p2l(xa.l2p(options.ax) + dx);
} else {
update[annbase + '.ax'] = options.ax + dx;
}

if(options.ayref === options.yref) {
update[annbase + '.ay'] = ya.p2l(ya.l2p(options.ay) + dy);
} else {
update[annbase + '.ay'] = options.ay + dy;
}

drawArrow(dx, dy);
}
else {
Expand Down
20 changes: 20 additions & 0 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ axes.coerceRef = function(containerIn, containerOut, gd, axLetter, dflt) {
return Lib.coerce(containerIn, containerOut, attrDef, refAttr);
};

//todo: duplicated per github PR 610. Should be consolidated with axes.coerceRef.
// find the list of possible axes to reference with an axref or ayref attribute
// and coerce it to that list
axes.coerceARef = function(containerIn, containerOut, gd, axLetter, dflt) {
var axlist = gd._fullLayout._has('gl2d') ? [] : axes.listIds(gd, axLetter),
refAttr = 'a' + axLetter + 'ref',
Copy link
Contributor

Choose a reason for hiding this comment

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

nicely done.

attrDef = {};

// data-ref annotations are not supported in gl2d yet

attrDef[refAttr] = {
valType: 'enumerated',
values: axlist.concat(['pixel']),
dflt: dflt || 'pixel' || axlist[0]
};

// axref, ayref
return Lib.coerce(containerIn, containerOut, attrDef, refAttr);
};

// empty out types for all axes containing these traces
// so we auto-set them again
axes.clearTypes = function(gd, traces) {
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.
4 changes: 3 additions & 1 deletion test/image/mocks/annotations.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
"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,"axref":"x","ayref":"y","x":5,"y":3,"ax":4,"ay":5},
{"text":"","showarrow":true,"borderwidth":1.2,"arrowhead":2,"axref":"x","ayref":"y","x":6,"y":2,"ax":3,"ay":3}
]
}
}
33 changes: 33 additions & 0 deletions test/jasmine/tests/annotations_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require('@src/plotly');
var Plots = require('@src/plots/plots');
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 pixel for axref/ayref', function() {
var annotationDefaults = {};
annotationDefaults._has = Plots._hasPlotType.bind(annotationDefaults);

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

expect(annotationDefaults.annotations[0].axref).toEqual('pixel');
expect(annotationDefaults.annotations[0].ayref).toEqual('pixel');
});

it('should convert ax/ay date coordinates to milliseconds if tail is in axis terms and axis is a date', function() {
var annotationOut = { xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] }};
annotationOut._has = Plots._hasPlotType.bind(annotationOut);

var annotationIn = {
annotations: [{ showarrow: true, axref: 'x', ayref: 'y', 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'));
});
});
});