diff --git a/src/components/fx/constants.js b/src/components/fx/constants.js index 48c9aa7ebba..0be903480f7 100644 --- a/src/components/fx/constants.js +++ b/src/components/fx/constants.js @@ -9,9 +9,6 @@ 'use strict'; module.exports = { - // max pixels away from mouse to allow a point to highlight - MAXDIST: 20, - // hover labels for multiple horizontal bars get tilted by this angle YANGLE: 60, diff --git a/src/components/fx/helpers.js b/src/components/fx/helpers.js index db2696182f2..d966a6d4a57 100644 --- a/src/components/fx/helpers.js +++ b/src/components/fx/helpers.js @@ -9,7 +9,6 @@ 'use strict'; var Lib = require('../../lib'); -var constants = require('./constants'); // look for either subplot or xaxis and yaxis attributes exports.getSubplot = function getSubplot(trace) { @@ -62,19 +61,16 @@ exports.getClosest = function getClosest(cd, distfn, pointData) { return pointData; }; -// for bar charts and others with finite-size objects: you must be inside -// it to see its hover info, so distance is infinite outside. -// But make distance inside be at least 1/4 MAXDIST, and a little bigger -// for bigger bars, to prioritize scatter and smaller bars over big bars -// -// note that for closest mode, two inbox's will get added in quadrature -// args are (signed) difference from the two opposite edges -// count one edge as in, so that over continuous ranges you never get a gap -exports.inbox = function inbox(v0, v1) { - if(v0 * v1 < 0 || v0 === 0) { - return constants.MAXDIST * (0.6 - 0.3 / Math.max(3, Math.abs(v0 - v1))); - } - return Infinity; +/* + * pseudo-distance function for hover effects on areas: inside the region + * distance is finite (`passVal`), outside it's Infinity. + * + * @param {number} v0: signed difference between the current position and the left edge + * @param {number} v1: signed difference between the current position and the right edge + * @param {number} passVal: the value to return on success + */ +exports.inbox = function inbox(v0, v1, passVal) { + return (v0 * v1 < 0 || v0 === 0) ? passVal : Infinity; }; exports.quadrature = function quadrature(dx, dy) { diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 32a3d0d980e..3d326d007cc 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -144,8 +144,7 @@ exports.loneHover = function loneHover(hoverItem, opts) { rotateLabels: false, bgColor: opts.bgColor || Color.background, container: container3, - outerContainer: outerContainer3, - hoverdistance: constants.MAXDIST + outerContainer: outerContainer3 }; var hoverLabel = createHoverText([pointData], fullOpts, opts.gd); @@ -162,9 +161,10 @@ function _hover(gd, evt, subplot, noHoverEvent) { // use those instead of finding overlayed plots var subplots = Array.isArray(subplot) ? subplot : [subplot]; - var fullLayout = gd._fullLayout, - plots = fullLayout._plots || [], - plotinfo = plots[subplot]; + var fullLayout = gd._fullLayout; + var plots = fullLayout._plots || []; + var plotinfo = plots[subplot]; + var hasCartesian = fullLayout._has('cartesian'); // list of all overlaid subplots to look at if(plotinfo) { @@ -351,9 +351,29 @@ function _hover(gd, evt, subplot, noHoverEvent) { trace: trace, xa: xaArray[subploti], ya: yaArray[subploti], + + // max distances for hover and spikes - for points that want to show but do not + // want to override other points, set distance/spikeDistance equal to max*Distance + // and it will not get filtered out but it will be guaranteed to have a greater + // distance than any point that calculated a real distance. + maxHoverDistance: hoverdistance, + maxSpikeDistance: spikedistance, + // point properties - override all of these index: false, // point index in trace - only used by plotly.js hoverdata consumers distance: Math.min(distance, hoverdistance), // pixel distance or pseudo-distance + + // distance/pseudo-distance for spikes. This distance should always be calculated + // as if in "closest" mode, and should only be set if this point should + // generate a spike. + spikeDistance: Infinity, + + // in some cases the spikes have different positioning from the hover label + // they don't need x0/x1, just one position + xSpike: undefined, + ySpike: undefined, + + // where and how to display the hover label color: Color.defaultLine, // trace color name: trace.name, x0: undefined, @@ -418,7 +438,8 @@ function _hover(gd, evt, subplot, noHoverEvent) { } // in closest mode, remove any existing (farther) points - // and don't look any farther than this latest point (or points, if boxes) + // and don't look any farther than this latest point (or points, some + // traces like box & violin make multiple hover labels at once) if(hovermode === 'closest' && hoverData.length > closedataPreviousLength) { hoverData.splice(0, closedataPreviousLength); distance = hoverData[0].distance; @@ -426,12 +447,19 @@ function _hover(gd, evt, subplot, noHoverEvent) { // Now if there is range to look in, find the points to draw the spikelines // Do it only if there is no hoverData - if(fullLayout._has('cartesian') && (spikedistance !== 0)) { + if(hasCartesian && (spikedistance !== 0)) { if(hoverData.length === 0) { pointData.distance = spikedistance; pointData.index = false; var closestPoints = trace._module.hoverPoints(pointData, xval, yval, 'closest', fullLayout._hoverlayer); if(closestPoints) { + closestPoints = closestPoints.filter(function(point) { + // some hover points, like scatter fills, do not allow spikes, + // so will generate a hover point but without a valid spikeDistance + return point.spikeDistance <= spikedistance; + }); + } + if(closestPoints && closestPoints.length) { var tmpPoint; var closestVPoints = closestPoints.filter(function(point) { return point.xa.showspikes; @@ -439,8 +467,8 @@ function _hover(gd, evt, subplot, noHoverEvent) { if(closestVPoints.length) { var closestVPt = closestVPoints[0]; if(isNumeric(closestVPt.x0) && isNumeric(closestVPt.y0)) { - tmpPoint = fillClosestPoint(closestVPt); - if(!spikePoints.vLinePoint || (spikePoints.vLinePoint.distance > tmpPoint.distance)) { + tmpPoint = fillSpikePoint(closestVPt); + if(!spikePoints.vLinePoint || (spikePoints.vLinePoint.spikeDistance > tmpPoint.spikeDistance)) { spikePoints.vLinePoint = tmpPoint; } } @@ -452,8 +480,8 @@ function _hover(gd, evt, subplot, noHoverEvent) { if(closestHPoints.length) { var closestHPt = closestHPoints[0]; if(isNumeric(closestHPt.x0) && isNumeric(closestHPt.y0)) { - tmpPoint = fillClosestPoint(closestHPt); - if(!spikePoints.hLinePoint || (spikePoints.hLinePoint.distance > tmpPoint.distance)) { + tmpPoint = fillSpikePoint(closestHPt); + if(!spikePoints.hLinePoint || (spikePoints.hLinePoint.spikeDistance > tmpPoint.spikeDistance)) { spikePoints.hLinePoint = tmpPoint; } } @@ -464,47 +492,28 @@ function _hover(gd, evt, subplot, noHoverEvent) { } function selectClosestPoint(pointsData, spikedistance) { - if(!pointsData.length) return null; - var resultPoint; - var pointsDistances = pointsData.map(function(point, index) { - var xa = point.xa, - ya = point.ya, - xpx = xa.c2p(xval), - ypx = ya.c2p(yval), - dxy = function(point) { - var rad = point.kink, - dx = (point.x1 + point.x0) / 2 - xpx, - dy = (point.y1 + point.y0) / 2 - ypx; - return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad); - }; - var distance = dxy(point); - return {distance: distance, index: index}; - }); - pointsDistances = pointsDistances - .filter(function(point) { - return point.distance <= spikedistance; - }) - .sort(function(a, b) { - return a.distance - b.distance; - }); - if(pointsDistances.length) { - resultPoint = pointsData[pointsDistances[0].index]; - } else { - resultPoint = null; + var resultPoint = null; + var minDistance = Infinity; + var thisSpikeDistance; + for(var i = 0; i < pointsData.length; i++) { + thisSpikeDistance = pointsData[i].spikeDistance; + if(thisSpikeDistance < minDistance && thisSpikeDistance <= spikedistance) { + resultPoint = pointsData[i]; + minDistance = thisSpikeDistance; + } } return resultPoint; } - function fillClosestPoint(point) { + function fillSpikePoint(point) { if(!point) return null; return { xa: point.xa, ya: point.ya, - x0: point.x0, - x1: point.x1, - y0: point.y0, - y1: point.y1, + x: point.xSpike !== undefined ? point.xSpike : (point.x0 + point.x1) / 2, + y: point.ySpike !== undefined ? point.ySpike : (point.y0 + point.y1) / 2, distance: point.distance, + spikeDistance: point.spikeDistance, curveNumber: point.trace.index, color: point.color, pointNumber: point.index @@ -525,26 +534,26 @@ function _hover(gd, evt, subplot, noHoverEvent) { gd._spikepoints = newspikepoints; // Now if it is not restricted by spikedistance option, set the points to draw the spikelines - if(fullLayout._has('cartesian') && (spikedistance !== 0)) { + if(hasCartesian && (spikedistance !== 0)) { if(hoverData.length !== 0) { var tmpHPointData = hoverData.filter(function(point) { return point.ya.showspikes; }); var tmpHPoint = selectClosestPoint(tmpHPointData, spikedistance); - spikePoints.hLinePoint = fillClosestPoint(tmpHPoint); + spikePoints.hLinePoint = fillSpikePoint(tmpHPoint); var tmpVPointData = hoverData.filter(function(point) { return point.xa.showspikes; }); var tmpVPoint = selectClosestPoint(tmpVPointData, spikedistance); - spikePoints.vLinePoint = fillClosestPoint(tmpVPoint); + spikePoints.vLinePoint = fillSpikePoint(tmpVPoint); } } // if hoverData is empty check for the spikes to draw and quit if there are none if(hoverData.length === 0) { var result = dragElement.unhoverRaw(gd, evt); - if(fullLayout._has('cartesian') && ((spikePoints.hLinePoint !== null) || (spikePoints.vLinePoint !== null))) { + if(hasCartesian && ((spikePoints.hLinePoint !== null) || (spikePoints.vLinePoint !== null))) { if(spikesChanged(oldspikepoints)) { createSpikelines(spikePoints, spikelineOpts); } @@ -552,7 +561,7 @@ function _hover(gd, evt, subplot, noHoverEvent) { return result; } - if(fullLayout._has('cartesian')) { + if(hasCartesian) { if(spikesChanged(oldspikepoints)) { createSpikelines(spikePoints, spikelineOpts); } @@ -653,20 +662,31 @@ function createHoverText(hoverData, opts, gd) { // show the common label, if any, on the axis // never show a common label in array mode, // even if sometimes there could be one - var showCommonLabel = c0.distance <= opts.hoverdistance && - (hovermode === 'x' || hovermode === 'y'); + var showCommonLabel = ( + (t0 !== undefined) && + (c0.distance <= opts.hoverdistance) && + (hovermode === 'x' || hovermode === 'y') + ); // all hover traces hoverinfo must contain the hovermode // to have common labels - var i, traceHoverinfo; - for(i = 0; i < hoverData.length; i++) { - traceHoverinfo = hoverData[i].hoverinfo || hoverData[i].trace.hoverinfo; - var parts = Array.isArray(traceHoverinfo) ? traceHoverinfo : traceHoverinfo.split('+'); - if(parts.indexOf('all') === -1 && - parts.indexOf(hovermode) === -1) { - showCommonLabel = false; - break; + if(showCommonLabel) { + var i, traceHoverinfo; + var allHaveZ = true; + for(i = 0; i < hoverData.length; i++) { + if(allHaveZ && hoverData[i].zLabel === undefined) allHaveZ = false; + + traceHoverinfo = hoverData[i].hoverinfo || hoverData[i].trace.hoverinfo; + var parts = Array.isArray(traceHoverinfo) ? traceHoverinfo : traceHoverinfo.split('+'); + if(parts.indexOf('all') === -1 && + parts.indexOf(hovermode) === -1) { + showCommonLabel = false; + break; + } } + + // xyz labels put all info in their main label, so have no need of a common label + if(allHaveZ) showCommonLabel = false; } var commonLabel = container.selectAll('g.axistext') @@ -1170,7 +1190,9 @@ function cleanPoint(d, hovermode) { fill('fontColor', 'htc', 'hoverlabel.font.color'); fill('nameLength', 'hnl', 'hoverlabel.namelength'); - d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2; + d.posref = hovermode === 'y' ? + (d.xa._offset + (d.x0 + d.x1) / 2) : + (d.ya._offset + (d.y0 + d.y1) / 2); // then constrain all the positions to be on the plot d.x0 = Lib.constrain(d.x0, 0, d.xa._length); @@ -1262,8 +1284,8 @@ function createSpikelines(closestPoints, opts) { hLinePointX = evt.pointerX; hLinePointY = evt.pointerY; } else { - hLinePointX = xa._offset + (hLinePoint.x0 + hLinePoint.x1) / 2; - hLinePointY = ya._offset + (hLinePoint.y0 + hLinePoint.y1) / 2; + hLinePointX = xa._offset + hLinePoint.x; + hLinePointY = ya._offset + hLinePoint.y; } var dfltHLineColor = tinycolor.readability(hLinePoint.color, contrastColor) < 1.5 ? Color.contrast(contrastColor) : hLinePoint.color; @@ -1338,8 +1360,8 @@ function createSpikelines(closestPoints, opts) { vLinePointX = evt.pointerX; vLinePointY = evt.pointerY; } else { - vLinePointX = xa._offset + (vLinePoint.x0 + vLinePoint.x1) / 2; - vLinePointY = ya._offset + (vLinePoint.y0 + vLinePoint.y1) / 2; + vLinePointX = xa._offset + vLinePoint.x; + vLinePointY = ya._offset + vLinePoint.y; } var dfltVLineColor = tinycolor.readability(vLinePoint.color, contrastColor) < 1.5 ? Color.contrast(contrastColor) : vLinePoint.color; diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js index 1196cfec9f1..542be419e6d 100644 --- a/src/components/fx/layout_attributes.js +++ b/src/components/fx/layout_attributes.js @@ -46,7 +46,11 @@ module.exports = { editType: 'none', description: [ 'Sets the default distance (in pixels) to look for data', - 'to add hover labels (-1 means no cutoff, 0 means no looking for data)' + 'to add hover labels (-1 means no cutoff, 0 means no looking for data).', + 'This is only a real distance for hovering on point-like objects,', + 'like scatter points. For area-like objects (bars, scatter fills, etc)', + 'hovering is on inside the area and off outside, but these objects', + 'will not supersede hover on point-like objects in case of conflict.' ].join(' ') }, spikedistance: { @@ -57,7 +61,10 @@ module.exports = { editType: 'none', description: [ 'Sets the default distance (in pixels) to look for data to draw', - 'spikelines to (-1 means no cutoff, 0 means no looking for data).' + 'spikelines to (-1 means no cutoff, 0 means no looking for data).', + 'As with hoverdistance, distance does not apply to area-like objects.', + 'In addition, some objects can be hovered on but will not generate', + 'spikelines, such as scatter fills.' ].join(' ') }, hoverlabel: { diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js index 7d5b8a8dcc6..b3c5f2eab35 100644 --- a/src/traces/bar/hover.js +++ b/src/traces/bar/hover.js @@ -19,8 +19,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var trace = cd[0].trace; var t = cd[0].t; var isClosest = (hovermode === 'closest'); + var maxHoverDistance = pointData.maxHoverDistance; + var maxSpikeDistance = pointData.maxSpikeDistance; - var posVal, sizeVal, posLetter, sizeLetter, dx, dy; + var posVal, sizeVal, posLetter, sizeLetter, dx, dy, pRangeCalc; function thisBarMinPos(di) { return di[posLetter] - di.w / 2; } function thisBarMaxPos(di) { return di[posLetter] + di.w / 2; } @@ -49,15 +51,26 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { return Math.max(thisBarMaxPos(di), di.p + t.bardelta / 2); }; + function _positionFn(_minPos, _maxPos) { + // add a little to the pseudo-distance for wider bars, so that like scatter, + // if you are over two overlapping bars, the narrower one wins. + return Fx.inbox(_minPos - posVal, _maxPos - posVal, + maxHoverDistance + Math.min(1, Math.abs(_maxPos - _minPos) / pRangeCalc) - 1); + } + function positionFn(di) { - return Fx.inbox(minPos(di) - posVal, maxPos(di) - posVal); + return _positionFn(minPos(di), maxPos(di)); + } + + function thisBarPositionFn(di) { + return _positionFn(thisBarMinPos(di), thisBarMaxPos(di)); } function sizeFn(di) { // add a gradient so hovering near the end of a // bar makes it a little closer match - return Fx.inbox(di.b - sizeVal, di[sizeLetter] - sizeVal) + - (di[sizeLetter] - sizeVal) / (di[sizeLetter] - di.b); + return Fx.inbox(di.b - sizeVal, di[sizeLetter] - sizeVal, + maxHoverDistance + (di[sizeLetter] - sizeVal) / (di[sizeLetter] - di.b) - 1); } if(trace.orientation === 'h') { @@ -80,7 +93,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var pa = pointData[posLetter + 'a']; var sa = pointData[sizeLetter + 'a']; - var distfn = Fx.getDistanceFunction(hovermode, dx, dy); + pRangeCalc = Math.abs(pa.r2c(pa.range[1]) - pa.r2c(pa.range[0])); + + function dxy(di) { return (dx(di) + dy(di)) / 2; } + var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); Fx.getClosest(cd, distfn, pointData); // skip the rest (for this trace) if we didn't find a close point @@ -116,6 +132,12 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { pointData[posLetter + '1'] = pa.c2p(maxPos(di), true); pointData[posLetter + 'LabelVal'] = di.p; + // spikelines always want "closest" distance regardless of hovermode + pointData.spikeDistance = (sizeFn(di) + thisBarPositionFn(di)) / 2 + maxSpikeDistance - maxHoverDistance; + // they also want to point to the data value, regardless of where the label goes + // in case of bars shifted within groups + pointData[posLetter + 'Spike'] = pa.c2p(di.p, true); + fillHoverText(di, trace, pointData); ErrorBars.hoverInfo(di, trace, pointData); diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js index 3f5fb006fd8..3ff3fe024ef 100644 --- a/src/traces/box/hover.js +++ b/src/traces/box/hover.js @@ -54,31 +54,29 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) { var isViolin = trace.type === 'violin'; var closeBoxData = []; - var pLetter, vLetter, pAxis, vAxis, vVal, pVal, dx, dy; + var pLetter, vLetter, pAxis, vAxis, vVal, pVal, dx, dy, dPos, + hoverPseudoDistance, spikePseudoDistance; - // closest mode: handicap box plots a little relative to others - // adjust inbox w.r.t. to calculate box size - var boxDelta = (hovermode === 'closest' && !isViolin) ? 2.5 * t.bdPos : t.bdPos; + var boxDelta = t.bdPos; var shiftPos = function(di) { return di.pos + t.bPos - pVal; }; - var dPos; if(isViolin && trace.side !== 'both') { if(trace.side === 'positive') { dPos = function(di) { var pos = shiftPos(di); - return Fx.inbox(pos, pos + boxDelta); + return Fx.inbox(pos, pos + boxDelta, hoverPseudoDistance); }; } if(trace.side === 'negative') { dPos = function(di) { var pos = shiftPos(di); - return Fx.inbox(pos - boxDelta, pos); + return Fx.inbox(pos - boxDelta, pos, hoverPseudoDistance); }; } } else { dPos = function(di) { var pos = shiftPos(di); - return Fx.inbox(pos - boxDelta, pos + boxDelta); + return Fx.inbox(pos - boxDelta, pos + boxDelta, hoverPseudoDistance); }; } @@ -86,11 +84,11 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) { if(isViolin) { dVal = function(di) { - return Fx.inbox(di.span[0] - vVal, di.span[1] - vVal); + return Fx.inbox(di.span[0] - vVal, di.span[1] - vVal, hoverPseudoDistance); }; } else { dVal = function(di) { - return Fx.inbox(di.min - vVal, di.max - vVal); + return Fx.inbox(di.min - vVal, di.max - vVal, hoverPseudoDistance); }; } @@ -114,7 +112,13 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) { vAxis = ya; } - var distfn = Fx.getDistanceFunction(hovermode, dx, dy); + // if two boxes are overlaying, let the narrowest one win + var pseudoDistance = Math.min(1, boxDelta / Math.abs(pAxis.r2c(pAxis.range[1]) - pAxis.r2c(pAxis.range[0]))); + hoverPseudoDistance = pointData.maxHoverDistance - pseudoDistance; + spikePseudoDistance = pointData.maxSpikeDistance - pseudoDistance; + + function dxy(di) { return (dx(di) + dy(di)) / 2; } + var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); Fx.getClosest(cd, distfn, pointData); // skip the rest (for this trace) if we didn't find a close point @@ -135,6 +139,10 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) { Axes.tickText(pAxis, pAxis.c2l(di.pos), 'hover').text; pointData[pLetter + 'LabelVal'] = di.pos; + var spikePosAttr = pLetter + 'Spike'; + pointData.spikeDistance = dxy(di) * spikePseudoDistance / hoverPseudoDistance; + pointData[spikePosAttr] = pAxis.c2p(di.pos, true); + // box plots: each "point" gets many labels var usedVals = {}; var attrs = ['med', 'min', 'q1', 'q3', 'max']; @@ -164,8 +172,10 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) { if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') { pointData2[vLetter + 'err'] = di.sd; } - // only keep name on the first item (median) + // only keep name and spikes on the first item (median) pointData.name = ''; + pointData.spikeDistance = undefined; + pointData[spikePosAttr] = undefined; closeBoxData.push(pointData2); } @@ -229,8 +239,12 @@ function hoverOnPoints(pointData, xval, yval) { xLabelVal: pt.x, y0: yc - rad, y1: yc + rad, - yLabelVal: pt.y + yLabelVal: pt.y, + spikeDistance: pointData.distance }); + var pLetter = trace.orientation === 'h' ? 'y' : 'x'; + var pa = trace.orientation === 'h' ? ya : xa; + closePtData[pLetter + 'Spike'] = pa.c2p(di.pos, true); fillHoverText(pt, trace, closePtData); return closePtData; diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js index 7fc667616ac..21aa0d08145 100644 --- a/src/traces/heatmap/hover.js +++ b/src/traces/heatmap/hover.js @@ -13,12 +13,7 @@ var Fx = require('../../components/fx'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); -var MAXDIST = Fx.constants.MAXDIST; - module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLayer, contour) { - // never let a heatmap override another type as closest point - if(pointData.distance < MAXDIST) return; - var cd0 = pointData.cd[0]; var trace = cd0.trace; var xa = pointData.xa; @@ -50,8 +45,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay return; } } - else if(Fx.inbox(xval - x[0], xval - x[x.length - 1]) > MAXDIST || - Fx.inbox(yval - y[0], yval - y[y.length - 1]) > MAXDIST) { + else if(Fx.inbox(xval - x[0], xval - x[x.length - 1], 0) > 0 || + Fx.inbox(yval - y[0], yval - y[y.length - 1], 0) > 0) { return; } else { @@ -117,7 +112,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay return [Lib.extendFlat(pointData, { index: [ny, nx], // never let a 2D override 1D type as closest point - distance: MAXDIST + 10, + distance: pointData.maxHoverDistance, + spikeDistance: pointData.maxSpikeDistance, x0: x0, x1: x1, y0: y0, diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js index da970a64118..8ab0dac0f4b 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -15,8 +15,6 @@ var getTraceColor = require('./get_trace_color'); var Color = require('../../components/color'); var fillHoverText = require('./fill_hover_text'); -var MAXDIST = Fx.constants.MAXDIST; - module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var cd = pointData.cd; var trace = cd[0].trace; @@ -32,32 +30,32 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { // didn't find a point if(hoveron.indexOf('points') !== -1) { var dx = function(di) { - // dx and dy are used in compare modes - here we want to always - // prioritize the closest data point, at least as long as markers are - // the same size or nonexistent, but still try to prioritize small markers too. - var rad = Math.max(3, di.mrc || 0); - var kink = 1 - 1 / rad; - var dxRaw = Math.abs(xa.c2p(di.x) - xpx); - var d = (dxRaw < rad) ? (kink * dxRaw / rad) : (dxRaw - rad + kink); - return d; - }, - dy = function(di) { - var rad = Math.max(3, di.mrc || 0); - var kink = 1 - 1 / rad; - var dyRaw = Math.abs(ya.c2p(di.y) - ypx); - return (dyRaw < rad) ? (kink * dyRaw / rad) : (dyRaw - rad + kink); - }, - dxy = function(di) { - // scatter points: d.mrc is the calculated marker radius - // adjust the distance so if you're inside the marker it - // always will show up regardless of point size, but - // prioritize smaller points - var rad = Math.max(minRad, di.mrc || 0); - var dx = xa.c2p(di.x) - xpx; - var dy = ya.c2p(di.y) - ypx; - return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - minRad / rad); - }, - distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); + // dx and dy are used in compare modes - here we want to always + // prioritize the closest data point, at least as long as markers are + // the same size or nonexistent, but still try to prioritize small markers too. + var rad = Math.max(3, di.mrc || 0); + var kink = 1 - 1 / rad; + var dxRaw = Math.abs(xa.c2p(di.x) - xpx); + var d = (dxRaw < rad) ? (kink * dxRaw / rad) : (dxRaw - rad + kink); + return d; + }; + var dy = function(di) { + var rad = Math.max(3, di.mrc || 0); + var kink = 1 - 1 / rad; + var dyRaw = Math.abs(ya.c2p(di.y) - ypx); + return (dyRaw < rad) ? (kink * dyRaw / rad) : (dyRaw - rad + kink); + }; + var dxy = function(di) { + // scatter points: d.mrc is the calculated marker radius + // adjust the distance so if you're inside the marker it + // always will show up regardless of point size, but + // prioritize smaller points + var rad = Math.max(minRad, di.mrc || 0); + var dx = xa.c2p(di.x) - xpx; + var dy = ya.c2p(di.y) - ypx; + return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - minRad / rad); + }; + var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); Fx.getClosest(cd, distfn, pointData); @@ -65,10 +63,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { if(pointData.index !== false) { // the closest data point - var di = cd[pointData.index], - xc = xa.c2p(di.x, true), - yc = ya.c2p(di.y, true), - rad = di.mrc || 1; + var di = cd[pointData.index]; + var xc = xa.c2p(di.x, true); + var yc = ya.c2p(di.y, true); + var rad = di.mrc || 1; Lib.extendFlat(pointData, { color: getTraceColor(trace, di), @@ -81,7 +79,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { y1: yc + rad, yLabelVal: di.y, - kink: Math.max(minRad, di.mrc || 0) + spikeDistance: dxy(di) }); fillHoverText(di, trace, pointData); @@ -93,14 +91,15 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { // even if hoveron is 'fills', only use it if we have polygons too if(hoveron.indexOf('fills') !== -1 && trace._polygons) { - var polygons = trace._polygons, - polygonsIn = [], - inside = false, - xmin = Infinity, - xmax = -Infinity, - ymin = Infinity, - ymax = -Infinity, - i, j, polygon, pts, xCross, x0, x1, y0, y1; + var polygons = trace._polygons; + var polygonsIn = []; + var inside = false; + var xmin = Infinity; + var xmax = -Infinity; + var ymin = Infinity; + var ymax = -Infinity; + + var i, j, polygon, pts, xCross, x0, x1, y0, y1; for(i = 0; i < polygons.length; i++) { polygon = polygons[i]; @@ -158,7 +157,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { Lib.extendFlat(pointData, { // never let a 2D override 1D type as closest point - distance: MAXDIST + 10, + // also: no spikeDistance, it's not allowed for fills + distance: pointData.maxHoverDistance, x0: xmin, x1: xmax, y0: yAvg, diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index faf4759b088..0de79eeb5df 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -14,7 +14,6 @@ var ErrorBars = require('../../components/errorbars'); var extend = require('object-assign'); var Axes = require('../../plots/cartesian/axes'); var kdtree = require('kdgrass'); -var Fx = require('../../components/fx'); var subTypes = require('../scatter/subtypes'); var calcColorscales = require('../scatter/colorscale_calc'); var Drawing = require('../../components/drawing'); @@ -32,7 +31,6 @@ var arrayRange = require('array-range'); var fillHoverText = require('../scatter/fill_hover_text'); var isNumeric = require('fast-isnumeric'); -var MAXDIST = Fx.constants.MAXDIST; var SYMBOL_SDF_SIZE = 200; var SYMBOL_SIZE = 20; var SYMBOL_STROKE = SYMBOL_SIZE / 20; @@ -983,23 +981,24 @@ function plot(container, subplot, cdata) { } function hoverPoints(pointData, xval, yval, hovermode) { - var cd = pointData.cd, - stash = cd[0].t, - trace = cd[0].trace, - xa = pointData.xa, - ya = pointData.ya, - x = stash.rawx, - y = stash.rawy, - xpx = xa.c2p(xval), - ypx = ya.c2p(yval), - ids; + var cd = pointData.cd; + var stash = cd[0].t; + var trace = cd[0].trace; + var xa = pointData.xa; + var ya = pointData.ya; + var x = stash.rawx; + var y = stash.rawy; + var xpx = xa.c2p(xval); + var ypx = ya.c2p(yval); + var maxDistance = pointData.distance; + var ids; // FIXME: make sure this is a proper way to calc search radius if(stash.tree) { - var xl = xa.p2c(xpx - MAXDIST), - xr = xa.p2c(xpx + MAXDIST), - yl = ya.p2c(ypx - MAXDIST), - yr = ya.p2c(ypx + MAXDIST); + var xl = xa.p2c(xpx - maxDistance); + var xr = xa.p2c(xpx + maxDistance); + var yl = ya.p2c(ypx - maxDistance); + var yr = ya.p2c(ypx + maxDistance); if(hovermode === 'x') { ids = stash.tree.range( @@ -1021,14 +1020,17 @@ function hoverPoints(pointData, xval, yval, hovermode) { // pick the id closest to the point // note that point possibly may not be found - var min = MAXDIST, id, ptx, pty, i, dx, dy, dist; + var minDist = maxDistance; + var id, ptx, pty, i, dx, dy, dist, dxy; if(hovermode === 'x') { for(i = 0; i < ids.length; i++) { ptx = x[ids[i]]; dx = Math.abs(xa.c2p(ptx) - xpx); - if(dx < min) { - min = dx; + if(dx < minDist) { + minDist = dx; + dy = ya.c2p(y[ids[i]]) - ypx; + dxy = Math.sqrt(dx * dx + dy * dy); id = ids[i]; } } @@ -1037,11 +1039,12 @@ function hoverPoints(pointData, xval, yval, hovermode) { for(i = 0; i < ids.length; i++) { ptx = x[ids[i]]; pty = y[ids[i]]; - dx = xa.c2p(ptx) - xpx, dy = ya.c2p(pty) - ypx; + dx = xa.c2p(ptx) - xpx; + dy = ya.c2p(pty) - ypx; dist = Math.sqrt(dx * dx + dy * dy); - if(dist < min) { - min = dist; + if(dist < minDist) { + minDist = dxy = dist; id = ids[i]; } } @@ -1124,7 +1127,9 @@ function hoverPoints(pointData, xval, yval, hovermode) { y1: yc + rad, yLabelVal: di.y, - cd: fakeCd + cd: fakeCd, + distance: minDist, + spikeDistance: dxy }); if(di.htx) pointData.text = di.htx; diff --git a/src/traces/violin/hover.js b/src/traces/violin/hover.js index c6a6aee15fe..e33ca102b59 100644 --- a/src/traces/violin/hover.js +++ b/src/traces/violin/hover.js @@ -63,6 +63,14 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay kdePointData[pLetter + '1'] = pOnPath[1]; kdePointData[vLetter + '0'] = kdePointData[vLetter + '1'] = vValPx; kdePointData[vLetter + 'Label'] = vLetter + ': ' + Axes.hoverLabelText(vAxis, vVal) + ', ' + cd[0].t.labels.kde + ' ' + kdeVal.toFixed(3); + + // move the spike to the KDE point + kdePointData.spikeDistance = closeBoxData[0].spikeDistance; + var spikePosAttr = pLetter + 'Spike'; + kdePointData[spikePosAttr] = closeBoxData[0][spikePosAttr]; + closeBoxData[0].spikeDistance = undefined; + closeBoxData[0][spikePosAttr] = undefined; + closeData.push(kdePointData); violinLineAttrs = {stroke: pointData.color}; diff --git a/test/image/mocks/gl2d_line_select.json b/test/image/mocks/gl2d_line_select.json deleted file mode 100644 index 4ad466bf6a6..00000000000 --- a/test/image/mocks/gl2d_line_select.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": [{ - "type": "scattergl", - "mode": "lines", - "x": [1, 2, 3], - "y": [1, 2, 1] - }], - "layout": { - "dragmode": "select", - "showlegend": false, - "width": 400, - "height": 400 - } -} diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 756f678dcca..b9f988346bb 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -1292,7 +1292,8 @@ describe('bar hover', function() { cd: cd[0], trace: cd[0][0].trace, xa: subplot.xaxis, - ya: subplot.yaxis + ya: subplot.yaxis, + maxHoverDistance: 20 }; } diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js index d6e0a128c34..86afca3add6 100644 --- a/test/jasmine/tests/box_test.js +++ b/test/jasmine/tests/box_test.js @@ -266,6 +266,7 @@ describe('Test box hover:', function() { fig.layout.hovermode = 'x'; return fig; }, + pos: [215, 200], nums: ['median: 0.55', 'min: 0', 'q1: 0.3', 'q3: 0.6', 'max: 0.7'], name: ['radishes', '', '', '', ''], axis: 'day 1' diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index e88c8988383..cddd0ea8860 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1082,42 +1082,68 @@ describe('hover info on stacked subplots', function() { afterEach(destroyGraphDiv); describe('hover info on stacked subplots with shared x-axis', function() { - var mock = require('@mocks/stacked_coupled_subplots.json'); + var mock = Lib.extendDeep({}, + require('@mocks/stacked_coupled_subplots.json'), + {data: [ + // Tweak the mock so the higher subplot sometimes has points + // higher *within the subplot*, sometimes lower. + // This was the problem in #2370 + {}, {y: [100, 120, 100]} + ]}); + + var gd; beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + gd = createGraphDiv(); + Plotly.plot(gd, mock.data, mock.layout).then(done); }); - it('responds to hover', function() { - var gd = document.getElementById('graph'); - Plotly.Fx.hover(gd, {xval: 3}, ['xy', 'xy2', 'xy3']); + function _check(xval, ptSpec1, ptSpec2) { + Lib.clearThrottle(); + Plotly.Fx.hover(gd, {xval: xval}, ['xy', 'xy2', 'xy3']); - expect(gd._hoverdata.length).toEqual(2); + expect(gd._hoverdata.length).toBe(2); expect(gd._hoverdata[0]).toEqual(jasmine.objectContaining( { - curveNumber: 1, - pointNumber: 1, - x: 3, - y: 110 + curveNumber: ptSpec1[0], + pointNumber: ptSpec1[1], + x: xval, + y: ptSpec1[2] })); expect(gd._hoverdata[1]).toEqual(jasmine.objectContaining( { - curveNumber: 2, - pointNumber: 0, - x: 3, - y: 1000 + curveNumber: ptSpec2[0], + pointNumber: ptSpec2[1], + x: xval, + y: ptSpec2[2] })); assertHoverLabelContent({ // There should be 2 pts being hovered over, // in two different traces, one in each plot. - nums: ['110', '1000'], - name: ['trace 1', 'trace 2'], - // There should be a single label on the x-axis with the shared x value, 3' - axis: '3' + nums: [String(ptSpec1[2]), String(ptSpec2[2])], + name: [ptSpec1[3], ptSpec2[3]], + // There should be a single label on the x-axis with the shared x value + axis: String(xval) + }); + + // ensure the hover label bounding boxes don't overlap, except a little margin of 5 px + // testing #2370 + var bBoxes = []; + d3.selectAll('g.hovertext').each(function() { + bBoxes.push(this.getBoundingClientRect()); }); + expect(bBoxes.length).toBe(2); + var disjointY = bBoxes[0].top >= bBoxes[1].bottom - 5 || bBoxes[1].top >= bBoxes[0].bottom - 5; + expect(disjointY).toBe(true, bBoxes.map(function(bb) { return {top: bb.top, bottom: bb.bottom}; })); + } + + it('responds to hover and keeps the labels from crossing', function() { + _check(2, [0, 2, 12, 'trace 0'], [1, 0, 100, 'trace 1']); + _check(3, [1, 1, 120, 'trace 1'], [2, 0, 1000, 'trace 2']); + _check(4, [1, 2, 100, 'trace 1'], [2, 1, 1100, 'trace 2']); }); }); @@ -1308,23 +1334,18 @@ describe('hover on fill', function() { afterEach(destroyGraphDiv); function assertLabelsCorrect(mousePos, labelPos, labelText) { - return new Promise(function(resolve) { - mouseEvent('mousemove', mousePos[0], mousePos[1]); - - setTimeout(function() { - var hoverText = d3.selectAll('g.hovertext'); - expect(hoverText.size()).toEqual(1); - expect(hoverText.text()).toEqual(labelText); + Lib.clearThrottle(); + mouseEvent('mousemove', mousePos[0], mousePos[1]); - var transformParts = hoverText.attr('transform').split('('); - expect(transformParts[0]).toEqual('translate'); - var transformCoords = transformParts[1].split(')')[0].split(','); - expect(+transformCoords[0]).toBeCloseTo(labelPos[0], -1.2, labelText + ':x'); - expect(+transformCoords[1]).toBeCloseTo(labelPos[1], -1.2, labelText + ':y'); + var hoverText = d3.selectAll('g.hovertext'); + expect(hoverText.size()).toEqual(1); + expect(hoverText.text()).toEqual(labelText); - resolve(); - }, HOVERMINTIME); - }); + var transformParts = hoverText.attr('transform').split('('); + expect(transformParts[0]).toEqual('translate'); + var transformCoords = transformParts[1].split(')')[0].split(','); + expect(+transformCoords[0]).toBeCloseTo(labelPos[0], -1.2, labelText + ':x'); + expect(+transformCoords[1]).toBeCloseTo(labelPos[1], -1.2, labelText + ':y'); } it('should always show one label in the right place', function(done) { @@ -1332,11 +1353,9 @@ describe('hover on fill', function() { mock.data.forEach(function(trace) { trace.hoveron = 'fills'; }); Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(function() { - return assertLabelsCorrect([242, 142], [252, 133.8], 'trace 2'); - }).then(function() { - return assertLabelsCorrect([242, 292], [233, 210], 'trace 1'); - }).then(function() { - return assertLabelsCorrect([147, 252], [158.925, 248.1], 'trace 0'); + assertLabelsCorrect([242, 142], [252, 133.8], 'trace 2'); + assertLabelsCorrect([242, 292], [233, 210], 'trace 1'); + assertLabelsCorrect([147, 252], [158.925, 248.1], 'trace 0'); }).then(done); }); @@ -1354,9 +1373,8 @@ describe('hover on fill', function() { margin: {l: 50, t: 50, r: 50, b: 50} }) .then(function() { - return assertLabelsCorrect([200, 200], [73.75, 250], 'trace 0'); - }) - .then(function() { + assertLabelsCorrect([200, 200], [73.75, 250], 'trace 0'); + return Plotly.restyle(gd, { x: [[6, 7, 8, 7]], y: [[5, 4, 5, 6]] @@ -1364,7 +1382,7 @@ describe('hover on fill', function() { }) .then(function() { // gives same results w/o closing point - return assertLabelsCorrect([200, 200], [73.75, 250], 'trace 0'); + assertLabelsCorrect([200, 200], [73.75, 250], 'trace 0'); }) .catch(fail) .then(done); @@ -1379,16 +1397,14 @@ describe('hover on fill', function() { // hover over a point when that's closest, even if you're over // a fill, because by default we have hoveron='points+fills' - return assertLabelsCorrect([237, 150], [240.0, 144], + assertLabelsCorrect([237, 150], [240.0, 144], 'trace 2Component A: 0.8Component B: 0.1Component C: 0.1'); - }).then(function() { + // the rest are hovers over fills - return assertLabelsCorrect([237, 170], [247.7, 166], 'trace 2'); - }).then(function() { - return assertLabelsCorrect([237, 218], [266.75, 265], 'trace 1'); - }).then(function() { - return assertLabelsCorrect([237, 240], [247.7, 254], 'trace 0'); - }).then(function() { + assertLabelsCorrect([237, 170], [247.7, 166], 'trace 2'); + assertLabelsCorrect([237, 218], [266.75, 265], 'trace 1'); + assertLabelsCorrect([237, 240], [247.7, 254], 'trace 0'); + // zoom in to test clipping of large out-of-viewport shapes return Plotly.relayout(gd, { 'ternary.aaxis.min': 0.5, @@ -1397,13 +1413,13 @@ describe('hover on fill', function() { }).then(function() { // this particular one has a hover label disconnected from the shape itself // so if we ever fix this, the test will have to be fixed too. - return assertLabelsCorrect([295, 218], [275.1, 166], 'trace 2'); - }).then(function() { + assertLabelsCorrect([295, 218], [275.1, 166], 'trace 2'); + // trigger an autoscale redraw, which goes through dragElement return doubleClick(237, 251); }).then(function() { // then make sure we can still select a *different* item afterward - return assertLabelsCorrect([237, 218], [266.75, 265], 'trace 1'); + assertLabelsCorrect([237, 218], [266.75, 265], 'trace 1'); }) .catch(fail) .then(done); @@ -1422,14 +1438,14 @@ describe('hover on fill', function() { // hover over a point when that's closest, even if you're over // a fill, because by default we have hoveron='points+fills' - return assertLabelsCorrect([237, 150], [240.0, 144], + assertLabelsCorrect([237, 150], [240.0, 144], 'trace 2Component A: 0.8Component B: 0.1Component C: 0.1'); - }).then(function() { + // hovers over fills - return assertLabelsCorrect([237, 170], [247.7, 166], 'trace 2'); - }).then(function() { + assertLabelsCorrect([237, 170], [247.7, 166], 'trace 2'); + // hover on the cartesian trace in the corner - return assertLabelsCorrect([363, 122], [363, 122], 'trace 38'); + assertLabelsCorrect([363, 122], [363, 122], 'trace 38'); }) .catch(fail) .then(done); @@ -1442,29 +1458,25 @@ describe('hover updates', function() { afterEach(destroyGraphDiv); function assertLabelsCorrect(mousePos, labelPos, labelText, msg) { - return new Promise(function(resolve) { - if(mousePos) { - mouseEvent('mousemove', mousePos[0], mousePos[1]); - } + Lib.clearThrottle(); - setTimeout(function() { - var hoverText = d3.selectAll('g.hovertext'); - if(labelPos) { - expect(hoverText.size()).toBe(1, msg); - expect(hoverText.text()).toBe(labelText, msg); - - var transformParts = hoverText.attr('transform').split('('); - expect(transformParts[0]).toBe('translate', msg); - var transformCoords = transformParts[1].split(')')[0].split(','); - expect(+transformCoords[0]).toBeCloseTo(labelPos[0], -1, labelText + ':x ' + msg); - expect(+transformCoords[1]).toBeCloseTo(labelPos[1], -1, labelText + ':y ' + msg); - } else { - expect(hoverText.size()).toEqual(0); - } + if(mousePos) { + mouseEvent('mousemove', mousePos[0], mousePos[1]); + } - resolve(); - }, HOVERMINTIME); - }); + var hoverText = d3.selectAll('g.hovertext'); + if(labelPos) { + expect(hoverText.size()).toBe(1, msg); + expect(hoverText.text()).toBe(labelText, msg); + + var transformParts = hoverText.attr('transform').split('('); + expect(transformParts[0]).toBe('translate', msg); + var transformCoords = transformParts[1].split(')')[0].split(','); + expect(+transformCoords[0]).toBeCloseTo(labelPos[0], -1, labelText + ':x ' + msg); + expect(+transformCoords[1]).toBeCloseTo(labelPos[1], -1, labelText + ':y ' + msg); + } else { + expect(hoverText.size()).toEqual(0); + } } it('should update the labels on animation', function(done) { @@ -1485,28 +1497,30 @@ describe('hover updates', function() { var gd = createGraphDiv(); Plotly.plot(gd, mock).then(function() { // The label text gets concatenated together when queried. Such is life. - return assertLabelsCorrect([100, 100], [103, 100], 'trace 00.5', 'animation/update 0'); + assertLabelsCorrect([100, 100], [103, 100], 'trace 00.5', 'animation/update 0'); }).then(function() { return Plotly.animate(gd, [{ data: [{x: [0], y: [0]}, {x: [0.5], y: [0.5]}], traces: [0, 1], }], {frame: {redraw: false, duration: 0}}); - }).then(function() { + }) + .then(delay(HOVERMINTIME)) + .then(function() { // No mouse event this time. Just change the data and check the label. // Ditto on concatenation. This is "trace 1" + "0.5" - return assertLabelsCorrect(null, [103, 100], 'trace 10.5', 'animation/update 1'); - }).then(function() { + assertLabelsCorrect(null, [103, 100], 'trace 10.5', 'animation/update 1'); + // Restyle to move the point out of the window: return Plotly.relayout(gd, {'xaxis.range': [2, 3]}); }).then(function() { // Assert label removed: - return assertLabelsCorrect(null, null, null, 'animation/update 2'); - }).then(function() { + assertLabelsCorrect(null, null, null, 'animation/update 2'); + // Move back to the original xaxis range: return Plotly.relayout(gd, {'xaxis.range': [0, 1]}); }).then(function() { // Assert label restored: - return assertLabelsCorrect(null, [103, 100], 'trace 10.5', 'animation/update 3'); + assertLabelsCorrect(null, [103, 100], 'trace 10.5', 'animation/update 3'); }).catch(fail).then(done); }); @@ -1515,12 +1529,8 @@ describe('hover updates', function() { var colors0 = ['#000000', '#000000', '#000000', '#000000', '#000000', '#000000', '#000000']; function unhover() { - return new Promise(function(resolve) { - mouseEvent('mousemove', 394, 285); - setTimeout(function() { - resolve(); - }, HOVERMINTIME); - }); + Lib.clearThrottle(); + mouseEvent('mousemove', 394, 285); } var hoverCnt = 0; @@ -1550,17 +1560,15 @@ describe('hover updates', function() { Plotly.restyle(gd, 'marker.color', [colors0.slice()]); }); - return assertLabelsCorrect([351, 251], [358, 272], '2', 'events 0'); - }) - .then(unhover) - .then(function() { + assertLabelsCorrect([351, 251], [358, 272], '2', 'events 0'); + + unhover(); expect(hoverCnt).toEqual(1); expect(unHoverCnt).toEqual(1); - return assertLabelsCorrect([420, 100], [435, 198], '3', 'events 1'); - }) - .then(unhover) - .then(function() { + assertLabelsCorrect([420, 100], [435, 198], '3', 'events 1'); + + unhover(); expect(hoverCnt).toEqual(2); expect(unHoverCnt).toEqual(2); }) diff --git a/test/jasmine/tests/hover_spikeline_test.js b/test/jasmine/tests/hover_spikeline_test.js index c9655f8269a..4720ddfc8a5 100644 --- a/test/jasmine/tests/hover_spikeline_test.js +++ b/test/jasmine/tests/hover_spikeline_test.js @@ -8,319 +8,475 @@ var fail = require('../assets/fail_test'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -describe('spikeline', function() { +describe('spikeline hover', function() { 'use strict'; + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + afterEach(destroyGraphDiv); - describe('hover', function() { - var gd; - - function makeMock(spikemode, hovermode) { - var _mock = Lib.extendDeep({}, require('@mocks/19.json')); - _mock.layout.xaxis.showspikes = true; - _mock.layout.xaxis.spikemode = spikemode; - _mock.layout.yaxis.showspikes = true; - _mock.layout.yaxis.spikemode = spikemode + '+marker'; - _mock.layout.xaxis2.showspikes = true; - _mock.layout.xaxis2.spikemode = spikemode; - _mock.layout.hovermode = hovermode; - return _mock; - } + function makeMock(spikemode, hovermode) { + var _mock = Lib.extendDeep({}, require('@mocks/19.json')); + _mock.layout.xaxis.showspikes = true; + _mock.layout.xaxis.spikemode = spikemode; + _mock.layout.yaxis.showspikes = true; + _mock.layout.yaxis.spikemode = spikemode + '+marker'; + _mock.layout.xaxis2.showspikes = true; + _mock.layout.xaxis2.spikemode = spikemode; + _mock.layout.hovermode = hovermode; + return _mock; + } - function _hover(evt, subplot) { - Fx.hover(gd, evt, subplot); - Lib.clearThrottle(); - } + function _hover(evt, subplot) { + if(!subplot) subplot = 'xy'; + Fx.hover(gd, evt, subplot); + Lib.clearThrottle(); + } - function _set_hovermode(hovermode) { - return Plotly.relayout(gd, 'hovermode', hovermode); - } + function _set_hovermode(hovermode) { + return Plotly.relayout(gd, 'hovermode', hovermode); + } - function _set_spikedistance(spikedistance) { - return Plotly.relayout(gd, 'spikedistance', spikedistance); - } + function _set_spikedistance(spikedistance) { + return Plotly.relayout(gd, 'spikedistance', spikedistance); + } - function _assert(lineExpect, circleExpect) { - var TOL = 5; - var lines = d3.selectAll('line.spikeline'); - var circles = d3.selectAll('circle.spikeline'); + function _assert(lineExpect, circleExpect) { + var TOL = 5; + var lines = d3.selectAll('line.spikeline'); + var circles = d3.selectAll('circle.spikeline'); - expect(lines.size()).toBe(lineExpect.length, '# of line nodes'); - expect(circles.size()).toBe(circleExpect.length, '# of circle nodes'); + expect(lines.size()).toBe(lineExpect.length * 2, '# of line nodes'); + expect(circles.size()).toBe(circleExpect.length, '# of circle nodes'); - lines.each(function(_, i) { - var sel = d3.select(this); - ['x1', 'y1', 'x2', 'y2'].forEach(function(d, j) { - expect(sel.attr(d)) - .toBeWithin(lineExpect[i][j], TOL, 'line ' + i + ' attr ' + d); - }); + lines.each(function(_, i) { + var sel = d3.select(this); + ['x1', 'y1', 'x2', 'y2'].forEach(function(d, j) { + expect(sel.attr(d)) + // we always have 2 lines with identical coords + .toBeWithin(lineExpect[Math.floor(i / 2)][j], TOL, 'line ' + i + ' attr ' + d); }); + }); - circles.each(function(_, i) { - var sel = d3.select(this); - ['cx', 'cy'].forEach(function(d, j) { - expect(sel.attr(d)) - .toBeWithin(circleExpect[i][j], TOL, 'circle ' + i + ' attr ' + d); - }); + circles.each(function(_, i) { + var sel = d3.select(this); + ['cx', 'cy'].forEach(function(d, j) { + expect(sel.attr(d)) + .toBeWithin(circleExpect[i][j], TOL, 'circle ' + i + ' attr ' + d); }); + }); + } + + it('draws lines and markers on enabled axes in the closest hovermode', function(done) { + var _mock = makeMock('toaxis', 'closest'); + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}); + _assert( + [[557, 401, 557, 250], [80, 250, 557, 250]], + [[83, 250]] + ); + + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[820, 220, 820, 167]], + [] + ); + }) + .catch(fail) + .then(done); + }); + + it('works the same for scattergl', function(done) { + var _mock = makeMock('toaxis', 'closest'); + _mock.data[0].type = 'scattergl'; + _mock.data[1].type = 'scattergl'; + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}); + _assert( + [[557, 401, 557, 250], [80, 250, 557, 250]], + [[83, 250]] + ); + + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[820, 220, 820, 167]], + [] + ); + }) + .catch(fail) + .then(done); + }); + + it('draws lines and markers on enabled axes w/o tick labels', function(done) { + var _mock = makeMock('toaxis', 'closest'); + + _mock.layout.xaxis.showticklabels = false; + _mock.layout.yaxis.showticklabels = false; + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}); + _assert( + [[557, 401, 557, 250], [80, 250, 557, 250]], + [[83, 250]] + ); + + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[820, 220, 820, 167]], + [] + ); + }) + .catch(fail) + .then(done); + }); + + it('draws lines and markers on enabled axes in the x hovermode', function(done) { + var _mock = makeMock('across', 'x'); + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}); + _assert( + [[557, 100, 557, 401], [80, 250, 1036, 250]], + [[83, 250]] + ); + + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[820, 116, 820, 220]], + [] + ); + }) + .catch(fail) + .then(done); + }); + + it('draws lines and markers on enabled axes in the spikesnap "cursor" mode', function(done) { + var _mock = makeMock('toaxis', 'x'); + + _mock.layout.xaxis.spikesnap = 'cursor'; + _mock.layout.yaxis.spikesnap = 'cursor'; + _mock.layout.xaxis2.spikesnap = 'cursor'; + + Plotly.plot(gd, _mock) + .then(function() { + _set_spikedistance(200); + }) + .then(function() { + _hover({xpx: 120, ypx: 180}); + _assert( + [[200, 401, 200, 280], [80, 280, 200, 280]], + [[83, 280]] + ); + + _hover({xpx: 31, ypx: 41}, 'x2y2'); + _assert( + [[682, 220, 682, 156]], + [] + ); + }) + .catch(fail) + .then(done); + }); + + it('doesn\'t switch between toaxis and across spikemodes on switching the hovermodes', function(done) { + var _mock = makeMock('toaxis', 'closest'); + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}); + _assert( + [[557, 401, 557, 250], [80, 250, 557, 250]], + [[83, 250]] + ); + + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[820, 220, 820, 167]], + [] + ); + + _set_hovermode('x'); + }) + .then(function() { + _hover({xval: 2, yval: 3}); + _assert( + [[557, 401, 557, 250], [80, 250, 557, 250]], + [[83, 250]] + ); + + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[820, 220, 820, 167]], + [] + ); + }) + .catch(fail) + .then(done); + }); + + it('increase the range of search for points to draw the spikelines on spikedistance change', function(done) { + var _mock = makeMock('toaxis', 'closest'); + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 1.6, yval: 2.6}); + _assert( + [], + [] + ); + + _hover({xval: 26, yval: 36}, 'x2y2'); + _assert( + [], + [] + ); + + _set_spikedistance(200); + }) + .then(function() { + _hover({xval: 1.6, yval: 2.6}); + _assert( + [[557, 401, 557, 250], [80, 250, 557, 250]], + [[83, 250]] + ); + + _hover({xval: 26, yval: 36}, 'x2y2'); + _assert( + [[820, 220, 820, 167]], + [] + ); + }) + .catch(fail) + .then(done); + }); + + it('correctly responds to setting the spikedistance to -1 by increasing ' + + 'the range of search for points to draw the spikelines to Infinity', function(done) { + var _mock = makeMock('toaxis', 'closest'); + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 1.6, yval: 2.6}); + _assert( + [], + [] + ); + + _hover({xval: 26, yval: 36}, 'x2y2'); + _assert( + [], + [] + ); + + _set_spikedistance(-1); + }) + .then(function() { + _hover({xval: 1.6, yval: 2.6}); + _assert( + [[557, 401, 557, 250], [80, 250, 557, 250]], + [[83, 250]] + ); + + _hover({xval: 26, yval: 36}, 'x2y2'); + _assert( + [[820, 220, 820, 167]], + [] + ); + }) + .catch(fail) + .then(done); + }); + + it('correctly responds to setting the spikedistance to 0 by disabling ' + + 'the search for points to draw the spikelines', function(done) { + var _mock = makeMock('toaxis', 'closest'); + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}); + _assert( + [[557, 401, 557, 250], [80, 250, 557, 250]], + [[83, 250]] + ); + + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[820, 220, 820, 167]], + [] + ); + + _set_spikedistance(0); + }) + .then(function() { + _hover({xval: 2, yval: 3}); + _assert( + [], + [] + ); + + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [], + [] + ); + }) + .catch(fail) + .then(done); + }); + + function spikeLayout() { + return { + width: 600, height: 600, margin: {l: 100, r: 100, t: 100, b: 100}, + showlegend: false, + xaxis: {range: [-0.5, 1.5], showspikes: true, spikemode: 'toaxis+marker'}, + yaxis: {range: [-1, 3], showspikes: true, spikemode: 'toaxis+marker'}, + hovermode: 'x', + boxmode: 'group', barmode: 'group', violinmode: 'group' + }; + } + + it('positions spikes at the data value on grouped bars', function(done) { + function _assertBarSpikes() { + // regardless of hovermode, you must be actually over the bar to see its spikes + _hover({xval: -0.2, yval: 0.8}); + _assert( + [[200, 500, 200, 300], [100, 300, 200, 300]], + [[200, 500], [100, 300]] + ); + + _hover({xval: -0.2, yval: 1.2}); + _assert([], []); + + _hover({xval: 0.2, yval: 1.8}); + _assert( + [[200, 500, 200, 200], [100, 200, 200, 200]], + [[200, 500], [100, 200]] + ); + + _hover({xval: 0.2, yval: 2.2}); + _assert([], []); } - it('draws lines and markers on enabled axes in the closest hovermode', function(done) { - gd = createGraphDiv(); - var _mock = makeMock('toaxis', 'closest'); - - Plotly.plot(gd, _mock).then(function() { - _hover({xval: 2, yval: 3}, 'xy'); - _assert( - [[557, 401, 557, 250], [557, 401, 557, 250], [80, 250, 557, 250], [80, 250, 557, 250]], - [[83, 250]] - ); - }) - .then(function() { - _hover({xval: 30, yval: 40}, 'x2y2'); - _assert( - [[820, 220, 820, 167], [820, 220, 820, 167]], - [] - ); - }) - .catch(fail) - .then(done); - }); + Plotly.newPlot(gd, [{ + type: 'bar', y: [1, 2] + }, { + type: 'bar', y: [2, 1] + }], spikeLayout()) + .then(_assertBarSpikes) + .then(function() { _set_hovermode('closest'); }) + .then(_assertBarSpikes) + .catch(fail) + .then(done); + }); - it('draws lines and markers on enabled axes w/o tick labels', function(done) { - gd = createGraphDiv(); - var _mock = makeMock('toaxis', 'closest'); - - _mock.layout.xaxis.showticklabels = false; - _mock.layout.yaxis.showticklabels = false; - - Plotly.plot(gd, _mock).then(function() { - _hover({xval: 2, yval: 3}, 'xy'); - _assert( - [[557, 401, 557, 250], [557, 401, 557, 250], [80, 250, 557, 250], [80, 250, 557, 250]], - [[83, 250]] - ); - }) - .then(function() { - _hover({xval: 30, yval: 40}, 'x2y2'); - _assert( - [[820, 220, 820, 167], [820, 220, 820, 167]], - [] - ); - }) - .catch(fail) - .then(done); - }); + it('positions spikes at the data value on grouped boxes', function(done) { + Plotly.newPlot(gd, [{ + type: 'box', x: [0, 0, 0, 0, 1, 1, 1, 1], y: [0, 0, 1, 1, 0, 0, 1, 1], boxpoints: 'all' + }, { + type: 'box', x: [0, 0, 0, 0, 1, 1, 1, 1], y: [2, 2, 1, 1, 2, 2, 1, 1] + }], spikeLayout()) + .then(function() { + // over the box: median line @ (0, 0.5) + _hover({xval: -0.1, yval: 0.1}); + _assert( + [[200, 500, 200, 350], [100, 350, 200, 350]], + [[200, 500], [100, 350]] + ); - it('draws lines and markers on enabled axes in the x hovermode', function(done) { - gd = createGraphDiv(); - var _mock = makeMock('across', 'x'); - - Plotly.plot(gd, _mock).then(function() { - _hover({xval: 2, yval: 3}, 'xy'); - _assert( - [[557, 100, 557, 401], [557, 100, 557, 401], [80, 250, 1036, 250], [80, 250, 1036, 250]], - [[83, 250]] - ); - }) - .then(function() { - _hover({xval: 30, yval: 40}, 'x2y2'); - _assert( - [[820, 116, 820, 220], [820, 116, 820, 220]], - [] - ); - }) - .catch(fail) - .then(done); - }); + // point hover @ (0, 0) + _hover({xval: -0.4, yval: 0.1}); + _assert( + [[200, 500, 200, 400], [100, 400, 200, 400]], + [[200, 500], [100, 400]] + ); + }) + .catch(fail) + .then(done); + }); - it('draws lines and markers on enabled axes in the spikesnap "cursor" mode', function(done) { - gd = createGraphDiv(); - var _mock = makeMock('toaxis', 'x'); - - _mock.layout.xaxis.spikesnap = 'cursor'; - _mock.layout.yaxis.spikesnap = 'cursor'; - _mock.layout.xaxis2.spikesnap = 'cursor'; - - Plotly.plot(gd, _mock) - .then(function() { - _set_spikedistance(200); - }) - .then(function() { - _hover({xpx: 120, ypx: 180}, 'xy'); - _assert( - [[200, 401, 200, 280], [200, 401, 200, 280], [80, 280, 200, 280], [80, 280, 200, 280]], - [[83, 280]] - ); - }) - .then(function() { - _hover({xpx: 31, ypx: 41}, 'x2y2'); - _assert( - [[682, 220, 682, 156], [682, 220, 682, 156]], - [] - ); - }) - .catch(fail) - .then(done); - }); + it('positions spikes correctly on grouped violins', function(done) { + Plotly.newPlot(gd, [{ + type: 'violin', x: [0, 0, 0, 0, 1, 1, 1, 1], y: [0, 0, 1, 1, 0, 0, 1, 1], side: 'positive', points: 'all' + }, { + type: 'violin', x: [0, 0, 0, 0, 1, 1, 1, 1], y: [2, 2, 1, 1, 2, 2, 1, 1], side: 'positive' + }], spikeLayout()) + .then(function() { + // over the violin: KDE @ (0, 0.2) + _hover({xval: -0.15, yval: 0.2}); + _assert( + [[200, 500, 200, 380], [100, 380, 200, 380]], + [[200, 500], [100, 380]] + ); - it('doesn\'t switch between toaxis and across spikemodes on switching the hovermodes', function(done) { - gd = createGraphDiv(); - var _mock = makeMock('toaxis', 'closest'); - - Plotly.plot(gd, _mock).then(function() { - _hover({xval: 2, yval: 3}, 'xy'); - _assert( - [[557, 401, 557, 250], [557, 401, 557, 250], [80, 250, 557, 250], [80, 250, 557, 250]], - [[83, 250]] - ); - }) - .then(function() { - _hover({xval: 30, yval: 40}, 'x2y2'); - _assert( - [[820, 220, 820, 167], [820, 220, 820, 167]], - [] - ); - }) - .then(function() { - _set_hovermode('x'); - }) - .then(function() { - _hover({xval: 2, yval: 3}, 'xy'); - _assert( - [[557, 401, 557, 250], [557, 401, 557, 250], [80, 250, 557, 250], [80, 250, 557, 250]], - [[83, 250]] - ); - }) - .then(function() { - _hover({xval: 30, yval: 40}, 'x2y2'); - _assert( - [[820, 220, 820, 167], [820, 220, 820, 167]], - [] - ); - }) - .catch(fail) - .then(done); - }); + // off the violin, not quite at the points + _hover({xval: -0.2, yval: 0.2}); + _assert([], []); - it('increase the range of search for points to draw the spikelines on spikedistance change', function(done) { - gd = createGraphDiv(); - var _mock = makeMock('toaxis', 'closest'); - - Plotly.plot(gd, _mock).then(function() { - _hover({xval: 1.6, yval: 2.6}, 'xy'); - _assert( - [], - [] - ); - }) - .then(function() { - _hover({xval: 26, yval: 36}, 'x2y2'); - _assert( - [], - [] - ); - }) - .then(function() { - _set_spikedistance(200); - }) - .then(function() { - _hover({xval: 1.6, yval: 2.6}, 'xy'); - _assert( - [[557, 401, 557, 250], [557, 401, 557, 250], [80, 250, 557, 250], [80, 250, 557, 250]], - [[83, 250]] - ); - }) - .then(function() { - _hover({xval: 26, yval: 36}, 'x2y2'); - _assert( - [[820, 220, 820, 167], [820, 220, 820, 167]], - [] - ); - }) - .catch(fail) - .then(done); - }); + // over a point + _hover({xval: -0.4, yval: 0.2}); + _assert( + [[200, 500, 200, 400], [100, 400, 200, 400]], + [[200, 500], [100, 400]] + ); + }) + .catch(fail) + .then(done); + }); - it('correctly responds to setting the spikedistance to -1 by increasing ' + - 'the range of search for points to draw the spikelines to Infinity', function(done) { - gd = createGraphDiv(); - var _mock = makeMock('toaxis', 'closest'); - - Plotly.plot(gd, _mock).then(function() { - _hover({xval: 1.6, yval: 2.6}, 'xy'); - _assert( - [], - [] - ); - }) - .then(function() { - _hover({xval: 26, yval: 36}, 'x2y2'); - _assert( - [], - [] - ); - }) - .then(function() { - _set_spikedistance(-1); - }) - .then(function() { - _hover({xval: 1.6, yval: 2.6}, 'xy'); - _assert( - [[557, 401, 557, 250], [557, 401, 557, 250], [80, 250, 557, 250], [80, 250, 557, 250]], - [[83, 250]] - ); - }) - .then(function() { - _hover({xval: 26, yval: 36}, 'x2y2'); - _assert( - [[820, 220, 820, 167], [820, 220, 820, 167]], - [] - ); - }) - .catch(fail) - .then(done); - }); + it('positions spikes correctly on heatmaps', function(done) { + Plotly.newPlot(gd, [{ + type: 'heatmap', x: [0, 1], y: [0, 2], z: [[1, 2], [3, 4]] + }], spikeLayout()) + .then(function() { + // heatmap bricks go past the x/y bounds + _hover({xval: -0.1, yval: 0.2}); + _assert( + [[200, 500, 200, 400], [100, 400, 200, 400]], + [[200, 500], [100, 400]] + ); + }) + .catch(fail) + .then(done); + }); - it('correctly responds to setting the spikedistance to 0 by disabling ' + - 'the search for points to draw the spikelines', function(done) { - gd = createGraphDiv(); - var _mock = makeMock('toaxis', 'closest'); - - Plotly.plot(gd, _mock).then(function() { - _hover({xval: 2, yval: 3}, 'xy'); - _assert( - [[557, 401, 557, 250], [557, 401, 557, 250], [80, 250, 557, 250], [80, 250, 557, 250]], - [[83, 250]] - ); - }) - .then(function() { - _hover({xval: 30, yval: 40}, 'x2y2'); - _assert( - [[820, 220, 820, 167], [820, 220, 820, 167]], - [] - ); - }) - .then(function() { - _set_spikedistance(0); - }) - .then(function() { - _hover({xval: 2, yval: 3}, 'xy'); - _assert( - [], - [] - ); - }) - .then(function() { - _hover({xval: 30, yval: 40}, 'x2y2'); - _assert( - [], - [] - ); - }) - .catch(fail) - .then(done); - }); + it('positions spikes correctly on contour maps', function(done) { + Plotly.newPlot(gd, [{ + type: 'contour', x: [0, 1], y: [0, 2], z: [[1, 2], [3, 4]] + }], spikeLayout()) + .then(function() { + // contour doesn't draw past the x/y bounds + _hover({xval: -0.1, yval: 0.2}); + _assert([], []); + + _hover({xval: 0.1, yval: 0.2}); + _assert( + [[200, 500, 200, 400], [100, 400, 200, 400]], + [[200, 500], [100, 400]] + ); + }) + .catch(fail) + .then(done); + }); + + it('does not show spikes on scatter fills', function(done) { + Plotly.newPlot(gd, [{ + x: [0, 0, 1, 1, 0], y: [0, 2, 2, 0, 0], fill: 'toself' + }], Lib.extendFlat({}, spikeLayout(), {hovermode: 'closest'})) + .then(function() { + // center of the fill: no spikes + _hover({xval: 0.5, yval: 1}); + _assert([], []); + + // sanity check: points still generate spikes + _hover({xval: 0, yval: 0}); + _assert( + [[200, 500, 200, 400], [100, 400, 200, 400]], + [[200, 500], [100, 400]] + ); + }) + .catch(fail) + .then(done); }); }); diff --git a/test/jasmine/tests/scatterpolargl_test.js b/test/jasmine/tests/scatterpolargl_test.js index 766e2551262..a3e68af50a6 100644 --- a/test/jasmine/tests/scatterpolargl_test.js +++ b/test/jasmine/tests/scatterpolargl_test.js @@ -39,8 +39,8 @@ describe('Test scatterpolargl hover:', function() { [{ desc: 'base', - nums: 'r: 2.920135\nθ: 138.2489°', - name: 'Trial 4' + nums: 'r: 3.886013\nθ: 125.2822°', + name: 'Trial 3' }, { desc: '(no labels - out of sector)', patch: function(fig) { @@ -56,8 +56,8 @@ describe('Test scatterpolargl hover:', function() { fig.layout.polar.angularaxis.thetaunit = 'radians'; return fig; }, - nums: 'r: 2.920135\nθ: 2.412898', - name: 'Trial 4' + nums: 'r: 3.886013\nθ: 2.186586', + name: 'Trial 3' }, { desc: 'on log radial axis', patch: function(fig) { diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index eb92824a06a..644a7d6a4c6 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -554,7 +554,7 @@ describe('@flaky Test select box and lasso in general:', function() { }); }); -describe('Test select box and lasso per trace:', function() { +describe('@flaky Test select box and lasso per trace:', function() { var gd; beforeEach(function() {