Skip to content

Commit 2b4584d

Browse files
authored
Merge pull request #6442 from dagroe/issue-3973-overlap-hover
Avoid overlap of point and axis hover labels
2 parents 7427f14 + fe529fa commit 2b4584d

File tree

3 files changed

+228
-31
lines changed

3 files changed

+228
-31
lines changed

draftlogs/6442_fix.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Avoid overlap of point and axis hover labels for `hovermode: 'x'|'y'` [[#6442](https://github.com/plotly/plotly.js/pull/6442)]

src/components/fx/hover.js

+154-27
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,15 @@ exports.loneHover = function loneHover(hoverItems, opts) {
209209

210210
var rotateLabels = false;
211211

212-
var hoverLabel = createHoverText(pointsData, {
212+
var hoverText = createHoverText(pointsData, {
213213
gd: gd,
214214
hovermode: 'closest',
215215
rotateLabels: rotateLabels,
216216
bgColor: opts.bgColor || Color.background,
217217
container: d3.select(opts.container),
218218
outerContainer: opts.outerContainer || opts.container
219219
});
220+
var hoverLabel = hoverText.hoverLabels;
220221

221222
// Fix vertical overlap
222223
var tooltipSpacing = 5;
@@ -819,7 +820,7 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
819820
fullLayout.paper_bgcolor
820821
);
821822

822-
var hoverLabels = createHoverText(hoverData, {
823+
var hoverText = createHoverText(hoverData, {
823824
gd: gd,
824825
hovermode: hovermode,
825826
rotateLabels: rotateLabels,
@@ -829,9 +830,10 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
829830
commonLabelOpts: fullLayout.hoverlabel,
830831
hoverdistance: fullLayout.hoverdistance
831832
});
833+
var hoverLabels = hoverText.hoverLabels;
832834

833835
if(!helpers.isUnifiedHover(hovermode)) {
834-
hoverAvoidOverlaps(hoverLabels, rotateLabels ? 'xa' : 'ya', fullLayout);
836+
hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, hoverText.commonLabelBoundingBox);
835837
alignHoverText(hoverLabels, rotateLabels, fullLayout._invScaleX, fullLayout._invScaleY);
836838
} // TODO: tagName hack is needed to appease geo.js's hack of using eventTarget=true
837839
// we should improve the "fx" API so other plots can use it without these hack.
@@ -942,6 +944,13 @@ function createHoverText(hoverData, opts) {
942944
.classed('axistext', true);
943945
commonLabel.exit().remove();
944946

947+
// set rect (without arrow) behind label below for later collision detection
948+
var commonLabelRect = {
949+
minX: 0,
950+
maxX: 0,
951+
minY: 0,
952+
maxY: 0
953+
};
945954
commonLabel.each(function() {
946955
var label = d3.select(this);
947956
var lpath = Lib.ensureSingle(label, 'path', '', function(s) {
@@ -995,7 +1004,7 @@ function createHoverText(hoverData, opts) {
9951004

9961005
lpath.attr('d', 'M-' + (halfWidth - HOVERARROWSIZE) + ',0' +
9971006
'L-' + (halfWidth - HOVERARROWSIZE * 2) + ',' + topsign + HOVERARROWSIZE +
998-
'H' + (HOVERTEXTPAD + tbb.width / 2) +
1007+
'H' + (halfWidth) +
9991008
'v' + topsign + (HOVERTEXTPAD * 2 + tbb.height) +
10001009
'H-' + halfWidth +
10011010
'V' + topsign + HOVERARROWSIZE +
@@ -1012,12 +1021,23 @@ function createHoverText(hoverData, opts) {
10121021
} else {
10131022
lpath.attr('d', 'M0,0' +
10141023
'L' + HOVERARROWSIZE + ',' + topsign + HOVERARROWSIZE +
1015-
'H' + (HOVERTEXTPAD + tbb.width / 2) +
1024+
'H' + (halfWidth) +
10161025
'v' + topsign + (HOVERTEXTPAD * 2 + tbb.height) +
1017-
'H-' + (HOVERTEXTPAD + tbb.width / 2) +
1026+
'H-' + (halfWidth) +
10181027
'V' + topsign + HOVERARROWSIZE +
10191028
'H-' + HOVERARROWSIZE + 'Z');
10201029
}
1030+
1031+
commonLabelRect.minX = lx - halfWidth;
1032+
commonLabelRect.maxX = lx + halfWidth;
1033+
if(xa.side === 'top') {
1034+
// label on negative y side
1035+
commonLabelRect.minY = ly - (HOVERTEXTPAD * 2 + tbb.height);
1036+
commonLabelRect.maxY = ly - HOVERTEXTPAD;
1037+
} else {
1038+
commonLabelRect.minY = ly + HOVERTEXTPAD;
1039+
commonLabelRect.maxY = ly + (HOVERTEXTPAD * 2 + tbb.height);
1040+
}
10211041
} else {
10221042
var anchor;
10231043
var sgn;
@@ -1045,6 +1065,17 @@ function createHoverText(hoverData, opts) {
10451065
'V-' + (HOVERTEXTPAD + tbb.height / 2) +
10461066
'H' + leftsign + HOVERARROWSIZE + 'V-' + HOVERARROWSIZE + 'Z');
10471067

1068+
commonLabelRect.minY = ly - (HOVERTEXTPAD + tbb.height / 2);
1069+
commonLabelRect.maxY = ly + (HOVERTEXTPAD + tbb.height / 2);
1070+
if(ya.side === 'right') {
1071+
commonLabelRect.minX = lx + HOVERARROWSIZE;
1072+
commonLabelRect.maxX = lx + HOVERARROWSIZE + (HOVERTEXTPAD * 2 + tbb.width);
1073+
} else {
1074+
// label on negative x side
1075+
commonLabelRect.minX = lx - HOVERARROWSIZE - (HOVERTEXTPAD * 2 + tbb.width);
1076+
commonLabelRect.maxX = lx - HOVERARROWSIZE;
1077+
}
1078+
10481079
var halfHeight = tbb.height / 2;
10491080
var lty = outerTop - tbb.top - halfHeight;
10501081
var clipId = 'clip' + fullLayout._uid + 'commonlabel' + ya._id;
@@ -1370,7 +1401,10 @@ function createHoverText(hoverData, opts) {
13701401
} else if(anchorStartOK) {
13711402
hty += dy / 2;
13721403
d.anchor = 'start';
1373-
} else d.anchor = 'middle';
1404+
} else {
1405+
d.anchor = 'middle';
1406+
}
1407+
d.crossPos = hty;
13741408
} else {
13751409
d.pos = hty;
13761410
anchorStartOK = htx + dx / 2 + txTotalWidth <= outerWidth;
@@ -1391,6 +1425,7 @@ function createHoverText(hoverData, opts) {
13911425
if(overflowR > 0) htx -= overflowR;
13921426
if(overflowL < 0) htx += -overflowL;
13931427
}
1428+
d.crossPos = htx;
13941429
}
13951430

13961431
tx.attr('text-anchor', d.anchor);
@@ -1399,7 +1434,10 @@ function createHoverText(hoverData, opts) {
13991434
(rotateLabels ? strRotate(YANGLE) : ''));
14001435
});
14011436

1402-
return hoverLabels;
1437+
return {
1438+
hoverLabels: hoverLabels,
1439+
commonLabelBoundingBox: commonLabelRect
1440+
};
14031441
}
14041442

14051443
function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) {
@@ -1493,7 +1531,9 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) {
14931531
// know what happens if the group spans all the way from one edge to
14941532
// the other, though it hardly matters - there's just too much
14951533
// information then.
1496-
function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) {
1534+
function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabelBoundingBox) {
1535+
var axKey = rotateLabels ? 'xa' : 'ya';
1536+
var crossAxKey = rotateLabels ? 'ya' : 'xa';
14971537
var nummoves = 0;
14981538
var axSign = 1;
14991539
var nLabels = hoverLabels.size();
@@ -1502,23 +1542,83 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) {
15021542
var pointgroups = new Array(nLabels);
15031543
var k = 0;
15041544

1545+
// get extent of axis hover label
1546+
var axisLabelMinX = commonLabelBoundingBox.minX;
1547+
var axisLabelMaxX = commonLabelBoundingBox.maxX;
1548+
var axisLabelMinY = commonLabelBoundingBox.minY;
1549+
var axisLabelMaxY = commonLabelBoundingBox.maxY;
1550+
1551+
var pX = function(x) { return x * fullLayout._invScaleX; };
1552+
var pY = function(y) { return y * fullLayout._invScaleY; };
1553+
15051554
hoverLabels.each(function(d) {
15061555
var ax = d[axKey];
1556+
var crossAx = d[crossAxKey];
15071557
var axIsX = ax._id.charAt(0) === 'x';
15081558
var rng = ax.range;
15091559

15101560
if(k === 0 && rng && ((rng[0] > rng[1]) !== axIsX)) {
15111561
axSign = -1;
15121562
}
1563+
var pmin = 0;
1564+
var pmax = (axIsX ? fullLayout.width : fullLayout.height);
1565+
// in hovermode avoid overlap between hover labels and axis label
1566+
if(fullLayout.hovermode === 'x' || fullLayout.hovermode === 'y') {
1567+
// extent of rect behind hover label on cross axis:
1568+
var offsets = getHoverLabelOffsets(d, rotateLabels);
1569+
var anchor = d.anchor;
1570+
var horzSign = anchor === 'end' ? -1 : 1;
1571+
var labelMin;
1572+
var labelMax;
1573+
if(anchor === 'middle') {
1574+
// use extent of centered rect either on x or y axis depending on current axis
1575+
labelMin = d.crossPos + (axIsX ? pY(offsets.y - d.by / 2) : pX(d.bx / 2 + d.tx2width / 2));
1576+
labelMax = labelMin + (axIsX ? pY(d.by) : pX(d.bx));
1577+
} else {
1578+
// use extend of path (see alignHoverText function) without arrow
1579+
if(axIsX) {
1580+
labelMin = d.crossPos + pY(HOVERARROWSIZE + offsets.y) - pY(d.by / 2 - HOVERARROWSIZE);
1581+
labelMax = labelMin + pY(d.by);
1582+
} else {
1583+
var startX = pX(horzSign * HOVERARROWSIZE + offsets.x);
1584+
var endX = startX + pX(horzSign * d.bx);
1585+
labelMin = d.crossPos + Math.min(startX, endX);
1586+
labelMax = d.crossPos + Math.max(startX, endX);
1587+
}
1588+
}
1589+
1590+
if(axIsX) {
1591+
if(axisLabelMinY !== undefined && axisLabelMaxY !== undefined && Math.min(labelMax, axisLabelMaxY) - Math.max(labelMin, axisLabelMinY) > 1) {
1592+
// has at least 1 pixel overlap with axis label
1593+
if(crossAx.side === 'left') {
1594+
pmin = crossAx._mainLinePosition;
1595+
pmax = fullLayout.width;
1596+
} else {
1597+
pmax = crossAx._mainLinePosition;
1598+
}
1599+
}
1600+
} else {
1601+
if(axisLabelMinX !== undefined && axisLabelMaxX !== undefined && Math.min(labelMax, axisLabelMaxX) - Math.max(labelMin, axisLabelMinX) > 1) {
1602+
// has at least 1 pixel overlap with axis label
1603+
if(crossAx.side === 'top') {
1604+
pmin = crossAx._mainLinePosition;
1605+
pmax = fullLayout.height;
1606+
} else {
1607+
pmax = crossAx._mainLinePosition;
1608+
}
1609+
}
1610+
}
1611+
}
1612+
15131613
pointgroups[k++] = [{
15141614
datum: d,
15151615
traceIndex: d.trace.index,
15161616
dp: 0,
15171617
pos: d.pos,
15181618
posref: d.posref,
15191619
size: d.by * (axIsX ? YFACTOR : 1) / 2,
1520-
pmin: 0,
1521-
pmax: (axIsX ? fullLayout.width : fullLayout.height)
1620+
pmin: pmin,
1621+
pmax: pmax
15221622
}];
15231623
});
15241624

@@ -1662,6 +1762,42 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) {
16621762
}
16631763
}
16641764

1765+
function getHoverLabelOffsets(hoverLabel, rotateLabels) {
1766+
var offsetX = 0;
1767+
var offsetY = hoverLabel.offset;
1768+
1769+
if(rotateLabels) {
1770+
offsetY *= -YSHIFTY;
1771+
offsetX = hoverLabel.offset * YSHIFTX;
1772+
}
1773+
1774+
return {
1775+
x: offsetX,
1776+
y: offsetY
1777+
};
1778+
}
1779+
1780+
/**
1781+
* Calculate the shift in x for text and text2 elements
1782+
*/
1783+
function getTextShiftX(hoverLabel) {
1784+
var alignShift = {start: 1, end: -1, middle: 0}[hoverLabel.anchor];
1785+
var textShiftX = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD);
1786+
var text2ShiftX = textShiftX + alignShift * (hoverLabel.txwidth + HOVERTEXTPAD);
1787+
1788+
var isMiddle = hoverLabel.anchor === 'middle';
1789+
if(isMiddle) {
1790+
textShiftX -= hoverLabel.tx2width / 2;
1791+
text2ShiftX += hoverLabel.txwidth / 2 + HOVERTEXTPAD;
1792+
}
1793+
1794+
return {
1795+
alignShift: alignShift,
1796+
textShiftX: textShiftX,
1797+
text2ShiftX: text2ShiftX
1798+
};
1799+
}
1800+
16651801
function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
16661802
var pX = function(x) { return x * scaleX; };
16671803
var pY = function(y) { return y * scaleY; };
@@ -1675,21 +1811,12 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
16751811
var tx = g.select('text.nums');
16761812
var anchor = d.anchor;
16771813
var horzSign = anchor === 'end' ? -1 : 1;
1678-
var alignShift = {start: 1, end: -1, middle: 0}[anchor];
1679-
var txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD);
1680-
var tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD);
1681-
var offsetX = 0;
1682-
var offsetY = d.offset;
1814+
var shiftX = getTextShiftX(d);
1815+
var offsets = getHoverLabelOffsets(d, rotateLabels);
1816+
var offsetX = offsets.x;
1817+
var offsetY = offsets.y;
16831818

16841819
var isMiddle = anchor === 'middle';
1685-
if(isMiddle) {
1686-
txx -= d.tx2width / 2;
1687-
tx2x += d.txwidth / 2 + HOVERTEXTPAD;
1688-
}
1689-
if(rotateLabels) {
1690-
offsetY *= -YSHIFTY;
1691-
offsetX = d.offset * YSHIFTX;
1692-
}
16931820

16941821
g.select('path')
16951822
.attr('d', isMiddle ?
@@ -1705,7 +1832,7 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
17051832
'V' + pY(offsetY - HOVERARROWSIZE) +
17061833
'Z'));
17071834

1708-
var posX = offsetX + txx;
1835+
var posX = offsetX + shiftX.textShiftX;
17091836
var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD;
17101837
var textAlign = d.textAlign || 'auto';
17111838

@@ -1728,11 +1855,11 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
17281855
if(d.tx2width) {
17291856
g.select('text.name')
17301857
.call(svgTextUtils.positionText,
1731-
pX(tx2x + alignShift * HOVERTEXTPAD + offsetX),
1858+
pX(shiftX.text2ShiftX + shiftX.alignShift * HOVERTEXTPAD + offsetX),
17321859
pY(offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD));
17331860
g.select('rect')
17341861
.call(Drawing.setRect,
1735-
pX(tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX),
1862+
pX(shiftX.text2ShiftX + (shiftX.alignShift - 1) * d.tx2width / 2 + offsetX),
17361863
pY(offsetY - d.by / 2 - 1),
17371864
pX(d.tx2width), pY(d.by + 2));
17381865
}

0 commit comments

Comments
 (0)