Skip to content

Commit e5db8ab

Browse files
committed
Add hoversort layout attribute for sorting unified hover label items by value
1 parent 2dcd2c9 commit e5db8ab

5 files changed

Lines changed: 281 additions & 0 deletions

File tree

src/components/fx/hover.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,15 @@ function createHoverText(hoverData, opts) {
13411341
mockLegend.entries.push([pt]);
13421342
}
13431343
mockLegend.entries.sort(function (a, b) {
1344+
var hoversort = fullLayout.hoversort;
1345+
if (hoversort === 'value descending' || hoversort === 'value ascending') {
1346+
var valueLetter = hovermode.charAt(0) === 'x' ? 'y' : 'x';
1347+
var aVal = a[0][valueLetter + 'LabelVal'];
1348+
var bVal = b[0][valueLetter + 'LabelVal'];
1349+
if (aVal !== bVal) {
1350+
return hoversort === 'value descending' ? bVal - aVal : aVal - bVal;
1351+
}
1352+
}
13441353
return a[0].trace.index - b[0].trace.index;
13451354
});
13461355
mockLegend.layer = container;

src/components/fx/layout_attributes.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ module.exports = {
7878
'If false, hover interactions are disabled.'
7979
].join(' ')
8080
},
81+
hoversort: {
82+
valType: 'enumerated',
83+
values: ['trace', 'value descending', 'value ascending'],
84+
dflt: 'trace',
85+
editType: 'none',
86+
description: [
87+
'Determines the order of items shown in unified hover labels.',
88+
'If *trace*, items are sorted by trace index.',
89+
'If *value descending*, items are sorted by value from largest to smallest.',
90+
'If *value ascending*, items are sorted by value from smallest to largest.',
91+
'Only applies when `hovermode` is *x unified* or *y unified*.'
92+
].join(' ')
93+
},
8194
hoversubplots: {
8295
valType: 'enumerated',
8396
values: ['single', 'overlaying', 'axis'],

src/components/fx/layout_defaults.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) {
1414
if(hoverMode) {
1515
coerce('hoverdistance');
1616
coerce('spikedistance');
17+
if(hoverMode.indexOf('unified') !== -1) {
18+
coerce('hoversort');
19+
}
1720
}
1821

1922
var dragMode = coerce('dragmode');

test/jasmine/tests/fx_test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,37 @@ describe('Fx defaults', function() {
234234
}
235235
});
236236
});
237+
238+
it('should coerce hoversort only for unified hovermode', function() {
239+
var out = _supply([{type: 'bar', y: [1, 2, 3]}], {
240+
hovermode: 'x unified',
241+
hoversort: 'value descending'
242+
});
243+
expect(out.layout.hoversort).toBe('value descending');
244+
});
245+
246+
it('should not coerce hoversort for non-unified hovermode', function() {
247+
var out = _supply([{type: 'bar', y: [1, 2, 3]}], {
248+
hovermode: 'closest',
249+
hoversort: 'value descending'
250+
});
251+
expect(out.layout.hoversort).toBeUndefined();
252+
});
253+
254+
it('should default hoversort to trace', function() {
255+
var out = _supply([{type: 'bar', y: [1, 2, 3]}], {
256+
hovermode: 'x unified'
257+
});
258+
expect(out.layout.hoversort).toBe('trace');
259+
});
260+
261+
it('should coerce hoversort for y unified hovermode', function() {
262+
var out = _supply([{type: 'bar', y: [1, 2, 3]}], {
263+
hovermode: 'y unified',
264+
hoversort: 'value ascending'
265+
});
266+
expect(out.layout.hoversort).toBe('value ascending');
267+
});
237268
});
238269

239270
describe('relayout', function() {

test/jasmine/tests/hover_label_test.js

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7315,3 +7315,228 @@ describe('hoverlabel.showarrow', function() {
73157315
.then(done, done.fail);
73167316
});
73177317
});
7318+
7319+
describe('hoversort', function() {
7320+
var gd;
7321+
7322+
beforeEach(function() {
7323+
gd = createGraphDiv();
7324+
});
7325+
7326+
afterEach(destroyGraphDiv);
7327+
7328+
function _hover(gd, opts) {
7329+
Fx.hover(gd, opts);
7330+
Lib.clearThrottle();
7331+
}
7332+
7333+
function getHoverLabel() {
7334+
var hoverLayer = d3Select('g.hoverlayer');
7335+
return hoverLayer.select('g.legend');
7336+
}
7337+
7338+
function assertLabel(expectation) {
7339+
var hover = getHoverLabel();
7340+
var title = hover.select('text.legendtitletext');
7341+
var traces = hover.selectAll('g.traces');
7342+
7343+
if(expectation.title) {
7344+
expect(title.text()).toBe(expectation.title);
7345+
}
7346+
7347+
expect(traces.size()).toBe(expectation.items.length, 'has the incorrect number of items');
7348+
traces.each(function(_, i) {
7349+
var e = d3Select(this);
7350+
expect(e.select('text').text()).toBe(expectation.items[i]);
7351+
});
7352+
}
7353+
7354+
it('should sort unified hover items by value descending', function(done) {
7355+
Plotly.newPlot(gd, {
7356+
data: [
7357+
{name: 'small', y: [1, 2, 3]},
7358+
{name: 'large', y: [10, 20, 30]},
7359+
{name: 'medium', y: [5, 10, 15]}
7360+
],
7361+
layout: {
7362+
hovermode: 'x unified',
7363+
hoversort: 'value descending',
7364+
showlegend: false,
7365+
width: 500,
7366+
height: 500
7367+
}
7368+
})
7369+
.then(function() {
7370+
_hover(gd, { xval: 2 });
7371+
assertLabel({title: '2', items: [
7372+
'large : 30',
7373+
'medium : 15',
7374+
'small : 3'
7375+
]});
7376+
})
7377+
.then(done, done.fail);
7378+
});
7379+
7380+
it('should sort unified hover items by value ascending', function(done) {
7381+
Plotly.newPlot(gd, {
7382+
data: [
7383+
{name: 'small', y: [1, 2, 3]},
7384+
{name: 'large', y: [10, 20, 30]},
7385+
{name: 'medium', y: [5, 10, 15]}
7386+
],
7387+
layout: {
7388+
hovermode: 'x unified',
7389+
hoversort: 'value ascending',
7390+
showlegend: false,
7391+
width: 500,
7392+
height: 500
7393+
}
7394+
})
7395+
.then(function() {
7396+
_hover(gd, { xval: 2 });
7397+
assertLabel({title: '2', items: [
7398+
'small : 3',
7399+
'medium : 15',
7400+
'large : 30'
7401+
]});
7402+
})
7403+
.then(done, done.fail);
7404+
});
7405+
7406+
it('should default to trace index order', function(done) {
7407+
Plotly.newPlot(gd, {
7408+
data: [
7409+
{name: 'small', y: [1, 2, 3]},
7410+
{name: 'large', y: [10, 20, 30]},
7411+
{name: 'medium', y: [5, 10, 15]}
7412+
],
7413+
layout: {
7414+
hovermode: 'x unified',
7415+
showlegend: false,
7416+
width: 500,
7417+
height: 500
7418+
}
7419+
})
7420+
.then(function() {
7421+
_hover(gd, { xval: 2 });
7422+
assertLabel({title: '2', items: [
7423+
'small : 3',
7424+
'large : 30',
7425+
'medium : 15'
7426+
]});
7427+
})
7428+
.then(done, done.fail);
7429+
});
7430+
7431+
it('should sort by value descending with bar charts', function(done) {
7432+
Plotly.newPlot(gd, {
7433+
data: [
7434+
{name: 'A', type: 'bar', y: [5, 10, 15]},
7435+
{name: 'B', type: 'bar', y: [20, 25, 30]},
7436+
{name: 'C', type: 'bar', y: [1, 2, 3]}
7437+
],
7438+
layout: {
7439+
hovermode: 'x unified',
7440+
hoversort: 'value descending',
7441+
showlegend: false,
7442+
width: 500,
7443+
height: 500
7444+
}
7445+
})
7446+
.then(function() {
7447+
_hover(gd, { xval: 1 });
7448+
assertLabel({title: '1', items: [
7449+
'B : 25',
7450+
'A : 10',
7451+
'C : 2'
7452+
]});
7453+
})
7454+
.then(done, done.fail);
7455+
});
7456+
7457+
it('should sort by value descending with y unified hovermode', function(done) {
7458+
Plotly.newPlot(gd, {
7459+
data: [
7460+
{name: 'first', x: [1, 10, 5]},
7461+
{name: 'second', x: [20, 2, 15]},
7462+
{name: 'third', x: [8, 8, 8]}
7463+
],
7464+
layout: {
7465+
hovermode: 'y unified',
7466+
hoversort: 'value descending',
7467+
showlegend: false,
7468+
width: 500,
7469+
height: 500
7470+
}
7471+
})
7472+
.then(function() {
7473+
_hover(gd, { yval: 0 });
7474+
assertLabel({title: '0', items: [
7475+
'second : 20',
7476+
'third : 8',
7477+
'first : 1'
7478+
]});
7479+
})
7480+
.then(done, done.fail);
7481+
});
7482+
7483+
it('should fall back to trace index when values are equal', function(done) {
7484+
Plotly.newPlot(gd, {
7485+
data: [
7486+
{name: 'A', y: [5, 10, 10]},
7487+
{name: 'B', y: [5, 20, 10]},
7488+
{name: 'C', y: [5, 5, 10]}
7489+
],
7490+
layout: {
7491+
hovermode: 'x unified',
7492+
hoversort: 'value descending',
7493+
showlegend: false,
7494+
width: 500,
7495+
height: 500
7496+
}
7497+
})
7498+
.then(function() {
7499+
// At xval=0, all values are 5 - should keep trace order
7500+
_hover(gd, { xval: 0 });
7501+
assertLabel({title: '0', items: [
7502+
'A : 5',
7503+
'B : 5',
7504+
'C : 5'
7505+
]});
7506+
})
7507+
.then(done, done.fail);
7508+
});
7509+
7510+
it('should dynamically update sort via relayout', function(done) {
7511+
Plotly.newPlot(gd, {
7512+
data: [
7513+
{name: 'low', y: [1, 2, 3]},
7514+
{name: 'high', y: [10, 20, 30]}
7515+
],
7516+
layout: {
7517+
hovermode: 'x unified',
7518+
hoversort: 'trace',
7519+
showlegend: false,
7520+
width: 500,
7521+
height: 500
7522+
}
7523+
})
7524+
.then(function() {
7525+
_hover(gd, { xval: 1 });
7526+
assertLabel({title: '1', items: [
7527+
'low : 2',
7528+
'high : 20'
7529+
]});
7530+
7531+
return Plotly.relayout(gd, 'hoversort', 'value descending');
7532+
})
7533+
.then(function() {
7534+
_hover(gd, { xval: 1 });
7535+
assertLabel({title: '1', items: [
7536+
'high : 20',
7537+
'low : 2'
7538+
]});
7539+
})
7540+
.then(done, done.fail);
7541+
});
7542+
});

0 commit comments

Comments
 (0)