diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 7fc51ce2184..4704f89abc6 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -704,7 +704,7 @@ function _hover(gd, evt, subplot, noHoverEvent) { var hoverLabels = createHoverText(hoverData, labelOpts, gd); - hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya', fullLayout); + hoverAvoidOverlaps(hoverLabels, rotateLabels ? 'xa' : 'ya', fullLayout); alignHoverText(hoverLabels, rotateLabels); @@ -879,6 +879,7 @@ function createHoverText(hoverData, opts, gd) { // show all the individual labels + // first create the objects var hoverLabels = container.selectAll('g.hovertext') .data(hoverData, function(d) { @@ -1091,17 +1092,21 @@ function createHoverText(hoverData, opts, gd) { // 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(hoverData, ax, fullLayout) { +function hoverAvoidOverlaps(hoverLabels, ax, fullLayout) { var nummoves = 0; var axSign = 1; + var nLabels = hoverLabels.size(); // make groups of touching points - var pointgroups = hoverData.map(function(d, i) { + var pointgroups = new Array(nLabels); + + hoverLabels.each(function(d, i) { var axis = d[ax]; var axIsX = axis._id.charAt(0) === 'x'; var rng = axis.range; if(!i && rng && ((rng[0] > rng[1]) !== axIsX)) axSign = -1; - return [{ + pointgroups[i] = [{ + datum: d, i: i, traceIndex: d.trace.index, dp: 0, @@ -1111,8 +1116,9 @@ function hoverAvoidOverlaps(hoverData, ax, fullLayout) { pmin: 0, pmax: (axIsX ? fullLayout.width : fullLayout.height) }]; - }) - .sort(function(a, b) { + }); + + pointgroups.sort(function(a, b) { return (a[0].posref - b[0].posref) || // for equal positions, sort trace indices increasing or decreasing // depending on whether the axis is reversed or not... so stacked @@ -1198,7 +1204,7 @@ function hoverAvoidOverlaps(hoverData, ax, fullLayout) { // loop through groups, combining them if they overlap, // until nothing moves - while(!donepositioning && nummoves <= hoverData.length) { + while(!donepositioning && nummoves <= nLabels) { // to avoid infinite loops, don't move more times // than there are traces nummoves++; @@ -1246,7 +1252,7 @@ function hoverAvoidOverlaps(hoverData, ax, fullLayout) { var grp = pointgroups[i]; for(j = grp.length - 1; j >= 0; j--) { var pt = grp[j]; - var hoverPt = hoverData[pt.i]; + var hoverPt = pt.datum; hoverPt.offset = pt.dp; hoverPt.del = pt.del; } diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 4eabcc61a9f..446bff0cf37 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1685,6 +1685,40 @@ describe('hover info', function() { .catch(failTest) .then(done); }); + + it('should avoid overlaps on *too close* pts are filtered out', function(done) { + Plotly.plot(gd, [ + {name: 'A', x: [9, 10], y: [9, 10]}, + {name: 'B', x: [8, 9], y: [9, 10]}, + {name: 'C', x: [9, 10], y: [10, 11]} + ], { + xaxis: {range: [0, 100]}, + yaxis: {range: [0, 100]}, + width: 700, + height: 450 + }) + .then(function() { _hover(gd, 67, 239); }) + .then(function() { + var nodesA = hoverInfoNodes('A'); + var nodesC = hoverInfoNodes('C'); + + // Ensure layout correct + assertLabelsInsideBoxes(nodesA, 'A'); + assertLabelsInsideBoxes(nodesC, 'C'); + assertSecondaryRightToPrimaryBox(nodesA, 'A'); + assertSecondaryRightToPrimaryBox(nodesC, 'C'); + + // Ensure stacking, finally + var boxA = nodesA.primaryBox.getBoundingClientRect(); + var boxC = nodesC.primaryBox.getBoundingClientRect(); + + // Be robust against floating point arithmetic and subtle future layout changes + expect(calcLineOverlap(boxA.top, boxA.bottom, boxC.top, boxC.bottom)) + .toBeWithin(0, 1); + }) + .catch(failTest) + .then(done); + }); }); describe('hovertemplate', function() {