Skip to content
Open
Show file tree
Hide file tree
Changes from all 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');
});
});

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