diff --git a/src/components/fx/layout_defaults.js b/src/components/fx/layout_defaults.js
index 13d5d631919..a4800d094f0 100644
--- a/src/components/fx/layout_defaults.js
+++ b/src/components/fx/layout_defaults.js
@@ -28,6 +28,14 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
     else hovermodeDflt = 'closest';
 
     coerce('hovermode', hovermodeDflt);
+
+    // if only mapbox subplots is present on graph,
+    // reset 'zoom' dragmode to 'pan' until 'zoom' is implemented,
+    // so that the correct modebar button is active
+    if(layoutOut._has('mapbox') && layoutOut._basePlotModules.length === 1 &&
+       layoutOut.dragmode === 'zoom') {
+        layoutOut.dragmode = 'pan';
+    }
 };
 
 function isHoriz(fullData) {
diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js
index a97a86eb37d..5cb86a9eea2 100644
--- a/src/components/modebar/manage.js
+++ b/src/components/modebar/manage.js
@@ -11,6 +11,7 @@
 
 var Axes = require('../../plots/cartesian/axes');
 var scatterSubTypes = require('../../traces/scatter/subtypes');
+var Registry = require('../../registry');
 
 var createModeBar = require('./modebar');
 var modeBarButtons = require('./buttons');
@@ -78,7 +79,8 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) {
         hasGeo = fullLayout._has('geo'),
         hasPie = fullLayout._has('pie'),
         hasGL2D = fullLayout._has('gl2d'),
-        hasTernary = fullLayout._has('ternary');
+        hasTernary = fullLayout._has('ternary'),
+        hasMapbox = fullLayout._has('mapbox');
 
     var groups = [];
 
@@ -121,7 +123,10 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) {
     if(((hasCartesian || hasGL2D) && !allAxesFixed) || hasTernary) {
         dragModeGroup = ['zoom2d', 'pan2d'];
     }
-    if((hasCartesian || hasTernary || hasGL2D) && isSelectable(fullData)) {
+    if(hasMapbox) {
+        dragModeGroup = ['pan2d'];
+    }
+    if(isSelectable(fullData)) {
         dragModeGroup.push('select2d');
         dragModeGroup.push('lasso2d');
     }
@@ -173,7 +178,7 @@ function isSelectable(fullData) {
 
         if(!trace._module || !trace._module.selectPoints) continue;
 
-        if(trace.type === 'scatter' || trace.type === 'scatterternary' || trace.type === 'scattergl') {
+        if(Registry.traceIs(trace, 'scatter-like')) {
             if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) {
                 selectable = true;
             }
diff --git a/src/constants/interactions.js b/src/constants/interactions.js
index 3e56a09f434..20044ea85f9 100644
--- a/src/constants/interactions.js
+++ b/src/constants/interactions.js
@@ -18,5 +18,8 @@ module.exports = {
 
     // ms between first mousedown and 2nd mouseup to constitute dblclick...
     // we don't seem to have access to the system setting
-    DBLCLICKDELAY: 300
+    DBLCLICKDELAY: 300,
+
+    // opacity dimming fraction for points that are not in selection
+    DESELECTDIM: 0.2
 };
diff --git a/src/lib/index.js b/src/lib/index.js
index 25eff611e5c..8518d176d1d 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -451,7 +451,7 @@ lib.minExtend = function(obj1, obj2) {
     for(i = 0; i < keys.length; i++) {
         k = keys[i];
         v = obj1[k];
-        if(k.charAt(0) === '_' || typeof v === 'function' || k === 'glTrace') continue;
+        if(k.charAt(0) === '_' || typeof v === 'function') continue;
         else if(k === 'module') objOut[k] = v;
         else if(Array.isArray(v)) objOut[k] = v.slice(0, arrayLen);
         else if(v && (typeof v === 'object')) objOut[k] = lib.minExtend(obj1[k], obj2[k]);
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index 64d1663910d..a007e9ea940 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -378,21 +378,27 @@ exports.doTicksRelayout = function(gd) {
 
 exports.doModeBar = function(gd) {
     var fullLayout = gd._fullLayout;
-    var subplotIds, scene, i;
+    var subplotIds, subplotObj, i;
 
     ModeBar.manage(gd);
     initInteractions(gd);
 
     subplotIds = Plots.getSubplotIds(fullLayout, 'gl3d');
     for(i = 0; i < subplotIds.length; i++) {
-        scene = fullLayout[subplotIds[i]]._scene;
-        scene.updateFx(fullLayout.dragmode, fullLayout.hovermode);
+        subplotObj = fullLayout[subplotIds[i]]._scene;
+        subplotObj.updateFx(fullLayout.dragmode, fullLayout.hovermode);
     }
 
     subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d');
     for(i = 0; i < subplotIds.length; i++) {
-        scene = fullLayout._plots[subplotIds[i]]._scene2d;
-        scene.updateFx(fullLayout.dragmode);
+        subplotObj = fullLayout._plots[subplotIds[i]]._scene2d;
+        subplotObj.updateFx(fullLayout.dragmode);
+    }
+
+    subplotIds = Plots.getSubplotIds(fullLayout, 'mapbox');
+    for(i = 0; i < subplotIds.length; i++) {
+        subplotObj = fullLayout[subplotIds[i]]._subplot;
+        subplotObj.updateFx(fullLayout);
     }
 
     return Plots.previousPromises(gd);
diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js
index 883acb4d0d8..0649a155296 100644
--- a/src/plots/cartesian/index.js
+++ b/src/plots/cartesian/index.js
@@ -184,11 +184,6 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
             oldFullLayout._infolayer.select('.' + axIds[i] + 'title').remove();
         }
     }
-
-    // clean selection
-    if(oldFullLayout._zoomlayer) {
-        oldFullLayout._zoomlayer.selectAll('.select-outline').remove();
-    }
 };
 
 exports.drawFramework = function(gd) {
diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js
index 045585cdbac..d8542f5c090 100644
--- a/src/plots/cartesian/select.js
+++ b/src/plots/cartesian/select.js
@@ -25,8 +25,9 @@ function getAxId(ax) { return ax._id; }
 module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
     var zoomLayer = dragOptions.gd._fullLayout._zoomlayer,
         dragBBox = dragOptions.element.getBoundingClientRect(),
-        xs = dragOptions.plotinfo.xaxis._offset,
-        ys = dragOptions.plotinfo.yaxis._offset,
+        plotinfo = dragOptions.plotinfo,
+        xs = plotinfo.xaxis._offset,
+        ys = plotinfo.yaxis._offset,
         x0 = startX - dragBBox.left,
         y0 = startY - dragBBox.top,
         x1 = x0,
@@ -71,6 +72,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
         searchInfo,
         selection = [],
         eventData;
+
     for(i = 0; i < gd.calcdata.length; i++) {
         cd = gd.calcdata[i];
         trace = cd[0].trace;
@@ -106,9 +108,41 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
 
     function ascending(a, b) { return a - b; }
 
+    // allow subplots to override fillRangeItems routine
+    var fillRangeItems;
+
+    if(plotinfo.fillRangeItems) {
+        fillRangeItems = plotinfo.fillRangeItems;
+    } else {
+        if(mode === 'select') {
+            fillRangeItems = function(eventData, poly) {
+                var ranges = eventData.range = {};
+
+                for(i = 0; i < allAxes.length; i++) {
+                    var ax = allAxes[i];
+                    var axLetter = ax._id.charAt(0);
+
+                    ranges[ax._id] = [
+                        ax.p2d(poly[axLetter + 'min']),
+                        ax.p2d(poly[axLetter + 'max'])
+                    ].sort(ascending);
+                }
+            };
+        } else {
+            fillRangeItems = function(eventData, poly, pts) {
+                var dataPts = eventData.lassoPoints = {};
+
+                for(i = 0; i < allAxes.length; i++) {
+                    var ax = allAxes[i];
+                    dataPts[ax._id] = pts.filtered.map(axValue(ax));
+                }
+            };
+        }
+    }
+
     dragOptions.moveFn = function(dx0, dy0) {
-        var poly,
-            ax;
+        var poly;
+
         x1 = Math.max(0, Math.min(pw, dx0 + x0));
         y1 = Math.max(0, Math.min(ph, dy0 + y0));
 
@@ -158,27 +192,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
         }
 
         eventData = {points: selection};
-
-        if(mode === 'select') {
-            var ranges = eventData.range = {},
-                axLetter;
-
-            for(i = 0; i < allAxes.length; i++) {
-                ax = allAxes[i];
-                axLetter = ax._id.charAt(0);
-                ranges[ax._id] = [
-                    ax.p2d(poly[axLetter + 'min']),
-                    ax.p2d(poly[axLetter + 'max'])].sort(ascending);
-            }
-        }
-        else {
-            var dataPts = eventData.lassoPoints = {};
-
-            for(i = 0; i < allAxes.length; i++) {
-                ax = allAxes[i];
-                dataPts[ax._id] = pts.filtered.map(axValue(ax));
-            }
-        }
+        fillRangeItems(eventData, poly, pts);
         dragOptions.gd.emit('plotly_selecting', eventData);
     };
 
diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js
index 30e28eace91..e2c923421b6 100644
--- a/src/plots/mapbox/mapbox.js
+++ b/src/plots/mapbox/mapbox.js
@@ -13,6 +13,8 @@ var mapboxgl = require('mapbox-gl');
 
 var Fx = require('../../components/fx');
 var Lib = require('../../lib');
+var dragElement = require('../../components/dragelement');
+var prepSelect = require('../cartesian/select');
 var constants = require('./constants');
 var layoutAttributes = require('./layout_attributes');
 var createMapboxLayer = require('./layers');
@@ -86,9 +88,9 @@ proto.plot = function(calcData, fullLayout, promises) {
 };
 
 proto.createMap = function(calcData, fullLayout, resolve, reject) {
-    var self = this,
-        gd = self.gd,
-        opts = self.opts;
+    var self = this;
+    var gd = self.gd;
+    var opts = self.opts;
 
     // store style id and URL or object
     var styleObj = self.styleObj = getStyleObj(opts.style);
@@ -107,7 +109,9 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
         pitch: opts.pitch,
 
         interactive: !self.isStatic,
-        preserveDrawingBuffer: self.isStatic
+        preserveDrawingBuffer: self.isStatic,
+
+        boxZoom: false
     });
 
     // clear navigation container
@@ -128,6 +132,8 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
         self.resolveOnRender(resolve);
     });
 
+    if(self.isStatic) return;
+
     // keep track of pan / zoom in user layout and emit relayout event
     map.on('moveend', function(eventData) {
         if(!self.map) return;
@@ -261,6 +267,7 @@ proto.updateLayout = function(fullLayout) {
 
     this.updateLayers();
     this.updateFramework(fullLayout);
+    this.updateFx(fullLayout);
     this.map.resize();
 };
 
@@ -314,6 +321,69 @@ proto.createFramework = function(fullLayout) {
     self.updateFramework(fullLayout);
 };
 
+proto.updateFx = function(fullLayout) {
+    var self = this;
+    var map = self.map;
+    var gd = self.gd;
+
+    if(self.isStatic) return;
+
+    function invert(pxpy) {
+        var obj = self.map.unproject(pxpy);
+        return [obj.lng, obj.lat];
+    }
+
+    var dragMode = fullLayout.dragmode;
+    var fillRangeItems;
+
+    if(dragMode === 'select') {
+        fillRangeItems = function(eventData, poly) {
+            var ranges = eventData.range = {};
+            ranges[self.id] = [
+                invert([poly.xmin, poly.ymin]),
+                invert([poly.xmax, poly.ymax])
+            ];
+        };
+    } else {
+        fillRangeItems = function(eventData, poly, pts) {
+            var dataPts = eventData.lassoPoints = {};
+            dataPts[self.id] = pts.filtered.map(invert);
+        };
+    }
+
+    if(dragMode === 'select' || dragMode === 'lasso') {
+        map.dragPan.disable();
+
+        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
+        };
+
+        dragOptions.prepFn = function(e, startX, startY) {
+            prepSelect(e, startX, startY, dragOptions, dragMode);
+        };
+
+        dragOptions.doneFn = function(dragged, numClicks) {
+            if(numClicks === 2) {
+                fullLayout._zoomlayer.selectAll('.select-outline').remove();
+            }
+        };
+
+        dragElement.init(dragOptions);
+    } else {
+        map.dragPan.enable();
+        self.div.onmousedown = null;
+    }
+};
+
 proto.updateFramework = function(fullLayout) {
     var domain = fullLayout[this.id].domain,
         size = fullLayout._size;
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 38416678886..de398f4353b 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -624,6 +624,10 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou
                 .selectAll(query).remove();
         }
     }
+
+    if(oldFullLayout._zoomlayer) {
+        oldFullLayout._zoomlayer.selectAll('.select-outline').remove();
+    }
 };
 
 plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLayout) {
diff --git a/src/plots/ternary/index.js b/src/plots/ternary/index.js
index 2b24ed0938a..e1da80af7b1 100644
--- a/src/plots/ternary/index.js
+++ b/src/plots/ternary/index.js
@@ -69,8 +69,4 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
             oldTernary.clipDef.remove();
         }
     }
-
-    if(oldFullLayout._zoomlayer) {
-        oldFullLayout._zoomlayer.selectAll('.select-outline').remove();
-    }
 };
diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js
index 197b540e8d2..6817e4c8162 100644
--- a/src/traces/scatter/index.js
+++ b/src/traces/scatter/index.js
@@ -34,7 +34,7 @@ Scatter.animatable = true;
 Scatter.moduleType = 'trace';
 Scatter.name = 'scatter';
 Scatter.basePlotModule = require('../../plots/cartesian');
-Scatter.categories = ['cartesian', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend'];
+Scatter.categories = ['cartesian', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like'];
 Scatter.meta = {
     description: [
         'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.',
diff --git a/src/traces/scatter/select.js b/src/traces/scatter/select.js
index 127179a790c..22aedd1685b 100644
--- a/src/traces/scatter/select.js
+++ b/src/traces/scatter/select.js
@@ -10,8 +10,7 @@
 'use strict';
 
 var subtypes = require('./subtypes');
-
-var DESELECTDIM = 0.2;
+var DESELECTDIM = require('../../constants/interactions').DESELECTDIM;
 
 module.exports = function selectPoints(searchInfo, polygon) {
     var cd = searchInfo.cd,
diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js
index f99a4a73a62..5b7be4466d9 100644
--- a/src/traces/scattergl/convert.js
+++ b/src/traces/scattergl/convert.js
@@ -27,9 +27,9 @@ var makeBubbleSizeFn = require('../scatter/make_bubble_size_func');
 var getTraceColor = require('../scatter/get_trace_color');
 var MARKER_SYMBOLS = require('../../constants/gl2d_markers');
 var DASHES = require('../../constants/gl2d_dashes');
+var DESELECTDIM = require('../../constants/interactions').DESELECTDIM;
 
 var AXES = ['xaxis', 'yaxis'];
-var DESELECTDIM = 0.2;
 var TRANSPARENT = [0, 0, 0, 0];
 
 function LineWithMarkers(scene, uid) {
@@ -322,8 +322,8 @@ proto.update = function(options, cdscatter) {
     this.color = getTraceColor(options, {});
 
     // provide reference for selecting points
-    if(cdscatter && cdscatter[0] && !cdscatter[0].glTrace) {
-        cdscatter[0].glTrace = this;
+    if(cdscatter && cdscatter[0] && !cdscatter[0]._glTrace) {
+        cdscatter[0]._glTrace = this;
     }
 };
 
diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js
index 551e9adad7d..35d292f1c90 100644
--- a/src/traces/scattergl/index.js
+++ b/src/traces/scattergl/index.js
@@ -23,7 +23,7 @@ ScatterGl.selectPoints = require('./select');
 ScatterGl.moduleType = 'trace';
 ScatterGl.name = 'scattergl';
 ScatterGl.basePlotModule = require('../../plots/gl2d');
-ScatterGl.categories = ['gl2d', 'symbols', 'errorBarsOK', 'markerColorscale', 'showLegend'];
+ScatterGl.categories = ['gl2d', 'symbols', 'errorBarsOK', 'markerColorscale', 'showLegend', 'scatter-like'];
 ScatterGl.meta = {
     description: [
         'The data visualized as scatter point or lines is set in `x` and `y`',
diff --git a/src/traces/scattergl/select.js b/src/traces/scattergl/select.js
index 548824ad740..f594c69dd74 100644
--- a/src/traces/scattergl/select.js
+++ b/src/traces/scattergl/select.js
@@ -22,8 +22,8 @@ module.exports = function selectPoints(searchInfo, polygon) {
         x,
         y;
 
-    var scattergl = cd[0].glTrace;
-    var scene = cd[0].glTrace.scene;
+    var glTrace = cd[0]._glTrace;
+    var scene = glTrace.scene;
 
     var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace));
     if(trace.visible !== true || hasOnlyLines) return;
@@ -53,7 +53,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
     // highlight selected points here
     trace.selection = selection;
 
-    scattergl.update(trace, cd);
+    glTrace.update(trace, cd);
     scene.glplot.setDirty();
 
     return selection;
diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js
index c061f1141aa..5085eeb48fe 100644
--- a/src/traces/scattermapbox/attributes.js
+++ b/src/traces/scattermapbox/attributes.js
@@ -82,9 +82,7 @@ module.exports = {
                 'are only available for *circle* symbols.'
             ].join(' ')
         },
-        opacity: extendFlat({}, markerAttrs.opacity, {
-            arrayOk: false
-        }),
+        opacity: markerAttrs.opacity,
         size: markerAttrs.size,
         sizeref: markerAttrs.sizeref,
         sizemin: markerAttrs.sizemin,
diff --git a/src/traces/scattermapbox/convert.js b/src/traces/scattermapbox/convert.js
index 01312110dae..7b88ee647d6 100644
--- a/src/traces/scattermapbox/convert.js
+++ b/src/traces/scattermapbox/convert.js
@@ -9,6 +9,8 @@
 
 'use strict';
 
+var isNumeric = require('fast-isnumeric');
+
 var Lib = require('../../lib');
 var BADNUM = require('../../constants/numerical').BADNUM;
 var geoJsonUtils = require('../../lib/geojson_utils');
@@ -17,10 +19,11 @@ var Colorscale = require('../../components/colorscale');
 var makeBubbleSizeFn = require('../scatter/make_bubble_size_func');
 var subTypes = require('../scatter/subtypes');
 var convertTextOpts = require('../../plots/mapbox/convert_text_opts');
+var DESELECTDIM = require('../../constants/interactions').DESELECTDIM;
 
 var COLOR_PROP = 'circle-color';
 var SIZE_PROP = 'circle-radius';
-
+var OPACITY_PROP = 'circle-opacity';
 
 module.exports = function convert(calcTrace) {
     var trace = calcTrace[0].trace;
@@ -80,12 +83,13 @@ module.exports = function convert(calcTrace) {
         var hash = {};
         hash[COLOR_PROP] = {};
         hash[SIZE_PROP] = {};
+        hash[OPACITY_PROP] = {};
 
         circle.geojson = makeCircleGeoJSON(calcTrace, hash);
         circle.layout.visibility = 'visible';
 
         Lib.extendFlat(circle.paint, {
-            'circle-opacity': trace.opacity * trace.marker.opacity,
+            'circle-opacity': calcCircleOpacity(trace, hash),
             'circle-color': calcCircleColor(trace, hash),
             'circle-radius': calcCircleRadius(trace, hash)
         });
@@ -179,8 +183,22 @@ function makeCircleGeoJSON(calcTrace, hash) {
     var sizeFn;
     if(subTypes.isBubble(trace)) {
         sizeFn = makeBubbleSizeFn(trace);
-    } else if(Array.isArray(marker.size)) {
-        sizeFn = Lib.identity;
+    }
+
+    function combineOpacities(d, mo) {
+        return trace.opacity * mo * (d.dim ? DESELECTDIM : 1);
+    }
+
+    var opacityFn;
+    if(Array.isArray(marker.opacity)) {
+        opacityFn = function(d) {
+            var mo = isNumeric(d.mo) ? +Lib.constrain(d.mo, 0, 1) : 0;
+            return combineOpacities(d, mo);
+        };
+    } else if(trace._hasDimmedPts) {
+        opacityFn = function(d) {
+            return combineOpacities(d, marker.opacity);
+        };
     }
 
     // Translate vals in trace arrayOk containers
@@ -204,7 +222,12 @@ function makeCircleGeoJSON(calcTrace, hash) {
             var mcc = calcPt.mcc = colorFn(calcPt.mc);
             translate(props, COLOR_PROP, mcc, i);
         }
-        if(sizeFn) translate(props, SIZE_PROP, sizeFn(calcPt.ms), i);
+        if(sizeFn) {
+            translate(props, SIZE_PROP, sizeFn(calcPt.ms), i);
+        }
+        if(opacityFn) {
+            translate(props, OPACITY_PROP, opacityFn(calcPt), i);
+        }
 
         features.push({
             type: 'Feature',
@@ -304,14 +327,9 @@ function calcCircleRadius(trace, hash) {
             stops.push([ hash[SIZE_PROP][val], +val ]);
         }
 
-        // stops indices must be sorted
-        stops.sort(function(a, b) {
-            return a[0] - b[0];
-        });
-
         out = {
             property: SIZE_PROP,
-            stops: stops
+            stops: stops.sort(ascending)
         };
     }
     else {
@@ -321,6 +339,31 @@ function calcCircleRadius(trace, hash) {
     return out;
 }
 
+function calcCircleOpacity(trace, hash) {
+    var marker = trace.marker;
+    var out;
+
+    if(Array.isArray(marker.opacity) || trace._hasDimmedPts) {
+        var vals = Object.keys(hash[OPACITY_PROP]);
+        var stops = [];
+
+        for(var i = 0; i < vals.length; i++) {
+            var val = vals[i];
+            stops.push([hash[OPACITY_PROP][val], +val]);
+        }
+
+        out = {
+            property: OPACITY_PROP,
+            stops: stops.sort(ascending)
+        };
+    }
+    else {
+        out = trace.opacity * marker.opacity;
+    }
+
+    return out;
+}
+
 function getFillFunc(attr) {
     if(Array.isArray(attr)) {
         return function(v) { return v; };
@@ -335,6 +378,8 @@ function getFillFunc(attr) {
 
 function blankFillFunc() { return ''; }
 
+function ascending(a, b) { return a[0] - b[0]; }
+
 // only need to check lon (OR lat)
 function isBADNUM(lonlat) {
     return lonlat[0] === BADNUM;
diff --git a/src/traces/scattermapbox/index.js b/src/traces/scattermapbox/index.js
index 6de4241ed82..251432a82ff 100644
--- a/src/traces/scattermapbox/index.js
+++ b/src/traces/scattermapbox/index.js
@@ -15,14 +15,15 @@ ScatterMapbox.attributes = require('./attributes');
 ScatterMapbox.supplyDefaults = require('./defaults');
 ScatterMapbox.colorbar = require('../scatter/colorbar');
 ScatterMapbox.calc = require('../scattergeo/calc');
+ScatterMapbox.plot = require('./plot');
 ScatterMapbox.hoverPoints = require('./hover');
 ScatterMapbox.eventData = require('./event_data');
-ScatterMapbox.plot = require('./plot');
+ScatterMapbox.selectPoints = require('./select');
 
 ScatterMapbox.moduleType = 'trace';
 ScatterMapbox.name = 'scattermapbox';
 ScatterMapbox.basePlotModule = require('../../plots/mapbox');
-ScatterMapbox.categories = ['mapbox', 'gl', 'symbols', 'markerColorscale', 'showLegend'];
+ScatterMapbox.categories = ['mapbox', 'gl', 'symbols', 'markerColorscale', 'showLegend', 'scatterlike'];
 ScatterMapbox.meta = {
     hrName: 'scatter_mapbox',
     description: [
diff --git a/src/traces/scattermapbox/plot.js b/src/traces/scattermapbox/plot.js
index f7cc4040a2f..0a4391371cf 100644
--- a/src/traces/scattermapbox/plot.js
+++ b/src/traces/scattermapbox/plot.js
@@ -92,6 +92,9 @@ proto.update = function update(calcTrace) {
         mapbox.setSourceData(this.idSourceSymbol, opts.symbol.geojson);
         mapbox.setOptions(this.idLayerSymbol, 'setPaintProperty', opts.symbol.paint);
     }
+
+    // link ref for quick update during selections
+    calcTrace[0].trace._glTrace = this;
 };
 
 proto.dispose = function dispose() {
diff --git a/src/traces/scattermapbox/select.js b/src/traces/scattermapbox/select.js
new file mode 100644
index 00000000000..5ceb57b51a4
--- /dev/null
+++ b/src/traces/scattermapbox/select.js
@@ -0,0 +1,57 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+var subtypes = require('../scatter/subtypes');
+
+module.exports = function selectPoints(searchInfo, polygon) {
+    var cd = searchInfo.cd;
+    var xa = searchInfo.xaxis;
+    var ya = searchInfo.yaxis;
+    var selection = [];
+    var trace = cd[0].trace;
+
+    var di, lonlat, x, y, i;
+
+    // flag used in ./convert.js
+    // to not insert data-driven 'circle-opacity' when we don't need to
+    trace._hasDimmedPts = false;
+
+    if(trace.visible !== true || !subtypes.hasMarkers(trace)) return;
+
+    if(polygon === false) {
+        for(i = 0; i < cd.length; i++) {
+            cd[i].dim = 0;
+        }
+    } else {
+        for(i = 0; i < cd.length; i++) {
+            di = cd[i];
+            lonlat = di.lonlat;
+            x = xa.c2p(lonlat);
+            y = ya.c2p(lonlat);
+
+            if(polygon.contains([x, y])) {
+                trace._hasDimmedPts = true;
+                selection.push({
+                    pointNumber: i,
+                    lon: lonlat[0],
+                    lat: lonlat[1]
+                });
+                di.dim = 0;
+            } else {
+                di.dim = 1;
+            }
+        }
+    }
+
+    trace._glTrace.update(cd);
+
+    return selection;
+};
diff --git a/src/traces/scatterternary/index.js b/src/traces/scatterternary/index.js
index e7e75c4ddfc..abbdc7f06b1 100644
--- a/src/traces/scatterternary/index.js
+++ b/src/traces/scatterternary/index.js
@@ -22,7 +22,7 @@ ScatterTernary.selectPoints = require('./select');
 ScatterTernary.moduleType = 'trace';
 ScatterTernary.name = 'scatterternary';
 ScatterTernary.basePlotModule = require('../../plots/ternary');
-ScatterTernary.categories = ['ternary', 'symbols', 'markerColorscale', 'showLegend'];
+ScatterTernary.categories = ['ternary', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'];
 ScatterTernary.meta = {
     hrName: 'scatter_ternary',
     description: [
diff --git a/tasks/noci_test.sh b/tasks/noci_test.sh
index b4a2283b673..17d7dfc4c50 100755
--- a/tasks/noci_test.sh
+++ b/tasks/noci_test.sh
@@ -8,7 +8,7 @@ EXIT_STATE=0
 npm run test-jasmine -- --tags=noCI --nowatch || EXIT_STATE=$?
 
 # mapbox image tests take too much resources on CI
-npm run test-image -- mapbox_* || EXIT_STATE=$?
+npm run test-image -- mapbox_* --queue || EXIT_STATE=$?
 
 # run gl2d image test again (some mocks are skipped on CI)
 npm run test-image-gl2d || EXIT_STATE=$?
diff --git a/test/image/baselines/mapbox_bubbles.png b/test/image/baselines/mapbox_bubbles.png
index 11b5e21aa70..e0544baa69a 100644
Binary files a/test/image/baselines/mapbox_bubbles.png and b/test/image/baselines/mapbox_bubbles.png differ
diff --git a/test/image/mocks/mapbox_bubbles.json b/test/image/mocks/mapbox_bubbles.json
index 756ec774227..3f72cc2dbce 100644
--- a/test/image/mocks/mapbox_bubbles.json
+++ b/test/image/mocks/mapbox_bubbles.json
@@ -2,72 +2,30 @@
   "data": [
     {
       "type": "scattermapbox",
-      "lon": [
-        10,
-        20,
-        30
-      ],
-      "lat": [
-        10,
-        20,
-        30
-      ],
+      "lon": [10, 20, 30],
+      "lat": [10, 20, 30],
       "marker": {
-        "size": [
-          20,
-          10,
-          40
-        ],
-        "color": [
-          "red",
-          "blue",
-          "orange"
-        ]
-      }
+        "size": [20, 10, 40],
+        "color": ["red", "blue", "orange"],
+        "opacity": [0.3, 0.5, 1]
+      },
+      "opacity": 0.7
     },
     {
       "type": "scattermapbox",
-      "lon": [
-        -75,
-        -120,
-        100
-      ],
-      "lat": [
-        45,
-        20,
-        -40
-      ],
+      "lon": [-75, -120, 100],
+      "lat": [45, 20, -40],
       "marker": {
-        "size": [
-          60,
-          20,
-          40
-        ],
-        "color": [
-          0,
-          20,
-          30
-        ],
+        "size": [60, 20, 40],
+        "color": [0, 20, 30],
         "colorbar": {},
         "cmin": 0,
         "cmax": 30,
         "colorscale": [
-          [
-            0,
-            "rgb(220,220,220)"
-          ],
-          [
-            0.2,
-            "rgb(245,195,157)"
-          ],
-          [
-            0.4,
-            "rgb(245,160,105)"
-          ],
-          [
-            1,
-            "rgb(178,10,28)"
-          ]
+          [0, "rgb(220,220,220)"],
+          [0.2, "rgb(245,195,157)"],
+          [0.4, "rgb(245,160,105)"],
+          [1, "rgb(178,10,28)"]
         ]
       }
     }
@@ -79,7 +37,7 @@
     },
     "showlegend": false,
     "height": 450,
-    "width": 1100,
-    "autosize": true
+    "width": 600,
+    "margin": {"l": 10}
   }
 }
diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js
index 49b4583ff7f..050605b2494 100644
--- a/test/jasmine/tests/mapbox_test.js
+++ b/test/jasmine/tests/mapbox_test.js
@@ -1,4 +1,5 @@
 var Plotly = require('@lib');
+var Plots = require('@src/plots/plots');
 var Lib = require('@src/lib');
 
 var constants = require('@src/plots/mapbox/constants');
@@ -31,7 +32,7 @@ describe('mapbox defaults', function() {
     beforeEach(function() {
         layoutOut = { font: { color: 'red' } };
 
-        // needs a ternary-ref in a trace in order to be detected
+        // needs a mapbox-ref in a trace in order to be detected
         fullData = [{ type: 'scattermapbox', subplot: 'mapbox' }];
     });
 
@@ -170,6 +171,16 @@ describe('mapbox defaults', function() {
         expect(layoutOut.mapbox.layers[3].fill).toBeUndefined();
         expect(layoutOut.mapbox.layers[3].circle).toBeUndefined();
     });
+
+    it('should set *layout.dragmode* to pan while zoom is not available', function() {
+        var gd = {
+            data: fullData,
+            layout: {}
+        };
+
+        Plots.supplyDefaults(gd);
+        expect(gd._fullLayout.dragmode).toBe('pan');
+    });
 });
 
 describe('mapbox credentials', function() {
diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js
index 5045fd8532d..938f96cd445 100644
--- a/test/jasmine/tests/scattermapbox_test.js
+++ b/test/jasmine/tests/scattermapbox_test.js
@@ -114,7 +114,7 @@ describe('scattermapbox convert', function() {
         jasmine.addMatchers(customMatchers);
     });
 
-    function _convert(trace) {
+    function _convert(trace, selected) {
         var gd = { data: [trace] };
         Plots.supplyDefaults(gd);
 
@@ -122,6 +122,18 @@ describe('scattermapbox convert', function() {
         Plots.doCalcdata(gd, fullTrace);
 
         var calcTrace = gd.calcdata[0];
+
+        if(selected) {
+            var hasDimmedPts = false;
+
+            selected.forEach(function(v, i) {
+                if(v) hasDimmedPts = true;
+                calcTrace[i].dim = v;
+            });
+
+            fullTrace._hasDimmedPts = hasDimmedPts;
+        }
+
         return convert(calcTrace);
     }
 
@@ -155,6 +167,8 @@ describe('scattermapbox convert', function() {
             stops: [ [0, 5], [1, 10], [2, 0] ]
         }, 'circle-radius stops');
 
+        expect(opts.circle.paint['circle-opacity']).toBe(0.7, 'circle-opacity');
+
         var circleProps = opts.circle.geojson.features.map(function(f) {
             return f.properties;
         });
@@ -169,6 +183,95 @@ describe('scattermapbox convert', function() {
         ], 'geojson feature properties');
     });
 
+    it('should fill circle-opacity correctly', function() {
+        var opts = _convert(Lib.extendFlat({}, base, {
+            mode: 'markers',
+            marker: {
+                symbol: 'circle',
+                size: 10,
+                color: 'red',
+                opacity: [1, null, 0.5, '0.5', '1', 0, 0.8]
+            },
+            opacity: 0.5
+        }));
+
+        assertVisibility(opts, ['none', 'none', 'visible', 'none']);
+        expect(opts.circle.paint['circle-color']).toBe('red', 'circle-color');
+        expect(opts.circle.paint['circle-radius']).toBe(5, 'circle-radius');
+
+        expect(opts.circle.paint['circle-opacity']).toEqual({
+            property: 'circle-opacity',
+            stops: [ [0, 0.5], [1, 0], [2, 0.25], [6, 0.4] ]
+        }, 'circle-opacity stops');
+
+        var circleProps = opts.circle.geojson.features.map(function(f) {
+            return f.properties;
+        });
+
+
+        expect(circleProps).toEqual([
+            { 'circle-opacity': 0 },
+            { 'circle-opacity': 1 },
+            { 'circle-opacity': 2 },
+            // lat === null
+            // lon === null
+            { 'circle-opacity': 1 },
+            { 'circle-opacity': 6 },
+        ], 'geojson feature properties');
+    });
+
+    it('should fill circle-opacity correctly during selections', function() {
+        var _base = {
+            type: 'scattermapbox',
+            mode: 'markers',
+            lon: [-10, 30, 20],
+            lat: [45, 90, 180],
+            marker: {symbol: 'circle'}
+        };
+
+        var specs = [{
+            patch: {},
+            selected: [0, 1, 1],
+            expected: {stops: [[0, 1], [1, 0.2]], props: [0, 1, 1]}
+        }, {
+            patch: {opacity: 0.5},
+            selected: [0, 1, 1],
+            expected: {stops: [[0, 0.5], [1, 0.1]], props: [0, 1, 1]}
+        }, {
+            patch: {
+                marker: {opacity: 0.6}
+            },
+            selected: [1, 0, 1],
+            expected: {stops: [[0, 0.12], [1, 0.6]], props: [0, 1, 0]}
+        }, {
+            patch: {
+                marker: {opacity: [0.5, 1, 0.6]}
+            },
+            selected: [1, 0, 1],
+            expected: {stops: [[0, 0.1], [1, 1], [2, 0.12]], props: [0, 1, 2]}
+        }, {
+            patch: {
+                marker: {opacity: [2, null, -0.6]}
+            },
+            selected: [1, 1, 1],
+            expected: {stops: [[0, 0.2], [1, 0]], props: [0, 1, 1]}
+        }];
+
+        specs.forEach(function(s, i) {
+            var msg0 = '- case ' + i + ' ';
+            var opts = _convert(Lib.extendDeep({}, _base, s.patch), s.selected);
+
+            expect(opts.circle.paint['circle-opacity'].stops)
+                .toEqual(s.expected.stops, msg0 + 'stops');
+
+            var props = opts.circle.geojson.features.map(function(f) {
+                return f.properties['circle-opacity'];
+            });
+
+            expect(props).toEqual(s.expected.props, msg0 + 'props');
+        });
+    });
+
     it('should generate correct output for fill + markers + lines traces', function() {
         var opts = _convert(Lib.extendFlat({}, base, {
             mode: 'markers+lines',
@@ -510,7 +613,6 @@ describe('@noCI scattermapbox hover', function() {
     });
 });
 
-
 describe('@noCI Test plotly events on a scattermapbox plot:', function() {
     var mock = require('@mocks/mapbox_0.json');
 
diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js
index 86d0825700c..ac26eccb435 100644
--- a/test/jasmine/tests/select_test.js
+++ b/test/jasmine/tests/select_test.js
@@ -10,6 +10,8 @@ var fail = require('../assets/fail_test');
 var mouseEvent = require('../assets/mouse_event');
 var customMatchers = require('../assets/custom_matchers');
 
+var LONG_TIMEOUT_INTERVAL = 5 * jasmine.DEFAULT_TIMEOUT_INTERVAL;
+
 
 describe('select box and lasso', function() {
     var mock = require('@mocks/14.json');
@@ -284,6 +286,11 @@ describe('select box and lasso', function() {
                 y: 2.75,
             }], 'with the correct selected points (2)');
 
+            expect(selectedData.lassoPoints.x).toBeCloseToArray(
+                [0.084, 0.087, 0.115, 0.103], 'lasso points x coords');
+            expect(selectedData.lassoPoints.y).toBeCloseToArray(
+                [4.648, 1.342, 1.247, 4.821], 'lasso points y coords');
+
             doubleClick(250, 200).then(function() {
                 expect(doubleClickData).toBe(null, 'with the correct deselect data');
                 done();
@@ -460,4 +467,134 @@ describe('select box and lasso', function() {
         .catch(fail)
         .then(done);
     });
+
+    it('@noCI should work on scattermapbox traces', function(done) {
+        var fig = Lib.extendDeep({}, require('@mocks/mapbox_bubbles-text'));
+        var gd = createGraphDiv();
+        var eventData;
+
+        fig.layout.dragmode = 'select';
+        fig.config = {
+            mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN
+        };
+
+        function assertPoints(expected) {
+            var pts = eventData.points || [];
+
+            expect(pts.length).toBe(expected.length, 'selected points length');
+
+            pts.forEach(function(p, i) {
+                var e = expected[i];
+                expect(p.lon).toBe(e.lon, 'selected pt lon val');
+                expect(p.lat).toBe(e.lat, 'selected pt lat val');
+            });
+        }
+
+        function assertRanges(expected) {
+            var ranges = (eventData.range || {}).mapbox || [];
+            expect(ranges).toBeCloseTo2DArray(expected, 1, 'select box range (in lon/lat)');
+        }
+
+        function assertLassoPoints(expected) {
+            var lassoPoints = (eventData.lassoPoints || {}).mapbox || [];
+            expect(lassoPoints).toBeCloseTo2DArray(expected, 1, 'lasso points (in lon/lat)');
+        }
+
+        Plotly.plot(gd, fig).then(function() {
+            var selectingCnt = 0;
+            var selectedCnt = 0;
+            var deselectCnt = 0;
+
+            gd.once('plotly_selecting', function() {
+                assertSelectionNodes(1, 2);
+                selectingCnt++;
+            });
+
+            gd.once('plotly_selected', function(d) {
+                assertSelectionNodes(0, 2);
+                selectedCnt++;
+                eventData = d;
+            });
+
+            gd.once('plotly_deselect', function() {
+                deselectCnt++;
+                assertSelectionNodes(0, 0);
+            });
+
+            drag([[370, 120], [500, 200]]);
+            assertPoints([{lon: 30, lat: 30}]);
+            assertRanges([[21.99, 34.55], [38.14, 25.98]]);
+
+            return doubleClick(250, 200).then(function() {
+                expect(selectingCnt).toBe(1, 'plotly_selecting call count');
+                expect(selectedCnt).toBe(1, 'plotly_selected call count');
+                expect(deselectCnt).toBe(1, 'plotly_deselect call count');
+            });
+        })
+        .then(function() {
+            return Plotly.relayout(gd, 'dragmode', 'lasso');
+        })
+        .then(function() {
+            var selectingCnt = 0;
+            var selectedCnt = 0;
+            var deselectCnt = 0;
+
+            gd.once('plotly_selecting', function() {
+                assertSelectionNodes(1, 2);
+                selectingCnt++;
+            });
+
+            gd.once('plotly_selected', function(d) {
+                assertSelectionNodes(0, 2);
+                selectedCnt++;
+                eventData = d;
+            });
+
+            gd.once('plotly_deselect', function() {
+                deselectCnt++;
+                assertSelectionNodes(0, 0);
+            });
+
+            drag([[300, 200], [300, 300], [400, 300], [400, 200], [300, 200]]);
+            assertPoints([{lon: 20, lat: 20}]);
+            assertLassoPoints([
+                [13.28, 25.97], [13.28, 14.33], [25.71, 14.33], [25.71, 25.97], [13.28, 25.97]
+            ]);
+
+            return doubleClick(250, 200).then(function() {
+                expect(selectingCnt).toBe(1, 'plotly_selecting call count');
+                expect(selectedCnt).toBe(1, 'plotly_selected call count');
+                expect(deselectCnt).toBe(1, 'plotly_deselect call count');
+            });
+        })
+        .then(function() {
+            // make selection handlers don't get called in 'pan' dragmode
+            return Plotly.relayout(gd, 'dragmode', 'pan');
+        })
+        .then(function() {
+            var selectingCnt = 0;
+            var selectedCnt = 0;
+            var deselectCnt = 0;
+
+            gd.once('plotly_selecting', function() {
+                selectingCnt++;
+            });
+
+            gd.once('plotly_selected', function() {
+                selectedCnt++;
+            });
+
+            gd.once('plotly_deselect', function() {
+                deselectCnt++;
+            });
+
+            return doubleClick(250, 200).then(function() {
+                expect(selectingCnt).toBe(0, 'plotly_selecting call count');
+                expect(selectedCnt).toBe(0, 'plotly_selected call count');
+                expect(deselectCnt).toBe(0, 'plotly_deselect call count');
+            });
+        })
+        .catch(fail)
+        .then(done);
+    }, LONG_TIMEOUT_INTERVAL);
 });