From 90082d5468106219065710545959a79dc52081b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=B6stl?= Date: Tue, 6 Mar 2018 11:11:38 +0100 Subject: [PATCH] Fix stacking of centered hover info elements #865 - Bug: boxes of multiple centered hover info elements had all the same vertical position, hence hiding each other, not "following" associated text labels in terms of positioning. --- src/components/fx/hover.js | 2 +- test/jasmine/tests/hover_label_test.js | 168 ++++++++++++++++++------- 2 files changed, 123 insertions(+), 47 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index cd78c93aebc..32d7c09e077 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1135,7 +1135,7 @@ function alignHoverText(hoverLabels, rotateLabels) { g.select('path').attr('d', d.anchor === 'middle' ? // middle aligned: rect centered on data - ('M-' + (d.bx / 2 + d.tx2width / 2) + ',-' + (d.by / 2) + + ('M-' + (d.bx / 2 + d.tx2width / 2) + ',' + (offsetY - d.by / 2) + 'h' + d.bx + 'v' + d.by + 'h-' + d.bx + 'Z') : // left or right aligned: side rect with arrow to data ('M0,0L' + (horzSign * HOVERARROWSIZE + offsetX) + ',' + (HOVERARROWSIZE + offsetY) + diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index d279891317f..847c7b96cc9 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1110,49 +1110,32 @@ describe('hover info', function() { }); - describe('centered', function() { - var trace1 = { - x: ['giraffes'], - y: [5], - name: 'LA Zoo', - type: 'bar', - text: ['Way too long hover info!'] - }; - var trace2 = { - x: ['giraffes'], - y: [5], - name: 'SF Zoo', - type: 'bar', - text: ['San Francisco'] - }; - var data = [trace1, trace2]; - var layout = {width: 600, height: 300, barmode: 'stack'}; - - var gd; - - beforeEach(function(done) { - gd = createGraphDiv(); - Plotly.plot(gd, data, layout).then(done); + function hoverInfoNodes(traceName) { + var g = d3.selectAll('g.hoverlayer g.hovertext').filter(function() { + return !d3.select(this).select('[data-unformatted="' + traceName + '"]').empty(); }); - function centeredHoverInfoNodes() { - var g = d3.selectAll('g.hoverlayer g.hovertext').filter(function() { - return !d3.select(this).select('[data-unformatted="LA Zoo"]').empty(); - }); + return { + primaryText: g.select('text:not([data-unformatted="' + traceName + '"])').node(), + primaryBox: g.select('path').node(), + secondaryText: g.select('[data-unformatted="' + traceName + '"]').node(), + secondaryBox: g.select('rect').node() + }; + } - return { - primaryText: g.select('text:not([data-unformatted="LA Zoo"])').node(), - primaryBox: g.select('path').node(), - secondaryText: g.select('[data-unformatted="LA Zoo"]').node(), - secondaryBox: g.select('rect').node() - }; - } + function ensureCentered(hoverInfoNodes) { + expect(hoverInfoNodes.primaryText.getAttribute('text-anchor')).toBe('middle'); + expect(hoverInfoNodes.secondaryText.getAttribute('text-anchor')).toBe('middle'); + return hoverInfoNodes; + } - function ensureCentered(hoverInfoNodes) { - expect(hoverInfoNodes.primaryText.getAttribute('text-anchor')).toBe('middle'); - expect(hoverInfoNodes.secondaryText.getAttribute('text-anchor')).toBe('middle'); - return hoverInfoNodes; - } + function assertLabelsInsideBoxes(nodes, msgPrefix) { + var msgPrefixFmt = msgPrefix ? '[' + msgPrefix + '] ' : ''; + + assertElemInside(nodes.primaryText, nodes.primaryBox, + msgPrefixFmt + 'Primary text inside box'); + assertElemInside(nodes.secondaryText, nodes.secondaryBox, + msgPrefixFmt + 'Secondary text inside box'); function assertElemInside(elem, container, msg) { var elemBB = elem.getBoundingClientRect(); @@ -1162,6 +1145,15 @@ describe('hover info', function() { contBB.top < elemBB.top && contBB.bottom > elemBB.bottom).toBe(true, msg); } + } + + function assertSecondaryRightToPrimaryBox(nodes, msgPrefix) { + var msgPrefixFmt = msgPrefix ? '[' + msgPrefix + '] ' : ''; + + assertElemRightTo(nodes.secondaryBox, nodes.primaryBox, + msgPrefixFmt + 'Secondary box right to primary box'); + assertElemTopsAligned(nodes.secondaryBox, nodes.primaryBox, + msgPrefixFmt + 'Top edges of primary and secondary boxes aligned'); function assertElemRightTo(elem, refElem, msg) { var elemBB = elem.getBoundingClientRect(); @@ -1169,7 +1161,7 @@ describe('hover info', function() { expect(elemBB.left >= refElemBB.right).toBe(true, msg); } - function assertTopsAligned(elem1, elem2, msg) { + function assertElemTopsAligned(elem1, elem2, msg) { var elem1BB = elem1.getBoundingClientRect(); var elem2BB = elem2.getBoundingClientRect(); @@ -1178,21 +1170,105 @@ describe('hover info', function() { var tolerance = 1.1; expect(elem1BB.top - elem2BB.top).toBeWithin(0, tolerance, msg); } + } + + describe('centered', function() { + var trace1 = { + x: ['giraffes'], + y: [5], + name: 'LA Zoo', + type: 'bar', + text: ['Way too long hover info!'] + }; + var trace2 = { + x: ['giraffes'], + y: [5], + name: 'SF Zoo', + type: 'bar', + text: ['San Francisco'] + }; + var data = [trace1, trace2]; + var layout = {width: 600, height: 300, barmode: 'stack'}; + + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + Plotly.plot(gd, data, layout).then(done); + }); it('renders labels inside boxes', function() { _hover(gd, 300, 150); - var nodes = ensureCentered(centeredHoverInfoNodes()); - assertElemInside(nodes.primaryText, nodes.primaryBox, 'Primary text inside box'); - assertElemInside(nodes.secondaryText, nodes.secondaryBox, 'Secondary text inside box'); + var nodes = ensureCentered(hoverInfoNodes('LA Zoo')); + assertLabelsInsideBoxes(nodes); }); it('renders secondary info box right to primary info box', function() { _hover(gd, 300, 150); - var nodes = ensureCentered(centeredHoverInfoNodes()); - assertElemRightTo(nodes.secondaryBox, nodes.primaryBox, 'Secondary box right to primary box'); - assertTopsAligned(nodes.secondaryBox, nodes.primaryBox, 'Top edges of primary and secondary boxes aligned'); + var nodes = ensureCentered(hoverInfoNodes('LA Zoo')); + assertSecondaryRightToPrimaryBox(nodes); + }); + }); + + describe('centered', function() { + var trace1 = { + x: ['giraffes'], + y: [5], + name: 'LA Zoo', + type: 'bar', + text: ['Way too long hover info!'] + }; + var trace2 = { + x: ['giraffes'], + y: [5], + name: 'SF Zoo', + type: 'bar', + text: ['Way too looooong hover info!'] + }; + var trace3 = { + x: ['giraffes'], + y: [5], + name: 'NY Zoo', + type: 'bar', + text: ['New York'] + }; + var data = [trace1, trace2, trace3]; + var layout = {width: 600, height: 300}; + + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + Plotly.plot(gd, data, layout).then(done); + }); + + function calcLineOverlap(minA, maxA, minB, maxB) { + expect(minA).toBeLessThan(maxA); + expect(minB).toBeLessThan(maxB); + + var overlap = Math.min(maxA, maxB) - Math.max(minA, minB); + return Math.max(0, overlap); + } + + it('stacks nicely upon each other', function() { + _hover(gd, 300, 150); + + var nodesLA = ensureCentered(hoverInfoNodes('LA Zoo')); + var nodesSF = ensureCentered(hoverInfoNodes('SF Zoo')); + + // Ensure layout correct + assertLabelsInsideBoxes(nodesLA, 'LA Zoo'); + assertLabelsInsideBoxes(nodesSF, 'SF Zoo'); + assertSecondaryRightToPrimaryBox(nodesLA, 'LA Zoo'); + assertSecondaryRightToPrimaryBox(nodesSF, 'SF Zoo'); + + // Ensure stacking, finally + var boxLABB = nodesLA.primaryBox.getBoundingClientRect(); + var boxSFBB = nodesSF.primaryBox.getBoundingClientRect(); + expect(calcLineOverlap(boxLABB.top, boxLABB.bottom, boxSFBB.top, boxSFBB.bottom)) + .toBeWithin(0, 1); // Be robust against floating point arithmetic and subtle future layout changes }); }); });