Skip to content

Commit 87e4e12

Browse files
authored
Merge pull request #5846 from plotly/fix5822-hover-with-period
Fix hover with period alignment points and improve positioning of spikes and unified hover label
2 parents 93de1ea + cd0b469 commit 87e4e12

File tree

19 files changed

+474
-115
lines changed

19 files changed

+474
-115
lines changed

draftlogs/5846_fix.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Fix hover with period alignment points and improve positioning of spikes and unified hover label
2+
in order not to obscure referring data points [[#5846](https://github.com/plotly/plotly.js/pull/5846)]

src/components/fx/hover.js

+102-39
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ var multipleHoverPoints = {
4646
candlestick: true
4747
};
4848

49+
var cartesianScatterPoints = {
50+
scatter: true,
51+
scattergl: true,
52+
splom: true
53+
};
54+
4955
// fx.hover: highlight data on hover
5056
// evt can be a mousemove event, or an object with data about what points
5157
// to hover on
@@ -574,12 +580,15 @@ function _hover(gd, evt, subplot, noHoverEvent) {
574580

575581
findHoverPoints();
576582

577-
function selectClosestPoint(pointsData, spikedistance) {
583+
function selectClosestPoint(pointsData, spikedistance, spikeOnWinning) {
578584
var resultPoint = null;
579585
var minDistance = Infinity;
580586
var thisSpikeDistance;
587+
581588
for(var i = 0; i < pointsData.length; i++) {
582589
thisSpikeDistance = pointsData[i].spikeDistance;
590+
if(spikeOnWinning && i === 0) thisSpikeDistance = -Infinity;
591+
583592
if(thisSpikeDistance <= minDistance && thisSpikeDistance <= spikedistance) {
584593
resultPoint = pointsData[i];
585594
minDistance = thisSpikeDistance;
@@ -616,19 +625,30 @@ function _hover(gd, evt, subplot, noHoverEvent) {
616625
};
617626
gd._spikepoints = newspikepoints;
618627

628+
var sortHoverData = function() {
629+
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });
630+
631+
// move period positioned points and box/bar-like traces to the end of the list
632+
hoverData = orderRangePoints(hoverData, hovermode);
633+
};
634+
sortHoverData();
635+
636+
var axLetter = hovermode.charAt(0);
637+
var spikeOnWinning = (axLetter === 'x' || axLetter === 'y') && hoverData[0] && cartesianScatterPoints[hoverData[0].trace.type];
638+
619639
// Now if it is not restricted by spikedistance option, set the points to draw the spikelines
620640
if(hasCartesian && (spikedistance !== 0)) {
621641
if(hoverData.length !== 0) {
622642
var tmpHPointData = hoverData.filter(function(point) {
623643
return point.ya.showspikes;
624644
});
625-
var tmpHPoint = selectClosestPoint(tmpHPointData, spikedistance);
645+
var tmpHPoint = selectClosestPoint(tmpHPointData, spikedistance, spikeOnWinning);
626646
spikePoints.hLinePoint = fillSpikePoint(tmpHPoint);
627647

628648
var tmpVPointData = hoverData.filter(function(point) {
629649
return point.xa.showspikes;
630650
});
631-
var tmpVPoint = selectClosestPoint(tmpVPointData, spikedistance);
651+
var tmpVPoint = selectClosestPoint(tmpVPointData, spikedistance, spikeOnWinning);
632652
spikePoints.vLinePoint = fillSpikePoint(tmpVPoint);
633653
}
634654
}
@@ -650,14 +670,6 @@ function _hover(gd, evt, subplot, noHoverEvent) {
650670
}
651671
}
652672

653-
var sortHoverData = function() {
654-
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });
655-
656-
// move period positioned points and box/bar-like traces to the end of the list
657-
hoverData = orderRangePoints(hoverData, hovermode);
658-
};
659-
sortHoverData();
660-
661673
if(
662674
helpers.isXYhover(_mode) &&
663675
hoverData[0].length !== 0 &&
@@ -1071,41 +1083,89 @@ function createHoverText(hoverData, opts, gd) {
10711083
legendDraw(gd, mockLegend);
10721084

10731085
// Position the hover
1074-
var winningPoint = hoverData[0];
1075-
var ly = axLetter === 'y' ?
1076-
(winningPoint.y0 + winningPoint.y1) / 2 : Lib.mean(hoverData.map(function(c) {return (c.y0 + c.y1) / 2;}));
1077-
var lx = axLetter === 'x' ?
1078-
(winningPoint.x0 + winningPoint.x1) / 2 : Lib.mean(hoverData.map(function(c) {return (c.x0 + c.x1) / 2;}));
1079-
10801086
var legendContainer = container.select('g.legend');
10811087
var tbb = legendContainer.node().getBoundingClientRect();
1082-
lx += xa._offset;
1083-
ly += ya._offset - tbb.height / 2;
1084-
1085-
// Change horizontal alignment to end up on screen
1086-
var txWidth = tbb.width + 2 * HOVERTEXTPAD;
1087-
var anchorStartOK = lx + txWidth <= outerWidth;
1088-
var anchorEndOK = lx - txWidth >= 0;
1089-
if(!anchorStartOK && anchorEndOK) {
1090-
lx -= txWidth;
1088+
var tWidth = tbb.width + 2 * HOVERTEXTPAD;
1089+
var tHeight = tbb.height + 2 * HOVERTEXTPAD;
1090+
var winningPoint = hoverData[0];
1091+
var avgX = (winningPoint.x0 + winningPoint.x1) / 2;
1092+
var avgY = (winningPoint.y0 + winningPoint.y1) / 2;
1093+
// When a scatter (or e.g. heatmap) point wins, it's OK for the hovelabel to occlude the bar and other points.
1094+
var pointWon = !(
1095+
Registry.traceIs(winningPoint.trace, 'bar-like') ||
1096+
Registry.traceIs(winningPoint.trace, 'box-violin')
1097+
);
1098+
1099+
var lyBottom, lyTop;
1100+
if(axLetter === 'y') {
1101+
if(pointWon) {
1102+
lyTop = avgY - HOVERTEXTPAD;
1103+
lyBottom = avgY + HOVERTEXTPAD;
1104+
} else {
1105+
lyTop = Math.min.apply(null, hoverData.map(function(c) { return Math.min(c.y0, c.y1); }));
1106+
lyBottom = Math.max.apply(null, hoverData.map(function(c) { return Math.max(c.y0, c.y1); }));
1107+
}
1108+
} else {
1109+
lyTop = lyBottom = Lib.mean(hoverData.map(function(c) { return (c.y0 + c.y1) / 2; })) - tHeight / 2;
1110+
}
1111+
1112+
var lxRight, lxLeft;
1113+
if(axLetter === 'x') {
1114+
if(pointWon) {
1115+
lxRight = avgX + HOVERTEXTPAD;
1116+
lxLeft = avgX - HOVERTEXTPAD;
1117+
} else {
1118+
lxRight = Math.max.apply(null, hoverData.map(function(c) { return Math.max(c.x0, c.x1); }));
1119+
lxLeft = Math.min.apply(null, hoverData.map(function(c) { return Math.min(c.x0, c.x1); }));
1120+
}
10911121
} else {
1092-
lx += 2 * HOVERTEXTPAD;
1122+
lxRight = lxLeft = Lib.mean(hoverData.map(function(c) { return (c.x0 + c.x1) / 2; })) - tWidth / 2;
10931123
}
10941124

1095-
// Change vertical alignement to end up on screen
1096-
var txHeight = tbb.height + 2 * HOVERTEXTPAD;
1097-
var overflowTop = ly <= outerTop;
1098-
var overflowBottom = ly + txHeight >= outerHeight;
1099-
var canFit = txHeight <= outerHeight;
1100-
if(canFit) {
1101-
if(overflowTop) {
1102-
ly = ya._offset + 2 * HOVERTEXTPAD;
1103-
} else if(overflowBottom) {
1104-
ly = outerHeight - txHeight;
1125+
var xOffset = xa._offset;
1126+
var yOffset = ya._offset;
1127+
lyBottom += yOffset;
1128+
lxRight += xOffset;
1129+
lxLeft += xOffset - tWidth;
1130+
lyTop += yOffset - tHeight;
1131+
1132+
var lx, ly; // top and left positions of the hover box
1133+
1134+
// horizontal alignment to end up on screen
1135+
if(lxRight + tWidth < outerWidth && lxRight >= 0) {
1136+
lx = lxRight;
1137+
} else if(lxLeft + tWidth < outerWidth && lxLeft >= 0) {
1138+
lx = lxLeft;
1139+
} else if(xOffset + tWidth < outerWidth) {
1140+
lx = xOffset; // subplot left corner
1141+
} else {
1142+
// closest left or right side of the paper
1143+
if(lxRight - avgX < avgX - lxLeft + tWidth) {
1144+
lx = outerWidth - tWidth;
1145+
} else {
1146+
lx = 0;
11051147
}
11061148
}
1107-
legendContainer.attr('transform', strTranslate(lx, ly));
1149+
lx += HOVERTEXTPAD;
1150+
1151+
// vertical alignement to end up on screen
1152+
if(lyBottom + tHeight < outerHeight && lyBottom >= 0) {
1153+
ly = lyBottom;
1154+
} else if(lyTop + tHeight < outerHeight && lyTop >= 0) {
1155+
ly = lyTop;
1156+
} else if(yOffset + tHeight < outerHeight) {
1157+
ly = yOffset; // subplot top corner
1158+
} else {
1159+
// closest top or bottom side of the paper
1160+
if(lyBottom - avgY < avgY - lyTop + tHeight) {
1161+
ly = outerHeight - tHeight;
1162+
} else {
1163+
ly = 0;
1164+
}
1165+
}
1166+
ly += HOVERTEXTPAD;
11081167

1168+
legendContainer.attr('transform', strTranslate(lx - 1, ly - 1));
11091169
return legendContainer;
11101170
}
11111171

@@ -1934,7 +1994,10 @@ function getCoord(axLetter, winningPoint, fullLayout) {
19341994
var val = winningPoint[axLetter + 'Val'];
19351995

19361996
if(ax.type === 'category') val = ax._categoriesMap[val];
1937-
else if(ax.type === 'date') val = ax.d2c(val);
1997+
else if(ax.type === 'date') {
1998+
var period = winningPoint[axLetter + 'Period'];
1999+
val = ax.d2c(period !== undefined ? period : val);
2000+
}
19382001

19392002
var cd0 = winningPoint.cd[winningPoint.index];
19402003
if(cd0 && cd0.t && cd0.t.posLetter === ax._id) {

src/plots/cartesian/align_period.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ var constants = require('../../constants/numerical');
88
var ONEAVGMONTH = constants.ONEAVGMONTH;
99

1010
module.exports = function alignPeriod(trace, ax, axLetter, vals) {
11-
if(ax.type !== 'date') return vals;
11+
if(ax.type !== 'date') return {vals: vals};
1212

1313
var alignment = trace[axLetter + 'periodalignment'];
14-
if(!alignment) return vals;
14+
if(!alignment) return {vals: vals};
1515

1616
var period = trace[axLetter + 'period'];
1717
var mPeriod;
1818
if(isNumeric(period)) {
1919
period = +period;
20-
if(period <= 0) return vals;
20+
if(period <= 0) return {vals: vals};
2121
} else if(typeof period === 'string' && period.charAt(0) === 'M') {
2222
var n = +(period.substring(1));
2323
if(n > 0 && Math.round(n) === n) {
2424
mPeriod = n;
25-
} else return vals;
25+
} else return {vals: vals};
2626
}
2727

2828
var calendar = ax.calendar;
@@ -35,6 +35,9 @@ module.exports = function alignPeriod(trace, ax, axLetter, vals) {
3535
var base = dateTime2ms(period0, calendar) || 0;
3636

3737
var newVals = [];
38+
var starts = [];
39+
var ends = [];
40+
3841
var len = vals.length;
3942
for(var i = 0; i < len; i++) {
4043
var v = vals[i];
@@ -77,6 +80,14 @@ module.exports = function alignPeriod(trace, ax, axLetter, vals) {
7780
isEnd ? endTime :
7881
(startTime + endTime) / 2
7982
);
83+
84+
starts[i] = startTime;
85+
ends[i] = endTime;
8086
}
81-
return newVals;
87+
88+
return {
89+
vals: newVals,
90+
starts: starts,
91+
ends: ends
92+
};
8293
};

src/traces/bar/calc.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,24 @@ var calcSelection = require('../scatter/calc_selection');
1010
module.exports = function calc(gd, trace) {
1111
var xa = Axes.getFromId(gd, trace.xaxis || 'x');
1212
var ya = Axes.getFromId(gd, trace.yaxis || 'y');
13-
var size, pos, origPos;
13+
var size, pos, origPos, pObj, hasPeriod;
1414

1515
var sizeOpts = {
1616
msUTC: !!(trace.base || trace.base === 0)
1717
};
1818

19-
var hasPeriod;
2019
if(trace.orientation === 'h') {
2120
size = xa.makeCalcdata(trace, 'x', sizeOpts);
2221
origPos = ya.makeCalcdata(trace, 'y');
23-
pos = alignPeriod(trace, ya, 'y', origPos);
22+
pObj = alignPeriod(trace, ya, 'y', origPos);
2423
hasPeriod = !!trace.yperiodalignment;
2524
} else {
2625
size = ya.makeCalcdata(trace, 'y', sizeOpts);
2726
origPos = xa.makeCalcdata(trace, 'x');
28-
pos = alignPeriod(trace, xa, 'x', origPos);
27+
pObj = alignPeriod(trace, xa, 'x', origPos);
2928
hasPeriod = !!trace.xperiodalignment;
3029
}
30+
pos = pObj.vals;
3131

3232
// create the "calculated data" to plot
3333
var serieslen = Math.min(pos.length, size.length);
@@ -39,6 +39,8 @@ module.exports = function calc(gd, trace) {
3939

4040
if(hasPeriod) {
4141
cd[i].orig_p = origPos[i]; // used by hover
42+
cd[i].pEnd = pObj.ends[i];
43+
cd[i].pStart = pObj.starts[i];
4244
}
4345

4446
if(trace.ids) {

src/traces/bar/cross_trace_calc.js

+8
Original file line numberDiff line numberDiff line change
@@ -436,12 +436,20 @@ function setBarCenterAndWidth(pa, sieve) {
436436
var barwidth = t.barwidth;
437437
var barwidthIsArray = Array.isArray(barwidth);
438438

439+
var trace = calcTrace[0].trace;
440+
var isPeriod = !!trace[pLetter + 'periodalignment'];
441+
439442
for(var j = 0; j < calcTrace.length; j++) {
440443
var calcBar = calcTrace[j];
441444

442445
// store the actual bar width and position, for use by hover
443446
var width = calcBar.w = barwidthIsArray ? barwidth[j] : barwidth;
444447
calcBar[pLetter] = calcBar.p + (poffsetIsArray ? poffset[j] : poffset) + width / 2;
448+
449+
if(isPeriod) {
450+
calcBar.wPeriod =
451+
calcBar.pEnd - calcBar.pStart;
452+
}
445453
}
446454
}
447455
}

src/traces/bar/hover.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,9 @@ function hoverOnBars(pointData, xval, yval, hovermode, opts) {
5757
function thisBarMaxPos(di) { return thisBarExtPos(di, 1); }
5858

5959
function thisBarExtPos(di, sgn) {
60-
if(period) {
61-
return di.p + sgn * Math.abs(di.p - di.orig_p);
62-
}
63-
return di[posLetter] + sgn * di.w / 2;
60+
var w = (period) ? di.wPeriod : di.w;
61+
62+
return di[posLetter] + sgn * w / 2;
6463
}
6564

6665
var minPos = isClosest || period ?
@@ -180,6 +179,9 @@ function hoverOnBars(pointData, xval, yval, hovermode, opts) {
180179

181180
var hasPeriod = di.orig_p !== undefined;
182181
pointData[posLetter + 'LabelVal'] = hasPeriod ? di.orig_p : di.p;
182+
if(hasPeriod) {
183+
pointData[posLetter + 'Period'] = di.p;
184+
}
183185

184186
pointData.labelLabel = hoverLabelText(pa, pointData[posLetter + 'LabelVal'], trace[posLetter + 'hoverformat']);
185187
pointData.valueLabel = hoverLabelText(sa, pointData[sizeLetter + 'LabelVal'], trace[sizeLetter + 'hoverformat']);

src/traces/box/calc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ function getPosArrays(trace, posLetter, posAxis, num) {
311311

312312
if(hasPosArray || (hasPos0 && hasPosStep)) {
313313
var origPos = posAxis.makeCalcdata(trace, posLetter);
314-
var pos = alignPeriod(trace, posAxis, posLetter, origPos);
314+
var pos = alignPeriod(trace, posAxis, posLetter, origPos).vals;
315315
return [pos, origPos];
316316
}
317317

src/traces/candlestick/calc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module.exports = function(gd, trace) {
1212
var ya = Axes.getFromId(gd, trace.yaxis);
1313

1414
var origX = xa.makeCalcdata(trace, 'x');
15-
var x = alignPeriod(trace, xa, 'x', origX);
15+
var x = alignPeriod(trace, xa, 'x', origX).vals;
1616

1717
var cd = calcCommon(gd, trace, origX, x, ya, ptFunc);
1818

src/traces/funnel/calc.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@ var BADNUM = require('../../constants/numerical').BADNUM;
99
module.exports = function calc(gd, trace) {
1010
var xa = Axes.getFromId(gd, trace.xaxis || 'x');
1111
var ya = Axes.getFromId(gd, trace.yaxis || 'y');
12-
var size, pos, origPos, i, cdi;
12+
var size, pos, origPos, pObj, hasPeriod, i, cdi;
1313

14-
var hasPeriod;
1514
if(trace.orientation === 'h') {
1615
size = xa.makeCalcdata(trace, 'x');
1716
origPos = ya.makeCalcdata(trace, 'y');
18-
pos = alignPeriod(trace, ya, 'y', origPos);
17+
pObj = alignPeriod(trace, ya, 'y', origPos);
1918
hasPeriod = !!trace.yperiodalignment;
2019
} else {
2120
size = ya.makeCalcdata(trace, 'y');
2221
origPos = xa.makeCalcdata(trace, 'x');
23-
pos = alignPeriod(trace, xa, 'x', origPos);
22+
pObj = alignPeriod(trace, xa, 'x', origPos);
2423
hasPeriod = !!trace.xperiodalignment;
2524
}
25+
pos = pObj.vals;
2626

2727
// create the "calculated data" to plot
2828
var serieslen = Math.min(pos.length, size.length);
@@ -55,6 +55,8 @@ module.exports = function calc(gd, trace) {
5555

5656
if(hasPeriod) {
5757
cd[i].orig_p = origPos[i]; // used by hover
58+
cd[i].pEnd = pObj.ends[i];
59+
cd[i].pStart = pObj.starts[i];
5860
}
5961

6062
if(trace.ids) {

0 commit comments

Comments
 (0)