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<br>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: '',