From 02ed2eb3ae352c223aa066d8f0b62e183dbb2235 Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Thu, 5 Apr 2018 14:25:42 -0400
Subject: [PATCH 01/11] move doAutoRangeAndConstraints, drawData and finalDraw
 to subroutines

... (i.e. out of plot_api.js),
    and move Plots.addLinks out of drawData into its own
    step in Plotly.plot.
---
 src/plot_api/plot_api.js    | 97 ++-----------------------------------
 src/plot_api/subroutines.js | 92 +++++++++++++++++++++++++++++++++++
 2 files changed, 96 insertions(+), 93 deletions(-)

diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index 89f2a7b7d2d..5747a2e9acc 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -29,7 +29,6 @@ var Drawing = require('../components/drawing');
 var Color = require('../components/color');
 var xmlnsNamespaces = require('../constants/xmlns_namespaces');
 var svgTextUtils = require('../lib/svg_text_utils');
-var clearGlCanvases = require('../lib/clear_gl_canvases');
 
 var defaultConfig = require('./plot_config');
 var manageArrays = require('./manage_arrays');
@@ -38,10 +37,6 @@ 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 numericNameWarningCount = 0;
 var numericNameWarningCountLimit = 5;
@@ -331,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
@@ -351,83 +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();
-            }
-        }
-
-        // TODO does this break or slow down parcoords??
-        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);
-        }
-
-        // 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,
@@ -439,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
     );
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index fa74da9e967..2e043ca4fbe 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -11,7 +11,9 @@
 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');
@@ -21,6 +23,10 @@ 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);
@@ -480,3 +486,89 @@ 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();
+        }
+    }
+
+    // TODO does this break or slow down parcoords??
+    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);
+    }
+
+    // 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);
+
+    // 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);
+};

From 3ad1eaad7073011b769fdfb477a1fed597444252 Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Thu, 5 Apr 2018 15:41:18 -0400
Subject: [PATCH 02/11] add 'axrange' editType

- this represents the minimal sequence for '(x|y)axis.range'
  relayout calls which are pretty common (e.g. on zoom/pan
  mouseup).
- by bypassing drawFramework, lsInner and initInteraction,
  this can save ~1000ms on 50x50 subplot grids.
---
 src/plot_api/edit_types.js               |  3 +-
 src/plot_api/plot_api.js                 | 36 +++++++++++++++++++
 src/plots/cartesian/axes.js              |  2 +-
 src/plots/cartesian/layout_attributes.js |  6 ++--
 test/jasmine/tests/plot_api_test.js      | 45 +++++++++++++++++++++++-
 5 files changed, 86 insertions(+), 6 deletions(-)

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 5747a2e9acc..494bf32df45 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -1683,6 +1683,26 @@ 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?
+                subroutines.doAutoRangeAndConstraints,
+                // TODO
+                // can target specific axes,
+                // do not have to redraw all axes here
+                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);
@@ -2147,6 +2167,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);
@@ -2299,6 +2327,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);
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index 29414f278dd..c44dd3db8f9 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -2188,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/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/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js
index 1924111d1a0..6f38e70db41 100644
--- a/test/jasmine/tests/plot_api_test.js
+++ b/test/jasmine/tests/plot_api_test.js
@@ -504,7 +504,10 @@ describe('Test plot api', function() {
             'layoutStyles',
             'doTicksRelayout',
             'doModeBar',
-            'doCamera'
+            'doCamera',
+            'doAutoRangeAndConstraints',
+            'drawData',
+            'finalDraw'
         ];
 
         var gd;
@@ -613,6 +616,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() {

From bb022818e7fe6c2c808ad440a633558398853325 Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Thu, 5 Apr 2018 15:48:48 -0400
Subject: [PATCH 03/11] speed up doModeBar subroutines for cartesian subplots

- split minimal updateFx part out of initInteractions
- set maindrag cursor class (which depends only on layout.dragmode)
  on <g .draglayer> instead of inner <rect> to update it for
  all subplots in < 1ms (that's a > 600ms improvement on 50x50 grids)
- use gd._fullLayout instead of scoped fullLayout in initInteractions
  and makeDragBox to ensure correct reference after doModeBar()
---
 src/plot_api/plot_api.js              |  2 +-
 src/plot_api/subroutines.js           |  3 +--
 src/plots/cartesian/dragbox.js        | 21 ++++++++++++---------
 src/plots/cartesian/graph_interact.js | 26 ++++++++++++++++++--------
 src/plots/cartesian/index.js          |  2 ++
 test/jasmine/tests/fx_test.js         |  7 +++----
 6 files changed, 37 insertions(+), 24 deletions(-)

diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index 494bf32df45..a2906090d84 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');
 
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index 2e043ca4fbe..135dacae301 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -19,8 +19,8 @@ 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');
@@ -465,7 +465,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;
diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js
index f89468b128a..48412502c5c 100644
--- a/src/plots/cartesian/dragbox.js
+++ b/src/plots/cartesian/dragbox.js
@@ -55,7 +55,6 @@ 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;
@@ -111,6 +110,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
 
     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;
@@ -131,6 +131,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
@@ -204,7 +206,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,
@@ -354,7 +356,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
         // 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;
         }
 
@@ -456,8 +458,6 @@ 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);
@@ -584,9 +584,9 @@ 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() {
@@ -892,9 +892,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';
     }
diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js
index 7508b288ae7..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) {
@@ -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 16fed7c3f88..eca498b809b 100644
--- a/src/plots/cartesian/index.js
+++ b/src/plots/cartesian/index.js
@@ -548,3 +548,5 @@ exports.toSVG = function(gd) {
 
     canvases.each(canvasToImage);
 };
+
+exports.updateFx = require('./graph_interact').updateFx;
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');
         }

From d07ae70e8ca09103a14fade802b4e1b8c1975754 Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Thu, 5 Apr 2018 15:51:24 -0400
Subject: [PATCH 04/11] improve fullLayout._has()

- allow fullLayout._has(/*trace type*/) to work
- use registry category hash object (instead of categories.indexOf)
  to find categories in fullLayout._modules
---
 src/plots/plots.js | 20 +++++++++-----------
 1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/src/plots/plots.js b/src/plots/plots.js
index 3e193ed6d98..1a0bd47886a 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -627,24 +627,22 @@ 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;

From 468119eb9d2d4a82c3a0a5489d4521e208d2b206 Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Thu, 5 Apr 2018 15:59:28 -0400
Subject: [PATCH 05/11] speed up dragbox

- replace indexOf with hash objects lookups
- add 'svg' and 'draggedPts' trace module categories
- speed up updateSubplots (called on pan and scroll) by
  splitting it into splom, scattergl, svg and draggedPts blocks,
  (draggedPts is very slow, svg can be slow at 50x50)
- ... some scope variable clean up
---
 src/plot_api/subroutines.js            |   2 +-
 src/plots/cartesian/dragbox.js         | 438 ++++++++++++++-----------
 src/plots/cartesian/transition_axes.js |   2 +-
 src/traces/bar/index.js                |   2 +-
 src/traces/box/index.js                |   2 +-
 src/traces/candlestick/index.js        |   2 +-
 src/traces/carpet/index.js             |   2 +-
 src/traces/contour/index.js            |   2 +-
 src/traces/contourcarpet/index.js      |   2 +-
 src/traces/heatmap/index.js            |   2 +-
 src/traces/histogram/index.js          |   2 +-
 src/traces/histogram2d/index.js        |   2 +-
 src/traces/histogram2dcontour/index.js |   2 +-
 src/traces/ohlc/index.js               |   2 +-
 src/traces/scatter/index.js            |   2 +-
 src/traces/scattercarpet/index.js      |   2 +-
 src/traces/violin/index.js             |   2 +-
 17 files changed, 270 insertions(+), 200 deletions(-)

diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index 135dacae301..57fad95e180 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -179,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
         });
diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js
index 48412502c5c..15f13e11426 100644
--- a/src/plots/cartesian/dragbox.js
+++ b/src/plots/cartesian/dragbox.js
@@ -59,53 +59,63 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
     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();
@@ -113,8 +123,6 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
     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
@@ -153,8 +161,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) {
@@ -186,7 +194,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),
@@ -336,8 +344,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();
@@ -349,8 +361,7 @@ 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
@@ -407,20 +418,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);
@@ -459,8 +474,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
         }
 
         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;
@@ -500,12 +515,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;
@@ -516,17 +531,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;
@@ -534,7 +549,6 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
         }
 
         updateSubplots([x0, y0, pw - dx, ph - dy]);
-
         ticksAndAnnotations(yActive, xActive);
     }
 
@@ -550,13 +564,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 = {};
@@ -593,7 +607,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
         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;
@@ -632,12 +646,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++) {
@@ -675,122 +689,153 @@ 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;
         }
 
-        clearGlCanvases(gd);
-
-        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);
-                var yaRange = Lib.simpleMap(ya2.range, ya2.r2l);
-                subplot._scene.update(
-                    {range: [xaRange[0], yaRange[0], xaRange[1], yaRange[1]]}
-                );
+        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]]});
+                }
             }
+        }
 
-            if(editX2) {
-                xScaleFactor2 = xScaleFactor;
-                clipDx = ew ? viewBox[0] : getShift(xa2, xScaleFactor2);
-            }
-            else {
-                xScaleFactor2 = getLinkedScaleFactor(xa2);
-                clipDx = scaleAndGetShift(xa2, xScaleFactor2);
-            }
+        if(hasSVG) {
+            var xScaleFactor = viewBox[2] / xa0._length;
+            var yScaleFactor = viewBox[3] / ya0._length;
 
-            if(editY2) {
-                yScaleFactor2 = yScaleFactor;
-                clipDy = ns ? viewBox[1] : getShift(ya2, yScaleFactor2);
-            }
-            else {
-                yScaleFactor2 = getLinkedScaleFactor(ya2);
-                clipDy = scaleAndGetShift(ya2, yScaleFactor2);
-            }
+            for(i = 0; i < subplots.length; i++) {
+                sp = plotinfos[subplots[i]];
+                xa = sp.xaxis;
+                ya = sp.yaxis;
 
-            // don't scale at all if neither axis is scalable here
-            if(!xScaleFactor2 && !yScaleFactor2) {
-                continue;
-            }
+                var editX2 = editX && !xa.fixedrange && xaHash[xa._id];
+                var editY2 = editY && !ya.fixedrange && yaHash[ya._id];
 
-            // but if only one is, reset the other axis scaling
-            if(!xScaleFactor2) xScaleFactor2 = 1;
-            if(!yScaleFactor2) yScaleFactor2 = 1;
+                var xScaleFactor2, yScaleFactor2;
+                var clipDx, clipDy;
 
-            var plotDx = xa2._offset - clipDx / xScaleFactor2,
-                plotDy = ya2._offset - clipDy / yScaleFactor2;
+                if(editX2) {
+                    xScaleFactor2 = xScaleFactor;
+                    clipDx = ew ? viewBox[0] : getShift(xa, xScaleFactor2);
+                } else {
+                    xScaleFactor2 = getLinkedScaleFactor(xa, xScaleFactor, yScaleFactor);
+                    clipDx = scaleAndGetShift(xa, xScaleFactor2);
+                }
 
-            fullLayout._defs.select('#' + subplot.clipId + '> rect')
-                .call(Drawing.setTranslate, clipDx, clipDy)
-                .call(Drawing.setScale, xScaleFactor2, yScaleFactor2);
+                if(editY2) {
+                    yScaleFactor2 = yScaleFactor;
+                    clipDy = ns ? viewBox[1] : getShift(ya, yScaleFactor2);
+                } else {
+                    yScaleFactor2 = getLinkedScaleFactor(ya, xScaleFactor, yScaleFactor);
+                    clipDy = scaleAndGetShift(ya, yScaleFactor2);
+                }
 
-            var traceGroups = subplot.plot
-                .selectAll('.scatterlayer .trace, .boxlayer .trace, .violinlayer .trace');
+                // don't scale at all if neither axis is scalable here
+                if(!xScaleFactor2 && !yScaleFactor2) {
+                    continue;
+                }
 
-            subplot.plot
-                .call(Drawing.setTranslate, plotDx, plotDy)
-                .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2);
+                // 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.
+                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');
+                }
+            }
+        }
+    }
 
-            // 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);
+    // 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;
 
-            subplot.plot.selectAll('.barlayer .trace')
-                .call(Drawing.hideOutsideRangePoints, subplot, '.bartext');
+        if(editX && links.xaHash[ax._id]) {
+            return xScaleFactor;
         }
+        if(editY && (isSubplotConstrained ? links.xaHash : links.yaHash)[ax._id]) {
+            return yScaleFactor;
+        }
+        return 0;
+    }
 
-        if(Registry.subplotsRegistry.splom) {
-            Registry.subplotsRegistry.splom.drag(gd);
+    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;
@@ -1000,40 +1045,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;
                     }
                 }
@@ -1048,10 +1093,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, xLinkID);
+        yaxesLinked.push(ya);
+        yaHashLinked[ya._id] = ya;
+    }
+
     return {
-        x: xLinks,
-        y: yLinks,
-        xy: isSubplotConstrained
+        xaHash: xaHashLinked,
+        yaHash: yaHashLinked,
+        xaxes: xaxesLinked,
+        yaxes: yaxesLinked,
+        isSubplotConstrained: isSubplotConstrained
     };
 }
 
@@ -1073,6 +1137,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,
 
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/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 e97fc2c1789..0a4c6a36b3f 100644
--- a/src/traces/carpet/index.js
+++ b/src/traces/carpet/index.js
@@ -20,7 +20,7 @@ Carpet.animatable = true;
 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/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/scattercarpet/index.js b/src/traces/scattercarpet/index.js
index 689f4cedf5f..6427a4e75a0 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'];
 ScatterCarpet.meta = {
     hrName: 'scatter_carpet',
     description: [
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,',

From 0979272d0c671efee26fae151865bbc3c5487629 Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Thu, 5 Apr 2018 16:00:45 -0400
Subject: [PATCH 06/11] clean splom drag logic

---
 src/traces/splom/base_plot.js | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js
index 1fb81e8fef3..9e051212bf1 100644
--- a/src/traces/splom/base_plot.js
+++ b/src/traces/splom/base_plot.js
@@ -42,14 +42,9 @@ function drag(gd) {
         var trace = cd0.trace;
         var scene = cd0.t._scene;
 
-        // FIXME: this probably should not be called for non-splom traces
-        if(!scene || !scene.matrixOptions) continue;
-
-        var opts = scene.matrixOptions;
-
         if(trace.type === 'splom' && scene && scene.matrix) {
             var activeLength = trace._activeLength;
-            var visibleLength = opts.data.length;
+            var visibleLength = scene.matrixOptions.data.length;
             var ranges = new Array(visibleLength);
             var k = 0;
 
@@ -67,7 +62,7 @@ function drag(gd) {
     }
 
     if(fullLayout._hasOnlyLargeSploms) {
-        fullLayout._modules[0].basePlotModule.drawGrid(gd);
+        drawGrid(gd);
     }
 }
 
@@ -228,7 +223,7 @@ module.exports = {
     drawFramework: Cartesian.drawFramework,
     plot: plot,
     drag: drag,
-    drawGrid: drawGrid,
     clean: clean,
+    updateFx: Cartesian.updateFx,
     toSVG: Cartesian.toSVG
 };

From ebb35ce657b484af2dfaddc92018bb1029d8fb37 Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Thu, 5 Apr 2018 16:00:57 -0400
Subject: [PATCH 07/11] lint in plot_api

---
 src/plot_api/plot_api.js | 25 +++++++++++++------------
 1 file changed, 13 insertions(+), 12 deletions(-)

diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index a2906090d84..624870d7a43 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -36,7 +36,7 @@ var helpers = require('./helpers');
 var subroutines = require('./subroutines');
 var editTypes = require('./edit_types');
 
-var cartesianConstants = require('../plots/cartesian/constants');
+var AX_NAME_PATTERN = require('../plots/cartesian/constants').AX_NAME_PATTERN;
 
 var numericNameWarningCount = 0;
 var numericNameWarningCountLimit = 5;
@@ -1296,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;
@@ -1661,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;
@@ -1923,7 +1923,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;
 
@@ -1976,8 +1976,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] = {};
@@ -2128,11 +2129,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;

From f9090b7ab6ea4d07cba73be2468179fd7428ac26 Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Fri, 6 Apr 2018 11:13:06 -0400
Subject: [PATCH 08/11] (fixup) add draggedPts to scattercarpet categories

---
 src/traces/scattercarpet/index.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/traces/scattercarpet/index.js b/src/traces/scattercarpet/index.js
index 6427a4e75a0..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 = ['svg', 'carpet', 'symbols', 'markerColorscale', 'showLegend', 'carpetDependent'];
+ScatterCarpet.categories = ['svg', 'carpet', 'symbols', 'markerColorscale', 'showLegend', 'carpetDependent', 'draggedPts'];
 ScatterCarpet.meta = {
     hrName: 'scatter_carpet',
     description: [

From d3fe40dab5708bf94f83b472986726e8f8ff65a9 Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Tue, 10 Apr 2018 14:14:05 -0400
Subject: [PATCH 09/11] replace selectAll with for(k in _plots) loop

---
 src/components/shapes/draw.js | 6 +++++-
 src/plot_api/subroutines.js   | 4 ----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js
index 271e37c4861..7e1bb7e1305 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/plot_api/subroutines.js b/src/plot_api/subroutines.js
index 57fad95e180..1be49a86e2a 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -529,10 +529,6 @@ exports.drawData = function(gd) {
         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);
 

From cc1b3dec723cd0ac7401fb7deca681838dc2964e Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Tue, 10 Apr 2018 15:51:55 -0400
Subject: [PATCH 10/11] fixup and :lock: dragbox with linked y axes

---
 src/plots/cartesian/dragbox.js                |  2 +-
 test/jasmine/tests/cartesian_interact_test.js | 24 +++++++++++++++++++
 2 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js
index 15f13e11426..b7299c93b4c 100644
--- a/src/plots/cartesian/dragbox.js
+++ b/src/plots/cartesian/dragbox.js
@@ -1105,7 +1105,7 @@ function calcLinks(gd, xaHash, yaHash) {
     var yaHashLinked = {};
     var yaxesLinked = [];
     for(yLinkID in yLinks) {
-        var ya = getFromId(gd, xLinkID);
+        var ya = getFromId(gd, yLinkID);
         yaxesLinked.push(ya);
         yaHashLinked[ya._id] = ya;
     }
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() {

From f7d637d6698e8e040369e89ae9d23841fbcca289 Mon Sep 17 00:00:00 2001
From: etienne <etienne@plot.ly>
Date: Wed, 11 Apr 2018 10:55:13 -0400
Subject: [PATCH 11/11] :books: link TODOs to gh issues

---
 src/plot_api/plot_api.js       | 4 ++++
 src/plot_api/subroutines.js    | 1 -
 src/plots/cartesian/dragbox.js | 3 ++-
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index 624870d7a43..21ae2ea2cb5 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -1693,10 +1693,14 @@ exports.relayout = function relayout(gd, astr, val) {
                 // 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
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index 3cdabcb9b64..7256c48ca2f 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -525,7 +525,6 @@ exports.drawData = function(gd) {
         }
     }
 
-    // TODO does this break or slow down parcoords??
     clearGlCanvases(gd);
 
     // loop over the base plot modules present on graph
diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js
index b7299c93b4c..1f2bbd9a5cd 100644
--- a/src/plots/cartesian/dragbox.js
+++ b/src/plots/cartesian/dragbox.js
@@ -788,7 +788,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
                     .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2);
 
                 // TODO move these selectAll calls out of here
-                // and stash them somewhere nice.
+                // 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');