From d96513185e89ebfa428fbadbbb0e9713ddd21170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Wed, 18 Jan 2023 16:46:04 +0100 Subject: [PATCH 01/15] Avoid overlap of point and axis hover labels close #3973 --- 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 8e027ad3d6b..cfc705a0e76 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1518,7 +1518,7 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) { posref: d.posref, size: d.by * (axIsX ? YFACTOR : 1) / 2, pmin: 0, - pmax: (axIsX ? fullLayout.width : fullLayout.height) + pmax: (axIsX ? fullLayout.width : fullLayout.height) - ax._mainLinePosition }]; }); From c86a12b2a71548caa1730bd27437cd57dd28da97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Tue, 24 Jan 2023 07:37:23 +0100 Subject: [PATCH 02/15] Fix for overlap on other axis and other sides --- src/components/fx/hover.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 0001c41be17..c2cb13de23b 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1494,6 +1494,7 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) { // the other, though it hardly matters - there's just too much // information then. function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) { + let crossAxKey = axKey === 'xa' ? 'ya' : 'xa'; var nummoves = 0; var axSign = 1; var nLabels = hoverLabels.size(); @@ -1504,12 +1505,32 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) { hoverLabels.each(function(d) { var ax = d[axKey]; + let 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; } + let pmin, pmax; + if (axIsX) { + if (crossAx.side === 'left') { + pmin = crossAx._mainLinePosition; + pmax = fullLayout.width; + } else { + pmin = 0; + pmax = crossAx._mainLinePosition; + } + } else { + if (crossAx.side === 'top') { + pmin = crossAx._mainLinePosition; + pmax = fullLayout.height; + } else { + pmin = 0; + pmax = crossAx._mainLinePosition; + } + } + pointgroups[k++] = [{ datum: d, traceIndex: d.trace.index, @@ -1517,8 +1538,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) - ax._mainLinePosition + pmin, + pmax }]; }); From 126e432062fc62ce182c40f5929e44121e6f57f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Tue, 24 Jan 2023 07:44:23 +0100 Subject: [PATCH 03/15] Apply changes only for compare modes (hovermode: 'x'|'y') --- src/components/fx/hover.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index c2cb13de23b..da7b9cb5c08 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1494,7 +1494,7 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) { // the other, though it hardly matters - there's just too much // information then. function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) { - let crossAxKey = axKey === 'xa' ? 'ya' : 'xa'; + var crossAxKey = axKey === 'xa' ? 'ya' : 'xa'; var nummoves = 0; var axSign = 1; var nLabels = hoverLabels.size(); @@ -1505,29 +1505,30 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) { hoverLabels.each(function(d) { var ax = d[axKey]; - let crossAx = d[crossAxKey]; + 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; } - let pmin, pmax; - if (axIsX) { - if (crossAx.side === 'left') { - pmin = crossAx._mainLinePosition; - pmax = fullLayout.width; - } else { - pmin = 0; - pmax = crossAx._mainLinePosition; - } - } else { - if (crossAx.side === 'top') { - pmin = crossAx._mainLinePosition; - pmax = fullLayout.height; + var pmin = 0; + var pmax = (axIsX ? fullLayout.width : fullLayout.height); + if (fullLayout.hovermode === 'x' || fullLayout.hovermode === 'y') { + if (axIsX) { + if (crossAx.side === 'left') { + pmin = crossAx._mainLinePosition; + pmax = fullLayout.width; + } else { + pmax = crossAx._mainLinePosition; + } } else { - pmin = 0; - pmax = crossAx._mainLinePosition; + if (crossAx.side === 'top') { + pmin = crossAx._mainLinePosition; + pmax = fullLayout.height; + } else { + pmax = crossAx._mainLinePosition; + } } } From 782bc993117bde1fe4fe2774458059b3fb0a1650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Mon, 30 Jan 2023 15:18:45 +0100 Subject: [PATCH 04/15] Apply changes only when hover labels intersect axis hover label --- src/components/fx/hover.js | 87 +++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index da7b9cb5c08..68127733f44 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 ? 'xa' : 'ya', fullLayout, hoverText.commonLabel); 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,7 @@ function createHoverText(hoverData, opts) { .classed('axistext', true); commonLabel.exit().remove(); + var commonLabelLx, commonLabelLy; commonLabel.each(function() { var label = d3.select(this); var lpath = Lib.ensureSingle(label, 'path', '', function(s) { @@ -1087,6 +1090,9 @@ function createHoverText(hoverData, opts) { } label.attr('transform', strTranslate(lx, ly)); + + commonLabelLx = lx; + commonLabelLy = ly; }); // Show a single hover label @@ -1370,7 +1376,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 +1400,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 +1409,14 @@ function createHoverText(hoverData, opts) { (rotateLabels ? strRotate(YANGLE) : '')); }); - return hoverLabels; + return { + hoverLabels: hoverLabels, + commonLabel: { + lx: commonLabelLx, + ly: commonLabelLy, + label: commonLabel + } + }; } function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) { @@ -1493,7 +1510,7 @@ 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, axKey, fullLayout, commonLabel) { var crossAxKey = axKey === 'xa' ? 'ya' : 'xa'; var nummoves = 0; var axSign = 1; @@ -1503,6 +1520,27 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) { var pointgroups = new Array(nLabels); var k = 0; + // get extent of axis hover label + var axisLabelMinX, axisLabelMaxX, axisLabelMinY, axisLabelMaxY; + if(commonLabel) { + commonLabel.label.each(function() { + var selection = d3.select(this); + if(selection && selection.length) { + var labels = selection[0]; + if(labels && labels.length) { + var label = labels[0]; + var bbox = label.getBBox(); + if(bbox) { + axisLabelMinX = commonLabel.lx; + axisLabelMaxX = commonLabel.lx + bbox.width; + axisLabelMinY = commonLabel.ly; + axisLabelMaxY = commonLabel.ly + bbox.height; + } + } + } + }); + } + hoverLabels.each(function(d) { var ax = d[axKey]; var crossAx = d[crossAxKey]; @@ -1514,20 +1552,29 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) { } var pmin = 0; var pmax = (axIsX ? fullLayout.width : fullLayout.height); - if (fullLayout.hovermode === 'x' || fullLayout.hovermode === 'y') { - if (axIsX) { - if (crossAx.side === 'left') { - pmin = crossAx._mainLinePosition; - pmax = fullLayout.width; - } else { - pmax = crossAx._mainLinePosition; + if(fullLayout.hovermode === 'x' || fullLayout.hovermode === 'y') { + // extent of hover label on cross axis: + var labelMinX = d.crossPos; + var labelMaxX = d.crossPos + d.txwidth; + if(axIsX) { + if(Math.max(labelMinX, axisLabelMinY) <= Math.min(labelMaxX, axisLabelMaxY)) { + // has overlap with axis label + if(crossAx.side === 'left') { + pmin = crossAx._mainLinePosition; + pmax = fullLayout.width; + } else { + pmax = crossAx._mainLinePosition; + } } } else { - if (crossAx.side === 'top') { - pmin = crossAx._mainLinePosition; - pmax = fullLayout.height; - } else { - pmax = crossAx._mainLinePosition; + if(Math.max(labelMinX, axisLabelMinX) <= Math.min(labelMaxX, axisLabelMaxX)) { + // has overlap with axis label + if(crossAx.side === 'top') { + pmin = crossAx._mainLinePosition; + pmax = fullLayout.height; + } else { + pmax = crossAx._mainLinePosition; + } } } } @@ -1539,8 +1586,8 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout) { pos: d.pos, posref: d.posref, size: d.by * (axIsX ? YFACTOR : 1) / 2, - pmin, - pmax + pmin: pmin, + pmax: pmax }]; }); From ded38a355751e7ba3c2d7e33ce89b642ad41e996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Tue, 31 Jan 2023 12:07:30 +0100 Subject: [PATCH 05/15] Ignore hover labels' arrows when calculating axis label overlap --- src/components/fx/hover.js | 108 +++++++++++++++---------- test/jasmine/tests/hover_label_test.js | 40 ++++++++- 2 files changed, 104 insertions(+), 44 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 68127733f44..ef4e74b2560 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -833,7 +833,7 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) { var hoverLabels = hoverText.hoverLabels; if(!helpers.isUnifiedHover(hovermode)) { - hoverAvoidOverlaps(hoverLabels, rotateLabels ? 'xa' : 'ya', fullLayout, hoverText.commonLabel); + hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, hoverText.commonLabel); 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. @@ -1510,8 +1510,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, commonLabel) { - var crossAxKey = axKey === 'xa' ? 'ya' : 'xa'; +function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) { + var axKey = rotateLabels ? 'xa' : 'ya'; + var crossAxKey = rotateLabels ? 'ya' : 'xa'; var nummoves = 0; var axSign = 1; var nLabels = hoverLabels.size(); @@ -1525,17 +1526,13 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout, commonLabel) { if(commonLabel) { commonLabel.label.each(function() { var selection = d3.select(this); - if(selection && selection.length) { - var labels = selection[0]; - if(labels && labels.length) { - var label = labels[0]; - var bbox = label.getBBox(); - if(bbox) { - axisLabelMinX = commonLabel.lx; - axisLabelMaxX = commonLabel.lx + bbox.width; - axisLabelMinY = commonLabel.ly; - axisLabelMaxY = commonLabel.ly + bbox.height; - } + if(selection && selection.length && selection[0] && selection[0].length && selection[0][0]) { + var bbox = selection[0][0].getBBox(); + if(bbox) { + axisLabelMinX = commonLabel.lx; + axisLabelMaxX = commonLabel.lx + bbox.width; + axisLabelMinY = commonLabel.ly; + axisLabelMaxY = commonLabel.ly + bbox.height; } } }); @@ -1553,11 +1550,16 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout, commonLabel) { var pmin = 0; var pmax = (axIsX ? fullLayout.width : fullLayout.height); if(fullLayout.hovermode === 'x' || fullLayout.hovermode === 'y') { - // extent of hover label on cross axis: - var labelMinX = d.crossPos; - var labelMaxX = d.crossPos + d.txwidth; + // extent of rect behind hover label on cross axis (without arrow): + var offsets = getHoverLabelOffsets(d, rotateLabels); + var shiftX = getLabelShiftX(d); + // calculation based on alignHoverText function + var offsetRectX = (shiftX.x2x + (shiftX.alignShift - 1) * d.tx2width / 2 + offsets.x) * fullLayout._invScaleX; + + var labelMinX = d.crossPos + offsetRectX; + var labelMaxX = labelMinX + d.tx2width; if(axIsX) { - if(Math.max(labelMinX, axisLabelMinY) <= Math.min(labelMaxX, axisLabelMaxY)) { + if(axisLabelMinY !== undefined && axisLabelMaxY !== undefined && Math.max(labelMinX, axisLabelMinY) <= Math.min(labelMaxX, axisLabelMaxY)) { // has overlap with axis label if(crossAx.side === 'left') { pmin = crossAx._mainLinePosition; @@ -1567,7 +1569,7 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout, commonLabel) { } } } else { - if(Math.max(labelMinX, axisLabelMinX) <= Math.min(labelMaxX, axisLabelMaxX)) { + if(axisLabelMinX !== undefined && axisLabelMaxX !== undefined && Math.max(labelMinX, axisLabelMinX) <= Math.min(labelMaxX, axisLabelMaxX)) { // has overlap with axis label if(crossAx.side === 'top') { pmin = crossAx._mainLinePosition; @@ -1731,6 +1733,39 @@ function hoverAvoidOverlaps(hoverLabels, axKey, fullLayout, commonLabel) { } } +function getHoverLabelOffsets(hoverLabel, rotateLabels) { + var offsetX = 0; + var offsetY = hoverLabel.offset; + + if(rotateLabels) { + offsetY *= -YSHIFTY; + offsetX = hoverLabel.offset * YSHIFTX; + } + + return { + x: offsetX, + y: offsetY + }; +} + +function getLabelShiftX(hoverLabel) { + var alignShift = {start: 1, end: -1, middle: 0}[hoverLabel.anchor]; + var txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD); + var tx2x = txx + alignShift * (hoverLabel.txwidth + HOVERTEXTPAD); + + var isMiddle = hoverLabel.anchor === 'middle'; + if(isMiddle) { + txx -= hoverLabel.tx2width / 2; + tx2x += hoverLabel.txwidth / 2 + HOVERTEXTPAD; + } + + return { + alignShift: alignShift, + xx: txx, + x2x: tx2x + }; +} + function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) { var pX = function(x) { return x * scaleX; }; var pY = function(y) { return y * scaleY; }; @@ -1744,38 +1779,27 @@ 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 = getLabelShiftX(d); + var offsets = getHoverLabelOffsets(d, rotateLabels); 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 ? // middle aligned: rect centered on data - ('M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsetY - d.by / 2) + + ('M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsets.y - d.by / 2) + 'h' + pX(d.bx) + 'v' + pY(d.by) + 'h-' + pX(d.bx) + 'Z') : // left or right aligned: side rect with arrow to data - ('M0,0L' + pX(horzSign * HOVERARROWSIZE + offsetX) + ',' + pY(HOVERARROWSIZE + offsetY) + + ('M0,0L' + pX(horzSign * HOVERARROWSIZE + offsets.x) + ',' + pY(HOVERARROWSIZE + offsets.y) + 'v' + pY(d.by / 2 - HOVERARROWSIZE) + 'h' + pX(horzSign * d.bx) + 'v-' + pY(d.by) + - 'H' + pX(horzSign * HOVERARROWSIZE + offsetX) + - 'V' + pY(offsetY - HOVERARROWSIZE) + + 'H' + pX(horzSign * HOVERARROWSIZE + offsets.x) + + 'V' + pY(offsets.y - HOVERARROWSIZE) + 'Z')); - var posX = offsetX + txx; - var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD; + var posX = offsets.x + shiftX.xx; + var posY = offsets.y + d.ty0 - d.by / 2 + HOVERTEXTPAD; var textAlign = d.textAlign || 'auto'; if(textAlign !== 'auto') { @@ -1797,12 +1821,12 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) { if(d.tx2width) { g.select('text.name') .call(svgTextUtils.positionText, - pX(tx2x + alignShift * HOVERTEXTPAD + offsetX), - pY(offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD)); + pX(shiftX.x2x + shiftX.alignShift * HOVERTEXTPAD + offsets.x), + pY(offsets.y + d.ty0 - d.by / 2 + HOVERTEXTPAD)); g.select('rect') .call(Drawing.setRect, - pX(tx2x + (alignShift - 1) * d.tx2width / 2 + offsetX), - pY(offsetY - d.by / 2 - 1), + pX(shiftX.x2x + (shiftX.alignShift - 1) * d.tx2width / 2 + offsets.x), + pY(offsets.y - 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..0a8565e46ff 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]; var layout = { width: 600, height: 600, showlegend: false, margin: {l: 100, r: 100, t: 100, b: 100}, @@ -1531,7 +1531,7 @@ 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); @@ -1547,6 +1547,42 @@ describe('hover info', function() { }); }); + 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]; + 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(6); + }) + .then(done, done.fail); + }); + }); + describe('alignment while avoiding overlaps:', function() { var gd; From 08ff50b629280d9dfdbb7eecb09a002db85046d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Tue, 31 Jan 2023 12:43:12 +0100 Subject: [PATCH 06/15] Fix tests --- 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 0a8565e46ff..03c8956ac8e 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, 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}, @@ -1534,14 +1534,14 @@ describe('hover info', function() { 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); }); @@ -1549,7 +1549,7 @@ describe('hover info', function() { 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]; + 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}, @@ -1577,7 +1577,7 @@ describe('hover info', function() { _hoverNatural(gd, 150, 200); _hoverNatural(gd, 200, 200); - expect(labelCount()).toBe(6); + expect(labelCount()).toBe(4); }) .then(done, done.fail); }); From 0426f42bc7b76f4295391abe5e562ee278c5148d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Wed, 1 Feb 2023 08:06:34 +0100 Subject: [PATCH 07/15] Fix bounds used: check overlap on y axis when hovermode is y --- src/components/fx/hover.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index ef4e74b2560..288f9a5789c 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1555,11 +1555,12 @@ function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) var shiftX = getLabelShiftX(d); // calculation based on alignHoverText function var offsetRectX = (shiftX.x2x + (shiftX.alignShift - 1) * d.tx2width / 2 + offsets.x) * fullLayout._invScaleX; + var offsetRectY = (offsets.y - d.by / 2 - 1) * fullLayout._invScaleY; - var labelMinX = d.crossPos + offsetRectX; - var labelMaxX = labelMinX + d.tx2width; + var labelMin = d.crossPos + (axIsX ? offsetRectY : offsetRectX); + var labelMax = labelMin + (axIsX ? d.tx2width * fullLayout._invScaleX : (d.by + 2) * fullLayout._invScaleY); if(axIsX) { - if(axisLabelMinY !== undefined && axisLabelMaxY !== undefined && Math.max(labelMinX, axisLabelMinY) <= Math.min(labelMaxX, axisLabelMaxY)) { + if(axisLabelMinY !== undefined && axisLabelMaxY !== undefined && Math.max(labelMin, axisLabelMinY) <= Math.min(labelMax, axisLabelMaxY)) { // has overlap with axis label if(crossAx.side === 'left') { pmin = crossAx._mainLinePosition; @@ -1569,7 +1570,7 @@ function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) } } } else { - if(axisLabelMinX !== undefined && axisLabelMaxX !== undefined && Math.max(labelMinX, axisLabelMinX) <= Math.min(labelMaxX, axisLabelMaxX)) { + if(axisLabelMinX !== undefined && axisLabelMaxX !== undefined && Math.max(labelMin, axisLabelMinX) <= Math.min(labelMax, axisLabelMaxX)) { // has overlap with axis label if(crossAx.side === 'top') { pmin = crossAx._mainLinePosition; From 4c3438f92107985bcb9984fe2fc19e6213208959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Mon, 6 Feb 2023 16:21:19 +0100 Subject: [PATCH 08/15] Add @noCI to test failing only on CI --- 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 03c8956ac8e..f61d2786f0b 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1661,7 +1661,7 @@ describe('hover info', function() { .then(done, done.fail); }); - it('centered-aligned, should stack nicely upon each other', function(done) { + it('@noCI centered-aligned, should stack nicely upon each other', function(done) { var trace1 = { x: ['giraffes'], y: [5], From 3b838808980c3a33afd6736944db35347e1adf4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Mon, 6 Feb 2023 22:14:03 +0100 Subject: [PATCH 09/15] Ignore very small overlap --- src/components/fx/hover.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 288f9a5789c..6202ff0729e 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1560,7 +1560,8 @@ function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) var labelMin = d.crossPos + (axIsX ? offsetRectY : offsetRectX); var labelMax = labelMin + (axIsX ? d.tx2width * fullLayout._invScaleX : (d.by + 2) * fullLayout._invScaleY); if(axIsX) { - if(axisLabelMinY !== undefined && axisLabelMaxY !== undefined && Math.max(labelMin, axisLabelMinY) <= Math.min(labelMax, axisLabelMaxY)) { + // at least 1 pixel overlap + if(axisLabelMinY !== undefined && axisLabelMaxY !== undefined && Math.min(labelMax, axisLabelMaxY) - Math.max(labelMin, axisLabelMinY) > 1) { // has overlap with axis label if(crossAx.side === 'left') { pmin = crossAx._mainLinePosition; @@ -1570,7 +1571,8 @@ function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) } } } else { - if(axisLabelMinX !== undefined && axisLabelMaxX !== undefined && Math.max(labelMin, axisLabelMinX) <= Math.min(labelMax, axisLabelMaxX)) { + // at least 1 pixel overlap + if(axisLabelMinX !== undefined && axisLabelMaxX !== undefined && Math.min(labelMax, axisLabelMaxX) - Math.max(labelMin, axisLabelMinX) > 1) { // has overlap with axis label if(crossAx.side === 'top') { pmin = crossAx._mainLinePosition; @@ -1782,25 +1784,27 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) { var horzSign = anchor === 'end' ? -1 : 1; var shiftX = getLabelShiftX(d); var offsets = getHoverLabelOffsets(d, rotateLabels); + var offsetX = offsets.x; + var offsetY = offsets.y; var isMiddle = anchor === 'middle'; g.select('path') .attr('d', isMiddle ? // middle aligned: rect centered on data - ('M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsets.y - d.by / 2) + + ('M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsetY - d.by / 2) + 'h' + pX(d.bx) + 'v' + pY(d.by) + 'h-' + pX(d.bx) + 'Z') : // left or right aligned: side rect with arrow to data - ('M0,0L' + pX(horzSign * HOVERARROWSIZE + offsets.x) + ',' + pY(HOVERARROWSIZE + offsets.y) + + ('M0,0L' + pX(horzSign * HOVERARROWSIZE + offsetX) + ',' + pY(HOVERARROWSIZE + offsetY) + 'v' + pY(d.by / 2 - HOVERARROWSIZE) + 'h' + pX(horzSign * d.bx) + 'v-' + pY(d.by) + - 'H' + pX(horzSign * HOVERARROWSIZE + offsets.x) + - 'V' + pY(offsets.y - HOVERARROWSIZE) + + 'H' + pX(horzSign * HOVERARROWSIZE + offsetX) + + 'V' + pY(offsetY - HOVERARROWSIZE) + 'Z')); - var posX = offsets.x + shiftX.xx; - var posY = offsets.y + d.ty0 - d.by / 2 + HOVERTEXTPAD; + var posX = offsetX + shiftX.xx; + var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD; var textAlign = d.textAlign || 'auto'; if(textAlign !== 'auto') { @@ -1822,12 +1826,12 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) { if(d.tx2width) { g.select('text.name') .call(svgTextUtils.positionText, - pX(shiftX.x2x + shiftX.alignShift * HOVERTEXTPAD + offsets.x), - pY(offsets.y + d.ty0 - d.by / 2 + HOVERTEXTPAD)); + pX(shiftX.x2x + shiftX.alignShift * HOVERTEXTPAD + offsetX), + pY(offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD)); g.select('rect') .call(Drawing.setRect, - pX(shiftX.x2x + (shiftX.alignShift - 1) * d.tx2width / 2 + offsets.x), - pY(offsets.y - d.by / 2 - 1), + pX(shiftX.x2x + (shiftX.alignShift - 1) * d.tx2width / 2 + offsetX), + pY(offsetY - d.by / 2 - 1), pX(d.tx2width), pY(d.by + 2)); } }); From 8805949f2ee2ce90c38477ca798e4b01f6b8a54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Mon, 6 Feb 2023 22:28:58 +0100 Subject: [PATCH 10/15] remove noCI from failing 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 f61d2786f0b..03c8956ac8e 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1661,7 +1661,7 @@ describe('hover info', function() { .then(done, done.fail); }); - it('@noCI centered-aligned, should stack nicely upon each other', function(done) { + it('centered-aligned, should stack nicely upon each other', function(done) { var trace1 = { x: ['giraffes'], y: [5], From 16adaac78c1c2c3a26325ef388aaa68660544c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Tue, 7 Feb 2023 09:24:05 +0100 Subject: [PATCH 11/15] use more descriptive variable names --- src/components/fx/hover.js | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 6202ff0729e..1ceb95cd73d 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1549,20 +1549,20 @@ function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) } 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 (without arrow): var offsets = getHoverLabelOffsets(d, rotateLabels); - var shiftX = getLabelShiftX(d); + var shiftX = getTextShiftX(d); // calculation based on alignHoverText function - var offsetRectX = (shiftX.x2x + (shiftX.alignShift - 1) * d.tx2width / 2 + offsets.x) * fullLayout._invScaleX; + var offsetRectX = (shiftX.text2ShiftX + (shiftX.alignShift - 1) * d.tx2width / 2 + offsets.x) * fullLayout._invScaleX; var offsetRectY = (offsets.y - d.by / 2 - 1) * fullLayout._invScaleY; var labelMin = d.crossPos + (axIsX ? offsetRectY : offsetRectX); var labelMax = labelMin + (axIsX ? d.tx2width * fullLayout._invScaleX : (d.by + 2) * fullLayout._invScaleY); if(axIsX) { - // at least 1 pixel overlap if(axisLabelMinY !== undefined && axisLabelMaxY !== undefined && Math.min(labelMax, axisLabelMaxY) - Math.max(labelMin, axisLabelMinY) > 1) { - // has overlap with axis label + // has at least 1 pixel overlap with axis label if(crossAx.side === 'left') { pmin = crossAx._mainLinePosition; pmax = fullLayout.width; @@ -1571,9 +1571,8 @@ function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) } } } else { - // at least 1 pixel overlap if(axisLabelMinX !== undefined && axisLabelMaxX !== undefined && Math.min(labelMax, axisLabelMaxX) - Math.max(labelMin, axisLabelMinX) > 1) { - // has overlap with axis label + // has at least 1 pixel overlap with axis label if(crossAx.side === 'top') { pmin = crossAx._mainLinePosition; pmax = fullLayout.height; @@ -1751,21 +1750,24 @@ function getHoverLabelOffsets(hoverLabel, rotateLabels) { }; } -function getLabelShiftX(hoverLabel) { +/** + * Calculate the shift in x for text and text2 elements + */ +function getTextShiftX(hoverLabel) { var alignShift = {start: 1, end: -1, middle: 0}[hoverLabel.anchor]; - var txx = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD); - var tx2x = txx + alignShift * (hoverLabel.txwidth + HOVERTEXTPAD); + var textShiftX = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD); + var text2ShiftX = textShiftX + alignShift * (hoverLabel.txwidth + HOVERTEXTPAD); var isMiddle = hoverLabel.anchor === 'middle'; if(isMiddle) { - txx -= hoverLabel.tx2width / 2; - tx2x += hoverLabel.txwidth / 2 + HOVERTEXTPAD; + textShiftX -= hoverLabel.tx2width / 2; + text2ShiftX += hoverLabel.txwidth / 2 + HOVERTEXTPAD; } return { alignShift: alignShift, - xx: txx, - x2x: tx2x + textShiftX: textShiftX, + text2ShiftX: text2ShiftX }; } @@ -1782,7 +1784,7 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) { var tx = g.select('text.nums'); var anchor = d.anchor; var horzSign = anchor === 'end' ? -1 : 1; - var shiftX = getLabelShiftX(d); + var shiftX = getTextShiftX(d); var offsets = getHoverLabelOffsets(d, rotateLabels); var offsetX = offsets.x; var offsetY = offsets.y; @@ -1803,7 +1805,7 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) { 'V' + pY(offsetY - HOVERARROWSIZE) + 'Z')); - var posX = offsetX + shiftX.xx; + var posX = offsetX + shiftX.textShiftX; var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD; var textAlign = d.textAlign || 'auto'; @@ -1826,11 +1828,11 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) { if(d.tx2width) { g.select('text.name') .call(svgTextUtils.positionText, - pX(shiftX.x2x + shiftX.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(shiftX.x2x + (shiftX.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)); } From c4383bcf473492cb6e5da574b25699d017540475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Fri, 24 Feb 2023 10:47:59 +0100 Subject: [PATCH 12/15] Use rect behind text instead of text2 for collision check --- src/components/fx/hover.js | 31 ++++++++++++++++++------ test/jasmine/tests/hover_label_test.js | 33 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 1ceb95cd73d..c55740466a3 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1538,6 +1538,9 @@ function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) }); } + 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]; @@ -1551,15 +1554,29 @@ function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) 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 (without arrow): + // extent of rect behind hover label on cross axis: var offsets = getHoverLabelOffsets(d, rotateLabels); - var shiftX = getTextShiftX(d); - // calculation based on alignHoverText function - var offsetRectX = (shiftX.text2ShiftX + (shiftX.alignShift - 1) * d.tx2width / 2 + offsets.x) * fullLayout._invScaleX; - var offsetRectY = (offsets.y - d.by / 2 - 1) * fullLayout._invScaleY; + 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); + } + } - var labelMin = d.crossPos + (axIsX ? offsetRectY : offsetRectX); - var labelMax = labelMin + (axIsX ? d.tx2width * fullLayout._invScaleX : (d.by + 2) * fullLayout._invScaleY); 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 diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 03c8956ac8e..afa751eca41 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1582,6 +1582,39 @@ describe('hover info', function() { .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(v=>({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); + }); + }); describe('alignment while avoiding overlaps:', function() { var gd; From 9e75c94c5300df9536b70d0187e0fd9779110659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Fri, 24 Feb 2023 10:59:44 +0100 Subject: [PATCH 13/15] Fix eslint in tests --- 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 afa751eca41..aa2923c1ae5 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1583,7 +1583,7 @@ describe('hover info', function() { }); }); 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(v=>({x:[100,200,300],y:[v,v+1,v+2]})); + 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}, From e5cee9e6a89553e215411c3e813fa1af705665c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Fri, 24 Feb 2023 20:36:05 +0100 Subject: [PATCH 14/15] Use actual axis label background path instead of bounding box for collision detection --- src/components/fx/hover.js | 68 ++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index c55740466a3..3cf438f28be 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -833,7 +833,7 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) { var hoverLabels = hoverText.hoverLabels; if(!helpers.isUnifiedHover(hovermode)) { - hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, hoverText.commonLabel); + 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. @@ -944,7 +944,13 @@ function createHoverText(hoverData, opts) { .classed('axistext', true); commonLabel.exit().remove(); - var commonLabelLx, commonLabelLy; + // 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) { @@ -998,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 + @@ -1015,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; @@ -1048,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; @@ -1090,9 +1118,6 @@ function createHoverText(hoverData, opts) { } label.attr('transform', strTranslate(lx, ly)); - - commonLabelLx = lx; - commonLabelLy = ly; }); // Show a single hover label @@ -1411,11 +1436,7 @@ function createHoverText(hoverData, opts) { return { hoverLabels: hoverLabels, - commonLabel: { - lx: commonLabelLx, - ly: commonLabelLy, - label: commonLabel - } + commonLabelBoundingBox: commonLabelRect }; } @@ -1510,7 +1531,7 @@ 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, rotateLabels, fullLayout, commonLabel) { +function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabelBoundingBox) { var axKey = rotateLabels ? 'xa' : 'ya'; var crossAxKey = rotateLabels ? 'ya' : 'xa'; var nummoves = 0; @@ -1522,21 +1543,10 @@ function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabel) var k = 0; // get extent of axis hover label - var axisLabelMinX, axisLabelMaxX, axisLabelMinY, axisLabelMaxY; - if(commonLabel) { - commonLabel.label.each(function() { - var selection = d3.select(this); - if(selection && selection.length && selection[0] && selection[0].length && selection[0][0]) { - var bbox = selection[0][0].getBBox(); - if(bbox) { - axisLabelMinX = commonLabel.lx; - axisLabelMaxX = commonLabel.lx + bbox.width; - axisLabelMinY = commonLabel.ly; - axisLabelMaxY = commonLabel.ly + bbox.height; - } - } - }); - } + 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; }; From fe529fa8d10405950041933545768e2707eb759f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gr=C3=B6ger?= Date: Mon, 27 Feb 2023 08:50:39 +0100 Subject: [PATCH 15/15] Add changelog --- draftlogs/6442_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/6442_fix.md 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