Skip to content

Commit 9cf04b6

Browse files
committed
add clicktoshow for annotations
1 parent 9efa9b7 commit 9cf04b6

File tree

7 files changed

+365
-4
lines changed

7 files changed

+365
-4
lines changed

src/components/annotations/annotation_defaults.js

Lines changed: 12 additions & 1 deletion
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');
@@ -92,5 +93,15 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
9293
Lib.noneOrAll(annIn, annOut, ['ax', 'ay']);
9394
}
9495

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+
95106
return annOut;
96107
};

src/components/annotations/attributes.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,42 @@ module.exports = {
310310
'corresponds to the closest side.'
311311
].join(' ')
312312
},
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.'
347+
].join(' ')
348+
},
313349

314350
_deprecated: {
315351
ref: {

src/components/annotations/click.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Copyright 2012-2016, 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+
}

src/components/annotations/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
'use strict';
1111

1212
var drawModule = require('./draw');
13+
var clickModule = require('./click');
1314

1415
module.exports = {
1516
moduleType: 'component',
@@ -20,5 +21,8 @@ module.exports = {
2021

2122
calcAutorange: require('./calc_autorange'),
2223
draw: drawModule.draw,
23-
drawOne: drawModule.drawOne
24+
drawOne: drawModule.drawOne,
25+
26+
hasClickToShow: clickModule.hasClickToShow,
27+
onClick: clickModule.onClick
2428
};

src/lib/override_cursor.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Copyright 2012-2016, 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 setCursor = require('./setCursor');
13+
14+
var STASHATTR = 'data-savedcursor';
15+
16+
/*
17+
* works with our CSS cursor classes (see css/_cursor.scss)
18+
* to override a previous cursor set on d3 single-element selections,
19+
* by moving the name of the original cursor to the data-savedcursor attr.
20+
* omit cursor to revert to the previously set value.
21+
*/
22+
module.exports = function overrideCursor(el3, csr) {
23+
var savedCursor = el3.attr(STASHATTR);
24+
if(csr) {
25+
if(savedCursor) {
26+
setCursor(el3, csr);
27+
}
28+
else {
29+
var classes = (el3.attr('class') || '').split(' ');
30+
for(var i = 0; i < classes.length; i++) {
31+
var cls = classes[i];
32+
if(cls.indexOf('cursor-') === 0) {
33+
el3.attr(STASHATTR, cls.substr(7))
34+
.classed(cls, false);
35+
}
36+
}
37+
}
38+
}
39+
else if(savedCursor) {
40+
el3.attr(STASHATTR, null);
41+
setCursor(el3, savedCursor);
42+
}
43+
};

src/plots/cartesian/graph_interact.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ var svgTextUtils = require('../../lib/svg_text_utils');
1919
var Color = require('../../components/color');
2020
var Drawing = require('../../components/drawing');
2121
var dragElement = require('../../components/dragelement');
22+
var overrideCursor = require('../../lib/override_cursor');
23+
var Registry = require('../../registry');
2224

2325
var Axes = require('./axes');
2426
var constants = require('./constants');
@@ -596,6 +598,13 @@ function hover(gd, evt, subplot) {
596598

597599
gd._hoverdata = newhoverdata;
598600

601+
// TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
602+
// we should improve the "fx" API so other plots can use it without these hack.
603+
if(evt.target && evt.target.tagName) {
604+
var hasClickToShow = Registry.getComponentMethod('annotations', 'hasClickToShow')(gd, newhoverdata);
605+
overrideCursor(d3.select(evt.target), hasClickToShow ? 'pointer' : '');
606+
}
607+
599608
if(!hoverChanged(gd, evt, oldhoverdata)) return;
600609

601610
if(oldhoverdata) {
@@ -1323,8 +1332,16 @@ function hoverChanged(gd, evt, oldhoverdata) {
13231332

13241333
// on click
13251334
fx.click = function(gd, evt) {
1335+
var annotationsDone = Registry.getComponentMethod('annotations', 'onClick')(gd, gd._hoverdata);
1336+
1337+
function emitClick() { gd.emit('plotly_click', {points: gd._hoverdata}); }
1338+
13261339
if(gd._hoverdata && evt && evt.target) {
1327-
gd.emit('plotly_click', {points: gd._hoverdata});
1340+
if(annotationsDone && annotationsDone.then) {
1341+
annotationsDone.then(emitClick);
1342+
}
1343+
else emitClick();
1344+
13281345
// why do we get a double event without this???
13291346
if(evt.stopImmediatePropagation) evt.stopImmediatePropagation();
13301347
}

0 commit comments

Comments
 (0)