diff --git a/draftlogs/6442_fix.md b/draftlogs/6442_fix.md
new file mode 100644
index 00000000000..bb9b0f7f348
--- /dev/null
+++ b/draftlogs/6442_fix.md
@@ -0,0 +1 @@
+ - Avoid overlap of point and axis hover labels for `hovermode: 'x'|'y'` [[#6442](https://github.com/plotly/plotly.js/pull/6442)]
\ No newline at end of file
diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index 3ec8f87db48..3cf438f28be 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -209,7 +209,7 @@ exports.loneHover = function loneHover(hoverItems, opts) {
var rotateLabels = false;
- var hoverLabel = createHoverText(pointsData, {
+ var hoverText = createHoverText(pointsData, {
gd: gd,
hovermode: 'closest',
rotateLabels: rotateLabels,
@@ -217,6 +217,7 @@ exports.loneHover = function loneHover(hoverItems, opts) {
container: d3.select(opts.container),
outerContainer: opts.outerContainer || opts.container
});
+ var hoverLabel = hoverText.hoverLabels;
// Fix vertical overlap
var tooltipSpacing = 5;
@@ -819,7 +820,7 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
fullLayout.paper_bgcolor
);
- var hoverLabels = createHoverText(hoverData, {
+ var hoverText = createHoverText(hoverData, {
gd: gd,
hovermode: hovermode,
rotateLabels: rotateLabels,
@@ -829,9 +830,10 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
commonLabelOpts: fullLayout.hoverlabel,
hoverdistance: fullLayout.hoverdistance
});
+ var hoverLabels = hoverText.hoverLabels;
if(!helpers.isUnifiedHover(hovermode)) {
- hoverAvoidOverlaps(hoverLabels, rotateLabels ? 'xa' : 'ya', fullLayout);
+ hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, hoverText.commonLabelBoundingBox);
alignHoverText(hoverLabels, rotateLabels, fullLayout._invScaleX, fullLayout._invScaleY);
} // TODO: tagName hack is needed to appease geo.js's hack of using eventTarget=true
// we should improve the "fx" API so other plots can use it without these hack.
@@ -942,6 +944,13 @@ function createHoverText(hoverData, opts) {
.classed('axistext', true);
commonLabel.exit().remove();
+ // set rect (without arrow) behind label below for later collision detection
+ var commonLabelRect = {
+ minX: 0,
+ maxX: 0,
+ minY: 0,
+ maxY: 0
+ };
commonLabel.each(function() {
var label = d3.select(this);
var lpath = Lib.ensureSingle(label, 'path', '', function(s) {
@@ -995,7 +1004,7 @@ function createHoverText(hoverData, opts) {
lpath.attr('d', 'M-' + (halfWidth - HOVERARROWSIZE) + ',0' +
'L-' + (halfWidth - HOVERARROWSIZE * 2) + ',' + topsign + HOVERARROWSIZE +
- 'H' + (HOVERTEXTPAD + tbb.width / 2) +
+ 'H' + (halfWidth) +
'v' + topsign + (HOVERTEXTPAD * 2 + tbb.height) +
'H-' + halfWidth +
'V' + topsign + HOVERARROWSIZE +
@@ -1012,12 +1021,23 @@ function createHoverText(hoverData, opts) {
} else {
lpath.attr('d', 'M0,0' +
'L' + HOVERARROWSIZE + ',' + topsign + HOVERARROWSIZE +
- 'H' + (HOVERTEXTPAD + tbb.width / 2) +
+ 'H' + (halfWidth) +
'v' + topsign + (HOVERTEXTPAD * 2 + tbb.height) +
- 'H-' + (HOVERTEXTPAD + tbb.width / 2) +
+ 'H-' + (halfWidth) +
'V' + topsign + HOVERARROWSIZE +
'H-' + HOVERARROWSIZE + 'Z');
}
+
+ commonLabelRect.minX = lx - halfWidth;
+ commonLabelRect.maxX = lx + halfWidth;
+ if(xa.side === 'top') {
+ // label on negative y side
+ commonLabelRect.minY = ly - (HOVERTEXTPAD * 2 + tbb.height);
+ commonLabelRect.maxY = ly - HOVERTEXTPAD;
+ } else {
+ commonLabelRect.minY = ly + HOVERTEXTPAD;
+ commonLabelRect.maxY = ly + (HOVERTEXTPAD * 2 + tbb.height);
+ }
} else {
var anchor;
var sgn;
@@ -1045,6 +1065,17 @@ function createHoverText(hoverData, opts) {
'V-' + (HOVERTEXTPAD + tbb.height / 2) +
'H' + leftsign + HOVERARROWSIZE + 'V-' + HOVERARROWSIZE + 'Z');
+ commonLabelRect.minY = ly - (HOVERTEXTPAD + tbb.height / 2);
+ commonLabelRect.maxY = ly + (HOVERTEXTPAD + tbb.height / 2);
+ if(ya.side === 'right') {
+ commonLabelRect.minX = lx + HOVERARROWSIZE;
+ commonLabelRect.maxX = lx + HOVERARROWSIZE + (HOVERTEXTPAD * 2 + tbb.width);
+ } else {
+ // label on negative x side
+ commonLabelRect.minX = lx - HOVERARROWSIZE - (HOVERTEXTPAD * 2 + tbb.width);
+ commonLabelRect.maxX = lx - HOVERARROWSIZE;
+ }
+
var halfHeight = tbb.height / 2;
var lty = outerTop - tbb.top - halfHeight;
var clipId = 'clip' + fullLayout._uid + 'commonlabel' + ya._id;
@@ -1370,7 +1401,10 @@ function createHoverText(hoverData, opts) {
} else if(anchorStartOK) {
hty += dy / 2;
d.anchor = 'start';
- } else d.anchor = 'middle';
+ } else {
+ d.anchor = 'middle';
+ }
+ d.crossPos = hty;
} else {
d.pos = hty;
anchorStartOK = htx + dx / 2 + txTotalWidth <= outerWidth;
@@ -1391,6 +1425,7 @@ function createHoverText(hoverData, opts) {
if(overflowR > 0) htx -= overflowR;
if(overflowL < 0) htx += -overflowL;
}
+ d.crossPos = htx;
}
tx.attr('text-anchor', d.anchor);
@@ -1399,7 +1434,10 @@ function createHoverText(hoverData, opts) {
(rotateLabels ? strRotate(YANGLE) : ''));
});
- return hoverLabels;
+ return {
+ hoverLabels: hoverLabels,
+ commonLabelBoundingBox: commonLabelRect
+ };
}
function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) {
@@ -1493,7 +1531,9 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) {
// know what happens if the group spans all the way from one edge to
// the other, though it hardly matters - there's just too much
// information then.
-function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) {
+function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabelBoundingBox) {
+ var axKey = rotateLabels ? 'xa' : 'ya';
+ var crossAxKey = rotateLabels ? 'ya' : 'xa';
var nummoves = 0;
var axSign = 1;
var nLabels = hoverLabels.size();
@@ -1502,14 +1542,74 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) {
var pointgroups = new Array(nLabels);
var k = 0;
+ // get extent of axis hover label
+ var axisLabelMinX = commonLabelBoundingBox.minX;
+ var axisLabelMaxX = commonLabelBoundingBox.maxX;
+ var axisLabelMinY = commonLabelBoundingBox.minY;
+ var axisLabelMaxY = commonLabelBoundingBox.maxY;
+
+ var pX = function(x) { return x * fullLayout._invScaleX; };
+ var pY = function(y) { return y * fullLayout._invScaleY; };
+
hoverLabels.each(function(d) {
var ax = d[axKey];
+ var crossAx = d[crossAxKey];
var axIsX = ax._id.charAt(0) === 'x';
var rng = ax.range;
if(k === 0 && rng && ((rng[0] > rng[1]) !== axIsX)) {
axSign = -1;
}
+ var pmin = 0;
+ var pmax = (axIsX ? fullLayout.width : fullLayout.height);
+ // in hovermode avoid overlap between hover labels and axis label
+ if(fullLayout.hovermode === 'x' || fullLayout.hovermode === 'y') {
+ // extent of rect behind hover label on cross axis:
+ var offsets = getHoverLabelOffsets(d, rotateLabels);
+ var anchor = d.anchor;
+ var horzSign = anchor === 'end' ? -1 : 1;
+ var labelMin;
+ var labelMax;
+ if(anchor === 'middle') {
+ // use extent of centered rect either on x or y axis depending on current axis
+ labelMin = d.crossPos + (axIsX ? pY(offsets.y - d.by / 2) : pX(d.bx / 2 + d.tx2width / 2));
+ labelMax = labelMin + (axIsX ? pY(d.by) : pX(d.bx));
+ } else {
+ // use extend of path (see alignHoverText function) without arrow
+ if(axIsX) {
+ labelMin = d.crossPos + pY(HOVERARROWSIZE + offsets.y) - pY(d.by / 2 - HOVERARROWSIZE);
+ labelMax = labelMin + pY(d.by);
+ } else {
+ var startX = pX(horzSign * HOVERARROWSIZE + offsets.x);
+ var endX = startX + pX(horzSign * d.bx);
+ labelMin = d.crossPos + Math.min(startX, endX);
+ labelMax = d.crossPos + Math.max(startX, endX);
+ }
+ }
+
+ if(axIsX) {
+ if(axisLabelMinY !== undefined && axisLabelMaxY !== undefined && Math.min(labelMax, axisLabelMaxY) - Math.max(labelMin, axisLabelMinY) > 1) {
+ // has at least 1 pixel overlap with axis label
+ if(crossAx.side === 'left') {
+ pmin = crossAx._mainLinePosition;
+ pmax = fullLayout.width;
+ } else {
+ pmax = crossAx._mainLinePosition;
+ }
+ }
+ } else {
+ if(axisLabelMinX !== undefined && axisLabelMaxX !== undefined && Math.min(labelMax, axisLabelMaxX) - Math.max(labelMin, axisLabelMinX) > 1) {
+ // has at least 1 pixel overlap with axis label
+ if(crossAx.side === 'top') {
+ pmin = crossAx._mainLinePosition;
+ pmax = fullLayout.height;
+ } else {
+ pmax = crossAx._mainLinePosition;
+ }
+ }
+ }
+ }
+
pointgroups[k++] = [{
datum: d,
traceIndex: d.trace.index,
@@ -1517,8 +1617,8 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) {
pos: d.pos,
posref: d.posref,
size: d.by * (axIsX ? YFACTOR : 1) / 2,
- pmin: 0,
- pmax: (axIsX ? fullLayout.width : fullLayout.height)
+ pmin: pmin,
+ pmax: pmax
}];
});
@@ -1662,6 +1762,42 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) {
}
}
+function getHoverLabelOffsets(hoverLabel, rotateLabels) {
+ var offsetX = 0;
+ var offsetY = hoverLabel.offset;
+
+ if(rotateLabels) {
+ offsetY *= -YSHIFTY;
+ offsetX = hoverLabel.offset * YSHIFTX;
+ }
+
+ return {
+ x: offsetX,
+ y: offsetY
+ };
+}
+
+/**
+ * Calculate the shift in x for text and text2 elements
+ */
+function getTextShiftX(hoverLabel) {
+ var alignShift = {start: 1, end: -1, middle: 0}[hoverLabel.anchor];
+ var textShiftX = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD);
+ var text2ShiftX = textShiftX + alignShift * (hoverLabel.txwidth + HOVERTEXTPAD);
+
+ var isMiddle = hoverLabel.anchor === 'middle';
+ if(isMiddle) {
+ textShiftX -= hoverLabel.tx2width / 2;
+ text2ShiftX += hoverLabel.txwidth / 2 + HOVERTEXTPAD;
+ }
+
+ return {
+ alignShift: alignShift,
+ textShiftX: textShiftX,
+ text2ShiftX: text2ShiftX
+ };
+}
+
function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
var pX = function(x) { return x * scaleX; };
var pY = function(y) { return y * scaleY; };
@@ -1675,21 +1811,12 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
var tx = g.select('text.nums');
var anchor = d.anchor;
var horzSign = anchor === 'end' ? -1 : 1;
- var alignShift = {start: 1, end: -1, middle: 0}[anchor];
- var txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD);
- var tx2x = txx + alignShift * (d.txwidth + HOVERTEXTPAD);
- var offsetX = 0;
- var offsetY = d.offset;
+ var shiftX = getTextShiftX(d);
+ var offsets = getHoverLabelOffsets(d, rotateLabels);
+ var offsetX = offsets.x;
+ var offsetY = offsets.y;
var isMiddle = anchor === 'middle';
- if(isMiddle) {
- txx -= d.tx2width / 2;
- tx2x += d.txwidth / 2 + HOVERTEXTPAD;
- }
- if(rotateLabels) {
- offsetY *= -YSHIFTY;
- offsetX = d.offset * YSHIFTX;
- }
g.select('path')
.attr('d', isMiddle ?
@@ -1705,7 +1832,7 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
'V' + pY(offsetY - HOVERARROWSIZE) +
'Z'));
- var posX = offsetX + txx;
+ var posX = offsetX + shiftX.textShiftX;
var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD;
var textAlign = d.textAlign || 'auto';
@@ -1728,11 +1855,11 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
if(d.tx2width) {
g.select('text.name')
.call(svgTextUtils.positionText,
- pX(tx2x + alignShift * HOVERTEXTPAD + offsetX),
+ pX(shiftX.text2ShiftX + shiftX.alignShift * HOVERTEXTPAD + offsetX),
pY(offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD));
g.select('rect')
.call(Drawing.setRect,
- pX(tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX),
+ pX(shiftX.text2ShiftX + (shiftX.alignShift - 1) * d.tx2width / 2 + offsetX),
pY(offsetY - d.by / 2 - 1),
pX(d.tx2width), pY(d.by + 2));
}
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index 8a1d771d83e..aa2923c1ae5 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -1513,7 +1513,7 @@ describe('hover info', function() {
describe('overflowing hover labels', function() {
var trace = {y: [1, 2, 3], text: ['', 'a
b
c', '']};
- var data = [trace, trace, trace, trace, trace, trace, trace];
+ var data = [trace, trace, trace, trace, trace, trace, trace, trace, trace, trace];
var layout = {
width: 600, height: 600, showlegend: false,
margin: {l: 100, r: 100, t: 100, b: 100},
@@ -1531,17 +1531,86 @@ describe('hover info', function() {
return d3Select(gd).selectAll('g.hovertext').size();
}
- it('shows as many labels as will fit on the div, not on the subplot', function(done) {
+ 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) {
_hoverNatural(gd, 200, 200);
- expect(labelCount()).toBe(7);
+ expect(labelCount()).toBe(8);
Plotly.relayout(gd, {'yaxis.domain': [0.48, 0.52]})
.then(function() {
_hoverNatural(gd, 150, 200);
_hoverNatural(gd, 200, 200);
- expect(labelCount()).toBe(7);
+ expect(labelCount()).toBe(8);
+ })
+ .then(done, done.fail);
+ });
+ });
+
+ describe('overlapping hover labels', function() {
+ var trace = {y: [1, 2, 3], x: ['01.01.2020', '02.01.2020', '03.01.2020'], text: ['', 'a
b
c', '']};
+ var data = [trace, trace, trace, trace, trace, trace, trace, trace, trace, trace];
+ var layout = {
+ width: 600, height: 600, showlegend: false,
+ margin: {l: 100, r: 100, t: 100, b: 100},
+ hovermode: 'x'
+ };
+
+ var gd;
+
+ beforeEach(function(done) {
+ gd = createGraphDiv();
+ Plotly.newPlot(gd, data, layout).then(done);
+ });
+
+ function labelCount() {
+ return d3Select(gd).selectAll('g.hovertext').size();
+ }
+
+ it('does not show labels that would overlap the axis hover label', function(done) {
+ _hoverNatural(gd, 200, 200);
+
+ expect(labelCount()).toBe(6);
+
+ Plotly.relayout(gd, {'yaxis.domain': [0.48, 0.52]})
+ .then(function() {
+ _hoverNatural(gd, 150, 200);
+ _hoverNatural(gd, 200, 200);
+
+ expect(labelCount()).toBe(4);
+ })
+ .then(done, done.fail);
+ });
+ });
+ describe('overlapping hover labels of different lengths', function() {
+ var data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map(function(v) {return {x: [100, 200, 300], y: [v, v + 1, v + 2]};});
+ var layout = {
+ width: 500, height: 400, showlegend: false,
+ margin: {l: 100, r: 100, t: 100, b: 100},
+ hovermode: 'x'
+ };
+
+ var gd;
+
+ beforeEach(function(done) {
+ gd = createGraphDiv();
+ Plotly.newPlot(gd, data, layout).then(done);
+ });
+
+ function labelCount() {
+ return d3Select(gd).selectAll('g.hovertext').size();
+ }
+
+ it('does not show labels that would overlap the axis hover label', function(done) {
+ _hoverNatural(gd, 130, 100);
+
+ expect(labelCount()).toBe(14);
+
+ Plotly.relayout(gd, {'yaxis.domain': [0.2, 0.8]})
+ .then(function() {
+ _hoverNatural(gd, 130, 100);
+
+ expect(labelCount()).toBe(12);
})
.then(done, done.fail);
});