diff --git a/BUILDING.md b/BUILDING.md
index 816ecb20daa..c2aea8a659e 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -40,13 +40,6 @@ then simply run,
 browserify index.js > bundle.js
 ```
 
-to trim meta information (and thus save a few bytes), run:
-
-
-```
-browserify -t path/to/plotly.js/tasks/util/compress_attributes.js index.js > bundle.js
-```
-
 ## Angular CLI
 
 Currently Angular CLI uses Webpack under the hood to bundle and build your Angular application.
diff --git a/package-lock.json b/package-lock.json
index 8b17ba63fc3..a17da6327a5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,29 +35,6 @@
         "commander": "^2.15.1"
       }
     },
-    "@etpinard/gl-text": {
-      "version": "1.1.6",
-      "resolved": "https://registry.npmjs.org/@etpinard/gl-text/-/gl-text-1.1.6.tgz",
-      "integrity": "sha512-sN007FwlqdSKJt2/cnGZu3jsAN7G4R/wxk/D6ZivPuQtrwJ42B68iuAqysJPgFepUTAsDRtGAOd1U7tZxfDJwA==",
-      "requires": {
-        "color-normalize": "^1.1.0",
-        "css-font": "^1.2.0",
-        "detect-kerning": "^2.1.2",
-        "es6-weak-map": "^2.0.2",
-        "flatten-vertex-data": "^1.0.2",
-        "font-atlas": "^2.1.0",
-        "font-measure": "^1.2.2",
-        "gl-util": "^3.0.7",
-        "is-plain-obj": "^1.1.0",
-        "object-assign": "^4.1.1",
-        "parse-rect": "^1.2.0",
-        "parse-unit": "^1.0.1",
-        "pick-by-alias": "^1.2.0",
-        "regl": "^1.3.6",
-        "to-px": "^1.0.1",
-        "typedarray-pool": "^1.1.0"
-      }
-    },
     "@mapbox/geojson-area": {
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz",
@@ -4889,6 +4866,30 @@
         "typedarray-pool": "^1.0.0"
       }
     },
+    "gl-text": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/gl-text/-/gl-text-1.1.6.tgz",
+      "integrity": "sha512-OB+Nc5JKO1gyYYqBOJrYvCvRXIecfVpIKP7AviQNY63jrWPM9hUFSwZG7sH/paVnR1yCZBVirqOPfiFeF1Qo4g==",
+      "requires": {
+        "bit-twiddle": "^1.0.2",
+        "color-normalize": "^1.1.0",
+        "css-font": "^1.2.0",
+        "detect-kerning": "^2.1.2",
+        "es6-weak-map": "^2.0.2",
+        "flatten-vertex-data": "^1.0.2",
+        "font-atlas": "^2.1.0",
+        "font-measure": "^1.2.2",
+        "gl-util": "^3.0.7",
+        "is-plain-obj": "^1.1.0",
+        "object-assign": "^4.1.1",
+        "parse-rect": "^1.2.0",
+        "parse-unit": "^1.0.1",
+        "pick-by-alias": "^1.2.0",
+        "regl": "^1.3.6",
+        "to-px": "^1.0.1",
+        "typedarray-pool": "^1.1.0"
+      }
+    },
     "gl-texture2d": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/gl-texture2d/-/gl-texture2d-2.1.0.tgz",
@@ -5171,9 +5172,9 @@
       }
     },
     "glslify": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/glslify/-/glslify-6.3.0.tgz",
-      "integrity": "sha512-9VWypdkvL907Jn37QcCZXIHpLbmqs+fjnmjNszSFc+5ztmsGFzcknjCgeF887+xxfx32oGrgN7xfkSwa1D0khA==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/glslify/-/glslify-6.3.1.tgz",
+      "integrity": "sha512-3Hy85N8NmpDprwAxZaOC9k+DBXEwblVZ+yHIyt1QYg5dIrYaiGorz2WWBRxdUzapjDsxdhQ1ad9GSlIebxeBmA==",
       "requires": {
         "bl": "^1.0.0",
         "concat-stream": "^1.5.2",
@@ -8860,9 +8861,9 @@
       }
     },
     "regl-line2d": {
-      "version": "3.0.9",
-      "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.0.9.tgz",
-      "integrity": "sha512-D3ASXgofHVcdxi6qfQRJ7YsAVHkK0i7rkKx9qwDLYoZ96eRyyFMDb1zA3ulrmarPnb/U2G7EfsYQDU3V96EP4Q==",
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.0.11.tgz",
+      "integrity": "sha512-nf0Ftpf6boR0oJ24Gs77J8pQE0wet59T1TkrK1f0TWKJgWgRXByxRHDD92m/KZ2dpl+XTvCORk2NRqitSJGwWw==",
       "requires": {
         "array-bounds": "^1.0.0",
         "array-normalize": "^1.1.3",
@@ -8871,7 +8872,7 @@
         "earcut": "^2.1.1",
         "es6-weak-map": "^2.0.2",
         "flatten-vertex-data": "^1.0.0",
-        "glslify": "^6.1.0",
+        "glslify": "^6.3.1",
         "object-assign": "^4.1.1",
         "parse-rect": "^1.2.0",
         "pick-by-alias": "^1.1.0",
@@ -8901,9 +8902,9 @@
       }
     },
     "regl-splom": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/regl-splom/-/regl-splom-1.0.3.tgz",
-      "integrity": "sha512-3oJT26xm91p303Jb3jMI7PptHYMSbR2/ZnTLolYGnC42jVp/e+xbbik1pTNFyeS5WiaE0M+Ssl3tUC6zgQ8nOw==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/regl-splom/-/regl-splom-1.0.4.tgz",
+      "integrity": "sha512-+iq/RJAJdHCp48wPbEGQ5qw29OXFVF/m7CzcuLZxwptjdkB/FHGKiMuyqclOSNQcEKFxQTvRRJMJJ6brd8VvrA==",
       "requires": {
         "array-bounds": "^1.0.1",
         "array-range": "^1.0.1",
@@ -8916,7 +8917,7 @@
         "pick-by-alias": "^1.2.0",
         "point-cluster": "^1.0.2",
         "raf": "^3.4.0",
-        "regl-scatter2d": "^3.0.0"
+        "regl-scatter2d": "^3.0.6"
       },
       "dependencies": {
         "binary-search-bounds": {
diff --git a/package.json b/package.json
index e0f42fb32e0..0219f2bc703 100644
--- a/package.json
+++ b/package.json
@@ -51,12 +51,12 @@
   },
   "browserify": {
     "transform": [
-      "glslify"
+      "glslify",
+      "./tasks/compress_attributes.js"
     ]
   },
   "dependencies": {
     "3d-view": "^2.0.0",
-    "@etpinard/gl-text": "^1.1.6",
     "@plotly/d3-sankey": "^0.5.0",
     "alpha-shape": "^1.0.0",
     "array-range": "^1.0.1",
@@ -85,7 +85,8 @@
     "gl-spikes2d": "^1.0.1",
     "gl-streamtube3d": "^1.0.0",
     "gl-surface3d": "^1.3.5",
-    "glslify": "^6.2.1",
+    "gl-text": "^1.1.6",
+    "glslify": "^6.3.1",
     "has-hover": "^1.0.1",
     "has-passive-events": "^1.0.0",
     "mapbox-gl": "0.45.0",
@@ -101,9 +102,9 @@
     "polybooljs": "^1.2.0",
     "regl": "^1.3.7",
     "regl-error2d": "^2.0.5",
-    "regl-line2d": "^3.0.9",
+    "regl-line2d": "^3.0.11",
     "regl-scatter2d": "^3.0.6",
-    "regl-splom": "^1.0.3",
+    "regl-splom": "^1.0.4",
     "right-now": "^1.0.0",
     "robust-orientation": "^1.1.3",
     "sane-topojson": "^2.0.0",
diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js
index 4c35c2b4086..2429a5541f5 100644
--- a/src/components/fx/layout_attributes.js
+++ b/src/components/fx/layout_attributes.js
@@ -18,6 +18,29 @@ fontAttrs.family.dflt = constants.HOVERFONT;
 fontAttrs.size.dflt = constants.HOVERFONTSIZE;
 
 module.exports = {
+    clickmode: {
+        valType: 'flaglist',
+        role: 'info',
+        flags: ['event', 'select'],
+        dflt: 'event',
+        editType: 'plot',
+        extras: ['none'],
+        description: [
+            'Determines the mode of single click interactions.',
+            '*event* is the default value and emits the `plotly_click`',
+            'event. In addition this mode emits the `plotly_selected` event',
+            'in drag modes *lasso* and *select*, but with no event data attached',
+            '(kept for compatibility reasons).',
+            'The *select* flag enables selecting single',
+            'data points via click. This mode also supports persistent selections,',
+            'meaning that pressing Shift while clicking, adds to / subtracts from an',
+            'existing selection. *select* with `hovermode`: *x* can be confusing, consider',
+            'explicitly setting `hovermode`: *closest* when using this feature.',
+            'Selection events are sent accordingly as long as *event* flag is set as well.',
+            'When the *event* flag is missing, `plotly_click` and `plotly_selected`',
+            'events are not fired.'
+        ].join(' ')
+    },
     dragmode: {
         valType: 'enumerated',
         role: 'info',
@@ -36,7 +59,16 @@ module.exports = {
         role: 'info',
         values: ['x', 'y', 'closest', false],
         editType: 'modebar',
-        description: 'Determines the mode of hover interactions.'
+        description: [
+            'Determines the mode of hover interactions.',
+            'If `clickmode` includes the *select* flag,',
+            '`hovermode` defaults to *closest*.',
+            'If `clickmode` lacks the *select* flag,',
+            'it defaults to *x* or *y* (depending on the trace\'s',
+            '`orientation` value) for plots based on',
+            'cartesian coordinates. For anything else the default',
+            'value is *closest*.',
+        ].join(' ')
     },
     hoverdistance: {
         valType: 'integer',
diff --git a/src/components/fx/layout_defaults.js b/src/components/fx/layout_defaults.js
index 742c6eb1621..5f7372d6edf 100644
--- a/src/components/fx/layout_defaults.js
+++ b/src/components/fx/layout_defaults.js
@@ -16,15 +16,21 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
         return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
     }
 
+    var clickmode = coerce('clickmode');
+
     var dragMode = coerce('dragmode');
     if(dragMode === 'select') coerce('selectdirection');
 
     var hovermodeDflt;
     if(layoutOut._has('cartesian')) {
-        // flag for 'horizontal' plots:
-        // determines the state of the mode bar 'compare' hovermode button
-        layoutOut._isHoriz = isHoriz(fullData);
-        hovermodeDflt = layoutOut._isHoriz ? 'y' : 'x';
+        if(clickmode.indexOf('select') > -1) {
+            hovermodeDflt = 'closest';
+        } else {
+            // flag for 'horizontal' plots:
+            // determines the state of the mode bar 'compare' hovermode button
+            layoutOut._isHoriz = isHoriz(fullData);
+            hovermodeDflt = layoutOut._isHoriz ? 'y' : 'x';
+        }
     }
     else hovermodeDflt = 'closest';
 
diff --git a/src/components/legend/style.js b/src/components/legend/style.js
index fd68381fcdd..21afb93c07f 100644
--- a/src/components/legend/style.js
+++ b/src/components/legend/style.js
@@ -208,7 +208,9 @@ module.exports = function style(s, gd) {
 
         var pts = ptgroup.selectAll('path.scatterpts')
             .data(showMarkers ? dMod : []);
-        pts.enter().append('path').classed('scatterpts', true)
+        // make sure marker is on the bottom, in case it enters after text
+        pts.enter().insert('path', ':first-child')
+            .classed('scatterpts', true)
             .attr('transform', 'translate(20,0)');
         pts.exit().remove();
         pts.call(Drawing.pointStyle, tMod, gd);
diff --git a/src/lib/polygon.js b/src/lib/polygon.js
index d84583e696e..fb07a677af6 100644
--- a/src/lib/polygon.js
+++ b/src/lib/polygon.js
@@ -31,8 +31,6 @@ var polygon = module.exports = {};
  *          returns boolean: is pt inside the polygon (including on its edges)
  */
 polygon.tester = function tester(ptsIn) {
-    if(Array.isArray(ptsIn[0][0])) return polygon.multitester(ptsIn);
-
     var pts = ptsIn.slice(),
         xmin = pts[0][0],
         xmax = xmin,
@@ -174,50 +172,6 @@ polygon.tester = function tester(ptsIn) {
     };
 };
 
-/**
- * Test multiple polygons
- */
-polygon.multitester = function multitester(list) {
-    var testers = [],
-        xmin = list[0][0][0],
-        xmax = xmin,
-        ymin = list[0][0][1],
-        ymax = ymin;
-
-    for(var i = 0; i < list.length; i++) {
-        var tester = polygon.tester(list[i]);
-        tester.subtract = list[i].subtract;
-        testers.push(tester);
-        xmin = Math.min(xmin, tester.xmin);
-        xmax = Math.max(xmax, tester.xmax);
-        ymin = Math.min(ymin, tester.ymin);
-        ymax = Math.max(ymax, tester.ymax);
-    }
-
-    function contains(pt, arg) {
-        var yes = false;
-        for(var i = 0; i < testers.length; i++) {
-            if(testers[i].contains(pt, arg)) {
-                // if contained by subtract polygon - exclude the point
-                yes = testers[i].subtract === false;
-            }
-        }
-
-        return yes;
-    }
-
-    return {
-        xmin: xmin,
-        xmax: xmax,
-        ymin: ymin,
-        ymax: ymax,
-        pts: [],
-        contains: contains,
-        isRect: false,
-        degenerate: false
-    };
-};
-
 /**
  * Test if a segment of a points array is bent or straight
  *
diff --git a/src/lib/prepare_regl.js b/src/lib/prepare_regl.js
index 13931e4eaa4..d262a510aa9 100644
--- a/src/lib/prepare_regl.js
+++ b/src/lib/prepare_regl.js
@@ -48,6 +48,17 @@ module.exports = function prepareRegl(gd, extensions) {
         } catch(e) {
             success = false;
         }
+
+        if(success) {
+            this.addEventListener('webglcontextlost', function(event) {
+                if(gd && gd.emit) {
+                    gd.emit('plotly_webglcontextlost', {
+                        event: event,
+                        layer: d.key
+                    });
+                }
+            }, false);
+        }
     });
 
     if(!success) {
diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js
index 8d66a4cb4a5..bdd180b17c4 100644
--- a/src/plots/cartesian/dragbox.js
+++ b/src/plots/cartesian/dragbox.js
@@ -30,6 +30,7 @@ var doTicksSingle = require('./axes').doTicksSingle;
 var getFromId = require('./axis_ids').getFromId;
 var prepSelect = require('./select').prepSelect;
 var clearSelect = require('./select').clearSelect;
+var selectOnClick = require('./select').selectOnClick;
 var scaleZoom = require('./scale_zoom');
 
 var constants = require('./constants');
@@ -148,7 +149,11 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
     };
 
     dragOptions.prepFn = function(e, startX, startY) {
+        var dragModePrev = dragOptions.dragmode;
         var dragModeNow = gd._fullLayout.dragmode;
+        if(dragModeNow !== dragModePrev) {
+            dragOptions.dragmode = dragModeNow;
+        }
 
         recomputeAxisLists();
 
@@ -178,7 +183,19 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
             prepSelect(e, startX, startY, dragOptions, dragModeNow);
         } else {
             dragOptions.clickFn = clickFn;
-            clearAndResetSelect();
+            if(isSelectOrLasso(dragModePrev)) {
+                // TODO Fix potential bug
+                // Note: clearing / resetting selection state only happens, when user
+                // triggers at least one interaction in pan/zoom mode. Otherwise, the
+                // select/lasso outlines are deleted (in plots.js.cleanPlot) but the selection
+                // cache isn't cleared. So when the user switches back to select/lasso and
+                // 'adds to a selection' with Shift, the "old", seemingly removed outlines
+                // are redrawn again because the selection cache still holds their coordinates.
+                // However, this isn't easily solved, since plots.js would need
+                // to have a reference to the dragOptions object (which holds the
+                // selection cache).
+                clearAndResetSelect();
+            }
 
             if(!allFixedRanges) {
                 if(dragModeNow === 'zoom') {
@@ -207,12 +224,20 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
     }
 
     function clickFn(numClicks, evt) {
+        var clickmode = gd._fullLayout.clickmode;
+
         removeZoombox(gd);
 
         if(numClicks === 2 && !singleEnd) doubleClick();
 
         if(isMainDrag) {
-            Fx.click(gd, evt, plotinfo.id);
+            if(clickmode.indexOf('select') > -1) {
+                selectOnClick(evt, gd, xaxes, yaxes, plotinfo.id, dragOptions);
+            }
+
+            if(clickmode.indexOf('event') > -1) {
+                Fx.click(gd, evt, plotinfo.id);
+            }
         }
         else if(numClicks === 1 && singleEnd) {
             var ax = ns ? ya0 : xa0,
diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js
index 498dfff60d2..fdc869f8690 100644
--- a/src/plots/cartesian/select.js
+++ b/src/plots/cartesian/select.js
@@ -26,7 +26,6 @@ var MINSELECT = constants.MINSELECT;
 
 var filteredPolygon = polygon.filter;
 var polygonTester = polygon.tester;
-var multipolygonTester = polygon.multitester;
 
 function getAxId(ax) { return ax._id; }
 
@@ -45,43 +44,13 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
     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 filterPoly, selectionTester, mergedPolygons, currentPolygon;
+    var i, 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;
-    } 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;
-    }
+    coerceSelectionsCache(e, gd, dragOptions);
 
     if(mode === 'lasso') {
         filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX);
@@ -106,52 +75,12 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
         .attr('d', 'M0,0Z');
 
 
-    // find the traces to search for selection points
-    var searchTraces = [];
     var throttleID = fullLayout._uid + constants.SELECTID;
     var selection = [];
 
-    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) {
-            if(
-                trace.subplot === dragOptions.subplot ||
-                trace.geo === dragOptions.subplot
-            ) {
-                searchTraces.push({
-                    _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({
-                _module: trace._module,
-                cd: cd,
-                xaxis: getFromId(gd, trace.xaxis),
-                yaxis: getFromId(gd, trace.yaxis)
-            });
-        }
-    }
+    // find the traces to search for selection points
+    var searchTraces = determineSearchTraces(gd, dragOptions.xaxes,
+      dragOptions.yaxes, dragOptions.subplot);
 
     function axValue(ax) {
         var index = (ax._id.charAt(0) === 'y') ? 1 : 0;
@@ -253,24 +182,19 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
         }
 
         // create outline & tester
-        if(dragOptions.polygons && dragOptions.polygons.length) {
+        if(dragOptions.selectionDefs && dragOptions.selectionDefs.length) {
             mergedPolygons = mergePolygons(dragOptions.mergedPolygons, currentPolygon, subtract);
             currentPolygon.subtract = subtract;
-            testPoly = multipolygonTester(dragOptions.polygons.concat([currentPolygon]));
+            selectionTester = multiTester(dragOptions.selectionDefs.concat([currentPolygon]));
         }
         else {
             mergedPolygons = [currentPolygon];
-            testPoly = polygonTester(currentPolygon);
+            selectionTester = polygonTester(currentPolygon);
         }
 
         // draw selection
-        var paths = [];
-        for(i = 0; i < mergedPolygons.length; i++) {
-            var ppts = mergedPolygons[i];
-            paths.push(ppts.join('L') + 'L' + ppts[0]);
-        }
-        outlines
-            .attr('d', 'M' + paths.join('M') + 'Z');
+        drawSelection(mergedPolygons, outlines);
+
 
         throttle.throttle(
             throttleID,
@@ -282,7 +206,7 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
                 for(i = 0; i < searchTraces.length; i++) {
                     searchInfo = searchTraces[i];
 
-                    traceSelection = searchInfo._module.selectPoints(searchInfo, testPoly);
+                    traceSelection = searchInfo._module.selectPoints(searchInfo, selectionTester);
                     traceSelections.push(traceSelection);
 
                     thisSelection = fillSelectionItem(traceSelection, searchInfo);
@@ -304,6 +228,8 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
     };
 
     dragOptions.clickFn = function(numClicks, evt) {
+        var clickmode = fullLayout.clickmode;
+
         corners.remove();
 
         throttle.done(throttleID).then(function() {
@@ -317,12 +243,23 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
                 }
 
                 updateSelectedState(gd, searchTraces);
+
+                clearSelectionsCache(dragOptions);
+
                 gd.emit('plotly_deselect', null);
-            }
-            else {
-                // TODO: remove in v2 - this was probably never intended to work as it does,
-                // but in case anyone depends on it we don't want to break it now.
-                gd.emit('plotly_selected', undefined);
+            } else {
+                if(clickmode.indexOf('select') > -1) {
+                    selectOnClick(evt, gd, dragOptions.xaxes, dragOptions.yaxes,
+                      dragOptions.subplot, dragOptions, outlines);
+                }
+
+                if(clickmode === 'event') {
+                    // TODO: remove in v2 - this was probably never intended to work as it does,
+                    // but in case anyone depends on it we don't want to break it now.
+                    // Note that click-to-select introduced pre v2 also emitts proper
+                    // event data when clickmode is having 'select' in its flag list.
+                    gd.emit('plotly_selected', undefined);
+                }
             }
 
             Fx.click(gd, evt);
@@ -336,10 +273,10 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
             throttle.clear(throttleID);
             dragOptions.gd.emit('plotly_selected', eventData);
 
-            if(currentPolygon && dragOptions.polygons) {
+            if(currentPolygon && dragOptions.selectionDefs) {
                 // save last polygons
                 currentPolygon.subtract = subtract;
-                dragOptions.polygons.push(currentPolygon);
+                dragOptions.selectionDefs.push(currentPolygon);
 
                 // we have to keep reference to arrays container
                 dragOptions.mergedPolygons.length = 0;
@@ -349,6 +286,380 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
     };
 }
 
+function selectOnClick(evt, gd, xAxes, yAxes, subplot, dragOptions, polygonOutlines) {
+    var hoverData = gd._hoverdata;
+    var clickmode = gd._fullLayout.clickmode;
+    var sendEvents = clickmode.indexOf('event') > -1;
+    var selection = [];
+    var searchTraces, searchInfo, currentSelectionDef, selectionTester, traceSelection;
+    var thisTracesSelection, pointOrBinSelected, subtract, eventData, i;
+
+    if(isHoverDataSet(hoverData)) {
+        coerceSelectionsCache(evt, gd, dragOptions);
+        searchTraces = determineSearchTraces(gd, xAxes, yAxes, subplot);
+        var clickedPtInfo = extractClickedPtInfo(hoverData, searchTraces);
+        var isBinnedTrace = clickedPtInfo.pointNumbers.length > 0;
+
+
+        // Note: potentially costly operation isPointOrBinSelected is
+        // called as late as possible through the use of an assignment
+        // in an if condition.
+        if(isBinnedTrace ?
+            isOnlyThisBinSelected(searchTraces, clickedPtInfo) :
+            isOnlyOnePointSelected(searchTraces) &&
+                (pointOrBinSelected = isPointOrBinSelected(clickedPtInfo)))
+        {
+            if(polygonOutlines) polygonOutlines.remove();
+            for(i = 0; i < searchTraces.length; i++) {
+                searchInfo = searchTraces[i];
+                searchInfo._module.selectPoints(searchInfo, false);
+            }
+
+            updateSelectedState(gd, searchTraces);
+
+            clearSelectionsCache(dragOptions);
+
+            if(sendEvents) {
+                gd.emit('plotly_deselect', null);
+            }
+        } else {
+            subtract = evt.shiftKey &&
+              (pointOrBinSelected !== undefined ?
+                pointOrBinSelected :
+                isPointOrBinSelected(clickedPtInfo));
+            currentSelectionDef = newPointSelectionDef(clickedPtInfo.pointNumber, clickedPtInfo.searchInfo, subtract);
+
+            var allSelectionDefs = dragOptions.selectionDefs.concat([currentSelectionDef]);
+            selectionTester = multiTester(allSelectionDefs);
+
+            for(i = 0; i < searchTraces.length; i++) {
+                traceSelection = searchTraces[i]._module.selectPoints(searchTraces[i], selectionTester);
+                thisTracesSelection = fillSelectionItem(traceSelection, searchTraces[i]);
+
+                if(selection.length) {
+                    for(var j = 0; j < thisTracesSelection.length; j++) {
+                        selection.push(thisTracesSelection[j]);
+                    }
+                }
+                else selection = thisTracesSelection;
+            }
+
+            eventData = {points: selection};
+            updateSelectedState(gd, searchTraces, eventData);
+
+            if(currentSelectionDef && dragOptions) {
+                dragOptions.selectionDefs.push(currentSelectionDef);
+            }
+
+            if(polygonOutlines) drawSelection(dragOptions.mergedPolygons, polygonOutlines);
+
+            if(sendEvents) {
+                gd.emit('plotly_selected', eventData);
+            }
+        }
+    }
+}
+
+/**
+ * Constructs a new point selection definition object.
+ */
+function newPointSelectionDef(pointNumber, searchInfo, subtract) {
+    return {
+        pointNumber: pointNumber,
+        searchInfo: searchInfo,
+        subtract: subtract
+    };
+}
+
+function isPointSelectionDef(o) {
+    return 'pointNumber' in o && 'searchInfo' in o;
+}
+
+/*
+ * Constructs a new point number tester.
+ */
+function newPointNumTester(pointSelectionDef) {
+    return {
+        xmin: 0,
+        xmax: 0,
+        ymin: 0,
+        ymax: 0,
+        pts: [],
+        contains: function(pt, omitFirstEdge, pointNumber, searchInfo) {
+            var idxWantedTrace = pointSelectionDef.searchInfo.cd[0].trace._expandedIndex;
+            var idxActualTrace = searchInfo.cd[0].trace._expandedIndex;
+            return idxActualTrace === idxWantedTrace &&
+              pointNumber === pointSelectionDef.pointNumber;
+        },
+        isRect: false,
+        degenerate: false,
+        subtract: pointSelectionDef.subtract
+    };
+}
+
+/**
+ * Wraps multiple selection testers.
+ *
+ * @param {Array} list - An array of selection testers.
+ *
+ * @return a selection tester object with a contains function
+ * that can be called to evaluate a point against all wrapped
+ * selection testers that were passed in list.
+ */
+function multiTester(list) {
+    var testers = [];
+    var xmin = isPointSelectionDef(list[0]) ? 0 : list[0][0][0];
+    var xmax = xmin;
+    var ymin = isPointSelectionDef(list[0]) ? 0 : list[0][0][1];
+    var ymax = ymin;
+
+    for(var i = 0; i < list.length; i++) {
+        if(isPointSelectionDef(list[i])) {
+            testers.push(newPointNumTester(list[i]));
+        } else {
+            var tester = polygon.tester(list[i]);
+            tester.subtract = list[i].subtract;
+            testers.push(tester);
+            xmin = Math.min(xmin, tester.xmin);
+            xmax = Math.max(xmax, tester.xmax);
+            ymin = Math.min(ymin, tester.ymin);
+            ymax = Math.max(ymax, tester.ymax);
+        }
+    }
+
+    /**
+     * Tests if the given point is within this tester.
+     *
+     * @param {Array} pt - [0] is the x coordinate, [1] is the y coordinate of the point.
+     * @param {*} arg - An optional parameter to pass down to wrapped testers.
+     * @param {number} pointNumber - The point number of the point within the underlying data array.
+     * @param {number} searchInfo - An object identifying the trace the point is contained in.
+     *
+     * @return {boolean} true if point is considered to be selected, false otherwise.
+     */
+    function contains(pt, arg, pointNumber, searchInfo) {
+        var contained = false;
+        for(var i = 0; i < testers.length; i++) {
+            if(testers[i].contains(pt, arg, pointNumber, searchInfo)) {
+                // if contained by subtract tester - exclude the point
+                contained = testers[i].subtract === false;
+            }
+        }
+
+        return contained;
+    }
+
+    return {
+        xmin: xmin,
+        xmax: xmax,
+        ymin: ymin,
+        ymax: ymax,
+        pts: [],
+        contains: contains,
+        isRect: false,
+        degenerate: false
+    };
+}
+
+function coerceSelectionsCache(evt, gd, dragOptions) {
+    var fullLayout = gd._fullLayout;
+    var zoomLayer = fullLayout._zoomlayer;
+    var plotinfo = dragOptions.plotinfo;
+
+    var selectingOnSameSubplot = (
+      fullLayout._lastSelectedSubplot &&
+      fullLayout._lastSelectedSubplot === plotinfo.id
+    );
+    var hasModifierKey = evt.shiftKey || evt.altKey;
+    if(selectingOnSameSubplot && hasModifierKey &&
+      (plotinfo.selection && plotinfo.selection.selectionDefs) && !dragOptions.selectionDefs) {
+        // take over selection definitions from prev mode, if any
+        dragOptions.selectionDefs = plotinfo.selection.selectionDefs;
+        dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons;
+    } else if(!hasModifierKey || !plotinfo.selection) {
+        clearSelectionsCache(dragOptions);
+    }
+
+    // clear selection outline when selecting a different subplot
+    if(!selectingOnSameSubplot) {
+        clearSelect(zoomLayer);
+        fullLayout._lastSelectedSubplot = plotinfo.id;
+    }
+}
+
+function clearSelectionsCache(dragOptions) {
+    var plotinfo = dragOptions.plotinfo;
+
+    plotinfo.selection = {};
+    plotinfo.selection.selectionDefs = dragOptions.selectionDefs = [];
+    plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = [];
+}
+
+function determineSearchTraces(gd, xAxes, yAxes, subplot) {
+    var searchTraces = [];
+    var xAxisIds = xAxes.map(getAxId);
+    var yAxisIds = yAxes.map(getAxId);
+    var cd, trace, i;
+
+    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(subplot && (trace.subplot === subplot || trace.geo === subplot)) {
+            searchTraces.push(createSearchInfo(trace._module, cd, xAxes[0], 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(createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]));
+        } else {
+            if(xAxisIds.indexOf(trace.xaxis) === -1) continue;
+            if(yAxisIds.indexOf(trace.yaxis) === -1) continue;
+
+            searchTraces.push(createSearchInfo(trace._module, cd,
+              getFromId(gd, trace.xaxis), getFromId(gd, trace.yaxis)));
+        }
+    }
+
+    return searchTraces;
+
+    function createSearchInfo(module, calcData, xaxis, yaxis) {
+        return {
+            _module: module,
+            cd: calcData,
+            xaxis: xaxis,
+            yaxis: yaxis
+        };
+    }
+}
+
+function drawSelection(polygons, outlines) {
+    var paths = [];
+    var i, d;
+
+    for(i = 0; i < polygons.length; i++) {
+        var ppts = polygons[i];
+        paths.push(ppts.join('L') + 'L' + ppts[0]);
+    }
+
+    d = polygons.length > 0 ?
+      'M' + paths.join('M') + 'Z' :
+      'M0,0Z';
+    outlines.attr('d', d);
+}
+
+function isHoverDataSet(hoverData) {
+    return hoverData &&
+      Array.isArray(hoverData) &&
+      hoverData[0].hoverOnBox !== true;
+}
+
+function extractClickedPtInfo(hoverData, searchTraces) {
+    var hoverDatum = hoverData[0];
+    var pointNumber = -1;
+    var pointNumbers = [];
+    var searchInfo, i;
+
+    for(i = 0; i < searchTraces.length; i++) {
+        searchInfo = searchTraces[i];
+        if(hoverDatum.fullData._expandedIndex === searchInfo.cd[0].trace._expandedIndex) {
+
+            // Special case for box (and violin)
+            if(hoverDatum.hoverOnBox === true) {
+                break;
+            }
+
+            // Hint: in some traces like histogram, one graphical element
+            // doesn't correspond to one particular data point, but to
+            // bins of data points. Thus, hoverDatum can have a binNumber
+            // property instead of pointNumber.
+            if(hoverDatum.pointNumber !== undefined) {
+                pointNumber = hoverDatum.pointNumber;
+            } else if(hoverDatum.binNumber !== undefined) {
+                pointNumber = hoverDatum.binNumber;
+                pointNumbers = hoverDatum.pointNumbers;
+            }
+
+            break;
+        }
+    }
+
+    return {
+        pointNumber: pointNumber,
+        pointNumbers: pointNumbers,
+        searchInfo: searchInfo
+    };
+}
+
+function isPointOrBinSelected(clickedPtInfo) {
+    var trace = clickedPtInfo.searchInfo.cd[0].trace;
+    var ptNum = clickedPtInfo.pointNumber;
+    var ptNums = clickedPtInfo.pointNumbers;
+    var ptNumsSet = ptNums.length > 0;
+
+    // When pointsNumbers is set (e.g. histogram's binning),
+    // it is assumed that when the first point of
+    // a bin is selected, all others are as well
+    var ptNumToTest = ptNumsSet ? ptNums[0] : ptNum;
+
+    // TODO potential performance improvement
+    // Primarily we need this function to determine if a click adds
+    // or subtracts from a selection.
+    // In cases `trace.selectedpoints` is a huge array, indexOf
+    // might be slow. One remedy would be to introduce a hash somewhere.
+    return trace.selectedpoints ? trace.selectedpoints.indexOf(ptNumToTest) > -1 : false;
+}
+
+function isOnlyThisBinSelected(searchTraces, clickedPtInfo) {
+    var tracesWithSelectedPts = [];
+    var searchInfo, trace, isSameTrace, i;
+
+    for(i = 0; i < searchTraces.length; i++) {
+        searchInfo = searchTraces[i];
+        if(searchInfo.cd[0].trace.selectedpoints && searchInfo.cd[0].trace.selectedpoints.length > 0) {
+            tracesWithSelectedPts.push(searchInfo);
+        }
+    }
+
+    if(tracesWithSelectedPts.length === 1) {
+        isSameTrace = tracesWithSelectedPts[0] === clickedPtInfo.searchInfo;
+        if(isSameTrace) {
+            trace = clickedPtInfo.searchInfo.cd[0].trace;
+            if(trace.selectedpoints.length === clickedPtInfo.pointNumbers.length) {
+                for(i = 0; i < clickedPtInfo.pointNumbers.length; i++) {
+                    if(trace.selectedpoints.indexOf(clickedPtInfo.pointNumbers[i]) < 0) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+function isOnlyOnePointSelected(searchTraces) {
+    var len = 0;
+    var searchInfo, trace, i;
+
+    for(i = 0; i < searchTraces.length; i++) {
+        searchInfo = searchTraces[i];
+        trace = searchInfo.cd[0].trace;
+        if(trace.selectedpoints) {
+            if(trace.selectedpoints.length > 1) return false;
+
+            len += trace.selectedpoints.length;
+            if(len > 1) return false;
+        }
+    }
+
+    return len === 1;
+}
+
 function updateSelectedState(gd, searchTraces, eventData) {
     var i, j, searchInfo, trace;
 
@@ -471,5 +782,6 @@ function clearSelect(zoomlayer) {
 
 module.exports = {
     prepSelect: prepSelect,
-    clearSelect: clearSelect
+    clearSelect: clearSelect,
+    selectOnClick: selectOnClick
 };
diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js
index e6d419503b5..83153e0dfe3 100644
--- a/src/plots/geo/geo.js
+++ b/src/plots/geo/geo.js
@@ -21,6 +21,7 @@ var Plots = require('../plots');
 var Axes = require('../cartesian/axes');
 var dragElement = require('../../components/dragelement');
 var prepSelect = require('../cartesian/select').prepSelect;
+var selectOnClick = require('../cartesian/select').selectOnClick;
 
 var createGeoZoom = require('./zoom');
 var constants = require('./constants');
@@ -354,6 +355,7 @@ proto.updateFx = function(fullLayout, geoLayout) {
     var gd = _this.graphDiv;
     var bgRect = _this.bgRect;
     var dragMode = fullLayout.dragmode;
+    var clickMode = fullLayout.clickmode;
 
     if(_this.isStatic) return;
 
@@ -376,6 +378,44 @@ proto.updateFx = function(fullLayout, geoLayout) {
         ]);
     }
 
+    var fillRangeItems;
+
+    if(dragMode === 'select') {
+        fillRangeItems = function(eventData, poly) {
+            var ranges = eventData.range = {};
+            ranges[_this.id] = [
+                invert([poly.xmin, poly.ymin]),
+                invert([poly.xmax, poly.ymax])
+            ];
+        };
+    } else if(dragMode === 'lasso') {
+        fillRangeItems = function(eventData, poly, pts) {
+            var dataPts = eventData.lassoPoints = {};
+            dataPts[_this.id] = pts.filtered.map(invert);
+        };
+    }
+
+    // Note: dragOptions is needed to be declared for all dragmodes because
+    // it's the object that holds persistent selection state.
+    var dragOptions = {
+        element: _this.bgRect.node(),
+        gd: gd,
+        plotinfo: {
+            id: _this.id,
+            xaxis: _this.xaxis,
+            yaxis: _this.yaxis,
+            fillRangeItems: fillRangeItems
+        },
+        xaxes: [_this.xaxis],
+        yaxes: [_this.yaxis],
+        subplot: _this.id,
+        clickFn: function(numClicks) {
+            if(numClicks === 2) {
+                fullLayout._zoomlayer.selectAll('.select-outline').remove();
+            }
+        }
+    };
+
     if(dragMode === 'pan') {
         bgRect.node().onmousedown = null;
         bgRect.call(createGeoZoom(_this, geoLayout));
@@ -384,41 +424,6 @@ proto.updateFx = function(fullLayout, geoLayout) {
     else if(dragMode === 'select' || dragMode === 'lasso') {
         bgRect.on('.zoom', null);
 
-        var fillRangeItems;
-
-        if(dragMode === 'select') {
-            fillRangeItems = function(eventData, poly) {
-                var ranges = eventData.range = {};
-                ranges[_this.id] = [
-                    invert([poly.xmin, poly.ymin]),
-                    invert([poly.xmax, poly.ymax])
-                ];
-            };
-        } else if(dragMode === 'lasso') {
-            fillRangeItems = function(eventData, poly, pts) {
-                var dataPts = eventData.lassoPoints = {};
-                dataPts[_this.id] = pts.filtered.map(invert);
-            };
-        }
-
-        var dragOptions = {
-            element: _this.bgRect.node(),
-            gd: gd,
-            plotinfo: {
-                xaxis: _this.xaxis,
-                yaxis: _this.yaxis,
-                fillRangeItems: fillRangeItems
-            },
-            xaxes: [_this.xaxis],
-            yaxes: [_this.yaxis],
-            subplot: _this.id,
-            clickFn: function(numClicks) {
-                if(numClicks === 2) {
-                    fullLayout._zoomlayer.selectAll('.select-outline').remove();
-                }
-            }
-        };
-
         dragOptions.prepFn = function(e, startX, startY) {
             prepSelect(e, startX, startY, dragOptions, dragMode);
         };
@@ -440,15 +445,26 @@ proto.updateFx = function(fullLayout, geoLayout) {
     });
 
     bgRect.on('mouseout', function() {
+        if(gd._dragging) return;
         dragElement.unhover(gd, d3.event);
     });
 
     bgRect.on('click', function() {
-        // TODO: like pie and mapbox, this doesn't support right-click
-        // actually this one is worse, as right-click starts a pan, or leaves
-        // select in a weird state.
-        // Also, only tangentially related, we should cancel hover during pan
-        Fx.click(gd, d3.event);
+        // For select and lasso the dragElement is handling clicks
+        if(dragMode !== 'select' && dragMode !== 'lasso') {
+            if(clickMode.indexOf('select') > -1) {
+                selectOnClick(d3.event, gd, [_this.xaxis], [_this.yaxis],
+                  _this.id, dragOptions);
+            }
+
+            if(clickMode.indexOf('event') > -1) {
+                // TODO: like pie and mapbox, this doesn't support right-click
+                // actually this one is worse, as right-click starts a pan, or leaves
+                // select in a weird state.
+                // Also, only tangentially related, we should cancel hover during pan
+                Fx.click(gd, d3.event);
+            }
+        }
     });
 };
 
diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js
index 2a3c9a9d489..99dc45152ec 100644
--- a/src/plots/gl3d/scene.js
+++ b/src/plots/gl3d/scene.js
@@ -170,6 +170,8 @@ function render(scene) {
 }
 
 function initializeGLPlot(scene, fullLayout, canvas, gl) {
+    var gd = scene.graphDiv;
+
     var glplotOptions = {
         canvas: canvas,
         gl: gl,
@@ -220,7 +222,7 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) {
 
         var update = {};
         update[scene.id + '.camera'] = getLayoutCamera(scene.camera);
-        scene.saveCamera(scene.graphDiv.layout);
+        scene.saveCamera(gd.layout);
         scene.graphDiv.emit('plotly_relayout', update);
     };
 
@@ -228,10 +230,14 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) {
     scene.glplot.canvas.addEventListener('wheel', relayoutCallback.bind(null, scene), passiveSupported ? {passive: false} : false);
 
     if(!scene.staticMode) {
-        scene.glplot.canvas.addEventListener('webglcontextlost', function(ev) {
-            Lib.warn('Lost WebGL context.');
-            ev.preventDefault();
-        });
+        scene.glplot.canvas.addEventListener('webglcontextlost', function(event) {
+            if(gd && gd.emit) {
+                gd.emit('plotly_webglcontextlost', {
+                    event: event,
+                    layer: scene.id
+                });
+            }
+        }, false);
     }
 
     if(!scene.camera) {
diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js
index c5332d05fd8..77da3871017 100644
--- a/src/plots/mapbox/mapbox.js
+++ b/src/plots/mapbox/mapbox.js
@@ -15,6 +15,7 @@ var Fx = require('../../components/fx');
 var Lib = require('../../lib');
 var dragElement = require('../../components/dragelement');
 var prepSelect = require('../cartesian/select').prepSelect;
+var selectOnClick = require('../cartesian/select').selectOnClick;
 var constants = require('./constants');
 var layoutAttributes = require('./layout_attributes');
 var createMapboxLayer = require('./layers');
@@ -176,15 +177,6 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
         Fx.hover(gd, evt, self.id);
     });
 
-    map.on('click', function(evt) {
-        // TODO: this does not support right-click. If we want to support it, we
-        // would likely need to change mapbox to use dragElement instead of straight
-        // mapbox event binding. Or perhaps better, make a simple wrapper with the
-        // right mousedown, mousemove, and mouseup handlers just for a left/right click
-        // pie would use this too.
-        Fx.click(gd, evt.originalEvent);
-    });
-
     function unhover() {
         Fx.loneUnhover(fullLayout._toppaper);
     }
@@ -221,11 +213,34 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
         gd.emit('plotly_relayout', evtData);
     }
 
-    // define clear select on map creation, to keep one ref per map,
+    // define event handlers on map creation, to keep one ref per map,
     // so that map.on / map.off in updateFx works as expected
     self.clearSelect = function() {
         gd._fullLayout._zoomlayer.selectAll('.select-outline').remove();
     };
+
+    /**
+     * Returns a click handler function that is supposed
+     * to handle clicks in pan mode.
+     */
+    self.onClickInPanFn = function(dragOptions) {
+        return function(evt) {
+            var clickMode = gd._fullLayout.clickmode;
+
+            if(clickMode.indexOf('select') > -1) {
+                selectOnClick(evt.originalEvent, gd, [self.xaxis], [self.yaxis], self.id, dragOptions);
+            }
+
+            if(clickMode.indexOf('event') > -1) {
+                // TODO: this does not support right-click. If we want to support it, we
+                // would likely need to change mapbox to use dragElement instead of straight
+                // mapbox event binding. Or perhaps better, make a simple wrapper with the
+                // right mousedown, mousemove, and mouseup handlers just for a left/right click
+                // pie would use this too.
+                Fx.click(gd, evt.originalEvent);
+            }
+        };
+    };
 };
 
 proto.updateMap = function(calcData, fullLayout, resolve, reject) {
@@ -382,32 +397,50 @@ proto.updateFx = function(fullLayout) {
         };
     }
 
+    // Note: dragOptions is needed to be declared for all dragmodes because
+    // it's the object that holds persistent selection state.
+    // Merge old dragOptions with new to keep possibly initialized
+    // persistent selection state.
+    var oldDragOptions = self.dragOptions;
+    self.dragOptions = Lib.extendDeep(oldDragOptions || {}, {
+        element: self.div,
+        gd: gd,
+        plotinfo: {
+            id: self.id,
+            xaxis: self.xaxis,
+            yaxis: self.yaxis,
+            fillRangeItems: fillRangeItems
+        },
+        xaxes: [self.xaxis],
+        yaxes: [self.yaxis],
+        subplot: self.id
+    });
+
+    // Unregister the old handler before potentially registering
+    // a new one. Otherwise multiple click handlers might
+    // be registered resulting in unwanted behavior.
+    map.off('click', self.onClickInPanHandler);
     if(dragMode === 'select' || dragMode === 'lasso') {
         map.dragPan.disable();
         map.on('zoomstart', self.clearSelect);
 
-        var dragOptions = {
-            element: self.div,
-            gd: gd,
-            plotinfo: {
-                xaxis: self.xaxis,
-                yaxis: self.yaxis,
-                fillRangeItems: fillRangeItems
-            },
-            xaxes: [self.xaxis],
-            yaxes: [self.yaxis],
-            subplot: self.id
+        self.dragOptions.prepFn = function(e, startX, startY) {
+            prepSelect(e, startX, startY, self.dragOptions, dragMode);
         };
 
-        dragOptions.prepFn = function(e, startX, startY) {
-            prepSelect(e, startX, startY, dragOptions, dragMode);
-        };
-
-        dragElement.init(dragOptions);
+        dragElement.init(self.dragOptions);
     } else {
         map.dragPan.enable();
         map.off('zoomstart', self.clearSelect);
         self.div.onmousedown = null;
+
+        // TODO: this does not support right-click. If we want to support it, we
+        // would likely need to change mapbox to use dragElement instead of straight
+        // mapbox event binding. Or perhaps better, make a simple wrapper with the
+        // right mousedown, mousemove, and mouseup handlers just for a left/right click
+        // pie would use this too.
+        self.onClickInPanHandler = self.onClickInPanFn(self.dragOptions);
+        map.on('click', self.onClickInPanHandler);
     }
 };
 
diff --git a/src/plots/plots.js b/src/plots/plots.js
index da202ed8005..4cc227fb3cb 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -1894,6 +1894,10 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) {
             return d.map(stripObj);
         }
 
+        if(Lib.isTypedArray(d)) {
+            return Lib.simpleMap(d, Lib.identity);
+        }
+
         // convert native dates to date strings...
         // mostly for external users exporting to plotly
         if(Lib.isJSDate(d)) return Lib.ms2DateTimeLocal(+d);
diff --git a/src/plots/polar/constants.js b/src/plots/polar/constants.js
index ca0c0734803..71b196f422c 100644
--- a/src/plots/polar/constants.js
+++ b/src/plots/polar/constants.js
@@ -22,10 +22,10 @@ module.exports = {
         'angular-grid',
         'radial-grid',
         'frontplot',
-        'angular-axis',
-        'radial-axis',
         'angular-line',
-        'radial-line'
+        'radial-line',
+        'angular-axis',
+        'radial-axis'
     ],
 
     radialDragBoxSize: 50,
diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js
index ab2c880daeb..d7fa1d6c31f 100644
--- a/src/plots/polar/layout_attributes.js
+++ b/src/plots/polar/layout_attributes.js
@@ -112,16 +112,6 @@ var radialAxisAttrs = {
 
     hoverformat: axesAttrs.hoverformat,
 
-    // More attributes:
-
-    // We'll need some attribute that determines the span
-    // to draw donut-like charts
-    // e.g. https://github.com/matplotlib/matplotlib/issues/4217
-    //
-    // maybe something like 'span' or 'hole' (like pie, but pie set it in data coords?)
-    // span: {},
-    // hole: 1
-
     editType: 'calc'
 };
 
@@ -256,6 +246,17 @@ module.exports = {
             'with *0* corresponding to rightmost limit of the polar subplot.'
         ].join(' ')
     },
+    hole: {
+        valType: 'number',
+        min: 0,
+        max: 1,
+        dflt: 0,
+        editType: 'plot',
+        role: 'info',
+        description: [
+            'Sets the fraction of the radius to cut out of the polar subplot.'
+        ].join(' ')
+    },
 
     bgcolor: {
         valType: 'color',
diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js
index f8bb7a9103b..462916536ac 100644
--- a/src/plots/polar/layout_defaults.js
+++ b/src/plots/polar/layout_defaults.js
@@ -30,6 +30,7 @@ function handleDefaults(contIn, contOut, coerce, opts) {
     opts.bgColor = Color.combine(bgColor, opts.paper_bgcolor);
 
     var sector = coerce('sector');
+    coerce('hole');
 
     // could optimize, subplotData is not always needed!
     var subplotData = getSubplotData(opts.fullData, constants.name, opts.id);
diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js
index e550b52b325..6c78215d339 100644
--- a/src/plots/polar/polar.js
+++ b/src/plots/polar/polar.js
@@ -25,6 +25,7 @@ var dragElement = require('../../components/dragelement');
 var Fx = require('../../components/fx');
 var Titles = require('../../components/titles');
 var prepSelect = require('../cartesian/select').prepSelect;
+var selectOnClick = require('../cartesian/select').selectOnClick;
 var clearSelect = require('../cartesian/select').clearSelect;
 var setCursor = require('../../lib/setcursor');
 
@@ -89,7 +90,7 @@ proto.plot = function(polarCalcData, fullLayout) {
     _this.updateLayers(fullLayout, polarLayout);
     _this.updateLayout(fullLayout, polarLayout);
     Plots.generalUpdatePerTraceModule(_this.gd, _this, polarCalcData, polarLayout);
-    _this.updateFx(fullLayout);
+    _this.updateFx(fullLayout, polarLayout);
 };
 
 proto.updateLayers = function(fullLayout, polarLayout) {
@@ -104,17 +105,17 @@ proto.updateLayers = function(fullLayout, polarLayout) {
     var isAngularAxisBelowTraces = angularLayout.layer === 'below traces';
     var isRadialAxisBelowTraces = radialLayout.layer === 'below traces';
 
-    if(isAngularAxisBelowTraces) layerData.push('angular-axis');
-    if(isRadialAxisBelowTraces) layerData.push('radial-axis');
     if(isAngularAxisBelowTraces) layerData.push('angular-line');
     if(isRadialAxisBelowTraces) layerData.push('radial-line');
+    if(isAngularAxisBelowTraces) layerData.push('angular-axis');
+    if(isRadialAxisBelowTraces) layerData.push('radial-axis');
 
     layerData.push('frontplot');
 
-    if(!isAngularAxisBelowTraces) layerData.push('angular-axis');
-    if(!isRadialAxisBelowTraces) layerData.push('radial-axis');
     if(!isAngularAxisBelowTraces) layerData.push('angular-line');
     if(!isRadialAxisBelowTraces) layerData.push('radial-line');
+    if(!isAngularAxisBelowTraces) layerData.push('angular-axis');
+    if(!isRadialAxisBelowTraces) layerData.push('radial-axis');
 
     var join = _this.framework.selectAll('.polarsublayer')
         .data(layerData, String);
@@ -126,9 +127,9 @@ proto.updateLayers = function(fullLayout, polarLayout) {
 
             switch(d) {
                 case 'frontplot':
-                    sel.append('g').classed('scatterlayer', true);
                     // TODO add option to place in 'backplot' layer??
                     sel.append('g').classed('barlayer', true);
+                    sel.append('g').classed('scatterlayer', true);
                     break;
                 case 'backplot':
                     sel.append('g').classed('maplayer', true);
@@ -234,6 +235,8 @@ proto.updateLayout = function(fullLayout, polarLayout) {
     var yOffset2 = _this.yOffset2 = gs.t + gs.h * (1 - yDomain2[1]);
     // circle radius in px
     var radius = _this.radius = xLength2 / dxSectorBBox;
+    // 'inner' radius in px (when polar.hole is set)
+    var innerRadius = _this.innerRadius = polarLayout.hole * radius;
     // circle center position in px
     var cx = _this.cx = xOffset2 - radius * sectorBBox[0];
     var cy = _this.cy = yOffset2 + radius * sectorBBox[3];
@@ -252,7 +255,7 @@ proto.updateLayout = function(fullLayout, polarLayout) {
             clockwise: 'bottom'
         }[radialLayout.side],
         // spans length 1 radius
-        domain: [0, radius / gs.w]
+        domain: [innerRadius / gs.w, radius / gs.w]
     });
 
     _this.angularAxis = _this.mockAxis(fullLayout, polarLayout, angularLayout, {
@@ -282,7 +285,7 @@ proto.updateLayout = function(fullLayout, polarLayout) {
         domain: yDomain2
     });
 
-    var dPath = _this.pathSector();
+    var dPath = _this.pathSubplot();
 
     _this.clipPaths.forTraces.select('path')
         .attr('d', dPath)
@@ -333,9 +336,9 @@ proto.mockCartesianAxis = function(fullLayout, polarLayout, opts) {
 
     ax.setRange = function() {
         var sectorBBox = _this.sectorBBox;
-        var rl = _this.radialAxis._rl;
-        var drl = rl[1] - rl[0];
         var ind = bboxIndices[axId];
+        var rl = _this.radialAxis._rl;
+        var drl = (rl[1] - rl[0]) / (1 - polarLayout.hole);
         ax.range = [sectorBBox[ind[0]] * drl, sectorBBox[ind[1]] * drl];
     };
 
@@ -371,6 +374,7 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
     var gd = _this.gd;
     var layers = _this.layers;
     var radius = _this.radius;
+    var innerRadius = _this.innerRadius;
     var cx = _this.cx;
     var cy = _this.cy;
     var radialLayout = polarLayout.radialaxis;
@@ -392,12 +396,12 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
 
     // easier to set rotate angle with custom translate function
     ax._transfn = function(d) {
-        return 'translate(' + ax.l2p(d.x) + ',0)';
+        return 'translate(' + (ax.l2p(d.x) + innerRadius) + ',0)';
     };
 
     // set special grid path function
     ax._gridpath = function(d) {
-        return _this.pathArc(ax.r2p(d.x));
+        return _this.pathArc(ax.r2p(d.x) + innerRadius);
     };
 
     var newTickLayout = strTickLayout(radialLayout);
@@ -428,7 +432,7 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
     .selectAll('path').attr('transform', null);
 
     updateElement(layers['radial-line'].select('line'), radialLayout.showline, {
-        x1: 0,
+        x1: innerRadius,
         y1: 0,
         x2: radius,
         y2: 0,
@@ -479,6 +483,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
     var gd = _this.gd;
     var layers = _this.layers;
     var radius = _this.radius;
+    var innerRadius = _this.innerRadius;
     var cx = _this.cx;
     var cy = _this.cy;
     var angularLayout = polarLayout.angularaxis;
@@ -491,11 +496,6 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
     // 't'ick to 'g'eometric radians is used all over the place here
     var t2g = function(d) { return ax.t2g(d.x); };
 
-    // (x,y) at max radius
-    function rad2xy(rad) {
-        return [radius * Math.cos(rad), radius * Math.sin(rad)];
-    }
-
     // run rad2deg on tick0 and ditck for thetaunit: 'radians' axes
     if(ax.type === 'linear' && ax.thetaunit === 'radians') {
         ax.tick0 = rad2deg(ax.tick0);
@@ -512,13 +512,17 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
     }
 
     ax._transfn = function(d) {
+        var sel = d3.select(this);
+        var hasElement = sel && sel.node();
+
+        // don't translate grid lines
+        if(hasElement && sel.classed('angularaxisgrid')) return '';
+
         var rad = t2g(d);
-        var xy = rad2xy(rad);
-        var out = strTranslate(cx + xy[0], cy - xy[1]);
+        var out = strTranslate(cx + radius * Math.cos(rad), cy - radius * Math.sin(rad));
 
-        // must also rotate ticks, but don't rotate labels and grid lines
-        var sel = d3.select(this);
-        if(sel && sel.node() && sel.classed('ticks')) {
+        // must also rotate ticks, but don't rotate labels
+        if(hasElement && sel.classed('ticks')) {
             out += strRotate(-rad2deg(rad));
         }
 
@@ -527,8 +531,10 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
 
     ax._gridpath = function(d) {
         var rad = t2g(d);
-        var xy = rad2xy(rad);
-        return 'M0,0L' + (-xy[0]) + ',' + xy[1];
+        var cosRad = Math.cos(rad);
+        var sinRad = Math.sin(rad);
+        return 'M' + [cx + innerRadius * cosRad, cy - innerRadius * sinRad] +
+            'L' + [cx + radius * cosRad, cy - radius * sinRad];
     };
 
     var offset4fontsize = (angularLayout.ticks !== 'outside' ? 0.7 : 0.5);
@@ -590,18 +596,22 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
     }
     _this.vangles = vangles;
 
+    // TODO maybe two arcs is better here?
+    // maybe split style attributes between inner and outer angular axes?
+
     updateElement(layers['angular-line'].select('path'), angularLayout.showline, {
-        d: _this.pathSector(),
+        d: _this.pathSubplot(),
         transform: strTranslate(cx, cy)
     })
     .attr('stroke-width', angularLayout.linewidth)
     .call(Color.stroke, angularLayout.linecolor);
 };
 
-proto.updateFx = function(fullLayout) {
+proto.updateFx = function(fullLayout, polarLayout) {
     if(!this.gd._context.staticPlot) {
         this.updateAngularDrag(fullLayout);
-        this.updateRadialDrag(fullLayout);
+        this.updateRadialDrag(fullLayout, polarLayout, 0);
+        this.updateRadialDrag(fullLayout, polarLayout, 1);
         this.updateMainDrag(fullLayout);
     }
 };
@@ -614,12 +624,14 @@ proto.updateMainDrag = function(fullLayout) {
     var MINZOOM = constants.MINZOOM;
     var OFFEDGE = constants.OFFEDGE;
     var radius = _this.radius;
+    var innerRadius = _this.innerRadius;
     var cx = _this.cx;
     var cy = _this.cy;
     var cxx = _this.cxx;
     var cyy = _this.cyy;
     var sectorInRad = _this.sectorInRad;
     var vangles = _this.vangles;
+    var radialAxis = _this.radialAxis;
     var clampTiny = helpers.clampTiny;
     var findXYatLength = helpers.findXYatLength;
     var findEnclosingVertexAngles = helpers.findEnclosingVertexAngles;
@@ -629,7 +641,7 @@ proto.updateMainDrag = function(fullLayout) {
     var mainDrag = dragBox.makeDragger(layers, 'path', 'maindrag', 'crosshair');
 
     d3.select(mainDrag)
-        .attr('d', _this.pathSector())
+        .attr('d', _this.pathSubplot())
         .attr('transform', strTranslate(cx, cy));
 
     var dragOpts = {
@@ -637,6 +649,7 @@ proto.updateMainDrag = function(fullLayout) {
         gd: gd,
         subplot: _this.id,
         plotinfo: {
+            id: _this.id,
             xaxis: _this.xaxis,
             yaxis: _this.yaxis
         },
@@ -727,7 +740,7 @@ proto.updateMainDrag = function(fullLayout) {
     function zoomPrep() {
         r0 = null;
         r1 = null;
-        path0 = _this.pathSector();
+        path0 = _this.pathSubplot();
         dimmed = false;
 
         var polarLayoutNow = gd._fullLayout[_this.id];
@@ -742,7 +755,7 @@ proto.updateMainDrag = function(fullLayout) {
     // N.B. this sets scoped 'r0' and 'r1'
     // return true if 'valid' zoom distance, false otherwise
     function clampAndSetR0R1(rr0, rr1) {
-        rr1 = Math.min(rr1, radius);
+        rr1 = Math.max(Math.min(rr1, radius), innerRadius);
 
         // starting or ending drag near center (outer edge),
         // clamps radial distance at origin (at r=radius)
@@ -831,16 +844,38 @@ proto.updateMainDrag = function(fullLayout) {
 
         dragBox.showDoubleClickNotifier(gd);
 
-        var radialAxis = _this.radialAxis;
         var rl = radialAxis._rl;
-        var drl = rl[1] - rl[0];
-        var updateObj = {};
-        updateObj[_this.id + '.radialaxis.range'] = [
-            rl[0] + r0 * drl / radius,
-            rl[0] + r1 * drl / radius
+        var m = (rl[1] - rl[0]) / (1 - innerRadius / radius) / radius;
+        var newRng = [
+            rl[0] + (r0 - innerRadius) * m,
+            rl[0] + (r1 - innerRadius) * m
         ];
+        Registry.call('relayout', gd, _this.id + '.radialaxis.range', newRng);
+    }
 
-        Registry.call('relayout', gd, updateObj);
+    function zoomClick(numClicks, evt) {
+        var clickMode = gd._fullLayout.clickmode;
+
+        dragBox.removeZoombox(gd);
+
+        // TODO double once vs twice logic (autorange vs fixed range)
+        if(numClicks === 2) {
+            var updateObj = {};
+            for(var k in _this.viewInitial) {
+                updateObj[_this.id + '.' + k] = _this.viewInitial[k];
+            }
+
+            gd.emit('plotly_doubleclick', null);
+            Registry.call('relayout', gd, updateObj);
+        }
+
+        if(clickMode.indexOf('select') > -1 && numClicks === 1) {
+            selectOnClick(evt, gd, [_this.xaxis], [_this.yaxis], _this.id, dragOpts);
+        }
+
+        if(clickMode.indexOf('event') > -1) {
+            Fx.click(gd, evt, _this.id);
+        }
     }
 
     dragOpts.prepFn = function(evt, startX, startY) {
@@ -865,6 +900,7 @@ proto.updateMainDrag = function(fullLayout) {
                 } else {
                     dragOpts.moveFn = zoomMove;
                 }
+                dragOpts.clickFn = zoomClick;
                 dragOpts.doneFn = zoomDone;
                 zoomPrep(evt, startX, startY);
                 break;
@@ -875,23 +911,6 @@ proto.updateMainDrag = function(fullLayout) {
         }
     };
 
-    dragOpts.clickFn = function(numClicks, evt) {
-        dragBox.removeZoombox(gd);
-
-        // TODO double once vs twice logic (autorange vs fixed range)
-        if(numClicks === 2) {
-            var updateObj = {};
-            for(var k in _this.viewInitial) {
-                updateObj[_this.id + '.' + k] = _this.viewInitial[k];
-            }
-
-            gd.emit('plotly_doubleclick', null);
-            Registry.call('relayout', gd, updateObj);
-        }
-
-        Fx.click(gd, evt, _this.id);
-    };
-
     mainDrag.onmousemove = function(evt) {
         Fx.hover(gd, evt, _this.id);
         gd._fullLayout._lasthover = mainDrag;
@@ -906,38 +925,52 @@ proto.updateMainDrag = function(fullLayout) {
     dragElement.init(dragOpts);
 };
 
-proto.updateRadialDrag = function(fullLayout) {
+proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) {
     var _this = this;
     var gd = _this.gd;
     var layers = _this.layers;
     var radius = _this.radius;
+    var innerRadius = _this.innerRadius;
     var cx = _this.cx;
     var cy = _this.cy;
     var radialAxis = _this.radialAxis;
+    var bl = constants.radialDragBoxSize;
+    var bl2 = bl / 2;
 
     if(!radialAxis.visible) return;
 
     var angle0 = deg2rad(_this.radialAxisAngle);
-    var rl0 = radialAxis._rl[0];
-    var rl1 = radialAxis._rl[1];
-    var drl = rl1 - rl0;
+    var rl = radialAxis._rl;
+    var rl0 = rl[0];
+    var rl1 = rl[1];
+    var rbase = rl[rngIndex];
+    var m = 0.75 * (rl[1] - rl[0]) / (1 - polarLayout.hole) / radius;
+
+    var tx, ty, className;
+    if(rngIndex) {
+        tx = cx + (radius + bl2) * Math.cos(angle0);
+        ty = cy - (radius + bl2) * Math.sin(angle0);
+        className = 'radialdrag';
+    } else {
+        // the 'inner' box can get called:
+        // - when polar.hole>0
+        // - when polar.sector isn't a full circle
+        // otherwise it is hidden behind the main drag.
+        tx = cx + (innerRadius - bl2) * Math.cos(angle0);
+        ty = cy - (innerRadius - bl2) * Math.sin(angle0);
+        className = 'radialdrag-inner';
+    }
 
-    var bl = constants.radialDragBoxSize;
-    var bl2 = bl / 2;
-    var radialDrag = dragBox.makeRectDragger(layers, 'radialdrag', 'crosshair', -bl2, -bl2, bl, bl);
+    var radialDrag = dragBox.makeRectDragger(layers, className, 'crosshair', -bl2, -bl2, bl, bl);
     var dragOpts = {element: radialDrag, gd: gd};
-    var tx = cx + (radius + bl2) * Math.cos(angle0);
-    var ty = cy - (radius + bl2) * Math.sin(angle0);
-
-    d3.select(radialDrag)
-        .attr('transform', strTranslate(tx, ty));
+    d3.select(radialDrag).attr('transform', strTranslate(tx, ty));
 
     // move function (either rotate or re-range flavor)
     var moveFn2;
     // rotate angle on done
     var angle1;
-    // re-range range[1] on done
-    var rng1;
+    // re-range range[1] (or range[0]) on done
+    var rprime;
 
     function moveFn(dx, dy) {
         if(moveFn2) {
@@ -958,12 +991,15 @@ proto.updateRadialDrag = function(fullLayout) {
     function doneFn() {
         if(angle1 !== null) {
             Registry.call('relayout', gd, _this.id + '.radialaxis.angle', angle1);
-        } else if(rng1 !== null) {
-            Registry.call('relayout', gd, _this.id + '.radialaxis.range[1]', rng1);
+        } else if(rprime !== null) {
+            Registry.call('relayout', gd, _this.id + '.radialaxis.range[' + rngIndex + ']', rprime);
         }
     }
 
     function rotateMove(dx, dy) {
+        // disable for inner drag boxes
+        if(rngIndex === 0) return;
+
         var x1 = tx + dx;
         var y1 = ty + dy;
 
@@ -983,14 +1019,17 @@ proto.updateRadialDrag = function(fullLayout) {
     function rerangeMove(dx, dy) {
         // project (dx, dy) unto unit radial axis vector
         var dr = Lib.dot([dx, -dy], [Math.cos(angle0), Math.sin(angle0)]);
-        rng1 = rl1 - drl * dr / radius * 0.75;
+        rprime = rbase - m * dr;
 
-        // make sure new range[1] does not change the range[0] -> range[1] sign
-        if((drl > 0) !== (rng1 > rl0)) return;
+        // make sure rprime does not change the range[0] -> range[1] sign
+        if((m > 0) !== (rngIndex ? rprime > rl0 : rprime < rl1)) {
+            rprime = null;
+            return;
+        }
 
         // update radial range -> update c2g -> update _m,_b
-        radialAxis.range[1] = rng1;
-        radialAxis._rl[1] = rng1;
+        radialAxis.range[rngIndex] = rprime;
+        radialAxis._rl[rngIndex] = rprime;
         radialAxis.setGeometry();
         radialAxis.setScale();
 
@@ -1018,7 +1057,7 @@ proto.updateRadialDrag = function(fullLayout) {
     dragOpts.prepFn = function() {
         moveFn2 = null;
         angle1 = null;
-        rng1 = null;
+        rprime = null;
 
         dragOpts.moveFn = moveFn;
         dragOpts.doneFn = doneFn;
@@ -1193,7 +1232,6 @@ proto.isPtInside = function(d) {
 };
 
 proto.pathArc = function(r) {
-    r = r || this.radius;
     var sectorInRad = this.sectorInRad;
     var vangles = this.vangles;
     var fn = vangles ? helpers.pathPolygon : Lib.pathArc;
@@ -1201,7 +1239,6 @@ proto.pathArc = function(r) {
 };
 
 proto.pathSector = function(r) {
-    r = r || this.radius;
     var sectorInRad = this.sectorInRad;
     var vangles = this.vangles;
     var fn = vangles ? helpers.pathPolygon : Lib.pathSector;
@@ -1215,6 +1252,12 @@ proto.pathAnnulus = function(r0, r1) {
     return fn(r0, r1, sectorInRad[0], sectorInRad[1], vangles);
 };
 
+proto.pathSubplot = function() {
+    var r0 = this.innerRadius;
+    var r1 = this.radius;
+    return r0 ? this.pathAnnulus(r0, r1) : this.pathSector(r1);
+};
+
 proto.fillViewInitialKey = function(key, val) {
     if(!(key in this.viewInitial)) {
         this.viewInitial[key] = val;
diff --git a/src/plots/polar/set_convert.js b/src/plots/polar/set_convert.js
index 7eb8af9b852..684a8224cc8 100644
--- a/src/plots/polar/set_convert.js
+++ b/src/plots/polar/set_convert.js
@@ -34,7 +34,7 @@ var rad2deg = Lib.rad2deg;
  *
  * Radial axis coordinate systems:
  * - d, c and l: same as for cartesian axes
- * - g: like calcdata but translated about `radialaxis.range[0]`
+ * - g: like calcdata but translated about `radialaxis.range[0]` & `polar.hole`
  *
  * Angular axis coordinate systems:
  * - d: data, in whatever form it's provided
@@ -61,25 +61,29 @@ module.exports = function setConvert(ax, polarLayout, fullLayout) {
 };
 
 function setConvertRadial(ax, polarLayout) {
+    var subplot = polarLayout._subplot;
+
     ax.setGeometry = function() {
         var rl0 = ax._rl[0];
         var rl1 = ax._rl[1];
 
+        var b = subplot.innerRadius;
+        var m = (subplot.radius - b) / (rl1 - rl0);
+        var b2 = b / m;
+
         var rFilter = rl0 > rl1 ?
             function(v) { return v <= 0; } :
             function(v) { return v >= 0; };
 
         ax.c2g = function(v) {
             var r = ax.c2l(v) - rl0;
-            return rFilter(r) ? r : 0;
+            return (rFilter(r) ? r : 0) + b2;
         };
 
         ax.g2c = function(v) {
-            return ax.l2c(v + rl0);
+            return ax.l2c(v + rl0 - b2);
         };
 
-        var m = polarLayout._subplot.radius / (rl1 - rl0);
-
         ax.g2p = function(v) { return v * m; };
         ax.c2p = function(v) { return ax.g2p(ax.c2g(v)); };
     };
diff --git a/src/plots/ternary/index.js b/src/plots/ternary/index.js
index fa5a265cb01..fd119f5f336 100644
--- a/src/plots/ternary/index.js
+++ b/src/plots/ternary/index.js
@@ -17,17 +17,29 @@ var TERNARY = 'ternary';
 
 exports.name = TERNARY;
 
-exports.attr = 'subplot';
+var attr = exports.attr = 'subplot';
 
 exports.idRoot = TERNARY;
 
 exports.idRegex = exports.attrRegex = counterRegex(TERNARY);
 
-exports.attributes = require('./layout/attributes');
+var attributes = exports.attributes = {};
+attributes[attr] = {
+    valType: 'subplotid',
+    role: 'info',
+    dflt: 'ternary',
+    editType: 'calc',
+    description: [
+        'Sets a reference between this trace\'s data coordinates and',
+        'a ternary subplot.',
+        'If *ternary* (the default value), the data refer to `layout.ternary`.',
+        'If *ternary2*, the data refer to `layout.ternary2`, and so on.'
+    ].join(' ')
+};
 
-exports.layoutAttributes = require('./layout/layout_attributes');
+exports.layoutAttributes = require('./layout_attributes');
 
-exports.supplyLayoutDefaults = require('./layout/defaults');
+exports.supplyLayoutDefaults = require('./layout_defaults');
 
 exports.plot = function plotTernary(gd) {
     var fullLayout = gd._fullLayout;
diff --git a/src/plots/ternary/layout/attributes.js b/src/plots/ternary/layout/attributes.js
deleted file mode 100644
index 585b109cb10..00000000000
--- a/src/plots/ternary/layout/attributes.js
+++ /dev/null
@@ -1,25 +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';
-
-
-module.exports = {
-    subplot: {
-        valType: 'subplotid',
-        role: 'info',
-        dflt: 'ternary',
-        editType: 'calc',
-        description: [
-            'Sets a reference between this trace\'s data coordinates and',
-            'a ternary subplot.',
-            'If *ternary* (the default value), the data refer to `layout.ternary`.',
-            'If *ternary2*, the data refer to `layout.ternary2`, and so on.'
-        ].join(' ')
-    }
-};
diff --git a/src/plots/ternary/layout/axis_defaults.js b/src/plots/ternary/layout/axis_defaults.js
deleted file mode 100644
index aa4f984c22a..00000000000
--- a/src/plots/ternary/layout/axis_defaults.js
+++ /dev/null
@@ -1,76 +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';
-
-var Lib = require('../../../lib');
-var layoutAttributes = require('./axis_attributes');
-var handleTickLabelDefaults = require('../../cartesian/tick_label_defaults');
-var handleTickMarkDefaults = require('../../cartesian/tick_mark_defaults');
-var handleTickValueDefaults = require('../../cartesian/tick_value_defaults');
-var handleLineGridDefaults = require('../../cartesian/line_grid_defaults');
-
-module.exports = function supplyLayoutDefaults(containerIn, containerOut, options) {
-    function coerce(attr, dflt) {
-        return Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt);
-    }
-
-    containerOut.type = 'linear'; // no other types allowed for ternary
-
-    var dfltColor = coerce('color');
-    // if axis.color was provided, use it for fonts too; otherwise,
-    // inherit from global font color in case that was provided.
-    var dfltFontColor = (dfltColor !== layoutAttributes.color.dflt) ? dfltColor : options.font.color;
-
-    var axName = containerOut._name,
-        letterUpper = axName.charAt(0).toUpperCase(),
-        dfltTitle = 'Component ' + letterUpper;
-
-    var title = coerce('title', dfltTitle);
-    containerOut._hovertitle = title === dfltTitle ? title : letterUpper;
-
-    Lib.coerceFont(coerce, 'titlefont', {
-        family: options.font.family,
-        size: Math.round(options.font.size * 1.2),
-        color: dfltFontColor
-    });
-
-    // range is just set by 'min' - max is determined by the other axes mins
-    coerce('min');
-
-    handleTickValueDefaults(containerIn, containerOut, coerce, 'linear');
-    handleTickLabelDefaults(containerIn, containerOut, coerce, 'linear', {});
-    handleTickMarkDefaults(containerIn, containerOut, coerce,
-        { outerTicks: true });
-
-    var showTickLabels = coerce('showticklabels');
-    if(showTickLabels) {
-        Lib.coerceFont(coerce, 'tickfont', {
-            family: options.font.family,
-            size: options.font.size,
-            color: dfltFontColor
-        });
-        coerce('tickangle');
-        coerce('tickformat');
-    }
-
-    handleLineGridDefaults(containerIn, containerOut, coerce, {
-        dfltColor: dfltColor,
-        bgColor: options.bgColor,
-        // default grid color is darker here (60%, vs cartesian default ~91%)
-        // because the grid is not square so the eye needs heavier cues to follow
-        blend: 60,
-        showLine: true,
-        showGrid: true,
-        noZeroLine: true,
-        attributes: layoutAttributes
-    });
-
-    coerce('hoverformat');
-    coerce('layer');
-};
diff --git a/src/plots/ternary/layout/defaults.js b/src/plots/ternary/layout/defaults.js
deleted file mode 100644
index 12bcdf22499..00000000000
--- a/src/plots/ternary/layout/defaults.js
+++ /dev/null
@@ -1,63 +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';
-
-var Color = require('../../../components/color');
-var Template = require('../../../plot_api/plot_template');
-
-var handleSubplotDefaults = require('../../subplot_defaults');
-var layoutAttributes = require('./layout_attributes');
-var handleAxisDefaults = require('./axis_defaults');
-
-var axesNames = ['aaxis', 'baxis', 'caxis'];
-
-module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
-    handleSubplotDefaults(layoutIn, layoutOut, fullData, {
-        type: 'ternary',
-        attributes: layoutAttributes,
-        handleDefaults: handleTernaryDefaults,
-        font: layoutOut.font,
-        paper_bgcolor: layoutOut.paper_bgcolor
-    });
-};
-
-function handleTernaryDefaults(ternaryLayoutIn, ternaryLayoutOut, coerce, options) {
-    var bgColor = coerce('bgcolor');
-    var sum = coerce('sum');
-    options.bgColor = Color.combine(bgColor, options.paper_bgcolor);
-    var axName, containerIn, containerOut;
-
-    // TODO: allow most (if not all) axis attributes to be set
-    // in the outer container and used as defaults in the individual axes?
-
-    for(var j = 0; j < axesNames.length; j++) {
-        axName = axesNames[j];
-        containerIn = ternaryLayoutIn[axName] || {};
-        containerOut = Template.newContainer(ternaryLayoutOut, axName);
-        containerOut._name = axName;
-
-        handleAxisDefaults(containerIn, containerOut, options);
-    }
-
-    // if the min values contradict each other, set them all to default (0)
-    // and delete *all* the inputs so the user doesn't get confused later by
-    // changing one and having them all change.
-    var aaxis = ternaryLayoutOut.aaxis,
-        baxis = ternaryLayoutOut.baxis,
-        caxis = ternaryLayoutOut.caxis;
-    if(aaxis.min + baxis.min + caxis.min >= sum) {
-        aaxis.min = 0;
-        baxis.min = 0;
-        caxis.min = 0;
-        if(ternaryLayoutIn.aaxis) delete ternaryLayoutIn.aaxis.min;
-        if(ternaryLayoutIn.baxis) delete ternaryLayoutIn.baxis.min;
-        if(ternaryLayoutIn.caxis) delete ternaryLayoutIn.caxis.min;
-    }
-}
diff --git a/src/plots/ternary/layout/layout_attributes.js b/src/plots/ternary/layout/layout_attributes.js
deleted file mode 100644
index 77c06326974..00000000000
--- a/src/plots/ternary/layout/layout_attributes.js
+++ /dev/null
@@ -1,38 +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';
-
-var colorAttrs = require('../../../components/color/attributes');
-var domainAttrs = require('../../domain').attributes;
-var ternaryAxesAttrs = require('./axis_attributes');
-var overrideAll = require('../../../plot_api/edit_types').overrideAll;
-
-module.exports = overrideAll({
-    domain: domainAttrs({name: 'ternary'}),
-
-    bgcolor: {
-        valType: 'color',
-        role: 'style',
-        dflt: colorAttrs.background,
-        description: 'Set the background color of the subplot'
-    },
-    sum: {
-        valType: 'number',
-        role: 'info',
-        dflt: 1,
-        min: 0,
-        description: [
-            'The number each triplet should sum to,',
-            'and the maximum range of each axis'
-        ].join(' ')
-    },
-    aaxis: ternaryAxesAttrs,
-    baxis: ternaryAxesAttrs,
-    caxis: ternaryAxesAttrs
-}, 'plot', 'from-root');
diff --git a/src/plots/ternary/layout/axis_attributes.js b/src/plots/ternary/layout_attributes.js
similarity index 67%
rename from src/plots/ternary/layout/axis_attributes.js
rename to src/plots/ternary/layout_attributes.js
index 2ed0bcc74e2..72397f0f876 100644
--- a/src/plots/ternary/layout/axis_attributes.js
+++ b/src/plots/ternary/layout_attributes.js
@@ -8,12 +8,14 @@
 
 'use strict';
 
+var colorAttrs = require('../../components/color/attributes');
+var domainAttrs = require('../domain').attributes;
+var axesAttrs = require('../cartesian/layout_attributes');
 
-var axesAttrs = require('../../cartesian/layout_attributes');
-var extendFlat = require('../../../lib/extend').extendFlat;
+var overrideAll = require('../../plot_api/edit_types').overrideAll;
+var extendFlat = require('../../lib/extend').extendFlat;
 
-
-module.exports = {
+var ternaryAxesAttrs = {
     title: axesAttrs.title,
     titlefont: axesAttrs.titlefont,
     color: axesAttrs.color,
@@ -63,3 +65,27 @@ module.exports = {
         ].join(' ')
     }
 };
+
+module.exports = overrideAll({
+    domain: domainAttrs({name: 'ternary'}),
+
+    bgcolor: {
+        valType: 'color',
+        role: 'style',
+        dflt: colorAttrs.background,
+        description: 'Set the background color of the subplot'
+    },
+    sum: {
+        valType: 'number',
+        role: 'info',
+        dflt: 1,
+        min: 0,
+        description: [
+            'The number each triplet should sum to,',
+            'and the maximum range of each axis'
+        ].join(' ')
+    },
+    aaxis: ternaryAxesAttrs,
+    baxis: ternaryAxesAttrs,
+    caxis: ternaryAxesAttrs
+}, 'plot', 'from-root');
diff --git a/src/plots/ternary/layout_defaults.js b/src/plots/ternary/layout_defaults.js
new file mode 100644
index 00000000000..7952a4559a5
--- /dev/null
+++ b/src/plots/ternary/layout_defaults.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 Color = require('../../components/color');
+var Template = require('../../plot_api/plot_template');
+var Lib = require('../../lib');
+
+var handleSubplotDefaults = require('../subplot_defaults');
+var handleTickLabelDefaults = require('../cartesian/tick_label_defaults');
+var handleTickMarkDefaults = require('../cartesian/tick_mark_defaults');
+var handleTickValueDefaults = require('../cartesian/tick_value_defaults');
+var handleLineGridDefaults = require('../cartesian/line_grid_defaults');
+var layoutAttributes = require('./layout_attributes');
+
+var axesNames = ['aaxis', 'baxis', 'caxis'];
+
+module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
+    handleSubplotDefaults(layoutIn, layoutOut, fullData, {
+        type: 'ternary',
+        attributes: layoutAttributes,
+        handleDefaults: handleTernaryDefaults,
+        font: layoutOut.font,
+        paper_bgcolor: layoutOut.paper_bgcolor
+    });
+};
+
+function handleTernaryDefaults(ternaryLayoutIn, ternaryLayoutOut, coerce, options) {
+    var bgColor = coerce('bgcolor');
+    var sum = coerce('sum');
+    options.bgColor = Color.combine(bgColor, options.paper_bgcolor);
+    var axName, containerIn, containerOut;
+
+    // TODO: allow most (if not all) axis attributes to be set
+    // in the outer container and used as defaults in the individual axes?
+
+    for(var j = 0; j < axesNames.length; j++) {
+        axName = axesNames[j];
+        containerIn = ternaryLayoutIn[axName] || {};
+        containerOut = Template.newContainer(ternaryLayoutOut, axName);
+        containerOut._name = axName;
+
+        handleAxisDefaults(containerIn, containerOut, options);
+    }
+
+    // if the min values contradict each other, set them all to default (0)
+    // and delete *all* the inputs so the user doesn't get confused later by
+    // changing one and having them all change.
+    var aaxis = ternaryLayoutOut.aaxis,
+        baxis = ternaryLayoutOut.baxis,
+        caxis = ternaryLayoutOut.caxis;
+    if(aaxis.min + baxis.min + caxis.min >= sum) {
+        aaxis.min = 0;
+        baxis.min = 0;
+        caxis.min = 0;
+        if(ternaryLayoutIn.aaxis) delete ternaryLayoutIn.aaxis.min;
+        if(ternaryLayoutIn.baxis) delete ternaryLayoutIn.baxis.min;
+        if(ternaryLayoutIn.caxis) delete ternaryLayoutIn.caxis.min;
+    }
+}
+
+function handleAxisDefaults(containerIn, containerOut, options) {
+    var axAttrs = layoutAttributes[containerOut._name];
+
+    function coerce(attr, dflt) {
+        return Lib.coerce(containerIn, containerOut, axAttrs, attr, dflt);
+    }
+
+    containerOut.type = 'linear'; // no other types allowed for ternary
+
+    var dfltColor = coerce('color');
+    // if axis.color was provided, use it for fonts too; otherwise,
+    // inherit from global font color in case that was provided.
+    var dfltFontColor = (dfltColor !== axAttrs.color.dflt) ? dfltColor : options.font.color;
+
+    var axName = containerOut._name,
+        letterUpper = axName.charAt(0).toUpperCase(),
+        dfltTitle = 'Component ' + letterUpper;
+
+    var title = coerce('title', dfltTitle);
+    containerOut._hovertitle = title === dfltTitle ? title : letterUpper;
+
+    Lib.coerceFont(coerce, 'titlefont', {
+        family: options.font.family,
+        size: Math.round(options.font.size * 1.2),
+        color: dfltFontColor
+    });
+
+    // range is just set by 'min' - max is determined by the other axes mins
+    coerce('min');
+
+    handleTickValueDefaults(containerIn, containerOut, coerce, 'linear');
+    handleTickLabelDefaults(containerIn, containerOut, coerce, 'linear', {});
+    handleTickMarkDefaults(containerIn, containerOut, coerce,
+        { outerTicks: true });
+
+    var showTickLabels = coerce('showticklabels');
+    if(showTickLabels) {
+        Lib.coerceFont(coerce, 'tickfont', {
+            family: options.font.family,
+            size: options.font.size,
+            color: dfltFontColor
+        });
+        coerce('tickangle');
+        coerce('tickformat');
+    }
+
+    handleLineGridDefaults(containerIn, containerOut, coerce, {
+        dfltColor: dfltColor,
+        bgColor: options.bgColor,
+        // default grid color is darker here (60%, vs cartesian default ~91%)
+        // because the grid is not square so the eye needs heavier cues to follow
+        blend: 60,
+        showLine: true,
+        showGrid: true,
+        noZeroLine: true,
+        attributes: axAttrs
+    });
+
+    coerce('hoverformat');
+    coerce('layer');
+}
diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js
index 436397bc5aa..a4ec4a7427c 100644
--- a/src/plots/ternary/ternary.js
+++ b/src/plots/ternary/ternary.js
@@ -25,6 +25,7 @@ var dragElement = require('../../components/dragelement');
 var Fx = require('../../components/fx');
 var Titles = require('../../components/titles');
 var prepSelect = require('../cartesian/select').prepSelect;
+var selectOnClick = require('../cartesian/select').selectOnClick;
 var clearSelect = require('../cartesian/select').clearSelect;
 var constants = require('../cartesian/constants');
 
@@ -33,6 +34,12 @@ function Ternary(options, fullLayout) {
     this.graphDiv = options.graphDiv;
     this.init(fullLayout);
     this.makeFramework(fullLayout);
+
+    // unfortunately, we have to keep track of some axis tick settings
+    // as ternary subplots do not implement the 'ticks' editType
+    this.aTickLayout = null;
+    this.bTickLayout = null;
+    this.cTickLayout = null;
 }
 
 module.exports = Ternary;
@@ -253,6 +260,8 @@ proto.adjustLayout = function(ternaryLayout, graphSize) {
         domain: [yDomain0, yDomain0 + yDomainFinal * w_over_h],
         _axislayer: _this.layers.aaxis,
         _gridlayer: _this.layers.agrid,
+        anchor: 'free',
+        position: 0,
         _pos: 0, // _this.xaxis.domain[0] * graphSize.w,
         _id: 'y',
         _length: w,
@@ -273,6 +282,8 @@ proto.adjustLayout = function(ternaryLayout, graphSize) {
         _axislayer: _this.layers.baxis,
         _gridlayer: _this.layers.bgrid,
         _counteraxis: _this.aaxis,
+        anchor: 'free',
+        position: 0,
         _pos: 0, // (1 - yDomain0) * graphSize.h,
         _id: 'x',
         _length: w,
@@ -295,6 +306,8 @@ proto.adjustLayout = function(ternaryLayout, graphSize) {
         _axislayer: _this.layers.caxis,
         _gridlayer: _this.layers.cgrid,
         _counteraxis: _this.baxis,
+        anchor: 'free',
+        position: 0,
         _pos: 0, // _this.xaxis.domain[1] * graphSize.w,
         _id: 'y',
         _length: w,
@@ -368,12 +381,33 @@ proto.adjustLayout = function(ternaryLayout, graphSize) {
 };
 
 proto.drawAxes = function(doTitles) {
-    var _this = this,
-        gd = _this.graphDiv,
-        titlesuffix = _this.id.substr(7) + 'title',
-        aaxis = _this.aaxis,
-        baxis = _this.baxis,
-        caxis = _this.caxis;
+    var _this = this;
+    var gd = _this.graphDiv;
+    var titlesuffix = _this.id.substr(7) + 'title';
+    var layers = _this.layers;
+    var aaxis = _this.aaxis;
+    var baxis = _this.baxis;
+    var caxis = _this.caxis;
+    var newTickLayout;
+
+    newTickLayout = strTickLayout(aaxis);
+    if(_this.aTickLayout !== newTickLayout) {
+        layers.aaxis.selectAll('.ytick').remove();
+        _this.aTickLayout = newTickLayout;
+    }
+
+    newTickLayout = strTickLayout(baxis);
+    if(_this.bTickLayout !== newTickLayout) {
+        layers.baxis.selectAll('.xtick').remove();
+        _this.bTickLayout = newTickLayout;
+    }
+
+    newTickLayout = strTickLayout(caxis);
+    if(_this.cTickLayout !== newTickLayout) {
+        layers.caxis.selectAll('.ytick').remove();
+        _this.cTickLayout = newTickLayout;
+    }
+
     // 3rd arg true below skips titles, so we can configure them
     // correctly later on.
     Axes.doTicksSingle(gd, aaxis, true);
@@ -423,6 +457,11 @@ proto.drawAxes = function(doTitles) {
     }
 };
 
+function strTickLayout(axLayout) {
+    return axLayout.ticks + String(axLayout.ticklen) + String(axLayout.showticklabels);
+}
+
+
 // hard coded paths for zoom corners
 // uses the same sizing as cartesian, length is MINZOOM/2, width is 3px
 var CLEN = constants.MINZOOM / 2 + 0.87;
@@ -452,6 +491,7 @@ proto.initInteractions = function() {
         element: dragger,
         gd: gd,
         plotinfo: {
+            id: _this.id,
             xaxis: _this.xaxis,
             yaxis: _this.yaxis
         },
@@ -462,21 +502,19 @@ proto.initInteractions = function() {
             dragOptions.xaxes = [_this.xaxis];
             dragOptions.yaxes = [_this.yaxis];
             var dragModeNow = gd._fullLayout.dragmode;
-            if(e.shiftKey) {
-                if(dragModeNow === 'pan') dragModeNow = 'zoom';
-                else dragModeNow = 'pan';
-            }
 
             if(dragModeNow === 'lasso') dragOptions.minDrag = 1;
             else dragOptions.minDrag = undefined;
 
             if(dragModeNow === 'zoom') {
                 dragOptions.moveFn = zoomMove;
+                dragOptions.clickFn = clickZoomPan;
                 dragOptions.doneFn = zoomDone;
                 zoomPrep(e, startX, startY);
             }
             else if(dragModeNow === 'pan') {
                 dragOptions.moveFn = plotDrag;
+                dragOptions.clickFn = clickZoomPan;
                 dragOptions.doneFn = dragDone;
                 panPrep();
                 clearSelect(zoomContainer);
@@ -484,24 +522,34 @@ proto.initInteractions = function() {
             else if(dragModeNow === 'select' || dragModeNow === 'lasso') {
                 prepSelect(e, startX, startY, dragOptions, dragModeNow);
             }
-        },
-        clickFn: function(numClicks, evt) {
-            removeZoombox(gd);
-
-            if(numClicks === 2) {
-                var attrs = {};
-                attrs[_this.id + '.aaxis.min'] = 0;
-                attrs[_this.id + '.baxis.min'] = 0;
-                attrs[_this.id + '.caxis.min'] = 0;
-                gd.emit('plotly_doubleclick', null);
-                Registry.call('relayout', gd, attrs);
-            }
-            Fx.click(gd, evt, _this.id);
         }
     };
 
     var x0, y0, mins0, span0, mins, lum, path0, dimmed, zb, corners;
 
+    function clickZoomPan(numClicks, evt) {
+        var clickMode = gd._fullLayout.clickmode;
+
+        removeZoombox(gd);
+
+        if(numClicks === 2) {
+            var attrs = {};
+            attrs[_this.id + '.aaxis.min'] = 0;
+            attrs[_this.id + '.baxis.min'] = 0;
+            attrs[_this.id + '.caxis.min'] = 0;
+            gd.emit('plotly_doubleclick', null);
+            Registry.call('relayout', gd, attrs);
+        }
+
+        if(clickMode.indexOf('select') > -1 && numClicks === 1) {
+            selectOnClick(evt, gd, [_this.xaxis], [_this.yaxis], _this.id, dragOptions);
+        }
+
+        if(clickMode.indexOf('event') > -1) {
+            Fx.click(gd, evt, _this.id);
+        }
+    }
+
     function zoomPrep(e, startX, startY) {
         var dragBBox = dragger.getBoundingClientRect();
         x0 = startX - dragBBox.left;
diff --git a/src/traces/bar/select.js b/src/traces/bar/select.js
index 04ede09356c..4d80b7b4836 100644
--- a/src/traces/bar/select.js
+++ b/src/traces/bar/select.js
@@ -8,14 +8,14 @@
 
 'use strict';
 
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
     var cd = searchInfo.cd;
     var xa = searchInfo.xaxis;
     var ya = searchInfo.yaxis;
     var selection = [];
     var i;
 
-    if(polygon === false) {
+    if(selectionTester === false) {
         // clear selection
         for(i = 0; i < cd.length; i++) {
             cd[i].selected = 0;
@@ -24,7 +24,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
         for(i = 0; i < cd.length; i++) {
             var di = cd[i];
 
-            if(polygon.contains(di.ct)) {
+            if(selectionTester.contains(di.ct, false, i, searchInfo)) {
                 selection.push({
                     pointNumber: i,
                     x: xa.c2d(di.x),
diff --git a/src/traces/box/event_data.js b/src/traces/box/event_data.js
new file mode 100644
index 00000000000..a12ee8eb67a
--- /dev/null
+++ b/src/traces/box/event_data.js
@@ -0,0 +1,24 @@
+/**
+* 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 = function eventData(out, pt) {
+
+    // Note: hoverOnBox property is needed for click-to-select
+    // to ignore when a box was clicked. This is the reason box
+    // implements this custom eventData function.
+    if(pt.hoverOnBox) out.hoverOnBox = pt.hoverOnBox;
+
+    if('xVal' in pt) out.x = pt.xVal;
+    if('yVal' in pt) out.y = pt.yVal;
+    if(pt.xa) out.xaxis = pt.xa;
+    if(pt.ya) out.yaxis = pt.ya;
+
+    return out;
+};
diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js
index 79e24509360..b4729279e1c 100644
--- a/src/traces/box/hover.js
+++ b/src/traces/box/hover.js
@@ -169,6 +169,10 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) {
         pointData2[vLetter + 'LabelVal'] = val;
         pointData2[vLetter + 'Label'] = (t.labels ? t.labels[attr] + ' ' : '') + Axes.hoverLabelText(vAxis, val);
 
+        // Note: introduced to be able to distinguish a
+        // clicked point from a box during click-to-select
+        pointData2.hoverOnBox = true;
+
         if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') {
             pointData2[vLetter + 'err'] = di.sd;
         }
diff --git a/src/traces/box/index.js b/src/traces/box/index.js
index 3ad049e1701..17931ec782d 100644
--- a/src/traces/box/index.js
+++ b/src/traces/box/index.js
@@ -20,6 +20,7 @@ Box.plot = require('./plot').plot;
 Box.style = require('./style').style;
 Box.styleOnSelect = require('./style').styleOnSelect;
 Box.hoverPoints = require('./hover').hoverPoints;
+Box.eventData = require('./event_data');
 Box.selectPoints = require('./select');
 
 Box.moduleType = 'trace';
diff --git a/src/traces/box/select.js b/src/traces/box/select.js
index 9ec9ed03e3f..069b9b1896f 100644
--- a/src/traces/box/select.js
+++ b/src/traces/box/select.js
@@ -8,14 +8,14 @@
 
 'use strict';
 
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
     var cd = searchInfo.cd;
     var xa = searchInfo.xaxis;
     var ya = searchInfo.yaxis;
     var selection = [];
     var i, j;
 
-    if(polygon === false) {
+    if(selectionTester === false) {
         for(i = 0; i < cd.length; i++) {
             for(j = 0; j < (cd[i].pts || []).length; j++) {
                 // clear selection
@@ -29,7 +29,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
                 var x = xa.c2p(pt.x);
                 var y = ya.c2p(pt.y);
 
-                if(polygon.contains([x, y])) {
+                if(selectionTester.contains([x, y], null, pt.i, searchInfo)) {
                     selection.push({
                         pointNumber: pt.i,
                         x: xa.c2d(pt.x),
diff --git a/src/traces/choropleth/select.js b/src/traces/choropleth/select.js
index c3a8f332c4d..9052c06a74e 100644
--- a/src/traces/choropleth/select.js
+++ b/src/traces/choropleth/select.js
@@ -8,7 +8,7 @@
 
 'use strict';
 
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
     var cd = searchInfo.cd;
     var xa = searchInfo.xaxis;
     var ya = searchInfo.yaxis;
@@ -16,7 +16,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
 
     var i, di, ct, x, y;
 
-    if(polygon === false) {
+    if(selectionTester === false) {
         for(i = 0; i < cd.length; i++) {
             cd[i].selected = 0;
         }
@@ -30,7 +30,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
             x = xa.c2p(ct);
             y = ya.c2p(ct);
 
-            if(polygon.contains([x, y])) {
+            if(selectionTester.contains([x, y], null, i, searchInfo)) {
                 selection.push({
                     pointNumber: i,
                     lon: ct[0],
diff --git a/src/traces/ohlc/select.js b/src/traces/ohlc/select.js
index 29bed35028f..a588e2ac164 100644
--- a/src/traces/ohlc/select.js
+++ b/src/traces/ohlc/select.js
@@ -8,7 +8,7 @@
 
 'use strict';
 
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
     var cd = searchInfo.cd;
     var xa = searchInfo.xaxis;
     var ya = searchInfo.yaxis;
@@ -17,7 +17,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
     // for (potentially grouped) candlesticks
     var posOffset = cd[0].t.bPos || 0;
 
-    if(polygon === false) {
+    if(selectionTester === false) {
         // clear selection
         for(i = 0; i < cd.length; i++) {
             cd[i].selected = 0;
@@ -26,7 +26,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
         for(i = 0; i < cd.length; i++) {
             var di = cd[i];
 
-            if(polygon.contains([xa.c2p(di.pos + posOffset), ya.c2p(di.yc)])) {
+            if(selectionTester.contains([xa.c2p(di.pos + posOffset), ya.c2p(di.yc)], null, di.i, searchInfo)) {
                 selection.push({
                     pointNumber: di.i,
                     x: xa.c2d(di.pos),
diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js
index 56980cdcf22..f881a5f6223 100644
--- a/src/traces/scatter/plot.js
+++ b/src/traces/scatter/plot.js
@@ -13,6 +13,8 @@ var d3 = require('d3');
 
 var Registry = require('../../registry');
 var Lib = require('../../lib');
+var ensureSingle = Lib.ensureSingle;
+var identity = Lib.identity;
 var Drawing = require('../../components/drawing');
 
 var subTypes = require('./subtypes');
@@ -43,7 +45,7 @@ module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transition
     // the z-order of fill layers is correct.
     linkTraces(gd, plotinfo, cdscatter);
 
-    createFills(gd, scatterLayer, plotinfo);
+    createFills(gd, join, plotinfo);
 
     // Sort the traces, once created, so that the ordering is preserved even when traces
     // are shown and hidden. This is needed since we're not just wiping everything out
@@ -52,10 +54,10 @@ module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transition
         uids[cdscatter[i][0].trace.uid] = i;
     }
 
-    scatterLayer.selectAll('g.trace').sort(function(a, b) {
+    join.sort(function(a, b) {
         var idx1 = uids[a[0].trace.uid];
         var idx2 = uids[b[0].trace.uid];
-        return idx1 > idx2 ? 1 : -1;
+        return idx1 - idx2;
     });
 
     if(hasTransition) {
@@ -97,51 +99,45 @@ module.exports = function plot(gd, plotinfo, cdscatter, scatterLayer, transition
     scatterLayer.selectAll('path:not([d])').remove();
 };
 
-function createFills(gd, scatterLayer, plotinfo) {
-    var trace;
+function createFills(gd, traceJoin, plotinfo) {
+    traceJoin.each(function(d) {
+        var fills = ensureSingle(d3.select(this), 'g', 'fills');
+        Drawing.setClipUrl(fills, plotinfo.layerClipId);
 
-    scatterLayer.selectAll('g.trace').each(function(d) {
-        var tr = d3.select(this);
-
-        // Loop only over the traces being redrawn:
-        trace = d[0].trace;
+        var trace = d[0].trace;
 
-        // make the fill-to-next path now for the NEXT trace, so it shows
-        // behind both lines.
+        var fillData = [];
+        if(trace.fill && (trace.fill.substr(0, 6) === 'tozero' || trace.fill === 'toself' ||
+                (trace.fill.substr(0, 2) === 'to' && !trace._prevtrace))
+        ) {
+            fillData = ['_ownFill'];
+        }
         if(trace._nexttrace) {
-            trace._nextFill = tr.select('.js-fill.js-tonext');
-            if(!trace._nextFill.size()) {
+            // make the fill-to-next path now for the NEXT trace, so it shows
+            // behind both lines.
+            fillData.push('_nextFill');
+        }
 
-                // If there is an existing tozero fill, we must insert this *after* that fill:
-                var loc = ':first-child';
-                if(tr.select('.js-fill.js-tozero').size()) {
-                    loc += ' + *';
-                }
+        var fillJoin = fills.selectAll('g')
+            .data(fillData, identity);
 
-                trace._nextFill = tr.insert('path', loc).attr('class', 'js-fill js-tonext');
-            }
-        } else {
-            tr.selectAll('.js-fill.js-tonext').remove();
-            trace._nextFill = null;
-        }
+        fillJoin.enter().append('g');
 
-        if(trace.fill && (trace.fill.substr(0, 6) === 'tozero' || trace.fill === 'toself' ||
-                (trace.fill.substr(0, 2) === 'to' && !trace._prevtrace))) {
-            trace._ownFill = tr.select('.js-fill.js-tozero');
-            if(!trace._ownFill.size()) {
-                trace._ownFill = tr.insert('path', ':first-child').attr('class', 'js-fill js-tozero');
-            }
-        } else {
-            tr.selectAll('.js-fill.js-tozero').remove();
-            trace._ownFill = null;
-        }
+        fillJoin.exit()
+            .each(function(d) { trace[d] = null; })
+            .remove();
 
-        tr.selectAll('.js-fill').call(Drawing.setClipUrl, plotinfo.layerClipId);
+        fillJoin.order().each(function(d) {
+            // make a path element inside the fill group, just so
+            // we can give it its own data later on and the group can
+            // keep its simple '_*Fill' data
+            trace[d] = ensureSingle(d3.select(this), 'path', 'js-fill');
+        });
     });
 }
 
 function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transitionOpts) {
-    var join, i;
+    var i;
 
     // Since this has been reorganized and we're executing this on individual traces,
     // we need to pass it the full list of cdscatter as well as this trace's index (idx)
@@ -157,12 +153,17 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
     var xa = plotinfo.xaxis,
         ya = plotinfo.yaxis;
 
-    var trace = cdscatter[0].trace,
-        line = trace.line,
-        tr = d3.select(element);
+    var trace = cdscatter[0].trace;
+    var line = trace.line;
+    var tr = d3.select(element);
+
+    var errorBarGroup = ensureSingle(tr, 'g', 'errorbars');
+    var lines = ensureSingle(tr, 'g', 'lines');
+    var points = ensureSingle(tr, 'g', 'points');
+    var text = ensureSingle(tr, 'g', 'text');
 
     // error bars are at the bottom
-    Registry.getComponentMethod('errorbars', 'plot')(tr, plotinfo, transitionOpts);
+    Registry.getComponentMethod('errorbars', 'plot')(errorBarGroup, plotinfo, transitionOpts);
 
     if(trace.visible !== true) return;
 
@@ -303,7 +304,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
         };
     }
 
-    var lineJoin = tr.selectAll('.js-line').data(segments);
+    var lineJoin = lines.selectAll('.js-line').data(segments);
 
     transition(lineJoin.exit())
         .style('opacity', 0)
@@ -325,6 +326,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
 
     if(segments.length) {
         if(ownFillEl3) {
+            ownFillEl3.datum(cdscatter);
             if(pt0 && pt1) {
                 if(ownFillDir) {
                     if(ownFillDir === 'y') {
@@ -412,11 +414,10 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
         return false;
     }
 
-    function makePoints(d) {
+    function makePoints(points, text, cdscatter) {
         var join, selection, hasNode;
 
-        var trace = d[0].trace;
-        var s = d3.select(this);
+        var trace = cdscatter[0].trace;
         var showMarkers = subTypes.hasMarkers(trace);
         var showText = subTypes.hasText(trace);
 
@@ -425,7 +426,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
         var textFilter = hideFilter;
 
         if(showMarkers || showText) {
-            var showFilter = Lib.identity;
+            var showFilter = identity;
             // if we're stacking, "infer zero" gap mode gets markers in the
             // gap points - because we've inferred a zero there - but other
             // modes (currently "interpolate", later "interrupt" hopefully)
@@ -446,7 +447,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
 
         // marker points
 
-        selection = s.selectAll('path.point');
+        selection = points.selectAll('path.point');
 
         join = selection.data(markerFilter, keyFunc);
 
@@ -498,7 +499,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
         }
 
         // text points
-        selection = s.selectAll('g');
+        selection = text.selectAll('g');
         join = selection.data(textFilter, keyFunc);
 
         // each text needs to go in its own 'g' in case
@@ -537,28 +538,16 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
         join.exit().remove();
     }
 
-    // NB: selectAll is evaluated on instantiation:
-    var pointSelection = tr.selectAll('.points');
-
-    // Join with new data
-    join = pointSelection.data([cdscatter]);
-
-    // Transition existing, but don't defer this to an async .transition since
-    // there's no timing involved:
-    pointSelection.each(makePoints);
-
-    join.enter().append('g')
-        .classed('points', true)
-        .each(makePoints);
-
-    join.exit().remove();
+    points.datum(cdscatter);
+    text.datum(cdscatter);
+    makePoints(points, text, cdscatter);
 
     // lastly, clip points groups of `cliponaxis !== false` traces
     // on `plotinfo._hasClipOnAxisFalse === true` subplots
-    join.each(function(d) {
-        var hasClipOnAxisFalse = d[0].trace.cliponaxis === false;
-        Drawing.setClipUrl(d3.select(this), hasClipOnAxisFalse ? null : plotinfo.layerClipId);
-    });
+    var hasClipOnAxisFalse = trace.cliponaxis === false;
+    var clipUrl = hasClipOnAxisFalse ? null : plotinfo.layerClipId;
+    Drawing.setClipUrl(points, clipUrl);
+    Drawing.setClipUrl(text, clipUrl);
 }
 
 function selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll) {
diff --git a/src/traces/scatter/select.js b/src/traces/scatter/select.js
index e980a6d7400..79e1a689e41 100644
--- a/src/traces/scatter/select.js
+++ b/src/traces/scatter/select.js
@@ -11,7 +11,7 @@
 
 var subtypes = require('./subtypes');
 
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
     var cd = searchInfo.cd,
         xa = searchInfo.xaxis,
         ya = searchInfo.yaxis,
@@ -25,7 +25,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
     var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace));
     if(hasOnlyLines) return [];
 
-    if(polygon === false) { // clear selection
+    if(selectionTester === false) { // clear selection
         for(i = 0; i < cd.length; i++) {
             cd[i].selected = 0;
         }
@@ -36,7 +36,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
             x = xa.c2p(di.x);
             y = ya.c2p(di.y);
 
-            if((di.i !== null) && polygon.contains([x, y])) {
+            if((di.i !== null) && selectionTester.contains([x, y], false, i, searchInfo)) {
                 selection.push({
                     pointNumber: di.i,
                     x: xa.c2d(di.x),
diff --git a/src/traces/scatter/style.js b/src/traces/scatter/style.js
index b9090261e93..4ce72bdf388 100644
--- a/src/traces/scatter/style.js
+++ b/src/traces/scatter/style.js
@@ -26,6 +26,12 @@ function style(gd, cd) {
         stylePoints(sel, trace, gd);
     });
 
+    s.selectAll('g.text').each(function(d) {
+        var sel = d3.select(this);
+        var trace = d.trace || d[0].trace;
+        styleText(sel, trace, gd);
+    });
+
     s.selectAll('g.trace path.js-line')
         .call(Drawing.lineGroupStyle);
 
@@ -37,6 +43,9 @@ function style(gd, cd) {
 
 function stylePoints(sel, trace, gd) {
     Drawing.pointStyle(sel.selectAll('path.point'), trace, gd);
+}
+
+function styleText(sel, trace, gd) {
     Drawing.textPointStyle(sel.selectAll('text'), trace, gd);
 }
 
@@ -49,11 +58,13 @@ function styleOnSelect(gd, cd) {
         Drawing.selectedTextStyle(s.selectAll('text'), trace);
     } else {
         stylePoints(s, trace, gd);
+        styleText(s, trace, gd);
     }
 }
 
 module.exports = {
     style: style,
     stylePoints: stylePoints,
+    styleText: styleText,
     styleOnSelect: styleOnSelect
 };
diff --git a/src/traces/scattergeo/select.js b/src/traces/scattergeo/select.js
index 4c11e6c3196..b6b9fe2b212 100644
--- a/src/traces/scattergeo/select.js
+++ b/src/traces/scattergeo/select.js
@@ -11,7 +11,7 @@
 var subtypes = require('../scatter/subtypes');
 var BADNUM = require('../../constants/numerical').BADNUM;
 
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
     var cd = searchInfo.cd;
     var xa = searchInfo.xaxis;
     var ya = searchInfo.yaxis;
@@ -23,7 +23,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
     var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace));
     if(hasOnlyLines) return [];
 
-    if(polygon === false) {
+    if(selectionTester === false) {
         for(i = 0; i < cd.length; i++) {
             cd[i].selected = 0;
         }
@@ -38,7 +38,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
             x = xa.c2p(lonlat);
             y = ya.c2p(lonlat);
 
-            if(polygon.contains([x, y])) {
+            if(selectionTester.contains([x, y], null, i, searchInfo)) {
                 selection.push({
                     pointNumber: i,
                     lon: lonlat[0],
diff --git a/src/traces/scattergeo/style.js b/src/traces/scattergeo/style.js
index c4d9a644321..6d89407e62f 100644
--- a/src/traces/scattergeo/style.js
+++ b/src/traces/scattergeo/style.js
@@ -12,7 +12,9 @@ var d3 = require('d3');
 var Drawing = require('../../components/drawing');
 var Color = require('../../components/color');
 
-var stylePoints = require('../scatter/style').stylePoints;
+var scatterStyle = require('../scatter/style');
+var stylePoints = scatterStyle.stylePoints;
+var styleText = scatterStyle.styleText;
 
 module.exports = function style(gd, calcTrace) {
     if(calcTrace) styleTrace(gd, calcTrace);
@@ -25,6 +27,7 @@ function styleTrace(gd, calcTrace) {
     s.style('opacity', calcTrace[0].trace.opacity);
 
     stylePoints(s, trace, gd);
+    styleText(s, trace, gd);
 
     // this part is incompatible with Drawing.lineGroupStyle
     s.selectAll('path.js-line')
diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js
index 0bb6dabe015..14f8b27d021 100644
--- a/src/traces/scattergl/index.js
+++ b/src/traces/scattergl/index.js
@@ -13,7 +13,7 @@ var createLine = require('regl-line2d');
 var createError = require('regl-error2d');
 var cluster = require('point-cluster');
 var arrayRange = require('array-range');
-var Text = require('@etpinard/gl-text');
+var Text = require('gl-text');
 
 var Registry = require('../../registry');
 var Lib = require('../../lib');
@@ -431,7 +431,7 @@ function plot(gd, subplot, cdata) {
         if(scene.fill2d) {
             scene.fillOptions = scene.fillOptions.map(function(fillOptions, i) {
                 var cdscatter = cdata[i];
-                if(!fillOptions || !cdscatter || !cdscatter[0] || !cdscatter[0].trace) return null;
+                if(!fillOptions || !cdscatter || !cdscatter[0] || !cdscatter[0].trace) return;
                 var cd = cdscatter[0];
                 var trace = cd.trace;
                 var stash = cd.t;
@@ -524,6 +524,7 @@ function plot(gd, subplot, cdata) {
     scene.unselectBatch = null;
     var dragmode = fullLayout.dragmode;
     var selectMode = dragmode === 'lasso' || dragmode === 'select';
+    var clickSelectEnabled = fullLayout.clickmode.indexOf('select') > -1;
 
     for(i = 0; i < cdata.length; i++) {
         var cd0 = cdata[i][0];
@@ -533,7 +534,7 @@ function plot(gd, subplot, cdata) {
         var x = stash.x;
         var y = stash.y;
 
-        if(trace.selectedpoints || selectMode) {
+        if(trace.selectedpoints || selectMode || clickSelectEnabled) {
             if(!selectMode) selectMode = true;
 
             if(!scene.selectBatch) {
@@ -822,7 +823,7 @@ function calcHover(pointData, x, y, trace) {
 }
 
 
-function selectPoints(searchInfo, polygon) {
+function selectPoints(searchInfo, selectionTester) {
     var cd = searchInfo.cd;
     var selection = [];
     var trace = cd[0].trace;
@@ -844,10 +845,10 @@ function selectPoints(searchInfo, polygon) {
     var unels = null;
     // FIXME: clearing selection does not work here
     var i;
-    if(polygon !== false && !polygon.degenerate) {
+    if(selectionTester !== false && !selectionTester.degenerate) {
         els = [], unels = [];
         for(i = 0; i < stash.count; i++) {
-            if(polygon.contains([stash.xpx[i], stash.ypx[i]])) {
+            if(selectionTester.contains([stash.xpx[i], stash.ypx[i]], false, i, searchInfo)) {
                 els.push(i);
                 selection.push({
                     pointNumber: i,
diff --git a/src/traces/scattermapbox/select.js b/src/traces/scattermapbox/select.js
index dd6f3536903..34bbeedf0e6 100644
--- a/src/traces/scattermapbox/select.js
+++ b/src/traces/scattermapbox/select.js
@@ -12,7 +12,7 @@ var Lib = require('../../lib');
 var subtypes = require('../scatter/subtypes');
 var BADNUM = require('../../constants/numerical').BADNUM;
 
-module.exports = function selectPoints(searchInfo, polygon) {
+module.exports = function selectPoints(searchInfo, selectionTester) {
     var cd = searchInfo.cd;
     var xa = searchInfo.xaxis;
     var ya = searchInfo.yaxis;
@@ -22,7 +22,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
 
     if(!subtypes.hasMarkers(trace)) return [];
 
-    if(polygon === false) {
+    if(selectionTester === false) {
         for(i = 0; i < cd.length; i++) {
             cd[i].selected = 0;
         }
@@ -35,7 +35,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
                 var lonlat2 = [Lib.modHalf(lonlat[0], 360), lonlat[1]];
                 var xy = [xa.c2p(lonlat2), ya.c2p(lonlat2)];
 
-                if(polygon.contains(xy)) {
+                if(selectionTester.contains(xy, null, i, searchInfo)) {
                     selection.push({
                         pointNumber: i,
                         lon: lonlat[0],
diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js
index 902646cb17a..cbbade4c81b 100644
--- a/src/traces/splom/index.js
+++ b/src/traces/splom/index.js
@@ -229,7 +229,9 @@ function plotOne(gd, cd0) {
         scene.matrix = createMatrix(regl);
     }
 
-    var selectMode = dragmode === 'lasso' || dragmode === 'select' || !!trace.selectedpoints;
+    var clickSelectEnabled = fullLayout.clickmode.indexOf('select') > -1;
+    var selectMode = dragmode === 'lasso' || dragmode === 'select' ||
+      !!trace.selectedpoints || clickSelectEnabled;
     scene.selectBatch = null;
     scene.unselectBatch = null;
 
@@ -346,7 +348,7 @@ function hoverPoints(pointData, xval, yval) {
     return [pointData];
 }
 
-function selectPoints(searchInfo, polygon) {
+function selectPoints(searchInfo, selectionTester) {
     var cd = searchInfo.cd;
     var trace = cd[0].trace;
     var stash = cd[0].t;
@@ -375,10 +377,10 @@ function selectPoints(searchInfo, polygon) {
     // filter out points by visible scatter ones
     var els = null;
     var unels = null;
-    if(polygon !== false && !polygon.degenerate) {
+    if(selectionTester !== false && !selectionTester.degenerate) {
         els = [], unels = [];
         for(i = 0; i < x.length; i++) {
-            if(polygon.contains([xpx[i], ypx[i]])) {
+            if(selectionTester.contains([xpx[i], ypx[i]], null, i, searchInfo)) {
                 els.push(i);
                 selection.push({
                     pointNumber: i,
diff --git a/tasks/bundle.js b/tasks/bundle.js
index 7c6586c1c14..a4f0fce1516 100644
--- a/tasks/bundle.js
+++ b/tasks/bundle.js
@@ -50,7 +50,6 @@ tasks.push(function(cb) {
     _bundle(constants.pathToPlotlyIndex, constants.pathToPlotlyDist, {
         standalone: 'Plotly',
         debug: DEV,
-        compressAttrs: true,
         pathToMinBundle: constants.pathToPlotlyDistMin
     }, cb);
 });
@@ -62,11 +61,12 @@ tasks.push(function(cb) {
     }, cb);
 });
 
-// Browserify the plotly.js with meta
+// Browserify plotly.js with meta and output plot-schema JSON
 tasks.push(function(cb) {
     _bundle(constants.pathToPlotlyIndex, constants.pathToPlotlyDistWithMeta, {
         standalone: 'Plotly',
         debug: DEV,
+        noCompress: true
     }, function() {
         makeSchema(constants.pathToPlotlyDistWithMeta, constants.pathToSchema)();
         cb();
@@ -79,7 +79,6 @@ constants.partialBundlePaths.forEach(function(pathObj) {
         _bundle(pathObj.index, pathObj.dist, {
             standalone: 'Plotly',
             debug: DEV,
-            compressAttrs: true,
             pathToMinBundle: pathObj.distMin
         }, cb);
     });
diff --git a/tasks/cibundle.js b/tasks/cibundle.js
index 5a2310e2295..7b53995b81e 100644
--- a/tasks/cibundle.js
+++ b/tasks/cibundle.js
@@ -11,12 +11,10 @@ var _bundle = require('./util/browserify_wrapper');
  *  - plotly.min.js bundle in dist/ (for requirejs test)
  */
 
-
 // Browserify plotly.js and plotly.min.js
 _bundle(constants.pathToPlotlyIndex, constants.pathToPlotlyBuild, {
     standalone: 'Plotly',
     debug: true,
-    compressAttrs: true,
     pathToMinBundle: constants.pathToPlotlyDistMin
 });
 
diff --git a/tasks/util/compress_attributes.js b/tasks/compress_attributes.js
similarity index 100%
rename from tasks/util/compress_attributes.js
rename to tasks/compress_attributes.js
diff --git a/tasks/util/browserify_wrapper.js b/tasks/util/browserify_wrapper.js
index 362a6121097..7a4e49113b4 100644
--- a/tasks/util/browserify_wrapper.js
+++ b/tasks/util/browserify_wrapper.js
@@ -7,7 +7,6 @@ var derequire = require('derequire');
 var through = require('through2');
 
 var constants = require('./constants');
-var compressAttributes = require('./compress_attributes');
 var strictD3 = require('./strict_d3');
 
 /** Convenience browserify wrapper
@@ -20,7 +19,7 @@ var strictD3 = require('./strict_d3');
  *  - debug {boolean} [optional]
  *  Additional option:
  *  - pathToMinBundle {string} path to destination minified bundle
- *  - compressAttrs {boolean} do we compress attribute meta?
+ *  - noCompress {boolean} skip attribute meta compression?
  * @param {function} cb callback
  *
  * Outputs one bundle (un-minified) file if opts.pathToMinBundle is omitted
@@ -38,10 +37,11 @@ module.exports = function _bundle(pathToIndex, pathToBundle, opts, cb) {
     browserifyOpts.standalone = opts.standalone;
     browserifyOpts.debug = opts.debug;
 
-    browserifyOpts.transform = [];
-    if(opts.compressAttrs) {
-        browserifyOpts.transform.push(compressAttributes);
+    if(opts.noCompress) {
+        browserifyOpts.ignoreTransform = './tasks/compress_attributes.js';
     }
+
+    browserifyOpts.transform = [];
     if(opts.debug) {
         browserifyOpts.transform.push(strictD3);
     }
diff --git a/tasks/util/watchified_bundle.js b/tasks/util/watchified_bundle.js
index 864716dca6f..05fb07c9526 100644
--- a/tasks/util/watchified_bundle.js
+++ b/tasks/util/watchified_bundle.js
@@ -6,7 +6,6 @@ var prettySize = require('prettysize');
 
 var constants = require('./constants');
 var common = require('./common');
-var compressAttributes = require('./compress_attributes');
 var strictD3 = require('./strict_d3');
 
 /**
@@ -23,7 +22,7 @@ module.exports = function makeWatchifiedBundle(onFirstBundleCallback) {
     var b = browserify(constants.pathToPlotlyIndex, {
         debug: true,
         standalone: 'Plotly',
-        transform: [strictD3, compressAttributes],
+        transform: [strictD3],
         cache: {},
         packageCache: {},
         plugin: [watchify]
diff --git a/test/image/baselines/gl2d_fill-ordering.png b/test/image/baselines/gl2d_fill-ordering.png
new file mode 100644
index 00000000000..7b71d7d2dcb
Binary files /dev/null and b/test/image/baselines/gl2d_fill-ordering.png differ
diff --git a/test/image/baselines/gl2d_text_chart_basic.png b/test/image/baselines/gl2d_text_chart_basic.png
index 93488811117..afece63c14c 100644
Binary files a/test/image/baselines/gl2d_text_chart_basic.png and b/test/image/baselines/gl2d_text_chart_basic.png differ
diff --git a/test/image/baselines/gl2d_text_chart_single-string.png b/test/image/baselines/gl2d_text_chart_single-string.png
index 733e3584811..bebed9f57f3 100644
Binary files a/test/image/baselines/gl2d_text_chart_single-string.png and b/test/image/baselines/gl2d_text_chart_single-string.png differ
diff --git a/test/image/baselines/glpolar_scatter.png b/test/image/baselines/glpolar_scatter.png
index d278a1bd29e..d631ae1d118 100644
Binary files a/test/image/baselines/glpolar_scatter.png and b/test/image/baselines/glpolar_scatter.png differ
diff --git a/test/image/baselines/glpolar_style.png b/test/image/baselines/glpolar_style.png
index 31c2b18b375..8e27ce4e66c 100644
Binary files a/test/image/baselines/glpolar_style.png and b/test/image/baselines/glpolar_style.png differ
diff --git a/test/image/baselines/glpolar_subplots.png b/test/image/baselines/glpolar_subplots.png
index ed807c1ab15..7fd76151d3f 100644
Binary files a/test/image/baselines/glpolar_subplots.png and b/test/image/baselines/glpolar_subplots.png differ
diff --git a/test/image/baselines/polar_bar-overlay.png b/test/image/baselines/polar_bar-overlay.png
index 54e29302826..c10f093cd84 100644
Binary files a/test/image/baselines/polar_bar-overlay.png and b/test/image/baselines/polar_bar-overlay.png differ
diff --git a/test/image/baselines/polar_bar-stacked.png b/test/image/baselines/polar_bar-stacked.png
index c232d2b3c0a..e317800d5fb 100644
Binary files a/test/image/baselines/polar_bar-stacked.png and b/test/image/baselines/polar_bar-stacked.png differ
diff --git a/test/image/baselines/polar_bar-width-base-offset.png b/test/image/baselines/polar_bar-width-base-offset.png
index 760c06c857c..eac19c97c6c 100644
Binary files a/test/image/baselines/polar_bar-width-base-offset.png and b/test/image/baselines/polar_bar-width-base-offset.png differ
diff --git a/test/image/baselines/polar_blank.png b/test/image/baselines/polar_blank.png
index d5c9b75dd63..ac57f2fcf44 100644
Binary files a/test/image/baselines/polar_blank.png and b/test/image/baselines/polar_blank.png differ
diff --git a/test/image/baselines/polar_categories.png b/test/image/baselines/polar_categories.png
index 36e92602dc3..67e45a62d91 100644
Binary files a/test/image/baselines/polar_categories.png and b/test/image/baselines/polar_categories.png differ
diff --git a/test/image/baselines/polar_dates.png b/test/image/baselines/polar_dates.png
index 6127f9afd71..b102e5ea70c 100644
Binary files a/test/image/baselines/polar_dates.png and b/test/image/baselines/polar_dates.png differ
diff --git a/test/image/baselines/polar_direction.png b/test/image/baselines/polar_direction.png
index 967aead4abc..4b57bf4a130 100644
Binary files a/test/image/baselines/polar_direction.png and b/test/image/baselines/polar_direction.png differ
diff --git a/test/image/baselines/polar_fills.png b/test/image/baselines/polar_fills.png
index efd3df8448b..fd34fb02040 100644
Binary files a/test/image/baselines/polar_fills.png and b/test/image/baselines/polar_fills.png differ
diff --git a/test/image/baselines/polar_funky-bars.png b/test/image/baselines/polar_funky-bars.png
index 721da8c124b..f986930a388 100644
Binary files a/test/image/baselines/polar_funky-bars.png and b/test/image/baselines/polar_funky-bars.png differ
diff --git a/test/image/baselines/polar_hole.png b/test/image/baselines/polar_hole.png
new file mode 100644
index 00000000000..b701371fe93
Binary files /dev/null and b/test/image/baselines/polar_hole.png differ
diff --git a/test/image/baselines/polar_line.png b/test/image/baselines/polar_line.png
index 8b5849a3303..bd00514df84 100644
Binary files a/test/image/baselines/polar_line.png and b/test/image/baselines/polar_line.png differ
diff --git a/test/image/baselines/polar_polygon-bars.png b/test/image/baselines/polar_polygon-bars.png
index a6dabee586c..892e5dd4ac5 100644
Binary files a/test/image/baselines/polar_polygon-bars.png and b/test/image/baselines/polar_polygon-bars.png differ
diff --git a/test/image/baselines/polar_polygon-grids.png b/test/image/baselines/polar_polygon-grids.png
index 8a6e41ffdef..dea0454b4d3 100644
Binary files a/test/image/baselines/polar_polygon-grids.png and b/test/image/baselines/polar_polygon-grids.png differ
diff --git a/test/image/baselines/polar_r0dr-theta0dtheta.png b/test/image/baselines/polar_r0dr-theta0dtheta.png
index e5b78152117..b11d35e2c42 100644
Binary files a/test/image/baselines/polar_r0dr-theta0dtheta.png and b/test/image/baselines/polar_r0dr-theta0dtheta.png differ
diff --git a/test/image/baselines/polar_radial-range.png b/test/image/baselines/polar_radial-range.png
index e79a88f3b3d..8e6e570dc3e 100644
Binary files a/test/image/baselines/polar_radial-range.png and b/test/image/baselines/polar_radial-range.png differ
diff --git a/test/image/baselines/polar_scatter.png b/test/image/baselines/polar_scatter.png
index 2a94c6851c4..c5fffaf3d30 100644
Binary files a/test/image/baselines/polar_scatter.png and b/test/image/baselines/polar_scatter.png differ
diff --git a/test/image/baselines/polar_sector.png b/test/image/baselines/polar_sector.png
index 78e5585892a..d81ffb8f7c9 100644
Binary files a/test/image/baselines/polar_sector.png and b/test/image/baselines/polar_sector.png differ
diff --git a/test/image/baselines/polar_subplots.png b/test/image/baselines/polar_subplots.png
index 140e0aac811..e441914242d 100644
Binary files a/test/image/baselines/polar_subplots.png and b/test/image/baselines/polar_subplots.png differ
diff --git a/test/image/baselines/polar_ticks.png b/test/image/baselines/polar_ticks.png
index 2cb629b084a..6a4eb5b642b 100644
Binary files a/test/image/baselines/polar_ticks.png and b/test/image/baselines/polar_ticks.png differ
diff --git a/test/image/baselines/polar_transforms.png b/test/image/baselines/polar_transforms.png
index c7c0a5eb218..b120caaafdc 100644
Binary files a/test/image/baselines/polar_transforms.png and b/test/image/baselines/polar_transforms.png differ
diff --git a/test/image/baselines/polar_wind-rose.png b/test/image/baselines/polar_wind-rose.png
index d1050119a3f..951dfe8f833 100644
Binary files a/test/image/baselines/polar_wind-rose.png and b/test/image/baselines/polar_wind-rose.png differ
diff --git a/test/image/baselines/ternary_noticks.png b/test/image/baselines/ternary_noticks.png
new file mode 100644
index 00000000000..e643f587845
Binary files /dev/null and b/test/image/baselines/ternary_noticks.png differ
diff --git a/test/image/mocks/gl2d_fill-ordering.json b/test/image/mocks/gl2d_fill-ordering.json
new file mode 100644
index 00000000000..49d491f95f8
--- /dev/null
+++ b/test/image/mocks/gl2d_fill-ordering.json
@@ -0,0 +1,40 @@
+{
+  "data": [
+    {
+      "x": [1, 2, 3, 4, 5, 6],
+      "y": [100, 100, 0, 0, 0, 0],
+      "fill": "tozeroy",
+      "type": "scattergl"
+    },
+    {
+      "x": [1, 2, 3, 4, 5, 6],
+      "y": [0, 0, 0, 100, 100, 0],
+      "fill": "tozeroy",
+      "type": "scattergl",
+      "mode": "none"
+    },
+    {
+      "x": [1, 2, 3, 4, 5, 6],
+      "y": [99, 99, 99, 100, 100, 100],
+      "type": "scattergl",
+      "mode": "lines+markers"
+    },
+    {
+      "x": [1, 2, 3, 4, 5, 6],
+      "y": [0, 0, 0, null, 50, 50],
+      "fill": "tozeroy",
+      "type": "scattergl",
+      "mode": "none"
+    },
+    {
+      "x": [1, 2, 3, 4, 5, 6],
+      "y": [100, 0, 0, 0, 0, 100],
+      "type": "scattergl",
+      "mode": "lines+markers"
+    }
+  ],
+  "layout": {
+    "margin": {"l": 40, "r": 50, "b": 80, "t": 40},
+    "legend": {"orientation": "h", "x": "0.5", "xanchor": "center"}
+  }
+}
diff --git a/test/image/mocks/polar_hole.json b/test/image/mocks/polar_hole.json
new file mode 100644
index 00000000000..4eb7d7dd016
--- /dev/null
+++ b/test/image/mocks/polar_hole.json
@@ -0,0 +1,101 @@
+{
+  "data": [
+    {
+        "type": "barpolar",
+        "r": [10, 12, 15]
+    },
+    {
+        "type": "scatterpolar",
+        "r": [10, 12, 15],
+        "theta0": 90
+    },
+
+    {
+        "type": "scatterpolar",
+        "subplot": "polar2",
+        "r": [100, 50, 200],
+        "marker": {"size": 20}
+    },
+    {
+        "type": "barpolar",
+        "subplot": "polar2",
+        "r": [100, 50, 200]
+    },
+
+    {
+        "type": "barpolar",
+        "subplot": "polar3",
+        "theta": ["a", "b", "c", "d", "b", "f", "a", "a"]
+    },
+    {
+        "type": "scatterpolar",
+        "subplot": "polar3",
+        "theta": ["a", "b", "c", "d", "b", "f", "a", "a"]
+    },
+
+    {
+        "type": "barpolar",
+        "subplot": "polar4",
+        "r": [10, 12, 15]
+    },
+    {
+        "type": "scatterpolar",
+        "subplot": "polar4",
+        "r": [10, 12, 5],
+        "theta": [0, 90, -90],
+        "cliponaxis": true,
+        "marker": {"size": 20}
+    },
+
+    {
+        "type": "barpolar",
+        "subplot": "polar5",
+        "r": [10, 12, 15]
+    },
+    {
+        "type": "scatterpolar",
+        "subplot": "polar5",
+        "r": [10, 12, 15]
+    }
+  ],
+  "layout": {
+    "width": 600,
+    "height": 800,
+    "margin": {"l": 40, "r": 40, "b": 40, "t": 40, "pad": 0},
+    "grid": {
+      "rows": 3,
+      "columns": 2,
+      "ygap": 0.2
+    },
+    "polar": {
+      "hole": 0.1,
+      "domain": {"row": 0, "column": 0}
+    },
+    "polar2": {
+      "hole": 0.4,
+      "domain": {"row": 0, "column": 1},
+      "angularaxis": {"layer": "below traces"},
+      "radialaxis": {"type": "log", "range": [1.65, 2.35]}
+    },
+    "polar3": {
+      "hole": 0.2,
+      "gridshape": "linear",
+      "angularaxis": {"direction": "clockwise"},
+      "domain": {"row": 1, "column": 0}
+    },
+    "polar4": {
+      "hole": 0.4,
+      "domain": {"row": 1, "column": 1},
+      "sector": [0, 180],
+      "angularaxis": {"direction": "clockwise"},
+      "radialaxis": {"angle": 90, "side": "counterclockwise", "range": [5, 15]}
+    },
+    "polar5": {
+      "hole": 0.5,
+      "domain": {"row": 2, "column": 0},
+      "radialaxis": {"range": [20, 0], "tickfont": {"color": "red", "size": 20}},
+      "angularaxis": {"linewidth": 3}
+    },
+    "showlegend": false
+  }
+}
diff --git a/test/image/mocks/ternary_noticks.json b/test/image/mocks/ternary_noticks.json
new file mode 100644
index 00000000000..bb0f110a3ab
--- /dev/null
+++ b/test/image/mocks/ternary_noticks.json
@@ -0,0 +1,31 @@
+{
+    "data": [
+        {
+            "a": [2, 1, 1],
+            "b": [1, 2, 1],
+            "c": [1, 1, 2.12345],
+            "type": "scatterternary"
+        }
+    ],
+    "layout": {
+        "ternary": {
+            "aaxis": {
+                "showticklabels": false,
+                "showline": false,
+                "title": "no labels / no line"
+            },
+            "baxis": {
+                "ticks": "",
+                "showticklabels": false,
+                "title": "no ticks / no labels"
+            },
+            "caxis": {
+                "showticklabels": false,
+                "showgrid": false,
+                "title": "no grid / no labels"
+            }
+        },
+        "height": 450,
+        "width": 700
+    }
+}
diff --git a/test/jasmine/assets/custom_assertions.js b/test/jasmine/assets/custom_assertions.js
index aab46b67f5e..4cdee1c84f7 100644
--- a/test/jasmine/assets/custom_assertions.js
+++ b/test/jasmine/assets/custom_assertions.js
@@ -312,6 +312,22 @@ exports.assertNodeOrder = function(selectorBehind, selectorInFront, msg) {
     }
 };
 
+/**
+ * Ordering test for any number of nodes - calls assertNodeOrder n-1 times.
+ * Note that we only take the first matching node for each selector, and it's
+ * not necessary that the nodes be siblings or at the same level of nesting.
+ *
+ * @param {Array[string]} selectorArray: css selectors in the order they should
+ *     appear in the document, from back to front.
+ * @param {string} msg: context for debugging
+ */
+exports.assertMultiNodeOrder = function(selectorArray, msg) {
+    for(var i = 0; i < selectorArray.length - 1; i++) {
+        var msgi = (msg ? msg + ' - ' : '') + 'entries ' + i + ' and ' + (i + 1);
+        exports.assertNodeOrder(selectorArray[i], selectorArray[i + 1], msgi);
+    }
+};
+
 function getParents(node) {
     var parent = node.parentNode;
     if(parent) return getParents(parent).concat(node);
@@ -324,3 +340,9 @@ function collectionToArray(collection) {
     for(var i = 0; i < len; i++) a[i] = collection[i];
     return a;
 }
+
+exports.assertD3Data = function(selection, expectedData) {
+    var data = [];
+    selection.each(function(d) { data.push(d); });
+    expect(data).toEqual(expectedData);
+};
diff --git a/test/jasmine/assets/double_click.js b/test/jasmine/assets/double_click.js
index 73d444d0782..222ecd7e5a4 100644
--- a/test/jasmine/assets/double_click.js
+++ b/test/jasmine/assets/double_click.js
@@ -3,22 +3,25 @@ var getNodeCoords = require('./get_node_coords');
 var DBLCLICKDELAY = require('../../../src/constants/interactions').DBLCLICKDELAY;
 
 /*
- * double click on a point.
- * you can either specify x,y as pixels, or
+ * Double click on a point.
+ * You can either specify x,y as pixels, or
  * you can specify node and optionally an edge ('n', 'se', 'w' etc)
- * to grab it by an edge or corner (otherwise the middle is used)
+ * to grab it by an edge or corner (otherwise the middle is used).
+ * You can also pass options for the underlying click, e.g.
+ * to specify modifier keys. See `click` function
+ * for more info.
  */
-module.exports = function doubleClick(x, y) {
+module.exports = function doubleClick(x, y, clickOpts) {
     if(typeof x === 'object') {
         var coords = getNodeCoords(x, y);
         x = coords.x;
         y = coords.y;
     }
     return new Promise(function(resolve) {
-        click(x, y);
+        click(x, y, clickOpts);
 
         setTimeout(function() {
-            click(x, y);
+            click(x, y, clickOpts);
             setTimeout(function() { resolve(); }, DBLCLICKDELAY / 2);
         }, DBLCLICKDELAY / 2);
     });
diff --git a/test/jasmine/assets/transitions.js b/test/jasmine/assets/transitions.js
new file mode 100644
index 00000000000..cf1be579d51
--- /dev/null
+++ b/test/jasmine/assets/transitions.js
@@ -0,0 +1,35 @@
+'use strict';
+
+/**
+ * Given n states (denoted by their indices 0..n-1) this routine produces
+ * a sequence of indices such that you efficiently execute each transition
+ * from any state to any other state.
+ */
+module.exports = function transitions(n) {
+    var out = [0];
+    var nextStates = [];
+    var i;
+    for(i = 0; i < n; i++) nextStates[i] = (i + 1) % n;
+    var finishedStates = 0;
+    var thisState = 0;
+    var nextState;
+    while(finishedStates < n) {
+        nextState = nextStates[thisState];
+        if(nextState === thisState) {
+            // I don't actually know how to prove that this algorithm works,
+            // but I've never seen it fail for n>1
+            // For prime n it's the same sequence as the one I started with
+            // (n transitions of +1 index, then n transitions +2 etc...)
+            // but this one works for non-prime n as well.
+            throw new Error('your transitions algo failed.');
+        }
+        nextStates[thisState] = (nextStates[thisState] + 1) % n;
+        if(nextStates[thisState] === thisState) finishedStates++;
+        out.push(nextState);
+        thisState = nextState;
+    }
+    if(out.length !== n * (n - 1) + 1) {
+        throw new Error('your transitions algo failed.');
+    }
+    return out;
+};
diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/bundle_tests/plotschema_test.js
similarity index 100%
rename from test/jasmine/tests/plotschema_test.js
rename to test/jasmine/bundle_tests/plotschema_test.js
diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js
index 66bce39e73c..4050c8ccc5e 100644
--- a/test/jasmine/karma.conf.js
+++ b/test/jasmine/karma.conf.js
@@ -274,6 +274,10 @@ if(isBundleTest) {
             func.defaultConfig.files.push(pathToIE9mock);
             func.defaultConfig.preprocessors[testFileGlob] = ['browserify'];
             break;
+        case 'plotschema':
+            func.defaultConfig.browserify.ignoreTransform = './tasks/compress_attributes.js';
+            func.defaultConfig.preprocessors[testFileGlob] = ['browserify'];
+            break;
         default:
             func.defaultConfig.preprocessors[testFileGlob] = ['browserify'];
             break;
diff --git a/test/jasmine/tests/barpolar_test.js b/test/jasmine/tests/barpolar_test.js
index a6aa3ef7f5f..b5314a8f330 100644
--- a/test/jasmine/tests/barpolar_test.js
+++ b/test/jasmine/tests/barpolar_test.js
@@ -274,6 +274,24 @@ describe('Test barpolar hover:', function() {
             extraText: 'r: 12
θ: 120°',
             color: '#1f77b4'
         }
+    }, {
+        desc: 'works on a subplot with hole>0',
+        traces: [{
+            r: [1, 2, 3],
+            theta: [0, 90, 180]
+        }],
+        layout: {
+            polar: {hole: 0.2}
+        },
+        xval: 1,
+        yval: 0,
+        exp: {
+            index: 0,
+            x: 290.67,
+            y: 200,
+            extraText: 'r: 1
θ: 0°',
+            color: '#1f77b4'
+        }
     }, {
         desc: 'on overlapping bars of same size, the narrower wins',
         traces: [{
diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js
index 21a018d7ff6..6f5c26b203b 100644
--- a/test/jasmine/tests/cartesian_test.js
+++ b/test/jasmine/tests/cartesian_test.js
@@ -7,6 +7,7 @@ var Drawing = require('@src/components/drawing');
 var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
 var failTest = require('../assets/fail_test');
+var assertD3Data = require('../assets/custom_assertions').assertD3Data;
 
 describe('restyle', function() {
     describe('scatter traces', function() {
@@ -21,37 +22,35 @@ describe('restyle', function() {
         it('reuses SVG fills', function(done) {
             var fills, firstToZero, secondToZero, firstToNext, secondToNext;
             var mock = Lib.extendDeep({}, require('@mocks/basic_area.json'));
+            function getFills() {
+                return d3.selectAll('g.trace.scatter .fills>g');
+            }
 
             Plotly.plot(gd, mock.data, mock.layout).then(function() {
-                // Assert there are two fills:
-                fills = d3.selectAll('g.trace.scatter .js-fill')[0];
+                fills = getFills();
 
-                // First is tozero, second is tonext:
-                expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(2);
-                expect(fills[0]).toBeClassed(['js-fill', 'js-tozero']);
-                expect(fills[1]).toBeClassed(['js-fill', 'js-tonext']);
+                // Assert there are two fills, first is tozero, second is tonext
+                assertD3Data(fills, ['_ownFill', '_nextFill']);
+
+                firstToZero = fills[0][0];
+                firstToNext = fills[0][1];
 
-                firstToZero = fills[0];
-                firstToNext = fills[1];
-            }).then(function() {
                 return Plotly.restyle(gd, {visible: [false]}, [1]);
             }).then(function() {
+                fills = getFills();
                 // Trace 1 hidden leaves only trace zero's tozero fill:
-                expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(1);
-                expect(fills[0]).toBeClassed(['js-fill', 'js-tozero']);
-            }).then(function() {
+                assertD3Data(fills, ['_ownFill']);
+
                 return Plotly.restyle(gd, {visible: [true]}, [1]);
             }).then(function() {
-                // Reshow means two fills again AND order is preserved:
-                fills = d3.selectAll('g.trace.scatter .js-fill')[0];
+                fills = getFills();
 
+                // Reshow means two fills again AND order is preserved
                 // First is tozero, second is tonext:
-                expect(d3.selectAll('g.trace.scatter .js-fill').size()).toEqual(2);
-                expect(fills[0]).toBeClassed(['js-fill', 'js-tozero']);
-                expect(fills[1]).toBeClassed(['js-fill', 'js-tonext']);
+                assertD3Data(fills, ['_ownFill', '_nextFill']);
 
-                secondToZero = fills[0];
-                secondToNext = fills[1];
+                secondToZero = fills[0][0];
+                secondToNext = fills[0][1];
 
                 // The identity of the first is retained:
                 expect(firstToZero).toBe(secondToZero);
@@ -61,8 +60,7 @@ describe('restyle', function() {
 
                 return Plotly.restyle(gd, 'visible', false);
             }).then(function() {
-                expect(d3.selectAll('g.trace.scatter').size()).toEqual(0);
-
+                expect(d3.selectAll('g.trace.scatter').size()).toBe(0);
             })
             .catch(failTest)
             .then(done);
diff --git a/test/jasmine/tests/gl2d_plot_interact_test.js b/test/jasmine/tests/gl2d_plot_interact_test.js
index 2982a653529..ecc46e7968b 100644
--- a/test/jasmine/tests/gl2d_plot_interact_test.js
+++ b/test/jasmine/tests/gl2d_plot_interact_test.js
@@ -214,6 +214,53 @@ describe('Test gl plot side effects', function() {
         .catch(failTest)
         .then(done);
     });
+
+    it('@gl should fire *plotly_webglcontextlost* when on webgl context lost', function(done) {
+        var _mock = Lib.extendDeep({}, require('@mocks/gl2d_12.json'));
+
+        function _trigger(name) {
+            var ev = new window.WebGLContextEvent('webglcontextlost');
+            var canvas = gd.querySelector('.gl-canvas-' + name);
+            canvas.dispatchEvent(ev);
+        }
+
+        Plotly.plot(gd, _mock).then(function() {
+            return new Promise(function(resolve, reject) {
+                gd.once('plotly_webglcontextlost', resolve);
+                setTimeout(reject, 10);
+                _trigger('context');
+            });
+        })
+        .then(function(eventData) {
+            expect((eventData || {}).event).toBeDefined();
+            expect((eventData || {}).layer).toBe('contextLayer');
+        })
+        .then(function() {
+            return new Promise(function(resolve, reject) {
+                gd.once('plotly_webglcontextlost', resolve);
+                setTimeout(reject, 10);
+                _trigger('focus');
+            });
+        })
+        .then(function(eventData) {
+            expect((eventData || {}).event).toBeDefined();
+            expect((eventData || {}).layer).toBe('focusLayer');
+        })
+        .then(function() {
+            return new Promise(function(resolve, reject) {
+                gd.once('plotly_webglcontextlost', reject);
+                setTimeout(resolve, 10);
+                _trigger('pick');
+            });
+        })
+        .then(function(eventData) {
+            // should add event listener on pick canvas which
+            // isn't used for scattergl traces
+            expect(eventData).toBeUndefined();
+        })
+        .catch(failTest)
+        .then(done);
+    });
 });
 
 describe('Test gl2d plots', function() {
diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js
index 64b22e447bc..89d00c3ac11 100644
--- a/test/jasmine/tests/gl3d_plot_interact_test.js
+++ b/test/jasmine/tests/gl3d_plot_interact_test.js
@@ -1425,4 +1425,25 @@ describe('Test removal of gl contexts', function() {
         })
         .then(done);
     });
+
+    it('@gl should fire *plotly_webglcontextlost* when on webgl context lost', function(done) {
+        var _mock = Lib.extendDeep({}, require('@mocks/gl3d_marker-arrays.json'));
+
+        Plotly.plot(gd, _mock).then(function() {
+            return new Promise(function(resolve, reject) {
+                gd.on('plotly_webglcontextlost', resolve);
+                setTimeout(reject, 10);
+
+                var ev = new window.WebGLContextEvent('webglcontextlost');
+                var canvas = gd.querySelector('div#scene > canvas');
+                canvas.dispatchEvent(ev);
+            });
+        })
+        .then(function(eventData) {
+            expect((eventData || {}).event).toBeDefined();
+            expect((eventData || {}).layer).toBe('scene');
+        })
+        .catch(failTest)
+        .then(done);
+    });
 });
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index aa0981c5ac3..437c47392d1 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -2454,3 +2454,52 @@ describe('hover distance', function() {
         });
     });
 });
+
+describe('hovermode defaults to', function() {
+    var gd;
+
+    beforeEach(function() {
+        gd = createGraphDiv();
+    });
+
+    afterEach(destroyGraphDiv);
+
+    it('\'closest\' for cartesian plots if clickmode includes \'select\'', function(done) {
+        Plotly.plot(gd, [{ x: [1, 2, 3], y: [4, 5, 6] }], { clickmode: 'event+select' })
+          .then(function() {
+              expect(gd._fullLayout.hovermode).toBe('closest');
+          })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('\'x\' for horizontal cartesian plots if clickmode lacks \'select\'', function(done) {
+        Plotly.plot(gd, [{ x: [1, 2, 3], y: [4, 5, 6], type: 'bar', orientation: 'h' }], { clickmode: 'event' })
+          .then(function() {
+              expect(gd._fullLayout.hovermode).toBe('y');
+          })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('\'y\' for vertical cartesian plots if clickmode lacks \'select\'', function(done) {
+        Plotly.plot(gd, [{ x: [1, 2, 3], y: [4, 5, 6], type: 'bar', orientation: 'v' }], { clickmode: 'event' })
+          .then(function() {
+              expect(gd._fullLayout.hovermode).toBe('x');
+          })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('\'closest\' for a non-cartesian plot', function(done) {
+        var mock = require('@mocks/polar_scatter.json');
+        expect(mock.layout.hovermode).toBeUndefined();
+
+        Plotly.plot(gd, mock.data, mock.layout)
+          .then(function() {
+              expect(gd._fullLayout.hovermode).toBe('closest');
+          })
+          .catch(failTest)
+          .then(done);
+    });
+});
diff --git a/test/jasmine/tests/parcoords_test.js b/test/jasmine/tests/parcoords_test.js
index fe05a10bf67..d1dea69c844 100644
--- a/test/jasmine/tests/parcoords_test.js
+++ b/test/jasmine/tests/parcoords_test.js
@@ -1052,6 +1052,36 @@ describe('parcoords basic use', function() {
         .catch(failTest)
         .then(done);
     });
+
+    it('@gl should fire *plotly_webglcontextlost* when on webgl context lost', function() {
+        var eventData;
+        var cnt = 0;
+        gd.on('plotly_webglcontextlost', function(d) {
+            eventData = d;
+            cnt++;
+        });
+
+        function trigger(name) {
+            var ev = new window.WebGLContextEvent('webglcontextlost');
+            var canvas = gd.querySelector('.gl-canvas-' + name);
+            canvas.dispatchEvent(ev);
+        }
+
+        function _assert(d, c) {
+            expect((eventData || {}).event).toBeDefined();
+            expect((eventData || {}).layer).toBe(d);
+            expect(cnt).toBe(c);
+        }
+
+        trigger('context');
+        _assert('contextLayer', 1);
+
+        trigger('focus');
+        _assert('focusLayer', 2);
+
+        trigger('pick');
+        _assert('pickLayer', 3);
+    });
 });
 
 describe('@noCI parcoords constraint interactions', function() {
diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js
index eb1e735387c..9b2469b630a 100644
--- a/test/jasmine/tests/plot_api_test.js
+++ b/test/jasmine/tests/plot_api_test.js
@@ -2962,7 +2962,7 @@ describe('Test plot api', function() {
             Plotly.newPlot(gd, data, layout)
             .then(countPlots)
             .then(function() {
-                expect(d3.select(gd).selectAll('.drag').size()).toBe(3);
+                expect(d3.select(gd).selectAll('.drag').size()).toBe(4);
 
                 return Plotly.react(gd, data, layout, {staticPlot: true});
             })
@@ -2972,7 +2972,7 @@ describe('Test plot api', function() {
                 return Plotly.react(gd, data, layout, {});
             })
             .then(function() {
-                expect(d3.select(gd).selectAll('.drag').size()).toBe(3);
+                expect(d3.select(gd).selectAll('.drag').size()).toBe(4);
             })
             .catch(failTest)
             .then(done);
diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js
index 4b5139bf818..ede60e8b130 100644
--- a/test/jasmine/tests/plots_test.js
+++ b/test/jasmine/tests/plots_test.js
@@ -500,6 +500,13 @@ describe('Test Plots', function() {
     });
 
     describe('Plots.graphJson', function() {
+        var gd;
+
+        beforeEach(function() {
+            gd = createGraphDiv();
+        });
+
+        afterEach(destroyGraphDiv);
 
         it('should serialize data, layout and frames', function(done) {
             var mock = {
@@ -533,7 +540,7 @@ describe('Test Plots', function() {
                 }]
             };
 
-            Plotly.plot(createGraphDiv(), mock).then(function(gd) {
+            Plotly.plot(gd, mock).then(function() {
                 var str = Plots.graphJson(gd, false, 'keepdata');
                 var obj = JSON.parse(str);
 
@@ -547,10 +554,38 @@ describe('Test Plots', function() {
                     name: 'garbage'
                 });
             })
-            .then(function() {
-                destroyGraphDiv();
-                done();
-            });
+            .catch(failTest)
+            .then(done);
+        });
+
+        it('should convert typed arrays to regular arrays', function(done) {
+            var trace = {
+                x: new Float32Array([1, 2, 3]),
+                y: new Float32Array([1, 2, 1]),
+                marker: {
+                    size: new Float32Array([20, 30, 10]),
+                    color: new Float32Array([10, 30, 20]),
+                    cmin: 10,
+                    cmax: 30,
+                    colorscale: [
+                        [0, 'rgb(255, 0, 0)'],
+                        [0.5, 'rgb(0, 255, 0)'],
+                        [1, 'rgb(0, 0, 255)']
+                    ]
+                }
+            };
+
+            Plotly.plot(gd, [trace]).then(function() {
+                var str = Plots.graphJson(gd, false, 'keepdata');
+                var obj = JSON.parse(str);
+
+                expect(obj.data[0].x).toEqual([1, 2, 3]);
+                expect(obj.data[0].y).toEqual([1, 2, 1]);
+                expect(obj.data[0].marker.size).toEqual([20, 30, 10]);
+                expect(obj.data[0].marker.color).toEqual([10, 30, 20]);
+            })
+            .catch(failTest)
+            .then(done);
         });
     });
 
diff --git a/test/jasmine/tests/polar_test.js b/test/jasmine/tests/polar_test.js
index c7ffc6a685c..84565b145d7 100644
--- a/test/jasmine/tests/polar_test.js
+++ b/test/jasmine/tests/polar_test.js
@@ -236,19 +236,19 @@ describe('Test relayout on polar subplots:', function() {
         .then(function() {
             _assert([
                 'draglayer', 'plotbg', 'backplot', 'angular-grid', 'radial-grid',
-                'radial-axis', 'radial-line',
+                'radial-line', 'radial-axis',
                 'frontplot',
-                'angular-axis', 'angular-line'
+                'angular-line', 'angular-axis'
             ]);
             return Plotly.relayout(gd, 'polar.angularaxis.layer', 'below traces');
         })
         .then(function() {
             _assert([
                 'draglayer', 'plotbg', 'backplot', 'angular-grid', 'radial-grid',
-                'angular-axis',
-                'radial-axis',
                 'angular-line',
                 'radial-line',
+                'angular-axis',
+                'radial-axis',
                 'frontplot'
             ]);
             return Plotly.relayout(gd, 'polar.radialaxis.layer', 'above traces');
@@ -256,9 +256,9 @@ describe('Test relayout on polar subplots:', function() {
         .then(function() {
             _assert([
                 'draglayer', 'plotbg', 'backplot', 'angular-grid', 'radial-grid',
-                'angular-axis', 'angular-line',
+                'angular-line', 'angular-axis',
                 'frontplot',
-                'radial-axis', 'radial-line'
+                'radial-line', 'radial-axis'
             ]);
             return Plotly.relayout(gd, 'polar.angularaxis.layer', null);
         })
@@ -381,75 +381,62 @@ describe('Test relayout on polar subplots:', function() {
         }
 
         function toggle(astr, vals, exps, selector, fn) {
-            return Plotly.relayout(gd, astr, vals[0]).then(function() {
-                fn(selector, exps[0], astr + ' ' + vals[0]);
-                return Plotly.relayout(gd, astr, vals[1]);
-            })
-            .then(function() {
-                fn(selector, exps[1], astr + ' ' + vals[1]);
-                return Plotly.relayout(gd, astr, vals[0]);
-            })
-            .then(function() {
-                fn(selector, exps[0], astr + ' ' + vals[0]);
-            });
+            return function() {
+                return Plotly.relayout(gd, astr, vals[0]).then(function() {
+                    fn(selector, exps[0], astr + ' ' + vals[0]);
+                    return Plotly.relayout(gd, astr, vals[1]);
+                })
+                .then(function() {
+                    fn(selector, exps[1], astr + ' ' + vals[1]);
+                    return Plotly.relayout(gd, astr, vals[0]);
+                })
+                .then(function() {
+                    fn(selector, exps[0], astr + ' ' + vals[0]);
+                });
+            };
         }
 
-        Plotly.plot(gd, fig).then(function() {
-            return toggle(
-                'polar.radialaxis.showline',
-                [true, false], [null, 'none'],
-                '.radial-line > line', assertDisplay
-            );
-        })
-        .then(function() {
-            return toggle(
-                'polar.radialaxis.showgrid',
-                [true, false], [null, 'none'],
-                '.radial-grid', assertDisplay
-            );
-        })
-        .then(function() {
-            return toggle(
-                'polar.radialaxis.showticklabels',
-                [true, false], [6, 0],
-                '.radial-axis > .xtick > text', assertCnt
-            );
-        })
-        .then(function() {
-            return toggle(
-                'polar.radialaxis.ticks',
-                ['outside', ''], [6, 0],
-                '.radial-axis > path.xtick', assertCnt
-            );
-        })
-        .then(function() {
-            return toggle(
-                'polar.angularaxis.showline',
-                [true, false], [null, 'none'],
-                '.angular-line > path', assertDisplay
-            );
-        })
-        .then(function() {
-            return toggle(
-                'polar.angularaxis.showgrid',
-                [true, false], [8, 0],
-                '.angular-grid > .angularaxis > path', assertCnt
-            );
-        })
-        .then(function() {
-            return toggle(
-                'polar.angularaxis.showticklabels',
-                [true, false], [8, 0],
-                '.angular-axis > .angularaxistick > text', assertCnt
-            );
-        })
-        .then(function() {
-            return toggle(
-                'polar.angularaxis.ticks',
-                ['outside', ''], [8, 0],
-                '.angular-axis > path.angularaxistick', assertCnt
-            );
-        })
+        Plotly.plot(gd, fig)
+        .then(toggle(
+            'polar.radialaxis.showline',
+            [true, false], [null, 'none'],
+            '.radial-line > line', assertDisplay
+        ))
+        .then(toggle(
+            'polar.radialaxis.showgrid',
+            [true, false], [null, 'none'],
+            '.radial-grid', assertDisplay
+        ))
+        .then(toggle(
+            'polar.radialaxis.showticklabels',
+            [true, false], [6, 0],
+            '.radial-axis > .xtick > text', assertCnt
+        ))
+        .then(toggle(
+            'polar.radialaxis.ticks',
+            ['outside', ''], [6, 0],
+            '.radial-axis > path.xtick', assertCnt
+        ))
+        .then(toggle(
+            'polar.angularaxis.showline',
+            [true, false], [null, 'none'],
+            '.angular-line > path', assertDisplay
+        ))
+        .then(toggle(
+            'polar.angularaxis.showgrid',
+            [true, false], [8, 0],
+            '.angular-grid > .angularaxis > path', assertCnt
+        ))
+        .then(toggle(
+            'polar.angularaxis.showticklabels',
+            [true, false], [8, 0],
+            '.angular-axis > .angularaxistick > text', assertCnt
+        ))
+        .then(toggle(
+            'polar.angularaxis.ticks',
+            ['outside', ''], [8, 0],
+            '.angular-axis > path.angularaxistick', assertCnt
+        ))
         .catch(failTest)
         .then(done);
     });
@@ -924,6 +911,13 @@ describe('Test polar interactions:', function() {
             expect(eventCnts.plotly_relayout)
                 .toBe(relayoutNumber, 'no new relayout events after *not far enough* cases');
         })
+        .then(_reset)
+        .then(function() { return Plotly.relayout(gd, 'polar.hole', 0.2); })
+        .then(function() { relayoutNumber++; })
+        .then(function() { return _drag([mid[0] + 30, mid[0] - 30], [50, -50]); })
+        .then(function() {
+            _assertDrag([1.15, 7.70], 'with polar.hole>0, from quadrant #1 move top-right');
+        })
         .catch(failTest)
         .then(done);
     });
@@ -1012,6 +1006,45 @@ describe('Test polar interactions:', function() {
         .then(done);
     });
 
+    it('should response to drag interactions on inner radial drag area', function(done) {
+        var fig = Lib.extendDeep({}, require('@mocks/polar_scatter.json'));
+        fig.layout.polar.hole = 0.2;
+        // to avoid dragging on hover labels
+        fig.layout.hovermode = false;
+        // adjust margins so that middle of plot area is at 300x300
+        // with its middle at [200,200]
+        fig.layout.width = 400;
+        fig.layout.height = 400;
+        fig.layout.margin = {l: 50, t: 50, b: 50, r: 50};
+
+        var dragPos0 = [200, 200];
+
+        // use 'special' drag method - as we need two mousemove events
+        // to activate the radial drag mode
+        function _drag(p0, dp) {
+            var node = d3.select('.polar > .draglayer > .radialdrag-inner').node();
+            return drag(node, dp[0], dp[1], null, p0[0], p0[1], 2);
+        }
+
+        function _assert(rng, msg) {
+            expect(gd._fullLayout.polar.radialaxis.range)
+                .toBeCloseToArray(rng, 1, msg + ' - range');
+        }
+
+        _plot(fig)
+        .then(function() { return _drag(dragPos0, [-50, 0]); })
+        .then(function() {
+            _assert([3.55, 11.36], 'move inward');
+        })
+        .then(function() { return Plotly.relayout(gd, 'polar.radialaxis.autorange', true); })
+        .then(function() { return _drag(dragPos0, [50, 0]); })
+        .then(function() {
+            _assert([-3.55, 11.36], 'move outward');
+        })
+        .catch(failTest)
+        .then(done);
+    });
+
     it('should response to drag interactions on angular drag area', function(done) {
         var fig = Lib.extendDeep({}, require('@mocks/polar_scatter.json'));
 
diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js
index 74817411abf..a49b8a69c3e 100644
--- a/test/jasmine/tests/scatter_test.js
+++ b/test/jasmine/tests/scatter_test.js
@@ -9,9 +9,11 @@ var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
 var customAssertions = require('../assets/custom_assertions');
 var failTest = require('../assets/fail_test');
+var transitions = require('../assets/transitions');
 
 var assertClip = customAssertions.assertClip;
 var assertNodeDisplay = customAssertions.assertNodeDisplay;
+var assertMultiNodeOrder = customAssertions.assertMultiNodeOrder;
 
 var getOpacity = function(node) { return Number(node.style.opacity); };
 var getFillOpacity = function(node) { return Number(node.style['fill-opacity']); };
@@ -629,6 +631,91 @@ describe('end-to-end scatter tests', function() {
         .then(done);
     });
 
+    it('should keep layering correct as mode & fill change', function(done) {
+        var fillCase = {name: 'fill', edit: {mode: 'none', fill: 'tonexty'}};
+        var i, j;
+
+        var cases = [fillCase];
+        var modeParts = ['lines', 'markers', 'text'];
+        for(i = 0; i < modeParts.length; i++) {
+            var modePart = modeParts[i];
+            var prevCasesLength = cases.length;
+
+            cases.push({name: modePart, edit: {mode: modePart, fill: 'none'}});
+            for(j = 0; j < prevCasesLength; j++) {
+                var prevCase = cases[j];
+                cases.push({
+                    name: prevCase.name + '_' + modePart,
+                    edit: {
+                        mode: (prevCase.edit.mode === 'none' ? '' : (prevCase.edit.mode + '+')) + modePart,
+                        fill: prevCase.edit.fill
+                    }
+                });
+            }
+        }
+
+        // visit each case N times, in an order that covers each *transition*
+        // from any case to any other case.
+        var indices = transitions(cases.length);
+
+        var p = Plotly.plot(gd, [
+            {y: [1, 2], text: 'a'},
+            {y: [2, 3], text: 'b'},
+            {y: [3, 4], text: 'c'}
+        ]);
+
+        function setMode(i) { return function() {
+            return Plotly.restyle(gd, cases[indices[i]].edit);
+        }; }
+
+        function testOrdering(i) { return function() {
+            var name = cases[indices[i]].name;
+            var hasFills = name.indexOf('fill') !== -1;
+            var hasLines = name.indexOf('lines') !== -1;
+            var hasMarkers = name.indexOf('markers') !== -1;
+            var hasText = name.indexOf('text') !== -1;
+            var tracei, prefix;
+
+            // construct the expected ordering based on case name
+            var selectorArray = [];
+            for(tracei = 0; tracei < 3; tracei++) {
+                prefix = '.xy .trace:nth-child(' + (tracei + 1) + ') ';
+
+                // two fills are attached to the first trace, one to the second
+                if(hasFills) {
+                    if(tracei === 0) {
+                        selectorArray.push(
+                            prefix + 'g:first-child>.js-fill',
+                            prefix + 'g:last-child>.js-fill');
+                    }
+                    else if(tracei === 1) selectorArray.push(prefix + 'g:last-child>.js-fill');
+                }
+                if(hasLines) selectorArray.push(prefix + '.js-line');
+                if(hasMarkers) selectorArray.push(prefix + '.point');
+                if(hasText) selectorArray.push(prefix + '.textpoint');
+            }
+
+            // ordering in the legend
+            for(tracei = 0; tracei < 3; tracei++) {
+                prefix = '.legend .traces:nth-child(' + (tracei + 1) + ') ';
+                if(hasFills) selectorArray.push(prefix + '.js-fill');
+                if(hasLines) selectorArray.push(prefix + '.js-line');
+                if(hasMarkers) selectorArray.push(prefix + '.scatterpts');
+                if(hasText) selectorArray.push(prefix + '.pointtext');
+            }
+
+            var msg = i ? ('from ' + cases[indices[i - 1]].name + ' to ') : 'from default to ';
+            msg += name;
+            assertMultiNodeOrder(selectorArray, msg);
+        }; }
+
+        for(i = 0; i < indices.length; i++) {
+            p = p.then(setMode(i)).then(testOrdering(i));
+        }
+
+        p.catch(failTest).then(done);
+    });
+
     function _assertNodes(ptStyle, txContent) {
         var pts = d3.selectAll('.point');
         var txs = d3.selectAll('.textpoint');
diff --git a/test/jasmine/tests/scatterpolar_test.js b/test/jasmine/tests/scatterpolar_test.js
index 14d2ea0a954..27584eb48db 100644
--- a/test/jasmine/tests/scatterpolar_test.js
+++ b/test/jasmine/tests/scatterpolar_test.js
@@ -150,6 +150,14 @@ describe('Test scatterpolar hover:', function() {
         pos: [465, 90],
         nums: 'r: 4\nθ: d',
         name: 'angular cate...'
+    }, {
+        desc: 'on a subplot with hole>0',
+        patch: function(fig) {
+            fig.layout.polar.hole = 0.2;
+            return fig;
+        },
+        nums: 'r: 1.108937\nθ: 115.4969°',
+        name: 'Trial 3'
     }]
     .forEach(function(specs) {
         it('should generate correct hover labels ' + specs.desc, function(done) {
diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js
index 821c7380e96..750071a2973 100644
--- a/test/jasmine/tests/select_test.js
+++ b/test/jasmine/tests/select_test.js
@@ -2,7 +2,9 @@ var d3 = require('d3');
 
 var Plotly = require('@lib/index');
 var Lib = require('@src/lib');
+var click = require('../assets/click');
 var doubleClick = require('../assets/double_click');
+var DBLCLICKDELAY = require('../../../src/constants/interactions').DBLCLICKDELAY;
 
 var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
@@ -52,7 +54,7 @@ function assertSelectionNodes(cornerCnt, outlineCnt, _msg) {
 }
 
 var selectingCnt, selectingData, selectedCnt, selectedData, deselectCnt, doubleClickData;
-var selectedPromise, deselectPromise;
+var selectedPromise, deselectPromise, clickedPromise;
 
 function resetEvents(gd) {
     selectingCnt = 0;
@@ -75,7 +77,13 @@ function resetEvents(gd) {
         });
 
         gd.on('plotly_selected', function(data) {
-            assertSelectionNodes(0, 2);
+            // With click-to-select supported, selection nodes are only
+            // in the DOM in certain circumstances.
+            if(data &&
+              gd._fullLayout.dragmode.indexOf('select') > -1 &&
+              gd._fullLayout.dragmode.indexOf('lasso') > -1) {
+                assertSelectionNodes(0, 2);
+            }
             selectedCnt++;
             selectedData = data;
             resolve();
@@ -90,6 +98,12 @@ function resetEvents(gd) {
             resolve();
         });
     });
+
+    clickedPromise = new Promise(function(resolve) {
+        gd.on('plotly_click', function() {
+            resolve();
+        });
+    });
 }
 
 function assertEventCounts(selecting, selected, deselect, msg) {
@@ -109,6 +123,659 @@ var BOXEVENTS = [1, 2, 1];
 // assumes 5 points in the lasso path
 var LASSOEVENTS = [4, 2, 1];
 
+var SELECT_PATH = [[93, 193], [143, 193]];
+var LASSO_PATH = [[316, 171], [318, 239], [335, 243], [328, 169]];
+
+describe('Click-to-select', function() {
+    var mock14Pts = {
+        '1': { x: 134, y: 116 },
+        '7': { x: 270, y: 160 },
+        '10': { x: 324, y: 198 },
+        '35': { x: 685, y: 341 }
+    };
+    var gd;
+
+    beforeEach(function() {
+        gd = createGraphDiv();
+    });
+
+    afterEach(destroyGraphDiv);
+
+    function plotMock14(layoutOpts) {
+        var mock = require('@mocks/14.json');
+        var defaultLayoutOpts = {
+            layout: {
+                clickmode: 'event+select',
+                dragmode: 'select',
+                hovermode: 'closest'
+            }
+        };
+        var mockCopy = Lib.extendDeep(
+          {},
+          mock,
+          defaultLayoutOpts,
+          { layout: layoutOpts });
+
+        return Plotly.plot(gd, mockCopy.data, mockCopy.layout);
+    }
+
+    /**
+     * Executes a click and before resets selection event handlers.
+     * By default, click is executed with a delay to prevent unwanted double clicks.
+     * Returns the `selectedPromise` promise for convenience.
+     */
+    function _click(x, y, clickOpts, immediate) {
+        resetEvents(gd);
+
+        // Too fast subsequent calls of `click` would
+        // produce an unwanted double click, thus we need
+        // to delay the click.
+        if(immediate) {
+            click(x, y, clickOpts);
+        } else {
+            setTimeout(function() {
+                click(x, y, clickOpts);
+            }, DBLCLICKDELAY * 1.01);
+        }
+
+        return selectedPromise;
+    }
+
+    function _clickPt(coords, clickOpts, immediate) {
+        expect(coords).toBeDefined('coords needs to be defined');
+        expect(coords.x).toBeDefined('coords.x needs to be defined');
+        expect(coords.y).toBeDefined('coords.y needs to be defined');
+
+        return _click(coords.x, coords.y, clickOpts, immediate);
+    }
+
+    /**
+     * Convenient helper to execute a click immediately.
+     */
+    function _immediateClickPt(coords, clickOpts) {
+        return _clickPt(coords, clickOpts, true);
+    }
+
+    /**
+     * Asserting selected points.
+     *
+     * @param expected can be a point number, an array
+     * of point numbers (for a single trace) or an array of point number
+     * arrays in case of multiple traces. undefined in an array of arrays
+     * is also allowed, e.g. useful when not all traces support selection.
+     */
+    function assertSelectedPoints(expected) {
+        var expectedPtsPerTrace = toArrayOfArrays(expected);
+        var expectedPts, traceNum;
+
+        for(traceNum = 0; traceNum < expectedPtsPerTrace.length; traceNum++) {
+            expectedPts = expectedPtsPerTrace[traceNum];
+            expect(gd._fullData[traceNum].selectedpoints).toEqual(expectedPts);
+            expect(gd.data[traceNum].selectedpoints).toEqual(expectedPts);
+        }
+
+        function toArrayOfArrays(expected) {
+            var isArrayInArray, i;
+
+            if(Array.isArray(expected)) {
+                isArrayInArray = false;
+                for(i = 0; i < expected.length; i++) {
+                    if(Array.isArray(expected[i])) {
+                        isArrayInArray = true;
+                        break;
+                    }
+                }
+
+                return isArrayInArray ? expected : [expected];
+            } else {
+                return [[expected]];
+            }
+        }
+    }
+
+    function assertSelectionCleared() {
+        gd._fullData.forEach(function(fullDataItem) {
+            expect(fullDataItem.selectedpoints).toBeUndefined();
+        });
+    }
+
+    it('selects a single data point when being clicked', function(done) {
+        plotMock14()
+          .then(function() { return _immediateClickPt(mock14Pts[7]); })
+          .then(function() { assertSelectedPoints(7); })
+          .catch(failTest)
+          .then(done);
+    });
+
+    describe('clears entire selection when the last selected data point', function() {
+        [{
+            desc: 'is clicked',
+            clickOpts: {}
+        }, {
+            desc: 'is clicked while add/subtract modifier keys are active',
+            clickOpts: { shiftKey: true }
+        }].forEach(function(testData) {
+            it('@flaky ' + testData.desc, function(done) {
+                plotMock14()
+                  .then(function() { return _immediateClickPt(mock14Pts[7]); })
+                  .then(function() {
+                      assertSelectedPoints(7);
+                      _clickPt(mock14Pts[7], testData.clickOpts);
+                      return deselectPromise;
+                  })
+                  .then(function() {
+                      assertSelectionCleared();
+                      return _clickPt(mock14Pts[35], testData.clickOpts);
+                  })
+                  .then(function() {
+                      assertSelectedPoints(35);
+                  })
+                  .catch(failTest)
+                  .then(done);
+            });
+        });
+    });
+
+    it('@flaky cleanly clears and starts selections although add/subtract mode on', function(done) {
+        plotMock14()
+          .then(function() {
+              return _immediateClickPt(mock14Pts[7]);
+          })
+          .then(function() {
+              assertSelectedPoints(7);
+              _clickPt(mock14Pts[7], { shiftKey: true });
+              return deselectPromise;
+          })
+          .then(function() {
+              assertSelectionCleared();
+              return _clickPt(mock14Pts[35], { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints(35);
+          })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('@flaky supports adding to an existing selection', function(done) {
+        plotMock14()
+          .then(function() { return _immediateClickPt(mock14Pts[7]); })
+          .then(function() {
+              assertSelectedPoints(7);
+              return _clickPt(mock14Pts[35], { shiftKey: true });
+          })
+          .then(function() { assertSelectedPoints([7, 35]); })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('@flaky supports subtracting from an existing selection', function(done) {
+        plotMock14()
+          .then(function() { return _immediateClickPt(mock14Pts[7]); })
+          .then(function() {
+              assertSelectedPoints(7);
+              return _clickPt(mock14Pts[35], { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([7, 35]);
+              return _clickPt(mock14Pts[7], { shiftKey: true });
+          })
+          .then(function() { assertSelectedPoints(35); })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('@flaky can be used interchangeably with lasso/box select', function(done) {
+        plotMock14()
+          .then(function() {
+              return _immediateClickPt(mock14Pts[35]);
+          })
+          .then(function() {
+              assertSelectedPoints(35);
+              drag(SELECT_PATH, { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([0, 1, 35]);
+              return _immediateClickPt(mock14Pts[7], { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([0, 1, 7, 35]);
+              return _clickPt(mock14Pts[1], { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([0, 7, 35]);
+              return Plotly.relayout(gd, 'dragmode', 'lasso');
+          })
+          .then(function() {
+              assertSelectedPoints([0, 7, 35]);
+              drag(LASSO_PATH, { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([0, 7, 10, 35]);
+              return _clickPt(mock14Pts[10], { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([0, 7, 35]);
+              drag([[670, 330], [695, 330], [695, 350], [670, 350]],
+                { shiftKey: true, altKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([0, 7]);
+              return _clickPt(mock14Pts[35], { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([0, 7, 35]);
+              return _clickPt(mock14Pts[7]);
+          })
+          .then(function() {
+              assertSelectedPoints([7]);
+              return doubleClick(650, 100);
+          })
+          .then(function() {
+              assertSelectionCleared();
+          })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('@gl works in a multi-trace plot', function(done) {
+        Plotly.plot(gd, [
+            {
+                x: [1, 3, 5, 4, 10, 12, 12, 7],
+                y: [2, 7, 6, 1, 0, 13, 6, 12],
+                type: 'scatter',
+                mode: 'markers',
+                marker: { size: 20 }
+            }, {
+                x: [1, 7, 6, 2],
+                y: [2, 3, 5, 4],
+                type: 'bar'
+            }, {
+                x: [7, 8, 9, 10],
+                y: [7, 9, 13, 21],
+                type: 'scattergl',
+                mode: 'markers',
+                marker: { size: 20 }
+            }
+        ], {
+            width: 400,
+            height: 600,
+            hovermode: 'closest',
+            dragmode: 'select',
+            clickmode: 'event+select'
+        })
+          .then(function() {
+              return _click(136, 369, {}, true); })
+          .then(function() {
+              assertSelectedPoints([[1], [], []]);
+              return _click(245, 136, { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([[1], [], [3]]);
+              return _click(183, 470, { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([[1], [2], [3]]);
+          })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('@flaky is supported in pan/zoom mode', function(done) {
+        plotMock14({ dragmode: 'zoom' })
+          .then(function() {
+              return _immediateClickPt(mock14Pts[35]);
+          })
+          .then(function() {
+              assertSelectedPoints(35);
+              return _clickPt(mock14Pts[7], { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([7, 35]);
+              return _clickPt(mock14Pts[7], { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints(35);
+              drag(LASSO_PATH);
+          })
+          .then(function() {
+              assertSelectedPoints(35);
+              _clickPt(mock14Pts[35], { shiftKey: true });
+              return deselectPromise;
+          })
+          .then(function() {
+              assertSelectionCleared();
+          })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('@flaky retains selected points when switching between pan and zoom mode', function(done) {
+        plotMock14({ dragmode: 'zoom' })
+          .then(function() {
+              return _immediateClickPt(mock14Pts[35]);
+          })
+          .then(function() {
+              assertSelectedPoints(35);
+              return Plotly.relayout(gd, 'dragmode', 'pan');
+          })
+          .then(function() {
+              assertSelectedPoints(35);
+              return _clickPt(mock14Pts[7], { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints([7, 35]);
+              return Plotly.relayout(gd, 'dragmode', 'zoom');
+          })
+          .then(function() {
+              assertSelectedPoints([7, 35]);
+              return _clickPt(mock14Pts[7], { shiftKey: true });
+          })
+          .then(function() {
+              assertSelectedPoints(35);
+          })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('@gl is supported by scattergl in pan/zoom mode', function(done) {
+        Plotly.plot(gd, [
+            {
+                x: [7, 8, 9, 10],
+                y: [7, 9, 13, 21],
+                type: 'scattergl',
+                mode: 'markers',
+                marker: { size: 20 }
+            }
+        ], {
+            width: 400,
+            height: 600,
+            hovermode: 'closest',
+            dragmode: 'zoom',
+            clickmode: 'event+select'
+        })
+          .then(function() {
+              return _click(230, 340, {}, true);
+          })
+          .then(function() {
+              assertSelectedPoints(2);
+          })
+          .catch(failTest)
+          .then(done);
+    });
+
+    it('@flaky deals correctly with histogram\'s binning in the persistent selection case', function(done) {
+        var mock = require('@mocks/histogram_colorscale.json');
+        var firstBinPts = [0];
+        var secondBinPts = [1, 2];
+        var thirdBinPts = [3, 4, 5];
+
+        mock.layout.clickmode = 'event+select';
+        Plotly.plot(gd, mock.data, mock.layout)
+          .then(function() {
+              return clickFirstBinImmediately();
+          })
+          .then(function() {
+              assertSelectedPoints(firstBinPts);
+              return shiftClickSecondBin();
+          })
+          .then(function() {
+              assertSelectedPoints([].concat(firstBinPts, secondBinPts));
+              return shiftClickThirdBin();
+          })
+          .then(function() {
+              assertSelectedPoints([].concat(firstBinPts, secondBinPts, thirdBinPts));
+              return clickFirstBin();
+          })
+          .then(function() {
+              assertSelectedPoints([].concat(firstBinPts));
+              clickFirstBin();
+              return deselectPromise;
+          })
+          .then(function() {
+              assertSelectionCleared();
+          })
+          .catch(failTest)
+          .then(done);
+
+        function clickFirstBinImmediately() { return _immediateClickPt({ x: 141, y: 358 }); }
+        function clickFirstBin() { return _click(141, 358); }
+        function shiftClickSecondBin() { return _click(239, 330, { shiftKey: true }); }
+        function shiftClickThirdBin() { return _click(351, 347, { shiftKey: true }); }
+    });
+
+    it('@flaky ignores clicks on boxes in a box trace type', function(done) {
+        var mock = Lib.extendDeep({}, require('@mocks/box_grouped_horz.json'));
+
+        mock.layout.clickmode = 'event+select';
+        mock.layout.width = 1100;
+        mock.layout.height = 450;
+
+        Plotly.plot(gd, mock.data, mock.layout)
+          .then(function() {
+              return clickPtImmediately();
+          })
+          .then(function() {
+              assertSelectedPoints(2);
+              clickPt();
+              return deselectPromise;
+          })
+          .then(function() {
+              assertSelectionCleared();
+              clickBox();
+              return clickedPromise;
+          })
+          .then(function() {
+              assertSelectionCleared();
+          })
+          .catch(failTest)
+          .then(done);
+
+        function clickPtImmediately() { return _immediateClickPt({ x: 610, y: 342 }); }
+        function clickPt() { return _clickPt({ x: 610, y: 342 }); }
+        function clickBox() { return _clickPt({ x: 565, y: 329 }); }
+    });
+
+    describe('is disabled when clickmode does not include \'select\'', function() {
+        ['select', 'lasso']
+          .forEach(function(dragmode) {
+              it('@flaky and dragmode is ' + dragmode, function(done) {
+                  plotMock14({ clickmode: 'event', dragmode: dragmode })
+                    .then(function() {
+                        // Still, the plotly_selected event should be thrown,
+                        // so return promise here
+                        return _immediateClickPt(mock14Pts[1]);
+                    })
+                    .then(function() {
+                        assertSelectionCleared();
+                    })
+                    .catch(failTest)
+                    .then(done);
+              });
+          });
+    });
+
+    describe('is disabled when clickmode does not include \'select\'', function() {
+        ['pan', 'zoom']
+          .forEach(function(dragmode) {
+              it('@flaky and dragmode is ' + dragmode, function(done) {
+                  plotMock14({ clickmode: 'event', dragmode: dragmode })
+                    .then(function() {
+                        _immediateClickPt(mock14Pts[1]);
+                        return clickedPromise;
+                    })
+                    .then(function() {
+                        assertSelectionCleared();
+                    })
+                    .catch(failTest)
+                    .then(done);
+              });
+          });
+    });
+
+    describe('is supported by', function() {
+        // On loading mocks:
+        // - Note, that `require` function calls are resolved at compile time
+        //   and thus dynamically concatenated mock paths won't work.
+        // - Some mocks don't specify a width and height, so this needs
+        //   to be set explicitly to ensure click coordinates fit.
+
+        // The non-gl traces: use @flaky CI annotation
+        [
+            testCase('histrogram', require('@mocks/histogram_colorscale.json'), 355, 301, [3, 4, 5]),
+            testCase('box', require('@mocks/box_grouped_horz.json'), 610, 342, [[2], [], []],
+              { width: 1100, height: 450 }),
+            testCase('violin', require('@mocks/violin_grouped.json'), 166, 187, [[3], [], []],
+              { width: 1100, height: 450 }),
+            testCase('ohlc', require('@mocks/ohlc_first.json'), 669, 165, [9]),
+            testCase('candlestick', require('@mocks/finance_style.json'), 331, 162, [[], [5]]),
+            testCase('choropleth', require('@mocks/geo_choropleth-text.json'), 440, 163, [6]),
+            testCase('scattergeo', require('@mocks/geo_scattergeo-locations.json'), 285, 240, [1]),
+            testCase('scatterternary', require('@mocks/ternary_markers.json'), 485, 335, [7]),
+
+            // Note that first trace (carpet) in mock doesn't support selection,
+            // thus undefined is expected
+            testCase('scattercarpet', require('@mocks/scattercarpet.json'), 532, 178,
+              [undefined, [], [], [], [], [], [2]], { width: 1100, height: 450 }),
+
+            // scatterpolar and scatterpolargl do not support pan (the default),
+            // so set dragmode to zoom
+            testCase('scatterpolar', require('@mocks/polar_scatter.json'), 130, 290,
+              [[], [], [], [19], [], []], { dragmode: 'zoom' }),
+        ]
+          .forEach(function(testCase) {
+              it('@flaky trace type ' + testCase.label, function(done) {
+                  _run(testCase, done);
+              });
+          });
+
+        // The gl traces: use @gl CI annotation
+        [
+            testCase('scatterpolargl', require('@mocks/glpolar_scatter.json'), 130, 290,
+              [[], [], [], [19], [], []], { dragmode: 'zoom' }),
+            testCase('splom', require('@mocks/splom_lower.json'), 427, 400, [[], [7], []])
+        ]
+          .forEach(function(testCase) {
+              it('@gl trace type ' + testCase.label, function(done) {
+                  _run(testCase, done);
+              });
+          });
+
+        // The mapbox traces: use @noCI annotation cause they are usually too resource-intensive
+        [
+            testCase('scattermapbox', require('@mocks/mapbox_0.json'), 650, 195, [[2], []], {},
+              { mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN })
+        ]
+          .forEach(function(testCase) {
+              it('@noCI trace type ' + testCase.label, function(done) {
+                  _run(testCase, done);
+              });
+          });
+
+        function _run(testCase, doneFn) {
+            Plotly.plot(gd, testCase.mock.data, testCase.mock.layout, testCase.mock.config)
+              .then(function() {
+                  return _immediateClickPt(testCase);
+              })
+              .then(function() {
+                  assertSelectedPoints(testCase.expectedPts);
+                  return Plotly.relayout(gd, 'dragmode', 'lasso');
+              })
+              .then(function() {
+                  _clickPt(testCase);
+                  return deselectPromise;
+              })
+              .then(function() {
+                  assertSelectionCleared();
+                  return _clickPt(testCase);
+              })
+              .then(function() {
+                  assertSelectedPoints(testCase.expectedPts);
+              })
+              .catch(failTest)
+              .then(doneFn);
+        }
+    });
+
+    describe('triggers \'plotly_selected\' before \'plotly_click\'', function() {
+        [
+            testCase('cartesian', require('@mocks/14.json'), 270, 160, [7]),
+            testCase('geo', require('@mocks/geo_scattergeo-locations.json'), 285, 240, [1]),
+            testCase('ternary', require('@mocks/ternary_markers.json'), 485, 335, [7]),
+            testCase('polar', require('@mocks/polar_scatter.json'), 130, 290,
+              [[], [], [], [19], [], []], { dragmode: 'zoom' })
+        ].forEach(function(testCase) {
+            it('@flaky for base plot ' + testCase.label, function(done) {
+                _run(testCase, done);
+            });
+        });
+
+        // The mapbox traces: use @noCI annotation cause they are usually too resource-intensive
+        [
+            testCase('mapbox', require('@mocks/mapbox_0.json'), 650, 195, [[2], []], {},
+              { mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN })
+        ].forEach(function(testCase) {
+            it('@noCI for base plot ' + testCase.label, function(done) {
+                _run(testCase, done);
+            });
+        });
+
+        function _run(testCase, doneFn) {
+            Plotly.plot(gd, testCase.mock.data, testCase.mock.layout, testCase.mock.config)
+              .then(function() {
+                  var clickHandlerCalled = false;
+                  var selectedHandlerCalled = false;
+
+                  gd.on('plotly_selected', function() {
+                      expect(clickHandlerCalled).toBe(false);
+                      selectedHandlerCalled = true;
+                  });
+                  gd.on('plotly_click', function() {
+                      clickHandlerCalled = true;
+                      expect(selectedHandlerCalled).toBe(true);
+                      doneFn();
+                  });
+
+                  return click(testCase.x, testCase.y);
+              })
+              .catch(failTest)
+              .then(doneFn);
+        }
+    });
+
+    function testCase(label, mock, x, y, expectedPts, layoutOptions, configOptions) {
+        var defaultLayoutOpts = {
+            layout: {
+                clickmode: 'event+select',
+                dragmode: 'pan',
+                hovermode: 'closest'
+            }
+        };
+        var customLayoutOptions = {
+            layout: layoutOptions
+        };
+        var customConfigOptions = {
+            config: configOptions
+        };
+        var mockCopy = Lib.extendDeep(
+          {},
+          mock,
+          defaultLayoutOpts,
+          customLayoutOptions,
+          customConfigOptions);
+
+        return {
+            label: label,
+            mock: mockCopy,
+            layoutOptions: layoutOptions,
+            x: x,
+            y: y,
+            expectedPts: expectedPts,
+            configOptions: configOptions
+        };
+    }
+});
+
 describe('Test select box and lasso in general:', function() {
     var mock = require('@mocks/14.json');
     var selectPath = [[93, 193], [143, 193]];
@@ -143,6 +810,7 @@ describe('Test select box and lasso in general:', function() {
     describe('select events', function() {
         var mockCopy = Lib.extendDeep({}, mock);
         mockCopy.layout.dragmode = 'select';
+        mockCopy.layout.hovermode = 'closest';
         mockCopy.data[0].ids = mockCopy.data[0].x
             .map(function(v) { return 'id-' + v; });
         mockCopy.data[0].customdata = mockCopy.data[0].y
@@ -293,6 +961,7 @@ describe('Test select box and lasso in general:', function() {
     describe('lasso events', function() {
         var mockCopy = Lib.extendDeep({}, mock);
         mockCopy.layout.dragmode = 'lasso';
+        mockCopy.layout.hovermode = 'closest';
         addInvisible(mockCopy);
 
         var gd;
@@ -627,6 +1296,43 @@ describe('Test select box and lasso in general:', function() {
         .then(done);
     });
 
+    it('should cleanly clear and restart selections on double click when add/subtract mode on', function(done) {
+        var gd = createGraphDiv();
+        var fig = Lib.extendDeep({}, require('@mocks/0.json'));
+
+        fig.layout.dragmode = 'select';
+        Plotly.plot(gd, fig)
+          .then(function() {
+              return drag([[350, 100], [400, 400]]);
+          })
+          .then(function() {
+              _assertSelectedPoints([49, 50, 51, 52, 53, 54, 55, 56, 57]);
+
+              // Note: although Shift has no behavioral effect on clearing a selection
+              // with a double click, users might hold the Shift key by accident.
+              // This test ensures selection is cleared as expected although
+              // the Shift key is held and no selection state is retained in any way.
+              return doubleClick(500, 200, { shiftKey: true });
+          })
+          .then(function() {
+              _assertSelectedPoints(null);
+              return drag([[450, 100], [500, 400]], { shiftKey: true });
+          })
+          .then(function() {
+              _assertSelectedPoints([67, 68, 69, 70, 71, 72, 73, 74]);
+          })
+          .catch(failTest)
+          .then(done);
+
+        function _assertSelectedPoints(selPts) {
+            if(selPts) {
+                expect(gd.data[0].selectedpoints).toEqual(selPts);
+            } else {
+                expect('selectedpoints' in gd.data[0]).toBe(false);
+            }
+        }
+    });
+
     it('@flaky should clear selected points on double click only on pan/lasso modes', function(done) {
         var gd = createGraphDiv();
         var fig = Lib.extendDeep({}, require('@mocks/0.json'));
@@ -635,6 +1341,7 @@ describe('Test select box and lasso in general:', function() {
         fig.layout.xaxis.range = [2, 8];
         fig.layout.yaxis.autorange = false;
         fig.layout.yaxis.range = [0, 3];
+        fig.layout.hovermode = 'closest';
 
         function _assert(msg, exp) {
             expect(gd.layout.xaxis.range)
@@ -1394,7 +2101,7 @@ describe('Test select box and lasso per trace:', function() {
         })
         .then(function() {
             return _run(
-                [[370, 120], [500, 200]], null, [280, 190], NOEVENTS, 'choropleth pan'
+                [[370, 120], [500, 200]], null, [200, 180], NOEVENTS, 'choropleth pan'
             );
         })
         .catch(failTest)
@@ -1833,14 +2540,14 @@ describe('Test select box and lasso per trace:', function() {
     it('@flaky should work on scatter/bar traces with text nodes', function(done) {
         var assertSelectedPoints = makeAssertSelectedPoints();
 
-        function assertFillOpacity(exp) {
+        function assertFillOpacity(exp, msg) {
             var txtPts = d3.select(gd).select('g.plot').selectAll('text');
 
-            expect(txtPts.size()).toBe(exp.length, '# of text nodes');
+            expect(txtPts.size()).toBe(exp.length, '# of text nodes: ' + msg);
 
             txtPts.each(function(_, i) {
                 var act = Number(this.style['fill-opacity']);
-                expect(act).toBe(exp[i], 'node ' + i + ' fill opacity');
+                expect(act).toBe(exp[i], 'node ' + i + ' fill opacity: ' + msg);
             });
         }
 
@@ -1857,6 +2564,7 @@ describe('Test select box and lasso per trace:', function() {
             textposition: 'outside'
         }], {
             dragmode: 'select',
+            hovermode: 'closest',
             showlegend: false,
             width: 400,
             height: 400,
@@ -1867,13 +2575,13 @@ describe('Test select box and lasso per trace:', function() {
                 [[10, 10], [100, 300]],
                 function() {
                     assertSelectedPoints({0: [0], 1: [0]});
-                    assertFillOpacity([1, 0.2, 0.2, 1, 0.2, 0.2]);
+                    assertFillOpacity([1, 0.2, 0.2, 1, 0.2, 0.2], '_run');
                 },
-                null, BOXEVENTS, 'selecting first scatter/bar text nodes'
+                [10, 10], BOXEVENTS, 'selecting first scatter/bar text nodes'
             );
         })
         .then(function() {
-            assertFillOpacity([1, 1, 1, 1, 1, 1]);
+            assertFillOpacity([1, 1, 1, 1, 1, 1], 'final');
         })
         .catch(failTest)
         .then(done);
diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js
index 8bd9847049a..f202749ed3b 100644
--- a/test/jasmine/tests/ternary_test.js
+++ b/test/jasmine/tests/ternary_test.js
@@ -1,7 +1,7 @@
 var Plotly = require('@lib');
 var Lib = require('@src/lib');
 
-var supplyLayoutDefaults = require('@src/plots/ternary/layout/defaults');
+var supplyLayoutDefaults = require('@src/plots/ternary/layout_defaults');
 
 var d3 = require('d3');
 var createGraphDiv = require('../assets/create_graph_div');
@@ -382,6 +382,60 @@ describe('ternary plots', function() {
         .then(done);
     });
 
+    it('should be able to hide/show ticks and tick labels', function(done) {
+        var gd = createGraphDiv();
+        var fig = Lib.extendDeep({}, require('@mocks/ternary_simple.json'));
+
+        function assertCnt(selector, expected, msg) {
+            var sel = d3.select(gd).selectAll(selector);
+            expect(sel.size()).toBe(expected, msg);
+        }
+
+        function toggle(selector, astr, vals, exps) {
+            return function() {
+                return Plotly.relayout(gd, astr, vals[0]).then(function() {
+                    assertCnt(selector, exps[0], astr + ' ' + vals[0]);
+                    return Plotly.relayout(gd, astr, vals[1]);
+                })
+                .then(function() {
+                    assertCnt(selector, exps[1], astr + ' ' + vals[1]);
+                    return Plotly.relayout(gd, astr, vals[0]);
+                })
+                .then(function() {
+                    assertCnt(selector, exps[0], astr + ' ' + vals[0]);
+                });
+            };
+        }
+
+        Plotly.plot(gd, fig)
+        .then(toggle(
+            '.aaxis > .ytick > text', 'ternary.aaxis.showticklabels',
+            [true, false], [4, 0]
+        ))
+        .then(toggle(
+            '.baxis > .xtick > text', 'ternary.baxis.showticklabels',
+            [true, false], [5, 0]
+        ))
+        .then(toggle(
+            '.caxis > .ytick > text', 'ternary.caxis.showticklabels',
+            [true, false], [4, 0]
+        ))
+        .then(toggle(
+            '.aaxis > path.ytick', 'ternary.aaxis.ticks',
+            ['outside', ''], [4, 0]
+        ))
+        .then(toggle(
+            '.baxis > path.xtick', 'ternary.baxis.ticks',
+            ['outside', ''], [5, 0]
+        ))
+        .then(toggle(
+            '.caxis > path.ytick', 'ternary.caxis.ticks',
+            ['outside', ''], [4, 0]
+        ))
+        .catch(failTest)
+        .then(done);
+    });
+
     it('should render a-axis and c-axis with negative offsets', function(done) {
         var gd = createGraphDiv();