Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7734_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add `hoversort` layout attribute to sort unified hover label items by value [[#7734](https://github.com/plotly/plotly.js/pull/7734)]
9 changes: 9 additions & 0 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,15 @@ function createHoverText(hoverData, opts) {
mockLegend.entries.push([pt]);
}
mockLegend.entries.sort(function (a, b) {
var hoversort = fullLayout.hoversort;
if (hoversort === 'value descending' || hoversort === 'value ascending') {
var valueLetter = hovermode.charAt(0) === 'x' ? 'y' : 'x';
var aVal = a[0][valueLetter + 'LabelVal'];
var bVal = b[0][valueLetter + 'LabelVal'];
if (aVal !== bVal) {
return hoversort === 'value descending' ? bVal - aVal : aVal - bVal;
}
}
return a[0].trace.index - b[0].trace.index;
});
mockLegend.layer = container;
Expand Down
13 changes: 13 additions & 0 deletions src/components/fx/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ module.exports = {
'If false, hover interactions are disabled.'
].join(' ')
},
hoversort: {
valType: 'enumerated',
values: ['trace', 'value descending', 'value ascending'],
dflt: 'trace',
editType: 'none',
description: [
'Determines the order of items shown in unified hover labels.',
'If *trace*, items are sorted by trace index.',
'If *value descending*, items are sorted by value from largest to smallest.',
'If *value ascending*, items are sorted by value from smallest to largest.',
'Only applies when `hovermode` is *x unified* or *y unified*.'
].join(' ')
},
hoversubplots: {
valType: 'enumerated',
values: ['single', 'overlaying', 'axis'],
Expand Down
3 changes: 3 additions & 0 deletions src/components/fx/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) {
if(hoverMode) {
coerce('hoverdistance');
coerce('spikedistance');
if(hoverMode.indexOf('unified') !== -1) {
coerce('hoversort');
}
}

var dragMode = coerce('dragmode');
Expand Down
31 changes: 31 additions & 0 deletions test/jasmine/tests/fx_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,37 @@ describe('Fx defaults', function() {
}
});
});

it('should coerce hoversort only for unified hovermode', function() {
var out = _supply([{type: 'bar', y: [1, 2, 3]}], {
hovermode: 'x unified',
hoversort: 'value descending'
});
expect(out.layout.hoversort).toBe('value descending');
});

it('should not coerce hoversort for non-unified hovermode', function() {
var out = _supply([{type: 'bar', y: [1, 2, 3]}], {
hovermode: 'closest',
hoversort: 'value descending'
});
expect(out.layout.hoversort).toBeUndefined();
});

it('should default hoversort to trace', function() {
var out = _supply([{type: 'bar', y: [1, 2, 3]}], {
hovermode: 'x unified'
});
expect(out.layout.hoversort).toBe('trace');
});

it('should coerce hoversort for y unified hovermode', function() {
var out = _supply([{type: 'bar', y: [1, 2, 3]}], {
hovermode: 'y unified',
hoversort: 'value ascending'
});
expect(out.layout.hoversort).toBe('value ascending');
});
Comment thread
emilykl marked this conversation as resolved.
Outdated
});

describe('relayout', function() {
Expand Down
225 changes: 225 additions & 0 deletions test/jasmine/tests/hover_label_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7315,3 +7315,228 @@ describe('hoverlabel.showarrow', function() {
.then(done, done.fail);
});
});

describe('hoversort', function() {
var gd;

beforeEach(function() {
gd = createGraphDiv();
});

afterEach(destroyGraphDiv);

function _hover(gd, opts) {
Fx.hover(gd, opts);
Lib.clearThrottle();
}

function getHoverLabel() {
var hoverLayer = d3Select('g.hoverlayer');
return hoverLayer.select('g.legend');
}

function assertLabel(expectation) {
var hover = getHoverLabel();
var title = hover.select('text.legendtitletext');
var traces = hover.selectAll('g.traces');

if(expectation.title) {
expect(title.text()).toBe(expectation.title);
}

expect(traces.size()).toBe(expectation.items.length, 'has the incorrect number of items');
traces.each(function(_, i) {
var e = d3Select(this);
expect(e.select('text').text()).toBe(expectation.items[i]);
});
}

it('should sort unified hover items by value descending', function(done) {
Plotly.newPlot(gd, {
data: [
{name: 'small', y: [1, 2, 3]},
{name: 'large', y: [10, 20, 30]},
{name: 'medium', y: [5, 10, 15]}
],
layout: {
hovermode: 'x unified',
hoversort: 'value descending',
showlegend: false,
width: 500,
height: 500
}
})
.then(function() {
_hover(gd, { xval: 2 });
assertLabel({title: '2', items: [
'large : 30',
'medium : 15',
'small : 3'
]});
})
.then(done, done.fail);
});

it('should sort unified hover items by value ascending', function(done) {
Plotly.newPlot(gd, {
data: [
{name: 'small', y: [1, 2, 3]},
{name: 'large', y: [10, 20, 30]},
{name: 'medium', y: [5, 10, 15]}
],
layout: {
hovermode: 'x unified',
hoversort: 'value ascending',
showlegend: false,
width: 500,
height: 500
}
})
.then(function() {
_hover(gd, { xval: 2 });
assertLabel({title: '2', items: [
'small : 3',
'medium : 15',
'large : 30'
]});
})
.then(done, done.fail);
});

it('should default to trace index order', function(done) {
Plotly.newPlot(gd, {
data: [
{name: 'small', y: [1, 2, 3]},
{name: 'large', y: [10, 20, 30]},
{name: 'medium', y: [5, 10, 15]}
],
layout: {
hovermode: 'x unified',
showlegend: false,
width: 500,
height: 500
}
})
.then(function() {
_hover(gd, { xval: 2 });
assertLabel({title: '2', items: [
'small : 3',
'large : 30',
'medium : 15'
]});
})
.then(done, done.fail);
});

it('should sort by value descending with bar charts', function(done) {
Plotly.newPlot(gd, {
data: [
{name: 'A', type: 'bar', y: [5, 10, 15]},
{name: 'B', type: 'bar', y: [20, 25, 30]},
{name: 'C', type: 'bar', y: [1, 2, 3]}
],
layout: {
hovermode: 'x unified',
hoversort: 'value descending',
showlegend: false,
width: 500,
height: 500
}
})
.then(function() {
_hover(gd, { xval: 1 });
assertLabel({title: '1', items: [
'B : 25',
'A : 10',
'C : 2'
]});
})
.then(done, done.fail);
});

it('should sort by value descending with y unified hovermode', function(done) {
Plotly.newPlot(gd, {
data: [
{name: 'first', x: [1, 10, 5]},
{name: 'second', x: [20, 2, 15]},
{name: 'third', x: [8, 8, 8]}
],
layout: {
hovermode: 'y unified',
hoversort: 'value descending',
showlegend: false,
width: 500,
height: 500
}
})
.then(function() {
_hover(gd, { yval: 0 });
assertLabel({title: '0', items: [
'second : 20',
'third : 8',
'first : 1'
]});
})
.then(done, done.fail);
});

it('should fall back to trace index when values are equal', function(done) {
Plotly.newPlot(gd, {
data: [
{name: 'A', y: [5, 10, 10]},
{name: 'B', y: [5, 20, 10]},
{name: 'C', y: [5, 5, 10]}
],
layout: {
hovermode: 'x unified',
hoversort: 'value descending',
showlegend: false,
width: 500,
height: 500
}
})
.then(function() {
// At xval=0, all values are 5 - should keep trace order
_hover(gd, { xval: 0 });
assertLabel({title: '0', items: [
'A : 5',
'B : 5',
'C : 5'
]});
})
.then(done, done.fail);
});

it('should dynamically update sort via relayout', function(done) {
Plotly.newPlot(gd, {
data: [
{name: 'low', y: [1, 2, 3]},
{name: 'high', y: [10, 20, 30]}
],
layout: {
hovermode: 'x unified',
hoversort: 'trace',
showlegend: false,
width: 500,
height: 500
}
})
.then(function() {
_hover(gd, { xval: 1 });
assertLabel({title: '1', items: [
'low : 2',
'high : 20'
]});

return Plotly.relayout(gd, 'hoversort', 'value descending');
})
.then(function() {
_hover(gd, { xval: 1 });
assertLabel({title: '1', items: [
'high : 20',
'low : 2'
]});
})
.then(done, done.fail);
});
});
16 changes: 16 additions & 0 deletions test/plot-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@
false
]
},
"displayNotifier": {
"description": "Determines whether or not notifier is displayed.",
"dflt": true,
"valType": "boolean"
},
"doubleClick": {
"description": "Sets the double click interaction mode. Has an effect only in cartesian plots. If *false*, double click is disable. If *reset*, double click resets the axis ranges to their initial values. If *autosize*, double click set the axis ranges to their autorange values. If *reset+autosize*, the odd double clicks resets the axis ranges to their initial values and even double clicks set the axis ranges to their autorange values.",
"dflt": "reset+autosize",
Expand Down Expand Up @@ -3058,6 +3063,17 @@
"y unified"
]
},
"hoversort": {
"description": "Determines the order of items shown in unified hover labels. If *trace*, items are sorted by trace index. If *value descending*, items are sorted by value from largest to smallest. If *value ascending*, items are sorted by value from smallest to largest. Only applies when `hovermode` is *x unified* or *y unified*.",
"dflt": "trace",
"editType": "none",
"valType": "enumerated",
"values": [
"trace",
"value descending",
"value ascending"
]
},
"hoversubplots": {
"description": "Determines expansion of hover effects to other subplots If *single* just the axis pair of the primary point is included without overlaying subplots. If *overlaying* all subplots using the main axis and occupying the same space are included. If *axis*, also include stacked subplots using the same axis when `hovermode` is set to *x*, *x unified*, *y* or *y unified*.",
"dflt": "overlaying",
Expand Down