diff --git a/src/lib/sort_traces.js b/src/lib/sort_traces.js
new file mode 100644
index 00000000000..d3086781deb
--- /dev/null
+++ b/src/lib/sort_traces.js
@@ -0,0 +1,144 @@
+'use strict';
+
+function zipArrays(arrays) {
+    var zipped = [];
+    arrays[0].forEach(function(e, i) {
+        var row = [];
+        arrays.forEach(function(arr) {
+            row.push(arr[i]);
+        });
+        zipped.push(row);
+    });
+    return zipped;
+}
+
+function sortObjecstByKey(a, b, key) {
+    if(a[key] === b[key]) return 0;
+    if(a[key] < b[key]) return -1;
+    return 1;
+}
+
+function matrixToObjectList(matrix, cols) {
+    var zipped = zipArrays(matrix);
+
+    var objList = [];
+
+    zipped.forEach(function(row) {
+        var objRow = {};
+        cols.forEach(function(col, idx) {
+            objRow[col] = row[idx];
+        });
+        objRow.y = row[row.length - 1];
+        objList.push(objRow);
+    });
+    return objList;
+}
+
+exports.matrixToObjectList = matrixToObjectList;
+
+function sortObjectList(cols, objList) {
+    var sortedObjectList = objList.map(function(e) {
+        return e;
+    });
+    cols.slice().reverse().forEach(function(key) {
+        sortedObjectList = sortedObjectList.sort(function(a, b) {
+            return sortObjecstByKey(a, b, key);
+        });
+    });
+    return sortedObjectList;
+}
+
+exports.sortObjectList = sortObjectList;
+
+function objectListToList(objectList) {
+    var list = [];
+    objectList.forEach(function(item) {
+        list.push(Object.values(item));
+    });
+    return list;
+}
+
+exports.objectListToList = objectListToList;
+
+function sortedMatrix(list, removeNull) {
+    var xs = [];
+    var y = [];
+
+    list.slice().forEach(function(item) {
+        var val = item.pop();
+
+        if(removeNull & item.includes(null)) {
+            return;
+        }
+
+        y.push(val);
+        xs.push(item);
+    });
+
+    return [xs, y];
+}
+
+exports.sortedMatrix = sortedMatrix;
+
+function squareMatrix(matrix) {
+    var width = matrix[0].length;
+    var height = matrix.length;
+
+    if(width === height) {
+        return matrix;
+    }
+
+    var newMatrix = [];
+
+    if(width > height) {
+        for(var rw = 0; rw < height; rw++) {
+            newMatrix.push(matrix[rw].slice());
+        }
+        for(var i = height; i < width; i++) {
+            newMatrix.push(Array(width));
+        }
+    } else {
+        for(var row = 0; row < height; row++) {
+            var rowExpansion = Array(height - width);
+            var rowSlice = matrix[row].slice();
+            Array.prototype.push.apply(rowSlice, rowExpansion);
+            newMatrix.push(rowSlice);
+        }
+    }
+    return newMatrix;
+}
+
+exports.squareMatrix = squareMatrix;
+
+function transpose(matrix) {
+    var height = matrix.length;
+    var width = matrix[0].length;
+
+    var squaredMatrix = squareMatrix(matrix);
+
+    var newMatrix = [];
+
+    // prevent inplace change and mantain the main diagonal
+    for(var rw = 0; rw < squaredMatrix.length; rw++) {
+        newMatrix.push(squaredMatrix[rw].slice());
+    }
+
+    for(var i = 0; i < newMatrix.length; i++) {
+        for(var j = 0; j < i; j++) {
+            newMatrix = newMatrix.slice();
+            var temp = newMatrix[i][j];
+            newMatrix[i][j] = newMatrix[j][i];
+            newMatrix[j][i] = temp;
+        }
+    }
+    if(width > height) {
+        for(var row = 0; row < newMatrix.length; row++) {
+            newMatrix[row] = newMatrix[row].slice(0, height);
+        }
+    } else {
+        newMatrix = newMatrix.slice(0, width);
+    }
+    return newMatrix;
+}
+
+exports.transpose = transpose;
\ No newline at end of file
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index 1386cb8418c..facd8c217d6 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -1880,19 +1880,36 @@ function formatCategory(ax, out) {
 }
 
 function formatMultiCategory(ax, out, hover) {
-    var v = Math.round(out.x);
-    var cats = ax._categories[v] || [];
-    var tt = cats[1] === undefined ? '' : String(cats[1]);
-    var tt2 = cats[0] === undefined ? '' : String(cats[0]);
+  var v = Math.round(out.x);
+  var cats =
+    ax._categories[v].map(function (cat) {
+      return cat;
+    }) || [];
+  var texts = cats
+    .slice()
+    .reverse()
+    .map(function (cat) {
+      return cat === undefined ? "" : String(cat);
+    });
 
-    if(hover) {
-        // TODO is this what we want?
-        out.text = tt2 + ' - ' + tt;
-    } else {
-        // setup for secondary labels
-        out.text = tt;
-        out.text2 = tt2;
-    }
+  if (hover) {
+    // TODO is this what we want?
+    var hoverText = "";
+    cats.forEach(function (text, index) {
+      text = String(text);
+      if (index < texts.length - 1) {
+        hoverText = hoverText + " " + text + " - ";
+      } else {
+        hoverText = hoverText + " " + text;
+      }
+    });
+
+    out.text = hoverText;
+  } else {
+    // setup for secondary labels
+    out.text = texts[0];
+    out.texts = texts;
+  }
 }
 
 function formatLinear(ax, out, hover, extraPrecision, hideexp) {
@@ -2616,40 +2633,72 @@ axes.drawOne = function(gd, ax, opts) {
         });
     });
 
-    if(ax.type === 'multicategory') {
-        var pad = {x: 2, y: 10}[axLetter];
-
-        seq.push(function() {
-            var bboxKey = {x: 'height', y: 'width'}[axLetter];
-            var standoff = getLabelLevelBbox()[bboxKey] + pad +
-                (ax._tickAngles[axId + 'tick'] ? ax.tickfont.size * LINE_SPACING : 0);
-
-            return axes.drawLabels(gd, ax, {
-                vals: getSecondaryLabelVals(ax, vals),
-                layer: mainAxLayer,
-                cls: axId + 'tick2',
-                repositionOnUpdate: true,
-                secondary: true,
-                transFn: transTickFn,
-                labelFns: axes.makeLabelFns(ax, mainLinePositionShift + standoff * majorTickSigns[4])
-            });
+  var tickNames = ["tick"];
+
+  if (ax.type === "multicategory") {
+    ax.levels
+      .slice()
+      .reverse()
+      .slice(0, ax.levelNr - 1)
+      .forEach(function (_lvl) {
+        var pad = { x: 0 * _lvl, y: 10 }[axLetter];
+
+        var tickName = "tick" + String(_lvl);
+        tickNames.push(tickName);
+
+        seq.push(function () {
+          var bboxKey = { x: "height", y: "width" }[axLetter];
+          var standoff =
+            _lvl * getLabelLevelBbox()[bboxKey] +
+            pad +
+            (ax._tickAngles[axId + "tick"]
+              ? ax.tickfont.size * LINE_SPACING
+              : 0);
+
+          return axes.drawLabels(gd, ax, {
+            vals: getSecondaryLabelVals(ax, vals, _lvl),
+            layer: mainAxLayer,
+            cls: axId + tickName,
+            repositionOnUpdate: true,
+            secondary: true,
+            transFn: transTickFn,
+            labelFns: axes.makeLabelFns(
+              ax,
+              mainLinePosition + standoff * majorTickSigns[4]
+            ),
+          });
         });
+      });
 
-        seq.push(function() {
-            ax._depth = majorTickSigns[4] * (getLabelLevelBbox('tick2')[ax.side] - mainLinePositionShift);
+    tickNames = tickNames.sort();
 
-            return drawDividers(gd, ax, {
-                vals: dividerVals,
-                layer: mainAxLayer,
-                path: axes.makeTickPath(ax, mainLinePositionShift, majorTickSigns[4], { len: ax._depth }),
-                transFn: transTickFn
-            });
+    ax.levels.forEach(function (_lvl, idx) {
+      seq.push(function () {
+        ax._depth =
+          majorTickSigns[4] *
+          (getLabelLevelBbox(tickNames.slice()[_lvl])[ax.side] -
+            mainLinePosition);
+
+        var levelDividers = dividerVals.slice().filter(function (divider) {
+          return divider.level === idx;
         });
-    } else if(ax.title.hasOwnProperty('standoff')) {
-        seq.push(function() {
-            ax._depth = majorTickSigns[4] * (getLabelLevelBbox()[ax.side] - mainLinePositionShift);
+
+        return drawDividers(gd, ax, {
+          vals: levelDividers,
+          layer: mainAxLayer,
+          path: axes.makeTickPath(ax, mainLinePosition, majorTickSigns[4], {
+            len: ax._depth,
+          }),
+          transFn: transTickFn,
+          level: _lvl,
         });
-    }
+      });
+    });
+} else if(ax.title.hasOwnProperty('standoff')) {
+    seq.push(function() {
+        ax._depth = majorTickSigns[4] * (getLabelLevelBbox()[ax.side] - mainLinePositionShift);
+    });
+}
 
     var hasRangeSlider = Registry.getComponentMethod('rangeslider', 'isVisible')(ax);
 
@@ -2824,55 +2873,76 @@ function getBoundaryVals(ax, vals) {
     return out;
 }
 
-function getSecondaryLabelVals(ax, vals) {
-    var out = [];
-    var lookup = {};
-
-    for(var i = 0; i < vals.length; i++) {
-        var d = vals[i];
-        if(lookup[d.text2]) {
-            lookup[d.text2].push(d.x);
-        } else {
-            lookup[d.text2] = [d.x];
-        }
+function getSecondaryLabelVals(ax, vals, level) {
+  var out = [];
+  var lookup = {};
+  var appearences = {};
+  var current;
+  var currentParent = null;
+  var parent = null;
+
+  for (var i = 0; i < vals.length; i++) {
+    var d = vals[i];
+    var text = d.texts[level];
+    parent = d.texts[level + 1];
+    if (lookup[text]) {
+      if ((d.texts[level] === current) & (parent === currentParent)) {
+        lookup[text][appearences[text]].push(d.x);
+      } else {
+        appearences[text] = appearences[text] + 1;
+        lookup[text].push([d.x]);
+      }
+    } else {
+      appearences[text] = 0;
+      lookup[text] = [[d.x]];
     }
+    current = d.texts[level];
+    currentParent = d.texts[level + 1];
+  }
 
-    for(var k in lookup) {
-        out.push(tickTextObj(ax, Lib.interp(lookup[k], 0.5), k));
-    }
+  Object.keys(lookup).forEach(function (key) {
+    lookup[key].forEach(function (pos) {
+      out.push(tickTextObj(ax, Lib.interp(pos, 0.5), key));
+    });
+  });
 
-    return out;
+  return out;
 }
 
 function getDividerVals(ax, vals) {
     var out = [];
     var i, current;
-
-    var reversed = (vals.length && vals[vals.length - 1].x < vals[0].x);
-
+  
+    var reversed = vals.length && vals[vals.length - 1].x < vals[0].x;
+  
     // never used for labels;
     // no need to worry about the other tickTextObj keys
-    var _push = function(d, bndIndex) {
-        var xb = d.xbnd[bndIndex];
-        if(xb !== null) {
-            out.push(Lib.extendFlat({}, d, {x: xb}));
-        }
+    var _push = function (d, bndIndex, level) {
+      var xb = d.xbnd[bndIndex];
+      if (xb !== null) {
+        var _out = Lib.extendFlat({}, d, { x: xb });
+        _out.level = level;
+        out.push(_out);
+      }
     };
-
-    if(ax.showdividers && vals.length) {
-        for(i = 0; i < vals.length; i++) {
-            var d = vals[i];
-            if(d.text2 !== current) {
-                _push(d, reversed ? 1 : 0);
-            }
-            current = d.text2;
+  
+    if (ax.showdividers && vals.length) {
+      ax.levels.forEach(function (_lvl) {
+        current = undefined;
+        for (i = 0; i < vals.length; i++) {
+          var d = vals[i];
+          if (d.texts[_lvl] !== current) {
+            _push(d, reversed ? 1 : 0, _lvl);
+          }
+          current = d.texts[_lvl];
+          // text2
         }
         _push(vals[i - 1], reversed ? 0 : 1);
+      });
     }
-
     return out;
-}
-
+  }
+  
 function calcLabelLevelBbox(ax, cls, mainLinePositionShift) {
     var top, bottom;
     var left, right;
@@ -3987,24 +4057,30 @@ axes.drawLabels = function(gd, ax, opts) {
  * - {fn} transFn
  */
 function drawDividers(gd, ax, opts) {
-    var cls = ax._id + 'divider';
+    var cls = ax._id + "divider";
     var vals = opts.vals;
-
-    var dividers = opts.layer.selectAll('path.' + cls)
-        .data(vals, tickDataFn);
-
-    dividers.exit().remove();
-
-    dividers.enter().insert('path', ':first-child')
-        .classed(cls, 1)
-        .classed('crisp', 1)
-        .call(Color.stroke, ax.dividercolor)
-        .style('stroke-width', Drawing.crispRound(gd, ax.dividerwidth, 1) + 'px');
-
+  
+    var dividers = opts.layer.selectAll("path." + cls).data(vals, tickDataFn);
+  
+    if (ax.type === "multicategory") {
+      if (opts.level === 0) {
+        dividers.exit().remove();
+      }
+    } else {
+      dividers.exit().remove();
+    }
+  
     dividers
-        .attr('transform', opts.transFn)
-        .attr('d', opts.path);
-}
+      .enter()
+      .insert("path", ":first-child")
+      .classed(cls, 1)
+      .classed("crisp", 1)
+      .call(Color.stroke, ax.dividercolor)
+      .style("stroke-width", Drawing.crispRound(gd, ax.dividerwidth, 1) + "px");
+  
+    dividers.attr("transform", opts.transFn).attr("d", opts.path);
+  }
+  
 
 /**
  * Get axis position in px, that is the distance for the graph's
diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js
index 14f17f9c2aa..3bf0425f493 100644
--- a/src/plots/cartesian/set_convert.js
+++ b/src/plots/cartesian/set_convert.js
@@ -156,9 +156,11 @@ module.exports = function setConvert(ax, fullLayout) {
         var arrayOut = new Array(len);
 
         for(var i = 0; i < len; i++) {
-            var v0 = (arrayIn[0] || [])[i];
-            var v1 = (arrayIn[1] || [])[i];
-            arrayOut[i] = getCategoryIndex([v0, v1]);
+            var vs = [];
+            for(var j = 0; j < ax.levelNr; j++) {
+                vs.push((arrayIn[j] || [])[i]);
+            }
+            arrayOut[i] = getCategoryIndex(vs);
         }
 
         return arrayOut;
@@ -333,6 +335,7 @@ module.exports = function setConvert(ax, fullLayout) {
         // N.B. multicategory axes don't define d2c and d2l,
         // as 'data-to-calcdata' conversion needs to take into
         // account all data array items as in ax.makeCalcdata.
+        var sortLib = require('../../lib/sort_traces');
 
         ax.r2d = ax.c2d = ax.l2d = getCategoryName;
         ax.d2r = ax.d2l_noadd = getCategoryPosition;
@@ -357,7 +360,13 @@ module.exports = function setConvert(ax, fullLayout) {
             return ensureNumber(v);
         };
 
-        ax.setupMultiCategory = function(fullData) {
+        ax.setupMultiCategory = function(gd) {
+            var fullData = gd._fullData;
+            // axes_test should set up category maps correctly for multicategory axes
+            if(!fullData) {
+                fullData = gd;
+            }
+
             var traceIndices = ax._traceIndices;
             var i, j;
 
@@ -371,49 +380,98 @@ module.exports = function setConvert(ax, fullLayout) {
                 }
             }
 
-            // [ [cnt, {$cat: index}], for 1,2 ]
-            var seen = [[0, {}], [0, {}]];
-            // [ [arrayIn[0][i], arrayIn[1][i]], for i .. N ]
-            var list = [];
+            var axLabels = [];
+            var fullObjectList = [];
+            var cols = [];
 
             for(i = 0; i < traceIndices.length; i++) {
                 var trace = fullData[traceIndices[i]];
+                cols = [];
+
+                for(var k = 0; k < fullData[traceIndices[0]][axLetter].length; k++) {
+                    cols.push('col' + k.toString());
+                }
+                if(cols.length < 2) {
+                    return;
+                }
 
                 if(axLetter in trace) {
                     var arrayIn = trace[axLetter];
-                    var len = trace._length || Lib.minRowLength(arrayIn);
-
-                    if(isArrayOrTypedArray(arrayIn[0]) && isArrayOrTypedArray(arrayIn[1])) {
-                        for(j = 0; j < len; j++) {
-                            var v0 = arrayIn[0][j];
-                            var v1 = arrayIn[1][j];
-
-                            if(isValidCategory(v0) && isValidCategory(v1)) {
-                                list.push([v0, v1]);
-
-                                if(!(v0 in seen[0][1])) {
-                                    seen[0][1][v0] = seen[0][0]++;
-                                }
-                                if(!(v1 in seen[1][1])) {
-                                    seen[1][1][v1] = seen[1][0]++;
-                                }
+                    if(isArrayOrTypedArray(arrayIn[0])) {
+                        var arrays = arrayIn.map(function(x) {
+                            return x;
+                        });
+                        var valLetter;
+                        if(trace.type === 'ohlc' | trace.type === 'candlestick') {
+                            var t = trace;
+                            var valsTransform = sortLib.transpose([t.open, t.high, t.low, t.close]);
+                            arrays.push(valsTransform);
+                        } else if(trace.z) {
+                            if(axLetter === 'x') {
+                                arrays.push(sortLib.transpose(trace.z));
+                            } else {
+                                arrays.push(trace.z);
                             }
+                            valLetter = 'z';
+                        } else if(axLetter === 'y' && trace.x) {
+                            arrays.push(trace.x);
+                            valLetter = 'x';
+                        } else if(trace.y) {
+                            arrays.push(trace.y);
+                            valLetter = 'y';
+                        } else {
+                            var nullArray = arrayIn[0].map(function() {return null;});
+                            arrays.push(nullArray);
+                        }
+                        var objList = sortLib.matrixToObjectList(arrays, cols);
+
+                        Array.prototype.push.apply(fullObjectList, objList);
+
+                        // convert the trace data from list to object and sort (backwards, stable sort)
+                        var sortedObjectList = sortLib.sortObjectList(cols, objList);
+                        var matrix = sortLib.objectListToList(sortedObjectList);
+                        var sortedMatrix = sortLib.sortedMatrix(matrix);
+
+                        axLabels = sortedMatrix[0].slice();
+                        var axVals = sortedMatrix[1];
+
+                        if(valLetter === 'z' & axLetter === 'x') {
+                            axVals = sortLib.transpose(axVals);
+                        }
+
+                        if(trace.type === 'ohlc' | trace.type === 'candlestick') {
+                            var sortedValsTransform = sortLib.transpose(axVals);
+                            gd._fullData[i].open = sortedValsTransform[0];
+                            gd._fullData[i].high = sortedValsTransform[1];
+                            gd._fullData[i].low = sortedValsTransform[2];
+                            gd._fullData[i].close = sortedValsTransform[3];
+                        }
+                        // Could/should set sorted y axis values for each trace as the sorted values are already available.
+                        // Need write access to gd._fullData, bad? Should probably be done right at newPlot, or on setting gd._fullData
+
+                        var transposedAxLabels = sortLib.transpose(axLabels);
+                        if(gd._fullData) {
+                            gd._fullData[i][axLetter] = transposedAxLabels;
+                        }
+                        if(valLetter) {
+                            gd._fullData[i][valLetter] = axVals;
                         }
                     }
                 }
             }
 
-            list.sort(function(a, b) {
-                var ind0 = seen[0][1];
-                var d = ind0[a[0]] - ind0[b[0]];
-                if(d) return d;
+            if(axLabels.length) {
+                ax.levelNr = axLabels[0].length;
+                ax.levels = axLabels[0].map(function(_, idx) {return idx;});
+                var fullSortedObjectList = sortLib.sortObjectList(cols, fullObjectList.slice());
+                var fullList = sortLib.objectListToList(fullSortedObjectList);
+                var fullSortedMatrix = sortLib.sortedMatrix(fullList, true);
 
-                var ind1 = seen[1][1];
-                return ind1[a[1]] - ind1[b[1]];
-            });
+                var fullXs = fullSortedMatrix[0].slice();
 
-            for(i = 0; i < list.length; i++) {
-                setCategoryIndex(list[i]);
+                for(i = 0; i < fullXs.length; i++) {
+                    setCategoryIndex(fullXs[i]);
+                }
             }
         };
     }
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 3dcddc841f4..c4f027948d7 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -3158,7 +3158,7 @@ plots.doCalcdata = function(gd, traces) {
         calcdata[i] = cd;
     }
 
-    setupAxisCategories(axList, fullData, fullLayout);
+    setupAxisCategories(axList, gd, fullLayout);
 
     // 'transform' loop - must calc container traces first
     // so that if their dependent traces can get transform properly
@@ -3166,7 +3166,7 @@ plots.doCalcdata = function(gd, traces) {
     for(i = 0; i < fullData.length; i++) transformCalci(i);
 
     // clear stuff that should recomputed in 'regular' loop
-    if(hasCalcTransform) setupAxisCategories(axList, fullData, fullLayout);
+    if(hasCalcTransform) setupAxisCategories(axList, gd, fullLayout);
 
     // 'regular' loop - make sure container traces (eg carpet) calc before
     // contained traces (eg contourcarpet)
@@ -3374,13 +3374,13 @@ function sortAxisCategoriesByValue(axList, gd) {
     return affectedTraces;
 }
 
-function setupAxisCategories(axList, fullData, fullLayout) {
+function setupAxisCategories(axList, gd, fullLayout) {
     var axLookup = {};
 
     function setupOne(ax) {
         ax.clearCalc();
         if(ax.type === 'multicategory') {
-            ax.setupMultiCategory(fullData);
+            ax.setupMultiCategory(gd);
         }
 
         axLookup[ax._id] = 1;