Skip to content

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

Merged
merged 14 commits into from
Jan 18, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
13 changes: 12 additions & 1 deletion 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 @@ -92,5 +93,15 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
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;
};
36 changes: 36 additions & 0 deletions src/components/annotations/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,42 @@ module.exports = {
'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(' ')
},

_deprecated: {
ref: {
Expand Down
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-2016, 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};
}
6 changes: 5 additions & 1 deletion src/components/annotations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
'use strict';

var drawModule = require('./draw');
var clickModule = require('./click');

module.exports = {
moduleType: 'component',
Expand All @@ -20,5 +21,8 @@ module.exports = {

calcAutorange: require('./calc_autorange'),
draw: drawModule.draw,
drawOne: drawModule.drawOne
drawOne: drawModule.drawOne,

hasClickToShow: clickModule.hasClickToShow,
onClick: clickModule.onClick
};
43 changes: 43 additions & 0 deletions src/lib/override_cursor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Copyright 2012-2016, 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 setCursor = require('./setCursor');

var STASHATTR = 'data-savedcursor';

/*
* works with our CSS cursor classes (see css/_cursor.scss)
* to override a previous cursor set on d3 single-element selections,
* by moving the name of the original cursor to the data-savedcursor attr.
* omit cursor to revert to the previously set value.
*/
module.exports = function overrideCursor(el3, csr) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to a few test cases for overrideCursor similar to the ones for setCursor here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

haha good call - overrideCursor was actually broken, though somehow it looked like it was working. Fixed & tested in ccbffd0

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks 🎉

var savedCursor = el3.attr(STASHATTR);
if(csr) {
if(savedCursor) {
setCursor(el3, csr);
}
else {
var classes = (el3.attr('class') || '').split(' ');
for(var i = 0; i < classes.length; i++) {
var cls = classes[i];
if(cls.indexOf('cursor-') === 0) {
el3.attr(STASHATTR, cls.substr(7))
.classed(cls, false);
}
}
}
}
else if(savedCursor) {
el3.attr(STASHATTR, null);
setCursor(el3, savedCursor);
}
};
19 changes: 18 additions & 1 deletion src/plots/cartesian/graph_interact.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ var svgTextUtils = require('../../lib/svg_text_utils');
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var dragElement = require('../../components/dragelement');
var overrideCursor = require('../../lib/override_cursor');
var Registry = require('../../registry');

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

gd._hoverdata = newhoverdata;

// TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
// we should improve the "fx" API so other plots can use it without these hack.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

FYI @etpinard

if(evt.target && evt.target.tagName) {
var hasClickToShow = Registry.getComponentMethod('annotations', 'hasClickToShow')(gd, newhoverdata);
overrideCursor(d3.select(evt.target), hasClickToShow ? 'pointer' : '');
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 for pointer ('help' looks weird to me).

}

if(!hoverChanged(gd, evt, oldhoverdata)) return;

if(oldhoverdata) {
Expand Down Expand Up @@ -1323,8 +1332,16 @@ function hoverChanged(gd, evt, oldhoverdata) {

// on click
fx.click = function(gd, evt) {
var annotationsDone = Registry.getComponentMethod('annotations', 'onClick')(gd, gd._hoverdata);

function emitClick() { gd.emit('plotly_click', {points: gd._hoverdata}); }

if(gd._hoverdata && evt && evt.target) {
gd.emit('plotly_click', {points: gd._hoverdata});
if(annotationsDone && annotationsDone.then) {
annotationsDone.then(emitClick);
}
else emitClick();

// why do we get a double event without this???
if(evt.stopImmediatePropagation) evt.stopImmediatePropagation();
}
Expand Down
Loading