Skip to content

Commit ded38a3

Browse files
author
Daniel Gröger
committed
Ignore hover labels' arrows when calculating axis label overlap
1 parent 782bc99 commit ded38a3

File tree

2 files changed

+104
-44
lines changed

2 files changed

+104
-44
lines changed

src/components/fx/hover.js

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
833833
var hoverLabels = hoverText.hoverLabels;
834834

835835
if(!helpers.isUnifiedHover(hovermode)) {
836-
hoverAvoidOverlaps(hoverLabels, rotateLabels ? 'xa' : 'ya', fullLayout, hoverText.commonLabel);
836+
hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, hoverText.commonLabel);
837837
alignHoverText(hoverLabels, rotateLabels, fullLayout._invScaleX, fullLayout._invScaleY);
838838
} // TODO: tagName hack is needed to appease geo.js's hack of using eventTarget=true
839839
// we should improve the "fx" API so other plots can use it without these hack.
@@ -1510,8 +1510,9 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) {
15101510
// know what happens if the group spans all the way from one edge to
15111511
// the other, though it hardly matters - there's just too much
15121512
// information then.
1513-
function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout, commonLabel) {
1514-
var crossAxKey = axKey === 'xa' ? 'ya' : 'xa';
1513+
function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) {
1514+
var axKey = rotateLabels ? 'xa' : 'ya';
1515+
var crossAxKey = rotateLabels ? 'ya' : 'xa';
15151516
var nummoves = 0;
15161517
var axSign = 1;
15171518
var nLabels = hoverLabels.size();
@@ -1525,17 +1526,13 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout, commonLabel) {
15251526
if(commonLabel) {
15261527
commonLabel.label.each(function() {
15271528
var selection = d3.select(this);
1528-
if(selection && selection.length) {
1529-
var labels = selection[0];
1530-
if(labels && labels.length) {
1531-
var label = labels[0];
1532-
var bbox = label.getBBox();
1533-
if(bbox) {
1534-
axisLabelMinX = commonLabel.lx;
1535-
axisLabelMaxX = commonLabel.lx + bbox.width;
1536-
axisLabelMinY = commonLabel.ly;
1537-
axisLabelMaxY = commonLabel.ly + bbox.height;
1538-
}
1529+
if(selection && selection.length && selection[0] && selection[0].length && selection[0][0]) {
1530+
var bbox = selection[0][0].getBBox();
1531+
if(bbox) {
1532+
axisLabelMinX = commonLabel.lx;
1533+
axisLabelMaxX = commonLabel.lx + bbox.width;
1534+
axisLabelMinY = commonLabel.ly;
1535+
axisLabelMaxY = commonLabel.ly + bbox.height;
15391536
}
15401537
}
15411538
});
@@ -1553,11 +1550,16 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout, commonLabel) {
15531550
var pmin = 0;
15541551
var pmax = (axIsX ? fullLayout.width : fullLayout.height);
15551552
if(fullLayout.hovermode === 'x' || fullLayout.hovermode === 'y') {
1556-
// extent of hover label on cross axis:
1557-
var labelMinX = d.crossPos;
1558-
var labelMaxX = d.crossPos + d.txwidth;
1553+
// extent of rect behind hover label on cross axis (without arrow):
1554+
var offsets = getHoverLabelOffsets(d, rotateLabels);
1555+
var shiftX = getLabelShiftX(d);
1556+
// calculation based on alignHoverText function
1557+
var offsetRectX = (shiftX.x2x + (shiftX.alignShift - 1) * d.tx2width / 2 + offsets.x) * fullLayout._invScaleX;
1558+
1559+
var labelMinX = d.crossPos + offsetRectX;
1560+
var labelMaxX = labelMinX + d.tx2width;
15591561
if(axIsX) {
1560-
if(Math.max(labelMinX, axisLabelMinY) <= Math.min(labelMaxX, axisLabelMaxY)) {
1562+
if(axisLabelMinY !== undefined && axisLabelMaxY !== undefined && Math.max(labelMinX, axisLabelMinY) <= Math.min(labelMaxX, axisLabelMaxY)) {
15611563
// has overlap with axis label
15621564
if(crossAx.side === 'left') {
15631565
pmin = crossAx._mainLinePosition;
@@ -1567,7 +1569,7 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout, commonLabel) {
15671569
}
15681570
}
15691571
} else {
1570-
if(Math.max(labelMinX, axisLabelMinX) <= Math.min(labelMaxX, axisLabelMaxX)) {
1572+
if(axisLabelMinX !== undefined && axisLabelMaxX !== undefined && Math.max(labelMinX, axisLabelMinX) <= Math.min(labelMaxX, axisLabelMaxX)) {
15711573
// has overlap with axis label
15721574
if(crossAx.side === 'top') {
15731575
pmin = crossAx._mainLinePosition;
@@ -1731,6 +1733,39 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout, commonLabel) {
17311733
}
17321734
}
17331735

1736+
function getHoverLabelOffsets(hoverLabel, rotateLabels) {
1737+
var offsetX = 0;
1738+
var offsetY = hoverLabel.offset;
1739+
1740+
if(rotateLabels) {
1741+
offsetY *= -YSHIFTY;
1742+
offsetX = hoverLabel.offset * YSHIFTX;
1743+
}
1744+
1745+
return {
1746+
x: offsetX,
1747+
y: offsetY
1748+
};
1749+
}
1750+
1751+
function getLabelShiftX(hoverLabel) {
1752+
var alignShift = {start: 1, end: -1, middle: 0}[hoverLabel.anchor];
1753+
var txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD);
1754+
var tx2x = txx + alignShift * (hoverLabel.txwidth + HOVERTEXTPAD);
1755+
1756+
var isMiddle = hoverLabel.anchor === 'middle';
1757+
if(isMiddle) {
1758+
txx -= hoverLabel.tx2width / 2;
1759+
tx2x += hoverLabel.txwidth / 2 + HOVERTEXTPAD;
1760+
}
1761+
1762+
return {
1763+
alignShift: alignShift,
1764+
xx: txx,
1765+
x2x: tx2x
1766+
};
1767+
}
1768+
17341769
function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
17351770
var pX = function(x) { return x * scaleX; };
17361771
var pY = function(y) { return y * scaleY; };
@@ -1744,38 +1779,27 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
17441779
var tx = g.select('text.nums');
17451780
var anchor = d.anchor;
17461781
var horzSign = anchor === 'end' ? -1 : 1;
1747-
var alignShift = {start: 1, end: -1, middle: 0}[anchor];
1748-
var txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD);
1749-
var tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD);
1750-
var offsetX = 0;
1751-
var offsetY = d.offset;
1782+
var shiftX = getLabelShiftX(d);
1783+
var offsets = getHoverLabelOffsets(d, rotateLabels);
17521784

17531785
var isMiddle = anchor === 'middle';
1754-
if(isMiddle) {
1755-
txx -= d.tx2width / 2;
1756-
tx2x += d.txwidth / 2 + HOVERTEXTPAD;
1757-
}
1758-
if(rotateLabels) {
1759-
offsetY *= -YSHIFTY;
1760-
offsetX = d.offset * YSHIFTX;
1761-
}
17621786

17631787
g.select('path')
17641788
.attr('d', isMiddle ?
17651789
// middle aligned: rect centered on data
1766-
('M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsetY - d.by / 2) +
1790+
('M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsets.y - d.by / 2) +
17671791
'h' + pX(d.bx) + 'v' + pY(d.by) + 'h-' + pX(d.bx) + 'Z') :
17681792
// left or right aligned: side rect with arrow to data
1769-
('M0,0L' + pX(horzSign * HOVERARROWSIZE + offsetX) + ',' + pY(HOVERARROWSIZE + offsetY) +
1793+
('M0,0L' + pX(horzSign * HOVERARROWSIZE + offsets.x) + ',' + pY(HOVERARROWSIZE + offsets.y) +
17701794
'v' + pY(d.by / 2 - HOVERARROWSIZE) +
17711795
'h' + pX(horzSign * d.bx) +
17721796
'v-' + pY(d.by) +
1773-
'H' + pX(horzSign * HOVERARROWSIZE + offsetX) +
1774-
'V' + pY(offsetY - HOVERARROWSIZE) +
1797+
'H' + pX(horzSign * HOVERARROWSIZE + offsets.x) +
1798+
'V' + pY(offsets.y - HOVERARROWSIZE) +
17751799
'Z'));
17761800

1777-
var posX = offsetX + txx;
1778-
var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD;
1801+
var posX = offsets.x + shiftX.xx;
1802+
var posY = offsets.y + d.ty0 - d.by / 2 + HOVERTEXTPAD;
17791803
var textAlign = d.textAlign || 'auto';
17801804

17811805
if(textAlign !== 'auto') {
@@ -1797,12 +1821,12 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
17971821
if(d.tx2width) {
17981822
g.select('text.name')
17991823
.call(svgTextUtils.positionText,
1800-
pX(tx2x + alignShift * HOVERTEXTPAD + offsetX),
1801-
pY(offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD));
1824+
pX(shiftX.x2x + shiftX.alignShift * HOVERTEXTPAD + offsets.x),
1825+
pY(offsets.y + d.ty0 - d.by / 2 + HOVERTEXTPAD));
18021826
g.select('rect')
18031827
.call(Drawing.setRect,
1804-
pX(tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX),
1805-
pY(offsetY - d.by / 2 - 1),
1828+
pX(shiftX.x2x + (shiftX.alignShift - 1) * d.tx2width / 2 + offsets.x),
1829+
pY(offsets.y - d.by / 2 - 1),
18061830
pX(d.tx2width), pY(d.by + 2));
18071831
}
18081832
});

test/jasmine/tests/hover_label_test.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,7 +1513,7 @@ describe('hover info', function() {
15131513

15141514
describe('overflowing hover labels', function() {
15151515
var trace = {y: [1, 2, 3], text: ['', 'a<br>b<br>c', '']};
1516-
var data = [trace, trace, trace, trace, trace, trace, trace];
1516+
var data = [trace, trace, trace, trace, trace, trace, trace, trace];
15171517
var layout = {
15181518
width: 600, height: 600, showlegend: false,
15191519
margin: {l: 100, r: 100, t: 100, b: 100},
@@ -1531,7 +1531,7 @@ describe('hover info', function() {
15311531
return d3Select(gd).selectAll('g.hovertext').size();
15321532
}
15331533

1534-
it('shows as many labels as will fit on the div, not on the subplot', function(done) {
1534+
it('shows as many labels as will fit on the div, not on the subplot, when labels do not overlap the axis label', function(done) {
15351535
_hoverNatural(gd, 200, 200);
15361536

15371537
expect(labelCount()).toBe(7);
@@ -1547,6 +1547,42 @@ describe('hover info', function() {
15471547
});
15481548
});
15491549

1550+
describe('overlapping hover labels', function() {
1551+
var trace = {y: [1, 2, 3], x: ['01.01.2020', '02.01.2020', '03.01.2020'], text: ['', 'a<br>b<br>c', '']};
1552+
var data = [trace, trace, trace, trace, trace, trace, trace, trace];
1553+
var layout = {
1554+
width: 600, height: 600, showlegend: false,
1555+
margin: {l: 100, r: 100, t: 100, b: 100},
1556+
hovermode: 'x'
1557+
};
1558+
1559+
var gd;
1560+
1561+
beforeEach(function(done) {
1562+
gd = createGraphDiv();
1563+
Plotly.newPlot(gd, data, layout).then(done);
1564+
});
1565+
1566+
function labelCount() {
1567+
return d3Select(gd).selectAll('g.hovertext').size();
1568+
}
1569+
1570+
it('does not show labels that would overlap the axis hover label', function(done) {
1571+
_hoverNatural(gd, 200, 200);
1572+
1573+
expect(labelCount()).toBe(6);
1574+
1575+
Plotly.relayout(gd, {'yaxis.domain': [0.48, 0.52]})
1576+
.then(function() {
1577+
_hoverNatural(gd, 150, 200);
1578+
_hoverNatural(gd, 200, 200);
1579+
1580+
expect(labelCount()).toBe(6);
1581+
})
1582+
.then(done, done.fail);
1583+
});
1584+
});
1585+
15501586
describe('alignment while avoiding overlaps:', function() {
15511587
var gd;
15521588

0 commit comments

Comments
 (0)