diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5afd8fee571..9dc14f4d5f2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -97,6 +97,25 @@ jobs:
       - store_artifacts:
           path: build
 
+  test-image2:
+    docker:
+      - image: plotly/testbed:latest
+    working_directory: /var/www/streambed/image_server/plotly.js/
+    steps:
+      - checkout
+      - attach_workspace:
+          at: /var/www/streambed/image_server/plotly.js/
+      - run:
+          name: Run and setup container
+          command: |
+            supervisord &
+            npm run docker -- setup
+      - run:
+          name: Run image tests
+          command: ./.circleci/test.sh image2
+      - store_artifacts:
+          path: build
+
   test-syntax:
     docker:
       - image: circleci/node:8.9.4
@@ -123,6 +142,9 @@ workflows:
       - test-image:
           requires:
             - build
+      - test-image2:
+          requires:
+            - build
       - test-syntax:
           requires:
             - build
diff --git a/.circleci/test.sh b/.circleci/test.sh
index c2e82b26442..0a660879059 100755
--- a/.circleci/test.sh
+++ b/.circleci/test.sh
@@ -41,6 +41,10 @@ case $1 in
 
     image)
         npm run test-image      || EXIT_STATE=$?
+        exit $EXIT_STATE
+        ;;
+
+    image2)
         npm run test-export     || EXIT_STATE=$?
         npm run test-image-gl2d || EXIT_STATE=$?
         exit $EXIT_STATE
diff --git a/lib/index-gl2d.js b/lib/index-gl2d.js
index bf378548a82..0b2d8358f21 100644
--- a/lib/index-gl2d.js
+++ b/lib/index-gl2d.js
@@ -12,6 +12,7 @@ var Plotly = require('./core');
 
 Plotly.register([
     require('./scattergl'),
+    require('./splom'),
     require('./pointcloud'),
     require('./heatmapgl'),
     require('./contourgl'),
diff --git a/lib/index.js b/lib/index.js
index 57e33be6c60..a2b7c9fdc61 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -31,6 +31,8 @@ Plotly.register([
     require('./choropleth'),
 
     require('./scattergl'),
+    require('./splom'),
+
     require('./pointcloud'),
     require('./heatmapgl'),
     require('./parcoords'),
diff --git a/lib/splom.js b/lib/splom.js
new file mode 100644
index 00000000000..04328ef414f
--- /dev/null
+++ b/lib/splom.js
@@ -0,0 +1,11 @@
+/**
+* Copyright 2012-2018, 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';
+
+module.exports = require('../src/traces/splom');
diff --git a/package-lock.json b/package-lock.json
index 98883ad63ac..5a9f24d5d3c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1714,6 +1714,14 @@
       "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
       "dev": true
     },
+    "color-alpha": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/color-alpha/-/color-alpha-1.0.2.tgz",
+      "integrity": "sha1-tDMtJAvCrOd72y0J9jlNkcupHjA=",
+      "requires": {
+        "color-parse": "1.3.5"
+      }
+    },
     "color-convert": {
       "version": "1.9.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
@@ -6959,8 +6967,7 @@
     "left-pad": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.2.0.tgz",
-      "integrity": "sha1-0wpzxrggHY99jnlWupYWCHpo4O4=",
-      "dev": true
+      "integrity": "sha1-0wpzxrggHY99jnlWupYWCHpo4O4="
     },
     "lerp": {
       "version": "1.0.3",
@@ -9161,8 +9168,7 @@
     "performance-now": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
-      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
-      "dev": true
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
     },
     "permutation-parity": {
       "version": "1.0.0",
@@ -9545,6 +9551,14 @@
         }
       }
     },
+    "raf": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz",
+      "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==",
+      "requires": {
+        "performance-now": "2.1.0"
+      }
+    },
     "randomatic": {
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
@@ -10055,9 +10069,9 @@
       }
     },
     "regl-line2d": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.0.1.tgz",
-      "integrity": "sha512-6hhuHQBMhWFde28/jKDTCSkc0ZdTpsJpKflGCFmUpBHo2DAOYQkNfC1ARe+eOO70FqoahkdIrB9OJKB8J8Nupg==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.0.2.tgz",
+      "integrity": "sha512-lIFjleuKg/tqHUVuWnd4fe1bn3RulaapHDThEaqPYM5XgKTWenzVrLvl7x0SIVkO5loc/n89/RYifBQQqzDuLg==",
       "requires": {
         "array-bounds": "1.0.1",
         "array-normalize": "1.1.3",
@@ -10074,9 +10088,9 @@
       }
     },
     "regl-scatter2d": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/regl-scatter2d/-/regl-scatter2d-3.0.0.tgz",
-      "integrity": "sha512-ylsZ/TZ4XB/qFncCTLLAFhXt503VE4m4/u699OwLMgncePenBl2K/9SvJ5GuNjuaJ4CY3Zj9q+zPGYChdDsDUw==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/regl-scatter2d/-/regl-scatter2d-3.0.1.tgz",
+      "integrity": "sha512-7tqdia7+o8pILPwu9BpnzxM6m11qgTzcoS0n3Hs+cFXLl3vXFmbM3y69LTJzDsASzSyPSb8RO60/dVJ9oAuuwQ==",
       "requires": {
         "array-range": "1.0.1",
         "array-rearrange": "2.2.2",
@@ -10095,6 +10109,42 @@
         "update-diff": "1.1.0"
       }
     },
+    "regl-splom": {
+      "version": "github:dy/regl-scattermatrix#c6d064ebc546ed71c48d8c5b7e66cfe0980ef77a",
+      "requires": {
+        "array-bounds": "1.0.1",
+        "array-range": "1.0.1",
+        "bubleify": "1.1.0",
+        "color-alpha": "1.0.2",
+        "defined": "1.0.0",
+        "flatten-vertex-data": "1.0.0",
+        "left-pad": "1.2.0",
+        "parse-rect": "1.2.0",
+        "pick-by-alias": "1.2.0",
+        "point-cluster": "1.0.2",
+        "raf": "3.4.0",
+        "regl-scatter2d": "3.0.1"
+      },
+      "dependencies": {
+        "binary-search-bounds": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.4.tgz",
+          "integrity": "sha512-2hg5kgdKql5ClF2ErBcSx0U5bnl5hgS4v7wMnLFodyR47yMtj2w+UAZB+0CiqyHct2q543i7Bi4/aMIegorCCg=="
+        },
+        "point-cluster": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/point-cluster/-/point-cluster-1.0.2.tgz",
+          "integrity": "sha512-pau5Py38SKgEJZ8pvD/bfXrz2TmQy6BEtMFZZSpjsQ2EmAe4CRO+HLhHw1gmgHVFaY/9KqhrfSeUPIsBOw8tDA==",
+          "requires": {
+            "array-bounds": "1.0.1",
+            "array-normalize": "1.1.3",
+            "binary-search-bounds": "2.0.4",
+            "clamp": "1.0.1",
+            "parse-rect": "1.2.0"
+          }
+        }
+      }
+    },
     "remove-trailing-separator": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
diff --git a/package.json b/package.json
index 8afa622f458..dd932ca9847 100644
--- a/package.json
+++ b/package.json
@@ -99,8 +99,9 @@
     "polybooljs": "^1.2.0",
     "regl": "^1.3.1",
     "regl-error2d": "^2.0.3",
-    "regl-line2d": "^3.0.1",
-    "regl-scatter2d": "^3.0.0",
+    "regl-line2d": "^3.0.2",
+    "regl-scatter2d": "^3.0.1",
+    "regl-splom": "^1.0.0",
     "right-now": "^1.0.0",
     "robust-orientation": "^1.1.3",
     "sane-topojson": "^2.0.0",
diff --git a/src/components/fx/helpers.js b/src/components/fx/helpers.js
index d966a6d4a57..2698a7e1761 100644
--- a/src/components/fx/helpers.js
+++ b/src/components/fx/helpers.js
@@ -11,10 +11,30 @@
 var Lib = require('../../lib');
 
 // look for either subplot or xaxis and yaxis attributes
+// does not handle splom case
 exports.getSubplot = function getSubplot(trace) {
     return trace.subplot || (trace.xaxis + trace.yaxis) || trace.geo;
 };
 
+// is trace in given list of subplots?
+// does handle splom case
+exports.isTraceInSubplots = function isTraceInSubplot(trace, subplots) {
+    if(trace.type === 'splom') {
+        var xaxes = trace.xaxes || [];
+        var yaxes = trace.yaxes || [];
+        for(var i = 0; i < xaxes.length; i++) {
+            for(var j = 0; j < yaxes.length; j++) {
+                if(subplots.indexOf(xaxes[i] + yaxes[j]) !== -1) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    return subplots.indexOf(exports.getSubplot(trace)) !== -1;
+};
+
 // convenience functions for mapping all relevant axes
 exports.flat = function flat(subplots, v) {
     var out = new Array(subplots.length);
diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index c7db8a13d7f..4215a388ac8 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -215,7 +215,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {
     var hoverdistance = fullLayout.hoverdistance === -1 ? Infinity : fullLayout.hoverdistance;
     var spikedistance = fullLayout.spikedistance === -1 ? Infinity : fullLayout.spikedistance;
 
-        // hoverData: the set of candidate points we've found to highlight
+    // hoverData: the set of candidate points we've found to highlight
     var hoverData = [],
 
         // searchData: the data to search in. Mostly this is just a copy of
@@ -265,7 +265,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {
         for(curvenum = 0; curvenum < gd.calcdata.length; curvenum++) {
             cd = gd.calcdata[curvenum];
             trace = cd[0].trace;
-            if(trace.hoverinfo !== 'skip' && subplots.indexOf(helpers.getSubplot(trace)) !== -1) {
+            if(trace.hoverinfo !== 'skip' && helpers.isTraceInSubplots(trace, subplots)) {
                 searchData.push(cd);
             }
         }
@@ -338,8 +338,15 @@ function _hover(gd, evt, subplot, noHoverEvent) {
         // the rest of this function from running and failing
         if(['carpet', 'contourcarpet'].indexOf(trace._module.name) !== -1) continue;
 
-        subplotId = helpers.getSubplot(trace);
-        subploti = subplots.indexOf(subplotId);
+        if(trace.type === 'splom') {
+            // splom traces do not generate overlay subplots,
+            // it is safe to assume here splom traces correspond to the 0th subplot
+            subploti = 0;
+            subplotId = subplots[subploti];
+        } else {
+            subplotId = helpers.getSubplot(trace);
+            subploti = subplots.indexOf(subplotId);
+        }
 
         // within one trace mode can sometimes be overridden
         mode = hovermode;
diff --git a/src/components/grid/index.js b/src/components/grid/index.js
index 5def46b284e..f03653522a2 100644
--- a/src/components/grid/index.js
+++ b/src/components/grid/index.js
@@ -143,7 +143,7 @@ var gridAttrs = {
         values: ['bottom', 'bottom plot', 'top plot', 'top'],
         dflt: 'bottom plot',
         role: 'info',
-        editType: 'ticks',
+        editType: 'plot',
         description: [
             'Sets where the x axis labels and titles go. *bottom* means',
             'the very bottom of the grid. *bottom plot* is the lowest plot',
@@ -155,7 +155,7 @@ var gridAttrs = {
         values: ['left', 'left plot', 'right plot', 'right'],
         dflt: 'left plot',
         role: 'info',
-        editType: 'ticks',
+        editType: 'plot',
         description: [
             'Sets where the y axis labels and titles go. *left* means',
             'the very left edge of the grid. *left plot* is the leftmost plot',
@@ -165,15 +165,30 @@ var gridAttrs = {
     editType: 'plot'
 };
 
+function getAxes(layout, grid, axLetter) {
+    var gridVal = grid[axLetter + 'axes'];
+    var splomVal = Object.keys((layout._splomAxes || {})[axLetter] || {});
+
+    if(Array.isArray(gridVal)) return gridVal;
+    if(splomVal.length) return splomVal;
+}
+
 // the shape of the grid - this needs to be done BEFORE supplyDataDefaults
 // so that non-subplot traces can place themselves in the grid
 function sizeDefaults(layoutIn, layoutOut) {
-    var gridIn = layoutIn.grid;
-    if(!gridIn) return;
+    var gridIn = layoutIn.grid || {};
+    var xAxes = getAxes(layoutOut, gridIn, 'x');
+    var yAxes = getAxes(layoutOut, gridIn, 'y');
+
+    if(!layoutIn.grid && !xAxes && !yAxes) return;
 
     var hasSubplotGrid = Array.isArray(gridIn.subplots) && Array.isArray(gridIn.subplots[0]);
-    var hasXaxes = Array.isArray(gridIn.xaxes);
-    var hasYaxes = Array.isArray(gridIn.yaxes);
+    var hasXaxes = Array.isArray(xAxes);
+    var hasYaxes = Array.isArray(yAxes);
+    var isSplomGenerated = (
+        hasXaxes && xAxes !== gridIn.xaxes &&
+        hasYaxes && yAxes !== gridIn.yaxes
+    );
 
     var dfltRows, dfltColumns;
 
@@ -182,8 +197,8 @@ function sizeDefaults(layoutIn, layoutOut) {
         dfltColumns = gridIn.subplots[0].length;
     }
     else {
-        if(hasYaxes) dfltRows = gridIn.yaxes.length;
-        if(hasXaxes) dfltColumns = gridIn.xaxes.length;
+        if(hasYaxes) dfltRows = yAxes.length;
+        if(hasXaxes) dfltColumns = xAxes.length;
     }
 
     var gridOut = layoutOut.grid = {};
@@ -206,17 +221,26 @@ function sizeDefaults(layoutIn, layoutOut) {
     var rowOrder = coerce('roworder');
     var reversed = rowOrder === 'top to bottom';
 
+    var dfltGapX = hasSubplotGrid ? 0.2 : 0.1;
+    var dfltGapY = hasSubplotGrid ? 0.3 : 0.1;
+
+    var dfltSideX, dfltSideY;
+    if(isSplomGenerated) {
+        dfltSideX = 'bottom';
+        dfltSideY = 'left';
+    }
+
     gridOut._domains = {
-        x: fillGridPositions('x', coerce, hasSubplotGrid ? 0.2 : 0.1, columns),
-        y: fillGridPositions('y', coerce, hasSubplotGrid ? 0.3 : 0.1, rows, reversed)
+        x: fillGridPositions('x', coerce, dfltGapX, dfltSideX, columns),
+        y: fillGridPositions('y', coerce, dfltGapY, dfltSideY, rows, reversed)
     };
 }
 
 // coerce x or y sizing attributes and return an array of domains for this direction
-function fillGridPositions(axLetter, coerce, dfltGap, len, reversed) {
+function fillGridPositions(axLetter, coerce, dfltGap, dfltSide, len, reversed) {
     var dirGap = coerce(axLetter + 'gap', dfltGap);
     var domain = coerce('domain.' + axLetter);
-    coerce(axLetter + 'side');
+    coerce(axLetter + 'side', dfltSide);
 
     var out = new Array(len);
     var start = domain[0];
@@ -236,7 +260,7 @@ function contentDefaults(layoutIn, layoutOut) {
     // make sure we got to the end of handleGridSizing
     if(!gridOut || !gridOut._domains) return;
 
-    var gridIn = layoutIn.grid;
+    var gridIn = layoutIn.grid || {};
     var subplots = layoutOut._subplots;
     var hasSubplotGrid = gridOut._hasSubplotGrid;
     var rows = gridOut.rows;
@@ -282,8 +306,10 @@ function contentDefaults(layoutIn, layoutOut) {
         }
     }
     else {
-        gridOut.xaxes = fillGridAxes(gridIn.xaxes, subplots.xaxis, columns, axisMap, 'x');
-        gridOut.yaxes = fillGridAxes(gridIn.yaxes, subplots.yaxis, rows, axisMap, 'y');
+        var xAxes = getAxes(layoutOut, gridIn, 'x');
+        var yAxes = getAxes(layoutOut, gridIn, 'y');
+        gridOut.xaxes = fillGridAxes(xAxes, subplots.xaxis, columns, axisMap, 'x');
+        gridOut.yaxes = fillGridAxes(yAxes, subplots.yaxis, rows, axisMap, 'y');
     }
 
     var anchors = gridOut._anchors = {};
diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js
index 53673e7c173..cd7bdf4acef 100644
--- a/src/components/shapes/draw.js
+++ b/src/components/shapes/draw.js
@@ -42,7 +42,11 @@ function draw(gd) {
     // Remove previous shapes before drawing new in shapes in fullLayout.shapes
     fullLayout._shapeUpperLayer.selectAll('path').remove();
     fullLayout._shapeLowerLayer.selectAll('path').remove();
-    fullLayout._shapeSubplotLayers.selectAll('path').remove();
+
+    for(var k in fullLayout._plots) {
+        var shapelayer = fullLayout._plots[k].shapelayer;
+        if(shapelayer) shapelayer.selectAll('path').remove();
+    }
 
     for(var i = 0; i < fullLayout.shapes.length; i++) {
         if(fullLayout.shapes[i].visible) {
diff --git a/src/lib/clear_gl_canvases.js b/src/lib/clear_gl_canvases.js
new file mode 100644
index 00000000000..8545339ab4c
--- /dev/null
+++ b/src/lib/clear_gl_canvases.js
@@ -0,0 +1,26 @@
+/**
+* Copyright 2012-2018, 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';
+
+/**
+ * Clear gl frame (if any). This is a common pattern as
+ * we usually set `preserveDrawingBuffer: true` during
+ * gl context creation (e.g. via `reglUtils.prepare`).
+ *
+ * @param {DOM node or object} gd : graph div object
+ */
+module.exports = function clearGlCanvases(gd) {
+    var fullLayout = gd._fullLayout;
+
+    if(fullLayout._glcanvas && fullLayout._glcanvas.size()) {
+        fullLayout._glcanvas.each(function(d) {
+            if(d.regl) d.regl.clear({color: true, depth: true});
+        });
+    }
+};
diff --git a/src/lib/coerce.js b/src/lib/coerce.js
index 4b9411d7b25..bb025e3f2c5 100644
--- a/src/lib/coerce.js
+++ b/src/lib/coerce.js
@@ -196,9 +196,10 @@ exports.valObjectMeta = {
             '\'geo\', \'geo2\', \'geo3\', ...'
         ].join(' '),
         requiredOpts: ['dflt'],
-        otherOpts: [],
-        coerceFunction: function(v, propOut, dflt) {
-            if(typeof v === 'string' && counterRegex(dflt).test(v)) {
+        otherOpts: ['regex'],
+        coerceFunction: function(v, propOut, dflt, opts) {
+            var regex = opts.regex || counterRegex(dflt);
+            if(typeof v === 'string' && regex.test(v)) {
                 propOut.set(v);
                 return;
             }
diff --git a/src/lib/prepare_regl.js b/src/lib/prepare_regl.js
new file mode 100644
index 00000000000..3c6ca297083
--- /dev/null
+++ b/src/lib/prepare_regl.js
@@ -0,0 +1,39 @@
+/**
+* Copyright 2012-2018, 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';
+
+// Note that this module should be ONLY required into
+// files corresponding to regl trace modules
+// so that bundles with non-regl only don't include
+// regl and all its bytes.
+var createRegl = require('regl');
+
+/**
+ * Idempotent version of createRegl. Create regl instances
+ * in the correct canvases with the correct attributes and
+ * options
+ *
+ * @param {DOM node or object} gd : graph div object
+ * @param {array} extensions : list of extension to pass to createRegl
+ */
+module.exports = function prepareRegl(gd, extensions) {
+    gd._fullLayout._glcanvas.each(function(d) {
+        if(d.regl) return;
+
+        d.regl = createRegl({
+            canvas: this,
+            attributes: {
+                antialias: !d.pick,
+                preserveDrawingBuffer: true
+            },
+            pixelRatio: gd._context.plotGlPixelRatio || global.devicePixelRatio,
+            extensions: extensions || []
+        });
+    });
+};
diff --git a/src/lib/typed_array_truncate.js b/src/lib/typed_array_truncate.js
deleted file mode 100644
index c43c17166d0..00000000000
--- a/src/lib/typed_array_truncate.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
-* Copyright 2012-2018, 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';
-
-function truncateFloat32(arrayIn, len) {
-    var arrayOut = new Float32Array(len);
-    for(var i = 0; i < len; i++) arrayOut[i] = arrayIn[i];
-    return arrayOut;
-}
-
-function truncateFloat64(arrayIn, len) {
-    var arrayOut = new Float64Array(len);
-    for(var i = 0; i < len; i++) arrayOut[i] = arrayIn[i];
-    return arrayOut;
-}
-
-/**
- * Truncate a typed array to some length.
- * For some reason, ES2015 Float32Array.prototype.slice takes
- * 2x as long, therefore we aren't checking for its existence
- */
-module.exports = function truncate(arrayIn, len) {
-    if(arrayIn instanceof Float32Array) return truncateFloat32(arrayIn, len);
-    if(arrayIn instanceof Float64Array) return truncateFloat64(arrayIn, len);
-    throw new Error('This array type is not yet supported by `truncate`.');
-};
diff --git a/src/plot_api/edit_types.js b/src/plot_api/edit_types.js
index 24701a9a443..ea6defdc2c5 100644
--- a/src/plot_api/edit_types.js
+++ b/src/plot_api/edit_types.js
@@ -35,7 +35,7 @@ var layoutOpts = {
     valType: 'flaglist',
     extras: ['none'],
     flags: [
-        'calc', 'calcIfAutorange', 'plot', 'legend', 'ticks', 'margins',
+        'calc', 'calcIfAutorange', 'plot', 'legend', 'ticks', 'axrange', 'margins',
         'layoutstyle', 'modebar', 'camera', 'arraydraw'
     ],
     description: [
@@ -48,6 +48,7 @@ var layoutOpts = {
         '*legend* only redraws the legend.',
         '*ticks* only redraws axis ticks, labels, and gridlines.',
         '*margins* recomputes ticklabel automargins.',
+        '*axrange* minimal sequence when updating axis ranges.',
         '*layoutstyle* reapplies global and SVG cartesian axis styles.',
         '*modebar* just updates the modebar.',
         '*camera* just updates the camera settings for gl3d scenes.',
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index b6aed6ba7d0..9ea68b9f5e4 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -22,11 +22,11 @@ var Registry = require('../registry');
 var PlotSchema = require('./plot_schema');
 var Plots = require('../plots/plots');
 var Polar = require('../plots/polar/legacy');
-var initInteractions = require('../plots/cartesian/graph_interact');
 
 var Axes = require('../plots/cartesian/axes');
 var Drawing = require('../components/drawing');
 var Color = require('../components/color');
+var initInteractions = require('../plots/cartesian/graph_interact').initInteractions;
 var xmlnsNamespaces = require('../constants/xmlns_namespaces');
 var svgTextUtils = require('../lib/svg_text_utils');
 
@@ -36,11 +36,7 @@ var helpers = require('./helpers');
 var subroutines = require('./subroutines');
 var editTypes = require('./edit_types');
 
-var cartesianConstants = require('../plots/cartesian/constants');
-var axisConstraints = require('../plots/cartesian/constraints');
-var enforceAxisConstraints = axisConstraints.enforce;
-var cleanAxisConstraints = axisConstraints.clean;
-var doAutoRange = require('../plots/cartesian/autorange').doAutoRange;
+var AX_NAME_PATTERN = require('../plots/cartesian/constants').AX_NAME_PATTERN;
 
 var numericNameWarningCount = 0;
 var numericNameWarningCountLimit = 5;
@@ -330,15 +326,7 @@ exports.plot = function(gd, data, layout, config) {
     function doAutoRangeAndConstraints() {
         if(gd._transitioning) return;
 
-        var axList = Axes.list(gd, '', true);
-        for(var i = 0; i < axList.length; i++) {
-            var ax = axList[i];
-            cleanAxisConstraints(gd, ax);
-
-            doAutoRange(ax);
-        }
-
-        enforceAxisConstraints(gd);
+        subroutines.doAutoRangeAndConstraints(gd);
 
         // store initial ranges *after* enforcing constraints, otherwise
         // we will never look like we're at the initial ranges
@@ -350,80 +338,6 @@ exports.plot = function(gd, data, layout, config) {
         return Axes.doTicks(gd, graphWasEmpty ? '' : 'redraw');
     }
 
-    // Now plot the data
-    function drawData() {
-        var calcdata = gd.calcdata,
-            i,
-            rangesliderContainers = fullLayout._infolayer.selectAll('g.rangeslider-container');
-
-        // in case of traces that were heatmaps or contour maps
-        // previously, remove them and their colorbars explicitly
-        for(i = 0; i < calcdata.length; i++) {
-            var trace = calcdata[i][0].trace,
-                isVisible = (trace.visible === true),
-                uid = trace.uid;
-
-            if(!isVisible || !Registry.traceIs(trace, '2dMap')) {
-                var query = (
-                    '.hm' + uid +
-                    ',.contour' + uid +
-                    ',#clip' + uid
-                );
-
-                fullLayout._paper
-                    .selectAll(query)
-                    .remove();
-
-                rangesliderContainers
-                    .selectAll(query)
-                    .remove();
-            }
-
-            if(!isVisible || !trace._module.colorbar) {
-                fullLayout._infolayer.selectAll('.cb' + uid).remove();
-            }
-        }
-
-        // loop over the base plot modules present on graph
-        var basePlotModules = fullLayout._basePlotModules;
-        for(i = 0; i < basePlotModules.length; i++) {
-            basePlotModules[i].plot(gd);
-        }
-
-        // keep reference to shape layers in subplots
-        var layerSubplot = fullLayout._paper.selectAll('.layer-subplot');
-        fullLayout._shapeSubplotLayers = layerSubplot.selectAll('.shapelayer');
-
-        // styling separate from drawing
-        Plots.style(gd);
-
-        // show annotations and shapes
-        Registry.getComponentMethod('shapes', 'draw')(gd);
-        Registry.getComponentMethod('annotations', 'draw')(gd);
-
-        // source links
-        Plots.addLinks(gd);
-
-        // Mark the first render as complete
-        fullLayout._replotting = false;
-
-        return Plots.previousPromises(gd);
-    }
-
-    // An initial paint must be completed before these components can be
-    // correctly sized and the whole plot re-margined. fullLayout._replotting must
-    // be set to false before these will work properly.
-    function finalDraw() {
-        Registry.getComponentMethod('shapes', 'draw')(gd);
-        Registry.getComponentMethod('images', 'draw')(gd);
-        Registry.getComponentMethod('annotations', 'draw')(gd);
-        Registry.getComponentMethod('legend', 'draw')(gd);
-        Registry.getComponentMethod('rangeslider', 'draw')(gd);
-        Registry.getComponentMethod('rangeselector', 'draw')(gd);
-        Registry.getComponentMethod('sliders', 'draw')(gd);
-        Registry.getComponentMethod('updatemenus', 'draw')(gd);
-    }
-
     var seq = [
         Plots.previousPromises,
         addFrames,
@@ -435,9 +349,10 @@ exports.plot = function(gd, data, layout, config) {
     seq.push(subroutines.layoutStyles);
     if(hasCartesian) seq.push(drawAxes);
     seq.push(
-        drawData,
-        finalDraw,
+        subroutines.drawData,
+        subroutines.finalDraw,
         initInteractions,
+        Plots.addLinks,
         Plots.rehover,
         Plots.previousPromises
     );
@@ -666,7 +581,7 @@ exports.newPlot = function(gd, data, layout, config) {
     gd = Lib.getGraphDiv(gd);
 
     // remove gl contexts
-    Plots.cleanPlot([], {}, gd._fullData || {}, gd._fullLayout || {});
+    Plots.cleanPlot([], {}, gd._fullData || [], gd._fullLayout || {}, gd.calcdata || []);
 
     Plots.purge(gd);
     return exports.plot(gd, data, layout, config);
@@ -1381,8 +1296,8 @@ exports.restyle = function restyle(gd, astr, val, _traces) {
 
     var traces = helpers.coerceTraceIndices(gd, _traces);
 
-    var specs = _restyle(gd, aobj, traces),
-        flags = specs.flags;
+    var specs = _restyle(gd, aobj, traces);
+    var flags = specs.flags;
 
     // clear calcdata and/or axis types if required so they get regenerated
     if(flags.clearCalc) gd.calcdata = undefined;
@@ -1746,8 +1661,8 @@ exports.relayout = function relayout(gd, astr, val) {
 
     if(Object.keys(aobj).length) gd.changed = true;
 
-    var specs = _relayout(gd, aobj),
-        flags = specs.flags;
+    var specs = _relayout(gd, aobj);
+    var flags = specs.flags;
 
     // clear calcdata if required
     if(flags.calc) gd.calcdata = undefined;
@@ -1768,6 +1683,30 @@ exports.relayout = function relayout(gd, astr, val) {
 
         if(flags.legend) seq.push(subroutines.doLegend);
         if(flags.layoutstyle) seq.push(subroutines.layoutStyles);
+
+        if(flags.axrange) {
+            // N.B. leave as sequence of subroutines (for now) instead of
+            // subroutine of its own so that finalDraw always gets
+            // executed after drawData
+            seq.push(
+                // TODO
+                // no test fail when commenting out doAutoRangeAndConstraints,
+                // but I think we do need this (maybe just the enforce part?)
+                // Am I right?
+                // More info in:
+                // https://github.com/plotly/plotly.js/issues/2540
+                subroutines.doAutoRangeAndConstraints,
+                // TODO
+                // can target specific axes,
+                // do not have to redraw all axes here
+                // See:
+                // https://github.com/plotly/plotly.js/issues/2547
+                subroutines.doTicksRelayout,
+                subroutines.drawData,
+                subroutines.finalDraw
+            );
+        }
+
         if(flags.ticks) seq.push(subroutines.doTicksRelayout);
         if(flags.modebar) seq.push(subroutines.doModeBar);
         if(flags.camera) seq.push(subroutines.doCamera);
@@ -1988,7 +1927,7 @@ function _relayout(gd, aobj) {
             }
             Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
         }
-        else if(pleaf.match(cartesianConstants.AX_NAME_PATTERN)) {
+        else if(pleaf.match(AX_NAME_PATTERN)) {
             var fullProp = Lib.nestedProperty(fullLayout, ai).get(),
                 newType = (vi || {}).type;
 
@@ -2041,8 +1980,9 @@ function _relayout(gd, aobj) {
             if(checkForAutorange && (refAutorange(gd, objToAutorange, 'x') || refAutorange(gd, objToAutorange, 'y'))) {
                 flags.calc = true;
             }
-            else editTypes.update(flags, updateValObject);
-
+            else {
+                editTypes.update(flags, updateValObject);
+            }
 
             // prepare the edits object we'll send to applyContainerArrayChanges
             if(!arrayEdits[arrayStr]) arrayEdits[arrayStr] = {};
@@ -2193,11 +2133,11 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) {
 
     var traces = helpers.coerceTraceIndices(gd, _traces);
 
-    var restyleSpecs = _restyle(gd, Lib.extendFlat({}, traceUpdate), traces),
-        restyleFlags = restyleSpecs.flags;
+    var restyleSpecs = _restyle(gd, Lib.extendFlat({}, traceUpdate), traces);
+    var restyleFlags = restyleSpecs.flags;
 
-    var relayoutSpecs = _relayout(gd, Lib.extendFlat({}, layoutUpdate)),
-        relayoutFlags = relayoutSpecs.flags;
+    var relayoutSpecs = _relayout(gd, Lib.extendFlat({}, layoutUpdate));
+    var relayoutFlags = relayoutSpecs.flags;
 
     // clear calcdata and/or axis types if required
     if(restyleFlags.clearCalc || relayoutFlags.calc) gd.calcdata = undefined;
@@ -2232,6 +2172,14 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) {
         if(restyleFlags.colorbars) seq.push(subroutines.doColorBars);
         if(relayoutFlags.legend) seq.push(subroutines.doLegend);
         if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles);
+        if(relayoutFlags.axrange) {
+            seq.push(
+                subroutines.doAutoRangeAndConstraints,
+                subroutines.doTicksRelayout,
+                subroutines.drawData,
+                subroutines.finalDraw
+            );
+        }
         if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout);
         if(relayoutFlags.modebar) seq.push(subroutines.doModeBar);
         if(relayoutFlags.camera) seq.push(subroutines.doCamera);
@@ -2384,6 +2332,14 @@ exports.react = function(gd, data, layout, config) {
             if(restyleFlags.colorbars) seq.push(subroutines.doColorBars);
             if(relayoutFlags.legend) seq.push(subroutines.doLegend);
             if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles);
+            if(relayoutFlags.axrange) {
+                seq.push(
+                    subroutines.doAutoRangeAndConstraints,
+                    subroutines.doTicksRelayout,
+                    subroutines.drawData,
+                    subroutines.finalDraw
+                );
+            }
             if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout);
             if(relayoutFlags.modebar) seq.push(subroutines.doModeBar);
             if(relayoutFlags.camera) seq.push(subroutines.doCamera);
@@ -3240,11 +3196,12 @@ exports.deleteFrames = function(gd, frameList) {
 exports.purge = function purge(gd) {
     gd = Lib.getGraphDiv(gd);
 
-    var fullLayout = gd._fullLayout || {},
-        fullData = gd._fullData || [];
+    var fullLayout = gd._fullLayout || {};
+    var fullData = gd._fullData || [];
+    var calcdata = gd.calcdata || [];
 
     // remove gl contexts
-    Plots.cleanPlot([], {}, fullData, fullLayout);
+    Plots.cleanPlot([], {}, fullData, fullLayout, calcdata);
 
     // purge properties
     Plots.purge(gd);
diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js
index 54994abf457..94638f8ff50 100644
--- a/src/plot_api/plot_schema.js
+++ b/src/plot_api/plot_schema.js
@@ -25,6 +25,7 @@ var editTypes = require('./edit_types');
 
 var extendFlat = Lib.extendFlat;
 var extendDeepAll = Lib.extendDeepAll;
+var isPlainObject = Lib.isPlainObject;
 
 var IS_SUBPLOT_OBJ = '_isSubplotObj';
 var IS_LINKED_TO_ARRAY = '_isLinkedToArray';
@@ -140,7 +141,7 @@ exports.crawl = function(attrs, callback, specifiedLevel, attrString) {
 
         if(exports.isValObject(attr)) return;
 
-        if(Lib.isPlainObject(attr) && attrName !== 'impliedEdits') {
+        if(isPlainObject(attr) && attrName !== 'impliedEdits') {
             exports.crawl(attr, callback, level + 1, fullAttrString);
         }
     });
@@ -387,7 +388,7 @@ function recurseIntoValObject(valObject, parts, i) {
     // the innermost schema item we find.
     for(; i < parts.length; i++) {
         var newValObject = valObject[parts[i]];
-        if(Lib.isPlainObject(newValObject)) valObject = newValObject;
+        if(isPlainObject(newValObject)) valObject = newValObject;
         else break;
 
         if(i === parts.length - 1) break;
@@ -486,13 +487,12 @@ function getLayoutAttributes() {
 
         if(!_module.layoutAttributes) continue;
 
-        if(_module.name === 'cartesian') {
-            handleBasePlotModule(layoutAttributes, _module, 'xaxis');
-            handleBasePlotModule(layoutAttributes, _module, 'yaxis');
-        }
-        else {
+        if(Array.isArray(_module.attr)) {
+            for(var i = 0; i < _module.attr.length; i++) {
+                handleBasePlotModule(layoutAttributes, _module, _module.attr[i]);
+            }
+        } else {
             var astr = _module.attr === 'subplot' ? _module.name : _module.attr;
-
             handleBasePlotModule(layoutAttributes, _module, astr);
         }
     }
@@ -565,6 +565,7 @@ function getFramesAttributes() {
 function formatAttributes(attrs) {
     mergeValTypeAndRole(attrs);
     formatArrayContainers(attrs);
+    stringify(attrs);
 
     return attrs;
 }
@@ -596,7 +597,7 @@ function mergeValTypeAndRole(attrs) {
                 attrs[attrName + 'src'] = makeSrcAttr(attrName);
             }
         }
-        else if(Lib.isPlainObject(attr)) {
+        else if(isPlainObject(attr)) {
             // all attrs container objects get role 'object'
             attr.role = 'object';
         }
@@ -624,6 +625,29 @@ function formatArrayContainers(attrs) {
     exports.crawl(attrs, callback);
 }
 
+// this can take around 10ms and should only be run from PlotSchema.get(),
+// to ensure JSON.stringify(PlotSchema.get()) gives the intended result.
+function stringify(attrs) {
+    function walk(attr) {
+        for(var k in attr) {
+            if(isPlainObject(attr[k])) {
+                walk(attr[k]);
+            } else if(Array.isArray(attr[k])) {
+                for(var i = 0; i < attr[k].length; i++) {
+                    walk(attr[k][i]);
+                }
+            } else {
+                // as JSON.stringify(/test/) // => {}
+                if(attr[k] instanceof RegExp) {
+                    attr[k] = attr[k].toString();
+                }
+            }
+        }
+    }
+
+    walk(attrs);
+}
+
 function assignPolarLayoutAttrs(layoutAttributes) {
     extendFlat(layoutAttributes, {
         radialaxis: polarAxisAttrs.radialaxis,
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index fa74da9e967..dfd76a1021f 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -11,16 +11,22 @@
 var d3 = require('d3');
 var Registry = require('../registry');
 var Plots = require('../plots/plots');
+
 var Lib = require('../lib');
+var clearGlCanvases = require('../lib/clear_gl_canvases');
 
 var Color = require('../components/color');
 var Drawing = require('../components/drawing');
 var Titles = require('../components/titles');
 var ModeBar = require('../components/modebar');
+
 var Axes = require('../plots/cartesian/axes');
-var initInteractions = require('../plots/cartesian/graph_interact');
 var cartesianConstants = require('../plots/cartesian/constants');
 var alignmentConstants = require('../constants/alignment');
+var axisConstraints = require('../plots/cartesian/constraints');
+var enforceAxisConstraints = axisConstraints.enforce;
+var cleanAxisConstraints = axisConstraints.clean;
+var doAutoRange = require('../plots/cartesian/autorange').doAutoRange;
 
 exports.layoutStyles = function(gd) {
     return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd);
@@ -173,7 +179,7 @@ exports.lsInner = function(gd) {
                 .append('rect');
         });
 
-        plotClip.select('rect').attr({
+        plotinfo.clipRect = plotClip.select('rect').attr({
             width: xa._length,
             height: ya._length
         });
@@ -451,6 +457,12 @@ exports.doLegend = function(gd) {
 
 exports.doTicksRelayout = function(gd) {
     Axes.doTicks(gd, 'redraw');
+
+    if(gd._fullLayout._hasOnlyLargeSploms) {
+        clearGlCanvases(gd);
+        Registry.subplotsRegistry.splom.plot(gd);
+    }
+
     exports.drawMainTitle(gd);
     return Plots.previousPromises(gd);
 };
@@ -459,7 +471,6 @@ exports.doModeBar = function(gd) {
     var fullLayout = gd._fullLayout;
 
     ModeBar.manage(gd);
-    initInteractions(gd);
 
     for(var i = 0; i < fullLayout._basePlotModules.length; i++) {
         var updateFx = fullLayout._basePlotModules[i].updateFx;
@@ -480,3 +491,84 @@ exports.doCamera = function(gd) {
         scene.setCamera(sceneLayout.camera);
     }
 };
+
+exports.drawData = function(gd) {
+    var fullLayout = gd._fullLayout;
+    var calcdata = gd.calcdata;
+    var rangesliderContainers = fullLayout._infolayer.selectAll('g.rangeslider-container');
+    var i;
+
+    // in case of traces that were heatmaps or contour maps
+    // previously, remove them and their colorbars explicitly
+    for(i = 0; i < calcdata.length; i++) {
+        var trace = calcdata[i][0].trace;
+        var isVisible = (trace.visible === true);
+        var uid = trace.uid;
+
+        if(!isVisible || !Registry.traceIs(trace, '2dMap')) {
+            var query = (
+                '.hm' + uid +
+                ',.contour' + uid +
+                ',#clip' + uid
+            );
+
+            fullLayout._paper
+                .selectAll(query)
+                .remove();
+
+            rangesliderContainers
+                .selectAll(query)
+                .remove();
+        }
+
+        if(!isVisible || !trace._module.colorbar) {
+            fullLayout._infolayer.selectAll('.cb' + uid).remove();
+        }
+    }
+
+    clearGlCanvases(gd);
+
+    // loop over the base plot modules present on graph
+    var basePlotModules = fullLayout._basePlotModules;
+    for(i = 0; i < basePlotModules.length; i++) {
+        basePlotModules[i].plot(gd);
+    }
+
+    // styling separate from drawing
+    Plots.style(gd);
+
+    // show annotations and shapes
+    Registry.getComponentMethod('shapes', 'draw')(gd);
+    Registry.getComponentMethod('annotations', 'draw')(gd);
+
+    // Mark the first render as complete
+    fullLayout._replotting = false;
+
+    return Plots.previousPromises(gd);
+};
+
+exports.doAutoRangeAndConstraints = function(gd) {
+    var axList = Axes.list(gd, '', true);
+
+    for(var i = 0; i < axList.length; i++) {
+        var ax = axList[i];
+        cleanAxisConstraints(gd, ax);
+        doAutoRange(ax);
+    }
+
+    enforceAxisConstraints(gd);
+};
+
+// An initial paint must be completed before these components can be
+// correctly sized and the whole plot re-margined. fullLayout._replotting must
+// be set to false before these will work properly.
+exports.finalDraw = function(gd) {
+    Registry.getComponentMethod('shapes', 'draw')(gd);
+    Registry.getComponentMethod('images', 'draw')(gd);
+    Registry.getComponentMethod('annotations', 'draw')(gd);
+    Registry.getComponentMethod('legend', 'draw')(gd);
+    Registry.getComponentMethod('rangeslider', 'draw')(gd);
+    Registry.getComponentMethod('rangeselector', 'draw')(gd);
+    Registry.getComponentMethod('sliders', 'draw')(gd);
+    Registry.getComponentMethod('updatemenus', 'draw')(gd);
+};
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index c44db0c9856..c44dd3db8f9 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -1502,9 +1502,9 @@ axes.makeClipPaths = function(gd) {
 //          ax._rl (stored linearized range for use by zoom/pan)
 //     or can pass in an axis object directly
 axes.doTicks = function(gd, axid, skipTitle) {
-    var fullLayout = gd._fullLayout,
-        ax,
-        independent = false;
+    var fullLayout = gd._fullLayout;
+    var ax;
+    var independent = false;
 
     // allow passing an independent axis object instead of id
     if(typeof axid === 'object') {
@@ -1517,18 +1517,14 @@ axes.doTicks = function(gd, axid, skipTitle) {
 
         if(axid === 'redraw') {
             fullLayout._paper.selectAll('g.subplot').each(function(subplot) {
-                var plotinfo = fullLayout._plots[subplot],
-                    xa = plotinfo.xaxis,
-                    ya = plotinfo.yaxis;
-
-                plotinfo.xaxislayer
-                    .selectAll('.' + xa._id + 'tick').remove();
-                plotinfo.yaxislayer
-                    .selectAll('.' + ya._id + 'tick').remove();
-                plotinfo.gridlayer
-                    .selectAll('path').remove();
-                plotinfo.zerolinelayer
-                    .selectAll('path').remove();
+                var plotinfo = fullLayout._plots[subplot];
+                var xa = plotinfo.xaxis;
+                var ya = plotinfo.yaxis;
+
+                plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick').remove();
+                plotinfo.yaxislayer.selectAll('.' + ya._id + 'tick').remove();
+                if(plotinfo.gridlayer) plotinfo.gridlayer.selectAll('path').remove();
+                if(plotinfo.zerolinelayer) plotinfo.zerolinelayer.selectAll('path').remove();
                 fullLayout._infolayer.select('.g-' + xa._id + 'title').remove();
                 fullLayout._infolayer.select('.g-' + ya._id + 'title').remove();
             });
@@ -1552,7 +1548,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
 
     var axLetter = axid.charAt(0);
     var counterLetter = axes.counterLetter(axid);
-    var vals = axes.calcTicks(ax);
+    var vals = ax._vals = axes.calcTicks(ax);
     var datafn = function(d) { return [d.text, d.x, ax.mirror, d.font, d.fontSize, d.fontColor].join('_'); };
     var tcls = axid + 'tick';
     var gcls = axid + 'grid';
@@ -2092,6 +2088,8 @@ axes.doTicks = function(gd, axid, skipTitle) {
     }
 
     function drawGrid(plotinfo, counteraxis, subplot) {
+        if(fullLayout._hasOnlyLargeSploms) return;
+
         var gridcontainer = plotinfo.gridlayer.selectAll('.' + axid);
         var zlcontainer = plotinfo.zerolinelayer;
         var gridvals = plotinfo['hidegrid' + axLetter] ? [] : valsClipped;
@@ -2190,7 +2188,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
             }
             drawTicks(mainPlotinfo[axLetter + 'axislayer'], tickpath);
 
-            tickSubplots = Object.keys(ax._linepositions);
+            tickSubplots = Object.keys(ax._linepositions || {});
         }
 
         tickSubplots.map(function(subplot) {
diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js
index cf34c9d4b21..220e5eca798 100644
--- a/src/plots/cartesian/axis_defaults.js
+++ b/src/plots/cartesian/axis_defaults.js
@@ -33,6 +33,7 @@ var setConvert = require('./set_convert');
  */
 module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, options, layoutOut) {
     var letter = options.letter;
+    var id = containerOut._id;
     var font = options.font || {};
 
     var visible = coerce('visible', !options.cheateronly);
@@ -69,8 +70,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
     // if axis.color was provided, use it for fonts too; otherwise,
     // inherit from global font color in case that was provided.
     var dfltFontColor = (dfltColor === containerIn.color) ? dfltColor : font.color;
+    // try to get default title from splom trace, fallback to graph-wide value
+    var dfltTitle = ((layoutOut._splomAxes || {})[letter] || {})[id] || layoutOut._dfltTitle[letter];
 
-    coerce('title', layoutOut._dfltTitle[letter]);
+    coerce('title', dfltTitle);
     Lib.coerceFont(coerce, 'titlefont', {
         family: font.family,
         size: Math.round(font.size * 1.2),
diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js
index 2153c8de1df..010c35fd57a 100644
--- a/src/plots/cartesian/dragbox.js
+++ b/src/plots/cartesian/dragbox.js
@@ -16,6 +16,7 @@ var supportsPassive = require('has-passive-events');
 var Registry = require('../../registry');
 var Lib = require('../../lib');
 var svgTextUtils = require('../../lib/svg_text_utils');
+var clearGlCanvases = require('../../lib/clear_gl_canvases');
 var Color = require('../../components/color');
 var Drawing = require('../../components/drawing');
 var Fx = require('../../components/fx');
@@ -27,7 +28,8 @@ var Plots = require('../plots');
 
 var doTicks = require('./axes').doTicks;
 var getFromId = require('./axis_ids').getFromId;
-var prepSelect = require('./select');
+var prepSelect = require('./select').prepSelect;
+var clearSelect = require('./select').clearSelect;
 var scaleZoom = require('./scale_zoom');
 
 var constants = require('./constants');
@@ -54,66 +56,74 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
     // within DBLCLICKDELAY so we can check for click or doubleclick events
     // dragged stores whether a drag has occurred, so we don't have to
     // redraw unnecessarily, ie if no move bigger than MINDRAG or MINZOOM px
-    var fullLayout = gd._fullLayout;
     var zoomlayer = gd._fullLayout._zoomlayer;
     var isMainDrag = (ns + ew === 'nsew');
     var singleEnd = (ns + ew).length === 1;
 
-    var subplots, xa, ya, xs, ys, pw, ph, xActive, yActive, cursor,
-        isSubplotConstrained, xaLinked, yaLinked;
+    // main subplot x and y (i.e. found in plotinfo - the main ones)
+    var xa0, ya0;
+    // {ax._id: ax} hash objects
+    var xaHash, yaHash;
+    // xaHash/yaHash values (arrays)
+    var xaxes, yaxes;
+    // main axis offsets
+    var xs, ys;
+    // main axis lengths
+    var pw, ph;
+    // contains keys 'xaHash', 'yaHash', 'xaxes', and 'yaxes'
+    // which are the x/y {ax._id: ax} hash objects and their values
+    // for linked axis relative to this subplot
+    var links;
+    // set to ew/ns val when active, set to '' when inactive
+    var xActive, yActive;
+    // are all axes in this subplot are fixed?
+    var allFixedRanges;
+    // is subplot constrained?
+    var isSubplotConstrained;
+    // do we need to edit x/y ranges?
+    var editX, editY;
 
     function recomputeAxisLists() {
-        xa = [plotinfo.xaxis];
-        ya = [plotinfo.yaxis];
-        var xa0 = xa[0];
-        var ya0 = ya[0];
+        xa0 = plotinfo.xaxis;
+        ya0 = plotinfo.yaxis;
         pw = xa0._length;
         ph = ya0._length;
+        xs = xa0._offset;
+        ys = ya0._offset;
 
-        var constraintGroups = fullLayout._axisConstraintGroups;
-        var xIDs = [xa0._id];
-        var yIDs = [ya0._id];
+        xaHash = {};
+        xaHash[xa0._id] = xa0;
+        yaHash = {};
+        yaHash[ya0._id] = ya0;
 
         // if we're dragging two axes at once, also drag overlays
-        subplots = [plotinfo].concat((ns && ew) ? plotinfo.overlays : []);
-
-        for(var i = 1; i < subplots.length; i++) {
-            var subplotXa = subplots[i].xaxis,
-                subplotYa = subplots[i].yaxis;
-
-            if(xa.indexOf(subplotXa) === -1) {
-                xa.push(subplotXa);
-                xIDs.push(subplotXa._id);
-            }
-
-            if(ya.indexOf(subplotYa) === -1) {
-                ya.push(subplotYa);
-                yIDs.push(subplotYa._id);
+        if(ns && ew) {
+            var overlays = plotinfo.overlays;
+            for(var i = 0; i < overlays.length; i++) {
+                var xa = overlays[i].xaxis;
+                xaHash[xa._id] = xa;
+                var ya = overlays[i].yaxis;
+                yaHash[ya._id] = ya;
             }
         }
 
-        xActive = isDirectionActive(xa, ew);
-        yActive = isDirectionActive(ya, ns);
-        cursor = getDragCursor(yActive + xActive, fullLayout.dragmode);
-        xs = xa0._offset;
-        ys = ya0._offset;
-
-        var links = calcLinks(constraintGroups, xIDs, yIDs);
-        isSubplotConstrained = links.xy;
+        xaxes = hashValues(xaHash);
+        yaxes = hashValues(yaHash);
+        xActive = isDirectionActive(xaxes, ew);
+        yActive = isDirectionActive(yaxes, ns);
+        allFixedRanges = !yActive && !xActive;
 
-        // finally make the list of axis objects to link
-        xaLinked = [];
-        for(var xLinkID in links.x) { xaLinked.push(getFromId(gd, xLinkID)); }
-        yaLinked = [];
-        for(var yLinkID in links.y) { yaLinked.push(getFromId(gd, yLinkID)); }
+        links = calcLinks(gd, xaHash, yaHash);
+        isSubplotConstrained = links.isSubplotConstrained;
+        editX = ew || isSubplotConstrained;
+        editY = ns || isSubplotConstrained;
     }
 
     recomputeAxisLists();
 
+    var cursor = getDragCursor(yActive + xActive, gd._fullLayout.dragmode, isMainDrag);
     var dragger = makeRectDragger(plotinfo, ns + ew + 'drag', cursor, x, y, w, h);
 
-    var allFixedRanges = !yActive && !xActive;
-
     // still need to make the element if the axes are disabled
     // but nuke its events (except for maindrag which needs them for hover)
     // and stop there
@@ -130,6 +140,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
         prepFn: function(e, startX, startY) {
             var dragModeNow = gd._fullLayout.dragmode;
 
+            recomputeAxisLists();
+
             if(!allFixedRanges) {
                 if(isMainDrag) {
                     // main dragger handles all drag modes, and changes
@@ -150,8 +162,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
             else dragOptions.minDrag = undefined;
 
             if(isSelectOrLasso(dragModeNow)) {
-                dragOptions.xaxes = xa;
-                dragOptions.yaxes = ya;
+                dragOptions.xaxes = xaxes;
+                dragOptions.yaxes = yaxes;
                 prepSelect(e, startX, startY, dragOptions, dragModeNow);
             }
             else if(allFixedRanges) {
@@ -183,7 +195,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
                 Fx.click(gd, evt, plotinfo.id);
             }
             else if(numClicks === 1 && singleEnd) {
-                var ax = ns ? ya[0] : xa[0],
+                var ax = ns ? ya0 : xa0,
                     end = (ns === 's' || ew === 'w') ? 0 : 1,
                     attrStr = ax._name + '.range[' + end + ']',
                     initialText = getEndText(ax, end),
@@ -203,7 +215,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
                         .call(svgTextUtils.makeEditable, {
                             gd: gd,
                             immediate: true,
-                            background: fullLayout.paper_bgcolor,
+                            background: gd._fullLayout.paper_bgcolor,
                             text: String(initialText),
                             fill: ax.tickfont ? ax.tickfont.color : '#444',
                             horizontalAlign: hAlign,
@@ -333,8 +345,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
         }
 
         // TODO: edit linked axes in zoomAxRanges and in dragTail
-        if(zoomMode === 'xy' || zoomMode === 'x') zoomAxRanges(xa, box.l / pw, box.r / pw, updates, xaLinked);
-        if(zoomMode === 'xy' || zoomMode === 'y') zoomAxRanges(ya, (ph - box.b) / ph, (ph - box.t) / ph, updates, yaLinked);
+        if(zoomMode === 'xy' || zoomMode === 'x') {
+            zoomAxRanges(xaxes, box.l / pw, box.r / pw, updates, links.xaxes);
+        }
+        if(zoomMode === 'xy' || zoomMode === 'y') {
+            zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes);
+        }
 
         removeZoombox(gd);
         dragTail();
@@ -346,14 +362,13 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
     // wait a little after scrolling before redrawing
     var redrawTimer = null;
     var REDRAWDELAY = constants.REDRAWDELAY;
-    var mainplot = plotinfo.mainplot ?
-            fullLayout._plots[plotinfo.mainplot] : plotinfo;
+    var mainplot = plotinfo.mainplot ? gd._fullLayout._plots[plotinfo.mainplot] : plotinfo;
 
     function zoomWheel(e) {
         // deactivate mousewheel scrolling on embedded graphs
         // devs can override this with layout._enablescrollzoom,
         // but _ ensures this setting won't leave their page
-        if(!gd._context.scrollZoom && !fullLayout._enablescrollzoom) {
+        if(!gd._context.scrollZoom && !gd._fullLayout._enablescrollzoom) {
             return;
         }
 
@@ -404,20 +419,24 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
             ax.range = axRange.map(doZoom);
         }
 
-        if(ew || isSubplotConstrained) {
+        if(editX) {
             // if we're only zooming this axis because of constraints,
             // zoom it about the center
             if(!ew) xfrac = 0.5;
 
-            for(i = 0; i < xa.length; i++) zoomWheelOneAxis(xa[i], xfrac, zoom);
+            for(i = 0; i < xaxes.length; i++) {
+                zoomWheelOneAxis(xaxes[i], xfrac, zoom);
+            }
 
             scrollViewBox[2] *= zoom;
             scrollViewBox[0] += scrollViewBox[2] * xfrac * (1 / zoom - 1);
         }
-        if(ns || isSubplotConstrained) {
+        if(editY) {
             if(!ns) yfrac = 0.5;
 
-            for(i = 0; i < ya.length; i++) zoomWheelOneAxis(ya[i], yfrac, zoom);
+            for(i = 0; i < yaxes.length; i++) {
+                zoomWheelOneAxis(yaxes[i], yfrac, zoom);
+            }
 
             scrollViewBox[3] *= zoom;
             scrollViewBox[1] += scrollViewBox[3] * (1 - yfrac) * (1 / zoom - 1);
@@ -455,11 +474,9 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
             return;
         }
 
-        recomputeAxisLists();
-
         if(xActive === 'ew' || yActive === 'ns') {
-            if(xActive) dragAxList(xa, dx);
-            if(yActive) dragAxList(ya, dy);
+            if(xActive) dragAxList(xaxes, dx);
+            if(yActive) dragAxList(yaxes, dy);
             updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]);
             ticksAndAnnotations(yActive, xActive);
             return;
@@ -499,12 +516,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
             dy = dxySign * dxyFraction * ph;
         }
 
-        if(xActive === 'w') dx = dz(xa, 0, dx);
-        else if(xActive === 'e') dx = dz(xa, 1, -dx);
+        if(xActive === 'w') dx = dz(xaxes, 0, dx);
+        else if(xActive === 'e') dx = dz(xaxes, 1, -dx);
         else if(!xActive) dx = 0;
 
-        if(yActive === 'n') dy = dz(ya, 1, dy);
-        else if(yActive === 's') dy = dz(ya, 0, -dy);
+        if(yActive === 'n') dy = dz(yaxes, 1, dy);
+        else if(yActive === 's') dy = dz(yaxes, 0, -dy);
         else if(!yActive) dy = 0;
 
         var x0 = (xActive === 'w') ? dx : 0;
@@ -515,17 +532,17 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
             if(!xActive && yActive.length === 1) {
                 // dragging one end of the y axis of a constrained subplot
                 // scale the other axis the same about its middle
-                for(i = 0; i < xa.length; i++) {
-                    xa[i].range = xa[i]._r.slice();
-                    scaleZoom(xa[i], 1 - dy / ph);
+                for(i = 0; i < xaxes.length; i++) {
+                    xaxes[i].range = xaxes[i]._r.slice();
+                    scaleZoom(xaxes[i], 1 - dy / ph);
                 }
                 dx = dy * pw / ph;
                 x0 = dx / 2;
             }
             if(!yActive && xActive.length === 1) {
-                for(i = 0; i < ya.length; i++) {
-                    ya[i].range = ya[i]._r.slice();
-                    scaleZoom(ya[i], 1 - dx / pw);
+                for(i = 0; i < yaxes.length; i++) {
+                    yaxes[i].range = yaxes[i]._r.slice();
+                    scaleZoom(yaxes[i], 1 - dx / pw);
                 }
                 dy = dx * ph / pw;
                 y0 = dy / 2;
@@ -533,7 +550,6 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
         }
 
         updateSubplots([x0, y0, pw - dx, ph - dy]);
-
         ticksAndAnnotations(yActive, xActive);
     }
 
@@ -549,13 +565,13 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
             }
         }
 
-        if(ew || isSubplotConstrained) {
-            pushActiveAxIds(xa);
-            pushActiveAxIds(xaLinked);
+        if(editX) {
+            pushActiveAxIds(xaxes);
+            pushActiveAxIds(links.xaxes);
         }
-        if(ns || isSubplotConstrained) {
-            pushActiveAxIds(ya);
-            pushActiveAxIds(yaLinked);
+        if(editY) {
+            pushActiveAxIds(yaxes);
+            pushActiveAxIds(links.yaxes);
         }
 
         updates = {};
@@ -583,16 +599,16 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
         // annotations and shapes 'draw' method is slow,
         // use the finer-grained 'drawOne' method instead
 
-        redrawObjs(fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne'));
-        redrawObjs(fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne'));
-        redrawObjs(fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true);
+        redrawObjs(gd._fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne'));
+        redrawObjs(gd._fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne'));
+        redrawObjs(gd._fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true);
     }
 
     function doubleClick() {
         if(gd._transitioningWithDuration) return;
 
         var doubleClickConfig = gd._context.doubleClick,
-            axList = (xActive ? xa : []).concat(yActive ? ya : []),
+            axList = (xActive ? xaxes : []).concat(yActive ? yaxes : []),
             attrs = {};
 
         var ax, i, rangeInitial;
@@ -631,12 +647,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
         else if(doubleClickConfig === 'reset') {
             // when we're resetting, reset all linked axes too, so we get back
             // to the fully-auto-with-constraints situation
-            if(xActive || isSubplotConstrained) axList = axList.concat(xaLinked);
-            if(yActive && !isSubplotConstrained) axList = axList.concat(yaLinked);
+            if(xActive || isSubplotConstrained) axList = axList.concat(links.xaxes);
+            if(yActive && !isSubplotConstrained) axList = axList.concat(links.yaxes);
 
             if(isSubplotConstrained) {
-                if(!xActive) axList = axList.concat(xa);
-                else if(!yActive) axList = axList.concat(ya);
+                if(!xActive) axList = axList.concat(xaxes);
+                else if(!yActive) axList = axList.concat(yaxes);
             }
 
             for(i = 0; i < axList.length; i++) {
@@ -674,128 +690,154 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
 
     // updateSubplots - find all plot viewboxes that should be
     // affected by this drag, and update them. look for all plots
-    // sharing an affected axis (including the one being dragged)
+    // sharing an affected axis (including the one being dragged),
+    // includes also scattergl and splom logic.
     function updateSubplots(viewBox) {
+        var fullLayout = gd._fullLayout;
         var plotinfos = fullLayout._plots;
-        var subplots = Object.keys(plotinfos);
-        var xScaleFactor = viewBox[2] / xa[0]._length;
-        var yScaleFactor = viewBox[3] / ya[0]._length;
-        var editX = ew || isSubplotConstrained;
-        var editY = ns || isSubplotConstrained;
-
-        var i, xScaleFactor2, yScaleFactor2, clipDx, clipDy;
-
-        // Find the appropriate scaling for this axis, if it's linked to the
-        // dragged axes by constraints. 0 is special, it means this axis shouldn't
-        // ever be scaled (will be converted to 1 if the other axis is scaled)
-        function getLinkedScaleFactor(ax) {
-            if(ax.fixedrange) return 0;
-
-            if(editX && xaLinked.indexOf(ax) !== -1) {
-                return xScaleFactor;
-            }
-            if(editY && (isSubplotConstrained ? xaLinked : yaLinked).indexOf(ax) !== -1) {
-                return yScaleFactor;
-            }
-            return 0;
-        }
+        var subplots = fullLayout._subplots.cartesian;
 
-        function scaleAndGetShift(ax, scaleFactor) {
-            if(scaleFactor) {
-                ax.range = ax._r.slice();
-                scaleZoom(ax, scaleFactor);
-                return getShift(ax, scaleFactor);
-            }
-            return 0;
+        // TODO can we move these to outer scope?
+        var hasScatterGl = fullLayout._has('scattergl');
+        var hasOnlyLargeSploms = fullLayout._hasOnlyLargeSploms;
+        var hasSplom = hasOnlyLargeSploms || fullLayout._has('splom');
+        var hasSVG = fullLayout._has('svg');
+        var hasDraggedPts = fullLayout._has('draggedPts');
+
+        var i, sp, xa, ya;
+
+        if(hasSplom || hasScatterGl) {
+            clearGlCanvases(gd);
         }
 
-        function getShift(ax, scaleFactor) {
-            return ax._length * (1 - scaleFactor) * FROM_TL[ax.constraintoward || 'middle'];
+        if(hasSplom) {
+            Registry.subplotsRegistry.splom.drag(gd);
+            if(hasOnlyLargeSploms) return;
         }
 
-        // clear gl frame, if any, since we preserve drawing buffer
-        // FIXME: code duplication with cartesian.plot
-        if(fullLayout._glcanvas && fullLayout._glcanvas.size()) {
-            fullLayout._glcanvas.each(function(d) {
-                if(d.regl) {
-                    d.regl.clear({
-                        color: true
-                    });
+        if(hasScatterGl) {
+            // loop over all subplots (w/o exceptions) here,
+            // as we cleared the gl canvases above
+            for(i = 0; i < subplots.length; i++) {
+                sp = plotinfos[subplots[i]];
+                xa = sp.xaxis;
+                ya = sp.yaxis;
+
+                var scene = sp._scene;
+                if(scene) {
+                    // FIXME: possibly we could update axis internal _r and _rl here
+                    var xrng = Lib.simpleMap(xa.range, xa.r2l);
+                    var yrng = Lib.simpleMap(ya.range, ya.r2l);
+                    scene.update({range: [xrng[0], yrng[0], xrng[1], yrng[1]]});
                 }
-            });
+            }
         }
 
-        for(i = 0; i < subplots.length; i++) {
-            var subplot = plotinfos[subplots[i]],
-                xa2 = subplot.xaxis,
-                ya2 = subplot.yaxis,
-                editX2 = editX && !xa2.fixedrange && (xa.indexOf(xa2) !== -1),
-                editY2 = editY && !ya2.fixedrange && (ya.indexOf(ya2) !== -1);
-
-            // scattergl translate
-            if(subplot._scene && subplot._scene.update) {
-                // FIXME: possibly we could update axis internal _r and _rl here
-                var xaRange = Lib.simpleMap(xa2.range, xa2.r2l),
-                    yaRange = Lib.simpleMap(ya2.range, ya2.r2l);
-                subplot._scene.update(
-                    {range: [xaRange[0], yaRange[0], xaRange[1], yaRange[1]]}
-                );
-            }
+        if(hasSVG) {
+            var xScaleFactor = viewBox[2] / xa0._length;
+            var yScaleFactor = viewBox[3] / ya0._length;
 
-            if(editX2) {
-                xScaleFactor2 = xScaleFactor;
-                clipDx = ew ? viewBox[0] : getShift(xa2, xScaleFactor2);
-            }
-            else {
-                xScaleFactor2 = getLinkedScaleFactor(xa2);
-                clipDx = scaleAndGetShift(xa2, xScaleFactor2);
-            }
+            for(i = 0; i < subplots.length; i++) {
+                sp = plotinfos[subplots[i]];
+                xa = sp.xaxis;
+                ya = sp.yaxis;
 
-            if(editY2) {
-                yScaleFactor2 = yScaleFactor;
-                clipDy = ns ? viewBox[1] : getShift(ya2, yScaleFactor2);
-            }
-            else {
-                yScaleFactor2 = getLinkedScaleFactor(ya2);
-                clipDy = scaleAndGetShift(ya2, yScaleFactor2);
-            }
+                var editX2 = editX && !xa.fixedrange && xaHash[xa._id];
+                var editY2 = editY && !ya.fixedrange && yaHash[ya._id];
 
-            // don't scale at all if neither axis is scalable here
-            if(!xScaleFactor2 && !yScaleFactor2) {
-                continue;
-            }
+                var xScaleFactor2, yScaleFactor2;
+                var clipDx, clipDy;
 
-            // but if only one is, reset the other axis scaling
-            if(!xScaleFactor2) xScaleFactor2 = 1;
-            if(!yScaleFactor2) yScaleFactor2 = 1;
+                if(editX2) {
+                    xScaleFactor2 = xScaleFactor;
+                    clipDx = ew ? viewBox[0] : getShift(xa, xScaleFactor2);
+                } else {
+                    xScaleFactor2 = getLinkedScaleFactor(xa, xScaleFactor, yScaleFactor);
+                    clipDx = scaleAndGetShift(xa, xScaleFactor2);
+                }
 
-            var plotDx = xa2._offset - clipDx / xScaleFactor2,
-                plotDy = ya2._offset - clipDy / yScaleFactor2;
+                if(editY2) {
+                    yScaleFactor2 = yScaleFactor;
+                    clipDy = ns ? viewBox[1] : getShift(ya, yScaleFactor2);
+                } else {
+                    yScaleFactor2 = getLinkedScaleFactor(ya, xScaleFactor, yScaleFactor);
+                    clipDy = scaleAndGetShift(ya, yScaleFactor2);
+                }
 
-            fullLayout._defs.select('#' + subplot.clipId + '> rect')
-                .call(Drawing.setTranslate, clipDx, clipDy)
-                .call(Drawing.setScale, xScaleFactor2, yScaleFactor2);
+                // don't scale at all if neither axis is scalable here
+                if(!xScaleFactor2 && !yScaleFactor2) {
+                    continue;
+                }
 
-            var traceGroups = subplot.plot
-                .selectAll('.scatterlayer .trace, .boxlayer .trace, .violinlayer .trace');
+                // but if only one is, reset the other axis scaling
+                if(!xScaleFactor2) xScaleFactor2 = 1;
+                if(!yScaleFactor2) yScaleFactor2 = 1;
+
+                var plotDx = xa._offset - clipDx / xScaleFactor2;
+                var plotDy = ya._offset - clipDy / yScaleFactor2;
+
+                // TODO could be more efficient here:
+                // setTranslate and setScale do a lot of extra work
+                // when working independently, should perhaps combine
+                // them into a single routine.
+                sp.clipRect
+                    .call(Drawing.setTranslate, clipDx, clipDy)
+                    .call(Drawing.setScale, xScaleFactor2, yScaleFactor2);
+
+                sp.plot
+                    .call(Drawing.setTranslate, plotDx, plotDy)
+                    .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2);
+
+                // TODO move these selectAll calls out of here
+                // and stash them somewhere nice, see:
+                // https://github.com/plotly/plotly.js/issues/2548
+                if(hasDraggedPts) {
+                    var traceGroups = sp.plot
+                        .selectAll('.scatterlayer .trace, .boxlayer .trace, .violinlayer .trace');
+
+                    // This is specifically directed at marker points in scatter, box and violin traces,
+                    // applying an inverse scale to individual points to counteract
+                    // the scale of the trace as a whole:
+                    traceGroups.selectAll('.point')
+                        .call(Drawing.setPointGroupScale, xScaleFactor2, yScaleFactor2);
+                    traceGroups.selectAll('.textpoint')
+                        .call(Drawing.setTextPointsScale, xScaleFactor2, yScaleFactor2);
+                    traceGroups
+                        .call(Drawing.hideOutsideRangePoints, sp);
+
+                    sp.plot.selectAll('.barlayer .trace')
+                        .call(Drawing.hideOutsideRangePoints, sp, '.bartext');
+                }
+            }
+        }
+    }
 
-            subplot.plot
-                .call(Drawing.setTranslate, plotDx, plotDy)
-                .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2);
+    // Find the appropriate scaling for this axis, if it's linked to the
+    // dragged axes by constraints. 0 is special, it means this axis shouldn't
+    // ever be scaled (will be converted to 1 if the other axis is scaled)
+    function getLinkedScaleFactor(ax, xScaleFactor, yScaleFactor) {
+        if(ax.fixedrange) return 0;
 
-            // This is specifically directed at marker points in scatter, box and violin traces,
-            // applying an inverse scale to individual points to counteract
-            // the scale of the trace as a whole:
-            traceGroups.selectAll('.point')
-                .call(Drawing.setPointGroupScale, xScaleFactor2, yScaleFactor2);
-            traceGroups.selectAll('.textpoint')
-                .call(Drawing.setTextPointsScale, xScaleFactor2, yScaleFactor2);
-            traceGroups
-                .call(Drawing.hideOutsideRangePoints, subplot);
+        if(editX && links.xaHash[ax._id]) {
+            return xScaleFactor;
+        }
+        if(editY && (isSubplotConstrained ? links.xaHash : links.yaHash)[ax._id]) {
+            return yScaleFactor;
+        }
+        return 0;
+    }
 
-            subplot.plot.selectAll('.barlayer .trace')
-                .call(Drawing.hideOutsideRangePoints, subplot, '.bartext');
+    function scaleAndGetShift(ax, scaleFactor) {
+        if(scaleFactor) {
+            ax.range = ax._r.slice();
+            scaleZoom(ax, scaleFactor);
+            return getShift(ax, scaleFactor);
         }
+        return 0;
+    }
+
+    function getShift(ax, scaleFactor) {
+        return ax._length * (1 - scaleFactor) * FROM_TL[ax.constraintoward || 'middle'];
     }
 
     return dragger;
@@ -897,9 +939,12 @@ function dZoom(d) {
         1 / (1 / Math.max(d, -0.3) + 3.222));
 }
 
-function getDragCursor(nsew, dragmode) {
+function getDragCursor(nsew, dragmode, isMainDrag) {
     if(!nsew) return 'pointer';
     if(nsew === 'nsew') {
+        // in this case here, clear cursor and
+        // use the cursor style set on <g .draglayer>
+        if(isMainDrag) return '';
         if(dragmode === 'pan') return 'move';
         return 'crosshair';
     }
@@ -930,13 +975,6 @@ function makeCorners(zoomlayer, xs, ys) {
         .attr('d', 'M0,0Z');
 }
 
-function clearSelect(zoomlayer) {
-    // until we get around to persistent selections, remove the outline
-    // here. The selection itself will be removed when the plot redraws
-    // at the end.
-    zoomlayer.selectAll('.select-outline').remove();
-}
-
 function updateZoombox(zb, corners, box, path0, dimmed, lum) {
     zb.attr('d',
         path0 + 'M' + (box.l) + ',' + (box.t) + 'v' + (box.h) +
@@ -1002,40 +1040,40 @@ function xyCorners(box) {
             'h' + clen + 'v3h-' + (clen + 3) + 'Z';
 }
 
-function calcLinks(constraintGroups, xIDs, yIDs) {
+function calcLinks(gd, xaHash, yaHash) {
+    var constraintGroups = gd._fullLayout._axisConstraintGroups;
     var isSubplotConstrained = false;
     var xLinks = {};
     var yLinks = {};
-    var i, j, k;
+    var xID, yID, xLinkID, yLinkID;
 
-    var group, xLinkID, yLinkID;
-    for(i = 0; i < constraintGroups.length; i++) {
-        group = constraintGroups[i];
+    for(var i = 0; i < constraintGroups.length; i++) {
+        var group = constraintGroups[i];
         // check if any of the x axes we're dragging is in this constraint group
-        for(j = 0; j < xIDs.length; j++) {
-            if(group[xIDs[j]]) {
+        for(xID in xaHash) {
+            if(group[xID]) {
                 // put the rest of these axes into xLinks, if we're not already
                 // dragging them, so we know to scale these axes automatically too
                 // to match the changes in the dragged x axes
                 for(xLinkID in group) {
-                    if((xLinkID.charAt(0) === 'x' ? xIDs : yIDs).indexOf(xLinkID) === -1) {
+                    if(!(xLinkID.charAt(0) === 'x' ? xaHash : yaHash)[xLinkID]) {
                         xLinks[xLinkID] = 1;
                     }
                 }
 
                 // check if the x and y axes of THIS drag are linked
-                for(k = 0; k < yIDs.length; k++) {
-                    if(group[yIDs[k]]) isSubplotConstrained = true;
+                for(yID in yaHash) {
+                    if(group[yID]) isSubplotConstrained = true;
                 }
             }
         }
 
         // now check if any of the y axes we're dragging is in this constraint group
         // only look for outside links, as we've already checked for links within the dragger
-        for(j = 0; j < yIDs.length; j++) {
-            if(group[yIDs[j]]) {
+        for(yID in yaHash) {
+            if(group[yID]) {
                 for(yLinkID in group) {
-                    if((yLinkID.charAt(0) === 'x' ? xIDs : yIDs).indexOf(yLinkID) === -1) {
+                    if(!(yLinkID.charAt(0) === 'x' ? xaHash : yaHash)[yLinkID]) {
                         yLinks[yLinkID] = 1;
                     }
                 }
@@ -1050,10 +1088,29 @@ function calcLinks(constraintGroups, xIDs, yIDs) {
         Lib.extendFlat(xLinks, yLinks);
         yLinks = {};
     }
+
+    var xaHashLinked = {};
+    var xaxesLinked = [];
+    for(xLinkID in xLinks) {
+        var xa = getFromId(gd, xLinkID);
+        xaxesLinked.push(xa);
+        xaHashLinked[xa._id] = xa;
+    }
+
+    var yaHashLinked = {};
+    var yaxesLinked = [];
+    for(yLinkID in yLinks) {
+        var ya = getFromId(gd, yLinkID);
+        yaxesLinked.push(ya);
+        yaHashLinked[ya._id] = ya;
+    }
+
     return {
-        x: xLinks,
-        y: yLinks,
-        xy: isSubplotConstrained
+        xaHash: xaHashLinked,
+        yaHash: yaHashLinked,
+        xaxes: xaxesLinked,
+        yaxes: yaxesLinked,
+        isSubplotConstrained: isSubplotConstrained
     };
 }
 
@@ -1075,6 +1132,12 @@ function attachWheelEventHandler(element, handler) {
     }
 }
 
+function hashValues(hash) {
+    var out = [];
+    for(var k in hash) out.push(hash[k]);
+    return out;
+}
+
 module.exports = {
     makeDragBox: makeDragBox,
 
@@ -1087,7 +1150,6 @@ module.exports = {
     xyCorners: xyCorners,
     transitionZoombox: transitionZoombox,
     removeZoombox: removeZoombox,
-    clearSelect: clearSelect,
     showDoubleClickNotifier: showDoubleClickNotifier,
 
     attachWheelEventHandler: attachWheelEventHandler
diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js
index a56d7befae1..f872ddef16e 100644
--- a/src/plots/cartesian/graph_interact.js
+++ b/src/plots/cartesian/graph_interact.js
@@ -13,11 +13,12 @@ var d3 = require('d3');
 
 var Fx = require('../../components/fx');
 var dragElement = require('../../components/dragelement');
+var setCursor = require('../../lib/setcursor');
 
-var constants = require('./constants');
 var makeDragBox = require('./dragbox').makeDragBox;
+var DRAGGERSIZE = require('./constants').DRAGGERSIZE;
 
-module.exports = function initInteractions(gd) {
+exports.initInteractions = function initInteractions(gd) {
     var fullLayout = gd._fullLayout;
 
     if(gd._context.staticPlot) {
@@ -26,7 +27,7 @@ module.exports = function initInteractions(gd) {
         return;
     }
 
-    if(!fullLayout._has('cartesian') && !fullLayout._has('gl2d')) return;
+    if(!fullLayout._has('cartesian') && !fullLayout._has('gl2d') && !fullLayout._has('splom')) return;
 
     var subplots = Object.keys(fullLayout._plots || {}).sort(function(a, b) {
         // sort overlays last, then by x axis number, then y axis number
@@ -43,12 +44,9 @@ module.exports = function initInteractions(gd) {
 
     subplots.forEach(function(subplot) {
         var plotinfo = fullLayout._plots[subplot];
-
         var xa = plotinfo.xaxis;
         var ya = plotinfo.yaxis;
 
-        var DRAGGERSIZE = constants.DRAGGERSIZE;
-
         // main and corner draggers need not be repeated for
         // overlaid subplots - these draggers drag them all
         if(!plotinfo.mainplot) {
@@ -139,17 +137,29 @@ module.exports = function initInteractions(gd) {
     var hoverLayer = fullLayout._hoverlayer.node();
 
     hoverLayer.onmousemove = function(evt) {
-        evt.target = fullLayout._lasthover;
+        evt.target = gd._fullLayout._lasthover;
         Fx.hover(gd, evt, fullLayout._hoversubplot);
     };
 
     hoverLayer.onclick = function(evt) {
-        evt.target = fullLayout._lasthover;
+        evt.target = gd._fullLayout._lasthover;
         Fx.click(gd, evt);
     };
 
     // also delegate mousedowns... TODO: does this actually work?
     hoverLayer.onmousedown = function(evt) {
-        fullLayout._lasthover.onmousedown(evt);
+        gd._fullLayout._lasthover.onmousedown(evt);
     };
+
+    exports.updateFx(fullLayout);
+};
+
+// Minimal set of update needed on 'modebar' edits.
+// We only need to update the <g .draglayer> cursor style.
+//
+// Note that changing the axis configuration and/or the fixedrange attribute
+// should trigger a full initInteractions.
+exports.updateFx = function(fullLayout) {
+    var cursor = fullLayout.dragmode === 'pan' ? 'move' : 'crosshair';
+    setCursor(fullLayout._draggers, cursor);
 };
diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js
index 2d454e4a68f..3304737bfb6 100644
--- a/src/plots/cartesian/index.js
+++ b/src/plots/cartesian/index.js
@@ -135,17 +135,6 @@ exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) {
         }
     }
 
-    // clear gl frame, if any, since we preserve drawing buffer
-    if(fullLayout._glcanvas && fullLayout._glcanvas.size()) {
-        fullLayout._glcanvas.each(function(d) {
-            if(d.regl) {
-                d.regl.clear({
-                    color: true
-                });
-            }
-        });
-    }
-
     for(i = 0; i < subplots.length; i++) {
         var subplot = subplots[i],
             subplotInfo = fullLayout._plots[subplot];
@@ -221,11 +210,23 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback
 }
 
 exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
-    var oldModules = oldFullLayout._modules || [],
-        newModules = newFullLayout._modules || [];
-
-    var hadScatter, hasScatter, hadGl, hasGl, i, oldPlots, ids, subplotInfo, moduleName;
-
+    var oldModules = oldFullLayout._modules || [];
+    var newModules = newFullLayout._modules || [];
+    var oldPlots = oldFullLayout._plots || {};
+
+    var hadScatter, hasScatter;
+    var hadGl, hasGl;
+    var i, k, subplotInfo, moduleName;
+
+    // when going from a large splom graph to something else,
+    // we need to clear <g subplot> so that the new cartesian subplot
+    // can have the correct layer ordering
+    if(oldFullLayout._hasOnlyLargeSploms && !newFullLayout._hasOnlyLargeSploms) {
+        for(k in oldPlots) {
+            subplotInfo = oldPlots[k];
+            if(subplotInfo.plotgroup) subplotInfo.plotgroup.remove();
+        }
+    }
 
     for(i = 0; i < oldModules.length; i++) {
         moduleName = oldModules[i].name;
@@ -240,12 +241,8 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
     }
 
     if(hadScatter && !hasScatter) {
-        oldPlots = oldFullLayout._plots;
-        ids = Object.keys(oldPlots || {});
-
-        for(i = 0; i < ids.length; i++) {
-            subplotInfo = oldPlots[ids[i]];
-
+        for(k in oldPlots) {
+            subplotInfo = oldPlots[k];
             if(subplotInfo.plot) {
                 subplotInfo.plot.select('g.scatterlayer')
                     .selectAll('g.trace')
@@ -260,11 +257,8 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
     }
 
     if(hadGl && !hasGl) {
-        oldPlots = oldFullLayout._plots;
-        ids = Object.keys(oldPlots || {});
-
-        for(i = 0; i < ids.length; i++) {
-            subplotInfo = oldPlots[ids[i]];
+        for(k in oldPlots) {
+            subplotInfo = oldPlots[k];
 
             if(subplotInfo._scene) {
                 subplotInfo._scene.destroy();
@@ -334,7 +328,7 @@ exports.drawFramework = function(gd) {
         // initialize list of overlay subplots
         plotinfo.overlays = [];
 
-        makeSubplotLayer(plotinfo);
+        makeSubplotLayer(gd, plotinfo);
 
         // fill in list of overlay subplots
         if(plotinfo.mainplot) {
@@ -350,7 +344,7 @@ exports.drawFramework = function(gd) {
 };
 
 exports.rangePlot = function(gd, plotinfo, cdSubplot) {
-    makeSubplotLayer(plotinfo);
+    makeSubplotLayer(gd, plotinfo);
     plotOne(gd, plotinfo, cdSubplot);
     Plots.style(gd);
 };
@@ -382,45 +376,59 @@ function makeSubplotData(gd) {
     return subplotData;
 }
 
-function makeSubplotLayer(plotinfo) {
+function makeSubplotLayer(gd, plotinfo) {
     var plotgroup = plotinfo.plotgroup;
     var id = plotinfo.id;
     var xLayer = constants.layerValue2layerClass[plotinfo.xaxis.layer];
     var yLayer = constants.layerValue2layerClass[plotinfo.yaxis.layer];
+    var hasOnlyLargeSploms = gd._fullLayout._hasOnlyLargeSploms;
 
     if(!plotinfo.mainplot) {
-        var backLayer = ensureSingle(plotgroup, 'g', 'layer-subplot');
-        plotinfo.shapelayer = ensureSingle(backLayer, 'g', 'shapelayer');
-        plotinfo.imagelayer = ensureSingle(backLayer, 'g', 'imagelayer');
-
-        plotinfo.gridlayer = ensureSingle(plotgroup, 'g', 'gridlayer');
-
-        plotinfo.zerolinelayer = ensureSingle(plotgroup, 'g', 'zerolinelayer');
-
-        ensureSingle(plotgroup, 'path', 'xlines-below');
-        ensureSingle(plotgroup, 'path', 'ylines-below');
-        plotinfo.overlinesBelow = ensureSingle(plotgroup, 'g', 'overlines-below');
-
-        ensureSingle(plotgroup, 'g', 'xaxislayer-below');
-        ensureSingle(plotgroup, 'g', 'yaxislayer-below');
-        plotinfo.overaxesBelow = ensureSingle(plotgroup, 'g', 'overaxes-below');
-
-        plotinfo.plot = ensureSingle(plotgroup, 'g', 'plot');
-        plotinfo.overplot = ensureSingle(plotgroup, 'g', 'overplot');
-
-        ensureSingle(plotgroup, 'path', 'xlines-above');
-        ensureSingle(plotgroup, 'path', 'ylines-above');
-        plotinfo.overlinesAbove = ensureSingle(plotgroup, 'g', 'overlines-above');
-
-        ensureSingle(plotgroup, 'g', 'xaxislayer-above');
-        ensureSingle(plotgroup, 'g', 'yaxislayer-above');
-        plotinfo.overaxesAbove = ensureSingle(plotgroup, 'g', 'overaxes-above');
-
-        // set refs to correct layers as determined by 'axis.layer'
-        plotinfo.xlines = plotgroup.select('.xlines-' + xLayer);
-        plotinfo.ylines = plotgroup.select('.ylines-' + yLayer);
-        plotinfo.xaxislayer = plotgroup.select('.xaxislayer-' + xLayer);
-        plotinfo.yaxislayer = plotgroup.select('.yaxislayer-' + yLayer);
+        if(hasOnlyLargeSploms) {
+            // TODO could do even better
+            // - we don't need plot (but we would have to mock it in lsInner
+            //   and other places
+            // - we don't (x|y)lines and (x|y)axislayer for most subplots
+            //   usually just the bottom x and left y axes.
+            plotinfo.plot = ensureSingle(plotgroup, 'g', 'plot');
+            plotinfo.xlines = ensureSingle(plotgroup, 'path', 'xlines-above');
+            plotinfo.ylines = ensureSingle(plotgroup, 'path', 'ylines-above');
+            plotinfo.xaxislayer = ensureSingle(plotgroup, 'g', 'xaxislayer-above');
+            plotinfo.yaxislayer = ensureSingle(plotgroup, 'g', 'yaxislayer-above');
+        }
+        else {
+            var backLayer = ensureSingle(plotgroup, 'g', 'layer-subplot');
+            plotinfo.shapelayer = ensureSingle(backLayer, 'g', 'shapelayer');
+            plotinfo.imagelayer = ensureSingle(backLayer, 'g', 'imagelayer');
+
+            plotinfo.gridlayer = ensureSingle(plotgroup, 'g', 'gridlayer');
+            plotinfo.zerolinelayer = ensureSingle(plotgroup, 'g', 'zerolinelayer');
+
+            ensureSingle(plotgroup, 'path', 'xlines-below');
+            ensureSingle(plotgroup, 'path', 'ylines-below');
+            plotinfo.overlinesBelow = ensureSingle(plotgroup, 'g', 'overlines-below');
+
+            ensureSingle(plotgroup, 'g', 'xaxislayer-below');
+            ensureSingle(plotgroup, 'g', 'yaxislayer-below');
+            plotinfo.overaxesBelow = ensureSingle(plotgroup, 'g', 'overaxes-below');
+
+            plotinfo.plot = ensureSingle(plotgroup, 'g', 'plot');
+            plotinfo.overplot = ensureSingle(plotgroup, 'g', 'overplot');
+
+            plotinfo.xlines = ensureSingle(plotgroup, 'path', 'xlines-above');
+            plotinfo.ylines = ensureSingle(plotgroup, 'path', 'ylines-above');
+            plotinfo.overlinesAbove = ensureSingle(plotgroup, 'g', 'overlines-above');
+
+            ensureSingle(plotgroup, 'g', 'xaxislayer-above');
+            ensureSingle(plotgroup, 'g', 'yaxislayer-above');
+            plotinfo.overaxesAbove = ensureSingle(plotgroup, 'g', 'overaxes-above');
+
+            // set refs to correct layers as determined by 'axis.layer'
+            plotinfo.xlines = plotgroup.select('.xlines-' + xLayer);
+            plotinfo.ylines = plotgroup.select('.ylines-' + yLayer);
+            plotinfo.xaxislayer = plotgroup.select('.xaxislayer-' + xLayer);
+            plotinfo.yaxislayer = plotgroup.select('.yaxislayer-' + yLayer);
+        }
     }
     else {
         var mainplotinfo = plotinfo.mainplotinfo;
@@ -455,14 +463,16 @@ function makeSubplotLayer(plotinfo) {
         plotinfo.yaxislayer = mainplotgroup.select('.overaxes-' + yLayer).select('.' + yId);
     }
 
-    ensureSingleAndAddDatum(plotinfo.gridlayer, 'g', plotinfo.xaxis._id);
-    ensureSingleAndAddDatum(plotinfo.gridlayer, 'g', 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++) {
-        ensureSingle(plotinfo.plot, 'g', constants.traceLayerClasses[i]);
+    if(!hasOnlyLargeSploms) {
+        ensureSingleAndAddDatum(plotinfo.gridlayer, 'g', plotinfo.xaxis._id);
+        ensureSingleAndAddDatum(plotinfo.gridlayer, 'g', plotinfo.yaxis._id);
+        plotinfo.gridlayer.selectAll('g').sort(axisIds.idSort);
+
+        for(var i = 0; i < constants.traceLayerClasses.length; i++) {
+            ensureSingle(plotinfo.plot, 'g', constants.traceLayerClasses[i]);
+        }
     }
 
     plotinfo.xlines
@@ -539,3 +549,5 @@ exports.toSVG = function(gd) {
 
     canvases.each(canvasToImage);
 };
+
+exports.updateFx = require('./graph_interact').updateFx;
diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js
index 8334ab5d0ec..25bfda44393 100644
--- a/src/plots/cartesian/layout_attributes.js
+++ b/src/plots/cartesian/layout_attributes.js
@@ -100,10 +100,10 @@ module.exports = {
         valType: 'info_array',
         role: 'info',
         items: [
-            {valType: 'any', editType: 'plot+margins', impliedEdits: {'^autorange': false}},
-            {valType: 'any', editType: 'plot+margins', impliedEdits: {'^autorange': false}}
+            {valType: 'any', editType: 'axrange+margins', impliedEdits: {'^autorange': false}},
+            {valType: 'any', editType: 'axrange+margins', impliedEdits: {'^autorange': false}}
         ],
-        editType: 'plot+margins',
+        editType: 'axrange+margins',
         impliedEdits: {'autorange': false},
         description: [
             'Sets the range of this axis.',
diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js
index 92837250622..4eda20dec49 100644
--- a/src/plots/cartesian/select.js
+++ b/src/plots/cartesian/select.js
@@ -10,59 +10,83 @@
 'use strict';
 
 var polybool = require('polybooljs');
+
+var Registry = require('../../registry');
+var Color = require('../../components/color');
+var Fx = require('../../components/fx');
+
 var polygon = require('../../lib/polygon');
 var throttle = require('../../lib/throttle');
-var color = require('../../components/color');
 var makeEventData = require('../../components/fx/helpers').makeEventData;
-var Fx = require('../../components/fx');
+var getFromId = require('./axis_ids').getFromId;
+var sortModules = require('../sort_modules').sortModules;
 
-var axes = require('./axes');
 var constants = require('./constants');
+var MINSELECT = constants.MINSELECT;
 
 var filteredPolygon = polygon.filter;
 var polygonTester = polygon.tester;
 var multipolygonTester = polygon.multitester;
-var MINSELECT = constants.MINSELECT;
 
 function getAxId(ax) { return ax._id; }
 
-module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
-    var gd = dragOptions.gd,
-        fullLayout = gd._fullLayout,
-        zoomLayer = fullLayout._zoomlayer,
-        dragBBox = dragOptions.element.getBoundingClientRect(),
-        plotinfo = dragOptions.plotinfo,
-        xs = plotinfo.xaxis._offset,
-        ys = plotinfo.yaxis._offset,
-        x0 = startX - dragBBox.left,
-        y0 = startY - dragBBox.top,
-        x1 = x0,
-        y1 = y0,
-        path0 = 'M' + x0 + ',' + y0,
-        pw = dragOptions.xaxes[0]._length,
-        ph = dragOptions.yaxes[0]._length,
-        xAxisIds = dragOptions.xaxes.map(getAxId),
-        yAxisIds = dragOptions.yaxes.map(getAxId),
-        allAxes = dragOptions.xaxes.concat(dragOptions.yaxes),
-        filterPoly, testPoly, mergedPolygons, currentPolygon,
-        subtract = e.altKey;
-
-
-    // take over selection polygons from prev mode, if any
-    if((e.shiftKey || e.altKey) && (plotinfo.selection && plotinfo.selection.polygons) && !dragOptions.polygons) {
+function prepSelect(e, startX, startY, dragOptions, mode) {
+    var gd = dragOptions.gd;
+    var fullLayout = gd._fullLayout;
+    var zoomLayer = fullLayout._zoomlayer;
+    var dragBBox = dragOptions.element.getBoundingClientRect();
+    var plotinfo = dragOptions.plotinfo;
+    var xs = plotinfo.xaxis._offset;
+    var ys = plotinfo.yaxis._offset;
+    var x0 = startX - dragBBox.left;
+    var y0 = startY - dragBBox.top;
+    var x1 = x0;
+    var y1 = y0;
+    var path0 = 'M' + x0 + ',' + y0;
+    var pw = dragOptions.xaxes[0]._length;
+    var ph = dragOptions.yaxes[0]._length;
+    var xAxisIds = dragOptions.xaxes.map(getAxId);
+    var yAxisIds = dragOptions.yaxes.map(getAxId);
+    var allAxes = dragOptions.xaxes.concat(dragOptions.yaxes);
+    var subtract = e.altKey;
+
+    var filterPoly, testPoly, mergedPolygons, currentPolygon;
+    var i, cd, trace, searchInfo, eventData;
+
+    var selectingOnSameSubplot = (
+        fullLayout._lastSelectedSubplot &&
+        fullLayout._lastSelectedSubplot === plotinfo.id
+    );
+
+    if(
+        selectingOnSameSubplot &&
+        (e.shiftKey || e.altKey) &&
+        (plotinfo.selection && plotinfo.selection.polygons) &&
+        !dragOptions.polygons
+    ) {
+        // take over selection polygons from prev mode, if any
         dragOptions.polygons = plotinfo.selection.polygons;
         dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons;
-    }
-    // create new polygons, if shift mode
-    else if((!e.shiftKey && !e.altKey) || ((e.shiftKey || e.altKey) && !plotinfo.selection)) {
+    } else if(
+        (!e.shiftKey && !e.altKey) ||
+        ((e.shiftKey || e.altKey) && !plotinfo.selection)
+    ) {
+        // create new polygons, if shift mode or selecting across different subplots
         plotinfo.selection = {};
         plotinfo.selection.polygons = dragOptions.polygons = [];
         plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = [];
     }
 
+    // clear selection outline when selecting a different subplot
+    if(!selectingOnSameSubplot) {
+        clearSelect(zoomLayer);
+        fullLayout._lastSelectedSubplot = plotinfo.id;
+    }
+
     if(mode === 'lasso') {
         filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX);
     }
+
     var outlines = zoomLayer.selectAll('path.select-outline-' + plotinfo.id).data([1, 2]);
 
     outlines.enter()
@@ -74,8 +98,8 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
     var corners = zoomLayer.append('path')
         .attr('class', 'zoombox-corners')
         .style({
-            fill: color.background,
-            stroke: color.defaultLine,
+            fill: Color.background,
+            stroke: Color.defaultLine,
             'stroke-width': 1
         })
         .attr('transform', 'translate(' + xs + ', ' + ys + ')')
@@ -86,11 +110,11 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
     var searchTraces = [];
     var throttleID = fullLayout._uid + constants.SELECTID;
     var selection = [];
-    var i, cd, trace, searchInfo, eventData;
 
     for(i = 0; i < gd.calcdata.length; i++) {
         cd = gd.calcdata[i];
         trace = cd[0].trace;
+
         if(trace.visible !== true || !trace._module || !trace._module.selectPoints) continue;
 
         if(dragOptions.subplot) {
@@ -99,23 +123,32 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
                 trace.geo === dragOptions.subplot
             ) {
                 searchTraces.push({
-                    selectPoints: trace._module.selectPoints,
-                    style: trace._module.style,
+                    _module: trace._module,
                     cd: cd,
                     xaxis: dragOptions.xaxes[0],
                     yaxis: dragOptions.yaxes[0]
                 });
             }
+        } else if(
+            trace.type === 'splom' &&
+            // FIXME: make sure we don't have more than single axis for splom
+            trace._xaxes[xAxisIds[0]] && trace._yaxes[yAxisIds[0]]
+        ) {
+            searchTraces.push({
+                _module: trace._module,
+                cd: cd,
+                xaxis: dragOptions.xaxes[0],
+                yaxis: dragOptions.yaxes[0]
+            });
         } else {
             if(xAxisIds.indexOf(trace.xaxis) === -1) continue;
             if(yAxisIds.indexOf(trace.yaxis) === -1) continue;
 
             searchTraces.push({
-                selectPoints: trace._module.selectPoints,
-                style: trace._module.style,
+                _module: trace._module,
                 cd: cd,
-                xaxis: axes.getFromId(gd, trace.xaxis),
-                yaxis: axes.getFromId(gd, trace.yaxis)
+                xaxis: getFromId(gd, trace.xaxis),
+                yaxis: getFromId(gd, trace.yaxis)
             });
         }
     }
@@ -238,7 +271,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
                 for(i = 0; i < searchTraces.length; i++) {
                     searchInfo = searchTraces[i];
 
-                    traceSelection = searchInfo.selectPoints(searchInfo, testPoly);
+                    traceSelection = searchInfo._module.selectPoints(searchInfo, testPoly);
                     traceSelections.push(traceSelection);
 
                     thisSelection = fillSelectionItem(traceSelection, searchInfo);
@@ -269,7 +302,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
                 outlines.remove();
                 for(i = 0; i < searchTraces.length; i++) {
                     searchInfo = searchTraces[i];
-                    searchInfo.selectPoints(searchInfo, false);
+                    searchInfo._module.selectPoints(searchInfo, false);
                 }
 
                 updateSelectedState(gd, searchTraces);
@@ -303,10 +336,10 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
             }
         });
     };
-};
+}
 
 function updateSelectedState(gd, searchTraces, eventData) {
-    var i, searchInfo, trace;
+    var i, j, searchInfo, trace;
 
     if(eventData) {
         var pts = eventData.points || [];
@@ -336,17 +369,44 @@ function updateSelectedState(gd, searchTraces, eventData) {
             trace = searchTraces[i].cd[0].trace;
             delete trace.selectedpoints;
             delete trace._input.selectedpoints;
-
-            // delete scattergl selection
-            if(searchTraces[i].cd[0].t && searchTraces[i].cd[0].t.scene) {
-                searchTraces[i].cd[0].t.scene.clearSelect();
-            }
         }
     }
 
+    // group searchInfo traces by trace modules
+    var lookup = {};
+
     for(i = 0; i < searchTraces.length; i++) {
         searchInfo = searchTraces[i];
-        if(searchInfo.style) searchInfo.style(gd, searchInfo.cd);
+
+        var name = searchInfo._module.name;
+        if(lookup[name]) {
+            lookup[name].push(searchInfo);
+        } else {
+            lookup[name] = [searchInfo];
+        }
+    }
+
+    var keys = Object.keys(lookup).sort(sortModules);
+
+    for(i = 0; i < keys.length; i++) {
+        var items = lookup[keys[i]];
+        var len = items.length;
+        var item0 = items[0];
+        var trace0 = item0.cd[0].trace;
+
+        if(Registry.traceIs(trace0, 'regl')) {
+            // plot regl traces per module
+            var cds = new Array(len);
+            for(j = 0; j < len; j++) {
+                cds[j] = items[j].cd;
+            }
+            item0._module.style(gd, cds);
+        } else {
+            // plot svg trace per trace
+            for(j = 0; j < len; j++) {
+                item0._module.style(gd, items[j].cd);
+            }
+        }
     }
 }
 
@@ -388,3 +448,15 @@ function fillSelectionItem(selection, searchInfo) {
 
     return selection;
 }
+
+function clearSelect(zoomlayer) {
+    // until we get around to persistent selections, remove the outline
+    // here. The selection itself will be removed when the plot redraws
+    // at the end.
+    zoomlayer.selectAll('.select-outline').remove();
+}
+
+module.exports = {
+    prepSelect: prepSelect,
+    clearSelect: clearSelect
+};
diff --git a/src/plots/cartesian/transition_axes.js b/src/plots/cartesian/transition_axes.js
index 281b9964b9b..c8d306d5e0e 100644
--- a/src/plots/cartesian/transition_axes.js
+++ b/src/plots/cartesian/transition_axes.js
@@ -233,7 +233,7 @@ module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCo
         var plotDx = xa2._offset - fracDx,
             plotDy = ya2._offset - fracDy;
 
-        fullLayout._defs.select('#' + subplot.clipId + '> rect')
+        subplot.clipRect
             .call(Drawing.setTranslate, clipDx, clipDy)
             .call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor);
 
diff --git a/src/plots/cartesian/type_defaults.js b/src/plots/cartesian/type_defaults.js
index eba7d535c3b..3d4f7c4bf35 100644
--- a/src/plots/cartesian/type_defaults.js
+++ b/src/plots/cartesian/type_defaults.js
@@ -46,8 +46,8 @@ function setAutoType(ax, data) {
     // only autotype if type is '-'
     if(ax.type !== '-') return;
 
-    var id = ax._id,
-        axLetter = id.charAt(0);
+    var id = ax._id;
+    var axLetter = id.charAt(0);
 
     // support 3d
     if(id.indexOf('scene') !== -1) id = axLetter;
@@ -63,18 +63,18 @@ function setAutoType(ax, data) {
         return;
     }
 
-    var calAttr = axLetter + 'calendar',
-        calendar = d0[calAttr];
+    var calAttr = axLetter + 'calendar';
+    var calendar = d0[calAttr];
+    var i;
 
     // check all boxes on this x axis to see
     // if they're dates, numbers, or categories
     if(isBoxWithoutPositionCoords(d0, axLetter)) {
-        var posLetter = getBoxPosLetter(d0),
-            boxPositions = [],
-            trace;
+        var posLetter = getBoxPosLetter(d0);
+        var boxPositions = [];
 
-        for(var i = 0; i < data.length; i++) {
-            trace = data[i];
+        for(i = 0; i < data.length; i++) {
+            var trace = data[i];
             if(!Registry.traceIs(trace, 'box-violin') ||
                (trace[axLetter + 'axis'] || axLetter) !== id) continue;
 
@@ -87,6 +87,16 @@ function setAutoType(ax, data) {
 
         ax.type = autoType(boxPositions, calendar);
     }
+    else if(d0.type === 'splom') {
+        var dimensions = d0.dimensions;
+        for(i = 0; i < dimensions.length; i++) {
+            var dim = dimensions[i];
+            if(dim.visible) {
+                ax.type = autoType(dim.values, calendar);
+                break;
+            }
+        }
+    }
     else {
         ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']], calendar);
     }
@@ -96,6 +106,13 @@ function getFirstNonEmptyTrace(data, id, axLetter) {
     for(var i = 0; i < data.length; i++) {
         var trace = data[i];
 
+        if(trace.type === 'splom' &&
+                trace._commonLength > 0 &&
+                trace['_' + axLetter + 'axes'][id]
+        ) {
+            return trace;
+        }
+
         if((trace[axLetter + 'axis'] || axLetter) === id) {
             if(isBoxWithoutPositionCoords(trace, axLetter)) {
                 return trace;
diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js
index f5ca806f1bb..e6d419503b5 100644
--- a/src/plots/geo/geo.js
+++ b/src/plots/geo/geo.js
@@ -20,7 +20,7 @@ var Fx = require('../../components/fx');
 var Plots = require('../plots');
 var Axes = require('../cartesian/axes');
 var dragElement = require('../../components/dragelement');
-var prepSelect = require('../cartesian/select');
+var prepSelect = require('../cartesian/select').prepSelect;
 
 var createGeoZoom = require('./zoom');
 var constants = require('./constants');
diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js
index 4df6dfd4555..18db3935a4d 100644
--- a/src/plots/mapbox/mapbox.js
+++ b/src/plots/mapbox/mapbox.js
@@ -14,7 +14,7 @@ var mapboxgl = require('mapbox-gl');
 var Fx = require('../../components/fx');
 var Lib = require('../../lib');
 var dragElement = require('../../components/dragelement');
-var prepSelect = require('../cartesian/select');
+var prepSelect = require('../cartesian/select').prepSelect;
 var constants = require('./constants');
 var layoutAttributes = require('./layout_attributes');
 var createMapboxLayer = require('./layers');
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 56c45fcdb17..80af2b79bc5 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -14,18 +14,20 @@ var isNumeric = require('fast-isnumeric');
 
 var Registry = require('../registry');
 var PlotSchema = require('../plot_api/plot_schema');
-var axisIDs = require('../plots/cartesian/axis_ids');
 var Lib = require('../lib');
-var _ = Lib._;
 var Color = require('../components/color');
 var BADNUM = require('../constants/numerical').BADNUM;
 
-var plots = module.exports = {};
+var axisIDs = require('../plots/cartesian/axis_ids');
+var sortBasePlotModules = require('./sort_modules').sortBasePlotModules;
 
 var animationAttrs = require('./animation_attributes');
 var frameAttrs = require('./frame_attributes');
 
 var relinkPrivateKeys = Lib.relinkPrivateKeys;
+var _ = Lib._;
+
+var plots = module.exports = {};
 
 // Expose registry methods on Plots for backward-compatibility
 Lib.extendFlat(plots, Registry);
@@ -287,6 +289,8 @@ plots.supplyDefaults = function(gd) {
     var newFullData = gd._fullData = [];
     var newData = gd.data || [];
 
+    var oldCalcdata = gd.calcdata || [];
+
     var context = gd._context || {};
 
     var i;
@@ -321,7 +325,6 @@ plots.supplyDefaults = function(gd) {
 
     // first fill in what we can of layout without looking at data
     // because fullData needs a few things from layout
-
     if(oldFullLayout._initialAutoSizeIsDone) {
 
         // coerce the updated layout while preserving width and height
@@ -364,12 +367,34 @@ plots.supplyDefaults = function(gd) {
     // clear the lists of trace and baseplot modules, and subplots
     newFullLayout._modules = [];
     newFullLayout._basePlotModules = [];
-    newFullLayout._subplots = emptySubplotLists();
+    var subplots = newFullLayout._subplots = emptySubplotLists();
+
+    // initialize axis and subplot hash objects for splom-generated grids
+    var splomAxes = newFullLayout._splomAxes = {x: {}, y: {}};
+    var splomSubplots = newFullLayout._splomSubplots = {};
 
     // then do the data
     newFullLayout._globalTransforms = (gd._context || {}).globalTransforms;
     plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout);
 
+    // redo grid size defaults with info about splom x/y axes,
+    // and fill in generated cartesian axes and subplots
+    var splomXa = Object.keys(splomAxes.x);
+    var splomYa = Object.keys(splomAxes.y);
+    if(splomXa.length > 1 && splomYa.length > 1) {
+        Registry.getComponentMethod('grid', 'sizeDefaults')(newLayout, newFullLayout);
+
+        for(i = 0; i < splomXa.length; i++) {
+            Lib.pushUnique(subplots.xaxis, splomXa[i]);
+        }
+        for(i = 0; i < splomYa.length; i++) {
+            Lib.pushUnique(subplots.yaxis, splomYa[i]);
+        }
+        for(var k in splomSubplots) {
+            Lib.pushUnique(subplots.cartesian, k);
+        }
+    }
+
     // attach helper method to check whether a plot type is present on graph
     newFullLayout._has = plots._hasPlotType.bind(newFullLayout);
 
@@ -389,6 +414,17 @@ plots.supplyDefaults = function(gd) {
     // finally, fill in the pieces of layout that may need to look at data
     plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData, gd._transitionData);
 
+    // turn on flag to optimize large splom-only graphs
+    // mostly by omitting SVG layers during Cartesian.drawFramework
+    newFullLayout._hasOnlyLargeSploms = (
+        newFullLayout._basePlotModules.length === 1 &&
+        newFullLayout._basePlotModules[0].name === 'splom' &&
+        splomXa.length > 15 &&
+        splomYa.length > 15 &&
+        newFullLayout.shapes.length === 0 &&
+        newFullLayout.images.length === 0
+    );
+
     // TODO remove in v2.0.0
     // add has-plot-type refs to fullLayout for backward compatibility
     newFullLayout._hasCartesian = newFullLayout._has('cartesian');
@@ -399,7 +435,7 @@ plots.supplyDefaults = function(gd) {
     newFullLayout._hasPie = newFullLayout._has('pie');
 
     // clean subplots and other artifacts from previous plot calls
-    plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout);
+    plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata);
 
     // relink / initialize subplot axis objects
     plots.linkSubplots(newFullData, newFullLayout, oldFullData, oldFullLayout);
@@ -418,10 +454,10 @@ plots.supplyDefaults = function(gd) {
     }
 
     // update object references in calcdata
-    if((gd.calcdata || []).length === newFullData.length) {
+    if(oldCalcdata.length === newFullData.length) {
         for(i = 0; i < newFullData.length; i++) {
             var newTrace = newFullData[i];
-            var cd0 = gd.calcdata[i][0];
+            var cd0 = oldCalcdata[i][0];
             if(cd0 && cd0.trace) {
                 if(cd0.trace._hasCalcTransform) {
                     remapTransformedArrays(cd0, newTrace);
@@ -431,6 +467,9 @@ plots.supplyDefaults = function(gd) {
             }
         }
     }
+
+    // sort base plot modules for consistent ordering
+    newFullLayout._basePlotModules.sort(sortBasePlotModules);
 };
 
 /**
@@ -596,30 +635,28 @@ plots.createTransitionData = function(gd) {
 // whether a certain plot type is present on plot
 // or trace has a category
 plots._hasPlotType = function(category) {
-    // check plot
-    var basePlotModules = this._basePlotModules || [];
     var i;
 
+    // check base plot modules
+    var basePlotModules = this._basePlotModules || [];
     for(i = 0; i < basePlotModules.length; i++) {
-        var _module = basePlotModules[i];
-
-        if(_module.name === category) return true;
+        if(basePlotModules[i].name === category) return true;
     }
 
-    // check trace
+    // check trace modules
     var modules = this._modules || [];
-
     for(i = 0; i < modules.length; i++) {
-        var modulei = modules[i];
-        if(modulei.categories && modulei.categories.indexOf(category) >= 0) {
-            return true;
-        }
+        var name = modules[i].name;
+        if(name === category) return true;
+        // N.B. this is modules[i] along with 'categories' as a hash object
+        var _module = Registry.modules[name];
+        if(_module && _module.categories[category]) return true;
     }
 
     return false;
 };
 
-plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
+plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata) {
     var i, j;
 
     var basePlotModules = oldFullLayout._basePlotModules || [];
@@ -627,7 +664,7 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou
         var _module = basePlotModules[i];
 
         if(_module.clean) {
-            _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout);
+            _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata);
         }
     }
 
@@ -1064,9 +1101,9 @@ plots.supplyTraceDefaults = function(traceIn, colorIndex, layout, traceInIndex)
     if(_module) {
         var basePlotModule = _module.basePlotModule;
         var subplotAttr = basePlotModule.attr;
-        if(subplotAttr) {
+        var subplotAttrs = basePlotModule.attributes;
+        if(subplotAttr && subplotAttrs) {
             var subplots = layout._subplots;
-            var subplotAttrs = basePlotModule.attributes;
             var subplotId = '';
 
             // TODO - currently if we draw an empty gl2d subplot, it draws
diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js
index 23a1faebca6..555a16252b7 100644
--- a/src/plots/polar/polar.js
+++ b/src/plots/polar/polar.js
@@ -22,7 +22,8 @@ var dragElement = require('../../components/dragelement');
 var dragBox = require('../cartesian/dragbox');
 var Fx = require('../../components/fx');
 var Titles = require('../../components/titles');
-var prepSelect = require('../cartesian/select');
+var prepSelect = require('../cartesian/select').prepSelect;
+var clearSelect = require('../cartesian/select').clearSelect;
 var setCursor = require('../../lib/setcursor');
 
 var MID_SHIFT = require('../../constants/alignment').MID_SHIFT;
@@ -634,7 +635,7 @@ proto.updateMainDrag = function(fullLayout, polarLayout) {
         zb = dragBox.makeZoombox(zoomlayer, lum, cx, cy, path0);
         zb.attr('fill-rule', 'evenodd');
         corners = dragBox.makeCorners(zoomlayer, cx, cy);
-        dragBox.clearSelect(zoomlayer);
+        clearSelect(zoomlayer);
     }
 
     function zoomMove(dx, dy) {
@@ -868,7 +869,7 @@ proto.updateRadialDrag = function(fullLayout, polarLayout) {
         dragOpts.moveFn = moveFn;
         dragOpts.doneFn = doneFn;
 
-        dragBox.clearSelect(fullLayout._zoomlayer);
+        clearSelect(fullLayout._zoomlayer);
     };
 
     dragOpts.clampFn = function(dx, dy) {
@@ -1000,7 +1001,7 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) {
         dragOpts.moveFn = moveFn;
         dragOpts.doneFn = doneFn;
 
-        dragBox.clearSelect(fullLayout._zoomlayer);
+        clearSelect(fullLayout._zoomlayer);
     };
 
     dragElement.init(dragOpts);
diff --git a/src/plots/sort_modules.js b/src/plots/sort_modules.js
new file mode 100644
index 00000000000..38090667d40
--- /dev/null
+++ b/src/plots/sort_modules.js
@@ -0,0 +1,25 @@
+/**
+* Copyright 2012-2018, 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';
+
+// always plot splom before cartesian (i.e. scattergl traces)
+function sortModules(a, b) {
+    if(a === 'splom') return -1;
+    if(b === 'splom') return 1;
+    return 0;
+}
+
+function sortBasePlotModules(a, b) {
+    return sortModules(a.name, b.name);
+}
+
+module.exports = {
+    sortBasePlotModules: sortBasePlotModules,
+    sortModules: sortModules
+};
diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js
index a20e706db8d..122430b660e 100644
--- a/src/plots/ternary/ternary.js
+++ b/src/plots/ternary/ternary.js
@@ -24,7 +24,8 @@ var Axes = require('../cartesian/axes');
 var dragElement = require('../../components/dragelement');
 var Fx = require('../../components/fx');
 var Titles = require('../../components/titles');
-var prepSelect = require('../cartesian/select');
+var prepSelect = require('../cartesian/select').prepSelect;
+var clearSelect = require('../cartesian/select').clearSelect;
 var constants = require('../cartesian/constants');
 
 function Ternary(options, fullLayout) {
@@ -478,7 +479,7 @@ proto.initInteractions = function() {
                 dragOptions.moveFn = plotDrag;
                 dragOptions.doneFn = dragDone;
                 panPrep();
-                clearSelect();
+                clearSelect(zoomContainer);
             }
             else if(dragModeNow === 'select' || dragModeNow === 'lasso') {
                 prepSelect(e, startX, startY, dragOptions, dragModeNow);
@@ -536,7 +537,7 @@ proto.initInteractions = function() {
             })
             .attr('d', 'M0,0Z');
 
-        clearSelect();
+        clearSelect(zoomContainer);
     }
 
     function getAFrac(x, y) { return 1 - (y / _this.h); }
@@ -680,13 +681,6 @@ proto.initInteractions = function() {
         Registry.call('relayout', gd, attrs);
     }
 
-    function clearSelect() {
-        // until we get around to persistent selections, remove the outline
-        // here. The selection itself will be removed when the plot redraws
-        // at the end.
-        zoomContainer.selectAll('.select-outline').remove();
-    }
-
     // finally, set up hover and click
     // these event handlers must already be set before dragElement.init
     // so it can stash them and override them.
diff --git a/src/traces/bar/index.js b/src/traces/bar/index.js
index 6ff430354b8..1798b2a7c88 100644
--- a/src/traces/bar/index.js
+++ b/src/traces/bar/index.js
@@ -27,7 +27,7 @@ Bar.selectPoints = require('./select');
 Bar.moduleType = 'trace';
 Bar.name = 'bar';
 Bar.basePlotModule = require('../../plots/cartesian');
-Bar.categories = ['cartesian', 'bar', 'oriented', 'markerColorscale', 'errorBarsOK', 'showLegend'];
+Bar.categories = ['cartesian', 'svg', 'bar', 'oriented', 'markerColorscale', 'errorBarsOK', 'showLegend', 'draggedPts'];
 Bar.meta = {
     description: [
         'The data visualized by the span of the bars is set in `y`',
diff --git a/src/traces/box/index.js b/src/traces/box/index.js
index 5395dd0af66..ad32d7000aa 100644
--- a/src/traces/box/index.js
+++ b/src/traces/box/index.js
@@ -24,7 +24,7 @@ Box.selectPoints = require('./select');
 Box.moduleType = 'trace';
 Box.name = 'box';
 Box.basePlotModule = require('../../plots/cartesian');
-Box.categories = ['cartesian', 'symbols', 'oriented', 'box-violin', 'showLegend'];
+Box.categories = ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'draggedPts'];
 Box.meta = {
     description: [
         'In vertical (horizontal) box plots,',
diff --git a/src/traces/candlestick/index.js b/src/traces/candlestick/index.js
index dff5c003935..ef94d47bc17 100644
--- a/src/traces/candlestick/index.js
+++ b/src/traces/candlestick/index.js
@@ -14,7 +14,7 @@ module.exports = {
     moduleType: 'trace',
     name: 'candlestick',
     basePlotModule: require('../../plots/cartesian'),
-    categories: ['cartesian', 'showLegend', 'candlestick'],
+    categories: ['cartesian', 'svg', 'showLegend', 'candlestick'],
     meta: {
         description: [
             'The candlestick is a style of financial chart describing',
diff --git a/src/traces/carpet/index.js b/src/traces/carpet/index.js
index 213d473b8f2..2ebbb14a766 100644
--- a/src/traces/carpet/index.js
+++ b/src/traces/carpet/index.js
@@ -21,7 +21,7 @@ Carpet.isContainer = true; // so carpet traces get `calc` before other traces
 Carpet.moduleType = 'trace';
 Carpet.name = 'carpet';
 Carpet.basePlotModule = require('../../plots/cartesian');
-Carpet.categories = ['cartesian', 'carpet', 'carpetAxis', 'notLegendIsolatable'];
+Carpet.categories = ['cartesian', 'svg', 'carpet', 'carpetAxis', 'notLegendIsolatable'];
 Carpet.meta = {
     description: [
         'The data describing carpet axis layout is set in `y` and (optionally)',
diff --git a/src/traces/contour/index.js b/src/traces/contour/index.js
index f56f61cd7ec..f498cf78d98 100644
--- a/src/traces/contour/index.js
+++ b/src/traces/contour/index.js
@@ -22,7 +22,7 @@ Contour.hoverPoints = require('./hover');
 Contour.moduleType = 'trace';
 Contour.name = 'contour';
 Contour.basePlotModule = require('../../plots/cartesian');
-Contour.categories = ['cartesian', '2dMap', 'contour', 'showLegend'];
+Contour.categories = ['cartesian', 'svg', '2dMap', 'contour', 'showLegend'];
 Contour.meta = {
     description: [
         'The data from which contour lines are computed is set in `z`.',
diff --git a/src/traces/contourcarpet/index.js b/src/traces/contourcarpet/index.js
index 7853dae6fc1..1529594f1bc 100644
--- a/src/traces/contourcarpet/index.js
+++ b/src/traces/contourcarpet/index.js
@@ -20,7 +20,7 @@ ContourCarpet.style = require('../contour/style');
 ContourCarpet.moduleType = 'trace';
 ContourCarpet.name = 'contourcarpet';
 ContourCarpet.basePlotModule = require('../../plots/cartesian');
-ContourCarpet.categories = ['cartesian', 'carpet', 'contour', 'symbols', 'showLegend', 'hasLines', 'carpetDependent'];
+ContourCarpet.categories = ['cartesian', 'svg', 'carpet', 'contour', 'symbols', 'showLegend', 'hasLines', 'carpetDependent'];
 ContourCarpet.meta = {
     hrName: 'contour_carpet',
     description: [
diff --git a/src/traces/heatmap/index.js b/src/traces/heatmap/index.js
index d50b941e377..12ccc878755 100644
--- a/src/traces/heatmap/index.js
+++ b/src/traces/heatmap/index.js
@@ -22,7 +22,7 @@ Heatmap.hoverPoints = require('./hover');
 Heatmap.moduleType = 'trace';
 Heatmap.name = 'heatmap';
 Heatmap.basePlotModule = require('../../plots/cartesian');
-Heatmap.categories = ['cartesian', '2dMap'];
+Heatmap.categories = ['cartesian', 'svg', '2dMap'];
 Heatmap.meta = {
     description: [
         'The data that describes the heatmap value-to-color mapping',
diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js
index f1c107e7555..c0949a06447 100644
--- a/src/traces/histogram/index.js
+++ b/src/traces/histogram/index.js
@@ -41,7 +41,7 @@ Histogram.eventData = require('./event_data');
 Histogram.moduleType = 'trace';
 Histogram.name = 'histogram';
 Histogram.basePlotModule = require('../../plots/cartesian');
-Histogram.categories = ['cartesian', 'bar', 'histogram', 'oriented', 'errorBarsOK', 'showLegend'];
+Histogram.categories = ['cartesian', 'svg', 'bar', 'histogram', 'oriented', 'errorBarsOK', 'showLegend'];
 Histogram.meta = {
     description: [
         'The sample data from which statistics are computed is set in `x`',
diff --git a/src/traces/histogram2d/index.js b/src/traces/histogram2d/index.js
index 324a952598e..c8e9695b8dc 100644
--- a/src/traces/histogram2d/index.js
+++ b/src/traces/histogram2d/index.js
@@ -23,7 +23,7 @@ Histogram2D.eventData = require('../histogram/event_data');
 Histogram2D.moduleType = 'trace';
 Histogram2D.name = 'histogram2d';
 Histogram2D.basePlotModule = require('../../plots/cartesian');
-Histogram2D.categories = ['cartesian', '2dMap', 'histogram'];
+Histogram2D.categories = ['cartesian', 'svg', '2dMap', 'histogram'];
 Histogram2D.meta = {
     hrName: 'histogram_2d',
     description: [
diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js
index 9c3d5f2e2da..7953ff48354 100644
--- a/src/traces/histogram2dcontour/index.js
+++ b/src/traces/histogram2dcontour/index.js
@@ -22,7 +22,7 @@ Histogram2dContour.hoverPoints = require('../contour/hover');
 Histogram2dContour.moduleType = 'trace';
 Histogram2dContour.name = 'histogram2dcontour';
 Histogram2dContour.basePlotModule = require('../../plots/cartesian');
-Histogram2dContour.categories = ['cartesian', '2dMap', 'contour', 'histogram'];
+Histogram2dContour.categories = ['cartesian', 'svg', '2dMap', 'contour', 'histogram'];
 Histogram2dContour.meta = {
     hrName: 'histogram_2d_contour',
     description: [
diff --git a/src/traces/ohlc/index.js b/src/traces/ohlc/index.js
index d8d2f12f650..cabac0d8568 100644
--- a/src/traces/ohlc/index.js
+++ b/src/traces/ohlc/index.js
@@ -14,7 +14,7 @@ module.exports = {
     moduleType: 'trace',
     name: 'ohlc',
     basePlotModule: require('../../plots/cartesian'),
-    categories: ['cartesian', 'showLegend'],
+    categories: ['cartesian', 'svg', 'showLegend'],
     meta: {
         description: [
             'The ohlc (short for Open-High-Low-Close) is a style of financial chart describing',
diff --git a/src/traces/parcoords/plot.js b/src/traces/parcoords/plot.js
index de08fc5e180..f8a0a485376 100644
--- a/src/traces/parcoords/plot.js
+++ b/src/traces/parcoords/plot.js
@@ -9,7 +9,7 @@
 'use strict';
 
 var parcoords = require('./parcoords');
-var createRegl = require('regl');
+var prepareRegl = require('../../lib/prepare_regl');
 
 module.exports = function plot(gd, cdparcoords) {
     var fullLayout = gd._fullLayout;
@@ -17,18 +17,7 @@ module.exports = function plot(gd, cdparcoords) {
     var root = fullLayout._paperdiv;
     var container = fullLayout._glcontainer;
 
-    // make sure proper regl instances are created
-    fullLayout._glcanvas.each(function(d) {
-        if(d.regl) return;
-        d.regl = createRegl({
-            canvas: this,
-            attributes: {
-                antialias: !d.pick,
-                preserveDrawingBuffer: true
-            },
-            pixelRatio: gd._context.plotGlPixelRatio || global.devicePixelRatio
-        });
-    });
+    prepareRegl(gd);
 
     var gdDimensions = {};
     var gdDimensionsOriginalOrder = {};
diff --git a/src/traces/pie/base_plot.js b/src/traces/pie/base_plot.js
index 081b63616a8..b6dc4549031 100644
--- a/src/traces/pie/base_plot.js
+++ b/src/traces/pie/base_plot.js
@@ -9,13 +9,13 @@
 'use strict';
 
 var Registry = require('../../registry');
-
+var getModuleCalcData = require('../../plots/get_data').getModuleCalcData;
 
 exports.name = 'pie';
 
 exports.plot = function(gd) {
     var Pie = Registry.getModule('pie');
-    var cdPie = getCdModule(gd.calcdata, Pie);
+    var cdPie = getModuleCalcData(gd.calcdata, Pie);
 
     if(cdPie.length) Pie.plot(gd, cdPie);
 };
@@ -28,18 +28,3 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
         oldFullLayout._pielayer.selectAll('g.trace').remove();
     }
 };
-
-function getCdModule(calcdata, _module) {
-    var cdModule = [];
-
-    for(var i = 0; i < calcdata.length; i++) {
-        var cd = calcdata[i];
-        var trace = cd[0].trace;
-
-        if((trace._module === _module) && (trace.visible === true)) {
-            cdModule.push(cd);
-        }
-    }
-
-    return cdModule;
-}
diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js
index f551f3f0249..9362e27efd6 100644
--- a/src/traces/scatter/calc.js
+++ b/src/traces/scatter/calc.js
@@ -75,7 +75,7 @@ function calcAxisExpansion(gd, trace, xa, ya, x, y, ppad) {
     }
 
     // if no error bars, markers or text, or fill to y=0 remove x padding
-    else if(!trace.error_y.visible && (
+    else if(!(trace.error_y || {}).visible && (
             ['tonexty', 'tozeroy'].indexOf(trace.fill) !== -1 ||
             (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace))
         )) {
diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js
index 8bca686de6a..3808dcf7628 100644
--- a/src/traces/scatter/index.js
+++ b/src/traces/scatter/index.js
@@ -34,7 +34,7 @@ Scatter.animatable = true;
 Scatter.moduleType = 'trace';
 Scatter.name = 'scatter';
 Scatter.basePlotModule = require('../../plots/cartesian');
-Scatter.categories = ['cartesian', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like'];
+Scatter.categories = ['cartesian', 'svg', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like', 'draggedPts'];
 Scatter.meta = {
     description: [
         'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.',
diff --git a/src/traces/scatter/subtypes.js b/src/traces/scatter/subtypes.js
index ddd32ad8547..23bb539b98e 100644
--- a/src/traces/scatter/subtypes.js
+++ b/src/traces/scatter/subtypes.js
@@ -18,8 +18,11 @@ module.exports = {
     },
 
     hasMarkers: function(trace) {
-        return trace.visible && trace.mode &&
-            trace.mode.indexOf('markers') !== -1;
+        return trace.visible && (
+            (trace.mode && trace.mode.indexOf('markers') !== -1) ||
+            // until splom implements 'mode'
+            trace.type === 'splom'
+        );
     },
 
     hasText: function(trace) {
diff --git a/src/traces/scattercarpet/index.js b/src/traces/scattercarpet/index.js
index 689f4cedf5f..c8864a6c45d 100644
--- a/src/traces/scattercarpet/index.js
+++ b/src/traces/scattercarpet/index.js
@@ -23,7 +23,7 @@ ScatterCarpet.eventData = require('./event_data');
 ScatterCarpet.moduleType = 'trace';
 ScatterCarpet.name = 'scattercarpet';
 ScatterCarpet.basePlotModule = require('../../plots/cartesian');
-ScatterCarpet.categories = ['carpet', 'symbols', 'markerColorscale', 'showLegend', 'carpetDependent'];
+ScatterCarpet.categories = ['svg', 'carpet', 'symbols', 'markerColorscale', 'showLegend', 'carpetDependent', 'draggedPts'];
 ScatterCarpet.meta = {
     hrName: 'scatter_carpet',
     description: [
diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js
index dac9fb5b750..8c4d4199438 100644
--- a/src/traces/scattergl/convert.js
+++ b/src/traces/scattergl/convert.js
@@ -87,7 +87,7 @@ function convertStyle(gd, trace) {
 }
 
 function convertMarkerStyle(trace) {
-    var count = trace._length || (trace.dimensions || [])._length;
+    var count = trace._length || trace._commonLength;
     var optsIn = trace.marker;
     var optsOut = {};
     var i;
@@ -401,6 +401,8 @@ function convertErrorBarPositions(gd, trace, positions) {
 
 module.exports = {
     convertStyle: convertStyle,
+    convertMarkerStyle: convertMarkerStyle,
+    convertMarkerSelection: convertMarkerSelection,
     convertLinePositions: convertLinePositions,
     convertErrorBarPositions: convertErrorBarPositions
 };
diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js
index ddebaf3df3b..f8ca803edd5 100644
--- a/src/traces/scattergl/index.js
+++ b/src/traces/scattergl/index.js
@@ -8,7 +8,6 @@
 
 'use strict';
 
-var createRegl = require('regl');
 var createScatter = require('regl-scatter2d');
 var createLine = require('regl-line2d');
 var createError = require('regl-error2d');
@@ -17,6 +16,7 @@ var arrayRange = require('array-range');
 
 var Registry = require('../../registry');
 var Lib = require('../../lib');
+var prepareRegl = require('../../lib/prepare_regl');
 var AxisIDs = require('../../plots/cartesian/axis_ids');
 
 var subTypes = require('../scatter/subtypes');
@@ -280,16 +280,6 @@ function sceneUpdate(gd, subplot) {
             }
         };
 
-        // remove selection
-        scene.clearSelect = function clearSelect() {
-            if(!scene.selectBatch) return;
-            scene.selectBatch = null;
-            scene.unselectBatch = null;
-            scene.scatter2d.update(scene.markerOptions);
-            scene.clear();
-            scene.draw();
-        };
-
         // remove scene resources
         scene.destroy = function destroy() {
             if(scene.fill2d) scene.fill2d.destroy();
@@ -336,20 +326,7 @@ function plot(gd, subplot, cdata) {
     var width = fullLayout.width;
     var height = fullLayout.height;
 
-    // make sure proper regl instances are created
-    fullLayout._glcanvas.each(function(d) {
-        if(d.regl || d.pick) return;
-        d.regl = createRegl({
-            canvas: this,
-            attributes: {
-                antialias: !d.pick,
-                preserveDrawingBuffer: true
-            },
-            extensions: ['ANGLE_instanced_arrays', 'OES_element_index_uint'],
-            pixelRatio: gd._context.plotGlPixelRatio || global.devicePixelRatio
-        });
-    });
-
+    prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']);
     var regl = fullLayout._glcanvas.data()[0].regl;
 
     // that is needed for fills
@@ -631,9 +608,9 @@ function hoverPoints(pointData, xval, yval, hovermode) {
 
     // pick the id closest to the point
     // note that point possibly may not be found
-    var minDist = maxDistance;
     var id, ptx, pty, i, dx, dy, dist, dxy;
 
+    var minDist = maxDistance;
     if(hovermode === 'x') {
         for(i = 0; i < ids.length; i++) {
             ptx = x[ids[i]];
@@ -662,9 +639,24 @@ function hoverPoints(pointData, xval, yval, hovermode) {
     }
 
     pointData.index = id;
+    pointData.distance = minDist;
+    pointData.dxy = dxy;
 
     if(id === undefined) return [pointData];
 
+    calcHover(pointData, x, y, trace);
+
+    return [pointData];
+}
+
+
+function calcHover(pointData, x, y, trace) {
+    var xa = pointData.xa;
+    var ya = pointData.ya;
+    var minDist = pointData.distance;
+    var dxy = pointData.dxy;
+    var id = pointData.index;
+
     // the closest data point
     var di = {
         pointNumber: id,
@@ -750,9 +742,10 @@ function hoverPoints(pointData, xval, yval, hovermode) {
     fillHoverText(di, trace, pointData);
     Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, pointData);
 
-    return [pointData];
+    return pointData;
 }
 
+
 function selectPoints(searchInfo, polygon) {
     var cd = searchInfo.cd;
     var selection = [];
@@ -813,13 +806,19 @@ function selectPoints(searchInfo, polygon) {
     return selection;
 }
 
-function style(gd, cd) {
-    if(cd) {
-        var stash = cd[0].t;
-        var scene = stash._scene;
+function style(gd, cds) {
+    if(!cds) return;
+
+    var stash = cds[0][0].t;
+    var scene = stash._scene;
+
+    // don't clear the subplot if there are splom traces
+    // on the graph
+    if(!gd._fullLayout._has('splom')) {
         scene.clear();
-        scene.draw();
     }
+
+    scene.draw();
 }
 
 module.exports = {
@@ -840,6 +839,7 @@ module.exports = {
 
     sceneOptions: sceneOptions,
     sceneUpdate: sceneUpdate,
+    calcHover: calcHover,
 
     meta: {
         hrName: 'scatter_gl',
diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js
new file mode 100644
index 00000000000..aa73330548d
--- /dev/null
+++ b/src/traces/splom/attributes.js
@@ -0,0 +1,128 @@
+/**
+* Copyright 2012-2018, 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 scatterGlAttrs = require('../scattergl/attributes');
+var cartesianIdRegex = require('../../plots/cartesian/constants').idRegex;
+
+function makeAxesValObject(axLetter) {
+    return {
+        valType: 'info_array',
+        freeLength: true,
+        role: 'info',
+        editType: 'calc',
+        items: {
+            valType: 'subplotid',
+            regex: cartesianIdRegex[axLetter],
+            editType: 'plot'
+        },
+        description: [
+            'Sets the list of ' + axLetter + ' axes',
+            'corresponding to this splom trace.',
+            'By default, a splom will match the first N ' + axLetter + 'axes',
+            'where N is the number of input dimensions.'
+        ].join(' ')
+    };
+}
+
+module.exports = {
+    dimensions: {
+        _isLinkedToArray: 'dimension',
+
+        visible: {
+            valType: 'boolean',
+            role: 'info',
+            dflt: true,
+            editType: 'calc',
+            description: [
+                'Determines whether or not this dimension is shown on the graph.',
+                'Note that even visible false dimension contribute to the',
+                'default grid generate by this splom trace.'
+            ].join(' ')
+        },
+        label: {
+            valType: 'string',
+            role: 'info',
+            editType: 'calc',
+            description: 'Sets the label corresponding to this splom dimension.'
+        },
+        values: {
+            valType: 'data_array',
+            role: 'info',
+            editType: 'calc+clearAxisTypes',
+            description: 'Sets the dimension values to be plotted.'
+        },
+
+        // TODO should add an attribute to pin down x only vars and y only vars
+        // like https://seaborn.pydata.org/generated/seaborn.pairplot.html
+        // x_vars and y_vars
+
+        // maybe more axis defaulting option e.g. `showgrid: false`
+
+        editType: 'calc+clearAxisTypes'
+    },
+
+    // mode: {}, (only 'markers' for now)
+
+    text: scatterGlAttrs.text,
+    marker: scatterGlAttrs.marker,
+
+    xaxes: makeAxesValObject('x'),
+    yaxes: makeAxesValObject('y'),
+
+    diagonal: {
+        visible: {
+            valType: 'boolean',
+            role: 'info',
+            dflt: true,
+            editType: 'calc',
+            description: [
+                'Determines whether or not subplots on the diagonal are displayed.'
+            ].join(' ')
+        },
+
+        // type: 'scattergl' | 'histogram' | 'box' | 'violin'
+        // ...
+        // more options
+
+        editType: 'calc'
+    },
+
+    showupperhalf: {
+        valType: 'boolean',
+        role: 'info',
+        dflt: true,
+        editType: 'calc',
+        description: [
+            'Determines whether or not subplots on the upper half',
+            'from the diagonal are displayed.'
+        ].join(' ')
+    },
+    showlowerhalf: {
+        valType: 'boolean',
+        role: 'info',
+        dflt: true,
+        editType: 'calc',
+        description: [
+            'Determines whether or not subplots on the lower half',
+            'from the diagonal are displayed.'
+        ].join(' ')
+    },
+
+    selected: {
+        marker: scatterGlAttrs.selected.marker,
+        editType: 'calc'
+    },
+    unselected: {
+        marker: scatterGlAttrs.unselected.marker,
+        editType: 'calc'
+    },
+
+    opacity: scatterGlAttrs.opacity
+};
diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js
new file mode 100644
index 00000000000..73fbc59d11f
--- /dev/null
+++ b/src/traces/splom/base_plot.js
@@ -0,0 +1,240 @@
+/**
+* Copyright 2012-2018, 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 createLine = require('regl-line2d');
+
+var Registry = require('../../registry');
+var Lib = require('../../lib');
+var prepareRegl = require('../../lib/prepare_regl');
+var getModuleCalcData = require('../../plots/get_data').getModuleCalcData;
+var Cartesian = require('../../plots/cartesian');
+var AxisIDs = require('../../plots/cartesian/axis_ids');
+
+var SPLOM = 'splom';
+
+function plot(gd) {
+    var fullLayout = gd._fullLayout;
+    var _module = Registry.getModule(SPLOM);
+    var splomCalcData = getModuleCalcData(gd.calcdata, _module);
+
+    prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']);
+
+    if(fullLayout._hasOnlyLargeSploms) {
+        drawGrid(gd);
+    }
+
+    _module.plot(gd, {}, splomCalcData);
+}
+
+function drag(gd) {
+    var cd = gd.calcdata;
+    var fullLayout = gd._fullLayout;
+
+    if(fullLayout._hasOnlyLargeSploms) {
+        drawGrid(gd);
+    }
+
+    for(var i = 0; i < cd.length; i++) {
+        var cd0 = cd[i][0];
+        var trace = cd0.trace;
+        var scene = cd0.t._scene;
+
+        if(trace.type === 'splom' && scene && scene.matrix) {
+            dragOne(gd, trace, scene);
+        }
+    }
+}
+
+function dragOne(gd, trace, scene) {
+    var dimensions = trace.dimensions;
+    var visibleLength = scene.matrixOptions.data.length;
+    var ranges = new Array(visibleLength);
+
+    for(var i = 0, k = 0; i < dimensions.length; i++) {
+        if(dimensions[i].visible) {
+            var rng = ranges[k] = new Array(4);
+
+            var xa = AxisIDs.getFromId(gd, trace._diag[i][0]);
+            if(xa) {
+                rng[0] = xa.r2l(xa.range[0]);
+                rng[2] = xa.r2l(xa.range[1]);
+            }
+
+            var ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
+            if(ya) {
+                rng[1] = ya.r2l(ya.range[0]);
+                rng[3] = ya.r2l(ya.range[1]);
+            }
+
+            k++;
+        }
+    }
+
+    if(scene.selectBatch) {
+        scene.matrix.update({ranges: ranges}, {ranges: ranges});
+        scene.matrix.draw(scene.unselectBatch, scene.selectBatch);
+    } else {
+        scene.matrix.update({ranges: ranges});
+        scene.matrix.draw();
+    }
+}
+
+function drawGrid(gd) {
+    var fullLayout = gd._fullLayout;
+    var regl = fullLayout._glcanvas.data()[0].regl;
+    var splomGrid = fullLayout._splomGrid;
+
+    if(!splomGrid) {
+        splomGrid = fullLayout._splomGrid = createLine(regl);
+    }
+
+    splomGrid.update(makeGridData(gd));
+    splomGrid.draw();
+}
+
+function makeGridData(gd) {
+    var fullLayout = gd._fullLayout;
+    var gs = fullLayout._size;
+    var fullView = [0, 0, fullLayout.width, fullLayout.height];
+    var lookup = {};
+    var k;
+
+    function push(prefix, ax, x0, x1, y0, y1) {
+        var lcolor = ax[prefix + 'color'];
+        var lwidth = ax[prefix + 'width'];
+        var key = String(lcolor + lwidth);
+
+        if(key in lookup) {
+            lookup[key].data.push(NaN, NaN, x0, x1, y0, y1);
+        } else {
+            lookup[key] = {
+                data: [x0, x1, y0, y1],
+                join: 'rect',
+                thickness: lwidth,
+                color: lcolor,
+                viewport: fullView,
+                range: fullView,
+                overlay: false
+            };
+        }
+    }
+
+    for(k in fullLayout._splomSubplots) {
+        var sp = fullLayout._plots[k];
+        var xa = sp.xaxis;
+        var ya = sp.yaxis;
+        var xVals = xa._vals;
+        var yVals = ya._vals;
+        // ya.l2p assumes top-to-bottom coordinate system (a la SVG),
+        // we need to compute bottom-to-top offsets and slopes:
+        var yOffset = gs.b + ya.domain[0] * gs.h;
+        var ym = -ya._m;
+        var yb = -ym * ya.r2l(ya.range[0], ya.calendar);
+        var x, y;
+
+        if(xa.showgrid) {
+            for(k = 0; k < xVals.length; k++) {
+                x = xa._offset + xa.l2p(xVals[k].x);
+                push('grid', xa, x, yOffset, x, yOffset + ya._length);
+            }
+        }
+        if(showZeroLine(xa)) {
+            x = xa._offset + xa.l2p(0);
+            push('zeroline', xa, x, yOffset, x, yOffset + ya._length);
+        }
+        if(ya.showgrid) {
+            for(k = 0; k < yVals.length; k++) {
+                y = yOffset + yb + ym * yVals[k].x;
+                push('grid', ya, xa._offset, y, xa._offset + xa._length, y);
+            }
+        }
+        if(showZeroLine(ya)) {
+            y = yOffset + yb + 0;
+            push('zeroline', ya, xa._offset, y, xa._offset + xa._length, y);
+        }
+    }
+
+    var gridBatches = [];
+    for(k in lookup) {
+        gridBatches.push(lookup[k]);
+    }
+
+    return gridBatches;
+}
+
+// just like in Axes.doTicks but without the loop over traces
+function showZeroLine(ax) {
+    var rng = Lib.simpleMap(ax.range, ax.r2l);
+    var p0 = ax.l2p(0);
+
+    return (
+        ax.zeroline &&
+        ax._vals && ax._vals.length &&
+        (rng[0] * rng[1] <= 0) &&
+        (ax.type === 'linear' || ax.type === '-') &&
+        ((p0 > 1 && p0 < ax._length - 1) || !ax.showline)
+    );
+}
+
+function clean(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata) {
+    var oldModules = oldFullLayout._modules || [];
+    var newModules = newFullLayout._modules || [];
+
+    var hadSplom, hasSplom;
+    var i;
+
+    for(i = 0; i < oldModules.length; i++) {
+        if(oldModules[i].name === 'splom') {
+            hadSplom = true;
+            break;
+        }
+    }
+    for(i = 0; i < newModules.length; i++) {
+        if(newModules[i].name === 'splom') {
+            hasSplom = true;
+            break;
+        }
+    }
+
+    if(hadSplom && !hasSplom) {
+        for(i = 0; i < oldCalcdata.length; i++) {
+            var cd0 = oldCalcdata[i][0];
+            var trace = cd0.trace;
+            var scene = cd0.t._scene;
+
+            if(trace.type === 'splom' && scene && scene.matrix) {
+                scene.matrix.destroy();
+                cd0.t._scene = null;
+            }
+        }
+    }
+
+    if(oldFullLayout._splomGrid &&
+        (!newFullLayout._hasOnlyLargeSploms && oldFullLayout._hasOnlyLargeSploms)) {
+        oldFullLayout._splomGrid.destroy();
+        oldFullLayout._splomGrid = null;
+    }
+
+    Cartesian.clean(newFullData, newFullLayout, oldFullData, oldFullLayout);
+}
+
+module.exports = {
+    name: SPLOM,
+    attr: Cartesian.attr,
+    attrRegex: Cartesian.attrRegex,
+    layoutAttributes: Cartesian.layoutAttributes,
+    supplyLayoutDefaults: Cartesian.supplyLayoutDefaults,
+    drawFramework: Cartesian.drawFramework,
+    plot: plot,
+    drag: drag,
+    clean: clean,
+    updateFx: Cartesian.updateFx,
+    toSVG: Cartesian.toSVG
+};
diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js
new file mode 100644
index 00000000000..aceab9290dd
--- /dev/null
+++ b/src/traces/splom/defaults.js
@@ -0,0 +1,182 @@
+/**
+* Copyright 2012-2018, 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 Lib = require('../../lib');
+
+var attributes = require('./attributes');
+var subTypes = require('../scatter/subtypes');
+var handleMarkerDefaults = require('../scatter/marker_defaults');
+var OPEN_RE = /-open/;
+
+module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
+    function coerce(attr, dflt) {
+        return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
+    }
+
+    var dimLength = handleDimensionsDefaults(traceIn, traceOut);
+
+    var showDiag = coerce('diagonal.visible');
+    var showUpper = coerce('showupperhalf');
+    var showLower = coerce('showlowerhalf');
+
+    if(!dimLength || (!showDiag && !showUpper && !showLower)) {
+        traceOut.visible = false;
+        return;
+    }
+
+    coerce('text');
+
+    handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce);
+
+    var isOpen = OPEN_RE.test(traceOut.marker.symbol);
+    var isBubble = subTypes.isBubble(traceOut);
+    coerce('marker.line.width', isOpen || isBubble ? 1 : 0);
+
+    handleAxisDefaults(traceIn, traceOut, layout, coerce);
+
+    Lib.coerceSelectionMarkerOpacity(traceOut, coerce);
+};
+
+function handleDimensionsDefaults(traceIn, traceOut) {
+    var dimensionsIn = traceIn.dimensions;
+    if(!Array.isArray(dimensionsIn)) return 0;
+
+    var dimLength = dimensionsIn.length;
+    var commonLength = 0;
+    var dimensionsOut = traceOut.dimensions = new Array(dimLength);
+    var dimIn;
+    var dimOut;
+    var i;
+
+    function coerce(attr, dflt) {
+        return Lib.coerce(dimIn, dimOut, attributes.dimensions, attr, dflt);
+    }
+
+    for(i = 0; i < dimLength; i++) {
+        dimIn = dimensionsIn[i];
+        dimOut = dimensionsOut[i] = {};
+
+        // coerce label even if dimensions may be `visible: false`,
+        // to fill in axis title defaults
+        coerce('label');
+
+        // wait until plot step to filter out visible false dimensions
+        var visible = coerce('visible');
+        if(!visible) continue;
+
+        var values = coerce('values');
+        if(!values || !values.length) {
+            dimOut.visible = false;
+            continue;
+        }
+
+        commonLength = Math.max(commonLength, values.length);
+        dimOut._index = i;
+    }
+
+    for(i = 0; i < dimLength; i++) {
+        dimOut = dimensionsOut[i];
+        if(dimOut.visible) dimOut._length = commonLength;
+    }
+
+    traceOut._commonLength = commonLength;
+
+    return dimensionsOut.length;
+}
+
+function handleAxisDefaults(traceIn, traceOut, layout, coerce) {
+    var dimensions = traceOut.dimensions;
+    var dimLength = dimensions.length;
+    var showUpper = traceOut.showupperhalf;
+    var showLower = traceOut.showlowerhalf;
+    var showDiag = traceOut.diagonal.visible;
+    var i, j;
+
+    // N.B. one less x axis AND one less y axis when hiding one half and the diagonal
+    var axDfltLength = !showDiag && (!showUpper || !showLower) ? dimLength - 1 : dimLength;
+
+    var xaxes = coerce('xaxes', fillAxisIdArray('x', axDfltLength));
+    var yaxes = coerce('yaxes', fillAxisIdArray('y', axDfltLength));
+
+    // to avoid costly indexOf
+    traceOut._xaxes = arrayToHashObject(xaxes);
+    traceOut._yaxes = arrayToHashObject(yaxes);
+
+    // allow users to under-specify number of axes
+    var axLength = Math.min(axDfltLength, xaxes.length, yaxes.length);
+
+    // fill in splom subplot keys
+    for(i = 0; i < axLength; i++) {
+        for(j = 0; j < axLength; j++) {
+            var id = [xaxes[i] + yaxes[j]];
+
+            if(i > j && showUpper) {
+                layout._splomSubplots[id] = 1;
+            } else if(i < j && showLower) {
+                layout._splomSubplots[id] = 1;
+            } else if(i === j && (showDiag || !showLower || !showUpper)) {
+                // need to include diagonal subplots when
+                // hiding one half and the diagonal
+                layout._splomSubplots[id] = 1;
+            }
+        }
+    }
+
+    // build list of [x,y] axis corresponding to each dimensions[i],
+    // very useful for passing options to regl-splom
+    var diag = traceOut._diag = new Array(dimLength);
+
+    // cases where showDiag and showLower or showUpper are false
+    // no special treatment as the xaxes and yaxes items no longer match
+    // the dimensions items 1-to-1
+    var xShift = !showDiag && !showLower ? -1 : 0;
+    var yShift = !showDiag && !showUpper ? -1 : 0;
+
+    for(i = 0; i < dimLength; i++) {
+        var dim = dimensions[i];
+        var xa = xaxes[i + xShift];
+        var ya = yaxes[i + yShift];
+
+        fillAxisStash(layout, xa, dim);
+        fillAxisStash(layout, ya, dim);
+
+        // note that some the entries here may be undefined
+        diag[i] = [xa, ya];
+    }
+}
+
+function fillAxisIdArray(axLetter, len) {
+    var out = new Array(len);
+
+    for(var i = 0; i < len; i++) {
+        out[i] = axLetter + (i ? i + 1 : '');
+    }
+
+    return out;
+}
+
+function fillAxisStash(layout, axId, dim) {
+    if(!axId) return;
+
+    var axLetter = axId.charAt(0);
+    var stash = layout._splomAxes[axLetter];
+
+    if(!(axId in stash)) {
+        stash[axId] = (dim || {}).label || '';
+    }
+}
+
+function arrayToHashObject(arr) {
+    var obj = {};
+    for(var i = 0; i < arr.length; i++) {
+        obj[arr[i]] = 1;
+    }
+    return obj;
+}
diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js
new file mode 100644
index 00000000000..3d9feff2f4b
--- /dev/null
+++ b/src/traces/splom/index.js
@@ -0,0 +1,488 @@
+/**
+* Copyright 2012-2018, 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 createMatrix = require('regl-splom');
+var arrayRange = require('array-range');
+
+var Registry = require('../../registry');
+var Grid = require('../../components/grid');
+var Lib = require('../../lib');
+var AxisIDs = require('../../plots/cartesian/axis_ids');
+
+var subTypes = require('../scatter/subtypes');
+var calcMarkerSize = require('../scatter/calc').calcMarkerSize;
+var calcAxisExpansion = require('../scatter/calc').calcAxisExpansion;
+var calcColorscales = require('../scatter/colorscale_calc');
+var convertMarkerSelection = require('../scattergl/convert').convertMarkerSelection;
+var convertMarkerStyle = require('../scattergl/convert').convertMarkerStyle;
+var calcHover = require('../scattergl').calcHover;
+
+var BADNUM = require('../../constants/numerical').BADNUM;
+var TOO_MANY_POINTS = require('../scattergl/constants').TOO_MANY_POINTS;
+
+function calc(gd, trace) {
+    var dimensions = trace.dimensions;
+    var commonLength = trace._commonLength;
+    var stash = {};
+    var opts = {};
+    // 'c' for calculated, 'l' for linear,
+    // only differ here for log axes, pass ldata to createMatrix as 'data'
+    var cdata = opts.cdata = [];
+    var ldata = opts.data = [];
+    var i, k, dim;
+
+    for(i = 0; i < dimensions.length; i++) {
+        dim = dimensions[i];
+
+        if(dim.visible) {
+            var axId = trace._diag[i][0] || trace._diag[i][1];
+            var ax = AxisIDs.getFromId(gd, axId);
+            if(ax) {
+                var ccol = makeCalcdata(ax, trace, dim);
+                var lcol = ax.type === 'log' ? Lib.simpleMap(ccol, ax.c2l) : ccol;
+                cdata.push(ccol);
+                ldata.push(lcol);
+            }
+        }
+    }
+
+    calcColorscales(trace);
+    Lib.extendFlat(opts, convertMarkerStyle(trace));
+
+    var visibleLength = cdata.length;
+    var hasTooManyPoints = (visibleLength * commonLength) > TOO_MANY_POINTS;
+
+    for(i = 0, k = 0; i < dimensions.length; i++) {
+        dim = dimensions[i];
+
+        if(dim.visible) {
+            var xa = AxisIDs.getFromId(gd, trace._diag[i][0]) || {};
+            var ya = AxisIDs.getFromId(gd, trace._diag[i][1]) || {};
+
+            // Re-use SVG scatter axis expansion routine except
+            // for graph with very large number of points where it
+            // performs poorly.
+            // In big data case, fake Axes.expand outputs with data bounds,
+            // and an average size for array marker.size inputs.
+            var ppad;
+            if(hasTooManyPoints) {
+                ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3));
+            } else {
+                ppad = calcMarkerSize(trace, commonLength);
+            }
+
+            calcAxisExpansion(gd, trace, xa, ya, cdata[k], cdata[k], ppad);
+            k++;
+        }
+    }
+
+    var scene = stash._scene = sceneUpdate(gd, stash);
+    if(!scene.matrix) scene.matrix = true;
+    scene.matrixOptions = opts;
+
+    scene.selectedOptions = convertMarkerSelection(trace, trace.selected);
+    scene.unselectedOptions = convertMarkerSelection(trace, trace.unselected);
+
+    return [{x: false, y: false, t: stash, trace: trace}];
+}
+
+function makeCalcdata(ax, trace, dim) {
+    // call makeCalcdata with fake input
+    var ccol = ax.makeCalcdata({
+        v: dim.values,
+        vcalendar: trace.calendar
+    }, 'v');
+
+    for(var i = 0; i < ccol.length; i++) {
+        ccol[i] = ccol[i] === BADNUM ? NaN : ccol[i];
+    }
+
+    return ccol;
+}
+
+function sceneUpdate(gd, stash) {
+    var scene = stash._scene;
+
+    var reset = {
+        dirty: true
+    };
+
+    var first = {
+        selectBatch: null,
+        unselectBatch: null,
+        matrix: false,
+        select: null
+    };
+
+    if(!scene) {
+        scene = stash._scene = Lib.extendFlat({}, reset, first);
+
+        scene.draw = function draw() {
+            // draw traces in selection mode
+            if(scene.matrix && scene.selectBatch) {
+                scene.matrix.draw(scene.unselectBatch, scene.selectBatch);
+            }
+
+            else if(scene.matrix) {
+                scene.matrix.draw();
+            }
+
+            scene.dirty = false;
+        };
+
+        // remove scene resources
+        scene.destroy = function destroy() {
+            if(scene.matrix) scene.matrix.destroy();
+
+            scene.matrixOptions = null;
+            scene.selectBatch = null;
+            scene.unselectBatch = null;
+
+            stash._scene = null;
+        };
+    }
+
+    // In case if we have scene from the last calc - reset data
+    if(!scene.dirty) {
+        Lib.extendFlat(scene, reset);
+    }
+
+    return scene;
+}
+
+function plot(gd, _, splomCalcData) {
+    if(!splomCalcData.length) return;
+
+    for(var i = 0; i < splomCalcData.length; i++) {
+        plotOne(gd, splomCalcData[i][0]);
+    }
+}
+
+function plotOne(gd, cd0) {
+    var fullLayout = gd._fullLayout;
+    var gs = fullLayout._size;
+    var trace = cd0.trace;
+    var stash = cd0.t;
+    var scene = stash._scene;
+    var matrixOpts = scene.matrixOptions;
+    var cdata = matrixOpts.cdata;
+    var regl = fullLayout._glcanvas.data()[0].regl;
+    var dragmode = fullLayout.dragmode;
+    var xa, ya;
+    var i, j, k;
+
+    if(cdata.length === 0) return;
+
+    // augment options with proper upper/lower halves
+    // regl-splom's default grid starts from bottom-left
+    matrixOpts.lower = trace.showupperhalf;
+    matrixOpts.upper = trace.showlowerhalf;
+    matrixOpts.diagonal = trace.diagonal.visible;
+
+    var dimensions = trace.dimensions;
+    var visibleLength = cdata.length;
+    var viewOpts = {};
+    viewOpts.ranges = new Array(visibleLength);
+    viewOpts.domains = new Array(visibleLength);
+
+    for(i = 0, k = 0; i < dimensions.length; i++) {
+        if(trace.dimensions[i].visible) {
+            var rng = viewOpts.ranges[k] = new Array(4);
+            var dmn = viewOpts.domains[k] = new Array(4);
+
+            xa = AxisIDs.getFromId(gd, trace._diag[i][0]);
+            if(xa) {
+                rng[0] = xa._rl[0];
+                rng[2] = xa._rl[1];
+                dmn[0] = xa.domain[0];
+                dmn[2] = xa.domain[1];
+            }
+
+            ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
+            if(ya) {
+                rng[1] = ya._rl[0];
+                rng[3] = ya._rl[1];
+                dmn[1] = ya.domain[0];
+                dmn[3] = ya.domain[1];
+            }
+
+            k++;
+        }
+    }
+
+    viewOpts.viewport = [gs.l, gs.b, gs.w + gs.l, gs.h + gs.b];
+
+    if(scene.matrix === true) {
+        scene.matrix = createMatrix(regl);
+    }
+
+    var selectMode = dragmode === 'lasso' || dragmode === 'select' || !!trace.selectedpoints;
+    scene.selectBatch = null;
+    scene.unselectBatch = null;
+
+    if(selectMode) {
+        var commonLength = trace._commonLength;
+
+        if(!scene.selectBatch) {
+            scene.selectBatch = [];
+            scene.unselectBatch = [];
+        }
+
+        // regenerate scene batch, if traces number changed during selection
+        if(trace.selectedpoints) {
+            scene.selectBatch = trace.selectedpoints;
+
+            var selPts = trace.selectedpoints;
+            var selDict = {};
+            for(i = 0; i < selPts.length; i++) {
+                selDict[selPts[i]] = true;
+            }
+            var unselPts = [];
+            for(i = 0; i < commonLength; i++) {
+                if(!selDict[i]) unselPts.push(i);
+            }
+            scene.unselectBatch = unselPts;
+        }
+
+        // precalculate px coords since we are not going to pan during select
+        var xpx = stash.xpx = new Array(visibleLength);
+        var ypx = stash.ypx = new Array(visibleLength);
+
+        for(i = 0, k = 0; i < dimensions.length; i++) {
+            if(trace.dimensions[i].visible) {
+                xa = AxisIDs.getFromId(gd, trace._diag[i][0]);
+                if(xa) {
+                    xpx[k] = new Array(commonLength);
+                    for(j = 0; j < commonLength; j++) {
+                        xpx[k][j] = xa.c2p(cdata[k][j]);
+                    }
+                }
+
+                ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
+                if(ya) {
+                    ypx[k] = new Array(commonLength);
+                    for(j = 0; j < commonLength; j++) {
+                        ypx[k][j] = ya.c2p(cdata[k][j]);
+                    }
+                }
+
+                k++;
+            }
+        }
+
+        if(scene.selectBatch) {
+            scene.matrix.update(matrixOpts, matrixOpts);
+            scene.matrix.update(scene.unselectedOptions, scene.selectedOptions);
+            scene.matrix.update(viewOpts, viewOpts);
+        }
+        else {
+            // delete selection pass
+            scene.matrix.update(viewOpts, null);
+        }
+    }
+    else {
+        scene.matrix.update(matrixOpts);
+        scene.matrix.update(viewOpts);
+        stash.xpx = stash.ypx = null;
+    }
+
+    scene.draw();
+}
+
+function hoverPoints(pointData, xval, yval) {
+    var cd = pointData.cd;
+    var trace = cd[0].trace;
+    var stash = cd[0].t;
+    var scene = stash._scene;
+    var cdata = scene.matrixOptions.cdata;
+    var xa = pointData.xa;
+    var ya = pointData.ya;
+    var xpx = xa.c2p(xval);
+    var ypx = ya.c2p(yval);
+    var maxDistance = pointData.distance;
+
+    var xi = getDimIndex(trace, xa);
+    var yi = getDimIndex(trace, ya);
+    if(xi === false || yi === false) return [pointData];
+
+    var x = cdata[xi];
+    var y = cdata[yi];
+
+    var id, dxy;
+    var minDist = maxDistance;
+
+    for(var i = 0; i < x.length; i++) {
+        var ptx = x[i];
+        var pty = y[i];
+        var dx = xa.c2p(ptx) - xpx;
+        var dy = ya.c2p(pty) - ypx;
+        var dist = Math.sqrt(dx * dx + dy * dy);
+
+        if(dist < minDist) {
+            minDist = dxy = dist;
+            id = i;
+        }
+    }
+
+    pointData.index = id;
+    pointData.distance = minDist;
+    pointData.dxy = dxy;
+
+    if(id === undefined) return [pointData];
+
+    calcHover(pointData, x, y, trace);
+
+    return [pointData];
+}
+
+function selectPoints(searchInfo, polygon) {
+    var cd = searchInfo.cd;
+    var trace = cd[0].trace;
+    var stash = cd[0].t;
+    var scene = stash._scene;
+    var cdata = scene.matrixOptions.cdata;
+    var xa = searchInfo.xaxis;
+    var ya = searchInfo.yaxis;
+    var selection = [];
+    var i;
+
+    if(!scene) return selection;
+
+    var hasOnlyLines = (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace));
+    if(trace.visible !== true || hasOnlyLines) return selection;
+
+    var xi = getDimIndex(trace, xa);
+    var yi = getDimIndex(trace, ya);
+    if(xi === false || yi === false) return selection;
+
+    var xpx = stash.xpx[xi];
+    var ypx = stash.ypx[yi];
+    var x = cdata[xi];
+    var y = cdata[yi];
+
+    // degenerate polygon does not enable selection
+    // filter out points by visible scatter ones
+    var els = null;
+    var unels = null;
+    if(polygon !== false && !polygon.degenerate) {
+        els = [], unels = [];
+        for(i = 0; i < x.length; i++) {
+            if(polygon.contains([xpx[i], ypx[i]])) {
+                els.push(i);
+                selection.push({
+                    pointNumber: i,
+                    x: x[i],
+                    y: y[i]
+                });
+            }
+            else {
+                unels.push(i);
+            }
+        }
+    } else {
+        unels = arrayRange(stash.count);
+    }
+
+    // make sure selectBatch is created
+    if(!scene.selectBatch) {
+        scene.selectBatch = [];
+        scene.unselectBatch = [];
+    }
+
+    if(!scene.selectBatch) {
+        // enter every trace select mode
+        for(i = 0; i < scene.count; i++) {
+            scene.selectBatch = [];
+            scene.unselectBatch = [];
+        }
+        // we should turn scatter2d into unselected once we have any points selected
+        scene.matrix.update(scene.unselectedOptions, scene.selectedOptions);
+    }
+
+    scene.selectBatch = els;
+    scene.unselectBatch = unels;
+
+
+    return selection;
+}
+
+function style(gd, cds) {
+    if(!cds) return;
+
+    var fullLayout = gd._fullLayout;
+    var cd0 = cds[0];
+    var scene0 = cd0[0].t._scene;
+    scene0.matrix.regl.clear({color: true, depth: true});
+
+    if(fullLayout._splomGrid) {
+        fullLayout._splomGrid.draw();
+    }
+
+    for(var i = 0; i < cds.length; i++) {
+        var scene = cds[i][0].t._scene;
+        scene.draw();
+    }
+
+    // redraw all subplot with scattergl traces,
+    // as we cleared the whole canvas above
+    if(fullLayout._has('cartesian')) {
+        for(var k in fullLayout._plots) {
+            var sp = fullLayout._plots[k];
+            if(sp._scene) sp._scene.draw();
+        }
+    }
+}
+
+function getDimIndex(trace, ax) {
+    var axId = ax._id;
+    var axLetter = axId.charAt(0);
+    var ind = {x: 0, y: 1}[axLetter];
+    var dimensions = trace.dimensions;
+
+    for(var i = 0, k = 0; i < dimensions.length; i++) {
+        if(dimensions[i].visible) {
+            if(trace._diag[i][ind] === axId) return k;
+            k++;
+        }
+    }
+    return false;
+}
+
+module.exports = {
+    moduleType: 'trace',
+    name: 'splom',
+
+    basePlotModule: require('./base_plot'),
+    categories: ['gl', 'regl', 'cartesian', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'],
+
+    attributes: require('./attributes'),
+    supplyDefaults: require('./defaults'),
+
+    calc: calc,
+    plot: plot,
+    hoverPoints: hoverPoints,
+    selectPoints: selectPoints,
+    style: style,
+
+    meta: {
+        description: [
+            'Splom traces generate scatter plot matrix visualizations.',
+            'Each splom `dimensions` items correspond to a generated axis.',
+            'Values for each of those dimensions are set in `dimensions[i].values`.',
+            'Splom traces support all `scattergl` marker style attributes.',
+            'Specify `layout.grid` attributes and/or layout x-axis and y-axis attributes',
+            'for more control over the axis positioning and style. '
+        ].join(' ')
+    }
+};
+
+// splom traces use the 'grid' component to generate their axes,
+// register it here
+Registry.register(Grid);
diff --git a/src/traces/violin/index.js b/src/traces/violin/index.js
index c97355078d2..26e39f644f6 100644
--- a/src/traces/violin/index.js
+++ b/src/traces/violin/index.js
@@ -23,7 +23,7 @@ module.exports = {
     moduleType: 'trace',
     name: 'violin',
     basePlotModule: require('../../plots/cartesian'),
-    categories: ['cartesian', 'symbols', 'oriented', 'box-violin', 'showLegend'],
+    categories: ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'draggedPts'],
     meta: {
         description: [
             'In vertical (horizontal) violin plots,',
diff --git a/tasks/baseline.js b/tasks/baseline.js
index 2e6f666a883..707ce8a74be 100644
--- a/tasks/baseline.js
+++ b/tasks/baseline.js
@@ -14,4 +14,6 @@ var cmd = containerCommands.getRunCmd(
 );
 
 console.log(msg);
-common.execCmd(cmd);
+common.execCmd(containerCommands.ping, function() {
+    common.execCmd(cmd);
+});
diff --git a/test/image/baselines/splom_0.png b/test/image/baselines/splom_0.png
new file mode 100644
index 00000000000..e4729fe2691
Binary files /dev/null and b/test/image/baselines/splom_0.png differ
diff --git a/test/image/baselines/splom_array-styles.png b/test/image/baselines/splom_array-styles.png
new file mode 100644
index 00000000000..d608b2f9d40
Binary files /dev/null and b/test/image/baselines/splom_array-styles.png differ
diff --git a/test/image/baselines/splom_dates.png b/test/image/baselines/splom_dates.png
new file mode 100644
index 00000000000..fa6cfc8aa57
Binary files /dev/null and b/test/image/baselines/splom_dates.png differ
diff --git a/test/image/baselines/splom_iris.png b/test/image/baselines/splom_iris.png
new file mode 100644
index 00000000000..a97f4c5b85a
Binary files /dev/null and b/test/image/baselines/splom_iris.png differ
diff --git a/test/image/baselines/splom_large.png b/test/image/baselines/splom_large.png
new file mode 100644
index 00000000000..1fcf729c532
Binary files /dev/null and b/test/image/baselines/splom_large.png differ
diff --git a/test/image/baselines/splom_log.png b/test/image/baselines/splom_log.png
new file mode 100644
index 00000000000..e32063f9f3e
Binary files /dev/null and b/test/image/baselines/splom_log.png differ
diff --git a/test/image/baselines/splom_lower-nodiag.png b/test/image/baselines/splom_lower-nodiag.png
new file mode 100644
index 00000000000..1a4f8f587ac
Binary files /dev/null and b/test/image/baselines/splom_lower-nodiag.png differ
diff --git a/test/image/baselines/splom_lower.png b/test/image/baselines/splom_lower.png
new file mode 100644
index 00000000000..d3a4518a7bf
Binary files /dev/null and b/test/image/baselines/splom_lower.png differ
diff --git a/test/image/baselines/splom_ragged-via-axes.png b/test/image/baselines/splom_ragged-via-axes.png
new file mode 100644
index 00000000000..b3a54479fa2
Binary files /dev/null and b/test/image/baselines/splom_ragged-via-axes.png differ
diff --git a/test/image/baselines/splom_ragged-via-visible-false.png b/test/image/baselines/splom_ragged-via-visible-false.png
new file mode 100644
index 00000000000..8b95fff9ffb
Binary files /dev/null and b/test/image/baselines/splom_ragged-via-visible-false.png differ
diff --git a/test/image/baselines/splom_upper-nodiag.png b/test/image/baselines/splom_upper-nodiag.png
new file mode 100644
index 00000000000..9398120da17
Binary files /dev/null and b/test/image/baselines/splom_upper-nodiag.png differ
diff --git a/test/image/baselines/splom_upper.png b/test/image/baselines/splom_upper.png
new file mode 100644
index 00000000000..2d955d8643d
Binary files /dev/null and b/test/image/baselines/splom_upper.png differ
diff --git a/test/image/baselines/splom_with-cartesian.png b/test/image/baselines/splom_with-cartesian.png
new file mode 100644
index 00000000000..db20b630af7
Binary files /dev/null and b/test/image/baselines/splom_with-cartesian.png differ
diff --git a/test/image/mocks/splom_0.json b/test/image/mocks/splom_0.json
new file mode 100644
index 00000000000..4bd69a7d75e
--- /dev/null
+++ b/test/image/mocks/splom_0.json
@@ -0,0 +1,12 @@
+{
+  "data": [{
+    "type": "splom",
+    "dimensions": [{
+      "values": [1, 2, 3],
+      "label": "A"
+    }, {
+      "values": [2, 5, 6],
+      "label": "B"
+    }]
+  }]
+}
diff --git a/test/image/mocks/splom_array-styles.json b/test/image/mocks/splom_array-styles.json
new file mode 100644
index 00000000000..1c59d79a657
--- /dev/null
+++ b/test/image/mocks/splom_array-styles.json
@@ -0,0 +1,22 @@
+{
+  "data": [{
+    "type": "splom",
+    "dimensions": [{
+      "values": [1, 2, 3],
+      "label": "A"
+    }, {
+      "values": [2, 5, 6],
+      "label": "B"
+    }],
+    "marker": {
+      "symbol": ["diamond", "cross", "square"],
+      "color": ["green", "blue", "red"],
+      "size": [20, 40, 10],
+      "line": {
+        "width": [2, 0, 3],
+        "color": ["red", "", "blue"]
+      },
+      "opacity": [1, 0.8, 0.6]
+    }
+  }]
+}
diff --git a/test/image/mocks/splom_dates.json b/test/image/mocks/splom_dates.json
new file mode 100644
index 00000000000..33be6f2bd8f
--- /dev/null
+++ b/test/image/mocks/splom_dates.json
@@ -0,0 +1,22 @@
+{
+  "data": [{
+    "type": "splom",
+    "dimensions": [{
+      "values": ["2000-01-01", "2001-02-01", "2010-10-03"],
+      "label": "A"
+    }, {
+      "values": ["2003-04-21", "2012-02-01", "2005-10-03"],
+      "label": "B"
+    }],
+    "marker": {
+      "symbol": "cross"
+    },
+    "selected": {
+      "marker": {"color": "green"}
+    },
+    "unselected": {
+      "marker": {"color": "red"}
+    },
+    "selectedpoints": [0, 2]
+  }]
+}
diff --git a/test/image/mocks/splom_iris.json b/test/image/mocks/splom_iris.json
new file mode 100644
index 00000000000..49d25615c23
--- /dev/null
+++ b/test/image/mocks/splom_iris.json
@@ -0,0 +1,696 @@
+{
+  "data": [
+    {
+      "type": "splom",
+      "name": "Setosa",
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "5.1",
+            "4.9",
+            "4.7",
+            "4.6",
+            "5.0",
+            "5.4",
+            "4.6",
+            "5.0",
+            "4.4",
+            "4.9",
+            "5.4",
+            "4.8",
+            "4.8",
+            "4.3",
+            "5.8",
+            "5.7",
+            "5.4",
+            "5.1",
+            "5.7",
+            "5.1",
+            "5.4",
+            "5.1",
+            "4.6",
+            "5.1",
+            "4.8",
+            "5.0",
+            "5.0",
+            "5.2",
+            "5.2",
+            "4.7",
+            "4.8",
+            "5.4",
+            "5.2",
+            "5.5",
+            "4.9",
+            "5.0",
+            "5.5",
+            "4.9",
+            "4.4",
+            "5.1",
+            "5.0",
+            "4.5",
+            "4.4",
+            "5.0",
+            "5.1",
+            "4.8",
+            "5.1",
+            "4.6",
+            "5.3",
+            "5.0"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.5",
+            "3.0",
+            "3.2",
+            "3.1",
+            "3.6",
+            "3.9",
+            "3.4",
+            "3.4",
+            "2.9",
+            "3.1",
+            "3.7",
+            "3.4",
+            "3.0",
+            "3.0",
+            "4.0",
+            "4.4",
+            "3.9",
+            "3.5",
+            "3.8",
+            "3.8",
+            "3.4",
+            "3.7",
+            "3.6",
+            "3.3",
+            "3.4",
+            "3.0",
+            "3.4",
+            "3.5",
+            "3.4",
+            "3.2",
+            "3.1",
+            "3.4",
+            "4.1",
+            "4.2",
+            "3.1",
+            "3.2",
+            "3.5",
+            "3.1",
+            "3.0",
+            "3.4",
+            "3.5",
+            "2.3",
+            "3.2",
+            "3.5",
+            "3.8",
+            "3.0",
+            "3.8",
+            "3.2",
+            "3.7",
+            "3.3"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "1.4",
+            "1.4",
+            "1.3",
+            "1.5",
+            "1.4",
+            "1.7",
+            "1.4",
+            "1.5",
+            "1.4",
+            "1.5",
+            "1.5",
+            "1.6",
+            "1.4",
+            "1.1",
+            "1.2",
+            "1.5",
+            "1.3",
+            "1.4",
+            "1.7",
+            "1.5",
+            "1.7",
+            "1.5",
+            "1.0",
+            "1.7",
+            "1.9",
+            "1.6",
+            "1.6",
+            "1.5",
+            "1.4",
+            "1.6",
+            "1.6",
+            "1.5",
+            "1.5",
+            "1.4",
+            "1.5",
+            "1.2",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.3",
+            "1.3",
+            "1.6",
+            "1.9",
+            "1.4",
+            "1.6",
+            "1.4",
+            "1.5",
+            "1.4"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.3",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.1",
+            "0.2",
+            "0.4",
+            "0.4",
+            "0.3",
+            "0.3",
+            "0.3",
+            "0.2",
+            "0.4",
+            "0.2",
+            "0.5",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.1",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.3",
+            "0.3",
+            "0.2",
+            "0.6",
+            "0.4",
+            "0.3",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "red"
+      }
+    },
+    {
+      "type": "splom",
+      "name": "Versicolor",
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "7.0",
+            "6.4",
+            "6.9",
+            "5.5",
+            "6.5",
+            "5.7",
+            "6.3",
+            "4.9",
+            "6.6",
+            "5.2",
+            "5.0",
+            "5.9",
+            "6.0",
+            "6.1",
+            "5.6",
+            "6.7",
+            "5.6",
+            "5.8",
+            "6.2",
+            "5.6",
+            "5.9",
+            "6.1",
+            "6.3",
+            "6.1",
+            "6.4",
+            "6.6",
+            "6.8",
+            "6.7",
+            "6.0",
+            "5.7",
+            "5.5",
+            "5.5",
+            "5.8",
+            "6.0",
+            "5.4",
+            "6.0",
+            "6.7",
+            "6.3",
+            "5.6",
+            "5.5",
+            "5.5",
+            "6.1",
+            "5.8",
+            "5.0",
+            "5.6",
+            "5.7",
+            "5.7",
+            "6.2",
+            "5.1",
+            "5.7"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.2",
+            "3.2",
+            "3.1",
+            "2.3",
+            "2.8",
+            "2.8",
+            "3.3",
+            "2.4",
+            "2.9",
+            "2.7",
+            "2.0",
+            "3.0",
+            "2.2",
+            "2.9",
+            "2.9",
+            "3.1",
+            "3.0",
+            "2.7",
+            "2.2",
+            "2.5",
+            "3.2",
+            "2.8",
+            "2.5",
+            "2.8",
+            "2.9",
+            "3.0",
+            "2.8",
+            "3.0",
+            "2.9",
+            "2.6",
+            "2.4",
+            "2.4",
+            "2.7",
+            "2.7",
+            "3.0",
+            "3.4",
+            "3.1",
+            "2.3",
+            "3.0",
+            "2.5",
+            "2.6",
+            "3.0",
+            "2.6",
+            "2.3",
+            "2.7",
+            "3.0",
+            "2.9",
+            "2.9",
+            "2.5",
+            "2.8"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "4.7",
+            "4.5",
+            "4.9",
+            "4.0",
+            "4.6",
+            "4.5",
+            "4.7",
+            "3.3",
+            "4.6",
+            "3.9",
+            "3.5",
+            "4.2",
+            "4.0",
+            "4.7",
+            "3.6",
+            "4.4",
+            "4.5",
+            "4.1",
+            "4.5",
+            "3.9",
+            "4.8",
+            "4.0",
+            "4.9",
+            "4.7",
+            "4.3",
+            "4.4",
+            "4.8",
+            "5.0",
+            "4.5",
+            "3.5",
+            "3.8",
+            "3.7",
+            "3.9",
+            "5.1",
+            "4.5",
+            "4.5",
+            "4.7",
+            "4.4",
+            "4.1",
+            "4.0",
+            "4.4",
+            "4.6",
+            "4.0",
+            "3.3",
+            "4.2",
+            "4.2",
+            "4.2",
+            "4.3",
+            "3.0",
+            "4.1"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "1.4",
+            "1.5",
+            "1.5",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.6",
+            "1.0",
+            "1.3",
+            "1.4",
+            "1.0",
+            "1.5",
+            "1.0",
+            "1.4",
+            "1.3",
+            "1.4",
+            "1.5",
+            "1.0",
+            "1.5",
+            "1.1",
+            "1.8",
+            "1.3",
+            "1.5",
+            "1.2",
+            "1.3",
+            "1.4",
+            "1.4",
+            "1.7",
+            "1.5",
+            "1.0",
+            "1.1",
+            "1.0",
+            "1.2",
+            "1.6",
+            "1.5",
+            "1.6",
+            "1.5",
+            "1.3",
+            "1.3",
+            "1.3",
+            "1.2",
+            "1.4",
+            "1.2",
+            "1.0",
+            "1.3",
+            "1.2",
+            "1.3",
+            "1.3",
+            "1.1",
+            "1.3"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "green"
+      }
+    },
+    {
+      "type": "splom",
+      "name": "Virginica",
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "6.3",
+            "5.8",
+            "7.1",
+            "6.3",
+            "6.5",
+            "7.6",
+            "4.9",
+            "7.3",
+            "6.7",
+            "7.2",
+            "6.5",
+            "6.4",
+            "6.8",
+            "5.7",
+            "5.8",
+            "6.4",
+            "6.5",
+            "7.7",
+            "7.7",
+            "6.0",
+            "6.9",
+            "5.6",
+            "7.7",
+            "6.3",
+            "6.7",
+            "7.2",
+            "6.2",
+            "6.1",
+            "6.4",
+            "7.2",
+            "7.4",
+            "7.9",
+            "6.4",
+            "6.3",
+            "6.1",
+            "7.7",
+            "6.3",
+            "6.4",
+            "6.0",
+            "6.9",
+            "6.7",
+            "6.9",
+            "5.8",
+            "6.8",
+            "6.7",
+            "6.7",
+            "6.3",
+            "6.5",
+            "6.2",
+            "5.9"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.3",
+            "2.7",
+            "3.0",
+            "2.9",
+            "3.0",
+            "3.0",
+            "2.5",
+            "2.9",
+            "2.5",
+            "3.6",
+            "3.2",
+            "2.7",
+            "3.0",
+            "2.5",
+            "2.8",
+            "3.2",
+            "3.0",
+            "3.8",
+            "2.6",
+            "2.2",
+            "3.2",
+            "2.8",
+            "2.8",
+            "2.7",
+            "3.3",
+            "3.2",
+            "2.8",
+            "3.0",
+            "2.8",
+            "3.0",
+            "2.8",
+            "3.8",
+            "2.8",
+            "2.8",
+            "2.6",
+            "3.0",
+            "3.4",
+            "3.1",
+            "3.0",
+            "3.1",
+            "3.1",
+            "3.1",
+            "2.7",
+            "3.2",
+            "3.3",
+            "3.0",
+            "2.5",
+            "3.0",
+            "3.4",
+            "3.0"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "6.0",
+            "5.1",
+            "5.9",
+            "5.6",
+            "5.8",
+            "6.6",
+            "4.5",
+            "6.3",
+            "5.8",
+            "6.1",
+            "5.1",
+            "5.3",
+            "5.5",
+            "5.0",
+            "5.1",
+            "5.3",
+            "5.5",
+            "6.7",
+            "6.9",
+            "5.0",
+            "5.7",
+            "4.9",
+            "6.7",
+            "4.9",
+            "5.7",
+            "6.0",
+            "4.8",
+            "4.9",
+            "5.6",
+            "5.8",
+            "6.1",
+            "6.4",
+            "5.6",
+            "5.1",
+            "5.6",
+            "6.1",
+            "5.6",
+            "5.5",
+            "4.8",
+            "5.4",
+            "5.6",
+            "5.1",
+            "5.1",
+            "5.9",
+            "5.7",
+            "5.2",
+            "5.0",
+            "5.2",
+            "5.4",
+            "5.1"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "2.5",
+            "1.9",
+            "2.1",
+            "1.8",
+            "2.2",
+            "2.1",
+            "1.7",
+            "1.8",
+            "1.8",
+            "2.5",
+            "2.0",
+            "1.9",
+            "2.1",
+            "2.0",
+            "2.4",
+            "2.3",
+            "1.8",
+            "2.2",
+            "2.3",
+            "1.5",
+            "2.3",
+            "2.0",
+            "2.0",
+            "1.8",
+            "2.1",
+            "1.8",
+            "1.8",
+            "1.8",
+            "2.1",
+            "1.6",
+            "1.9",
+            "2.0",
+            "2.2",
+            "1.5",
+            "1.4",
+            "2.3",
+            "2.4",
+            "1.8",
+            "1.8",
+            "2.1",
+            "2.4",
+            "2.3",
+            "1.9",
+            "2.3",
+            "2.5",
+            "2.3",
+            "1.9",
+            "2.0",
+            "2.3",
+            "1.8"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "blue"
+      }
+    }
+  ],
+  "layout": {
+	"title": "Iris dataset splom",
+  	"width": 600,
+	"height": 500
+  }
+}
diff --git a/test/image/mocks/splom_large.json b/test/image/mocks/splom_large.json
new file mode 100644
index 00000000000..1c8ec021e7a
--- /dev/null
+++ b/test/image/mocks/splom_large.json
@@ -0,0 +1,302 @@
+{
+  "data": [
+    {
+      "type": "splom",
+      "dimensions": [
+        {
+          "values": [
+            0.08580232218589545,
+            0.6767470576947283,
+            0.8399763393645798,
+            0.6233479334978567,
+            0.08481140931214393,
+            0.27902188027625985,
+            0.025236798479649547,
+            0.34783438974705616,
+            0.6285357690300772,
+            0.13128833631128067
+          ]
+        },
+        {
+          "values": [
+            0.05246062923863537,
+            0.46333116868608726,
+            0.16715434507619897,
+            0.05018811233733689,
+            0.2853876011714973,
+            0.7507188960271824,
+            0.7414407081329897,
+            0.4931548658431939,
+            0.008003443608825211,
+            0.8401897754160457
+          ]
+        },
+        {
+          "values": [
+            0.06520810239108199,
+            0.8495982108318272,
+            0.9046045659524642,
+            0.5725252028953924,
+            0.8553173173083635,
+            0.29449164621853474,
+            0.7745611908355845,
+            0.4757865646903625,
+            0.14563435980102013,
+            0.897525101750952
+          ]
+        },
+        {
+          "values": [
+            0.435165783447075,
+            0.8082148816441925,
+            0.31066944105379823,
+            0.8166938174345009,
+            0.6703382645297264,
+            0.18962249374111084,
+            0.860080342517918,
+            0.5591463874851217,
+            0.4826869165499734,
+            0.7729727246187266
+          ]
+        },
+        {
+          "values": [
+            0.630098234895871,
+            0.10669275996298366,
+            0.05951757006824776,
+            0.5588158941493411,
+            0.1765069271002584,
+            0.015520260287421817,
+            0.9541158554813636,
+            0.66976671303951,
+            0.12611169188802474,
+            0.32517056116627363
+          ]
+        },
+        {
+          "values": [
+            0.5571953297919265,
+            0.08237697621302642,
+            0.46356179788018537,
+            0.2283577655950968,
+            0.671248603613811,
+            0.45165109690210636,
+            0.8550100301829344,
+            0.43670534843721587,
+            0.18312610870964297,
+            0.17910843367122276
+          ]
+        },
+        {
+          "values": [
+            0.20510914327614138,
+            0.6009777248652213,
+            0.25303102693529844,
+            0.43614098207137664,
+            0.2593560264066288,
+            0.9335830621077947,
+            0.05298398343530342,
+            0.6010736830375696,
+            0.7180627032220392,
+            0.7916282763768157
+          ]
+        },
+        {
+          "values": [
+            0.4701029548510385,
+            0.1615863398834989,
+            0.27793363854136377,
+            0.5006702407502888,
+            0.3769713203055185,
+            0.3698032504585034,
+            0.7368873675721836,
+            0.8255155665205895,
+            0.5837545175648222,
+            0.7362198040889636
+          ]
+        },
+        {
+          "values": [
+            0.3247821731546314,
+            0.9700757962151294,
+            0.8779166627266124,
+            0.9903510723754136,
+            0.7783682733815458,
+            0.5488486035822286,
+            0.4114313577586548,
+            0.030434854263728717,
+            0.7456544486246504,
+            0.8103302611231877
+          ]
+        },
+        {
+          "values": [
+            0.4269295190776543,
+            0.1609650068167563,
+            0.92842164174826,
+            0.6442505274731554,
+            0.7334215664663768,
+            0.9218766505786056,
+            0.44096562168773,
+            0.8741879191864952,
+            0.5950608359707568,
+            0.4529164095766698
+          ]
+        },
+        {
+          "values": [
+            0.6754203197568098,
+            0.6808047852168977,
+            0.4353488050618257,
+            0.35516657099681637,
+            0.15142747472405116,
+            0.05994585078335124,
+            0.727449276326178,
+            0.5834772499810623,
+            0.141839296932462,
+            0.5338457923596154
+          ]
+        },
+        {
+          "values": [
+            0.47302662880016744,
+            0.8157932884895502,
+            0.4544100891785916,
+            0.9704393989173103,
+            0.5641930022582622,
+            0.24022301089480513,
+            0.8516804346875824,
+            0.2424559182552377,
+            0.9810817730745405,
+            0.5722415623556636
+          ]
+        },
+        {
+          "values": [
+            0.9835886244745469,
+            0.005823780299727188,
+            0.5840877831857845,
+            0.40081036928303715,
+            0.47573859777702054,
+            0.9485985220903039,
+            0.7887429836279782,
+            0.6610711008500716,
+            0.6008170543130837,
+            0.2935777438750209
+          ]
+        },
+        {
+          "values": [
+            0.9879737148378489,
+            0.9425983110033131,
+            0.7892004437897853,
+            0.957875505374933,
+            0.31571744199972906,
+            0.034133515508368184,
+            0.7089369307443012,
+            0.9566945192342795,
+            0.1469133087864285,
+            0.38255414983814706
+          ]
+        },
+        {
+          "values": [
+            0.7258788568242582,
+            0.15803434660836024,
+            0.48035145055604733,
+            0.1754872010631272,
+            0.0943373904795064,
+            0.5284316947074943,
+            0.3449912254162446,
+            0.4743126200307999,
+            0.6169826341413327,
+            0.6237454994146361
+          ]
+        },
+        {
+          "values": [
+            0.7014025222418512,
+            0.48264213331162287,
+            0.5321815314013991,
+            0.5095588522475991,
+            0.09731577257522939,
+            0.3803088428846304,
+            0.43210179892978906,
+            0.28328046635762205,
+            0.5255134053702564,
+            0.05773536414283709
+          ]
+        },
+        {
+          "values": [
+            0.06202609532657033,
+            0.8665158901026537,
+            0.3218485709140262,
+            0.49053646244974103,
+            0.19532382906181467,
+            0.2675695479993139,
+            0.5871232161585691,
+            0.6760894505442174,
+            0.896410114646955,
+            0.04000902917396676
+          ]
+        },
+        {
+          "values": [
+            0.6871231252786911,
+            0.034405780777586825,
+            0.5302260880695147,
+            0.37267746064321083,
+            0.21779005210840952,
+            0.8520888106169335,
+            0.3820957228405504,
+            0.29543100636475117,
+            0.3324100288461711,
+            0.6317047124071087
+          ]
+        },
+        {
+          "values": [
+            0.8670401189375461,
+            0.7983338320962376,
+            0.8367762774071041,
+            0.5174816210864217,
+            0.007785283032919477,
+            0.059056926891100536,
+            0.8979079754970136,
+            0.14826669761408717,
+            0.5356319781873422,
+            0.7527907633534512
+          ]
+        },
+        {
+          "values": [
+            0.38348125720617166,
+            0.4723378247456569,
+            0.9472589658711628,
+            0.3536431949921497,
+            0.5983873814500404,
+            0.49254652667510146,
+            0.015100469074888823,
+            0.11575656088204078,
+            0.21089120172691755,
+            0.3102753400426521
+          ]
+        }
+      ]
+    }
+  ],
+  "layout": {
+    "width": 1500,
+    "height": 1500,
+    "xaxis": {
+      "dtick": 0.1,
+      "gridcolor": "cyan",
+      "gridwidth": 2
+    },
+    "yaxis": {
+      "zerolinecolor": "red",
+      "zerolinewidth": 2
+    }
+  }
+}
diff --git a/test/image/mocks/splom_log.json b/test/image/mocks/splom_log.json
new file mode 100644
index 00000000000..a7adf6f99a0
--- /dev/null
+++ b/test/image/mocks/splom_log.json
@@ -0,0 +1,24 @@
+{
+  "data": [{
+    "type": "splom",
+    "dimensions": [{
+      "values": [1e1, 1e2, 1e3],
+      "label": "A"
+    }, {
+      "values": [1e2, 1e5, 1e6],
+      "label": "B"
+    }],
+    "marker": {
+      "size": 20,
+      "line": {"width": 2, "color": "#444"},
+      "color": [10, 40, 100],
+      "colorscale": "Reds"
+    }
+  }],
+  "layout": {
+    "xaxis": {"type": "log"},
+    "yaxis": {"type": "log"},
+    "xaxis2": {"type": "log"},
+    "yaxis2": {"type": "log"}
+  }
+}
diff --git a/test/image/mocks/splom_lower-nodiag.json b/test/image/mocks/splom_lower-nodiag.json
new file mode 100644
index 00000000000..4ade038cdc1
--- /dev/null
+++ b/test/image/mocks/splom_lower-nodiag.json
@@ -0,0 +1,706 @@
+{
+  "data": [
+    {
+      "type": "splom",
+      "name": "Setosa",
+      "showupperhalf": false,
+      "diagonal": {"visible": false},
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "5.1",
+            "4.9",
+            "4.7",
+            "4.6",
+            "5.0",
+            "5.4",
+            "4.6",
+            "5.0",
+            "4.4",
+            "4.9",
+            "5.4",
+            "4.8",
+            "4.8",
+            "4.3",
+            "5.8",
+            "5.7",
+            "5.4",
+            "5.1",
+            "5.7",
+            "5.1",
+            "5.4",
+            "5.1",
+            "4.6",
+            "5.1",
+            "4.8",
+            "5.0",
+            "5.0",
+            "5.2",
+            "5.2",
+            "4.7",
+            "4.8",
+            "5.4",
+            "5.2",
+            "5.5",
+            "4.9",
+            "5.0",
+            "5.5",
+            "4.9",
+            "4.4",
+            "5.1",
+            "5.0",
+            "4.5",
+            "4.4",
+            "5.0",
+            "5.1",
+            "4.8",
+            "5.1",
+            "4.6",
+            "5.3",
+            "5.0"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.5",
+            "3.0",
+            "3.2",
+            "3.1",
+            "3.6",
+            "3.9",
+            "3.4",
+            "3.4",
+            "2.9",
+            "3.1",
+            "3.7",
+            "3.4",
+            "3.0",
+            "3.0",
+            "4.0",
+            "4.4",
+            "3.9",
+            "3.5",
+            "3.8",
+            "3.8",
+            "3.4",
+            "3.7",
+            "3.6",
+            "3.3",
+            "3.4",
+            "3.0",
+            "3.4",
+            "3.5",
+            "3.4",
+            "3.2",
+            "3.1",
+            "3.4",
+            "4.1",
+            "4.2",
+            "3.1",
+            "3.2",
+            "3.5",
+            "3.1",
+            "3.0",
+            "3.4",
+            "3.5",
+            "2.3",
+            "3.2",
+            "3.5",
+            "3.8",
+            "3.0",
+            "3.8",
+            "3.2",
+            "3.7",
+            "3.3"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "1.4",
+            "1.4",
+            "1.3",
+            "1.5",
+            "1.4",
+            "1.7",
+            "1.4",
+            "1.5",
+            "1.4",
+            "1.5",
+            "1.5",
+            "1.6",
+            "1.4",
+            "1.1",
+            "1.2",
+            "1.5",
+            "1.3",
+            "1.4",
+            "1.7",
+            "1.5",
+            "1.7",
+            "1.5",
+            "1.0",
+            "1.7",
+            "1.9",
+            "1.6",
+            "1.6",
+            "1.5",
+            "1.4",
+            "1.6",
+            "1.6",
+            "1.5",
+            "1.5",
+            "1.4",
+            "1.5",
+            "1.2",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.3",
+            "1.3",
+            "1.6",
+            "1.9",
+            "1.4",
+            "1.6",
+            "1.4",
+            "1.5",
+            "1.4"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.3",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.1",
+            "0.2",
+            "0.4",
+            "0.4",
+            "0.3",
+            "0.3",
+            "0.3",
+            "0.2",
+            "0.4",
+            "0.2",
+            "0.5",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.1",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.3",
+            "0.3",
+            "0.2",
+            "0.6",
+            "0.4",
+            "0.3",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "red"
+      }
+    },
+    {
+      "type": "splom",
+      "name": "Versicolor",
+      "showupperhalf": false,
+      "diagonal": {"visible": false},
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "7.0",
+            "6.4",
+            "6.9",
+            "5.5",
+            "6.5",
+            "5.7",
+            "6.3",
+            "4.9",
+            "6.6",
+            "5.2",
+            "5.0",
+            "5.9",
+            "6.0",
+            "6.1",
+            "5.6",
+            "6.7",
+            "5.6",
+            "5.8",
+            "6.2",
+            "5.6",
+            "5.9",
+            "6.1",
+            "6.3",
+            "6.1",
+            "6.4",
+            "6.6",
+            "6.8",
+            "6.7",
+            "6.0",
+            "5.7",
+            "5.5",
+            "5.5",
+            "5.8",
+            "6.0",
+            "5.4",
+            "6.0",
+            "6.7",
+            "6.3",
+            "5.6",
+            "5.5",
+            "5.5",
+            "6.1",
+            "5.8",
+            "5.0",
+            "5.6",
+            "5.7",
+            "5.7",
+            "6.2",
+            "5.1",
+            "5.7"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.2",
+            "3.2",
+            "3.1",
+            "2.3",
+            "2.8",
+            "2.8",
+            "3.3",
+            "2.4",
+            "2.9",
+            "2.7",
+            "2.0",
+            "3.0",
+            "2.2",
+            "2.9",
+            "2.9",
+            "3.1",
+            "3.0",
+            "2.7",
+            "2.2",
+            "2.5",
+            "3.2",
+            "2.8",
+            "2.5",
+            "2.8",
+            "2.9",
+            "3.0",
+            "2.8",
+            "3.0",
+            "2.9",
+            "2.6",
+            "2.4",
+            "2.4",
+            "2.7",
+            "2.7",
+            "3.0",
+            "3.4",
+            "3.1",
+            "2.3",
+            "3.0",
+            "2.5",
+            "2.6",
+            "3.0",
+            "2.6",
+            "2.3",
+            "2.7",
+            "3.0",
+            "2.9",
+            "2.9",
+            "2.5",
+            "2.8"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "4.7",
+            "4.5",
+            "4.9",
+            "4.0",
+            "4.6",
+            "4.5",
+            "4.7",
+            "3.3",
+            "4.6",
+            "3.9",
+            "3.5",
+            "4.2",
+            "4.0",
+            "4.7",
+            "3.6",
+            "4.4",
+            "4.5",
+            "4.1",
+            "4.5",
+            "3.9",
+            "4.8",
+            "4.0",
+            "4.9",
+            "4.7",
+            "4.3",
+            "4.4",
+            "4.8",
+            "5.0",
+            "4.5",
+            "3.5",
+            "3.8",
+            "3.7",
+            "3.9",
+            "5.1",
+            "4.5",
+            "4.5",
+            "4.7",
+            "4.4",
+            "4.1",
+            "4.0",
+            "4.4",
+            "4.6",
+            "4.0",
+            "3.3",
+            "4.2",
+            "4.2",
+            "4.2",
+            "4.3",
+            "3.0",
+            "4.1"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "1.4",
+            "1.5",
+            "1.5",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.6",
+            "1.0",
+            "1.3",
+            "1.4",
+            "1.0",
+            "1.5",
+            "1.0",
+            "1.4",
+            "1.3",
+            "1.4",
+            "1.5",
+            "1.0",
+            "1.5",
+            "1.1",
+            "1.8",
+            "1.3",
+            "1.5",
+            "1.2",
+            "1.3",
+            "1.4",
+            "1.4",
+            "1.7",
+            "1.5",
+            "1.0",
+            "1.1",
+            "1.0",
+            "1.2",
+            "1.6",
+            "1.5",
+            "1.6",
+            "1.5",
+            "1.3",
+            "1.3",
+            "1.3",
+            "1.2",
+            "1.4",
+            "1.2",
+            "1.0",
+            "1.3",
+            "1.2",
+            "1.3",
+            "1.3",
+            "1.1",
+            "1.3"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "green"
+      }
+    },
+    {
+      "type": "splom",
+      "name": "Virginica",
+      "showupperhalf": false,
+      "diagonal": {"visible": false},
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "6.3",
+            "5.8",
+            "7.1",
+            "6.3",
+            "6.5",
+            "7.6",
+            "4.9",
+            "7.3",
+            "6.7",
+            "7.2",
+            "6.5",
+            "6.4",
+            "6.8",
+            "5.7",
+            "5.8",
+            "6.4",
+            "6.5",
+            "7.7",
+            "7.7",
+            "6.0",
+            "6.9",
+            "5.6",
+            "7.7",
+            "6.3",
+            "6.7",
+            "7.2",
+            "6.2",
+            "6.1",
+            "6.4",
+            "7.2",
+            "7.4",
+            "7.9",
+            "6.4",
+            "6.3",
+            "6.1",
+            "7.7",
+            "6.3",
+            "6.4",
+            "6.0",
+            "6.9",
+            "6.7",
+            "6.9",
+            "5.8",
+            "6.8",
+            "6.7",
+            "6.7",
+            "6.3",
+            "6.5",
+            "6.2",
+            "5.9"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.3",
+            "2.7",
+            "3.0",
+            "2.9",
+            "3.0",
+            "3.0",
+            "2.5",
+            "2.9",
+            "2.5",
+            "3.6",
+            "3.2",
+            "2.7",
+            "3.0",
+            "2.5",
+            "2.8",
+            "3.2",
+            "3.0",
+            "3.8",
+            "2.6",
+            "2.2",
+            "3.2",
+            "2.8",
+            "2.8",
+            "2.7",
+            "3.3",
+            "3.2",
+            "2.8",
+            "3.0",
+            "2.8",
+            "3.0",
+            "2.8",
+            "3.8",
+            "2.8",
+            "2.8",
+            "2.6",
+            "3.0",
+            "3.4",
+            "3.1",
+            "3.0",
+            "3.1",
+            "3.1",
+            "3.1",
+            "2.7",
+            "3.2",
+            "3.3",
+            "3.0",
+            "2.5",
+            "3.0",
+            "3.4",
+            "3.0"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "6.0",
+            "5.1",
+            "5.9",
+            "5.6",
+            "5.8",
+            "6.6",
+            "4.5",
+            "6.3",
+            "5.8",
+            "6.1",
+            "5.1",
+            "5.3",
+            "5.5",
+            "5.0",
+            "5.1",
+            "5.3",
+            "5.5",
+            "6.7",
+            "6.9",
+            "5.0",
+            "5.7",
+            "4.9",
+            "6.7",
+            "4.9",
+            "5.7",
+            "6.0",
+            "4.8",
+            "4.9",
+            "5.6",
+            "5.8",
+            "6.1",
+            "6.4",
+            "5.6",
+            "5.1",
+            "5.6",
+            "6.1",
+            "5.6",
+            "5.5",
+            "4.8",
+            "5.4",
+            "5.6",
+            "5.1",
+            "5.1",
+            "5.9",
+            "5.7",
+            "5.2",
+            "5.0",
+            "5.2",
+            "5.4",
+            "5.1"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "2.5",
+            "1.9",
+            "2.1",
+            "1.8",
+            "2.2",
+            "2.1",
+            "1.7",
+            "1.8",
+            "1.8",
+            "2.5",
+            "2.0",
+            "1.9",
+            "2.1",
+            "2.0",
+            "2.4",
+            "2.3",
+            "1.8",
+            "2.2",
+            "2.3",
+            "1.5",
+            "2.3",
+            "2.0",
+            "2.0",
+            "1.8",
+            "2.1",
+            "1.8",
+            "1.8",
+            "1.8",
+            "2.1",
+            "1.6",
+            "1.9",
+            "2.0",
+            "2.2",
+            "1.5",
+            "1.4",
+            "2.3",
+            "2.4",
+            "1.8",
+            "1.8",
+            "2.1",
+            "2.4",
+            "2.3",
+            "1.9",
+            "2.3",
+            "2.5",
+            "2.3",
+            "1.9",
+            "2.0",
+            "2.3",
+            "1.8"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "blue"
+      }
+    }
+  ],
+  "layout": {
+	"title": "Iris dataset splom",
+  	"width": 600,
+	"height": 500,
+    "legend": {
+      "x": 1,
+      "xanchor": "right"
+    }
+  }
+}
diff --git a/test/image/mocks/splom_lower.json b/test/image/mocks/splom_lower.json
new file mode 100644
index 00000000000..192f1c61c85
--- /dev/null
+++ b/test/image/mocks/splom_lower.json
@@ -0,0 +1,699 @@
+{
+  "data": [
+    {
+      "type": "splom",
+      "name": "Setosa",
+      "showupperhalf": false,
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "5.1",
+            "4.9",
+            "4.7",
+            "4.6",
+            "5.0",
+            "5.4",
+            "4.6",
+            "5.0",
+            "4.4",
+            "4.9",
+            "5.4",
+            "4.8",
+            "4.8",
+            "4.3",
+            "5.8",
+            "5.7",
+            "5.4",
+            "5.1",
+            "5.7",
+            "5.1",
+            "5.4",
+            "5.1",
+            "4.6",
+            "5.1",
+            "4.8",
+            "5.0",
+            "5.0",
+            "5.2",
+            "5.2",
+            "4.7",
+            "4.8",
+            "5.4",
+            "5.2",
+            "5.5",
+            "4.9",
+            "5.0",
+            "5.5",
+            "4.9",
+            "4.4",
+            "5.1",
+            "5.0",
+            "4.5",
+            "4.4",
+            "5.0",
+            "5.1",
+            "4.8",
+            "5.1",
+            "4.6",
+            "5.3",
+            "5.0"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.5",
+            "3.0",
+            "3.2",
+            "3.1",
+            "3.6",
+            "3.9",
+            "3.4",
+            "3.4",
+            "2.9",
+            "3.1",
+            "3.7",
+            "3.4",
+            "3.0",
+            "3.0",
+            "4.0",
+            "4.4",
+            "3.9",
+            "3.5",
+            "3.8",
+            "3.8",
+            "3.4",
+            "3.7",
+            "3.6",
+            "3.3",
+            "3.4",
+            "3.0",
+            "3.4",
+            "3.5",
+            "3.4",
+            "3.2",
+            "3.1",
+            "3.4",
+            "4.1",
+            "4.2",
+            "3.1",
+            "3.2",
+            "3.5",
+            "3.1",
+            "3.0",
+            "3.4",
+            "3.5",
+            "2.3",
+            "3.2",
+            "3.5",
+            "3.8",
+            "3.0",
+            "3.8",
+            "3.2",
+            "3.7",
+            "3.3"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "1.4",
+            "1.4",
+            "1.3",
+            "1.5",
+            "1.4",
+            "1.7",
+            "1.4",
+            "1.5",
+            "1.4",
+            "1.5",
+            "1.5",
+            "1.6",
+            "1.4",
+            "1.1",
+            "1.2",
+            "1.5",
+            "1.3",
+            "1.4",
+            "1.7",
+            "1.5",
+            "1.7",
+            "1.5",
+            "1.0",
+            "1.7",
+            "1.9",
+            "1.6",
+            "1.6",
+            "1.5",
+            "1.4",
+            "1.6",
+            "1.6",
+            "1.5",
+            "1.5",
+            "1.4",
+            "1.5",
+            "1.2",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.3",
+            "1.3",
+            "1.6",
+            "1.9",
+            "1.4",
+            "1.6",
+            "1.4",
+            "1.5",
+            "1.4"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.3",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.1",
+            "0.2",
+            "0.4",
+            "0.4",
+            "0.3",
+            "0.3",
+            "0.3",
+            "0.2",
+            "0.4",
+            "0.2",
+            "0.5",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.1",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.3",
+            "0.3",
+            "0.2",
+            "0.6",
+            "0.4",
+            "0.3",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "red"
+      }
+    },
+    {
+      "type": "splom",
+      "name": "Versicolor",
+      "showupperhalf": false,
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "7.0",
+            "6.4",
+            "6.9",
+            "5.5",
+            "6.5",
+            "5.7",
+            "6.3",
+            "4.9",
+            "6.6",
+            "5.2",
+            "5.0",
+            "5.9",
+            "6.0",
+            "6.1",
+            "5.6",
+            "6.7",
+            "5.6",
+            "5.8",
+            "6.2",
+            "5.6",
+            "5.9",
+            "6.1",
+            "6.3",
+            "6.1",
+            "6.4",
+            "6.6",
+            "6.8",
+            "6.7",
+            "6.0",
+            "5.7",
+            "5.5",
+            "5.5",
+            "5.8",
+            "6.0",
+            "5.4",
+            "6.0",
+            "6.7",
+            "6.3",
+            "5.6",
+            "5.5",
+            "5.5",
+            "6.1",
+            "5.8",
+            "5.0",
+            "5.6",
+            "5.7",
+            "5.7",
+            "6.2",
+            "5.1",
+            "5.7"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.2",
+            "3.2",
+            "3.1",
+            "2.3",
+            "2.8",
+            "2.8",
+            "3.3",
+            "2.4",
+            "2.9",
+            "2.7",
+            "2.0",
+            "3.0",
+            "2.2",
+            "2.9",
+            "2.9",
+            "3.1",
+            "3.0",
+            "2.7",
+            "2.2",
+            "2.5",
+            "3.2",
+            "2.8",
+            "2.5",
+            "2.8",
+            "2.9",
+            "3.0",
+            "2.8",
+            "3.0",
+            "2.9",
+            "2.6",
+            "2.4",
+            "2.4",
+            "2.7",
+            "2.7",
+            "3.0",
+            "3.4",
+            "3.1",
+            "2.3",
+            "3.0",
+            "2.5",
+            "2.6",
+            "3.0",
+            "2.6",
+            "2.3",
+            "2.7",
+            "3.0",
+            "2.9",
+            "2.9",
+            "2.5",
+            "2.8"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "4.7",
+            "4.5",
+            "4.9",
+            "4.0",
+            "4.6",
+            "4.5",
+            "4.7",
+            "3.3",
+            "4.6",
+            "3.9",
+            "3.5",
+            "4.2",
+            "4.0",
+            "4.7",
+            "3.6",
+            "4.4",
+            "4.5",
+            "4.1",
+            "4.5",
+            "3.9",
+            "4.8",
+            "4.0",
+            "4.9",
+            "4.7",
+            "4.3",
+            "4.4",
+            "4.8",
+            "5.0",
+            "4.5",
+            "3.5",
+            "3.8",
+            "3.7",
+            "3.9",
+            "5.1",
+            "4.5",
+            "4.5",
+            "4.7",
+            "4.4",
+            "4.1",
+            "4.0",
+            "4.4",
+            "4.6",
+            "4.0",
+            "3.3",
+            "4.2",
+            "4.2",
+            "4.2",
+            "4.3",
+            "3.0",
+            "4.1"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "1.4",
+            "1.5",
+            "1.5",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.6",
+            "1.0",
+            "1.3",
+            "1.4",
+            "1.0",
+            "1.5",
+            "1.0",
+            "1.4",
+            "1.3",
+            "1.4",
+            "1.5",
+            "1.0",
+            "1.5",
+            "1.1",
+            "1.8",
+            "1.3",
+            "1.5",
+            "1.2",
+            "1.3",
+            "1.4",
+            "1.4",
+            "1.7",
+            "1.5",
+            "1.0",
+            "1.1",
+            "1.0",
+            "1.2",
+            "1.6",
+            "1.5",
+            "1.6",
+            "1.5",
+            "1.3",
+            "1.3",
+            "1.3",
+            "1.2",
+            "1.4",
+            "1.2",
+            "1.0",
+            "1.3",
+            "1.2",
+            "1.3",
+            "1.3",
+            "1.1",
+            "1.3"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "green"
+      }
+    },
+    {
+      "type": "splom",
+      "name": "Virginica",
+      "showupperhalf": false,
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "6.3",
+            "5.8",
+            "7.1",
+            "6.3",
+            "6.5",
+            "7.6",
+            "4.9",
+            "7.3",
+            "6.7",
+            "7.2",
+            "6.5",
+            "6.4",
+            "6.8",
+            "5.7",
+            "5.8",
+            "6.4",
+            "6.5",
+            "7.7",
+            "7.7",
+            "6.0",
+            "6.9",
+            "5.6",
+            "7.7",
+            "6.3",
+            "6.7",
+            "7.2",
+            "6.2",
+            "6.1",
+            "6.4",
+            "7.2",
+            "7.4",
+            "7.9",
+            "6.4",
+            "6.3",
+            "6.1",
+            "7.7",
+            "6.3",
+            "6.4",
+            "6.0",
+            "6.9",
+            "6.7",
+            "6.9",
+            "5.8",
+            "6.8",
+            "6.7",
+            "6.7",
+            "6.3",
+            "6.5",
+            "6.2",
+            "5.9"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.3",
+            "2.7",
+            "3.0",
+            "2.9",
+            "3.0",
+            "3.0",
+            "2.5",
+            "2.9",
+            "2.5",
+            "3.6",
+            "3.2",
+            "2.7",
+            "3.0",
+            "2.5",
+            "2.8",
+            "3.2",
+            "3.0",
+            "3.8",
+            "2.6",
+            "2.2",
+            "3.2",
+            "2.8",
+            "2.8",
+            "2.7",
+            "3.3",
+            "3.2",
+            "2.8",
+            "3.0",
+            "2.8",
+            "3.0",
+            "2.8",
+            "3.8",
+            "2.8",
+            "2.8",
+            "2.6",
+            "3.0",
+            "3.4",
+            "3.1",
+            "3.0",
+            "3.1",
+            "3.1",
+            "3.1",
+            "2.7",
+            "3.2",
+            "3.3",
+            "3.0",
+            "2.5",
+            "3.0",
+            "3.4",
+            "3.0"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "6.0",
+            "5.1",
+            "5.9",
+            "5.6",
+            "5.8",
+            "6.6",
+            "4.5",
+            "6.3",
+            "5.8",
+            "6.1",
+            "5.1",
+            "5.3",
+            "5.5",
+            "5.0",
+            "5.1",
+            "5.3",
+            "5.5",
+            "6.7",
+            "6.9",
+            "5.0",
+            "5.7",
+            "4.9",
+            "6.7",
+            "4.9",
+            "5.7",
+            "6.0",
+            "4.8",
+            "4.9",
+            "5.6",
+            "5.8",
+            "6.1",
+            "6.4",
+            "5.6",
+            "5.1",
+            "5.6",
+            "6.1",
+            "5.6",
+            "5.5",
+            "4.8",
+            "5.4",
+            "5.6",
+            "5.1",
+            "5.1",
+            "5.9",
+            "5.7",
+            "5.2",
+            "5.0",
+            "5.2",
+            "5.4",
+            "5.1"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "2.5",
+            "1.9",
+            "2.1",
+            "1.8",
+            "2.2",
+            "2.1",
+            "1.7",
+            "1.8",
+            "1.8",
+            "2.5",
+            "2.0",
+            "1.9",
+            "2.1",
+            "2.0",
+            "2.4",
+            "2.3",
+            "1.8",
+            "2.2",
+            "2.3",
+            "1.5",
+            "2.3",
+            "2.0",
+            "2.0",
+            "1.8",
+            "2.1",
+            "1.8",
+            "1.8",
+            "1.8",
+            "2.1",
+            "1.6",
+            "1.9",
+            "2.0",
+            "2.2",
+            "1.5",
+            "1.4",
+            "2.3",
+            "2.4",
+            "1.8",
+            "1.8",
+            "2.1",
+            "2.4",
+            "2.3",
+            "1.9",
+            "2.3",
+            "2.5",
+            "2.3",
+            "1.9",
+            "2.0",
+            "2.3",
+            "1.8"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "blue"
+      }
+    }
+  ],
+  "layout": {
+	"title": "Iris dataset splom",
+  	"width": 600,
+	"height": 500
+  }
+}
diff --git a/test/image/mocks/splom_ragged-via-axes.json b/test/image/mocks/splom_ragged-via-axes.json
new file mode 100644
index 00000000000..d305a6a801c
--- /dev/null
+++ b/test/image/mocks/splom_ragged-via-axes.json
@@ -0,0 +1,58 @@
+{
+  "data": [
+    {
+      "type": "splom",
+      "dimensions": [
+        {
+          "values": [
+            1,
+            2,
+            3
+          ],
+          "label": "A"
+        },
+        {
+          "values": [
+            4,
+            5,
+            6
+          ],
+          "label": "B"
+        }
+      ]
+    },
+    {
+      "type": "splom",
+      "dimensions": [
+        {
+          "values": [
+            1,
+            2,
+            3
+          ],
+          "label": "C"
+        },
+        {
+          "values": [
+            4,
+            5,
+            6
+          ],
+          "label": "D"
+        }
+      ],
+      "xaxes": [
+        "x3",
+        "x4"
+      ],
+      "yaxes": [
+        "y3",
+        "y4"
+      ]
+    }
+  ],
+  "layout": {
+    "title": "ragged sploms using trace xaxes yaxes",
+    "showlegend": false
+  }
+}
diff --git a/test/image/mocks/splom_ragged-via-visible-false.json b/test/image/mocks/splom_ragged-via-visible-false.json
new file mode 100644
index 00000000000..29c62e9a64c
--- /dev/null
+++ b/test/image/mocks/splom_ragged-via-visible-false.json
@@ -0,0 +1,56 @@
+{
+  "data": [
+    {
+      "type": "splom",
+      "dimensions": [
+        {
+          "values": [
+            1,
+            2,
+            3
+          ],
+          "label": "A"
+        },
+        {
+          "values": [
+            4,
+            5,
+            6
+          ],
+          "label": "B"
+        }
+      ]
+    },
+    {
+      "type": "splom",
+      "dimensions": [
+        {
+          "visible": false
+        },
+        {
+          "visible": false
+        },
+        {
+          "values": [
+            1,
+            2,
+            3
+          ],
+          "label": "C"
+        },
+        {
+          "values": [
+            4,
+            5,
+            6
+          ],
+          "label": "D"
+        }
+      ]
+    }
+  ],
+  "layout": {
+    "showlegend": false,
+    "title": "ragged sploms using visible false dimensions"
+  }
+}
diff --git a/test/image/mocks/splom_upper-nodiag.json b/test/image/mocks/splom_upper-nodiag.json
new file mode 100644
index 00000000000..4e04bea432e
--- /dev/null
+++ b/test/image/mocks/splom_upper-nodiag.json
@@ -0,0 +1,708 @@
+{
+  "data": [
+    {
+      "type": "splom",
+      "name": "Setosa",
+      "showlowerhalf": false,
+      "diagonal": {"visible": false},
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "5.1",
+            "4.9",
+            "4.7",
+            "4.6",
+            "5.0",
+            "5.4",
+            "4.6",
+            "5.0",
+            "4.4",
+            "4.9",
+            "5.4",
+            "4.8",
+            "4.8",
+            "4.3",
+            "5.8",
+            "5.7",
+            "5.4",
+            "5.1",
+            "5.7",
+            "5.1",
+            "5.4",
+            "5.1",
+            "4.6",
+            "5.1",
+            "4.8",
+            "5.0",
+            "5.0",
+            "5.2",
+            "5.2",
+            "4.7",
+            "4.8",
+            "5.4",
+            "5.2",
+            "5.5",
+            "4.9",
+            "5.0",
+            "5.5",
+            "4.9",
+            "4.4",
+            "5.1",
+            "5.0",
+            "4.5",
+            "4.4",
+            "5.0",
+            "5.1",
+            "4.8",
+            "5.1",
+            "4.6",
+            "5.3",
+            "5.0"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.5",
+            "3.0",
+            "3.2",
+            "3.1",
+            "3.6",
+            "3.9",
+            "3.4",
+            "3.4",
+            "2.9",
+            "3.1",
+            "3.7",
+            "3.4",
+            "3.0",
+            "3.0",
+            "4.0",
+            "4.4",
+            "3.9",
+            "3.5",
+            "3.8",
+            "3.8",
+            "3.4",
+            "3.7",
+            "3.6",
+            "3.3",
+            "3.4",
+            "3.0",
+            "3.4",
+            "3.5",
+            "3.4",
+            "3.2",
+            "3.1",
+            "3.4",
+            "4.1",
+            "4.2",
+            "3.1",
+            "3.2",
+            "3.5",
+            "3.1",
+            "3.0",
+            "3.4",
+            "3.5",
+            "2.3",
+            "3.2",
+            "3.5",
+            "3.8",
+            "3.0",
+            "3.8",
+            "3.2",
+            "3.7",
+            "3.3"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "1.4",
+            "1.4",
+            "1.3",
+            "1.5",
+            "1.4",
+            "1.7",
+            "1.4",
+            "1.5",
+            "1.4",
+            "1.5",
+            "1.5",
+            "1.6",
+            "1.4",
+            "1.1",
+            "1.2",
+            "1.5",
+            "1.3",
+            "1.4",
+            "1.7",
+            "1.5",
+            "1.7",
+            "1.5",
+            "1.0",
+            "1.7",
+            "1.9",
+            "1.6",
+            "1.6",
+            "1.5",
+            "1.4",
+            "1.6",
+            "1.6",
+            "1.5",
+            "1.5",
+            "1.4",
+            "1.5",
+            "1.2",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.3",
+            "1.3",
+            "1.6",
+            "1.9",
+            "1.4",
+            "1.6",
+            "1.4",
+            "1.5",
+            "1.4"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.3",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.1",
+            "0.2",
+            "0.4",
+            "0.4",
+            "0.3",
+            "0.3",
+            "0.3",
+            "0.2",
+            "0.4",
+            "0.2",
+            "0.5",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.1",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.3",
+            "0.3",
+            "0.2",
+            "0.6",
+            "0.4",
+            "0.3",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "red"
+      }
+    },
+    {
+      "type": "splom",
+      "name": "Versicolor",
+      "showlowerhalf": false,
+      "diagonal": {"visible": false},
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "7.0",
+            "6.4",
+            "6.9",
+            "5.5",
+            "6.5",
+            "5.7",
+            "6.3",
+            "4.9",
+            "6.6",
+            "5.2",
+            "5.0",
+            "5.9",
+            "6.0",
+            "6.1",
+            "5.6",
+            "6.7",
+            "5.6",
+            "5.8",
+            "6.2",
+            "5.6",
+            "5.9",
+            "6.1",
+            "6.3",
+            "6.1",
+            "6.4",
+            "6.6",
+            "6.8",
+            "6.7",
+            "6.0",
+            "5.7",
+            "5.5",
+            "5.5",
+            "5.8",
+            "6.0",
+            "5.4",
+            "6.0",
+            "6.7",
+            "6.3",
+            "5.6",
+            "5.5",
+            "5.5",
+            "6.1",
+            "5.8",
+            "5.0",
+            "5.6",
+            "5.7",
+            "5.7",
+            "6.2",
+            "5.1",
+            "5.7"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.2",
+            "3.2",
+            "3.1",
+            "2.3",
+            "2.8",
+            "2.8",
+            "3.3",
+            "2.4",
+            "2.9",
+            "2.7",
+            "2.0",
+            "3.0",
+            "2.2",
+            "2.9",
+            "2.9",
+            "3.1",
+            "3.0",
+            "2.7",
+            "2.2",
+            "2.5",
+            "3.2",
+            "2.8",
+            "2.5",
+            "2.8",
+            "2.9",
+            "3.0",
+            "2.8",
+            "3.0",
+            "2.9",
+            "2.6",
+            "2.4",
+            "2.4",
+            "2.7",
+            "2.7",
+            "3.0",
+            "3.4",
+            "3.1",
+            "2.3",
+            "3.0",
+            "2.5",
+            "2.6",
+            "3.0",
+            "2.6",
+            "2.3",
+            "2.7",
+            "3.0",
+            "2.9",
+            "2.9",
+            "2.5",
+            "2.8"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "4.7",
+            "4.5",
+            "4.9",
+            "4.0",
+            "4.6",
+            "4.5",
+            "4.7",
+            "3.3",
+            "4.6",
+            "3.9",
+            "3.5",
+            "4.2",
+            "4.0",
+            "4.7",
+            "3.6",
+            "4.4",
+            "4.5",
+            "4.1",
+            "4.5",
+            "3.9",
+            "4.8",
+            "4.0",
+            "4.9",
+            "4.7",
+            "4.3",
+            "4.4",
+            "4.8",
+            "5.0",
+            "4.5",
+            "3.5",
+            "3.8",
+            "3.7",
+            "3.9",
+            "5.1",
+            "4.5",
+            "4.5",
+            "4.7",
+            "4.4",
+            "4.1",
+            "4.0",
+            "4.4",
+            "4.6",
+            "4.0",
+            "3.3",
+            "4.2",
+            "4.2",
+            "4.2",
+            "4.3",
+            "3.0",
+            "4.1"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "1.4",
+            "1.5",
+            "1.5",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.6",
+            "1.0",
+            "1.3",
+            "1.4",
+            "1.0",
+            "1.5",
+            "1.0",
+            "1.4",
+            "1.3",
+            "1.4",
+            "1.5",
+            "1.0",
+            "1.5",
+            "1.1",
+            "1.8",
+            "1.3",
+            "1.5",
+            "1.2",
+            "1.3",
+            "1.4",
+            "1.4",
+            "1.7",
+            "1.5",
+            "1.0",
+            "1.1",
+            "1.0",
+            "1.2",
+            "1.6",
+            "1.5",
+            "1.6",
+            "1.5",
+            "1.3",
+            "1.3",
+            "1.3",
+            "1.2",
+            "1.4",
+            "1.2",
+            "1.0",
+            "1.3",
+            "1.2",
+            "1.3",
+            "1.3",
+            "1.1",
+            "1.3"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "green"
+      }
+    },
+    {
+      "type": "splom",
+      "name": "Virginica",
+      "showlowerhalf": false,
+      "diagonal": {"visible": false},
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "6.3",
+            "5.8",
+            "7.1",
+            "6.3",
+            "6.5",
+            "7.6",
+            "4.9",
+            "7.3",
+            "6.7",
+            "7.2",
+            "6.5",
+            "6.4",
+            "6.8",
+            "5.7",
+            "5.8",
+            "6.4",
+            "6.5",
+            "7.7",
+            "7.7",
+            "6.0",
+            "6.9",
+            "5.6",
+            "7.7",
+            "6.3",
+            "6.7",
+            "7.2",
+            "6.2",
+            "6.1",
+            "6.4",
+            "7.2",
+            "7.4",
+            "7.9",
+            "6.4",
+            "6.3",
+            "6.1",
+            "7.7",
+            "6.3",
+            "6.4",
+            "6.0",
+            "6.9",
+            "6.7",
+            "6.9",
+            "5.8",
+            "6.8",
+            "6.7",
+            "6.7",
+            "6.3",
+            "6.5",
+            "6.2",
+            "5.9"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.3",
+            "2.7",
+            "3.0",
+            "2.9",
+            "3.0",
+            "3.0",
+            "2.5",
+            "2.9",
+            "2.5",
+            "3.6",
+            "3.2",
+            "2.7",
+            "3.0",
+            "2.5",
+            "2.8",
+            "3.2",
+            "3.0",
+            "3.8",
+            "2.6",
+            "2.2",
+            "3.2",
+            "2.8",
+            "2.8",
+            "2.7",
+            "3.3",
+            "3.2",
+            "2.8",
+            "3.0",
+            "2.8",
+            "3.0",
+            "2.8",
+            "3.8",
+            "2.8",
+            "2.8",
+            "2.6",
+            "3.0",
+            "3.4",
+            "3.1",
+            "3.0",
+            "3.1",
+            "3.1",
+            "3.1",
+            "2.7",
+            "3.2",
+            "3.3",
+            "3.0",
+            "2.5",
+            "3.0",
+            "3.4",
+            "3.0"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "6.0",
+            "5.1",
+            "5.9",
+            "5.6",
+            "5.8",
+            "6.6",
+            "4.5",
+            "6.3",
+            "5.8",
+            "6.1",
+            "5.1",
+            "5.3",
+            "5.5",
+            "5.0",
+            "5.1",
+            "5.3",
+            "5.5",
+            "6.7",
+            "6.9",
+            "5.0",
+            "5.7",
+            "4.9",
+            "6.7",
+            "4.9",
+            "5.7",
+            "6.0",
+            "4.8",
+            "4.9",
+            "5.6",
+            "5.8",
+            "6.1",
+            "6.4",
+            "5.6",
+            "5.1",
+            "5.6",
+            "6.1",
+            "5.6",
+            "5.5",
+            "4.8",
+            "5.4",
+            "5.6",
+            "5.1",
+            "5.1",
+            "5.9",
+            "5.7",
+            "5.2",
+            "5.0",
+            "5.2",
+            "5.4",
+            "5.1"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "2.5",
+            "1.9",
+            "2.1",
+            "1.8",
+            "2.2",
+            "2.1",
+            "1.7",
+            "1.8",
+            "1.8",
+            "2.5",
+            "2.0",
+            "1.9",
+            "2.1",
+            "2.0",
+            "2.4",
+            "2.3",
+            "1.8",
+            "2.2",
+            "2.3",
+            "1.5",
+            "2.3",
+            "2.0",
+            "2.0",
+            "1.8",
+            "2.1",
+            "1.8",
+            "1.8",
+            "1.8",
+            "2.1",
+            "1.6",
+            "1.9",
+            "2.0",
+            "2.2",
+            "1.5",
+            "1.4",
+            "2.3",
+            "2.4",
+            "1.8",
+            "1.8",
+            "2.1",
+            "2.4",
+            "2.3",
+            "1.9",
+            "2.3",
+            "2.5",
+            "2.3",
+            "1.9",
+            "2.0",
+            "2.3",
+            "1.8"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "blue"
+      }
+    }
+  ],
+  "layout": {
+	"title": "Iris dataset splom",
+  	"width": 600,
+	"height": 500,
+    "legend": {
+      "x": 0,
+      "xanchor": "left",
+      "y": 0,
+      "yanchor": "bottom"
+    }
+  }
+}
diff --git a/test/image/mocks/splom_upper.json b/test/image/mocks/splom_upper.json
new file mode 100644
index 00000000000..ba30aa70a03
--- /dev/null
+++ b/test/image/mocks/splom_upper.json
@@ -0,0 +1,699 @@
+{
+  "data": [
+    {
+      "type": "splom",
+      "name": "Setosa",
+      "showlowerhalf": false,
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "5.1",
+            "4.9",
+            "4.7",
+            "4.6",
+            "5.0",
+            "5.4",
+            "4.6",
+            "5.0",
+            "4.4",
+            "4.9",
+            "5.4",
+            "4.8",
+            "4.8",
+            "4.3",
+            "5.8",
+            "5.7",
+            "5.4",
+            "5.1",
+            "5.7",
+            "5.1",
+            "5.4",
+            "5.1",
+            "4.6",
+            "5.1",
+            "4.8",
+            "5.0",
+            "5.0",
+            "5.2",
+            "5.2",
+            "4.7",
+            "4.8",
+            "5.4",
+            "5.2",
+            "5.5",
+            "4.9",
+            "5.0",
+            "5.5",
+            "4.9",
+            "4.4",
+            "5.1",
+            "5.0",
+            "4.5",
+            "4.4",
+            "5.0",
+            "5.1",
+            "4.8",
+            "5.1",
+            "4.6",
+            "5.3",
+            "5.0"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.5",
+            "3.0",
+            "3.2",
+            "3.1",
+            "3.6",
+            "3.9",
+            "3.4",
+            "3.4",
+            "2.9",
+            "3.1",
+            "3.7",
+            "3.4",
+            "3.0",
+            "3.0",
+            "4.0",
+            "4.4",
+            "3.9",
+            "3.5",
+            "3.8",
+            "3.8",
+            "3.4",
+            "3.7",
+            "3.6",
+            "3.3",
+            "3.4",
+            "3.0",
+            "3.4",
+            "3.5",
+            "3.4",
+            "3.2",
+            "3.1",
+            "3.4",
+            "4.1",
+            "4.2",
+            "3.1",
+            "3.2",
+            "3.5",
+            "3.1",
+            "3.0",
+            "3.4",
+            "3.5",
+            "2.3",
+            "3.2",
+            "3.5",
+            "3.8",
+            "3.0",
+            "3.8",
+            "3.2",
+            "3.7",
+            "3.3"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "1.4",
+            "1.4",
+            "1.3",
+            "1.5",
+            "1.4",
+            "1.7",
+            "1.4",
+            "1.5",
+            "1.4",
+            "1.5",
+            "1.5",
+            "1.6",
+            "1.4",
+            "1.1",
+            "1.2",
+            "1.5",
+            "1.3",
+            "1.4",
+            "1.7",
+            "1.5",
+            "1.7",
+            "1.5",
+            "1.0",
+            "1.7",
+            "1.9",
+            "1.6",
+            "1.6",
+            "1.5",
+            "1.4",
+            "1.6",
+            "1.6",
+            "1.5",
+            "1.5",
+            "1.4",
+            "1.5",
+            "1.2",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.3",
+            "1.3",
+            "1.6",
+            "1.9",
+            "1.4",
+            "1.6",
+            "1.4",
+            "1.5",
+            "1.4"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.3",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.1",
+            "0.2",
+            "0.4",
+            "0.4",
+            "0.3",
+            "0.3",
+            "0.3",
+            "0.2",
+            "0.4",
+            "0.2",
+            "0.5",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.4",
+            "0.1",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.1",
+            "0.2",
+            "0.2",
+            "0.3",
+            "0.3",
+            "0.2",
+            "0.6",
+            "0.4",
+            "0.3",
+            "0.2",
+            "0.2",
+            "0.2",
+            "0.2"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "red"
+      }
+    },
+    {
+      "type": "splom",
+      "name": "Versicolor",
+      "showlowerhalf": false,
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "7.0",
+            "6.4",
+            "6.9",
+            "5.5",
+            "6.5",
+            "5.7",
+            "6.3",
+            "4.9",
+            "6.6",
+            "5.2",
+            "5.0",
+            "5.9",
+            "6.0",
+            "6.1",
+            "5.6",
+            "6.7",
+            "5.6",
+            "5.8",
+            "6.2",
+            "5.6",
+            "5.9",
+            "6.1",
+            "6.3",
+            "6.1",
+            "6.4",
+            "6.6",
+            "6.8",
+            "6.7",
+            "6.0",
+            "5.7",
+            "5.5",
+            "5.5",
+            "5.8",
+            "6.0",
+            "5.4",
+            "6.0",
+            "6.7",
+            "6.3",
+            "5.6",
+            "5.5",
+            "5.5",
+            "6.1",
+            "5.8",
+            "5.0",
+            "5.6",
+            "5.7",
+            "5.7",
+            "6.2",
+            "5.1",
+            "5.7"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.2",
+            "3.2",
+            "3.1",
+            "2.3",
+            "2.8",
+            "2.8",
+            "3.3",
+            "2.4",
+            "2.9",
+            "2.7",
+            "2.0",
+            "3.0",
+            "2.2",
+            "2.9",
+            "2.9",
+            "3.1",
+            "3.0",
+            "2.7",
+            "2.2",
+            "2.5",
+            "3.2",
+            "2.8",
+            "2.5",
+            "2.8",
+            "2.9",
+            "3.0",
+            "2.8",
+            "3.0",
+            "2.9",
+            "2.6",
+            "2.4",
+            "2.4",
+            "2.7",
+            "2.7",
+            "3.0",
+            "3.4",
+            "3.1",
+            "2.3",
+            "3.0",
+            "2.5",
+            "2.6",
+            "3.0",
+            "2.6",
+            "2.3",
+            "2.7",
+            "3.0",
+            "2.9",
+            "2.9",
+            "2.5",
+            "2.8"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "4.7",
+            "4.5",
+            "4.9",
+            "4.0",
+            "4.6",
+            "4.5",
+            "4.7",
+            "3.3",
+            "4.6",
+            "3.9",
+            "3.5",
+            "4.2",
+            "4.0",
+            "4.7",
+            "3.6",
+            "4.4",
+            "4.5",
+            "4.1",
+            "4.5",
+            "3.9",
+            "4.8",
+            "4.0",
+            "4.9",
+            "4.7",
+            "4.3",
+            "4.4",
+            "4.8",
+            "5.0",
+            "4.5",
+            "3.5",
+            "3.8",
+            "3.7",
+            "3.9",
+            "5.1",
+            "4.5",
+            "4.5",
+            "4.7",
+            "4.4",
+            "4.1",
+            "4.0",
+            "4.4",
+            "4.6",
+            "4.0",
+            "3.3",
+            "4.2",
+            "4.2",
+            "4.2",
+            "4.3",
+            "3.0",
+            "4.1"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "1.4",
+            "1.5",
+            "1.5",
+            "1.3",
+            "1.5",
+            "1.3",
+            "1.6",
+            "1.0",
+            "1.3",
+            "1.4",
+            "1.0",
+            "1.5",
+            "1.0",
+            "1.4",
+            "1.3",
+            "1.4",
+            "1.5",
+            "1.0",
+            "1.5",
+            "1.1",
+            "1.8",
+            "1.3",
+            "1.5",
+            "1.2",
+            "1.3",
+            "1.4",
+            "1.4",
+            "1.7",
+            "1.5",
+            "1.0",
+            "1.1",
+            "1.0",
+            "1.2",
+            "1.6",
+            "1.5",
+            "1.6",
+            "1.5",
+            "1.3",
+            "1.3",
+            "1.3",
+            "1.2",
+            "1.4",
+            "1.2",
+            "1.0",
+            "1.3",
+            "1.2",
+            "1.3",
+            "1.3",
+            "1.1",
+            "1.3"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "green"
+      }
+    },
+    {
+      "type": "splom",
+      "name": "Virginica",
+      "showlowerhalf": false,
+      "dimensions": [
+        {
+          "label": "SepalLength",
+          "values": [
+            "6.3",
+            "5.8",
+            "7.1",
+            "6.3",
+            "6.5",
+            "7.6",
+            "4.9",
+            "7.3",
+            "6.7",
+            "7.2",
+            "6.5",
+            "6.4",
+            "6.8",
+            "5.7",
+            "5.8",
+            "6.4",
+            "6.5",
+            "7.7",
+            "7.7",
+            "6.0",
+            "6.9",
+            "5.6",
+            "7.7",
+            "6.3",
+            "6.7",
+            "7.2",
+            "6.2",
+            "6.1",
+            "6.4",
+            "7.2",
+            "7.4",
+            "7.9",
+            "6.4",
+            "6.3",
+            "6.1",
+            "7.7",
+            "6.3",
+            "6.4",
+            "6.0",
+            "6.9",
+            "6.7",
+            "6.9",
+            "5.8",
+            "6.8",
+            "6.7",
+            "6.7",
+            "6.3",
+            "6.5",
+            "6.2",
+            "5.9"
+          ]
+        },
+        {
+          "label": "SepalWidth",
+          "values": [
+            "3.3",
+            "2.7",
+            "3.0",
+            "2.9",
+            "3.0",
+            "3.0",
+            "2.5",
+            "2.9",
+            "2.5",
+            "3.6",
+            "3.2",
+            "2.7",
+            "3.0",
+            "2.5",
+            "2.8",
+            "3.2",
+            "3.0",
+            "3.8",
+            "2.6",
+            "2.2",
+            "3.2",
+            "2.8",
+            "2.8",
+            "2.7",
+            "3.3",
+            "3.2",
+            "2.8",
+            "3.0",
+            "2.8",
+            "3.0",
+            "2.8",
+            "3.8",
+            "2.8",
+            "2.8",
+            "2.6",
+            "3.0",
+            "3.4",
+            "3.1",
+            "3.0",
+            "3.1",
+            "3.1",
+            "3.1",
+            "2.7",
+            "3.2",
+            "3.3",
+            "3.0",
+            "2.5",
+            "3.0",
+            "3.4",
+            "3.0"
+          ]
+        },
+        {
+          "label": "PetalLength",
+          "values": [
+            "6.0",
+            "5.1",
+            "5.9",
+            "5.6",
+            "5.8",
+            "6.6",
+            "4.5",
+            "6.3",
+            "5.8",
+            "6.1",
+            "5.1",
+            "5.3",
+            "5.5",
+            "5.0",
+            "5.1",
+            "5.3",
+            "5.5",
+            "6.7",
+            "6.9",
+            "5.0",
+            "5.7",
+            "4.9",
+            "6.7",
+            "4.9",
+            "5.7",
+            "6.0",
+            "4.8",
+            "4.9",
+            "5.6",
+            "5.8",
+            "6.1",
+            "6.4",
+            "5.6",
+            "5.1",
+            "5.6",
+            "6.1",
+            "5.6",
+            "5.5",
+            "4.8",
+            "5.4",
+            "5.6",
+            "5.1",
+            "5.1",
+            "5.9",
+            "5.7",
+            "5.2",
+            "5.0",
+            "5.2",
+            "5.4",
+            "5.1"
+          ]
+        },
+        {
+          "label": "PetalWidth",
+          "values": [
+            "2.5",
+            "1.9",
+            "2.1",
+            "1.8",
+            "2.2",
+            "2.1",
+            "1.7",
+            "1.8",
+            "1.8",
+            "2.5",
+            "2.0",
+            "1.9",
+            "2.1",
+            "2.0",
+            "2.4",
+            "2.3",
+            "1.8",
+            "2.2",
+            "2.3",
+            "1.5",
+            "2.3",
+            "2.0",
+            "2.0",
+            "1.8",
+            "2.1",
+            "1.8",
+            "1.8",
+            "1.8",
+            "2.1",
+            "1.6",
+            "1.9",
+            "2.0",
+            "2.2",
+            "1.5",
+            "1.4",
+            "2.3",
+            "2.4",
+            "1.8",
+            "1.8",
+            "2.1",
+            "2.4",
+            "2.3",
+            "1.9",
+            "2.3",
+            "2.5",
+            "2.3",
+            "1.9",
+            "2.0",
+            "2.3",
+            "1.8"
+          ]
+        }
+      ],
+      "marker": {
+        "color": "blue"
+      }
+    }
+  ],
+  "layout": {
+	"title": "Iris dataset splom",
+  	"width": 600,
+	"height": 500
+  }
+}
diff --git a/test/image/mocks/splom_with-cartesian.json b/test/image/mocks/splom_with-cartesian.json
new file mode 100644
index 00000000000..4640328febe
--- /dev/null
+++ b/test/image/mocks/splom_with-cartesian.json
@@ -0,0 +1,45 @@
+{
+  "data": [{
+    "type": "scattergl",
+    "name": "scattergl",
+    "mode": "markers",
+    "text": "should be above splom",
+    "x": [2],
+    "y": [2]
+  }, {
+    "type": "splom",
+    "name": "splom",
+    "dimensions": [{
+      "values": [1, 2, 3],
+      "label": "A"
+    }, {
+      "values": [2, 5, 6],
+      "label": "B"
+    }]
+  }, {
+    "name": "scatter",
+    "y": [1, 2, 1]
+  }, {
+    "type": "box",
+    "name": "box",
+    "x0": 0,
+    "y": [1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 6],
+    "yaxis": "y2"
+  }, {
+    "type": "scattergl",
+    "name": "scattergl",
+    "y": [1, 2, 1],
+    "xaxis": "x2"
+  }],
+  "layout": {
+    "grid": {
+      "roworder": "bottom to top"
+    },
+    "legend": {
+      "x": 0,
+      "y": 1.1,
+      "yanchor": "bottom",
+      "orientation": "h"
+    }
+  }
+}
diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js
index bb9d6c3073b..d105bb5b4a3 100644
--- a/test/jasmine/tests/cartesian_interact_test.js
+++ b/test/jasmine/tests/cartesian_interact_test.js
@@ -530,6 +530,30 @@ describe('axis zoom/pan and main plot zoom', function() {
         .catch(failTest)
         .then(done);
     });
+
+    it('updates linked axes when there are constraints (axes_scaleanchor mock)', function(done) {
+        var fig = Lib.extendDeep({}, require('@mocks/axes_scaleanchor.json'));
+
+        function _assert(y3rng, y4rng) {
+            expect(gd._fullLayout.yaxis3.range).toBeCloseToArray(y3rng, 2, 'y3 rng');
+            expect(gd._fullLayout.yaxis4.range).toBeCloseToArray(y4rng, 2, 'y3 rng');
+        }
+
+        Plotly.plot(gd, fig)
+        .then(function() {
+            _assert([-0.36, 4.36], [-0.36, 4.36]);
+        })
+        .then(doDrag('x2y3', 'nsew', 0, 100))
+        .then(function() {
+            _assert([-0.36, 2], [0.82, 3.18]);
+        })
+        .then(doDrag('x2y4', 'nsew', 0, 50))
+        .then(function() {
+            _assert([0.41, 1.23], [1.18, 2]);
+        })
+        .catch(failTest)
+        .then(done);
+    });
 });
 
 describe('Event data:', function() {
diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js
index 293d482e63d..50b2172c9e9 100644
--- a/test/jasmine/tests/fx_test.js
+++ b/test/jasmine/tests/fx_test.js
@@ -201,13 +201,12 @@ describe('relayout', function() {
     afterEach(destroyGraphDiv);
 
     it('should update main drag with correct', function(done) {
-
         function assertMainDrag(cursor, isActive) {
             expect(d3.selectAll('rect.nsewdrag').size()).toEqual(1, 'number of nodes');
-            var mainDrag = d3.select('rect.nsewdrag'),
-                node = mainDrag.node();
+            var mainDrag = d3.select('rect.nsewdrag');
+            var node = mainDrag.node();
 
-            expect(mainDrag.classed('cursor-' + cursor)).toBe(true, 'cursor ' + cursor);
+            expect(window.getComputedStyle(node).cursor).toBe(cursor, 'cursor ' + cursor);
             expect(node.style.pointerEvents).toEqual('all', 'pointer event');
             expect(!!node.onmousedown).toBe(isActive, 'mousedown handler');
         }
diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js
index 96319a39fb2..c5177d04480 100644
--- a/test/jasmine/tests/plot_api_test.js
+++ b/test/jasmine/tests/plot_api_test.js
@@ -516,7 +516,10 @@ describe('Test plot api', function() {
             'layoutStyles',
             'doTicksRelayout',
             'doModeBar',
-            'doCamera'
+            'doCamera',
+            'doAutoRangeAndConstraints',
+            'drawData',
+            'finalDraw'
         ];
 
         var gd;
@@ -625,6 +628,46 @@ describe('Test plot api', function() {
                 expectReplot(attr);
             }
         });
+
+        it('should trigger minimal sequence for cartesian axis range updates', function() {
+            var seq = ['doAutoRangeAndConstraints', 'doTicksRelayout', 'drawData', 'finalDraw'];
+
+            function _assert(msg) {
+                expect(gd.calcdata).toBeDefined();
+                mockedMethods.forEach(function(m) {
+                    expect(subroutines[m].calls.count()).toBe(
+                        seq.indexOf(m) === -1 ? 0 : 1,
+                        '# of ' + m + ' calls - ' + msg
+                    );
+                });
+            }
+
+            var trace = {y: [1, 2, 1]};
+
+            var specs = [
+                ['relayout', ['xaxis.range[0]', 0]],
+                ['relayout', ['xaxis.range[1]', 3]],
+                ['relayout', ['xaxis.range', [-1, 5]]],
+                ['update', [{}, {'xaxis.range': [-1, 10]}]],
+                ['react', [[trace], {xaxis: {range: [0, 1]}}]]
+            ];
+
+            specs.forEach(function(s) {
+                // create 'real' div for Plotly.react to work
+                gd = createGraphDiv();
+                Plotly.plot(gd, [trace], {xaxis: {range: [1, 2]}});
+                mock(gd);
+
+                Plotly[s[0]](gd, s[1][0], s[1][1]);
+
+                _assert([
+                    'Plotly.', s[0],
+                    '(gd, ', JSON.stringify(s[1][0]), ', ', JSON.stringify(s[1][1]), ')'
+                ].join(''));
+
+                destroyGraphDiv();
+            });
+        });
     });
 
     describe('Plotly.restyle subroutines switchboard', function() {
@@ -2838,6 +2881,7 @@ describe('Test plot api', function() {
             ['range_selector_style', require('@mocks/range_selector_style.json')],
             ['range_slider_multiple', require('@mocks/range_slider_multiple.json')],
             ['sankey_energy', require('@mocks/sankey_energy.json')],
+            ['splom_iris', require('@mocks/splom_iris.json')],
             ['table_wrapped_birds', require('@mocks/table_wrapped_birds.json')],
             ['ternary_fill', require('@mocks/ternary_fill.json')],
             ['text_chart_arrays', require('@mocks/text_chart_arrays.json')],
diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js
index 753a9b95523..61b3ae29497 100644
--- a/test/jasmine/tests/plots_test.js
+++ b/test/jasmine/tests/plots_test.js
@@ -13,7 +13,6 @@ describe('Test Plots', function() {
     'use strict';
 
     describe('Plots.supplyDefaults', function() {
-
         it('should not throw an error when gd is a plain object', function() {
             var height = 100,
                 gd = {
@@ -154,6 +153,26 @@ describe('Test Plots', function() {
 
             testSanitizeMarginsHasBeenCalledOnlyOnce(gd);
         });
+
+        it('should sort base plot modules on fullLayout object', function() {
+            var gd = Lib.extendDeep({}, require('@mocks/plot_types.json'));
+            gd.data.unshift({type: 'scattergl'});
+            gd.data.push({type: 'splom'});
+
+            supplyAllDefaults(gd);
+            var names = gd._fullLayout._basePlotModules.map(function(m) {
+                return m.name;
+            });
+
+            expect(names).toEqual([
+                'splom',
+                'cartesian',
+                'gl3d',
+                'geo',
+                'pie',
+                'ternary'
+            ]);
+        });
     });
 
     describe('Plots.supplyLayoutGlobalDefaults should', function() {
diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js
index c071673f909..452ede89a77 100644
--- a/test/jasmine/tests/plotschema_test.js
+++ b/test/jasmine/tests/plotschema_test.js
@@ -349,6 +349,15 @@ describe('plot schema', function() {
         expect(scatterglSchema.error_y.copy_ystyle).toBeUndefined();
         expect(scatterglSchema.error_y.copy_zstyle).toBeUndefined();
     });
+
+    it('should convert regex valObject fields to strings', function() {
+        var splomAttrs = plotSchema.traces.splom.attributes;
+
+        expect(typeof splomAttrs.xaxes.items.regex).toBe('string');
+        expect(splomAttrs.xaxes.items.regex).toBe('/^x([2-9]|[1-9][0-9]+)?$/');
+        expect(typeof splomAttrs.yaxes.items.regex).toBe('string');
+        expect(splomAttrs.yaxes.items.regex).toBe('/^y([2-9]|[1-9][0-9]+)?$/');
+    });
 });
 
 describe('getTraceValObject', function() {
diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js
new file mode 100644
index 00000000000..357f5b943e2
--- /dev/null
+++ b/test/jasmine/tests/splom_test.js
@@ -0,0 +1,914 @@
+var Plotly = require('@lib');
+var Lib = require('@src/lib');
+var Plots = require('@src/plots/plots');
+var SUBPLOT_PATTERN = require('@src/plots/cartesian/constants').SUBPLOT_PATTERN;
+
+var d3 = require('d3');
+var supplyAllDefaults = require('../assets/supply_defaults');
+var createGraphDiv = require('../assets/create_graph_div');
+var destroyGraphDiv = require('../assets/destroy_graph_div');
+var failTest = require('../assets/fail_test');
+var mouseEvent = require('../assets/mouse_event');
+var drag = require('../assets/drag');
+
+var customAssertions = require('../assets/custom_assertions');
+var assertHoverLabelContent = customAssertions.assertHoverLabelContent;
+
+describe('Test splom trace defaults:', function() {
+    var gd;
+
+    function _supply(opts, layout) {
+        gd = {};
+        opts = Array.isArray(opts) ? opts : [opts];
+
+        gd.data = opts.map(function(o) {
+            return Lib.extendFlat({type: 'splom'}, o || {});
+        });
+        gd.layout = layout || {};
+
+        supplyAllDefaults(gd);
+    }
+
+    it('should set `visible: false` dimensions-less traces', function() {
+        _supply([{}, {dimensions: []}]);
+
+        expect(gd._fullData[0].visible).toBe(false);
+        expect(gd._fullData[1].visible).toBe(false);
+    });
+
+    it('should set `visible: false` to traces with showupperhalf, showlowerhalf, and diagonal.visible false', function() {
+        _supply({
+            dimensions: [{
+                values: [1, 2, 3]
+            }],
+            showupperhalf: false,
+            showlowerhalf: false,
+            diagonal: {visible: false}
+        });
+
+        expect(gd._fullData[0].visible).toBe(false);
+    });
+
+    it('should set `visible: false` to values-less dimensions', function() {
+        _supply({
+            dimensions: [
+                'not-an-object',
+                {other: 'stuff'}
+            ]
+        });
+
+        expect(gd._fullData[0].dimensions[0].visible).toBe(false);
+        expect(gd._fullData[0].dimensions[1].visible).toBe(false);
+    });
+
+    it('should work with only one dimensions', function() {
+        _supply({
+            dimensions: [
+                {values: [2, 1, 2]}
+            ]
+        });
+
+        var fullLayout = gd._fullLayout;
+        expect(fullLayout.xaxis.domain).toBeCloseToArray([0, 1]);
+        expect(fullLayout.yaxis.domain).toBeCloseToArray([0, 1]);
+    });
+
+    it('should set `grid.xaxes` and `grid.yaxes` default using the new of dimensions', function() {
+        _supply({
+            dimensions: [
+                {values: [1, 2, 3]},
+                {values: [2, 1, 2]}
+            ]
+        });
+
+        var fullTrace = gd._fullData[0];
+        expect(fullTrace._commonLength).toBe(3, 'common length');
+        expect(fullTrace.dimensions[0]._length).toBe(3, 'dim 0 length');
+        expect(fullTrace.dimensions[1]._length).toBe(3, 'dim 1 length');
+        expect(fullTrace.xaxes).toEqual(['x', 'x2']);
+        expect(fullTrace.yaxes).toEqual(['y', 'y2']);
+
+        var fullLayout = gd._fullLayout;
+        expect(fullLayout.xaxis.domain).toBeCloseToArray([0, 0.47]);
+        expect(fullLayout.yaxis.domain).toBeCloseToArray([0.53, 1]);
+        expect(fullLayout.xaxis2.domain).toBeCloseToArray([0.53, 1]);
+        expect(fullLayout.yaxis2.domain).toBeCloseToArray([0, 0.47]);
+
+        var subplots = fullLayout._subplots;
+        expect(subplots.xaxis).toEqual(['x', 'x2']);
+        expect(subplots.yaxis).toEqual(['y', 'y2']);
+        expect(subplots.cartesian).toEqual(['xy', 'xy2', 'x2y', 'x2y2']);
+    });
+
+    it('should use special `grid.xside` and `grid.yside` defaults on splom generated grids', function() {
+        var gridOut;
+
+        _supply({
+            dimensions: [
+                {values: [1, 2, 3]},
+                {values: [2, 1, 2]}
+            ]
+        });
+
+        gridOut = gd._fullLayout.grid;
+        expect(gridOut.xside).toBe('bottom');
+        expect(gridOut.yside).toBe('left');
+
+        _supply({
+            dimensions: [
+                {values: [1, 2, 3]},
+                {values: [2, 1, 2]}
+            ]
+        }, {
+            grid: {
+                xaxes: ['x', 'x2'],
+                yaxes: ['y', 'y2']
+            }
+        });
+
+        gridOut = gd._fullLayout.grid;
+        expect(gridOut.xside).toBe('bottom plot');
+        expect(gridOut.yside).toBe('left plot');
+    });
+
+    it('should honor `grid.xaxes` and `grid.yaxes` settings', function() {
+        _supply({
+            dimensions: [
+                {values: [1, 2, 3]},
+                {values: [2, 1, 2]}
+            ]
+        }, {
+            grid: {domain: {x: [0, 0.5], y: [0, 0.5]}}
+        });
+
+        var fullLayout = gd._fullLayout;
+        expect(fullLayout.xaxis.domain).toBeCloseToArray([0, 0.24]);
+        expect(fullLayout.yaxis.domain).toBeCloseToArray([0.26, 0.5]);
+        expect(fullLayout.xaxis2.domain).toBeCloseToArray([0.26, 0.5]);
+        expect(fullLayout.yaxis2.domain).toBeCloseToArray([0, 0.24]);
+    });
+
+    it('should honor xaxis and yaxis settings', function() {
+        _supply({
+            dimensions: [
+                {values: [1, 2, 3]},
+                {values: [2, 1, 2]}
+            ]
+        }, {
+            xaxis: {domain: [0, 0.4]},
+            yaxis2: {domain: [0, 0.3]}
+        });
+
+        var fullLayout = gd._fullLayout;
+        expect(fullLayout.xaxis.domain).toBeCloseToArray([0, 0.4]);
+        expect(fullLayout.yaxis.domain).toBeCloseToArray([0.53, 1]);
+        expect(fullLayout.xaxis2.domain).toBeCloseToArray([0.53, 1]);
+        expect(fullLayout.yaxis2.domain).toBeCloseToArray([0, 0.3]);
+    });
+
+    it('should set axis title default using dimensions *label*', function() {
+        _supply({
+            dimensions: [{
+                label: 'A',
+                values: [2, 3, 1]
+            }, {
+                label: 'B',
+                values: [1, 2, 1]
+            }]
+        });
+
+        var fullLayout = gd._fullLayout;
+        expect(fullLayout.xaxis.title).toBe('A');
+        expect(fullLayout.yaxis.title).toBe('A');
+        expect(fullLayout.xaxis2.title).toBe('B');
+        expect(fullLayout.yaxis2.title).toBe('B');
+    });
+
+    it('should set axis title default using dimensions *label* (even visible false dimensions)', function() {
+        _supply({
+            dimensions: [{
+                label: 'A',
+                values: [2, 3, 1]
+            }, {
+                label: 'B',
+                visible: false
+            }, {
+                label: 'C',
+                values: [1, 2, 1]
+            }]
+        });
+
+        var fullLayout = gd._fullLayout;
+        expect(fullLayout.xaxis.title).toBe('A');
+        expect(fullLayout.yaxis.title).toBe('A');
+        expect(fullLayout.xaxis2.title).toBe('B');
+        expect(fullLayout.yaxis2.title).toBe('B');
+        expect(fullLayout.xaxis3.title).toBe('C');
+        expect(fullLayout.yaxis3.title).toBe('C');
+    });
+
+    it('should ignore (x|y)axes values beyond dimensions length', function() {
+        _supply({
+            dimensions: [{
+                label: 'A',
+                values: [2, 3, 1]
+            }, {
+                label: 'B',
+                values: [0, 1, 0.5]
+            }, {
+                label: 'C',
+                values: [1, 2, 1]
+            }],
+            xaxes: ['x', 'x2', 'x3', 'x4'],
+            yaxes: ['y', 'y2', 'y3', 'y4']
+        });
+
+        var fullTrace = gd._fullData[0];
+        // keeps 1-to-1 relationship with input data
+        expect(fullTrace.xaxes).toEqual(['x', 'x2', 'x3', 'x4']);
+        expect(fullTrace.yaxes).toEqual(['y', 'y2', 'y3', 'y4']);
+
+        var fullLayout = gd._fullLayout;
+        // this here does the 'ignoring' part
+        expect(Object.keys(fullLayout._splomSubplots)).toEqual([
+            'xy', 'xy2', 'xy3',
+            'x2y', 'x2y2', 'x2y3',
+            'x3y', 'x3y2', 'x3y3'
+        ]);
+        expect(fullLayout.xaxis.title).toBe('A');
+        expect(fullLayout.yaxis.title).toBe('A');
+        expect(fullLayout.xaxis2.title).toBe('B');
+        expect(fullLayout.yaxis2.title).toBe('B');
+        expect(fullLayout.xaxis3.title).toBe('C');
+        expect(fullLayout.yaxis3.title).toBe('C');
+        expect(fullLayout.xaxis4).toBe(undefined);
+        expect(fullLayout.yaxis4).toBe(undefined);
+    });
+
+    it('should ignore (x|y)axes values beyond dimensions length (case 2)', function() {
+        _supply({
+            dimensions: [{
+                label: 'A',
+                values: [2, 3, 1]
+            }, {
+                label: 'B',
+                values: [0, 1, 0.5]
+            }, {
+                label: 'C',
+                values: [1, 2, 1]
+            }],
+            xaxes: ['x2', 'x3', 'x4', 'x5'],
+            yaxes: ['y2', 'y3', 'y4', 'y5']
+        });
+
+        var fullTrace = gd._fullData[0];
+        // keeps 1-to-1 relationship with input data
+        expect(fullTrace.xaxes).toEqual(['x2', 'x3', 'x4', 'x5']);
+        expect(fullTrace.yaxes).toEqual(['y2', 'y3', 'y4', 'y5']);
+
+        var fullLayout = gd._fullLayout;
+        // this here does the 'ignoring' part
+        expect(Object.keys(fullLayout._splomSubplots)).toEqual([
+            'x2y2', 'x2y3', 'x2y4',
+            'x3y2', 'x3y3', 'x3y4',
+            'x4y2', 'x4y3', 'x4y4'
+        ]);
+        expect(fullLayout.xaxis).toBe(undefined);
+        expect(fullLayout.yaxis).toBe(undefined);
+        expect(fullLayout.xaxis2.title).toBe('A');
+        expect(fullLayout.yaxis2.title).toBe('A');
+        expect(fullLayout.xaxis3.title).toBe('B');
+        expect(fullLayout.yaxis3.title).toBe('B');
+        expect(fullLayout.xaxis4.title).toBe('C');
+        expect(fullLayout.yaxis4.title).toBe('C');
+        expect(fullLayout.xaxis5).toBe(undefined);
+        expect(fullLayout.yaxis5).toBe(undefined);
+    });
+
+    it('should ignore dimensions beyond (x|y)axes length', function() {
+        _supply({
+            dimensions: [{
+                label: 'A',
+                values: [2, 3, 1]
+            }, {
+                label: 'B',
+                values: [0, 1, 0.5]
+            }, {
+                label: 'C',
+                values: [1, 2, 1]
+            }],
+            xaxes: ['x2', 'x3'],
+            yaxes: ['y2', 'y3']
+        });
+
+        var fullTrace = gd._fullData[0];
+        expect(fullTrace.xaxes).toEqual(['x2', 'x3']);
+        expect(fullTrace.yaxes).toEqual(['y2', 'y3']);
+        // keep 1-to-1 relationship with input data
+        expect(fullTrace.dimensions.length).toBe(3);
+
+        var fullLayout = gd._fullLayout;
+        // this here does the 'ignoring' part
+        expect(Object.keys(fullLayout._splomSubplots)).toEqual([
+            'x2y2', 'x2y3',
+            'x3y2', 'x3y3'
+        ]);
+    });
+
+    it('should lead to correct axis auto type value', function() {
+        _supply({
+            dimensions: [
+                {values: ['a', 'b', 'c']},
+                {values: ['A', 't', 'd']}
+            ]
+        });
+
+        var fullLayout = gd._fullLayout;
+        expect(fullLayout.xaxis.type).toBe('category');
+        expect(fullLayout.yaxis.type).toBe('category');
+    });
+
+    it('should lead to correct axis auto type value (case 2)', function() {
+        _supply({
+            dimensions: [
+                {visible: false, values: ['2018-01-01', '2018-02-01', '2018-03-03']},
+                {values: ['2018-01-01', '2018-02-01', '2018-03-03']}
+            ]
+        });
+
+        var fullLayout = gd._fullLayout;
+        expect(fullLayout.xaxis.type).toBe('date');
+        expect(fullLayout.yaxis.type).toBe('date');
+    });
+});
+
+describe('@gl Test splom interactions:', function() {
+    var gd;
+
+    beforeEach(function() {
+        gd = createGraphDiv();
+    });
+
+    afterEach(function() {
+        Plotly.purge(gd);
+        destroyGraphDiv();
+    });
+
+    it('should destroy gl objects on Plots.cleanPlot', function(done) {
+        var fig = Lib.extendDeep({}, require('@mocks/splom_large.json'));
+
+        Plotly.plot(gd, fig).then(function() {
+            expect(gd._fullLayout._splomGrid).toBeDefined();
+            expect(gd.calcdata[0][0].t._scene).toBeDefined();
+
+            return Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout, gd.calcdata);
+        })
+        .then(function() {
+            expect(gd._fullLayout._splomGrid).toBe(null);
+            expect(gd.calcdata[0][0].t._scene).toBe(null);
+        })
+        .catch(failTest)
+        .then(done);
+    });
+
+    it('when hasOnlyLargeSploms, should create correct regl-line2d data for grid', function(done) {
+        var fig = Lib.extendDeep({}, require('@mocks/splom_large.json'));
+        var cnt = 1;
+
+        function _assert(dims) {
+            var gridData = gd._fullLayout._splomGrid.passes;
+            var gridLengths = gridData.map(function(d) { return d.count * 2; });
+            var msg = ' - call #' + cnt;
+
+            expect(Object.keys(gridData).length)
+                .toBe(dims.length, '# of batches' + msg);
+            gridLengths.forEach(function(l, i) {
+                expect(l).toBe(dims[i], '# of coords in batch ' + i + msg);
+            });
+            cnt++;
+        }
+
+        Plotly.plot(gd, fig).then(function() {
+            _assert([1198, 3478, 16318, 118]);
+            return Plotly.restyle(gd, 'showupperhalf', false);
+        })
+        .then(function() {
+            _assert([1198, 1882, 8452, 4]);
+            return Plotly.restyle(gd, 'diagonal.visible', false);
+        })
+        .then(function() {
+            _assert([1138, 1702, 7636, 4]);
+            return Plotly.restyle(gd, {
+                showupperhalf: true,
+                showlowerhalf: false
+            });
+        })
+        .then(function() {
+            _assert([64, 1594, 7852, 112]);
+            return Plotly.restyle(gd, 'diagonal.visible', true);
+        })
+        .then(function() {
+            _assert([58, 1768, 8680, 118]);
+            return Plotly.relayout(gd, {
+                'xaxis.gridcolor': null,
+                'xaxis.gridwidth': null,
+                'yaxis.zerolinecolor': null,
+                'yaxis.zerolinewidth': null
+            });
+        })
+        .then(function() {
+            // one batch for all 'grid' lines
+            // and another for all 'zeroline' lines
+            _assert([8740, 1888]);
+        })
+        .catch(failTest)
+        .then(done);
+    });
+
+    it('should update properly in-and-out of hasOnlyLargeSploms regime', function(done) {
+        var figLarge = Lib.extendDeep({}, require('@mocks/splom_large.json'));
+        var dimsLarge = figLarge.data[0].dimensions;
+        var dimsSmall = dimsLarge.slice(0, 5);
+        var cnt = 1;
+
+        function _assert(exp) {
+            var msg = ' - call #' + cnt;
+            var subplots = d3.selectAll('g.cartesianlayer > g.subplot');
+
+            expect(subplots.size())
+                .toBe(exp.subplotCnt, '# of <g.subplot>' + msg);
+
+            var failedSubplots = [];
+            subplots.each(function(d, i) {
+                var actual = this.children.length;
+                var expected = typeof exp.innerSubplotNodeCnt === 'function' ?
+                    exp.innerSubplotNodeCnt(d, i) :
+                    exp.innerSubplotNodeCnt;
+                if(actual !== expected) {
+                    failedSubplots.push([d, actual, 'vs', expected].join(' '));
+                }
+            });
+            expect(failedSubplots)
+                .toEqual([], '# of nodes inside <g.subplot>' + msg);
+
+            expect(!!gd._fullLayout._splomGrid)
+                .toBe(exp.hasSplomGrid, 'has regl-line2d splom grid' + msg);
+
+            cnt++;
+        }
+
+        Plotly.plot(gd, figLarge).then(function() {
+            _assert({
+                subplotCnt: 400,
+                innerSubplotNodeCnt: 5,
+                hasSplomGrid: true
+            });
+            return Plotly.restyle(gd, 'dimensions', [dimsSmall]);
+        })
+        .then(function() {
+            _assert({
+                subplotCnt: 25,
+                innerSubplotNodeCnt: 17,
+                hasSplomGrid: false
+            });
+
+            // make sure 'new' subplot layers are in order
+            var gridIndex = -1;
+            var xaxisIndex = -1;
+            var subplot0 = d3.select('g.cartesianlayer > g.subplot').node();
+            for(var i in subplot0.children) {
+                var cl = subplot0.children[i].classList;
+                if(cl) {
+                    if(cl.contains('gridlayer')) gridIndex = +i;
+                    else if(cl.contains('xaxislayer-above')) xaxisIndex = +i;
+                }
+            }
+            // from large -> small splom:
+            // grid layer would be above xaxis layer,
+            // if we didn't clear subplot children.
+            expect(gridIndex).toBe(1, '<g.gridlayer> index');
+            expect(xaxisIndex).toBe(14, '<g.xaxislayer-above> index');
+
+            return Plotly.restyle(gd, 'dimensions', [dimsLarge]);
+        })
+        .then(function() {
+            _assert({
+                subplotCnt: 400,
+                // from small -> large splom:
+                // no need to clear subplots children in existing subplots,
+                // new subplots though have reduced number of children.
+                innerSubplotNodeCnt: function(d) {
+                    var p = d.match(SUBPLOT_PATTERN);
+                    return (p[1] > 5 || p[2] > 5) ? 5 : 17;
+                },
+                hasSplomGrid: true
+            });
+        })
+        .catch(failTest)
+        .then(done);
+    });
+
+    it('should correctly move axis layers when relayouting *grid.(x|y)side*', function(done) {
+        var fig = Lib.extendDeep({}, require('@mocks/splom_upper-nodiag.json'));
+
+        function _assert(exp) {
+            var g = d3.select(gd).select('g.cartesianlayer');
+            for(var k in exp) {
+                // all ticks are set to same position,
+                // only check first one
+                var tick0 = g.select('g.' + k + 'tick > text');
+                var pos = {x: 'y', y: 'x'}[k.charAt(0)];
+                expect(+tick0.attr(pos))
+                    .toBeWithin(exp[k], 1, pos + ' position for axis ' + k);
+            }
+        }
+
+        Plotly.plot(gd, fig).then(function() {
+            expect(gd._fullLayout.grid.xside).toBe('bottom', 'sanity check dflt grid.xside');
+            expect(gd._fullLayout.grid.yside).toBe('left', 'sanity check dflt grid.yside');
+
+            _assert({
+                x: 433, x2: 433, x3: 433,
+                y: 80, y2: 80, y3: 80
+            });
+            return Plotly.relayout(gd, 'grid.yside', 'left plot');
+        })
+        .then(function() {
+            _assert({
+                x: 433, x2: 433, x3: 433,
+                y: 79, y2: 230, y3: 382
+            });
+            return Plotly.relayout(gd, 'grid.xside', 'bottom plot');
+        })
+        .then(function() {
+            _assert({
+                x: 212, x2: 323, x3: 433,
+                y: 79, y2: 230, y3: 382
+            });
+        })
+        .catch(failTest)
+        .then(done);
+    });
+});
+
+describe('@gl Test splom hover:', function() {
+    var gd;
+
+    afterEach(function() {
+        Plotly.purge(gd);
+        destroyGraphDiv();
+    });
+
+    function run(s, done) {
+        gd = createGraphDiv();
+
+        var fig = Lib.extendDeep({},
+            s.mock || require('@mocks/splom_iris.json')
+        );
+
+        if(s.patch) {
+            fig = s.patch(fig);
+        }
+
+        var pos = s.pos || [200, 100];
+
+        return Plotly.plot(gd, fig).then(function() {
+            var to = setTimeout(function() {
+                failTest('no event data received');
+                done();
+            }, 100);
+
+            gd.on('plotly_hover', function(d) {
+                clearTimeout(to);
+                assertHoverLabelContent(s);
+
+                var msg = ' - event data ' + s.desc;
+                var actual = d.points || [];
+                var exp = s.evtPts;
+                expect(actual.length).toBe(exp.length, 'pt length' + msg);
+                for(var i = 0; i < exp.length; i++) {
+                    for(var k in exp[i]) {
+                        var m = 'key ' + k + ' in pt ' + i + msg;
+                        expect(actual[i][k]).toBe(exp[i][k], m);
+                    }
+                }
+
+                // w/o this purge gets called before
+                // hover throttle is complete
+                setTimeout(done, 0);
+            });
+
+            mouseEvent('mousemove', pos[0], pos[1]);
+        })
+        .catch(failTest);
+    }
+
+    var specs = [{
+        desc: 'basic',
+        nums: '7.7',
+        name: 'Virginica',
+        axis: '2.6',
+        evtPts: [{x: 2.6, y: 7.7, pointNumber: 18, curveNumber: 2}]
+    }, {
+        desc: 'hovermode closest',
+        patch: function(fig) {
+            fig.layout.hovermode = 'closest';
+            return fig;
+        },
+        nums: '(2.6, 7.7)',
+        name: 'Virginica',
+        evtPts: [{x: 2.6, y: 7.7, pointNumber: 18, curveNumber: 2}]
+    }, {
+        desc: 'skipping over visible false dims',
+        patch: function(fig) {
+            fig.data[0].dimensions[0].visible = false;
+            return fig;
+        },
+        nums: '7.7',
+        name: 'Virginica',
+        axis: '2.6',
+        evtPts: [{x: 2.6, y: 7.7, pointNumber: 18, curveNumber: 2}]
+    }, {
+        desc: 'on log axes',
+        mock: require('@mocks/splom_log.json'),
+        patch: function(fig) {
+            fig.layout.margin = {t: 0, l: 0, b: 0, r: 0};
+            fig.layout.width = 400;
+            fig.layout.height = 400;
+            return fig;
+        },
+        pos: [20, 380],
+        nums: '100',
+        axis: '10',
+        evtPts: [{x: 10, y: 100, pointNumber: 0}]
+    }, {
+        desc: 'on date axes',
+        mock: require('@mocks/splom_dates.json'),
+        patch: function(fig) {
+            fig.layout = {
+                margin: {t: 0, l: 0, b: 0, r: 0},
+                width: 400,
+                height: 400
+            };
+            return fig;
+        },
+        pos: [20, 380],
+        nums: 'Apr 2003',
+        axis: 'Jan 2000',
+        evtPts: [{x: '2000-01-01', y: '2003-04-21', pointNumber: 0}]
+    }];
+
+    specs.forEach(function(s) {
+        it('should generate correct hover labels ' + s.desc, function(done) {
+            run(s, done);
+        });
+    });
+});
+
+describe('@gl Test splom drag:', function() {
+    var gd;
+
+    beforeEach(function() {
+        gd = createGraphDiv();
+    });
+
+    afterEach(function() {
+        Plotly.purge(gd);
+        destroyGraphDiv();
+    });
+
+    function _drag(p0, p1) {
+        var node = d3.select('.nsewdrag[data-subplot="xy"]').node();
+        var dx = p1[0] - p0[0];
+        var dy = p1[1] - p0[1];
+        return drag(node, dx, dy, null, p0[0], p0[1]);
+    }
+
+    it('should update scattermatrix ranges on pan', function(done) {
+        var fig = require('@mocks/splom_iris.json');
+        fig.layout.dragmode = 'pan';
+
+        var xaxes = ['xaxis', 'xaxis2', 'xaxis3'];
+        var yaxes = ['yaxis', 'yaxis2', 'yaxis3'];
+
+        function _assertRanges(msg, xRanges, yRanges) {
+            xaxes.forEach(function(n, i) {
+                expect(gd._fullLayout[n].range)
+                    .toBeCloseToArray(xRanges[i], 1, n + ' range - ' + msg);
+            });
+            yaxes.forEach(function(n, i) {
+                expect(gd._fullLayout[n].range)
+                    .toBeCloseToArray(yRanges[i], 1, n + ' range - ' + msg);
+            });
+        }
+
+        Plotly.plot(gd, fig)
+        .then(function() {
+            var scene = gd.calcdata[0][0].t._scene;
+            spyOn(scene.matrix, 'update');
+            spyOn(scene.matrix, 'draw');
+
+            _assertRanges('before drag', [
+                [3.9, 8.3],
+                [1.7, 4.7],
+                [0.3, 7.6]
+            ], [
+                [3.8, 8.4],
+                [1.7, 4.7],
+                [0.3, 7.6]
+            ]);
+        })
+        .then(function() { return _drag([130, 130], [150, 150]); })
+        .then(function() {
+            var scene = gd.calcdata[0][0].t._scene;
+            // N.B. _drag triggers two updateSubplots call
+            // - 1 update and 1 draw call per updateSubplot
+            // - 2 update calls (1 for data, 1 for view opts)
+            //   during splom plot on mouseup
+            // - 1 draw call during splom plot on mouseup
+            expect(scene.matrix.update).toHaveBeenCalledTimes(4);
+            expect(scene.matrix.draw).toHaveBeenCalledTimes(3);
+
+            _assertRanges('after drag', [
+                [2.9, 7.3],
+                [1.7, 4.7],
+                [0.3, 7.6]
+            ], [
+                [5.1, 9.6],
+                [1.7, 4.7],
+                [0.3, 7.6]
+            ]);
+        })
+        .catch(failTest)
+        .then(done);
+    });
+});
+
+describe('@gl Test splom select:', function() {
+    var gd;
+    var ptData;
+    var subplot;
+
+    beforeEach(function() {
+        gd = createGraphDiv();
+    });
+
+    afterEach(function() {
+        Plotly.purge(gd);
+        destroyGraphDiv();
+    });
+
+    function _select(path, opts) {
+        return new Promise(function(resolve, reject) {
+            opts = opts || {};
+            ptData = null;
+            subplot = null;
+
+            var to = setTimeout(function() {
+                reject('fail: plotly_selected not emitter');
+            }, 200);
+
+            gd.once('plotly_selected', function(d) {
+                clearTimeout(to);
+                ptData = (d || {}).points;
+                subplot = Object.keys(d.range || {}).join('');
+                resolve();
+            });
+
+            Lib.clearThrottle();
+            mouseEvent('mousemove', path[0][0], path[0][1], opts);
+            mouseEvent('mousedown', path[0][0], path[0][1], opts);
+
+            var len = path.length;
+            path.slice(1, len).forEach(function(pt) {
+                Lib.clearThrottle();
+                mouseEvent('mousemove', pt[0], pt[1], opts);
+            });
+
+            mouseEvent('mouseup', path[len - 1][0], path[len - 1][1], opts);
+        });
+    }
+
+    it('should emit correct event data and draw selection outlines', function(done) {
+        var fig = require('@mocks/splom_0.json');
+        fig.layout = {
+            dragmode: 'select',
+            width: 400,
+            height: 400,
+            margin: {l: 0, t: 0, r: 0, b: 0},
+            grid: {xgap: 0, ygap: 0}
+        };
+
+        function _assert(_msg, ptExp, otherExp) {
+            var msg = ' - ' + _msg;
+
+            expect(ptData.length).toBe(ptExp.length, 'pt length' + msg);
+            for(var i = 0; i < ptExp.length; i++) {
+                for(var k in ptExp[i]) {
+                    var m = 'key ' + k + ' in pt ' + i + msg;
+                    expect(ptData[i][k]).toBe(ptExp[i][k], m);
+                }
+            }
+
+            expect(subplot).toBe(otherExp.subplot, 'subplot of selection' + msg);
+
+            expect(d3.selectAll('.zoomlayer > .select-outline').size())
+                .toBe(otherExp.selectionOutlineCnt, 'selection outline cnt' + msg);
+        }
+
+        Plotly.newPlot(gd, fig)
+        .then(function() { return _select([[5, 5], [195, 195]]); })
+        .then(function() {
+            _assert('first', [
+                {pointNumber: 0, x: 1, y: 1},
+                {pointNumber: 1, x: 2, y: 2},
+                {pointNumber: 2, x: 3, y: 3}
+            ], {
+                subplot: 'xy',
+                selectionOutlineCnt: 2
+            });
+        })
+        .then(function() { return _select([[50, 50], [100, 100]]); })
+        .then(function() {
+            _assert('second', [
+                {pointNumber: 1, x: 2, y: 2}
+            ], {
+                subplot: 'xy',
+                selectionOutlineCnt: 2
+            });
+        })
+        .then(function() { return _select([[5, 195], [100, 100]], {shiftKey: true}); })
+        .then(function() {
+            _assert('multi-select', [
+                {pointNumber: 0, x: 1, y: 1},
+                {pointNumber: 1, x: 2, y: 2}
+            ], {
+                subplot: 'xy',
+                // still '2' as the selection get merged
+                selectionOutlineCnt: 2
+            });
+        })
+        .then(function() { return _select([[205, 205], [395, 395]]); })
+        .then(function() {
+            _assert('across other subplot', [
+                {pointNumber: 0, x: 2, y: 2},
+                {pointNumber: 1, x: 5, y: 5},
+                {pointNumber: 2, x: 6, y: 6}
+            ], {
+                subplot: 'x2y2',
+                // outlines from previous subplot are cleared!
+                selectionOutlineCnt: 2
+            });
+        })
+        .then(function() { return _select([[50, 50], [100, 100]]); })
+        .then(function() {
+            _assert('multi-select across other subplot (prohibited for now)', [
+                {pointNumber: 1, x: 2, y: 2}
+            ], {
+                subplot: 'xy',
+                // outlines from previous subplot are cleared!
+                selectionOutlineCnt: 2
+            });
+        })
+        .catch(failTest)
+        .then(done);
+    });
+
+    it('should redraw splom traces before scattergl trace (if any)', function(done) {
+        var fig = require('@mocks/splom_with-cartesian.json');
+        fig.layout.dragmode = 'select';
+        fig.layout.width = 400;
+        fig.layout.height = 400;
+        fig.layout.margin = {l: 0, t: 0, r: 0, b: 0};
+        fig.layout.grid.xgap = 0;
+        fig.layout.grid.ygap = 0;
+
+        var cnt = 0;
+        var scatterGlCnt = 0;
+        var splomCnt = 0;
+
+        Plotly.newPlot(gd, fig).then(function() {
+            // 'scattergl' trace module
+            spyOn(gd._fullLayout._modules[0], 'style').and.callFake(function() {
+                cnt++;
+                scatterGlCnt = cnt;
+            });
+            // 'splom' trace module
+            spyOn(gd._fullLayout._modules[1], 'style').and.callFake(function() {
+                cnt++;
+                splomCnt = cnt;
+            });
+        })
+        .then(function() { return _select([[20, 395], [195, 205]]); })
+        .then(function() {
+            expect(gd._fullLayout._modules[0].style).toHaveBeenCalledTimes(1);
+            expect(gd._fullLayout._modules[1].style).toHaveBeenCalledTimes(1);
+
+            expect(cnt).toBe(2);
+            expect(splomCnt).toBe(1, 'splom redraw before scattergl');
+            expect(scatterGlCnt).toBe(2, 'scattergl redraw after splom');
+        })
+        .catch(failTest)
+        .then(done);
+    });
+});