diff --git a/src/components/legend/get_legend_data.js b/src/components/legend/get_legend_data.js index 7e09fa32337..f5a7b19fbbb 100644 --- a/src/components/legend/get_legend_data.js +++ b/src/components/legend/get_legend_data.js @@ -4,6 +4,9 @@ var Registry = require('../../registry'); var helpers = require('./helpers'); module.exports = function getLegendData(calcdata, opts) { + var grouped = helpers.isGrouped(opts); + var reversed = helpers.isReversed(opts); + var lgroupToTraces = {}; var lgroups = []; var hasOneNonBlankGroup = false; @@ -18,14 +21,14 @@ module.exports = function getLegendData(calcdata, opts) { // TODO: check this against fullData legendgroups? var uniqueGroup = '~~i' + lgroupi; lgroups.push(uniqueGroup); - lgroupToTraces[uniqueGroup] = [[legendItem]]; + lgroupToTraces[uniqueGroup] = [legendItem]; lgroupi++; } else if(lgroups.indexOf(legendGroup) === -1) { lgroups.push(legendGroup); hasOneNonBlankGroup = true; - lgroupToTraces[legendGroup] = [[legendItem]]; + lgroupToTraces[legendGroup] = [legendItem]; } else { - lgroupToTraces[legendGroup].push([legendItem]); + lgroupToTraces[legendGroup].push(legendItem); } } @@ -66,31 +69,66 @@ module.exports = function getLegendData(calcdata, opts) { // won't draw a legend in this case if(!lgroups.length) return []; - // rearrange lgroupToTraces into a d3-friendly array of arrays - var lgroupsLength = lgroups.length; - var ltraces; - var legendData; - - if(hasOneNonBlankGroup && helpers.isGrouped(opts)) { - legendData = new Array(lgroupsLength); + // collapse all groups into one if all groups are blank + var shouldCollapse = !hasOneNonBlankGroup || !grouped; - for(i = 0; i < lgroupsLength; i++) { - ltraces = lgroupToTraces[lgroups[i]]; - legendData[i] = helpers.isReversed(opts) ? ltraces.reverse() : ltraces; + var legendData = []; + for(i = 0; i < lgroups.length; i++) { + var t = lgroupToTraces[lgroups[i]]; + if(shouldCollapse) { + legendData.push(t[0]); + } else { + legendData.push(t); } - } else { - // collapse all groups into one if all groups are blank - legendData = [new Array(lgroupsLength)]; + } + if(shouldCollapse) legendData = [legendData]; + + for(i = 0; i < legendData.length; i++) { + // find minimum rank within group + var groupMinRank = Infinity; + for(j = 0; j < legendData[i].length; j++) { + var rank = legendData[i][j].trace.legendrank; + if(groupMinRank > rank) groupMinRank = rank; + } + + // record on first group element + legendData[i][0]._groupMinRank = groupMinRank; + legendData[i][0]._preGroupSort = i; + } - for(i = 0; i < lgroupsLength; i++) { - ltraces = lgroupToTraces[lgroups[i]][0]; - legendData[0][helpers.isReversed(opts) ? lgroupsLength - i - 1 : i] = ltraces; + var orderFn1 = function(a, b) { + return ( + (a[0]._groupMinRank - b[0]._groupMinRank) || + (a[0]._preGroupSort - b[0]._preGroupSort) // fallback for old Chrome < 70 https://bugs.chromium.org/p/v8/issues/detail?id=90 + ); + }; + + var orderFn2 = function(a, b) { + return ( + (a.trace.legendrank - b.trace.legendrank) || + (a._preSort - b._preSort) // fallback for old Chrome < 70 https://bugs.chromium.org/p/v8/issues/detail?id=90 + ); + }; + + // sort considering minimum group legendrank + legendData.forEach(function(a, k) { a[0]._preGroupSort = k; }); + legendData.sort(orderFn1); + for(i = 0; i < legendData.length; i++) { + // sort considering trace.legendrank and legend.traceorder + legendData[i].forEach(function(a, k) { a._preSort = k; }); + legendData[i].sort(orderFn2); + if(reversed) legendData[i].reverse(); + + // rearrange lgroupToTraces into a d3-friendly array of arrays + for(j = 0; j < legendData[i].length; j++) { + legendData[i][j] = [ + legendData[i][j] + ]; } - lgroupsLength = 1; } // number of legend groups - needed in legend/draw.js - opts._lgroupsLength = lgroupsLength; + opts._lgroupsLength = legendData.length; // maximum name/label length - needed in legend/draw.js opts._maxNameLength = maxNameLength; diff --git a/src/plots/attributes.js b/src/plots/attributes.js index 20ff1c2a3b3..c1a9aa456a9 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -41,6 +41,19 @@ module.exports = { 'when toggling legend items.' ].join(' ') }, + legendrank: { + valType: 'number', + dflt: 1000, + editType: 'style', + description: [ + 'Sets the legend rank for this trace.', + 'Items and groups with smaller ranks are presented on top/left side while', + 'with `*reversed* `legend.traceorder` they are on bottom/right side.', + 'The default legendrank is 1000,', + 'so that you can use ranks less than 1000 to place certain items before all unranked items,', + 'and ranks greater than 1000 to go after all unranked items.' + ].join(' ') + }, opacity: { valType: 'number', min: 0, diff --git a/src/plots/plots.js b/src/plots/plots.js index a3789026460..dd22614ccae 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1309,6 +1309,7 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac ); coerce('legendgroup'); + coerce('legendrank'); traceOut._dfltShowLegend = true; } else { diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index 0434074ad7a..cb0ddb4d912 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -198,6 +198,7 @@ module.exports = { hoverlabel: undefined, ids: undefined, legendgroup: undefined, + legendrank: undefined, opacity: undefined, selectedpoints: undefined, showlegend: undefined diff --git a/test/image/baselines/legendrank.png b/test/image/baselines/legendrank.png new file mode 100644 index 00000000000..2f5d879126e Binary files /dev/null and b/test/image/baselines/legendrank.png differ diff --git a/test/image/baselines/legendrank2.png b/test/image/baselines/legendrank2.png new file mode 100644 index 00000000000..35261255629 Binary files /dev/null and b/test/image/baselines/legendrank2.png differ diff --git a/test/image/mocks/legendrank.json b/test/image/mocks/legendrank.json new file mode 100644 index 00000000000..182b9c7afae --- /dev/null +++ b/test/image/mocks/legendrank.json @@ -0,0 +1,51 @@ +{ + "data": [ + {"type": "bar", "name": "1", "y": [1], "yaxis": "y2", "legendgroup": "two", "legendrank": 3}, + { + "legendrank": 2, + "legendgroup": "pie", + "type": "pie", + "labels": ["a","b","c","c","c","a"], + "textinfo": "none", + "domain": { + "x": [0, 0.45], + "y": [0.35, 0.65] + } + }, + { + "legendrank": 1, + "legendgroup": "pie", + "type": "pie", + "labels": ["z","x","x","x","y", "y"], + "sort": false, + "textinfo": "none", + "domain": { + "x": [0.55, 1], + "y": [0.35, 0.65] + } + }, + {"type": "scatter", "name": "2", "y": [2], "yaxis": "y", "legendgroup": "one", "legendrank": 2}, + {"type": "scatter", "name": "1", "y": [1], "yaxis": "y", "legendgroup": "one", "legendrank": 1}, + {"type": "bar", "name": "2", "y": [2], "yaxis": "y2", "legendgroup": "two", "legendrank": 2}, + {"type": "scatter", "name": "3", "y": [3], "yaxis": "y", "legendgroup": "one", "legendrank": 3}, + {"type": "bar", "name": "3", "y": [3], "yaxis": "y2", "legendgroup": "two", "legendrank": 1} + ], + "layout": { + "title": { + "text": "legendrank" + }, + "hovermode": "x unified", + "margin": { + "t": 50 + }, + "width": 300, + "height": 400, + "yaxis2": { + "domain": [0.7, 1] + }, + "yaxis": { + "autorange": "reversed", + "domain": [0, 0.3] + } + } +} diff --git a/test/image/mocks/legendrank2.json b/test/image/mocks/legendrank2.json new file mode 100644 index 00000000000..5cb28a72483 --- /dev/null +++ b/test/image/mocks/legendrank2.json @@ -0,0 +1,44 @@ +{ + "data": [ + { + "name": "A", + "legendrank": 2, + "y": [-2] + }, + { + "name": "D", + "legendrank": 4, + "y": [-4], + "legendgroup": "bottom" + }, + { + "name": "E", + "legendrank": 4, + "y": [-4], + "legendgroup": "bottom" + }, + { + "name": "B", + "legendrank": 1, + "y": [-1], + "legendgroup": "top" + }, + { + "name": "C", + "legendrank": 3, + "y": [-3], + "legendgroup": "top" + } + ], + "layout": { + "title": { + "text": "rank groups using
minimum of the group" + }, + "width": 300, + "height": 300, + "margin": { + "b": 25 + }, + "hovermode": "x unified" + } +} diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index a4599f9e6b8..054d2e36b07 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -248,7 +248,7 @@ describe('legend defaults', function() { }); }); -describe('legend getLegendData', function() { +describe('legend getLegendData user-defined legendrank', function() { 'use strict'; var calcdata, opts, legendData, expected; @@ -256,19 +256,21 @@ describe('legend getLegendData', function() { it('should group legendgroup traces', function() { calcdata = [ [{trace: { + legendrank: 3, type: 'scatter', visible: true, legendgroup: 'group', showlegend: true - }}], [{trace: { + legendrank: 2, type: 'bar', visible: 'legendonly', legendgroup: '', showlegend: true }}], [{trace: { + legendrank: 1, type: 'scatter', visible: true, legendgroup: 'group', @@ -283,14 +285,15 @@ describe('legend getLegendData', function() { expected = [ [ - [{trace: { + [{_preSort: 1, trace: { + legendrank: 1, type: 'scatter', visible: true, legendgroup: 'group', showlegend: true - }}], - [{trace: { + [{_groupMinRank: 1, _preGroupSort: 0, _preSort: 0, trace: { + legendrank: 3, type: 'scatter', visible: true, legendgroup: 'group', @@ -298,7 +301,8 @@ describe('legend getLegendData', function() { }}] ], [ - [{trace: { + [{_groupMinRank: 2, _preGroupSort: 1, _preSort: 0, trace: { + legendrank: 2, type: 'bar', visible: 'legendonly', legendgroup: '', @@ -314,23 +318,242 @@ describe('legend getLegendData', function() { it('should collapse when data has only one group', function() { calcdata = [ [{trace: { + legendrank: 3, + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + }}], + [{trace: { + legendrank: 2, + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + }}], + [{trace: { + legendrank: 1, type: 'scatter', visible: true, legendgroup: '', showlegend: true + }}] + ]; + opts = { + traceorder: 'grouped' + }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [{_preSort: 2, trace: { + legendrank: 1, + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + }}], + [{_preSort: 1, trace: { + legendrank: 2, + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + }}], + [{_groupMinRank: 1, _preGroupSort: 0, _preSort: 0, trace: { + legendrank: 3, + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + }}] + ] + ]; + + expect(legendData).toEqual(expected); + expect(opts._lgroupsLength).toEqual(1); + }); + + it('should return empty array when legend data has no traces', function() { + calcdata = [ + [{trace: { + legendrank: 3, + type: 'histogram', + visible: true, + legendgroup: '', + showlegend: false + }}], + [{trace: { + legendrank: 2, + type: 'box', + visible: 'legendonly', + legendgroup: '', + showlegend: false + }}], + [{trace: { + legendrank: 1, + type: 'heatmap', + visible: true, + legendgroup: '' + }}] + ]; + opts = { + _main: true, + traceorder: 'normal' + }; + legendData = getLegendData(calcdata, opts); + expect(legendData).toEqual([]); + }); + + it('should reverse the order when legend.traceorder is set', function() { + calcdata = [ + [{trace: { + legendrank: 3, + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true }}], [{trace: { + legendrank: 2, type: 'bar', visible: 'legendonly', legendgroup: '', showlegend: true }}], [{trace: { + legendrank: 1, + type: 'box', + visible: true, + legendgroup: '', + showlegend: true + }}] + ]; + opts = { + traceorder: 'reversed' + }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [{_groupMinRank: 1, _preGroupSort: 0, _preSort: 0, trace: { + legendrank: 3, + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + }}], + [{_preSort: 1, trace: { + legendrank: 2, + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + }}], + [{_preSort: 2, trace: { + legendrank: 1, + type: 'box', + visible: true, + legendgroup: '', + showlegend: true + }}] + ] + ]; + + expect(legendData).toEqual(expected); + expect(opts._lgroupsLength).toEqual(1); + }); + + it('should reverse the trace order within groups when reversed+grouped', function() { + calcdata = [ + [{trace: { + legendrank: 3, type: 'scatter', visible: true, + legendgroup: 'group', + showlegend: true + }}], + [{trace: { + legendrank: 2, + type: 'bar', + visible: 'legendonly', legendgroup: '', showlegend: true + }}], + [{trace: { + legendrank: 1, + type: 'box', + visible: true, + legendgroup: 'group', + showlegend: true + }}] + ]; + opts = { + traceorder: 'reversed+grouped' + }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [{_groupMinRank: 1, _preGroupSort: 0, _preSort: 0, trace: { + legendrank: 3, + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + }}], + [{_preSort: 1, trace: { + legendrank: 1, + type: 'box', + visible: true, + legendgroup: 'group', + showlegend: true + }}] + ], + [ + [{_groupMinRank: 2, _preGroupSort: 1, _preSort: 0, trace: { + legendrank: 2, + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + }}] + ] + ]; + + expect(legendData).toEqual(expected); + expect(opts._lgroupsLength).toEqual(2); + }); +}); + +describe('legend getLegendData default legendrank', function() { + 'use strict'; + + var calcdata, opts, legendData, expected; + + it('should group legendgroup traces', function() { + calcdata = [ + [{trace: { + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + }}], + [{trace: { + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + }}], + [{trace: { + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true }}] ]; opts = { @@ -341,20 +564,75 @@ describe('legend getLegendData', function() { expected = [ [ - [{trace: { + [{_groupMinRank: Infinity, _preGroupSort: 0, _preSort: 0, trace: { + type: 'scatter', + visible: true, + legendgroup: 'group', + showlegend: true + }}], + [{_preSort: 1, trace: { type: 'scatter', visible: true, + legendgroup: 'group', + showlegend: true + }}] + ], + [ + [{_groupMinRank: Infinity, _preGroupSort: 1, _preSort: 0, trace: { + type: 'bar', + visible: 'legendonly', legendgroup: '', showlegend: true + }}] + ] + ]; + + expect(legendData).toEqual(expected); + expect(opts._lgroupsLength).toEqual(2); + }); + it('should collapse when data has only one group', function() { + calcdata = [ + [{trace: { + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + }}], + [{trace: { + type: 'bar', + visible: 'legendonly', + legendgroup: '', + showlegend: true + }}], + [{trace: { + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true + }}] + ]; + opts = { + traceorder: 'grouped' + }; + + legendData = getLegendData(calcdata, opts); + + expected = [ + [ + [{_groupMinRank: Infinity, _preGroupSort: 0, _preSort: 0, trace: { + type: 'scatter', + visible: true, + legendgroup: '', + showlegend: true }}], - [{trace: { + [{_preSort: 1, trace: { type: 'bar', visible: 'legendonly', legendgroup: '', showlegend: true }}], - [{trace: { + [{_preSort: 2, trace: { type: 'scatter', visible: true, legendgroup: '', @@ -374,7 +652,6 @@ describe('legend getLegendData', function() { visible: true, legendgroup: '', showlegend: false - }}], [{trace: { type: 'box', @@ -404,7 +681,6 @@ describe('legend getLegendData', function() { visible: true, legendgroup: '', showlegend: true - }}], [{trace: { type: 'bar', @@ -427,20 +703,19 @@ describe('legend getLegendData', function() { expected = [ [ - [{trace: { + [{_preSort: 2, trace: { type: 'box', visible: true, legendgroup: '', showlegend: true - }}], - [{trace: { + [{_preSort: 1, trace: { type: 'bar', visible: 'legendonly', legendgroup: '', showlegend: true }}], - [{trace: { + [{_groupMinRank: Infinity, _preGroupSort: 0, _preSort: 0, trace: { type: 'scatter', visible: true, legendgroup: '', @@ -460,7 +735,6 @@ describe('legend getLegendData', function() { visible: true, legendgroup: 'group', showlegend: true - }}], [{trace: { type: 'bar', @@ -483,14 +757,13 @@ describe('legend getLegendData', function() { expected = [ [ - [{trace: { + [{_preSort: 1, trace: { type: 'box', visible: true, legendgroup: 'group', showlegend: true - }}], - [{trace: { + [{_groupMinRank: Infinity, _preGroupSort: 0, _preSort: 0, trace: { type: 'scatter', visible: true, legendgroup: 'group', @@ -498,7 +771,7 @@ describe('legend getLegendData', function() { }}] ], [ - [{trace: { + [{_groupMinRank: Infinity, _preGroupSort: 1, _preSort: 0, trace: { type: 'bar', visible: 'legendonly', legendgroup: '',