Skip to content

Commit 90082d5

Browse files
committed
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.
1 parent 3bff26f commit 90082d5

File tree

2 files changed

+123
-47
lines changed

2 files changed

+123
-47
lines changed

Diff for: src/components/fx/hover.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1135,7 +1135,7 @@ function alignHoverText(hoverLabels, rotateLabels) {
11351135

11361136
g.select('path').attr('d', d.anchor === 'middle' ?
11371137
// middle aligned: rect centered on data
1138-
('M-' + (d.bx / 2 + d.tx2width / 2) + ',-' + (d.by / 2) +
1138+
('M-' + (d.bx / 2 + d.tx2width / 2) + ',' + (offsetY - d.by / 2) +
11391139
'h' + d.bx + 'v' + d.by + 'h-' + d.bx + 'Z') :
11401140
// left or right aligned: side rect with arrow to data
11411141
('M0,0L' + (horzSign * HOVERARROWSIZE + offsetX) + ',' + (HOVERARROWSIZE + offsetY) +

Diff for: test/jasmine/tests/hover_label_test.js

+122-46
Original file line numberDiff line numberDiff line change
@@ -1110,49 +1110,32 @@ describe('hover info', function() {
11101110

11111111
});
11121112

1113-
describe('centered', function() {
1114-
var trace1 = {
1115-
x: ['giraffes'],
1116-
y: [5],
1117-
name: 'LA Zoo',
1118-
type: 'bar',
1119-
text: ['Way too long hover info!']
1120-
};
1121-
var trace2 = {
1122-
x: ['giraffes'],
1123-
y: [5],
1124-
name: 'SF Zoo',
1125-
type: 'bar',
1126-
text: ['San Francisco']
1127-
};
1128-
var data = [trace1, trace2];
1129-
var layout = {width: 600, height: 300, barmode: 'stack'};
1130-
1131-
var gd;
1132-
1133-
beforeEach(function(done) {
1134-
gd = createGraphDiv();
1135-
Plotly.plot(gd, data, layout).then(done);
1113+
function hoverInfoNodes(traceName) {
1114+
var g = d3.selectAll('g.hoverlayer g.hovertext').filter(function() {
1115+
return !d3.select(this).select('[data-unformatted="' + traceName + '"]').empty();
11361116
});
11371117

1138-
function centeredHoverInfoNodes() {
1139-
var g = d3.selectAll('g.hoverlayer g.hovertext').filter(function() {
1140-
return !d3.select(this).select('[data-unformatted="LA Zoo"]').empty();
1141-
});
1118+
return {
1119+
primaryText: g.select('text:not([data-unformatted="' + traceName + '"])').node(),
1120+
primaryBox: g.select('path').node(),
1121+
secondaryText: g.select('[data-unformatted="' + traceName + '"]').node(),
1122+
secondaryBox: g.select('rect').node()
1123+
};
1124+
}
11421125

1143-
return {
1144-
primaryText: g.select('text:not([data-unformatted="LA Zoo"])').node(),
1145-
primaryBox: g.select('path').node(),
1146-
secondaryText: g.select('[data-unformatted="LA Zoo"]').node(),
1147-
secondaryBox: g.select('rect').node()
1148-
};
1149-
}
1126+
function ensureCentered(hoverInfoNodes) {
1127+
expect(hoverInfoNodes.primaryText.getAttribute('text-anchor')).toBe('middle');
1128+
expect(hoverInfoNodes.secondaryText.getAttribute('text-anchor')).toBe('middle');
1129+
return hoverInfoNodes;
1130+
}
11501131

1151-
function ensureCentered(hoverInfoNodes) {
1152-
expect(hoverInfoNodes.primaryText.getAttribute('text-anchor')).toBe('middle');
1153-
expect(hoverInfoNodes.secondaryText.getAttribute('text-anchor')).toBe('middle');
1154-
return hoverInfoNodes;
1155-
}
1132+
function assertLabelsInsideBoxes(nodes, msgPrefix) {
1133+
var msgPrefixFmt = msgPrefix ? '[' + msgPrefix + '] ' : '';
1134+
1135+
assertElemInside(nodes.primaryText, nodes.primaryBox,
1136+
msgPrefixFmt + 'Primary text inside box');
1137+
assertElemInside(nodes.secondaryText, nodes.secondaryBox,
1138+
msgPrefixFmt + 'Secondary text inside box');
11561139

11571140
function assertElemInside(elem, container, msg) {
11581141
var elemBB = elem.getBoundingClientRect();
@@ -1162,14 +1145,23 @@ describe('hover info', function() {
11621145
contBB.top < elemBB.top &&
11631146
contBB.bottom > elemBB.bottom).toBe(true, msg);
11641147
}
1148+
}
1149+
1150+
function assertSecondaryRightToPrimaryBox(nodes, msgPrefix) {
1151+
var msgPrefixFmt = msgPrefix ? '[' + msgPrefix + '] ' : '';
1152+
1153+
assertElemRightTo(nodes.secondaryBox, nodes.primaryBox,
1154+
msgPrefixFmt + 'Secondary box right to primary box');
1155+
assertElemTopsAligned(nodes.secondaryBox, nodes.primaryBox,
1156+
msgPrefixFmt + 'Top edges of primary and secondary boxes aligned');
11651157

11661158
function assertElemRightTo(elem, refElem, msg) {
11671159
var elemBB = elem.getBoundingClientRect();
11681160
var refElemBB = refElem.getBoundingClientRect();
11691161
expect(elemBB.left >= refElemBB.right).toBe(true, msg);
11701162
}
11711163

1172-
function assertTopsAligned(elem1, elem2, msg) {
1164+
function assertElemTopsAligned(elem1, elem2, msg) {
11731165
var elem1BB = elem1.getBoundingClientRect();
11741166
var elem2BB = elem2.getBoundingClientRect();
11751167

@@ -1178,21 +1170,105 @@ describe('hover info', function() {
11781170
var tolerance = 1.1;
11791171
expect(elem1BB.top - elem2BB.top).toBeWithin(0, tolerance, msg);
11801172
}
1173+
}
1174+
1175+
describe('centered', function() {
1176+
var trace1 = {
1177+
x: ['giraffes'],
1178+
y: [5],
1179+
name: 'LA Zoo',
1180+
type: 'bar',
1181+
text: ['Way too long hover info!']
1182+
};
1183+
var trace2 = {
1184+
x: ['giraffes'],
1185+
y: [5],
1186+
name: 'SF Zoo',
1187+
type: 'bar',
1188+
text: ['San Francisco']
1189+
};
1190+
var data = [trace1, trace2];
1191+
var layout = {width: 600, height: 300, barmode: 'stack'};
1192+
1193+
var gd;
1194+
1195+
beforeEach(function(done) {
1196+
gd = createGraphDiv();
1197+
Plotly.plot(gd, data, layout).then(done);
1198+
});
11811199

11821200
it('renders labels inside boxes', function() {
11831201
_hover(gd, 300, 150);
11841202

1185-
var nodes = ensureCentered(centeredHoverInfoNodes());
1186-
assertElemInside(nodes.primaryText, nodes.primaryBox, 'Primary text inside box');
1187-
assertElemInside(nodes.secondaryText, nodes.secondaryBox, 'Secondary text inside box');
1203+
var nodes = ensureCentered(hoverInfoNodes('LA Zoo'));
1204+
assertLabelsInsideBoxes(nodes);
11881205
});
11891206

11901207
it('renders secondary info box right to primary info box', function() {
11911208
_hover(gd, 300, 150);
11921209

1193-
var nodes = ensureCentered(centeredHoverInfoNodes());
1194-
assertElemRightTo(nodes.secondaryBox, nodes.primaryBox, 'Secondary box right to primary box');
1195-
assertTopsAligned(nodes.secondaryBox, nodes.primaryBox, 'Top edges of primary and secondary boxes aligned');
1210+
var nodes = ensureCentered(hoverInfoNodes('LA Zoo'));
1211+
assertSecondaryRightToPrimaryBox(nodes);
1212+
});
1213+
});
1214+
1215+
describe('centered', function() {
1216+
var trace1 = {
1217+
x: ['giraffes'],
1218+
y: [5],
1219+
name: 'LA Zoo',
1220+
type: 'bar',
1221+
text: ['Way too long hover info!']
1222+
};
1223+
var trace2 = {
1224+
x: ['giraffes'],
1225+
y: [5],
1226+
name: 'SF Zoo',
1227+
type: 'bar',
1228+
text: ['Way too looooong hover info!']
1229+
};
1230+
var trace3 = {
1231+
x: ['giraffes'],
1232+
y: [5],
1233+
name: 'NY Zoo',
1234+
type: 'bar',
1235+
text: ['New York']
1236+
};
1237+
var data = [trace1, trace2, trace3];
1238+
var layout = {width: 600, height: 300};
1239+
1240+
var gd;
1241+
1242+
beforeEach(function(done) {
1243+
gd = createGraphDiv();
1244+
Plotly.plot(gd, data, layout).then(done);
1245+
});
1246+
1247+
function calcLineOverlap(minA, maxA, minB, maxB) {
1248+
expect(minA).toBeLessThan(maxA);
1249+
expect(minB).toBeLessThan(maxB);
1250+
1251+
var overlap = Math.min(maxA, maxB) - Math.max(minA, minB);
1252+
return Math.max(0, overlap);
1253+
}
1254+
1255+
it('stacks nicely upon each other', function() {
1256+
_hover(gd, 300, 150);
1257+
1258+
var nodesLA = ensureCentered(hoverInfoNodes('LA Zoo'));
1259+
var nodesSF = ensureCentered(hoverInfoNodes('SF Zoo'));
1260+
1261+
// Ensure layout correct
1262+
assertLabelsInsideBoxes(nodesLA, 'LA Zoo');
1263+
assertLabelsInsideBoxes(nodesSF, 'SF Zoo');
1264+
assertSecondaryRightToPrimaryBox(nodesLA, 'LA Zoo');
1265+
assertSecondaryRightToPrimaryBox(nodesSF, 'SF Zoo');
1266+
1267+
// Ensure stacking, finally
1268+
var boxLABB = nodesLA.primaryBox.getBoundingClientRect();
1269+
var boxSFBB = nodesSF.primaryBox.getBoundingClientRect();
1270+
expect(calcLineOverlap(boxLABB.top, boxLABB.bottom, boxSFBB.top, boxSFBB.bottom))
1271+
.toBeWithin(0, 1); // Be robust against floating point arithmetic and subtle future layout changes
11961272
});
11971273
});
11981274
});

0 commit comments

Comments
 (0)