diff --git a/dist/translation-keys.txt b/dist/translation-keys.txt
index 42154ee4f3b..1f81c5ed361 100644
--- a/dist/translation-keys.txt
+++ b/dist/translation-keys.txt
@@ -1,12 +1,12 @@
 Autoscale                                              // components/modebar/buttons.js:139
 Box Select                                             // components/modebar/buttons.js:103
-Click to enter Colorscale title                        // plots/plots.js:437
-Click to enter Component A title                       // plots/ternary/ternary.js:386
-Click to enter Component B title                       // plots/ternary/ternary.js:400
-Click to enter Component C title                       // plots/ternary/ternary.js:411
+Click to enter Colorscale title                        // plots/plots.js:303
+Click to enter Component A title                       // plots/ternary/ternary.js:392
+Click to enter Component B title                       // plots/ternary/ternary.js:406
+Click to enter Component C title                       // plots/ternary/ternary.js:417
 Click to enter Plot title                              // plot_api/plot_api.js:579
-Click to enter X axis title                            // plots/plots.js:435
-Click to enter Y axis title                            // plots/plots.js:436
+Click to enter X axis title                            // plots/plots.js:301
+Click to enter Y axis title                            // plots/plots.js:302
 Compare data on hover                                  // components/modebar/buttons.js:167
 Double-click on legend to isolate one trace            // components/legend/handle_click.js:90
 Double-click to zoom back out                          // plots/cartesian/dragbox.js:299
@@ -17,18 +17,18 @@ Lasso Select                                           // components/modebar/but
 Orbital rotation                                       // components/modebar/buttons.js:279
 Pan                                                    // components/modebar/buttons.js:94
 Produced with Plotly                                   // components/modebar/modebar.js:256
-Reset                                                  // components/modebar/buttons.js:432
+Reset                                                  // components/modebar/buttons.js:431
 Reset axes                                             // components/modebar/buttons.js:148
-Reset camera to default                                // components/modebar/buttons.js:314
-Reset camera to last save                              // components/modebar/buttons.js:322
-Reset view                                             // components/modebar/buttons.js:583
-Reset views                                            // components/modebar/buttons.js:529
+Reset camera to default                                // components/modebar/buttons.js:313
+Reset camera to last save                              // components/modebar/buttons.js:321
+Reset view                                             // components/modebar/buttons.js:582
+Reset views                                            // components/modebar/buttons.js:528
 Show closest data on hover                             // components/modebar/buttons.js:157
 Snapshot succeeded                                     // components/modebar/buttons.js:66
 Sorry, there was a problem downloading your snapshot!  // components/modebar/buttons.js:69
 Taking snapshot - this may take a few seconds          // components/modebar/buttons.js:57
-Toggle Spike Lines                                     // components/modebar/buttons.js:548
-Toggle show closest data on hover                      // components/modebar/buttons.js:353
+Toggle Spike Lines                                     // components/modebar/buttons.js:547
+Toggle show closest data on hover                      // components/modebar/buttons.js:352
 Turntable rotation                                     // components/modebar/buttons.js:288
 Zoom                                                   // components/modebar/buttons.js:85
 Zoom in                                                // components/modebar/buttons.js:121
@@ -52,5 +52,5 @@ q1:                                                    // traces/box/calc.js:130
 q3:                                                    // traces/box/calc.js:131
 source:                                                // traces/sankey/plot.js:140
 target:                                                // traces/sankey/plot.js:141
-trace                                                  // plots/plots.js:439
+trace                                                  // plots/plots.js:305
 upper fence:                                           // traces/box/calc.js:135
\ No newline at end of file
diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js
index 8da79d83448..436125c4801 100644
--- a/src/components/annotations/calc_autorange.js
+++ b/src/components/annotations/calc_autorange.js
@@ -23,19 +23,19 @@ module.exports = function calcAutorange(gd) {
 
     var annotationAxes = {};
     annotationList.forEach(function(ann) {
-        annotationAxes[ann.xref] = true;
-        annotationAxes[ann.yref] = true;
+        annotationAxes[ann.xref] = 1;
+        annotationAxes[ann.yref] = 1;
     });
 
-    var autorangedAnnos = Axes.list(gd).filter(function(ax) {
-        return ax.autorange && annotationAxes[ax._id];
-    });
-    if(!autorangedAnnos.length) return;
-
-    return Lib.syncOrAsync([
-        draw,
-        annAutorange
-    ], gd);
+    for(var axId in annotationAxes) {
+        var ax = Axes.getFromId(gd, axId);
+        if(ax && ax.autorange) {
+            return Lib.syncOrAsync([
+                draw,
+                annAutorange
+            ], gd);
+        }
+    }
 };
 
 function annAutorange(gd) {
diff --git a/src/components/annotations/index.js b/src/components/annotations/index.js
index a3cd7893545..339c740f49b 100644
--- a/src/components/annotations/index.js
+++ b/src/components/annotations/index.js
@@ -18,6 +18,7 @@ module.exports = {
 
     layoutAttributes: require('./attributes'),
     supplyLayoutDefaults: require('./defaults'),
+    includeBasePlot: require('../../plots/cartesian/include_components')('annotations'),
 
     calcAutorange: require('./calc_autorange'),
     draw: drawModule.draw,
diff --git a/src/components/annotations3d/index.js b/src/components/annotations3d/index.js
index 4ff43fa9f56..48bbd3d7717 100644
--- a/src/components/annotations3d/index.js
+++ b/src/components/annotations3d/index.js
@@ -8,6 +8,9 @@
 
 'use strict';
 
+var Registry = require('../../registry');
+var Lib = require('../../lib');
+
 module.exports = {
     moduleType: 'component',
     name: 'annotations3d',
@@ -20,7 +23,24 @@ module.exports = {
 
     layoutAttributes: require('./attributes'),
     handleDefaults: require('./defaults'),
+    includeBasePlot: includeGL3D,
 
     convert: require('./convert'),
     draw: require('./draw')
 };
+
+function includeGL3D(layoutIn, layoutOut) {
+    var GL3D = Registry.subplotsRegistry.gl3d;
+    if(!GL3D) return;
+
+    var attrRegex = GL3D.attrRegex;
+
+    var keys = Object.keys(layoutIn);
+    for(var i = 0; i < keys.length; i++) {
+        var k = keys[i];
+        if(attrRegex.test(k) && (layoutIn[k].annotations || []).length) {
+            Lib.pushUnique(layoutOut._basePlotModules, GL3D);
+            Lib.pushUnique(layoutOut._subplots.gl3d, k);
+        }
+    }
+}
diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js
index 57f853006da..0f2a561a027 100644
--- a/src/components/colorbar/draw.js
+++ b/src/components/colorbar/draw.js
@@ -341,10 +341,13 @@ module.exports = function draw(gd, id) {
                 }
             }
 
-            container.selectAll('.cbfills,.cblines,.cbaxis')
+            container.selectAll('.cbfills,.cblines')
                 .attr('transform', 'translate(0,' +
                     Math.round(gs.h * (1 - cbAxisOut.domain[1])) + ')');
 
+            cbAxisOut._axislayer.attr('transform', 'translate(0,' +
+                Math.round(-gs.t) + ')');
+
             var fills = container.select('.cbfills')
                 .selectAll('rect.cbfill')
                     .data(filllevels);
@@ -433,7 +436,7 @@ module.exports = function draw(gd, id) {
                                 selection: d3.select(gd).selectAll('g.' + cbAxisOut._id + 'tick'),
                                 side: opts.titleside,
                                 offsetLeft: gs.l,
-                                offsetTop: gs.t,
+                                offsetTop: 0,
                                 maxShift: fullLayout.width
                             },
                             attributes: {x: x, y: y, 'text-anchor': 'middle'},
diff --git a/src/components/images/index.js b/src/components/images/index.js
index 3a4269ef8df..f9bec099aae 100644
--- a/src/components/images/index.js
+++ b/src/components/images/index.js
@@ -14,6 +14,7 @@ module.exports = {
 
     layoutAttributes: require('./attributes'),
     supplyLayoutDefaults: require('./defaults'),
+    includeBasePlot: require('../../plots/cartesian/include_components')('images'),
 
     draw: require('./draw'),
 
diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js
index 681516f5b86..3432194f22e 100644
--- a/src/components/modebar/buttons.js
+++ b/src/components/modebar/buttons.js
@@ -11,7 +11,7 @@
 
 var Plotly = require('../../plotly');
 var Plots = require('../../plots/plots');
-var Axes = require('../../plots/cartesian/axes');
+var axisIds = require('../../plots/cartesian/axis_ids');
 var Lib = require('../../lib');
 var downloadImage = require('../../snapshot/download');
 var Icons = require('../../../build/ploticon');
@@ -175,15 +175,15 @@ modeBarButtons.hoverCompareCartesian = {
 };
 
 function handleCartesian(gd, ev) {
-    var button = ev.currentTarget,
-        astr = button.getAttribute('data-attr'),
-        val = button.getAttribute('data-val') || true,
-        fullLayout = gd._fullLayout,
-        aobj = {},
-        axList = Axes.list(gd, null, true),
-        ax,
-        allEnabled = 'on',
-        i;
+    var button = ev.currentTarget;
+    var astr = button.getAttribute('data-attr');
+    var val = button.getAttribute('data-val') || true;
+    var fullLayout = gd._fullLayout;
+    var aobj = {};
+    var axList = axisIds.list(gd, null, true);
+    var allEnabled = 'on';
+
+    var ax, i;
 
     if(astr === 'zoom') {
         var mag = (val === 'in') ? 0.5 : 2,
@@ -293,12 +293,11 @@ modeBarButtons.tableRotation = {
 };
 
 function handleDrag3d(gd, ev) {
-    var button = ev.currentTarget,
-        attr = button.getAttribute('data-attr'),
-        val = button.getAttribute('data-val') || true,
-        fullLayout = gd._fullLayout,
-        sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'),
-        layoutUpdate = {};
+    var button = ev.currentTarget;
+    var attr = button.getAttribute('data-attr');
+    var val = button.getAttribute('data-val') || true;
+    var sceneIds = gd._fullLayout._subplots.gl3d;
+    var layoutUpdate = {};
 
     var parts = attr.split('.');
 
@@ -326,11 +325,11 @@ modeBarButtons.resetCameraLastSave3d = {
 };
 
 function handleCamera3d(gd, ev) {
-    var button = ev.currentTarget,
-        attr = button.getAttribute('data-attr'),
-        fullLayout = gd._fullLayout,
-        sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'),
-        aobj = {};
+    var button = ev.currentTarget;
+    var attr = button.getAttribute('data-attr');
+    var fullLayout = gd._fullLayout;
+    var sceneIds = fullLayout._subplots.gl3d;
+    var aobj = {};
 
     for(var i = 0; i < sceneIds.length; i++) {
         var sceneId = sceneIds[i],
@@ -360,19 +359,19 @@ modeBarButtons.hoverClosest3d = {
 };
 
 function handleHover3d(gd, ev) {
-    var button = ev.currentTarget,
-        val = button._previousVal || false,
-        layout = gd.layout,
-        fullLayout = gd._fullLayout,
-        sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d');
+    var button = ev.currentTarget;
+    var val = button._previousVal || false;
+    var layout = gd.layout;
+    var fullLayout = gd._fullLayout;
+    var sceneIds = fullLayout._subplots.gl3d;
 
-    var axes = ['xaxis', 'yaxis', 'zaxis'],
-        spikeAttrs = ['showspikes', 'spikesides', 'spikethickness', 'spikecolor'];
+    var axes = ['xaxis', 'yaxis', 'zaxis'];
+    var spikeAttrs = ['showspikes', 'spikesides', 'spikethickness', 'spikecolor'];
 
     // initialize 'current spike' object to be stored in the DOM
-    var currentSpikes = {},
-        axisSpikes = {},
-        layoutUpdate = {};
+    var currentSpikes = {};
+    var axisSpikes = {};
+    var layoutUpdate = {};
 
     if(val) {
         layoutUpdate = Lib.extendDeep(layout, val);
@@ -452,7 +451,7 @@ function handleGeo(gd, ev) {
     var attr = button.getAttribute('data-attr');
     var val = button.getAttribute('data-val') || true;
     var fullLayout = gd._fullLayout;
-    var geoIds = Plots.getSubplotIds(fullLayout, 'geo');
+    var geoIds = fullLayout._subplots.geo;
 
     for(var i = 0; i < geoIds.length; i++) {
         var id = geoIds[i];
@@ -563,11 +562,11 @@ modeBarButtons.toggleSpikelines = {
 };
 
 function setSpikelineVisibility(gd) {
-    var fullLayout = gd._fullLayout,
-        axList = Axes.list(gd, null, true),
-        ax,
-        axName,
-        aobj = {};
+    var fullLayout = gd._fullLayout;
+    var axList = axisIds.list(gd, null, true);
+    var aobj = {};
+
+    var ax, axName;
 
     for(var i = 0; i < axList.length; i++) {
         ax = axList[i];
@@ -590,7 +589,7 @@ modeBarButtons.resetViewMapbox = {
 
 function resetView(gd, subplotType) {
     var fullLayout = gd._fullLayout;
-    var subplotIds = Plots.getSubplotIds(fullLayout, subplotType);
+    var subplotIds = fullLayout._subplots[subplotType];
     var aObj = {};
 
     for(var i = 0; i < subplotIds.length; i++) {
diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js
index bedee3dcb7d..706b9e254b0 100644
--- a/src/components/modebar/manage.js
+++ b/src/components/modebar/manage.js
@@ -9,7 +9,7 @@
 
 'use strict';
 
-var Axes = require('../../plots/cartesian/axes');
+var axisIds = require('../../plots/cartesian/axis_ids');
 var scatterSubTypes = require('../../traces/scatter/subtypes');
 var Registry = require('../../registry');
 
@@ -150,17 +150,15 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) {
 }
 
 function areAllAxesFixed(fullLayout) {
-    var axList = Axes.list({_fullLayout: fullLayout}, null, true);
-    var allFixed = true;
+    var axList = axisIds.list({_fullLayout: fullLayout}, null, true);
 
     for(var i = 0; i < axList.length; i++) {
         if(!axList[i].fixedrange) {
-            allFixed = false;
-            break;
+            return false;
         }
     }
 
-    return allFixed;
+    return true;
 }
 
 // look for traces that support selection
diff --git a/src/components/shapes/index.js b/src/components/shapes/index.js
index 444ede3cd59..be1a542bbdc 100644
--- a/src/components/shapes/index.js
+++ b/src/components/shapes/index.js
@@ -17,6 +17,7 @@ module.exports = {
 
     layoutAttributes: require('./attributes'),
     supplyLayoutDefaults: require('./defaults'),
+    includeBasePlot: require('../../plots/cartesian/include_components')('shapes'),
 
     calcAutorange: require('./calc_autorange'),
     draw: drawModule.draw,
diff --git a/src/constants/alignment.js b/src/constants/alignment.js
index 1ed61b55ce1..66ff87c2947 100644
--- a/src/constants/alignment.js
+++ b/src/constants/alignment.js
@@ -38,5 +38,12 @@ module.exports = {
     // of the font, and according to wikipedia:
     //   an "average" font might have a cap height of 70% of the em
     // https://en.wikipedia.org/wiki/Em_(typography)#History
-    MID_SHIFT: 0.35
+    MID_SHIFT: 0.35,
+
+    OPPOSITE_SIDE: {
+        left: 'right',
+        right: 'left',
+        top: 'bottom',
+        bottom: 'top'
+    }
 };
diff --git a/src/lib/index.js b/src/lib/index.js
index 88c7cc495c9..e7acdce54b7 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -858,3 +858,29 @@ lib.templateString = function(string, obj) {
         return getterCache[key]() || '';
     });
 };
+
+/*
+ * alphanumeric string sort, tailored for subplot IDs like scene2, scene10, x10y13 etc
+ */
+var char0 = 48;
+var char9 = 57;
+lib.subplotSort = function(a, b) {
+    var l = Math.min(a.length, b.length) + 1;
+    var numA = 0;
+    var numB = 0;
+    for(var i = 0; i < l; i++) {
+        var charA = a.charCodeAt(i) || 0;
+        var charB = b.charCodeAt(i) || 0;
+        var isNumA = charA >= char0 && charA <= char9;
+        var isNumB = charB >= char0 && charB <= char9;
+
+        if(isNumA) numA = 10 * numA + charA - char0;
+        if(isNumB) numB = 10 * numB + charB - char0;
+
+        if(!isNumA || !isNumB) {
+            if(numA !== numB) return numA - numB;
+            if(charA !== charB) return charA - charB;
+        }
+    }
+    return numB - numA;
+};
diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js
index 66baee79534..8261969c4f8 100644
--- a/src/plot_api/helpers.js
+++ b/src/plot_api/helpers.js
@@ -15,7 +15,9 @@ var m4FromQuat = require('gl-mat4/fromQuat');
 var Registry = require('../registry');
 var Lib = require('../lib');
 var Plots = require('../plots/plots');
-var Axes = require('../plots/cartesian/axes');
+var AxisIds = require('../plots/cartesian/axis_ids');
+var cleanId = AxisIds.cleanId;
+var getFromTrace = AxisIds.getFromTrace;
 var Color = require('../components/color');
 
 
@@ -45,38 +47,78 @@ exports.cleanLayout = function(layout) {
         if(!layout.yaxis) layout.yaxis = layout.yaxis1;
         delete layout.yaxis1;
     }
+    if(layout.scene1) {
+        if(!layout.scene) layout.scene = layout.scene1;
+        delete layout.scene1;
+    }
 
-    var axList = Axes.list({_fullLayout: layout});
-    for(i = 0; i < axList.length; i++) {
-        var ax = axList[i];
-        if(ax.anchor && ax.anchor !== 'free') {
-            ax.anchor = Axes.cleanId(ax.anchor);
-        }
-        if(ax.overlaying) ax.overlaying = Axes.cleanId(ax.overlaying);
+    var axisAttrRegex = (Plots.subplotsRegistry.cartesian || {}).attrRegex;
+    var sceneAttrRegex = (Plots.subplotsRegistry.gl3d || {}).attrRegex;
 
-        // old method of axis type - isdate and islog (before category existed)
-        if(!ax.type) {
-            if(ax.isdate) ax.type = 'date';
-            else if(ax.islog) ax.type = 'log';
-            else if(ax.isdate === false && ax.islog === false) ax.type = 'linear';
-        }
-        if(ax.autorange === 'withzero' || ax.autorange === 'tozero') {
-            ax.autorange = true;
-            ax.rangemode = 'tozero';
+    var keys = Object.keys(layout);
+    for(i = 0; i < keys.length; i++) {
+        var key = keys[i];
+
+        // modifications to cartesian axes
+        if(axisAttrRegex && axisAttrRegex.test(key)) {
+            var ax = layout[key];
+            if(ax.anchor && ax.anchor !== 'free') {
+                ax.anchor = cleanId(ax.anchor);
+            }
+            if(ax.overlaying) ax.overlaying = cleanId(ax.overlaying);
+
+            // old method of axis type - isdate and islog (before category existed)
+            if(!ax.type) {
+                if(ax.isdate) ax.type = 'date';
+                else if(ax.islog) ax.type = 'log';
+                else if(ax.isdate === false && ax.islog === false) ax.type = 'linear';
+            }
+            if(ax.autorange === 'withzero' || ax.autorange === 'tozero') {
+                ax.autorange = true;
+                ax.rangemode = 'tozero';
+            }
+            delete ax.islog;
+            delete ax.isdate;
+            delete ax.categories; // replaced by _categories
+
+            // prune empty domain arrays made before the new nestedProperty
+            if(emptyContainer(ax, 'domain')) delete ax.domain;
+
+            // autotick -> tickmode
+            if(ax.autotick !== undefined) {
+                if(ax.tickmode === undefined) {
+                    ax.tickmode = ax.autotick ? 'auto' : 'linear';
+                }
+                delete ax.autotick;
+            }
         }
-        delete ax.islog;
-        delete ax.isdate;
-        delete ax.categories; // replaced by _categories
 
-        // prune empty domain arrays made before the new nestedProperty
-        if(emptyContainer(ax, 'domain')) delete ax.domain;
+        // modifications for 3D scenes
+        else if(sceneAttrRegex && sceneAttrRegex.test(key)) {
+            var scene = layout[key];
+
+            // clean old Camera coords
+            var cameraposition = scene.cameraposition;
+
+            if(Array.isArray(cameraposition) && cameraposition[0].length === 4) {
+                var rotation = cameraposition[0],
+                    center = cameraposition[1],
+                    radius = cameraposition[2],
+                    mat = m4FromQuat([], rotation),
+                    eye = [];
 
-        // autotick -> tickmode
-        if(ax.autotick !== undefined) {
-            if(ax.tickmode === undefined) {
-                ax.tickmode = ax.autotick ? 'auto' : 'linear';
+                for(j = 0; j < 3; ++j) {
+                    eye[j] = center[j] + radius * mat[2 + 4 * j];
+                }
+
+                scene.camera = {
+                    eye: {x: eye[0], y: eye[1], z: eye[2]},
+                    center: {x: center[0], y: center[1], z: center[2]},
+                    up: {x: mat[1], y: mat[5], z: mat[9]}
+                };
+
+                delete scene.cameraposition;
             }
-            delete ax.autotick;
         }
     }
 
@@ -139,43 +181,6 @@ exports.cleanLayout = function(layout) {
      */
     if(layout.dragmode === 'rotate') layout.dragmode = 'orbit';
 
-    // cannot have scene1, numbering goes scene, scene2, scene3...
-    if(layout.scene1) {
-        if(!layout.scene) layout.scene = layout.scene1;
-        delete layout.scene1;
-    }
-
-    /*
-     * Clean up Scene layouts
-     */
-    var sceneIds = Plots.getSubplotIds(layout, 'gl3d');
-    for(i = 0; i < sceneIds.length; i++) {
-        var scene = layout[sceneIds[i]];
-
-        // clean old Camera coords
-        var cameraposition = scene.cameraposition;
-
-        if(Array.isArray(cameraposition) && cameraposition[0].length === 4) {
-            var rotation = cameraposition[0],
-                center = cameraposition[1],
-                radius = cameraposition[2],
-                mat = m4FromQuat([], rotation),
-                eye = [];
-
-            for(j = 0; j < 3; ++j) {
-                eye[j] = center[i] + radius * mat[2 + 4 * j];
-            }
-
-            scene.camera = {
-                eye: {x: eye[0], y: eye[1], z: eye[2]},
-                center: {x: center[0], y: center[1], z: center[2]},
-                up: {x: mat[1], y: mat[5], z: mat[9]}
-            };
-
-            delete scene.cameraposition;
-        }
-    }
-
     // sanitize rgb(fractions) and rgba(fractions) that old tinycolor
     // supported, but new tinycolor does not because they're not valid css
     Color.clean(layout);
@@ -187,7 +192,7 @@ function cleanAxRef(container, attr) {
     var valIn = container[attr],
         axLetter = attr.charAt(0);
     if(valIn && valIn !== 'paper') {
-        container[attr] = Axes.cleanId(valIn, axLetter);
+        container[attr] = cleanId(valIn, axLetter);
     }
 }
 
@@ -268,8 +273,8 @@ exports.cleanData = function(data, existingData) {
         }
 
         // axis ids x1 -> x, y1-> y
-        if(trace.xaxis) trace.xaxis = Axes.cleanId(trace.xaxis, 'x');
-        if(trace.yaxis) trace.yaxis = Axes.cleanId(trace.yaxis, 'y');
+        if(trace.xaxis) trace.xaxis = cleanId(trace.xaxis, 'x');
+        if(trace.yaxis) trace.yaxis = cleanId(trace.yaxis, 'y');
 
         // scene ids scene1 -> scene
         if(Registry.traceIs(trace, 'gl3d') && trace.scene) {
@@ -529,7 +534,7 @@ exports.clearAxisTypes = function(gd, traces, layoutUpdate) {
     for(var i = 0; i < traces.length; i++) {
         var trace = gd._fullData[i];
         for(var j = 0; j < 3; j++) {
-            var ax = Axes.getFromTrace(gd, trace, axLetters[j]);
+            var ax = getFromTrace(gd, trace, axLetters[j]);
 
             // do not clear log type - that's never an auto result so must have been intentional
             if(ax && ax.type !== 'log') {
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index 22016271911..c6e9802cf46 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -276,7 +276,7 @@ Plotly.plot = function(gd, data, layout, config) {
             return;
         }
 
-        var subplots = Plots.getSubplotIds(fullLayout, 'cartesian');
+        var subplots = fullLayout._subplots.cartesian;
         var modules = fullLayout._modules;
         var setPositionsArray = [];
 
@@ -2052,7 +2052,7 @@ function _relayout(gd, aobj) {
     }
 
     // figure out if we need to recalculate axis constraints
-    var constraints = fullLayout._axisConstraintGroups;
+    var constraints = fullLayout._axisConstraintGroups || [];
     for(axId in rangesAltered) {
         for(i = 0; i < constraints.length; i++) {
             var group = constraints[i];
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index 78fae75a679..539a96cf152 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -21,6 +21,7 @@ var Titles = require('../components/titles');
 var ModeBar = require('../components/modebar');
 var initInteractions = require('../plots/cartesian/graph_interact');
 var cartesianConstants = require('../plots/cartesian/constants');
+var alignmentConstants = require('../constants/alignment');
 
 exports.layoutStyles = function(gd) {
     return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd);
@@ -53,8 +54,44 @@ exports.lsInner = function(gd) {
     var hasSVGCartesian = fullLayout._has('cartesian');
     var i;
 
-    // clear axis line positions, to be set in the subplot loop below
-    for(i = 0; i < axList.length; i++) axList[i]._linepositions = {};
+    function getLinePosition(ax, counterAx, side) {
+        var lwHalf = ax._lw / 2;
+
+        if(ax._id.charAt(0) === 'x') {
+            if(!counterAx) return gs.t + gs.h * (1 - (ax.position || 0)) + (lwHalf % 1);
+            else if(side === 'top') return counterAx._offset - pad - lwHalf;
+            return counterAx._offset + counterAx._length + pad + lwHalf;
+        }
+
+        if(!counterAx) return gs.l + gs.w * (ax.position || 0) + (lwHalf % 1);
+        else if(side === 'right') return counterAx._offset + counterAx._length + pad + lwHalf;
+        return counterAx._offset - pad - lwHalf;
+    }
+
+    // some preparation of axis position info
+    for(i = 0; i < axList.length; i++) {
+        var ax = axList[i];
+
+        // reset scale in case the margins have changed
+        ax.setScale();
+
+        var counterAx = ax._anchorAxis;
+
+        // clear axis line positions, to be set in the subplot loop below
+        ax._linepositions = {};
+
+        // stash crispRounded linewidth so we don't need to pass gd all over the place
+        ax._lw = Drawing.crispRound(gd, ax.linewidth, 1);
+
+        // figure out the main axis line and main mirror line position.
+        // it's easier to follow the logic if we handle these separately from
+        // ax._linepositions, which are really only used by mirror=allticks
+        // for the non-main-subplot ticks.
+        ax._mainLinePosition = getLinePosition(ax, counterAx, ax.side);
+        ax._mainMirrorPosition = (ax.mirror && counterAx) ?
+            getLinePosition(ax, counterAx,
+                alignmentConstants.OPPOSITE_SIDE[ax.side]) : null;
+    }
 
     fullLayout._paperdiv
         .style({
@@ -129,16 +166,11 @@ exports.lsInner = function(gd) {
         fullLayout._plots[subplot].bg = d3.select(this);
     });
 
-    var freeFinished = {};
     subplotSelection.each(function(subplot) {
         var plotinfo = fullLayout._plots[subplot];
         var xa = plotinfo.xaxis;
         var ya = plotinfo.yaxis;
 
-        // reset scale in case the margins have changed
-        xa.setScale();
-        ya.setScale();
-
         if(plotinfo.bg && hasSVGCartesian) {
             plotinfo.bg
                 .call(Drawing.setRect,
@@ -193,18 +225,38 @@ exports.lsInner = function(gd) {
         // to DRY up Drawing.setClipUrl calls downstream
         plotinfo.layerClipId = layerClipId;
 
-        var xIsFree = !xa._anchorAxis;
-        var showFreeX = xIsFree && !freeFinished[xa._id];
-        var showBottom = shouldShowLine(xa, ya, 'bottom');
-        var showTop = shouldShowLine(xa, ya, 'top');
+        // figure out extra axis line and tick positions as needed
+        if(!hasSVGCartesian) return;
 
-        var yIsFree = !ya._anchorAxis;
-        var showFreeY = yIsFree && !freeFinished[ya._id];
-        var showLeft = shouldShowLine(ya, xa, 'left');
-        var showRight = shouldShowLine(ya, xa, 'right');
+        var xLinesXLeft, xLinesXRight, xLinesYBottom, xLinesYTop,
+            leftYLineWidth, rightYLineWidth;
+        var yLinesYBottom, yLinesYTop, yLinesXLeft, yLinesXRight,
+            connectYBottom, connectYTop;
+        var extraSubplot;
 
-        var xlw = Drawing.crispRound(gd, xa.linewidth, 1);
-        var ylw = Drawing.crispRound(gd, ya.linewidth, 1);
+        function xLinePath(y) {
+            return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight;
+        }
+
+        function xLinePathFree(y) {
+            return 'M' + xa._offset + ',' + y + 'h' + xa._length;
+        }
+
+        function yLinePath(x) {
+            return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom;
+        }
+
+        function yLinePathFree(x) {
+            return 'M' + x + ',' + ya._offset + 'v' + ya._length;
+        }
+
+        function mainPath(ax, pathFn, pathFnFree) {
+            if(!ax.showline || subplot !== ax._mainSubplot) return '';
+            if(!ax._anchorAxis) return pathFnFree(ax._mainLinePosition);
+            var out = pathFn(ax._mainLinePosition);
+            if(ax.mirror) out += pathFn(ax._mainMirrorPosition);
+            return out;
+        }
 
         /*
          * x lines get longer where they meet y lines, to make a crisp corner.
@@ -220,15 +272,33 @@ exports.lsInner = function(gd) {
          *    -----
          *     x2
          */
-        var leftYLineWidth = findCounterAxisLineWidth(gd, xa, ylw, showLeft, 'left', axList);
-        var xLinesXLeft = (!xIsFree && leftYLineWidth) ?
-            (-pad - leftYLineWidth) : 0;
-        var rightYLineWidth = findCounterAxisLineWidth(gd, xa, ylw, showRight, 'right', axList);
-        var xLinesXRight = xa._length + ((!xIsFree && rightYLineWidth) ?
-            (pad + rightYLineWidth) : 0);
-        var xLinesYFree = gs.h * (1 - (xa.position || 0)) + ((xlw / 2) % 1);
-        var xLinesYBottom = ya._length + pad + xlw / 2;
-        var xLinesYTop = -pad - xlw / 2;
+        if(shouldShowLinesOrTicks(xa, subplot)) {
+            leftYLineWidth = findCounterAxisLineWidth(xa, 'left', ya, axList);
+            xLinesXLeft = xa._offset - (leftYLineWidth ? (pad + leftYLineWidth) : 0);
+            rightYLineWidth = findCounterAxisLineWidth(xa, 'right', ya, axList);
+            xLinesXRight = xa._offset + xa._length + (rightYLineWidth ? (pad + rightYLineWidth) : 0);
+            xLinesYBottom = getLinePosition(xa, ya, 'bottom');
+            xLinesYTop = getLinePosition(xa, ya, 'top');
+
+            // save axis line positions for extra ticks to reference
+            // each subplot that gets ticks from "allticks" gets an entry:
+            //    [left or bottom, right or top]
+            extraSubplot = (!xa._anchorAxis || subplot !== xa._mainSubplot);
+            if(extraSubplot && xa.ticks && xa.mirror === 'allticks') {
+                xa._linepositions[subplot] = [xLinesYBottom, xLinesYTop];
+            }
+
+            var xPath = mainPath(xa, xLinePath, xLinePathFree);
+            if(extraSubplot && xa.showline && (xa.mirror === 'all' || xa.mirror === 'allticks')) {
+                xPath += xLinePath(xLinesYBottom) + xLinePath(xLinesYTop);
+            }
+
+            plotinfo.xlines
+                .attr('d', xPath || 'M0,0')
+                .style('stroke-width', xa._lw + 'px')
+                .call(Color.stroke, xa.showline ?
+                    xa.linecolor : 'rgba(0,0,0,0)');
+        }
 
         /*
          * y lines that meet x axes get longer only by margin.pad, because
@@ -241,105 +311,30 @@ exports.lsInner = function(gd) {
          *       |
          *       +-----
          */
-        var connectYBottom = !yIsFree && findCounterAxisLineWidth(
-                gd, ya, xlw, showBottom, 'bottom', axList);
-        var yLinesYBottom = ya._length + (connectYBottom ? pad : 0);
-        var connectYTop = !yIsFree && findCounterAxisLineWidth(
-                gd, ya, xlw, showTop, 'top', axList);
-        var yLinesYTop = connectYTop ? -pad : 0;
-        var yLinesXFree = gs.w * (ya.position || 0) + ((ylw / 2) % 1);
-        var yLinesXLeft = -pad - ylw / 2;
-        var yLinesXRight = xa._length + pad + ylw / 2;
-
-        function xLinePath(y, showThis) {
-            if(!showThis) return '';
-            return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight;
-        }
-
-        function yLinePath(x, showThis) {
-            if(!showThis) return '';
-            return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom;
-        }
-
-        // save axis line positions for ticks, draggers, etc to reference
-        // each subplot gets an entry:
-        //    [left or bottom, right or top, free, main]
-        // main is the position at which to draw labels and draggers, if any
-        xa._linepositions[subplot] = [
-            showBottom ? xLinesYBottom : undefined,
-            showTop ? xLinesYTop : undefined,
-            showFreeX ? xLinesYFree : undefined
-        ];
-        if(xa._anchorAxis === ya) {
-            xa._linepositions[subplot][3] = xa.side === 'top' ?
-                xLinesYTop : xLinesYBottom;
-        }
-        else if(showFreeX) {
-            xa._linepositions[subplot][3] = xLinesYFree;
-        }
-
-        ya._linepositions[subplot] = [
-            showLeft ? yLinesXLeft : undefined,
-            showRight ? yLinesXRight : undefined,
-            showFreeY ? yLinesXFree : undefined
-        ];
-        if(ya._anchorAxis === xa) {
-            ya._linepositions[subplot][3] = ya.side === 'right' ?
-                yLinesXRight : yLinesXLeft;
-        }
-        else if(showFreeY) {
-            ya._linepositions[subplot][3] = yLinesXFree;
-        }
+        if(shouldShowLinesOrTicks(ya, subplot)) {
+            connectYBottom = findCounterAxisLineWidth(ya, 'bottom', xa, axList);
+            yLinesYBottom = ya._offset + ya._length + (connectYBottom ? pad : 0);
+            connectYTop = findCounterAxisLineWidth(ya, 'top', xa, axList);
+            yLinesYTop = ya._offset - (connectYTop ? pad : 0);
+            yLinesXLeft = getLinePosition(ya, xa, 'left');
+            yLinesXRight = getLinePosition(ya, xa, 'right');
+
+            extraSubplot = (!ya._anchorAxis || subplot !== xa._mainSubplot);
+            if(extraSubplot && ya.ticks && ya.mirror === 'allticks') {
+                ya._linepositions[subplot] = [yLinesXLeft, yLinesXRight];
+            }
 
-        // translate all the extra stuff to have the
-        // same origin as the plot area or axes
-        var origin = 'translate(' + xa._offset + ',' + ya._offset + ')';
-        var originX = origin;
-        var originY = origin;
-        if(showFreeX) {
-            originX = 'translate(' + xa._offset + ',' + gs.t + ')';
-            xLinesYTop += ya._offset - gs.t;
-            xLinesYBottom += ya._offset - gs.t;
-        }
-        if(showFreeY) {
-            originY = 'translate(' + gs.l + ',' + ya._offset + ')';
-            yLinesXLeft += xa._offset - gs.l;
-            yLinesXRight += xa._offset - gs.l;
-        }
+            var yPath = mainPath(ya, yLinePath, yLinePathFree);
+            if(extraSubplot && ya.showline && (ya.mirror === 'all' || ya.mirror === 'allticks')) {
+                yPath += yLinePath(yLinesXLeft) + yLinePath(yLinesXRight);
+            }
 
-        if(hasSVGCartesian) {
-            plotinfo.xlines
-                .attr('transform', originX)
-                .attr('d', (
-                    xLinePath(xLinesYBottom, showBottom) +
-                    xLinePath(xLinesYTop, showTop) +
-                    xLinePath(xLinesYFree, showFreeX)) ||
-                    // so it doesn't barf with no lines shown
-                    'M0,0')
-                .style('stroke-width', xlw + 'px')
-                .call(Color.stroke, xa.showline ?
-                    xa.linecolor : 'rgba(0,0,0,0)');
             plotinfo.ylines
-                .attr('transform', originY)
-                .attr('d', (
-                    yLinePath(yLinesXLeft, showLeft) +
-                    yLinePath(yLinesXRight, showRight) +
-                    yLinePath(yLinesXFree, showFreeY)) ||
-                    'M0,0')
-                .style('stroke-width', ylw + 'px')
+                .attr('d', yPath || 'M0,0')
+                .style('stroke-width', ya._lw + 'px')
                 .call(Color.stroke, ya.showline ?
                     ya.linecolor : 'rgba(0,0,0,0)');
         }
-
-        plotinfo.xaxislayer.attr('transform', originX);
-        plotinfo.yaxislayer.attr('transform', originY);
-        plotinfo.gridlayer.attr('transform', origin);
-        plotinfo.zerolinelayer.attr('transform', origin);
-        plotinfo.draglayer.attr('transform', origin);
-
-        // mark free axes as displayed, so we don't draw them again
-        if(showFreeX) freeFinished[xa._id] = 1;
-        if(showFreeY) freeFinished[ya._id] = 1;
     });
 
     Plotly.Axes.makeClipPaths(gd);
@@ -349,60 +344,52 @@ exports.lsInner = function(gd) {
     return gd._promises.length && Promise.all(gd._promises);
 };
 
-function shouldShowLine(ax, counterAx, side) {
-    return (ax._anchorAxis === counterAx && (ax.mirror || ax.side === side)) ||
-        ax.mirror === 'all' || ax.mirror === 'allticks' ||
-        (ax.mirrors && ax.mirrors[counterAx._id + side]);
+function shouldShowLinesOrTicks(ax, subplot) {
+    return (ax.ticks || ax.showline) &&
+        (subplot === ax._mainSubplot || ax.mirror === 'all' || ax.mirror === 'allticks');
 }
 
-function findCounterAxes(gd, ax, axList) {
-    var counterAxes = [];
-    var anchorAx = ax._anchorAxis;
-    if(anchorAx) {
-        var counterMain = anchorAx._mainAxis;
-        if(counterAxes.indexOf(counterMain) === -1) {
-            counterAxes.push(counterMain);
-            for(var i = 0; i < axList.length; i++) {
-                if(axList[i].overlaying === counterMain._id &&
-                    counterAxes.indexOf(axList[i]) === -1
-                ) {
-                    counterAxes.push(axList[i]);
-                }
-            }
-        }
+/*
+ * should we draw a line on counterAx at this side of ax?
+ * It's assumed that counterAx is known to overlay the subplot we're working on
+ * but it may not be its main axis.
+ */
+function shouldShowLineThisSide(ax, side, counterAx) {
+    // does counterAx get a line at all?
+    if(!counterAx.showline || !counterAx._lw) return false;
+
+    // are we drawing *all* lines for counterAx?
+    if(counterAx.mirror === 'all' || counterAx.mirror === 'allticks') return true;
+
+    var anchorAx = counterAx._anchorAxis;
+
+    // is this a free axis? free axes can only have a subplot side-line with all(ticks)? mirroring
+    if(!anchorAx) return false;
+
+    // in order to handle cases where the user forgot to anchor this axis correctly
+    // (because its default anchor has the same domain on the relevant end)
+    // check whether the relevant position is the same.
+    var sideIndex = alignmentConstants.FROM_BL[side];
+    if(counterAx.side === side) {
+        return anchorAx.domain[sideIndex] === ax.domain[sideIndex];
     }
-    return counterAxes;
+    return counterAx.mirror && anchorAx.domain[1 - sideIndex] === ax.domain[1 - sideIndex];
 }
 
-function findLineWidth(gd, axes, side) {
-    for(var i = 0; i < axes.length; i++) {
-        var ax = axes[i];
-        var anchorAx = ax._anchorAxis;
-        if(anchorAx && shouldShowLine(ax, anchorAx, side)) {
-            return Drawing.crispRound(gd, ax.linewidth);
-        }
+/*
+ * Is there another axis intersecting `side` end of `ax`?
+ * First look at `counterAx` (the axis for this subplot),
+ * then at all other potential counteraxes on or overlaying this subplot.
+ * Take the line width from the first one that has a line.
+ */
+function findCounterAxisLineWidth(ax, side, counterAx, axList) {
+    if(shouldShowLineThisSide(ax, side, counterAx)) {
+        return counterAx._lw;
     }
-}
-
-function findCounterAxisLineWidth(gd, ax, subplotCounterLineWidth,
-        subplotCounterIsShown, side, axList) {
-    if(subplotCounterIsShown) return subplotCounterLineWidth;
-
-    var i;
-
-    // find all counteraxes for this one, then of these, find the
-    // first one that has a visible line on this side
-    var mainAxis = ax._mainAxis;
-    var counterAxes = findCounterAxes(gd, mainAxis, axList);
-
-    var lineWidth = findLineWidth(gd, counterAxes, side);
-    if(lineWidth) return lineWidth;
-
-    for(i = 0; i < axList.length; i++) {
-        if(axList[i].overlaying === mainAxis._id) {
-            counterAxes = findCounterAxes(gd, axList[i], axList);
-            lineWidth = findLineWidth(gd, counterAxes, side);
-            if(lineWidth) return lineWidth;
+    for(var i = 0; i < axList.length; i++) {
+        var axi = axList[i];
+        if(axi._mainAxis === counterAx._mainAxis && shouldShowLineThisSide(ax, side, axi)) {
+            return axi._lw;
         }
     }
     return 0;
@@ -503,12 +490,12 @@ exports.doModeBar = function(gd) {
 };
 
 exports.doCamera = function(gd) {
-    var fullLayout = gd._fullLayout,
-        sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d');
+    var fullLayout = gd._fullLayout;
+    var sceneIds = fullLayout._subplots.gl3d;
 
     for(var i = 0; i < sceneIds.length; i++) {
-        var sceneLayout = fullLayout[sceneIds[i]],
-            scene = sceneLayout._scene;
+        var sceneLayout = fullLayout[sceneIds[i]];
+        var scene = sceneLayout._scene;
 
         scene.setCamera(sceneLayout.camera);
     }
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index 957762ec52b..135e81f72a4 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -34,9 +34,6 @@ var MID_SHIFT = require('../../constants/alignment').MID_SHIFT;
 
 var axes = module.exports = {};
 
-axes.layoutAttributes = require('./layout_attributes');
-axes.supplyLayoutDefaults = require('./layout_defaults');
-
 axes.setConvert = require('./set_convert');
 var autoType = require('./axis_autotype');
 
@@ -60,10 +57,10 @@ axes.getFromTrace = axisIds.getFromTrace;
  *     Only required if it's different from `dflt`
  */
 axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption) {
-    var axLetter = attr.charAt(attr.length - 1),
-        axlist = axes.listIds(gd, axLetter),
-        refAttr = attr + 'ref',
-        attrDef = {};
+    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] || extraOption;
     if(!extraOption) extraOption = dflt;
@@ -1601,95 +1598,25 @@ axes.getTickFormat = function(ax) {
     return tickstop ? tickstop.value : ax.tickformat;
 };
 
-axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/;
-
-// getSubplots - extract all combinations of axes we need to make plots for
+// getSubplots - extract all subplot IDs we need
 // as an array of items like 'xy', 'x2y', 'x2y2'...
 // sorted by x (x,x2,x3...) then y
 // optionally restrict to only subplots containing axis object ax
-// looks both for combinations of x and y found in the data
-// and at axes and their anchors
 axes.getSubplots = function(gd, ax) {
-    var subplots = [];
-    var i, j, sp;
-
-    // look for subplots in the data
-    var data = gd._fullData || gd.data || [];
-
-    for(i = 0; i < data.length; i++) {
-        var trace = data[i];
-
-        if(trace.visible === false || trace.visible === 'legendonly' ||
-            !(Registry.traceIs(trace, 'cartesian') || Registry.traceIs(trace, 'gl2d'))
-        ) continue;
-
-        var xId = trace.xaxis || 'x',
-            yId = trace.yaxis || 'y';
-        sp = xId + yId;
-
-        if(subplots.indexOf(sp) === -1) subplots.push(sp);
-    }
-
-    // look for subplots in the axes/anchors, so that we at least draw all axes
-    var axesList = axes.list(gd, '', true);
-
-    function hasAx2(sp, ax2) {
-        return sp.indexOf(ax2._id) !== -1;
-    }
-
-    for(i = 0; i < axesList.length; i++) {
-        var ax2 = axesList[i],
-            ax2Letter = ax2._id.charAt(0),
-            ax3Id = (ax2.anchor === 'free') ?
-                ((ax2Letter === 'x') ? 'y' : 'x') :
-                ax2.anchor,
-            ax3 = axes.getFromId(gd, ax3Id);
+    var subplotObj = gd._fullLayout._subplots;
+    var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []);
 
-        // look if ax2 is already represented in the data
-        var foundAx2 = false;
-        for(j = 0; j < subplots.length; j++) {
-            if(hasAx2(subplots[j], ax2)) {
-                foundAx2 = true;
-                break;
-            }
-        }
-
-        // ignore free axes that already represented in the data
-        if(ax2.anchor === 'free' && foundAx2) continue;
-
-        // ignore anchor-less axes
-        if(!ax3) continue;
-
-        sp = (ax2Letter === 'x') ?
-            ax2._id + ax3._id :
-            ax3._id + ax2._id;
-
-        if(subplots.indexOf(sp) === -1) subplots.push(sp);
-    }
-
-    // filter invalid subplots
-    var spMatch = axes.subplotMatch,
-        allSubplots = [];
-
-    for(i = 0; i < subplots.length; i++) {
-        sp = subplots[i];
-        if(spMatch.test(sp)) allSubplots.push(sp);
-    }
+    var out = ax ? axes.findSubplotsWithAxis(allSubplots, ax) : allSubplots;
 
-    // sort the subplot ids
-    allSubplots.sort(function(a, b) {
-        var aMatch = a.match(spMatch),
-            bMatch = b.match(spMatch);
+    out.sort(function(a, b) {
+        var aParts = a.substr(1).split('y');
+        var bParts = b.substr(1).split('y');
 
-        if(aMatch[1] === bMatch[1]) {
-            return +(aMatch[2] || 1) - (bMatch[2] || 1);
-        }
-
-        return +(aMatch[1]||0) - (bMatch[1]||0);
+        if(aParts[0] === bParts[0]) return +aParts[1] - +bParts[1];
+        return +aParts[0] - +bParts[0];
     });
 
-    if(ax) return axes.findSubplotsWithAxis(allSubplots, ax);
-    return allSubplots;
+    return out;
 };
 
 // find all subplots with axis 'ax'
@@ -1847,7 +1774,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
     if(axLetter === 'x') {
         sides = ['bottom', 'top'];
         transfn = function(d) {
-            return 'translate(' + ax.l2p(d.x) + ',0)';
+            return 'translate(' + (ax._offset + ax.l2p(d.x)) + ',0)';
         };
         tickpathfn = function(shift, len) {
             if(ax._counterangle) {
@@ -1860,7 +1787,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
     else if(axLetter === 'y') {
         sides = ['left', 'right'];
         transfn = function(d) {
-            return 'translate(0,' + ax.l2p(d.x) + ')';
+            return 'translate(0,' + (ax._offset + ax.l2p(d.x)) + ')';
         };
         tickpathfn = function(shift, len) {
             if(ax._counterangle) {
@@ -2269,13 +2196,15 @@ axes.doTicks = function(gd, axid, skipTitle) {
     }
 
     function drawGrid(plotinfo, counteraxis, subplot) {
-        var gridcontainer = plotinfo.gridlayer,
-            zlcontainer = plotinfo.zerolinelayer,
-            gridvals = plotinfo['hidegrid' + axLetter] ? [] : valsClipped,
-            gridpath = ax._gridpath ||
-                'M0,0' + ((axLetter === 'x') ? 'v' : 'h') + counteraxis._length,
-            grid = gridcontainer.selectAll('path.' + gcls)
-                .data((ax.showgrid === false) ? [] : gridvals, datafn);
+        var gridcontainer = plotinfo.gridlayer.selectAll('.' + axid);
+        var zlcontainer = plotinfo.zerolinelayer;
+        var gridvals = plotinfo['hidegrid' + axLetter] ? [] : valsClipped;
+        var gridpath = ax._gridpath || ((axLetter === 'x' ?
+                ('M0,' + counteraxis._offset + 'v') :
+                ('M' + counteraxis._offset + ',0h')
+            ) + counteraxis._length);
+        var grid = gridcontainer.selectAll('path.' + gcls)
+            .data((ax.showgrid === false) ? [] : gridvals, datafn);
         grid.enter().append('path').classed(gcls, 1)
             .classed('crisp', 1)
             .attr('d', gridpath)
@@ -2304,10 +2233,18 @@ axes.doTicks = function(gd, axid, skipTitle) {
                 (ax.type === 'linear' || ax.type === '-') && gridvals.length &&
                 (hasBarsOrFill || clipEnds({x: 0}) || !ax.showline);
             var zl = zlcontainer.selectAll('path.' + zcls)
-                .data(showZl ? [{x: 0}] : []);
+                .data(showZl ? [{x: 0, id: axid}] : []);
             zl.enter().append('path').classed(zcls, 1).classed('zl', 1)
                 .classed('crisp', 1)
-                .attr('d', gridpath);
+                .attr('d', gridpath)
+                .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.
+                    zlcontainer.selectAll('path').sort(function(da, db) {
+                        return axisIds.idSort(da.id, db.id);
+                    });
+                });
             zl.attr('transform', transfn)
                 .call(Color.stroke, ax.zerolinecolor || Color.defaultLine)
                 .style('stroke-width', zeroLineWidth + 'px');
@@ -2326,54 +2263,59 @@ axes.doTicks = function(gd, axid, skipTitle) {
         }
         return drawLabels(ax._axislayer, ax._pos);
     }
-    else {
+    else if(fullLayout._has('cartesian')) {
         subplots = axes.getSubplots(gd, ax);
-        var alldone = subplots.map(function(subplot) {
+
+        // keep track of which subplots (by main conteraxis) we've already
+        // drawn grids for, so we don't overdraw overlaying subplots
+        var finishedGrids = {};
+
+        subplots.map(function(subplot) {
             var plotinfo = fullLayout._plots[subplot];
+            var counterAxis = plotinfo[counterLetter + 'axis'];
 
-            if(!fullLayout._has('cartesian')) return;
+            var mainCounterID = counterAxis._mainAxis._id;
+            if(finishedGrids[mainCounterID]) return;
+            finishedGrids[mainCounterID] = 1;
 
-            var container = plotinfo[axLetter + 'axislayer'],
+            drawGrid(plotinfo, counterAxis, subplot);
+        });
 
-                // [bottom or left, top or right, free, main]
-                linepositions = ax._linepositions[subplot] || [],
-                counteraxis = plotinfo[counterLetter + 'axis'],
-                mainSubplot = counteraxis._id === ax.anchor,
-                ticksides = [false, false, false],
-                tickpath = '';
+        var mainSubplot = ax._mainSubplot;
+        var mainPlotinfo = fullLayout._plots[mainSubplot];
+        var tickSubplots = [];
 
-            // ticks
-            if(ax.mirror === 'allticks') ticksides = [true, true, false];
-            else if(mainSubplot) {
-                if(ax.mirror === 'ticks') ticksides = [true, true, false];
-                else ticksides[sides.indexOf(axside)] = true;
-            }
-            if(ax.mirrors) {
-                for(i = 0; i < 2; i++) {
-                    var thisMirror = ax.mirrors[counteraxis._id + sides[i]];
-                    if(thisMirror === 'ticks' || thisMirror === 'labels') {
-                        ticksides[i] = true;
-                    }
-                }
+        if(ax.ticks) {
+            var mainSign = ticksign[2];
+            var tickpath = tickpathfn(ax._mainLinePosition + pad * mainSign, mainSign * ax.ticklen);
+            if(ax._anchorAxis && ax.mirror && ax.mirror !== true) {
+                tickpath += tickpathfn(ax._mainMirrorPosition - pad * mainSign, -mainSign * ax.ticklen);
             }
+            drawTicks(mainPlotinfo[axLetter + 'axislayer'], tickpath);
 
-            // free axis ticks
-            if(linepositions[2] !== undefined) ticksides[2] = true;
+            tickSubplots = Object.keys(ax._linepositions);
+        }
 
-            ticksides.forEach(function(showside, sidei) {
-                var pos = linepositions[sidei],
-                    tsign = ticksign[sidei];
-                if(showside && isNumeric(pos)) {
-                    tickpath += tickpathfn(pos + pad * tsign, tsign * ax.ticklen);
-                }
-            });
+        tickSubplots.map(function(subplot) {
+            var plotinfo = fullLayout._plots[subplot];
+
+            var container = plotinfo[axLetter + 'axislayer'];
+
+            // [bottom or left, top or right]
+            // free and main are handled above
+            var linepositions = ax._linepositions[subplot] || [];
+
+            function tickPathSide(sidei) {
+                var tsign = ticksign[sidei];
+                return tickpathfn(linepositions[sidei] + pad * tsign, tsign * ax.ticklen);
+            }
+
+            drawTicks(container, tickPathSide(0) + tickPathSide(1));
+        });
 
-            drawTicks(container, tickpath);
-            drawGrid(plotinfo, counteraxis, subplot);
-            return drawLabels(container, linepositions[3]);
-        }).filter(function(onedone) { return onedone && onedone.then; });
+        var mainContainer = mainPlotinfo[axLetter + 'axislayer'];
 
-        return alldone.length ? Promise.all(alldone) : 0;
+        return drawLabels(mainContainer, ax._mainLinePosition);
     }
 };
 
diff --git a/src/plots/cartesian/axis_ids.js b/src/plots/cartesian/axis_ids.js
index 63b9fae6e77..e53306695f9 100644
--- a/src/plots/cartesian/axis_ids.js
+++ b/src/plots/cartesian/axis_ids.js
@@ -9,8 +9,6 @@
 'use strict';
 
 var Registry = require('../../registry');
-var Plots = require('../plots');
-var Lib = require('../../lib');
 
 var constants = require('./constants');
 
@@ -41,53 +39,43 @@ exports.cleanId = function cleanId(id, axLetter) {
     return id.charAt(0) + axNum;
 };
 
-// get all axis object names
-// optionally restricted to only x or y or z by string axLetter
-// and optionally 2D axes only, not those inside 3D scenes
-function listNames(gd, axLetter, only2d) {
+// get all axis objects, as restricted in listNames
+exports.list = function(gd, axLetter, only2d) {
     var fullLayout = gd._fullLayout;
     if(!fullLayout) return [];
 
-    function filterAxis(obj, extra) {
-        var keys = Object.keys(obj),
-            axMatch = /^[xyz]axis[0-9]*/,
-            out = [];
-
-        for(var i = 0; i < keys.length; i++) {
-            var k = keys[i];
-            if(axLetter && k.charAt(0) !== axLetter) continue;
-            if(axMatch.test(k)) out.push(extra + k);
-        }
+    var idList = exports.listIds(gd, axLetter);
+    var out = new Array(idList.length);
+    var i;
 
-        return out.sort();
+    for(i = 0; i < idList.length; i++) {
+        var idi = idList[i];
+        out[i] = fullLayout[idi.charAt(0) + 'axis' + idi.substr(1)];
     }
 
-    var names = filterAxis(fullLayout, '');
-    if(only2d) return names;
+    if(!only2d) {
+        var sceneIds3D = fullLayout._subplots.gl3d || [];
 
-    var sceneIds3D = Plots.getSubplotIds(fullLayout, 'gl3d') || [];
-    for(var i = 0; i < sceneIds3D.length; i++) {
-        var sceneId = sceneIds3D[i];
-        names = names.concat(
-            filterAxis(fullLayout[sceneId], sceneId + '.')
-        );
-    }
+        for(i = 0; i < sceneIds3D.length; i++) {
+            var scene = fullLayout[sceneIds3D[i]];
 
-    return names;
-}
+            if(axLetter) out.push(scene[axLetter + 'axis']);
+            else out.push(scene.xaxis, scene.yaxis, scene.zaxis);
+        }
+    }
 
-// get all axis objects, as restricted in listNames
-exports.list = function(gd, axletter, only2d) {
-    return listNames(gd, axletter, only2d)
-        .map(function(axName) {
-            return Lib.nestedProperty(gd._fullLayout, axName).get();
-        });
+    return out;
 };
 
 // get all axis ids, optionally restricted by letter
 // this only makes sense for 2d axes
-exports.listIds = function(gd, axletter) {
-    return listNames(gd, axletter, true).map(exports.name2id);
+exports.listIds = function(gd, axLetter) {
+    var fullLayout = gd._fullLayout;
+    if(!fullLayout) return [];
+
+    var subplotLists = fullLayout._subplots;
+    if(axLetter) return subplotLists[axLetter + 'axis'];
+    return subplotLists.xaxis.concat(subplotLists.yaxis);
 };
 
 // get an axis object from its id 'x','x2' etc
@@ -118,3 +106,11 @@ exports.getFromTrace = function(gd, fullTrace, type) {
 
     return ax;
 };
+
+// sort x, x2, x10, y, y2, y10...
+exports.idSort = function(id1, id2) {
+    var letter1 = id1.charAt(0);
+    var letter2 = id2.charAt(0);
+    if(letter1 !== letter2) return letter1 > letter2 ? 1 : -1;
+    return +(id1.substr(1) || 1) - +(id2.substr(1) || 1);
+};
diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js
index 6edfe4fcb9f..c07d83d3199 100644
--- a/src/plots/cartesian/constants.js
+++ b/src/plots/cartesian/constants.js
@@ -29,6 +29,9 @@ module.exports = {
     AX_ID_PATTERN: /^[xyz][0-9]*$/,
     AX_NAME_PATTERN: /^[xyz]axis[0-9]*$/,
 
+    // and for 2D subplots
+    SUBPLOT_PATTERN: /^x([0-9]*)y([0-9]*)$/,
+
     // pixels to move mouse before you stop clamping to starting point
     MINDRAG: 8,
 
diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js
index ac5cca0b979..bbcef1e4a52 100644
--- a/src/plots/cartesian/constraints.js
+++ b/src/plots/cartesian/constraints.js
@@ -19,7 +19,7 @@ var FROM_BL = require('../../constants/alignment').FROM_BL;
 
 exports.enforce = function enforceAxisConstraints(gd) {
     var fullLayout = gd._fullLayout;
-    var constraintGroups = fullLayout._axisConstraintGroups;
+    var constraintGroups = fullLayout._axisConstraintGroups || [];
 
     var i, j, axisID, ax, normScale, mode, factor;
 
diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js
index 572fdc6c5e8..e76827ca92f 100644
--- a/src/plots/cartesian/graph_interact.js
+++ b/src/plots/cartesian/graph_interact.js
@@ -9,8 +9,6 @@
 
 'use strict';
 
-var isNumeric = require('fast-isnumeric');
-
 var Fx = require('../../components/fx');
 var dragElement = require('../../components/dragelement');
 
@@ -38,25 +36,17 @@ module.exports = function initInteractions(gd) {
     subplots.forEach(function(subplot) {
         var plotinfo = fullLayout._plots[subplot];
 
-        var xa = plotinfo.xaxis,
-            ya = plotinfo.yaxis,
-
-            // the y position of the main x axis line
-            y0 = (xa._linepositions[subplot] || [])[3],
-
-            // the x position of the main y axis line
-            x0 = (ya._linepositions[subplot] || [])[3];
+        var xa = plotinfo.xaxis;
+        var ya = plotinfo.yaxis;
 
         var DRAGGERSIZE = constants.DRAGGERSIZE;
-        if(isNumeric(y0) && xa.side === 'top') y0 -= DRAGGERSIZE;
-        if(isNumeric(x0) && ya.side !== 'right') x0 -= DRAGGERSIZE;
 
         // main and corner draggers need not be repeated for
         // overlaid subplots - these draggers drag them all
         if(!plotinfo.mainplot) {
             // main dragger goes over the grids and data, so we use its
             // mousemove events for all data hover effects
-            var maindrag = dragBox(gd, plotinfo, 0, 0,
+            var maindrag = dragBox(gd, plotinfo, xa._offset, ya._offset,
                 xa._length, ya._length, 'ns', 'ew');
 
             maindrag.onmousemove = function(evt) {
@@ -100,36 +90,40 @@ module.exports = function initInteractions(gd) {
 
             // corner draggers
             if(gd._context.showAxisDragHandles) {
-                dragBox(gd, plotinfo, -DRAGGERSIZE, -DRAGGERSIZE,
+                dragBox(gd, plotinfo, xa._offset - DRAGGERSIZE, ya._offset - DRAGGERSIZE,
                     DRAGGERSIZE, DRAGGERSIZE, 'n', 'w');
-                dragBox(gd, plotinfo, xa._length, -DRAGGERSIZE,
+                dragBox(gd, plotinfo, xa._offset + xa._length, ya._offset - DRAGGERSIZE,
                     DRAGGERSIZE, DRAGGERSIZE, 'n', 'e');
-                dragBox(gd, plotinfo, -DRAGGERSIZE, ya._length,
+                dragBox(gd, plotinfo, xa._offset - DRAGGERSIZE, ya._offset + ya._length,
                     DRAGGERSIZE, DRAGGERSIZE, 's', 'w');
-                dragBox(gd, plotinfo, xa._length, ya._length,
+                dragBox(gd, plotinfo, xa._offset + xa._length, ya._offset + ya._length,
                     DRAGGERSIZE, DRAGGERSIZE, 's', 'e');
             }
         }
         if(gd._context.showAxisDragHandles) {
             // x axis draggers - if you have overlaid plots,
             // these drag each axis separately
-            if(isNumeric(y0)) {
-                if(xa.anchor === 'free') y0 -= fullLayout._size.h * (1 - ya.domain[1]);
-                dragBox(gd, plotinfo, xa._length * 0.1, y0,
+            if(subplot === xa._mainSubplot) {
+                // the y position of the main x axis line
+                var y0 = xa._mainLinePosition;
+                if(xa.side === 'top') y0 -= DRAGGERSIZE;
+                dragBox(gd, plotinfo, xa._offset + xa._length * 0.1, y0,
                     xa._length * 0.8, DRAGGERSIZE, '', 'ew');
-                dragBox(gd, plotinfo, 0, y0,
+                dragBox(gd, plotinfo, xa._offset, y0,
                     xa._length * 0.1, DRAGGERSIZE, '', 'w');
-                dragBox(gd, plotinfo, xa._length * 0.9, y0,
+                dragBox(gd, plotinfo, xa._offset + xa._length * 0.9, y0,
                     xa._length * 0.1, DRAGGERSIZE, '', 'e');
             }
             // y axis draggers
-            if(isNumeric(x0)) {
-                if(ya.anchor === 'free') x0 -= fullLayout._size.w * xa.domain[0];
-                dragBox(gd, plotinfo, x0, ya._length * 0.1,
+            if(subplot === ya._mainSubplot) {
+                // the x position of the main y axis line
+                var x0 = ya._mainLinePosition;
+                if(ya.side !== 'right') x0 -= DRAGGERSIZE;
+                dragBox(gd, plotinfo, x0, ya._offset + ya._length * 0.1,
                     DRAGGERSIZE, ya._length * 0.8, 'ns', '');
-                dragBox(gd, plotinfo, x0, ya._length * 0.9,
+                dragBox(gd, plotinfo, x0, ya._offset + ya._length * 0.9,
                     DRAGGERSIZE, ya._length * 0.1, 's', '');
-                dragBox(gd, plotinfo, x0, 0,
+                dragBox(gd, plotinfo, x0, ya._offset,
                     DRAGGERSIZE, ya._length * 0.1, 'n', '');
             }
         }
diff --git a/src/plots/cartesian/include_components.js b/src/plots/cartesian/include_components.js
new file mode 100644
index 00000000000..3a2ca4097b5
--- /dev/null
+++ b/src/plots/cartesian/include_components.js
@@ -0,0 +1,73 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+var Registry = require('../../registry');
+var Lib = require('../../lib');
+
+/**
+ * Factory function for checking component arrays for subplot references.
+ *
+ * @param {string} containerArrayName: the top-level array in gd.layout to check
+ *   If an item in this container is found that references a cartesian x and/or y axis,
+ *   ensure cartesian is marked as a base plot module and record the axes (and subplot
+ *   if both refs are axes) in gd._fullLayout
+ *
+ * @return {function}: with args layoutIn (gd.layout) and layoutOut (gd._fullLayout)
+ * as expected of a component includeBasePlot method
+ */
+module.exports = function makeIncludeComponents(containerArrayName) {
+    return function includeComponents(layoutIn, layoutOut) {
+        var array = layoutIn[containerArrayName];
+        if(!Array.isArray(array)) return;
+
+        var Cartesian = Registry.subplotsRegistry.cartesian;
+        var idRegex = Cartesian.idRegex;
+        var subplots = layoutOut._subplots;
+        var xaList = subplots.xaxis;
+        var yaList = subplots.yaxis;
+        var cartesianList = subplots.cartesian;
+        var hasCartesianOrGL2D = layoutOut._has('cartesian') || layoutOut._has('gl2d');
+
+        for(var i = 0; i < array.length; i++) {
+            var itemi = array[i];
+            if(!Lib.isPlainObject(itemi)) continue;
+
+            var xref = itemi.xref;
+            var yref = itemi.yref;
+
+            var hasXref = idRegex.x.test(xref);
+            var hasYref = idRegex.y.test(yref);
+            if(hasXref || hasYref) {
+                if(!hasCartesianOrGL2D) Lib.pushUnique(layoutOut._basePlotModules, Cartesian);
+
+                var newAxis = false;
+                if(hasXref && xaList.indexOf(xref) === -1) {
+                    xaList.push(xref);
+                    newAxis = true;
+                }
+                if(hasYref && yaList.indexOf(yref) === -1) {
+                    yaList.push(yref);
+                    newAxis = true;
+                }
+
+                /*
+                 * Notice the logic here: only add a subplot for a component if
+                 * it's referencing both x and y axes AND it's creating a new axis
+                 * so for example if your plot already has xy and x2y2, an annotation
+                 * on x2y or xy2 will not create a new subplot.
+                 */
+                if(newAxis && hasXref && hasYref) {
+                    cartesianList.push(xref + yref);
+                }
+            }
+        }
+    };
+};
diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js
index a9db08dd172..4b83662d6d9 100644
--- a/src/plots/cartesian/index.js
+++ b/src/plots/cartesian/index.js
@@ -12,6 +12,7 @@
 var d3 = require('d3');
 var Lib = require('../../lib');
 var Plots = require('../plots');
+var getModuleCalcData = require('../get_data').getModuleCalcData;
 
 var axisIds = require('./axis_ids');
 var constants = require('./constants');
@@ -30,13 +31,92 @@ exports.attributes = require('./attributes');
 
 exports.layoutAttributes = require('./layout_attributes');
 
+exports.supplyLayoutDefaults = require('./layout_defaults');
+
 exports.transitionAxes = require('./transition_axes');
 
+exports.finalizeSubplots = function(layoutIn, layoutOut) {
+    var subplots = layoutOut._subplots;
+    var xList = subplots.xaxis;
+    var yList = subplots.yaxis;
+    var spSVG = subplots.cartesian;
+    var spAll = spSVG.concat(subplots.gl2d || []);
+    var allX = {};
+    var allY = {};
+    var i, xi, yi;
+
+    for(i = 0; i < spAll.length; i++) {
+        var parts = spAll[i].split('y');
+        allX[parts[0]] = 1;
+        allY['y' + parts[1]] = 1;
+    }
+
+    // check for x axes with no subplot, and make one from the anchor of that x axis
+    for(i = 0; i < xList.length; i++) {
+        xi = xList[i];
+        if(!allX[xi]) {
+            yi = (layoutIn[axisIds.id2name(xi)] || {}).anchor;
+            if(!constants.idRegex.y.test(yi)) yi = 'y';
+            spSVG.push(xi + yi);
+            spAll.push(xi + yi);
+
+            if(!allY[yi]) {
+                allY[yi] = 1;
+                Lib.pushUnique(yList, yi);
+            }
+        }
+    }
+
+    // same for y axes with no subplot
+    for(i = 0; i < yList.length; i++) {
+        yi = yList[i];
+        if(!allY[yi]) {
+            xi = (layoutIn[axisIds.id2name(yi)] || {}).anchor;
+            if(!constants.idRegex.x.test(xi)) xi = 'x';
+            spSVG.push(xi + yi);
+            spAll.push(xi + yi);
+
+            if(!allX[xi]) {
+                allX[xi] = 1;
+                Lib.pushUnique(xList, xi);
+            }
+        }
+    }
+
+    // finally, if we've gotten here we're supposed to show cartesian...
+    // so if there are NO subplots at all, make one from the first
+    // x & y axes in the input layout
+    if(!spAll.length) {
+        var keys = Object.keys(layoutIn);
+        xi = '';
+        yi = '';
+        for(i = 0; i < keys.length; i++) {
+            var ki = keys[i];
+            if(constants.attrRegex.test(ki)) {
+                var axLetter = ki.charAt(0);
+                if(axLetter === 'x') {
+                    if(!xi || (+ki.substr(5) < +xi.substr(5))) {
+                        xi = ki;
+                    }
+                }
+                else if(!yi || (+ki.substr(5) < +yi.substr(5))) {
+                    yi = ki;
+                }
+            }
+        }
+        xi = xi ? axisIds.name2id(xi) : 'x';
+        yi = yi ? axisIds.name2id(yi) : 'y';
+        xList.push(xi);
+        yList.push(yi);
+        spSVG.push(xi + yi);
+    }
+};
+
 exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) {
-    var fullLayout = gd._fullLayout,
-        subplots = Plots.getSubplotIds(fullLayout, 'cartesian'),
-        calcdata = gd.calcdata,
-        i;
+    var fullLayout = gd._fullLayout;
+    var subplots = fullLayout._subplots.cartesian;
+    var calcdata = gd.calcdata;
+    var i;
 
     // If traces is not provided, then it's a complete replot and missing
     // traces are removed
@@ -127,15 +207,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback
         if(_module.basePlotModule.name !== 'cartesian') continue;
 
         // plot all traces of this type on this subplot at once
-        var cdModule = [];
-        for(var k = 0; k < cdSubplot.length; k++) {
-            var cd = cdSubplot[k],
-                trace = cd[0].trace;
-
-            if((trace._module === _module) && (trace.visible === true)) {
-                cdModule.push(cd);
-            }
-        }
+        var cdModule = getModuleCalcData(cdSubplot, _module);
 
         _module.plot(gd, plotinfo, cdModule, transitionOpts, makeOnCompleteCallback);
     }
@@ -181,19 +253,29 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
             .remove();
     }
 
+    var oldSubplotList = oldFullLayout._subplots || {};
+    var newSubplotList = newFullLayout._subplots || {xaxis: [], yaxis: []};
+
+    // delete any titles we don't need anymore
+    // check if axis list has changed, and if so clear old titles
+    if(oldSubplotList.xaxis && oldSubplotList.yaxis) {
+        var oldAxIDs = oldSubplotList.xaxis.concat(oldSubplotList.yaxis);
+        var newAxIDs = newSubplotList.xaxis.concat(newSubplotList.yaxis);
+
+        for(i = 0; i < oldAxIDs.length; i++) {
+            if(newAxIDs.indexOf(oldAxIDs[i]) === -1) {
+                oldFullLayout._infolayer.selectAll('.g-' + oldAxIDs[i] + 'title').remove();
+            }
+        }
+    }
+
+    // if we've gotten rid of all cartesian traces, remove all the subplot svg items
     var hadCartesian = (oldFullLayout._has && oldFullLayout._has('cartesian'));
     var hasCartesian = (newFullLayout._has && newFullLayout._has('cartesian'));
 
     if(hadCartesian && !hasCartesian) {
-        var subplotLayers = oldFullLayout._cartesianlayer.selectAll('.subplot');
-        var axIds = axisIds.listIds({ _fullLayout: oldFullLayout });
-
-        subplotLayers.call(purgeSubplotLayers, oldFullLayout);
+        purgeSubplotLayers(oldFullLayout._cartesianlayer.selectAll('.subplot'), oldFullLayout);
         oldFullLayout._defs.selectAll('.axesclip').remove();
-
-        for(i = 0; i < axIds.length; i++) {
-            oldFullLayout._infolayer.select('.' + axIds[i] + 'title').remove();
-        }
     }
 };
 
@@ -287,10 +369,8 @@ function makeSubplotLayer(plotinfo) {
         plotinfo.imagelayer = joinLayer(backLayer, 'g', 'imagelayer');
 
         plotinfo.gridlayer = joinLayer(plotgroup, 'g', 'gridlayer');
-        plotinfo.overgrid = joinLayer(plotgroup, 'g', 'overgrid');
 
         plotinfo.zerolinelayer = joinLayer(plotgroup, 'g', 'zerolinelayer');
-        plotinfo.overzero = joinLayer(plotgroup, 'g', 'overzero');
 
         joinLayer(plotgroup, 'path', 'xlines-below');
         joinLayer(plotgroup, 'path', 'ylines-below');
@@ -328,8 +408,8 @@ function makeSubplotLayer(plotinfo) {
         // their other components to the corresponding
         // extra groups of their main plots.
 
-        plotinfo.gridlayer = joinLayer(mainplotinfo.overgrid, 'g', id);
-        plotinfo.zerolinelayer = joinLayer(mainplotinfo.overzero, 'g', id);
+        plotinfo.gridlayer = mainplotinfo.gridlayer;
+        plotinfo.zerolinelayer = mainplotinfo.zerolinelayer;
 
         joinLayer(mainplotinfo.overlinesBelow, 'path', xId);
         joinLayer(mainplotinfo.overlinesBelow, 'path', yId);
@@ -350,6 +430,10 @@ function makeSubplotLayer(plotinfo) {
         plotinfo.yaxislayer = mainplotgroup.select('.overaxes-' + yLayer).select('.' + yId);
     }
 
+    joinLayer(plotinfo.gridlayer, 'g', plotinfo.xaxis._id, plotinfo.xaxis._id);
+    joinLayer(plotinfo.gridlayer, 'g', plotinfo.yaxis._id, plotinfo.yaxis._id);
+    plotinfo.gridlayer.selectAll('g').sort(axisIds.idSort);
+
     // common attributes for all subplots, overlays or not
 
     for(var i = 0; i < constants.traceLayerClasses.length; i++) {
@@ -403,9 +487,9 @@ function purgeSubplotLayers(layers, fullLayout) {
     }
 }
 
-function joinLayer(parent, nodeType, className) {
+function joinLayer(parent, nodeType, className, dataVal) {
     var layer = parent.selectAll('.' + className)
-        .data([0]);
+        .data([dataVal || 0]);
 
     layer.enter().append(nodeType)
         .classed(className, true);
diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js
index 6765fdcdec7..c4f5ef57c05 100644
--- a/src/plots/cartesian/layout_defaults.js
+++ b/src/plots/cartesian/layout_defaults.js
@@ -14,7 +14,6 @@ var Lib = require('../../lib');
 var Color = require('../../components/color');
 var basePlotLayoutAttributes = require('../layout_attributes');
 
-var constants = require('./constants');
 var layoutAttributes = require('./layout_attributes');
 var handleTypeDefaults = require('./type_defaults');
 var handleAxisDefaults = require('./axis_defaults');
@@ -24,40 +23,28 @@ var axisIds = require('./axis_ids');
 
 
 module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
-    var layoutKeys = Object.keys(layoutIn),
-        xaListCartesian = [],
-        yaListCartesian = [],
-        xaListGl2d = [],
-        yaListGl2d = [],
-        xaListCheater = [],
-        xaListNonCheater = [],
-        outerTicks = {},
-        noGrids = {},
-        i;
+    var xaCheater = {};
+    var xaNonCheater = {};
+    var outerTicks = {};
+    var noGrids = {};
+    var i;
 
     // look for axes in the data
     for(i = 0; i < fullData.length; i++) {
         var trace = fullData[i];
-        var listX, listY;
 
-        if(Registry.traceIs(trace, 'cartesian')) {
-            listX = xaListCartesian;
-            listY = yaListCartesian;
+        if(!Registry.traceIs(trace, 'cartesian') && !Registry.traceIs(trace, 'gl2d')) {
+            continue;
         }
-        else if(Registry.traceIs(trace, 'gl2d')) {
-            listX = xaListGl2d;
-            listY = yaListGl2d;
-        }
-        else continue;
 
-        var xaName = axisIds.id2name(trace.xaxis),
-            yaName = axisIds.id2name(trace.yaxis);
+        var xaName = axisIds.id2name(trace.xaxis);
+        var yaName = axisIds.id2name(trace.yaxis);
 
         // Two things trigger axis visibility:
         // 1. is not carpet
         // 2. carpet that's not cheater
         if(!Registry.traceIs(trace, 'carpet') || (trace.type === 'carpet' && !trace._cheater)) {
-            if(xaName) Lib.pushUnique(xaListNonCheater, xaName);
+            if(xaName) xaNonCheater[xaName] = 1;
         }
 
         // The above check for definitely-not-cheater is not adequate. This
@@ -65,13 +52,9 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
         // full condition triggering hiding is:
         //   *could* be a cheater and *is not definitely visible*
         if(trace.type === 'carpet' && trace._cheater) {
-            if(xaName) Lib.pushUnique(xaListCheater, xaName);
+            if(xaName) xaCheater[xaName] = 1;
         }
 
-        // add axes implied by traces
-        if(xaName && listX.indexOf(xaName) === -1) listX.push(xaName);
-        if(yaName && listY.indexOf(yaName) === -1) listY.push(yaName);
-
         // check for default formatting tweaks
         if(Registry.traceIs(trace, '2dMap')) {
             outerTicks[xaName] = true;
@@ -84,51 +67,17 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
         }
     }
 
-    // N.B. Ignore orphan axes (i.e. axes that have no data attached to them)
-    // if gl3d or geo is present on graph. This is retain backward compatible.
-    //
-    // TODO drop this in version 2.0
-    var ignoreOrphan = (layoutOut._has('gl3d') || layoutOut._has('geo'));
-
-    if(!ignoreOrphan) {
-        for(i = 0; i < layoutKeys.length; i++) {
-            var key = layoutKeys[i];
-
-            // orphan layout axes are considered cartesian subplots
-
-            if(xaListGl2d.indexOf(key) === -1 &&
-                xaListCartesian.indexOf(key) === -1 &&
-                    constants.xAxisMatch.test(key)) {
-                xaListCartesian.push(key);
-            }
-            else if(yaListGl2d.indexOf(key) === -1 &&
-                yaListCartesian.indexOf(key) === -1 &&
-                    constants.yAxisMatch.test(key)) {
-                yaListCartesian.push(key);
-            }
-        }
-    }
-
-    // make sure that plots with orphan cartesian axes
-    // are considered 'cartesian'
-    if(xaListCartesian.length && yaListCartesian.length) {
-        Lib.pushUnique(layoutOut._basePlotModules, Registry.subplotsRegistry.cartesian);
-    }
-
-    function axSort(a, b) {
-        var aNum = Number(a.substr(5) || 1),
-            bNum = Number(b.substr(5) || 1);
-        return aNum - bNum;
-    }
-
-    var xaList = xaListCartesian.concat(xaListGl2d).sort(axSort),
-        yaList = yaListCartesian.concat(yaListGl2d).sort(axSort),
-        axesList = xaList.concat(yaList);
+    var subplots = layoutOut._subplots;
+    var xIds = subplots.xaxis;
+    var yIds = subplots.yaxis;
+    var xNames = Lib.simpleMap(xIds, axisIds.id2name);
+    var yNames = Lib.simpleMap(yIds, axisIds.id2name);
+    var axNames = xNames.concat(yNames);
 
     // plot_bgcolor only makes sense if there's a (2D) plot!
     // TODO: bgcolor for each subplot, to inherit from the main one
     var plot_bgcolor = Color.background;
-    if(xaList.length && yaList.length) {
+    if(xIds.length && yIds.length) {
         plot_bgcolor = Lib.coerce(layoutIn, layoutOut, basePlotLayoutAttributes, 'plot_bgcolor');
     }
 
@@ -141,14 +90,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
     }
 
     function getCounterAxes(axLetter) {
-        var list = {x: yaList, y: xaList}[axLetter];
-        return Lib.simpleMap(list, axisIds.name2id);
+        return (axLetter === 'x') ? yIds : xIds;
     }
 
     var counterAxes = {x: getCounterAxes('x'), y: getCounterAxes('y')};
 
     function getOverlayableAxes(axLetter, axName) {
-        var list = {x: xaList, y: yaList}[axLetter];
+        var list = (axLetter === 'x') ? xNames : yNames;
         var out = [];
 
         for(var j = 0; j < list.length; j++) {
@@ -163,8 +111,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
     }
 
     // first pass creates the containers, determines types, and handles most of the settings
-    for(i = 0; i < axesList.length; i++) {
-        axName = axesList[i];
+    for(i = 0; i < axNames.length; i++) {
+        axName = axNames[i];
 
         if(!Lib.isPlainObject(layoutIn[axName])) {
             layoutIn[axName] = {};
@@ -186,7 +134,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
             data: fullData,
             bgColor: bgColor,
             calendar: layoutOut.calendar,
-            cheateronly: axLetter === 'x' && (xaListCheater.indexOf(axName) !== -1 && xaListNonCheater.indexOf(axName) === -1)
+            cheateronly: axLetter === 'x' && xaCheater[axName] && !xaNonCheater[axName]
         };
 
         handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut);
@@ -211,11 +159,11 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
     }
 
     // quick second pass for range slider and selector defaults
-    var rangeSliderDefaults = Registry.getComponentMethod('rangeslider', 'handleDefaults'),
-        rangeSelectorDefaults = Registry.getComponentMethod('rangeselector', 'handleDefaults');
+    var rangeSliderDefaults = Registry.getComponentMethod('rangeslider', 'handleDefaults');
+    var rangeSelectorDefaults = Registry.getComponentMethod('rangeselector', 'handleDefaults');
 
-    for(i = 0; i < xaList.length; i++) {
-        axName = xaList[i];
+    for(i = 0; i < xNames.length; i++) {
+        axName = xNames[i];
         axLayoutIn = layoutIn[axName];
         axLayoutOut = layoutOut[axName];
 
@@ -226,7 +174,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
                 axLayoutIn,
                 axLayoutOut,
                 layoutOut,
-                yaList,
+                yNames,
                 axLayoutOut.calendar
             );
         }
@@ -234,8 +182,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
         coerce('fixedrange');
     }
 
-    for(i = 0; i < yaList.length; i++) {
-        axName = yaList[i];
+    for(i = 0; i < yNames.length; i++) {
+        axName = yNames[i];
         axLayoutIn = layoutIn[axName];
         axLayoutOut = layoutOut[axName];
 
@@ -259,8 +207,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
     layoutOut._axisConstraintGroups = [];
     var allAxisIds = counterAxes.x.concat(counterAxes.y);
 
-    for(i = 0; i < axesList.length; i++) {
-        axName = axesList[i];
+    for(i = 0; i < axNames.length; i++) {
+        axName = axNames[i];
         axLetter = axName.charAt(0);
 
         axLayoutIn = layoutIn[axName];
diff --git a/src/plots/geo/index.js b/src/plots/geo/index.js
index 076411c0d82..388136794ab 100644
--- a/src/plots/geo/index.js
+++ b/src/plots/geo/index.js
@@ -10,7 +10,7 @@
 'use strict';
 
 var createGeo = require('./geo');
-var Plots = require('../../plots/plots');
+var getSubplotCalcData = require('../../plots/get_data').getSubplotCalcData;
 var counterRegex = require('../../lib').counterRegex;
 
 var GEO = 'geo';
@@ -32,7 +32,7 @@ exports.supplyLayoutDefaults = require('./layout/defaults');
 exports.plot = function plotGeo(gd) {
     var fullLayout = gd._fullLayout;
     var calcData = gd.calcdata;
-    var geoIds = Plots.getSubplotIds(fullLayout, GEO);
+    var geoIds = fullLayout._subplots[GEO];
 
     /**
      * If 'plotly-geo-assets.js' is not included,
@@ -44,7 +44,7 @@ exports.plot = function plotGeo(gd) {
 
     for(var i = 0; i < geoIds.length; i++) {
         var geoId = geoIds[i];
-        var geoCalcData = Plots.getSubplotCalcData(calcData, GEO, geoId);
+        var geoCalcData = getSubplotCalcData(calcData, GEO, geoId);
         var geoLayout = fullLayout[geoId];
         var geo = geoLayout._subplot;
 
@@ -65,7 +65,7 @@ exports.plot = function plotGeo(gd) {
 };
 
 exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
-    var oldGeoKeys = Plots.getSubplotIds(oldFullLayout, GEO);
+    var oldGeoKeys = oldFullLayout._subplots[GEO] || [];
 
     for(var i = 0; i < oldGeoKeys.length; i++) {
         var oldGeoKey = oldGeoKeys[i];
@@ -79,7 +79,7 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
 };
 
 exports.updateFx = function(fullLayout) {
-    var subplotIds = Plots.getSubplotIds(fullLayout, GEO);
+    var subplotIds = fullLayout._subplots[GEO];
 
     for(var i = 0; i < subplotIds.length; i++) {
         var subplotLayout = fullLayout[subplotIds[i]];
diff --git a/src/plots/get_data.js b/src/plots/get_data.js
new file mode 100644
index 00000000000..765388964d7
--- /dev/null
+++ b/src/plots/get_data.js
@@ -0,0 +1,92 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var Registry = require('../registry');
+var SUBPLOT_PATTERN = require('./cartesian/constants').SUBPLOT_PATTERN;
+
+/**
+ * Get calcdata traces(s) associated with a given subplot
+ *
+ * @param {array} calcData (as in gd.calcdata)
+ * @param {string} type subplot type
+ * @param {string} subplotId subplot id to look for
+ *
+ * @return {array} array of calcdata traces
+ */
+exports.getSubplotCalcData = function(calcData, type, subplotId) {
+    var basePlotModule = Registry.subplotsRegistry[type];
+    if(!basePlotModule) return [];
+
+    var attr = basePlotModule.attr;
+    var subplotCalcData = [];
+
+    for(var i = 0; i < calcData.length; i++) {
+        var calcTrace = calcData[i];
+        var trace = calcTrace[0].trace;
+
+        if(trace[attr] === subplotId) subplotCalcData.push(calcTrace);
+    }
+
+    return subplotCalcData;
+};
+
+exports.getModuleCalcData = function(calcdata, typeOrModule) {
+    var moduleCalcData = [];
+    var _module = typeof typeOrModule === 'string' ? Registry.getModule(typeOrModule) : typeOrModule;
+    if(!_module) return moduleCalcData;
+
+    for(var i = 0; i < calcdata.length; i++) {
+        var cd = calcdata[i];
+        var trace = cd[0].trace;
+
+        if((trace._module === _module) && (trace.visible === true)) moduleCalcData.push(cd);
+    }
+
+    return moduleCalcData;
+};
+
+/**
+ * Get the data trace(s) associated with a given subplot.
+ *
+ * @param {array} data  plotly full data array.
+ * @param {string} type subplot type to look for.
+ * @param {string} subplotId subplot id to look for.
+ *
+ * @return {array} list of trace objects.
+ *
+ */
+exports.getSubplotData = function getSubplotData(data, type, subplotId) {
+    if(!Registry.subplotsRegistry[type]) return [];
+
+    var attr = Registry.subplotsRegistry[type].attr;
+    var subplotData = [];
+    var trace, subplotX, subplotY;
+
+    if(type === 'gl2d') {
+        var spmatch = subplotId.match(SUBPLOT_PATTERN);
+        subplotX = 'x' + spmatch[1];
+        subplotY = 'y' + spmatch[2];
+    }
+
+    for(var i = 0; i < data.length; i++) {
+        trace = data[i];
+
+        if(type === 'gl2d' && Registry.traceIs(trace, 'gl2d')) {
+            if(trace[attr[0]] === subplotX && trace[attr[1]] === subplotY) {
+                subplotData.push(trace);
+            }
+        }
+        else {
+            if(trace[attr] === subplotId) subplotData.push(trace);
+        }
+    }
+
+    return subplotData;
+};
diff --git a/src/plots/gl2d/convert.js b/src/plots/gl2d/convert.js
index fd50b3a787d..948fa8e4d47 100644
--- a/src/plots/gl2d/convert.js
+++ b/src/plots/gl2d/convert.js
@@ -9,7 +9,6 @@
 
 'use strict';
 
-var Plots = require('../plots');
 var Axes = require('../cartesian/axes');
 
 var convertHTMLToUnicode = require('../../lib/html2unicode');
@@ -183,9 +182,9 @@ proto.merge = function(options) {
 
 // is an axis shared with an already-drawn subplot ?
 proto.hasSharedAxis = function(ax) {
-    var scene = this.scene,
-        subplotIds = Plots.getSubplotIds(scene.fullLayout, 'gl2d'),
-        list = Axes.findSubplotsWithAxis(subplotIds, ax);
+    var scene = this.scene;
+    var subplotIds = scene.fullLayout._subplots.gl2d;
+    var list = Axes.findSubplotsWithAxis(subplotIds, ax);
 
     // if index === 0, then the subplot is already drawn as subplots
     // are drawn in order.
diff --git a/src/plots/gl2d/index.js b/src/plots/gl2d/index.js
index 177de42ffab..eba58d8442b 100644
--- a/src/plots/gl2d/index.js
+++ b/src/plots/gl2d/index.js
@@ -12,11 +12,12 @@
 var overrideAll = require('../../plot_api/edit_types').overrideAll;
 
 var Scene2D = require('./scene2d');
-var Plots = require('../plots');
+var layoutGlobalAttrs = require('../layout_attributes');
 var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
 var constants = require('../cartesian/constants');
 var Cartesian = require('../cartesian');
 var fxAttrs = require('../../components/fx/layout_attributes');
+var getSubplotData = require('../get_data').getSubplotData;
 
 exports.name = 'gl2d';
 
@@ -30,6 +31,12 @@ exports.attrRegex = constants.attrRegex;
 
 exports.attributes = require('../cartesian/attributes');
 
+exports.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) {
+    if(!layoutOut._has('cartesian')) {
+        Cartesian.supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+    }
+};
+
 // gl2d uses svg axis attributes verbatim, but overrides editType
 // this could potentially be just `layoutAttributes` but it would
 // still need special handling somewhere to give it precedence over
@@ -38,7 +45,7 @@ exports.layoutAttrOverrides = overrideAll(Cartesian.layoutAttributes, 'plot', 'f
 
 // similar overrides for base plot attributes (and those added by components)
 exports.baseLayoutAttrOverrides = overrideAll({
-    plot_bgcolor: Plots.layoutAttributes.plot_bgcolor,
+    plot_bgcolor: layoutGlobalAttrs.plot_bgcolor,
     hoverlabel: fxAttrs.hoverlabel
     // dragmode needs calc but only when transitioning TO lasso or select
     // so for now it's left inside _relayout
@@ -46,14 +53,14 @@ exports.baseLayoutAttrOverrides = overrideAll({
 }, 'plot', 'nested');
 
 exports.plot = function plotGl2d(gd) {
-    var fullLayout = gd._fullLayout,
-        fullData = gd._fullData,
-        subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d');
+    var fullLayout = gd._fullLayout;
+    var fullData = gd._fullData;
+    var subplotIds = fullLayout._subplots.gl2d;
 
     for(var i = 0; i < subplotIds.length; i++) {
         var subplotId = subplotIds[i],
             subplotObj = fullLayout._plots[subplotId],
-            fullSubplotData = Plots.getSubplotData(fullData, 'gl2d', subplotId);
+            fullSubplotData = getSubplotData(fullData, 'gl2d', subplotId);
 
         // ref. to corresp. Scene instance
         var scene = subplotObj._scene2d;
@@ -79,7 +86,7 @@ exports.plot = function plotGl2d(gd) {
 };
 
 exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
-    var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, 'gl2d');
+    var oldSceneKeys = oldFullLayout._subplots.gl2d || [];
 
     for(var i = 0; i < oldSceneKeys.length; i++) {
         var id = oldSceneKeys[i],
@@ -89,7 +96,7 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
         if(!oldSubplot._scene2d) continue;
 
         // if no traces are present, delete gl2d subplot
-        var subplotData = Plots.getSubplotData(newFullData, 'gl2d', id);
+        var subplotData = getSubplotData(newFullData, 'gl2d', id);
         if(subplotData.length === 0) {
             oldSubplot._scene2d.destroy();
             delete oldFullLayout._plots[id];
@@ -107,8 +114,8 @@ exports.drawFramework = function(gd) {
 };
 
 exports.toSVG = function(gd) {
-    var fullLayout = gd._fullLayout,
-        subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d');
+    var fullLayout = gd._fullLayout;
+    var subplotIds = fullLayout._subplots.gl2d;
 
     for(var i = 0; i < subplotIds.length; i++) {
         var subplot = fullLayout._plots[subplotIds[i]],
@@ -132,7 +139,7 @@ exports.toSVG = function(gd) {
 };
 
 exports.updateFx = function(fullLayout) {
-    var subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d');
+    var subplotIds = fullLayout._subplots.gl2d;
 
     for(var i = 0; i < subplotIds.length; i++) {
         var subplotObj = fullLayout._plots[subplotIds[i]]._scene2d;
diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js
index 2d8b3a74ffb..3a6433d6dc8 100644
--- a/src/plots/gl2d/scene2d.js
+++ b/src/plots/gl2d/scene2d.js
@@ -22,13 +22,15 @@ var createOptions = require('./convert');
 var createCamera = require('./camera');
 var convertHTMLToUnicode = require('../../lib/html2unicode');
 var showNoWebGlMsg = require('../../lib/show_no_webgl_msg');
-var axisConstraints = require('../../plots/cartesian/constraints');
+var axisConstraints = require('../cartesian/constraints');
 var enforceAxisConstraints = axisConstraints.enforce;
 var cleanAxisConstraints = axisConstraints.clean;
 
 var AXES = ['xaxis', 'yaxis'];
 var STATIC_CANVAS, STATIC_CONTEXT;
 
+var SUBPLOT_PATTERN = require('../cartesian/constants').SUBPLOT_PATTERN;
+
 
 function Scene2D(options, fullLayout) {
     this.container = options.container;
@@ -296,9 +298,9 @@ function compareTicks(a, b) {
 proto.updateRefs = function(newFullLayout) {
     this.fullLayout = newFullLayout;
 
-    var spmatch = Axes.subplotMatch,
-        xaxisName = 'xaxis' + this.id.match(spmatch)[1],
-        yaxisName = 'yaxis' + this.id.match(spmatch)[2];
+    var spmatch = this.id.match(SUBPLOT_PATTERN);
+    var xaxisName = 'xaxis' + spmatch[1];
+    var yaxisName = 'yaxis' + spmatch[2];
 
     this.xaxis = this.fullLayout[xaxisName];
     this.yaxis = this.fullLayout[yaxisName];
diff --git a/src/plots/gl3d/index.js b/src/plots/gl3d/index.js
index e3e2e9094ca..60175470f1c 100644
--- a/src/plots/gl3d/index.js
+++ b/src/plots/gl3d/index.js
@@ -13,7 +13,7 @@ var overrideAll = require('../../plot_api/edit_types').overrideAll;
 var fxAttrs = require('../../components/fx/layout_attributes');
 
 var Scene = require('./scene');
-var Plots = require('../plots');
+var getSubplotData = require('../get_data').getSubplotData;
 var Lib = require('../../lib');
 var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
 
@@ -40,13 +40,13 @@ exports.baseLayoutAttrOverrides = overrideAll({
 exports.supplyLayoutDefaults = require('./layout/defaults');
 
 exports.plot = function plotGl3d(gd) {
-    var fullLayout = gd._fullLayout,
-        fullData = gd._fullData,
-        sceneIds = Plots.getSubplotIds(fullLayout, GL3D);
+    var fullLayout = gd._fullLayout;
+    var fullData = gd._fullData;
+    var sceneIds = fullLayout._subplots[GL3D];
 
     for(var i = 0; i < sceneIds.length; i++) {
         var sceneId = sceneIds[i],
-            fullSceneData = Plots.getSubplotData(fullData, GL3D, sceneId),
+            fullSceneData = getSubplotData(fullData, GL3D, sceneId),
             sceneLayout = fullLayout[sceneId],
             scene = sceneLayout._scene;
 
@@ -75,7 +75,7 @@ exports.plot = function plotGl3d(gd) {
 };
 
 exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
-    var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, GL3D);
+    var oldSceneKeys = oldFullLayout._subplots[GL3D] || [];
 
     for(var i = 0; i < oldSceneKeys.length; i++) {
         var oldSceneKey = oldSceneKeys[i];
@@ -93,14 +93,14 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
 };
 
 exports.toSVG = function(gd) {
-    var fullLayout = gd._fullLayout,
-        sceneIds = Plots.getSubplotIds(fullLayout, GL3D),
-        size = fullLayout._size;
+    var fullLayout = gd._fullLayout;
+    var sceneIds = fullLayout._subplots[GL3D];
+    var size = fullLayout._size;
 
     for(var i = 0; i < sceneIds.length; i++) {
-        var sceneLayout = fullLayout[sceneIds[i]],
-            domain = sceneLayout.domain,
-            scene = sceneLayout._scene;
+        var sceneLayout = fullLayout[sceneIds[i]];
+        var domain = sceneLayout.domain;
+        var scene = sceneLayout._scene;
 
         var imageData = scene.toImage('png');
         var image = fullLayout._glimages.append('svg:image');
@@ -130,7 +130,7 @@ exports.cleanId = function cleanId(id) {
 };
 
 exports.updateFx = function(fullLayout) {
-    var subplotIds = Plots.getSubplotIds(fullLayout, GL3D);
+    var subplotIds = fullLayout._subplots[GL3D];
 
     for(var i = 0; i < subplotIds.length; i++) {
         var subplotObj = fullLayout[subplotIds[i]]._scene;
diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js
index 3e07d25be92..a62f555385f 100644
--- a/src/plots/layout_attributes.js
+++ b/src/plots/layout_attributes.js
@@ -134,7 +134,7 @@ module.exports = {
         description: 'Sets the color of paper where the graph is drawn.'
     },
     plot_bgcolor: {
-        // defined here, but set in Axes.supplyLayoutDefaults
+        // defined here, but set in cartesian.supplyLayoutDefaults
         // because it needs to know if there are (2D) axes or not
         valType: 'color',
         role: 'style',
diff --git a/src/plots/mapbox/index.js b/src/plots/mapbox/index.js
index b417cb59729..7a8b012ff06 100644
--- a/src/plots/mapbox/index.js
+++ b/src/plots/mapbox/index.js
@@ -12,7 +12,7 @@
 var mapboxgl = require('mapbox-gl');
 
 var Lib = require('../../lib');
-var Plots = require('../plots');
+var getSubplotCalcData = require('../../plots/get_data').getSubplotCalcData;
 var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
 
 var createMapbox = require('./mapbox');
@@ -49,16 +49,16 @@ exports.layoutAttributes = require('./layout_attributes');
 exports.supplyLayoutDefaults = require('./layout_defaults');
 
 exports.plot = function plotMapbox(gd) {
-    var fullLayout = gd._fullLayout,
-        calcData = gd.calcdata,
-        mapboxIds = Plots.getSubplotIds(fullLayout, MAPBOX);
+    var fullLayout = gd._fullLayout;
+    var calcData = gd.calcdata;
+    var mapboxIds = fullLayout._subplots[MAPBOX];
 
     var accessToken = findAccessToken(gd, mapboxIds);
     mapboxgl.accessToken = accessToken;
 
     for(var i = 0; i < mapboxIds.length; i++) {
         var id = mapboxIds[i],
-            subplotCalcData = Plots.getSubplotCalcData(calcData, MAPBOX, id),
+            subplotCalcData = getSubplotCalcData(calcData, MAPBOX, id),
             opts = fullLayout[id],
             mapbox = opts._subplot;
 
@@ -91,7 +91,7 @@ exports.plot = function plotMapbox(gd) {
 };
 
 exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
-    var oldMapboxKeys = Plots.getSubplotIds(oldFullLayout, MAPBOX);
+    var oldMapboxKeys = oldFullLayout._subplots[MAPBOX] || [];
 
     for(var i = 0; i < oldMapboxKeys.length; i++) {
         var oldMapboxKey = oldMapboxKeys[i];
@@ -103,9 +103,9 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
 };
 
 exports.toSVG = function(gd) {
-    var fullLayout = gd._fullLayout,
-        subplotIds = Plots.getSubplotIds(fullLayout, MAPBOX),
-        size = fullLayout._size;
+    var fullLayout = gd._fullLayout;
+    var subplotIds = fullLayout._subplots[MAPBOX];
+    var size = fullLayout._size;
 
     for(var i = 0; i < subplotIds.length; i++) {
         var opts = fullLayout[subplotIds[i]],
@@ -157,7 +157,7 @@ function findAccessToken(gd, mapboxIds) {
 }
 
 exports.updateFx = function(fullLayout) {
-    var subplotIds = Plots.getSubplotIds(fullLayout, MAPBOX);
+    var subplotIds = fullLayout._subplots[MAPBOX];
 
     for(var i = 0; i < subplotIds.length; i++) {
         var subplotObj = fullLayout[subplotIds[i]]._subplot;
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 53cb80e65b4..e7a31c22aa2 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -15,6 +15,7 @@ var isNumeric = require('fast-isnumeric');
 var Plotly = require('../plotly');
 var PlotSchema = require('../plot_api/plot_schema');
 var Registry = require('../registry');
+var axisIDs = require('../plots/cartesian/axis_ids');
 var Lib = require('../lib');
 var _ = Lib._;
 var Color = require('../components/color');
@@ -38,7 +39,6 @@ plots.layoutAttributes = require('./layout_attributes');
 // TODO make this a plot attribute?
 plots.fontWeight = 'normal';
 
-var subplotsRegistry = plots.subplotsRegistry;
 var transformsRegistry = plots.transformsRegistry;
 
 var ErrorBars = require('../components/errorbars');
@@ -49,143 +49,6 @@ plots.computeAPICommandBindings = commandModule.computeAPICommandBindings;
 plots.manageCommandObserver = commandModule.manageCommandObserver;
 plots.hasSimpleAPICommandBindings = commandModule.hasSimpleAPICommandBindings;
 
-/**
- * Find subplot ids in data.
- * Meant to be used in the defaults step.
- *
- * Use plots.getSubplotIds to grab the current
- * subplot ids later on in Plotly.plot.
- *
- * @param {array} data plotly data array
- *      (intended to be _fullData, but does not have to be).
- * @param {string} type subplot type to look for.
- *
- * @return {array} list of subplot ids (strings).
- *      N.B. these ids possibly un-ordered.
- *
- * TODO incorporate cartesian/gl2d axis finders in this paradigm.
- */
-plots.findSubplotIds = function findSubplotIds(data, type) {
-    var subplotIds = [];
-
-    if(!plots.subplotsRegistry[type]) return subplotIds;
-
-    var attr = plots.subplotsRegistry[type].attr;
-
-    for(var i = 0; i < data.length; i++) {
-        var trace = data[i];
-
-        if(plots.traceIs(trace, type) && subplotIds.indexOf(trace[attr]) === -1) {
-            subplotIds.push(trace[attr]);
-        }
-    }
-
-    return subplotIds;
-};
-
-/**
- * Get the ids of the current subplots.
- *
- * @param {object} layout plotly full layout object.
- * @param {string} type subplot type to look for.
- *
- * @return {array} list of ordered subplot ids (strings).
- *
- */
-plots.getSubplotIds = function getSubplotIds(layout, type) {
-    var _module = plots.subplotsRegistry[type];
-
-    if(!_module) return [];
-
-    // layout must be 'fullLayout' here
-    if(type === 'cartesian' && (!layout._has || !layout._has('cartesian'))) return [];
-    if(type === 'gl2d' && (!layout._has || !layout._has('gl2d'))) return [];
-    if(type === 'cartesian' || type === 'gl2d') {
-        return Object.keys(layout._plots || {});
-    }
-
-    var attrRegex = _module.attrRegex,
-        layoutKeys = Object.keys(layout),
-        subplotIds = [];
-
-    for(var i = 0; i < layoutKeys.length; i++) {
-        var layoutKey = layoutKeys[i];
-
-        if(attrRegex.test(layoutKey)) subplotIds.push(layoutKey);
-    }
-
-    // order the ids
-    var idLen = _module.idRoot.length;
-    subplotIds.sort(function(a, b) {
-        var aNum = +(a.substr(idLen) || 1),
-            bNum = +(b.substr(idLen) || 1);
-        return aNum - bNum;
-    });
-
-    return subplotIds;
-};
-
-/**
- * Get the data trace(s) associated with a given subplot.
- *
- * @param {array} data  plotly full data array.
- * @param {string} type subplot type to look for.
- * @param {string} subplotId subplot id to look for.
- *
- * @return {array} list of trace objects.
- *
- */
-plots.getSubplotData = function getSubplotData(data, type, subplotId) {
-    if(!plots.subplotsRegistry[type]) return [];
-
-    var attr = plots.subplotsRegistry[type].attr,
-        subplotData = [],
-        trace;
-
-    for(var i = 0; i < data.length; i++) {
-        trace = data[i];
-
-        if(type === 'gl2d' && plots.traceIs(trace, 'gl2d')) {
-            var spmatch = Plotly.Axes.subplotMatch,
-                subplotX = 'x' + subplotId.match(spmatch)[1],
-                subplotY = 'y' + subplotId.match(spmatch)[2];
-
-            if(trace[attr[0]] === subplotX && trace[attr[1]] === subplotY) {
-                subplotData.push(trace);
-            }
-        }
-        else {
-            if(trace[attr] === subplotId) subplotData.push(trace);
-        }
-    }
-
-    return subplotData;
-};
-
-/**
- * Get calcdata traces(s) associated with a given subplot
- *
- * @param {array} calcData (as in gd.calcdata)
- * @param {string} type subplot type
- * @param {string} subplotId subplot id to look for
- *
- * @return {array} array of calcdata traces
- */
-plots.getSubplotCalcData = function(calcData, type, subplotId) {
-    if(!plots.subplotsRegistry[type]) return [];
-
-    var attr = plots.subplotsRegistry[type].attr;
-    var subplotCalcData = [];
-
-    for(var i = 0; i < calcData.length; i++) {
-        var calcTrace = calcData[i],
-            trace = calcTrace[0].trace;
-
-        if(trace[attr] === subplotId) subplotCalcData.push(calcTrace);
-    }
-
-    return subplotCalcData;
-};
 
 // in some cases the browser doesn't seem to know how big
 // the text is at first, so it needs to draw it,
@@ -484,6 +347,11 @@ plots.supplyDefaults = function(gd) {
     // keep track of how many traces are inputted
     newFullLayout._dataLength = newData.length;
 
+    // clear the lists of trace and baseplot modules, and subplots
+    newFullLayout._modules = [];
+    newFullLayout._basePlotModules = [];
+    newFullLayout._subplots = emptySubplotLists();
+
     // then do the data
     newFullLayout._globalTransforms = (gd._context || {}).globalTransforms;
     plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout);
@@ -529,7 +397,7 @@ plots.supplyDefaults = function(gd) {
     plots.doAutoMargin(gd);
 
     // set scale after auto margin routine
-    var axList = Plotly.Axes.list(gd);
+    var axList = axisIDs.list(gd);
     for(i = 0; i < axList.length; i++) {
         var ax = axList[i];
         ax.setScale();
@@ -551,6 +419,48 @@ plots.supplyDefaults = function(gd) {
     }
 };
 
+/**
+ * Make a container for collecting subplots we need to display.
+ *
+ * Finds all subplot types we need to enumerate once and caches it,
+ * but makes a new output object each time.
+ * Single-trace subplots (which have no `id`) such as pie, table, etc
+ * do not need to be collected because we just draw all visible traces.
+ */
+var collectableSubplotTypes;
+function emptySubplotLists() {
+    var out = {};
+    var i, j;
+
+    if(!collectableSubplotTypes) {
+        collectableSubplotTypes = [];
+
+        var subplotsRegistry = Registry.subplotsRegistry;
+
+        for(var subplotType in subplotsRegistry) {
+            var subplotModule = subplotsRegistry[subplotType];
+            var subplotAttr = subplotModule.attr;
+
+            if(subplotAttr) {
+                collectableSubplotTypes.push(subplotType);
+
+                // special case, currently just for cartesian:
+                // we need to enumerate axes, not just subplots
+                if(Array.isArray(subplotAttr)) {
+                    for(j = 0; j < subplotAttr.length; j++) {
+                        Lib.pushUnique(collectableSubplotTypes, subplotAttr[j]);
+                    }
+                }
+            }
+        }
+    }
+
+    for(i = 0; i < collectableSubplotTypes.length; i++) {
+        out[collectableSubplotTypes[i]] = [];
+    }
+    return out;
+}
+
 function remapTransformedArrays(cd0, newTrace) {
     var oldTrace = cd0.trace;
     var arrayAttrs = oldTrace._arrayAttrs;
@@ -763,23 +673,29 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou
 };
 
 plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
-    var oldSubplots = oldFullLayout._plots || {},
-        newSubplots = newFullLayout._plots = {};
+    var oldSubplots = oldFullLayout._plots || {};
+    var newSubplots = newFullLayout._plots = {};
+    var newSubplotList = newFullLayout._subplots;
 
     var mockGd = {
         _fullData: newFullData,
         _fullLayout: newFullLayout
     };
 
-    var ids = Plotly.Axes.getSubplots(mockGd);
+    var ids = newSubplotList.cartesian.concat(newSubplotList.gl2d || []);
 
-    var i;
+    var i, j, id, ax;
+
+    // sort subplot lists
+    for(var subplotType in newSubplotList) {
+        newSubplotList[subplotType].sort(Lib.subplotSort);
+    }
 
     for(i = 0; i < ids.length; i++) {
-        var id = ids[i];
+        id = ids[i];
         var oldSubplot = oldSubplots[id];
-        var xaxis = Plotly.Axes.getFromId(mockGd, id, 'x');
-        var yaxis = Plotly.Axes.getFromId(mockGd, id, 'y');
+        var xaxis = axisIDs.getFromId(mockGd, id, 'x');
+        var yaxis = axisIDs.getFromId(mockGd, id, 'y');
         var plotinfo;
 
         if(oldSubplot) {
@@ -812,7 +728,7 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa
         // find this out here, once of for all.
         plotinfo._hasClipOnAxisFalse = false;
 
-        for(var j = 0; j < newFullData.length; j++) {
+        for(j = 0; j < newFullData.length; j++) {
             var trace = newFullData[j];
 
             if(
@@ -828,13 +744,13 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa
 
     // while we're at it, link overlaying axes to their main axes and
     // anchored axes to the axes they're anchored to
-    var axList = Plotly.Axes.list(mockGd, null, true);
+    var axList = axisIDs.list(mockGd, null, true);
     for(i = 0; i < axList.length; i++) {
-        var ax = axList[i];
+        ax = axList[i];
         var mainAx = null;
 
         if(ax.overlaying) {
-            mainAx = Plotly.Axes.getFromId(mockGd, ax.overlaying);
+            mainAx = axisIDs.getFromId(mockGd, ax.overlaying);
 
             // you cannot overlay an axis that's already overlaying another
             if(mainAx && mainAx.overlaying) {
@@ -856,7 +772,44 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa
 
         ax._anchorAxis = ax.anchor === 'free' ?
             null :
-            Plotly.Axes.getFromId(mockGd, ax.anchor);
+            axisIDs.getFromId(mockGd, ax.anchor);
+    }
+
+    for(i = 0; i < axList.length; i++) {
+        // Figure out which subplot to draw ticks, labels, & axis lines on
+        // do this as a separate loop so we already have all the
+        // _mainAxis and _anchorAxis links set
+        ax = axList[i];
+        var isX = ax._id.charAt(0) === 'x';
+        var anchorAx = ax._mainAxis._anchorAxis;
+        var mainSubplotID = '';
+        var nextBestMainSubplotID = '';
+        var anchorID = '';
+        // First try the main ID with the anchor
+        if(anchorAx) {
+            anchorID = anchorAx._mainAxis._id;
+            mainSubplotID = isX ? (ax._id + anchorID) : (anchorID + ax._id);
+        }
+        // Then look for a subplot with the counteraxis overlaying the anchor
+        // If that fails just use the first subplot including this axis
+        if(!mainSubplotID || ids.indexOf(mainSubplotID) === -1) {
+            mainSubplotID = '';
+            for(j = 0; j < ids.length; j++) {
+                id = ids[j];
+                var yIndex = id.indexOf('y');
+                var idPart = isX ? id.substr(0, yIndex) : id.substr(yIndex);
+                var counterPart = isX ? id.substr(yIndex) : id.substr(0, yIndex);
+                if(idPart === ax._id) {
+                    if(!nextBestMainSubplotID) nextBestMainSubplotID = id;
+                    var counterAx = axisIDs.getFromId(mockGd, counterPart);
+                    if(anchorID && counterAx.overlaying === anchorID) {
+                        mainSubplotID = id;
+                        break;
+                    }
+                }
+            }
+        }
+        ax._mainSubplot = mainSubplotID || nextBestMainSubplotID;
     }
 };
 
@@ -906,10 +859,12 @@ plots.clearExpandedTraceDefaultColors = function(trace) {
 
 
 plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) {
+    var modules = fullLayout._modules;
+    var basePlotModules = fullLayout._basePlotModules;
+    var cnt = 0;
+    var colorCnt = 0;
+
     var i, fullTrace, trace;
-    var modules = fullLayout._modules = [],
-        basePlotModules = fullLayout._basePlotModules = [],
-        cnt = 0;
 
     fullLayout._transformModules = [];
 
@@ -919,10 +874,19 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) {
         var _module = fullTrace._module;
         if(!_module) return;
 
-        Lib.pushUnique(modules, _module);
+        if(fullTrace.visible === true) Lib.pushUnique(modules, _module);
         Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule);
 
         cnt++;
+
+        // TODO: do we really want color not to increment for explicitly invisible traces?
+        // This logic is weird, but matches previous behavior: traces that you explicitly
+        // set to visible:false do not increment the color, but traces WE determine to be
+        // empty or invalid (and thus set to visible:false) DO increment color.
+        // I kind of think we should just let all traces increment color, visible or not.
+        // see mock: axes-autotype-empty vs. a test of restyling visible: false that
+        // I can't find right now...
+        if(fullTrace._input.visible !== false) colorCnt++;
     }
 
     var carpetIndex = {};
@@ -930,7 +894,7 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) {
 
     for(i = 0; i < dataIn.length; i++) {
         trace = dataIn[i];
-        fullTrace = plots.supplyTraceDefaults(trace, cnt, fullLayout, i);
+        fullTrace = plots.supplyTraceDefaults(trace, colorCnt, fullLayout, i);
 
         fullTrace.index = i;
         fullTrace._input = trace;
@@ -1076,49 +1040,63 @@ plots.supplyFrameDefaults = function(frameIn) {
     return frameOut;
 };
 
-plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInIndex) {
+plots.supplyTraceDefaults = function(traceIn, colorIndex, layout, traceInIndex) {
     var colorway = layout.colorway || Color.defaults;
     var traceOut = {},
-        defaultColor = colorway[traceOutIndex % colorway.length];
+        defaultColor = colorway[colorIndex % colorway.length];
+
+    var i;
 
     function coerce(attr, dflt) {
         return Lib.coerce(traceIn, traceOut, plots.attributes, attr, dflt);
     }
 
-    function coerceSubplotAttr(subplotType, subplotAttr) {
-        if(!plots.traceIs(traceOut, subplotType)) return;
-
-        return Lib.coerce(traceIn, traceOut,
-            plots.subplotsRegistry[subplotType].attributes, subplotAttr);
-    }
-
     var visible = coerce('visible');
 
     coerce('type');
     coerce('uid');
     coerce('name', layout._traceWord + ' ' + traceInIndex);
 
-    // coerce subplot attributes of all registered subplot types
-    var subplotTypes = Object.keys(subplotsRegistry);
-    for(var i = 0; i < subplotTypes.length; i++) {
-        var subplotType = subplotTypes[i];
-
-        // done below (only when visible is true)
-        // TODO unified this pattern
-        if(['cartesian', 'gl2d'].indexOf(subplotType) !== -1) continue;
-
-        var attr = subplotsRegistry[subplotType].attr;
+    // we want even invisible traces to make their would-be subplots visible
+    // so coerce the subplot id(s) now no matter what
+    var _module = plots.getModule(traceOut);
+    traceOut._module = _module;
+    if(_module) {
+        var basePlotModule = _module.basePlotModule;
+        var subplotAttr = basePlotModule.attr;
+        if(subplotAttr) {
+            var subplots = layout._subplots;
+            var subplotAttrs = basePlotModule.attributes;
+            var subplotId = '';
+
+            // TODO - currently if we draw an empty gl2d subplot, it draws
+            // nothing then gets stuck and you can't get it back without newPlot
+            // sort this out in the regl refactor? but for now just drop empty gl2d subplots
+            if(basePlotModule.name !== 'gl2d' || visible) {
+                if(Array.isArray(subplotAttr)) {
+                    for(i = 0; i < subplotAttr.length; i++) {
+                        var attri = subplotAttr[i];
+                        var vali = Lib.coerce(traceIn, traceOut, subplotAttrs, attri);
+
+                        if(subplots[attri]) Lib.pushUnique(subplots[attri], vali);
+                        subplotId += vali;
+                    }
+                }
+                else {
+                    subplotId = Lib.coerce(traceIn, traceOut, subplotAttrs, subplotAttr);
+                }
 
-        if(attr) coerceSubplotAttr(subplotType, attr);
+                if(subplots[basePlotModule.name]) {
+                    Lib.pushUnique(subplots[basePlotModule.name], subplotId);
+                }
+            }
+        }
     }
 
     if(visible) {
         coerce('customdata');
         coerce('ids');
 
-        var _module = plots.getModule(traceOut);
-        traceOut._module = _module;
-
         if(plots.traceIs(traceOut, 'showLegend')) {
             coerce('showlegend');
             coerce('legendgroup');
@@ -1138,12 +1116,6 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde
 
         if(!plots.traceIs(traceOut, 'noOpacity')) coerce('opacity');
 
-        coerceSubplotAttr('cartesian', 'xaxis');
-        coerceSubplotAttr('cartesian', 'yaxis');
-
-        coerceSubplotAttr('gl2d', 'xaxis');
-        coerceSubplotAttr('gl2d', 'yaxis');
-
         if(plots.traceIs(traceOut, 'notLegendIsolatable')) {
             // This clears out the legendonly state for traces like carpet that
             // cannot be isolated in the legend
@@ -1377,21 +1349,37 @@ function calculateReservedMargins(margins) {
 }
 
 plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, transitionData) {
-    var i, _module;
+    var componentsRegistry = Registry.componentsRegistry;
+    var basePlotModules = layoutOut._basePlotModules;
+    var component, i, _module;
+
+    var Cartesian = Registry.subplotsRegistry.cartesian;
+
+    // check if any components need to add more base plot modules
+    // that weren't captured by traces
+    for(component in componentsRegistry) {
+        _module = componentsRegistry[component];
+
+        if(_module.includeBasePlot) {
+            _module.includeBasePlot(layoutIn, layoutOut);
+        }
+    }
 
-    // can't be be part of basePlotModules loop
-    // in order to handle the orphan axes case
-    Plotly.Axes.supplyLayoutDefaults(layoutIn, layoutOut, fullData);
+    // make sure we *at least* have some cartesian axes
+    if(!basePlotModules.length) {
+        basePlotModules.push(Cartesian);
+    }
+
+    // ensure all cartesian axes have at least one subplot
+    if(layoutOut._has('cartesian')) {
+        Cartesian.finalizeSubplots(layoutIn, layoutOut);
+    }
 
     // base plot module layout defaults
-    var basePlotModules = layoutOut._basePlotModules;
     for(i = 0; i < basePlotModules.length; i++) {
         _module = basePlotModules[i];
 
-        // done above already
-        if(_module.name === 'cartesian') continue;
-
-        // e.g. gl2d does not have a layout-defaults step
+        // e.g. pie does not have a layout-defaults step
         if(_module.supplyLayoutDefaults) {
             _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData);
         }
@@ -1417,9 +1405,8 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans
         }
     }
 
-    var components = Object.keys(Registry.componentsRegistry);
-    for(i = 0; i < components.length; i++) {
-        _module = Registry.componentsRegistry[components[i]];
+    for(component in componentsRegistry) {
+        _module = componentsRegistry[component];
 
         if(_module.supplyLayoutDefaults) {
             _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData);
@@ -1603,10 +1590,7 @@ plots.doAutoMargin = function(gd) {
         // now cycle through all the combinations of l and r
         // (and t and b) to find the required margins
 
-        var pmKeys = Object.keys(pm);
-
-        for(var i = 0; i < pmKeys.length; i++) {
-            var k1 = pmKeys[i];
+        for(var k1 in pm) {
 
             var pushleft = pm[k1].l || {},
                 pushbottom = pm[k1].b || {},
@@ -1615,9 +1599,7 @@ plots.doAutoMargin = function(gd) {
                 fb = pushbottom.val,
                 pb = pushbottom.size;
 
-            for(var j = 0; j < pmKeys.length; j++) {
-                var k2 = pmKeys[j];
-
+            for(var k2 in pm) {
                 if(isNumeric(pl) && pm[k2].r) {
                     var fr = pm[k2].r.val,
                         pr = pm[k2].r.size;
@@ -2283,7 +2265,7 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts)
 };
 
 plots.doCalcdata = function(gd, traces) {
-    var axList = Plotly.Axes.list(gd),
+    var axList = axisIDs.list(gd),
         fullData = gd._fullData,
         fullLayout = gd._fullLayout;
 
@@ -2466,31 +2448,24 @@ plots.generalUpdatePerTraceModule = function(subplot, subplotCalcData, subplotLa
         }
     }
 
-    var moduleNamesOld = Object.keys(traceHashOld);
-    var moduleNames = Object.keys(traceHash);
-
     // when a trace gets deleted, make sure that its module's
     // plot method is called so that it is properly
     // removed from the DOM.
-    for(i = 0; i < moduleNamesOld.length; i++) {
-        var moduleName = moduleNamesOld[i];
+    for(var moduleNameOld in traceHashOld) {
 
-        if(moduleNames.indexOf(moduleName) === -1) {
-            var fakeCalcTrace = traceHashOld[moduleName][0],
+        if(!traceHash[moduleNameOld]) {
+            var fakeCalcTrace = traceHashOld[moduleNameOld][0],
                 fakeTrace = fakeCalcTrace[0].trace;
 
             fakeTrace.visible = false;
-            traceHash[moduleName] = [fakeCalcTrace];
+            traceHash[moduleNameOld] = [fakeCalcTrace];
         }
     }
 
-    // update list of module names to include 'fake' traces added above
-    moduleNames = Object.keys(traceHash);
-
     // call module plot method
-    for(i = 0; i < moduleNames.length; i++) {
-        var moduleCalcData = traceHash[moduleNames[i]],
-            _module = moduleCalcData[0][0].trace._module;
+    for(var moduleName in traceHash) {
+        var moduleCalcData = traceHash[moduleName];
+        var _module = moduleCalcData[0][0].trace._module;
 
         _module.plot(subplot, filterVisible(moduleCalcData), subplotLayout);
     }
diff --git a/src/plots/subplot_defaults.js b/src/plots/subplot_defaults.js
index 1da3202973d..3e1d49900d1 100644
--- a/src/plots/subplot_defaults.js
+++ b/src/plots/subplot_defaults.js
@@ -10,7 +10,6 @@
 'use strict';
 
 var Lib = require('../lib');
-var Plots = require('./plots');
 
 
 /**
@@ -41,13 +40,13 @@ var Plots = require('./plots');
  * }
  */
 module.exports = function handleSubplotDefaults(layoutIn, layoutOut, fullData, opts) {
-    var subplotType = opts.type,
-        subplotAttributes = opts.attributes,
-        handleDefaults = opts.handleDefaults,
-        partition = opts.partition || 'x';
+    var subplotType = opts.type;
+    var subplotAttributes = opts.attributes;
+    var handleDefaults = opts.handleDefaults;
+    var partition = opts.partition || 'x';
 
-    var ids = Plots.findSubplotIds(fullData, subplotType),
-        idsLength = ids.length;
+    var ids = layoutOut._subplots[subplotType];
+    var idsLength = ids.length;
 
     var subplotLayoutIn, subplotLayoutOut;
 
diff --git a/src/plots/ternary/index.js b/src/plots/ternary/index.js
index a4227ecbc01..d6a210d9d03 100644
--- a/src/plots/ternary/index.js
+++ b/src/plots/ternary/index.js
@@ -11,7 +11,7 @@
 
 var Ternary = require('./ternary');
 
-var Plots = require('../../plots/plots');
+var getSubplotCalcData = require('../../plots/get_data').getSubplotCalcData;
 var counterRegex = require('../../lib').counterRegex;
 var TERNARY = 'ternary';
 
@@ -30,13 +30,13 @@ exports.layoutAttributes = require('./layout/layout_attributes');
 exports.supplyLayoutDefaults = require('./layout/defaults');
 
 exports.plot = function plotTernary(gd) {
-    var fullLayout = gd._fullLayout,
-        calcData = gd.calcdata,
-        ternaryIds = Plots.getSubplotIds(fullLayout, TERNARY);
+    var fullLayout = gd._fullLayout;
+    var calcData = gd.calcdata;
+    var ternaryIds = fullLayout._subplots[TERNARY];
 
     for(var i = 0; i < ternaryIds.length; i++) {
         var ternaryId = ternaryIds[i],
-            ternaryCalcData = Plots.getSubplotCalcData(calcData, TERNARY, ternaryId),
+            ternaryCalcData = getSubplotCalcData(calcData, TERNARY, ternaryId),
             ternary = fullLayout[ternaryId]._subplot;
 
         // If ternary is not instantiated, create one!
@@ -57,7 +57,7 @@ exports.plot = function plotTernary(gd) {
 };
 
 exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
-    var oldTernaryKeys = Plots.getSubplotIds(oldFullLayout, TERNARY);
+    var oldTernaryKeys = oldFullLayout._subplots[TERNARY] || [];
 
     for(var i = 0; i < oldTernaryKeys.length; i++) {
         var oldTernaryKey = oldTernaryKeys[i];
diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js
index 1240c653c60..9068a09576f 100644
--- a/src/plots/ternary/ternary.js
+++ b/src/plots/ternary/ternary.js
@@ -156,6 +156,9 @@ proto.updateLayers = function(ternaryLayout) {
             } else if(d === 'grids') {
                 grids.forEach(function(d) {
                     layers[d] = s.append('g').classed('grid ' + d, true);
+
+                    var fictID = (d === 'bgrid') ? 'x' : 'y';
+                    layers[d].append('g').classed(fictID, true);
                 });
             }
         });
@@ -318,16 +321,19 @@ proto.adjustLayout = function(ternaryLayout, graphSize) {
 
     // TODO: shift axes to accommodate linewidth*sin(30) tick mark angle
 
-    var bTransform = 'translate(' + x0 + ',' + (y0 + h) + ')';
+    // TODO: there's probably an easier way to handle these translations/offsets now...
+    var bTransform = 'translate(' + (x0 - baxis._offset) + ',' + (y0 + h) + ')';
 
     _this.layers.baxis.attr('transform', bTransform);
     _this.layers.bgrid.attr('transform', bTransform);
 
-    var aTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + ')rotate(30)';
+    var aTransform = 'translate(' + (x0 + w / 2) + ',' + y0 +
+        ')rotate(30)translate(0,-' + aaxis._offset + ')';
     _this.layers.aaxis.attr('transform', aTransform);
     _this.layers.agrid.attr('transform', aTransform);
 
-    var cTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + ')rotate(-30)';
+    var cTransform = 'translate(' + (x0 + w / 2) + ',' + y0 +
+        ')rotate(-30)translate(0,-' + caxis._offset + ')';
     _this.layers.caxis.attr('transform', cTransform);
     _this.layers.cgrid.attr('transform', cTransform);
 
diff --git a/src/snapshot/cloneplot.js b/src/snapshot/cloneplot.js
index a03e2667b6c..0b4aff611ab 100644
--- a/src/snapshot/cloneplot.js
+++ b/src/snapshot/cloneplot.js
@@ -10,7 +10,6 @@
 'use strict';
 
 var Lib = require('../lib');
-var Plots = require('../plots/plots');
 
 var extendFlat = Lib.extendFlat;
 var extendDeep = Lib.extendDeep;
@@ -101,8 +100,11 @@ module.exports = function clonePlot(graphObj, options) {
         }
     }
 
-    var sceneIds = Plots.getSubplotIds(newLayout, 'gl3d');
-
+    // TODO: does this scene modification really belong here?
+    // If we still need it, can it move into the gl3d module?
+    var sceneIds = Object.keys(newLayout).filter(function(key) {
+        return key.match(/^scene\d*$/);
+    });
     if(sceneIds.length) {
         var axesImageOverride = {};
         if(options.tileClass === 'thumbnail') {
diff --git a/src/traces/parcoords/base_plot.js b/src/traces/parcoords/base_plot.js
index c562787b289..fd6ce675b5b 100644
--- a/src/traces/parcoords/base_plot.js
+++ b/src/traces/parcoords/base_plot.js
@@ -9,22 +9,22 @@
 'use strict';
 
 var d3 = require('d3');
-var Plots = require('../../plots/plots');
+var getModuleCalcData = require('../../plots/get_data').getModuleCalcData;
 var parcoordsPlot = require('./plot');
 var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
 
-exports.name = 'parcoords';
+var PARCOORDS = 'parcoords';
 
-exports.attr = 'type';
+exports.name = PARCOORDS;
 
 exports.plot = function(gd) {
-    var calcData = Plots.getSubplotCalcData(gd.calcdata, 'parcoords', 'parcoords');
+    var calcData = getModuleCalcData(gd.calcdata, PARCOORDS);
     if(calcData.length) parcoordsPlot(gd, calcData);
 };
 
 exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
-    var hadParcoords = (oldFullLayout._has && oldFullLayout._has('parcoords'));
-    var hasParcoords = (newFullLayout._has && newFullLayout._has('parcoords'));
+    var hadParcoords = (oldFullLayout._has && oldFullLayout._has(PARCOORDS));
+    var hasParcoords = (newFullLayout._has && newFullLayout._has(PARCOORDS));
 
     if(hadParcoords && !hasParcoords) {
         oldFullLayout._paperdiv.selectAll('.parcoords').remove();
diff --git a/src/traces/sankey/base_plot.js b/src/traces/sankey/base_plot.js
index 942dfd58b6e..7126f50fa32 100644
--- a/src/traces/sankey/base_plot.js
+++ b/src/traces/sankey/base_plot.js
@@ -9,26 +9,26 @@
 'use strict';
 
 var overrideAll = require('../../plot_api/edit_types').overrideAll;
-var Plots = require('../../plots/plots');
+var getModuleCalcData = require('../../plots/get_data').getModuleCalcData;
 var plot = require('./plot');
 var fxAttrs = require('../../components/fx/layout_attributes');
 
-exports.name = 'sankey';
+var SANKEY = 'sankey';
 
-exports.attr = 'type';
+exports.name = SANKEY;
 
 exports.baseLayoutAttrOverrides = overrideAll({
     hoverlabel: fxAttrs.hoverlabel
 }, 'plot', 'nested');
 
 exports.plot = function(gd) {
-    var calcData = Plots.getSubplotCalcData(gd.calcdata, 'sankey', 'sankey');
-    if(calcData.length) plot(gd, calcData);
+    var calcData = getModuleCalcData(gd.calcdata, SANKEY);
+    plot(gd, calcData);
 };
 
 exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
-    var hadPlot = (oldFullLayout._has && oldFullLayout._has('sankey'));
-    var hasPlot = (newFullLayout._has && newFullLayout._has('sankey'));
+    var hadPlot = (oldFullLayout._has && oldFullLayout._has(SANKEY));
+    var hasPlot = (newFullLayout._has && newFullLayout._has(SANKEY));
 
     if(hadPlot && !hasPlot) {
         oldFullLayout._paperdiv.selectAll('.sankey').remove();
diff --git a/src/traces/table/base_plot.js b/src/traces/table/base_plot.js
index c68e3de2dbd..89bd4f4db9d 100644
--- a/src/traces/table/base_plot.js
+++ b/src/traces/table/base_plot.js
@@ -8,21 +8,21 @@
 
 'use strict';
 
-var Plots = require('../../plots/plots');
+var getModuleCalcData = require('../../plots/get_data').getModuleCalcData;
 var tablePlot = require('./plot');
 
-exports.name = 'table';
+var TABLE = 'table';
 
-exports.attr = 'type';
+exports.name = TABLE;
 
 exports.plot = function(gd) {
-    var calcData = Plots.getSubplotCalcData(gd.calcdata, 'table', 'table');
+    var calcData = getModuleCalcData(gd.calcdata, TABLE);
     if(calcData.length) tablePlot(gd, calcData);
 };
 
 exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
-    var hadTable = (oldFullLayout._has && oldFullLayout._has('table'));
-    var hasTable = (newFullLayout._has && newFullLayout._has('table'));
+    var hadTable = (oldFullLayout._has && oldFullLayout._has(TABLE));
+    var hasTable = (newFullLayout._has && newFullLayout._has(TABLE));
 
     if(hadTable && !hasTable) {
         oldFullLayout._paperdiv.selectAll('.table').remove();
diff --git a/tasks/test_syntax.js b/tasks/test_syntax.js
index 9c6e9804bd6..2262d4ff095 100644
--- a/tasks/test_syntax.js
+++ b/tasks/test_syntax.js
@@ -261,7 +261,7 @@ function assertCircularDeps() {
         var logs = [];
 
         // see https://github.com/plotly/plotly.js/milestone/9
-        var MAX_ALLOWED_CIRCULAR_DEPS = 17;
+        var MAX_ALLOWED_CIRCULAR_DEPS = 16;
 
         if(circularDeps.length > MAX_ALLOWED_CIRCULAR_DEPS) {
             console.log(circularDeps.join('\n'));
diff --git a/test/image/baselines/20.png b/test/image/baselines/20.png
index abfccd22f1b..2e8b83f7def 100644
Binary files a/test/image/baselines/20.png and b/test/image/baselines/20.png differ
diff --git a/test/image/baselines/airfoil.png b/test/image/baselines/airfoil.png
index 81c0340bdec..4edfcadbd74 100644
Binary files a/test/image/baselines/airfoil.png and b/test/image/baselines/airfoil.png differ
diff --git a/test/image/baselines/cheater_contour.png b/test/image/baselines/cheater_contour.png
index 2c3645d5778..8c7c33b9e57 100644
Binary files a/test/image/baselines/cheater_contour.png and b/test/image/baselines/cheater_contour.png differ
diff --git a/test/image/baselines/empty.png b/test/image/baselines/empty.png
index 6e9598c9fa5..d8494d3b276 100644
Binary files a/test/image/baselines/empty.png and b/test/image/baselines/empty.png differ
diff --git a/test/image/baselines/geo_multiple-usa-choropleths.png b/test/image/baselines/geo_multiple-usa-choropleths.png
index bfd37cca985..b90c2d32de0 100644
Binary files a/test/image/baselines/geo_multiple-usa-choropleths.png and b/test/image/baselines/geo_multiple-usa-choropleths.png differ
diff --git a/test/image/baselines/gl3d_ibm-plot.png b/test/image/baselines/gl3d_ibm-plot.png
index 946b58ea7d1..ba049929342 100644
Binary files a/test/image/baselines/gl3d_ibm-plot.png and b/test/image/baselines/gl3d_ibm-plot.png differ
diff --git a/test/image/baselines/gl3d_opacity-surface.png b/test/image/baselines/gl3d_opacity-surface.png
index ac6e2fd016c..b0ae82b5ad7 100644
Binary files a/test/image/baselines/gl3d_opacity-surface.png and b/test/image/baselines/gl3d_opacity-surface.png differ
diff --git a/test/image/baselines/multiple_axes_double.png b/test/image/baselines/multiple_axes_double.png
index e053363b4a8..a003e3754e7 100644
Binary files a/test/image/baselines/multiple_axes_double.png and b/test/image/baselines/multiple_axes_double.png differ
diff --git a/test/image/baselines/multiple_axes_multiple.png b/test/image/baselines/multiple_axes_multiple.png
index 7cf933fe0e7..d4bd58f54a1 100644
Binary files a/test/image/baselines/multiple_axes_multiple.png and b/test/image/baselines/multiple_axes_multiple.png differ
diff --git a/test/image/baselines/overlaying-axis-lines.png b/test/image/baselines/overlaying-axis-lines.png
index 7df0a3d4360..8d5a6d95ca5 100644
Binary files a/test/image/baselines/overlaying-axis-lines.png and b/test/image/baselines/overlaying-axis-lines.png differ
diff --git a/test/image/baselines/ternary_axis_layers.png b/test/image/baselines/ternary_axis_layers.png
index e2e02dbf1b1..36daec4b21d 100644
Binary files a/test/image/baselines/ternary_axis_layers.png and b/test/image/baselines/ternary_axis_layers.png differ
diff --git a/test/image/baselines/titles-avoid-labels.png b/test/image/baselines/titles-avoid-labels.png
index 58dbb96cef3..35166b37985 100644
Binary files a/test/image/baselines/titles-avoid-labels.png and b/test/image/baselines/titles-avoid-labels.png differ
diff --git a/test/image/baselines/world-cals.png b/test/image/baselines/world-cals.png
index 7bf5e99b49d..200ed69241a 100644
Binary files a/test/image/baselines/world-cals.png and b/test/image/baselines/world-cals.png differ
diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js
index e37a965a6e2..babbe0da795 100644
--- a/test/jasmine/tests/annotations_test.js
+++ b/test/jasmine/tests/annotations_test.js
@@ -25,6 +25,11 @@ describe('Test annotations', function() {
         function _supply(layoutIn, layoutOut) {
             layoutOut = layoutOut || {};
             layoutOut._has = Plots._hasPlotType.bind(layoutOut);
+            layoutOut._subplots = {xaxis: ['x', 'x2'], yaxis: ['y', 'y2']};
+            ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(axName) {
+                if(!layoutOut[axName]) layoutOut[axName] = {type: 'linear', range: [0, 1]};
+                Axes.setConvert(layoutOut[axName]);
+            });
 
             Annotations.supplyLayoutDefaults(layoutIn, layoutOut);
 
@@ -89,7 +94,6 @@ describe('Test annotations', function() {
             var layoutOut = {
                 xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] }
             };
-            Axes.setConvert(layoutOut.xaxis);
 
             _supply(layoutIn, layoutOut);
 
@@ -132,10 +136,6 @@ describe('Test annotations', function() {
                 yaxis2: {type: 'category', range: [0, 1]}
             };
 
-            ['xaxis', 'xaxis2', 'yaxis', 'yaxis2'].forEach(function(k) {
-                Axes.setConvert(layoutOut[k]);
-            });
-
             _supply(layoutIn, layoutOut);
 
             expect(layoutOut.annotations[0]._xclick).toBe(10, 'paper x');
@@ -721,8 +721,8 @@ describe('annotations autorange', function() {
                 text: 'LT',
                 x: -1,
                 y: 3,
-                xref: 'x5', // will be converted to 'x' and xaxis should autorange
-                yref: 'y5', // same 'y' -> yaxis
+                xref: 'xq', // will be converted to 'x' and xaxis should autorange
+                yref: 'yz', // same 'y' -> yaxis
                 ax: 50,
                 ay: 50
             }});
diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js
index 2e33ddab044..9c05bf23584 100644
--- a/test/jasmine/tests/axes_test.js
+++ b/test/jasmine/tests/axes_test.js
@@ -8,8 +8,10 @@ var Color = require('@src/components/color');
 var tinycolor = require('tinycolor2');
 
 var handleTickValueDefaults = require('@src/plots/cartesian/tick_value_defaults');
+var Cartesian = require('@src/plots/cartesian');
 var Axes = require('@src/plots/cartesian/axes');
 var Fx = require('@src/components/fx');
+var supplyLayoutDefaults = require('@src/plots/cartesian/layout_defaults');
 
 var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
@@ -180,13 +182,12 @@ describe('Test axes', function() {
             layoutOut = {
                 _has: Plots._hasPlotType,
                 _basePlotModules: [],
-                _dfltTitle: {x: 'x', y: 'y'}
+                _dfltTitle: {x: 'x', y: 'y'},
+                _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}
             };
             fullData = [];
         });
 
-        var supplyLayoutDefaults = Axes.supplyLayoutDefaults;
-
         it('should set undefined linewidth/linecolor if linewidth, linecolor or showline is not supplied', function() {
             layoutIn = {
                 xaxis: {},
@@ -269,69 +270,6 @@ describe('Test axes', function() {
             expect(layoutOut.xaxis.zerolinecolor).toBe(undefined);
         });
 
-        it('should detect orphan axes (lone axes case)', function() {
-            layoutIn = {
-                xaxis: {},
-                yaxis: {}
-            };
-            fullData = [];
-
-            supplyLayoutDefaults(layoutIn, layoutOut, fullData);
-            expect(layoutOut._basePlotModules[0].name).toEqual('cartesian');
-        });
-
-        it('should detect orphan axes (gl2d trace conflict case)', function() {
-            layoutIn = {
-                xaxis: {},
-                yaxis: {}
-            };
-            fullData = [{
-                type: 'scattergl',
-                xaxis: 'x',
-                yaxis: 'y'
-            }];
-
-            supplyLayoutDefaults(layoutIn, layoutOut, fullData);
-            expect(layoutOut._basePlotModules).toEqual([]);
-        });
-
-        it('should detect orphan axes (gl2d + cartesian case)', function() {
-            layoutIn = {
-                xaxis2: {},
-                yaxis2: {}
-            };
-            fullData = [{
-                type: 'scattergl',
-                xaxis: 'x',
-                yaxis: 'y'
-            }];
-
-            supplyLayoutDefaults(layoutIn, layoutOut, fullData);
-            expect(layoutOut._basePlotModules[0].name).toEqual('cartesian');
-        });
-
-        it('should detect orphan axes (gl3d present case)', function() {
-            layoutIn = {
-                xaxis: {},
-                yaxis: {}
-            };
-            layoutOut._basePlotModules = [ { name: 'gl3d' }];
-
-            supplyLayoutDefaults(layoutIn, layoutOut, fullData);
-            expect(layoutOut._basePlotModules).toEqual([ { name: 'gl3d' }]);
-        });
-
-        it('should detect orphan axes (geo present case)', function() {
-            layoutIn = {
-                xaxis: {},
-                yaxis: {}
-            };
-            layoutOut._basePlotModules = [ { name: 'geo' }];
-
-            supplyLayoutDefaults(layoutIn, layoutOut, fullData);
-            expect(layoutOut._basePlotModules).toEqual([ { name: 'geo' }]);
-        });
-
         it('should use \'axis.color\' as default for \'axis.titlefont.color\'', function() {
             layoutIn = {
                 xaxis: { color: 'red' },
@@ -339,7 +277,9 @@ describe('Test axes', function() {
                 yaxis2: { titlefont: { color: 'yellow' } }
             };
 
-            layoutOut.font = { color: 'blue' },
+            layoutOut.font = { color: 'blue' };
+            layoutOut._subplots.cartesian.push('xy2');
+            layoutOut._subplots.yaxis.push('y2');
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
             expect(layoutOut.xaxis.titlefont.color).toEqual('red');
@@ -353,6 +293,8 @@ describe('Test axes', function() {
                 yaxis: { linecolor: 'blue' },
                 yaxis2: { showline: true }
             };
+            layoutOut._subplots.cartesian.push('xy2');
+            layoutOut._subplots.yaxis.push('y2');
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
             expect(layoutOut.xaxis.linecolor).toEqual('red');
@@ -366,6 +308,8 @@ describe('Test axes', function() {
                 yaxis: { zerolinecolor: 'blue' },
                 yaxis2: { showzeroline: true }
             };
+            layoutOut._subplots.cartesian.push('xy2');
+            layoutOut._subplots.yaxis.push('y2');
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
             expect(layoutOut.xaxis.zerolinecolor).toEqual('red');
@@ -381,6 +325,8 @@ describe('Test axes', function() {
                 yaxis: { gridcolor: 'blue' },
                 yaxis2: { showgrid: true }
             };
+            layoutOut._subplots.cartesian.push('xy2');
+            layoutOut._subplots.yaxis.push('y2');
 
             var bgColor = Color.combine('yellow', 'green'),
                 frac = 100 * (0xe - 0x4) / (0xf - 0x4);
@@ -429,6 +375,8 @@ describe('Test axes', function() {
                 yaxis2: { range: [1, 'b'] },
                 yaxis3: { range: [null, {}] }
             };
+            layoutOut._subplots.cartesian.push('x2y2', 'xy3');
+            layoutOut._subplots.yaxis.push('x2', 'y2', 'y3');
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
 
@@ -444,6 +392,8 @@ describe('Test axes', function() {
                 yaxis: { range: ['1', 2] },
                 yaxis2: { range: [1, '2'] }
             };
+            layoutOut._subplots.cartesian.push('x2y2');
+            layoutOut._subplots.yaxis.push('x2', 'y2');
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
 
@@ -465,6 +415,8 @@ describe('Test axes', function() {
                 xaxis4: {scaleanchor: 'y3', scaleratio: 7},
                 xaxis5: {scaleanchor: 'y3', scaleratio: 9}
             };
+            layoutOut._subplots.cartesian.push('x2y2', 'x3y3', 'x4y3', 'x5y3');
+            layoutOut._subplots.yaxis.push('x2', 'x3', 'x4', 'x5', 'y2', 'y3');
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
 
@@ -496,6 +448,8 @@ describe('Test axes', function() {
                 xaxis4: {scaleanchor: 'x', scaleratio: 13}, // x<->x is OK now
                 yaxis4: {scaleanchor: 'y', scaleratio: 17}, // y<->y is OK now
             };
+            layoutOut._subplots.cartesian.push('x2y2', 'x3y3', 'x4y4');
+            layoutOut._subplots.yaxis.push('x2', 'x3', 'x4', 'y2', 'y3', 'y4');
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
 
@@ -521,6 +475,8 @@ describe('Test axes', function() {
                 yaxis: {scaleanchor: 'x4', scaleratio: 3}, // doesn't exist
                 xaxis2: {scaleanchor: 'yaxis', scaleratio: 5} // must be an id, not a name
             };
+            layoutOut._subplots.cartesian.push('x2y');
+            layoutOut._subplots.yaxis.push('x2');
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
 
@@ -540,6 +496,8 @@ describe('Test axes', function() {
                 xaxis2: {type: 'date', scaleanchor: 'y', scaleratio: 3},
                 yaxis2: {type: 'category', scaleanchor: 'x2', scaleratio: 5}
             };
+            layoutOut._subplots.cartesian.push('x2y2');
+            layoutOut._subplots.yaxis.push('x2', 'y2');
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
 
@@ -562,6 +520,8 @@ describe('Test axes', function() {
                 xaxis2: {},
                 yaxis2: {scaleanchor: 'x', scaleratio: 5}
             };
+            layoutOut._subplots.cartesian.push('x2y2');
+            layoutOut._subplots.yaxis.push('x2', 'y2');
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
 
@@ -1089,7 +1049,7 @@ describe('Test axes', function() {
     describe('handleTickValueDefaults', function() {
         function mockSupplyDefaults(axIn, axOut, axType) {
             function coerce(attr, dflt) {
-                return Lib.coerce(axIn, axOut, Axes.layoutAttributes, attr, dflt);
+                return Lib.coerce(axIn, axOut, Cartesian.layoutAttributes, attr, dflt);
             }
 
             handleTickValueDefaults(axIn, axOut, coerce, axType);
@@ -1282,7 +1242,8 @@ describe('Test axes', function() {
                     xaxis: { range: [0, 0.5] },
                     yaxis: { range: [0, 0.5] },
                     xaxis2: { range: [0.5, 1] },
-                    yaxis2: { range: [0.5, 1] }
+                    yaxis2: { range: [0.5, 1] },
+                    _subplots: {xaxis: ['x', 'x2'], yaxis: ['y', 'y2'], cartesian: ['xy', 'x2y2']}
                 }
             };
         });
@@ -1344,6 +1305,7 @@ describe('Test axes', function() {
         it('returns array of axes in fullLayout', function() {
             gd = {
                 _fullLayout: {
+                    _subplots: {xaxis: ['x'], yaxis: ['y', 'y2']},
                     xaxis: { _id: 'x' },
                     yaxis: { _id: 'y' },
                     yaxis2: { _id: 'y2' }
@@ -1357,6 +1319,7 @@ describe('Test axes', function() {
         it('returns array of axes, including the ones in scenes', function() {
             gd = {
                 _fullLayout: {
+                    _subplots: {xaxis: [], yaxis: [], gl3d: ['scene', 'scene2']},
                     scene: {
                         xaxis: { _id: 'x' },
                         yaxis: { _id: 'y' },
@@ -1380,6 +1343,7 @@ describe('Test axes', function() {
         it('returns array of axes, excluding the ones in scenes with only2d option', function() {
             gd = {
                 _fullLayout: {
+                    _subplots: {xaxis: ['x2'], yaxis: ['y2'], gl3d: ['scene']},
                     scene: {
                         xaxis: { _id: 'x' },
                         yaxis: { _id: 'y' },
@@ -1397,11 +1361,11 @@ describe('Test axes', function() {
         it('returns array of axes, of particular ax letter with axLetter option', function() {
             gd = {
                 _fullLayout: {
+                    _subplots: {xaxis: ['x2'], yaxis: ['y2'], gl3d: ['scene']},
                     scene: {
-                        xaxis: { _id: 'x' },
+                        xaxis: { _id: 'x', _thisIs3d: true },
                         yaxis: { _id: 'y' },
-                        zaxis: { _id: 'z'
-                        }
+                        zaxis: { _id: 'z' }
                     },
                     xaxis2: { _id: 'x2' },
                     yaxis2: { _id: 'y2' }
@@ -1409,54 +1373,28 @@ describe('Test axes', function() {
             };
 
             expect(listFunc(gd, 'x'))
-                .toEqual([{ _id: 'x2' }, { _id: 'x' }]);
+                .toEqual([{ _id: 'x2' }, { _id: 'x', _thisIs3d: true }]);
         });
 
     });
 
     describe('getSubplots', function() {
         var getSubplots = Axes.getSubplots;
-        var gd;
-
-        it('returns list of subplots ids (from data only)', function() {
-            gd = {
-                data: [
-                    { type: 'scatter' },
-                    { type: 'scattergl', xaxis: 'x2', yaxis: 'y2' }
-                ]
-            };
-
-            expect(getSubplots(gd))
-                .toEqual(['xy', 'x2y2']);
-        });
-
-        it('returns list of subplots ids (from fullLayout only)', function() {
-            gd = {
-                _fullLayout: {
-                    xaxis: { _id: 'x', anchor: 'y' },
-                    yaxis: { _id: 'y', anchor: 'x' },
-                    xaxis2: { _id: 'x2', anchor: 'y2' },
-                    yaxis2: { _id: 'y2', anchor: 'x2' }
+        var gd = {
+            _fullLayout: {
+                _subplots: {
+                    cartesian: ['x2y2'],
+                    gl2d: ['xy']
                 }
-            };
+            }
+        };
 
+        it('returns only what was prepopulated in fullLayout._subplots', function() {
             expect(getSubplots(gd))
                 .toEqual(['xy', 'x2y2']);
         });
 
         it('returns list of subplots ids of particular axis with ax option', function() {
-            gd = {
-                data: [
-                    { type: 'scatter' },
-                    { type: 'scattergl', xaxis: 'x3', yaxis: 'y3' }
-                ],
-                _fullLayout: {
-                    xaxis2: { _id: 'x2', anchor: 'y2' },
-                    yaxis2: { _id: 'y2', anchor: 'x2' },
-                    yaxis3: { _id: 'y3', anchor: 'free' }
-                }
-            };
-
             expect(getSubplots(gd, { _id: 'x' }))
                 .toEqual(['xy']);
         });
diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js
index 3ef3fbd7fd1..4ba2c5e27dc 100644
--- a/test/jasmine/tests/cartesian_interact_test.js
+++ b/test/jasmine/tests/cartesian_interact_test.js
@@ -240,8 +240,8 @@ describe('axis zoom/pan and main plot zoom', function() {
         return Plotly.newPlot(gd, data, layout, config)
         .then(checkRanges({}, 'initial'))
         .then(function() {
-            expect(Object.keys(gd._fullLayout._plots))
-                .toEqual(['xy', 'xy2', 'x2y', 'x3y3']);
+            expect(Object.keys(gd._fullLayout._plots).sort())
+                .toEqual(['xy', 'xy2', 'x2y', 'x3y3'].sort());
 
             // nsew, n, ns, s, w, ew, e, ne, nw, se, sw
             expect(document.querySelectorAll('.drag[data-subplot="xy"]').length).toBe(11);
diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js
index 415ccc0e20d..6ebf32f3ef5 100644
--- a/test/jasmine/tests/cartesian_test.js
+++ b/test/jasmine/tests/cartesian_test.js
@@ -281,13 +281,15 @@ describe('subplot creation / deletion:', function() {
 
     it('should clear orphan subplot when adding traces to blank graph', function(done) {
 
-        function assertCartesianSubplot(len) {
+        function assertOrphanSubplot(len) {
             expect(d3.select('.subplot.xy').size()).toEqual(len);
-            expect(d3.select('.subplot.x2y2').size()).toEqual(len);
-            expect(d3.select('.x2title').size()).toEqual(len);
-            expect(d3.select('.x2title').size()).toEqual(len);
             expect(d3.select('.ytitle').size()).toEqual(len);
             expect(d3.select('.ytitle').size()).toEqual(len);
+
+            // we only make one orphan subplot now
+            expect(d3.select('.subplot.x2y2').size()).toEqual(0);
+            expect(d3.select('.x2title').size()).toEqual(0);
+            expect(d3.select('.x2title').size()).toEqual(0);
         }
 
         Plotly.plot(gd, [], {
@@ -297,7 +299,7 @@ describe('subplot creation / deletion:', function() {
             yaxis2: { title: 'Y2', anchor: 'x2' }
         })
         .then(function() {
-            assertCartesianSubplot(1);
+            assertOrphanSubplot(1);
 
             return Plotly.addTraces(gd, [{
                 type: 'scattergeo',
@@ -306,7 +308,7 @@ describe('subplot creation / deletion:', function() {
             }]);
         })
         .then(function() {
-            assertCartesianSubplot(0);
+            assertOrphanSubplot(0);
         })
         .catch(failTest)
         .then(done);
@@ -485,11 +487,11 @@ describe('subplot creation / deletion:', function() {
             var info = d3.select('.infolayer');
 
             expect(g.selectAll('.xtick').size()).toBe(xaxis[0], 'x tick cnt');
-            expect(g.selectAll('.gridlayer > .xgrid').size()).toBe(xaxis[1], 'x gridline cnt');
+            expect(g.selectAll('.gridlayer .xgrid').size()).toBe(xaxis[1], 'x gridline cnt');
             expect(info.selectAll('.g-xtitle').size()).toBe(xaxis[2], 'x title cnt');
 
             expect(g.selectAll('.ytick').size()).toBe(yaxis[0], 'y tick cnt');
-            expect(g.selectAll('.gridlayer > .ygrid').size()).toBe(yaxis[1], 'y gridline cnt');
+            expect(g.selectAll('.gridlayer .ygrid').size()).toBe(yaxis[1], 'y gridline cnt');
             expect(info.selectAll('.g-ytitle').size()).toBe(yaxis[2], 'y title cnt');
         }
 
diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js
index 1c425b84357..1d7f7a05ecd 100644
--- a/test/jasmine/tests/fx_test.js
+++ b/test/jasmine/tests/fx_test.js
@@ -24,7 +24,11 @@ describe('Fx defaults', function() {
 
     it('should default (blank version)', function() {
         var layoutOut = _supply().layout;
-        expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest');
+        // we get a blank cartesian subplot that has no traces...
+        // so all traces are horizontal -> hovermode defaults to y
+        // we could add a special case to push this back to x, but
+        // it seems like it has no practical consequence.
+        expect(layoutOut.hovermode).toBe('y', 'hovermode to y');
         expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom');
     });
 
diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js
index 51f8b1e901a..db0b9c71253 100644
--- a/test/jasmine/tests/geo_test.js
+++ b/test/jasmine/tests/geo_test.js
@@ -36,7 +36,7 @@ describe('Test Geo layout defaults', function() {
     var layoutIn, layoutOut, fullData;
 
     beforeEach(function() {
-        layoutOut = {};
+        layoutOut = {_subplots: {geo: ['geo']}};
 
         // needs a geo-ref in a trace in order to be detected
         fullData = [{ type: 'scattergeo', geo: 'geo' }];
@@ -242,6 +242,7 @@ describe('Test Geo layout defaults', function() {
     });
 
     it('should add geo data-only geos into layoutIn (converse)', function() {
+        layoutOut._subplots.geo = [];
         layoutIn = {};
         fullData = [{ type: 'scatter' }];
 
diff --git a/test/jasmine/tests/gl3dlayout_test.js b/test/jasmine/tests/gl3dlayout_test.js
index cbcdaf72ea4..2540a53a6f5 100644
--- a/test/jasmine/tests/gl3dlayout_test.js
+++ b/test/jasmine/tests/gl3dlayout_test.js
@@ -18,7 +18,11 @@ describe('Test Gl3d layout defaults', function() {
         var supplyLayoutDefaults = Gl3d.supplyLayoutDefaults;
 
         beforeEach(function() {
-            layoutOut = { _basePlotModules: ['gl3d'], _dfltTitle: {x: 'xxx', y: 'yyy', colorbar: 'cbbb'} };
+            layoutOut = {
+                _basePlotModules: ['gl3d'],
+                _dfltTitle: {x: 'xxx', y: 'yyy', colorbar: 'cbbb'},
+                _subplots: {gl3d: ['scene']}
+            };
 
             // needs a scene-ref in a trace in order to be detected
             fullData = [ { type: 'scatter3d', scene: 'scene' }];
@@ -239,6 +243,7 @@ describe('Test Gl3d layout defaults', function() {
 
         it('should add scene data-only scenes into layoutIn (converse)', function() {
             layoutIn = {};
+            layoutOut._subplots.gl3d = [];
             fullData = [{ type: 'scatter' }];
 
             supplyLayoutDefaults(layoutIn, layoutOut, fullData);
diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js
index ff3120faa3d..69b0f93a62b 100644
--- a/test/jasmine/tests/gl_plot_interact_test.js
+++ b/test/jasmine/tests/gl_plot_interact_test.js
@@ -309,8 +309,8 @@ describe('Test gl3d plots', function() {
         .then(function() {
             expect(countCanvases()).toEqual(1);
             expect(gd.layout.scene).toEqual(sceneLayout);
-            expect(gd.layout.xaxis).toBeUndefined();
-            expect(gd.layout.yaxis).toBeUndefined();
+            expect(gd.layout.xaxis === undefined).toBe(true);
+            expect(gd.layout.yaxis === undefined).toBe(true);
             expect(gd._fullLayout._has('gl3d')).toBe(true);
             expect(gd._fullLayout.scene._scene).toBeDefined();
 
@@ -322,7 +322,7 @@ describe('Test gl3d plots', function() {
             expect(gd.layout.xaxis).toBeDefined();
             expect(gd.layout.yaxis).toBeDefined();
             expect(gd._fullLayout._has('gl3d')).toBe(false);
-            expect(gd._fullLayout.scene).toBeUndefined();
+            expect(gd._fullLayout.scene === undefined).toBe(true);
 
             return Plotly.restyle(gd, 'type', 'scatter3d');
         })
@@ -349,7 +349,7 @@ describe('Test gl3d plots', function() {
         .then(function() {
             expect(countCanvases()).toEqual(0);
             expect(gd._fullLayout._has('gl3d')).toBe(false);
-            expect(gd._fullLayout.scene).toBeUndefined();
+            expect(gd._fullLayout.scene === undefined).toBe(true);
         })
         .then(done);
     });
@@ -424,7 +424,7 @@ describe('Test gl3d modebar handlers', function() {
     var gd, modeBar;
 
     function assertScenes(cont, attr, val) {
-        var sceneIds = Plots.getSubplotIds(cont, 'gl3d');
+        var sceneIds = cont._subplots.gl3d;
 
         sceneIds.forEach(function(sceneId) {
             var thisVal = Lib.nestedProperty(cont[sceneId], attr).get();
@@ -479,7 +479,7 @@ describe('Test gl3d modebar handlers', function() {
         expect(buttonZoom3d.isActive()).toBe(false);
 
         buttonZoom3d.click();
-        assertScenes(gd.layout, 'dragmode', 'zoom');
+        assertScenes(gd._fullLayout, 'dragmode', 'zoom');
         expect(gd.layout.dragmode).toBe(undefined);
         expect(gd._fullLayout.dragmode).toBe('zoom');
         expect(buttonTurntable.isActive()).toBe(false);
@@ -500,7 +500,7 @@ describe('Test gl3d modebar handlers', function() {
         expect(buttonPan3d.isActive()).toBe(false);
 
         buttonPan3d.click();
-        assertScenes(gd.layout, 'dragmode', 'pan');
+        assertScenes(gd._fullLayout, 'dragmode', 'pan');
         expect(gd.layout.dragmode).toBe(undefined);
         expect(gd._fullLayout.dragmode).toBe('zoom');
         expect(buttonTurntable.isActive()).toBe(false);
@@ -521,7 +521,7 @@ describe('Test gl3d modebar handlers', function() {
         expect(buttonOrbit.isActive()).toBe(false);
 
         buttonOrbit.click();
-        assertScenes(gd.layout, 'dragmode', 'orbit');
+        assertScenes(gd._fullLayout, 'dragmode', 'orbit');
         expect(gd.layout.dragmode).toBe(undefined);
         expect(gd._fullLayout.dragmode).toBe('zoom');
         expect(buttonTurntable.isActive()).toBe(false);
@@ -946,7 +946,12 @@ describe('Test gl2d plots', function() {
         var OBJECT_PER_TRACE = 2;
 
         var objects = function() {
-            return gd._fullLayout._plots.xy._scene2d.glplot.objects;
+            try {
+                return gd._fullLayout._plots.xy._scene2d.glplot.objects;
+            }
+            catch(e) {
+                return [];
+            }
         };
 
         Plotly.plot(gd, _mock)
@@ -969,7 +974,7 @@ describe('Test gl2d plots', function() {
             return Plotly.restyle(gd, 'visible', false);
         })
         .then(function() {
-            expect(gd._fullLayout._plots.xy._scene2d).toBeUndefined();
+            expect(objects().length).toBe(0);
 
             return Plotly.restyle(gd, 'visible', true);
         })
@@ -977,6 +982,7 @@ describe('Test gl2d plots', function() {
             expect(objects().length).toEqual(OBJECT_PER_TRACE);
             expect(objects()[0].data.length).not.toEqual(0);
         })
+        .catch(fail)
         .then(done);
     });
 
@@ -1603,13 +1609,13 @@ describe('Test gl3d annotations', function() {
         })
         .then(function() {
             assertAnnotationCntPerScene('scene', 1);
-            assertAnnotationCntPerScene('scene2', 0);
+            assertAnnotationCntPerScene('scene2', 2);
 
             return Plotly.deleteTraces(gd, [0]);
         })
         .then(function() {
-            assertAnnotationCntPerScene('scene', 0);
-            assertAnnotationCntPerScene('scene2', 0);
+            assertAnnotationCntPerScene('scene', 1);
+            assertAnnotationCntPerScene('scene2', 2);
         })
         .catch(fail)
         .then(done);
diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js
index a102fd9b857..1f0afd929c0 100644
--- a/test/jasmine/tests/heatmap_test.js
+++ b/test/jasmine/tests/heatmap_test.js
@@ -22,7 +22,8 @@ describe('heatmap supplyDefaults', function() {
     var defaultColor = '#444',
         layout = {
             font: Plots.layoutAttributes.font,
-            _dfltTitle: {colorbar: 'cb'}
+            _dfltTitle: {colorbar: 'cb'},
+            _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}
         };
 
     var supplyDefaults = Heatmap.supplyDefaults;
diff --git a/test/jasmine/tests/layout_images_test.js b/test/jasmine/tests/layout_images_test.js
index fb0b2743732..fca0b88747f 100644
--- a/test/jasmine/tests/layout_images_test.js
+++ b/test/jasmine/tests/layout_images_test.js
@@ -21,7 +21,10 @@ describe('Layout images', function() {
 
         beforeEach(function() {
             layoutIn = { images: [] };
-            layoutOut = { _has: Plots._hasPlotType };
+            layoutOut = {
+                _has: Plots._hasPlotType,
+                _subplots: {xaxis: [], yaxis: []}
+            };
         });
 
         it('should reject when there is no `source`', function() {
diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js
index 12b61aaa31a..c9700131069 100644
--- a/test/jasmine/tests/lib_test.js
+++ b/test/jasmine/tests/lib_test.js
@@ -1968,6 +1968,22 @@ describe('Test lib.js:', function() {
             expect(function() { return Lib.relativeAttr('x.y.', 'z'); }).toThrow();
         });
     });
+
+    describe('subplotSort', function() {
+        it('puts xy subplots in the right order', function() {
+            var a = ['x10y', 'x10y20', 'x10y12', 'x10y2', 'xy', 'x2y12', 'xy2', 'xy15'];
+            a.sort(Lib.subplotSort);
+            expect(a).toEqual(['xy', 'xy2', 'xy15', 'x2y12', 'x10y', 'x10y2', 'x10y12', 'x10y20']);
+        });
+
+        it('puts simple subplots in the right order', function() {
+            ['scene', 'geo', 'ternary', 'mapbox'].forEach(function(v) {
+                var a = [v + '100', v + '43', v, v + '10', v + '2'];
+                a.sort(Lib.subplotSort);
+                expect(a).toEqual([v, v + '2', v + '10', v + '43', v + '100']);
+            });
+        });
+    });
 });
 
 describe('Queue', function() {
diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js
index 67dff7bae4e..3ca613b6b36 100644
--- a/test/jasmine/tests/mapbox_test.js
+++ b/test/jasmine/tests/mapbox_test.js
@@ -32,7 +32,7 @@ describe('mapbox defaults', function() {
     var layoutIn, layoutOut, fullData;
 
     beforeEach(function() {
-        layoutOut = { font: { color: 'red' } };
+        layoutOut = { font: { color: 'red' }, _subplots: {mapbox: ['mapbox']} };
 
         // needs a mapbox-ref in a trace in order to be detected
         fullData = [{ type: 'scattermapbox', subplot: 'mapbox' }];
@@ -66,6 +66,7 @@ describe('mapbox defaults', function() {
         };
 
         fullData.push({ type: 'scattermapbox', subplot: 'mapbox2' });
+        layoutOut._subplots.mapbox.push('mapbox2');
 
         supplyLayoutDefaults(layoutIn, layoutOut, fullData);
         expect(layoutOut.mapbox.style).toEqual('light');
@@ -317,7 +318,9 @@ describe('@noCI, mapbox plots', function() {
         expect(countVisibleTraces(gd, modes)).toEqual(2);
 
         Plotly.restyle(gd, 'visible', false).then(function() {
-            expect(gd._fullLayout.mapbox).toBeUndefined();
+            expect(gd._fullLayout.mapbox === undefined).toBe(false);
+
+            expect(countVisibleTraces(gd, modes)).toEqual(0);
 
             return Plotly.restyle(gd, 'visible', true);
         })
@@ -381,7 +384,7 @@ describe('@noCI, mapbox plots', function() {
             return Plotly.deleteTraces(gd, [0, 1, 2]);
         })
         .then(function() {
-            expect(gd._fullLayout.mapbox).toBeUndefined();
+            expect(gd._fullLayout.mapbox === undefined).toBe(true);
 
             done();
         });
diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js
index 9f0bd48b54e..f17dbbd9f8e 100644
--- a/test/jasmine/tests/modebar_test.js
+++ b/test/jasmine/tests/modebar_test.js
@@ -25,12 +25,13 @@ describe('ModeBar', function() {
         return parent;
     }
 
-    function getMockGraphInfo() {
+    function getMockGraphInfo(xaxes, yaxes) {
         return {
             _fullLayout: {
                 dragmode: 'zoom',
                 _paperdiv: d3.select(getMockContainerTree()),
-                _has: Plots._hasPlotType
+                _has: Plots._hasPlotType,
+                _subplots: {xaxis: xaxes || [], yaxis: yaxes || []}
             },
             _fullData: [],
             _context: {
@@ -188,7 +189,7 @@ describe('ModeBar', function() {
                 ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']
             ]);
 
-            var gd = getMockGraphInfo();
+            var gd = getMockGraphInfo(['x'], ['y']);
             gd._fullLayout._basePlotModules = [{ name: 'cartesian' }];
             gd._fullLayout.xaxis = {fixedrange: false};
 
@@ -206,7 +207,7 @@ describe('ModeBar', function() {
                 ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']
             ]);
 
-            var gd = getMockGraphInfo();
+            var gd = getMockGraphInfo(['x'], ['y']);
             gd._fullLayout._basePlotModules = [{ name: 'cartesian' }];
             gd._fullLayout.xaxis = {fixedrange: false};
             gd._fullData = [{
@@ -230,7 +231,7 @@ describe('ModeBar', function() {
                 ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']
             ]);
 
-            var gd = getMockGraphInfo();
+            var gd = getMockGraphInfo(['x'], ['y']);
             gd._fullLayout._basePlotModules = [{ name: 'cartesian' }];
             gd._fullLayout.xaxis = {fixedrange: false};
             gd._fullData = [{
@@ -364,7 +365,7 @@ describe('ModeBar', function() {
                 ['hoverClosestGl2d']
             ]);
 
-            var gd = getMockGraphInfo();
+            var gd = getMockGraphInfo(['x'], ['y']);
             gd._fullLayout._basePlotModules = [{ name: 'gl2d' }];
             gd._fullLayout.xaxis = {fixedrange: false};
 
@@ -427,7 +428,7 @@ describe('ModeBar', function() {
                 ['toggleHover']
             ]);
 
-            var gd = getMockGraphInfo();
+            var gd = getMockGraphInfo(['x'], ['y']);
             gd._fullData = [{
                 type: 'scatter',
                 visible: true,
@@ -569,7 +570,7 @@ describe('ModeBar', function() {
 
         // gives 11 buttons in 5 groups by default
         function setupGraphInfo() {
-            var gd = getMockGraphInfo();
+            var gd = getMockGraphInfo(['x'], ['y']);
             gd._fullLayout._basePlotModules = [{ name: 'cartesian' }];
             gd._fullLayout.xaxis = {fixedrange: false};
             return gd;
diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js
index bcddf542b33..29d9792b684 100644
--- a/test/jasmine/tests/plot_interact_test.js
+++ b/test/jasmine/tests/plot_interact_test.js
@@ -5,6 +5,7 @@ var Lib = require('@src/lib');
 
 var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
+var failTest = require('../assets/fail_test');
 
 // This suite is more of a test of the structure of interaction elements on
 // various plot types. Tests of actual mouse interactions on cartesian plots
@@ -111,12 +112,13 @@ describe('Test plot structure', function() {
                     return Plotly.relayout(gd, {xaxis: null, yaxis: null});
                 }).then(function() {
                     expect(countScatterTraces()).toEqual(0);
-                    expect(countSubplots()).toEqual(0);
-                    expect(countClipPaths()).toEqual(0);
-                    expect(countDraggers()).toEqual(0);
-
-                    done();
-                });
+                    // we still make one empty cartesian subplot if no other subplots are described
+                    expect(countSubplots()).toEqual(1);
+                    expect(countClipPaths()).toEqual(4);
+                    expect(countDraggers()).toEqual(1);
+                })
+                .catch(failTest)
+                .then(done);
             });
 
             it('should restore layout axes when they get deleted', function(done) {
@@ -156,9 +158,9 @@ describe('Test plot structure', function() {
                     expect(countSubplots()).toEqual(1);
                     expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4);
                     expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4);
-
-                    done();
-                });
+                })
+                .catch(failTest)
+                .then(done);
             });
         });
 
@@ -273,46 +275,88 @@ describe('Test plot structure', function() {
                         .then(done);
                 });
 
+                function assertClassCount(container3, msg, classes) {
+                    Object.keys(classes).forEach(function(cls) {
+                        expect(container3.selectAll('.' + cls).size())
+                            .toBe(classes[cls], msg + ': ' + cls);
+                    });
+                }
+
                 it('should be removed of traces in sequence', function(done) {
                     expect(countSubplots()).toEqual(4);
                     assertHeatmapNodes(4);
                     assertContourNodes(2);
                     expect(countColorBars()).toEqual(1);
+                    assertClassCount(gd._fullLayout._infolayer, 'initial', {
+                        'g-gtitle': 1,
+                        'g-xtitle': 1,
+                        'g-x2title': 1,
+                        'g-ytitle': 1,
+                        'g-y2title': 1
+                    });
 
                     Plotly.deleteTraces(gd, [0]).then(function() {
-                        expect(countSubplots()).toEqual(4);
-                        expect(countClipPaths()).toEqual(12);
-                        expect(countDraggers()).toEqual(4);
+                        expect(countSubplots()).toEqual(3);
+                        expect(countClipPaths()).toEqual(11);
+                        expect(countDraggers()).toEqual(3);
                         assertHeatmapNodes(3);
                         assertContourNodes(2);
                         expect(countColorBars()).toEqual(0);
+                        assertClassCount(gd._fullLayout._infolayer, '1 down', {
+                            'g-gtitle': 1,
+                            'g-xtitle': 1,
+                            'g-x2title': 1,
+                            'g-ytitle': 1,
+                            'g-y2title': 1
+                        });
 
                         return Plotly.deleteTraces(gd, [0]);
                     }).then(function() {
-                        expect(countSubplots()).toEqual(4);
-                        expect(countClipPaths()).toEqual(12);
-                        expect(countDraggers()).toEqual(4);
+                        expect(countSubplots()).toEqual(2);
+                        expect(countClipPaths()).toEqual(7);
+                        expect(countDraggers()).toEqual(2);
                         assertHeatmapNodes(2);
                         assertContourNodes(2);
                         expect(countColorBars()).toEqual(0);
+                        assertClassCount(gd._fullLayout._infolayer, '2 down', {
+                            'g-gtitle': 1,
+                            'g-xtitle': 1,
+                            'g-x2title': 1,
+                            'g-ytitle': 0,
+                            'g-y2title': 1
+                        });
 
                         return Plotly.deleteTraces(gd, [0]);
                     }).then(function() {
-                        expect(countSubplots()).toEqual(4);
-                        expect(countClipPaths()).toEqual(12);
-                        expect(countDraggers()).toEqual(4);
+                        expect(countSubplots()).toEqual(1);
+                        expect(countClipPaths()).toEqual(4);
+                        expect(countDraggers()).toEqual(1);
                         assertHeatmapNodes(1);
                         assertContourNodes(1);
                         expect(countColorBars()).toEqual(0);
+                        assertClassCount(gd._fullLayout._infolayer, '3 down', {
+                            'g-gtitle': 1,
+                            'g-xtitle': 0,
+                            'g-x2title': 1,
+                            'g-ytitle': 0,
+                            'g-y2title': 1
+                        });
 
                         return Plotly.deleteTraces(gd, [0]);
                     }).then(function() {
-                        expect(countSubplots()).toEqual(3);
-                        expect(countClipPaths()).toEqual(11);
-                        expect(countDraggers()).toEqual(3);
+                        expect(countSubplots()).toEqual(1);
+                        expect(countClipPaths()).toEqual(4);
+                        expect(countDraggers()).toEqual(1);
                         assertHeatmapNodes(0);
                         assertContourNodes(0);
                         expect(countColorBars()).toEqual(0);
+                        assertClassCount(gd._fullLayout._infolayer, 'all gone', {
+                            'g-gtitle': 1,
+                            'g-xtitle': 1,
+                            'g-x2title': 0,
+                            'g-ytitle': 1,
+                            'g-y2title': 0
+                        });
 
                         var update = {
                             xaxis: null,
@@ -323,15 +367,22 @@ describe('Test plot structure', function() {
 
                         return Plotly.relayout(gd, update);
                     }).then(function() {
-                        expect(countSubplots()).toEqual(0);
-                        expect(countClipPaths()).toEqual(0);
-                        expect(countDraggers()).toEqual(0);
+                        expect(countSubplots()).toEqual(1);
+                        expect(countClipPaths()).toEqual(4);
+                        expect(countDraggers()).toEqual(1);
                         assertHeatmapNodes(0);
                         assertContourNodes(0);
                         expect(countColorBars()).toEqual(0);
-
-                        done();
-                    });
+                        assertClassCount(gd._fullLayout._infolayer, 'cleared layout axes', {
+                            'g-gtitle': 1,
+                            'g-xtitle': 1,
+                            'g-x2title': 0,
+                            'g-ytitle': 1,
+                            'g-y2title': 0
+                        });
+                    })
+                    .catch(failTest)
+                    .then(done);
                 });
 
             });
@@ -388,10 +439,10 @@ describe('Test plot structure', function() {
 
                 Plotly.deleteTraces(gd, [0]).then(function() {
                     expect(countPieTraces()).toEqual(0);
-                    expect(countSubplots()).toEqual(0);
-
-                    done();
-                });
+                    expect(countSubplots()).toEqual(1);
+                })
+                .catch(failTest)
+                .then(done);
             });
 
             it('should be able to be restyled to a bar chart and back', function(done) {
@@ -409,9 +460,9 @@ describe('Test plot structure', function() {
                     expect(countPieTraces()).toEqual(1);
                     expect(countBarTraces()).toEqual(0);
                     expect(countSubplots()).toEqual(0);
-
-                    done();
-                });
+                })
+                .catch(failTest)
+                .then(done);
 
             });
         });
@@ -522,9 +573,9 @@ describe('plot svg clip paths', function() {
                 expect(cp.substring(0, 5)).toEqual('url(#');
                 expect(cp.substring(cp.length - 1)).toEqual(')');
             });
-
-            done();
-        });
+        })
+        .catch(failTest)
+        .then(done);
     });
 
     it('should set clip path url to ids appended to window url', function(done) {
@@ -550,7 +601,8 @@ describe('plot svg clip paths', function() {
             });
 
             base.remove();
-            done();
-        });
+        })
+        .catch(failTest)
+        .then(done);
     });
 });
diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js
index 9725a29556d..a290d30eb93 100644
--- a/test/jasmine/tests/plots_test.js
+++ b/test/jasmine/tests/plots_test.js
@@ -224,7 +224,7 @@ describe('Test Plots', function() {
 
     describe('Plots.supplyTraceDefaults', function() {
         var supplyTraceDefaults = Plots.supplyTraceDefaults,
-            layout = {};
+            layout = {_subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}};
 
         var traceIn, traceOut;
 
@@ -269,93 +269,6 @@ describe('Test Plots', function() {
         });
     });
 
-    describe('Plots.getSubplotIds', function() {
-        var getSubplotIds = Plots.getSubplotIds;
-
-        it('returns scene ids in order', function() {
-            var layout = {
-                scene2: {},
-                scene: {},
-                scene3: {}
-            };
-
-            expect(getSubplotIds(layout, 'gl3d'))
-                .toEqual(['scene', 'scene2', 'scene3']);
-
-            expect(getSubplotIds(layout, 'cartesian'))
-                .toEqual([]);
-            expect(getSubplotIds(layout, 'geo'))
-                .toEqual([]);
-            expect(getSubplotIds(layout, 'no-valid-subplot-type'))
-                .toEqual([]);
-        });
-
-        it('returns geo ids in order', function() {
-            var layout = {
-                geo2: {},
-                geo: {},
-                geo3: {}
-            };
-
-            expect(getSubplotIds(layout, 'geo'))
-                .toEqual(['geo', 'geo2', 'geo3']);
-
-            expect(getSubplotIds(layout, 'cartesian'))
-                .toEqual([]);
-            expect(getSubplotIds(layout, 'gl3d'))
-                .toEqual([]);
-            expect(getSubplotIds(layout, 'no-valid-subplot-type'))
-                .toEqual([]);
-        });
-
-        it('returns cartesian ids', function() {
-            var layout = {
-                _has: Plots._hasPlotType,
-                _plots: { xy: {}, x2y2: {} }
-            };
-
-            expect(getSubplotIds(layout, 'cartesian'))
-                .toEqual([]);
-
-            layout._basePlotModules = [{ name: 'cartesian' }];
-            expect(getSubplotIds(layout, 'cartesian'))
-                .toEqual(['xy', 'x2y2']);
-            expect(getSubplotIds(layout, 'gl2d'))
-                .toEqual([]);
-
-            layout._basePlotModules = [{ name: 'gl2d' }];
-            expect(getSubplotIds(layout, 'gl2d'))
-                .toEqual(['xy', 'x2y2']);
-            expect(getSubplotIds(layout, 'cartesian'))
-                .toEqual([]);
-
-        });
-    });
-
-    describe('Plots.findSubplotIds', function() {
-        var findSubplotIds = Plots.findSubplotIds;
-        var ids;
-
-        it('should return subplots ids found in the data', function() {
-            var data = [{
-                type: 'scatter3d',
-                scene: 'scene'
-            }, {
-                type: 'surface',
-                scene: 'scene2'
-            }, {
-                type: 'choropleth',
-                geo: 'geo'
-            }];
-
-            ids = findSubplotIds(data, 'geo');
-            expect(ids).toEqual(['geo']);
-
-            ids = findSubplotIds(data, 'gl3d');
-            expect(ids).toEqual(['scene', 'scene2']);
-        });
-    });
-
     describe('Plots.resize', function() {
         var gd;
 
@@ -596,7 +509,9 @@ describe('Test Plots', function() {
         });
     });
 
-    describe('Plots.getSubplotCalcData', function() {
+    describe('getSubplotCalcData', function() {
+        var getSubplotCalcData = require('@src/plots/get_data').getSubplotCalcData;
+
         var trace0 = { geo: 'geo2' };
         var trace1 = { subplot: 'ternary10' };
         var trace2 = { subplot: 'ternary10' };
@@ -608,22 +523,22 @@ describe('Test Plots', function() {
         ];
 
         it('should extract calcdata traces associated with subplot (1)', function() {
-            var out = Plots.getSubplotCalcData(cd, 'geo', 'geo2');
+            var out = getSubplotCalcData(cd, 'geo', 'geo2');
             expect(out).toEqual([[{ trace: trace0 }]]);
         });
 
         it('should extract calcdata traces associated with subplot (2)', function() {
-            var out = Plots.getSubplotCalcData(cd, 'ternary', 'ternary10');
+            var out = getSubplotCalcData(cd, 'ternary', 'ternary10');
             expect(out).toEqual([[{ trace: trace1 }], [{ trace: trace2 }]]);
         });
 
         it('should return [] when no calcdata traces where found', function() {
-            var out = Plots.getSubplotCalcData(cd, 'geo', 'geo');
+            var out = getSubplotCalcData(cd, 'geo', 'geo');
             expect(out).toEqual([]);
         });
 
         it('should return [] when subplot type is invalid', function() {
-            var out = Plots.getSubplotCalcData(cd, 'non-sense', 'geo2');
+            var out = getSubplotCalcData(cd, 'non-sense', 'geo2');
             expect(out).toEqual([]);
         });
     });
@@ -756,4 +671,134 @@ describe('Test Plots', function() {
             });
         });
     });
+
+    describe('subplot cleaning logic', function() {
+        var gd;
+
+        beforeEach(function() { gd = createGraphDiv(); });
+
+        afterEach(destroyGraphDiv);
+
+        function assertCartesian(subplotsSVG, subplotsGL2D, msg) {
+            var subplotsAll = subplotsSVG.concat(subplotsGL2D);
+            var subplots3 = d3.select(gd).selectAll('.cartesianlayer .subplot');
+            expect(subplots3.size()).toBe(subplotsAll.length, msg);
+
+            subplotsAll.forEach(function(subplot) {
+                expect(d3.select(gd).selectAll('.cartesianlayer .subplot.' + subplot).size())
+                    .toBe(1, msg + ' - ' + subplot);
+            });
+
+            subplotsSVG.forEach(function(subplot) {
+                expect((gd._fullLayout._plots[subplot] || {})._scene2d)
+                    .toBeUndefined(msg + ' - cartesian ' + subplot);
+            });
+
+            subplotsGL2D.forEach(function(subplot) {
+                expect((gd._fullLayout._plots[subplot] || {})._scene2d)
+                    .toBeDefined(msg + ' - gl2d ' + subplot);
+            });
+        }
+
+        var subplotSelectors = {
+            gl3d: '.gl-container>div[id^="scene"]',
+            geo: '.geolayer>g',
+            mapbox: '.mapboxgl-map',
+            parcoords: '.parcoords-line-layers',
+            pie: '.pielayer .trace',
+            sankey: '.sankey',
+            ternary: '.ternarylayer>g'
+        };
+
+        function assertSubplot(type, n, msg) {
+            expect(d3.select(gd).selectAll(subplotSelectors[type]).size())
+                .toBe(n, msg + ' - ' + type);
+        }
+
+        // opts.cartesian and opts.gl2d should be arrays of subplot ids ('xy', 'x2y2' etc)
+        // others should be counts: gl3d, geo, mapbox, parcoords, pie, ternary
+        // if omitted, that subplot type is assumed to not exist
+        function assertSubplots(opts, msg) {
+            msg = msg || '';
+            assertCartesian(opts.cartesian || [], opts.gl2d || [], msg);
+            Object.keys(subplotSelectors).forEach(function(type) {
+                assertSubplot(type, opts[type] || 0, msg);
+            });
+        }
+
+        var jsLogo = 'https://images.plot.ly/language-icons/api-home/js-logo.png';
+
+        it('makes at least a blank cartesian subplot', function(done) {
+            Plotly.newPlot(gd, [], {})
+            .then(function() {
+                assertSubplots({cartesian: ['xy']}, 'totally blank');
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('uses the first x & y axes it finds in making a blank cartesian subplot', function(done) {
+            Plotly.newPlot(gd, [], {xaxis3: {}, yaxis4: {}})
+            .then(function() {
+                assertSubplots({cartesian: ['x3y4']}, 'blank with axis objects');
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('shows expected cartesian subplots from visible traces and components', function(done) {
+            Plotly.newPlot(gd, [
+                {y: [1, 2]}
+            ], {
+                // strange case: x2 is anchored to y2, so we show y2
+                // even though no trace or component references it, only x2
+                annotations: [{xref: 'x2', yref: 'paper'}],
+                xaxis2: {anchor: 'y2'},
+                images: [{xref: 'x3', yref: 'y3', source: jsLogo}],
+                shapes: [{xref: 'x5', yref: 'y5'}]
+            })
+            .then(function() {
+                assertSubplots({cartesian: ['xy', 'x2y2', 'x3y3', 'x5y5']}, 'visible components');
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('shows expected cartesian subplots from invisible traces and components', function(done) {
+            Plotly.newPlot(gd, [
+                {y: [1, 2], visible: false}
+            ], {
+                // strange case: x2 is anchored to y2, so we show y2
+                // even though no trace or component references it, only x2
+                annotations: [{xref: 'x2', yref: 'paper', visible: false}],
+                xaxis2: {anchor: 'y2'},
+                images: [{xref: 'x3', yref: 'y3', source: jsLogo, visible: false}],
+                shapes: [{xref: 'x5', yref: 'y5', visible: false}]
+            })
+            .then(function() {
+                assertSubplots({cartesian: ['xy', 'x2y2', 'x3y3', 'x5y5']}, 'invisible components');
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('ignores unused axis and subplot objects', function(done) {
+            Plotly.plot('graph', [{
+                type: 'pie',
+                values: [1]
+            }], {
+                xaxis: {},
+                yaxis: {},
+                scene: {},
+                geo: {},
+                ternary: {},
+                mapbox: {}
+            })
+            .then(function() {
+                assertSubplots({pie: 1}, 'just pie');
+            })
+            .catch(fail)
+            .then(done);
+        });
+    });
 });
diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js
index 3cdfa6a2237..94ca4160da6 100644
--- a/test/jasmine/tests/range_slider_test.js
+++ b/test/jasmine/tests/range_slider_test.js
@@ -577,6 +577,11 @@ describe('the range slider', function() {
 
         it('should default to *true* when range slider is visible', function() {
             var mock = {
+                data: [
+                    {y: [1, 2]},
+                    {y: [1, 2], yaxis: 'y2'},
+                    {y: [1, 2], yaxis: 'y3'}
+                ],
                 layout: {
                     xaxis: { rangeslider: {} },
                     yaxis: { anchor: 'x' },
@@ -595,6 +600,11 @@ describe('the range slider', function() {
 
         it('should honor user settings', function() {
             var mock = {
+                data: [
+                    {y: [1, 2]},
+                    {y: [1, 2], yaxis: 'y2'},
+                    {y: [1, 2], yaxis: 'y3'}
+                ],
                 layout: {
                     xaxis: { rangeslider: {} },
                     yaxis: { anchor: 'x', fixedrange: false },
diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js
index 10675a5b6ff..fb9e1247dd3 100644
--- a/test/jasmine/tests/shapes_test.js
+++ b/test/jasmine/tests/shapes_test.js
@@ -60,7 +60,8 @@ describe('Test shapes defaults:', function() {
             xaxis: {type: 'linear', range: [0, 20]},
             yaxis: {type: 'log', range: [1, 5]},
             xaxis2: {type: 'date', range: ['2006-06-05', '2006-06-09']},
-            yaxis2: {type: 'category', range: [-0.5, 7.5]}
+            yaxis2: {type: 'category', range: [-0.5, 7.5]},
+            _subplots: {xaxis: ['x', 'x2'], yaxis: ['y', 'y2']}
         };
 
         Axes.setConvert(fullLayout.xaxis);
diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js
index 5715e48499a..ebed92f9dd0 100644
--- a/test/jasmine/tests/ternary_test.js
+++ b/test/jasmine/tests/ternary_test.js
@@ -378,7 +378,8 @@ describe('ternary defaults', function() {
 
     beforeEach(function() {
         layoutOut = {
-            font: { color: 'red' }
+            font: { color: 'red' },
+            _subplots: {ternary: ['ternary']}
         };
 
         // needs a ternary-ref in a trace in order to be detected
diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js
index ad9b3c5e159..f419504d03d 100644
--- a/test/jasmine/tests/toimage_test.js
+++ b/test/jasmine/tests/toimage_test.js
@@ -149,7 +149,7 @@ describe('Plotly.toImage', function() {
         .then(function() { return Plotly.toImage(gd, {format: 'png', imageDataOnly: true}); })
         .then(function(d) {
             expect(d.indexOf('data:image/')).toBe(-1);
-            expect(d.length).toBeWithin(53660, 1e3, 'png image length');
+            expect(d.length).toBeWithin(54660, 3e3, 'png image length');
         })
         .then(function() { return Plotly.toImage(gd, {format: 'jpeg', imageDataOnly: true}); })
         .then(function(d) {
@@ -159,7 +159,7 @@ describe('Plotly.toImage', function() {
         .then(function() { return Plotly.toImage(gd, {format: 'svg', imageDataOnly: true}); })
         .then(function(d) {
             expect(d.indexOf('data:image/')).toBe(-1);
-            expect(d.length).toBeWithin(39485, 1e3, 'svg image length');
+            expect(d.length).toBeWithin(32062, 1e3, 'svg image length');
         })
         .then(function() { return Plotly.toImage(gd, {format: 'webp', imageDataOnly: true}); })
         .then(function(d) {
diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js
index cc7034c739d..7dc565a1fad 100644
--- a/test/jasmine/tests/transform_filter_test.js
+++ b/test/jasmine/tests/transform_filter_test.js
@@ -14,7 +14,10 @@ var assertStyle = customAssertions.assertStyle;
 
 describe('filter transforms defaults:', function() {
 
-    var fullLayout = { _transformModules: [] };
+    var fullLayout = {
+        _transformModules: [],
+        _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}
+    };
 
     var traceIn, traceOut;
 
diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js
index d55ab8bf58d..8a41e03db95 100644
--- a/test/jasmine/tests/transform_groupby_test.js
+++ b/test/jasmine/tests/transform_groupby_test.js
@@ -9,6 +9,15 @@ var customAssertions = require('../assets/custom_assertions');
 var assertDims = customAssertions.assertDims;
 var assertStyle = customAssertions.assertStyle;
 
+
+function supplyDataDefaults(dataIn, dataOut) {
+    return Plots.supplyDataDefaults(dataIn, dataOut, {}, {
+        _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']},
+        _modules: [],
+        _basePlotModules: []
+    });
+}
+
 describe('groupby', function() {
 
     describe('one-to-many transforms:', function() {
@@ -261,7 +270,7 @@ describe('groupby', function() {
                 ]
             }];
 
-            Plots.supplyDataDefaults(dataIn, dataOut, {}, {});
+            supplyDataDefaults(dataIn, dataOut);
 
             for(var i = 0; i < dataOut.length; i++) {
                 uniqueColors[dataOut[i].marker.color] = true;
@@ -717,7 +726,7 @@ describe('groupby', function() {
                 ]
             }];
 
-            Plots.supplyDataDefaults(dataIn, dataOut, {}, {});
+            supplyDataDefaults(dataIn, dataOut);
 
             for(var i = 0; i < dataOut.length; i++) {
                 colors.push(dataOut[i].marker.color);
diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js
index 829d84569db..d322679b8a4 100644
--- a/test/jasmine/tests/transform_multi_test.js
+++ b/test/jasmine/tests/transform_multi_test.js
@@ -12,10 +12,24 @@ var supplyAllDefaults = require('../assets/supply_defaults');
 var assertDims = customAssertions.assertDims;
 var assertStyle = customAssertions.assertStyle;
 
+var mockFullLayout = {
+    _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']},
+    _modules: [],
+    _basePlotModules: [],
+    _has: function() {},
+    _dfltTitle: {x: 'xxx', y: 'yyy'}
+};
+
+
 describe('general transforms:', function() {
     'use strict';
 
-    var fullLayout = { _transformModules: [] };
+    var fullLayout = {
+        _transformModules: [],
+        _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']},
+        _modules: [],
+        _basePlotModules: []
+    };
 
     var traceIn, traceOut;
 
@@ -60,7 +74,7 @@ describe('general transforms:', function() {
         expect(traceOut.y).toBe(traceIn.y);
     });
 
-    it('supplyTraceDefaults should honored global transforms', function() {
+    it('supplyTraceDefaults should honor global transforms', function() {
         traceIn = {
             y: [2, 1, 2],
             transforms: [{
@@ -75,7 +89,10 @@ describe('general transforms:', function() {
             _transformModules: [],
             _globalTransforms: [{
                 type: 'filter'
-            }]
+            }],
+            _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']},
+            _modules: [],
+            _basePlotModules: []
         };
 
         traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout);
@@ -117,7 +134,7 @@ describe('general transforms:', function() {
         }];
 
         var dataOut = [];
-        Plots.supplyDataDefaults(dataIn, dataOut, {}, []);
+        Plots.supplyDataDefaults(dataIn, dataOut, {}, mockFullLayout);
 
         var msg;
 
@@ -180,19 +197,25 @@ describe('user-defined transforms:', function() {
         var transformIn = { type: 'fake' };
         var transformOut = {};
 
+        var calledSupplyDefaults = false;
+        var calledTransform = false;
+        var calledSupplyLayoutDefaults = false;
+
         var dataIn = [{
             transforms: [transformIn]
         }];
 
-        var fullData = [],
-            layout = {},
-            fullLayout = { _has: function() {} },
-            transitionData = {};
+        var fullData = [];
+        var layout = {};
+        var fullLayout = Lib.extendDeep({}, mockFullLayout);
+        var transitionData = {};
 
         function assertSupplyDefaultsArgs(_transformIn, traceOut, _layout) {
             expect(_transformIn).toBe(transformIn);
             expect(_layout).toBe(fullLayout);
 
+            calledSupplyDefaults = true;
+
             return transformOut;
         }
 
@@ -203,6 +226,8 @@ describe('user-defined transforms:', function() {
             expect(opts.layout).toBe(layout);
             expect(opts.fullLayout).toBe(fullLayout);
 
+            calledTransform = true;
+
             return dataOut;
         }
 
@@ -211,6 +236,8 @@ describe('user-defined transforms:', function() {
             expect(_fullLayout).toBe(fullLayout);
             expect(_fullData).toBe(fullData);
             expect(_transitionData).toBe(transitionData);
+
+            calledSupplyLayoutDefaults = true;
         }
 
         var fakeTransformModule = {
@@ -226,6 +253,9 @@ describe('user-defined transforms:', function() {
         Plots.supplyDataDefaults(dataIn, fullData, layout, fullLayout);
         Plots.supplyLayoutModuleDefaults(layout, fullLayout, fullData, transitionData);
         delete Plots.transformsRegistry.fake;
+        expect(calledSupplyDefaults).toBe(true);
+        expect(calledTransform).toBe(true);
+        expect(calledSupplyLayoutDefaults).toBe(true);
     });
 
 });
diff --git a/test/jasmine/tests/transform_sort_test.js b/test/jasmine/tests/transform_sort_test.js
index 38e2be3b79d..8f2e37d080c 100644
--- a/test/jasmine/tests/transform_sort_test.js
+++ b/test/jasmine/tests/transform_sort_test.js
@@ -12,6 +12,11 @@ var supplyAllDefaults = require('../assets/supply_defaults');
 describe('Test sort transform defaults:', function() {
     function _supply(trace, layout) {
         layout = layout || {};
+        Lib.extendDeep(layout, {
+            _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']},
+            _modules: [],
+            _basePlotModules: []
+        });
         return Plots.supplyTraceDefaults(trace, 0, layout);
     }
 
diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js
index d5597f23322..3014e3540a3 100644
--- a/test/jasmine/tests/validate_test.js
+++ b/test/jasmine/tests/validate_test.js
@@ -177,7 +177,11 @@ describe('Plotly.validate', function() {
     });
 
     it('should work with isLinkedToArray attributes', function() {
-        var out = Plotly.validate([], {
+        var out = Plotly.validate([
+            {y: [1, 2]},
+            {y: [1, 2], xaxis: 'x2'},
+            {y: [1, 2], xaxis: 'x3'}
+        ], {
             annotations: [{
                 text: 'some text'
             }, {
@@ -394,7 +398,10 @@ describe('Plotly.validate', function() {
     });
 
     it('should catch input errors for attribute with dynamic defaults', function() {
-        var out = Plotly.validate([], {
+        var out = Plotly.validate([
+            {y: [1, 2]},
+            {y: [1, 2], xaxis: 'x2', yaxis: 'y2'}
+        ], {
             xaxis: {
                 constrain: 'domain',
                 constraintoward: 'bottom'