Skip to content

Commit 4f00c99

Browse files
authored
Merge pull request #1265 from plotly/note-align-standoff
Annotation additions: standoff, anchor with arrow, clicktoshow
2 parents 2ae4c6d + ccbffd0 commit 4f00c99

17 files changed

+720
-232
lines changed

src/components/annotations/annotation_defaults.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
2525
}
2626

2727
var visible = coerce('visible', !itemOpts.itemIsNotPlainObject);
28+
var clickToShow = coerce('clicktoshow');
2829

29-
if(!visible) return annOut;
30+
if(!(visible || clickToShow)) return annOut;
3031

3132
coerce('opacity');
3233
coerce('align');
@@ -75,7 +76,7 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
7576
}
7677

7778
// xanchor, yanchor
78-
else coerce(axLetter + 'anchor');
79+
coerce(axLetter + 'anchor');
7980
}
8081

8182
// if you have one coordinate you should have both
@@ -86,10 +87,21 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
8687
coerce('arrowhead');
8788
coerce('arrowsize');
8889
coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2);
90+
coerce('standoff');
8991

9092
// if you have one part of arrow length you should have both
9193
Lib.noneOrAll(annIn, annOut, ['ax', 'ay']);
9294
}
9395

96+
if(clickToShow) {
97+
var xClick = coerce('xclick');
98+
var yClick = coerce('yclick');
99+
100+
// put the actual click data to bind to into private attributes
101+
// so we don't have to do this little bit of logic on every hover event
102+
annOut._xclick = (xClick === undefined) ? annOut.x : xClick;
103+
annOut._yclick = (yClick === undefined) ? annOut.y : yClick;
104+
}
105+
94106
return annOut;
95107
};

src/components/annotations/arrow_paths.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121

2222
module.exports = [
2323
// no arrow
24-
'',
24+
{
25+
path: '',
26+
backoff: 0
27+
},
2528
// wide with flat back
2629
{
2730
path: 'M-2.4,-3V3L0.6,0Z',

src/components/annotations/attributes.js

+55-8
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ module.exports = {
140140
role: 'style',
141141
description: 'Sets the width (in px) of annotation arrow.'
142142
},
143+
standoff: {
144+
valType: 'number',
145+
min: 0,
146+
dflt: 0,
147+
role: 'style',
148+
description: [
149+
'Sets a distance, in pixels, to move the arrowhead away from the',
150+
'position it is pointing at, for example to point at the edge of',
151+
'a marker independent of zoom.'
152+
].join(' ')
153+
},
143154
ax: {
144155
valType: 'any',
145156
role: 'info',
@@ -236,17 +247,17 @@ module.exports = {
236247
dflt: 'auto',
237248
role: 'info',
238249
description: [
239-
'Sets the annotation\'s horizontal position anchor',
250+
'Sets the text box\'s horizontal position anchor',
240251
'This anchor binds the `x` position to the *left*, *center*',
241252
'or *right* of the annotation.',
242253
'For example, if `x` is set to 1, `xref` to *paper* and',
243254
'`xanchor` to *right* then the right-most portion of the',
244255
'annotation lines up with the right-most edge of the',
245256
'plotting area.',
246257
'If *auto*, the anchor is equivalent to *center* for',
247-
'data-referenced annotations',
248-
'whereas for paper-referenced, the anchor picked corresponds',
249-
'to the closest side.'
258+
'data-referenced annotations or if there is an arrow,',
259+
'whereas for paper-referenced with no arrow, the anchor picked',
260+
'corresponds to the closest side.'
250261
].join(' ')
251262
},
252263
yref: {
@@ -286,17 +297,53 @@ module.exports = {
286297
dflt: 'auto',
287298
role: 'info',
288299
description: [
289-
'Sets the annotation\'s vertical position anchor',
300+
'Sets the text box\'s vertical position anchor',
290301
'This anchor binds the `y` position to the *top*, *middle*',
291302
'or *bottom* of the annotation.',
292303
'For example, if `y` is set to 1, `yref` to *paper* and',
293304
'`yanchor` to *top* then the top-most portion of the',
294305
'annotation lines up with the top-most edge of the',
295306
'plotting area.',
296307
'If *auto*, the anchor is equivalent to *middle* for',
297-
'data-referenced annotations',
298-
'whereas for paper-referenced, the anchor picked corresponds',
299-
'to the closest side.'
308+
'data-referenced annotations or if there is an arrow,',
309+
'whereas for paper-referenced with no arrow, the anchor picked',
310+
'corresponds to the closest side.'
311+
].join(' ')
312+
},
313+
clicktoshow: {
314+
valType: 'enumerated',
315+
values: [false, 'onoff', 'onout'],
316+
dflt: false,
317+
role: 'style',
318+
description: [
319+
'Makes this annotation respond to clicks on the plot.',
320+
'If you click a data point that exactly matches the `x` and `y`',
321+
'values of this annotation, and it is hidden (visible: false),',
322+
'it will appear. In *onoff* mode, you must click the same point',
323+
'again to make it disappear, so if you click multiple points,',
324+
'you can show multiple annotations. In *onout* mode, a click',
325+
'anywhere else in the plot (on another data point or not) will',
326+
'hide this annotation.',
327+
'If you need to show/hide this annotation in response to different',
328+
'`x` or `y` values, you can set `xclick` and/or `yclick`. This is',
329+
'useful for example to label the side of a bar. To label markers',
330+
'though, `standoff` is preferred over `xclick` and `yclick`.'
331+
].join(' ')
332+
},
333+
xclick: {
334+
valType: 'any',
335+
role: 'info',
336+
description: [
337+
'Toggle this annotation when clicking a data point whose `x` value',
338+
'is `xclick` rather than the annotation\'s `x` value.'
339+
].join(' ')
340+
},
341+
yclick: {
342+
valType: 'any',
343+
role: 'info',
344+
description: [
345+
'Toggle this annotation when clicking a data point whose `y` value',
346+
'is `yclick` rather than the annotation\'s `y` value.'
300347
].join(' ')
301348
},
302349

src/components/annotations/calc_autorange.js

+37-29
Original file line numberDiff line numberDiff line change
@@ -45,41 +45,49 @@ function annAutorange(gd) {
4545
// relative to their anchor points
4646
// use the arrow and the text bg rectangle,
4747
// as the whole anno may include hidden text in its bbox
48-
fullLayout.annotations.forEach(function(ann) {
48+
Lib.filterVisible(fullLayout.annotations).forEach(function(ann) {
4949
var xa = Axes.getFromId(gd, ann.xref),
50-
ya = Axes.getFromId(gd, ann.yref);
51-
52-
if(!(xa || ya)) return;
53-
54-
var halfWidth = (ann._xsize || 0) / 2,
55-
xShift = ann._xshift || 0,
56-
halfHeight = (ann._ysize || 0) / 2,
57-
yShift = ann._yshift || 0,
58-
leftSize = halfWidth - xShift,
59-
rightSize = halfWidth + xShift,
60-
topSize = halfHeight - yShift,
61-
bottomSize = halfHeight + yShift;
62-
63-
if(ann.showarrow) {
64-
var headSize = 3 * ann.arrowsize * ann.arrowwidth;
65-
leftSize = Math.max(leftSize, headSize);
66-
rightSize = Math.max(rightSize, headSize);
67-
topSize = Math.max(topSize, headSize);
68-
bottomSize = Math.max(bottomSize, headSize);
69-
}
50+
ya = Axes.getFromId(gd, ann.yref),
51+
headSize = 3 * ann.arrowsize * ann.arrowwidth || 0;
7052

7153
if(xa && xa.autorange) {
72-
Axes.expand(xa, [xa.r2c(ann.x)], {
73-
ppadplus: rightSize,
74-
ppadminus: leftSize
75-
});
54+
if(ann.axref === ann.xref) {
55+
// expand for the arrowhead (padded by arrowhead)
56+
Axes.expand(xa, [xa.r2c(ann.x)], {
57+
ppadplus: headSize,
58+
ppadminus: headSize
59+
});
60+
// again for the textbox (padded by textbox)
61+
Axes.expand(xa, [xa.r2c(ann.ax)], {
62+
ppadplus: ann._xpadplus,
63+
ppadminus: ann._xpadminus
64+
});
65+
}
66+
else {
67+
Axes.expand(xa, [xa.r2c(ann.x)], {
68+
ppadplus: Math.max(ann._xpadplus, headSize),
69+
ppadminus: Math.max(ann._xpadminus, headSize)
70+
});
71+
}
7672
}
7773

7874
if(ya && ya.autorange) {
79-
Axes.expand(ya, [ya.r2c(ann.y)], {
80-
ppadplus: bottomSize,
81-
ppadminus: topSize
82-
});
75+
if(ann.ayref === ann.yref) {
76+
Axes.expand(ya, [ya.r2c(ann.y)], {
77+
ppadplus: headSize,
78+
ppadminus: headSize
79+
});
80+
Axes.expand(ya, [ya.r2c(ann.ay)], {
81+
ppadplus: ann._ypadplus,
82+
ppadminus: ann._ypadminus
83+
});
84+
}
85+
else {
86+
Axes.expand(ya, [ya.r2c(ann.y)], {
87+
ppadplus: Math.max(ann._ypadplus, headSize),
88+
ppadminus: Math.max(ann._ypadminus, headSize)
89+
});
90+
}
8391
}
8492
});
8593
}

src/components/annotations/click.js

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
var Plotly = require('../../plotly');
13+
14+
15+
module.exports = {
16+
hasClickToShow: hasClickToShow,
17+
onClick: onClick
18+
};
19+
20+
/*
21+
* hasClickToShow: does the given hoverData have ANY annotations which will
22+
* turn ON if we click here? (used by hover events to set cursor)
23+
*
24+
* gd: graphDiv
25+
* hoverData: a hoverData array, as included with the *plotly_hover* or
26+
* *plotly_click* events in the `points` attribute
27+
*
28+
* returns: boolean
29+
*/
30+
function hasClickToShow(gd, hoverData) {
31+
var sets = getToggleSets(gd, hoverData);
32+
return sets.on.length > 0 || sets.explicitOff.length > 0;
33+
}
34+
35+
/*
36+
* onClick: perform the toggling (via Plotly.update) implied by clicking
37+
* at this hoverData
38+
*
39+
* gd: graphDiv
40+
* hoverData: a hoverData array, as included with the *plotly_hover* or
41+
* *plotly_click* events in the `points` attribute
42+
*
43+
* returns: Promise that the update is complete
44+
*/
45+
function onClick(gd, hoverData) {
46+
var toggleSets = getToggleSets(gd, hoverData),
47+
onSet = toggleSets.on,
48+
offSet = toggleSets.off.concat(toggleSets.explicitOff),
49+
update = {},
50+
i;
51+
52+
if(!(onSet.length || offSet.length)) return;
53+
54+
for(i = 0; i < onSet.length; i++) {
55+
update['annotations[' + onSet[i] + '].visible'] = true;
56+
}
57+
58+
for(i = 0; i < offSet.length; i++) {
59+
update['annotations[' + offSet[i] + '].visible'] = false;
60+
}
61+
62+
return Plotly.update(gd, {}, update);
63+
}
64+
65+
/*
66+
* getToggleSets: find the annotations which will turn on or off at this
67+
* hoverData
68+
*
69+
* gd: graphDiv
70+
* hoverData: a hoverData array, as included with the *plotly_hover* or
71+
* *plotly_click* events in the `points` attribute
72+
*
73+
* returns: {
74+
* on: Array (indices of annotations to turn on),
75+
* off: Array (indices to turn off because you're not hovering on them),
76+
* explicitOff: Array (indices to turn off because you *are* hovering on them)
77+
* }
78+
*/
79+
function getToggleSets(gd, hoverData) {
80+
var annotations = gd._fullLayout.annotations,
81+
onSet = [],
82+
offSet = [],
83+
explicitOffSet = [],
84+
hoverLen = (hoverData || []).length;
85+
86+
var i, j, anni, showMode, pointj, toggleType;
87+
88+
for(i = 0; i < annotations.length; i++) {
89+
anni = annotations[i];
90+
showMode = anni.clicktoshow;
91+
if(showMode) {
92+
for(j = 0; j < hoverLen; j++) {
93+
pointj = hoverData[j];
94+
if(pointj.x === anni._xclick && pointj.y === anni._yclick &&
95+
pointj.xaxis._id === anni.xref &&
96+
pointj.yaxis._id === anni.yref) {
97+
// match! toggle this annotation
98+
// regardless of its clicktoshow mode
99+
// but if it's onout mode, off is implicit
100+
if(anni.visible) {
101+
if(showMode === 'onout') toggleType = offSet;
102+
else toggleType = explicitOffSet;
103+
}
104+
else {
105+
toggleType = onSet;
106+
}
107+
toggleType.push(i);
108+
break;
109+
}
110+
}
111+
112+
if(j === hoverLen) {
113+
// no match - only turn this annotation OFF, and only if
114+
// showmode is 'onout'
115+
if(anni.visible && showMode === 'onout') offSet.push(i);
116+
}
117+
}
118+
}
119+
120+
return {on: onSet, off: offSet, explicitOff: explicitOffSet};
121+
}

0 commit comments

Comments
 (0)