Skip to content
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

Annotation additions: standoff, anchor with arrow, clicktoshow #1265

Merged
merged 14 commits into from
Jan 18, 2017
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/components/annotations/annotation_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
}

var visible = coerce('visible', !itemOpts.itemIsNotPlainObject);
var clickToShow = coerce('clicktoshow');

if(!visible) return annOut;
if(!(visible || clickToShow)) return annOut;

coerce('opacity');
coerce('align');
Expand Down Expand Up @@ -75,7 +76,7 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
}

// xanchor, yanchor
else coerce(axLetter + 'anchor');
coerce(axLetter + 'anchor');
}

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

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

if(clickToShow) {
var xClick = coerce('xclick');
var yClick = coerce('yclick');

// put the actual click data to bind to into private attributes
// so we don't have to do this little bit of logic on every hover event
annOut._xclick = (xClick === undefined) ? annOut.x : xClick;
annOut._yclick = (yClick === undefined) ? annOut.y : yClick;
}

return annOut;
};
5 changes: 4 additions & 1 deletion src/components/annotations/arrow_paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@

module.exports = [
// no arrow
'',
{
path: '',
backoff: 0
},
// wide with flat back
{
path: 'M-2.4,-3V3L0.6,0Z',
Expand Down
63 changes: 55 additions & 8 deletions src/components/annotations/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Copy link
Contributor

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.

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: {
Copy link
Contributor

Choose a reason for hiding this comment

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

xclick and yclick makes sense in the current state of annotations.

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

Copy link
Contributor

Choose a reason for hiding this comment

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

gifrecord_2017-01-16_172046

fun 🎉

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(' ')
},

Expand Down
66 changes: 37 additions & 29 deletions src/components/annotations/calc_autorange.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Lib.filterVisible was unnecessary before because we didn't coerce any other attributes when visible was false, so these annotations wouldn't look like they're on axes. But now I need the position and reference attributes even when annotations aren't visible in order to determine what to show on clicks, hence this filter is needed too.

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)
});
}
}
});
}
121 changes: 121 additions & 0 deletions src/components/annotations/click.js
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};
}