diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1f31df70a85..6d2821ddeb7 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,4 +1,8 @@
+# This config is equivalent to both the '.circleci/extended/orb-free.yml' and the base '.circleci/config.yml'
 version: 2.1
+
+# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
+# See: https://circleci.com/docs/2.0/orb-intro/
 orbs:
   browser-tools: circleci/browser-tools@1.2.2
 
@@ -190,7 +194,7 @@ jobs:
     working_directory: ~/plotly.js
     steps:
       - browser-tools/install-browser-tools: &browser-versions
-          firefox-version: '81.0'
+          firefox-version: "81.0"
           install-chrome: false
           install-chromedriver: false
       - attach_workspace:
@@ -209,7 +213,7 @@ jobs:
     working_directory: ~/plotly.js
     steps:
       - browser-tools/install-browser-tools: &browser-versions
-          firefox-version: '82.0'
+          firefox-version: "82.0"
           install-chrome: false
           install-chromedriver: false
       - attach_workspace:
@@ -347,8 +351,8 @@ jobs:
       - run:
           name: Install poppler-utils to have pdftops for exporting eps
           command: |
-              sudo apt-get update --allow-releaseinfo-change
-              sudo apt-get install poppler-utils
+            sudo apt-get update --allow-releaseinfo-change
+            sudo apt-get install poppler-utils
       - run:
           name: Create svg, jpg, jpeg, webp, pdf and eps files
           command: python3 test/image/make_exports.py
@@ -467,8 +471,8 @@ jobs:
           command: npm run no-new-func
 
 workflows:
-  version: 2
-  build-and-test:
+  sample: # This is the name of the workflow, feel free to change it to better match your workflow.
+    # Inside the workflow, you define the jobs you want to run.
     jobs:
       - install-and-cibuild-node12
 
diff --git a/delete.txt b/delete.txt
new file mode 100644
index 00000000000..f6c4fd068ea
--- /dev/null
+++ b/delete.txt
@@ -0,0 +1 @@
+delete me
\ No newline at end of file
diff --git a/devtools/test_dashboard/index - boxplot.html b/devtools/test_dashboard/index - boxplot.html
new file mode 100644
index 00000000000..dc9c10d469e
--- /dev/null
+++ b/devtools/test_dashboard/index - boxplot.html	
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Plotly.js Devtools</title>
+    <link
+      rel="stylesheet"
+      type="text/css"
+      href="//fonts.googleapis.com/css?family=Open+Sans:600,400,300,200|Droid+Sans|PT+Sans+Narrow|Gravitas+One|Droid+Sans+Mono|Droid+Serif|Raleway|Old+Standard+TT"
+    />
+    <link rel="stylesheet" type="text/css" href="./style.css" />
+  </head>
+  <body>
+    <header>
+      <img
+        src="http://images.plot.ly/logo/plotlyjs-logo@2x.png"
+        onClick="Tabs.reload();"
+      />
+      <span id="reload-time"></span>
+
+      <input id="mocks-search" type="text" placeholder="mocks search" />
+      <input id="css-transform" type="text" placeholder="css transform" />
+    </header>
+
+    <section id="mocks-list"></section>
+    <div id="plots">
+      <div id="graph">
+        <div id="tester" style="width: 600px; height: 250px"></div>
+      </div>
+    </div>
+    <div id="snapshot"></div>
+
+    <script src="../../node_modules/mathjax-v2/MathJax.js?config=TeX-AMS-MML_SVG"></script>
+    <script charset="utf-8" id="source" src="../../build/plotly.js">
+      // type="module"
+      // import Plotly from "../../build/plotly.js"
+    </script>
+    <script charset="utf-8" src="../../build/test_dashboard-bundle.js"></script>
+    <script>
+      let x1 = [
+        1234, 1234, 1234, 2222, 3333, 4444, 4444, 2222, 3333, 4444, 4444, 1234,
+        2222, 3333, 3333, 4444, 4444, 1234, 1234, 1234, 2222, 2222, 4444, 4444,
+        4444,
+      ];
+
+      let x2 = [
+        1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.2, 1.25, 1.25, 1.25, 1.1, 1.1, 1.1,
+        1.1, 1.1, 1.1, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.25,
+      ];
+
+      let x3 = [
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+        2,
+      ];
+
+      let y = [
+        74.794746, 73.263952, 73.175294, 89.929494, 87.071843, 78.117551,
+        76.690038, 85.818503, 97.373488, 76.747523, 73.172017, 68.891727,
+        79.342691, 84.390967, 82.548122, 70.278573, 72.255796, 74.747855,
+        73.312634, 70.067858, 81.736596, 84.71945, 80.099938, 82.208555,
+        74.795867,
+      ];
+
+      var trace1 = {
+        x: [x1, x2, x3],
+
+        y: y,
+        type: "box",
+      };
+
+      // var trace2 = {
+      //   x: [x1, x2],
+
+      //   y: y,
+      //   type: "box",
+      // };
+
+      var data = [trace1];
+
+      Plotly.newPlot("tester", data);
+    </script>
+  </body>
+</html>
diff --git a/devtools/test_dashboard/index copy 2.html b/devtools/test_dashboard/index copy 2.html
new file mode 100644
index 00000000000..e4596963f0a
--- /dev/null
+++ b/devtools/test_dashboard/index copy 2.html	
@@ -0,0 +1,171 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Plotly.js Devtools</title>
+    <link
+      rel="stylesheet"
+      type="text/css"
+      href="//fonts.googleapis.com/css?family=Open+Sans:600,400,300,200|Droid+Sans|PT+Sans+Narrow|Gravitas+One|Droid+Sans+Mono|Droid+Serif|Raleway|Old+Standard+TT"
+    />
+    <link rel="stylesheet" type="text/css" href="./style.css" />
+  </head>
+  <body>
+    <header>
+      <img
+        src="http://images.plot.ly/logo/plotlyjs-logo@2x.png"
+        onClick="Tabs.reload();"
+      />
+      <span id="reload-time"></span>
+
+      <input id="mocks-search" type="text" placeholder="mocks search" />
+      <input id="css-transform" type="text" placeholder="css transform" />
+    </header>
+
+    <section id="mocks-list"></section>
+    <div id="plots">
+      <div id="graph">
+        <div id="tester" style="width: 600px; height: 250px"></div>
+      </div>
+    </div>
+    <div id="snapshot"></div>
+
+    <script src="../../node_modules/mathjax-v2/MathJax.js?config=TeX-AMS-MML_SVG"></script>
+    <script charset="utf-8" id="source" src="../../build/plotly.js">
+      // type="module"
+      // import Plotly from "../../build/plotly.js"
+    </script>
+    <script charset="utf-8" src="../../build/test_dashboard-bundle.js"></script>
+    <script>
+      // let fullData = [
+      //   {
+      //     x: [
+      //       ["AA", "AA", "AA", "BB"],
+      //       //  "DD", "DD", "DD", "DD", "BB", "BB"],
+      //       ["DD", "CC", "DD", "DD"],
+      //       ["2", "1", "3", "4"],
+      //       // "VV", "MM", "AA"],
+      //     ],
+      //     y: [10, 8, 11, 12],
+      //     line: {
+      //       color: "orange",
+      //       width: 3,
+      //     },
+      //     //  14, 17, 8, 10, 10, 9],
+      //     // type: "bar",
+      //   },
+      //   {
+      //     x: [
+      //       ["AA", "AA", "AA", "BB"],
+      //       //  "DD", "DD", "DD", "DD", "BB", "BB"],
+      //       ["DD", "CC", "DD", "DD"],
+      //       ["2", "1", "3", "4"],
+      //       // "VV", "MM", "AA"],
+      //     ],
+      //     y: [12, 10, 13, 14],
+      //     line: {
+      //       color: "blue",
+      //       width: 3,
+      //     },
+      //     //  14, 17, 8, 10, 10, 9],
+      //     // type: "bar",
+      //   },
+      // ];
+      // var trace1 = {
+      //   x: [
+      //     ["AA", "AA", "AA", "BB"],
+      //     ["DD", "CC", "DD", "DD"],
+      //     ["2", "1", "3", "4"],
+      //   ],
+
+      //   // y: y0,
+      //   // y: [12, 10, 13, 14],
+      //   y: [y0, y1, y2, y3],
+      //   type: "box",
+      // };
+      // TESTER = document.getElementById("tester");
+      // // Plotly.newPlot(TESTER, fullData, {
+      // //   margin: { t: 0 },
+      // // });
+    </script>
+    <script>
+      var y0 = [];
+      var y1 = [];
+      var y2 = [];
+      var y3 = [];
+
+      for (var i = 0; i < 50; i++) {
+        y0[i] = Math.random();
+        y1[i] = Math.random() * 10;
+        y2[i] = Math.random() * 20;
+        y3[i] = Math.random() * 30;
+      }
+
+      var ys = [y0, y1, y2, y3];
+
+      var spreadYs = [...y0, ...y1, ...y2, ...y3];
+
+      var trace1 = {
+        x: [
+          ["AA", "AA", "AA", "BB"],
+          ["DD", "CC", "DD", "DD"],
+          ["2", "1", "3", "4"],
+        ],
+
+        // y: y0,
+        y: spreadYs,
+        // y: [12, 10, 13, 14],
+        // y: [y0, y1, y2, y3],
+        type: "box",
+      };
+      var data = [trace1];
+
+      // Plotly.newPlot("tester", data);
+    </script>
+    <script>
+      let x1 = [
+        1111, 1111, 1111, 1111, 2222, 2222, 3333, 3333, 3333, 4444, 4444, 4444,
+        4444, 1111, 1111, 1111, 2222, 2222, 2222, 4444, 4444, 3333, 4444, 4444,
+        4444,
+      ];
+
+      let x2 = [
+        1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.2, 1.25, 1.25, 1.25, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.25,
+      ];
+
+      let x3 = [
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+        2,
+      ];
+
+      let y = [
+        68.89172741012955, 74.7947463431226, 73.26395158502746,
+        73.17529436801782, 89.92949351516832, 79.34269143320005,
+        84.39096735673589, 87.07184294113185, 82.5481218835995,
+        70.27857346850803, 78.11755050565934, 76.69003807419446,
+        72.25579607260464, 74.74785526725371, 73.31263433475021,
+        70.06785817806055, 81.73659630835789, 84.71945002499666,
+        85.81850329438534, 80.09993832704379, 82.20855460624422,
+        97.37348751144206, 76.7475232225902, 74.79586728812728,
+        73.17201742069297,
+      ];
+
+      var trace1 = {
+        x: [x1, x2, x3],
+
+        y: y,
+        type: "box",
+      };
+
+      // var trace2 = {
+      //   x: [x1, x2],
+
+      //   y: y,
+      //   type: "box",
+      // };
+
+      var data = [trace1];
+
+      Plotly.newPlot("tester", data, { margin: { t: 0, l: 50, r: 0 } });
+    </script>
+  </body>
+</html>
diff --git a/devtools/test_dashboard/index copy.html b/devtools/test_dashboard/index copy.html
new file mode 100644
index 00000000000..bbe0c94afa3
--- /dev/null
+++ b/devtools/test_dashboard/index copy.html	
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Plotly.js Devtools</title>
+    <link
+      rel="stylesheet"
+      type="text/css"
+      href="//fonts.googleapis.com/css?family=Open+Sans:600,400,300,200|Droid+Sans|PT+Sans+Narrow|Gravitas+One|Droid+Sans+Mono|Droid+Serif|Raleway|Old+Standard+TT"
+    />
+    <link rel="stylesheet" type="text/css" href="./style.css" />
+  </head>
+  <body>
+    <header>
+      <img
+        src="http://images.plot.ly/logo/plotlyjs-logo@2x.png"
+        onClick="Tabs.reload();"
+      />
+      <span id="reload-time"></span>
+
+      <input id="mocks-search" type="text" placeholder="mocks search" />
+      <input id="css-transform" type="text" placeholder="css transform" />
+    </header>
+
+    <section id="mocks-list"></section>
+    <div id="plots">
+      <div id="graph">
+        <div id="tester" style="width: 600px; height: 250px"></div>
+      </div>
+    </div>
+    <div id="snapshot"></div>
+
+    <script src="../../node_modules/mathjax-v2/MathJax.js?config=TeX-AMS-MML_SVG"></script>
+    <script charset="utf-8" id="source" src="../../build/plotly.js">
+      // type="module"
+      // import Plotly from "../../build/plotly.js"
+    </script>
+    <script charset="utf-8" src="../../build/test_dashboard-bundle.js"></script>
+    <script>
+      let fullData = [
+        {
+          x: [
+            ["AA", "AA", "AA", "BB"],
+            //  "DD", "DD", "DD", "DD", "BB", "BB"],
+            ["DD", "CC", "DD", "DD"],
+            ["2", "1", "3", "4"],
+            // "VV", "MM", "AA"],
+          ],
+          y: [10, 8, 11, 12],
+          line: {
+            color: "orange",
+            width: 3,
+          },
+          //  14, 17, 8, 10, 10, 9],
+          // type: "bar",
+        },
+        {
+          x: [
+            ["AA", "AA", "AA", "BB"],
+            //  "DD", "DD", "DD", "DD", "BB", "BB"],
+            ["DD", "CC", "DD", "DD"],
+            ["2", "1", "3", "4"],
+            // "VV", "MM", "AA"],
+          ],
+          y: [12, 10, 13, 14],
+          line: {
+            color: "blue",
+            width: 3,
+          },
+          //  14, 17, 8, 10, 10, 9],
+          // type: "bar",
+        },
+      ];
+      var trace1 = {
+        x: [
+          ["AA", "AA", "AA", "BB"],
+          ["DD", "CC", "DD", "DD"],
+          ["2", "1", "3", "4"],
+        ],
+
+        // y: y0,
+        // y: [12, 10, 13, 14],
+        y: [y0, y1, y2, y3],
+        type: "box",
+      };
+      TESTER = document.getElementById("tester");
+      // Plotly.newPlot(TESTER, fullData, {
+      //   margin: { t: 0 },
+      // });
+    </script>
+    <script>
+      var y0 = [];
+      var y1 = [];
+      var y2 = [];
+      var y3 = [];
+
+      for (var i = 0; i < 50; i++) {
+        y0[i] = Math.random();
+        y1[i] = Math.random() * 10;
+        y2[i] = Math.random() * 20;
+        y3[i] = Math.random() * 30;
+      }
+
+      var ys = [y0, y1, y2, y3];
+
+      var spreadYs = [...y0, ...y1, ...y2, ...y3];
+
+      var trace1 = {
+        x: [
+          ["AA", "AA", "AA", "BB"],
+          ["DD", "CC", "DD", "DD"],
+          ["2", "1", "3", "4"],
+        ],
+
+        // y: y0,
+        y: spreadYs,
+        // y: [12, 10, 13, 14],
+        // y: [y0, y1, y2, y3],
+        type: "box",
+      };
+      var data = [trace1];
+
+      // Plotly.newPlot("tester", data);
+    </script>
+    <script>
+      let x1 = [
+        1234, 1234, 1234, 2222, 3333, 4444, 4444, 2222, 3333, 4444, 4444, 1234,
+        2222, 3333, 3333, 4444, 4444, 1234, 1234, 1234, 2222, 2222, 4444, 4444,
+        4444,
+      ];
+
+      let x2 = [
+        1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.2, 1.25, 1.25, 1.25, 1.1, 1.1, 1.1,
+        1.1, 1.1, 1.1, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.25,
+      ];
+
+      let y = [
+        74.794746, 73.263952, 73.175294, 89.929494, 87.071843, 78.117551,
+        76.690038, 85.818503, 97.373488, 76.747523, 73.172017, 68.891727,
+        79.342691, 84.390967, 82.548122, 70.278573, 72.255796, 74.747855,
+        73.312634, 70.067858, 81.736596, 84.71945, 80.099938, 82.208555,
+        74.795867,
+      ];
+
+      var trace1 = {
+        x: [x1, x2],
+
+        y: y.slice(0, 12),
+        type: "box",
+      };
+
+      var trace2 = {
+        x: [x1, x2],
+
+        y: y.slice(12, 24),
+        type: "box",
+      };
+
+      var data = [trace1];
+
+      Plotly.newPlot("plot", data);
+    </script>
+  </body>
+</html>
diff --git a/devtools/test_dashboard/index.html b/devtools/test_dashboard/index.html
index 0c0c9e89760..13f8a3c48d3 100644
--- a/devtools/test_dashboard/index.html
+++ b/devtools/test_dashboard/index.html
@@ -1,27 +1,526 @@
 <!DOCTYPE html>
 <html>
-<head>
-  <title>Plotly.js Devtools</title>
-  <link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Open+Sans:600,400,300,200|Droid+Sans|PT+Sans+Narrow|Gravitas+One|Droid+Sans+Mono|Droid+Serif|Raleway|Old+Standard+TT"/>
-  <link rel="stylesheet" type="text/css" href="./style.css">
-</head>
-<body>
-  <header>
-    <img src="http://images.plot.ly/logo/plotlyjs-logo@2x.png" onClick="Tabs.reload();" />
-    <span id="reload-time"></span>
-
-    <input id="mocks-search" type="text" placeholder="mocks search" />
-    <input id="css-transform" type="text" placeholder="css transform" />
-  </header>
-
-  <section id="mocks-list"></section>
-  <div id="plots">
-    <div id="graph"></div>
-  </div>
-  <div id="snapshot"></div>
-
-  <script src="../../node_modules/mathjax-v2/MathJax.js?config=TeX-AMS-MML_SVG"></script>
-  <script charset="utf-8" id="source" src="../../build/plotly.js" type="module"></script>
-  <script charset="utf-8" src="../../build/test_dashboard-bundle.js"></script>
-</body>
+  <head>
+    <title>Plotly.js Devtools</title>
+    <link
+      rel="stylesheet"
+      type="text/css"
+      href="//fonts.googleapis.com/css?family=Open+Sans:600,400,300,200|Droid+Sans|PT+Sans+Narrow|Gravitas+One|Droid+Sans+Mono|Droid+Serif|Raleway|Old+Standard+TT"
+    />
+    <link rel="stylesheet" type="text/css" href="./style.css" />
+  </head>
+  <body>
+    <header>
+      <img
+        src="http://images.plot.ly/logo/plotlyjs-logo@2x.png"
+        onClick="Tabs.reload();"
+      />
+      <span id="reload-time"></span>
+
+      <input id="mocks-search" type="text" placeholder="mocks search" />
+      <input id="css-transform" type="text" placeholder="css transform" />
+    </header>
+
+    <section id="mocks-list"></section>
+    <div id="plots">
+      <div id="graph">
+        <div id="tester" style="width: 600px; height: 450px"></div>
+      </div>
+    </div>
+    <div id="snapshot"></div>
+
+    <script src="../../node_modules/mathjax-v2/MathJax.js?config=TeX-AMS-MML_SVG"></script>
+    <script charset="utf-8" id="source" src="../../build/plotly.js">
+      // type="module"
+      // import Plotly from "../../build/plotly.js"
+    </script>
+    <script charset="utf-8" src="../../build/test_dashboard-bundle.js"></script>
+
+    <script>
+      var y0 = [];
+      var y1 = [];
+      var y2 = [];
+      var y3 = [];
+
+      for (var i = 0; i < 50; i++) {
+        y0[i] = Math.random();
+        y1[i] = Math.random() * 10;
+        y2[i] = Math.random() * 20;
+        y3[i] = Math.random() * 30;
+      }
+
+      var ys = [y0, y1, y2, y3];
+
+      var spreadYs = [...y0, ...y1, ...y2, ...y3];
+
+      var trace1 = {
+        x: [
+          ["AA", "AA", "AA", "BB"],
+          ["DD", "CC", "DD", "DD"],
+          ["2", "1", "3", "4"],
+        ],
+
+        // y: y0,
+        y: spreadYs,
+        // y: [12, 10, 13, 14],
+        // y: [y0, y1, y2, y3],
+        type: "box",
+      };
+      var data = [trace1];
+
+      // Plotly.newPlot("tester", data);
+    </script>
+    <script>
+      let x1 = [
+        1111, 1111, 1111, 1111, 2222, 2222, 3333, 3333, 3333, 4444, 4444, 4444,
+        4444, 1111, 1111, 1111, 2222, 2222, 2222, 4444, 4444, 3333, 4444, 4444,
+        4444,
+      ];
+
+      let x2 = [
+        1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.2, 1.25, 1.25, 1.25, 1.1, 1.1, 1.1,
+        1.1, 1.1, 1.1, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.25,
+      ];
+
+      let x3 = [
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+        2,
+      ];
+
+      let y = [
+        68.89172741012955, 74.7947463431226, 73.26395158502746,
+        73.17529436801782, 89.92949351516832, 79.34269143320005,
+        84.39096735673589, 87.07184294113185, 82.5481218835995,
+        70.27857346850803, 78.11755050565934, 76.69003807419446,
+        72.25579607260464, 74.74785526725371, 73.31263433475021,
+        70.06785817806055, 81.73659630835789, 84.71945002499666,
+        85.81850329438534, 80.09993832704379, 82.20855460624422,
+        97.37348751144206, 76.7475232225902, 74.79586728812728,
+        73.17201742069297,
+      ];
+
+      var trace1 = {
+        x: [x1, x2, x3],
+
+        y: y,
+        type: "box",
+      };
+
+      var trace2 = {
+        x: [
+          // [1111, 1111, 1111],
+          // [1.1, 1.1, 1.1],
+          // [1, 2, 3],
+          [1111, 1111],
+          [1.1, 1.2],
+          [1, 2],
+          // [1111], [1.1], [1],
+          // [1111, 1.1, 2],
+        ],
+
+        // y: [72, 67, 62],
+        y: [72, 67],
+        // type: "scatter",
+      };
+      var trace3 = {
+        // x: [[1111], [1.1], [2]],
+        // y: [67],
+        // // type: "scatter",
+      };
+
+      // var trace2 = {
+      //   x: [
+      //     ["AA", "AA", "AA", "BB"],
+      //     ["DD", "CC", "DD", "DD"],
+      //     ["2", "1", "3", "4"],
+      //   ],
+
+      //   // y: y0,
+      //   y: [12, 10, 13, 14],
+      // }
+      //   y: [y0, y1, y2, y3],
+      //   type: "box",
+      // };
+
+      var data = [trace1];
+
+      // Plotly.newPlot("tester", data, { margin: { t: 0, l: 50, r: 0 } });
+    </script>
+    <script>
+      var data = [
+        {
+          z: [
+            [0.304, 1.465, 2.474, 3.05, 4.38, 5.245, 6.12],
+            [0.3515, 1.326, 2.18, 3.26, 4.41, 5.25, 6.11],
+            [0.3994, 1.167, 2.09, 3.306, 4.305, 5.35, 6.0],
+            [0.297, 1.295, 2.49, 3.428, 4.13, 5.41, 6.38],
+            [0.4602, 1.2256, 2.3356, 3.0667, 4.498, 5.411, 6.29],
+            [0.0197, 1.274, 2.407, 3.22, 4.47, 5.44, 6.28],
+            [0.32, 1.44, 2.303, 3.115, 4.49, 5.25, 6.46],
+            [0.4446, 1.223, 2.367, 3.253, 4.385, 5.08, 6.19],
+            [0.1304, 1.046, 2.45, 3.226, 4.34, 5.4, 6.05],
+          ],
+          x: [
+            ["2017", "2017", "2017", "2017", "2018", "2018", "2018"],
+            ["q1", "q2", "q3", "q4", "q1", "q2", "q3"],
+          ],
+          y: [
+            ["G1", "G1", "G1", "G2", "G2", "G2", "G3", "G3", "G3"],
+            ["A", "B", "C", "A", "B", "C", "A", "B", "C"],
+          ],
+          type: "heatmap",
+          hoverongaps: false,
+        },
+      ];
+
+      // Plotly.newPlot("tester", data);
+    </script>
+    <script>
+      let data2 = [
+        {
+          name: "ohlc",
+          type: "ohlc",
+          open: [10, 11, 12, 13, 12, 13, 14, 15, 16],
+          high: [15, 16, 17, 18, 17, 18, 19, 20, 21],
+          low: [7, 8, 9, 10, 9, 10, 11, 12, 13],
+          close: [9, 10, 12, 13, 13, 12, 14, 14, 17],
+          x: [
+            [
+              "Group 1",
+              "Group 1",
+              "Group 1",
+              "Group 2",
+              "Group 2",
+              "Group 2",
+              "Group 3",
+              "Group 3",
+              "Group 3",
+            ],
+            ["a", "b", "c", "a", "b", "c", "a", "b", "c"],
+          ],
+        },
+        {
+          name: "candlestick",
+          type: "candlestick",
+          open: [20, 21, 22, 23, 22, 23, 24, 25, 26],
+          high: [25, 26, 27, 28, 27, 28, 29, 30, 31],
+          low: [17, 18, 19, 20, 19, 20, 21, 22, 23],
+          close: [19, 20, 22, 23, 23, 22, 24, 24, 27],
+          x: [
+            [
+              "Group 1",
+              "Group 1",
+              "Group 1",
+              "Group 2",
+              "Group 2",
+              "Group 2",
+              "Group 3",
+              "Group 3",
+              "Group 3",
+            ],
+            ["a", "b", "c", "a", "b", "c", "a", "b", "c"],
+          ],
+        },
+      ];
+      let layout = {
+        title: {
+          text: "Finance traces on multicategory x-axis",
+          xref: "paper",
+          x: 0,
+        },
+        legend: {
+          x: 1,
+          xanchor: "right",
+          y: 1,
+          yanchor: "bottom",
+        },
+      };
+      // Plotly.newPlot("tester", data2);
+    </script>
+    <script>
+      let data3 = [{ x: ["a", "b", "c"], base: [0.2, -0.2, 1], type: "bar" }];
+      // Plotly.newPlot("tester", data3);
+    </script>
+    <script>
+    let data4 = [
+      {
+        x: [
+          0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3, 0.6, 0.7,
+          0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2, 0.2, 0.2, 0.6, 1, 0.5,
+          0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3,
+        ],
+        y: [
+          [
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+          ],
+
+          [
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+          ],
+        ],
+        name: "kale",
+        marker: {
+          color: "#3D9970",
+        },
+        orientation: "h",
+        type: "violin",
+      },
+      {
+        x: [
+          0.6, 0.7, 0.3, 0.6, 0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2, 0.1, 0.3,
+          0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5, 0.6, 0.7, 0.3, 0.6, 0,
+          0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2,
+        ],
+        y: [
+          [
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+          ],
+
+          [
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+          ],
+        ],
+        name: "radishes",
+        marker: {
+          color: "#FF4136",
+        },
+        orientation: "h",
+        type: "violin",
+      },
+      {
+        x: [
+          0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5, 0.1, 0.3,
+          0.1, 0.9, 0.6, 0.6, 0.9, 1, 0.3, 0.6, 0.8, 0.5, 0.2, 0.2, 0.6, 1, 0.5,
+          0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3,
+        ],
+        y: [
+          [
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2016",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2017",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+            "2018",
+          ],
+
+          [
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 1",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+            "day 2",
+          ],
+        ],
+        name: "carrots",
+        marker: {
+          color: "#FF851B",
+        },
+        orientation: "h",
+        type: "violin",
+      },
+    ];
+      Plotly.newPlot("tester", data4);
+    </script>
+  </body>
 </html>
diff --git a/package-lock.json b/package-lock.json
index e6dd94e1092..7ee763f4bd7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7886,9 +7886,9 @@
       }
     },
     "node_modules/jasmine-core": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
-      "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
+      "version": "3.99.1",
+      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz",
+      "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==",
       "dev": true
     },
     "node_modules/jest-worker": {
@@ -14405,8 +14405,7 @@
     "@mapbox/mapbox-gl-supported": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz",
-      "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==",
-      "requires": {}
+      "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg=="
     },
     "@mapbox/node-pre-gyp": {
       "version": "1.0.8",
@@ -14992,8 +14991,7 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz",
       "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==",
-      "dev": true,
-      "requires": {}
+      "dev": true
     },
     "acorn-globals": {
       "version": "6.0.0",
@@ -15017,8 +15015,7 @@
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
       "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
-      "dev": true,
-      "requires": {}
+      "dev": true
     },
     "acorn-walk": {
       "version": "7.1.1",
@@ -19073,9 +19070,9 @@
       }
     },
     "jasmine-core": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
-      "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
+      "version": "3.99.1",
+      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz",
+      "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==",
       "dev": true
     },
     "jest-worker": {
@@ -19333,8 +19330,7 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/karma-jasmine-spec-tags/-/karma-jasmine-spec-tags-1.3.0.tgz",
       "integrity": "sha512-J1iAZtcEcK/pCkaYsU0VbNJae+3Awz/3p1dhWnKgvcB4FkbBK1TIOi4qswJ6HAmmcdJZgudffXpslcNQJZH9sw==",
-      "dev": true,
-      "requires": {}
+      "dev": true
     },
     "karma-spec-reporter": {
       "version": "0.0.36",
@@ -21761,8 +21757,7 @@
     "source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-      "devOptional": true
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
     },
     "source-map-js": {
       "version": "1.0.2",
@@ -21960,11 +21955,6 @@
         "fs-extra": "^10.0.0"
       }
     },
-    "string_decoder": {
-      "version": "0.10.31",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
-      "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
-    },
     "string-split-by": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz",
diff --git a/src/lib/sort_traces.js b/src/lib/sort_traces.js
new file mode 100644
index 00000000000..9e4985e84ae
--- /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;
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index dfdb0e5166d..1506cfa3cff 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -1,21 +1,21 @@
-'use strict';
+"use strict";
 
-var d3 = require('@plotly/d3');
-var isNumeric = require('fast-isnumeric');
-var Plots = require('../../plots/plots');
+var d3 = require("@plotly/d3");
+var isNumeric = require("fast-isnumeric");
+var Plots = require("../../plots/plots");
 
-var Registry = require('../../registry');
-var Lib = require('../../lib');
+var Registry = require("../../registry");
+var Lib = require("../../lib");
 var strTranslate = Lib.strTranslate;
-var svgTextUtils = require('../../lib/svg_text_utils');
-var Titles = require('../../components/titles');
-var Color = require('../../components/color');
-var Drawing = require('../../components/drawing');
+var svgTextUtils = require("../../lib/svg_text_utils");
+var Titles = require("../../components/titles");
+var Color = require("../../components/color");
+var Drawing = require("../../components/drawing");
 
-var axAttrs = require('./layout_attributes');
-var cleanTicks = require('./clean_ticks');
+var axAttrs = require("./layout_attributes");
+var cleanTicks = require("./clean_ticks");
 
-var constants = require('../../constants/numerical');
+var constants = require("../../constants/numerical");
 var ONEMAXYEAR = constants.ONEMAXYEAR;
 var ONEAVGYEAR = constants.ONEAVGYEAR;
 var ONEMINYEAR = constants.ONEMINYEAR;
@@ -34,21 +34,21 @@ var ONESEC = constants.ONESEC;
 var MINUS_SIGN = constants.MINUS_SIGN;
 var BADNUM = constants.BADNUM;
 
-var ZERO_PATH = { K: 'zeroline' };
-var GRID_PATH = { K: 'gridline', L: 'path' };
-var MINORGRID_PATH = { K: 'minor-gridline', L: 'path' };
-var TICK_PATH = { K: 'tick', L: 'path' };
-var TICK_TEXT = { K: 'tick', L: 'text' };
+var ZERO_PATH = { K: "zeroline" };
+var GRID_PATH = { K: "gridline", L: "path" };
+var MINORGRID_PATH = { K: "minor-gridline", L: "path" };
+var TICK_PATH = { K: "tick", L: "path" };
+var TICK_TEXT = { K: "tick", L: "text" };
 var MARGIN_MAPPING = {
-    width: ['x', 'r', 'l', 'xl', 'xr'],
-    height: ['y', 't', 'b', 'yt', 'yb'],
-    right: ['r', 'xr'],
-    left: ['l', 'xl'],
-    top: ['t', 'yt'],
-    bottom: ['b', 'yb']
+  width: ["x", "r", "l", "xl", "xr"],
+  height: ["y", "t", "b", "yt", "yb"],
+  right: ["r", "xr"],
+  left: ["l", "xl"],
+  top: ["t", "yt"],
+  bottom: ["b", "yb"],
 };
 
-var alignmentConstants = require('../../constants/alignment');
+var alignmentConstants = require("../../constants/alignment");
 var MID_SHIFT = alignmentConstants.MID_SHIFT;
 var CAP_SHIFT = alignmentConstants.CAP_SHIFT;
 var LINE_SPACING = alignmentConstants.LINE_SPACING;
@@ -56,12 +56,12 @@ var OPPOSITE_SIDE = alignmentConstants.OPPOSITE_SIDE;
 
 var TEXTPAD = 3;
 
-var axes = module.exports = {};
+var axes = (module.exports = {});
 
-axes.setConvert = require('./set_convert');
-var autoType = require('./axis_autotype');
+axes.setConvert = require("./set_convert");
+var autoType = require("./axis_autotype");
 
-var axisIds = require('./axis_ids');
+var axisIds = require("./axis_ids");
 var idSort = axisIds.idSort;
 var isLinked = axisIds.isLinked;
 
@@ -74,17 +74,14 @@ axes.listIds = axisIds.listIds;
 axes.getFromId = axisIds.getFromId;
 axes.getFromTrace = axisIds.getFromTrace;
 
-var autorange = require('./autorange');
+var autorange = require("./autorange");
 axes.getAutoRange = autorange.getAutoRange;
 axes.findExtremes = autorange.findExtremes;
 
 var epsilon = 0.0001;
 function expandRange(range) {
-    var delta = (range[1] - range[0]) * epsilon;
-    return [
-        range[0] - delta,
-        range[1] + delta
-    ];
+  var delta = (range[1] - range[0]) * epsilon;
+  return [range[0] - delta, range[1] + delta];
 }
 
 /*
@@ -98,28 +95,46 @@ function expandRange(range) {
  * extraOption: aside from existing axes with this letter, what non-axis value is allowed?
  *     Only required if it's different from `dflt`
  */
-axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption) {
-    var axLetter = attr.charAt(attr.length - 1);
-    var axlist = gd._fullLayout._subplots[axLetter + 'axis'];
-    var refAttr = attr + 'ref';
-    var attrDef = {};
-
-    if(!dflt) dflt = axlist[0] || (typeof extraOption === 'string' ? extraOption : extraOption[0]);
-    if(!extraOption) extraOption = dflt;
-    axlist = axlist.concat(axlist.map(function(x) { return x + ' domain'; }));
-
-    // data-ref annotations are not supported in gl2d yet
-
-    attrDef[refAttr] = {
-        valType: 'enumerated',
-        values: axlist.concat(extraOption ?
-            (typeof extraOption === 'string' ? [extraOption] : extraOption) :
-            []),
-        dflt: dflt
-    };
-
-    // xref, yref
-    return Lib.coerce(containerIn, containerOut, attrDef, refAttr);
+axes.coerceRef = function (
+  containerIn,
+  containerOut,
+  gd,
+  attr,
+  dflt,
+  extraOption
+) {
+  var axLetter = attr.charAt(attr.length - 1);
+  var axlist = gd._fullLayout._subplots[axLetter + "axis"];
+  var refAttr = attr + "ref";
+  var attrDef = {};
+
+  if (!dflt)
+    dflt =
+      axlist[0] ||
+      (typeof extraOption === "string" ? extraOption : extraOption[0]);
+  if (!extraOption) extraOption = dflt;
+  axlist = axlist.concat(
+    axlist.map(function (x) {
+      return x + " domain";
+    })
+  );
+
+  // data-ref annotations are not supported in gl2d yet
+
+  attrDef[refAttr] = {
+    valType: "enumerated",
+    values: axlist.concat(
+      extraOption
+        ? typeof extraOption === "string"
+          ? [extraOption]
+          : extraOption
+        : []
+    ),
+    dflt: dflt,
+  };
+
+  // xref, yref
+  return Lib.coerce(containerIn, containerOut, attrDef, refAttr);
 };
 
 /*
@@ -130,11 +145,21 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption
  * ar: the axis reference string
  *
  */
-axes.getRefType = function(ar) {
-    if(ar === undefined) { return ar; }
-    if(ar === 'paper') { return 'paper'; }
-    if(ar === 'pixel') { return 'pixel'; }
-    if(/( domain)$/.test(ar)) { return 'domain'; } else { return 'range'; }
+axes.getRefType = function (ar) {
+  if (ar === undefined) {
+    return ar;
+  }
+  if (ar === "paper") {
+    return "paper";
+  }
+  if (ar === "pixel") {
+    return "pixel";
+  }
+  if (/( domain)$/.test(ar)) {
+    return "domain";
+  } else {
+    return "range";
+  }
 };
 
 /*
@@ -159,397 +184,425 @@ axes.getRefType = function(ar) {
  * - for date axes: JS Dates or milliseconds, and convert to date strings
  * - for other types: coerce them to numbers
  */
-axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) {
-    var cleanPos, pos;
-    var axRefType = axes.getRefType(axRef);
-    if(axRefType !== 'range') {
-        cleanPos = Lib.ensureNumber;
-        pos = coerce(attr, dflt);
-    } else {
-        var ax = axes.getFromId(gd, axRef);
-        dflt = ax.fraction2r(dflt);
-        pos = coerce(attr, dflt);
-        cleanPos = ax.cleanPos;
-    }
-    containerOut[attr] = cleanPos(pos);
+axes.coercePosition = function (containerOut, gd, coerce, axRef, attr, dflt) {
+  var cleanPos, pos;
+  var axRefType = axes.getRefType(axRef);
+  if (axRefType !== "range") {
+    cleanPos = Lib.ensureNumber;
+    pos = coerce(attr, dflt);
+  } else {
+    var ax = axes.getFromId(gd, axRef);
+    dflt = ax.fraction2r(dflt);
+    pos = coerce(attr, dflt);
+    cleanPos = ax.cleanPos;
+  }
+  containerOut[attr] = cleanPos(pos);
 };
 
-axes.cleanPosition = function(pos, gd, axRef) {
-    var cleanPos = (axRef === 'paper' || axRef === 'pixel') ?
-        Lib.ensureNumber :
-        axes.getFromId(gd, axRef).cleanPos;
+axes.cleanPosition = function (pos, gd, axRef) {
+  var cleanPos =
+    axRef === "paper" || axRef === "pixel"
+      ? Lib.ensureNumber
+      : axes.getFromId(gd, axRef).cleanPos;
 
-    return cleanPos(pos);
+  return cleanPos(pos);
 };
 
-axes.redrawComponents = function(gd, axIds) {
-    axIds = axIds ? axIds : axes.listIds(gd);
+axes.redrawComponents = function (gd, axIds) {
+  axIds = axIds ? axIds : axes.listIds(gd);
 
-    var fullLayout = gd._fullLayout;
+  var fullLayout = gd._fullLayout;
 
-    function _redrawOneComp(moduleName, methodName, stashName, shortCircuit) {
-        var method = Registry.getComponentMethod(moduleName, methodName);
-        var stash = {};
+  function _redrawOneComp(moduleName, methodName, stashName, shortCircuit) {
+    var method = Registry.getComponentMethod(moduleName, methodName);
+    var stash = {};
 
-        for(var i = 0; i < axIds.length; i++) {
-            var ax = fullLayout[axes.id2name(axIds[i])];
-            var indices = ax[stashName];
+    for (var i = 0; i < axIds.length; i++) {
+      var ax = fullLayout[axes.id2name(axIds[i])];
+      var indices = ax[stashName];
 
-            for(var j = 0; j < indices.length; j++) {
-                var ind = indices[j];
+      for (var j = 0; j < indices.length; j++) {
+        var ind = indices[j];
 
-                if(!stash[ind]) {
-                    method(gd, ind);
-                    stash[ind] = 1;
-                    // once is enough for images (which doesn't use the `i` arg anyway)
-                    if(shortCircuit) return;
-                }
-            }
+        if (!stash[ind]) {
+          method(gd, ind);
+          stash[ind] = 1;
+          // once is enough for images (which doesn't use the `i` arg anyway)
+          if (shortCircuit) return;
         }
+      }
     }
+  }
 
-    // annotations and shapes 'draw' method is slow,
-    // use the finer-grained 'drawOne' method instead
-    _redrawOneComp('annotations', 'drawOne', '_annIndices');
-    _redrawOneComp('shapes', 'drawOne', '_shapeIndices');
-    _redrawOneComp('images', 'draw', '_imgIndices', true);
-    _redrawOneComp('selections', 'drawOne', '_selectionIndices');
+  // annotations and shapes 'draw' method is slow,
+  // use the finer-grained 'drawOne' method instead
+  _redrawOneComp("annotations", "drawOne", "_annIndices");
+  _redrawOneComp("shapes", "drawOne", "_shapeIndices");
+  _redrawOneComp("images", "draw", "_imgIndices", true);
+  _redrawOneComp("selections", "drawOne", "_selectionIndices");
 };
 
-var getDataConversions = axes.getDataConversions = function(gd, trace, target, targetArray) {
-    var ax;
-
-    // If target points to an axis, use the type we already have for that
-    // axis to find the data type. Otherwise use the values to autotype.
-    var d2cTarget = (target === 'x' || target === 'y' || target === 'z') ?
-        target :
-        targetArray;
-
-    // In the case of an array target, make a mock data array
-    // and call supplyDefaults to the data type and
-    // setup the data-to-calc method.
-    if(Array.isArray(d2cTarget)) {
-        ax = {
-            type: autoType(targetArray, undefined, {
-                autotypenumbers: gd._fullLayout.autotypenumbers
-            }),
-            _categories: []
-        };
-        axes.setConvert(ax);
+var getDataConversions = (axes.getDataConversions = function (
+  gd,
+  trace,
+  target,
+  targetArray
+) {
+  var ax;
+
+  // If target points to an axis, use the type we already have for that
+  // axis to find the data type. Otherwise use the values to autotype.
+  var d2cTarget =
+    target === "x" || target === "y" || target === "z" ? target : targetArray;
+
+  // In the case of an array target, make a mock data array
+  // and call supplyDefaults to the data type and
+  // setup the data-to-calc method.
+  if (Array.isArray(d2cTarget)) {
+    ax = {
+      type: autoType(targetArray, undefined, {
+        autotypenumbers: gd._fullLayout.autotypenumbers,
+      }),
+      _categories: [],
+    };
+    axes.setConvert(ax);
 
-        // build up ax._categories (usually done during ax.makeCalcdata()
-        if(ax.type === 'category') {
-            for(var i = 0; i < targetArray.length; i++) {
-                ax.d2c(targetArray[i]);
-            }
-        }
-        // TODO what to do for transforms?
-    } else {
-        ax = axes.getFromTrace(gd, trace, d2cTarget);
+    // build up ax._categories (usually done during ax.makeCalcdata()
+    if (ax.type === "category") {
+      for (var i = 0; i < targetArray.length; i++) {
+        ax.d2c(targetArray[i]);
+      }
     }
+    // TODO what to do for transforms?
+  } else {
+    ax = axes.getFromTrace(gd, trace, d2cTarget);
+  }
 
-    // if 'target' has corresponding axis
-    // -> use setConvert method
-    if(ax) return {d2c: ax.d2c, c2d: ax.c2d};
+  // if 'target' has corresponding axis
+  // -> use setConvert method
+  if (ax) return { d2c: ax.d2c, c2d: ax.c2d };
 
-    // special case for 'ids'
-    // -> cast to String
-    if(d2cTarget === 'ids') return {d2c: toString, c2d: toString};
+  // special case for 'ids'
+  // -> cast to String
+  if (d2cTarget === "ids") return { d2c: toString, c2d: toString };
 
-    // otherwise (e.g. numeric-array of 'marker.color' or 'marker.size')
-    // -> cast to Number
+  // otherwise (e.g. numeric-array of 'marker.color' or 'marker.size')
+  // -> cast to Number
 
-    return {d2c: toNum, c2d: toNum};
-};
+  return { d2c: toNum, c2d: toNum };
+});
 
-function toNum(v) { return +v; }
-function toString(v) { return String(v); }
+function toNum(v) {
+  return +v;
+}
+function toString(v) {
+  return String(v);
+}
 
-axes.getDataToCoordFunc = function(gd, trace, target, targetArray) {
-    return getDataConversions(gd, trace, target, targetArray).d2c;
+axes.getDataToCoordFunc = function (gd, trace, target, targetArray) {
+  return getDataConversions(gd, trace, target, targetArray).d2c;
 };
 
 // get counteraxis letter for this axis (name or id)
 // this can also be used as the id for default counter axis
-axes.counterLetter = function(id) {
-    var axLetter = id.charAt(0);
-    if(axLetter === 'x') return 'y';
-    if(axLetter === 'y') return 'x';
+axes.counterLetter = function (id) {
+  var axLetter = id.charAt(0);
+  if (axLetter === "x") return "y";
+  if (axLetter === "y") return "x";
 };
 
 // incorporate a new minimum difference and first tick into
 // forced
 // note that _forceTick0 is linearized, so needs to be turned into
 // a range value for setting tick0
-axes.minDtick = function(ax, newDiff, newFirst, allow) {
-    // doesn't make sense to do forced min dTick on log or category axes,
-    // and the plot itself may decide to cancel (ie non-grouped bars)
-    if(['log', 'category', 'multicategory'].indexOf(ax.type) !== -1 || !allow) {
-        ax._minDtick = 0;
-    } else if(ax._minDtick === undefined) {
-        // undefined means there's nothing there yet
-
-        ax._minDtick = newDiff;
-        ax._forceTick0 = newFirst;
-    } else if(ax._minDtick) {
-        if((ax._minDtick / newDiff + 1e-6) % 1 < 2e-6 &&
-            // existing minDtick is an integer multiple of newDiff
-            // (within rounding err)
-            // and forceTick0 can be shifted to newFirst
-
-                (((newFirst - ax._forceTick0) / newDiff % 1) +
-                    1.000001) % 1 < 2e-6) {
-            ax._minDtick = newDiff;
-            ax._forceTick0 = newFirst;
-        } else if((newDiff / ax._minDtick + 1e-6) % 1 > 2e-6 ||
-            // if the converse is true (newDiff is a multiple of minDtick and
-            // newFirst can be shifted to forceTick0) then do nothing - same
-            // forcing stands. Otherwise, cancel forced minimum
-
-                (((newFirst - ax._forceTick0) / ax._minDtick % 1) +
-                    1.000001) % 1 > 2e-6) {
-            ax._minDtick = 0;
-        }
+axes.minDtick = function (ax, newDiff, newFirst, allow) {
+  // doesn't make sense to do forced min dTick on log or category axes,
+  // and the plot itself may decide to cancel (ie non-grouped bars)
+  if (["log", "category", "multicategory"].indexOf(ax.type) !== -1 || !allow) {
+    ax._minDtick = 0;
+  } else if (ax._minDtick === undefined) {
+    // undefined means there's nothing there yet
+
+    ax._minDtick = newDiff;
+    ax._forceTick0 = newFirst;
+  } else if (ax._minDtick) {
+    if (
+      (ax._minDtick / newDiff + 1e-6) % 1 < 2e-6 &&
+      // existing minDtick is an integer multiple of newDiff
+      // (within rounding err)
+      // and forceTick0 can be shifted to newFirst
+
+      ((((newFirst - ax._forceTick0) / newDiff) % 1) + 1.000001) % 1 < 2e-6
+    ) {
+      ax._minDtick = newDiff;
+      ax._forceTick0 = newFirst;
+    } else if (
+      (newDiff / ax._minDtick + 1e-6) % 1 > 2e-6 ||
+      // if the converse is true (newDiff is a multiple of minDtick and
+      // newFirst can be shifted to forceTick0) then do nothing - same
+      // forcing stands. Otherwise, cancel forced minimum
+
+      ((((newFirst - ax._forceTick0) / ax._minDtick) % 1) + 1.000001) % 1 > 2e-6
+    ) {
+      ax._minDtick = 0;
     }
+  }
 };
 
 // save a copy of the initial axis ranges in fullLayout
 // use them in mode bar and dblclick events
-axes.saveRangeInitial = function(gd, overwrite) {
-    var axList = axes.list(gd, '', true);
-    var hasOneAxisChanged = false;
-
-    for(var i = 0; i < axList.length; i++) {
-        var ax = axList[i];
-        var isNew =
-            ax._rangeInitial0 === undefined &&
-            ax._rangeInitial1 === undefined;
-
-        var hasChanged = isNew || (
-            ax.range[0] !== ax._rangeInitial0 ||
-            ax.range[1] !== ax._rangeInitial1
-        );
-
-        var autorange = ax.autorange;
-        if((isNew && autorange !== true) || (overwrite && hasChanged)) {
-            ax._rangeInitial0 = (autorange === 'min' || autorange === 'max reversed') ? undefined : ax.range[0];
-            ax._rangeInitial1 = (autorange === 'max' || autorange === 'min reversed') ? undefined : ax.range[1];
-            ax._autorangeInitial = autorange;
-            hasOneAxisChanged = true;
-        }
-    }
-
-    return hasOneAxisChanged;
+axes.saveRangeInitial = function (gd, overwrite) {
+  var axList = axes.list(gd, "", true);
+  var hasOneAxisChanged = false;
+
+  for (var i = 0; i < axList.length; i++) {
+    var ax = axList[i];
+    var isNew =
+      ax._rangeInitial0 === undefined && ax._rangeInitial1 === undefined;
+
+    var hasChanged =
+      isNew ||
+      ax.range[0] !== ax._rangeInitial0 ||
+      ax.range[1] !== ax._rangeInitial1;
+
+    var autorange = ax.autorange;
+    if ((isNew && autorange !== true) || (overwrite && hasChanged)) {
+      ax._rangeInitial0 =
+        autorange === "min" || autorange === "max reversed"
+          ? undefined
+          : ax.range[0];
+      ax._rangeInitial1 =
+        autorange === "max" || autorange === "min reversed"
+          ? undefined
+          : ax.range[1];
+      ax._autorangeInitial = autorange;
+      hasOneAxisChanged = true;
+    }
+  }
+
+  return hasOneAxisChanged;
 };
 
 // save a copy of the initial spike visibility
-axes.saveShowSpikeInitial = function(gd, overwrite) {
-    var axList = axes.list(gd, '', true);
-    var hasOneAxisChanged = false;
-    var allSpikesEnabled = 'on';
-
-    for(var i = 0; i < axList.length; i++) {
-        var ax = axList[i];
-        var isNew = (ax._showSpikeInitial === undefined);
-        var hasChanged = isNew || !(ax.showspikes === ax._showspikes);
-
-        if(isNew || (overwrite && hasChanged)) {
-            ax._showSpikeInitial = ax.showspikes;
-            hasOneAxisChanged = true;
-        }
-
-        if(allSpikesEnabled === 'on' && !ax.showspikes) {
-            allSpikesEnabled = 'off';
-        }
-    }
-    gd._fullLayout._cartesianSpikesEnabled = allSpikesEnabled;
-    return hasOneAxisChanged;
-};
-
-axes.autoBin = function(data, ax, nbins, is2d, calendar, size) {
-    var dataMin = Lib.aggNums(Math.min, null, data);
-    var dataMax = Lib.aggNums(Math.max, null, data);
-
-    if(ax.type === 'category' || ax.type === 'multicategory') {
-        return {
-            start: dataMin - 0.5,
-            end: dataMax + 0.5,
-            size: Math.max(1, Math.round(size) || 1),
-            _dataSpan: dataMax - dataMin,
-        };
-    }
+axes.saveShowSpikeInitial = function (gd, overwrite) {
+  var axList = axes.list(gd, "", true);
+  var hasOneAxisChanged = false;
+  var allSpikesEnabled = "on";
 
-    if(!calendar) calendar = ax.calendar;
+  for (var i = 0; i < axList.length; i++) {
+    var ax = axList[i];
+    var isNew = ax._showSpikeInitial === undefined;
+    var hasChanged = isNew || !(ax.showspikes === ax._showspikes);
 
-    // piggyback off tick code to make "nice" bin sizes and edges
-    var dummyAx;
-    if(ax.type === 'log') {
-        dummyAx = {
-            type: 'linear',
-            range: [dataMin, dataMax]
-        };
-    } else {
-        dummyAx = {
-            type: ax.type,
-            range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar),
-            calendar: calendar
-        };
+    if (isNew || (overwrite && hasChanged)) {
+      ax._showSpikeInitial = ax.showspikes;
+      hasOneAxisChanged = true;
     }
-    axes.setConvert(dummyAx);
-
-    size = size && cleanTicks.dtick(size, dummyAx.type);
-
-    if(size) {
-        dummyAx.dtick = size;
-        dummyAx.tick0 = cleanTicks.tick0(undefined, dummyAx.type, calendar);
-    } else {
-        var size0;
-        if(nbins) size0 = ((dataMax - dataMin) / nbins);
-        else {
-            // totally auto: scale off std deviation so the highest bin is
-            // somewhat taller than the total number of bins, but don't let
-            // the size get smaller than the 'nice' rounded down minimum
-            // difference between values
-            var distinctData = Lib.distinctVals(data);
-            var msexp = Math.pow(10, Math.floor(
-                Math.log(distinctData.minDiff) / Math.LN10));
-            var minSize = msexp * Lib.roundUp(
-                distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true);
-            size0 = Math.max(minSize, 2 * Lib.stdev(data) /
-                Math.pow(data.length, is2d ? 0.25 : 0.4));
-
-            // fallback if ax.d2c output BADNUMs
-            // e.g. when user try to plot categorical bins
-            // on a layout.xaxis.type: 'linear'
-            if(!isNumeric(size0)) size0 = 1;
-        }
 
-        axes.autoTicks(dummyAx, size0);
+    if (allSpikesEnabled === "on" && !ax.showspikes) {
+      allSpikesEnabled = "off";
     }
+  }
+  gd._fullLayout._cartesianSpikesEnabled = allSpikesEnabled;
+  return hasOneAxisChanged;
+};
 
-    var finalSize = dummyAx.dtick;
-    var binStart = axes.tickIncrement(
-            axes.tickFirst(dummyAx), finalSize, 'reverse', calendar);
-    var binEnd, bincount;
+axes.autoBin = function (data, ax, nbins, is2d, calendar, size) {
+  var dataMin = Lib.aggNums(Math.min, null, data);
+  var dataMax = Lib.aggNums(Math.max, null, data);
 
-    // check for too many data points right at the edges of bins
-    // (>50% within 1% of bin edges) or all data points integral
-    // and offset the bins accordingly
-    if(typeof finalSize === 'number') {
-        binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax);
+  if (ax.type === "category" || ax.type === "multicategory") {
+    return {
+      start: dataMin - 0.5,
+      end: dataMax + 0.5,
+      size: Math.max(1, Math.round(size) || 1),
+      _dataSpan: dataMax - dataMin,
+    };
+  }
 
-        bincount = 1 + Math.floor((dataMax - binStart) / finalSize);
-        binEnd = binStart + bincount * finalSize;
-    } else {
-        // month ticks - should be the only nonlinear kind we have at this point.
-        // dtick (as supplied by axes.autoTick) only has nonlinear values on
-        // date and log axes, but even if you display a histogram on a log axis
-        // we bin it on a linear axis (which one could argue against, but that's
-        // a separate issue)
-        if(dummyAx.dtick.charAt(0) === 'M') {
-            binStart = autoShiftMonthBins(binStart, data, finalSize, dataMin, calendar);
-        }
+  if (!calendar) calendar = ax.calendar;
 
-        // calculate the endpoint for nonlinear ticks - you have to
-        // just increment until you're done
-        binEnd = binStart;
-        bincount = 0;
-        while(binEnd <= dataMax) {
-            binEnd = axes.tickIncrement(binEnd, finalSize, false, calendar);
-            bincount++;
-        }
-    }
-
-    return {
-        start: ax.c2r(binStart, 0, calendar),
-        end: ax.c2r(binEnd, 0, calendar),
-        size: finalSize,
-        _dataSpan: dataMax - dataMin
+  // piggyback off tick code to make "nice" bin sizes and edges
+  var dummyAx;
+  if (ax.type === "log") {
+    dummyAx = {
+      type: "linear",
+      range: [dataMin, dataMax],
     };
+  } else {
+    dummyAx = {
+      type: ax.type,
+      range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar),
+      calendar: calendar,
+    };
+  }
+  axes.setConvert(dummyAx);
+
+  size = size && cleanTicks.dtick(size, dummyAx.type);
+
+  if (size) {
+    dummyAx.dtick = size;
+    dummyAx.tick0 = cleanTicks.tick0(undefined, dummyAx.type, calendar);
+  } else {
+    var size0;
+    if (nbins) size0 = (dataMax - dataMin) / nbins;
+    else {
+      // totally auto: scale off std deviation so the highest bin is
+      // somewhat taller than the total number of bins, but don't let
+      // the size get smaller than the 'nice' rounded down minimum
+      // difference between values
+      var distinctData = Lib.distinctVals(data);
+      var msexp = Math.pow(
+        10,
+        Math.floor(Math.log(distinctData.minDiff) / Math.LN10)
+      );
+      var minSize =
+        msexp *
+        Lib.roundUp(distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true);
+      size0 = Math.max(
+        minSize,
+        (2 * Lib.stdev(data)) / Math.pow(data.length, is2d ? 0.25 : 0.4)
+      );
+
+      // fallback if ax.d2c output BADNUMs
+      // e.g. when user try to plot categorical bins
+      // on a layout.xaxis.type: 'linear'
+      if (!isNumeric(size0)) size0 = 1;
+    }
+
+    axes.autoTicks(dummyAx, size0);
+  }
+
+  var finalSize = dummyAx.dtick;
+  var binStart = axes.tickIncrement(
+    axes.tickFirst(dummyAx),
+    finalSize,
+    "reverse",
+    calendar
+  );
+  var binEnd, bincount;
+
+  // check for too many data points right at the edges of bins
+  // (>50% within 1% of bin edges) or all data points integral
+  // and offset the bins accordingly
+  if (typeof finalSize === "number") {
+    binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax);
+
+    bincount = 1 + Math.floor((dataMax - binStart) / finalSize);
+    binEnd = binStart + bincount * finalSize;
+  } else {
+    // month ticks - should be the only nonlinear kind we have at this point.
+    // dtick (as supplied by axes.autoTick) only has nonlinear values on
+    // date and log axes, but even if you display a histogram on a log axis
+    // we bin it on a linear axis (which one could argue against, but that's
+    // a separate issue)
+    if (dummyAx.dtick.charAt(0) === "M") {
+      binStart = autoShiftMonthBins(
+        binStart,
+        data,
+        finalSize,
+        dataMin,
+        calendar
+      );
+    }
+
+    // calculate the endpoint for nonlinear ticks - you have to
+    // just increment until you're done
+    binEnd = binStart;
+    bincount = 0;
+    while (binEnd <= dataMax) {
+      binEnd = axes.tickIncrement(binEnd, finalSize, false, calendar);
+      bincount++;
+    }
+  }
+
+  return {
+    start: ax.c2r(binStart, 0, calendar),
+    end: ax.c2r(binEnd, 0, calendar),
+    size: finalSize,
+    _dataSpan: dataMax - dataMin,
+  };
 };
 
-
 function autoShiftNumericBins(binStart, data, ax, dataMin, dataMax) {
-    var edgecount = 0;
-    var midcount = 0;
-    var intcount = 0;
-    var blankCount = 0;
-
-    function nearEdge(v) {
-        // is a value within 1% of a bin edge?
-        return (1 + (v - binStart) * 100 / ax.dtick) % 100 < 2;
-    }
-
-    for(var i = 0; i < data.length; i++) {
-        if(data[i] % 1 === 0) intcount++;
-        else if(!isNumeric(data[i])) blankCount++;
-
-        if(nearEdge(data[i])) edgecount++;
-        if(nearEdge(data[i] + ax.dtick / 2)) midcount++;
-    }
-    var dataCount = data.length - blankCount;
-
-    if(intcount === dataCount && ax.type !== 'date') {
-        if(ax.dtick < 1) {
-            // all integers: if bin size is <1, it's because
-            // that was specifically requested (large nbins)
-            // so respect that... but center the bins containing
-            // integers on those integers
-
-            binStart = dataMin - 0.5 * ax.dtick;
-        } else {
-            // otherwise start half an integer down regardless of
-            // the bin size, just enough to clear up endpoint
-            // ambiguity about which integers are in which bins.
-
-            binStart -= 0.5;
-            if(binStart + ax.dtick < dataMin) binStart += ax.dtick;
-        }
-    } else if(midcount < dataCount * 0.1) {
-        if(edgecount > dataCount * 0.3 ||
-                nearEdge(dataMin) || nearEdge(dataMax)) {
-            // lots of points at the edge, not many in the middle
-            // shift half a bin
-            var binshift = ax.dtick / 2;
-            binStart += (binStart + binshift < dataMin) ? binshift : -binshift;
-        }
-    }
-    return binStart;
+  var edgecount = 0;
+  var midcount = 0;
+  var intcount = 0;
+  var blankCount = 0;
+
+  function nearEdge(v) {
+    // is a value within 1% of a bin edge?
+    return (1 + ((v - binStart) * 100) / ax.dtick) % 100 < 2;
+  }
+
+  for (var i = 0; i < data.length; i++) {
+    if (data[i] % 1 === 0) intcount++;
+    else if (!isNumeric(data[i])) blankCount++;
+
+    if (nearEdge(data[i])) edgecount++;
+    if (nearEdge(data[i] + ax.dtick / 2)) midcount++;
+  }
+  var dataCount = data.length - blankCount;
+
+  if (intcount === dataCount && ax.type !== "date") {
+    if (ax.dtick < 1) {
+      // all integers: if bin size is <1, it's because
+      // that was specifically requested (large nbins)
+      // so respect that... but center the bins containing
+      // integers on those integers
+
+      binStart = dataMin - 0.5 * ax.dtick;
+    } else {
+      // otherwise start half an integer down regardless of
+      // the bin size, just enough to clear up endpoint
+      // ambiguity about which integers are in which bins.
+
+      binStart -= 0.5;
+      if (binStart + ax.dtick < dataMin) binStart += ax.dtick;
+    }
+  } else if (midcount < dataCount * 0.1) {
+    if (edgecount > dataCount * 0.3 || nearEdge(dataMin) || nearEdge(dataMax)) {
+      // lots of points at the edge, not many in the middle
+      // shift half a bin
+      var binshift = ax.dtick / 2;
+      binStart += binStart + binshift < dataMin ? binshift : -binshift;
+    }
+  }
+  return binStart;
 }
 
-
 function autoShiftMonthBins(binStart, data, dtick, dataMin, calendar) {
-    var stats = Lib.findExactDates(data, calendar);
-    // number of data points that needs to be an exact value
-    // to shift that increment to (near) the bin center
-    var threshold = 0.8;
-
-    if(stats.exactDays > threshold) {
-        var numMonths = Number(dtick.substr(1));
-
-        if((stats.exactYears > threshold) && (numMonths % 12 === 0)) {
-            // The exact middle of a non-leap-year is 1.5 days into July
-            // so if we start the bins here, all but leap years will
-            // get hover-labeled as exact years.
-            binStart = axes.tickIncrement(binStart, 'M6', 'reverse') + ONEDAY * 1.5;
-        } else if(stats.exactMonths > threshold) {
-            // Months are not as clean, but if we shift half the *longest*
-            // month (31/2 days) then 31-day months will get labeled exactly
-            // and shorter months will get labeled with the correct month
-            // but shifted 12-36 hours into it.
-            binStart = axes.tickIncrement(binStart, 'M1', 'reverse') + ONEDAY * 15.5;
-        } else {
-            // Shifting half a day is exact, but since these are month bins it
-            // will always give a somewhat odd-looking label, until we do something
-            // smarter like showing the bin boundaries (or the bounds of the actual
-            // data in each bin)
-            binStart -= HALFDAY;
-        }
-        var nextBinStart = axes.tickIncrement(binStart, dtick);
-
-        if(nextBinStart <= dataMin) return nextBinStart;
+  var stats = Lib.findExactDates(data, calendar);
+  // number of data points that needs to be an exact value
+  // to shift that increment to (near) the bin center
+  var threshold = 0.8;
+
+  if (stats.exactDays > threshold) {
+    var numMonths = Number(dtick.substr(1));
+
+    if (stats.exactYears > threshold && numMonths % 12 === 0) {
+      // The exact middle of a non-leap-year is 1.5 days into July
+      // so if we start the bins here, all but leap years will
+      // get hover-labeled as exact years.
+      binStart = axes.tickIncrement(binStart, "M6", "reverse") + ONEDAY * 1.5;
+    } else if (stats.exactMonths > threshold) {
+      // Months are not as clean, but if we shift half the *longest*
+      // month (31/2 days) then 31-day months will get labeled exactly
+      // and shorter months will get labeled with the correct month
+      // but shifted 12-36 hours into it.
+      binStart = axes.tickIncrement(binStart, "M1", "reverse") + ONEDAY * 15.5;
+    } else {
+      // Shifting half a day is exact, but since these are month bins it
+      // will always give a somewhat odd-looking label, until we do something
+      // smarter like showing the bin boundaries (or the bounds of the actual
+      // data in each bin)
+      binStart -= HALFDAY;
     }
-    return binStart;
+    var nextBinStart = axes.tickIncrement(binStart, dtick);
+
+    if (nextBinStart <= dataMin) return nextBinStart;
+  }
+  return binStart;
 }
 
 // ----------------------------------------------------
@@ -557,346 +610,352 @@ function autoShiftMonthBins(binStart, data, dtick, dataMin, calendar) {
 // ----------------------------------------------------
 
 // ensure we have minor tick0 and dtick calculated
-axes.prepMinorTicks = function(mockAx, ax, opts) {
-    if(!ax.minor.dtick) {
-        delete mockAx.dtick;
-        var hasMajor = ax.dtick && isNumeric(ax._tmin);
-        var mockMinorRange;
-        if(hasMajor) {
-            var tick2 = axes.tickIncrement(ax._tmin, ax.dtick, true);
-            // mock range a tiny bit smaller than one major tick interval
-            mockMinorRange = [ax._tmin, tick2 * 0.99 + ax._tmin * 0.01];
+axes.prepMinorTicks = function (mockAx, ax, opts) {
+  if (!ax.minor.dtick) {
+    delete mockAx.dtick;
+    var hasMajor = ax.dtick && isNumeric(ax._tmin);
+    var mockMinorRange;
+    if (hasMajor) {
+      var tick2 = axes.tickIncrement(ax._tmin, ax.dtick, true);
+      // mock range a tiny bit smaller than one major tick interval
+      mockMinorRange = [ax._tmin, tick2 * 0.99 + ax._tmin * 0.01];
+    } else {
+      var rl = Lib.simpleMap(ax.range, ax.r2l);
+      // If we don't have a major dtick, the concept of minor ticks is a little
+      // ambiguous - just take a stab and say minor.nticks should span 1/5 the axis
+      mockMinorRange = [rl[0], 0.8 * rl[0] + 0.2 * rl[1]];
+    }
+    mockAx.range = Lib.simpleMap(mockMinorRange, ax.l2r);
+    mockAx._isMinor = true;
+
+    axes.prepTicks(mockAx, opts);
+
+    if (hasMajor) {
+      var numericMajor = isNumeric(ax.dtick);
+      var numericMinor = isNumeric(mockAx.dtick);
+      var majorNum = numericMajor ? ax.dtick : +ax.dtick.substring(1);
+      var minorNum = numericMinor ? mockAx.dtick : +mockAx.dtick.substring(1);
+      if (numericMajor && numericMinor) {
+        if (!isMultiple(majorNum, minorNum)) {
+          // give up on minor ticks - outside the below exceptions,
+          // this can only happen if minor.nticks is smaller than two jumps
+          // in the auto-tick scale and the first jump is not an even multiple
+          // (5 -> 2 or for dates 3 ->2, 15 -> 10 etc)  or if you provided
+          // an explicit dtick, in which case it's fine to give up,
+          // you can provide an explicit minor.dtick.
+          if (majorNum === 2 * ONEWEEK && minorNum === 3 * ONEDAY) {
+            mockAx.dtick = ONEWEEK;
+          } else if (majorNum === ONEWEEK && !(ax._input.minor || {}).nticks) {
+            // minor.nticks defaults to 5, but in this one case we want 7,
+            // so the minor ticks show on all days of the week
+            mockAx.dtick = ONEDAY;
+          } else if (isClose(majorNum / minorNum, 2.5)) {
+            // 5*10^n -> 2*10^n and you've set nticks < 5
+            // quarters are pretty common, we don't do this by default as it
+            // would add an extra digit to display, but minor has no labels
+            mockAx.dtick = majorNum / 2;
+          } else {
+            mockAx.dtick = majorNum;
+          }
+        } else if (majorNum === 2 * ONEWEEK && minorNum === 2 * ONEDAY) {
+          // this is a weird one: we don't want to automatically choose
+          // 2-day minor ticks for 2-week major, even though it IS an even multiple,
+          // because people would expect to see the weeks clearly
+          mockAx.dtick = ONEWEEK;
+        }
+      } else if (String(ax.dtick).charAt(0) === "M") {
+        if (numericMinor) {
+          mockAx.dtick = "M1";
         } else {
-            var rl = Lib.simpleMap(ax.range, ax.r2l);
-            // If we don't have a major dtick, the concept of minor ticks is a little
-            // ambiguous - just take a stab and say minor.nticks should span 1/5 the axis
-            mockMinorRange = [rl[0], 0.8 * rl[0] + 0.2 * rl[1]];
+          if (!isMultiple(majorNum, minorNum)) {
+            // unless you provided an explicit ax.dtick (in which case
+            // it's OK for us to give up, you can provide an explicit
+            // minor.dtick too), this can only happen with:
+            // minor.nticks < 3 and dtick === M3, or
+            // minor.nticks < 5 and dtick === 5 * 10^n years
+            // so in all cases we just give up.
+            mockAx.dtick = ax.dtick;
+          } else if (majorNum >= 12 && minorNum === 2) {
+            // another special carve-out: for year major ticks, don't show
+            // 2-month minor ticks, bump to quarters
+            mockAx.dtick = "M3";
+          }
         }
-        mockAx.range = Lib.simpleMap(mockMinorRange, ax.l2r);
-        mockAx._isMinor = true;
-
-        axes.prepTicks(mockAx, opts);
-
-        if(hasMajor) {
-            var numericMajor = isNumeric(ax.dtick);
-            var numericMinor = isNumeric(mockAx.dtick);
-            var majorNum = numericMajor ? ax.dtick : +ax.dtick.substring(1);
-            var minorNum = numericMinor ? mockAx.dtick : +mockAx.dtick.substring(1);
-            if(numericMajor && numericMinor) {
-                if(!isMultiple(majorNum, minorNum)) {
-                    // give up on minor ticks - outside the below exceptions,
-                    // this can only happen if minor.nticks is smaller than two jumps
-                    // in the auto-tick scale and the first jump is not an even multiple
-                    // (5 -> 2 or for dates 3 ->2, 15 -> 10 etc)  or if you provided
-                    // an explicit dtick, in which case it's fine to give up,
-                    // you can provide an explicit minor.dtick.
-                    if((majorNum === 2 * ONEWEEK) && (minorNum === 3 * ONEDAY)) {
-                        mockAx.dtick = ONEWEEK;
-                    } else if(majorNum === ONEWEEK && !(ax._input.minor || {}).nticks) {
-                        // minor.nticks defaults to 5, but in this one case we want 7,
-                        // so the minor ticks show on all days of the week
-                        mockAx.dtick = ONEDAY;
-                    } else if(isClose(majorNum / minorNum, 2.5)) {
-                        // 5*10^n -> 2*10^n and you've set nticks < 5
-                        // quarters are pretty common, we don't do this by default as it
-                        // would add an extra digit to display, but minor has no labels
-                        mockAx.dtick = majorNum / 2;
-                    } else {
-                        mockAx.dtick = majorNum;
-                    }
-                } else if(majorNum === 2 * ONEWEEK && minorNum === 2 * ONEDAY) {
-                    // this is a weird one: we don't want to automatically choose
-                    // 2-day minor ticks for 2-week major, even though it IS an even multiple,
-                    // because people would expect to see the weeks clearly
-                    mockAx.dtick = ONEWEEK;
-                }
-            } else if(String(ax.dtick).charAt(0) === 'M') {
-                if(numericMinor) {
-                    mockAx.dtick = 'M1';
-                } else {
-                    if(!isMultiple(majorNum, minorNum)) {
-                        // unless you provided an explicit ax.dtick (in which case
-                        // it's OK for us to give up, you can provide an explicit
-                        // minor.dtick too), this can only happen with:
-                        // minor.nticks < 3 and dtick === M3, or
-                        // minor.nticks < 5 and dtick === 5 * 10^n years
-                        // so in all cases we just give up.
-                        mockAx.dtick = ax.dtick;
-                    } else if((majorNum >= 12) && (minorNum === 2)) {
-                        // another special carve-out: for year major ticks, don't show
-                        // 2-month minor ticks, bump to quarters
-                        mockAx.dtick = 'M3';
-                    }
-                }
-            } else if(String(mockAx.dtick).charAt(0) === 'L') {
-                if(String(ax.dtick).charAt(0) === 'L') {
-                    if(!isMultiple(majorNum, minorNum)) {
-                        mockAx.dtick = isClose(majorNum / minorNum, 2.5) ? (ax.dtick / 2) : ax.dtick;
-                    }
-                } else {
-                    mockAx.dtick = 'D1';
-                }
-            } else if(mockAx.dtick === 'D2' && +ax.dtick > 1) {
-                // the D2 log axis tick spacing is confusing for unlabeled minor ticks if
-                // the major dtick is more than one order of magnitude.
-                mockAx.dtick = 1;
-            }
+      } else if (String(mockAx.dtick).charAt(0) === "L") {
+        if (String(ax.dtick).charAt(0) === "L") {
+          if (!isMultiple(majorNum, minorNum)) {
+            mockAx.dtick = isClose(majorNum / minorNum, 2.5)
+              ? ax.dtick / 2
+              : ax.dtick;
+          }
+        } else {
+          mockAx.dtick = "D1";
         }
-        // put back the original range, to use to find the full set of minor ticks
-        mockAx.range = ax.range;
-    }
-    if(ax.minor._tick0Init === undefined) {
-        // ensure identical tick0
-        mockAx.tick0 = ax.tick0;
-    }
+      } else if (mockAx.dtick === "D2" && +ax.dtick > 1) {
+        // the D2 log axis tick spacing is confusing for unlabeled minor ticks if
+        // the major dtick is more than one order of magnitude.
+        mockAx.dtick = 1;
+      }
+    }
+    // put back the original range, to use to find the full set of minor ticks
+    mockAx.range = ax.range;
+  }
+  if (ax.minor._tick0Init === undefined) {
+    // ensure identical tick0
+    mockAx.tick0 = ax.tick0;
+  }
 };
 
 function isMultiple(bigger, smaller) {
-    return Math.abs((bigger / smaller + 0.5) % 1 - 0.5) < 0.001;
+  return Math.abs(((bigger / smaller + 0.5) % 1) - 0.5) < 0.001;
 }
 
 function isClose(a, b) {
-    return Math.abs((a / b) - 1) < 0.001;
+  return Math.abs(a / b - 1) < 0.001;
 }
 
 // ensure we have tick0, dtick, and tick rounding calculated
-axes.prepTicks = function(ax, opts) {
-    var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);
-
-    // calculate max number of (auto) ticks to display based on plot size
-    if(ax.tickmode === 'auto' || !ax.dtick) {
-        var nt = ax.nticks;
-        var minPx;
-
-        if(!nt) {
-            if(ax.type === 'category' || ax.type === 'multicategory') {
-                minPx = ax.tickfont ? Lib.bigFont(ax.tickfont.size || 12) : 15;
-                nt = ax._length / minPx;
-            } else {
-                minPx = ax._id.charAt(0) === 'y' ? 40 : 80;
-                nt = Lib.constrain(ax._length / minPx, 4, 9) + 1;
-            }
-
-            // radial axes span half their domain,
-            // multiply nticks value by two to get correct number of auto ticks.
-            if(ax._name === 'radialaxis') nt *= 2;
-        }
+axes.prepTicks = function (ax, opts) {
+  var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);
 
-        if(!(ax.minor && ax.minor.tickmode !== 'array')) {
-            // add a couple of extra digits for filling in ticks when we
-            // have explicit tickvals without tick text
-            if(ax.tickmode === 'array') nt *= 100;
-        }
+  // calculate max number of (auto) ticks to display based on plot size
+  if (ax.tickmode === "auto" || !ax.dtick) {
+    var nt = ax.nticks;
+    var minPx;
 
-        ax._roughDTick = Math.abs(rng[1] - rng[0]) / nt;
-        axes.autoTicks(ax, ax._roughDTick);
+    if (!nt) {
+      if (ax.type === "category" || ax.type === "multicategory") {
+        minPx = ax.tickfont ? Lib.bigFont(ax.tickfont.size || 12) : 15;
+        nt = ax._length / minPx;
+      } else {
+        minPx = ax._id.charAt(0) === "y" ? 40 : 80;
+        nt = Lib.constrain(ax._length / minPx, 4, 9) + 1;
+      }
 
-        // check for a forced minimum dtick
-        if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) {
-            ax.dtick = ax._minDtick;
-            ax.tick0 = ax.l2r(ax._forceTick0);
-        }
+      // radial axes span half their domain,
+      // multiply nticks value by two to get correct number of auto ticks.
+      if (ax._name === "radialaxis") nt *= 2;
     }
 
-    if(ax.ticklabelmode === 'period') {
-        adjustPeriodDelta(ax);
+    if (!(ax.minor && ax.minor.tickmode !== "array")) {
+      // add a couple of extra digits for filling in ticks when we
+      // have explicit tickvals without tick text
+      if (ax.tickmode === "array") nt *= 100;
     }
 
-    // check for missing tick0
-    if(!ax.tick0) {
-        ax.tick0 = (ax.type === 'date') ? '2000-01-01' : 0;
+    ax._roughDTick = Math.abs(rng[1] - rng[0]) / nt;
+    axes.autoTicks(ax, ax._roughDTick);
+
+    // check for a forced minimum dtick
+    if (ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) {
+      ax.dtick = ax._minDtick;
+      ax.tick0 = ax.l2r(ax._forceTick0);
     }
+  }
+
+  if (ax.ticklabelmode === "period") {
+    adjustPeriodDelta(ax);
+  }
+
+  // check for missing tick0
+  if (!ax.tick0) {
+    ax.tick0 = ax.type === "date" ? "2000-01-01" : 0;
+  }
 
-    // ensure we don't try to make ticks below our minimum precision
-    // see https://github.com/plotly/plotly.js/issues/2892
-    if(ax.type === 'date' && ax.dtick < 0.1) ax.dtick = 0.1;
+  // ensure we don't try to make ticks below our minimum precision
+  // see https://github.com/plotly/plotly.js/issues/2892
+  if (ax.type === "date" && ax.dtick < 0.1) ax.dtick = 0.1;
 
-    // now figure out rounding of tick values
-    autoTickRound(ax);
+  // now figure out rounding of tick values
+  autoTickRound(ax);
 };
 
 function nMonths(dtick) {
-    return +(dtick.substring(1));
+  return +dtick.substring(1);
 }
 
-function adjustPeriodDelta(ax) { // adjusts ax.dtick and sets ax._definedDelta
-    var definedDelta;
-
-    function mDate() {
-        return !(
-            isNumeric(ax.dtick) ||
-            ax.dtick.charAt(0) !== 'M'
-        );
-    }
-    var isMDate = mDate();
-    var tickformat = axes.getTickFormat(ax);
-    if(tickformat) {
-        var noDtick = ax._dtickInit !== ax.dtick;
-        if(
-            !(/%[fLQsSMX]/.test(tickformat))
-            // %f: microseconds as a decimal number [000000, 999999]
-            // %L: milliseconds as a decimal number [000, 999]
-            // %Q: milliseconds since UNIX epoch
-            // %s: seconds since UNIX epoch
-            // %S: second as a decimal number [00,61]
-            // %M: minute as a decimal number [00,59]
-            // %X: the locale’s time, such as %-I:%M:%S %p
-        ) {
-            if(
-                /%[HI]/.test(tickformat)
-                // %H: hour (24-hour clock) as a decimal number [00,23]
-                // %I: hour (12-hour clock) as a decimal number [01,12]
-            ) {
-                definedDelta = ONEHOUR;
-                if(noDtick && !isMDate && ax.dtick < ONEHOUR) ax.dtick = ONEHOUR;
-            } else if(
-                /%p/.test(tickformat) // %p: either AM or PM
-            ) {
-                definedDelta = HALFDAY;
-                if(noDtick && !isMDate && ax.dtick < HALFDAY) ax.dtick = HALFDAY;
-            } else if(
-                /%[Aadejuwx]/.test(tickformat)
-                // %A: full weekday name
-                // %a: abbreviated weekday name
-                // %d: zero-padded day of the month as a decimal number [01,31]
-                // %e: space-padded day of the month as a decimal number [ 1,31]
-                // %j: day of the year as a decimal number [001,366]
-                // %u: Monday-based (ISO 8601) weekday as a decimal number [1,7]
-                // %w: Sunday-based weekday as a decimal number [0,6]
-                // %x: the locale’s date, such as %-m/%-d/%Y
-            ) {
-                definedDelta = ONEDAY;
-                if(noDtick && !isMDate && ax.dtick < ONEDAY) ax.dtick = ONEDAY;
-            } else if(
-                /%[UVW]/.test(tickformat)
-                // %U: Sunday-based week of the year as a decimal number [00,53]
-                // %V: ISO 8601 week of the year as a decimal number [01, 53]
-                // %W: Monday-based week of the year as a decimal number [00,53]
-            ) {
-                definedDelta = ONEWEEK;
-                if(noDtick && !isMDate && ax.dtick < ONEWEEK) ax.dtick = ONEWEEK;
-            } else if(
-                /%[Bbm]/.test(tickformat)
-                // %B: full month name
-                // %b: abbreviated month name
-                // %m: month as a decimal number [01,12]
-            ) {
-                definedDelta = ONEAVGMONTH;
-                if(noDtick && (
-                    isMDate ? nMonths(ax.dtick) < 1 : ax.dtick < ONEMINMONTH)
-                ) ax.dtick = 'M1';
-            } else if(
-                /%[q]/.test(tickformat)
-                // %q: quarter of the year as a decimal number [1,4]
-            ) {
-                definedDelta = ONEAVGQUARTER;
-                if(noDtick && (
-                    isMDate ? nMonths(ax.dtick) < 3 : ax.dtick < ONEMINQUARTER)
-                ) ax.dtick = 'M3';
-            } else if(
-                /%[Yy]/.test(tickformat)
-                // %Y: year with century as a decimal number, such as 1999
-                // %y: year without century as a decimal number [00,99]
-            ) {
-                definedDelta = ONEAVGYEAR;
-                if(noDtick && (
-                    isMDate ? nMonths(ax.dtick) < 12 : ax.dtick < ONEMINYEAR)
-                ) ax.dtick = 'M12';
-            }
-        }
+function adjustPeriodDelta(ax) {
+  // adjusts ax.dtick and sets ax._definedDelta
+  var definedDelta;
+
+  function mDate() {
+    return !(isNumeric(ax.dtick) || ax.dtick.charAt(0) !== "M");
+  }
+  var isMDate = mDate();
+  var tickformat = axes.getTickFormat(ax);
+  if (tickformat) {
+    var noDtick = ax._dtickInit !== ax.dtick;
+    if (
+      !/%[fLQsSMX]/.test(tickformat)
+      // %f: microseconds as a decimal number [000000, 999999]
+      // %L: milliseconds as a decimal number [000, 999]
+      // %Q: milliseconds since UNIX epoch
+      // %s: seconds since UNIX epoch
+      // %S: second as a decimal number [00,61]
+      // %M: minute as a decimal number [00,59]
+      // %X: the locale’s time, such as %-I:%M:%S %p
+    ) {
+      if (
+        /%[HI]/.test(tickformat)
+        // %H: hour (24-hour clock) as a decimal number [00,23]
+        // %I: hour (12-hour clock) as a decimal number [01,12]
+      ) {
+        definedDelta = ONEHOUR;
+        if (noDtick && !isMDate && ax.dtick < ONEHOUR) ax.dtick = ONEHOUR;
+      } else if (
+        /%p/.test(tickformat) // %p: either AM or PM
+      ) {
+        definedDelta = HALFDAY;
+        if (noDtick && !isMDate && ax.dtick < HALFDAY) ax.dtick = HALFDAY;
+      } else if (
+        /%[Aadejuwx]/.test(tickformat)
+        // %A: full weekday name
+        // %a: abbreviated weekday name
+        // %d: zero-padded day of the month as a decimal number [01,31]
+        // %e: space-padded day of the month as a decimal number [ 1,31]
+        // %j: day of the year as a decimal number [001,366]
+        // %u: Monday-based (ISO 8601) weekday as a decimal number [1,7]
+        // %w: Sunday-based weekday as a decimal number [0,6]
+        // %x: the locale’s date, such as %-m/%-d/%Y
+      ) {
+        definedDelta = ONEDAY;
+        if (noDtick && !isMDate && ax.dtick < ONEDAY) ax.dtick = ONEDAY;
+      } else if (
+        /%[UVW]/.test(tickformat)
+        // %U: Sunday-based week of the year as a decimal number [00,53]
+        // %V: ISO 8601 week of the year as a decimal number [01, 53]
+        // %W: Monday-based week of the year as a decimal number [00,53]
+      ) {
+        definedDelta = ONEWEEK;
+        if (noDtick && !isMDate && ax.dtick < ONEWEEK) ax.dtick = ONEWEEK;
+      } else if (
+        /%[Bbm]/.test(tickformat)
+        // %B: full month name
+        // %b: abbreviated month name
+        // %m: month as a decimal number [01,12]
+      ) {
+        definedDelta = ONEAVGMONTH;
+        if (
+          noDtick &&
+          (isMDate ? nMonths(ax.dtick) < 1 : ax.dtick < ONEMINMONTH)
+        )
+          ax.dtick = "M1";
+      } else if (
+        /%[q]/.test(tickformat)
+        // %q: quarter of the year as a decimal number [1,4]
+      ) {
+        definedDelta = ONEAVGQUARTER;
+        if (
+          noDtick &&
+          (isMDate ? nMonths(ax.dtick) < 3 : ax.dtick < ONEMINQUARTER)
+        )
+          ax.dtick = "M3";
+      } else if (
+        /%[Yy]/.test(tickformat)
+        // %Y: year with century as a decimal number, such as 1999
+        // %y: year without century as a decimal number [00,99]
+      ) {
+        definedDelta = ONEAVGYEAR;
+        if (
+          noDtick &&
+          (isMDate ? nMonths(ax.dtick) < 12 : ax.dtick < ONEMINYEAR)
+        )
+          ax.dtick = "M12";
+      }
     }
+  }
 
-    isMDate = mDate();
-    if(isMDate && ax.tick0 === ax._dowTick0) {
-        // discard Sunday/Monday tweaks
-        ax.tick0 = ax._rawTick0;
-    }
+  isMDate = mDate();
+  if (isMDate && ax.tick0 === ax._dowTick0) {
+    // discard Sunday/Monday tweaks
+    ax.tick0 = ax._rawTick0;
+  }
 
-    ax._definedDelta = definedDelta;
+  ax._definedDelta = definedDelta;
 }
 
 function positionPeriodTicks(tickVals, ax, definedDelta) {
-    for(var i = 0; i < tickVals.length; i++) {
-        var v = tickVals[i].value;
-
-        var a = i;
-        var b = i + 1;
-        if(i < tickVals.length - 1) {
-            a = i;
-            b = i + 1;
-        } else if(i > 0) {
-            a = i - 1;
-            b = i;
-        } else {
-            a = i;
-            b = i;
-        }
-
-        var A = tickVals[a].value;
-        var B = tickVals[b].value;
-        var actualDelta = Math.abs(B - A);
-        var delta = definedDelta || actualDelta;
-        var periodLength = 0;
-
-        if(delta >= ONEMINYEAR) {
-            if(actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) {
-                periodLength = actualDelta;
-            } else {
-                periodLength = ONEAVGYEAR;
-            }
-        } else if(definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) {
-            if(actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) {
-                periodLength = actualDelta;
-            } else {
-                periodLength = ONEAVGQUARTER;
-            }
-        } else if(delta >= ONEMINMONTH) {
-            if(actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) {
-                periodLength = actualDelta;
-            } else {
-                periodLength = ONEAVGMONTH;
-            }
-        } else if(definedDelta === ONEWEEK && delta >= ONEWEEK) {
-            periodLength = ONEWEEK;
-        } else if(delta >= ONEDAY) {
-            periodLength = ONEDAY;
-        } else if(definedDelta === HALFDAY && delta >= HALFDAY) {
-            periodLength = HALFDAY;
-        } else if(definedDelta === ONEHOUR && delta >= ONEHOUR) {
-            periodLength = ONEHOUR;
-        }
-
-        var inBetween;
-        if(periodLength >= actualDelta) {
-            // ensure new label positions remain between ticks
-            periodLength = actualDelta;
-            inBetween = true;
-        }
-
-        var endPeriod = v + periodLength;
-        if(ax.rangebreaks && periodLength > 0) {
-            var nAll = 84; // highly divisible 7 * 12
-            var n = 0;
-            for(var c = 0; c < nAll; c++) {
-                var r = (c + 0.5) / nAll;
-                if(ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++;
-            }
-            periodLength *= n / nAll;
-
-            if(!periodLength) {
-                tickVals[i].drop = true;
-            }
-
-            if(inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods
-        }
-
-        if(
-            periodLength > 0 || // not instant
-            i === 0 // taking care first tick added
-        ) {
-            tickVals[i].periodX = v + periodLength / 2;
-        }
+  for (var i = 0; i < tickVals.length; i++) {
+    var v = tickVals[i].value;
+
+    var a = i;
+    var b = i + 1;
+    if (i < tickVals.length - 1) {
+      a = i;
+      b = i + 1;
+    } else if (i > 0) {
+      a = i - 1;
+      b = i;
+    } else {
+      a = i;
+      b = i;
+    }
+
+    var A = tickVals[a].value;
+    var B = tickVals[b].value;
+    var actualDelta = Math.abs(B - A);
+    var delta = definedDelta || actualDelta;
+    var periodLength = 0;
+
+    if (delta >= ONEMINYEAR) {
+      if (actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) {
+        periodLength = actualDelta;
+      } else {
+        periodLength = ONEAVGYEAR;
+      }
+    } else if (definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) {
+      if (actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) {
+        periodLength = actualDelta;
+      } else {
+        periodLength = ONEAVGQUARTER;
+      }
+    } else if (delta >= ONEMINMONTH) {
+      if (actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) {
+        periodLength = actualDelta;
+      } else {
+        periodLength = ONEAVGMONTH;
+      }
+    } else if (definedDelta === ONEWEEK && delta >= ONEWEEK) {
+      periodLength = ONEWEEK;
+    } else if (delta >= ONEDAY) {
+      periodLength = ONEDAY;
+    } else if (definedDelta === HALFDAY && delta >= HALFDAY) {
+      periodLength = HALFDAY;
+    } else if (definedDelta === ONEHOUR && delta >= ONEHOUR) {
+      periodLength = ONEHOUR;
+    }
+
+    var inBetween;
+    if (periodLength >= actualDelta) {
+      // ensure new label positions remain between ticks
+      periodLength = actualDelta;
+      inBetween = true;
+    }
+
+    var endPeriod = v + periodLength;
+    if (ax.rangebreaks && periodLength > 0) {
+      var nAll = 84; // highly divisible 7 * 12
+      var n = 0;
+      for (var c = 0; c < nAll; c++) {
+        var r = (c + 0.5) / nAll;
+        if (ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++;
+      }
+      periodLength *= n / nAll;
+
+      if (!periodLength) {
+        tickVals[i].drop = true;
+      }
+
+      if (inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods
+    }
+
+    if (
+      periodLength > 0 || // not instant
+      i === 0 // taking care first tick added
+    ) {
+      tickVals[i].periodX = v + periodLength / 2;
     }
+  }
 }
 
 // calculate the ticks: text, values, positioning
@@ -904,412 +963,411 @@ function positionPeriodTicks(tickVals, ax, definedDelta) {
 // in any case, set tickround to # of digits to round tick labels to,
 // or codes to this effect for log and date scales
 axes.calcTicks = function calcTicks(ax, opts) {
-    var type = ax.type;
-    var calendar = ax.calendar;
-    var ticklabelstep = ax.ticklabelstep;
-    var isPeriod = ax.ticklabelmode === 'period';
-
-    var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);
-    var axrev = (rng[1] < rng[0]);
-    var minRange = Math.min(rng[0], rng[1]);
-    var maxRange = Math.max(rng[0], rng[1]);
+  var type = ax.type;
+  var calendar = ax.calendar;
+  var ticklabelstep = ax.ticklabelstep;
+  var isPeriod = ax.ticklabelmode === "period";
 
-    var maxTicks = Math.max(1000, ax._length || 0);
+  var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);
+  var axrev = rng[1] < rng[0];
+  var minRange = Math.min(rng[0], rng[1]);
+  var maxRange = Math.max(rng[0], rng[1]);
 
-    var ticksOut = [];
-    var minorTicks = [];
+  var maxTicks = Math.max(1000, ax._length || 0);
 
-    var tickVals = [];
-    var minorTickVals = [];
+  var ticksOut = [];
+  var minorTicks = [];
 
-    var hasMinor = ax.minor && (ax.minor.ticks || ax.minor.showgrid);
+  var tickVals = [];
+  var minorTickVals = [];
 
-    // calc major first
-    for(var major = 1; major >= (hasMinor ? 0 : 1); major--) {
-        var isMinor = !major;
+  var hasMinor = ax.minor && (ax.minor.ticks || ax.minor.showgrid);
 
-        if(major) {
-            ax._dtickInit = ax.dtick;
-            ax._tick0Init = ax.tick0;
-        } else {
-            ax.minor._dtickInit = ax.minor.dtick;
-            ax.minor._tick0Init = ax.minor.tick0;
-        }
-
-        var mockAx = major ? ax : Lib.extendFlat({}, ax, ax.minor);
-
-        if(isMinor) {
-            axes.prepMinorTicks(mockAx, ax, opts);
-        } else {
-            axes.prepTicks(mockAx, opts);
-        }
+  // calc major first
+  for (var major = 1; major >= (hasMinor ? 0 : 1); major--) {
+    var isMinor = !major;
 
-        // now that we've figured out the auto values for formatting
-        // in case we're missing some ticktext, we can break out for array ticks
-        if(mockAx.tickmode === 'array') {
-            if(major) {
-                tickVals = [];
-                ticksOut = arrayTicks(ax);
-            } else {
-                minorTickVals = [];
-                minorTicks = arrayTicks(ax);
-            }
-            continue;
-        }
-
-        // fill tickVals based on overlaying axis
-        if(mockAx.tickmode === 'sync') {
-            tickVals = [];
-            ticksOut = syncTicks(ax);
-            continue;
-        }
-
-        // add a tiny bit so we get ticks which may have rounded out
-        var exRng = expandRange(rng);
-        var startTick = exRng[0];
-        var endTick = exRng[1];
-
-        var numDtick = isNumeric(mockAx.dtick);
-        var isDLog = (type === 'log') && !(numDtick || mockAx.dtick.charAt(0) === 'L');
+    if (major) {
+      ax._dtickInit = ax.dtick;
+      ax._tick0Init = ax.tick0;
+    } else {
+      ax.minor._dtickInit = ax.minor.dtick;
+      ax.minor._tick0Init = ax.minor.tick0;
+    }
 
-        // find the first tick
-        var x0 = axes.tickFirst(mockAx, opts);
+    var mockAx = major ? ax : Lib.extendFlat({}, ax, ax.minor);
 
-        if(major) {
-            ax._tmin = x0;
+    if (isMinor) {
+      axes.prepMinorTicks(mockAx, ax, opts);
+    } else {
+      axes.prepTicks(mockAx, opts);
+    }
 
-            // No visible ticks? Quit.
-            // I've only seen this on category axes with all categories off the edge.
-            if((x0 < startTick) !== axrev) break;
+    // now that we've figured out the auto values for formatting
+    // in case we're missing some ticktext, we can break out for array ticks
+    if (mockAx.tickmode === "array") {
+      if (major) {
+        tickVals = [];
+        ticksOut = arrayTicks(ax);
+      } else {
+        minorTickVals = [];
+        minorTicks = arrayTicks(ax);
+      }
+      continue;
+    }
 
-            // return the full set of tick vals
-            if(type === 'category' || type === 'multicategory') {
-                endTick = (axrev) ? Math.max(-0.5, endTick) :
-                    Math.min(ax._categories.length - 0.5, endTick);
-            }
-        }
+    // fill tickVals based on overlaying axis
+    if (mockAx.tickmode === "sync") {
+      tickVals = [];
+      ticksOut = syncTicks(ax);
+      continue;
+    }
 
-        var prevX = null;
-        var x = x0;
-        var majorId;
-
-        if(major) {
-            // ids for ticklabelstep
-            var _dTick;
-            if(numDtick) {
-                _dTick = ax.dtick;
-            } else {
-                if(type === 'date') {
-                    if(typeof ax.dtick === 'string' && ax.dtick.charAt(0) === 'M') {
-                        _dTick = ONEAVGMONTH * ax.dtick.substring(1);
-                    }
-                } else {
-                    _dTick = ax._roughDTick;
-                }
-            }
-
-            majorId = Math.round((
-                ax.r2l(x) -
-                ax.r2l(ax.tick0)
-            ) / _dTick) - 1;
+    // add a tiny bit so we get ticks which may have rounded out
+    var exRng = expandRange(rng);
+    var startTick = exRng[0];
+    var endTick = exRng[1];
+
+    var numDtick = isNumeric(mockAx.dtick);
+    var isDLog =
+      type === "log" && !(numDtick || mockAx.dtick.charAt(0) === "L");
+
+    // find the first tick
+    var x0 = axes.tickFirst(mockAx, opts);
+
+    if (major) {
+      ax._tmin = x0;
+
+      // No visible ticks? Quit.
+      // I've only seen this on category axes with all categories off the edge.
+      if (x0 < startTick !== axrev) break;
+
+      // return the full set of tick vals
+      if (type === "category" || type === "multicategory") {
+        endTick = axrev
+          ? Math.max(-0.5, endTick)
+          : Math.min(ax._categories.length - 0.5, endTick);
+      }
+    }
+
+    var prevX = null;
+    var x = x0;
+    var majorId;
+
+    if (major) {
+      // ids for ticklabelstep
+      var _dTick;
+      if (numDtick) {
+        _dTick = ax.dtick;
+      } else {
+        if (type === "date") {
+          if (typeof ax.dtick === "string" && ax.dtick.charAt(0) === "M") {
+            _dTick = ONEAVGMONTH * ax.dtick.substring(1);
+          }
+        } else {
+          _dTick = ax._roughDTick;
         }
+      }
 
-        var dtick = mockAx.dtick;
+      majorId = Math.round((ax.r2l(x) - ax.r2l(ax.tick0)) / _dTick) - 1;
+    }
 
-        if(mockAx.rangebreaks && mockAx._tick0Init !== mockAx.tick0) {
-            // adjust tick0
-            x = moveOutsideBreak(x, ax);
-            if(!axrev) {
-                x = axes.tickIncrement(x, dtick, !axrev, calendar);
-            }
-        }
+    var dtick = mockAx.dtick;
 
-        if(major && isPeriod) {
-            // add one item to label period before tick0
-            x = axes.tickIncrement(x, dtick, !axrev, calendar);
-            majorId--;
-        }
+    if (mockAx.rangebreaks && mockAx._tick0Init !== mockAx.tick0) {
+      // adjust tick0
+      x = moveOutsideBreak(x, ax);
+      if (!axrev) {
+        x = axes.tickIncrement(x, dtick, !axrev, calendar);
+      }
+    }
 
-        for(;
-            axrev ?
-                (x >= endTick) :
-                (x <= endTick);
-            x = axes.tickIncrement(
-                x,
-                dtick,
-                axrev,
-                calendar
-            )
-        ) {
-            if(major) majorId++;
-
-            if(mockAx.rangebreaks) {
-                if(!axrev) {
-                    if(x < startTick) continue;
-                    if(mockAx.maskBreaks(x) === BADNUM && moveOutsideBreak(x, mockAx) >= maxRange) break;
-                }
-            }
-
-            // prevent infinite loops - no more than one tick per pixel,
-            // and make sure each value is different from the previous
-            if(tickVals.length > maxTicks || x === prevX) break;
-            prevX = x;
-
-            var obj = { value: x };
-
-            if(major) {
-                if(isDLog && (x !== (x | 0))) {
-                    obj.simpleLabel = true;
-                }
-
-                if(ticklabelstep > 1 && majorId % ticklabelstep) {
-                    obj.skipLabel = true;
-                }
-
-                tickVals.push(obj);
-            } else {
-                obj.minor = true;
-
-                minorTickVals.push(obj);
-            }
-        }
+    if (major && isPeriod) {
+      // add one item to label period before tick0
+      x = axes.tickIncrement(x, dtick, !axrev, calendar);
+      majorId--;
     }
 
-    if(hasMinor) {
-        var canOverlap =
-            (ax.minor.ticks === 'inside' && ax.ticks === 'outside') ||
-            (ax.minor.ticks === 'outside' && ax.ticks === 'inside');
-
-        if(!canOverlap) {
-            // remove duplicate minors
-
-            var majorValues = tickVals.map(function(d) { return d.value; });
-
-            var list = [];
-            for(var k = 0; k < minorTickVals.length; k++) {
-                var T = minorTickVals[k];
-                var v = T.value;
-                if(majorValues.indexOf(v) !== -1) {
-                    continue;
-                }
-                var found = false;
-                for(var q = 0; !found && (q < tickVals.length); q++) {
-                    if(
-                        // add 10e6 to eliminate problematic digits
-                        10e6 + tickVals[q].value ===
-                        10e6 + v
-                    ) {
-                        found = true;
-                    }
-                }
-                if(!found) list.push(T);
-            }
-            minorTickVals = list;
+    for (
+      ;
+      axrev ? x >= endTick : x <= endTick;
+      x = axes.tickIncrement(x, dtick, axrev, calendar)
+    ) {
+      if (major) majorId++;
+
+      if (mockAx.rangebreaks) {
+        if (!axrev) {
+          if (x < startTick) continue;
+          if (
+            mockAx.maskBreaks(x) === BADNUM &&
+            moveOutsideBreak(x, mockAx) >= maxRange
+          )
+            break;
         }
-    }
+      }
 
-    if(isPeriod) positionPeriodTicks(tickVals, ax, ax._definedDelta);
+      // prevent infinite loops - no more than one tick per pixel,
+      // and make sure each value is different from the previous
+      if (tickVals.length > maxTicks || x === prevX) break;
+      prevX = x;
 
-    var i;
-    if(ax.rangebreaks) {
-        var flip = ax._id.charAt(0) === 'y';
+      var obj = { value: x };
 
-        var fontSize = 1; // one pixel minimum
-        if(ax.tickmode === 'auto') {
-            fontSize = ax.tickfont ? ax.tickfont.size : 12;
+      if (major) {
+        if (isDLog && x !== (x | 0)) {
+          obj.simpleLabel = true;
         }
 
-        var prevL = NaN;
-        for(i = tickVals.length - 1; i > -1; i--) {
-            if(tickVals[i].drop) {
-                tickVals.splice(i, 1);
-                continue;
-            }
-
-            tickVals[i].value = moveOutsideBreak(tickVals[i].value, ax);
-
-            // avoid overlaps
-            var l = ax.c2p(tickVals[i].value);
-            if(flip ?
-                (prevL > l - fontSize) :
-                (prevL < l + fontSize)
-            ) { // ensure one pixel minimum
-                tickVals.splice(axrev ? i + 1 : i, 1);
-            } else {
-                prevL = l;
-            }
+        if (ticklabelstep > 1 && majorId % ticklabelstep) {
+          obj.skipLabel = true;
         }
-    }
 
-    // If same angle over a full circle, the last tick vals is a duplicate.
-    // TODO must do something similar for angular date axes.
-    if(isAngular(ax) && Math.abs(rng[1] - rng[0]) === 360) {
-        tickVals.pop();
-    }
+        tickVals.push(obj);
+      } else {
+        obj.minor = true;
 
-    // save the last tick as well as first, so we can
-    // show the exponent only on the last one
-    ax._tmax = (tickVals[tickVals.length - 1] || {}).value;
-
-    // for showing the rest of a date when the main tick label is only the
-    // latter part: ax._prevDateHead holds what we showed most recently.
-    // Start with it cleared and mark that we're in calcTicks (ie calculating a
-    // whole string of these so we should care what the previous date head was!)
-    ax._prevDateHead = '';
-    ax._inCalcTicks = true;
-
-    var lastVisibleHead;
-    var hideLabel = function(tick) {
-        tick.text = '';
-        ax._prevDateHead = lastVisibleHead;
-    };
+        minorTickVals.push(obj);
+      }
+    }
+  }
 
-    tickVals = tickVals.concat(minorTickVals);
+  if (hasMinor) {
+    var canOverlap =
+      (ax.minor.ticks === "inside" && ax.ticks === "outside") ||
+      (ax.minor.ticks === "outside" && ax.ticks === "inside");
 
-    var t, p;
-    for(i = 0; i < tickVals.length; i++) {
-        var _minor = tickVals[i].minor;
-        var _value = tickVals[i].value;
+    if (!canOverlap) {
+      // remove duplicate minors
 
-        if(_minor) {
-            minorTicks.push({
-                x: _value,
-                minor: true
-            });
-        } else {
-            lastVisibleHead = ax._prevDateHead;
+      var majorValues = tickVals.map(function (d) {
+        return d.value;
+      });
 
-            t = axes.tickText(
-                ax,
-                _value,
-                false, // hover
-                tickVals[i].simpleLabel // noSuffixPrefix
-            );
-
-            p = tickVals[i].periodX;
-            if(p !== undefined) {
-                t.periodX = p;
-                if(p > maxRange || p < minRange) { // hide label if outside the range
-                    if(p > maxRange) t.periodX = maxRange;
-                    if(p < minRange) t.periodX = minRange;
-
-                    hideLabel(t);
-                }
-            }
+      var list = [];
+      for (var k = 0; k < minorTickVals.length; k++) {
+        var T = minorTickVals[k];
+        var v = T.value;
+        if (majorValues.indexOf(v) !== -1) {
+          continue;
+        }
+        var found = false;
+        for (var q = 0; !found && q < tickVals.length; q++) {
+          if (
+            // add 10e6 to eliminate problematic digits
+            10e6 + tickVals[q].value ===
+            10e6 + v
+          ) {
+            found = true;
+          }
+        }
+        if (!found) list.push(T);
+      }
+      minorTickVals = list;
+    }
+  }
+
+  if (isPeriod) positionPeriodTicks(tickVals, ax, ax._definedDelta);
+
+  var i;
+  if (ax.rangebreaks) {
+    var flip = ax._id.charAt(0) === "y";
+
+    var fontSize = 1; // one pixel minimum
+    if (ax.tickmode === "auto") {
+      fontSize = ax.tickfont ? ax.tickfont.size : 12;
+    }
+
+    var prevL = NaN;
+    for (i = tickVals.length - 1; i > -1; i--) {
+      if (tickVals[i].drop) {
+        tickVals.splice(i, 1);
+        continue;
+      }
+
+      tickVals[i].value = moveOutsideBreak(tickVals[i].value, ax);
+
+      // avoid overlaps
+      var l = ax.c2p(tickVals[i].value);
+      if (flip ? prevL > l - fontSize : prevL < l + fontSize) {
+        // ensure one pixel minimum
+        tickVals.splice(axrev ? i + 1 : i, 1);
+      } else {
+        prevL = l;
+      }
+    }
+  }
+
+  // If same angle over a full circle, the last tick vals is a duplicate.
+  // TODO must do something similar for angular date axes.
+  if (isAngular(ax) && Math.abs(rng[1] - rng[0]) === 360) {
+    tickVals.pop();
+  }
+
+  // save the last tick as well as first, so we can
+  // show the exponent only on the last one
+  ax._tmax = (tickVals[tickVals.length - 1] || {}).value;
+
+  // for showing the rest of a date when the main tick label is only the
+  // latter part: ax._prevDateHead holds what we showed most recently.
+  // Start with it cleared and mark that we're in calcTicks (ie calculating a
+  // whole string of these so we should care what the previous date head was!)
+  ax._prevDateHead = "";
+  ax._inCalcTicks = true;
+
+  var lastVisibleHead;
+  var hideLabel = function (tick) {
+    tick.text = "";
+    ax._prevDateHead = lastVisibleHead;
+  };
+
+  tickVals = tickVals.concat(minorTickVals);
+
+  var t, p;
+  for (i = 0; i < tickVals.length; i++) {
+    var _minor = tickVals[i].minor;
+    var _value = tickVals[i].value;
+
+    if (_minor) {
+      minorTicks.push({
+        x: _value,
+        minor: true,
+      });
+    } else {
+      lastVisibleHead = ax._prevDateHead;
+
+      t = axes.tickText(
+        ax,
+        _value,
+        false, // hover
+        tickVals[i].simpleLabel // noSuffixPrefix
+      );
+
+      p = tickVals[i].periodX;
+      if (p !== undefined) {
+        t.periodX = p;
+        if (p > maxRange || p < minRange) {
+          // hide label if outside the range
+          if (p > maxRange) t.periodX = maxRange;
+          if (p < minRange) t.periodX = minRange;
+
+          hideLabel(t);
+        }
+      }
 
-            if(tickVals[i].skipLabel) {
-                hideLabel(t);
-            }
+      if (tickVals[i].skipLabel) {
+        hideLabel(t);
+      }
 
-            ticksOut.push(t);
-        }
+      ticksOut.push(t);
     }
-    ticksOut = ticksOut.concat(minorTicks);
+  }
+  ticksOut = ticksOut.concat(minorTicks);
 
-    ax._inCalcTicks = false;
+  ax._inCalcTicks = false;
 
-    if(isPeriod && ticksOut.length) {
-        // drop very first tick that we added to handle period
-        ticksOut[0].noTick = true;
-    }
+  if (isPeriod && ticksOut.length) {
+    // drop very first tick that we added to handle period
+    ticksOut[0].noTick = true;
+  }
 
-    return ticksOut;
+  return ticksOut;
 };
 
 function filterRangeBreaks(ax, ticksOut) {
-    if(ax.rangebreaks) {
-        // remove ticks falling inside rangebreaks
-        ticksOut = ticksOut.filter(function(d) {
-            return ax.maskBreaks(d.x) !== BADNUM;
-        });
-    }
+  if (ax.rangebreaks) {
+    // remove ticks falling inside rangebreaks
+    ticksOut = ticksOut.filter(function (d) {
+      return ax.maskBreaks(d.x) !== BADNUM;
+    });
+  }
 
-    return ticksOut;
+  return ticksOut;
 }
 
 function syncTicks(ax) {
-    // get the overlaying axis
-    var baseAxis = ax._mainAxis;
-
-    var ticksOut = [];
-    if(baseAxis._vals) {
-        for(var i = 0; i < baseAxis._vals.length; i++) {
-            // filter vals with noTick flag
-            if(baseAxis._vals[i].noTick) {
-                continue;
-            }
-
-            // get the position of the every tick
-            var pos = baseAxis.l2p(baseAxis._vals[i].x);
-
-            // get the tick for the current axis based on position
-            var vali = ax.p2l(pos);
-            var obj = axes.tickText(ax, vali);
-
-            // assign minor ticks
-            if(baseAxis._vals[i].minor) {
-                obj.minor = true;
-                obj.text = '';
-            }
-
-            ticksOut.push(obj);
-        }
-    }
+  // get the overlaying axis
+  var baseAxis = ax._mainAxis;
 
-    ticksOut = filterRangeBreaks(ax, ticksOut);
+  var ticksOut = [];
+  if (baseAxis._vals) {
+    for (var i = 0; i < baseAxis._vals.length; i++) {
+      // filter vals with noTick flag
+      if (baseAxis._vals[i].noTick) {
+        continue;
+      }
 
-    return ticksOut;
-}
+      // get the position of the every tick
+      var pos = baseAxis.l2p(baseAxis._vals[i].x);
 
-function arrayTicks(ax) {
-    var rng = Lib.simpleMap(ax.range, ax.r2l);
-    var exRng = expandRange(rng);
-    var tickMin = Math.min(exRng[0], exRng[1]);
-    var tickMax = Math.max(exRng[0], exRng[1]);
+      // get the tick for the current axis based on position
+      var vali = ax.p2l(pos);
+      var obj = axes.tickText(ax, vali);
 
-    // make sure showing ticks doesn't accidentally add new categories
-    // TODO multicategory, if we allow ticktext / tickvals
-    var tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l;
+      // assign minor ticks
+      if (baseAxis._vals[i].minor) {
+        obj.minor = true;
+        obj.text = "";
+      }
 
-    // array ticks on log axes always show the full number
-    // (if no explicit ticktext overrides it)
-    if(ax.type === 'log' && String(ax.dtick).charAt(0) !== 'L') {
-        ax.dtick = 'L' + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1);
+      ticksOut.push(obj);
     }
+  }
 
-    var ticksOut = [];
-    for(var isMinor = 0; isMinor <= 1; isMinor++) {
-        if(isMinor && !ax.minor) continue;
-        var vals = !isMinor ? ax.tickvals : ax.minor.tickvals;
-        var text = !isMinor ? ax.ticktext : [];
+  ticksOut = filterRangeBreaks(ax, ticksOut);
 
-        if(!vals) continue;
-
-
-        // without a text array, just format the given values as any other ticks
-        // except with more precision to the numbers
-        if(!Array.isArray(text)) text = [];
-
-        for(var i = 0; i < vals.length; i++) {
-            var vali = tickVal2l(vals[i]);
-            if(vali > tickMin && vali < tickMax) {
-                var obj = text[i] === undefined ?
-                        axes.tickText(ax, vali) :
-                        tickTextObj(ax, vali, String(text[i]));
-
-                if(isMinor) {
-                    obj.minor = true;
-                    obj.text = '';
-                }
+  return ticksOut;
+}
 
-                ticksOut.push(obj);
-            }
+function arrayTicks(ax) {
+  var rng = Lib.simpleMap(ax.range, ax.r2l);
+  var exRng = expandRange(rng);
+  var tickMin = Math.min(exRng[0], exRng[1]);
+  var tickMax = Math.max(exRng[0], exRng[1]);
+
+  // make sure showing ticks doesn't accidentally add new categories
+  // TODO multicategory, if we allow ticktext / tickvals
+  var tickVal2l = ax.type === "category" ? ax.d2l_noadd : ax.d2l;
+
+  // array ticks on log axes always show the full number
+  // (if no explicit ticktext overrides it)
+  if (ax.type === "log" && String(ax.dtick).charAt(0) !== "L") {
+    ax.dtick =
+      "L" + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1);
+  }
+
+  var ticksOut = [];
+  for (var isMinor = 0; isMinor <= 1; isMinor++) {
+    if (isMinor && !ax.minor) continue;
+    var vals = !isMinor ? ax.tickvals : ax.minor.tickvals;
+    var text = !isMinor ? ax.ticktext : [];
+
+    if (!vals) continue;
+
+    // without a text array, just format the given values as any other ticks
+    // except with more precision to the numbers
+    if (!Array.isArray(text)) text = [];
+
+    for (var i = 0; i < vals.length; i++) {
+      var vali = tickVal2l(vals[i]);
+      if (vali > tickMin && vali < tickMax) {
+        var obj =
+          text[i] === undefined
+            ? axes.tickText(ax, vali)
+            : tickTextObj(ax, vali, String(text[i]));
+
+        if (isMinor) {
+          obj.minor = true;
+          obj.text = "";
         }
+
+        ticksOut.push(obj);
+      }
     }
+  }
 
-    ticksOut = filterRangeBreaks(ax, ticksOut);
+  ticksOut = filterRangeBreaks(ax, ticksOut);
 
-    return ticksOut;
+  return ticksOut;
 }
 
 var roundBase10 = [2, 5, 10];
@@ -1319,13 +1377,15 @@ var roundBase60 = [1, 2, 5, 10, 15, 30];
 var roundDays = [1, 2, 3, 7, 14];
 // approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2)
 // these don't have to be exact, just close enough to round to the right value
-var roundLog1 = [-0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1];
+var roundLog1 = [
+  -0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1,
+];
 var roundLog2 = [-0.301, 0, 0.301, 0.699, 1];
 // N.B. `thetaunit; 'radians' angular axes must be converted to degrees
 var roundAngles = [15, 30, 45, 90, 180];
 
 function roundDTick(roughDTick, base, roundingSet) {
-    return base * Lib.roundUp(roughDTick / base, roundingSet);
+  return base * Lib.roundUp(roughDTick / base, roundingSet);
 }
 
 // autoTicks: calculate best guess at pleasant ticks for this axis
@@ -1344,106 +1404,109 @@ function roundDTick(roughDTick, base, roundingSet) {
 //      log with linear ticks: L# where # is the linear tick spacing
 //      log showing powers plus some intermediates:
 //          D1 shows all digits, D2 shows 2 and 5
-axes.autoTicks = function(ax, roughDTick, isMinor) {
-    var base;
-
-    function getBase(v) {
-        return Math.pow(v, Math.floor(Math.log(roughDTick) / Math.LN10));
-    }
-
-    if(ax.type === 'date') {
-        ax.tick0 = Lib.dateTick0(ax.calendar, 0);
-
-        // the criteria below are all based on the rough spacing we calculate
-        // being > half of the final unit - so precalculate twice the rough val
-        var roughX2 = 2 * roughDTick;
-
-        if(roughX2 > ONEAVGYEAR) {
-            roughDTick /= ONEAVGYEAR;
-            base = getBase(10);
-            ax.dtick = 'M' + (12 * roundDTick(roughDTick, base, roundBase10));
-        } else if(roughX2 > ONEAVGMONTH) {
-            roughDTick /= ONEAVGMONTH;
-            ax.dtick = 'M' + roundDTick(roughDTick, 1, roundBase24);
-        } else if(roughX2 > ONEDAY) {
-            ax.dtick = roundDTick(roughDTick, ONEDAY, ax._hasDayOfWeekBreaks ? [1, 2, 7, 14] : roundDays);
-            if(!isMinor) {
-                // get week ticks on sunday
-                // this will also move the base tick off 2000-01-01 if dtick is
-                // 2 or 3 days... but that's a weird enough case that we'll ignore it.
-                var tickformat = axes.getTickFormat(ax);
-                var isPeriod = ax.ticklabelmode === 'period';
-                if(isPeriod) ax._rawTick0 = ax.tick0;
-
-                if(/%[uVW]/.test(tickformat)) {
-                    ax.tick0 = Lib.dateTick0(ax.calendar, 2); // Monday
-                } else {
-                    ax.tick0 = Lib.dateTick0(ax.calendar, 1); // Sunday
-                }
-
-                if(isPeriod) ax._dowTick0 = ax.tick0;
-            }
-        } else if(roughX2 > ONEHOUR) {
-            ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24);
-        } else if(roughX2 > ONEMIN) {
-            ax.dtick = roundDTick(roughDTick, ONEMIN, roundBase60);
-        } else if(roughX2 > ONESEC) {
-            ax.dtick = roundDTick(roughDTick, ONESEC, roundBase60);
+axes.autoTicks = function (ax, roughDTick, isMinor) {
+  var base;
+
+  function getBase(v) {
+    return Math.pow(v, Math.floor(Math.log(roughDTick) / Math.LN10));
+  }
+
+  if (ax.type === "date") {
+    ax.tick0 = Lib.dateTick0(ax.calendar, 0);
+
+    // the criteria below are all based on the rough spacing we calculate
+    // being > half of the final unit - so precalculate twice the rough val
+    var roughX2 = 2 * roughDTick;
+
+    if (roughX2 > ONEAVGYEAR) {
+      roughDTick /= ONEAVGYEAR;
+      base = getBase(10);
+      ax.dtick = "M" + 12 * roundDTick(roughDTick, base, roundBase10);
+    } else if (roughX2 > ONEAVGMONTH) {
+      roughDTick /= ONEAVGMONTH;
+      ax.dtick = "M" + roundDTick(roughDTick, 1, roundBase24);
+    } else if (roughX2 > ONEDAY) {
+      ax.dtick = roundDTick(
+        roughDTick,
+        ONEDAY,
+        ax._hasDayOfWeekBreaks ? [1, 2, 7, 14] : roundDays
+      );
+      if (!isMinor) {
+        // get week ticks on sunday
+        // this will also move the base tick off 2000-01-01 if dtick is
+        // 2 or 3 days... but that's a weird enough case that we'll ignore it.
+        var tickformat = axes.getTickFormat(ax);
+        var isPeriod = ax.ticklabelmode === "period";
+        if (isPeriod) ax._rawTick0 = ax.tick0;
+
+        if (/%[uVW]/.test(tickformat)) {
+          ax.tick0 = Lib.dateTick0(ax.calendar, 2); // Monday
         } else {
-            // milliseconds
-            base = getBase(10);
-            ax.dtick = roundDTick(roughDTick, base, roundBase10);
-        }
-    } else if(ax.type === 'log') {
-        ax.tick0 = 0;
-        var rng = Lib.simpleMap(ax.range, ax.r2l);
-        if(ax._isMinor) {
-            // Log axes by default get MORE than nTicks based on the metrics below
-            // But for minor ticks we don't want this increase, we already have
-            // the major ticks.
-            roughDTick *= 1.5;
+          ax.tick0 = Lib.dateTick0(ax.calendar, 1); // Sunday
         }
-        if(roughDTick > 0.7) {
-            // only show powers of 10
-            ax.dtick = Math.ceil(roughDTick);
-        } else if(Math.abs(rng[1] - rng[0]) < 1) {
-            // span is less than one power of 10
-            var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick);
-
-            // ticks on a linear scale, labeled fully
-            roughDTick = Math.abs(Math.pow(10, rng[1]) -
-                Math.pow(10, rng[0])) / nt;
-            base = getBase(10);
-            ax.dtick = 'L' + roundDTick(roughDTick, base, roundBase10);
-        } else {
-            // include intermediates between powers of 10,
-            // labeled with small digits
-            // ax.dtick = "D2" (show 2 and 5) or "D1" (show all digits)
-            ax.dtick = (roughDTick > 0.3) ? 'D2' : 'D1';
-        }
-    } else if(ax.type === 'category' || ax.type === 'multicategory') {
-        ax.tick0 = 0;
-        ax.dtick = Math.ceil(Math.max(roughDTick, 1));
-    } else if(isAngular(ax)) {
-        ax.tick0 = 0;
-        base = 1;
-        ax.dtick = roundDTick(roughDTick, base, roundAngles);
-    } else {
-        // auto ticks always start at 0
-        ax.tick0 = 0;
-        base = getBase(10);
-        ax.dtick = roundDTick(roughDTick, base, roundBase10);
-    }
-
-    // prevent infinite loops
-    if(ax.dtick === 0) ax.dtick = 1;
 
-    // TODO: this is from log axis histograms with autorange off
-    if(!isNumeric(ax.dtick) && typeof ax.dtick !== 'string') {
-        var olddtick = ax.dtick;
-        ax.dtick = 1;
-        throw 'ax.dtick error: ' + String(olddtick);
+        if (isPeriod) ax._dowTick0 = ax.tick0;
+      }
+    } else if (roughX2 > ONEHOUR) {
+      ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24);
+    } else if (roughX2 > ONEMIN) {
+      ax.dtick = roundDTick(roughDTick, ONEMIN, roundBase60);
+    } else if (roughX2 > ONESEC) {
+      ax.dtick = roundDTick(roughDTick, ONESEC, roundBase60);
+    } else {
+      // milliseconds
+      base = getBase(10);
+      ax.dtick = roundDTick(roughDTick, base, roundBase10);
     }
+  } else if (ax.type === "log") {
+    ax.tick0 = 0;
+    var rng = Lib.simpleMap(ax.range, ax.r2l);
+    if (ax._isMinor) {
+      // Log axes by default get MORE than nTicks based on the metrics below
+      // But for minor ticks we don't want this increase, we already have
+      // the major ticks.
+      roughDTick *= 1.5;
+    }
+    if (roughDTick > 0.7) {
+      // only show powers of 10
+      ax.dtick = Math.ceil(roughDTick);
+    } else if (Math.abs(rng[1] - rng[0]) < 1) {
+      // span is less than one power of 10
+      var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick);
+
+      // ticks on a linear scale, labeled fully
+      roughDTick = Math.abs(Math.pow(10, rng[1]) - Math.pow(10, rng[0])) / nt;
+      base = getBase(10);
+      ax.dtick = "L" + roundDTick(roughDTick, base, roundBase10);
+    } else {
+      // include intermediates between powers of 10,
+      // labeled with small digits
+      // ax.dtick = "D2" (show 2 and 5) or "D1" (show all digits)
+      ax.dtick = roughDTick > 0.3 ? "D2" : "D1";
+    }
+  } else if (ax.type === "category" || ax.type === "multicategory") {
+    ax.tick0 = 0;
+    ax.dtick = Math.ceil(Math.max(roughDTick, 1));
+  } else if (isAngular(ax)) {
+    ax.tick0 = 0;
+    base = 1;
+    ax.dtick = roundDTick(roughDTick, base, roundAngles);
+  } else {
+    // auto ticks always start at 0
+    ax.tick0 = 0;
+    base = getBase(10);
+    ax.dtick = roundDTick(roughDTick, base, roundBase10);
+  }
+
+  // prevent infinite loops
+  if (ax.dtick === 0) ax.dtick = 1;
+
+  // TODO: this is from log axis histograms with autorange off
+  if (!isNumeric(ax.dtick) && typeof ax.dtick !== "string") {
+    var olddtick = ax.dtick;
+    ax.dtick = 1;
+    throw "ax.dtick error: " + String(olddtick);
+  }
 };
 
 // after dtick is already known, find tickround = precision
@@ -1452,64 +1515,67 @@ axes.autoTicks = function(ax, roughDTick, isMinor) {
 //   for date ticks, the last date part to show (y,m,d,H,M,S)
 //      or an integer # digits past seconds
 function autoTickRound(ax) {
-    var dtick = ax.dtick;
-
-    ax._tickexponent = 0;
-    if(!isNumeric(dtick) && typeof dtick !== 'string') {
-        dtick = 1;
-    }
-
-    if(ax.type === 'category' || ax.type === 'multicategory') {
-        ax._tickround = null;
-    }
-    if(ax.type === 'date') {
-        // If tick0 is unusual, give tickround a bit more information
-        // not necessarily *all* the information in tick0 though, if it's really odd
-        // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19
-        // take off a leading minus (year < 0) and i (intercalary month) so length is consistent
-        var tick0ms = ax.r2l(ax.tick0);
-        var tick0str = ax.l2r(tick0ms).replace(/(^-|i)/g, '');
-        var tick0len = tick0str.length;
-
-        if(String(dtick).charAt(0) === 'M') {
-            // any tick0 more specific than a year: alway show the full date
-            if(tick0len > 10 || tick0str.substr(5) !== '01-01') ax._tickround = 'd';
-            // show the month unless ticks are full multiples of a year
-            else ax._tickround = (+(dtick.substr(1)) % 12 === 0) ? 'y' : 'm';
-        } else if((dtick >= ONEDAY && tick0len <= 10) || (dtick >= ONEDAY * 15)) ax._tickround = 'd';
-        else if((dtick >= ONEMIN && tick0len <= 16) || (dtick >= ONEHOUR)) ax._tickround = 'M';
-        else if((dtick >= ONESEC && tick0len <= 19) || (dtick >= ONEMIN)) ax._tickround = 'S';
-        else {
-            // tickround is a number of digits of fractional seconds
-            // of any two adjacent ticks, at least one will have the maximum fractional digits
-            // of all possible ticks - so take the max. length of tick0 and the next one
-            var tick1len = ax.l2r(tick0ms + dtick).replace(/^-/, '').length;
-            ax._tickround = Math.max(tick0len, tick1len) - 20;
-
-            // We shouldn't get here... but in case there's a situation I'm
-            // not thinking of where tick0str and tick1str are identical or
-            // something, fall back on maximum precision
-            if(ax._tickround < 0) ax._tickround = 4;
-        }
-    } else if(isNumeric(dtick) || dtick.charAt(0) === 'L') {
-        // linear or log (except D1, D2)
-        var rng = ax.range.map(ax.r2d || Number);
-        if(!isNumeric(dtick)) dtick = Number(dtick.substr(1));
-        // 2 digits past largest digit of dtick
-        ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01);
-
-        var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1]));
-        var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01);
-        var minexponent = ax.minexponent === undefined ? 3 : ax.minexponent;
-        if(Math.abs(rangeexp) > minexponent) {
-            if(isSIFormat(ax.exponentformat) && !beyondSI(rangeexp)) {
-                ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3);
-            } else ax._tickexponent = rangeexp;
-        }
-    } else {
-        // D1 or D2 (log)
-        ax._tickround = null;
-    }
+  var dtick = ax.dtick;
+
+  ax._tickexponent = 0;
+  if (!isNumeric(dtick) && typeof dtick !== "string") {
+    dtick = 1;
+  }
+
+  if (ax.type === "category" || ax.type === "multicategory") {
+    ax._tickround = null;
+  }
+  if (ax.type === "date") {
+    // If tick0 is unusual, give tickround a bit more information
+    // not necessarily *all* the information in tick0 though, if it's really odd
+    // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19
+    // take off a leading minus (year < 0) and i (intercalary month) so length is consistent
+    var tick0ms = ax.r2l(ax.tick0);
+    var tick0str = ax.l2r(tick0ms).replace(/(^-|i)/g, "");
+    var tick0len = tick0str.length;
+
+    if (String(dtick).charAt(0) === "M") {
+      // any tick0 more specific than a year: alway show the full date
+      if (tick0len > 10 || tick0str.substr(5) !== "01-01") ax._tickround = "d";
+      // show the month unless ticks are full multiples of a year
+      else ax._tickround = +dtick.substr(1) % 12 === 0 ? "y" : "m";
+    } else if ((dtick >= ONEDAY && tick0len <= 10) || dtick >= ONEDAY * 15)
+      ax._tickround = "d";
+    else if ((dtick >= ONEMIN && tick0len <= 16) || dtick >= ONEHOUR)
+      ax._tickround = "M";
+    else if ((dtick >= ONESEC && tick0len <= 19) || dtick >= ONEMIN)
+      ax._tickround = "S";
+    else {
+      // tickround is a number of digits of fractional seconds
+      // of any two adjacent ticks, at least one will have the maximum fractional digits
+      // of all possible ticks - so take the max. length of tick0 and the next one
+      var tick1len = ax.l2r(tick0ms + dtick).replace(/^-/, "").length;
+      ax._tickround = Math.max(tick0len, tick1len) - 20;
+
+      // We shouldn't get here... but in case there's a situation I'm
+      // not thinking of where tick0str and tick1str are identical or
+      // something, fall back on maximum precision
+      if (ax._tickround < 0) ax._tickround = 4;
+    }
+  } else if (isNumeric(dtick) || dtick.charAt(0) === "L") {
+    // linear or log (except D1, D2)
+    var rng = ax.range.map(ax.r2d || Number);
+    if (!isNumeric(dtick)) dtick = Number(dtick.substr(1));
+    // 2 digits past largest digit of dtick
+    ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01);
+
+    var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1]));
+    var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01);
+    var minexponent = ax.minexponent === undefined ? 3 : ax.minexponent;
+    if (Math.abs(rangeexp) > minexponent) {
+      if (isSIFormat(ax.exponentformat) && !beyondSI(rangeexp)) {
+        ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3);
+      } else ax._tickexponent = rangeexp;
+    }
+  } else {
+    // D1 or D2 (log)
+    ax._tickround = null;
+  }
 }
 
 // months and years don't have constant millisecond values
@@ -1518,95 +1584,104 @@ function autoTickRound(ax) {
 // for pure powers of 10
 // numeric ticks always have constant differences, other datetime ticks
 // can all be calculated as constant number of milliseconds
-axes.tickIncrement = function(x, dtick, axrev, calendar) {
-    var axSign = axrev ? -1 : 1;
+axes.tickIncrement = function (x, dtick, axrev, calendar) {
+  var axSign = axrev ? -1 : 1;
 
-    // includes linear, all dates smaller than month, and pure 10^n in log
-    if(isNumeric(dtick)) return Lib.increment(x, axSign * dtick);
+  // includes linear, all dates smaller than month, and pure 10^n in log
+  if (isNumeric(dtick)) return Lib.increment(x, axSign * dtick);
 
-    // everything else is a string, one character plus a number
-    var tType = dtick.charAt(0);
-    var dtSigned = axSign * Number(dtick.substr(1));
+  // everything else is a string, one character plus a number
+  var tType = dtick.charAt(0);
+  var dtSigned = axSign * Number(dtick.substr(1));
 
-    // Dates: months (or years - see Lib.incrementMonth)
-    if(tType === 'M') return Lib.incrementMonth(x, dtSigned, calendar);
+  // Dates: months (or years - see Lib.incrementMonth)
+  if (tType === "M") return Lib.incrementMonth(x, dtSigned, calendar);
 
-    // Log scales: Linear, Digits
-    if(tType === 'L') return Math.log(Math.pow(10, x) + dtSigned) / Math.LN10;
+  // Log scales: Linear, Digits
+  if (tType === "L") return Math.log(Math.pow(10, x) + dtSigned) / Math.LN10;
 
-    // log10 of 2,5,10, or all digits (logs just have to be
-    // close enough to round)
-    if(tType === 'D') {
-        var tickset = (dtick === 'D2') ? roundLog2 : roundLog1;
-        var x2 = x + axSign * 0.01;
-        var frac = Lib.roundUp(Lib.mod(x2, 1), tickset, axrev);
+  // log10 of 2,5,10, or all digits (logs just have to be
+  // close enough to round)
+  if (tType === "D") {
+    var tickset = dtick === "D2" ? roundLog2 : roundLog1;
+    var x2 = x + axSign * 0.01;
+    var frac = Lib.roundUp(Lib.mod(x2, 1), tickset, axrev);
 
-        return Math.floor(x2) +
-            Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10;
-    }
+    return (
+      Math.floor(x2) + Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10
+    );
+  }
 
-    throw 'unrecognized dtick ' + String(dtick);
+  throw "unrecognized dtick " + String(dtick);
 };
 
 // calculate the first tick on an axis
-axes.tickFirst = function(ax, opts) {
-    var r2l = ax.r2l || Number;
-    var rng = Lib.simpleMap(ax.range, r2l, undefined, undefined, opts);
-    var axrev = rng[1] < rng[0];
-    var sRound = axrev ? Math.floor : Math.ceil;
-    // add a tiny extra bit to make sure we get ticks
-    // that may have been rounded out
-    var r0 = expandRange(rng)[0];
-    var dtick = ax.dtick;
-    var tick0 = r2l(ax.tick0);
-
-    if(isNumeric(dtick)) {
-        var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0;
-
-        // make sure no ticks outside the category list
-        if(ax.type === 'category' || ax.type === 'multicategory') {
-            tmin = Lib.constrain(tmin, 0, ax._categories.length - 1);
-        }
-        return tmin;
-    }
+axes.tickFirst = function (ax, opts) {
+  var r2l = ax.r2l || Number;
+  var rng = Lib.simpleMap(ax.range, r2l, undefined, undefined, opts);
+  var axrev = rng[1] < rng[0];
+  var sRound = axrev ? Math.floor : Math.ceil;
+  // add a tiny extra bit to make sure we get ticks
+  // that may have been rounded out
+  var r0 = expandRange(rng)[0];
+  var dtick = ax.dtick;
+  var tick0 = r2l(ax.tick0);
+
+  if (isNumeric(dtick)) {
+    var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0;
+
+    // make sure no ticks outside the category list
+    if (ax.type === "category" || ax.type === "multicategory") {
+      tmin = Lib.constrain(tmin, 0, ax._categories.length - 1);
+    }
+    return tmin;
+  }
+
+  var tType = dtick.charAt(0);
+  var dtNum = Number(dtick.substr(1));
+
+  // Dates: months (or years)
+  if (tType === "M") {
+    var cnt = 0;
+    var t0 = tick0;
+    var t1, mult, newDTick;
+
+    // This algorithm should work for *any* nonlinear (but close to linear!)
+    // tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3.
+    while (cnt < 10) {
+      t1 = axes.tickIncrement(t0, dtick, axrev, ax.calendar);
+      if ((t1 - r0) * (t0 - r0) <= 0) {
+        // t1 and t0 are on opposite sides of r0! we've succeeded!
+        if (axrev) return Math.min(t0, t1);
+        return Math.max(t0, t1);
+      }
+      mult = (r0 - (t0 + t1) / 2) / (t1 - t0);
+      newDTick = tType + (Math.abs(Math.round(mult)) || 1) * dtNum;
+      t0 = axes.tickIncrement(
+        t0,
+        newDTick,
+        mult < 0 ? !axrev : axrev,
+        ax.calendar
+      );
+      cnt++;
+    }
+    Lib.error("tickFirst did not converge", ax);
+    return t0;
+  } else if (tType === "L") {
+    // Log scales: Linear, Digits
 
-    var tType = dtick.charAt(0);
-    var dtNum = Number(dtick.substr(1));
-
-    // Dates: months (or years)
-    if(tType === 'M') {
-        var cnt = 0;
-        var t0 = tick0;
-        var t1, mult, newDTick;
-
-        // This algorithm should work for *any* nonlinear (but close to linear!)
-        // tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3.
-        while(cnt < 10) {
-            t1 = axes.tickIncrement(t0, dtick, axrev, ax.calendar);
-            if((t1 - r0) * (t0 - r0) <= 0) {
-                // t1 and t0 are on opposite sides of r0! we've succeeded!
-                if(axrev) return Math.min(t0, t1);
-                return Math.max(t0, t1);
-            }
-            mult = (r0 - ((t0 + t1) / 2)) / (t1 - t0);
-            newDTick = tType + ((Math.abs(Math.round(mult)) || 1) * dtNum);
-            t0 = axes.tickIncrement(t0, newDTick, mult < 0 ? !axrev : axrev, ax.calendar);
-            cnt++;
-        }
-        Lib.error('tickFirst did not converge', ax);
-        return t0;
-    } else if(tType === 'L') {
-        // Log scales: Linear, Digits
-
-        return Math.log(sRound(
-            (Math.pow(10, r0) - tick0) / dtNum) * dtNum + tick0) / Math.LN10;
-    } else if(tType === 'D') {
-        var tickset = (dtick === 'D2') ? roundLog2 : roundLog1;
-        var frac = Lib.roundUp(Lib.mod(r0, 1), tickset, axrev);
-
-        return Math.floor(r0) +
-            Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10;
-    } else throw 'unrecognized dtick ' + String(dtick);
+    return (
+      Math.log(sRound((Math.pow(10, r0) - tick0) / dtNum) * dtNum + tick0) /
+      Math.LN10
+    );
+  } else if (tType === "D") {
+    var tickset = dtick === "D2" ? roundLog2 : roundLog1;
+    var frac = Lib.roundUp(Lib.mod(r0, 1), tickset, axrev);
+
+    return (
+      Math.floor(r0) + Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10
+    );
+  } else throw "unrecognized dtick " + String(dtick);
 };
 
 // draw the text for one tick.
@@ -1615,77 +1690,78 @@ axes.tickFirst = function(ax, opts) {
 // ax is the axis layout, x is the tick value
 // hover is a (truthy) flag for whether to show numbers with a bit
 // more precision for hovertext
-axes.tickText = function(ax, x, hover, noSuffixPrefix) {
-    var out = tickTextObj(ax, x);
-    var arrayMode = ax.tickmode === 'array';
-    var extraPrecision = hover || arrayMode;
-    var axType = ax.type;
-    // TODO multicategory, if we allow ticktext / tickvals
-    var tickVal2l = axType === 'category' ? ax.d2l_noadd : ax.d2l;
-    var i;
-
-    if(arrayMode && Array.isArray(ax.ticktext)) {
-        var rng = Lib.simpleMap(ax.range, ax.r2l);
-        var minDiff = (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / 10000;
-
-        for(i = 0; i < ax.ticktext.length; i++) {
-            if(Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break;
-        }
-        if(i < ax.ticktext.length) {
-            out.text = String(ax.ticktext[i]);
-            return out;
-        }
-    }
-
-    function isHidden(showAttr) {
-        if(showAttr === undefined) return true;
-        if(hover) return showAttr === 'none';
-
-        var firstOrLast = {
-            first: ax._tmin,
-            last: ax._tmax
-        }[showAttr];
-
-        return showAttr !== 'all' && x !== firstOrLast;
-    }
-
-    var hideexp = hover ?
-        'never' :
-        ax.exponentformat !== 'none' && isHidden(ax.showexponent) ? 'hide' : '';
-
-    if(axType === 'date') formatDate(ax, out, hover, extraPrecision);
-    else if(axType === 'log') formatLog(ax, out, hover, extraPrecision, hideexp);
-    else if(axType === 'category') formatCategory(ax, out);
-    else if(axType === 'multicategory') formatMultiCategory(ax, out, hover);
-    else if(isAngular(ax)) formatAngle(ax, out, hover, extraPrecision, hideexp);
-    else formatLinear(ax, out, hover, extraPrecision, hideexp);
-
-    // add prefix and suffix
-    if(!noSuffixPrefix) {
-        if(ax.tickprefix && !isHidden(ax.showtickprefix)) out.text = ax.tickprefix + out.text;
-        if(ax.ticksuffix && !isHidden(ax.showticksuffix)) out.text += ax.ticksuffix;
-    }
-
-    if(ax.labelalias && ax.labelalias.hasOwnProperty(out.text)) {
-        var t = ax.labelalias[out.text];
-        if(typeof t === 'string') out.text = t;
-    }
-
-    // Setup ticks and grid lines boundaries
-    // at 1/2 a 'category' to the left/bottom
-    if(ax.tickson === 'boundaries' || ax.showdividers) {
-        var inbounds = function(v) {
-            var p = ax.l2p(v);
-            return p >= 0 && p <= ax._length ? v : null;
-        };
+axes.tickText = function (ax, x, hover, noSuffixPrefix) {
+  var out = tickTextObj(ax, x);
+  var arrayMode = ax.tickmode === "array";
+  var extraPrecision = hover || arrayMode;
+  var axType = ax.type;
+  // TODO multicategory, if we allow ticktext / tickvals
+  var tickVal2l = axType === "category" ? ax.d2l_noadd : ax.d2l;
+  var i;
+
+  if (arrayMode && Array.isArray(ax.ticktext)) {
+    var rng = Lib.simpleMap(ax.range, ax.r2l);
+    var minDiff = (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / 10000;
+
+    for (i = 0; i < ax.ticktext.length; i++) {
+      if (Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break;
+    }
+    if (i < ax.ticktext.length) {
+      out.text = String(ax.ticktext[i]);
+      return out;
+    }
+  }
+
+  function isHidden(showAttr) {
+    if (showAttr === undefined) return true;
+    if (hover) return showAttr === "none";
+
+    var firstOrLast = {
+      first: ax._tmin,
+      last: ax._tmax,
+    }[showAttr];
+
+    return showAttr !== "all" && x !== firstOrLast;
+  }
+
+  var hideexp = hover
+    ? "never"
+    : ax.exponentformat !== "none" && isHidden(ax.showexponent)
+    ? "hide"
+    : "";
+
+  if (axType === "date") formatDate(ax, out, hover, extraPrecision);
+  else if (axType === "log") formatLog(ax, out, hover, extraPrecision, hideexp);
+  else if (axType === "category") formatCategory(ax, out);
+  else if (axType === "multicategory") formatMultiCategory(ax, out, hover);
+  else if (isAngular(ax)) formatAngle(ax, out, hover, extraPrecision, hideexp);
+  else formatLinear(ax, out, hover, extraPrecision, hideexp);
+
+  // add prefix and suffix
+  if (!noSuffixPrefix) {
+    if (ax.tickprefix && !isHidden(ax.showtickprefix))
+      out.text = ax.tickprefix + out.text;
+    if (ax.ticksuffix && !isHidden(ax.showticksuffix))
+      out.text += ax.ticksuffix;
+  }
+
+  if (ax.labelalias && ax.labelalias.hasOwnProperty(out.text)) {
+    var t = ax.labelalias[out.text];
+    if (typeof t === "string") out.text = t;
+  }
+
+  // Setup ticks and grid lines boundaries
+  // at 1/2 a 'category' to the left/bottom
+  if (ax.tickson === "boundaries" || ax.showdividers) {
+    var inbounds = function (v) {
+      var p = ax.l2p(v);
+      return p >= 0 && p <= ax._length ? v : null;
+    };
 
-        out.xbnd = [
-            inbounds(out.x - 0.5),
-            inbounds(out.x + ax.dtick - 0.5)
-        ];
-    }
+    out.xbnd = [inbounds(out.x - 0.5), inbounds(out.x + ax.dtick - 0.5)];
+  }
 
-    return out;
+  return out;
 };
 
 /**
@@ -1700,291 +1776,325 @@ axes.tickText = function(ax, x, hover, noSuffixPrefix) {
  *     first value and second value as a range (ie '<val1> - <val2>') if the second value is provided and
  *     it's different from the first value.
  */
-axes.hoverLabelText = function(ax, values, hoverformat) {
-    if(hoverformat) ax = Lib.extendFlat({}, ax, {hoverformat: hoverformat});
-
-    var val = Array.isArray(values) ? values[0] : values;
-    var val2 = Array.isArray(values) ? values[1] : undefined;
-    if(val2 !== undefined && val2 !== val) {
-        return (
-            axes.hoverLabelText(ax, val, hoverformat) + ' - ' +
-            axes.hoverLabelText(ax, val2, hoverformat)
-        );
-    }
+axes.hoverLabelText = function (ax, values, hoverformat) {
+  if (hoverformat) ax = Lib.extendFlat({}, ax, { hoverformat: hoverformat });
+
+  var val = Array.isArray(values) ? values[0] : values;
+  var val2 = Array.isArray(values) ? values[1] : undefined;
+  if (val2 !== undefined && val2 !== val) {
+    return (
+      axes.hoverLabelText(ax, val, hoverformat) +
+      " - " +
+      axes.hoverLabelText(ax, val2, hoverformat)
+    );
+  }
 
-    var logOffScale = (ax.type === 'log' && val <= 0);
-    var tx = axes.tickText(ax, ax.c2l(logOffScale ? -val : val), 'hover').text;
+  var logOffScale = ax.type === "log" && val <= 0;
+  var tx = axes.tickText(ax, ax.c2l(logOffScale ? -val : val), "hover").text;
 
-    if(logOffScale) {
-        return val === 0 ? '0' : MINUS_SIGN + tx;
-    }
+  if (logOffScale) {
+    return val === 0 ? "0" : MINUS_SIGN + tx;
+  }
 
-    // TODO: should we do something special if the axis calendar and
-    // the data calendar are different? Somehow display both dates with
-    // their system names? Right now it will just display in the axis calendar
-    // but users could add the other one as text.
-    return tx;
+  // TODO: should we do something special if the axis calendar and
+  // the data calendar are different? Somehow display both dates with
+  // their system names? Right now it will just display in the axis calendar
+  // but users could add the other one as text.
+  return tx;
 };
 
 function tickTextObj(ax, x, text) {
-    var tf = ax.tickfont || {};
-
-    return {
-        x: x,
-        dx: 0,
-        dy: 0,
-        text: text || '',
-        fontSize: tf.size,
-        font: tf.family,
-        fontColor: tf.color
-    };
+  var tf = ax.tickfont || {};
+
+  return {
+    x: x,
+    dx: 0,
+    dy: 0,
+    text: text || "",
+    fontSize: tf.size,
+    font: tf.family,
+    fontColor: tf.color,
+  };
 }
 
 function formatDate(ax, out, hover, extraPrecision) {
-    var tr = ax._tickround;
-    var fmt = (hover && ax.hoverformat) || axes.getTickFormat(ax);
-
-    // Only apply extra precision if no explicit format was provided.
-    extraPrecision = !fmt && extraPrecision;
-
-    if(extraPrecision) {
-        // second or sub-second precision: extra always shows max digits.
-        // for other fields, extra precision just adds one field.
-        if(isNumeric(tr)) tr = 4;
-        else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr];
-    }
-
-    var dateStr = Lib.formatDate(out.x, fmt, tr, ax._dateFormat, ax.calendar, ax._extraFormat);
-    var headStr;
-
-    var splitIndex = dateStr.indexOf('\n');
-    if(splitIndex !== -1) {
-        headStr = dateStr.substr(splitIndex + 1);
-        dateStr = dateStr.substr(0, splitIndex);
-    }
-
-    if(extraPrecision) {
-        // if extraPrecision led to trailing zeros, strip them off
-        // actually, this can lead to removing even more zeros than
-        // in the original rounding, but that's fine because in these
-        // contexts uniformity is not so important (if there's even
-        // anything to be uniform with!)
-
-        // can we remove the whole time part?
-        if(headStr !== undefined && (dateStr === '00:00:00' || dateStr === '00:00')) {
-            dateStr = headStr;
-            headStr = '';
-        } else if(dateStr.length === 8) {
-            // strip off seconds if they're zero (zero fractional seconds
-            // are already omitted)
-            // but we never remove minutes and leave just hours
-            dateStr = dateStr.replace(/:00$/, '');
-        }
-    }
-
-    if(headStr) {
-        if(hover) {
-            // hover puts it all on one line, so headPart works best up front
-            // except for year headPart: turn this into "Jan 1, 2000" etc.
-            if(tr === 'd') dateStr += ', ' + headStr;
-            else dateStr = headStr + (dateStr ? ', ' + dateStr : '');
-        } else {
-            if(
-                !ax._inCalcTicks ||
-                ax._prevDateHead !== headStr
-            ) {
-                ax._prevDateHead = headStr;
-                dateStr += '<br>' + headStr;
-            } else {
-                var isInside = insideTicklabelposition(ax);
-                var side = ax._trueSide || ax.side; // polar mocks the side of the radial axis
-                if(
-                    (!isInside && side === 'top') ||
-                    (isInside && side === 'bottom')
-                ) {
-                    dateStr += '<br> ';
-                }
-            }
+  var tr = ax._tickround;
+  var fmt = (hover && ax.hoverformat) || axes.getTickFormat(ax);
+
+  // Only apply extra precision if no explicit format was provided.
+  extraPrecision = !fmt && extraPrecision;
+
+  if (extraPrecision) {
+    // second or sub-second precision: extra always shows max digits.
+    // for other fields, extra precision just adds one field.
+    if (isNumeric(tr)) tr = 4;
+    else tr = { y: "m", m: "d", d: "M", M: "S", S: 4 }[tr];
+  }
+
+  var dateStr = Lib.formatDate(
+    out.x,
+    fmt,
+    tr,
+    ax._dateFormat,
+    ax.calendar,
+    ax._extraFormat
+  );
+  var headStr;
+
+  var splitIndex = dateStr.indexOf("\n");
+  if (splitIndex !== -1) {
+    headStr = dateStr.substr(splitIndex + 1);
+    dateStr = dateStr.substr(0, splitIndex);
+  }
+
+  if (extraPrecision) {
+    // if extraPrecision led to trailing zeros, strip them off
+    // actually, this can lead to removing even more zeros than
+    // in the original rounding, but that's fine because in these
+    // contexts uniformity is not so important (if there's even
+    // anything to be uniform with!)
+
+    // can we remove the whole time part?
+    if (
+      headStr !== undefined &&
+      (dateStr === "00:00:00" || dateStr === "00:00")
+    ) {
+      dateStr = headStr;
+      headStr = "";
+    } else if (dateStr.length === 8) {
+      // strip off seconds if they're zero (zero fractional seconds
+      // are already omitted)
+      // but we never remove minutes and leave just hours
+      dateStr = dateStr.replace(/:00$/, "");
+    }
+  }
+
+  if (headStr) {
+    if (hover) {
+      // hover puts it all on one line, so headPart works best up front
+      // except for year headPart: turn this into "Jan 1, 2000" etc.
+      if (tr === "d") dateStr += ", " + headStr;
+      else dateStr = headStr + (dateStr ? ", " + dateStr : "");
+    } else {
+      if (!ax._inCalcTicks || ax._prevDateHead !== headStr) {
+        ax._prevDateHead = headStr;
+        dateStr += "<br>" + headStr;
+      } else {
+        var isInside = insideTicklabelposition(ax);
+        var side = ax._trueSide || ax.side; // polar mocks the side of the radial axis
+        if ((!isInside && side === "top") || (isInside && side === "bottom")) {
+          dateStr += "<br> ";
         }
+      }
     }
+  }
 
-    out.text = dateStr;
+  out.text = dateStr;
 }
 
 function formatLog(ax, out, hover, extraPrecision, hideexp) {
-    var dtick = ax.dtick;
-    var x = out.x;
-    var tickformat = ax.tickformat;
-    var dtChar0 = typeof dtick === 'string' && dtick.charAt(0);
-
-    if(hideexp === 'never') {
-        // If this is a hover label, then we must *never* hide the exponent
-        // for the sake of display, which could give the wrong value by
-        // potentially many orders of magnitude. If hideexp was 'never', then
-        // it's now succeeded by preventing the other condition from automating
-        // this choice. Thus we can unset it so that the axis formatting takes
-        // precedence.
-        hideexp = '';
-    }
-
-    if(extraPrecision && (dtChar0 !== 'L')) {
-        dtick = 'L3';
-        dtChar0 = 'L';
-    }
+  var dtick = ax.dtick;
+  var x = out.x;
+  var tickformat = ax.tickformat;
+  var dtChar0 = typeof dtick === "string" && dtick.charAt(0);
+
+  if (hideexp === "never") {
+    // If this is a hover label, then we must *never* hide the exponent
+    // for the sake of display, which could give the wrong value by
+    // potentially many orders of magnitude. If hideexp was 'never', then
+    // it's now succeeded by preventing the other condition from automating
+    // this choice. Thus we can unset it so that the axis formatting takes
+    // precedence.
+    hideexp = "";
+  }
+
+  if (extraPrecision && dtChar0 !== "L") {
+    dtick = "L3";
+    dtChar0 = "L";
+  }
+
+  if (tickformat || dtChar0 === "L") {
+    out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision);
+  } else if (
+    isNumeric(dtick) ||
+    (dtChar0 === "D" && Lib.mod(x + 0.01, 1) < 0.1)
+  ) {
+    var p = Math.round(x);
+    var absP = Math.abs(p);
+    var exponentFormat = ax.exponentformat;
+    if (
+      exponentFormat === "power" ||
+      (isSIFormat(exponentFormat) && beyondSI(p))
+    ) {
+      if (p === 0) out.text = 1;
+      else if (p === 1) out.text = "10";
+      else out.text = "10<sup>" + (p > 1 ? "" : MINUS_SIGN) + absP + "</sup>";
 
-    if(tickformat || (dtChar0 === 'L')) {
-        out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision);
-    } else if(isNumeric(dtick) || ((dtChar0 === 'D') && (Lib.mod(x + 0.01, 1) < 0.1))) {
-        var p = Math.round(x);
-        var absP = Math.abs(p);
-        var exponentFormat = ax.exponentformat;
-        if(exponentFormat === 'power' || (isSIFormat(exponentFormat) && beyondSI(p))) {
-            if(p === 0) out.text = 1;
-            else if(p === 1) out.text = '10';
-            else out.text = '10<sup>' + (p > 1 ? '' : MINUS_SIGN) + absP + '</sup>';
-
-            out.fontSize *= 1.25;
-        } else if((exponentFormat === 'e' || exponentFormat === 'E') && absP > 2) {
-            out.text = '1' + exponentFormat + (p > 0 ? '+' : MINUS_SIGN) + absP;
-        } else {
-            out.text = numFormat(Math.pow(10, x), ax, '', 'fakehover');
-            if(dtick === 'D1' && ax._id.charAt(0) === 'y') {
-                out.dy -= out.fontSize / 6;
-            }
-        }
-    } else if(dtChar0 === 'D') {
-        out.text = String(Math.round(Math.pow(10, Lib.mod(x, 1))));
-        out.fontSize *= 0.75;
-    } else throw 'unrecognized dtick ' + String(dtick);
-
-    // if 9's are printed on log scale, move the 10's away a bit
-    if(ax.dtick === 'D1') {
-        var firstChar = String(out.text).charAt(0);
-        if(firstChar === '0' || firstChar === '1') {
-            if(ax._id.charAt(0) === 'y') {
-                out.dx -= out.fontSize / 4;
-            } else {
-                out.dy += out.fontSize / 2;
-                out.dx += (ax.range[1] > ax.range[0] ? 1 : -1) *
-                    out.fontSize * (x < 0 ? 0.5 : 0.25);
-            }
-        }
-    }
+      out.fontSize *= 1.25;
+    } else if ((exponentFormat === "e" || exponentFormat === "E") && absP > 2) {
+      out.text = "1" + exponentFormat + (p > 0 ? "+" : MINUS_SIGN) + absP;
+    } else {
+      out.text = numFormat(Math.pow(10, x), ax, "", "fakehover");
+      if (dtick === "D1" && ax._id.charAt(0) === "y") {
+        out.dy -= out.fontSize / 6;
+      }
+    }
+  } else if (dtChar0 === "D") {
+    out.text = String(Math.round(Math.pow(10, Lib.mod(x, 1))));
+    out.fontSize *= 0.75;
+  } else throw "unrecognized dtick " + String(dtick);
+
+  // if 9's are printed on log scale, move the 10's away a bit
+  if (ax.dtick === "D1") {
+    var firstChar = String(out.text).charAt(0);
+    if (firstChar === "0" || firstChar === "1") {
+      if (ax._id.charAt(0) === "y") {
+        out.dx -= out.fontSize / 4;
+      } else {
+        out.dy += out.fontSize / 2;
+        out.dx +=
+          (ax.range[1] > ax.range[0] ? 1 : -1) *
+          out.fontSize *
+          (x < 0 ? 0.5 : 0.25);
+      }
+    }
+  }
 }
 
 function formatCategory(ax, out) {
-    var tt = ax._categories[Math.round(out.x)];
-    if(tt === undefined) tt = '';
-    out.text = String(tt);
+  var tt = ax._categories[Math.round(out.x)];
+  if (tt === undefined) tt = "";
+  out.text = String(tt);
 }
 
 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]);
-
-    if(hover) {
-        // TODO is this what we want?
-        out.text = tt2 + ' - ' + tt;
-    } else {
-        // setup for secondary labels
-        out.text = tt;
-        out.text2 = tt2;
-    }
+  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?
+    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) {
-    if(hideexp === 'never') {
-        // If this is a hover label, then we must *never* hide the exponent
-        // for the sake of display, which could give the wrong value by
-        // potentially many orders of magnitude. If hideexp was 'never', then
-        // it's now succeeded by preventing the other condition from automating
-        // this choice. Thus we can unset it so that the axis formatting takes
-        // precedence.
-        hideexp = '';
-    } else if(ax.showexponent === 'all' && Math.abs(out.x / ax.dtick) < 1e-6) {
-        // don't add an exponent to zero if we're showing all exponents
-        // so the only reason you'd show an exponent on zero is if it's the
-        // ONLY tick to get an exponent (first or last)
-        hideexp = 'hide';
-    }
-    out.text = numFormat(out.x, ax, hideexp, extraPrecision);
+  if (hideexp === "never") {
+    // If this is a hover label, then we must *never* hide the exponent
+    // for the sake of display, which could give the wrong value by
+    // potentially many orders of magnitude. If hideexp was 'never', then
+    // it's now succeeded by preventing the other condition from automating
+    // this choice. Thus we can unset it so that the axis formatting takes
+    // precedence.
+    hideexp = "";
+  } else if (ax.showexponent === "all" && Math.abs(out.x / ax.dtick) < 1e-6) {
+    // don't add an exponent to zero if we're showing all exponents
+    // so the only reason you'd show an exponent on zero is if it's the
+    // ONLY tick to get an exponent (first or last)
+    hideexp = "hide";
+  }
+  out.text = numFormat(out.x, ax, hideexp, extraPrecision);
 }
 
 function formatAngle(ax, out, hover, extraPrecision, hideexp) {
-    if(ax.thetaunit === 'radians' && !hover) {
-        var num = out.x / 180;
+  if (ax.thetaunit === "radians" && !hover) {
+    var num = out.x / 180;
 
-        if(num === 0) {
-            out.text = '0';
+    if (num === 0) {
+      out.text = "0";
+    } else {
+      var frac = num2frac(num);
+
+      if (frac[1] >= 100) {
+        out.text = numFormat(Lib.deg2rad(out.x), ax, hideexp, extraPrecision);
+      } else {
+        var isNeg = out.x < 0;
+
+        if (frac[1] === 1) {
+          if (frac[0] === 1) out.text = "π";
+          else out.text = frac[0] + "π";
         } else {
-            var frac = num2frac(num);
-
-            if(frac[1] >= 100) {
-                out.text = numFormat(Lib.deg2rad(out.x), ax, hideexp, extraPrecision);
-            } else {
-                var isNeg = out.x < 0;
-
-                if(frac[1] === 1) {
-                    if(frac[0] === 1) out.text = 'π';
-                    else out.text = frac[0] + 'π';
-                } else {
-                    out.text = [
-                        '<sup>', frac[0], '</sup>',
-                        '⁄',
-                        '<sub>', frac[1], '</sub>',
-                        'π'
-                    ].join('');
-                }
-
-                if(isNeg) out.text = MINUS_SIGN + out.text;
-            }
+          out.text = [
+            "<sup>",
+            frac[0],
+            "</sup>",
+            "⁄",
+            "<sub>",
+            frac[1],
+            "</sub>",
+            "π",
+          ].join("");
         }
-    } else {
-        out.text = numFormat(out.x, ax, hideexp, extraPrecision);
+
+        if (isNeg) out.text = MINUS_SIGN + out.text;
+      }
     }
+  } else {
+    out.text = numFormat(out.x, ax, hideexp, extraPrecision);
+  }
 }
 
 // inspired by
 // https://github.com/yisibl/num2fraction/blob/master/index.js
 function num2frac(num) {
-    function almostEq(a, b) {
-        return Math.abs(a - b) <= 1e-6;
-    }
-
-    function findGCD(a, b) {
-        return almostEq(b, 0) ? a : findGCD(b, a % b);
-    }
-
-    function findPrecision(n) {
-        var e = 1;
-        while(!almostEq(Math.round(n * e) / e, n)) {
-            e *= 10;
-        }
-        return e;
-    }
-
-    var precision = findPrecision(num);
-    var number = num * precision;
-    var gcd = Math.abs(findGCD(number, precision));
-
-    return [
-        // numerator
-        Math.round(number / gcd),
-        // denominator
-        Math.round(precision / gcd)
-    ];
+  function almostEq(a, b) {
+    return Math.abs(a - b) <= 1e-6;
+  }
+
+  function findGCD(a, b) {
+    return almostEq(b, 0) ? a : findGCD(b, a % b);
+  }
+
+  function findPrecision(n) {
+    var e = 1;
+    while (!almostEq(Math.round(n * e) / e, n)) {
+      e *= 10;
+    }
+    return e;
+  }
+
+  var precision = findPrecision(num);
+  var number = num * precision;
+  var gcd = Math.abs(findGCD(number, precision));
+
+  return [
+    // numerator
+    Math.round(number / gcd),
+    // denominator
+    Math.round(precision / gcd),
+  ];
 }
 
 // format a number (tick value) according to the axis settings
 // new, more reliable procedure than d3.round or similar:
 // add half the rounding increment, then stringify and truncate
 // also automatically switch to sci. notation
-var SIPREFIXES = ['f', 'p', 'n', 'μ', 'm', '', 'k', 'M', 'G', 'T'];
+var SIPREFIXES = ["f", "p", "n", "μ", "m", "", "k", "M", "G", "T"];
 
 function isSIFormat(exponentFormat) {
-    return exponentFormat === 'SI' || exponentFormat === 'B';
+  return exponentFormat === "SI" || exponentFormat === "B";
 }
 
 // are we beyond the range of common SI prefixes?
@@ -1995,179 +2105,203 @@ function isSIFormat(exponentFormat) {
 // 10^15 -> 1x10^15
 // 10^16 -> 1x10^16
 function beyondSI(exponent) {
-    return exponent > 14 || exponent < -15;
+  return exponent > 14 || exponent < -15;
 }
 
 function numFormat(v, ax, fmtoverride, hover) {
-    var isNeg = v < 0;
-    // max number of digits past decimal point to show
-    var tickRound = ax._tickround;
-    var exponentFormat = fmtoverride || ax.exponentformat || 'B';
-    var exponent = ax._tickexponent;
-    var tickformat = axes.getTickFormat(ax);
-    var separatethousands = ax.separatethousands;
-
-    // special case for hover: set exponent just for this value, and
-    // add a couple more digits of precision over tick labels
-    if(hover) {
-        // make a dummy axis obj to get the auto rounding and exponent
-        var ah = {
-            exponentformat: exponentFormat,
-            minexponent: ax.minexponent,
-            dtick: ax.showexponent === 'none' ? ax.dtick :
-                (isNumeric(v) ? Math.abs(v) || 1 : 1),
-            // if not showing any exponents, don't change the exponent
-            // from what we calculate
-            range: ax.showexponent === 'none' ? ax.range.map(ax.r2d) : [0, v || 1]
-        };
-        autoTickRound(ah);
-        tickRound = (Number(ah._tickround) || 0) + 4;
-        exponent = ah._tickexponent;
-        if(ax.hoverformat) tickformat = ax.hoverformat;
-    }
-
-    if(tickformat) return ax._numFormat(tickformat)(v).replace(/-/g, MINUS_SIGN);
-
-    // 'epsilon' - rounding increment
-    var e = Math.pow(10, -tickRound) / 2;
-
-    // exponentFormat codes:
-    // 'e' (1.2e+6, default)
-    // 'E' (1.2E+6)
-    // 'SI' (1.2M)
-    // 'B' (same as SI except 10^9=B not G)
-    // 'none' (1200000)
-    // 'power' (1.2x10^6)
-    // 'hide' (1.2, use 3rd argument=='hide' to eg
-    //      only show exponent on last tick)
-    if(exponentFormat === 'none') exponent = 0;
-
-    // take the sign out, put it back manually at the end
-    // - makes cases easier
-    v = Math.abs(v);
-    if(v < e) {
-        // 0 is just 0, but may get exponent if it's the last tick
-        v = '0';
-        isNeg = false;
+  var isNeg = v < 0;
+  // max number of digits past decimal point to show
+  var tickRound = ax._tickround;
+  var exponentFormat = fmtoverride || ax.exponentformat || "B";
+  var exponent = ax._tickexponent;
+  var tickformat = axes.getTickFormat(ax);
+  var separatethousands = ax.separatethousands;
+
+  // special case for hover: set exponent just for this value, and
+  // add a couple more digits of precision over tick labels
+  if (hover) {
+    // make a dummy axis obj to get the auto rounding and exponent
+    var ah = {
+      exponentformat: exponentFormat,
+      minexponent: ax.minexponent,
+      dtick:
+        ax.showexponent === "none"
+          ? ax.dtick
+          : isNumeric(v)
+          ? Math.abs(v) || 1
+          : 1,
+      // if not showing any exponents, don't change the exponent
+      // from what we calculate
+      range: ax.showexponent === "none" ? ax.range.map(ax.r2d) : [0, v || 1],
+    };
+    autoTickRound(ah);
+    tickRound = (Number(ah._tickround) || 0) + 4;
+    exponent = ah._tickexponent;
+    if (ax.hoverformat) tickformat = ax.hoverformat;
+  }
+
+  if (tickformat) return ax._numFormat(tickformat)(v).replace(/-/g, MINUS_SIGN);
+
+  // 'epsilon' - rounding increment
+  var e = Math.pow(10, -tickRound) / 2;
+
+  // exponentFormat codes:
+  // 'e' (1.2e+6, default)
+  // 'E' (1.2E+6)
+  // 'SI' (1.2M)
+  // 'B' (same as SI except 10^9=B not G)
+  // 'none' (1200000)
+  // 'power' (1.2x10^6)
+  // 'hide' (1.2, use 3rd argument=='hide' to eg
+  //      only show exponent on last tick)
+  if (exponentFormat === "none") exponent = 0;
+
+  // take the sign out, put it back manually at the end
+  // - makes cases easier
+  v = Math.abs(v);
+  if (v < e) {
+    // 0 is just 0, but may get exponent if it's the last tick
+    v = "0";
+    isNeg = false;
+  } else {
+    v += e;
+    // take out a common exponent, if any
+    if (exponent) {
+      v *= Math.pow(10, -exponent);
+      tickRound += exponent;
+    }
+    // round the mantissa
+    if (tickRound === 0) v = String(Math.floor(v));
+    else if (tickRound < 0) {
+      v = String(Math.round(v));
+      v = v.substr(0, v.length + tickRound);
+      for (var i = tickRound; i < 0; i++) v += "0";
     } else {
-        v += e;
-        // take out a common exponent, if any
-        if(exponent) {
-            v *= Math.pow(10, -exponent);
-            tickRound += exponent;
-        }
-        // round the mantissa
-        if(tickRound === 0) v = String(Math.floor(v));
-        else if(tickRound < 0) {
-            v = String(Math.round(v));
-            v = v.substr(0, v.length + tickRound);
-            for(var i = tickRound; i < 0; i++) v += '0';
-        } else {
-            v = String(v);
-            var dp = v.indexOf('.') + 1;
-            if(dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, '');
-        }
-        // insert appropriate decimal point and thousands separator
-        v = Lib.numSeparate(v, ax._separators, separatethousands);
-    }
-
-    // add exponent
-    if(exponent && exponentFormat !== 'hide') {
-        if(isSIFormat(exponentFormat) && beyondSI(exponent)) exponentFormat = 'power';
-
-        var signedExponent;
-        if(exponent < 0) signedExponent = MINUS_SIGN + -exponent;
-        else if(exponentFormat !== 'power') signedExponent = '+' + exponent;
-        else signedExponent = String(exponent);
-
-        if(exponentFormat === 'e' || exponentFormat === 'E') {
-            v += exponentFormat + signedExponent;
-        } else if(exponentFormat === 'power') {
-            v += '×10<sup>' + signedExponent + '</sup>';
-        } else if(exponentFormat === 'B' && exponent === 9) {
-            v += 'B';
-        } else if(isSIFormat(exponentFormat)) {
-            v += SIPREFIXES[exponent / 3 + 5];
-        }
-    }
-
-    // put sign back in and return
-    // replace standard minus character (which is technically a hyphen)
-    // with a true minus sign
-    if(isNeg) return MINUS_SIGN + v;
-    return v;
+      v = String(v);
+      var dp = v.indexOf(".") + 1;
+      if (dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, "");
+    }
+    // insert appropriate decimal point and thousands separator
+    v = Lib.numSeparate(v, ax._separators, separatethousands);
+  }
+
+  // add exponent
+  if (exponent && exponentFormat !== "hide") {
+    if (isSIFormat(exponentFormat) && beyondSI(exponent))
+      exponentFormat = "power";
+
+    var signedExponent;
+    if (exponent < 0) signedExponent = MINUS_SIGN + -exponent;
+    else if (exponentFormat !== "power") signedExponent = "+" + exponent;
+    else signedExponent = String(exponent);
+
+    if (exponentFormat === "e" || exponentFormat === "E") {
+      v += exponentFormat + signedExponent;
+    } else if (exponentFormat === "power") {
+      v += "×10<sup>" + signedExponent + "</sup>";
+    } else if (exponentFormat === "B" && exponent === 9) {
+      v += "B";
+    } else if (isSIFormat(exponentFormat)) {
+      v += SIPREFIXES[exponent / 3 + 5];
+    }
+  }
+
+  // put sign back in and return
+  // replace standard minus character (which is technically a hyphen)
+  // with a true minus sign
+  if (isNeg) return MINUS_SIGN + v;
+  return v;
 }
 
-axes.getTickFormat = function(ax) {
-    var i;
-
-    function convertToMs(dtick) {
-        return typeof dtick !== 'string' ? dtick : Number(dtick.replace('M', '')) * ONEAVGMONTH;
-    }
-
-    function compareLogTicks(left, right) {
-        var priority = ['L', 'D'];
-        if(typeof left === typeof right) {
-            if(typeof left === 'number') {
-                return left - right;
-            } else {
-                var leftPriority = priority.indexOf(left.charAt(0));
-                var rightPriority = priority.indexOf(right.charAt(0));
-                if(leftPriority === rightPriority) {
-                    return Number(left.replace(/(L|D)/g, '')) - Number(right.replace(/(L|D)/g, ''));
-                } else {
-                    return leftPriority - rightPriority;
-                }
-            }
+axes.getTickFormat = function (ax) {
+  var i;
+
+  function convertToMs(dtick) {
+    return typeof dtick !== "string"
+      ? dtick
+      : Number(dtick.replace("M", "")) * ONEAVGMONTH;
+  }
+
+  function compareLogTicks(left, right) {
+    var priority = ["L", "D"];
+    if (typeof left === typeof right) {
+      if (typeof left === "number") {
+        return left - right;
+      } else {
+        var leftPriority = priority.indexOf(left.charAt(0));
+        var rightPriority = priority.indexOf(right.charAt(0));
+        if (leftPriority === rightPriority) {
+          return (
+            Number(left.replace(/(L|D)/g, "")) -
+            Number(right.replace(/(L|D)/g, ""))
+          );
         } else {
-            return typeof left === 'number' ? 1 : -1;
+          return leftPriority - rightPriority;
         }
-    }
-
-    function isProperStop(dtick, range, convert) {
-        var convertFn = convert || function(x) { return x;};
-        var leftDtick = range[0];
-        var rightDtick = range[1];
-        return ((!leftDtick && typeof leftDtick !== 'number') || convertFn(leftDtick) <= convertFn(dtick)) &&
-               ((!rightDtick && typeof rightDtick !== 'number') || convertFn(rightDtick) >= convertFn(dtick));
-    }
-
-    function isProperLogStop(dtick, range) {
-        var isLeftDtickNull = range[0] === null;
-        var isRightDtickNull = range[1] === null;
-        var isDtickInRangeLeft = compareLogTicks(dtick, range[0]) >= 0;
-        var isDtickInRangeRight = compareLogTicks(dtick, range[1]) <= 0;
-        return (isLeftDtickNull || isDtickInRangeLeft) && (isRightDtickNull || isDtickInRangeRight);
-    }
+      }
+    } else {
+      return typeof left === "number" ? 1 : -1;
+    }
+  }
+
+  function isProperStop(dtick, range, convert) {
+    var convertFn =
+      convert ||
+      function (x) {
+        return x;
+      };
+    var leftDtick = range[0];
+    var rightDtick = range[1];
+    return (
+      ((!leftDtick && typeof leftDtick !== "number") ||
+        convertFn(leftDtick) <= convertFn(dtick)) &&
+      ((!rightDtick && typeof rightDtick !== "number") ||
+        convertFn(rightDtick) >= convertFn(dtick))
+    );
+  }
 
-    var tickstop, stopi;
-    if(ax.tickformatstops && ax.tickformatstops.length > 0) {
-        switch(ax.type) {
-            case 'date':
-            case 'linear': {
-                for(i = 0; i < ax.tickformatstops.length; i++) {
-                    stopi = ax.tickformatstops[i];
-                    if(stopi.enabled && isProperStop(ax.dtick, stopi.dtickrange, convertToMs)) {
-                        tickstop = stopi;
-                        break;
-                    }
-                }
-                break;
-            }
-            case 'log': {
-                for(i = 0; i < ax.tickformatstops.length; i++) {
-                    stopi = ax.tickformatstops[i];
-                    if(stopi.enabled && isProperLogStop(ax.dtick, stopi.dtickrange)) {
-                        tickstop = stopi;
-                        break;
-                    }
-                }
-                break;
-            }
-            default:
+  function isProperLogStop(dtick, range) {
+    var isLeftDtickNull = range[0] === null;
+    var isRightDtickNull = range[1] === null;
+    var isDtickInRangeLeft = compareLogTicks(dtick, range[0]) >= 0;
+    var isDtickInRangeRight = compareLogTicks(dtick, range[1]) <= 0;
+    return (
+      (isLeftDtickNull || isDtickInRangeLeft) &&
+      (isRightDtickNull || isDtickInRangeRight)
+    );
+  }
+
+  var tickstop, stopi;
+  if (ax.tickformatstops && ax.tickformatstops.length > 0) {
+    switch (ax.type) {
+      case "date":
+      case "linear": {
+        for (i = 0; i < ax.tickformatstops.length; i++) {
+          stopi = ax.tickformatstops[i];
+          if (
+            stopi.enabled &&
+            isProperStop(ax.dtick, stopi.dtickrange, convertToMs)
+          ) {
+            tickstop = stopi;
+            break;
+          }
         }
+        break;
+      }
+      case "log": {
+        for (i = 0; i < ax.tickformatstops.length; i++) {
+          stopi = ax.tickformatstops[i];
+          if (stopi.enabled && isProperLogStop(ax.dtick, stopi.dtickrange)) {
+            tickstop = stopi;
+            break;
+          }
+        }
+        break;
+      }
+      default:
     }
-    return tickstop ? tickstop.value : ax.tickformat;
+  }
+  return tickstop ? tickstop.value : ax.tickformat;
 };
 
 // getSubplots - extract all subplot IDs we need
@@ -2177,83 +2311,92 @@ axes.getTickFormat = function(ax) {
 //
 // NOTE: this is currently only used OUTSIDE plotly.js (toolpanel, webapp)
 // ideally we get rid of it there (or just copy this there) and remove it here
-axes.getSubplots = function(gd, ax) {
-    var subplotObj = gd._fullLayout._subplots;
-    var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []);
+axes.getSubplots = function (gd, ax) {
+  var subplotObj = gd._fullLayout._subplots;
+  var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []);
 
-    var out = ax ? axes.findSubplotsWithAxis(allSubplots, ax) : allSubplots;
+  var out = ax ? axes.findSubplotsWithAxis(allSubplots, ax) : allSubplots;
 
-    out.sort(function(a, b) {
-        var aParts = a.substr(1).split('y');
-        var bParts = b.substr(1).split('y');
+  out.sort(function (a, b) {
+    var aParts = a.substr(1).split("y");
+    var bParts = b.substr(1).split("y");
 
-        if(aParts[0] === bParts[0]) return +aParts[1] - +bParts[1];
-        return +aParts[0] - +bParts[0];
-    });
+    if (aParts[0] === bParts[0]) return +aParts[1] - +bParts[1];
+    return +aParts[0] - +bParts[0];
+  });
 
-    return out;
+  return out;
 };
 
 // find all subplots with axis 'ax'
 // NOTE: this is only used in axes.getSubplots (only used outside plotly.js) and
 // gl2d/convert (where it restricts axis subplots to only those with gl2d)
-axes.findSubplotsWithAxis = function(subplots, ax) {
-    var axMatch = new RegExp(
-        (ax._id.charAt(0) === 'x') ? ('^' + ax._id + 'y') : (ax._id + '$')
-    );
-    var subplotsWithAx = [];
-
-    for(var i = 0; i < subplots.length; i++) {
-        var sp = subplots[i];
-        if(axMatch.test(sp)) subplotsWithAx.push(sp);
-    }
-
-    return subplotsWithAx;
+axes.findSubplotsWithAxis = function (subplots, ax) {
+  var axMatch = new RegExp(
+    ax._id.charAt(0) === "x" ? "^" + ax._id + "y" : ax._id + "$"
+  );
+  var subplotsWithAx = [];
+
+  for (var i = 0; i < subplots.length; i++) {
+    var sp = subplots[i];
+    if (axMatch.test(sp)) subplotsWithAx.push(sp);
+  }
+
+  return subplotsWithAx;
 };
 
 // makeClipPaths: prepare clipPaths for all single axes and all possible xy pairings
-axes.makeClipPaths = function(gd) {
-    var fullLayout = gd._fullLayout;
-
-    // for more info: https://github.com/plotly/plotly.js/issues/2595
-    if(fullLayout._hasOnlyLargeSploms) return;
-
-    var fullWidth = {_offset: 0, _length: fullLayout.width, _id: ''};
-    var fullHeight = {_offset: 0, _length: fullLayout.height, _id: ''};
-    var xaList = axes.list(gd, 'x', true);
-    var yaList = axes.list(gd, 'y', true);
-    var clipList = [];
-    var i, j;
-
-    for(i = 0; i < xaList.length; i++) {
-        clipList.push({x: xaList[i], y: fullHeight});
-        for(j = 0; j < yaList.length; j++) {
-            if(i === 0) clipList.push({x: fullWidth, y: yaList[j]});
-            clipList.push({x: xaList[i], y: yaList[j]});
-        }
-    }
-
-    // selectors don't work right with camelCase tags,
-    // have to use class instead
-    // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I
-    var axClips = fullLayout._clips.selectAll('.axesclip')
-        .data(clipList, function(d) { return d.x._id + d.y._id; });
-
-    axClips.enter().append('clipPath')
-        .classed('axesclip', true)
-        .attr('id', function(d) { return 'clip' + fullLayout._uid + d.x._id + d.y._id; })
-      .append('rect');
-
-    axClips.exit().remove();
-
-    axClips.each(function(d) {
-        d3.select(this).select('rect').attr({
-            x: d.x._offset || 0,
-            y: d.y._offset || 0,
-            width: d.x._length || 1,
-            height: d.y._length || 1
-        });
+axes.makeClipPaths = function (gd) {
+  var fullLayout = gd._fullLayout;
+
+  // for more info: https://github.com/plotly/plotly.js/issues/2595
+  if (fullLayout._hasOnlyLargeSploms) return;
+
+  var fullWidth = { _offset: 0, _length: fullLayout.width, _id: "" };
+  var fullHeight = { _offset: 0, _length: fullLayout.height, _id: "" };
+  var xaList = axes.list(gd, "x", true);
+  var yaList = axes.list(gd, "y", true);
+  var clipList = [];
+  var i, j;
+
+  for (i = 0; i < xaList.length; i++) {
+    clipList.push({ x: xaList[i], y: fullHeight });
+    for (j = 0; j < yaList.length; j++) {
+      if (i === 0) clipList.push({ x: fullWidth, y: yaList[j] });
+      clipList.push({ x: xaList[i], y: yaList[j] });
+    }
+  }
+
+  // selectors don't work right with camelCase tags,
+  // have to use class instead
+  // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I
+  var axClips = fullLayout._clips
+    .selectAll(".axesclip")
+    .data(clipList, function (d) {
+      return d.x._id + d.y._id;
     });
+
+  axClips
+    .enter()
+    .append("clipPath")
+    .classed("axesclip", true)
+    .attr("id", function (d) {
+      return "clip" + fullLayout._uid + d.x._id + d.y._id;
+    })
+    .append("rect");
+
+  axClips.exit().remove();
+
+  axClips.each(function (d) {
+    d3.select(this)
+      .select("rect")
+      .attr({
+        x: d.x._offset || 0,
+        y: d.y._offset || 0,
+        width: d.x._length || 1,
+        height: d.y._length || 1,
+      });
+  });
 };
 
 /**
@@ -2279,80 +2422,88 @@ axes.makeClipPaths = function(gd) {
  * - ax._r (stored range for use by zoom/pan)
  * - ax._rl (stored linearized range for use by zoom/pan)
  */
-axes.draw = function(gd, arg, opts) {
-    var fullLayout = gd._fullLayout;
-
-    if(arg === 'redraw') {
-        fullLayout._paper.selectAll('g.subplot').each(function(d) {
-            var id = d[0];
-            var plotinfo = fullLayout._plots[id];
-            if(plotinfo) {
-                var xa = plotinfo.xaxis;
-                var ya = plotinfo.yaxis;
-
-                plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick').remove();
-                plotinfo.yaxislayer.selectAll('.' + ya._id + 'tick').remove();
-                plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick2').remove();
-                plotinfo.yaxislayer.selectAll('.' + ya._id + 'tick2').remove();
-                plotinfo.xaxislayer.selectAll('.' + xa._id + 'divider').remove();
-                plotinfo.yaxislayer.selectAll('.' + ya._id + 'divider').remove();
-
-                if(plotinfo.minorGridlayer) plotinfo.minorGridlayer.selectAll('path').remove();
-                if(plotinfo.gridlayer) plotinfo.gridlayer.selectAll('path').remove();
-                if(plotinfo.zerolinelayer) plotinfo.zerolinelayer.selectAll('path').remove();
-
-                fullLayout._infolayer.select('.g-' + xa._id + 'title').remove();
-                fullLayout._infolayer.select('.g-' + ya._id + 'title').remove();
-            }
-        });
-    }
-
-    var axList = (!arg || arg === 'redraw') ? axes.listIds(gd) : arg;
-
-    var fullAxList = axes.list(gd);
-    // Get the list of the overlaying axis for all 'shift' axes
-    var overlayingShiftedAx = fullAxList.filter(function(ax) {
-        return ax.autoshift;
-    }).map(function(ax) {
-        return ax.overlaying;
+axes.draw = function (gd, arg, opts) {
+  var fullLayout = gd._fullLayout;
+
+  if (arg === "redraw") {
+    fullLayout._paper.selectAll("g.subplot").each(function (d) {
+      var id = d[0];
+      var plotinfo = fullLayout._plots[id];
+      if (plotinfo) {
+        var xa = plotinfo.xaxis;
+        var ya = plotinfo.yaxis;
+
+        plotinfo.xaxislayer.selectAll("." + xa._id + "tick").remove();
+        plotinfo.yaxislayer.selectAll("." + ya._id + "tick").remove();
+        plotinfo.xaxislayer.selectAll("." + xa._id + "tick2").remove();
+        plotinfo.yaxislayer.selectAll("." + ya._id + "tick2").remove();
+        plotinfo.xaxislayer.selectAll("." + xa._id + "divider").remove();
+        plotinfo.yaxislayer.selectAll("." + ya._id + "divider").remove();
+
+        if (plotinfo.minorGridlayer)
+          plotinfo.minorGridlayer.selectAll("path").remove();
+        if (plotinfo.gridlayer) plotinfo.gridlayer.selectAll("path").remove();
+        if (plotinfo.zerolinelayer)
+          plotinfo.zerolinelayer.selectAll("path").remove();
+
+        fullLayout._infolayer.select(".g-" + xa._id + "title").remove();
+        fullLayout._infolayer.select(".g-" + ya._id + "title").remove();
+      }
+    });
+  }
+
+  var axList = !arg || arg === "redraw" ? axes.listIds(gd) : arg;
+
+  var fullAxList = axes.list(gd);
+  // Get the list of the overlaying axis for all 'shift' axes
+  var overlayingShiftedAx = fullAxList
+    .filter(function (ax) {
+      return ax.autoshift;
+    })
+    .map(function (ax) {
+      return ax.overlaying;
     });
 
-    // order axes that have dependency to other axes
-    axList.map(function(axId) {
-        var ax = axes.getFromId(gd, axId);
+  // order axes that have dependency to other axes
+  axList.map(function (axId) {
+    var ax = axes.getFromId(gd, axId);
 
-        if(ax.tickmode === 'sync' && ax.overlaying) {
-            var overlayingIndex = axList.findIndex(function(axis) {return axis === ax.overlaying;});
+    if (ax.tickmode === "sync" && ax.overlaying) {
+      var overlayingIndex = axList.findIndex(function (axis) {
+        return axis === ax.overlaying;
+      });
 
-            if(overlayingIndex >= 0) {
-                axList.unshift(axList.splice(overlayingIndex, 1).shift());
-            }
-        }
-    });
+      if (overlayingIndex >= 0) {
+        axList.unshift(axList.splice(overlayingIndex, 1).shift());
+      }
+    }
+  });
 
-    var axShifts = {false: {left: 0, right: 0}};
+  var axShifts = { false: { left: 0, right: 0 } };
 
-    return Lib.syncOrAsync(axList.map(function(axId) {
-        return function() {
-            if(!axId) return;
+  return Lib.syncOrAsync(
+    axList.map(function (axId) {
+      return function () {
+        if (!axId) return;
 
-            var ax = axes.getFromId(gd, axId);
+        var ax = axes.getFromId(gd, axId);
 
-            if(!opts) opts = {};
-            opts.axShifts = axShifts;
-            opts.overlayingShiftedAx = overlayingShiftedAx;
+        if (!opts) opts = {};
+        opts.axShifts = axShifts;
+        opts.overlayingShiftedAx = overlayingShiftedAx;
 
-            var axDone = axes.drawOne(gd, ax, opts);
+        var axDone = axes.drawOne(gd, ax, opts);
 
-            if(ax._shiftPusher) {
-                incrementShift(ax, ax._fullDepth || 0, axShifts, true);
-            }
-            ax._r = ax.range.slice();
-            ax._rl = Lib.simpleMap(ax._r, ax.r2l);
+        if (ax._shiftPusher) {
+          incrementShift(ax, ax._fullDepth || 0, axShifts, true);
+        }
+        ax._r = ax.range.slice();
+        ax._rl = Lib.simpleMap(ax._r, ax.r2l);
 
-            return axDone;
-        };
-    }));
+        return axDone;
+      };
+    })
+  );
 };
 
 /**
@@ -2382,535 +2533,647 @@ axes.draw = function(gd, arg, opts) {
  * - ax._depth (when required only):
  * - and calls ax.setScale
  */
-axes.drawOne = function(gd, ax, opts) {
-    opts = opts || {};
-
-    var axShifts = opts.axShifts || {};
-    var overlayingShiftedAx = opts.overlayingShiftedAx || [];
-
-    var i, sp, plotinfo;
-
-    ax.setScale();
-
-    var fullLayout = gd._fullLayout;
-    var axId = ax._id;
-    var axLetter = axId.charAt(0);
-    var counterLetter = axes.counterLetter(axId);
-    var mainPlotinfo = fullLayout._plots[ax._mainSubplot];
-
-    // this happens when updating matched group with 'missing' axes
-    if(!mainPlotinfo) return;
-
-    ax._shiftPusher = ax.autoshift ||
-        overlayingShiftedAx.indexOf(ax._id) !== -1 ||
-        overlayingShiftedAx.indexOf(ax.overlaying) !== -1;
-    // An axis is also shifted by 1/2 of its own linewidth and inside tick length if applicable
-    // as well as its manually specified `shift` val if we're in the context of `autoshift`
-    if(ax._shiftPusher & ax.anchor === 'free') {
-        var selfPush = (ax.linewidth / 2 || 0);
-        if(ax.ticks === 'inside') {
-            selfPush += ax.ticklen;
-        }
-        incrementShift(ax, selfPush, axShifts, true);
-        incrementShift(ax, (ax.shift || 0), axShifts, false);
-    }
-
-    // Somewhat inelegant way of making sure that the shift value is only updated when the
-    // Axes.DrawOne() function is called from the right context. An issue when redrawing the
-    // axis as result of using the dragbox, for example.
-    if(opts.skipTitle !== true || ax._shift === undefined) ax._shift = setShiftVal(ax, axShifts);
-
-    var mainAxLayer = mainPlotinfo[axLetter + 'axislayer'];
-    var mainLinePosition = ax._mainLinePosition;
-    var mainLinePositionShift = mainLinePosition += ax._shift;
-    var mainMirrorPosition = ax._mainMirrorPosition;
-
-    var vals = ax._vals = axes.calcTicks(ax);
-
-    // Add a couple of axis properties that should cause us to recreate
-    // elements. Used in d3 data function.
-    var axInfo = [ax.mirror, mainLinePositionShift, mainMirrorPosition].join('_');
-    for(i = 0; i < vals.length; i++) {
-        vals[i].axInfo = axInfo;
-    }
-
-    // stash selections to avoid DOM queries e.g.
-    // - stash tickLabels selection, so that drawTitle can use it to scoot title
-    ax._selections = {};
-    // stash tick angle (including the computed 'auto' values) per tick-label class
-    // linkup 'previous' tick angles on redraws
-    if(ax._tickAngles) ax._prevTickAngles = ax._tickAngles;
-    ax._tickAngles = {};
-    // measure [in px] between axis position and outward-most part of bounding box
-    // (touching either the tick label or ticks)
-    // depth can be expansive to compute, so we only do so when required
-    ax._depth = null;
-
-    // calcLabelLevelBbox can be expensive,
-    // so make sure to not call it twice during the same Axes.drawOne call
-    // by stashing label-level bounding boxes per tick-label class
-    var llbboxes = {};
-    function getLabelLevelBbox(suffix) {
-        var cls = axId + (suffix || 'tick');
-        if(!llbboxes[cls]) llbboxes[cls] = calcLabelLevelBbox(ax, cls);
-        return llbboxes[cls];
-    }
-
-    if(!ax.visible) return;
-
-    var transTickFn = axes.makeTransTickFn(ax);
-    var transTickLabelFn = axes.makeTransTickLabelFn(ax);
-
-    var tickVals;
-    // We remove zero lines, grid lines, and inside ticks if they're within 1px of the end
-    // The key case here is removing zero lines when the axis bound is zero
-    var valsClipped;
-
-    var insideTicks = ax.ticks === 'inside';
-    var outsideTicks = ax.ticks === 'outside';
+axes.drawOne = function (gd, ax, opts) {
+  opts = opts || {};
+
+  var axShifts = opts.axShifts || {};
+  var overlayingShiftedAx = opts.overlayingShiftedAx || [];
+
+  var i, sp, plotinfo;
+
+  ax.setScale();
+
+  var fullLayout = gd._fullLayout;
+  var axId = ax._id;
+  var axLetter = axId.charAt(0);
+  var counterLetter = axes.counterLetter(axId);
+  var mainPlotinfo = fullLayout._plots[ax._mainSubplot];
+
+  // this happens when updating matched group with 'missing' axes
+  if (!mainPlotinfo) return;
+
+  ax._shiftPusher =
+    ax.autoshift ||
+    overlayingShiftedAx.indexOf(ax._id) !== -1 ||
+    overlayingShiftedAx.indexOf(ax.overlaying) !== -1;
+  // An axis is also shifted by 1/2 of its own linewidth and inside tick length if applicable
+  // as well as its manually specified `shift` val if we're in the context of `autoshift`
+  if (ax._shiftPusher & (ax.anchor === "free")) {
+    var selfPush = ax.linewidth / 2 || 0;
+    if (ax.ticks === "inside") {
+      selfPush += ax.ticklen;
+    }
+    incrementShift(ax, selfPush, axShifts, true);
+    incrementShift(ax, ax.shift || 0, axShifts, false);
+  }
+
+  // Somewhat inelegant way of making sure that the shift value is only updated when the
+  // Axes.DrawOne() function is called from the right context. An issue when redrawing the
+  // axis as result of using the dragbox, for example.
+  if (opts.skipTitle !== true || ax._shift === undefined)
+    ax._shift = setShiftVal(ax, axShifts);
+
+  var mainAxLayer = mainPlotinfo[axLetter + "axislayer"];
+  var mainLinePosition = ax._mainLinePosition;
+  var mainLinePositionShift = (mainLinePosition += ax._shift);
+  var mainMirrorPosition = ax._mainMirrorPosition;
+
+  var vals = (ax._vals = axes.calcTicks(ax));
+
+  // Add a couple of axis properties that should cause us to recreate
+  // elements. Used in d3 data function.
+  var axInfo = [ax.mirror, mainLinePositionShift, mainMirrorPosition].join("_");
+  for (i = 0; i < vals.length; i++) {
+    vals[i].axInfo = axInfo;
+  }
+
+  // stash selections to avoid DOM queries e.g.
+  // - stash tickLabels selection, so that drawTitle can use it to scoot title
+  ax._selections = {};
+  // stash tick angle (including the computed 'auto' values) per tick-label class
+  // linkup 'previous' tick angles on redraws
+  if (ax._tickAngles) ax._prevTickAngles = ax._tickAngles;
+  ax._tickAngles = {};
+  // measure [in px] between axis position and outward-most part of bounding box
+  // (touching either the tick label or ticks)
+  // depth can be expansive to compute, so we only do so when required
+  ax._depth = null;
+
+  // calcLabelLevelBbox can be expensive,
+  // so make sure to not call it twice during the same Axes.drawOne call
+  // by stashing label-level bounding boxes per tick-label class
+  var llbboxes = {};
+  function getLabelLevelBbox(suffix) {
+    var cls = axId + (suffix || "tick");
+    if (!llbboxes[cls]) llbboxes[cls] = calcLabelLevelBbox(ax, cls);
+    return llbboxes[cls];
+  }
+
+  if (!ax.visible) return;
+
+  var transTickFn = axes.makeTransTickFn(ax);
+  var transTickLabelFn = axes.makeTransTickLabelFn(ax);
+
+  var tickVals;
+  // We remove zero lines, grid lines, and inside ticks if they're within 1px of the end
+  // The key case here is removing zero lines when the axis bound is zero
+  var valsClipped;
+
+  var insideTicks = ax.ticks === "inside";
+  var outsideTicks = ax.ticks === "outside";
+
+  if (ax.tickson === "boundaries") {
+    var boundaryVals = getBoundaryVals(ax, vals);
+    valsClipped = axes.clipEnds(ax, boundaryVals);
+    tickVals = insideTicks ? valsClipped : boundaryVals;
+  } else {
+    valsClipped = axes.clipEnds(ax, vals);
+    tickVals =
+      insideTicks && ax.ticklabelmode !== "period" ? valsClipped : vals;
+  }
+
+  var gridVals = (ax._gridVals = valsClipped);
+  var dividerVals = getDividerVals(ax, vals);
+
+  if (!fullLayout._hasOnlyLargeSploms) {
+    var subplotsWithAx = ax._subplotsWith;
+
+    // keep track of which subplots (by main counter axis) we've already
+    // drawn grids for, so we don't overdraw overlaying subplots
+    var finishedGrids = {};
+
+    for (i = 0; i < subplotsWithAx.length; i++) {
+      sp = subplotsWithAx[i];
+      plotinfo = fullLayout._plots[sp];
+
+      var counterAxis = plotinfo[counterLetter + "axis"];
+      var mainCounterID = counterAxis._mainAxis._id;
+      if (finishedGrids[mainCounterID]) continue;
+      finishedGrids[mainCounterID] = 1;
+
+      var gridPath =
+        axLetter === "x"
+          ? "M0," + counterAxis._offset + "v" + counterAxis._length
+          : "M" + counterAxis._offset + ",0h" + counterAxis._length;
+
+      axes.drawGrid(gd, ax, {
+        vals: gridVals,
+        counterAxis: counterAxis,
+        layer: plotinfo.gridlayer.select("." + axId),
+        minorLayer: plotinfo.minorGridlayer.select("." + axId),
+        path: gridPath,
+        transFn: transTickFn,
+      });
+      axes.drawZeroLine(gd, ax, {
+        counterAxis: counterAxis,
+        layer: plotinfo.zerolinelayer,
+        path: gridPath,
+        transFn: transTickFn,
+      });
+    }
+  }
+
+  var tickPath;
+
+  var majorTickSigns = axes.getTickSigns(ax);
+  var minorTickSigns = axes.getTickSigns(ax, "minor");
+
+  if (ax.ticks || (ax.minor && ax.minor.ticks)) {
+    var majorTickPath = axes.makeTickPath(
+      ax,
+      mainLinePositionShift,
+      majorTickSigns[2]
+    );
+    var minorTickPath = axes.makeTickPath(
+      ax,
+      mainLinePositionShift,
+      minorTickSigns[2],
+      { minor: true }
+    );
 
-    if(ax.tickson === 'boundaries') {
-        var boundaryVals = getBoundaryVals(ax, vals);
-        valsClipped = axes.clipEnds(ax, boundaryVals);
-        tickVals = insideTicks ? valsClipped : boundaryVals;
+    var mirrorMajorTickPath;
+    var mirrorMinorTickPath;
+
+    var fullMajorTickPath;
+    var fullMinorTickPath;
+
+    if (ax._anchorAxis && ax.mirror && ax.mirror !== true) {
+      mirrorMajorTickPath = axes.makeTickPath(
+        ax,
+        mainMirrorPosition,
+        majorTickSigns[3]
+      );
+      mirrorMinorTickPath = axes.makeTickPath(
+        ax,
+        mainMirrorPosition,
+        minorTickSigns[3],
+        { minor: true }
+      );
+
+      fullMajorTickPath = majorTickPath + mirrorMajorTickPath;
+      fullMinorTickPath = minorTickPath + mirrorMinorTickPath;
     } else {
-        valsClipped = axes.clipEnds(ax, vals);
-        tickVals = (insideTicks && ax.ticklabelmode !== 'period') ? valsClipped : vals;
-    }
-
-    var gridVals = ax._gridVals = valsClipped;
-    var dividerVals = getDividerVals(ax, vals);
-
-    if(!fullLayout._hasOnlyLargeSploms) {
-        var subplotsWithAx = ax._subplotsWith;
-
-        // keep track of which subplots (by main counter axis) we've already
-        // drawn grids for, so we don't overdraw overlaying subplots
-        var finishedGrids = {};
-
-        for(i = 0; i < subplotsWithAx.length; i++) {
-            sp = subplotsWithAx[i];
-            plotinfo = fullLayout._plots[sp];
-
-            var counterAxis = plotinfo[counterLetter + 'axis'];
-            var mainCounterID = counterAxis._mainAxis._id;
-            if(finishedGrids[mainCounterID]) continue;
-            finishedGrids[mainCounterID] = 1;
-
-            var gridPath = axLetter === 'x' ?
-                'M0,' + counterAxis._offset + 'v' + counterAxis._length :
-                'M' + counterAxis._offset + ',0h' + counterAxis._length;
+      mirrorMajorTickPath = "";
+      mirrorMinorTickPath = "";
+      fullMajorTickPath = majorTickPath;
+      fullMinorTickPath = minorTickPath;
+    }
+
+    if (ax.showdividers && outsideTicks && ax.tickson === "boundaries") {
+      var dividerLookup = {};
+      for (i = 0; i < dividerVals.length; i++) {
+        dividerLookup[dividerVals[i].x] = 1;
+      }
+      tickPath = function (d) {
+        return dividerLookup[d.x] ? mirrorMajorTickPath : fullMajorTickPath;
+      };
+    } else {
+      tickPath = function (d) {
+        return d.minor ? fullMinorTickPath : fullMajorTickPath;
+      };
+    }
+  }
+
+  axes.drawTicks(gd, ax, {
+    vals: tickVals,
+    layer: mainAxLayer,
+    path: tickPath,
+    transFn: transTickFn,
+  });
+
+  if (ax.mirror === "allticks") {
+    var tickSubplots = Object.keys(ax._linepositions || {});
+
+    for (i = 0; i < tickSubplots.length; i++) {
+      sp = tickSubplots[i];
+      plotinfo = fullLayout._plots[sp];
+      // [bottom or left, top or right], free and main are handled above
+      var linepositions = ax._linepositions[sp] || [];
+
+      var p0 = linepositions[0];
+      var p1 = linepositions[1];
+      var isMinor = linepositions[2];
+
+      var spTickPath =
+        axes.makeTickPath(
+          ax,
+          p0,
+          isMinor ? majorTickSigns[0] : minorTickSigns[0],
+          { minor: isMinor }
+        ) +
+        axes.makeTickPath(
+          ax,
+          p1,
+          isMinor ? majorTickSigns[1] : minorTickSigns[1],
+          { minor: isMinor }
+        );
 
-            axes.drawGrid(gd, ax, {
-                vals: gridVals,
-                counterAxis: counterAxis,
-                layer: plotinfo.gridlayer.select('.' + axId),
-                minorLayer: plotinfo.minorGridlayer.select('.' + axId),
-                path: gridPath,
-                transFn: transTickFn
-            });
-            axes.drawZeroLine(gd, ax, {
-                counterAxis: counterAxis,
-                layer: plotinfo.zerolinelayer,
-                path: gridPath,
-                transFn: transTickFn
-            });
-        }
+      axes.drawTicks(gd, ax, {
+        vals: tickVals,
+        layer: plotinfo[axLetter + "axislayer"],
+        path: spTickPath,
+        transFn: transTickFn,
+      });
     }
+  }
 
-    var tickPath;
+  var seq = [];
 
-    var majorTickSigns = axes.getTickSigns(ax);
-    var minorTickSigns = axes.getTickSigns(ax, 'minor');
+  // tick labels - for now just the main labels.
+  // TODO: mirror labels, esp for subplots
 
-    if(ax.ticks || (ax.minor && ax.minor.ticks)) {
-        var majorTickPath = axes.makeTickPath(ax, mainLinePositionShift, majorTickSigns[2]);
-        var minorTickPath = axes.makeTickPath(ax, mainLinePositionShift, minorTickSigns[2], { minor: true });
-
-        var mirrorMajorTickPath;
-        var mirrorMinorTickPath;
-
-        var fullMajorTickPath;
-        var fullMinorTickPath;
-
-        if(ax._anchorAxis && ax.mirror && ax.mirror !== true) {
-            mirrorMajorTickPath = axes.makeTickPath(ax, mainMirrorPosition, majorTickSigns[3]);
-            mirrorMinorTickPath = axes.makeTickPath(ax, mainMirrorPosition, minorTickSigns[3], { minor: true });
-
-            fullMajorTickPath = majorTickPath + mirrorMajorTickPath;
-            fullMinorTickPath = minorTickPath + mirrorMinorTickPath;
-        } else {
-            mirrorMajorTickPath = '';
-            mirrorMinorTickPath = '';
-            fullMajorTickPath = majorTickPath;
-            fullMinorTickPath = minorTickPath;
-        }
-
-        if(ax.showdividers && outsideTicks && ax.tickson === 'boundaries') {
-            var dividerLookup = {};
-            for(i = 0; i < dividerVals.length; i++) {
-                dividerLookup[dividerVals[i].x] = 1;
-            }
-            tickPath = function(d) {
-                return dividerLookup[d.x] ? mirrorMajorTickPath : fullMajorTickPath;
-            };
-        } else {
-            tickPath = function(d) {
-                return d.minor ? fullMinorTickPath : fullMajorTickPath;
-            };
-        }
-    }
-
-    axes.drawTicks(gd, ax, {
-        vals: tickVals,
-        layer: mainAxLayer,
-        path: tickPath,
-        transFn: transTickFn
+  seq.push(function () {
+    return axes.drawLabels(gd, ax, {
+      vals: vals,
+      layer: mainAxLayer,
+      plotinfo: plotinfo,
+      transFn: transTickLabelFn,
+      labelFns: axes.makeLabelFns(ax, mainLinePositionShift),
     });
+  });
+
+  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]
+            ),
+          });
+        });
+      });
 
-    if(ax.mirror === 'allticks') {
-        var tickSubplots = Object.keys(ax._linepositions || {});
-
-        for(i = 0; i < tickSubplots.length; i++) {
-            sp = tickSubplots[i];
-            plotinfo = fullLayout._plots[sp];
-            // [bottom or left, top or right], free and main are handled above
-            var linepositions = ax._linepositions[sp] || [];
-
-            var p0 = linepositions[0];
-            var p1 = linepositions[1];
-            var isMinor = linepositions[2];
-
-            var spTickPath =
-                axes.makeTickPath(ax, p0,
-                    isMinor ? majorTickSigns[0] : minorTickSigns[0],
-                    { minor: isMinor }
-                ) +
-                axes.makeTickPath(ax, p1,
-                    isMinor ? majorTickSigns[1] : minorTickSigns[1],
-                    { minor: isMinor }
-                );
-
-            axes.drawTicks(gd, ax, {
-                vals: tickVals,
-                layer: plotinfo[axLetter + 'axislayer'],
-                path: spTickPath,
-                transFn: transTickFn
-            });
-        }
-    }
-
-    var seq = [];
+    tickNames = tickNames.sort();
 
-    // tick labels - for now just the main labels.
-    // TODO: mirror labels, esp for subplots
+    ax.levels.forEach(function (_lvl, idx) {
+      seq.push(function () {
+        ax._depth =
+          majorTickSigns[4] *
+          (getLabelLevelBbox(tickNames.slice()[_lvl])[ax.side] -
+            mainLinePosition);
 
-    seq.push(function() {
-        return axes.drawLabels(gd, ax, {
-            vals: vals,
-            layer: mainAxLayer,
-            plotinfo: plotinfo,
-            transFn: transTickLabelFn,
-            labelFns: axes.makeLabelFns(ax, mainLinePositionShift)
+        var levelDividers = dividerVals.slice().filter(function (divider) {
+          return divider.level === idx;
         });
-    });
 
-    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])
-            });
+        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);
+    });
+  }
 
-        seq.push(function() {
-            ax._depth = majorTickSigns[4] * (getLabelLevelBbox('tick2')[ax.side] - mainLinePositionShift);
+  var hasRangeSlider = Registry.getComponentMethod(
+    "rangeslider",
+    "isVisible"
+  )(ax);
 
-            return drawDividers(gd, ax, {
-                vals: dividerVals,
-                layer: mainAxLayer,
-                path: axes.makeTickPath(ax, mainLinePositionShift, majorTickSigns[4], { len: ax._depth }),
-                transFn: transTickFn
-            });
-        });
-    } else if(ax.title.hasOwnProperty('standoff')) {
-        seq.push(function() {
-            ax._depth = majorTickSigns[4] * (getLabelLevelBbox()[ax.side] - mainLinePositionShift);
-        });
-    }
+  if (!opts.skipTitle && !(hasRangeSlider && ax.side === "bottom")) {
+    seq.push(function () {
+      return drawTitle(gd, ax);
+    });
+  }
 
-    var hasRangeSlider = Registry.getComponentMethod('rangeslider', 'isVisible')(ax);
+  seq.push(function () {
+    var s = ax.side.charAt(0);
+    var sMirror = OPPOSITE_SIDE[ax.side].charAt(0);
+    var pos = axes.getPxPosition(gd, ax);
+    var outsideTickLen = outsideTicks ? ax.ticklen : 0;
+    var llbbox;
+
+    var push;
+    var mirrorPush;
+    var rangeSliderPush;
+
+    if (ax.automargin || hasRangeSlider || ax._shiftPusher) {
+      if (ax.type === "multicategory") {
+        // hardcoded tick name, breakes only with plotly.py. Not sure if this is the right selection
+        // llbbox = getLabelLevelBbox('tick2');
+        llbbox = getLabelLevelBbox("tick" + String(ax.levelNr - 1));
+      } else {
+        llbbox = getLabelLevelBbox();
+        if (axLetter === "x" && s === "b") {
+          ax._depth = Math.max(
+            llbbox.width > 0 ? llbbox.bottom - pos : 0,
+            outsideTickLen
+          );
+        }
+      }
+    }
+
+    var axDepth = 0;
+    var titleDepth = 0;
+    if (ax._shiftPusher) {
+      axDepth = Math.max(
+        outsideTickLen,
+        llbbox.height > 0
+          ? s === "l"
+            ? pos - llbbox.left
+            : llbbox.right - pos
+          : 0
+      );
+      if (ax.title.text !== fullLayout._dfltTitle[axLetter]) {
+        titleDepth = (ax._titleStandoff || 0) + (ax._titleScoot || 0);
+        if (s === "l") {
+          titleDepth += approxTitleDepth(ax);
+        }
+      }
 
-    if(!opts.skipTitle &&
-        !(hasRangeSlider && ax.side === 'bottom')
-    ) {
-        seq.push(function() { return drawTitle(gd, ax); });
+      ax._fullDepth = Math.max(axDepth, titleDepth);
     }
 
-    seq.push(function() {
-        var s = ax.side.charAt(0);
-        var sMirror = OPPOSITE_SIDE[ax.side].charAt(0);
-        var pos = axes.getPxPosition(gd, ax);
-        var outsideTickLen = outsideTicks ? ax.ticklen : 0;
-        var llbbox;
-
-        var push;
-        var mirrorPush;
-        var rangeSliderPush;
-
-        if(ax.automargin || hasRangeSlider || ax._shiftPusher) {
-            if(ax.type === 'multicategory') {
-                llbbox = getLabelLevelBbox('tick2');
-            } else {
-                llbbox = getLabelLevelBbox();
-                if(axLetter === 'x' && s === 'b') {
-                    ax._depth = Math.max(llbbox.width > 0 ? llbbox.bottom - pos : 0, outsideTickLen);
-                }
-            }
+    if (ax.automargin) {
+      push = { x: 0, y: 0, r: 0, l: 0, t: 0, b: 0 };
+      var domainIndices = [0, 1];
+      var shift = typeof ax._shift === "number" ? ax._shift : 0;
+      if (axLetter === "x") {
+        if (s === "b") {
+          push[s] = ax._depth;
+        } else {
+          push[s] = ax._depth = Math.max(
+            llbbox.width > 0 ? pos - llbbox.top : 0,
+            outsideTickLen
+          );
+          domainIndices.reverse();
         }
 
-        var axDepth = 0;
-        var titleDepth = 0;
-        if(ax._shiftPusher) {
-            axDepth = Math.max(
-                outsideTickLen,
-                llbbox.height > 0 ? (s === 'l' ? pos - llbbox.left : llbbox.right - pos) : 0
-            );
-            if(ax.title.text !== fullLayout._dfltTitle[axLetter]) {
-                titleDepth = (ax._titleStandoff || 0) + (ax._titleScoot || 0);
-                if(s === 'l') {
-                    titleDepth += approxTitleDepth(ax);
-                }
-            }
-
-            ax._fullDepth = Math.max(axDepth, titleDepth);
+        if (llbbox.width > 0) {
+          var rExtra = llbbox.right - (ax._offset + ax._length);
+          if (rExtra > 0) {
+            push.xr = 1;
+            push.r = rExtra;
+          }
+          var lExtra = ax._offset - llbbox.left;
+          if (lExtra > 0) {
+            push.xl = 0;
+            push.l = lExtra;
+          }
+        }
+      } else {
+        if (s === "l") {
+          ax._depth = Math.max(
+            llbbox.height > 0 ? pos - llbbox.left : 0,
+            outsideTickLen
+          );
+          push[s] = ax._depth - shift;
+        } else {
+          ax._depth = Math.max(
+            llbbox.height > 0 ? llbbox.right - pos : 0,
+            outsideTickLen
+          );
+          push[s] = ax._depth + shift;
+          domainIndices.reverse();
         }
 
-        if(ax.automargin) {
-            push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0};
-            var domainIndices = [0, 1];
-            var shift = typeof ax._shift === 'number' ? ax._shift : 0;
-            if(axLetter === 'x') {
-                if(s === 'b') {
-                    push[s] = ax._depth;
-                } else {
-                    push[s] = ax._depth = Math.max(llbbox.width > 0 ? pos - llbbox.top : 0, outsideTickLen);
-                    domainIndices.reverse();
-                }
-
-                if(llbbox.width > 0) {
-                    var rExtra = llbbox.right - (ax._offset + ax._length);
-                    if(rExtra > 0) {
-                        push.xr = 1;
-                        push.r = rExtra;
-                    }
-                    var lExtra = ax._offset - llbbox.left;
-                    if(lExtra > 0) {
-                        push.xl = 0;
-                        push.l = lExtra;
-                    }
-                }
-            } else {
-                if(s === 'l') {
-                    ax._depth = Math.max(llbbox.height > 0 ? pos - llbbox.left : 0, outsideTickLen);
-                    push[s] = ax._depth - shift;
-                } else {
-                    ax._depth = Math.max(llbbox.height > 0 ? llbbox.right - pos : 0, outsideTickLen);
-                    push[s] = ax._depth + shift;
-                    domainIndices.reverse();
-                }
-
-                if(llbbox.height > 0) {
-                    var bExtra = llbbox.bottom - (ax._offset + ax._length);
-                    if(bExtra > 0) {
-                        push.yb = 0;
-                        push.b = bExtra;
-                    }
-                    var tExtra = ax._offset - llbbox.top;
-                    if(tExtra > 0) {
-                        push.yt = 1;
-                        push.t = tExtra;
-                    }
-                }
-            }
-
-            push[counterLetter] = ax.anchor === 'free' ?
-                ax.position :
-                ax._anchorAxis.domain[domainIndices[0]];
-
-            if(ax.title.text !== fullLayout._dfltTitle[axLetter]) {
-                push[s] += approxTitleDepth(ax) + (ax.title.standoff || 0);
-            }
-
-            if(ax.mirror && ax.anchor !== 'free') {
-                mirrorPush = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0};
-
-                mirrorPush[sMirror] = ax.linewidth;
-                if(ax.mirror && ax.mirror !== true) mirrorPush[sMirror] += outsideTickLen;
-
-                if(ax.mirror === true || ax.mirror === 'ticks') {
-                    mirrorPush[counterLetter] = ax._anchorAxis.domain[domainIndices[1]];
-                } else if(ax.mirror === 'all' || ax.mirror === 'allticks') {
-                    mirrorPush[counterLetter] = [ax._counterDomainMin, ax._counterDomainMax][domainIndices[1]];
-                }
-            }
+        if (llbbox.height > 0) {
+          var bExtra = llbbox.bottom - (ax._offset + ax._length);
+          if (bExtra > 0) {
+            push.yb = 0;
+            push.b = bExtra;
+          }
+          var tExtra = ax._offset - llbbox.top;
+          if (tExtra > 0) {
+            push.yt = 1;
+            push.t = tExtra;
+          }
         }
-        if(hasRangeSlider) {
-            rangeSliderPush = Registry.getComponentMethod('rangeslider', 'autoMarginOpts')(gd, ax);
+      }
+
+      push[counterLetter] =
+        ax.anchor === "free"
+          ? ax.position
+          : ax._anchorAxis.domain[domainIndices[0]];
+
+      if (ax.title.text !== fullLayout._dfltTitle[axLetter]) {
+        push[s] += approxTitleDepth(ax) + (ax.title.standoff || 0);
+      }
+
+      if (ax.mirror && ax.anchor !== "free") {
+        mirrorPush = { x: 0, y: 0, r: 0, l: 0, t: 0, b: 0 };
+
+        mirrorPush[sMirror] = ax.linewidth;
+        if (ax.mirror && ax.mirror !== true)
+          mirrorPush[sMirror] += outsideTickLen;
+
+        if (ax.mirror === true || ax.mirror === "ticks") {
+          mirrorPush[counterLetter] = ax._anchorAxis.domain[domainIndices[1]];
+        } else if (ax.mirror === "all" || ax.mirror === "allticks") {
+          mirrorPush[counterLetter] = [
+            ax._counterDomainMin,
+            ax._counterDomainMax,
+          ][domainIndices[1]];
         }
+      }
+    }
+    if (hasRangeSlider) {
+      rangeSliderPush = Registry.getComponentMethod(
+        "rangeslider",
+        "autoMarginOpts"
+      )(gd, ax);
+    }
 
-        if(typeof ax.automargin === 'string') {
-            filterPush(push, ax.automargin);
-            filterPush(mirrorPush, ax.automargin);
-        }
+    if (typeof ax.automargin === "string") {
+      filterPush(push, ax.automargin);
+      filterPush(mirrorPush, ax.automargin);
+    }
 
-        Plots.autoMargin(gd, axAutoMarginID(ax), push);
-        Plots.autoMargin(gd, axMirrorAutoMarginID(ax), mirrorPush);
-        Plots.autoMargin(gd, rangeSliderAutoMarginID(ax), rangeSliderPush);
-    });
+    Plots.autoMargin(gd, axAutoMarginID(ax), push);
+    Plots.autoMargin(gd, axMirrorAutoMarginID(ax), mirrorPush);
+    Plots.autoMargin(gd, rangeSliderAutoMarginID(ax), rangeSliderPush);
+  });
 
-    return Lib.syncOrAsync(seq);
+  return Lib.syncOrAsync(seq);
 };
 
 function filterPush(push, automargin) {
-    if(!push) return;
-
-    var keepMargin = Object.keys(MARGIN_MAPPING).reduce(function(data, nextKey) {
-        if(automargin.indexOf(nextKey) !== -1) {
-            MARGIN_MAPPING[nextKey].forEach(function(key) { data[key] = 1;});
-        }
-        return data;
-    }, {});
-    Object.keys(push).forEach(function(key) {
-        if(!keepMargin[key]) {
-            if(key.length === 1) push[key] = 0;
-            else delete push[key];
-        }
-    });
+  if (!push) return;
+
+  var keepMargin = Object.keys(MARGIN_MAPPING).reduce(function (data, nextKey) {
+    if (automargin.indexOf(nextKey) !== -1) {
+      MARGIN_MAPPING[nextKey].forEach(function (key) {
+        data[key] = 1;
+      });
+    }
+    return data;
+  }, {});
+  Object.keys(push).forEach(function (key) {
+    if (!keepMargin[key]) {
+      if (key.length === 1) push[key] = 0;
+      else delete push[key];
+    }
+  });
 }
 
 function getBoundaryVals(ax, vals) {
-    var out = [];
-    var i;
-
-    // boundaryVals are 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 out = [];
+  var i;
 
-    if(vals.length) {
-        for(i = 0; i < vals.length; i++) {
-            _push(vals[i], 0);
-        }
-        _push(vals[i - 1], 1);
+  // boundaryVals are 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 }));
     }
+  };
 
-    return out;
-}
+  if (vals.length) {
+    for (i = 0; i < vals.length; i++) {
+      _push(vals[i], 0);
+    }
+    _push(vals[i - 1], 1);
+  }
 
-function getSecondaryLabelVals(ax, vals) {
-    var out = [];
-    var lookup = {};
+  return out;
+}
 
-    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);
-
-    // 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}));
-        }
-    };
-
-    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;
+  var out = [];
+  var i, current;
+
+  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, 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) {
+    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);
         }
-        _push(vals[i - 1], reversed ? 0 : 1);
-    }
-
-    return out;
+        current = d.texts[_lvl];
+        // text2
+      }
+      _push(vals[i - 1], reversed ? 0 : 1);
+    });
+  }
+  return out;
 }
 
 function calcLabelLevelBbox(ax, cls) {
-    var top, bottom;
-    var left, right;
-
-    if(ax._selections[cls].size()) {
-        top = Infinity;
-        bottom = -Infinity;
-        left = Infinity;
-        right = -Infinity;
-        ax._selections[cls].each(function() {
-            var thisLabel = selectTickLabel(this);
-            // Use parent node <g.(x|y)tick>, to make Drawing.bBox
-            // retrieve a bbox computed with transform info
-            //
-            // To improve perf, it would be nice to use `thisLabel.node()`
-            // (like in fixLabelOverlaps) instead and use Axes.getPxPosition
-            // together with the makeLabelFns outputs and `tickangle`
-            // to compute one bbox per (tick value x tick style)
-            var bb = Drawing.bBox(thisLabel.node().parentNode);
-            top = Math.min(top, bb.top);
-            bottom = Math.max(bottom, bb.bottom);
-            left = Math.min(left, bb.left);
-            right = Math.max(right, bb.right);
-        });
-    } else {
-        top = 0;
-        bottom = 0;
-        left = 0;
-        right = 0;
-    }
-
-    return {
-        top: top,
-        bottom: bottom,
-        left: left,
-        right: right,
-        height: bottom - top,
-        width: right - left
-    };
+  var top, bottom;
+  var left, right;
+
+  if (ax._selections[cls].size()) {
+    top = Infinity;
+    bottom = -Infinity;
+    left = Infinity;
+    right = -Infinity;
+    ax._selections[cls].each(function () {
+      var thisLabel = selectTickLabel(this);
+      // Use parent node <g.(x|y)tick>, to make Drawing.bBox
+      // retrieve a bbox computed with transform info
+      //
+      // To improve perf, it would be nice to use `thisLabel.node()`
+      // (like in fixLabelOverlaps) instead and use Axes.getPxPosition
+      // together with the makeLabelFns outputs and `tickangle`
+      // to compute one bbox per (tick value x tick style)
+      var bb = Drawing.bBox(thisLabel.node().parentNode);
+      top = Math.min(top, bb.top);
+      bottom = Math.max(bottom, bb.bottom);
+      left = Math.min(left, bb.left);
+      right = Math.max(right, bb.right);
+    });
+  } else {
+    top = 0;
+    bottom = 0;
+    left = 0;
+    right = 0;
+  }
+
+  return {
+    top: top,
+    bottom: bottom,
+    left: left,
+    right: right,
+    height: bottom - top,
+    width: right - left,
+  };
 }
 
 /**
@@ -2927,22 +3190,24 @@ function calcLabelLevelBbox(ax, cls) {
  *  - [3]: sign for ticks mirroring 'ax.side'
  *  - [4]: sign of arrow starting at axis pointing towards margin
  */
-axes.getTickSigns = function(ax, minor) {
-    var axLetter = ax._id.charAt(0);
-    var sideOpposite = {x: 'top', y: 'right'}[axLetter];
-    var main = ax.side === sideOpposite ? 1 : -1;
-    var out = [-1, 1, main, -main];
-    // then we flip if outside XOR y axis
-
-    var ticks = minor ? (ax.minor || {}).ticks : ax.ticks;
-    if((ticks !== 'inside') === (axLetter === 'x')) {
-        out = out.map(function(v) { return -v; });
-    }
-    // independent of `ticks`; do not flip this one
-    if(ax.side) {
-        out.push({l: -1, t: -1, r: 1, b: 1}[ax.side.charAt(0)]);
-    }
-    return out;
+axes.getTickSigns = function (ax, minor) {
+  var axLetter = ax._id.charAt(0);
+  var sideOpposite = { x: "top", y: "right" }[axLetter];
+  var main = ax.side === sideOpposite ? 1 : -1;
+  var out = [-1, 1, main, -main];
+  // then we flip if outside XOR y axis
+
+  var ticks = minor ? (ax.minor || {}).ticks : ax.ticks;
+  if ((ticks !== "inside") === (axLetter === "x")) {
+    out = out.map(function (v) {
+      return -v;
+    });
+  }
+  // independent of `ticks`; do not flip this one
+  if (ax.side) {
+    out.push({ l: -1, t: -1, r: 1, b: 1 }[ax.side.charAt(0)]);
+  }
+  return out;
 };
 
 /**
@@ -2954,80 +3219,75 @@ axes.getTickSigns = function(ax, minor) {
  *  - {fn} l2p
  * @return {fn} function of calcTicks items
  */
-axes.makeTransTickFn = function(ax) {
-    return ax._id.charAt(0) === 'x' ?
-        function(d) { return strTranslate(ax._offset + ax.l2p(d.x), 0); } :
-        function(d) { return strTranslate(0, ax._offset + ax.l2p(d.x)); };
+axes.makeTransTickFn = function (ax) {
+  return ax._id.charAt(0) === "x"
+    ? function (d) {
+        return strTranslate(ax._offset + ax.l2p(d.x), 0);
+      }
+    : function (d) {
+        return strTranslate(0, ax._offset + ax.l2p(d.x));
+      };
 };
 
-axes.makeTransTickLabelFn = function(ax) {
-    var uv = getTickLabelUV(ax);
-    var u = uv[0];
-    var v = uv[1];
-
-    return ax._id.charAt(0) === 'x' ?
-        function(d) {
-            return strTranslate(
-                u + ax._offset + ax.l2p(getPosX(d)),
-                v
-            );
-        } :
-        function(d) {
-            return strTranslate(
-                v,
-                u + ax._offset + ax.l2p(getPosX(d))
-            );
-        };
+axes.makeTransTickLabelFn = function (ax) {
+  var uv = getTickLabelUV(ax);
+  var u = uv[0];
+  var v = uv[1];
+
+  return ax._id.charAt(0) === "x"
+    ? function (d) {
+        return strTranslate(u + ax._offset + ax.l2p(getPosX(d)), v);
+      }
+    : function (d) {
+        return strTranslate(v, u + ax._offset + ax.l2p(getPosX(d)));
+      };
 };
 
 function getPosX(d) {
-    return d.periodX !== undefined ? d.periodX : d.x;
+  return d.periodX !== undefined ? d.periodX : d.x;
 }
 
 // u is a shift along the axis,
 // v is a shift perpendicular to the axis
 function getTickLabelUV(ax) {
-    var ticklabelposition = ax.ticklabelposition || '';
-    var has = function(str) {
-        return ticklabelposition.indexOf(str) !== -1;
-    };
-
-    var isTop = has('top');
-    var isLeft = has('left');
-    var isRight = has('right');
-    var isBottom = has('bottom');
-    var isInside = has('inside');
-
-    var isAligned = isBottom || isLeft || isTop || isRight;
-
-    // early return
-    if(!isAligned && !isInside) return [0, 0];
-
-    var side = ax.side;
-
-    var u = isAligned ? (ax.tickwidth || 0) / 2 : 0;
-    var v = TEXTPAD;
-
-    var fontSize = ax.tickfont ? ax.tickfont.size : 12;
-    if(isBottom || isTop) {
-        u += fontSize * CAP_SHIFT;
-        v += (ax.linewidth || 0) / 2;
-    }
-    if(isLeft || isRight) {
-        u += (ax.linewidth || 0) / 2;
-        v += TEXTPAD;
-    }
-    if(isInside && side === 'top') {
-        v -= fontSize * (1 - CAP_SHIFT);
-    }
-
-    if(isLeft || isTop) u = -u;
-    if(side === 'bottom' || side === 'right') v = -v;
-
-    return [
-        isAligned ? u : 0,
-        isInside ? v : 0
-    ];
+  var ticklabelposition = ax.ticklabelposition || "";
+  var has = function (str) {
+    return ticklabelposition.indexOf(str) !== -1;
+  };
+
+  var isTop = has("top");
+  var isLeft = has("left");
+  var isRight = has("right");
+  var isBottom = has("bottom");
+  var isInside = has("inside");
+
+  var isAligned = isBottom || isLeft || isTop || isRight;
+
+  // early return
+  if (!isAligned && !isInside) return [0, 0];
+
+  var side = ax.side;
+
+  var u = isAligned ? (ax.tickwidth || 0) / 2 : 0;
+  var v = TEXTPAD;
+
+  var fontSize = ax.tickfont ? ax.tickfont.size : 12;
+  if (isBottom || isTop) {
+    u += fontSize * CAP_SHIFT;
+    v += (ax.linewidth || 0) / 2;
+  }
+  if (isLeft || isRight) {
+    u += (ax.linewidth || 0) / 2;
+    v += TEXTPAD;
+  }
+  if (isInside && side === "top") {
+    v -= fontSize * (1 - CAP_SHIFT);
+  }
+
+  if (isLeft || isTop) u = -u;
+  if (side === "bottom" || side === "right") v = -v;
+
+  return [isAligned ? u : 0, isInside ? v : 0];
 }
 
 /**
@@ -3043,20 +3303,20 @@ function getTickLabelUV(ax) {
  * - {number (optional)} len tick length
  * @return {string}
  */
-axes.makeTickPath = function(ax, shift, sgn, opts) {
-    if(!opts) opts = {};
-    var minor = opts.minor;
-    if(minor && !ax.minor) return '';
+axes.makeTickPath = function (ax, shift, sgn, opts) {
+  if (!opts) opts = {};
+  var minor = opts.minor;
+  if (minor && !ax.minor) return "";
 
-    var len = opts.len !== undefined ? opts.len :
-        minor ? ax.minor.ticklen : ax.ticklen;
+  var len =
+    opts.len !== undefined ? opts.len : minor ? ax.minor.ticklen : ax.ticklen;
 
-    var axLetter = ax._id.charAt(0);
-    var pad = (ax.linewidth || 1) / 2;
+  var axLetter = ax._id.charAt(0);
+  var pad = (ax.linewidth || 1) / 2;
 
-    return axLetter === 'x' ?
-        'M0,' + (shift + pad * sgn) + 'v' + (len * sgn) :
-        'M' + (shift + pad * sgn) + ',0h' + (len * sgn);
+  return axLetter === "x"
+    ? "M0," + (shift + pad * sgn) + "v" + len * sgn
+    : "M" + (shift + pad * sgn) + ",0h" + len * sgn;
 };
 
 /**
@@ -3080,159 +3340,169 @@ axes.makeTickPath = function(ax, shift, sgn, opts) {
  *  - {number} labelStandoff (gap parallel to ticks)
  *  - {number} labelShift (gap perpendicular to ticks)
  */
-axes.makeLabelFns = function(ax, shift, angle) {
-    var ticklabelposition = ax.ticklabelposition || '';
-    var has = function(str) {
-        return ticklabelposition.indexOf(str) !== -1;
-    };
-
-    var isTop = has('top');
-    var isLeft = has('left');
-    var isRight = has('right');
-    var isBottom = has('bottom');
-    var isAligned = isBottom || isLeft || isTop || isRight;
-
-    var insideTickLabels = has('inside');
-    var labelsOverTicks =
-        (ticklabelposition === 'inside' && ax.ticks === 'inside') ||
-        (!insideTickLabels && ax.ticks === 'outside' && ax.tickson !== 'boundaries');
-
-    var labelStandoff = 0;
-    var labelShift = 0;
-
-    var tickLen = labelsOverTicks ? ax.ticklen : 0;
-    if(insideTickLabels) {
-        tickLen *= -1;
-    } else if(isAligned) {
-        tickLen = 0;
-    }
-
-    if(labelsOverTicks) {
-        labelStandoff += tickLen;
-        if(angle) {
-            var rad = Lib.deg2rad(angle);
-            labelStandoff = tickLen * Math.cos(rad) + 1;
-            labelShift = tickLen * Math.sin(rad);
+axes.makeLabelFns = function (ax, shift, angle) {
+  var ticklabelposition = ax.ticklabelposition || "";
+  var has = function (str) {
+    return ticklabelposition.indexOf(str) !== -1;
+  };
+
+  var isTop = has("top");
+  var isLeft = has("left");
+  var isRight = has("right");
+  var isBottom = has("bottom");
+  var isAligned = isBottom || isLeft || isTop || isRight;
+
+  var insideTickLabels = has("inside");
+  var labelsOverTicks =
+    (ticklabelposition === "inside" && ax.ticks === "inside") ||
+    (!insideTickLabels &&
+      ax.ticks === "outside" &&
+      ax.tickson !== "boundaries");
+
+  var labelStandoff = 0;
+  var labelShift = 0;
+
+  var tickLen = labelsOverTicks ? ax.ticklen : 0;
+  if (insideTickLabels) {
+    tickLen *= -1;
+  } else if (isAligned) {
+    tickLen = 0;
+  }
+
+  if (labelsOverTicks) {
+    labelStandoff += tickLen;
+    if (angle) {
+      var rad = Lib.deg2rad(angle);
+      labelStandoff = tickLen * Math.cos(rad) + 1;
+      labelShift = tickLen * Math.sin(rad);
+    }
+  }
+
+  if (ax.showticklabels && (labelsOverTicks || ax.showline)) {
+    labelStandoff += 0.2 * ax.tickfont.size;
+  }
+  labelStandoff += ((ax.linewidth || 1) / 2) * (insideTickLabels ? -1 : 1);
+
+  var out = {
+    labelStandoff: labelStandoff,
+    labelShift: labelShift,
+  };
+
+  var x0, y0, ff, flipIt;
+  var xQ = 0;
+
+  var side = ax.side;
+  var axLetter = ax._id.charAt(0);
+  var tickangle = ax.tickangle;
+  var endSide;
+  if (axLetter === "x") {
+    endSide =
+      (!insideTickLabels && side === "bottom") ||
+      (insideTickLabels && side === "top");
+
+    flipIt = endSide ? 1 : -1;
+    if (insideTickLabels) flipIt *= -1;
+
+    x0 = labelShift * flipIt;
+    y0 = shift + labelStandoff * flipIt;
+    ff = endSide ? 1 : -0.2;
+    if (Math.abs(tickangle) === 90) {
+      if (insideTickLabels) {
+        ff += MID_SHIFT;
+      } else {
+        if (tickangle === -90 && side === "bottom") {
+          ff = CAP_SHIFT;
+        } else if (tickangle === 90 && side === "top") {
+          ff = MID_SHIFT;
+        } else {
+          ff = 0.5;
         }
-    }
+      }
 
-    if(ax.showticklabels && (labelsOverTicks || ax.showline)) {
-        labelStandoff += 0.2 * ax.tickfont.size;
+      xQ = (MID_SHIFT / 2) * (tickangle / 90);
     }
-    labelStandoff += (ax.linewidth || 1) / 2 * (insideTickLabels ? -1 : 1);
 
-    var out = {
-        labelStandoff: labelStandoff,
-        labelShift: labelShift
+    out.xFn = function (d) {
+      return d.dx + x0 + xQ * d.fontSize;
     };
+    out.yFn = function (d) {
+      return d.dy + y0 + d.fontSize * ff;
+    };
+    out.anchorFn = function (d, a) {
+      if (isAligned) {
+        if (isLeft) return "end";
+        if (isRight) return "start";
+      }
 
-    var x0, y0, ff, flipIt;
-    var xQ = 0;
-
-    var side = ax.side;
-    var axLetter = ax._id.charAt(0);
-    var tickangle = ax.tickangle;
-    var endSide;
-    if(axLetter === 'x') {
-        endSide =
-            (!insideTickLabels && side === 'bottom') ||
-            (insideTickLabels && side === 'top');
-
-        flipIt = endSide ? 1 : -1;
-        if(insideTickLabels) flipIt *= -1;
-
-        x0 = labelShift * flipIt;
-        y0 = shift + labelStandoff * flipIt;
-        ff = endSide ? 1 : -0.2;
-        if(Math.abs(tickangle) === 90) {
-            if(insideTickLabels) {
-                ff += MID_SHIFT;
-            } else {
-                if(tickangle === -90 && side === 'bottom') {
-                    ff = CAP_SHIFT;
-                } else if(tickangle === 90 && side === 'top') {
-                    ff = MID_SHIFT;
-                } else {
-                    ff = 0.5;
-                }
-            }
-
-            xQ = (MID_SHIFT / 2) * (tickangle / 90);
-        }
-
-        out.xFn = function(d) { return d.dx + x0 + xQ * d.fontSize; };
-        out.yFn = function(d) { return d.dy + y0 + d.fontSize * ff; };
-        out.anchorFn = function(d, a) {
-            if(isAligned) {
-                if(isLeft) return 'end';
-                if(isRight) return 'start';
-            }
-
-            if(!isNumeric(a) || a === 0 || a === 180) {
-                return 'middle';
-            }
-
-            return ((a * flipIt < 0) !== insideTickLabels) ? 'end' : 'start';
-        };
-        out.heightFn = function(d, a, h) {
-            return (a < -60 || a > 60) ? -0.5 * h :
-                ((ax.side === 'top') !== insideTickLabels) ? -h :
-                0;
-        };
-    } else if(axLetter === 'y') {
-        endSide =
-            (!insideTickLabels && side === 'left') ||
-            (insideTickLabels && side === 'right');
-
-        flipIt = endSide ? 1 : -1;
-        if(insideTickLabels) flipIt *= -1;
+      if (!isNumeric(a) || a === 0 || a === 180) {
+        return "middle";
+      }
 
-        x0 = labelStandoff;
-        y0 = labelShift * flipIt;
+      return a * flipIt < 0 !== insideTickLabels ? "end" : "start";
+    };
+    out.heightFn = function (d, a, h) {
+      return a < -60 || a > 60
+        ? -0.5 * h
+        : (ax.side === "top") !== insideTickLabels
+        ? -h
+        : 0;
+    };
+  } else if (axLetter === "y") {
+    endSide =
+      (!insideTickLabels && side === "left") ||
+      (insideTickLabels && side === "right");
+
+    flipIt = endSide ? 1 : -1;
+    if (insideTickLabels) flipIt *= -1;
+
+    x0 = labelStandoff;
+    y0 = labelShift * flipIt;
+    ff = 0;
+    if (!insideTickLabels && Math.abs(tickangle) === 90) {
+      if (
+        (tickangle === -90 && side === "left") ||
+        (tickangle === 90 && side === "right")
+      ) {
+        ff = CAP_SHIFT;
+      } else {
+        ff = 0.5;
+      }
+    }
+
+    if (insideTickLabels) {
+      var ang = isNumeric(tickangle) ? +tickangle : 0;
+      if (ang !== 0) {
+        var rA = Lib.deg2rad(ang);
+        xQ = Math.abs(Math.sin(rA)) * CAP_SHIFT * flipIt;
         ff = 0;
-        if(!insideTickLabels && Math.abs(tickangle) === 90) {
-            if(
-                (tickangle === -90 && side === 'left') ||
-                (tickangle === 90 && side === 'right')
-            ) {
-                ff = CAP_SHIFT;
-            } else {
-                ff = 0.5;
-            }
-        }
-
-        if(insideTickLabels) {
-            var ang = isNumeric(tickangle) ? +tickangle : 0;
-            if(ang !== 0) {
-                var rA = Lib.deg2rad(ang);
-                xQ = Math.abs(Math.sin(rA)) * CAP_SHIFT * flipIt;
-                ff = 0;
-            }
-        }
+      }
+    }
 
-        out.xFn = function(d) { return d.dx + shift - (x0 + d.fontSize * ff) * flipIt + xQ * d.fontSize; };
-        out.yFn = function(d) { return d.dy + y0 + d.fontSize * MID_SHIFT; };
-        out.anchorFn = function(d, a) {
-            if(isNumeric(a) && Math.abs(a) === 90) {
-                return 'middle';
-            }
+    out.xFn = function (d) {
+      return d.dx + shift - (x0 + d.fontSize * ff) * flipIt + xQ * d.fontSize;
+    };
+    out.yFn = function (d) {
+      return d.dy + y0 + d.fontSize * MID_SHIFT;
+    };
+    out.anchorFn = function (d, a) {
+      if (isNumeric(a) && Math.abs(a) === 90) {
+        return "middle";
+      }
 
-            return endSide ? 'end' : 'start';
-        };
-        out.heightFn = function(d, a, h) {
-            if(ax.side === 'right') a *= -1;
+      return endSide ? "end" : "start";
+    };
+    out.heightFn = function (d, a, h) {
+      if (ax.side === "right") a *= -1;
 
-            return a < -30 ? -h :
-                a < 30 ? -0.5 * h :
-                0;
-        };
-    }
+      return a < -30 ? -h : a < 30 ? -0.5 * h : 0;
+    };
+  }
 
-    return out;
+  return out;
 };
 
 function tickDataFn(d) {
-    return [d.text, d.x, d.axInfo, d.font, d.fontSize, d.fontColor].join('_');
+  return [d.text, d.x, d.axInfo, d.font, d.fontSize, d.fontColor].join("_");
 }
 
 /**
@@ -3251,48 +3521,57 @@ function tickDataFn(d) {
  * - {fn} transFn
  * - {boolean} crisp (set to false to unset crisp-edge SVG rendering)
  */
-axes.drawTicks = function(gd, ax, opts) {
-    opts = opts || {};
-
-    var cls = ax._id + 'tick';
-
-    var vals = []
-        .concat(ax.minor && ax.minor.ticks ?
-            // minor vals
-            opts.vals.filter(function(d) { return d.minor && !d.noTick; }) :
-            []
-        )
-        .concat(ax.ticks ?
-            // major vals
-            opts.vals.filter(function(d) { return !d.minor && !d.noTick; }) :
-            []
-        );
+axes.drawTicks = function (gd, ax, opts) {
+  opts = opts || {};
+
+  var cls = ax._id + "tick";
+
+  var vals = []
+    .concat(
+      ax.minor && ax.minor.ticks
+        ? // minor vals
+          opts.vals.filter(function (d) {
+            return d.minor && !d.noTick;
+          })
+        : []
+    )
+    .concat(
+      ax.ticks
+        ? // major vals
+          opts.vals.filter(function (d) {
+            return !d.minor && !d.noTick;
+          })
+        : []
+    );
 
-    var ticks = opts.layer.selectAll('path.' + cls)
-        .data(vals, tickDataFn);
-
-    ticks.exit().remove();
-
-    ticks.enter().append('path')
-        .classed(cls, 1)
-        .classed('ticks', 1)
-        .classed('crisp', opts.crisp !== false)
-        .each(function(d) {
-            return Color.stroke(d3.select(this), d.minor ? ax.minor.tickcolor : ax.tickcolor);
-        })
-        .style('stroke-width', function(d) {
-            return Drawing.crispRound(
-                gd,
-                d.minor ? ax.minor.tickwidth : ax.tickwidth,
-                1
-            ) + 'px';
-        })
-        .attr('d', opts.path)
-        .style('display', null); // visible
-
-    hideCounterAxisInsideTickLabels(ax, [TICK_PATH]);
-
-    ticks.attr('transform', opts.transFn);
+  var ticks = opts.layer.selectAll("path." + cls).data(vals, tickDataFn);
+
+  ticks.exit().remove();
+
+  ticks
+    .enter()
+    .append("path")
+    .classed(cls, 1)
+    .classed("ticks", 1)
+    .classed("crisp", opts.crisp !== false)
+    .each(function (d) {
+      return Color.stroke(
+        d3.select(this),
+        d.minor ? ax.minor.tickcolor : ax.tickcolor
+      );
+    })
+    .style("stroke-width", function (d) {
+      return (
+        Drawing.crispRound(gd, d.minor ? ax.minor.tickwidth : ax.tickwidth, 1) +
+        "px"
+      );
+    })
+    .attr("d", opts.path)
+    .style("display", null); // visible
+
+  hideCounterAxisInsideTickLabels(ax, [TICK_PATH]);
+
+  ticks.attr("transform", opts.transFn);
 };
 
 /**
@@ -3317,80 +3596,90 @@ axes.drawTicks = function(gd, ax, opts) {
  * - {fn} transFn
  * - {boolean} crisp (set to false to unset crisp-edge SVG rendering)
  */
-axes.drawGrid = function(gd, ax, opts) {
-    opts = opts || {};
-
-    if(ax.tickmode === 'sync') {
-        // for tickmode sync we use the overlaying axis grid
-        return;
-    }
-
-    var cls = ax._id + 'grid';
-
-    var hasMinor = ax.minor && ax.minor.showgrid;
-    var minorVals = hasMinor ? opts.vals.filter(function(d) { return d.minor; }) : [];
-    var majorVals = ax.showgrid ? opts.vals.filter(function(d) { return !d.minor; }) : [];
-
-    var counterAx = opts.counterAxis;
-    if(counterAx && axes.shouldShowZeroLine(gd, ax, counterAx)) {
-        var isArrayMode = ax.tickmode === 'array';
-        for(var i = 0; i < majorVals.length; i++) {
-            var xi = majorVals[i].x;
-            if(isArrayMode ? !xi : (Math.abs(xi) < ax.dtick / 100)) {
-                majorVals = majorVals.slice(0, i).concat(majorVals.slice(i + 1));
-                // In array mode you can in principle have multiple
-                // ticks at 0, so test them all. Otherwise once we found
-                // one we can stop.
-                if(isArrayMode) i--;
-                else break;
-            }
-        }
-    }
+axes.drawGrid = function (gd, ax, opts) {
+  opts = opts || {};
+
+  if (ax.tickmode === "sync") {
+    // for tickmode sync we use the overlaying axis grid
+    return;
+  }
+
+  var cls = ax._id + "grid";
+
+  var hasMinor = ax.minor && ax.minor.showgrid;
+  var minorVals = hasMinor
+    ? opts.vals.filter(function (d) {
+        return d.minor;
+      })
+    : [];
+  var majorVals = ax.showgrid
+    ? opts.vals.filter(function (d) {
+        return !d.minor;
+      })
+    : [];
+
+  var counterAx = opts.counterAxis;
+  if (counterAx && axes.shouldShowZeroLine(gd, ax, counterAx)) {
+    var isArrayMode = ax.tickmode === "array";
+    for (var i = 0; i < majorVals.length; i++) {
+      var xi = majorVals[i].x;
+      if (isArrayMode ? !xi : Math.abs(xi) < ax.dtick / 100) {
+        majorVals = majorVals.slice(0, i).concat(majorVals.slice(i + 1));
+        // In array mode you can in principle have multiple
+        // ticks at 0, so test them all. Otherwise once we found
+        // one we can stop.
+        if (isArrayMode) i--;
+        else break;
+      }
+    }
+  }
+
+  ax._gw = Drawing.crispRound(gd, ax.gridwidth, 1);
+
+  var wMinor = !hasMinor ? 0 : Drawing.crispRound(gd, ax.minor.gridwidth, 1);
+
+  var majorLayer = opts.layer;
+  var minorLayer = opts.minorLayer;
+  for (var major = 1; major >= 0; major--) {
+    var layer = major ? majorLayer : minorLayer;
+    if (!layer) continue;
+
+    var grid = layer
+      .selectAll("path." + cls)
+      .data(major ? majorVals : minorVals, tickDataFn);
+
+    grid.exit().remove();
+
+    grid
+      .enter()
+      .append("path")
+      .classed(cls, 1)
+      .classed("crisp", opts.crisp !== false);
+
+    grid
+      .attr("transform", opts.transFn)
+      .attr("d", opts.path)
+      .each(function (d) {
+        return Color.stroke(
+          d3.select(this),
+          d.minor ? ax.minor.gridcolor : ax.gridcolor || "#ddd"
+        );
+      })
+      .style("stroke-dasharray", function (d) {
+        return Drawing.dashStyle(
+          d.minor ? ax.minor.griddash : ax.griddash,
+          d.minor ? ax.minor.gridwidth : ax.gridwidth
+        );
+      })
+      .style("stroke-width", function (d) {
+        return (d.minor ? wMinor : ax._gw) + "px";
+      })
+      .style("display", null); // visible
 
-    ax._gw =
-        Drawing.crispRound(gd, ax.gridwidth, 1);
-
-    var wMinor = !hasMinor ? 0 :
-        Drawing.crispRound(gd, ax.minor.gridwidth, 1);
-
-    var majorLayer = opts.layer;
-    var minorLayer = opts.minorLayer;
-    for(var major = 1; major >= 0; major--) {
-        var layer = major ? majorLayer : minorLayer;
-        if(!layer) continue;
-
-        var grid = layer.selectAll('path.' + cls)
-            .data(major ? majorVals : minorVals, tickDataFn);
-
-        grid.exit().remove();
-
-        grid.enter().append('path')
-            .classed(cls, 1)
-            .classed('crisp', opts.crisp !== false);
-
-        grid.attr('transform', opts.transFn)
-            .attr('d', opts.path)
-            .each(function(d) {
-                return Color.stroke(d3.select(this), d.minor ?
-                    ax.minor.gridcolor :
-                    (ax.gridcolor || '#ddd')
-                );
-            })
-            .style('stroke-dasharray', function(d) {
-                return Drawing.dashStyle(
-                    d.minor ? ax.minor.griddash : ax.griddash,
-                    d.minor ? ax.minor.gridwidth : ax.gridwidth
-                );
-            })
-            .style('stroke-width', function(d) {
-                return (d.minor ? wMinor : ax._gw) + 'px';
-            })
-            .style('display', null); // visible
-
-        if(typeof opts.path === 'function') grid.attr('d', opts.path);
-    }
+    if (typeof opts.path === "function") grid.attr("d", opts.path);
+  }
 
-    hideCounterAxisInsideTickLabels(ax, [GRID_PATH, MINORGRID_PATH]);
+  hideCounterAxisInsideTickLabels(ax, [GRID_PATH, MINORGRID_PATH]);
 };
 
 /**
@@ -3410,37 +3699,42 @@ axes.drawGrid = function(gd, ax, opts) {
  * - {fn} transFn
  * - {boolean} crisp (set to false to unset crisp-edge SVG rendering)
  */
-axes.drawZeroLine = function(gd, ax, opts) {
-    opts = opts || opts;
-
-    var cls = ax._id + 'zl';
-    var show = axes.shouldShowZeroLine(gd, ax, opts.counterAxis);
-
-    var zl = opts.layer.selectAll('path.' + cls)
-        .data(show ? [{x: 0, id: ax._id}] : []);
-
-    zl.exit().remove();
-
-    zl.enter().append('path')
-        .classed(cls, 1)
-        .classed('zl', 1)
-        .classed('crisp', opts.crisp !== false)
-        .each(function() {
-            // use the fact that only one element can enter to trigger a sort.
-            // If several zerolines enter at the same time we will sort once per,
-            // but generally this should be a minimal overhead.
-            opts.layer.selectAll('path').sort(function(da, db) {
-                return idSort(da.id, db.id);
-            });
-        });
+axes.drawZeroLine = function (gd, ax, opts) {
+  opts = opts || opts;
+
+  var cls = ax._id + "zl";
+  var show = axes.shouldShowZeroLine(gd, ax, opts.counterAxis);
+
+  var zl = opts.layer
+    .selectAll("path." + cls)
+    .data(show ? [{ x: 0, id: ax._id }] : []);
+
+  zl.exit().remove();
+
+  zl.enter()
+    .append("path")
+    .classed(cls, 1)
+    .classed("zl", 1)
+    .classed("crisp", opts.crisp !== false)
+    .each(function () {
+      // use the fact that only one element can enter to trigger a sort.
+      // If several zerolines enter at the same time we will sort once per,
+      // but generally this should be a minimal overhead.
+      opts.layer.selectAll("path").sort(function (da, db) {
+        return idSort(da.id, db.id);
+      });
+    });
 
-    zl.attr('transform', opts.transFn)
-        .attr('d', opts.path)
-        .call(Color.stroke, ax.zerolinecolor || Color.defaultLine)
-        .style('stroke-width', Drawing.crispRound(gd, ax.zerolinewidth, ax._gw || 1) + 'px')
-        .style('display', null); // visible
+  zl.attr("transform", opts.transFn)
+    .attr("d", opts.path)
+    .call(Color.stroke, ax.zerolinecolor || Color.defaultLine)
+    .style(
+      "stroke-width",
+      Drawing.crispRound(gd, ax.zerolinewidth, ax._gw || 1) + "px"
+    )
+    .style("display", null); // visible
 
-    hideCounterAxisInsideTickLabels(ax, [ZERO_PATH]);
+  hideCounterAxisInsideTickLabels(ax, [ZERO_PATH]);
 };
 
 /**
@@ -3467,467 +3761,499 @@ axes.drawZeroLine = function(gd, ax, opts) {
  *  + {fn} anchorFn
  *  + {fn} heightFn
  */
-axes.drawLabels = function(gd, ax, opts) {
-    opts = opts || {};
-
-    var fullLayout = gd._fullLayout;
-    var axId = ax._id;
-    var axLetter = axId.charAt(0);
-    var cls = opts.cls || axId + 'tick';
-
-    var vals = opts.vals.filter(function(d) { return d.text; });
-
-    var labelFns = opts.labelFns;
-    var tickAngle = opts.secondary ? 0 : ax.tickangle;
-    var prevAngle = (ax._prevTickAngles || {})[cls];
-
-    var tickLabels = opts.layer.selectAll('g.' + cls)
-        .data(ax.showticklabels ? vals : [], tickDataFn);
-
-    var labelsReady = [];
-
-    tickLabels.enter().append('g')
-        .classed(cls, 1)
-        .append('text')
-            // only so tex has predictable alignment that we can
-            // alter later
-            .attr('text-anchor', 'middle')
-            .each(function(d) {
-                var thisLabel = d3.select(this);
-                var newPromise = gd._promises.length;
-
-                thisLabel
-                    .call(svgTextUtils.positionText, labelFns.xFn(d), labelFns.yFn(d))
-                    .call(Drawing.font, d.font, d.fontSize, d.fontColor)
-                    .text(d.text)
-                    .call(svgTextUtils.convertToTspans, gd);
-
-                if(gd._promises[newPromise]) {
-                    // if we have an async label, we'll deal with that
-                    // all here so take it out of gd._promises and
-                    // instead position the label and promise this in
-                    // labelsReady
-                    labelsReady.push(gd._promises.pop().then(function() {
-                        positionLabels(thisLabel, tickAngle);
-                    }));
-                } else {
-                    // sync label: just position it now.
-                    positionLabels(thisLabel, tickAngle);
-                }
-            });
-
-    hideCounterAxisInsideTickLabels(ax, [TICK_TEXT]);
+axes.drawLabels = function (gd, ax, opts) {
+  opts = opts || {};
+
+  var fullLayout = gd._fullLayout;
+  var axId = ax._id;
+  var axLetter = axId.charAt(0);
+  var cls = opts.cls || axId + "tick";
+
+  var vals = opts.vals.filter(function (d) {
+    return d.text;
+  });
+
+  var labelFns = opts.labelFns;
+  var tickAngle = opts.secondary ? 0 : ax.tickangle;
+  var prevAngle = (ax._prevTickAngles || {})[cls];
+
+  var tickLabels = opts.layer
+    .selectAll("g." + cls)
+    .data(ax.showticklabels ? vals : [], tickDataFn);
+
+  var labelsReady = [];
+
+  tickLabels
+    .enter()
+    .append("g")
+    .classed(cls, 1)
+    .append("text")
+    // only so tex has predictable alignment that we can
+    // alter later
+    .attr("text-anchor", "middle")
+    .each(function (d) {
+      var thisLabel = d3.select(this);
+      var newPromise = gd._promises.length;
+
+      thisLabel
+        .call(svgTextUtils.positionText, labelFns.xFn(d), labelFns.yFn(d))
+        .call(Drawing.font, d.font, d.fontSize, d.fontColor)
+        .text(d.text)
+        .call(svgTextUtils.convertToTspans, gd);
+
+      if (gd._promises[newPromise]) {
+        // if we have an async label, we'll deal with that
+        // all here so take it out of gd._promises and
+        // instead position the label and promise this in
+        // labelsReady
+        labelsReady.push(
+          gd._promises.pop().then(function () {
+            positionLabels(thisLabel, tickAngle);
+          })
+        );
+      } else {
+        // sync label: just position it now.
+        positionLabels(thisLabel, tickAngle);
+      }
+    });
 
-    tickLabels.exit().remove();
+  hideCounterAxisInsideTickLabels(ax, [TICK_TEXT]);
 
-    if(opts.repositionOnUpdate) {
-        tickLabels.each(function(d) {
-            d3.select(this).select('text')
-                .call(svgTextUtils.positionText, labelFns.xFn(d), labelFns.yFn(d));
-        });
-    }
+  tickLabels.exit().remove();
 
-    function positionLabels(s, angle) {
-        s.each(function(d) {
-            var thisLabel = d3.select(this);
-            var mathjaxGroup = thisLabel.select('.text-math-group');
-            var anchor = labelFns.anchorFn(d, angle);
-
-            var transform = opts.transFn.call(thisLabel.node(), d) +
-                ((isNumeric(angle) && +angle !== 0) ?
-                (' rotate(' + angle + ',' + labelFns.xFn(d) + ',' +
-                    (labelFns.yFn(d) - d.fontSize / 2) + ')') :
-                '');
-
-            // how much to shift a multi-line label to center it vertically.
-            var nLines = svgTextUtils.lineCount(thisLabel);
-            var lineHeight = LINE_SPACING * d.fontSize;
-            var anchorHeight = labelFns.heightFn(d, isNumeric(angle) ? +angle : 0, (nLines - 1) * lineHeight);
-
-            if(anchorHeight) {
-                transform += strTranslate(0, anchorHeight);
-            }
-
-            if(mathjaxGroup.empty()) {
-                var thisText = thisLabel.select('text');
-                thisText.attr({
-                    transform: transform,
-                    'text-anchor': anchor
-                });
-
-                thisText.style('opacity', 1); // visible
-
-                if(ax._adjustTickLabelsOverflow) {
-                    ax._adjustTickLabelsOverflow();
-                }
-            } else {
-                var mjWidth = Drawing.bBox(mathjaxGroup.node()).width;
-                var mjShift = mjWidth * {end: -0.5, start: 0.5}[anchor];
-                mathjaxGroup.attr('transform', transform + strTranslate(mjShift, 0));
-            }
+  if (opts.repositionOnUpdate) {
+    tickLabels.each(function (d) {
+      d3.select(this)
+        .select("text")
+        .call(svgTextUtils.positionText, labelFns.xFn(d), labelFns.yFn(d));
+    });
+  }
+
+  function positionLabels(s, angle) {
+    s.each(function (d) {
+      var thisLabel = d3.select(this);
+      var mathjaxGroup = thisLabel.select(".text-math-group");
+      var anchor = labelFns.anchorFn(d, angle);
+
+      var transform =
+        opts.transFn.call(thisLabel.node(), d) +
+        (isNumeric(angle) && +angle !== 0
+          ? " rotate(" +
+            angle +
+            "," +
+            labelFns.xFn(d) +
+            "," +
+            (labelFns.yFn(d) - d.fontSize / 2) +
+            ")"
+          : "");
+
+      // how much to shift a multi-line label to center it vertically.
+      var nLines = svgTextUtils.lineCount(thisLabel);
+      var lineHeight = LINE_SPACING * d.fontSize;
+      var anchorHeight = labelFns.heightFn(
+        d,
+        isNumeric(angle) ? +angle : 0,
+        (nLines - 1) * lineHeight
+      );
+
+      if (anchorHeight) {
+        transform += strTranslate(0, anchorHeight);
+      }
+
+      if (mathjaxGroup.empty()) {
+        var thisText = thisLabel.select("text");
+        thisText.attr({
+          transform: transform,
+          "text-anchor": anchor,
         });
-    }
-
-    ax._adjustTickLabelsOverflow = function() {
-        var ticklabeloverflow = ax.ticklabeloverflow;
-        if(!ticklabeloverflow || ticklabeloverflow === 'allow') return;
 
-        var hideOverflow = ticklabeloverflow.indexOf('hide') !== -1;
+        thisText.style("opacity", 1); // visible
 
-        var isX = ax._id.charAt(0) === 'x';
-        // div positions
-        var p0 = 0;
-        var p1 = isX ?
-            gd._fullLayout.width :
-            gd._fullLayout.height;
-
-        if(ticklabeloverflow.indexOf('domain') !== -1) {
-            // domain positions
-            var rl = Lib.simpleMap(ax.range, ax.r2l);
-            p0 = ax.l2p(rl[0]) + ax._offset;
-            p1 = ax.l2p(rl[1]) + ax._offset;
+        if (ax._adjustTickLabelsOverflow) {
+          ax._adjustTickLabelsOverflow();
         }
+      } else {
+        var mjWidth = Drawing.bBox(mathjaxGroup.node()).width;
+        var mjShift = mjWidth * { end: -0.5, start: 0.5 }[anchor];
+        mathjaxGroup.attr("transform", transform + strTranslate(mjShift, 0));
+      }
+    });
+  }
 
-        var min = Math.min(p0, p1);
-        var max = Math.max(p0, p1);
-
-        var side = ax.side;
-
-        var visibleLabelMin = Infinity;
-        var visibleLabelMax = -Infinity;
-
-        tickLabels.each(function(d) {
-            var thisLabel = d3.select(this);
-            var mathjaxGroup = thisLabel.select('.text-math-group');
-
-            if(mathjaxGroup.empty()) {
-                var bb = Drawing.bBox(thisLabel.node());
-                var adjust = 0;
-                if(isX) {
-                    if(bb.right > max) adjust = 1;
-                    else if(bb.left < min) adjust = 1;
-                } else {
-                    if(bb.bottom > max) adjust = 1;
-                    else if(bb.top + (ax.tickangle ? 0 : d.fontSize / 4) < min) adjust = 1;
-                }
-
-                var t = thisLabel.select('text');
-                if(adjust) {
-                    if(hideOverflow) t.style('opacity', 0); // hidden
-                } else {
-                    t.style('opacity', 1); // visible
-
-                    if(side === 'bottom' || side === 'right') {
-                        visibleLabelMin = Math.min(visibleLabelMin, isX ? bb.top : bb.left);
-                    } else {
-                        visibleLabelMin = -Infinity;
-                    }
-
-                    if(side === 'top' || side === 'left') {
-                        visibleLabelMax = Math.max(visibleLabelMax, isX ? bb.bottom : bb.right);
-                    } else {
-                        visibleLabelMax = Infinity;
-                    }
-                }
-            } // TODO: hide mathjax?
-        });
+  ax._adjustTickLabelsOverflow = function () {
+    var ticklabeloverflow = ax.ticklabeloverflow;
+    if (!ticklabeloverflow || ticklabeloverflow === "allow") return;
 
-        for(var subplot in fullLayout._plots) {
-            var plotinfo = fullLayout._plots[subplot];
-            if(ax._id !== plotinfo.xaxis._id && ax._id !== plotinfo.yaxis._id) continue;
-            var anchorAx = isX ? plotinfo.yaxis : plotinfo.xaxis;
-            if(anchorAx) {
-                anchorAx['_visibleLabelMin_' + ax._id] = visibleLabelMin;
-                anchorAx['_visibleLabelMax_' + ax._id] = visibleLabelMax;
-            }
-        }
-    };
+    var hideOverflow = ticklabeloverflow.indexOf("hide") !== -1;
 
-    ax._hideCounterAxisInsideTickLabels = function(partialOpts) {
-        var isX = ax._id.charAt(0) === 'x';
+    var isX = ax._id.charAt(0) === "x";
+    // div positions
+    var p0 = 0;
+    var p1 = isX ? gd._fullLayout.width : gd._fullLayout.height;
 
-        var anchoredAxes = [];
-        for(var subplot in fullLayout._plots) {
-            var plotinfo = fullLayout._plots[subplot];
-            if(ax._id !== plotinfo.xaxis._id && ax._id !== plotinfo.yaxis._id) continue;
-            anchoredAxes.push(isX ? plotinfo.yaxis : plotinfo.xaxis);
-        }
+    if (ticklabeloverflow.indexOf("domain") !== -1) {
+      // domain positions
+      var rl = Lib.simpleMap(ax.range, ax.r2l);
+      p0 = ax.l2p(rl[0]) + ax._offset;
+      p1 = ax.l2p(rl[1]) + ax._offset;
+    }
 
-        anchoredAxes.forEach(function(anchorAx, idx) {
-            if(anchorAx && insideTicklabelposition(anchorAx)) {
-                (partialOpts || [
-                    ZERO_PATH,
-                    MINORGRID_PATH,
-                    GRID_PATH,
-                    TICK_PATH,
-                    TICK_TEXT
-                ]).forEach(function(e) {
-                    var isPeriodLabel =
-                        e.K === 'tick' &&
-                        e.L === 'text' &&
-                        ax.ticklabelmode === 'period';
-
-                    var mainPlotinfo = fullLayout._plots[ax._mainSubplot];
-
-                    var sel;
-                    if(e.K === ZERO_PATH.K) sel = mainPlotinfo.zerolinelayer.selectAll('.' + ax._id + 'zl');
-                    else if(e.K === MINORGRID_PATH.K) sel = mainPlotinfo.minorGridlayer.selectAll('.' + ax._id);
-                    else if(e.K === GRID_PATH.K) sel = mainPlotinfo.gridlayer.selectAll('.' + ax._id);
-                    else sel = mainPlotinfo[ax._id.charAt(0) + 'axislayer'];
-
-                    sel.each(function() {
-                        var w = d3.select(this);
-                        if(e.L) w = w.selectAll(e.L);
-
-                        w.each(function(d) {
-                            var q = ax.l2p(
-                                isPeriodLabel ? getPosX(d) : d.x
-                            ) + ax._offset;
-
-                            var t = d3.select(this);
-                            if(
-                                q < ax['_visibleLabelMax_' + anchorAx._id] &&
-                                q > ax['_visibleLabelMin_' + anchorAx._id]
-                            ) {
-                                t.style('display', 'none'); // hidden
-                            } else if(e.K === 'tick' && !idx) {
-                                t.style('display', null); // visible
-                            }
-                        });
-                    });
-                });
-            }
-        });
-    };
+    var min = Math.min(p0, p1);
+    var max = Math.max(p0, p1);
 
-    // make sure all labels are correctly positioned at their base angle
-    // the positionLabels call above is only for newly drawn labels.
-    // do this without waiting, using the last calculated angle to
-    // minimize flicker, then do it again when we know all labels are
-    // there, putting back the prescribed angle to check for overlaps.
-    positionLabels(tickLabels, (prevAngle + 1) ? prevAngle : tickAngle);
+    var side = ax.side;
 
-    function allLabelsReady() {
-        return labelsReady.length && Promise.all(labelsReady);
-    }
+    var visibleLabelMin = Infinity;
+    var visibleLabelMax = -Infinity;
 
-    var autoangle = null;
-
-    function fixLabelOverlaps() {
-        positionLabels(tickLabels, tickAngle);
-
-        // check for auto-angling if x labels overlap
-        // don't auto-angle at all for log axes with
-        // base and digit format
-        if(vals.length && axLetter === 'x' && !isNumeric(tickAngle) &&
-            (ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')
-        ) {
-            autoangle = 0;
-
-            var maxFontSize = 0;
-            var lbbArray = [];
-            var i;
-
-            tickLabels.each(function(d) {
-                maxFontSize = Math.max(maxFontSize, d.fontSize);
-
-                var x = ax.l2p(d.x);
-                var thisLabel = selectTickLabel(this);
-                var bb = Drawing.bBox(thisLabel.node());
-
-                lbbArray.push({
-                    // ignore about y, just deal with x overlaps
-                    top: 0,
-                    bottom: 10,
-                    height: 10,
-                    left: x - bb.width / 2,
-                    // impose a 2px gap
-                    right: x + bb.width / 2 + 2,
-                    width: bb.width + 2
-                });
-            });
+    tickLabels.each(function (d) {
+      var thisLabel = d3.select(this);
+      var mathjaxGroup = thisLabel.select(".text-math-group");
 
-            if((ax.tickson === 'boundaries' || ax.showdividers) && !opts.secondary) {
-                var gap = 2;
-                if(ax.ticks) gap += ax.tickwidth / 2;
-
-                // TODO should secondary labels also fall into this fix-overlap regime?
-
-                for(i = 0; i < lbbArray.length; i++) {
-                    var xbnd = vals[i].xbnd;
-                    var lbb = lbbArray[i];
-                    if(
-                        (xbnd[0] !== null && (lbb.left - ax.l2p(xbnd[0])) < gap) ||
-                        (xbnd[1] !== null && (ax.l2p(xbnd[1]) - lbb.right) < gap)
-                    ) {
-                        autoangle = 90;
-                        break;
-                    }
-                }
-            } else {
-                var vLen = vals.length;
-                var tickSpacing = Math.abs((vals[vLen - 1].x - vals[0].x) * ax._m) / (vLen - 1);
-
-                var ticklabelposition = ax.ticklabelposition || '';
-                var has = function(str) {
-                    return ticklabelposition.indexOf(str) !== -1;
-                };
-                var isTop = has('top');
-                var isLeft = has('left');
-                var isRight = has('right');
-                var isBottom = has('bottom');
-                var isAligned = isBottom || isLeft || isTop || isRight;
-                var pad = !isAligned ? 0 :
-                    (ax.tickwidth || 0) + 2 * TEXTPAD;
-
-                var rotate90 = (tickSpacing < maxFontSize * 2.5) || ax.type === 'multicategory' || ax._name === 'realaxis';
-
-                // any overlap at all - set 30 degrees or 90 degrees
-                for(i = 0; i < lbbArray.length - 1; i++) {
-                    if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1], pad)) {
-                        autoangle = rotate90 ? 90 : 30;
-                        break;
-                    }
-                }
-            }
-
-            if(autoangle) {
-                positionLabels(tickLabels, autoangle);
-            }
+      if (mathjaxGroup.empty()) {
+        var bb = Drawing.bBox(thisLabel.node());
+        var adjust = 0;
+        if (isX) {
+          if (bb.right > max) adjust = 1;
+          else if (bb.left < min) adjust = 1;
+        } else {
+          if (bb.bottom > max) adjust = 1;
+          else if (bb.top + (ax.tickangle ? 0 : d.fontSize / 4) < min)
+            adjust = 1;
         }
-    }
-
-    if(ax._selections) {
-        ax._selections[cls] = tickLabels;
-    }
 
-    var seq = [allLabelsReady];
-
-    // N.B. during auto-margin redraws, if the axis fixed its label overlaps
-    // by rotating 90 degrees, do not attempt to re-fix its label overlaps
-    // as this can lead to infinite redraw loops!
-    if(ax.automargin && fullLayout._redrawFromAutoMarginCount && prevAngle === 90) {
-        autoangle = 90;
-        seq.push(function() {
-            positionLabels(tickLabels, prevAngle);
-        });
-    } else {
-        seq.push(fixLabelOverlaps);
-    }
+        var t = thisLabel.select("text");
+        if (adjust) {
+          if (hideOverflow) t.style("opacity", 0); // hidden
+        } else {
+          t.style("opacity", 1); // visible
+
+          if (side === "bottom" || side === "right") {
+            visibleLabelMin = Math.min(visibleLabelMin, isX ? bb.top : bb.left);
+          } else {
+            visibleLabelMin = -Infinity;
+          }
+
+          if (side === "top" || side === "left") {
+            visibleLabelMax = Math.max(
+              visibleLabelMax,
+              isX ? bb.bottom : bb.right
+            );
+          } else {
+            visibleLabelMax = Infinity;
+          }
+        }
+      } // TODO: hide mathjax?
+    });
 
-    // save current tick angle for future redraws
-    if(ax._tickAngles) {
-        seq.push(function() {
-            ax._tickAngles[cls] = autoangle === null ?
-                (isNumeric(tickAngle) ? tickAngle : 0) :
-                autoangle;
-        });
+    for (var subplot in fullLayout._plots) {
+      var plotinfo = fullLayout._plots[subplot];
+      if (ax._id !== plotinfo.xaxis._id && ax._id !== plotinfo.yaxis._id)
+        continue;
+      var anchorAx = isX ? plotinfo.yaxis : plotinfo.xaxis;
+      if (anchorAx) {
+        anchorAx["_visibleLabelMin_" + ax._id] = visibleLabelMin;
+        anchorAx["_visibleLabelMax_" + ax._id] = visibleLabelMax;
+      }
     }
+  };
 
-    var computeTickLabelBoundingBoxes = function() {
-        var labelsMaxW = 0;
-        var labelsMaxH = 0;
-        tickLabels.each(function(d, i) {
-            var thisLabel = selectTickLabel(this);
-            var mathjaxGroup = thisLabel.select('.text-math-group');
-
-            if(mathjaxGroup.empty()) {
-                var bb;
+  ax._hideCounterAxisInsideTickLabels = function (partialOpts) {
+    var isX = ax._id.charAt(0) === "x";
 
-                if(ax._vals[i]) {
-                    bb = ax._vals[i].bb || Drawing.bBox(thisLabel.node());
-                    ax._vals[i].bb = bb;
-                }
+    var anchoredAxes = [];
+    for (var subplot in fullLayout._plots) {
+      var plotinfo = fullLayout._plots[subplot];
+      if (ax._id !== plotinfo.xaxis._id && ax._id !== plotinfo.yaxis._id)
+        continue;
+      anchoredAxes.push(isX ? plotinfo.yaxis : plotinfo.xaxis);
+    }
 
-                labelsMaxW = Math.max(labelsMaxW, bb.width);
-                labelsMaxH = Math.max(labelsMaxH, bb.height);
-            }
+    anchoredAxes.forEach(function (anchorAx, idx) {
+      if (anchorAx && insideTicklabelposition(anchorAx)) {
+        (
+          partialOpts || [
+            ZERO_PATH,
+            MINORGRID_PATH,
+            GRID_PATH,
+            TICK_PATH,
+            TICK_TEXT,
+          ]
+        ).forEach(function (e) {
+          var isPeriodLabel =
+            e.K === "tick" && e.L === "text" && ax.ticklabelmode === "period";
+
+          var mainPlotinfo = fullLayout._plots[ax._mainSubplot];
+
+          var sel;
+          if (e.K === ZERO_PATH.K)
+            sel = mainPlotinfo.zerolinelayer.selectAll("." + ax._id + "zl");
+          else if (e.K === MINORGRID_PATH.K)
+            sel = mainPlotinfo.minorGridlayer.selectAll("." + ax._id);
+          else if (e.K === GRID_PATH.K)
+            sel = mainPlotinfo.gridlayer.selectAll("." + ax._id);
+          else sel = mainPlotinfo[ax._id.charAt(0) + "axislayer"];
+
+          sel.each(function () {
+            var w = d3.select(this);
+            if (e.L) w = w.selectAll(e.L);
+
+            w.each(function (d) {
+              var q = ax.l2p(isPeriodLabel ? getPosX(d) : d.x) + ax._offset;
+
+              var t = d3.select(this);
+              if (
+                q < ax["_visibleLabelMax_" + anchorAx._id] &&
+                q > ax["_visibleLabelMin_" + anchorAx._id]
+              ) {
+                t.style("display", "none"); // hidden
+              } else if (e.K === "tick" && !idx) {
+                t.style("display", null); // visible
+              }
+            });
+          });
         });
-
-        return {
-            labelsMaxW: labelsMaxW,
-            labelsMaxH: labelsMaxH
-        };
-    };
-
-    var anchorAx = ax._anchorAxis;
-    if(
-        anchorAx && (anchorAx.autorange || anchorAx.insiderange) &&
-        insideTicklabelposition(ax) &&
-        !isLinked(fullLayout, ax._id)
+      }
+    });
+  };
+
+  // make sure all labels are correctly positioned at their base angle
+  // the positionLabels call above is only for newly drawn labels.
+  // do this without waiting, using the last calculated angle to
+  // minimize flicker, then do it again when we know all labels are
+  // there, putting back the prescribed angle to check for overlaps.
+  positionLabels(tickLabels, prevAngle + 1 ? prevAngle : tickAngle);
+
+  function allLabelsReady() {
+    return labelsReady.length && Promise.all(labelsReady);
+  }
+
+  var autoangle = null;
+
+  function fixLabelOverlaps() {
+    positionLabels(tickLabels, tickAngle);
+
+    // check for auto-angling if x labels overlap
+    // don't auto-angle at all for log axes with
+    // base and digit format
+    if (
+      vals.length &&
+      axLetter === "x" &&
+      !isNumeric(tickAngle) &&
+      (ax.type !== "log" || String(ax.dtick).charAt(0) !== "D")
     ) {
-        if(!fullLayout._insideTickLabelsUpdaterange) {
-            fullLayout._insideTickLabelsUpdaterange = {};
+      autoangle = 0;
+
+      var maxFontSize = 0;
+      var lbbArray = [];
+      var i;
+
+      tickLabels.each(function (d) {
+        maxFontSize = Math.max(maxFontSize, d.fontSize);
+
+        var x = ax.l2p(d.x);
+        var thisLabel = selectTickLabel(this);
+        var bb = Drawing.bBox(thisLabel.node());
+
+        lbbArray.push({
+          // ignore about y, just deal with x overlaps
+          top: 0,
+          bottom: 10,
+          height: 10,
+          left: x - bb.width / 2,
+          // impose a 2px gap
+          right: x + bb.width / 2 + 2,
+          width: bb.width + 2,
+        });
+      });
+
+      if ((ax.tickson === "boundaries" || ax.showdividers) && !opts.secondary) {
+        var gap = 2;
+        if (ax.ticks) gap += ax.tickwidth / 2;
+
+        // TODO should secondary labels also fall into this fix-overlap regime?
+
+        for (i = 0; i < lbbArray.length; i++) {
+          var xbnd = vals[i].xbnd;
+          var lbb = lbbArray[i];
+          if (
+            (xbnd[0] !== null && lbb.left - ax.l2p(xbnd[0]) < gap) ||
+            (xbnd[1] !== null && ax.l2p(xbnd[1]) - lbb.right < gap)
+          ) {
+            autoangle = 90;
+            break;
+          }
         }
-
-        if(anchorAx.autorange) {
-            fullLayout._insideTickLabelsUpdaterange[anchorAx._name + '.autorange'] = anchorAx.autorange;
-
-            seq.push(computeTickLabelBoundingBoxes);
+      } else {
+        var vLen = vals.length;
+        var tickSpacing =
+          Math.abs((vals[vLen - 1].x - vals[0].x) * ax._m) / (vLen - 1);
+
+        var ticklabelposition = ax.ticklabelposition || "";
+        var has = function (str) {
+          return ticklabelposition.indexOf(str) !== -1;
+        };
+        var isTop = has("top");
+        var isLeft = has("left");
+        var isRight = has("right");
+        var isBottom = has("bottom");
+        var isAligned = isBottom || isLeft || isTop || isRight;
+        var pad = !isAligned ? 0 : (ax.tickwidth || 0) + 2 * TEXTPAD;
+
+        var rotate90 =
+          tickSpacing < maxFontSize * 2.5 ||
+          ax.type === "multicategory" ||
+          ax._name === "realaxis";
+
+        // any overlap at all - set 30 degrees or 90 degrees
+        for (i = 0; i < lbbArray.length - 1; i++) {
+          if (Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1], pad)) {
+            autoangle = rotate90 ? 90 : 30;
+            break;
+          }
         }
+      }
+
+      if (autoangle) {
+        positionLabels(tickLabels, autoangle);
+      }
+    }
+  }
+
+  if (ax._selections) {
+    ax._selections[cls] = tickLabels;
+  }
+
+  var seq = [allLabelsReady];
+
+  // N.B. during auto-margin redraws, if the axis fixed its label overlaps
+  // by rotating 90 degrees, do not attempt to re-fix its label overlaps
+  // as this can lead to infinite redraw loops!
+  if (
+    ax.automargin &&
+    fullLayout._redrawFromAutoMarginCount &&
+    prevAngle === 90
+  ) {
+    autoangle = 90;
+    seq.push(function () {
+      positionLabels(tickLabels, prevAngle);
+    });
+  } else {
+    seq.push(fixLabelOverlaps);
+  }
+
+  // save current tick angle for future redraws
+  if (ax._tickAngles) {
+    seq.push(function () {
+      ax._tickAngles[cls] =
+        autoangle === null ? (isNumeric(tickAngle) ? tickAngle : 0) : autoangle;
+    });
+  }
 
-        if(anchorAx.insiderange) {
-            var BBs = computeTickLabelBoundingBoxes();
-            var move = ax._id.charAt(0) === 'y' ?
-                BBs.labelsMaxW :
-                BBs.labelsMaxH;
-
-            move += 2 * TEXTPAD;
-
-            if(ax.ticklabelposition === 'inside') {
-                move += ax.ticklen || 0;
-            }
-
-            var sgn = (ax.side === 'right' || ax.side === 'top') ? 1 : -1;
-            var index = sgn === 1 ? 1 : 0;
-            var otherIndex = sgn === 1 ? 0 : 1;
-
-            var newRange = [];
-            newRange[otherIndex] = anchorAx.range[otherIndex];
-
-            var p0 = anchorAx.d2p(anchorAx.range[index]);
-            var p1 = anchorAx.d2p(anchorAx.range[otherIndex]);
-            var dist = Math.abs(p1 - p0);
-            if(dist - move > 0) {
-                dist -= move;
-                move *= 1 + move / dist;
-            } else {
-                move = 0;
-            }
-
-            if(ax._id.charAt(0) !== 'y') move = -move;
+  var computeTickLabelBoundingBoxes = function () {
+    var labelsMaxW = 0;
+    var labelsMaxH = 0;
+    tickLabels.each(function (d, i) {
+      var thisLabel = selectTickLabel(this);
+      var mathjaxGroup = thisLabel.select(".text-math-group");
 
-            newRange[index] = anchorAx.p2d(
-                anchorAx.d2p(anchorAx.range[index]) +
-                sgn * move
-            );
+      if (mathjaxGroup.empty()) {
+        var bb;
 
-            // handle partial ranges in insiderange
-            if(
-                anchorAx.autorange === 'min' ||
-                anchorAx.autorange === 'max reversed'
-            ) {
-                newRange[0] = null;
-
-                anchorAx._rangeInitial0 = undefined;
-                anchorAx._rangeInitial1 = undefined;
-            } else if(
-                anchorAx.autorange === 'max' ||
-                anchorAx.autorange === 'min reversed'
-            ) {
-                newRange[1] = null;
-
-                anchorAx._rangeInitial0 = undefined;
-                anchorAx._rangeInitial1 = undefined;
-            }
-
-            fullLayout._insideTickLabelsUpdaterange[anchorAx._name + '.range'] = newRange;
+        if (ax._vals[i]) {
+          bb = ax._vals[i].bb || Drawing.bBox(thisLabel.node());
+          ax._vals[i].bb = bb;
         }
-    }
 
-    var done = Lib.syncOrAsync(seq);
-    if(done && done.then) gd._promises.push(done);
-    return done;
+        labelsMaxW = Math.max(labelsMaxW, bb.width);
+        labelsMaxH = Math.max(labelsMaxH, bb.height);
+      }
+    });
+
+    return {
+      labelsMaxW: labelsMaxW,
+      labelsMaxH: labelsMaxH,
+    };
+  };
+
+  var anchorAx = ax._anchorAxis;
+  if (
+    anchorAx &&
+    (anchorAx.autorange || anchorAx.insiderange) &&
+    insideTicklabelposition(ax) &&
+    !isLinked(fullLayout, ax._id)
+  ) {
+    if (!fullLayout._insideTickLabelsUpdaterange) {
+      fullLayout._insideTickLabelsUpdaterange = {};
+    }
+
+    if (anchorAx.autorange) {
+      fullLayout._insideTickLabelsUpdaterange[anchorAx._name + ".autorange"] =
+        anchorAx.autorange;
+
+      seq.push(computeTickLabelBoundingBoxes);
+    }
+
+    if (anchorAx.insiderange) {
+      var BBs = computeTickLabelBoundingBoxes();
+      var move = ax._id.charAt(0) === "y" ? BBs.labelsMaxW : BBs.labelsMaxH;
+
+      move += 2 * TEXTPAD;
+
+      if (ax.ticklabelposition === "inside") {
+        move += ax.ticklen || 0;
+      }
+
+      var sgn = ax.side === "right" || ax.side === "top" ? 1 : -1;
+      var index = sgn === 1 ? 1 : 0;
+      var otherIndex = sgn === 1 ? 0 : 1;
+
+      var newRange = [];
+      newRange[otherIndex] = anchorAx.range[otherIndex];
+
+      var p0 = anchorAx.d2p(anchorAx.range[index]);
+      var p1 = anchorAx.d2p(anchorAx.range[otherIndex]);
+      var dist = Math.abs(p1 - p0);
+      if (dist - move > 0) {
+        dist -= move;
+        move *= 1 + move / dist;
+      } else {
+        move = 0;
+      }
+
+      if (ax._id.charAt(0) !== "y") move = -move;
+
+      newRange[index] = anchorAx.p2d(
+        anchorAx.d2p(anchorAx.range[index]) + sgn * move
+      );
+
+      // handle partial ranges in insiderange
+      if (
+        anchorAx.autorange === "min" ||
+        anchorAx.autorange === "max reversed"
+      ) {
+        newRange[0] = null;
+
+        anchorAx._rangeInitial0 = undefined;
+        anchorAx._rangeInitial1 = undefined;
+      } else if (
+        anchorAx.autorange === "max" ||
+        anchorAx.autorange === "min reversed"
+      ) {
+        newRange[1] = null;
+
+        anchorAx._rangeInitial0 = undefined;
+        anchorAx._rangeInitial1 = undefined;
+      }
+
+      fullLayout._insideTickLabelsUpdaterange[anchorAx._name + ".range"] =
+        newRange;
+    }
+  }
+
+  var done = Lib.syncOrAsync(seq);
+  if (done && done.then) gd._promises.push(done);
+  return done;
 };
 
 /**
@@ -3946,23 +4272,28 @@ axes.drawLabels = function(gd, ax, opts) {
  * - {fn} transFn
  */
 function drawDividers(gd, ax, opts) {
-    var cls = ax._id + 'divider';
-    var vals = opts.vals;
+  var cls = ax._id + "divider";
+  var vals = opts.vals;
 
-    var dividers = opts.layer.selectAll('path.' + cls)
-        .data(vals, tickDataFn);
+  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.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
+    .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);
+  dividers.attr("transform", opts.transFn).attr("d", opts.path);
 }
 
 /**
@@ -3979,31 +4310,31 @@ function drawDividers(gd, ax, opts) {
  *  - {number} position
  * @return {number}
  */
-axes.getPxPosition = function(gd, ax) {
-    var gs = gd._fullLayout._size;
-    var axLetter = ax._id.charAt(0);
-    var side = ax.side;
-    var anchorAxis;
-
-    if(ax.anchor !== 'free') {
-        anchorAxis = ax._anchorAxis;
-    } else if(axLetter === 'x') {
-        anchorAxis = {
-            _offset: gs.t + (1 - (ax.position || 0)) * gs.h,
-            _length: 0
-        };
-    } else if(axLetter === 'y') {
-        anchorAxis = {
-            _offset: gs.l + (ax.position || 0) * gs.w + ax._shift,
-            _length: 0
-        };
-    }
+axes.getPxPosition = function (gd, ax) {
+  var gs = gd._fullLayout._size;
+  var axLetter = ax._id.charAt(0);
+  var side = ax.side;
+  var anchorAxis;
+
+  if (ax.anchor !== "free") {
+    anchorAxis = ax._anchorAxis;
+  } else if (axLetter === "x") {
+    anchorAxis = {
+      _offset: gs.t + (1 - (ax.position || 0)) * gs.h,
+      _length: 0,
+    };
+  } else if (axLetter === "y") {
+    anchorAxis = {
+      _offset: gs.l + (ax.position || 0) * gs.w + ax._shift,
+      _length: 0,
+    };
+  }
 
-    if(side === 'top' || side === 'left') {
-        return anchorAxis._offset;
-    } else if(side === 'bottom' || side === 'right') {
-        return anchorAxis._offset + anchorAxis._length;
-    }
+  if (side === "top" || side === "left") {
+    return anchorAxis._offset;
+  } else if (side === "bottom" || side === "right") {
+    return anchorAxis._offset + anchorAxis._length;
+  }
 };
 
 /**
@@ -4016,17 +4347,15 @@ axes.getPxPosition = function(gd, ax) {
  * @return {number} (in px)
  */
 function approxTitleDepth(ax) {
-    var fontSize = ax.title.font.size;
-    var extraLines = (ax.title.text.match(svgTextUtils.BR_TAG_ALL) || []).length;
-    if(ax.title.hasOwnProperty('standoff')) {
-        return extraLines ?
-            fontSize * (CAP_SHIFT + (extraLines * LINE_SPACING)) :
-            fontSize * CAP_SHIFT;
-    } else {
-        return extraLines ?
-            fontSize * (extraLines + 1) * LINE_SPACING :
-            fontSize;
-    }
+  var fontSize = ax.title.font.size;
+  var extraLines = (ax.title.text.match(svgTextUtils.BR_TAG_ALL) || []).length;
+  if (ax.title.hasOwnProperty("standoff")) {
+    return extraLines
+      ? fontSize * (CAP_SHIFT + extraLines * LINE_SPACING)
+      : fontSize * CAP_SHIFT;
+  } else {
+    return extraLines ? fontSize * (extraLines + 1) * LINE_SPACING : fontSize;
+  }
 }
 
 /**
@@ -4047,188 +4376,198 @@ function approxTitleDepth(ax) {
  *  - {boolean} showticklabels
  */
 function drawTitle(gd, ax) {
-    var fullLayout = gd._fullLayout;
-    var axId = ax._id;
-    var axLetter = axId.charAt(0);
-    var fontSize = ax.title.font.size;
-    var titleStandoff;
-
-    if(ax.title.hasOwnProperty('standoff')) {
-        titleStandoff = ax._depth + ax.title.standoff + approxTitleDepth(ax);
+  var fullLayout = gd._fullLayout;
+  var axId = ax._id;
+  var axLetter = axId.charAt(0);
+  var fontSize = ax.title.font.size;
+  var titleStandoff;
+
+  if (ax.title.hasOwnProperty("standoff")) {
+    titleStandoff = ax._depth + ax.title.standoff + approxTitleDepth(ax);
+  } else {
+    var isInside = insideTicklabelposition(ax);
+
+    if (ax.type === "multicategory") {
+      titleStandoff = ax._depth;
     } else {
-        var isInside = insideTicklabelposition(ax);
-
-        if(ax.type === 'multicategory') {
-            titleStandoff = ax._depth;
-        } else {
-            var offsetBase = 1.5 * fontSize;
-            if(isInside) {
-                offsetBase = 0.5 * fontSize;
-                if(ax.ticks === 'outside') {
-                    offsetBase += ax.ticklen;
-                }
-            }
-            titleStandoff = 10 + offsetBase + (ax.linewidth ? ax.linewidth - 1 : 0);
-        }
-
-        if(!isInside) {
-            if(axLetter === 'x') {
-                titleStandoff += ax.side === 'top' ?
-                    fontSize * (ax.showticklabels ? 1 : 0) :
-                    fontSize * (ax.showticklabels ? 1.5 : 0.5);
-            } else {
-                titleStandoff += ax.side === 'right' ?
-                    fontSize * (ax.showticklabels ? 1 : 0.5) :
-                    fontSize * (ax.showticklabels ? 0.5 : 0);
-            }
+      var offsetBase = 1.5 * fontSize;
+      if (isInside) {
+        offsetBase = 0.5 * fontSize;
+        if (ax.ticks === "outside") {
+          offsetBase += ax.ticklen;
         }
-    }
-
-    var pos = axes.getPxPosition(gd, ax);
-    var transform, x, y;
+      }
+      titleStandoff = 10 + offsetBase + (ax.linewidth ? ax.linewidth - 1 : 0);
+    }
+
+    if (!isInside) {
+      if (axLetter === "x") {
+        titleStandoff +=
+          ax.side === "top"
+            ? fontSize * (ax.showticklabels ? 1 : 0)
+            : fontSize * (ax.showticklabels ? 1.5 : 0.5);
+      } else {
+        titleStandoff +=
+          ax.side === "right"
+            ? fontSize * (ax.showticklabels ? 1 : 0.5)
+            : fontSize * (ax.showticklabels ? 0.5 : 0);
+      }
+    }
+  }
+
+  var pos = axes.getPxPosition(gd, ax);
+  var transform, x, y;
+
+  if (axLetter === "x") {
+    x = ax._offset + ax._length / 2;
+    y = ax.side === "top" ? pos - titleStandoff : pos + titleStandoff;
+  } else {
+    y = ax._offset + ax._length / 2;
+    x = ax.side === "right" ? pos + titleStandoff : pos - titleStandoff;
+    transform = { rotate: "-90", offset: 0 };
+  }
+
+  var avoid;
+
+  if (ax.type !== "multicategory") {
+    var tickLabels = ax._selections[ax._id + "tick"];
+
+    avoid = {
+      selection: tickLabels,
+      side: ax.side,
+    };
 
-    if(axLetter === 'x') {
-        x = ax._offset + ax._length / 2;
-        y = (ax.side === 'top') ? pos - titleStandoff : pos + titleStandoff;
-    } else {
-        y = ax._offset + ax._length / 2;
-        x = (ax.side === 'right') ? pos + titleStandoff : pos - titleStandoff;
-        transform = {rotate: '-90', offset: 0};
+    if (tickLabels && tickLabels.node() && tickLabels.node().parentNode) {
+      var translation = Drawing.getTranslate(tickLabels.node().parentNode);
+      avoid.offsetLeft = translation.x;
+      avoid.offsetTop = translation.y;
     }
 
-    var avoid;
-
-    if(ax.type !== 'multicategory') {
-        var tickLabels = ax._selections[ax._id + 'tick'];
-
-        avoid = {
-            selection: tickLabels,
-            side: ax.side
-        };
-
-        if(tickLabels && tickLabels.node() && tickLabels.node().parentNode) {
-            var translation = Drawing.getTranslate(tickLabels.node().parentNode);
-            avoid.offsetLeft = translation.x;
-            avoid.offsetTop = translation.y;
-        }
-
-        if(ax.title.hasOwnProperty('standoff')) {
-            avoid.pad = 0;
-        }
+    if (ax.title.hasOwnProperty("standoff")) {
+      avoid.pad = 0;
     }
+  }
 
-    ax._titleStandoff = titleStandoff;
+  ax._titleStandoff = titleStandoff;
 
-    return Titles.draw(gd, axId + 'title', {
-        propContainer: ax,
-        propName: ax._name + '.title.text',
-        placeholder: fullLayout._dfltTitle[axLetter],
-        avoid: avoid,
-        transform: transform,
-        attributes: {x: x, y: y, 'text-anchor': 'middle'}
-    });
+  return Titles.draw(gd, axId + "title", {
+    propContainer: ax,
+    propName: ax._name + ".title.text",
+    placeholder: fullLayout._dfltTitle[axLetter],
+    avoid: avoid,
+    transform: transform,
+    attributes: { x: x, y: y, "text-anchor": "middle" },
+  });
 }
 
-axes.shouldShowZeroLine = function(gd, ax, counterAxis) {
-    var rng = Lib.simpleMap(ax.range, ax.r2l);
-    return (
-        (rng[0] * rng[1] <= 0) &&
-        ax.zeroline &&
-        (ax.type === 'linear' || ax.type === '-') &&
-        !(ax.rangebreaks && ax.maskBreaks(0) === BADNUM) &&
-        (
-            clipEnds(ax, 0) ||
-            !anyCounterAxLineAtZero(gd, ax, counterAxis, rng) ||
-            hasBarsOrFill(gd, ax)
-        )
-    );
+axes.shouldShowZeroLine = function (gd, ax, counterAxis) {
+  var rng = Lib.simpleMap(ax.range, ax.r2l);
+  return (
+    rng[0] * rng[1] <= 0 &&
+    ax.zeroline &&
+    (ax.type === "linear" || ax.type === "-") &&
+    !(ax.rangebreaks && ax.maskBreaks(0) === BADNUM) &&
+    (clipEnds(ax, 0) ||
+      !anyCounterAxLineAtZero(gd, ax, counterAxis, rng) ||
+      hasBarsOrFill(gd, ax))
+  );
 };
 
-axes.clipEnds = function(ax, vals) {
-    return vals.filter(function(d) { return clipEnds(ax, d.x); });
+axes.clipEnds = function (ax, vals) {
+  return vals.filter(function (d) {
+    return clipEnds(ax, d.x);
+  });
 };
 
 function clipEnds(ax, l) {
-    var p = ax.l2p(l);
-    return (p > 1 && p < ax._length - 1);
+  var p = ax.l2p(l);
+  return p > 1 && p < ax._length - 1;
 }
 
 function anyCounterAxLineAtZero(gd, ax, counterAxis, rng) {
-    var mainCounterAxis = counterAxis._mainAxis;
-    if(!mainCounterAxis) return;
-
-    var fullLayout = gd._fullLayout;
-    var axLetter = ax._id.charAt(0);
-    var counterLetter = axes.counterLetter(ax._id);
-
-    var zeroPosition = ax._offset + (
-        ((Math.abs(rng[0]) < Math.abs(rng[1])) === (axLetter === 'x')) ?
-        0 : ax._length
-    );
+  var mainCounterAxis = counterAxis._mainAxis;
+  if (!mainCounterAxis) return;
 
-    function lineNearZero(ax2) {
-        if(!ax2.showline || !ax2.linewidth) return false;
-        var tolerance = Math.max((ax2.linewidth + ax.zerolinewidth) / 2, 1);
+  var fullLayout = gd._fullLayout;
+  var axLetter = ax._id.charAt(0);
+  var counterLetter = axes.counterLetter(ax._id);
 
-        function closeEnough(pos2) {
-            return typeof pos2 === 'number' && Math.abs(pos2 - zeroPosition) < tolerance;
-        }
+  var zeroPosition =
+    ax._offset +
+    (Math.abs(rng[0]) < Math.abs(rng[1]) === (axLetter === "x")
+      ? 0
+      : ax._length);
 
-        if(closeEnough(ax2._mainLinePosition) || closeEnough(ax2._mainMirrorPosition)) {
-            return true;
-        }
-        var linePositions = ax2._linepositions || {};
-        for(var k in linePositions) {
-            if(closeEnough(linePositions[k][0]) || closeEnough(linePositions[k][1])) {
-                return true;
-            }
-        }
-    }
+  function lineNearZero(ax2) {
+    if (!ax2.showline || !ax2.linewidth) return false;
+    var tolerance = Math.max((ax2.linewidth + ax.zerolinewidth) / 2, 1);
 
-    var plotinfo = fullLayout._plots[counterAxis._mainSubplot];
-    if(!(plotinfo.mainplotinfo || plotinfo).overlays.length) {
-        return lineNearZero(counterAxis, zeroPosition);
+    function closeEnough(pos2) {
+      return (
+        typeof pos2 === "number" && Math.abs(pos2 - zeroPosition) < tolerance
+      );
     }
 
-    var counterLetterAxes = axes.list(gd, counterLetter);
-    for(var i = 0; i < counterLetterAxes.length; i++) {
-        var counterAxis2 = counterLetterAxes[i];
-        if(
-            counterAxis2._mainAxis === mainCounterAxis &&
-            lineNearZero(counterAxis2, zeroPosition)
-        ) {
-            return true;
-        }
+    if (
+      closeEnough(ax2._mainLinePosition) ||
+      closeEnough(ax2._mainMirrorPosition)
+    ) {
+      return true;
+    }
+    var linePositions = ax2._linepositions || {};
+    for (var k in linePositions) {
+      if (
+        closeEnough(linePositions[k][0]) ||
+        closeEnough(linePositions[k][1])
+      ) {
+        return true;
+      }
+    }
+  }
+
+  var plotinfo = fullLayout._plots[counterAxis._mainSubplot];
+  if (!(plotinfo.mainplotinfo || plotinfo).overlays.length) {
+    return lineNearZero(counterAxis, zeroPosition);
+  }
+
+  var counterLetterAxes = axes.list(gd, counterLetter);
+  for (var i = 0; i < counterLetterAxes.length; i++) {
+    var counterAxis2 = counterLetterAxes[i];
+    if (
+      counterAxis2._mainAxis === mainCounterAxis &&
+      lineNearZero(counterAxis2, zeroPosition)
+    ) {
+      return true;
     }
+  }
 }
 
 function hasBarsOrFill(gd, ax) {
-    var fullData = gd._fullData;
-    var subplot = ax._mainSubplot;
-    var axLetter = ax._id.charAt(0);
-
-    for(var i = 0; i < fullData.length; i++) {
-        var trace = fullData[i];
-
-        if(trace.visible === true && (trace.xaxis + trace.yaxis) === subplot) {
-            if(
-                Registry.traceIs(trace, 'bar-like') &&
-                trace.orientation === {x: 'h', y: 'v'}[axLetter]
-            ) return true;
-
-            if(
-                trace.fill &&
-                trace.fill.charAt(trace.fill.length - 1) === axLetter
-            ) return true;
-        }
+  var fullData = gd._fullData;
+  var subplot = ax._mainSubplot;
+  var axLetter = ax._id.charAt(0);
+
+  for (var i = 0; i < fullData.length; i++) {
+    var trace = fullData[i];
+
+    if (trace.visible === true && trace.xaxis + trace.yaxis === subplot) {
+      if (
+        Registry.traceIs(trace, "bar-like") &&
+        trace.orientation === { x: "h", y: "v" }[axLetter]
+      )
+        return true;
+
+      if (trace.fill && trace.fill.charAt(trace.fill.length - 1) === axLetter)
+        return true;
     }
-    return false;
+  }
+  return false;
 }
 
 function selectTickLabel(gTick) {
-    var s = d3.select(gTick);
-    var mj = s.select('.text-math-group');
-    return mj.empty() ? s.select('text') : mj;
+  var s = d3.select(gTick);
+  var mj = s.select(".text-math-group");
+  return mj.empty() ? s.select("text") : mj;
 }
 
 /**
@@ -4240,215 +4579,245 @@ function selectTickLabel(gTick) {
  * We're probably also doing multiple redraws in this case, would be faster
  * if we can just do the whole calculation ahead of time and draw once.
  */
-axes.allowAutoMargin = function(gd) {
-    var axList = axes.list(gd, '', true);
-    for(var i = 0; i < axList.length; i++) {
-        var ax = axList[i];
-        if(ax.automargin) {
-            Plots.allowAutoMargin(gd, axAutoMarginID(ax));
-            if(ax.mirror) {
-                Plots.allowAutoMargin(gd, axMirrorAutoMarginID(ax));
-            }
-        }
-        if(Registry.getComponentMethod('rangeslider', 'isVisible')(ax)) {
-            Plots.allowAutoMargin(gd, rangeSliderAutoMarginID(ax));
-        }
-    }
+axes.allowAutoMargin = function (gd) {
+  var axList = axes.list(gd, "", true);
+  for (var i = 0; i < axList.length; i++) {
+    var ax = axList[i];
+    if (ax.automargin) {
+      Plots.allowAutoMargin(gd, axAutoMarginID(ax));
+      if (ax.mirror) {
+        Plots.allowAutoMargin(gd, axMirrorAutoMarginID(ax));
+      }
+    }
+    if (Registry.getComponentMethod("rangeslider", "isVisible")(ax)) {
+      Plots.allowAutoMargin(gd, rangeSliderAutoMarginID(ax));
+    }
+  }
 };
 
-function axAutoMarginID(ax) { return ax._id + '.automargin'; }
-function axMirrorAutoMarginID(ax) { return axAutoMarginID(ax) + '.mirror'; }
-function rangeSliderAutoMarginID(ax) { return ax._id + '.rangeslider'; }
+function axAutoMarginID(ax) {
+  return ax._id + ".automargin";
+}
+function axMirrorAutoMarginID(ax) {
+  return axAutoMarginID(ax) + ".mirror";
+}
+function rangeSliderAutoMarginID(ax) {
+  return ax._id + ".rangeslider";
+}
 
 // swap all the presentation attributes of the axes showing these traces
-axes.swap = function(gd, traces) {
-    var axGroups = makeAxisGroups(gd, traces);
+axes.swap = function (gd, traces) {
+  var axGroups = makeAxisGroups(gd, traces);
 
-    for(var i = 0; i < axGroups.length; i++) {
-        swapAxisGroup(gd, axGroups[i].x, axGroups[i].y);
-    }
+  for (var i = 0; i < axGroups.length; i++) {
+    swapAxisGroup(gd, axGroups[i].x, axGroups[i].y);
+  }
 };
 
 function makeAxisGroups(gd, traces) {
-    var groups = [];
-    var i, j;
-
-    for(i = 0; i < traces.length; i++) {
-        var groupsi = [];
-        var xi = gd._fullData[traces[i]].xaxis;
-        var yi = gd._fullData[traces[i]].yaxis;
-        if(!xi || !yi) continue; // not a 2D cartesian trace?
-
-        for(j = 0; j < groups.length; j++) {
-            if(groups[j].x.indexOf(xi) !== -1 || groups[j].y.indexOf(yi) !== -1) {
-                groupsi.push(j);
-            }
-        }
+  var groups = [];
+  var i, j;
 
-        if(!groupsi.length) {
-            groups.push({x: [xi], y: [yi]});
-            continue;
-        }
+  for (i = 0; i < traces.length; i++) {
+    var groupsi = [];
+    var xi = gd._fullData[traces[i]].xaxis;
+    var yi = gd._fullData[traces[i]].yaxis;
+    if (!xi || !yi) continue; // not a 2D cartesian trace?
 
-        var group0 = groups[groupsi[0]];
-        var groupj;
+    for (j = 0; j < groups.length; j++) {
+      if (groups[j].x.indexOf(xi) !== -1 || groups[j].y.indexOf(yi) !== -1) {
+        groupsi.push(j);
+      }
+    }
 
-        if(groupsi.length > 1) {
-            for(j = 1; j < groupsi.length; j++) {
-                groupj = groups[groupsi[j]];
-                mergeAxisGroups(group0.x, groupj.x);
-                mergeAxisGroups(group0.y, groupj.y);
-            }
-        }
-        mergeAxisGroups(group0.x, [xi]);
-        mergeAxisGroups(group0.y, [yi]);
+    if (!groupsi.length) {
+      groups.push({ x: [xi], y: [yi] });
+      continue;
+    }
+
+    var group0 = groups[groupsi[0]];
+    var groupj;
+
+    if (groupsi.length > 1) {
+      for (j = 1; j < groupsi.length; j++) {
+        groupj = groups[groupsi[j]];
+        mergeAxisGroups(group0.x, groupj.x);
+        mergeAxisGroups(group0.y, groupj.y);
+      }
     }
+    mergeAxisGroups(group0.x, [xi]);
+    mergeAxisGroups(group0.y, [yi]);
+  }
 
-    return groups;
+  return groups;
 }
 
 function mergeAxisGroups(intoSet, fromSet) {
-    for(var i = 0; i < fromSet.length; i++) {
-        if(intoSet.indexOf(fromSet[i]) === -1) intoSet.push(fromSet[i]);
-    }
+  for (var i = 0; i < fromSet.length; i++) {
+    if (intoSet.indexOf(fromSet[i]) === -1) intoSet.push(fromSet[i]);
+  }
 }
 
 function swapAxisGroup(gd, xIds, yIds) {
-    var xFullAxes = [];
-    var yFullAxes = [];
-    var layout = gd.layout;
-    var i, j;
-
-    for(i = 0; i < xIds.length; i++) xFullAxes.push(axes.getFromId(gd, xIds[i]));
-    for(i = 0; i < yIds.length; i++) yFullAxes.push(axes.getFromId(gd, yIds[i]));
-
-    var allAxKeys = Object.keys(axAttrs);
-
-    var noSwapAttrs = [
-        'anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle', 'editType'
-    ];
-    var numericTypes = ['linear', 'log'];
-
-    for(i = 0; i < allAxKeys.length; i++) {
-        var keyi = allAxKeys[i];
-        var xVal = xFullAxes[0][keyi];
-        var yVal = yFullAxes[0][keyi];
-        var allEqual = true;
-        var coerceLinearX = false;
-        var coerceLinearY = false;
-        if(keyi.charAt(0) === '_' || typeof xVal === 'function' ||
-                noSwapAttrs.indexOf(keyi) !== -1) {
-            continue;
-        }
-        for(j = 1; j < xFullAxes.length && allEqual; j++) {
-            var xVali = xFullAxes[j][keyi];
-            if(keyi === 'type' && numericTypes.indexOf(xVal) !== -1 &&
-                    numericTypes.indexOf(xVali) !== -1 && xVal !== xVali) {
-                // type is special - if we find a mixture of linear and log,
-                // coerce them all to linear on flipping
-                coerceLinearX = true;
-            } else if(xVali !== xVal) allEqual = false;
-        }
-        for(j = 1; j < yFullAxes.length && allEqual; j++) {
-            var yVali = yFullAxes[j][keyi];
-            if(keyi === 'type' && numericTypes.indexOf(yVal) !== -1 &&
-                    numericTypes.indexOf(yVali) !== -1 && yVal !== yVali) {
-                // type is special - if we find a mixture of linear and log,
-                // coerce them all to linear on flipping
-                coerceLinearY = true;
-            } else if(yFullAxes[j][keyi] !== yVal) allEqual = false;
-        }
-        if(allEqual) {
-            if(coerceLinearX) layout[xFullAxes[0]._name].type = 'linear';
-            if(coerceLinearY) layout[yFullAxes[0]._name].type = 'linear';
-            swapAxisAttrs(layout, keyi, xFullAxes, yFullAxes, gd._fullLayout._dfltTitle);
-        }
-    }
-
-    // now swap x&y for any annotations anchored to these x & y
-    for(i = 0; i < gd._fullLayout.annotations.length; i++) {
-        var ann = gd._fullLayout.annotations[i];
-        if(xIds.indexOf(ann.xref) !== -1 &&
-                yIds.indexOf(ann.yref) !== -1) {
-            Lib.swapAttrs(layout.annotations[i], ['?']);
-        }
-    }
+  var xFullAxes = [];
+  var yFullAxes = [];
+  var layout = gd.layout;
+  var i, j;
+
+  for (i = 0; i < xIds.length; i++) xFullAxes.push(axes.getFromId(gd, xIds[i]));
+  for (i = 0; i < yIds.length; i++) yFullAxes.push(axes.getFromId(gd, yIds[i]));
+
+  var allAxKeys = Object.keys(axAttrs);
+
+  var noSwapAttrs = [
+    "anchor",
+    "domain",
+    "overlaying",
+    "position",
+    "side",
+    "tickangle",
+    "editType",
+  ];
+  var numericTypes = ["linear", "log"];
+
+  for (i = 0; i < allAxKeys.length; i++) {
+    var keyi = allAxKeys[i];
+    var xVal = xFullAxes[0][keyi];
+    var yVal = yFullAxes[0][keyi];
+    var allEqual = true;
+    var coerceLinearX = false;
+    var coerceLinearY = false;
+    if (
+      keyi.charAt(0) === "_" ||
+      typeof xVal === "function" ||
+      noSwapAttrs.indexOf(keyi) !== -1
+    ) {
+      continue;
+    }
+    for (j = 1; j < xFullAxes.length && allEqual; j++) {
+      var xVali = xFullAxes[j][keyi];
+      if (
+        keyi === "type" &&
+        numericTypes.indexOf(xVal) !== -1 &&
+        numericTypes.indexOf(xVali) !== -1 &&
+        xVal !== xVali
+      ) {
+        // type is special - if we find a mixture of linear and log,
+        // coerce them all to linear on flipping
+        coerceLinearX = true;
+      } else if (xVali !== xVal) allEqual = false;
+    }
+    for (j = 1; j < yFullAxes.length && allEqual; j++) {
+      var yVali = yFullAxes[j][keyi];
+      if (
+        keyi === "type" &&
+        numericTypes.indexOf(yVal) !== -1 &&
+        numericTypes.indexOf(yVali) !== -1 &&
+        yVal !== yVali
+      ) {
+        // type is special - if we find a mixture of linear and log,
+        // coerce them all to linear on flipping
+        coerceLinearY = true;
+      } else if (yFullAxes[j][keyi] !== yVal) allEqual = false;
+    }
+    if (allEqual) {
+      if (coerceLinearX) layout[xFullAxes[0]._name].type = "linear";
+      if (coerceLinearY) layout[yFullAxes[0]._name].type = "linear";
+      swapAxisAttrs(
+        layout,
+        keyi,
+        xFullAxes,
+        yFullAxes,
+        gd._fullLayout._dfltTitle
+      );
+    }
+  }
+
+  // now swap x&y for any annotations anchored to these x & y
+  for (i = 0; i < gd._fullLayout.annotations.length; i++) {
+    var ann = gd._fullLayout.annotations[i];
+    if (xIds.indexOf(ann.xref) !== -1 && yIds.indexOf(ann.yref) !== -1) {
+      Lib.swapAttrs(layout.annotations[i], ["?"]);
+    }
+  }
 }
 
 function swapAxisAttrs(layout, key, xFullAxes, yFullAxes, dfltTitle) {
-    // in case the value is the default for either axis,
-    // look at the first axis in each list and see if
-    // this key's value is undefined
-    var np = Lib.nestedProperty;
-    var xVal = np(layout[xFullAxes[0]._name], key).get();
-    var yVal = np(layout[yFullAxes[0]._name], key).get();
-    var i;
-
-    if(key === 'title') {
-        // special handling of placeholder titles
-        if(xVal && xVal.text === dfltTitle.x) {
-            xVal.text = dfltTitle.y;
-        }
-        if(yVal && yVal.text === dfltTitle.y) {
-            yVal.text = dfltTitle.x;
-        }
-    }
-
-    for(i = 0; i < xFullAxes.length; i++) {
-        np(layout, xFullAxes[i]._name + '.' + key).set(yVal);
-    }
-    for(i = 0; i < yFullAxes.length; i++) {
-        np(layout, yFullAxes[i]._name + '.' + key).set(xVal);
-    }
+  // in case the value is the default for either axis,
+  // look at the first axis in each list and see if
+  // this key's value is undefined
+  var np = Lib.nestedProperty;
+  var xVal = np(layout[xFullAxes[0]._name], key).get();
+  var yVal = np(layout[yFullAxes[0]._name], key).get();
+  var i;
+
+  if (key === "title") {
+    // special handling of placeholder titles
+    if (xVal && xVal.text === dfltTitle.x) {
+      xVal.text = dfltTitle.y;
+    }
+    if (yVal && yVal.text === dfltTitle.y) {
+      yVal.text = dfltTitle.x;
+    }
+  }
+
+  for (i = 0; i < xFullAxes.length; i++) {
+    np(layout, xFullAxes[i]._name + "." + key).set(yVal);
+  }
+  for (i = 0; i < yFullAxes.length; i++) {
+    np(layout, yFullAxes[i]._name + "." + key).set(xVal);
+  }
 }
 
 function isAngular(ax) {
-    return ax._id === 'angularaxis';
+  return ax._id === "angularaxis";
 }
 
 function moveOutsideBreak(v, ax) {
-    var len = ax._rangebreaks.length;
-    for(var k = 0; k < len; k++) {
-        var brk = ax._rangebreaks[k];
-        if(v >= brk.min && v < brk.max) {
-            return brk.max;
-        }
-    }
-    return v;
+  var len = ax._rangebreaks.length;
+  for (var k = 0; k < len; k++) {
+    var brk = ax._rangebreaks[k];
+    if (v >= brk.min && v < brk.max) {
+      return brk.max;
+    }
+  }
+  return v;
 }
 
 function insideTicklabelposition(ax) {
-    return ((ax.ticklabelposition || '').indexOf('inside') !== -1);
+  return (ax.ticklabelposition || "").indexOf("inside") !== -1;
 }
 
 function hideCounterAxisInsideTickLabels(ax, opts) {
-    if(insideTicklabelposition(ax._anchorAxis || {})) {
-        if(ax._hideCounterAxisInsideTickLabels) {
-            ax._hideCounterAxisInsideTickLabels(opts);
-        }
+  if (insideTicklabelposition(ax._anchorAxis || {})) {
+    if (ax._hideCounterAxisInsideTickLabels) {
+      ax._hideCounterAxisInsideTickLabels(opts);
     }
+  }
 }
 
 function incrementShift(ax, shiftVal, axShifts, normalize) {
-    // Need to set 'overlay' for anchored axis
-    var overlay = ((ax.anchor !== 'free') && ((ax.overlaying === undefined) || (ax.overlaying === false))) ? ax._id : ax.overlaying;
-    var shiftValAdj;
-    if(normalize) {
-        shiftValAdj = ax.side === 'right' ? shiftVal : -shiftVal;
-    } else {
-        shiftValAdj = shiftVal;
-    }
-    if(!(overlay in axShifts)) {
-        axShifts[overlay] = {};
-    }
-    if(!(ax.side in axShifts[overlay])) {
-        axShifts[overlay][ax.side] = 0;
-    }
-    axShifts[overlay][ax.side] += shiftValAdj;
+  // Need to set 'overlay' for anchored axis
+  var overlay =
+    ax.anchor !== "free" &&
+    (ax.overlaying === undefined || ax.overlaying === false)
+      ? ax._id
+      : ax.overlaying;
+  var shiftValAdj;
+  if (normalize) {
+    shiftValAdj = ax.side === "right" ? shiftVal : -shiftVal;
+  } else {
+    shiftValAdj = shiftVal;
+  }
+  if (!(overlay in axShifts)) {
+    axShifts[overlay] = {};
+  }
+  if (!(ax.side in axShifts[overlay])) {
+    axShifts[overlay][ax.side] = 0;
+  }
+  axShifts[overlay][ax.side] += shiftValAdj;
 }
 
 function setShiftVal(ax, axShifts) {
-    return ax.autoshift ?
-        axShifts[ax.overlaying][ax.side] :
-        (ax.shift || 0);
+  return ax.autoshift ? axShifts[ax.overlaying][ax.side] : ax.shift || 0;
 }
diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js
index 14f17f9c2aa..8c167206b5d 100644
--- a/src/plots/cartesian/set_convert.js
+++ b/src/plots/cartesian/set_convert.js
@@ -156,9 +156,13 @@ 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]);
+            }
+            // var v0 = (arrayIn[0] || [])[i];
+            // var v1 = (arrayIn[1] || [])[i];
+            arrayOut[i] = getCategoryIndex(vs);
         }
 
         return arrayOut;
@@ -333,6 +337,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,9 +362,14 @@ 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;
+            var i;
 
             var group = ax._matchGroup;
             if(group && ax._categories.length === 0) {
@@ -371,49 +381,99 @@ 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 = [];
+            // Don't think that the trace should be drawn at all if the lengths don't match. Removing the arrays length check. It is better to fail loudly than silently.
             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 ind1 = seen[1][1];
-                return ind1[a[1]] - ind1[b[1]];
-            });
+                var fullSortedObjectList = sortLib.sortObjectList(cols, fullObjectList.slice());
+                var fullList = sortLib.objectListToList(fullSortedObjectList);
+                var fullSortedMatrix = sortLib.sortedMatrix(fullList, true);
 
-            for(i = 0; i < list.length; i++) {
-                setCategoryIndex(list[i]);
+                var fullXs = fullSortedMatrix[0].slice();
+
+                for(i = 0; i < fullXs.length; i++) {
+                    setCategoryIndex(fullXs[i]);
+                }
             }
         };
     }
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 5a6b5e0466c..b32d05bd9e1 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -3136,7 +3136,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
@@ -3144,7 +3144,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)
@@ -3352,13 +3352,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;
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index 165b90912f5..63ce83e9b53 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -6010,6 +6010,7 @@ describe('hovermode: (x|y)unified', function() {
         var mockOhlc = require('../../image/mocks/finance_multicategory.json');
         var mockCopy = Lib.extendDeep({}, mockOhlc);
         mockCopy.layout.hovermode = 'x unified';
+
         Plotly.newPlot(gd, mockCopy)
             .then(function(gd) {
                 _hover(gd, {curveNumber: 0, pointNumber: 0});