diff --git a/build/ploticon.js b/build/ploticon.js
index 84b86fb0aa6..4dd7accc816 100644
--- a/build/ploticon.js
+++ b/build/ploticon.js
@@ -114,5 +114,11 @@ module.exports = {
'path': 'm0 850l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m285 0l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m-857-286l0-143 143 0 0 143-143 0z m857 0l0-143 143 0 0 143-143 0z m-857-285l0-143 143 0 0 143-143 0z m857 0l0-143 143 0 0 143-143 0z m-857-286l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m285 0l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z',
'ascent': 850,
'descent': -150
+ },
+ 'spikeline': {
+ 'width': 1000,
+ 'path': 'M512 409c0-57-46-104-103-104-57 0-104 47-104 104 0 57 47 103 104 103 57 0 103-46 103-103z m-327-39l92 0 0 92-92 0z m-185 0l92 0 0 92-92 0z m370-186l92 0 0 93-92 0z m0-184l92 0 0 92-92 0z',
+ 'ascent': 850,
+ 'descent': -150
}
};
diff --git a/src/components/dragelement/unhover.js b/src/components/dragelement/unhover.js
index 1905cdf6825..731b1470637 100644
--- a/src/components/dragelement/unhover.js
+++ b/src/components/dragelement/unhover.js
@@ -41,6 +41,8 @@ unhover.raw = function unhoverRaw(gd, evt) {
}
fullLayout._hoverlayer.selectAll('g').remove();
+ fullLayout._hoverlayer.selectAll('line').remove();
+ fullLayout._hoverlayer.selectAll('circle').remove();
gd._hoverdata = undefined;
if(evt.target && oldhoverdata) {
diff --git a/src/components/drawing/attributes.js b/src/components/drawing/attributes.js
new file mode 100644
index 00000000000..0ea5dbe3620
--- /dev/null
+++ b/src/components/drawing/attributes.js
@@ -0,0 +1,26 @@
+/**
+* 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';
+
+exports.dash = {
+ valType: 'string',
+ // string type usually doesn't take values... this one should really be
+ // a special type or at least a special coercion function, from the GUI
+ // you only get these values but elsewhere the user can supply a list of
+ // dash lengths in px, and it will be honored
+ values: ['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'],
+ dflt: 'solid',
+ role: 'style',
+ description: [
+ 'Sets the dash style of lines. Set to a dash type string',
+ '(*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*)',
+ 'or a dash length list in px (eg *5px,10px,2px,2px*).'
+ ].join(' ')
+};
diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js
index fa995b0641e..67245728b47 100644
--- a/src/components/drawing/index.js
+++ b/src/components/drawing/index.js
@@ -113,6 +113,17 @@ drawing.lineGroupStyle = function(s, lw, lc, ld) {
drawing.dashLine = function(s, dash, lineWidth) {
lineWidth = +lineWidth || 0;
+
+ dash = drawing.dashStyle(dash, lineWidth);
+
+ s.style({
+ 'stroke-dasharray': dash,
+ 'stroke-width': lineWidth + 'px'
+ });
+};
+
+drawing.dashStyle = function(dash, lineWidth) {
+ lineWidth = +lineWidth || 1;
var dlw = Math.max(lineWidth, 3);
if(dash === 'solid') dash = '';
@@ -127,10 +138,7 @@ drawing.dashLine = function(s, dash, lineWidth) {
}
// otherwise user wrote the dasharray themselves - leave it be
- s.style({
- 'stroke-dasharray': dash,
- 'stroke-width': lineWidth + 'px'
- });
+ return dash;
};
drawing.fillGroupStyle = function(s) {
diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js
index aae0503e368..776a75df610 100644
--- a/src/components/modebar/buttons.js
+++ b/src/components/modebar/buttons.js
@@ -177,17 +177,20 @@ function handleCartesian(gd, ev) {
astr = button.getAttribute('data-attr'),
val = button.getAttribute('data-val') || true,
fullLayout = gd._fullLayout,
- aobj = {};
+ aobj = {},
+ axList = Axes.list(gd, null, true),
+ ax,
+ allEnabled = 'on',
+ i;
if(astr === 'zoom') {
var mag = (val === 'in') ? 0.5 : 2,
r0 = (1 + mag) / 2,
- r1 = (1 - mag) / 2,
- axList = Axes.list(gd, null, true);
+ r1 = (1 - mag) / 2;
- var ax, axName;
+ var axName;
- for(var i = 0; i < axList.length; i++) {
+ for(i = 0; i < axList.length; i++) {
ax = axList[i];
if(!ax.fixedrange) {
@@ -202,6 +205,12 @@ function handleCartesian(gd, ev) {
aobj[axName + '.range[0]'] = rangeInitial[0];
aobj[axName + '.range[1]'] = rangeInitial[1];
}
+ if(ax._showSpikeInitial !== undefined) {
+ aobj[axName + '.showspikes'] = ax._showSpikeInitial;
+ if(allEnabled === 'on' && !ax._showSpikeInitial) {
+ allEnabled = 'off';
+ }
+ }
}
else {
var rangeNow = [
@@ -219,12 +228,24 @@ function handleCartesian(gd, ev) {
}
}
}
+ fullLayout._cartesianSpikesEnabled = allEnabled;
}
else {
// if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y'
if(astr === 'hovermode' && (val === 'x' || val === 'y')) {
val = fullLayout._isHoriz ? 'y' : 'x';
button.setAttribute('data-val', val);
+ if(val !== 'closest') {
+ fullLayout._cartesianSpikesEnabled = 'off';
+ }
+ } else if(astr === 'hovermode' && val === 'closest') {
+ for(i = 0; i < axList.length; i++) {
+ ax = axList[i];
+ if(allEnabled === 'on' && !ax.showspikes) {
+ allEnabled = 'off';
+ }
+ }
+ fullLayout._cartesianSpikesEnabled = allEnabled;
}
aobj[astr] = val;
@@ -518,3 +539,38 @@ modeBarButtons.resetViews = {
// geo subplots.
}
};
+
+modeBarButtons.toggleSpikelines = {
+ name: 'toggleSpikelines',
+ title: 'Toggle Spike Lines',
+ icon: Icons.spikeline,
+ attr: '_cartesianSpikesEnabled',
+ val: 'on',
+ click: function(gd) {
+ var fullLayout = gd._fullLayout;
+
+ fullLayout._cartesianSpikesEnabled = fullLayout.hovermode === 'closest' ?
+ (fullLayout._cartesianSpikesEnabled === 'on' ? 'off' : 'on') : 'on';
+
+ var aobj = setSpikelineVisibility(gd);
+
+ aobj.hovermode = 'closest';
+ Plotly.relayout(gd, aobj);
+ }
+};
+
+function setSpikelineVisibility(gd) {
+ var fullLayout = gd._fullLayout,
+ axList = Axes.list(gd, null, true),
+ ax,
+ axName,
+ aobj = {};
+
+ for(var i = 0; i < axList.length; i++) {
+ ax = axList[i];
+ axName = ax._name;
+ aobj[axName + '.showspikes'] = fullLayout._cartesianSpikesEnabled === 'on' ? true : false;
+ }
+
+ return aobj;
+}
diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js
index 57e5e2d17b3..f3732850f8c 100644
--- a/src/components/modebar/manage.js
+++ b/src/components/modebar/manage.js
@@ -138,7 +138,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) {
addGroup(['hoverClosestGl2d']);
}
else if(hasCartesian) {
- addGroup(['hoverClosestCartesian', 'hoverCompareCartesian']);
+ addGroup(['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']);
}
else if(hasPie) {
addGroup(['hoverClosestPie']);
diff --git a/src/components/modebar/modebar.js b/src/components/modebar/modebar.js
index 054a8a91928..5e87b2413a8 100644
--- a/src/components/modebar/modebar.js
+++ b/src/components/modebar/modebar.js
@@ -149,7 +149,7 @@ proto.createButton = function(config) {
button.setAttribute('data-toggle', config.toggle || false);
if(config.toggle) d3.select(button).classed('active', true);
- button.appendChild(this.createIcon(config.icon || Icons.question));
+ button.appendChild(this.createIcon(config.icon || Icons.question, config.name));
button.setAttribute('data-gravity', config.gravity || 'n');
return button;
@@ -162,7 +162,7 @@ proto.createButton = function(config) {
* @Param {string} thisIcon.path
* @Return {HTMLelement}
*/
-proto.createIcon = function(thisIcon) {
+proto.createIcon = function(thisIcon, name) {
var iconHeight = thisIcon.ascent - thisIcon.descent,
svgNS = 'http://www.w3.org/2000/svg',
icon = document.createElementNS(svgNS, 'svg'),
@@ -172,8 +172,12 @@ proto.createIcon = function(thisIcon) {
icon.setAttribute('width', (thisIcon.width / iconHeight) + 'em');
icon.setAttribute('viewBox', [0, 0, thisIcon.width, iconHeight].join(' '));
+ var transform = name === 'toggleSpikelines' ?
+ 'matrix(1.5 0 0 -1.5 0 ' + thisIcon.ascent + ')' :
+ 'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')';
+
path.setAttribute('d', thisIcon.path);
- path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')');
+ path.setAttribute('transform', transform);
icon.appendChild(path);
return icon;
diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js
index a8974f2825e..83407b34067 100644
--- a/src/components/shapes/attributes.js
+++ b/src/components/shapes/attributes.js
@@ -9,11 +9,10 @@
'use strict';
var annAttrs = require('../annotations/attributes');
-var scatterAttrs = require('../../traces/scatter/attributes');
+var scatterLineAttrs = require('../../traces/scatter/attributes').line;
+var dash = require('../drawing/attributes').dash;
var extendFlat = require('../../lib/extend').extendFlat;
-var scatterLineAttrs = scatterAttrs.line;
-
module.exports = {
_isLinkedToArray: 'shape',
@@ -151,7 +150,7 @@ module.exports = {
line: {
color: scatterLineAttrs.color,
width: scatterLineAttrs.width,
- dash: scatterLineAttrs.dash,
+ dash: dash,
role: 'info'
},
fillcolor: {
diff --git a/src/fonts/ploticon/ploticon.svg b/src/fonts/ploticon/ploticon.svg
index a72ac5b551d..5007169983b 100644
--- a/src/fonts/ploticon/ploticon.svg
+++ b/src/fonts/ploticon/ploticon.svg
@@ -25,6 +25,7 @@
+
-
\ No newline at end of file
+
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index 2494c9d76e2..e712a42047c 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -153,6 +153,9 @@ Plotly.plot = function(gd, data, layout, config) {
makePlotFramework(gd);
}
+ // save initial show spikes once per graph
+ if(graphWasEmpty) Plotly.Axes.saveShowSpikeInitial(gd);
+
// prepare the data and find the autorange
// generate calcdata, if we need to
@@ -2121,14 +2124,16 @@ function _relayout(gd, aobj) {
flags.doticks = flags.dolayoutstyle = true;
}
/*
- * hovermode and dragmode don't need any redrawing, since they just
- * affect reaction to user input, everything else, assume full replot.
+ * hovermode, dragmode, and spikes don't need any redrawing, since they just
+ * affect reaction to user input. Everything else, assume full replot.
* height, width, autosize get dealt with below. Except for the case of
* of subplots - scenes - which require scene.updateFx to be called.
*/
- else if(['hovermode', 'dragmode'].indexOf(ai) !== -1) flags.domodebar = true;
- else if(['hovermode', 'dragmode', 'height',
- 'width', 'autosize'].indexOf(ai) === -1) {
+ else if(['hovermode', 'dragmode'].indexOf(ai) !== -1 ||
+ ai.indexOf('spike') !== -1) {
+ flags.domodebar = true;
+ }
+ else if(['height', 'width', 'autosize'].indexOf(ai) === -1) {
flags.doplot = true;
}
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index e77bc52831e..7903c792782 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -361,6 +361,35 @@ axes.saveRangeInitial = function(gd, overwrite) {
return hasOneAxisChanged;
};
+// save a copy of the initial spike visibility
+axes.saveShowSpikeInitial = function(gd, overwrite) {
+ var axList = axes.list(gd, '', true),
+ hasOneAxisChanged = false,
+ allEnabled = 'on';
+
+ for(var i = 0; i < axList.length; i++) {
+ var ax = axList[i];
+
+ var isNew = (ax._showSpikeInitial === undefined);
+ var hasChanged = (
+ isNew || !(
+ ax.showspikes === ax._showspikes
+ )
+ );
+
+ if((isNew) || (overwrite && hasChanged)) {
+ ax._showSpikeInitial = ax.showspikes;
+ hasOneAxisChanged = true;
+ }
+
+ if(allEnabled === 'on' && !ax.showspikes) {
+ allEnabled = 'off';
+ }
+ }
+ gd._fullLayout._cartesianSpikesEnabled = allEnabled;
+ return hasOneAxisChanged;
+};
+
// axes.expand: if autoranging, include new data in the outer limits
// for this axis
// data is an array of numbers (ie already run through ax.d2c)
@@ -1603,7 +1632,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
// set scaling to pixels
ax.setScale();
- var axletter = axid.charAt(0),
+ var axLetter = axid.charAt(0),
counterLetter = axes.counterLetter(axid),
vals = axes.calcTicks(ax),
datafn = function(d) { return [d.text, d.x, ax.mirror].join('_'); },
@@ -1617,7 +1646,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
gridWidth = Drawing.crispRound(gd, ax.gridwidth, 1),
zeroLineWidth = Drawing.crispRound(gd, ax.zerolinewidth, gridWidth),
tickWidth = Drawing.crispRound(gd, ax.tickwidth, 1),
- sides, transfn, tickpathfn,
+ sides, transfn, tickpathfn, subplots,
i;
if(ax._counterangle && ax.ticks === 'outside') {
@@ -1627,7 +1656,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
}
// positioning arguments for x vs y axes
- if(axletter === 'x') {
+ if(axLetter === 'x') {
sides = ['bottom', 'top'];
transfn = function(d) {
return 'translate(' + ax.l2p(d.x) + ',0)';
@@ -1640,7 +1669,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
else return 'M0,' + shift + 'v' + len;
};
}
- else if(axletter === 'y') {
+ else if(axLetter === 'y') {
sides = ['left', 'right'];
transfn = function(d) {
return 'translate(0,' + ax.l2p(d.x) + ')';
@@ -1661,7 +1690,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
// which direction do the side[0], side[1], and free ticks go?
// then we flip if outside XOR y axis
ticksign = [-1, 1, axside === sides[1] ? 1 : -1];
- if((ax.ticks !== 'inside') === (axletter === 'x')) {
+ if((ax.ticks !== 'inside') === (axLetter === 'x')) {
ticksign = ticksign.map(function(v) { return -v; });
}
@@ -1695,12 +1724,12 @@ axes.doTicks = function(gd, axid, skipTitle) {
var tickLabels = container.selectAll('g.' + tcls).data(vals, datafn);
if(!ax.showticklabels || !isNumeric(position)) {
tickLabels.remove();
- drawAxTitle(axid);
+ drawAxTitle();
return;
}
var labelx, labely, labelanchor, labelpos0, flipit;
- if(axletter === 'x') {
+ if(axLetter === 'x') {
flipit = (axside === 'bottom') ? 1 : -1;
labelx = function(d) { return d.dx + labelShift * flipit; };
labelpos0 = position + (labelStandoff + pad) * flipit;
@@ -1816,7 +1845,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
// check for auto-angling if x labels overlap
// don't auto-angle at all for log axes with
// base and digit format
- if(axletter === 'x' && !isNumeric(ax.tickangle) &&
+ if(axLetter === 'x' && !isNumeric(ax.tickangle) &&
(ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')) {
var lbbArray = [];
tickLabels.each(function(d) {
@@ -1861,12 +1890,59 @@ axes.doTicks = function(gd, axid, skipTitle) {
// (so it can move out of the way if needed)
// TODO: separate out scoot so we don't need to do
// a full redraw of the title (mostly relevant for MathJax)
- drawAxTitle(axid);
+ drawAxTitle();
return axid + ' done';
}
function calcBoundingBox() {
- ax._boundingBox = container.node().getBoundingClientRect();
+ var bBox = container.node().getBoundingClientRect();
+ var gdBB = gd.getBoundingClientRect();
+
+ /*
+ * the way we're going to use this, the positioning that matters
+ * is relative to the origin of gd. This is important particularly
+ * if gd is scrollable, and may have been scrolled between the time
+ * we calculate this and the time we use it
+ */
+ ax._boundingBox = {
+ width: bBox.width,
+ height: bBox.height,
+ left: bBox.left - gdBB.left,
+ right: bBox.right - gdBB.left,
+ top: bBox.top - gdBB.top,
+ bottom: bBox.bottom - gdBB.top
+ };
+
+ /*
+ * for spikelines: what's the full domain of positions in the
+ * opposite direction that are associated with this axis?
+ * This means any axes that we make a subplot with, plus the
+ * position of the axis itself if it's free.
+ */
+ if(subplots) {
+ var fullRange = ax._counterSpan = [Infinity, -Infinity];
+
+ for(i = 0; i < subplots.length; i++) {
+ var subplot = fullLayout._plots[subplots[i]];
+ var counterAxis = subplot[(axLetter === 'x') ? 'yaxis' : 'xaxis'];
+
+ extendRange(fullRange, [
+ counterAxis._offset,
+ counterAxis._offset + counterAxis._length
+ ]);
+ }
+
+ if(ax.anchor === 'free') {
+ extendRange(fullRange, (axLetter === 'x') ?
+ [ax._boundingBox.bottom, ax._boundingBox.top] :
+ [ax._boundingBox.right, ax._boundingBox.left]);
+ }
+ }
+
+ function extendRange(range, newRange) {
+ range[0] = Math.min(range[0], newRange[0]);
+ range[1] = Math.max(range[1], newRange[1]);
+ }
}
var done = Lib.syncOrAsync([
@@ -1878,7 +1954,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
return done;
}
- function drawAxTitle(axid) {
+ function drawAxTitle() {
if(skipTitle) return;
// now this only applies to regular cartesian axes; colorbars and
@@ -1949,16 +2025,16 @@ axes.doTicks = function(gd, axid, skipTitle) {
function traceHasBarsOrFill(trace, subplot) {
if(trace.visible !== true || trace.xaxis + trace.yaxis !== subplot) return false;
- if(Registry.traceIs(trace, 'bar') && trace.orientation === {x: 'h', y: 'v'}[axletter]) return true;
- return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axletter;
+ if(Registry.traceIs(trace, 'bar') && trace.orientation === {x: 'h', y: 'v'}[axLetter]) return true;
+ return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axLetter;
}
function drawGrid(plotinfo, counteraxis, subplot) {
var gridcontainer = plotinfo.gridlayer,
zlcontainer = plotinfo.zerolinelayer,
- gridvals = plotinfo['hidegrid' + axletter] ? [] : valsClipped,
+ gridvals = plotinfo['hidegrid' + axLetter] ? [] : valsClipped,
gridpath = ax._gridpath ||
- 'M0,0' + ((axletter === 'x') ? 'v' : 'h') + counteraxis._length,
+ 'M0,0' + ((axLetter === 'x') ? 'v' : 'h') + counteraxis._length,
grid = gridcontainer.selectAll('path.' + gcls)
.data((ax.showgrid === false) ? [] : gridvals, datafn);
grid.enter().append('path').classed(gcls, 1)
@@ -2012,12 +2088,13 @@ axes.doTicks = function(gd, axid, skipTitle) {
return drawLabels(ax._axislayer, ax._pos);
}
else {
- var alldone = axes.getSubplots(gd, ax).map(function(subplot) {
+ subplots = axes.getSubplots(gd, ax);
+ var alldone = subplots.map(function(subplot) {
var plotinfo = fullLayout._plots[subplot];
if(!fullLayout._has('cartesian')) return;
- var container = plotinfo[axletter + 'axislayer'],
+ var container = plotinfo[axLetter + 'axislayer'],
// [bottom or left, top or right, free, main]
linepositions = ax._linepositions[subplot] || [],
diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js
index d1307a8cbb5..9265753e9d5 100644
--- a/src/plots/cartesian/graph_interact.js
+++ b/src/plots/cartesian/graph_interact.js
@@ -11,6 +11,7 @@
var d3 = require('d3');
var isNumeric = require('fast-isnumeric');
+var tinycolor = require('tinycolor2');
var Lib = require('../../lib');
var Events = require('../../lib/events');
@@ -555,30 +556,8 @@ function hover(gd, evt, subplot) {
// nothing left: remove all labels and quit
if(hoverData.length === 0) return dragElement.unhoverRaw(gd, evt);
- // if there's more than one horz bar trace,
- // rotate the labels so they don't overlap
- var rotateLabels = hovermode === 'y' && searchData.length > 1;
-
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });
- var bgColor = Color.combine(
- fullLayout.plot_bgcolor || Color.background,
- fullLayout.paper_bgcolor
- );
-
- var labelOpts = {
- hovermode: hovermode,
- rotateLabels: rotateLabels,
- bgColor: bgColor,
- container: fullLayout._hoverlayer,
- outerContainer: fullLayout._paperdiv
- };
- var hoverLabels = createHoverText(hoverData, labelOpts);
-
- hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya');
-
- alignHoverText(hoverLabels, rotateLabels);
-
// lastly, emit custom hover/unhover events
var oldhoverdata = gd._hoverdata,
newhoverdata = [];
@@ -610,6 +589,39 @@ function hover(gd, evt, subplot) {
gd._hoverdata = newhoverdata;
+ if(hoverChanged(gd, evt, oldhoverdata) && fullLayout._hasCartesian) {
+ var spikelineOpts = {
+ hovermode: hovermode,
+ fullLayout: fullLayout,
+ container: fullLayout._hoverlayer,
+ outerContainer: fullLayout._paperdiv
+ };
+ createSpikelines(hoverData, spikelineOpts);
+ }
+
+ // if there's more than one horz bar trace,
+ // rotate the labels so they don't overlap
+ var rotateLabels = hovermode === 'y' && searchData.length > 1;
+
+ var bgColor = Color.combine(
+ fullLayout.plot_bgcolor || Color.background,
+ fullLayout.paper_bgcolor
+ );
+
+ var labelOpts = {
+ hovermode: hovermode,
+ rotateLabels: rotateLabels,
+ bgColor: bgColor,
+ container: fullLayout._hoverlayer,
+ outerContainer: fullLayout._paperdiv
+ };
+
+ var hoverLabels = createHoverText(hoverData, labelOpts);
+
+ hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya');
+
+ alignHoverText(hoverLabels, rotateLabels);
+
// 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) {
@@ -617,7 +629,8 @@ function hover(gd, evt, subplot) {
overrideCursor(d3.select(evt.target), hasClickToShow ? 'pointer' : '');
}
- if(!hoverChanged(gd, evt, oldhoverdata)) return;
+ // don't emit events if called manually
+ if(!evt.target || !hoverChanged(gd, evt, oldhoverdata)) return;
if(oldhoverdata) {
gd.emit('plotly_unhover', {
@@ -837,8 +850,142 @@ fx.loneUnhover = function(containerOrSelection) {
d3.select(containerOrSelection);
selection.selectAll('g.hovertext').remove();
+ selection.selectAll('.spikeline').remove();
};
+function createSpikelines(hoverData, opts) {
+ var hovermode = opts.hovermode;
+ var container = opts.container;
+ var c0 = hoverData[0];
+ var xa = c0.xa;
+ var ya = c0.ya;
+ var showX = xa.showspikes;
+ var showY = ya.showspikes;
+
+ // Remove old spikeline items
+ container.selectAll('.spikeline').remove();
+
+ if(hovermode !== 'closest' || !(showX || showY)) return;
+
+ var fullLayout = opts.fullLayout;
+ var xPoint = xa._offset + (c0.x0 + c0.x1) / 2;
+ var yPoint = ya._offset + (c0.y0 + c0.y1) / 2;
+ var contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor);
+ var dfltDashColor = tinycolor.readability(c0.color, contrastColor) < 1.5 ?
+ Color.contrast(contrastColor) : c0.color;
+
+ if(showY) {
+ var yMode = ya.spikemode;
+ var yThickness = ya.spikethickness;
+ var yColor = ya.spikecolor || dfltDashColor;
+ var yBB = ya._boundingBox;
+ var xEdge = ((yBB.left + yBB.right) / 2) < xPoint ? yBB.right : yBB.left;
+
+ if(yMode.indexOf('toaxis') !== -1 || yMode.indexOf('across') !== -1) {
+ var xBase = xEdge;
+ var xEndSpike = xPoint;
+ if(yMode.indexOf('across') !== -1) {
+ xBase = ya._counterSpan[0];
+ xEndSpike = ya._counterSpan[1];
+ }
+
+ // Background horizontal Line (to y-axis)
+ container.append('line')
+ .attr({
+ 'x1': xBase,
+ 'x2': xEndSpike,
+ 'y1': yPoint,
+ 'y2': yPoint,
+ 'stroke-width': yThickness + 2,
+ 'stroke': contrastColor
+ })
+ .classed('spikeline', true)
+ .classed('crisp', true);
+
+ // Foreground horizontal line (to y-axis)
+ container.append('line')
+ .attr({
+ 'x1': xBase,
+ 'x2': xEndSpike,
+ 'y1': yPoint,
+ 'y2': yPoint,
+ 'stroke-width': yThickness,
+ 'stroke': yColor,
+ 'stroke-dasharray': Drawing.dashStyle(ya.spikedash, yThickness)
+ })
+ .classed('spikeline', true)
+ .classed('crisp', true);
+ }
+ // Y axis marker
+ if(yMode.indexOf('marker') !== -1) {
+ container.append('circle')
+ .attr({
+ 'cx': xEdge + (ya.side !== 'right' ? yThickness : -yThickness),
+ 'cy': yPoint,
+ 'r': yThickness,
+ 'fill': yColor
+ })
+ .classed('spikeline', true);
+ }
+ }
+
+ if(showX) {
+ var xMode = xa.spikemode;
+ var xThickness = xa.spikethickness;
+ var xColor = xa.spikecolor || dfltDashColor;
+ var xBB = xa._boundingBox;
+ var yEdge = ((xBB.top + xBB.bottom) / 2) < yPoint ? xBB.bottom : xBB.top;
+
+ if(xMode.indexOf('toaxis') !== -1 || xMode.indexOf('across') !== -1) {
+ var yBase = yEdge;
+ var yEndSpike = yPoint;
+ if(xMode.indexOf('across') !== -1) {
+ yBase = xa._counterSpan[0];
+ yEndSpike = xa._counterSpan[1];
+ }
+
+ // Background vertical line (to x-axis)
+ container.append('line')
+ .attr({
+ 'x1': xPoint,
+ 'x2': xPoint,
+ 'y1': yBase,
+ 'y2': yEndSpike,
+ 'stroke-width': xThickness + 2,
+ 'stroke': contrastColor
+ })
+ .classed('spikeline', true)
+ .classed('crisp', true);
+
+ // Foreground vertical line (to x-axis)
+ container.append('line')
+ .attr({
+ 'x1': xPoint,
+ 'x2': xPoint,
+ 'y1': yBase,
+ 'y2': yEndSpike,
+ 'stroke-width': xThickness,
+ 'stroke': xColor,
+ 'stroke-dasharray': Drawing.dashStyle(xa.spikedash, xThickness)
+ })
+ .classed('spikeline', true)
+ .classed('crisp', true);
+ }
+
+ // X axis marker
+ if(xMode.indexOf('marker') !== -1) {
+ container.append('circle')
+ .attr({
+ 'cx': xPoint,
+ 'cy': yEdge - (xa.side !== 'top' ? xThickness : -xThickness),
+ 'r': xThickness,
+ 'fill': xColor
+ })
+ .classed('spikeline', true);
+ }
+ }
+}
+
function createHoverText(hoverData, opts) {
var hovermode = opts.hovermode,
rotateLabels = opts.rotateLabels,
@@ -1359,9 +1506,7 @@ function alignHoverText(hoverLabels, rotateLabels) {
}
function hoverChanged(gd, evt, oldhoverdata) {
- // don't emit any events if nothing changed or
- // if fx.hover was called manually
- if(!evt.target) return false;
+ // don't emit any events if nothing changed
if(!oldhoverdata || oldhoverdata.length !== gd._hoverdata.length) return true;
for(var i = oldhoverdata.length - 1; i >= 0; i--) {
diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js
index f826ca522ba..f85f3bdf551 100644
--- a/src/plots/cartesian/layout_attributes.js
+++ b/src/plots/cartesian/layout_attributes.js
@@ -10,6 +10,7 @@
var fontAttrs = require('../font_attributes');
var colorAttrs = require('../../components/color/attributes');
+var dash = require('../../components/drawing/attributes').dash;
var extendFlat = require('../../lib/extend').extendFlat;
var constants = require('./constants');
@@ -279,6 +280,45 @@ module.exports = {
role: 'style',
description: 'Determines whether or not the tick labels are drawn.'
},
+ showspikes: {
+ valType: 'boolean',
+ dflt: false,
+ role: 'style',
+ description: [
+ 'Determines whether or not spikes (aka droplines) are drawn for this axis.',
+ 'Note: This only takes affect when hovermode = closest'
+ ].join(' ')
+ },
+ spikecolor: {
+ valType: 'color',
+ dflt: null,
+ role: 'style',
+ description: 'Sets the spike color. If undefined, will use the series color'
+ },
+ spikethickness: {
+ valType: 'number',
+ dflt: 3,
+ role: 'style',
+ description: 'Sets the width (in px) of the zero line.'
+ },
+ spikedash: extendFlat({}, dash, {dflt: 'dash'}),
+ spikemode: {
+ valType: 'flaglist',
+ flags: ['toaxis', 'across', 'marker'],
+ role: 'style',
+ dflt: 'toaxis',
+ description: [
+ 'Determines the drawing mode for the spike line',
+ 'If *toaxis*, the line is drawn from the data point to the axis the ',
+ 'series is plotted on.',
+
+ 'If *across*, the line is drawn across the entire plot area, and',
+ 'supercedes *toaxis*.',
+
+ 'If *marker*, then a marker dot is drawn on the axis the series is',
+ 'plotted on'
+ ].join(' ')
+ },
tickfont: extendFlat({}, fontAttrs, {
description: 'Sets the tick font.'
}),
diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js
index 468e685234b..871b9c6520c 100644
--- a/src/plots/cartesian/layout_defaults.js
+++ b/src/plots/cartesian/layout_defaults.js
@@ -173,6 +173,14 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut);
+ var showSpikes = coerce('showspikes');
+ if(showSpikes) {
+ coerce('spikecolor');
+ coerce('spikethickness');
+ coerce('spikedash');
+ coerce('spikemode');
+ }
+
var positioningOptions = {
letter: axLetter,
counterAxes: counterAxes[axLetter],
diff --git a/src/traces/contour/attributes.js b/src/traces/contour/attributes.js
index ee547aa5dbe..069a965af9c 100644
--- a/src/traces/contour/attributes.js
+++ b/src/traces/contour/attributes.js
@@ -12,6 +12,7 @@ var heatmapAttrs = require('../heatmap/attributes');
var scatterAttrs = require('../scatter/attributes');
var colorscaleAttrs = require('../../components/colorscale/attributes');
var colorbarAttrs = require('../../components/colorbar/attributes');
+var dash = require('../../components/drawing/attributes').dash;
var extendFlat = require('../../lib/extend').extendFlat;
var scatterLineAttrs = scatterAttrs.line;
@@ -118,7 +119,7 @@ module.exports = extendFlat({}, {
].join(' ')
}),
width: scatterLineAttrs.width,
- dash: scatterLineAttrs.dash,
+ dash: dash,
smoothing: extendFlat({}, scatterLineAttrs.smoothing, {
description: [
'Sets the amount of smoothing for the contour lines,',
diff --git a/src/traces/ohlc/attributes.js b/src/traces/ohlc/attributes.js
index 02d50c76486..938a1d1c81a 100644
--- a/src/traces/ohlc/attributes.js
+++ b/src/traces/ohlc/attributes.js
@@ -11,6 +11,7 @@
var Lib = require('../../lib');
var scatterAttrs = require('../scatter/attributes');
+var dash = require('../../components/drawing/attributes').dash;
var INCREASING_COLOR = '#3D9970';
var DECREASING_COLOR = '#FF4136';
@@ -38,9 +39,9 @@ var directionAttrs = {
},
line: {
- color: Lib.extendFlat({}, lineAttrs.color),
- width: Lib.extendFlat({}, lineAttrs.width),
- dash: Lib.extendFlat({}, lineAttrs.dash),
+ color: lineAttrs.color,
+ width: lineAttrs.width,
+ dash: dash,
}
};
@@ -87,9 +88,9 @@ module.exports = {
'`decreasing.line.width`.'
].join(' ')
}),
- dash: Lib.extendFlat({}, lineAttrs.dash, {
+ dash: Lib.extendFlat({}, dash, {
description: [
- lineAttrs.dash,
+ dash.description,
'Note that this style setting can also be set per',
'direction via `increasing.line.dash` and',
'`decreasing.line.dash`.'
diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js
index 38b3015a4bb..d1bc72ba14c 100644
--- a/src/traces/scatter/attributes.js
+++ b/src/traces/scatter/attributes.js
@@ -11,6 +11,7 @@
var colorAttributes = require('../../components/colorscale/color_attributes');
var errorBarAttrs = require('../../components/errorbars/attributes');
var colorbarAttrs = require('../../components/colorbar/attributes');
+var dash = require('../../components/drawing/attributes').dash;
var Drawing = require('../../components/drawing');
var constants = require('./constants');
@@ -163,20 +164,7 @@ module.exports = {
'*0* corresponds to no smoothing (equivalent to a *linear* shape).'
].join(' ')
},
- dash: {
- valType: 'string',
- // string type usually doesn't take values... this one should really be
- // a special type or at least a special coercion function, from the GUI
- // you only get these values but elsewhere the user can supply a list of
- // dash lengths in px, and it will be honored
- values: ['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'],
- dflt: 'solid',
- role: 'style',
- description: [
- 'Sets the style of the lines. Set to a dash string type',
- 'or a dash length in px.'
- ].join(' ')
- },
+ dash: dash,
simplify: {
valType: 'boolean',
dflt: true,
diff --git a/src/traces/scatter/line_defaults.js b/src/traces/scatter/line_defaults.js
index f0fc1660492..176cbaccc2a 100644
--- a/src/traces/scatter/line_defaults.js
+++ b/src/traces/scatter/line_defaults.js
@@ -13,7 +13,7 @@ var hasColorscale = require('../../components/colorscale/has_colorscale');
var colorscaleDefaults = require('../../components/colorscale/defaults');
-module.exports = function lineDefaults(traceIn, traceOut, defaultColor, layout, coerce) {
+module.exports = function lineDefaults(traceIn, traceOut, defaultColor, layout, coerce, opts) {
var markerColor = (traceIn.marker || {}).color;
coerce('line.color', defaultColor);
@@ -27,5 +27,5 @@ module.exports = function lineDefaults(traceIn, traceOut, defaultColor, layout,
}
coerce('line.width');
- coerce('line.dash');
+ if(!(opts || {}).noDash) coerce('line.dash');
};
diff --git a/src/traces/scatter/marker_defaults.js b/src/traces/scatter/marker_defaults.js
index 3211d62426e..65560aecb75 100644
--- a/src/traces/scatter/marker_defaults.js
+++ b/src/traces/scatter/marker_defaults.js
@@ -16,7 +16,7 @@ var colorscaleDefaults = require('../../components/colorscale/defaults');
var subTypes = require('./subtypes');
-module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce) {
+module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce, opts) {
var isBubble = subTypes.isBubble(traceIn),
lineColor = (traceIn.line || {}).color,
defaultMLC;
@@ -33,22 +33,24 @@ module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout
colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.', cLetter: 'c'});
}
- // if there's a line with a different color than the marker, use
- // that line color as the default marker line color
- // (except when it's an array)
- // mostly this is for transparent markers to behave nicely
- if(lineColor && !Array.isArray(lineColor) && (traceOut.marker.color !== lineColor)) {
- defaultMLC = lineColor;
+ if(!(opts || {}).noLine) {
+ // if there's a line with a different color than the marker, use
+ // that line color as the default marker line color
+ // (except when it's an array)
+ // mostly this is for transparent markers to behave nicely
+ if(lineColor && !Array.isArray(lineColor) && (traceOut.marker.color !== lineColor)) {
+ defaultMLC = lineColor;
+ }
+ else if(isBubble) defaultMLC = Color.background;
+ else defaultMLC = Color.defaultLine;
+
+ coerce('marker.line.color', defaultMLC);
+ if(hasColorscale(traceIn, 'marker.line')) {
+ colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.line.', cLetter: 'c'});
+ }
+
+ coerce('marker.line.width', isBubble ? 1 : 0);
}
- else if(isBubble) defaultMLC = Color.background;
- else defaultMLC = Color.defaultLine;
-
- coerce('marker.line.color', defaultMLC);
- if(hasColorscale(traceIn, 'marker.line')) {
- colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.line.', cLetter: 'c'});
- }
-
- coerce('marker.line.width', isBubble ? 1 : 0);
if(isBubble) {
coerce('marker.sizeref');
diff --git a/src/traces/scatter3d/attributes.js b/src/traces/scatter3d/attributes.js
index ac0b957be63..7a74fd5b256 100644
--- a/src/traces/scatter3d/attributes.js
+++ b/src/traces/scatter3d/attributes.js
@@ -11,6 +11,7 @@
var scatterAttrs = require('../scatter/attributes');
var colorAttributes = require('../../components/colorscale/color_attributes');
var errorBarAttrs = require('../../components/errorbars/attributes');
+var DASHES = require('../../constants/gl3d_dashes');
var MARKER_SYMBOLS = require('../../constants/gl_markers');
var extendFlat = require('../../lib/extend').extendFlat;
@@ -114,7 +115,13 @@ module.exports = {
connectgaps: scatterAttrs.connectgaps,
line: extendFlat({}, {
width: scatterLineAttrs.width,
- dash: scatterLineAttrs.dash,
+ dash: {
+ valType: 'enumerated',
+ values: Object.keys(DASHES),
+ dflt: 'solid',
+ role: 'style',
+ description: 'Sets the dash style of the lines.'
+ },
showscale: {
valType: 'boolean',
role: 'info',
diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js
index 1b5ddeffc19..a341110e24f 100644
--- a/src/traces/scattergeo/attributes.js
+++ b/src/traces/scattergeo/attributes.js
@@ -11,6 +11,7 @@
var scatterAttrs = require('../scatter/attributes');
var plotAttrs = require('../../plots/attributes');
var colorAttributes = require('../../components/colorscale/color_attributes');
+var dash = require('../../components/drawing/attributes').dash;
var extendFlat = require('../../lib/extend').extendFlat;
@@ -79,7 +80,7 @@ module.exports = {
line: {
color: scatterLineAttrs.color,
width: scatterLineAttrs.width,
- dash: scatterLineAttrs.dash
+ dash: dash
},
connectgaps: scatterAttrs.connectgaps,
diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js
index 1518093db19..c061f1141aa 100644
--- a/src/traces/scattermapbox/attributes.js
+++ b/src/traces/scattermapbox/attributes.js
@@ -61,10 +61,10 @@ module.exports = {
line: {
color: lineAttrs.color,
- width: lineAttrs.width,
+ width: lineAttrs.width
// TODO
- dash: lineAttrs.dash
+ // dash: dash
},
connectgaps: scatterAttrs.connectgaps,
diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js
index ad884eb7f32..f7a51c65e95 100644
--- a/src/traces/scattermapbox/defaults.js
+++ b/src/traces/scattermapbox/defaults.js
@@ -18,7 +18,6 @@ var handleTextDefaults = require('../scatter/text_defaults');
var handleFillColorDefaults = require('../scatter/fillcolor_defaults');
var attributes = require('./attributes');
-var scatterAttrs = require('../scatter/attributes');
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
@@ -26,15 +25,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}
- function coerceMarker(attr, dflt) {
- var attrs = (attr.indexOf('.line') === -1) ? attributes : scatterAttrs;
-
- // use 'scatter' attributes for 'marker.line.' attr,
- // so that we can reuse the scatter marker defaults
-
- return Lib.coerce(traceIn, traceOut, attrs, attr, dflt);
- }
-
var len = handleLonLatDefaults(traceIn, traceOut, coerce);
if(!len) {
traceOut.visible = false;
@@ -46,16 +36,18 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerce('mode');
if(subTypes.hasLines(traceOut)) {
- handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce);
+ handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noDash: true});
coerce('connectgaps');
}
if(subTypes.hasMarkers(traceOut)) {
- handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerceMarker);
+ handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noLine: true});
// array marker.size and marker.color are only supported with circles
var marker = traceOut.marker;
+ // we need mock marker.line object to make legends happy
+ marker.line = {width: 0};
if(marker.symbol !== 'circle') {
if(Array.isArray(marker.size)) marker.size = marker.size[0];
diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js
index b0c0d4a2acf..4e8c36c48a8 100644
--- a/src/traces/scatterternary/attributes.js
+++ b/src/traces/scatterternary/attributes.js
@@ -12,6 +12,7 @@ var scatterAttrs = require('../scatter/attributes');
var plotAttrs = require('../../plots/attributes');
var colorAttributes = require('../../components/colorscale/color_attributes');
var colorbarAttrs = require('../../components/colorbar/attributes');
+var dash = require('../../components/drawing/attributes').dash;
var extendFlat = require('../../lib/extend').extendFlat;
@@ -88,7 +89,7 @@ module.exports = {
line: {
color: scatterLineAttrs.color,
width: scatterLineAttrs.width,
- dash: scatterLineAttrs.dash,
+ dash: dash,
shape: extendFlat({}, scatterLineAttrs.shape,
{values: ['linear', 'spline']}),
smoothing: scatterLineAttrs.smoothing
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index b8830036e8e..eeb0a665743 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -9,6 +9,7 @@ var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var mouseEvent = require('../assets/mouse_event');
var click = require('../assets/click');
+var delay = require('../assets/delay');
var doubleClick = require('../assets/double_click');
var fail = require('../assets/fail_test');
@@ -532,6 +533,44 @@ describe('hover info', function() {
expect(hovers.size()).toEqual(0);
});
});
+
+ describe('hover events', function() {
+ var data = [{x: [1, 2, 3], y: [1, 3, 2], type: 'bar'}];
+ var layout = {width: 600, height: 400};
+ var gd;
+
+ beforeEach(function(done) {
+ gd = createGraphDiv();
+ Plotly.plot(gd, data, layout).then(done);
+ });
+
+ it('should emit events only if the event looks user-driven', function(done) {
+ var hoverHandler = jasmine.createSpy();
+ gd.on('plotly_hover', hoverHandler);
+
+ var gdBB = gd.getBoundingClientRect();
+ var event = {clientX: gdBB.left + 300, clientY: gdBB.top + 200};
+
+ Promise.resolve().then(function() {
+ Fx.hover(gd, event, 'xy');
+ })
+ .then(delay(constants.HOVERMINTIME * 1.1))
+ .then(function() {
+ Fx.unhover(gd);
+ })
+ .then(function() {
+ expect(hoverHandler).not.toHaveBeenCalled();
+ var dragger = gd.querySelector('.nsewdrag');
+
+ Fx.hover(gd, Lib.extendFlat({target: dragger}, event), 'xy');
+ })
+ .then(function() {
+ expect(hoverHandler).toHaveBeenCalledTimes(1);
+ })
+ .catch(fail)
+ .then(done);
+ });
+ });
});
describe('hover info on stacked subplots', function() {
diff --git a/test/jasmine/tests/hover_spikeline_test.js b/test/jasmine/tests/hover_spikeline_test.js
new file mode 100644
index 00000000000..f8651bbc7ed
--- /dev/null
+++ b/test/jasmine/tests/hover_spikeline_test.js
@@ -0,0 +1,43 @@
+var d3 = require('d3');
+
+var Plotly = require('@lib/index');
+var Fx = require('@src/plots/cartesian/graph_interact');
+var Lib = require('@src/lib');
+
+var createGraphDiv = require('../assets/create_graph_div');
+var destroyGraphDiv = require('../assets/destroy_graph_div');
+
+describe('spikeline', function() {
+ 'use strict';
+
+ var mock = require('@mocks/19.json');
+
+ afterEach(destroyGraphDiv);
+
+ describe('hover', function() {
+ var mockCopy = Lib.extendDeep({}, mock);
+
+ mockCopy.layout.xaxis.showspikes = true;
+ mockCopy.layout.xaxis.spikemode = 'toaxis';
+ mockCopy.layout.yaxis.showspikes = true;
+ mockCopy.layout.yaxis.spikemode = 'toaxis+marker';
+ mockCopy.layout.xaxis2.showspikes = true;
+ mockCopy.layout.xaxis2.spikemode = 'toaxis';
+ mockCopy.layout.hovermode = 'closest';
+ beforeEach(function(done) {
+ Plotly.plot(createGraphDiv(), mockCopy.data, mockCopy.layout).then(done);
+ });
+
+ it('draws lines and markers on enabled axes', function() {
+ Fx.hover('graph', {xval: 2, yval: 3}, 'xy');
+ expect(d3.selectAll('line.spikeline').size()).toEqual(4);
+ expect(d3.selectAll('circle.spikeline').size()).toEqual(1);
+ });
+
+ it('doesn\'t draw lines and markers on disabled axes', function() {
+ Fx.hover('graph', {xval: 30, yval: 40}, 'x2y2');
+ expect(d3.selectAll('line.spikeline').size()).toEqual(2);
+ expect(d3.selectAll('circle.spikeline').size()).toEqual(0);
+ });
+ });
+});
diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js
index b3be9a51b43..5133e5fc975 100644
--- a/test/jasmine/tests/modebar_test.js
+++ b/test/jasmine/tests/modebar_test.js
@@ -185,7 +185,7 @@ describe('ModeBar', function() {
['toImage', 'sendDataToCloud'],
['zoom2d', 'pan2d'],
['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'],
- ['hoverClosestCartesian', 'hoverCompareCartesian']
+ ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']
]);
var gd = getMockGraphInfo();
@@ -203,7 +203,7 @@ describe('ModeBar', function() {
['toImage', 'sendDataToCloud'],
['zoom2d', 'pan2d', 'select2d', 'lasso2d'],
['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'],
- ['hoverClosestCartesian', 'hoverCompareCartesian']
+ ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']
]);
var gd = getMockGraphInfo();
@@ -225,7 +225,7 @@ describe('ModeBar', function() {
it('creates mode bar (cartesian fixed-axes version)', function() {
var buttons = getButtons([
['toImage', 'sendDataToCloud'],
- ['hoverClosestCartesian', 'hoverCompareCartesian']
+ ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']
]);
var gd = getMockGraphInfo();
@@ -412,7 +412,7 @@ describe('ModeBar', function() {
var buttons = getButtons([
['toImage', 'sendDataToCloud'],
['zoom2d', 'pan2d'],
- ['hoverClosestCartesian', 'hoverCompareCartesian']
+ ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']
]);
var gd = getMockGraphInfo();
@@ -544,7 +544,7 @@ describe('ModeBar', function() {
var modeBar = gd._fullLayout._modeBar;
expect(countGroups(modeBar)).toEqual(6);
- expect(countButtons(modeBar)).toEqual(10);
+ expect(countButtons(modeBar)).toEqual(11);
});
it('sets up buttons with modeBarButtonsToAdd and modeBarButtonToRemove (2)', function() {
@@ -564,7 +564,7 @@ describe('ModeBar', function() {
var modeBar = gd._fullLayout._modeBar;
expect(countGroups(modeBar)).toEqual(7);
- expect(countButtons(modeBar)).toEqual(12);
+ expect(countButtons(modeBar)).toEqual(13);
});
it('sets up buttons with fully custom modeBarButtons', function() {
@@ -612,7 +612,7 @@ describe('ModeBar', function() {
});
describe('modebar on clicks', function() {
- var gd, modeBar;
+ var gd, modeBar, buttonClosest, buttonCompare, buttonToggle, hovermodeButtons;
beforeAll(function() {
jasmine.addMatchers(customMatchers);
@@ -685,6 +685,10 @@ describe('ModeBar', function() {
gd = createGraphDiv();
Plotly.plot(gd, mockData, mockLayout).then(function() {
modeBar = gd._fullLayout._modeBar;
+ buttonToggle = selectButton(modeBar, 'toggleSpikelines');
+ buttonCompare = selectButton(modeBar, 'hoverCompareCartesian');
+ buttonClosest = selectButton(modeBar, 'hoverClosestCartesian');
+ hovermodeButtons = [buttonCompare, buttonClosest];
done();
});
});
@@ -758,21 +762,53 @@ describe('ModeBar', function() {
});
describe('buttons hoverCompareCartesian and hoverClosestCartesian ', function() {
- it('should update layout hovermode', function() {
- var buttonCompare = selectButton(modeBar, 'hoverCompareCartesian'),
- buttonClosest = selectButton(modeBar, 'hoverClosestCartesian'),
- buttons = [buttonCompare, buttonClosest];
+ it('should update layout hovermode', function() {
expect(gd._fullLayout.hovermode).toBe('x');
- assertActive(buttons, buttonCompare);
+ assertActive(hovermodeButtons, buttonCompare);
buttonClosest.click();
expect(gd._fullLayout.hovermode).toBe('closest');
- assertActive(buttons, buttonClosest);
+ assertActive(hovermodeButtons, buttonClosest);
buttonCompare.click();
expect(gd._fullLayout.hovermode).toBe('x');
- assertActive(buttons, buttonCompare);
+ assertActive(hovermodeButtons, buttonCompare);
+ });
+ });
+
+ describe('button toggleSpikelines', function() {
+ it('should update layout hovermode', function() {
+ expect(gd._fullLayout.hovermode).toBe('x');
+ assertActive(hovermodeButtons, buttonCompare);
+
+ buttonToggle.click();
+ expect(gd._fullLayout.hovermode).toBe('closest');
+ assertActive(hovermodeButtons, buttonClosest);
+ });
+ it('should makes spikelines visible', function() {
+ buttonToggle.click();
+ expect(gd._fullLayout._cartesianSpikesEnabled).toBe('on');
+
+ buttonToggle.click();
+ expect(gd._fullLayout._cartesianSpikesEnabled).toBe('off');
+ });
+ it('should become disabled when hovermode is switched off closest', function() {
+ buttonToggle.click();
+ expect(gd._fullLayout._cartesianSpikesEnabled).toBe('on');
+
+ buttonCompare.click();
+ expect(gd._fullLayout._cartesianSpikesEnabled).toBe('off');
+ });
+ it('should be re-enabled when hovermode is set to closest if it was previously on', function() {
+ buttonToggle.click();
+ expect(gd._fullLayout._cartesianSpikesEnabled).toBe('on');
+
+ buttonCompare.click();
+ expect(gd._fullLayout._cartesianSpikesEnabled).toBe('off');
+
+ buttonClosest.click();
+ expect(gd._fullLayout._cartesianSpikesEnabled).toBe('on');
});
});
});