Skip to content

Commit 108900f

Browse files
authored
Merge pull request #3043 from plotly/rotate-horiz-box-violin-hover-labels
Rotate hover labels in a few more scenarios
2 parents 85a2004 + d9f3db7 commit 108900f

File tree

4 files changed

+211
-23
lines changed

4 files changed

+211
-23
lines changed

src/components/fx/hover.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ function _hover(gd, evt, subplot, noHoverEvent) {
237237
vLinePoint: null
238238
};
239239

240+
// does subplot have one (or more) horizontal traces?
241+
// This is used to determine whether we rotate the labels or not
242+
var hasOneHorizontalTrace = false;
243+
240244
// Figure out what we're hovering on:
241245
// mouse location or user-supplied data
242246

@@ -245,8 +249,12 @@ function _hover(gd, evt, subplot, noHoverEvent) {
245249
hovermode = 'array';
246250
for(itemnum = 0; itemnum < evt.length; itemnum++) {
247251
cd = gd.calcdata[evt[itemnum].curveNumber||0];
252+
trace = cd[0].trace;
248253
if(cd[0].trace.hoverinfo !== 'skip') {
249254
searchData.push(cd);
255+
if(trace.orientation === 'h') {
256+
hasOneHorizontalTrace = true;
257+
}
250258
}
251259
}
252260
}
@@ -256,6 +264,9 @@ function _hover(gd, evt, subplot, noHoverEvent) {
256264
trace = cd[0].trace;
257265
if(trace.hoverinfo !== 'skip' && helpers.isTraceInSubplots(trace, subplots)) {
258266
searchData.push(cd);
267+
if(trace.orientation === 'h') {
268+
hasOneHorizontalTrace = true;
269+
}
259270
}
260271
}
261272

@@ -577,9 +588,10 @@ function _hover(gd, evt, subplot, noHoverEvent) {
577588

578589
gd._hoverdata = newhoverdata;
579590

580-
// if there's more than one horz bar trace,
581-
// rotate the labels so they don't overlap
582-
var rotateLabels = hovermode === 'y' && searchData.length > 1;
591+
var rotateLabels = (
592+
(hovermode === 'y' && (searchData.length > 1 || hoverData.length > 1)) ||
593+
(hovermode === 'closest' && hasOneHorizontalTrace && hoverData.length > 1)
594+
);
583595

584596
var bgColor = Color.combine(
585597
fullLayout.plot_bgcolor || Color.background,

test/jasmine/assets/custom_assertions.js

+31-20
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ function count(selector) {
9090
* - nums {string || array of strings}
9191
* - name {string || array of strings}
9292
* - axis {string}
93+
* - vOrder {array of number}
94+
* - hOrder {array of number}
95+
* - isRotated {boolean}
9396
* @param {string} msg
9497
*/
9598
exports.assertHoverLabelContent = function(expectation, msg) {
@@ -103,17 +106,22 @@ exports.assertHoverLabelContent = function(expectation, msg) {
103106
var axMsg = 'common axis hover label';
104107
var axCnt = count(axSelector);
105108

109+
var reRotate = /(\brotate\(.*?\);?)/;
110+
106111
if(ptCnt === 1) {
107-
assertLabelContent(
108-
d3.select(ptSelector + '> text.nums'),
109-
expectation.nums,
110-
ptMsg + ' (nums)'
111-
);
112-
assertLabelContent(
113-
d3.select(ptSelector + '> text.name'),
114-
expectation.name,
115-
ptMsg + ' (name)'
116-
);
112+
var g = d3.select(ptSelector);
113+
var numsSel = g.select('text.nums');
114+
var nameSel = g.select('text.name');
115+
116+
assertLabelContent(numsSel, expectation.nums, ptMsg + ' (nums)');
117+
assertLabelContent(nameSel, expectation.name, ptMsg + ' (name)');
118+
119+
if('isRotated' in expectation) {
120+
expect(g.attr('transform').match(reRotate))
121+
.negateIf(expectation.isRotated)
122+
.toBe(null, ptMsg + ' should be rotated');
123+
124+
}
117125
} else if(ptCnt > 1) {
118126
if(!Array.isArray(expectation.nums) || !Array.isArray(expectation.name)) {
119127
fail(ptMsg + ': expecting more than 1 labels.');
@@ -124,16 +132,19 @@ exports.assertHoverLabelContent = function(expectation, msg) {
124132

125133
var bboxes = [];
126134
d3.selectAll(ptSelector).each(function(_, i) {
127-
assertLabelContent(
128-
d3.select(this).select('text.nums'),
129-
expectation.nums[i],
130-
ptMsg + ' (nums ' + i + ')'
131-
);
132-
assertLabelContent(
133-
d3.select(this).select('text.name'),
134-
expectation.name[i],
135-
ptMsg + ' (name ' + i + ')'
136-
);
135+
var g = d3.select(this);
136+
var numsSel = g.select('text.nums');
137+
var nameSel = g.select('text.name');
138+
139+
assertLabelContent(numsSel, expectation.nums[i], ptMsg + ' (nums ' + i + ')');
140+
assertLabelContent(nameSel, expectation.name[i], ptMsg + ' (name ' + i + ')');
141+
142+
if('isRotated' in expectation) {
143+
expect(g.attr('transform').match(reRotate))
144+
.negateIf(expectation.isRotated)
145+
.toBe(null, ptMsg + ' ' + i + ' should be rotated');
146+
}
147+
137148
bboxes.push({bbox: this.getBoundingClientRect(), index: i});
138149
});
139150
if(expectation.vOrder) {

test/jasmine/tests/hover_label_test.js

+107
Original file line numberDiff line numberDiff line change
@@ -2536,6 +2536,113 @@ describe('hover distance', function() {
25362536
});
25372537
});
25382538

2539+
describe('hover label rotation:', function() {
2540+
var gd;
2541+
2542+
function _hover(gd, opts) {
2543+
Fx.hover(gd, opts);
2544+
Lib.clearThrottle();
2545+
}
2546+
2547+
describe('when a single pt is picked', function() {
2548+
afterAll(destroyGraphDiv);
2549+
2550+
beforeAll(function(done) {
2551+
gd = createGraphDiv();
2552+
2553+
Plotly.plot(gd, [{
2554+
type: 'bar',
2555+
orientation: 'h',
2556+
y: [0, 1, 2],
2557+
x: [1, 2, 1]
2558+
}, {
2559+
type: 'bar',
2560+
orientation: 'h',
2561+
y: [3, 4, 5],
2562+
x: [1, 2, 1]
2563+
}], {
2564+
hovermode: 'y'
2565+
})
2566+
.catch(failTest)
2567+
.then(done);
2568+
});
2569+
2570+
it('should rotate labels under *hovermode:y*', function() {
2571+
_hover(gd, { xval: 2, yval: 1 });
2572+
assertHoverLabelContent({
2573+
nums: '2',
2574+
name: 'trace 0',
2575+
axis: '1',
2576+
// N.B. could be changed to be made consistent with 'closest'
2577+
isRotated: true
2578+
});
2579+
});
2580+
2581+
it('should not rotate labels under *hovermode:closest*', function(done) {
2582+
Plotly.relayout(gd, 'hovermode', 'closest').then(function() {
2583+
_hover(gd, { xval: 1.9, yval: 1 });
2584+
assertHoverLabelContent({
2585+
nums: '(2, 1)',
2586+
name: 'trace 0',
2587+
axis: '',
2588+
isRotated: false
2589+
});
2590+
})
2591+
.catch(failTest)
2592+
.then(done);
2593+
});
2594+
});
2595+
2596+
describe('when mulitple pts are picked', function() {
2597+
afterAll(destroyGraphDiv);
2598+
2599+
beforeAll(function(done) {
2600+
gd = createGraphDiv();
2601+
2602+
Plotly.plot(gd, [{
2603+
type: 'bar',
2604+
orientation: 'h',
2605+
y: [0, 1, 2],
2606+
x: [1, 2, 1]
2607+
}, {
2608+
type: 'bar',
2609+
orientation: 'h',
2610+
y: [0, 1, 2],
2611+
x: [1, 2, 1]
2612+
}], {
2613+
hovermode: 'y'
2614+
})
2615+
.catch(failTest)
2616+
.then(done);
2617+
});
2618+
2619+
it('should rotate labels under *hovermode:y*', function() {
2620+
_hover(gd, { xval: 2, yval: 1 });
2621+
assertHoverLabelContent({
2622+
nums: ['2', '2'],
2623+
name: ['trace 0', 'trace 1'],
2624+
axis: '1',
2625+
isRotated: true
2626+
});
2627+
});
2628+
2629+
it('should not rotate labels under *hovermode:closest*', function(done) {
2630+
Plotly.relayout(gd, 'hovermode', 'closest').then(function() {
2631+
_hover(gd, { xval: 1.9, yval: 1 });
2632+
assertHoverLabelContent({
2633+
nums: '(2, 1)',
2634+
// N.B. only showing the 'top' trace
2635+
name: 'trace 1',
2636+
axis: '',
2637+
isRotated: false
2638+
});
2639+
})
2640+
.catch(failTest)
2641+
.then(done);
2642+
});
2643+
});
2644+
});
2645+
25392646
describe('hovermode defaults to', function() {
25402647
var gd;
25412648

test/jasmine/tests/violin_test.js

+58
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,64 @@ describe('Test violin hover:', function() {
431431
nums: 'x: 42.43046, kde: 0.083',
432432
name: '',
433433
axis: 'Saturday'
434+
}, {
435+
desc: 'single horizontal violin',
436+
mock: require('@mocks/violin_non-linear.json'),
437+
pos: [310, 160],
438+
nums: ['median: C', 'min: A', 'q1: B', 'q3: D', 'max: G', 'upper fence: D', 'x: C, kde: 1.005'],
439+
name: ['categories', '', '', '', '', '', ''],
440+
axis: 'categories',
441+
hOrder: [4, 5, 3, 6, 0, 2, 1],
442+
isRotated: true
443+
}, {
444+
desc: 'multiple horizontal violins',
445+
mock: require('@mocks/box_grouped_horz.json'),
446+
patch: function(fig) {
447+
fig.data.forEach(function(t) {
448+
t.type = 'violin';
449+
t.hoveron = 'violins';
450+
});
451+
fig.layout.violinmode = 'group';
452+
return fig;
453+
},
454+
nums: ['median: 0.4', 'min: 0.1', 'q1: 0.2', 'q3: 0.7', 'max: 0.9'],
455+
name: ['kale', '', '', '', ''],
456+
axis: 'day 2',
457+
hOrder: [4, 3, 0, 2, 1],
458+
isRotated: true
459+
}, {
460+
desc: 'multiple horizontal violins (under hovermode:closest)',
461+
mock: require('@mocks/box_grouped_horz.json'),
462+
patch: function(fig) {
463+
fig.data.forEach(function(t) {
464+
t.type = 'violin';
465+
t.hoveron = 'violins';
466+
});
467+
fig.layout.violinmode = 'group';
468+
fig.layout.hovermode = 'closest';
469+
return fig;
470+
},
471+
pos: [200, 175],
472+
nums: [
473+
'(median: 0.7, day 2)', '(min: 0.2, day 2)', '(q1: 0.5, day 2)',
474+
'(q3: 0.8, day 2)', '(max: 0.9, day 2)'
475+
],
476+
name: ['radishes', '', '', '', ''],
477+
hOrder: [4, 3, 0, 2, 1],
478+
isRotated: true
479+
}, {
480+
desc: 'hovering over single pt on horizontal violin should not rotate labels',
481+
mock: require('@mocks/violin_old-faithful.json'),
482+
patch: function(fig) {
483+
fig.data[0].x = fig.data[0].y;
484+
delete fig.data[0].y;
485+
fig.layout = {hovermode: 'closest'};
486+
return fig;
487+
},
488+
pos: [539, 293],
489+
nums: '(96, Old Faithful)',
490+
name: '',
491+
isRotated: false
434492
}]
435493
.forEach(function(specs) {
436494
it('should generate correct hover labels ' + specs.desc, function(done) {

0 commit comments

Comments
 (0)