diff --git a/src/components/annotations/annotation_defaults.js b/src/components/annotations/annotation_defaults.js
index 32fd5b0f0a0..7df91978c59 100644
--- a/src/components/annotations/annotation_defaults.js
+++ b/src/components/annotations/annotation_defaults.js
@@ -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');
@@ -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
@@ -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;
};
diff --git a/src/components/annotations/arrow_paths.js b/src/components/annotations/arrow_paths.js
index 59e09ad7580..3f27bbaf83a 100644
--- a/src/components/annotations/arrow_paths.js
+++ b/src/components/annotations/arrow_paths.js
@@ -21,7 +21,10 @@
module.exports = [
// no arrow
- '',
+ {
+ path: '',
+ backoff: 0
+ },
// wide with flat back
{
path: 'M-2.4,-3V3L0.6,0Z',
diff --git a/src/components/annotations/attributes.js b/src/components/annotations/attributes.js
index 143a5a41534..cdaccd8bc73 100644
--- a/src/components/annotations/attributes.js
+++ b/src/components/annotations/attributes.js
@@ -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,7 +247,7 @@ 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',
@@ -244,9 +255,9 @@ module.exports = {
'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,7 +297,7 @@ 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',
@@ -294,9 +305,45 @@ module.exports = {
'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: {
+ 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(' ')
},
diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js
index 011f5f83760..f68ea537c63 100644
--- a/src/components/annotations/calc_autorange.js
+++ b/src/components/annotations/calc_autorange.js
@@ -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) {
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)
+ });
+ }
}
});
}
diff --git a/src/components/annotations/click.js b/src/components/annotations/click.js
new file mode 100644
index 00000000000..8fe77ce8286
--- /dev/null
+++ b/src/components/annotations/click.js
@@ -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};
+}
diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js
index a6421f366a2..815701a4ddf 100644
--- a/src/components/annotations/draw.js
+++ b/src/components/annotations/draw.js
@@ -24,7 +24,7 @@ var dragElement = require('../dragelement');
var handleAnnotationDefaults = require('./annotation_defaults');
var supplyLayoutDefaults = require('./defaults');
-var arrowhead = require('./draw_arrow_head');
+var drawArrowHead = require('./draw_arrow_head');
// Annotations are stored in gd.layout.annotations, an array of objects
@@ -248,14 +248,17 @@ function drawOne(gd, index, opt, value) {
var xa = Axes.getFromId(gd, options.xref),
ya = Axes.getFromId(gd, options.yref),
- annPosPx = {x: 0, y: 0},
+
+ // calculated pixel positions
+ // x & y each will get text, head, and tail as appropriate
+ annPosPx = {x: {}, y: {}},
textangle = +options.textangle || 0;
// create the components
// made a single group to contain all, so opacity can work right
// with border/arrow together this could handle a whole bunch of
// cleanup at this point, but works for now
- var anngroup = fullLayout._infolayer.append('g')
+ var annGroup = fullLayout._infolayer.append('g')
.classed('annotation', true)
.attr('data-index', String(index))
.style('opacity', options.opacity)
@@ -269,17 +272,17 @@ function drawOne(gd, index, opt, value) {
});
// another group for text+background so that they can rotate together
- var anng = anngroup.append('g')
+ var annTextGroup = annGroup.append('g')
.classed('annotation-text-g', true)
.attr('data-index', String(index));
- var ann = anng.append('g');
+ var annTextGroupInner = annTextGroup.append('g');
var borderwidth = options.borderwidth,
borderpad = options.borderpad,
borderfull = borderwidth + borderpad;
- var annbg = ann.append('rect')
+ var annTextBG = annTextGroupInner.append('rect')
.attr('class', 'bg')
.style('stroke-width', borderwidth + 'px')
.call(Color.stroke, options.bordercolor)
@@ -287,7 +290,7 @@ function drawOne(gd, index, opt, value) {
var font = options.font;
- var anntext = ann.append('text')
+ var annText = annTextGroupInner.append('text')
.classed('annotation', true)
.attr('data-unformatted', options.text)
.text(options.text);
@@ -309,12 +312,12 @@ function drawOne(gd, index, opt, value) {
// make sure lines are aligned the way they will be
// at the end, even if their position changes
- anntext.selectAll('tspan.line').attr({y: 0, x: 0});
+ annText.selectAll('tspan.line').attr({y: 0, x: 0});
- var mathjaxGroup = ann.select('.annotation-math-group'),
+ var mathjaxGroup = annTextGroupInner.select('.annotation-math-group'),
hasMathjax = !mathjaxGroup.empty(),
anntextBB = Drawing.bBox(
- (hasMathjax ? mathjaxGroup : anntext).node()),
+ (hasMathjax ? mathjaxGroup : annText).node()),
annwidth = anntextBB.width,
annheight = anntextBB.height,
outerwidth = Math.round(annwidth + 2 * borderfull),
@@ -344,22 +347,37 @@ function drawOne(gd, index, opt, value) {
var annotationIsOffscreen = false;
['x', 'y'].forEach(function(axLetter) {
var axRef = options[axLetter + 'ref'] || axLetter,
+ tailRef = options['a' + axLetter + 'ref'],
ax = Axes.getFromId(gd, axRef),
- dimAngle = (textangle + (axLetter === 'x' ? 0 : 90)) * Math.PI / 180,
- annSize = outerwidth * Math.abs(Math.cos(dimAngle)) +
- outerheight * Math.abs(Math.sin(dimAngle)),
+ dimAngle = (textangle + (axLetter === 'x' ? 0 : -90)) * Math.PI / 180,
+ // note that these two can be either positive or negative
+ annSizeFromWidth = outerwidth * Math.cos(dimAngle),
+ annSizeFromHeight = outerheight * Math.sin(dimAngle),
+ // but this one is the positive total size
+ annSize = Math.abs(annSizeFromWidth) + Math.abs(annSizeFromHeight),
anchor = options[axLetter + 'anchor'],
- alignPosition;
-
- // calculate pixel position
+ posPx = annPosPx[axLetter],
+ basePx,
+ textPadShift,
+ alignPosition,
+ autoAlignFraction,
+ textShift;
+
+ /*
+ * calculate the *primary* pixel position
+ * which is the arrowhead if there is one,
+ * otherwise the text anchor point
+ */
if(ax) {
- // hide the annotation if it's pointing
- // outside the visible plot (as long as the axis
- // isn't autoranged - then we need to draw it
- // anyway to get its bounding box)
+ /*
+ * hide the annotation if it's pointing outside the visible plot
+ * as long as the axis isn't autoranged - then we need to draw it
+ * anyway to get its bounding box. When we're dragging, an axis can
+ * still look autoranged even though it won't be when the drag finishes.
+ */
var posFraction = ax.r2fraction(options[axLetter]);
- if(!ax.autorange && (posFraction < 0 || posFraction > 1)) {
- if(options['a' + axLetter + 'ref'] === axRef) {
+ if((gd._dragging || !ax.autorange) && (posFraction < 0 || posFraction > 1)) {
+ if(tailRef === axRef) {
posFraction = ax.r2fraction(options['a' + axLetter]);
if(posFraction < 0 || posFraction > 1) {
annotationIsOffscreen = true;
@@ -371,139 +389,148 @@ function drawOne(gd, index, opt, value) {
if(annotationIsOffscreen) return;
}
- annPosPx[axLetter] = ax._offset + ax.r2p(options[axLetter]);
- alignPosition = 0.5;
+ basePx = ax._offset + ax.r2p(options[axLetter]);
+ autoAlignFraction = 0.5;
}
else {
- alignPosition = options[axLetter];
- if(axLetter === 'y') alignPosition = 1 - alignPosition;
- annPosPx[axLetter] = (axLetter === 'x') ?
- (gs.l + gs.w * alignPosition) :
- (gs.t + gs.h * alignPosition);
+ if(axLetter === 'x') {
+ alignPosition = options[axLetter];
+ basePx = gs.l + gs.w * alignPosition;
+ }
+ else {
+ alignPosition = 1 - options[axLetter];
+ basePx = gs.t + gs.h * alignPosition;
+ }
+ autoAlignFraction = options.showarrow ? 0.5 : alignPosition;
}
- var alignShift = 0;
- if(options['a' + axLetter + 'ref'] === axRef) {
- annPosPx['aa' + axLetter] = ax._offset + ax.r2p(options['a' + axLetter]);
- } else {
- if(options.showarrow) {
- alignShift = options['a' + axLetter];
+ // now translate this into pixel positions of head, tail, and text
+ // as well as paddings for autorange
+ if(options.showarrow) {
+ posPx.head = basePx;
+
+ var arrowLength = options['a' + axLetter];
+
+ // with an arrow, the text rotates around the anchor point
+ textShift = annSizeFromWidth * shiftFraction(0.5, options.xanchor) -
+ annSizeFromHeight * shiftFraction(0.5, options.yanchor);
+
+ if(tailRef === axRef) {
+ posPx.tail = ax._offset + ax.r2p(arrowLength);
+ // tail is data-referenced: autorange pads the text in px from the tail
+ textPadShift = textShift;
}
else {
- alignShift = annSize * shiftFraction(alignPosition, anchor);
+ posPx.tail = basePx + arrowLength;
+ // tail is specified in px from head, so autorange also pads vs head
+ textPadShift = textShift + arrowLength;
+ }
+
+ posPx.text = posPx.tail + textShift;
+
+ // constrain pixel/paper referenced so the draggers are at least
+ // partially visible
+ var maxPx = fullLayout[(axLetter === 'x') ? 'width' : 'height'];
+ if(axRef === 'paper') {
+ posPx.head = Lib.constrain(posPx.head, 1, maxPx - 1);
+ }
+ if(tailRef === 'pixel') {
+ var shiftPlus = -Math.max(posPx.tail - 3, posPx.text),
+ shiftMinus = Math.min(posPx.tail + 3, posPx.text) - maxPx;
+ if(shiftPlus > 0) {
+ posPx.tail += shiftPlus;
+ posPx.text += shiftPlus;
+ }
+ else if(shiftMinus > 0) {
+ posPx.tail -= shiftMinus;
+ posPx.text -= shiftMinus;
+ }
}
- annPosPx[axLetter] += alignShift;
+ }
+ else {
+ // with no arrow, the text rotates and *then* we put the anchor
+ // relative to the new bounding box
+ textShift = annSize * shiftFraction(autoAlignFraction, anchor);
+ textPadShift = textShift;
+ posPx.text = basePx + textShift;
}
+ options['_' + axLetter + 'padplus'] = (annSize / 2) + textPadShift;
+ options['_' + axLetter + 'padminus'] = (annSize / 2) - textPadShift;
+
// save the current axis type for later log/linear changes
options['_' + axLetter + 'type'] = ax && ax.type;
-
- // save the size and shift in this dim for autorange
- options['_' + axLetter + 'size'] = annSize;
- options['_' + axLetter + 'shift'] = alignShift;
});
if(annotationIsOffscreen) {
- ann.remove();
+ annTextGroupInner.remove();
return;
}
- var arrowX, arrowY;
-
- // make sure the arrowhead (if there is one)
- // and the annotation center are visible
- if(options.showarrow) {
- if(options.axref === options.xref) {
- // we don't want to constrain if the tail is absolute
- // or the slope (which is meaningful) will change.
- arrowX = annPosPx.x;
- } else {
- arrowX = Lib.constrain(annPosPx.x - options.ax, 1, fullLayout.width - 1);
- }
-
- if(options.ayref === options.yref) {
- // we don't want to constrain if the tail is absolute
- // or the slope (which is meaningful) will change.
- arrowY = annPosPx.y;
- } else {
- arrowY = Lib.constrain(annPosPx.y - options.ay, 1, fullLayout.height - 1);
- }
- }
- annPosPx.x = Lib.constrain(annPosPx.x, 1, fullLayout.width - 1);
- annPosPx.y = Lib.constrain(annPosPx.y, 1, fullLayout.height - 1);
-
- var texty = borderfull - anntextBB.top,
- textx = borderfull - anntextBB.left;
-
if(hasMathjax) {
mathjaxGroup.select('svg').attr({x: borderfull - 1, y: borderfull});
}
else {
- anntext.attr({x: textx, y: texty});
- anntext.selectAll('tspan.line').attr({y: texty, x: textx});
+ var texty = borderfull - anntextBB.top,
+ textx = borderfull - anntextBB.left;
+ annText.attr({x: textx, y: texty});
+ annText.selectAll('tspan.line').attr({y: texty, x: textx});
}
- annbg.call(Drawing.setRect, borderwidth / 2, borderwidth / 2,
+ annTextBG.call(Drawing.setRect, borderwidth / 2, borderwidth / 2,
outerwidth - borderwidth, outerheight - borderwidth);
- var annX = 0, annY = 0;
- if(options.axref === options.xref) {
- annX = Math.round(annPosPx.aax - outerwidth / 2);
- } else {
- annX = Math.round(annPosPx.x - outerwidth / 2);
- }
-
- if(options.ayref === options.yref) {
- annY = Math.round(annPosPx.aay - outerheight / 2);
- } else {
- annY = Math.round(annPosPx.y - outerheight / 2);
- }
+ annTextGroupInner.call(Lib.setTranslate,
+ Math.round(annPosPx.x.text - outerwidth / 2),
+ Math.round(annPosPx.y.text - outerheight / 2));
- ann.call(Lib.setTranslate, annX, annY);
+ /*
+ * rotate text and background
+ * we already calculated the text center position *as rotated*
+ * because we needed that for autoranging anyway, so now whether
+ * we have an arrow or not, we rotate about the text center.
+ */
+ annTextGroup.attr({transform: 'rotate(' + textangle + ',' +
+ annPosPx.x.text + ',' + annPosPx.y.text + ')'});
var annbase = 'annotations[' + index + ']';
- // add the arrow
- // uses options[arrowwidth,arrowcolor,arrowhead] for styling
+ /*
+ * add the arrow
+ * uses options[arrowwidth,arrowcolor,arrowhead] for styling
+ * dx and dy are normally zero, but when you are dragging the textbox
+ * while the head stays put, dx and dy are the pixel offsets
+ */
var drawArrow = function(dx, dy) {
d3.select(gd)
.selectAll('.annotation-arrow-g[data-index="' + index + '"]')
.remove();
- // find where to start the arrow:
- // at the border of the textbox, if that border is visible,
- // or at the edge of the lines of text, if the border is hidden
- // TODO: tspan bounding box fails in chrome
- // looks like there may be a cross-browser solution, see
- // http://stackoverflow.com/questions/5364980/
- // how-to-get-the-width-of-an-svg-tspan-element
- var arrowX0, arrowY0;
-
- if(options.axref === options.xref) {
- arrowX0 = annPosPx.aax + dx;
- } else {
- arrowX0 = annPosPx.x + dx;
- }
- if(options.ayref === options.yref) {
- arrowY0 = annPosPx.aay + dy;
- } else {
- arrowY0 = annPosPx.y + dy;
- }
+ var headX = annPosPx.x.head,
+ headY = annPosPx.y.head,
+ tailX = annPosPx.x.tail + dx,
+ tailY = annPosPx.y.tail + dy,
+ textX = annPosPx.x.text + dx,
+ textY = annPosPx.y.text + dy,
- // create transform matrix and related functions
- var transform =
- Lib.rotationXYMatrix(textangle, arrowX0, arrowY0),
+ // find the edge of the text box, where we'll start the arrow:
+ // create transform matrix to rotate the text box corners
+ transform = Lib.rotationXYMatrix(textangle, textX, textY),
applyTransform = Lib.apply2DTransform(transform),
applyTransform2 = Lib.apply2DTransform2(transform),
// calculate and transform bounding box
- xHalf = annbg.attr('width') / 2,
- yHalf = annbg.attr('height') / 2,
+ width = +annTextBG.attr('width'),
+ height = +annTextBG.attr('height'),
+ xLeft = textX - 0.5 * width,
+ xRight = xLeft + width,
+ yTop = textY - 0.5 * height,
+ yBottom = yTop + height,
edges = [
- [arrowX0 - xHalf, arrowY0 - yHalf, arrowX0 - xHalf, arrowY0 + yHalf],
- [arrowX0 - xHalf, arrowY0 + yHalf, arrowX0 + xHalf, arrowY0 + yHalf],
- [arrowX0 + xHalf, arrowY0 + yHalf, arrowX0 + xHalf, arrowY0 - yHalf],
- [arrowX0 + xHalf, arrowY0 - yHalf, arrowX0 - xHalf, arrowY0 - yHalf]
+ [xLeft, yTop, xLeft, yBottom],
+ [xLeft, yBottom, xRight, yBottom],
+ [xRight, yBottom, xRight, yTop],
+ [xRight, yTop, xLeft, yTop]
].map(applyTransform2);
// Remove the line if it ends inside the box. Use ray
@@ -512,7 +539,7 @@ function drawOne(gd, index, opt, value) {
// to get the parity of the number of intersections.
if(edges.reduce(function(a, x) {
return a ^
- !!lineIntersect(arrowX, arrowY, arrowX + 1e6, arrowY + 1e6,
+ !!lineIntersect(headX, headY, headX + 1e6, headY + 1e6,
x[0], x[1], x[2], x[3]);
}, false)) {
// no line or arrow - so quit drawArrow now
@@ -520,50 +547,61 @@ function drawOne(gd, index, opt, value) {
}
edges.forEach(function(x) {
- var p = lineIntersect(arrowX0, arrowY0, arrowX, arrowY,
+ var p = lineIntersect(tailX, tailY, headX, headY,
x[0], x[1], x[2], x[3]);
if(p) {
- arrowX0 = p.x;
- arrowY0 = p.y;
+ tailX = p.x;
+ tailY = p.y;
}
});
var strokewidth = options.arrowwidth,
arrowColor = options.arrowcolor;
- var arrowgroup = anngroup.append('g')
+ var arrowGroup = annGroup.append('g')
.style({opacity: Color.opacity(arrowColor)})
.classed('annotation-arrow-g', true)
.attr('data-index', String(index));
- var arrow = arrowgroup.append('path')
- .attr('d', 'M' + arrowX0 + ',' + arrowY0 + 'L' + arrowX + ',' + arrowY)
+ var arrow = arrowGroup.append('path')
+ .attr('d', 'M' + tailX + ',' + tailY + 'L' + headX + ',' + headY)
.style('stroke-width', strokewidth + 'px')
.call(Color.stroke, Color.rgb(arrowColor));
- arrowhead(arrow, options.arrowhead, 'end', options.arrowsize);
-
- var arrowdrag = arrowgroup.append('path')
- .classed('annotation', true)
- .classed('anndrag', true)
- .attr({
- 'data-index': String(index),
- d: 'M3,3H-3V-3H3ZM0,0L' + (arrowX0 - arrowX) + ',' + (arrowY0 - arrowY),
- transform: 'translate(' + arrowX + ',' + arrowY + ')'
- })
- .style('stroke-width', (strokewidth + 6) + 'px')
- .call(Color.stroke, 'rgba(0,0,0,0)')
- .call(Color.fill, 'rgba(0,0,0,0)');
-
- if(gd._context.editable) {
+ drawArrowHead(arrow, options.arrowhead, 'end', options.arrowsize, options.standoff);
+
+ // the arrow dragger is a small square right at the head, then a line to the tail,
+ // all expanded by a stroke width of 6px plus the arrow line width
+ if(gd._context.editable && arrow.node().parentNode) {
+ var arrowDragHeadX = headX;
+ var arrowDragHeadY = headY;
+ if(options.standoff) {
+ var arrowLength = Math.sqrt(Math.pow(headX - tailX, 2) + Math.pow(headY - tailY, 2));
+ arrowDragHeadX += options.standoff * (tailX - headX) / arrowLength;
+ arrowDragHeadY += options.standoff * (tailY - headY) / arrowLength;
+ }
+ var arrowDrag = arrowGroup.append('path')
+ .classed('annotation', true)
+ .classed('anndrag', true)
+ .attr({
+ 'data-index': String(index),
+ d: 'M3,3H-3V-3H3ZM0,0L' + (tailX - arrowDragHeadX) + ',' + (tailY - arrowDragHeadY),
+ transform: 'translate(' + arrowDragHeadX + ',' + arrowDragHeadY + ')'
+ })
+ .style('stroke-width', (strokewidth + 6) + 'px')
+ .call(Color.stroke, 'rgba(0,0,0,0)')
+ .call(Color.fill, 'rgba(0,0,0,0)');
+
var update,
annx0,
anny0;
+ // dragger for the arrow & head: translates the whole thing
+ // (head/tail/text) all together
dragElement.init({
- element: arrowdrag.node(),
+ element: arrowDrag.node(),
prepFn: function() {
- var pos = Lib.getTranslate(ann);
+ var pos = Lib.getTranslate(annTextGroupInner);
annx0 = pos.x;
anny0 = pos.y;
@@ -576,33 +614,32 @@ function drawOne(gd, index, opt, value) {
}
},
moveFn: function(dx, dy) {
- arrowgroup.attr('transform', 'translate(' + dx + ',' + dy + ')');
-
var annxy0 = applyTransform(annx0, anny0),
xcenter = annxy0[0] + dx,
ycenter = annxy0[1] + dy;
- ann.call(Lib.setTranslate, xcenter, ycenter);
+ annTextGroupInner.call(Lib.setTranslate, xcenter, ycenter);
update[annbase + '.x'] = xa ?
xa.p2r(xa.r2p(options.x) + dx) :
- ((arrowX + dx - gs.l) / gs.w);
+ ((headX + dx - gs.l) / gs.w);
update[annbase + '.y'] = ya ?
ya.p2r(ya.r2p(options.y) + dy) :
- (1 - ((arrowY + dy - gs.t) / gs.h));
+ (1 - ((headY + dy - gs.t) / gs.h));
if(options.axref === options.xref) {
update[annbase + '.ax'] = xa ?
xa.p2r(xa.r2p(options.ax) + dx) :
- ((arrowX + dx - gs.l) / gs.w);
+ ((headX + dx - gs.l) / gs.w);
}
if(options.ayref === options.yref) {
update[annbase + '.ay'] = ya ?
ya.p2r(ya.r2p(options.ay) + dy) :
- (1 - ((arrowY + dy - gs.t) / gs.h));
+ (1 - ((headY + dy - gs.t) / gs.h));
}
- anng.attr({
+ arrowGroup.attr('transform', 'translate(' + dx + ',' + dy + ')');
+ annTextGroup.attr({
transform: 'rotate(' + textangle + ',' +
xcenter + ',' + ycenter + ')'
});
@@ -620,28 +657,20 @@ function drawOne(gd, index, opt, value) {
if(options.showarrow) drawArrow(0, 0);
- // create transform matrix and related functions
- var transform = Lib.rotationXYMatrix(textangle,
- annPosPx.x, annPosPx.y),
- applyTransform = Lib.apply2DTransform(transform);
-
// user dragging the annotation (text, not arrow)
if(gd._context.editable) {
- var x0,
- y0,
- update;
+ var update,
+ baseTextTransform;
+ // dragger for the textbox: if there's an arrow, just drag the
+ // textbox and tail, leave the head untouched
dragElement.init({
- element: ann.node(),
+ element: annTextGroupInner.node(),
prepFn: function() {
- var pos = Lib.getTranslate(ann);
-
- x0 = pos.x;
- y0 = pos.y;
+ baseTextTransform = annTextGroup.attr('transform');
update = {};
},
moveFn: function(dx, dy) {
- ann.call(Lib.setTranslate, x0 + dx, y0 + dy);
var csr = 'pointer';
if(options.showarrow) {
if(options.axref === options.xref) {
@@ -685,21 +714,14 @@ function drawOne(gd, index, opt, value) {
}
}
- var xy1 = applyTransform(x0, y0),
- x1 = xy1[0] + dx,
- y1 = xy1[1] + dy;
-
- ann.call(Lib.setTranslate, x0 + dx, y0 + dy);
-
- anng.attr({
- transform: 'rotate(' + textangle + ',' +
- x1 + ',' + y1 + ')'
+ annTextGroup.attr({
+ transform: 'translate(' + dx + ',' + dy + ')' + baseTextTransform
});
- setCursor(ann, csr);
+ setCursor(annTextGroupInner, csr);
},
doneFn: function(dragged) {
- setCursor(ann);
+ setCursor(annTextGroupInner);
if(dragged) {
Plotly.relayout(gd, update);
var notesBox = document.querySelector('.js-notes-box-panel');
@@ -711,7 +733,7 @@ function drawOne(gd, index, opt, value) {
}
if(gd._context.editable) {
- anntext.call(svgTextUtils.makeEditable, ann)
+ annText.call(svgTextUtils.makeEditable, annTextGroupInner)
.call(textLayout)
.on('edit', function(_text) {
options.text = _text;
@@ -728,12 +750,7 @@ function drawOne(gd, index, opt, value) {
Plotly.relayout(gd, update);
});
}
- else anntext.call(textLayout);
-
- // rotate and position text and background
- anng.attr({transform: 'rotate(' + textangle + ',' +
- annPosPx.x + ',' + annPosPx.y + ')'})
- .call(Drawing.setPosition, annPosPx.x, annPosPx.y);
+ else annText.call(textLayout);
}
// look for intersection of two line segments
diff --git a/src/components/annotations/draw_arrow_head.js b/src/components/annotations/draw_arrow_head.js
index c959c9394df..69e5181914c 100644
--- a/src/components/annotations/draw_arrow_head.js
+++ b/src/components/annotations/draw_arrow_head.js
@@ -22,11 +22,10 @@ var ARROWPATHS = require('./arrow_paths');
// ends is 'start', 'end' (default), 'start+end'
// mag is magnification vs. default (default 1)
-module.exports = function drawArrowHead(el3, style, ends, mag) {
+module.exports = function drawArrowHead(el3, style, ends, mag, standoff) {
if(!isNumeric(mag)) mag = 1;
var el = el3.node(),
headStyle = ARROWPATHS[style||0];
- if(!headStyle) return;
if(typeof ends !== 'string' || !ends) ends = 'end';
@@ -35,7 +34,7 @@ module.exports = function drawArrowHead(el3, style, ends, mag) {
opacity = el3.style('stroke-opacity') || 1,
doStart = ends.indexOf('start') >= 0,
doEnd = ends.indexOf('end') >= 0,
- backOff = headStyle.backoff * scale,
+ backOff = headStyle.backoff * scale + standoff,
start,
end,
startRot,
@@ -44,9 +43,17 @@ module.exports = function drawArrowHead(el3, style, ends, mag) {
if(el.nodeName === 'line') {
start = {x: +el3.attr('x1'), y: +el3.attr('y1')};
end = {x: +el3.attr('x2'), y: +el3.attr('y2')};
- startRot = Math.atan2(start.y - end.y, start.x - end.x);
+
+ var dx = start.x - end.x,
+ dy = start.y - end.y;
+
+ startRot = Math.atan2(dy, dx);
endRot = startRot + Math.PI;
if(backOff) {
+ if(backOff * backOff > dx * dx + dy * dy) {
+ hideLine();
+ return;
+ }
var backOffX = backOff * Math.cos(startRot),
backOffY = backOff * Math.sin(startRot);
@@ -70,6 +77,11 @@ module.exports = function drawArrowHead(el3, style, ends, mag) {
// combine the two
dashArray = '';
+ if(pathlen < backOff) {
+ hideLine();
+ return;
+ }
+
if(doStart) {
var start0 = el.getPointAtLength(0),
dstart = el.getPointAtLength(0.1);
@@ -94,7 +106,10 @@ module.exports = function drawArrowHead(el3, style, ends, mag) {
if(dashArray) el3.style('stroke-dasharray', dashArray);
}
- var drawhead = function(p, rot) {
+ function hideLine() { el3.style('stroke-dasharray', '0px,100px'); }
+
+ function drawhead(p, rot) {
+ if(!headStyle.path) return;
if(style > 5) rot = 0; // don't rotate square or circle
d3.select(el.parentElement).append('path')
.attr({
@@ -110,7 +125,7 @@ module.exports = function drawArrowHead(el3, style, ends, mag) {
opacity: opacity,
'stroke-width': 0
});
- };
+ }
if(doStart) drawhead(start, startRot);
if(doEnd) drawhead(end, endRot);
diff --git a/src/components/annotations/index.js b/src/components/annotations/index.js
index 18db2d770f4..bb32b6b69df 100644
--- a/src/components/annotations/index.js
+++ b/src/components/annotations/index.js
@@ -10,6 +10,7 @@
'use strict';
var drawModule = require('./draw');
+var clickModule = require('./click');
module.exports = {
moduleType: 'component',
@@ -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
};
diff --git a/src/lib/override_cursor.js b/src/lib/override_cursor.js
new file mode 100644
index 00000000000..ebbd290951e
--- /dev/null
+++ b/src/lib/override_cursor.js
@@ -0,0 +1,47 @@
+/**
+* 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 setCursor = require('./setcursor');
+
+var STASHATTR = 'data-savedcursor';
+var NO_CURSOR = '!!';
+
+/*
+ * 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) {
+ var savedCursor = el3.attr(STASHATTR);
+ if(csr) {
+ if(!savedCursor) {
+ 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);
+ }
+ }
+ if(!el3.attr(STASHATTR)) {
+ el3.attr(STASHATTR, NO_CURSOR);
+ }
+ }
+ setCursor(el3, csr);
+ }
+ else if(savedCursor) {
+ el3.attr(STASHATTR, null);
+
+ if(savedCursor === NO_CURSOR) setCursor(el3);
+ else setCursor(el3, savedCursor);
+ }
+};
diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js
index 5c361729a28..ea2f8c908ec 100644
--- a/src/plots/cartesian/graph_interact.js
+++ b/src/plots/cartesian/graph_interact.js
@@ -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');
@@ -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.
+ if(evt.target && evt.target.tagName) {
+ var hasClickToShow = Registry.getComponentMethod('annotations', 'hasClickToShow')(gd, newhoverdata);
+ overrideCursor(d3.select(evt.target), hasClickToShow ? 'pointer' : '');
+ }
+
if(!hoverChanged(gd, evt, oldhoverdata)) return;
if(oldhoverdata) {
@@ -1319,8 +1328,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();
}
diff --git a/test/image/baselines/annotations-autorange.png b/test/image/baselines/annotations-autorange.png
index 07df4be6243..cab99da4b8d 100644
Binary files a/test/image/baselines/annotations-autorange.png and b/test/image/baselines/annotations-autorange.png differ
diff --git a/test/image/baselines/annotations.png b/test/image/baselines/annotations.png
index 41ffc4f779f..f4e3dad3d4e 100644
Binary files a/test/image/baselines/annotations.png and b/test/image/baselines/annotations.png differ
diff --git a/test/image/mocks/annotations-autorange.json b/test/image/mocks/annotations-autorange.json
index 6ec2838daad..433b86aa376 100644
--- a/test/image/mocks/annotations-autorange.json
+++ b/test/image/mocks/annotations-autorange.json
@@ -56,9 +56,15 @@
{"ay":0,"ax":-50,"x":2,"y":1.5,"text":"Right"},
{"x":1.5,"y":2,"text":"Top","ay":50,"ax":0},
{"x":1.5,"y":1,"text":"Bottom","ay":-50,"ax":0},
- {"xref":"x2","yref":"y2","text":"From left","y":2,"ax":-50,"ay":0,"x":"2001-01-01"},
+ {
+ "xref":"x2","yref":"y2","text":"From left","y":2,"ax":-17,"ay":0,"x":"2001-01-01",
+ "xanchor": "right", "yanchor": "top", "textangle": 35, "bordercolor": "#444"
+ },
{"xref":"x2","yref":"y2","text":"From right","y":2,"x":"2001-03-01","ay":0,"ax":50},
- {"xref":"x2","yref":"y2","text":"From top","y":3,"ax":0,"ay":-50,"x":"2001-02-01"},
+ {
+ "xref":"x2","yref":"y2","text":"From top","y":3,"ax":0,"ay":-27,"x":"2001-02-01",
+ "xanchor": "left", "yanchor": "bottom", "textangle": -15, "bordercolor": "#444"
+ },
{"xref":"x2","yref":"y2","text":"From Bottom","y":1,"ax":0,"ay":50,"x":"2001-02-01"},
{"xref":"x3","yref":"y3","text":"Left
no
arrow","y":1.5,"x":1,"showarrow":false},
{"xref":"x3","yref":"y3","text":"Right
no
arrow","y":1.5,"x":2,"showarrow":false},
diff --git a/test/image/mocks/annotations.json b/test/image/mocks/annotations.json
index 06cbdd5b48f..ed767593165 100644
--- a/test/image/mocks/annotations.json
+++ b/test/image/mocks/annotations.json
@@ -45,7 +45,17 @@
"arrowcolor":"rgb(166, 28, 0)","borderpad":3,"textangle":50,"x":5,"y":1
},
{"text":"","showarrow":true,"borderwidth":1.2,"arrowhead":2,"axref":"x","ayref":"y","x":5,"y":3,"ax":4,"ay":5},
- {"text":"","showarrow":true,"borderwidth":1.2,"arrowhead":2,"axref":"x","ayref":"y","x":6,"y":2,"ax":3,"ay":3}
+ {"text":"","showarrow":true,"borderwidth":1.2,"arrowhead":2,"axref":"x","ayref":"y","x":6,"y":2,"ax":3,"ay":3},
+ {
+ "text": "arrow ML
+standoff", "x": 2, "y": 2, "ax": 20, "ay": 20,
+ "xanchor": "left", "yanchor": "middle", "standoff": 5
+ },
+ {
+ "text": "angle, arrow TR
box+standoff", "x": 2, "y": 2, "ax": -20, "ay": -20,
+ "xanchor": "right", "yanchor": "top", "standoff": 5, "textangle": 45, "bordercolor": "#444"
+ },
+ {"text": "off-plot, do not show!", "x": 5.1, "y": 3.5, "showarrow": false},
+ {"text": "off-plot 2", "x": 3, "y": 5.1, "showarrow": true, "ax": 0, "ay": 100}
]
}
}
diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js
index fd99e6c6881..880f273ed70 100644
--- a/test/jasmine/tests/annotations_test.js
+++ b/test/jasmine/tests/annotations_test.js
@@ -47,7 +47,8 @@ describe('Test annotations', function() {
expect(item).toEqual({
visible: false,
_input: {},
- _index: i
+ _index: i,
+ clicktoshow: false
});
});
});
@@ -296,3 +297,131 @@ describe('annotations autosize', function() {
.then(done);
});
});
+
+describe('annotation clicktoshow', function() {
+ var gd;
+
+ afterEach(destroyGraphDiv);
+
+ function layout() {
+ return {
+ xaxis: {domain: [0, 0.5]},
+ xaxis2: {domain: [0.5, 1], anchor: 'y2'},
+ yaxis2: {anchor: 'x2'},
+ annotations: [
+ {x: 1, y: 2, xref: 'x', yref: 'y', text: 'index0'}, // (1,2) selects
+ {x: 1, y: 3, xref: 'x', yref: 'y', text: 'index1'},
+ {x: 2, y: 3, xref: 'x', yref: 'y', text: 'index2'}, // ** (2,3) selects
+ {x: 4, y: 2, xref: 'x', yref: 'y', text: 'index3'},
+ {x: 1, y: 2, xref: 'x2', yref: 'y', text: 'index4'},
+ {x: 1, y: 2, xref: 'x', yref: 'y2', text: 'index5'},
+ {x: 1, xclick: 5, y: 2, xref: 'x', yref: 'y', text: 'index6'},
+ {x: 1, y: 2, yclick: 6, xref: 'x', yref: 'y', text: 'index7'},
+ {x: 1, y: 2.0000001, xref: 'x', yref: 'y', text: 'index8'},
+ {x: 1, y: 2, xref: 'x', yref: 'y', text: 'index9'}, // (1,2) selects
+ {x: 7, xclick: 1, y: 2, xref: 'x', yref: 'y', text: 'index10'}, // (1,2) selects
+ {x: 1, y: 8, yclick: 2, xref: 'x', yref: 'y', text: 'index11'}, // (1,2) selects
+ {x: 1, y: 2, xref: 'paper', yref: 'y', text: 'index12'},
+ {x: 1, y: 2, xref: 'x', yref: 'paper', text: 'index13'},
+ {x: 1, y: 2, xref: 'paper', yref: 'paper', text: 'index14'}
+ ]
+ };
+ }
+
+ var data = [
+ {x: [0, 1, 2], y: [1, 2, 3]},
+ {x: [0, 1, 2], y: [1, 2, 3], xaxis: 'x2', yaxis: 'y2'}
+ ];
+
+ function hoverData(xyPairs) {
+ // hovering on nothing can have undefined hover data - must be supported
+ if(!xyPairs.length) return;
+
+ return xyPairs.map(function(xy) {
+ return {
+ x: xy[0],
+ y: xy[1],
+ xaxis: gd._fullLayout.xaxis,
+ yaxis: gd._fullLayout.yaxis
+ };
+ });
+ }
+
+ function checkVisible(opts) {
+ gd._fullLayout.annotations.forEach(function(ann, i) {
+ expect(ann.visible).toBe(opts.on.indexOf(i) !== -1, 'i: ' + i + ', step: ' + opts.step);
+ });
+ }
+
+ function allAnnotations(attr, value) {
+ var update = {};
+ for(var i = 0; i < gd.layout.annotations.length; i++) {
+ update['annotations[' + i + '].' + attr] = value;
+ }
+ return update;
+ }
+
+ function clickAndCheck(opts) {
+ return function() {
+ expect(Annotations.hasClickToShow(gd, hoverData(opts.newPts)))
+ .toBe(opts.newCTS, 'step: ' + opts.step);
+
+ var clickResult = Annotations.onClick(gd, hoverData(opts.newPts));
+ if(clickResult && clickResult.then) {
+ return clickResult.then(function() { checkVisible(opts); });
+ }
+ else {
+ checkVisible(opts);
+ }
+ };
+ }
+
+ function updateAndCheck(opts) {
+ return function() {
+ return Plotly.update(gd, {}, opts.update).then(function() {
+ checkVisible(opts);
+ });
+ };
+ }
+
+ var allIndices = layout().annotations.map(function(v, i) { return i; });
+
+ it('should select only clicktoshow annotations matching x, y, and axes of any point', function(done) {
+ gd = createGraphDiv();
+
+ // first try to select without adding clicktoshow, both visible and invisible
+ Plotly.plot(gd, data, layout())
+ // clicktoshow is off initially, so it doesn't *expect* clicking will
+ // do anything, and it doesn't *actually* do anything.
+ .then(clickAndCheck({newPts: [[1, 2]], newCTS: false, on: allIndices, step: 1}))
+ .then(updateAndCheck({update: allAnnotations('visible', false), on: [], step: 2}))
+ // still nothing happens with hidden annotations
+ .then(clickAndCheck({newPts: [[1, 2]], newCTS: false, on: [], step: 3}))
+
+ // turn on clicktoshow (onout mode) and we see some action!
+ .then(updateAndCheck({update: allAnnotations('clicktoshow', 'onout'), on: [], step: 4}))
+ .then(clickAndCheck({newPts: [[1, 2]], newCTS: true, on: [0, 9, 10, 11], step: 5}))
+ .then(clickAndCheck({newPts: [[2, 3]], newCTS: true, on: [2], step: 6}))
+ // clicking the same point again will close all, but in onout mode hasClickToShow
+ // is false because closing notes is kind of passive
+ .then(clickAndCheck({newPts: [[2, 3]], newCTS: false, on: [], step: 7}))
+ // now click two points (as if in compare hovermode)
+ .then(clickAndCheck({newPts: [[1, 2], [2, 3]], newCTS: true, on: [0, 2, 9, 10, 11], step: 8}))
+ // close all by clicking somewhere else
+ .then(clickAndCheck({newPts: [[0, 1]], newCTS: false, on: [], step: 9}))
+
+ // now switch to onoff mode
+ .then(updateAndCheck({update: allAnnotations('clicktoshow', 'onoff'), on: [], step: 10}))
+ // again, clicking a point turns those annotations on
+ .then(clickAndCheck({newPts: [[1, 2]], newCTS: true, on: [0, 9, 10, 11], step: 11}))
+ // clicking a different point (or no point at all) leaves open annotations the same
+ .then(clickAndCheck({newPts: [[0, 1]], newCTS: false, on: [0, 9, 10, 11], step: 12}))
+ .then(clickAndCheck({newPts: [], newCTS: false, on: [0, 9, 10, 11], step: 13}))
+ // clicking another point turns it on too, without turning off the original
+ .then(clickAndCheck({newPts: [[0, 1], [2, 3]], newCTS: true, on: [0, 2, 9, 10, 11], step: 14}))
+ // finally click each one off
+ .then(clickAndCheck({newPts: [[1, 2]], newCTS: true, on: [2], step: 15}))
+ .then(clickAndCheck({newPts: [[2, 3]], newCTS: true, on: [], step: 16}))
+ .then(done);
+ });
+});
diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js
index 4991a6e5ab5..3ef05bfd90a 100644
--- a/test/jasmine/tests/lib_test.js
+++ b/test/jasmine/tests/lib_test.js
@@ -1,5 +1,6 @@
var Lib = require('@src/lib');
var setCursor = require('@src/lib/setcursor');
+var overrideCursor = require('@src/lib/override_cursor');
var d3 = require('d3');
var Plotly = require('@lib');
@@ -1191,6 +1192,56 @@ describe('Test lib.js:', function() {
});
});
+ describe('overrideCursor', function() {
+
+ beforeEach(function() {
+ this.el3 = d3.select(createGraphDiv());
+ });
+
+ afterEach(destroyGraphDiv);
+
+ it('should apply the new cursor(s) and revert to the original when removed', function() {
+ this.el3
+ .classed('cursor-before', true)
+ .classed('not-a-cursor', true)
+ .classed('another', true);
+
+ overrideCursor(this.el3, 'after');
+ expect(this.el3.attr('class')).toBe('not-a-cursor another cursor-after');
+
+ overrideCursor(this.el3, 'later');
+ expect(this.el3.attr('class')).toBe('not-a-cursor another cursor-later');
+
+ overrideCursor(this.el3);
+ expect(this.el3.attr('class')).toBe('not-a-cursor another cursor-before');
+ });
+
+ it('should apply the new cursor(s) and revert to the none when removed', function() {
+ this.el3
+ .classed('not-a-cursor', true)
+ .classed('another', true);
+
+ overrideCursor(this.el3, 'after');
+ expect(this.el3.attr('class')).toBe('not-a-cursor another cursor-after');
+
+ overrideCursor(this.el3, 'later');
+ expect(this.el3.attr('class')).toBe('not-a-cursor another cursor-later');
+
+ overrideCursor(this.el3);
+ expect(this.el3.attr('class')).toBe('not-a-cursor another');
+ });
+
+ it('should do nothing if no existing or new override is present', function() {
+ this.el3
+ .classed('cursor-before', true)
+ .classed('not-a-cursor', true);
+
+ overrideCursor(this.el3);
+
+ expect(this.el3.attr('class')).toBe('cursor-before not-a-cursor');
+ });
+ });
+
describe('getTranslate', function() {
it('should work with regular DOM elements', function() {
diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js
index 36c7ec0f045..949aaaa14c3 100644
--- a/test/jasmine/tests/transition_test.js
+++ b/test/jasmine/tests/transition_test.js
@@ -68,7 +68,8 @@ function runTests(transitionDuration) {
it('transitions an annotation', function(done) {
function annotationPosition() {
var g = gd._fullLayout._infolayer.select('.annotation').select('.annotation-text-g');
- return [parseInt(g.attr('x')), parseInt(g.attr('y'))];
+ var bBox = g.node().getBoundingClientRect();
+ return [bBox.left, bBox.top];
}
var p1, p2;