-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Annotation additions: standoff, anchor with arrow, clicktoshow #1265
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
Changes from 13 commits
8518ee9
45bc0e3
d69ed98
8f0038d
83482fb
9efa9b7
9cf04b6
2533ec5
578ea87
d77ff15
13947ef
582adfd
b683903
ccbffd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -140,6 +140,17 @@ module.exports = { | |
role: 'style', | ||
description: 'Sets the width (in px) of annotation arrow.' | ||
}, | ||
standoff: { | ||
valType: 'number', | ||
min: 0, | ||
dflt: 0, | ||
role: 'style', | ||
description: [ | ||
'Sets a distance, in pixels, to move the arrowhead away from the', | ||
'position it is pointing at, for example to point at the edge of', | ||
'a marker independent of zoom.' | ||
].join(' ') | ||
}, | ||
ax: { | ||
valType: 'any', | ||
role: 'info', | ||
|
@@ -236,17 +247,17 @@ module.exports = { | |
dflt: 'auto', | ||
role: 'info', | ||
description: [ | ||
'Sets the annotation\'s horizontal position anchor', | ||
'Sets the text box\'s horizontal position anchor', | ||
'This anchor binds the `x` position to the *left*, *center*', | ||
'or *right* of the annotation.', | ||
'For example, if `x` is set to 1, `xref` to *paper* and', | ||
'`xanchor` to *right* then the right-most portion of the', | ||
'annotation lines up with the right-most edge of the', | ||
'plotting area.', | ||
'If *auto*, the anchor is equivalent to *center* for', | ||
'data-referenced annotations', | ||
'whereas for paper-referenced, the anchor picked corresponds', | ||
'to the closest side.' | ||
'data-referenced annotations or if there is an arrow,', | ||
'whereas for paper-referenced with no arrow, the anchor picked', | ||
'corresponds to the closest side.' | ||
].join(' ') | ||
}, | ||
yref: { | ||
|
@@ -286,17 +297,53 @@ module.exports = { | |
dflt: 'auto', | ||
role: 'info', | ||
description: [ | ||
'Sets the annotation\'s vertical position anchor', | ||
'Sets the text box\'s vertical position anchor', | ||
'This anchor binds the `y` position to the *top*, *middle*', | ||
'or *bottom* of the annotation.', | ||
'For example, if `y` is set to 1, `yref` to *paper* and', | ||
'`yanchor` to *top* then the top-most portion of the', | ||
'annotation lines up with the top-most edge of the', | ||
'plotting area.', | ||
'If *auto*, the anchor is equivalent to *middle* for', | ||
'data-referenced annotations', | ||
'whereas for paper-referenced, the anchor picked corresponds', | ||
'to the closest side.' | ||
'data-referenced annotations or if there is an arrow,', | ||
'whereas for paper-referenced with no arrow, the anchor picked', | ||
'corresponds to the closest side.' | ||
].join(' ') | ||
}, | ||
clicktoshow: { | ||
valType: 'enumerated', | ||
values: [false, 'onoff', 'onout'], | ||
dflt: false, | ||
role: 'style', | ||
description: [ | ||
'Makes this annotation respond to clicks on the plot.', | ||
'If you click a data point that exactly matches the `x` and `y`', | ||
'values of this annotation, and it is hidden (visible: false),', | ||
'it will appear. In *onoff* mode, you must click the same point', | ||
'again to make it disappear, so if you click multiple points,', | ||
'you can show multiple annotations. In *onout* mode, a click', | ||
'anywhere else in the plot (on another data point or not) will', | ||
'hide this annotation.', | ||
'If you need to show/hide this annotation in response to different', | ||
'`x` or `y` values, you can set `xclick` and/or `yclick`. This is', | ||
'useful for example to label the side of a bar. To label markers', | ||
'though, `standoff` is preferred over `xclick` and `yclick`.' | ||
].join(' ') | ||
}, | ||
xclick: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That said, I'm a little afraid about how these attribute will scale when we add data-referenced to other plot types. I'll elaborate on those concerns in more details in #751 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
valType: 'any', | ||
role: 'info', | ||
description: [ | ||
'Toggle this annotation when clicking a data point whose `x` value', | ||
'is `xclick` rather than the annotation\'s `x` value.' | ||
].join(' ') | ||
}, | ||
yclick: { | ||
valType: 'any', | ||
role: 'info', | ||
description: [ | ||
'Toggle this annotation when clicking a data point whose `y` value', | ||
'is `yclick` rather than the annotation\'s `y` value.' | ||
].join(' ') | ||
}, | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,41 +45,49 @@ function annAutorange(gd) { | |
// relative to their anchor points | ||
// use the arrow and the text bg rectangle, | ||
// as the whole anno may include hidden text in its bbox | ||
fullLayout.annotations.forEach(function(ann) { | ||
Lib.filterVisible(fullLayout.annotations).forEach(function(ann) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
var xa = Axes.getFromId(gd, ann.xref), | ||
ya = Axes.getFromId(gd, ann.yref); | ||
|
||
if(!(xa || ya)) return; | ||
|
||
var halfWidth = (ann._xsize || 0) / 2, | ||
xShift = ann._xshift || 0, | ||
halfHeight = (ann._ysize || 0) / 2, | ||
yShift = ann._yshift || 0, | ||
leftSize = halfWidth - xShift, | ||
rightSize = halfWidth + xShift, | ||
topSize = halfHeight - yShift, | ||
bottomSize = halfHeight + yShift; | ||
|
||
if(ann.showarrow) { | ||
var headSize = 3 * ann.arrowsize * ann.arrowwidth; | ||
leftSize = Math.max(leftSize, headSize); | ||
rightSize = Math.max(rightSize, headSize); | ||
topSize = Math.max(topSize, headSize); | ||
bottomSize = Math.max(bottomSize, headSize); | ||
} | ||
ya = Axes.getFromId(gd, ann.yref), | ||
headSize = 3 * ann.arrowsize * ann.arrowwidth || 0; | ||
|
||
if(xa && xa.autorange) { | ||
Axes.expand(xa, [xa.r2c(ann.x)], { | ||
ppadplus: rightSize, | ||
ppadminus: leftSize | ||
}); | ||
if(ann.axref === ann.xref) { | ||
// expand for the arrowhead (padded by arrowhead) | ||
Axes.expand(xa, [xa.r2c(ann.x)], { | ||
ppadplus: headSize, | ||
ppadminus: headSize | ||
}); | ||
// again for the textbox (padded by textbox) | ||
Axes.expand(xa, [xa.r2c(ann.ax)], { | ||
ppadplus: ann._xpadplus, | ||
ppadminus: ann._xpadminus | ||
}); | ||
} | ||
else { | ||
Axes.expand(xa, [xa.r2c(ann.x)], { | ||
ppadplus: Math.max(ann._xpadplus, headSize), | ||
ppadminus: Math.max(ann._xpadminus, headSize) | ||
}); | ||
} | ||
} | ||
|
||
if(ya && ya.autorange) { | ||
Axes.expand(ya, [ya.r2c(ann.y)], { | ||
ppadplus: bottomSize, | ||
ppadminus: topSize | ||
}); | ||
if(ann.ayref === ann.yref) { | ||
Axes.expand(ya, [ya.r2c(ann.y)], { | ||
ppadplus: headSize, | ||
ppadminus: headSize | ||
}); | ||
Axes.expand(ya, [ya.r2c(ann.ay)], { | ||
ppadplus: ann._ypadplus, | ||
ppadminus: ann._ypadminus | ||
}); | ||
} | ||
else { | ||
Axes.expand(ya, [ya.r2c(ann.y)], { | ||
ppadplus: Math.max(ann._ypadplus, headSize), | ||
ppadminus: Math.max(ann._ypadminus, headSize) | ||
}); | ||
} | ||
} | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/** | ||
* Copyright 2012-2017, Plotly, Inc. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
|
||
'use strict'; | ||
|
||
var Plotly = require('../../plotly'); | ||
|
||
|
||
module.exports = { | ||
hasClickToShow: hasClickToShow, | ||
onClick: onClick | ||
}; | ||
|
||
/* | ||
* hasClickToShow: does the given hoverData have ANY annotations which will | ||
* turn ON if we click here? (used by hover events to set cursor) | ||
* | ||
* gd: graphDiv | ||
* hoverData: a hoverData array, as included with the *plotly_hover* or | ||
* *plotly_click* events in the `points` attribute | ||
* | ||
* returns: boolean | ||
*/ | ||
function hasClickToShow(gd, hoverData) { | ||
var sets = getToggleSets(gd, hoverData); | ||
return sets.on.length > 0 || sets.explicitOff.length > 0; | ||
} | ||
|
||
/* | ||
* onClick: perform the toggling (via Plotly.update) implied by clicking | ||
* at this hoverData | ||
* | ||
* gd: graphDiv | ||
* hoverData: a hoverData array, as included with the *plotly_hover* or | ||
* *plotly_click* events in the `points` attribute | ||
* | ||
* returns: Promise that the update is complete | ||
*/ | ||
function onClick(gd, hoverData) { | ||
var toggleSets = getToggleSets(gd, hoverData), | ||
onSet = toggleSets.on, | ||
offSet = toggleSets.off.concat(toggleSets.explicitOff), | ||
update = {}, | ||
i; | ||
|
||
if(!(onSet.length || offSet.length)) return; | ||
|
||
for(i = 0; i < onSet.length; i++) { | ||
update['annotations[' + onSet[i] + '].visible'] = true; | ||
} | ||
|
||
for(i = 0; i < offSet.length; i++) { | ||
update['annotations[' + offSet[i] + '].visible'] = false; | ||
} | ||
|
||
return Plotly.update(gd, {}, update); | ||
} | ||
|
||
/* | ||
* getToggleSets: find the annotations which will turn on or off at this | ||
* hoverData | ||
* | ||
* gd: graphDiv | ||
* hoverData: a hoverData array, as included with the *plotly_hover* or | ||
* *plotly_click* events in the `points` attribute | ||
* | ||
* returns: { | ||
* on: Array (indices of annotations to turn on), | ||
* off: Array (indices to turn off because you're not hovering on them), | ||
* explicitOff: Array (indices to turn off because you *are* hovering on them) | ||
* } | ||
*/ | ||
function getToggleSets(gd, hoverData) { | ||
var annotations = gd._fullLayout.annotations, | ||
onSet = [], | ||
offSet = [], | ||
explicitOffSet = [], | ||
hoverLen = (hoverData || []).length; | ||
|
||
var i, j, anni, showMode, pointj, toggleType; | ||
|
||
for(i = 0; i < annotations.length; i++) { | ||
anni = annotations[i]; | ||
showMode = anni.clicktoshow; | ||
if(showMode) { | ||
for(j = 0; j < hoverLen; j++) { | ||
pointj = hoverData[j]; | ||
if(pointj.x === anni._xclick && pointj.y === anni._yclick && | ||
pointj.xaxis._id === anni.xref && | ||
pointj.yaxis._id === anni.yref) { | ||
// match! toggle this annotation | ||
// regardless of its clicktoshow mode | ||
// but if it's onout mode, off is implicit | ||
if(anni.visible) { | ||
if(showMode === 'onout') toggleType = offSet; | ||
else toggleType = explicitOffSet; | ||
} | ||
else { | ||
toggleType = onSet; | ||
} | ||
toggleType.push(i); | ||
break; | ||
} | ||
} | ||
|
||
if(j === hoverLen) { | ||
// no match - only turn this annotation OFF, and only if | ||
// showmode is 'onout' | ||
if(anni.visible && showMode === 'onout') offSet.push(i); | ||
} | ||
} | ||
} | ||
|
||
return {on: onSet, off: offSet, explicitOff: explicitOffSet}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 for
clicktoshow
Thanks for that very clear description.