From b4bdec325acba165aff1efce4f732140daa4bbaf Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 20 Jul 2021 17:21:24 -0400 Subject: [PATCH 01/28] fix scattergl test --- test/jasmine/tests/hover_label_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index f804dc6e367..afe6d69450d 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -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', From 53fea8ed934786cde3543544243f58aac02017ad Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 20 Jul 2021 17:25:30 -0400 Subject: [PATCH 02/28] fix hover on scatter and scattergl with periods --- src/components/fx/hover.js | 5 ++- src/traces/scatter/hover.js | 5 +-- src/traces/scattergl/hover.js | 26 +++++++++------ test/jasmine/tests/hover_label_test.js | 46 ++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 550f514246f..8d9aeac2bf7 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1934,7 +1934,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/traces/scatter/hover.js b/src/traces/scatter/hover.js index b4b6a62dd23..5fb2f7f9a0d 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -28,14 +28,12 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { 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) { 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); }; var dxy = function(di) { @@ -89,6 +87,9 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { hovertemplate: trace.hovertemplate }); + if(di.orig_x !== undefined) pointData.xPeriod = di.x; + if(di.orig_y !== undefined) pointData.yPeriod = di.y; + fillText(di, trace, pointData); Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, pointData); diff --git a/src/traces/scattergl/hover.js b/src/traces/scattergl/hover.js index e1b2a16ecd5..90f4d097130 100644 --- a/src/traces/scattergl/hover.js +++ b/src/traces/scattergl/hover.js @@ -41,42 +41,43 @@ 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') { 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(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; 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 +177,9 @@ function calcHover(pointData, x, y, trace) { hovertemplate: di.ht }); + if(origX) pointData2.xPeriod = di.x; + if(origY) 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/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index afe6d69450d..a6ee2e7956c 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -5407,6 +5407,52 @@ describe('hovermode: (x|y)unified', function() { }); }); + it('two end positioned scatter period', function(done) { + var fig = { + data: [{ + x: [ + '1970-01-01', + '1970-07-01', + '1971-01-01' + ], + xperiod: 'M6', + xperiodalignment: 'end', + y: [1, 2, 3] + }, { + x: [ + '1970-01-01', + '1970-07-01', + '1971-01-01', + ], + xperiod: 'M6', + xperiodalignment: 'end', + y: [11, 12, 13] + }], + layout: { + showlegend: false, + width: 600, + height: 400, + hovermode: 'x unified' + } + }; + + Plotly.newPlot(gd, fig) + .then(function(gd) { + _hover(gd, { xpx: 200, ypx: 200 }); + assertLabel({title: 'Jul 1, 1970', items: [ + 'trace 0 : 2', + 'trace 1 : 12' + ]}); + + _hover(gd, { xpx: 400, ypx: 200 }); + assertLabel({title: 'Jan 1, 1971', items: [ + 'trace 0 : 3', + 'trace 1 : 13' + ]}); + }) + .then(done, done.fail); + }); + it('period with hover distance -1 include closest not farthest', function(done) { Plotly.newPlot(gd, { data: [ From 2933803a10f8e8b251a4af2f82905838ea27148f Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 21 Jul 2021 10:28:47 -0400 Subject: [PATCH 03/28] fix hover on bars with period --- src/plots/cartesian/align_period.js | 21 ++++++++++++++++----- src/traces/bar/calc.js | 10 ++++++---- src/traces/bar/cross_trace_calc.js | 8 ++++++++ src/traces/bar/hover.js | 10 ++++++---- src/traces/box/calc.js | 2 +- src/traces/candlestick/calc.js | 2 +- src/traces/funnel/calc.js | 10 ++++++---- src/traces/heatmap/calc.js | 4 ++-- src/traces/heatmap/convert_column_xyz.js | 4 ++-- src/traces/ohlc/calc.js | 2 +- src/traces/scatter/calc.js | 4 ++-- src/traces/scattergl/calc.js | 4 ++-- src/traces/waterfall/calc.js | 11 +++++++---- 13 files changed, 60 insertions(+), 32 deletions(-) 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..f20cd445904 100644 --- a/src/traces/scatter/calc.js +++ b/src/traces/scatter/calc.js @@ -18,8 +18,8 @@ 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 x = alignPeriod(trace, xa, 'x', origX).vals; + var y = alignPeriod(trace, ya, 'y', origY).vals; var serieslen = trace._length; var cd = new Array(serieslen); diff --git a/src/traces/scattergl/calc.js b/src/traces/scattergl/calc.js index 2f65c916d0f..bba37a9aeec 100644 --- a/src/traces/scattergl/calc.js +++ b/src/traces/scattergl/calc.js @@ -31,8 +31,8 @@ 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 x = alignPeriod(trace, xa, 'x', origX).vals; + var y = alignPeriod(trace, ya, 'y', origY).vals; trace._x = x; trace._y = y; diff --git a/src/traces/waterfall/calc.js b/src/traces/waterfall/calc.js index ee7e351e730..55cc667b3f7 100644 --- a/src/traces/waterfall/calc.js +++ b/src/traces/waterfall/calc.js @@ -17,20 +17,21 @@ 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); + pos = pObj.vals; 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 +86,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) { From 775a8e04974a4f5d78e5c992b3c3e4fec3ff2efb Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 21 Jul 2021 10:29:15 -0400 Subject: [PATCH 04/28] improve and adjust jasmine tests --- test/jasmine/tests/hover_label_test.js | 156 ++++++++++++++++--------- 1 file changed, 104 insertions(+), 52 deletions(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index a6ee2e7956c..05b5240035e 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -5377,29 +5377,29 @@ describe('hovermode: (x|y)unified', function() { .then(function(gd) { _hover(gd, { xpx: 40, ypx: 200 }); assertLabel({title: 'Jan', items: [ - 'bar : (Jan 1, 2000, 1)', - 'start : 1', - 'end : 1' + 'start : 1' ]}); _hover(gd, { xpx: 100, ypx: 200 }); + assertLabel({title: 'Jan 1, 2000', items: [ + 'bar : 1' + ]}); + + _hover(gd, { xpx: 210, ypx: 200 }); assertLabel({title: 'Jan', items: [ 'bar : (Jan 1, 2000, 1)', - 'start : 1', - 'end : 1' + 'start : (Feb, 2)', + 'end : 1', ]}); _hover(gd, { xpx: 360, ypx: 200 }); - assertLabel({title: 'Feb', items: [ - 'bar : (Feb 1, 2000, 2)', - 'start : 2', - 'end : 2' + assertLabel({title: 'Feb 1, 2000', items: [ + 'bar : 2' ]}); _hover(gd, { xpx: 400, ypx: 200 }); assertLabel({title: 'Feb', items: [ 'bar : (Feb 1, 2000, 2)', - 'start : 2', 'end : 2' ]}); }) @@ -5407,50 +5407,102 @@ describe('hovermode: (x|y)unified', function() { }); }); - it('two end positioned scatter period', function(done) { - var fig = { - data: [{ - x: [ - '1970-01-01', - '1970-07-01', - '1971-01-01' - ], - xperiod: 'M6', - xperiodalignment: 'end', - y: [1, 2, 3] - }, { - x: [ - '1970-01-01', - '1970-07-01', - '1971-01-01', - ], - xperiod: 'M6', - xperiodalignment: 'end', - y: [11, 12, 13] - }], - layout: { - showlegend: false, - width: 600, - height: 400, - hovermode: 'x unified' - } - }; + [{ + type: 'scatter', + alignment: 'start' + }, { + type: 'scatter', + alignment: 'middle' + }, { + type: 'scatter', + alignment: 'end' + }, { + 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 ? t.barmode + ' ' : '') + t.type + 's', 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: 200, ypx: 200 }); - assertLabel({title: 'Jul 1, 1970', items: [ - 'trace 0 : 2', - 'trace 1 : 12' - ]}); + Plotly.newPlot(gd, fig) + .then(function(gd) { + _hover(gd, { xpx: 200, ypx: 200 }); + assertLabel({title: 'Jul 1, 1970', items: [ + 'trace 0 : 12', + 'trace 1 : 2' + ]}); - _hover(gd, { xpx: 400, ypx: 200 }); - assertLabel({title: 'Jan 1, 1971', items: [ - 'trace 0 : 3', - 'trace 1 : 13' - ]}); - }) - .then(done, done.fail); + _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) { From 168bc751654bbe3f915b4195774c380168294dad Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 21 Jul 2021 10:55:08 -0400 Subject: [PATCH 05/28] draft log for PR 5846 --- draftlogs/5846_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/5846_fix.md diff --git a/draftlogs/5846_fix.md b/draftlogs/5846_fix.md new file mode 100644 index 00000000000..f2dc2ab0c35 --- /dev/null +++ b/draftlogs/5846_fix.md @@ -0,0 +1 @@ + - Fix misplaced hover with period positioning [[#5846](https://github.com/plotly/plotly.js/pull/5846)] From 3deca0d41a7dd4c3e930527f77e1b41fc280bcab Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 21 Jul 2021 11:45:02 -0400 Subject: [PATCH 06/28] drop unused line --- src/traces/waterfall/calc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/traces/waterfall/calc.js b/src/traces/waterfall/calc.js index 55cc667b3f7..151c9c91683 100644 --- a/src/traces/waterfall/calc.js +++ b/src/traces/waterfall/calc.js @@ -23,7 +23,6 @@ module.exports = function calc(gd, trace) { size = xa.makeCalcdata(trace, 'x'); origPos = ya.makeCalcdata(trace, 'y'); pObj = alignPeriod(trace, ya, 'y', origPos); - pos = pObj.vals; hasPeriod = !!trace.yperiodalignment; } else { size = ya.makeCalcdata(trace, 'y'); From 7c37e982f164cc6951954fbbb9c7adb46b5f5385 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 21 Jul 2021 12:26:05 -0400 Subject: [PATCH 07/28] fix typo and adjust test --- test/jasmine/tests/hover_label_test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 05b5240035e..87ede74db62 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -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' }, { @@ -5381,25 +5381,25 @@ describe('hovermode: (x|y)unified', function() { ]}); _hover(gd, { xpx: 100, ypx: 200 }); - assertLabel({title: 'Jan 1, 2000', items: [ + assertLabel({title: 'Jan', items: [ 'bar : 1' ]}); _hover(gd, { xpx: 210, ypx: 200 }); assertLabel({title: 'Jan', items: [ - 'bar : (Jan 1, 2000, 1)', + 'bar : 1', 'start : (Feb, 2)', 'end : 1', ]}); _hover(gd, { xpx: 360, ypx: 200 }); - assertLabel({title: 'Feb 1, 2000', items: [ + assertLabel({title: 'Feb', items: [ 'bar : 2' ]}); _hover(gd, { xpx: 400, ypx: 200 }); assertLabel({title: 'Feb', items: [ - 'bar : (Feb 1, 2000, 2)', + 'bar : 2', 'end : 2' ]}); }) From e5a405722f57fab80c5371f370ed1b99f379e434 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 21 Jul 2021 14:34:22 -0400 Subject: [PATCH 08/28] scatters with period ranges --- src/traces/scatter/calc.js | 10 +- src/traces/scatter/hover.js | 42 ++++-- src/traces/scattergl/calc.js | 18 ++- src/traces/scattergl/hover.js | 28 +++- test/jasmine/tests/hover_label_test.js | 172 +++++++++++++++---------- 5 files changed, 183 insertions(+), 87 deletions(-) diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js index f20cd445904..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).vals; - var y = alignPeriod(trace, ya, 'y', origY).vals; + 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 5fb2f7f9a0d..c86a5b902f8 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -18,29 +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); 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); 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; @@ -87,9 +112,6 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { hovertemplate: trace.hovertemplate }); - if(di.orig_x !== undefined) pointData.xPeriod = di.x; - if(di.orig_y !== undefined) 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 bba37a9aeec..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).vals; - var y = alignPeriod(trace, ya, 'y', origY).vals; + 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 90f4d097130..41e25458f6f 100644 --- a/src/traces/scattergl/hover.js +++ b/src/traces/scattergl/hover.js @@ -45,14 +45,39 @@ function hoverPoints(pointData, xval, yval, hovermode) { var minDist = maxDistance; if(hovermode === 'x') { + var xPeriod = !!trace.xperiodalignment; + var yPeriod = !!trace.yperiodalignment; + for(i = 0; i < ids.length; i++) { k = ids[i]; ptx = x[k]; + dx = Math.abs(xa.c2p(ptx) - xpx); + 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; 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); closestId = ids[i]; } @@ -177,9 +202,6 @@ function calcHover(pointData, x, y, trace) { hovertemplate: di.ht }); - if(origX) pointData2.xPeriod = di.x; - if(origY) 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/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 87ede74db62..bba72bdbc99 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' @@ -5337,7 +5337,7 @@ describe('hovermode: (x|y)unified', function() { }); ['scatter', 'scattergl'].forEach(function(scatterType) { - it(scatterType + ' period points alignments', function(done) { + fit(scatterType + ' period points alignments', function(done) { Plotly.newPlot(gd, { data: [ { @@ -5375,33 +5375,19 @@ describe('hovermode: (x|y)unified', function() { } }) .then(function(gd) { - _hover(gd, { xpx: 40, ypx: 200 }); - assertLabel({title: 'Jan', items: [ - 'start : 1' - ]}); - - _hover(gd, { xpx: 100, ypx: 200 }); + _hover(gd, { xpx: 50, ypx: 200 }); assertLabel({title: 'Jan', items: [ - 'bar : 1' + 'bar : 1', + 'start : 1', + 'end : 1', ]}); - _hover(gd, { xpx: 210, ypx: 200 }); + _hover(gd, { xpx: 150, ypx: 200 }); assertLabel({title: 'Jan', items: [ 'bar : 1', - 'start : (Feb, 2)', + 'start : 1', 'end : 1', ]}); - - _hover(gd, { xpx: 360, ypx: 200 }); - assertLabel({title: 'Feb', items: [ - 'bar : 2' - ]}); - - _hover(gd, { xpx: 400, ypx: 200 }); - assertLabel({title: 'Feb', items: [ - 'bar : 2', - 'end : 2' - ]}); }) .then(done, done.fail); }); @@ -5409,49 +5395,16 @@ describe('hovermode: (x|y)unified', function() { [{ type: 'scatter', - alignment: 'start' + alignment: 'start', + x: 350 }, { type: 'scatter', - alignment: 'middle' + alignment: 'middle', + x: 250 }, { type: 'scatter', - alignment: 'end' - }, { - 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' + alignment: 'end', + x: 150 }].forEach(function(t) { it('two ' + t.alignment + ' period positioned ' + (t.barmode ? t.barmode + ' ' : '') + t.type + 's', function(done) { var fig = { @@ -5489,20 +5442,103 @@ describe('hovermode: (x|y)unified', function() { Plotly.newPlot(gd, fig) .then(function(gd) { - _hover(gd, { xpx: 200, ypx: 200 }); + _hover(gd, { xpx: t.x, ypx: 200 }); assertLabel({title: 'Jul 1, 1970', items: [ 'trace 0 : 12', 'trace 1 : 2' ]}); - - _hover(gd, { xpx: 400, ypx: 200 }); - assertLabel({title: 'Jan 1, 1971', items: [ - 'trace 0 : 13', - 'trace 1 : 3' - ]}); }) .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 ? t.barmode + ' ' : '') + t.type + 's', 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) { From bd528a2e341310f1cbbb64ff02f8545fabc4db6c Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 21 Jul 2021 16:15:43 -0400 Subject: [PATCH 09/28] adjust scatter and bar test titles --- test/jasmine/tests/hover_label_test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index bba72bdbc99..8c382881536 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -5406,7 +5406,7 @@ describe('hovermode: (x|y)unified', function() { alignment: 'end', x: 150 }].forEach(function(t) { - it('two ' + t.alignment + ' period positioned ' + (t.barmode ? t.barmode + ' ' : '') + t.type + 's', function(done) { + it('two ' + t.alignment + ' period positioned ' + t.type + ' points', function(done) { var fig = { data: [{ x: [ @@ -5432,7 +5432,6 @@ describe('hovermode: (x|y)unified', function() { y: [1, 2, 3] }], layout: { - barmode: t.barmode, showlegend: false, width: 600, height: 400, @@ -5488,7 +5487,7 @@ describe('hovermode: (x|y)unified', function() { barmode: 'stacked', alignment: 'end' }].forEach(function(t) { - it('two ' + t.alignment + ' period positioned ' + (t.barmode ? t.barmode + ' ' : '') + t.type + 's', function(done) { + it('two ' + t.alignment + ' period positioned ' + t.barmode + ' bars', function(done) { var fig = { data: [{ x: [ From 85821d1ccacee3664c1d3bcaf2d2261edd07571a Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 21 Jul 2021 16:56:36 -0400 Subject: [PATCH 10/28] fit -> it --- test/jasmine/tests/hover_label_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 8c382881536..b7f9f648261 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -5337,7 +5337,7 @@ describe('hovermode: (x|y)unified', function() { }); ['scatter', 'scattergl'].forEach(function(scatterType) { - fit(scatterType + ' period points alignments', function(done) { + it(scatterType + ' period points alignments', function(done) { Plotly.newPlot(gd, { data: [ { From 9fbd1869e8dcf416f050c4e98212e17fc362fb19 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 08:38:36 -0400 Subject: [PATCH 11/28] refactor: enter long lines --- src/components/fx/hover.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 8d9aeac2bf7..1ee82fa231c 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1073,9 +1073,11 @@ function createHoverText(hoverData, opts, gd) { // 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;})); + (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;})); + (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(); From 8a36d8f2fafd17b1770654707d7e8bcf68328c9f Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 08:56:07 -0400 Subject: [PATCH 12/28] position hover labels in respect to max and min of hoverset not the winning point --- src/components/fx/hover.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 1ee82fa231c..54f8ecf716b 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1071,12 +1071,11 @@ function createHoverText(hoverData, opts, gd) { legendDraw(gd, mockLegend); // Position the hover - var winningPoint = hoverData[0]; var ly = axLetter === 'y' ? - (winningPoint.y0 + winningPoint.y1) / 2 : + Math.min.apply(null, hoverData.map(function(c) {return c.y1;})) : Lib.mean(hoverData.map(function(c) {return (c.y0 + c.y1) / 2;})); var lx = axLetter === 'x' ? - (winningPoint.x0 + winningPoint.x1) / 2 : + Math.max.apply(null, hoverData.map(function(c) {return c.x1;})) : Lib.mean(hoverData.map(function(c) {return (c.x0 + c.x1) / 2;})); var legendContainer = container.select('g.legend'); From f128cf93820354917a3a033938450f9d9eed57c5 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 10:58:18 -0400 Subject: [PATCH 13/28] for end alignment of unified hover label use minimum to avoid overlap with data points --- src/components/fx/hover.js | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 54f8ecf716b..5b4129328c2 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1071,25 +1071,38 @@ function createHoverText(hoverData, opts, gd) { legendDraw(gd, mockLegend); // Position the hover - var ly = axLetter === 'y' ? - Math.min.apply(null, hoverData.map(function(c) {return c.y1;})) : - Lib.mean(hoverData.map(function(c) {return (c.y0 + c.y1) / 2;})); - var lx = axLetter === 'x' ? - Math.max.apply(null, hoverData.map(function(c) {return c.x1;})) : - Lib.mean(hoverData.map(function(c) {return (c.x0 + c.x1) / 2;})); + var ly; + if(axLetter === 'y') { + ly = Math.min.apply(null, hoverData.map(function(c) {return c.y1;})); + } else { + ly = Lib.mean(hoverData.map(function(c) {return (c.y0 + c.y1) / 2;})); + } + + var lxRight, lxLeft; + if(axLetter === 'x') { + lxRight = Math.max.apply(null, hoverData.map(function(c) {return c.x1;})); + lxLeft = Math.min.apply(null, hoverData.map(function(c) {return c.x0;})); + } else { + lxRight = lxLeft = 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; + lxRight += xa._offset; + lxLeft += xa._offset; ly += ya._offset - tbb.height / 2; + var lx = lxRight; + // 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) { + var anchorRightOK = lxRight + txWidth <= outerWidth; + var anchorLeftOK = lxLeft - txWidth >= 0; + if(!anchorRightOK && anchorLeftOK) { + lx = lxLeft; lx -= txWidth; } else { + lx = lxRight; lx += 2 * HOVERTEXTPAD; } From b26fcbb19065bf494163db7b39ca5ce68e9733ef Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 11:38:50 -0400 Subject: [PATCH 14/28] similar positioning of hover box for y unified --- src/components/fx/hover.js | 40 +++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 5b4129328c2..e2799d506b4 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1071,28 +1071,33 @@ function createHoverText(hoverData, opts, gd) { legendDraw(gd, mockLegend); // Position the hover - var ly; + var lyBottom, lyTop; if(axLetter === 'y') { - ly = Math.min.apply(null, hoverData.map(function(c) {return c.y1;})); + 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 { - ly = Lib.mean(hoverData.map(function(c) {return (c.y0 + c.y1) / 2;})); + lyTop = lyBottom = Lib.mean(hoverData.map(function(c) {return (c.y0 + c.y1) / 2;})); } var lxRight, lxLeft; if(axLetter === 'x') { - lxRight = Math.max.apply(null, hoverData.map(function(c) {return c.x1;})); - lxLeft = Math.min.apply(null, hoverData.map(function(c) {return c.x0;})); + 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 { lxRight = lxLeft = Lib.mean(hoverData.map(function(c) {return (c.x0 + c.x1) / 2;})); } var legendContainer = container.select('g.legend'); var tbb = legendContainer.node().getBoundingClientRect(); - lxRight += xa._offset; - lxLeft += xa._offset; - ly += ya._offset - tbb.height / 2; + var xOffset = xa._offset; + var yOffset = ya._offset; + lxRight += xOffset; + lxLeft += xOffset; + lyTop += yOffset; + lyBottom += yOffset; var lx = lxRight; + var ly = lyTop; // Change horizontal alignment to end up on screen var txWidth = tbb.width + 2 * HOVERTEXTPAD; @@ -1108,18 +1113,17 @@ function createHoverText(hoverData, opts, gd) { // 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 anchorBottomOK = lyBottom + txHeight <= outerHeight; + var anchorTopOK = lyTop - txHeight >= 0; + if(!anchorTopOK && anchorBottomOK) { + ly = lyBottom; + ly += 2 * HOVERTEXTPAD; + } else { + ly = lyTop; + ly -= txHeight; } - legendContainer.attr('transform', strTranslate(lx, ly)); + legendContainer.attr('transform', strTranslate(lx, ly)); return legendContainer; } From 5a80515f2d0d7e0e92a29936b732653e01a13414 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 11:48:56 -0400 Subject: [PATCH 15/28] refactor --- src/components/fx/hover.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index e2799d506b4..18c1988588d 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1089,6 +1089,9 @@ function createHoverText(hoverData, opts, gd) { var legendContainer = container.select('g.legend'); var tbb = legendContainer.node().getBoundingClientRect(); + var txWidth = tbb.width + 2 * HOVERTEXTPAD; + var txHeight = tbb.height + 2 * HOVERTEXTPAD; + var xOffset = xa._offset; var yOffset = ya._offset; lxRight += xOffset; @@ -1100,7 +1103,6 @@ function createHoverText(hoverData, opts, gd) { var ly = lyTop; // Change horizontal alignment to end up on screen - var txWidth = tbb.width + 2 * HOVERTEXTPAD; var anchorRightOK = lxRight + txWidth <= outerWidth; var anchorLeftOK = lxLeft - txWidth >= 0; if(!anchorRightOK && anchorLeftOK) { @@ -1112,7 +1114,6 @@ function createHoverText(hoverData, opts, gd) { } // Change vertical alignement to end up on screen - var txHeight = tbb.height + 2 * HOVERTEXTPAD; var anchorBottomOK = lyBottom + txHeight <= outerHeight; var anchorTopOK = lyTop - txHeight >= 0; if(!anchorTopOK && anchorBottomOK) { From 9716ef07e55e98383b6edd5228a58aa68d32a530 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 13:40:08 -0400 Subject: [PATCH 16/28] improve unified hover box positioning --- src/components/fx/hover.js | 43 +++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 18c1988588d..6a62b7d0832 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1089,39 +1089,34 @@ function createHoverText(hoverData, opts, gd) { var legendContainer = container.select('g.legend'); var tbb = legendContainer.node().getBoundingClientRect(); - var txWidth = tbb.width + 2 * HOVERTEXTPAD; - var txHeight = tbb.height + 2 * HOVERTEXTPAD; + var tWidth = tbb.width; + var tHeight = tbb.height; var xOffset = xa._offset; var yOffset = ya._offset; - lxRight += xOffset; - lxLeft += xOffset; - lyTop += yOffset; - lyBottom += yOffset; - - var lx = lxRight; - var ly = lyTop; - - // Change horizontal alignment to end up on screen - var anchorRightOK = lxRight + txWidth <= outerWidth; - var anchorLeftOK = lxLeft - txWidth >= 0; - if(!anchorRightOK && anchorLeftOK) { + lyBottom += yOffset + HOVERTEXTPAD; + lxRight += xOffset + HOVERTEXTPAD; + lxLeft += xOffset - tWidth - HOVERTEXTPAD; + lyTop += yOffset - tHeight - HOVERTEXTPAD; + + var lx, ly; + + // horizontal alignment to end up on screen + if(lxRight + tWidth + HOVERTEXTPAD <= outerWidth && lxRight - HOVERTEXTPAD >= 0) { + lx = lxRight; + } else if(lxLeft + HOVERTEXTPAD <= outerWidth && lxLeft - HOVERTEXTPAD >= 0) { lx = lxLeft; - lx -= txWidth; } else { - lx = lxRight; - lx += 2 * HOVERTEXTPAD; + lx = xOffset; } - // Change vertical alignement to end up on screen - var anchorBottomOK = lyBottom + txHeight <= outerHeight; - var anchorTopOK = lyTop - txHeight >= 0; - if(!anchorTopOK && anchorBottomOK) { + // vertical alignement to end up on screen + if(lyBottom + tHeight + HOVERTEXTPAD <= outerHeight && lyBottom - HOVERTEXTPAD >= 0) { ly = lyBottom; - ly += 2 * HOVERTEXTPAD; - } else { + } else if(lyTop + HOVERTEXTPAD <= outerHeight && lyTop - HOVERTEXTPAD >= 0) { ly = lyTop; - ly -= txHeight; + } else { + ly = yOffset; } legendContainer.attr('transform', strTranslate(lx, ly)); From c62d52072a76683f91f99963c5c43730393ddb8d Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 14:30:10 -0400 Subject: [PATCH 17/28] edit PR log --- draftlogs/5846_fix.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/draftlogs/5846_fix.md b/draftlogs/5846_fix.md index f2dc2ab0c35..d7ad5e6d965 100644 --- a/draftlogs/5846_fix.md +++ b/draftlogs/5846_fix.md @@ -1 +1,2 @@ - - Fix misplaced hover with period positioning [[#5846](https://github.com/plotly/plotly.js/pull/5846)] + - Fix hover with period alignment points and improve positioning of unified hover label + in order not to obscure referring data points [[#5846](https://github.com/plotly/plotly.js/pull/5846)] From 21a1a93c3bd4dde2d844f245df5569bd8c2cf0bf Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 15:53:22 -0400 Subject: [PATCH 18/28] simplify logic and handle the case of not fitting inside the subplot --- src/components/fx/hover.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 6a62b7d0832..73ed687e40c 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1089,35 +1089,41 @@ function createHoverText(hoverData, opts, gd) { var legendContainer = container.select('g.legend'); var tbb = legendContainer.node().getBoundingClientRect(); - var tWidth = tbb.width; - var tHeight = tbb.height; + var tWidth = tbb.width + 2 * HOVERTEXTPAD; + var tHeight = tbb.height + 2 * HOVERTEXTPAD; var xOffset = xa._offset; var yOffset = ya._offset; - lyBottom += yOffset + HOVERTEXTPAD; - lxRight += xOffset + HOVERTEXTPAD; - lxLeft += xOffset - tWidth - HOVERTEXTPAD; - lyTop += yOffset - tHeight - HOVERTEXTPAD; + lyBottom += yOffset; + lxRight += xOffset; + lxLeft += xOffset - tWidth; + lyTop += yOffset - tHeight; - var lx, ly; + var lx, ly; // top and left positions of the hover box // horizontal alignment to end up on screen - if(lxRight + tWidth + HOVERTEXTPAD <= outerWidth && lxRight - HOVERTEXTPAD >= 0) { + if(lxRight + tWidth <= outerWidth && lxRight >= 0) { lx = lxRight; - } else if(lxLeft + HOVERTEXTPAD <= outerWidth && lxLeft - HOVERTEXTPAD >= 0) { + } else if(lxLeft + tWidth <= outerWidth && lxLeft >= 0) { lx = lxLeft; + } else if(xOffset + tWidth <= outerWidth) { + lx = xOffset; // subplot left corner } else { - lx = xOffset; + lx = 0; // paper left corner } + lx += HOVERTEXTPAD; // vertical alignement to end up on screen - if(lyBottom + tHeight + HOVERTEXTPAD <= outerHeight && lyBottom - HOVERTEXTPAD >= 0) { + if(lyBottom + tHeight <= outerHeight && lyBottom >= 0) { ly = lyBottom; - } else if(lyTop + HOVERTEXTPAD <= outerHeight && lyTop - HOVERTEXTPAD >= 0) { + } else if(lyTop + tHeight <= outerHeight && lyTop >= 0) { ly = lyTop; + } else if(yOffset + tHeight <= outerHeight) { + ly = yOffset; // subplot top corner } else { - ly = yOffset; + ly = 0; // paper top corner } + ly += HOVERTEXTPAD; legendContainer.attr('transform', strTranslate(lx, ly)); return legendContainer; From 16f4554b76f95956a40789795b5910764a0b3cfb Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 16:20:28 -0400 Subject: [PATCH 19/28] when the scatter point wins, OK to occlude bar & other points --- src/components/fx/hover.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 73ed687e40c..7b5354536e9 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1071,20 +1071,35 @@ function createHoverText(hoverData, opts, gd) { legendDraw(gd, mockLegend); // Position the hover + var winningPoint = hoverData[0]; + + // When the scatter point wins, it's OK for the hovelabel to occlude the bar and other points. + var scatterWon = winningPoint.trace.type === 'scatter'; + var lyBottom, lyTop; if(axLetter === 'y') { - 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);})); + if(scatterWon) { + lyTop = Math.min(winningPoint.y0, winningPoint.y1); + lyBottom = Math.max(winningPoint.y0, winningPoint.y1); + } 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;})); + lyTop = lyBottom = Lib.mean(hoverData.map(function(c) { return (c.y0 + c.y1) / 2; })); } var lxRight, lxLeft; if(axLetter === 'x') { - 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);})); + if(scatterWon) { + lxRight = Math.max(winningPoint.x0, winningPoint.x1); + lxLeft = Math.min(winningPoint.x0, winningPoint.x1); + } 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 { - lxRight = lxLeft = Lib.mean(hoverData.map(function(c) {return (c.x0 + c.x1) / 2;})); + lxRight = lxLeft = Lib.mean(hoverData.map(function(c) { return (c.x0 + c.x1) / 2; })); } var legendContainer = container.select('g.legend'); From 8ed847adccdbbef7c6c31a3e626af1cf6ef54eda Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 16:30:06 -0400 Subject: [PATCH 20/28] fix syntax --- src/components/fx/hover.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 7b5354536e9..899e928a282 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1095,7 +1095,7 @@ function createHoverText(hoverData, opts, gd) { lxRight = Math.max(winningPoint.x0, winningPoint.x1); lxLeft = Math.min(winningPoint.x0, winningPoint.x1); } else { - lxRight = Math.max.apply(null, hoverData.map(function(c) { return Math.max(c.x0, c.x1); }) ); + 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 { From 30f61c834a2d0d7b8539c1bfca29b259ef2c2dc2 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 17:12:14 -0400 Subject: [PATCH 21/28] drop out of range pixel from the end --- src/components/fx/hover.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 899e928a282..d4c674fe272 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1117,11 +1117,11 @@ function createHoverText(hoverData, opts, gd) { var lx, ly; // top and left positions of the hover box // horizontal alignment to end up on screen - if(lxRight + tWidth <= outerWidth && lxRight >= 0) { + if(lxRight + tWidth < outerWidth && lxRight >= 0) { lx = lxRight; - } else if(lxLeft + tWidth <= outerWidth && lxLeft >= 0) { + } else if(lxLeft + tWidth < outerWidth && lxLeft >= 0) { lx = lxLeft; - } else if(xOffset + tWidth <= outerWidth) { + } else if(xOffset + tWidth < outerWidth) { lx = xOffset; // subplot left corner } else { lx = 0; // paper left corner @@ -1129,11 +1129,11 @@ function createHoverText(hoverData, opts, gd) { lx += HOVERTEXTPAD; // vertical alignement to end up on screen - if(lyBottom + tHeight <= outerHeight && lyBottom >= 0) { + if(lyBottom + tHeight < outerHeight && lyBottom >= 0) { ly = lyBottom; - } else if(lyTop + tHeight <= outerHeight && lyTop >= 0) { + } else if(lyTop + tHeight < outerHeight && lyTop >= 0) { ly = lyTop; - } else if(yOffset + tHeight <= outerHeight) { + } else if(yOffset + tHeight < outerHeight) { ly = yOffset; // subplot top corner } else { ly = 0; // paper top corner From dca5b1ec95dac51aad3e501d42fbd2384b5da279 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 22 Jul 2021 18:59:30 -0400 Subject: [PATCH 22/28] for x|y hovermodes display spikeline on the winning point - also adjust scattergl and splom hover label position --- src/components/fx/hover.js | 36 ++++++++++++++-------- test/jasmine/tests/hover_spikeline_test.js | 15 ++++++--- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index d4c674fe272..67f710137af 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 && @@ -1074,7 +1086,7 @@ function createHoverText(hoverData, opts, gd) { var winningPoint = hoverData[0]; // When the scatter point wins, it's OK for the hovelabel to occlude the bar and other points. - var scatterWon = winningPoint.trace.type === 'scatter'; + var scatterWon = cartesianScatterPoints[winningPoint.trace.type]; var lyBottom, lyTop; if(axLetter === 'y') { 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); }); From df6353d4e68c59fd013129e7a7f50a48f3fd7a3a Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 23 Jul 2021 08:54:30 -0400 Subject: [PATCH 23/28] adjust vertical and horizontal alignment of unified hover label --- src/components/fx/hover.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 67f710137af..d54173f692f 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1083,8 +1083,11 @@ function createHoverText(hoverData, opts, gd) { legendDraw(gd, mockLegend); // Position the hover + var legendContainer = container.select('g.legend'); + var tbb = legendContainer.node().getBoundingClientRect(); + var tWidth = tbb.width + 2 * HOVERTEXTPAD; + var tHeight = tbb.height + 2 * HOVERTEXTPAD; var winningPoint = hoverData[0]; - // When the scatter point wins, it's OK for the hovelabel to occlude the bar and other points. var scatterWon = cartesianScatterPoints[winningPoint.trace.type]; @@ -1098,7 +1101,7 @@ function createHoverText(hoverData, opts, gd) { 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; })); + lyTop = lyBottom = Lib.mean(hoverData.map(function(c) { return (c.y0 + c.y1) / 2; })) - tHeight / 2; } var lxRight, lxLeft; @@ -1111,14 +1114,9 @@ function createHoverText(hoverData, opts, gd) { lxLeft = Math.min.apply(null, hoverData.map(function(c) { return Math.min(c.x0, c.x1); })); } } else { - lxRight = lxLeft = Lib.mean(hoverData.map(function(c) { return (c.x0 + c.x1) / 2; })); + lxRight = lxLeft = Lib.mean(hoverData.map(function(c) { return (c.x0 + c.x1) / 2; })) - tWidth / 2; } - var legendContainer = container.select('g.legend'); - var tbb = legendContainer.node().getBoundingClientRect(); - var tWidth = tbb.width + 2 * HOVERTEXTPAD; - var tHeight = tbb.height + 2 * HOVERTEXTPAD; - var xOffset = xa._offset; var yOffset = ya._offset; lyBottom += yOffset; From 9217db4e7023acf479cb23bd110ef4b593b680c4 Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 23 Jul 2021 09:54:26 -0400 Subject: [PATCH 24/28] handle end edge case --- src/traces/scatter/hover.js | 3 ++ src/traces/scattergl/hover.js | 3 ++ test/jasmine/tests/hover_label_test.js | 58 ++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js index c86a5b902f8..682d1a474d0 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -112,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/hover.js b/src/traces/scattergl/hover.js index 41e25458f6f..adf39744e01 100644 --- a/src/traces/scattergl/hover.js +++ b/src/traces/scattergl/hover.js @@ -202,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/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index b7f9f648261..9b828bc4d9f 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -5393,6 +5393,64 @@ describe('hovermode: (x|y)unified', function() { }); }); + ['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: 350, ypx: 200 }); + assertLabel({title: 'Feb', items: [ + 'bar : 2', + 'start : 2', + 'end : 2', + ]}); + }) + .then(done, done.fail); + }); + }); + [{ type: 'scatter', alignment: 'start', From 1c7a69d8782f6c4732c39aa1d54ba1215379a9ba Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 23 Jul 2021 10:25:57 -0400 Subject: [PATCH 25/28] adjustment also for winning types e.g. heatmap use scatter logic --- src/components/fx/hover.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index d54173f692f..d38cde94820 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1088,14 +1088,17 @@ function createHoverText(hoverData, opts, gd) { var tWidth = tbb.width + 2 * HOVERTEXTPAD; var tHeight = tbb.height + 2 * HOVERTEXTPAD; var winningPoint = hoverData[0]; - // When the scatter point wins, it's OK for the hovelabel to occlude the bar and other points. - var scatterWon = cartesianScatterPoints[winningPoint.trace.type]; + // 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(scatterWon) { - lyTop = Math.min(winningPoint.y0, winningPoint.y1); - lyBottom = Math.max(winningPoint.y0, winningPoint.y1); + if(pointWon) { + lyTop = (winningPoint.y0 + winningPoint.y1) / 2 - HOVERTEXTPAD; + lyBottom = (winningPoint.y0 + winningPoint.y1) / 2 + 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); })); @@ -1106,9 +1109,9 @@ function createHoverText(hoverData, opts, gd) { var lxRight, lxLeft; if(axLetter === 'x') { - if(scatterWon) { - lxRight = Math.max(winningPoint.x0, winningPoint.x1); - lxLeft = Math.min(winningPoint.x0, winningPoint.x1); + if(pointWon) { + lxRight = (winningPoint.x0 + winningPoint.x1) / 2 + HOVERTEXTPAD; + lxLeft = (winningPoint.x0 + winningPoint.x1) / 2 - 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); })); From f0ef7dbcb2309ff0dd3f6af8bba78ca5d8094601 Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 23 Jul 2021 10:52:28 -0400 Subject: [PATCH 26/28] choose the side of the paper which is closest if unified hover did not fit in subplot --- src/components/fx/hover.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index d38cde94820..1da67e9a5e9 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1088,6 +1088,8 @@ function createHoverText(hoverData, opts, gd) { 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') || @@ -1097,8 +1099,8 @@ function createHoverText(hoverData, opts, gd) { var lyBottom, lyTop; if(axLetter === 'y') { if(pointWon) { - lyTop = (winningPoint.y0 + winningPoint.y1) / 2 - HOVERTEXTPAD; - lyBottom = (winningPoint.y0 + winningPoint.y1) / 2 + HOVERTEXTPAD; + 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); })); @@ -1110,8 +1112,8 @@ function createHoverText(hoverData, opts, gd) { var lxRight, lxLeft; if(axLetter === 'x') { if(pointWon) { - lxRight = (winningPoint.x0 + winningPoint.x1) / 2 + HOVERTEXTPAD; - lxLeft = (winningPoint.x0 + winningPoint.x1) / 2 - HOVERTEXTPAD; + 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); })); @@ -1137,7 +1139,12 @@ function createHoverText(hoverData, opts, gd) { } else if(xOffset + tWidth < outerWidth) { lx = xOffset; // subplot left corner } else { - lx = 0; // paper left corner + // closest left or right side of the paper + if(lxRight - avgX < avgX - lxLeft + tWidth) { + lx = outerWidth - tWidth; + } else { + lx = 0; + } } lx += HOVERTEXTPAD; @@ -1149,7 +1156,12 @@ function createHoverText(hoverData, opts, gd) { } else if(yOffset + tHeight < outerHeight) { ly = yOffset; // subplot top corner } else { - ly = 0; // paper top corner + // closest top or bottom side of the paper + if(lyBottom - avgY < avgY - lyTop + tHeight) { + ly = outerHeight - tHeight; + } else { + ly = 0; + } } ly += HOVERTEXTPAD; From 4a0a504bf9abc7d543c33ecec57f90229cf9eabb Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 23 Jul 2021 10:58:18 -0400 Subject: [PATCH 27/28] update PR log --- draftlogs/5846_fix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/draftlogs/5846_fix.md b/draftlogs/5846_fix.md index d7ad5e6d965..77d4da22ed6 100644 --- a/draftlogs/5846_fix.md +++ b/draftlogs/5846_fix.md @@ -1,2 +1,2 @@ - - Fix hover with period alignment points and improve positioning of unified hover label + - 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)] From cd0b4692f57dfa7a596a63f4ab2e3344dbd8bf06 Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 23 Jul 2021 11:20:32 -0400 Subject: [PATCH 28/28] move unified hover by one pixel better help adjustment tested with period_positioning9 mock & horizontal variant --- src/components/fx/hover.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 1da67e9a5e9..7773787fc73 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1165,7 +1165,7 @@ function createHoverText(hoverData, opts, gd) { } ly += HOVERTEXTPAD; - legendContainer.attr('transform', strTranslate(lx, ly)); + legendContainer.attr('transform', strTranslate(lx - 1, ly - 1)); return legendContainer; }