diff --git a/draftlogs/5846_fix.md b/draftlogs/5846_fix.md new file mode 100644 index 00000000000..77d4da22ed6 --- /dev/null +++ b/draftlogs/5846_fix.md @@ -0,0 +1,2 @@ + - Fix hover with period alignment points and improve positioning of spikes and unified hover label + in order not to obscure referring data points [[#5846](https://github.com/plotly/plotly.js/pull/5846)] diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 550f514246f..7773787fc73 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -46,6 +46,12 @@ var multipleHoverPoints = { candlestick: true }; +var cartesianScatterPoints = { + scatter: true, + scattergl: true, + splom: true +}; + // fx.hover: highlight data on hover // evt can be a mousemove event, or an object with data about what points // to hover on @@ -574,12 +580,15 @@ function _hover(gd, evt, subplot, noHoverEvent) { findHoverPoints(); - function selectClosestPoint(pointsData, spikedistance) { + function selectClosestPoint(pointsData, spikedistance, spikeOnWinning) { var resultPoint = null; var minDistance = Infinity; var thisSpikeDistance; + for(var i = 0; i < pointsData.length; i++) { thisSpikeDistance = pointsData[i].spikeDistance; + if(spikeOnWinning && i === 0) thisSpikeDistance = -Infinity; + if(thisSpikeDistance <= minDistance && thisSpikeDistance <= spikedistance) { resultPoint = pointsData[i]; minDistance = thisSpikeDistance; @@ -616,19 +625,30 @@ function _hover(gd, evt, subplot, noHoverEvent) { }; gd._spikepoints = newspikepoints; + var sortHoverData = function() { + hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; }); + + // move period positioned points and box/bar-like traces to the end of the list + hoverData = orderRangePoints(hoverData, hovermode); + }; + sortHoverData(); + + var axLetter = hovermode.charAt(0); + var spikeOnWinning = (axLetter === 'x' || axLetter === 'y') && hoverData[0] && cartesianScatterPoints[hoverData[0].trace.type]; + // Now if it is not restricted by spikedistance option, set the points to draw the spikelines if(hasCartesian && (spikedistance !== 0)) { if(hoverData.length !== 0) { var tmpHPointData = hoverData.filter(function(point) { return point.ya.showspikes; }); - var tmpHPoint = selectClosestPoint(tmpHPointData, spikedistance); + var tmpHPoint = selectClosestPoint(tmpHPointData, spikedistance, spikeOnWinning); spikePoints.hLinePoint = fillSpikePoint(tmpHPoint); var tmpVPointData = hoverData.filter(function(point) { return point.xa.showspikes; }); - var tmpVPoint = selectClosestPoint(tmpVPointData, spikedistance); + var tmpVPoint = selectClosestPoint(tmpVPointData, spikedistance, spikeOnWinning); spikePoints.vLinePoint = fillSpikePoint(tmpVPoint); } } @@ -650,14 +670,6 @@ function _hover(gd, evt, subplot, noHoverEvent) { } } - var sortHoverData = function() { - hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; }); - - // move period positioned points and box/bar-like traces to the end of the list - hoverData = orderRangePoints(hoverData, hovermode); - }; - sortHoverData(); - if( helpers.isXYhover(_mode) && hoverData[0].length !== 0 && @@ -1071,41 +1083,89 @@ function createHoverText(hoverData, opts, gd) { legendDraw(gd, mockLegend); // Position the hover - var winningPoint = hoverData[0]; - var ly = axLetter === 'y' ? - (winningPoint.y0 + winningPoint.y1) / 2 : Lib.mean(hoverData.map(function(c) {return (c.y0 + c.y1) / 2;})); - var lx = axLetter === 'x' ? - (winningPoint.x0 + winningPoint.x1) / 2 : Lib.mean(hoverData.map(function(c) {return (c.x0 + c.x1) / 2;})); - var legendContainer = container.select('g.legend'); var tbb = legendContainer.node().getBoundingClientRect(); - lx += xa._offset; - ly += ya._offset - tbb.height / 2; - - // Change horizontal alignment to end up on screen - var txWidth = tbb.width + 2 * HOVERTEXTPAD; - var anchorStartOK = lx + txWidth <= outerWidth; - var anchorEndOK = lx - txWidth >= 0; - if(!anchorStartOK && anchorEndOK) { - lx -= txWidth; + var tWidth = tbb.width + 2 * HOVERTEXTPAD; + var tHeight = tbb.height + 2 * HOVERTEXTPAD; + var winningPoint = hoverData[0]; + var avgX = (winningPoint.x0 + winningPoint.x1) / 2; + var avgY = (winningPoint.y0 + winningPoint.y1) / 2; + // When a scatter (or e.g. heatmap) point wins, it's OK for the hovelabel to occlude the bar and other points. + var pointWon = !( + Registry.traceIs(winningPoint.trace, 'bar-like') || + Registry.traceIs(winningPoint.trace, 'box-violin') + ); + + var lyBottom, lyTop; + if(axLetter === 'y') { + if(pointWon) { + lyTop = avgY - HOVERTEXTPAD; + lyBottom = avgY + HOVERTEXTPAD; + } else { + lyTop = Math.min.apply(null, hoverData.map(function(c) { return Math.min(c.y0, c.y1); })); + lyBottom = Math.max.apply(null, hoverData.map(function(c) { return Math.max(c.y0, c.y1); })); + } + } else { + lyTop = lyBottom = Lib.mean(hoverData.map(function(c) { return (c.y0 + c.y1) / 2; })) - tHeight / 2; + } + + var lxRight, lxLeft; + if(axLetter === 'x') { + if(pointWon) { + lxRight = avgX + HOVERTEXTPAD; + lxLeft = avgX - HOVERTEXTPAD; + } else { + lxRight = Math.max.apply(null, hoverData.map(function(c) { return Math.max(c.x0, c.x1); })); + lxLeft = Math.min.apply(null, hoverData.map(function(c) { return Math.min(c.x0, c.x1); })); + } } else { - lx += 2 * HOVERTEXTPAD; + lxRight = lxLeft = Lib.mean(hoverData.map(function(c) { return (c.x0 + c.x1) / 2; })) - tWidth / 2; } - // Change vertical alignement to end up on screen - var txHeight = tbb.height + 2 * HOVERTEXTPAD; - var overflowTop = ly <= outerTop; - var overflowBottom = ly + txHeight >= outerHeight; - var canFit = txHeight <= outerHeight; - if(canFit) { - if(overflowTop) { - ly = ya._offset + 2 * HOVERTEXTPAD; - } else if(overflowBottom) { - ly = outerHeight - txHeight; + var xOffset = xa._offset; + var yOffset = ya._offset; + lyBottom += yOffset; + lxRight += xOffset; + lxLeft += xOffset - tWidth; + lyTop += yOffset - tHeight; + + var lx, ly; // top and left positions of the hover box + + // horizontal alignment to end up on screen + if(lxRight + tWidth < outerWidth && lxRight >= 0) { + lx = lxRight; + } else if(lxLeft + tWidth < outerWidth && lxLeft >= 0) { + lx = lxLeft; + } else if(xOffset + tWidth < outerWidth) { + lx = xOffset; // subplot left corner + } else { + // closest left or right side of the paper + if(lxRight - avgX < avgX - lxLeft + tWidth) { + lx = outerWidth - tWidth; + } else { + lx = 0; } } - legendContainer.attr('transform', strTranslate(lx, ly)); + lx += HOVERTEXTPAD; + + // vertical alignement to end up on screen + if(lyBottom + tHeight < outerHeight && lyBottom >= 0) { + ly = lyBottom; + } else if(lyTop + tHeight < outerHeight && lyTop >= 0) { + ly = lyTop; + } else if(yOffset + tHeight < outerHeight) { + ly = yOffset; // subplot top corner + } else { + // closest top or bottom side of the paper + if(lyBottom - avgY < avgY - lyTop + tHeight) { + ly = outerHeight - tHeight; + } else { + ly = 0; + } + } + ly += HOVERTEXTPAD; + legendContainer.attr('transform', strTranslate(lx - 1, ly - 1)); return legendContainer; } @@ -1934,7 +1994,10 @@ function getCoord(axLetter, winningPoint, fullLayout) { var val = winningPoint[axLetter + 'Val']; if(ax.type === 'category') val = ax._categoriesMap[val]; - else if(ax.type === 'date') val = ax.d2c(val); + else if(ax.type === 'date') { + var period = winningPoint[axLetter + 'Period']; + val = ax.d2c(period !== undefined ? period : val); + } var cd0 = winningPoint.cd[winningPoint.index]; if(cd0 && cd0.t && cd0.t.posLetter === ax._id) { diff --git a/src/plots/cartesian/align_period.js b/src/plots/cartesian/align_period.js index 7a72f6491cf..ba6d8f1de81 100644 --- a/src/plots/cartesian/align_period.js +++ b/src/plots/cartesian/align_period.js @@ -8,21 +8,21 @@ var constants = require('../../constants/numerical'); var ONEAVGMONTH = constants.ONEAVGMONTH; module.exports = function alignPeriod(trace, ax, axLetter, vals) { - if(ax.type !== 'date') return vals; + if(ax.type !== 'date') return {vals: vals}; var alignment = trace[axLetter + 'periodalignment']; - if(!alignment) return vals; + if(!alignment) return {vals: vals}; var period = trace[axLetter + 'period']; var mPeriod; if(isNumeric(period)) { period = +period; - if(period <= 0) return vals; + if(period <= 0) return {vals: vals}; } else if(typeof period === 'string' && period.charAt(0) === 'M') { var n = +(period.substring(1)); if(n > 0 && Math.round(n) === n) { mPeriod = n; - } else return vals; + } else return {vals: vals}; } var calendar = ax.calendar; @@ -35,6 +35,9 @@ module.exports = function alignPeriod(trace, ax, axLetter, vals) { var base = dateTime2ms(period0, calendar) || 0; var newVals = []; + var starts = []; + var ends = []; + var len = vals.length; for(var i = 0; i < len; i++) { var v = vals[i]; @@ -77,6 +80,14 @@ module.exports = function alignPeriod(trace, ax, axLetter, vals) { isEnd ? endTime : (startTime + endTime) / 2 ); + + starts[i] = startTime; + ends[i] = endTime; } - return newVals; + + return { + vals: newVals, + starts: starts, + ends: ends + }; }; diff --git a/src/traces/bar/calc.js b/src/traces/bar/calc.js index 8877112eeee..2216c2469db 100644 --- a/src/traces/bar/calc.js +++ b/src/traces/bar/calc.js @@ -10,24 +10,24 @@ var calcSelection = require('../scatter/calc_selection'); module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'); var ya = Axes.getFromId(gd, trace.yaxis || 'y'); - var size, pos, origPos; + var size, pos, origPos, pObj, hasPeriod; var sizeOpts = { msUTC: !!(trace.base || trace.base === 0) }; - var hasPeriod; if(trace.orientation === 'h') { size = xa.makeCalcdata(trace, 'x', sizeOpts); origPos = ya.makeCalcdata(trace, 'y'); - pos = alignPeriod(trace, ya, 'y', origPos); + pObj = alignPeriod(trace, ya, 'y', origPos); hasPeriod = !!trace.yperiodalignment; } else { size = ya.makeCalcdata(trace, 'y', sizeOpts); origPos = xa.makeCalcdata(trace, 'x'); - pos = alignPeriod(trace, xa, 'x', origPos); + pObj = alignPeriod(trace, xa, 'x', origPos); hasPeriod = !!trace.xperiodalignment; } + pos = pObj.vals; // create the "calculated data" to plot var serieslen = Math.min(pos.length, size.length); @@ -39,6 +39,8 @@ module.exports = function calc(gd, trace) { if(hasPeriod) { cd[i].orig_p = origPos[i]; // used by hover + cd[i].pEnd = pObj.ends[i]; + cd[i].pStart = pObj.starts[i]; } if(trace.ids) { diff --git a/src/traces/bar/cross_trace_calc.js b/src/traces/bar/cross_trace_calc.js index f1cad80924d..72203d40318 100644 --- a/src/traces/bar/cross_trace_calc.js +++ b/src/traces/bar/cross_trace_calc.js @@ -436,12 +436,20 @@ function setBarCenterAndWidth(pa, sieve) { var barwidth = t.barwidth; var barwidthIsArray = Array.isArray(barwidth); + var trace = calcTrace[0].trace; + var isPeriod = !!trace[pLetter + 'periodalignment']; + for(var j = 0; j < calcTrace.length; j++) { var calcBar = calcTrace[j]; // store the actual bar width and position, for use by hover var width = calcBar.w = barwidthIsArray ? barwidth[j] : barwidth; calcBar[pLetter] = calcBar.p + (poffsetIsArray ? poffset[j] : poffset) + width / 2; + + if(isPeriod) { + calcBar.wPeriod = + calcBar.pEnd - calcBar.pStart; + } } } } diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js index deb78362095..55605428ac0 100644 --- a/src/traces/bar/hover.js +++ b/src/traces/bar/hover.js @@ -57,10 +57,9 @@ function hoverOnBars(pointData, xval, yval, hovermode, opts) { function thisBarMaxPos(di) { return thisBarExtPos(di, 1); } function thisBarExtPos(di, sgn) { - if(period) { - return di.p + sgn * Math.abs(di.p - di.orig_p); - } - return di[posLetter] + sgn * di.w / 2; + var w = (period) ? di.wPeriod : di.w; + + return di[posLetter] + sgn * w / 2; } var minPos = isClosest || period ? @@ -180,6 +179,9 @@ function hoverOnBars(pointData, xval, yval, hovermode, opts) { var hasPeriod = di.orig_p !== undefined; pointData[posLetter + 'LabelVal'] = hasPeriod ? di.orig_p : di.p; + if(hasPeriod) { + pointData[posLetter + 'Period'] = di.p; + } pointData.labelLabel = hoverLabelText(pa, pointData[posLetter + 'LabelVal'], trace[posLetter + 'hoverformat']); pointData.valueLabel = hoverLabelText(sa, pointData[sizeLetter + 'LabelVal'], trace[sizeLetter + 'hoverformat']); diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index 2b810f07679..83eadb0b1f8 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -311,7 +311,7 @@ function getPosArrays(trace, posLetter, posAxis, num) { if(hasPosArray || (hasPos0 && hasPosStep)) { var origPos = posAxis.makeCalcdata(trace, posLetter); - var pos = alignPeriod(trace, posAxis, posLetter, origPos); + var pos = alignPeriod(trace, posAxis, posLetter, origPos).vals; return [pos, origPos]; } diff --git a/src/traces/candlestick/calc.js b/src/traces/candlestick/calc.js index b8f7ec316b8..6044707e816 100644 --- a/src/traces/candlestick/calc.js +++ b/src/traces/candlestick/calc.js @@ -12,7 +12,7 @@ module.exports = function(gd, trace) { var ya = Axes.getFromId(gd, trace.yaxis); var origX = xa.makeCalcdata(trace, 'x'); - var x = alignPeriod(trace, xa, 'x', origX); + var x = alignPeriod(trace, xa, 'x', origX).vals; var cd = calcCommon(gd, trace, origX, x, ya, ptFunc); diff --git a/src/traces/funnel/calc.js b/src/traces/funnel/calc.js index 164fba1ff3e..4a379289c93 100644 --- a/src/traces/funnel/calc.js +++ b/src/traces/funnel/calc.js @@ -9,20 +9,20 @@ var BADNUM = require('../../constants/numerical').BADNUM; module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'); var ya = Axes.getFromId(gd, trace.yaxis || 'y'); - var size, pos, origPos, i, cdi; + var size, pos, origPos, pObj, hasPeriod, i, cdi; - var hasPeriod; if(trace.orientation === 'h') { size = xa.makeCalcdata(trace, 'x'); origPos = ya.makeCalcdata(trace, 'y'); - pos = alignPeriod(trace, ya, 'y', origPos); + pObj = alignPeriod(trace, ya, 'y', origPos); hasPeriod = !!trace.yperiodalignment; } else { size = ya.makeCalcdata(trace, 'y'); origPos = xa.makeCalcdata(trace, 'x'); - pos = alignPeriod(trace, xa, 'x', origPos); + pObj = alignPeriod(trace, xa, 'x', origPos); hasPeriod = !!trace.xperiodalignment; } + pos = pObj.vals; // create the "calculated data" to plot var serieslen = Math.min(pos.length, size.length); @@ -55,6 +55,8 @@ module.exports = function calc(gd, trace) { if(hasPeriod) { cd[i].orig_p = origPos[i]; // used by hover + cd[i].pEnd = pObj.ends[i]; + cd[i].pStart = pObj.starts[i]; } if(trace.ids) { diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index ce05bbf3f84..a190c388284 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -54,8 +54,8 @@ module.exports = function calc(gd, trace) { } else { origX = trace.x ? xa.makeCalcdata(trace, 'x') : []; origY = trace.y ? ya.makeCalcdata(trace, 'y') : []; - x = alignPeriod(trace, xa, 'x', origX); - y = alignPeriod(trace, ya, 'y', origY); + x = alignPeriod(trace, xa, 'x', origX).vals; + y = alignPeriod(trace, ya, 'y', origY).vals; trace._x = x; trace._y = y; } diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index 643af02e52b..11564fd00c1 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -8,8 +8,8 @@ module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, var colLen = trace._length; var col1 = ax1.makeCalcdata(trace, var1Name); var col2 = ax2.makeCalcdata(trace, var2Name); - col1 = alignPeriod(trace, ax1, var1Name, col1); - col2 = alignPeriod(trace, ax2, var2Name, col2); + col1 = alignPeriod(trace, ax1, var1Name, col1).vals; + col2 = alignPeriod(trace, ax2, var2Name, col2).vals; var textCol = trace.text; var hasColumnText = (textCol !== undefined && Lib.isArray1D(textCol)); diff --git a/src/traces/ohlc/calc.js b/src/traces/ohlc/calc.js index e0d0a85f83f..831518652c5 100644 --- a/src/traces/ohlc/calc.js +++ b/src/traces/ohlc/calc.js @@ -144,7 +144,7 @@ function convertTickWidth(gd, xa, trace) { var origX = xa.makeCalcdata(tracei, 'x'); tracei._origX = origX; - var xcalc = alignPeriod(trace, xa, 'x', origX); + var xcalc = alignPeriod(trace, xa, 'x', origX).vals; tracei._xcalc = xcalc; var _minDiff = Lib.distinctVals(xcalc).minDiff; diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js index 7efbf5b96d9..2c2ac558a84 100644 --- a/src/traces/scatter/calc.js +++ b/src/traces/scatter/calc.js @@ -18,8 +18,10 @@ function calc(gd, trace) { var ya = Axes.getFromId(gd, trace.yaxis || 'y'); var origX = xa.makeCalcdata(trace, 'x'); var origY = ya.makeCalcdata(trace, 'y'); - var x = alignPeriod(trace, xa, 'x', origX); - var y = alignPeriod(trace, ya, 'y', origY); + var xObj = alignPeriod(trace, xa, 'x', origX); + var yObj = alignPeriod(trace, ya, 'y', origY); + var x = xObj.vals; + var y = yObj.vals; var serieslen = trace._length; var cd = new Array(serieslen); @@ -64,9 +66,13 @@ function calc(gd, trace) { if(hasPeriodX) { cdi.orig_x = origX[i]; // used by hover + cdi.xEnd = xObj.ends[i]; + cdi.xStart = xObj.starts[i]; } if(hasPeriodY) { cdi.orig_y = origY[i]; // used by hover + cdi.yEnd = yObj.ends[i]; + cdi.yStart = yObj.starts[i]; } } else if(stackGroupOpts && (isV ? xValid : yValid)) { // if we're stacking we need to hold on to all valid positions diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js index b4b6a62dd23..682d1a474d0 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -18,31 +18,54 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var hoveron = trace.hoveron || ''; var minRad = (trace.mode.indexOf('markers') !== -1) ? 3 : 0.5; + var xPeriod = !!trace.xperiodalignment; + var yPeriod = !!trace.yperiodalignment; + // look for points to hover on first, then take fills only if we // didn't find a point + if(hoveron.indexOf('points') !== -1) { + // 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 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. + if(xPeriod) { + var x0 = xa.c2p(di.xStart); + var x1 = xa.c2p(di.xEnd); + + return ( + xpx >= Math.min(x0, x1) && + xpx <= Math.max(x0, x1) + ) ? 0 : Infinity; + } + var rad = Math.max(3, di.mrc || 0); var kink = 1 - 1 / rad; var dxRaw = Math.abs(xa.c2p(di.x) - xpx); - if(di.orig_x !== undefined) dxRaw += xa.c2p(di.orig_x) - xa.c2p(di.x); return (dxRaw < rad) ? (kink * dxRaw / rad) : (dxRaw - rad + kink); }; var dy = function(di) { + if(yPeriod) { + var y0 = ya.c2p(di.yStart); + var y1 = ya.c2p(di.yEnd); + + return ( + ypx >= Math.min(y0, y1) && + ypx <= Math.max(y0, y1) + ) ? 0 : Infinity; + } + var rad = Math.max(3, di.mrc || 0); var kink = 1 - 1 / rad; var dyRaw = Math.abs(ya.c2p(di.y) - ypx); - if(di.orig_y !== undefined) dyRaw += ya.c2p(di.orig_y) - ya.c2p(di.y); return (dyRaw < rad) ? (kink * dyRaw / rad) : (dyRaw - rad + kink); }; + + // 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 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; @@ -89,6 +112,9 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { hovertemplate: trace.hovertemplate }); + if(trace.xperiodalignment === 'end') pointData.xPeriod = di.x; + if(trace.yperiodalignment === 'end') pointData.yPeriod = di.y; + fillText(di, trace, pointData); Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, pointData); diff --git a/src/traces/scattergl/calc.js b/src/traces/scattergl/calc.js index 2f65c916d0f..d27f12e5fc4 100644 --- a/src/traces/scattergl/calc.js +++ b/src/traces/scattergl/calc.js @@ -31,13 +31,23 @@ module.exports = function calc(gd, trace) { var origX = xa.makeCalcdata(trace, 'x'); var origY = ya.makeCalcdata(trace, 'y'); - var x = alignPeriod(trace, xa, 'x', origX); - var y = alignPeriod(trace, ya, 'y', origY); + var xObj = alignPeriod(trace, xa, 'x', origX); + var yObj = alignPeriod(trace, ya, 'y', origY); + var x = xObj.vals; + var y = yObj.vals; trace._x = x; trace._y = y; - if(trace.xperiodalignment) trace._origX = origX; - if(trace.yperiodalignment) trace._origY = origY; + if(trace.xperiodalignment) { + trace._origX = origX; + trace._xStarts = xObj.starts; + trace._xEnds = xObj.ends; + } + if(trace.yperiodalignment) { + trace._origY = origY; + trace._yStarts = yObj.starts; + trace._yEnds = yObj.ends; + } // we need hi-precision for scatter2d, // regl-scatter2d uses NaNs for bad/missing values diff --git a/src/traces/scattergl/hover.js b/src/traces/scattergl/hover.js index e1b2a16ecd5..adf39744e01 100644 --- a/src/traces/scattergl/hover.js +++ b/src/traces/scattergl/hover.js @@ -41,42 +41,68 @@ function hoverPoints(pointData, xval, yval, hovermode) { // pick the id closest to the point // note that point possibly may not be found - var id, ptx, pty, i, dx, dy, dist, dxy; + var k, closestId, ptx, pty, i, dx, dy, dist, dxy; var minDist = maxDistance; if(hovermode === 'x') { + var xPeriod = !!trace.xperiodalignment; + var yPeriod = !!trace.yperiodalignment; + for(i = 0; i < ids.length; i++) { - ptx = x[ids[i]]; + k = ids[i]; + ptx = x[k]; + dx = Math.abs(xa.c2p(ptx) - xpx); - if(trace._origX && trace._origX[i] !== undefined) dx += xa.c2p(trace._origX[i]) - xa.c2p(ptx); + if(xPeriod) { + var x0 = xa.c2p(trace._xStarts[k]); + var x1 = xa.c2p(trace._xEnds[k]); + + dx = ( + xpx >= Math.min(x0, x1) && + xpx <= Math.max(x0, x1) + ) ? 0 : Infinity; + } + if(dx < minDist) { minDist = dx; - dy = ya.c2p(y[ids[i]]) - ypx; - if(trace._origY && trace._origY[i] !== undefined) dy += ya.c2p(trace._origY[i]) - ya.c2p(pty); + pty = y[k]; + dy = ya.c2p(pty) - ypx; + + if(yPeriod) { + var y0 = ya.c2p(trace._yStarts[k]); + var y1 = ya.c2p(trace._yEnds[k]); + + dy = ( + ypx >= Math.min(y0, y1) && + ypx <= Math.max(y0, y1) + ) ? 0 : Infinity; + } + dxy = Math.sqrt(dx * dx + dy * dy); - id = ids[i]; + closestId = ids[i]; } } } else { for(i = ids.length - 1; i > -1; i--) { - ptx = x[ids[i]]; - pty = y[ids[i]]; + k = ids[i]; + ptx = x[k]; + pty = y[k]; dx = xa.c2p(ptx) - xpx; dy = ya.c2p(pty) - ypx; dist = Math.sqrt(dx * dx + dy * dy); if(dist < minDist) { minDist = dxy = dist; - id = ids[i]; + closestId = k; } } } - pointData.index = id; + pointData.index = closestId; pointData.distance = minDist; pointData.dxy = dxy; - if(id === undefined) return [pointData]; + if(closestId === undefined) return [pointData]; return [calcHover(pointData, x, y, trace)]; } @@ -176,6 +202,9 @@ function calcHover(pointData, x, y, trace) { hovertemplate: di.ht }); + if(trace.xperiodalignment === 'end') pointData2.xPeriod = di.x; + if(trace.yperiodalignment === 'end') pointData2.yPeriod = di.y; + if(di.htx) pointData2.text = di.htx; else if(di.tx) pointData2.text = di.tx; else if(trace.text) pointData2.text = trace.text; diff --git a/src/traces/waterfall/calc.js b/src/traces/waterfall/calc.js index ee7e351e730..151c9c91683 100644 --- a/src/traces/waterfall/calc.js +++ b/src/traces/waterfall/calc.js @@ -17,20 +17,20 @@ function isTotal(a) { module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'); var ya = Axes.getFromId(gd, trace.yaxis || 'y'); - var size, pos, origPos; + var size, pos, origPos, pObj, hasPeriod; - var hasPeriod; if(trace.orientation === 'h') { size = xa.makeCalcdata(trace, 'x'); origPos = ya.makeCalcdata(trace, 'y'); - pos = alignPeriod(trace, ya, 'y', origPos); + pObj = alignPeriod(trace, ya, 'y', origPos); hasPeriod = !!trace.yperiodalignment; } else { size = ya.makeCalcdata(trace, 'y'); origPos = xa.makeCalcdata(trace, 'x'); - pos = alignPeriod(trace, xa, 'x', origPos); + pObj = alignPeriod(trace, xa, 'x', origPos); hasPeriod = !!trace.xperiodalignment; } + pos = pObj.vals; // create the "calculated data" to plot var serieslen = Math.min(pos.length, size.length); @@ -85,6 +85,8 @@ module.exports = function calc(gd, trace) { if(hasPeriod) { cd[i].orig_p = origPos[i]; // used by hover + cd[i].pEnd = pObj.ends[i]; + cd[i].pStart = pObj.starts[i]; } if(trace.ids) { diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index f804dc6e367..9b828bc4d9f 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -5274,7 +5274,7 @@ describe('hovermode: (x|y)unified', function() { 'bar : 2' ]}); - _hover(gd, { xpx: 100, ypx: 200 }); + _hover(gd, { xpx: 110, ypx: 200 }); assertLabel({title: 'Jan 1, 2000', items: [ 'bar : (Jan, 1)', 'scatter : 1.1' @@ -5345,7 +5345,7 @@ describe('hovermode: (x|y)unified', function() { type: 'bar', x: ['2000-01', '2000-02'], y: [1, 2], - xhoverfrmat: '%b', + xhoverformat: '%b', xperiod: 'M1' }, { @@ -5359,7 +5359,7 @@ describe('hovermode: (x|y)unified', function() { }, { name: 'end', - type: 'scatter', + type: scatterType, x: ['2000-01', '2000-02'], y: [1, 2], xhoverformat: '%b', @@ -5375,38 +5375,229 @@ describe('hovermode: (x|y)unified', function() { } }) .then(function(gd) { - _hover(gd, { xpx: 40, ypx: 200 }); + _hover(gd, { xpx: 50, ypx: 200 }); assertLabel({title: 'Jan', items: [ - 'bar : (Jan 1, 2000, 1)', + 'bar : 1', 'start : 1', - 'end : 1' + 'end : 1', ]}); - _hover(gd, { xpx: 100, ypx: 200 }); + _hover(gd, { xpx: 150, ypx: 200 }); assertLabel({title: 'Jan', items: [ - 'bar : (Jan 1, 2000, 1)', + 'bar : 1', 'start : 1', - 'end : 1' + 'end : 1', ]}); + }) + .then(done, done.fail); + }); + }); - _hover(gd, { xpx: 360, ypx: 200 }); - assertLabel({title: 'Feb', items: [ - 'bar : (Feb 1, 2000, 2)', - 'start : 2', - 'end : 2' + ['scatter', 'scattergl'].forEach(function(scatterType) { + it(scatterType + ' period points alignments (all end edge case)', function(done) { + Plotly.newPlot(gd, { + data: [ + { + name: 'bar', + type: 'bar', + x: ['2000-01', '2000-02'], + y: [1, 2], + xhoverformat: '%b', + xperiod: 'M1', + xperiodalignment: 'end' + }, + { + name: 'start', + type: scatterType, + x: ['2000-01', '2000-02'], + y: [1, 2], + xhoverformat: '%b', + xperiod: 'M1', + xperiodalignment: 'end' + }, + { + name: 'end', + type: scatterType, + x: ['2000-01', '2000-02'], + y: [1, 2], + xhoverformat: '%b', + xperiod: 'M1', + xperiodalignment: 'end' + }, + ], + layout: { + showlegend: false, + width: 600, + height: 400, + hovermode: 'x unified' + } + }) + .then(function(gd) { + _hover(gd, { xpx: 50, ypx: 200 }); + assertLabel({title: 'Jan', items: [ + 'bar : 1', + 'start : 1', + 'end : 1', ]}); - _hover(gd, { xpx: 400, ypx: 200 }); + _hover(gd, { xpx: 350, ypx: 200 }); assertLabel({title: 'Feb', items: [ - 'bar : (Feb 1, 2000, 2)', + 'bar : 2', 'start : 2', - 'end : 2' + 'end : 2', ]}); }) .then(done, done.fail); }); }); + [{ + type: 'scatter', + alignment: 'start', + x: 350 + }, { + type: 'scatter', + alignment: 'middle', + x: 250 + }, { + type: 'scatter', + alignment: 'end', + x: 150 + }].forEach(function(t) { + it('two ' + t.alignment + ' period positioned ' + t.type + ' points', function(done) { + var fig = { + data: [{ + x: [ + '1970-01-01', + '1970-07-01', + '1971-01-01' + ], + xperiod: 'M6', + xperiodalignment: t.alignment, + type: t.type, + hovertemplate: '%{y}', + y: [11, 12, 13] + }, { + x: [ + '1970-01-01', + '1970-07-01', + '1971-01-01', + ], + xperiod: 'M6', + xperiodalignment: t.alignment, + type: t.type, + hovertemplate: '%{y}', + y: [1, 2, 3] + }], + layout: { + showlegend: false, + width: 600, + height: 400, + hovermode: 'x unified' + } + }; + + Plotly.newPlot(gd, fig) + .then(function(gd) { + _hover(gd, { xpx: t.x, ypx: 200 }); + assertLabel({title: 'Jul 1, 1970', items: [ + 'trace 0 : 12', + 'trace 1 : 2' + ]}); + }) + .then(done, done.fail); + }); + + [{ + type: 'bar', + barmode: 'overlay', + alignment: 'start' + }, { + type: 'bar', + barmode: 'group', + alignment: 'middle' + }, { + type: 'bar', + barmode: 'group', + alignment: 'end' + }, { + type: 'bar', + barmode: 'group', + alignment: 'start' + }, { + type: 'bar', + barmode: 'overlay', + alignment: 'middle' + }, { + type: 'bar', + barmode: 'overlay', + alignment: 'end' + }, { + type: 'bar', + barmode: 'stacked', + alignment: 'start' + }, { + type: 'bar', + barmode: 'stacked', + alignment: 'middle' + }, { + type: 'bar', + barmode: 'stacked', + alignment: 'end' + }].forEach(function(t) { + it('two ' + t.alignment + ' period positioned ' + t.barmode + ' bars', function(done) { + var fig = { + data: [{ + x: [ + '1970-01-01', + '1970-07-01', + '1971-01-01' + ], + xperiod: 'M6', + xperiodalignment: t.alignment, + type: t.type, + hovertemplate: '%{y}', + y: [11, 12, 13] + }, { + x: [ + '1970-01-01', + '1970-07-01', + '1971-01-01', + ], + xperiod: 'M6', + xperiodalignment: t.alignment, + type: t.type, + hovertemplate: '%{y}', + y: [1, 2, 3] + }], + layout: { + barmode: t.barmode, + showlegend: false, + width: 600, + height: 400, + hovermode: 'x unified' + } + }; + + Plotly.newPlot(gd, fig) + .then(function(gd) { + _hover(gd, { xpx: 100, ypx: 200 }); + assertLabel({title: 'Jan 1, 1970', items: [ + 'trace 0 : 11', + 'trace 1 : 1' + ]}); + + _hover(gd, { xpx: 400, ypx: 200 }); + assertLabel({title: 'Jan 1, 1971', items: [ + 'trace 0 : 13', + 'trace 1 : 3' + ]}); + }) + .then(done, done.fail); + }); + }); + }); + it('period with hover distance -1 include closest not farthest', function(done) { Plotly.newPlot(gd, { data: [ diff --git a/test/jasmine/tests/hover_spikeline_test.js b/test/jasmine/tests/hover_spikeline_test.js index 40ec0de2ee8..51b2feba0e4 100644 --- a/test/jasmine/tests/hover_spikeline_test.js +++ b/test/jasmine/tests/hover_spikeline_test.js @@ -523,12 +523,17 @@ describe('spikeline hover', function() { _hover({xpx: 200, ypx: 200}); lines = d3SelectAll('line.spikeline'); expect(lines.size()).toBe(4); - expect(lines[0][1].getAttribute('stroke')).toBe('blue'); + expect(lines[0][1].getAttribute('stroke')).toBe('green'); _hover({xpx: 200, ypx: 350}); lines = d3SelectAll('line.spikeline'); expect(lines.size()).toBe(4); expect(lines[0][1].getAttribute('stroke')).toBe('green'); + + _hover({xpx: 300, ypx: 350}); + lines = d3SelectAll('line.spikeline'); + expect(lines.size()).toBe(4); + expect(lines[0][1].getAttribute('stroke')).toBe('blue'); }) .then(done, done.fail); }); @@ -717,10 +722,10 @@ describe('spikeline hover', function() { .then(done, done.fail); }); - it('correctly draws lines up to the last point', function(done) { + it('correctly draws lines up to the winning point', function(done) { Plotly.newPlot(gd, [ {type: 'bar', y: [5, 7, 9, 6, 4, 3]}, - {y: [5, 7, 9, 6, 4, 3]}, + {y: [5, 7, 9, 6, 4, 3], marker: {color: 'green'}}, {y: [5, 7, 9, 6, 4, 3], marker: {color: 'red'}} ], { hovermode: 'x', @@ -735,8 +740,8 @@ describe('spikeline hover', function() { var lines = d3SelectAll('line.spikeline'); expect(lines.size()).toBe(4); - expect(lines[0][1].getAttribute('stroke')).toBe('red'); - expect(lines[0][3].getAttribute('stroke')).toBe('red'); + expect(lines[0][1].getAttribute('stroke')).toBe('green'); + expect(lines[0][3].getAttribute('stroke')).toBe('green'); }) .then(done, done.fail); });