diff --git a/devtools/test_dashboard/devtools.js b/devtools/test_dashboard/devtools.js
index 0922d66dd49..b1a0242dd20 100644
--- a/devtools/test_dashboard/devtools.js
+++ b/devtools/test_dashboard/devtools.js
@@ -202,7 +202,7 @@ function searchMocks(e) {
 
     results.forEach(function(r) {
         var result = document.createElement('span');
-        result.className = 'search-result';
+        result.className = getResultClass(r.name);
         result.innerText = r.name;
 
         result.addEventListener('click', function() {
@@ -212,6 +212,10 @@ function searchMocks(e) {
             // Clear plots and plot selected.
             Tabs.purge();
             Tabs.plotMock(mockName);
+
+            mocksList.querySelectorAll('span').forEach(function(el) {
+                el.className = getResultClass(el.innerText);
+            });
         });
 
         mocksList.appendChild(result);
@@ -222,8 +226,16 @@ function searchMocks(e) {
     });
 }
 
+function getNameFromHash() {
+    return window.location.hash.replace(/^#/, '');
+}
+
+function getResultClass(name) {
+    return 'search-result' + (getNameFromHash() === name ? ' search-result__selected' : '');
+}
+
 function plotFromHash() {
-    var initialMock = window.location.hash.replace(/^#/, '');
+    var initialMock = getNameFromHash();
 
     if(initialMock.length > 0) {
         Tabs.plotMock(initialMock);
diff --git a/devtools/test_dashboard/style.css b/devtools/test_dashboard/style.css
index 524c88e7df1..bea18056312 100644
--- a/devtools/test_dashboard/style.css
+++ b/devtools/test_dashboard/style.css
@@ -56,6 +56,9 @@ header span{
   color: #fff;
   background-color: #4983EC;
 }
+.search-result__selected{
+  background-color: #DDDDEE;
+}
 #plots{
   overflow: scroll;
 }
diff --git a/src/components/annotations/attributes.js b/src/components/annotations/attributes.js
index a5298f29b42..8559c240f68 100644
--- a/src/components/annotations/attributes.js
+++ b/src/components/annotations/attributes.js
@@ -20,7 +20,7 @@ module.exports = {
         valType: 'boolean',
         role: 'info',
         dflt: true,
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Determines whether or not this annotation is visible.'
         ].join(' ')
@@ -29,7 +29,7 @@ module.exports = {
     text: {
         valType: 'string',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the text associated with this annotation.',
             'Plotly uses a subset of HTML tags to do things like',
@@ -42,14 +42,14 @@ module.exports = {
         valType: 'angle',
         dflt: 0,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the angle at which the `text` is drawn',
             'with respect to the horizontal.'
         ].join(' ')
     },
     font: fontAttrs({
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         colorEditType: 'arraydraw',
         description: 'Sets the annotation text font.'
     }),
@@ -58,7 +58,7 @@ module.exports = {
         min: 1,
         dflt: null,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets an explicit width for the text box. null (default) lets the',
             'text set the box width. Wider text will be clipped.',
@@ -70,7 +70,7 @@ module.exports = {
         min: 1,
         dflt: null,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets an explicit height for the text box. null (default) lets the',
             'text set the box height. Taller text will be clipped.'
@@ -131,7 +131,7 @@ module.exports = {
         min: 0,
         dflt: 1,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the padding (in px) between the `text`',
             'and the enclosing border.'
@@ -142,7 +142,7 @@ module.exports = {
         min: 0,
         dflt: 1,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the width (in px) of the border enclosing',
             'the annotation `text`.'
@@ -153,7 +153,7 @@ module.exports = {
         valType: 'boolean',
         dflt: true,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Determines whether or not the annotation is drawn with an arrow.',
             'If *true*, `text` is placed near the arrow\'s tail.',
@@ -198,7 +198,7 @@ module.exports = {
         min: 0.3,
         dflt: 1,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the size of the end annotation arrow head, relative to `arrowwidth`.',
             'A value of 1 (default) gives a head about 3x as wide as the line.'
@@ -209,7 +209,7 @@ module.exports = {
         min: 0.3,
         dflt: 1,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the size of the start annotation arrow head, relative to `arrowwidth`.',
             'A value of 1 (default) gives a head about 3x as wide as the line.'
@@ -219,7 +219,7 @@ module.exports = {
         valType: 'number',
         min: 0.1,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: 'Sets the width (in px) of annotation arrow line.'
     },
     standoff: {
@@ -227,7 +227,7 @@ module.exports = {
         min: 0,
         dflt: 0,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets a distance, in pixels, to move the end arrowhead away from the',
             'position it is pointing at, for example to point at the edge of',
@@ -241,7 +241,7 @@ module.exports = {
         min: 0,
         dflt: 0,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets a distance, in pixels, to move the start arrowhead away from the',
             'position it is pointing at, for example to point at the edge of',
@@ -253,7 +253,7 @@ module.exports = {
     ax: {
         valType: 'any',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the x component of the arrow tail about the arrow head.',
             'If `axref` is `pixel`, a positive (negative) ',
@@ -266,7 +266,7 @@ module.exports = {
     ay: {
         valType: 'any',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the y component of the arrow tail about the arrow head.',
             'If `ayref` is `pixel`, a positive (negative) ',
@@ -333,7 +333,7 @@ module.exports = {
     x: {
         valType: 'any',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the annotation\'s x position.',
             'If the axis `type` is *log*, then you must take the',
@@ -351,7 +351,7 @@ module.exports = {
         values: ['auto', 'left', 'center', 'right'],
         dflt: 'auto',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the text box\'s horizontal position anchor',
             'This anchor binds the `x` position to the *left*, *center*',
@@ -370,7 +370,7 @@ module.exports = {
         valType: 'number',
         dflt: 0,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Shifts the position of the whole annotation and arrow to the',
             'right (positive) or left (negative) by this many pixels.'
@@ -396,7 +396,7 @@ module.exports = {
     y: {
         valType: 'any',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the annotation\'s y position.',
             'If the axis `type` is *log*, then you must take the',
@@ -414,7 +414,7 @@ module.exports = {
         values: ['auto', 'top', 'middle', 'bottom'],
         dflt: 'auto',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the text box\'s vertical position anchor',
             'This anchor binds the `y` position to the *top*, *middle*',
@@ -433,7 +433,7 @@ module.exports = {
         valType: 'number',
         dflt: 0,
         role: 'style',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Shifts the position of the whole annotation and arrow up',
             '(positive) or down (negative) by this many pixels.'
diff --git a/src/components/annotations3d/convert.js b/src/components/annotations3d/convert.js
index a4ba3f1d9a7..89d7c6ceb39 100644
--- a/src/components/annotations3d/convert.js
+++ b/src/components/annotations3d/convert.js
@@ -50,7 +50,7 @@ function mockAnnAxes(ann, scene) {
     Axes.setConvert(ann._xa);
     ann._xa._offset = size.l + domain.x[0] * size.w;
     ann._xa.l2p = function() {
-        return 0.5 * (1 + ann.pdata[0] / ann.pdata[3]) * size.w * (domain.x[1] - domain.x[0]);
+        return 0.5 * (1 + ann._pdata[0] / ann._pdata[3]) * size.w * (domain.x[1] - domain.x[0]);
     };
 
     ann._ya = {};
@@ -58,6 +58,6 @@ function mockAnnAxes(ann, scene) {
     Axes.setConvert(ann._ya);
     ann._ya._offset = size.t + (1 - domain.y[1]) * size.h;
     ann._ya.l2p = function() {
-        return 0.5 * (1 - ann.pdata[1] / ann.pdata[3]) * size.h * (domain.y[1] - domain.y[0]);
+        return 0.5 * (1 - ann._pdata[1] / ann._pdata[3]) * size.h * (domain.y[1] - domain.y[0]);
     };
 }
diff --git a/src/components/annotations3d/draw.js b/src/components/annotations3d/draw.js
index 56bc1af21b4..e1600bb7c24 100644
--- a/src/components/annotations3d/draw.js
+++ b/src/components/annotations3d/draw.js
@@ -38,7 +38,7 @@ module.exports = function draw(scene) {
                 .select('.annotation-' + scene.id + '[data-index="' + i + '"]')
                 .remove();
         } else {
-            ann.pdata = project(scene.glplot.cameraParams, [
+            ann._pdata = project(scene.glplot.cameraParams, [
                 fullSceneLayout.xaxis.r2l(ann.x) * dataScale[0],
                 fullSceneLayout.yaxis.r2l(ann.y) * dataScale[1],
                 fullSceneLayout.zaxis.r2l(ann.z) * dataScale[2]
diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js
index cff1522de71..8dbf3cf8e9c 100644
--- a/src/components/legend/defaults.js
+++ b/src/components/legend/defaults.js
@@ -18,15 +18,13 @@ var helpers = require('./helpers');
 
 
 module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
-    var containerIn = layoutIn.legend || {},
-        containerOut = layoutOut.legend = {};
+    var containerIn = layoutIn.legend || {};
+    var containerOut = {};
 
-    var visibleTraces = 0,
-        defaultOrder = 'normal',
-        defaultX,
-        defaultY,
-        defaultXAnchor,
-        defaultYAnchor;
+    var visibleTraces = 0;
+    var defaultOrder = 'normal';
+
+    var defaultX, defaultY, defaultXAnchor, defaultYAnchor;
 
     for(var i = 0; i < fullData.length; i++) {
         var trace = fullData[i];
@@ -58,6 +56,8 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
 
     if(showLegend === false) return;
 
+    layoutOut.legend = containerOut;
+
     coerce('bgcolor', layoutOut.paper_bgcolor);
     coerce('bordercolor');
     coerce('borderwidth');
diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js
index 6c6d6a54d86..3ba1a94194b 100644
--- a/src/components/legend/draw.js
+++ b/src/components/legend/draw.js
@@ -22,7 +22,10 @@ var handleClick = require('./handle_click');
 
 var constants = require('./constants');
 var interactConstants = require('../../constants/interactions');
-var LINE_SPACING = require('../../constants/alignment').LINE_SPACING;
+var alignmentConstants = require('../../constants/alignment');
+var LINE_SPACING = alignmentConstants.LINE_SPACING;
+var FROM_TL = alignmentConstants.FROM_TL;
+var FROM_BR = alignmentConstants.FROM_BR;
 
 var getLegendData = require('./get_legend_data');
 var style = require('./style');
@@ -141,7 +144,7 @@ module.exports = function draw(gd) {
 
     computeLegendDimensions(gd, groups, traces);
 
-    if(opts.height > lyMax) {
+    if(opts._height > lyMax) {
         // If the legend doesn't fit in the plot area,
         // do not expand the vertical margins.
         expandHorizontalMargin(gd);
@@ -157,21 +160,21 @@ module.exports = function draw(gd) {
         ly = gs.t + gs.h * (1 - opts.y);
 
     if(anchorUtils.isRightAnchor(opts)) {
-        lx -= opts.width;
+        lx -= opts._width;
     }
     else if(anchorUtils.isCenterAnchor(opts)) {
-        lx -= opts.width / 2;
+        lx -= opts._width / 2;
     }
 
     if(anchorUtils.isBottomAnchor(opts)) {
-        ly -= opts.height;
+        ly -= opts._height;
     }
     else if(anchorUtils.isMiddleAnchor(opts)) {
-        ly -= opts.height / 2;
+        ly -= opts._height / 2;
     }
 
     // Make sure the legend left and right sides are visible
-    var legendWidth = opts.width,
+    var legendWidth = opts._width,
         legendWidthMax = gs.w;
 
     if(legendWidth > legendWidthMax) {
@@ -181,13 +184,13 @@ module.exports = function draw(gd) {
     else {
         if(lx + legendWidth > lxMax) lx = lxMax - legendWidth;
         if(lx < lxMin) lx = lxMin;
-        legendWidth = Math.min(lxMax - lx, opts.width);
+        legendWidth = Math.min(lxMax - lx, opts._width);
     }
 
     // Make sure the legend top and bottom are visible
     // (legends with a scroll bar are not allowed to stretch beyond the extended
     // margins)
-    var legendHeight = opts.height,
+    var legendHeight = opts._height,
         legendHeightMax = gs.h;
 
     if(legendHeight > legendHeightMax) {
@@ -197,7 +200,7 @@ module.exports = function draw(gd) {
     else {
         if(ly + legendHeight > lyMax) ly = lyMax - legendHeight;
         if(ly < lyMin) ly = lyMin;
-        legendHeight = Math.min(lyMax - ly, opts.height);
+        legendHeight = Math.min(lyMax - ly, opts._height);
     }
 
     // Set size and position of all the elements that make up a legend:
@@ -207,11 +210,11 @@ module.exports = function draw(gd) {
     var scrollBarYMax = legendHeight -
             constants.scrollBarHeight -
             2 * constants.scrollBarMargin,
-        scrollBoxYMax = opts.height - legendHeight,
+        scrollBoxYMax = opts._height - legendHeight,
         scrollBarY,
         scrollBoxY;
 
-    if(opts.height <= legendHeight || gd._context.staticPlot) {
+    if(opts._height <= legendHeight || gd._context.staticPlot) {
         // if scrollbar should not be shown.
         bg.attr({
             width: legendWidth - opts.borderwidth,
@@ -533,8 +536,8 @@ function computeLegendDimensions(gd, groups, traces) {
 
     var extraWidth = 0;
 
-    opts.width = 0;
-    opts.height = 0;
+    opts._width = 0;
+    opts._height = 0;
 
     if(helpers.isVertical(opts)) {
         if(isGrouped) {
@@ -550,23 +553,23 @@ function computeLegendDimensions(gd, groups, traces) {
 
             Drawing.setTranslate(this,
                 borderwidth,
-                (5 + borderwidth + opts.height + textHeight / 2));
+                (5 + borderwidth + opts._height + textHeight / 2));
 
-            opts.height += textHeight;
-            opts.width = Math.max(opts.width, textWidth);
+            opts._height += textHeight;
+            opts._width = Math.max(opts._width, textWidth);
         });
 
-        opts.width += 45 + borderwidth * 2;
-        opts.height += 10 + borderwidth * 2;
+        opts._width += 45 + borderwidth * 2;
+        opts._height += 10 + borderwidth * 2;
 
         if(isGrouped) {
-            opts.height += (opts._lgroupsLength - 1) * opts.tracegroupgap;
+            opts._height += (opts._lgroupsLength - 1) * opts.tracegroupgap;
         }
 
         extraWidth = 40;
     }
     else if(isGrouped) {
-        var groupXOffsets = [opts.width],
+        var groupXOffsets = [opts._width],
             groupData = groups.data();
 
         for(var i = 0, n = groupData.length; i < n; i++) {
@@ -576,9 +579,9 @@ function computeLegendDimensions(gd, groups, traces) {
 
             var groupWidth = 40 + Math.max.apply(null, textWidths);
 
-            opts.width += opts.tracegroupgap + groupWidth;
+            opts._width += opts.tracegroupgap + groupWidth;
 
-            groupXOffsets.push(opts.width);
+            groupXOffsets.push(opts._width);
         }
 
         groups.each(function(d, i) {
@@ -601,11 +604,11 @@ function computeLegendDimensions(gd, groups, traces) {
                 groupHeight += textHeight;
             });
 
-            opts.height = Math.max(opts.height, groupHeight);
+            opts._height = Math.max(opts._height, groupHeight);
         });
 
-        opts.height += 10 + borderwidth * 2;
-        opts.width += borderwidth * 2;
+        opts._height += 10 + borderwidth * 2;
+        opts._width += borderwidth * 2;
     }
     else {
         var rowHeight = 0,
@@ -631,7 +634,7 @@ function computeLegendDimensions(gd, groups, traces) {
             if((borderwidth + offsetX + traceGap + traceWidth) > (fullLayout.width - (fullLayout.margin.r + fullLayout.margin.l))) {
                 offsetX = 0;
                 rowHeight = rowHeight + maxTraceHeight;
-                opts.height = opts.height + maxTraceHeight;
+                opts._height = opts._height + maxTraceHeight;
                 // reset for next row
                 maxTraceHeight = 0;
             }
@@ -640,22 +643,22 @@ function computeLegendDimensions(gd, groups, traces) {
                 (borderwidth + offsetX),
                 (5 + borderwidth + legendItem.height / 2) + rowHeight);
 
-            opts.width += traceGap + traceWidth;
-            opts.height = Math.max(opts.height, legendItem.height);
+            opts._width += traceGap + traceWidth;
+            opts._height = Math.max(opts._height, legendItem.height);
 
             // keep track of tallest trace in group
             offsetX += traceGap + traceWidth;
             maxTraceHeight = Math.max(legendItem.height, maxTraceHeight);
         });
 
-        opts.width += borderwidth * 2;
-        opts.height += 10 + borderwidth * 2;
+        opts._width += borderwidth * 2;
+        opts._height += 10 + borderwidth * 2;
 
     }
 
     // make sure we're only getting full pixels
-    opts.width = Math.ceil(opts.width);
-    opts.height = Math.ceil(opts.height);
+    opts._width = Math.ceil(opts._width);
+    opts._height = Math.ceil(opts._height);
 
     traces.each(function(d) {
         var legendItem = d[0],
@@ -664,7 +667,7 @@ function computeLegendDimensions(gd, groups, traces) {
         bg.call(Drawing.setRect,
             0,
             -legendItem.height / 2,
-            (gd._context.edits.legendText ? 0 : opts.width) + extraWidth,
+            (gd._context.edits.legendText ? 0 : opts._width) + extraWidth,
             legendItem.height
         );
     });
@@ -694,10 +697,10 @@ function expandMargin(gd) {
     Plots.autoMargin(gd, 'legend', {
         x: opts.x,
         y: opts.y,
-        l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0),
-        r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0),
-        b: opts.height * ({top: 1, middle: 0.5}[yanchor] || 0),
-        t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0)
+        l: opts._width * (FROM_TL[xanchor]),
+        r: opts._width * (FROM_BR[xanchor]),
+        b: opts._height * (FROM_BR[yanchor]),
+        t: opts._height * (FROM_TL[yanchor])
     });
 }
 
@@ -717,8 +720,8 @@ function expandHorizontalMargin(gd) {
     Plots.autoMargin(gd, 'legend', {
         x: opts.x,
         y: 0.5,
-        l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0),
-        r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0),
+        l: opts._width * (FROM_TL[xanchor]),
+        r: opts._width * (FROM_BR[xanchor]),
         b: 0,
         t: 0
     });
diff --git a/src/components/rangeselector/draw.js b/src/components/rangeselector/draw.js
index a2631065fd2..c5730804d24 100644
--- a/src/components/rangeselector/draw.js
+++ b/src/components/rangeselector/draw.js
@@ -19,7 +19,10 @@ var svgTextUtils = require('../../lib/svg_text_utils');
 var axisIds = require('../../plots/cartesian/axis_ids');
 var anchorUtils = require('../legend/anchor_utils');
 
-var LINE_SPACING = require('../../constants/alignment').LINE_SPACING;
+var alignmentConstants = require('../../constants/alignment');
+var LINE_SPACING = alignmentConstants.LINE_SPACING;
+var FROM_TL = alignmentConstants.FROM_TL;
+var FROM_BR = alignmentConstants.FROM_BR;
 
 var constants = require('./constants');
 var getUpdateObject = require('./get_update_object');
@@ -58,7 +61,7 @@ module.exports = function draw(gd) {
             var button = d3.select(this);
             var update = getUpdateObject(axisLayout, d);
 
-            d.isActive = isActive(axisLayout, d, update);
+            d._isActive = isActive(axisLayout, d, update);
 
             button.call(drawButtonRect, selectorLayout, d);
             button.call(drawButtonText, selectorLayout, d, gd);
@@ -70,22 +73,17 @@ module.exports = function draw(gd) {
             });
 
             button.on('mouseover', function() {
-                d.isHovered = true;
+                d._isHovered = true;
                 button.call(drawButtonRect, selectorLayout, d);
             });
 
             button.on('mouseout', function() {
-                d.isHovered = false;
+                d._isHovered = false;
                 button.call(drawButtonRect, selectorLayout, d);
             });
         });
 
-        // N.B. this mutates selectorLayout
-        reposition(gd, buttons, selectorLayout, axisLayout._name);
-
-        selector.attr('transform', 'translate(' +
-            selectorLayout.lx + ',' + selectorLayout.ly +
-        ')');
+        reposition(gd, buttons, selectorLayout, axisLayout._name, selector);
     });
 
 };
@@ -143,7 +141,7 @@ function drawButtonRect(button, selectorLayout, d) {
 }
 
 function getFillColor(selectorLayout, d) {
-    return (d.isActive || d.isHovered) ?
+    return (d._isActive || d._isHovered) ?
         selectorLayout.activecolor :
         selectorLayout.bgcolor;
 }
@@ -175,9 +173,9 @@ function getLabel(opts) {
     return opts.count + opts.step.charAt(0);
 }
 
-function reposition(gd, buttons, opts, axName) {
-    opts.width = 0;
-    opts.height = 0;
+function reposition(gd, buttons, opts, axName, selector) {
+    var width = 0;
+    var height = 0;
 
     var borderWidth = opts.borderwidth;
 
@@ -188,7 +186,7 @@ function reposition(gd, buttons, opts, axName) {
         var tHeight = opts.font.size * LINE_SPACING;
         var hEff = Math.max(tHeight * svgTextUtils.lineCount(text), 16) + 3;
 
-        opts.height = Math.max(opts.height, hEff);
+        height = Math.max(height, hEff);
     });
 
     buttons.each(function() {
@@ -207,59 +205,59 @@ function reposition(gd, buttons, opts, axName) {
         // TODO add buttongap attribute
 
         button.attr('transform', 'translate(' +
-            (borderWidth + opts.width) + ',' + borderWidth +
+            (borderWidth + width) + ',' + borderWidth +
         ')');
 
         rect.attr({
             x: 0,
             y: 0,
             width: wEff,
-            height: opts.height
+            height: height
         });
 
         svgTextUtils.positionText(text, wEff / 2,
-            opts.height / 2 - ((tLines - 1) * tHeight / 2) + 3);
+            height / 2 - ((tLines - 1) * tHeight / 2) + 3);
 
-        opts.width += wEff + 5;
+        width += wEff + 5;
     });
 
-    buttons.selectAll('rect').attr('height', opts.height);
-
     var graphSize = gd._fullLayout._size;
-    opts.lx = graphSize.l + graphSize.w * opts.x;
-    opts.ly = graphSize.t + graphSize.h * (1 - opts.y);
+    var lx = graphSize.l + graphSize.w * opts.x;
+    var ly = graphSize.t + graphSize.h * (1 - opts.y);
 
     var xanchor = 'left';
     if(anchorUtils.isRightAnchor(opts)) {
-        opts.lx -= opts.width;
+        lx -= width;
         xanchor = 'right';
     }
     if(anchorUtils.isCenterAnchor(opts)) {
-        opts.lx -= opts.width / 2;
+        lx -= width / 2;
         xanchor = 'center';
     }
 
     var yanchor = 'top';
     if(anchorUtils.isBottomAnchor(opts)) {
-        opts.ly -= opts.height;
+        ly -= height;
         yanchor = 'bottom';
     }
     if(anchorUtils.isMiddleAnchor(opts)) {
-        opts.ly -= opts.height / 2;
+        ly -= height / 2;
         yanchor = 'middle';
     }
 
-    opts.width = Math.ceil(opts.width);
-    opts.height = Math.ceil(opts.height);
-    opts.lx = Math.round(opts.lx);
-    opts.ly = Math.round(opts.ly);
+    width = Math.ceil(width);
+    height = Math.ceil(height);
+    lx = Math.round(lx);
+    ly = Math.round(ly);
 
     Plots.autoMargin(gd, axName + '-range-selector', {
         x: opts.x,
         y: opts.y,
-        l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0),
-        r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0),
-        b: opts.height * ({top: 1, middle: 0.5}[yanchor] || 0),
-        t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0)
+        l: width * FROM_TL[xanchor],
+        r: width * FROM_BR[xanchor],
+        b: height * FROM_BR[yanchor],
+        t: height * FROM_TL[yanchor]
     });
+
+    selector.attr('transform', 'translate(' + lx + ',' + ly + ')');
 }
diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js
index 9b058be50da..37268dff026 100644
--- a/src/components/rangeslider/attributes.js
+++ b/src/components/rangeslider/attributes.js
@@ -38,6 +38,7 @@ module.exports = {
         dflt: true,
         role: 'style',
         editType: 'calc',
+        impliedEdits: {'range[0]': undefined, 'range[1]': undefined},
         description: [
             'Determines whether or not the range slider range is',
             'computed in relation to the input data.',
@@ -48,10 +49,11 @@ module.exports = {
         valType: 'info_array',
         role: 'info',
         items: [
-            {valType: 'any', editType: 'calc'},
-            {valType: 'any', editType: 'calc'}
+            {valType: 'any', editType: 'calc', impliedEdits: {'^autorange': false}},
+            {valType: 'any', editType: 'calc', impliedEdits: {'^autorange': false}}
         ],
         editType: 'calc',
+        impliedEdits: {'autorange': false},
         description: [
             'Sets the range of the range slider.',
             'If not set, defaults to the full xaxis range.',
diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js
index d4c7ce6acc2..106934833fa 100644
--- a/src/components/rangeslider/defaults.js
+++ b/src/components/rangeslider/defaults.js
@@ -38,18 +38,6 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
     coerce('autorange', !axOut.isValidRange(containerIn.range));
     coerce('range');
 
-    // Expand slider range to the axis range
-    // TODO: what if the ranges are reversed?
-    if(containerOut.range) {
-        var outRange = containerOut.range,
-            axRange = axOut.range;
-
-        outRange[0] = axOut.l2r(Math.min(axOut.r2l(outRange[0]), axOut.r2l(axRange[0])));
-        outRange[1] = axOut.l2r(Math.max(axOut.r2l(outRange[1]), axOut.r2l(axRange[1])));
-    }
-
-    axOut.cleanRange('rangeslider.range');
-
     // to map back range slider (auto) range
     containerOut._input = containerIn;
 };
diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js
index 11c5ec6431c..2690438d9b1 100644
--- a/src/components/rangeslider/draw.js
+++ b/src/components/rangeslider/draw.js
@@ -80,6 +80,21 @@ module.exports = function(gd) {
             opts = axisOpts[constants.name],
             oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)];
 
+        // update range
+        // Expand slider range to the axis range
+        // TODO: what if the ranges are reversed?
+        if(opts.range) {
+            var outRange = opts.range;
+            var axRange = axisOpts.range;
+
+            outRange[0] = axisOpts.l2r(Math.min(axisOpts.r2l(outRange[0]), axisOpts.r2l(axRange[0])));
+            outRange[1] = axisOpts.l2r(Math.max(axisOpts.r2l(outRange[1]), axisOpts.r2l(axRange[1])));
+            opts._input.range = outRange.slice();
+        }
+
+        axisOpts.cleanRange('rangeslider.range');
+
+
         // update range slider dimensions
 
         var margin = fullLayout.margin,
diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js
index 0a65317d58f..622cf147015 100644
--- a/src/components/shapes/attributes.js
+++ b/src/components/shapes/attributes.js
@@ -20,7 +20,7 @@ module.exports = {
         valType: 'boolean',
         role: 'info',
         dflt: true,
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Determines whether or not this shape is visible.'
         ].join(' ')
@@ -30,7 +30,7 @@ module.exports = {
         valType: 'enumerated',
         values: ['circle', 'rect', 'path', 'line'],
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Specifies the shape type to be drawn.',
 
@@ -74,7 +74,7 @@ module.exports = {
     x0: {
         valType: 'any',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the shape\'s starting x position.',
             'See `type` for more info.'
@@ -83,7 +83,7 @@ module.exports = {
     x1: {
         valType: 'any',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the shape\'s end x position.',
             'See `type` for more info.'
@@ -103,7 +103,7 @@ module.exports = {
     y0: {
         valType: 'any',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the shape\'s starting y position.',
             'See `type` for more info.'
@@ -112,7 +112,7 @@ module.exports = {
     y1: {
         valType: 'any',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'Sets the shape\'s end y position.',
             'See `type` for more info.'
@@ -122,7 +122,7 @@ module.exports = {
     path: {
         valType: 'string',
         role: 'info',
-        editType: 'calcIfAutorange',
+        editType: 'calcIfAutorange+arraydraw',
         description: [
             'For `type` *path* - a valid SVG path but with the pixel values',
             'replaced by data values. There are a few restrictions / quirks',
@@ -158,10 +158,10 @@ module.exports = {
     },
     line: {
         color: extendFlat({}, scatterLineAttrs.color, {editType: 'arraydraw'}),
-        width: extendFlat({}, scatterLineAttrs.width, {editType: 'calcIfAutorange'}),
+        width: extendFlat({}, scatterLineAttrs.width, {editType: 'calcIfAutorange+arraydraw'}),
         dash: extendFlat({}, dash, {editType: 'arraydraw'}),
         role: 'info',
-        editType: 'calcIfAutorange'
+        editType: 'calcIfAutorange+arraydraw'
     },
     fillcolor: {
         valType: 'color',
diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js
index bd4941f7e96..1e7dff5caa4 100644
--- a/src/components/sliders/draw.js
+++ b/src/components/sliders/draw.js
@@ -18,7 +18,10 @@ var svgTextUtils = require('../../lib/svg_text_utils');
 var anchorUtils = require('../legend/anchor_utils');
 
 var constants = require('./constants');
-var LINE_SPACING = require('../../constants/alignment').LINE_SPACING;
+var alignmentConstants = require('../../constants/alignment');
+var LINE_SPACING = alignmentConstants.LINE_SPACING;
+var FROM_TL = alignmentConstants.FROM_TL;
+var FROM_BR = alignmentConstants.FROM_BR;
 
 
 module.exports = function draw(gd) {
@@ -98,7 +101,7 @@ function makeSliderData(fullLayout, gd) {
     for(var i = 0; i < contOpts.length; i++) {
         var item = contOpts[i];
         if(!item.visible || !item.steps.length) continue;
-        item.gd = gd;
+        item._gd = gd;
         sliderData.push(item);
     }
 
@@ -136,7 +139,9 @@ function findDimensions(gd, sliderOpts) {
 
     sliderLabels.remove();
 
-    sliderOpts.inputAreaWidth = Math.max(
+    var dims = sliderOpts._dims = {};
+
+    dims.inputAreaWidth = Math.max(
         constants.railWidth,
         constants.gripHeight
     );
@@ -144,37 +149,33 @@ function findDimensions(gd, sliderOpts) {
     // calculate some overall dimensions - some of these are needed for
     // calculating the currentValue dimensions
     var graphSize = gd._fullLayout._size;
-    sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x;
-    sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y);
+    dims.lx = graphSize.l + graphSize.w * sliderOpts.x;
+    dims.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y);
 
     if(sliderOpts.lenmode === 'fraction') {
         // fraction:
-        sliderOpts.outerLength = Math.round(graphSize.w * sliderOpts.len);
+        dims.outerLength = Math.round(graphSize.w * sliderOpts.len);
     } else {
         // pixels:
-        sliderOpts.outerLength = sliderOpts.len;
+        dims.outerLength = sliderOpts.len;
     }
 
-    // Set the length-wise padding so that the grip ends up *on* the end of
-    // the bar when at either extreme
-    sliderOpts.lenPad = Math.round(constants.gripWidth * 0.5);
-
     // The length of the rail, *excluding* padding on either end:
-    sliderOpts.inputAreaStart = 0;
-    sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.pad.l - sliderOpts.pad.r);
+    dims.inputAreaStart = 0;
+    dims.inputAreaLength = Math.round(dims.outerLength - sliderOpts.pad.l - sliderOpts.pad.r);
 
-    var textableInputLength = sliderOpts.inputAreaLength - 2 * constants.stepInset;
+    var textableInputLength = dims.inputAreaLength - 2 * constants.stepInset;
     var availableSpacePerLabel = textableInputLength / (sliderOpts.steps.length - 1);
     var computedSpacePerLabel = maxLabelWidth + constants.labelPadding;
-    sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel));
-    sliderOpts.labelHeight = labelHeight;
+    dims.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel));
+    dims.labelHeight = labelHeight;
 
     // loop over all possible values for currentValue to find the
     // area we need for it
-    sliderOpts.currentValueMaxWidth = 0;
-    sliderOpts.currentValueHeight = 0;
-    sliderOpts.currentValueTotalHeight = 0;
-    sliderOpts.currentValueMaxLines = 1;
+    dims.currentValueMaxWidth = 0;
+    dims.currentValueHeight = 0;
+    dims.currentValueTotalHeight = 0;
+    dims.currentValueMaxLines = 1;
 
     if(sliderOpts.currentvalue.visible) {
         // Get the dimensions of the current value label:
@@ -184,50 +185,50 @@ function findDimensions(gd, sliderOpts) {
             var curValPrefix = drawCurrentValue(dummyGroup, sliderOpts, stepOpts.label);
             var curValSize = (curValPrefix.node() && Drawing.bBox(curValPrefix.node())) || {width: 0, height: 0};
             var lines = svgTextUtils.lineCount(curValPrefix);
-            sliderOpts.currentValueMaxWidth = Math.max(sliderOpts.currentValueMaxWidth, Math.ceil(curValSize.width));
-            sliderOpts.currentValueHeight = Math.max(sliderOpts.currentValueHeight, Math.ceil(curValSize.height));
-            sliderOpts.currentValueMaxLines = Math.max(sliderOpts.currentValueMaxLines, lines);
+            dims.currentValueMaxWidth = Math.max(dims.currentValueMaxWidth, Math.ceil(curValSize.width));
+            dims.currentValueHeight = Math.max(dims.currentValueHeight, Math.ceil(curValSize.height));
+            dims.currentValueMaxLines = Math.max(dims.currentValueMaxLines, lines);
         });
 
-        sliderOpts.currentValueTotalHeight = sliderOpts.currentValueHeight + sliderOpts.currentvalue.offset;
+        dims.currentValueTotalHeight = dims.currentValueHeight + sliderOpts.currentvalue.offset;
 
         dummyGroup.remove();
     }
 
-    sliderOpts.height = sliderOpts.currentValueTotalHeight + constants.tickOffset + sliderOpts.ticklen + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b;
+    dims.height = dims.currentValueTotalHeight + constants.tickOffset + sliderOpts.ticklen + constants.labelOffset + dims.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b;
 
     var xanchor = 'left';
     if(anchorUtils.isRightAnchor(sliderOpts)) {
-        sliderOpts.lx -= sliderOpts.outerLength;
+        dims.lx -= dims.outerLength;
         xanchor = 'right';
     }
     if(anchorUtils.isCenterAnchor(sliderOpts)) {
-        sliderOpts.lx -= sliderOpts.outerLength / 2;
+        dims.lx -= dims.outerLength / 2;
         xanchor = 'center';
     }
 
     var yanchor = 'top';
     if(anchorUtils.isBottomAnchor(sliderOpts)) {
-        sliderOpts.ly -= sliderOpts.height;
+        dims.ly -= dims.height;
         yanchor = 'bottom';
     }
     if(anchorUtils.isMiddleAnchor(sliderOpts)) {
-        sliderOpts.ly -= sliderOpts.height / 2;
+        dims.ly -= dims.height / 2;
         yanchor = 'middle';
     }
 
-    sliderOpts.outerLength = Math.ceil(sliderOpts.outerLength);
-    sliderOpts.height = Math.ceil(sliderOpts.height);
-    sliderOpts.lx = Math.round(sliderOpts.lx);
-    sliderOpts.ly = Math.round(sliderOpts.ly);
+    dims.outerLength = Math.ceil(dims.outerLength);
+    dims.height = Math.ceil(dims.height);
+    dims.lx = Math.round(dims.lx);
+    dims.ly = Math.round(dims.ly);
 
     Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index, {
         x: sliderOpts.x,
         y: sliderOpts.y,
-        l: sliderOpts.outerLength * ({right: 1, center: 0.5}[xanchor] || 0),
-        r: sliderOpts.outerLength * ({left: 1, center: 0.5}[xanchor] || 0),
-        b: sliderOpts.height * ({top: 1, middle: 0.5}[yanchor] || 0),
-        t: sliderOpts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0)
+        l: dims.outerLength * FROM_TL[xanchor],
+        r: dims.outerLength * FROM_BR[xanchor],
+        b: dims.height * FROM_BR[yanchor],
+        t: dims.height * FROM_TL[yanchor]
     });
 }
 
@@ -250,8 +251,10 @@ function drawSlider(gd, sliderGroup, sliderOpts) {
         .call(drawTouchRect, gd, sliderOpts)
         .call(drawGrip, gd, sliderOpts);
 
+    var dims = sliderOpts._dims;
+
     // Position the rectangle:
-    Drawing.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.pad.l, sliderOpts.ly + sliderOpts.pad.t);
+    Drawing.setTranslate(sliderGroup, dims.lx + sliderOpts.pad.l, dims.ly + sliderOpts.pad.t);
 
     sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), false);
     sliderGroup.call(drawCurrentValue, sliderOpts);
@@ -264,17 +267,18 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) {
     var x0, textAnchor;
     var text = sliderGroup.selectAll('text')
         .data([0]);
+    var dims = sliderOpts._dims;
 
     switch(sliderOpts.currentvalue.xanchor) {
         case 'right':
             // This is anchored left and adjusted by the width of the longest label
             // so that the prefix doesn't move. The goal of this is to emphasize
             // what's actually changing and make the update less distracting.
-            x0 = sliderOpts.inputAreaLength - constants.currentValueInset - sliderOpts.currentValueMaxWidth;
+            x0 = dims.inputAreaLength - constants.currentValueInset - dims.currentValueMaxWidth;
             textAnchor = 'left';
             break;
         case 'center':
-            x0 = sliderOpts.inputAreaLength * 0.5;
+            x0 = dims.inputAreaLength * 0.5;
             textAnchor = 'middle';
             break;
         default:
@@ -305,11 +309,11 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) {
 
     text.call(Drawing.font, sliderOpts.currentvalue.font)
         .text(str)
-        .call(svgTextUtils.convertToTspans, sliderOpts.gd);
+        .call(svgTextUtils.convertToTspans, sliderOpts._gd);
 
     var lines = svgTextUtils.lineCount(text);
 
-    var y0 = (sliderOpts.currentValueMaxLines + 1 - lines) *
+    var y0 = (dims.currentValueMaxLines + 1 - lines) *
         sliderOpts.currentvalue.font.size * LINE_SPACING;
 
     svgTextUtils.positionText(text, x0, y0);
@@ -351,7 +355,7 @@ function drawLabel(item, data, sliderOpts) {
 
     text.call(Drawing.font, sliderOpts.font)
         .text(data.step.label)
-        .call(svgTextUtils.convertToTspans, sliderOpts.gd);
+        .call(svgTextUtils.convertToTspans, sliderOpts._gd);
 
     return text;
 }
@@ -359,12 +363,13 @@ function drawLabel(item, data, sliderOpts) {
 function drawLabelGroup(sliderGroup, sliderOpts) {
     var labels = sliderGroup.selectAll('g.' + constants.labelsClass)
         .data([0]);
+    var dims = sliderOpts._dims;
 
     labels.enter().append('g')
         .classed(constants.labelsClass, true);
 
     var labelItems = labels.selectAll('g.' + constants.labelGroupClass)
-        .data(sliderOpts.labelSteps);
+        .data(dims.labelSteps);
 
     labelItems.enter().append('g')
         .classed(constants.labelGroupClass, true);
@@ -384,7 +389,7 @@ function drawLabelGroup(sliderGroup, sliderOpts) {
                 // if the label spans multiple lines
                 sliderOpts.font.size * LINE_SPACING +
                 constants.labelOffset +
-                sliderOpts.currentValueTotalHeight
+                dims.currentValueTotalHeight
         );
     });
 
@@ -488,6 +493,7 @@ function attachGripEvents(item, gd, sliderGroup) {
 function drawTicks(sliderGroup, sliderOpts) {
     var tick = sliderGroup.selectAll('rect.' + constants.tickRectClass)
         .data(sliderOpts.steps);
+    var dims = sliderOpts._dims;
 
     tick.enter().append('rect')
         .classed(constants.tickRectClass, true);
@@ -500,7 +506,7 @@ function drawTicks(sliderGroup, sliderOpts) {
     });
 
     tick.each(function(d, i) {
-        var isMajor = i % sliderOpts.labelStride === 0;
+        var isMajor = i % dims.labelStride === 0;
         var item = d3.select(this);
 
         item
@@ -509,19 +515,20 @@ function drawTicks(sliderGroup, sliderOpts) {
 
         Drawing.setTranslate(item,
             normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * sliderOpts.tickwidth,
-            (isMajor ? constants.tickOffset : constants.minorTickOffset) + sliderOpts.currentValueTotalHeight
+            (isMajor ? constants.tickOffset : constants.minorTickOffset) + dims.currentValueTotalHeight
         );
     });
 
 }
 
 function computeLabelSteps(sliderOpts) {
-    sliderOpts.labelSteps = [];
+    var dims = sliderOpts._dims;
+    dims.labelSteps = [];
     var i0 = 0;
     var nsteps = sliderOpts.steps.length;
 
-    for(var i = i0; i < nsteps; i += sliderOpts.labelStride) {
-        sliderOpts.labelSteps.push({
+    for(var i = i0; i < nsteps; i += dims.labelStride) {
+        dims.labelSteps.push({
             fraction: i / (nsteps - 1),
             step: sliderOpts.steps[i]
         });
@@ -546,23 +553,26 @@ function setGripPosition(sliderGroup, sliderOpts, position, doTransition) {
 
     // Drawing.setTranslate doesn't work here becasue of the transition duck-typing.
     // It's also not necessary because there are no other transitions to preserve.
-    el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + (sliderOpts.currentValueTotalHeight) + ')');
+    el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + (sliderOpts._dims.currentValueTotalHeight) + ')');
 }
 
 // Convert a number from [0-1] to a pixel position relative to the slider group container:
 function normalizedValueToPosition(sliderOpts, normalizedPosition) {
-    return sliderOpts.inputAreaStart + constants.stepInset +
-        (sliderOpts.inputAreaLength - 2 * constants.stepInset) * Math.min(1, Math.max(0, normalizedPosition));
+    var dims = sliderOpts._dims;
+    return dims.inputAreaStart + constants.stepInset +
+        (dims.inputAreaLength - 2 * constants.stepInset) * Math.min(1, Math.max(0, normalizedPosition));
 }
 
 // Convert a position relative to the slider group to a nubmer in [0, 1]
 function positionToNormalizedValue(sliderOpts, position) {
-    return Math.min(1, Math.max(0, (position - constants.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * constants.stepInset - 2 * sliderOpts.inputAreaStart)));
+    var dims = sliderOpts._dims;
+    return Math.min(1, Math.max(0, (position - constants.stepInset - dims.inputAreaStart) / (dims.inputAreaLength - 2 * constants.stepInset - 2 * dims.inputAreaStart)));
 }
 
 function drawTouchRect(sliderGroup, gd, sliderOpts) {
     var rect = sliderGroup.selectAll('rect.' + constants.railTouchRectClass)
         .data([0]);
+    var dims = sliderOpts._dims;
 
     rect.enter().append('rect')
         .classed(constants.railTouchRectClass, true)
@@ -570,23 +580,24 @@ function drawTouchRect(sliderGroup, gd, sliderOpts) {
         .style('pointer-events', 'all');
 
     rect.attr({
-        width: sliderOpts.inputAreaLength,
-        height: Math.max(sliderOpts.inputAreaWidth, constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight)
+        width: dims.inputAreaLength,
+        height: Math.max(dims.inputAreaWidth, constants.tickOffset + sliderOpts.ticklen + dims.labelHeight)
     })
         .call(Color.fill, sliderOpts.bgcolor)
         .attr('opacity', 0);
 
-    Drawing.setTranslate(rect, 0, sliderOpts.currentValueTotalHeight);
+    Drawing.setTranslate(rect, 0, dims.currentValueTotalHeight);
 }
 
 function drawRail(sliderGroup, sliderOpts) {
     var rect = sliderGroup.selectAll('rect.' + constants.railRectClass)
         .data([0]);
+    var dims = sliderOpts._dims;
 
     rect.enter().append('rect')
         .classed(constants.railRectClass, true);
 
-    var computedLength = sliderOpts.inputAreaLength - constants.railInset * 2;
+    var computedLength = dims.inputAreaLength - constants.railInset * 2;
 
     rect.attr({
         width: computedLength,
@@ -601,7 +612,7 @@ function drawRail(sliderGroup, sliderOpts) {
 
     Drawing.setTranslate(rect,
         constants.railInset,
-        (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5 + sliderOpts.currentValueTotalHeight
+        (dims.inputAreaWidth - constants.railWidth) * 0.5 + dims.currentValueTotalHeight
     );
 }
 
diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js
index 22fa00ade5b..ea916cef2f6 100644
--- a/src/components/updatemenus/draw.js
+++ b/src/components/updatemenus/draw.js
@@ -192,6 +192,7 @@ function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, button
 function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) {
     var header = gHeader.selectAll('g.' + constants.headerClassName)
         .data([0]);
+    var dims = menuOpts._dims;
 
     header.enter().append('g')
         .classed(constants.headerClassName, true)
@@ -201,8 +202,8 @@ function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) {
         headerOpts = menuOpts.buttons[active] || constants.blankHeaderOpts,
         posOpts = { y: menuOpts.pad.t, yPad: 0, x: menuOpts.pad.l, xPad: 0, index: 0 },
         positionOverrides = {
-            width: menuOpts.headerWidth,
-            height: menuOpts.headerHeight
+            width: dims.headerWidth,
+            height: dims.headerHeight
         };
 
     header
@@ -221,8 +222,8 @@ function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) {
         .text(constants.arrowSymbol[menuOpts.direction]);
 
     arrow.attr({
-        x: menuOpts.headerWidth - constants.arrowOffsetX + menuOpts.pad.l,
-        y: menuOpts.headerHeight / 2 + constants.textOffsetY + menuOpts.pad.t
+        x: dims.headerWidth - constants.arrowOffsetX + menuOpts.pad.l,
+        y: dims.headerHeight / 2 + constants.textOffsetY + menuOpts.pad.t
     });
 
     header.on('click', function() {
@@ -250,7 +251,7 @@ function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) {
     });
 
     // translate header group
-    Drawing.setTranslate(gHeader, menuOpts.lx, menuOpts.ly);
+    Drawing.setTranslate(gHeader, dims.lx, dims.ly);
 }
 
 function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) {
@@ -290,28 +291,29 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) {
 
     var x0 = 0;
     var y0 = 0;
+    var dims = menuOpts._dims;
 
     var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1;
 
     if(menuOpts.type === 'dropdown') {
         if(isVertical) {
-            y0 = menuOpts.headerHeight + constants.gapButtonHeader;
+            y0 = dims.headerHeight + constants.gapButtonHeader;
         } else {
-            x0 = menuOpts.headerWidth + constants.gapButtonHeader;
+            x0 = dims.headerWidth + constants.gapButtonHeader;
         }
     }
 
     if(menuOpts.type === 'dropdown' && menuOpts.direction === 'up') {
-        y0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openHeight;
+        y0 = -constants.gapButtonHeader + constants.gapButton - dims.openHeight;
     }
 
     if(menuOpts.type === 'dropdown' && menuOpts.direction === 'left') {
-        x0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openWidth;
+        x0 = -constants.gapButtonHeader + constants.gapButton - dims.openWidth;
     }
 
     var posOpts = {
-        x: menuOpts.lx + x0 + menuOpts.pad.l,
-        y: menuOpts.ly + y0 + menuOpts.pad.t,
+        x: dims.lx + x0 + menuOpts.pad.l,
+        y: dims.ly + y0 + menuOpts.pad.t,
         yPad: constants.gapButton,
         xPad: constants.gapButton,
         index: 0,
@@ -355,12 +357,12 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) {
     buttons.call(styleButtons, menuOpts);
 
     if(isVertical) {
-        scrollBoxPosition.w = Math.max(menuOpts.openWidth, menuOpts.headerWidth);
+        scrollBoxPosition.w = Math.max(dims.openWidth, dims.headerWidth);
         scrollBoxPosition.h = posOpts.y - scrollBoxPosition.t;
     }
     else {
         scrollBoxPosition.w = posOpts.x - scrollBoxPosition.l;
-        scrollBoxPosition.h = Math.max(menuOpts.openHeight, menuOpts.headerHeight);
+        scrollBoxPosition.h = Math.max(dims.openHeight, dims.headerHeight);
     }
 
     scrollBoxPosition.direction = menuOpts.direction;
@@ -377,8 +379,9 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) {
 
 function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, position) {
     // enable the scrollbox
-    var direction = menuOpts.direction,
-        isVertical = (direction === 'up' || direction === 'down');
+    var direction = menuOpts.direction;
+    var isVertical = (direction === 'up' || direction === 'down');
+    var dims = menuOpts._dims;
 
     var active = menuOpts.active,
         translateX, translateY,
@@ -386,13 +389,13 @@ function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, position) {
     if(isVertical) {
         translateY = 0;
         for(i = 0; i < active; i++) {
-            translateY += menuOpts.heights[i] + constants.gapButton;
+            translateY += dims.heights[i] + constants.gapButton;
         }
     }
     else {
         translateX = 0;
         for(i = 0; i < active; i++) {
-            translateX += menuOpts.widths[i] + constants.gapButton;
+            translateX += dims.widths[i] + constants.gapButton;
         }
     }
 
@@ -502,16 +505,18 @@ function styleOnMouseOut(item, menuOpts) {
 
 // find item dimensions (this mutates menuOpts)
 function findDimensions(gd, menuOpts) {
-    menuOpts.width1 = 0;
-    menuOpts.height1 = 0;
-    menuOpts.heights = [];
-    menuOpts.widths = [];
-    menuOpts.totalWidth = 0;
-    menuOpts.totalHeight = 0;
-    menuOpts.openWidth = 0;
-    menuOpts.openHeight = 0;
-    menuOpts.lx = 0;
-    menuOpts.ly = 0;
+    var dims = menuOpts._dims = {
+        width1: 0,
+        height1: 0,
+        heights: [],
+        widths: [],
+        totalWidth: 0,
+        totalHeight: 0,
+        openWidth: 0,
+        openHeight: 0,
+        lx: 0,
+        ly: 0
+    };
 
     var fakeButtons = Drawing.tester.selectAll('g.' + constants.dropdownButtonClassName)
         .data(menuOpts.buttons);
@@ -543,79 +548,79 @@ function findDimensions(gd, menuOpts) {
 
         // Store per-item sizes since a row of horizontal buttons, for example,
         // don't all need to be the same width:
-        menuOpts.widths[i] = wEff;
-        menuOpts.heights[i] = hEff;
+        dims.widths[i] = wEff;
+        dims.heights[i] = hEff;
 
         // Height and width of individual element:
-        menuOpts.height1 = Math.max(menuOpts.height1, hEff);
-        menuOpts.width1 = Math.max(menuOpts.width1, wEff);
+        dims.height1 = Math.max(dims.height1, hEff);
+        dims.width1 = Math.max(dims.width1, wEff);
 
         if(isVertical) {
-            menuOpts.totalWidth = Math.max(menuOpts.totalWidth, wEff);
-            menuOpts.openWidth = menuOpts.totalWidth;
-            menuOpts.totalHeight += hEff + constants.gapButton;
-            menuOpts.openHeight += hEff + constants.gapButton;
+            dims.totalWidth = Math.max(dims.totalWidth, wEff);
+            dims.openWidth = dims.totalWidth;
+            dims.totalHeight += hEff + constants.gapButton;
+            dims.openHeight += hEff + constants.gapButton;
         } else {
-            menuOpts.totalWidth += wEff + constants.gapButton;
-            menuOpts.openWidth += wEff + constants.gapButton;
-            menuOpts.totalHeight = Math.max(menuOpts.totalHeight, hEff);
-            menuOpts.openHeight = menuOpts.totalHeight;
+            dims.totalWidth += wEff + constants.gapButton;
+            dims.openWidth += wEff + constants.gapButton;
+            dims.totalHeight = Math.max(dims.totalHeight, hEff);
+            dims.openHeight = dims.totalHeight;
         }
     });
 
     if(isVertical) {
-        menuOpts.totalHeight -= constants.gapButton;
+        dims.totalHeight -= constants.gapButton;
     } else {
-        menuOpts.totalWidth -= constants.gapButton;
+        dims.totalWidth -= constants.gapButton;
     }
 
 
-    menuOpts.headerWidth = menuOpts.width1 + constants.arrowPadX;
-    menuOpts.headerHeight = menuOpts.height1;
+    dims.headerWidth = dims.width1 + constants.arrowPadX;
+    dims.headerHeight = dims.height1;
 
     if(menuOpts.type === 'dropdown') {
         if(isVertical) {
-            menuOpts.width1 += constants.arrowPadX;
-            menuOpts.totalHeight = menuOpts.height1;
+            dims.width1 += constants.arrowPadX;
+            dims.totalHeight = dims.height1;
         } else {
-            menuOpts.totalWidth = menuOpts.width1;
+            dims.totalWidth = dims.width1;
         }
-        menuOpts.totalWidth += constants.arrowPadX;
+        dims.totalWidth += constants.arrowPadX;
     }
 
     fakeButtons.remove();
 
-    var paddedWidth = menuOpts.totalWidth + menuOpts.pad.l + menuOpts.pad.r;
-    var paddedHeight = menuOpts.totalHeight + menuOpts.pad.t + menuOpts.pad.b;
+    var paddedWidth = dims.totalWidth + menuOpts.pad.l + menuOpts.pad.r;
+    var paddedHeight = dims.totalHeight + menuOpts.pad.t + menuOpts.pad.b;
 
     var graphSize = gd._fullLayout._size;
-    menuOpts.lx = graphSize.l + graphSize.w * menuOpts.x;
-    menuOpts.ly = graphSize.t + graphSize.h * (1 - menuOpts.y);
+    dims.lx = graphSize.l + graphSize.w * menuOpts.x;
+    dims.ly = graphSize.t + graphSize.h * (1 - menuOpts.y);
 
     var xanchor = 'left';
     if(anchorUtils.isRightAnchor(menuOpts)) {
-        menuOpts.lx -= paddedWidth;
+        dims.lx -= paddedWidth;
         xanchor = 'right';
     }
     if(anchorUtils.isCenterAnchor(menuOpts)) {
-        menuOpts.lx -= paddedWidth / 2;
+        dims.lx -= paddedWidth / 2;
         xanchor = 'center';
     }
 
     var yanchor = 'top';
     if(anchorUtils.isBottomAnchor(menuOpts)) {
-        menuOpts.ly -= paddedHeight;
+        dims.ly -= paddedHeight;
         yanchor = 'bottom';
     }
     if(anchorUtils.isMiddleAnchor(menuOpts)) {
-        menuOpts.ly -= paddedHeight / 2;
+        dims.ly -= paddedHeight / 2;
         yanchor = 'middle';
     }
 
-    menuOpts.totalWidth = Math.ceil(menuOpts.totalWidth);
-    menuOpts.totalHeight = Math.ceil(menuOpts.totalHeight);
-    menuOpts.lx = Math.round(menuOpts.lx);
-    menuOpts.ly = Math.round(menuOpts.ly);
+    dims.totalWidth = Math.ceil(dims.totalWidth);
+    dims.totalHeight = Math.ceil(dims.totalHeight);
+    dims.lx = Math.round(dims.lx);
+    dims.ly = Math.round(dims.ly);
 
     Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index, {
         x: menuOpts.x,
@@ -634,16 +639,17 @@ function setItemPosition(item, menuOpts, posOpts, overrideOpts) {
     var text = item.select('.' + constants.itemTextClassName);
     var borderWidth = menuOpts.borderwidth;
     var index = posOpts.index;
+    var dims = menuOpts._dims;
 
     Drawing.setTranslate(item, borderWidth + posOpts.x, borderWidth + posOpts.y);
 
     var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1;
-    var finalHeight = overrideOpts.height || (isVertical ? menuOpts.heights[index] : menuOpts.height1);
+    var finalHeight = overrideOpts.height || (isVertical ? dims.heights[index] : dims.height1);
 
     rect.attr({
         x: 0,
         y: 0,
-        width: overrideOpts.width || (isVertical ? menuOpts.width1 : menuOpts.widths[index]),
+        width: overrideOpts.width || (isVertical ? dims.width1 : dims.widths[index]),
         height: finalHeight
     });
 
@@ -655,9 +661,9 @@ function setItemPosition(item, menuOpts, posOpts, overrideOpts) {
         finalHeight / 2 - spanOffset + constants.textOffsetY);
 
     if(isVertical) {
-        posOpts.y += menuOpts.heights[index] + posOpts.yPad;
+        posOpts.y += dims.heights[index] + posOpts.yPad;
     } else {
-        posOpts.x += menuOpts.widths[index] + posOpts.xPad;
+        posOpts.x += dims.widths[index] + posOpts.xPad;
     }
 
     posOpts.index++;
diff --git a/src/constants/alignment.js b/src/constants/alignment.js
index 5e4d6836b38..a63cc18266d 100644
--- a/src/constants/alignment.js
+++ b/src/constants/alignment.js
@@ -29,6 +29,15 @@ module.exports = {
         middle: 0.5,
         top: 0
     },
+    // from bottom right: sometimes you just need the opposite of ^^
+    FROM_BR: {
+        left: 1,
+        center: 0.5,
+        right: 0,
+        bottom: 0,
+        middle: 0.5,
+        top: 1
+    },
     // multiple of fontSize to get the vertical offset between lines
     LINE_SPACING: 1.3,
 
diff --git a/src/core.js b/src/core.js
index f8abab28eb4..8afc64171ab 100644
--- a/src/core.js
+++ b/src/core.js
@@ -33,6 +33,7 @@ exports.restyle = Plotly.restyle;
 exports.relayout = Plotly.relayout;
 exports.redraw = Plotly.redraw;
 exports.update = Plotly.update;
+exports.react = Plotly.react;
 exports.extendTraces = Plotly.extendTraces;
 exports.prependTraces = Plotly.prependTraces;
 exports.addTraces = Plotly.addTraces;
diff --git a/src/lib/coerce.js b/src/lib/coerce.js
index 07627121c1f..26f15e57932 100644
--- a/src/lib/coerce.js
+++ b/src/lib/coerce.js
@@ -406,6 +406,9 @@ exports.coerceSelectionMarkerOpacity = function(traceOut, coerce) {
     if(!traceOut.marker) return;
 
     var mo = traceOut.marker.opacity;
+    // you can still have a `marker` container with no markers if there's text
+    if(mo === undefined) return;
+
     var smoDflt;
     var usmoDflt;
 
diff --git a/src/lib/push_unique.js b/src/lib/push_unique.js
index 87b9f4cc672..ca2dcf30d4c 100644
--- a/src/lib/push_unique.js
+++ b/src/lib/push_unique.js
@@ -11,6 +11,8 @@
 /**
  * Push array with unique items
  *
+ * Ignores falsy items, except 0 so we can use it to construct arrays of indices.
+ *
  * @param {array} array
  *  array to be filled
  * @param {any} item
@@ -30,7 +32,7 @@ module.exports = function pushUnique(array, item) {
         }
         array.push(item);
     }
-    else if(item && array.indexOf(item) === -1) array.push(item);
+    else if((item || item === 0) && array.indexOf(item) === -1) array.push(item);
 
     return array;
 };
diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js
index 5f0f5c20772..38a5d715dd5 100644
--- a/src/plot_api/helpers.js
+++ b/src/plot_api/helpers.js
@@ -196,8 +196,14 @@ function cleanAxRef(container, attr) {
     }
 }
 
-// Make a few changes to the data right away
-// before it gets used for anything
+/*
+ * cleanData: Make a few changes to the data right away
+ * before it gets used for anything
+ * Mostly for backward compatibility, modifies the data traces users provide.
+ *
+ * Important: if you're going to add something here that modifies a data array,
+ * update it in place so the new array === the old one.
+ */
 exports.cleanData = function(data, existingData) {
     // Enforce unique IDs
     var suids = [], // seen uids --- so we can weed out incoming repeats
@@ -283,7 +289,9 @@ exports.cleanData = function(data, existingData) {
 
         if(!Registry.traceIs(trace, 'pie') && !Registry.traceIs(trace, 'bar')) {
             if(Array.isArray(trace.textposition)) {
-                trace.textposition = trace.textposition.map(cleanTextPosition);
+                for(i = 0; i < trace.textposition.length; i++) {
+                    trace.textposition[i] = cleanTextPosition(trace.textposition[i]);
+                }
             }
             else if(trace.textposition) {
                 trace.textposition = cleanTextPosition(trace.textposition);
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index 644251611c4..9aa3f9712ce 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -58,6 +58,13 @@ var numericNameWarningCountLimit = 5;
  * @param {object} config
  *      configuration options (see ./plot_config.js for more info)
  *
+ * OR
+ *
+ * @param {string id or DOM element} gd
+ *      the id or DOM element of the graph container div
+ * @param {object} figure
+ *      object containing `data`, `layout`, `config`, and `frames` members
+ *
  */
 Plotly.plot = function(gd, data, layout, config) {
     var frames;
@@ -1379,8 +1386,7 @@ function _restyle(gd, aobj, traces) {
     // for the undo / redo queue
     var redoit = {},
         undoit = {},
-        axlist,
-        flagAxForDelete = {};
+        axlist;
 
     // make a new empty vals array for undoit
     function a0() { return traces.map(function() { return undefined; }); }
@@ -1523,9 +1529,6 @@ function _restyle(gd, aobj, traces) {
                 } else if(Registry.traceIs(cont, 'cartesian')) {
                     Lib.nestedProperty(cont, 'marker.colors')
                         .set(Lib.nestedProperty(cont, 'marker.color').get());
-                    // look for axes that are no longer in use and delete them
-                    flagAxForDelete[cont.xaxis || 'x'] = true;
-                    flagAxForDelete[cont.yaxis || 'y'] = true;
                 }
             }
 
@@ -1632,26 +1635,6 @@ function _restyle(gd, aobj, traces) {
         }
     }
 
-    // check axes we've flagged for possible deletion
-    // flagAxForDelete is a hash so we can make sure we only get each axis once
-    var axListForDelete = Object.keys(flagAxForDelete);
-    axisLoop:
-    for(i = 0; i < axListForDelete.length; i++) {
-        var axId = axListForDelete[i],
-            axLetter = axId.charAt(0),
-            axAttr = axLetter + 'axis';
-
-        for(var j = 0; j < data.length; j++) {
-            if(Registry.traceIs(data[j], 'cartesian') &&
-                    (data[j][axAttr] || axLetter) === axId) {
-                continue axisLoop;
-            }
-        }
-
-        // no data on this axis - delete it.
-        doextra('LAYOUT' + Plotly.Axes.id2name(axId), null, 0);
-    }
-
     // combine a few flags together;
     if(flags.calc || (flags.calcIfAutorange && autorangeOn)) {
         flags.clearCalc = true;
@@ -1806,25 +1789,6 @@ function _relayout(gd, aobj) {
         if(val !== undefined) p.set(val);
     }
 
-    // for editing annotations or shapes - is it on autoscaled axes?
-    function refAutorange(obj, axLetter) {
-        if(!Lib.isPlainObject(obj)) return false;
-        var axRef = obj[axLetter + 'ref'] || axLetter,
-            ax = Plotly.Axes.getFromId(gd, axRef);
-
-        if(!ax && axRef.charAt(0) === axLetter) {
-            // fall back on the primary axis in case we've referenced a
-            // nonexistent axis (as we do above if axRef is missing).
-            // This assumes the object defaults to data referenced, which
-            // is the case for shapes and annotations but not for images.
-            // The only thing this is used for is to determine whether to
-            // do a full `recalc`, so the only ill effect of this error is
-            // to waste some time.
-            ax = Plotly.Axes.getFromId(gd, axLetter);
-        }
-        return (ax || {}).autorange;
-    }
-
     // for constraint enforcement: keep track of all axes (as {id: name})
     // we're editing the (auto)range of, so we can tell the others constrained
     // to scale with them that it's OK for them to shrink
@@ -2018,7 +1982,7 @@ function _relayout(gd, aobj) {
                 else Lib.warn('unrecognized full object value', aobj);
             }
 
-            if(checkForAutorange && (refAutorange(objToAutorange, 'x') || refAutorange(objToAutorange, 'y'))) {
+            if(checkForAutorange && (refAutorange(gd, objToAutorange, 'x') || refAutorange(gd, objToAutorange, 'y'))) {
                 flags.calc = true;
             }
             else editTypes.update(flags, updateValObject);
@@ -2114,6 +2078,25 @@ function _relayout(gd, aobj) {
     };
 }
 
+// for editing annotations or shapes - is it on autoscaled axes?
+function refAutorange(gd, obj, axLetter) {
+    if(!Lib.isPlainObject(obj)) return false;
+    var axRef = obj[axLetter + 'ref'] || axLetter,
+        ax = Plotly.Axes.getFromId(gd, axRef);
+
+    if(!ax && axRef.charAt(0) === axLetter) {
+        // fall back on the primary axis in case we've referenced a
+        // nonexistent axis (as we do above if axRef is missing).
+        // This assumes the object defaults to data referenced, which
+        // is the case for shapes and annotations but not for images.
+        // The only thing this is used for is to determine whether to
+        // do a full `recalc`, so the only ill effect of this error is
+        // to waste some time.
+        ax = Plotly.Axes.getFromId(gd, axLetter);
+    }
+    return (ax || {}).autorange;
+}
+
 /**
  * update: update trace and layout attributes of an existing plot
  *
@@ -2208,6 +2191,404 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, _traces) {
     });
 };
 
+/**
+ * Plotly.react:
+ * A plot/update method that takes the full plot state (same API as plot/newPlot)
+ * and diffs to determine the minimal update pathway
+ *
+ * @param {string id or DOM element} gd
+ *      the id or DOM element of the graph container div
+ * @param {array of objects} data
+ *      array of traces, containing the data and display information for each trace
+ * @param {object} layout
+ *      object describing the overall display of the plot,
+ *      all the stuff that doesn't pertain to any individual trace
+ * @param {object} config
+ *      configuration options (see ./plot_config.js for more info)
+ *
+ * OR
+ *
+ * @param {string id or DOM element} gd
+ *      the id or DOM element of the graph container div
+ * @param {object} figure
+ *      object containing `data`, `layout`, `config`, and `frames` members
+ *
+ */
+Plotly.react = function(gd, data, layout, config) {
+    var frames, plotDone;
+
+    function addFrames() { return Plotly.addFrames(gd, frames); }
+
+    gd = Lib.getGraphDiv(gd);
+
+    var oldFullData = gd._fullData;
+    var oldFullLayout = gd._fullLayout;
+
+    // you can use this as the initial draw as well as to update
+    if(!Lib.isPlotDiv(gd) || !oldFullData || !oldFullLayout) {
+        plotDone = Plotly.newPlot(gd, data, layout, config);
+    }
+    else {
+
+        if(Lib.isPlainObject(data)) {
+            var obj = data;
+            data = obj.data;
+            layout = obj.layout;
+            config = obj.config;
+            frames = obj.frames;
+        }
+
+        var configChanged = false;
+        // assume that if there's a config at all, we're reacting to it too,
+        // and completely replace the previous config
+        if(config) {
+            var oldConfig = Lib.extendDeep({}, gd._context);
+            gd._context = undefined;
+            setPlotContext(gd, config);
+            configChanged = diffConfig(oldConfig, gd._context);
+        }
+
+        gd.data = data || [];
+        helpers.cleanData(gd.data, []);
+        gd.layout = layout || {};
+        helpers.cleanLayout(gd.layout);
+
+        Plots.supplyDefaults(gd);
+
+        var newFullData = gd._fullData;
+        var newFullLayout = gd._fullLayout;
+        var immutable = newFullLayout.datarevision === undefined;
+
+        var restyleFlags = diffData(gd, oldFullData, newFullData, immutable);
+        var relayoutFlags = diffLayout(gd, oldFullLayout, newFullLayout, immutable);
+
+        // clear calcdata if required
+        if(restyleFlags.calc || relayoutFlags.calc) gd.calcdata = undefined;
+
+        // Note: what restyle/relayout use impliedEdits and clearAxisTypes for
+        // must be handled by the user when using Plotly.react.
+
+        // fill in redraw sequence
+        var seq = [];
+
+        if(frames) {
+            gd._transitionData = {};
+            Plots.createTransitionData(gd);
+            seq.push(addFrames);
+        }
+
+        if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) {
+            gd._fullLayout._skipDefaults = true;
+            seq.push(Plotly.plot);
+        }
+        else {
+            for(var componentType in relayoutFlags.arrays) {
+                var indices = relayoutFlags.arrays[componentType];
+                if(indices.length) {
+                    var drawOne = Registry.getComponentMethod(componentType, 'drawOne');
+                    if(drawOne !== Lib.noop) {
+                        for(var i = 0; i < indices.length; i++) {
+                            drawOne(gd, indices[i]);
+                        }
+                    }
+                    else {
+                        var draw = Registry.getComponentMethod(componentType, 'draw');
+                        if(draw === Lib.noop) {
+                            throw new Error('cannot draw components: ' + componentType);
+                        }
+                        draw(gd);
+                    }
+                }
+            }
+
+            seq.push(Plots.previousPromises);
+            if(restyleFlags.style) seq.push(subroutines.doTraceStyle);
+            if(restyleFlags.colorbars) seq.push(subroutines.doColorBars);
+            if(relayoutFlags.legend) seq.push(subroutines.doLegend);
+            if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles);
+            if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout);
+            if(relayoutFlags.modebar) seq.push(subroutines.doModeBar);
+            if(relayoutFlags.camera) seq.push(subroutines.doCamera);
+        }
+
+        seq.push(Plots.rehover);
+
+        plotDone = Lib.syncOrAsync(seq, gd);
+        if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd);
+    }
+
+    return plotDone.then(function() {
+        gd.emit('plotly_react', {
+            data: data,
+            layout: layout
+        });
+
+        return gd;
+    });
+
+};
+
+function diffData(gd, oldFullData, newFullData, immutable) {
+    if(oldFullData.length !== newFullData.length) {
+        return {
+            fullReplot: true,
+            clearCalc: true
+        };
+    }
+
+    var flags = editTypes.traceFlags();
+    flags.arrays = {};
+    var i, trace;
+
+    function getTraceValObject(parts) {
+        return PlotSchema.getTraceValObject(trace, parts);
+    }
+
+    var diffOpts = {
+        getValObject: getTraceValObject,
+        flags: flags,
+        immutable: immutable,
+        gd: gd
+    };
+
+    for(i = 0; i < oldFullData.length; i++) {
+        trace = newFullData[i];
+        diffOpts.autoranged = trace.xaxis ? (
+            Plotly.Axes.getFromId(gd, trace.xaxis).autorange ||
+            Plotly.Axes.getFromId(gd, trace.yaxis).autorange
+        ) : false;
+        getDiffFlags(oldFullData[i], trace, [], diffOpts);
+    }
+
+    if(flags.calc || flags.plot || flags.calcIfAutorange) {
+        flags.fullReplot = true;
+    }
+
+    return flags;
+}
+
+function diffLayout(gd, oldFullLayout, newFullLayout, immutable) {
+    var flags = editTypes.layoutFlags();
+    flags.arrays = {};
+
+    function getLayoutValObject(parts) {
+        return PlotSchema.getLayoutValObject(newFullLayout, parts);
+    }
+
+    var diffOpts = {
+        getValObject: getLayoutValObject,
+        flags: flags,
+        immutable: immutable,
+        gd: gd
+    };
+
+    getDiffFlags(oldFullLayout, newFullLayout, [], diffOpts);
+
+    if(flags.plot || flags.calc) {
+        flags.layoutReplot = true;
+    }
+
+    return flags;
+}
+
+function getDiffFlags(oldContainer, newContainer, outerparts, opts) {
+    var valObject, key;
+
+    var getValObject = opts.getValObject;
+    var flags = opts.flags;
+    var immutable = opts.immutable;
+    var inArray = opts.inArray;
+    var arrayIndex = opts.arrayIndex;
+    var gd = opts.gd;
+    var autoranged = opts.autoranged;
+
+    function changed() {
+        var editType = valObject.editType;
+        if(editType.indexOf('calcIfAutorange') !== -1 && (autoranged || (autoranged === undefined && (
+            refAutorange(gd, newContainer, 'x') || refAutorange(gd, newContainer, 'y')
+        )))) {
+            flags.calc = true;
+            return;
+        }
+        if(inArray && editType.indexOf('arraydraw') !== -1) {
+            Lib.pushUnique(flags.arrays[inArray], arrayIndex);
+            return;
+        }
+        editTypes.update(flags, valObject);
+    }
+
+    function valObjectCanBeDataArray(valObject) {
+        return valObject.valType === 'data_array' || valObject.arrayOk;
+    }
+
+    // for transforms: look at _fullInput rather than the transform result, which often
+    // contains generated arrays.
+    var newFullInput = newContainer._fullInput;
+    var oldFullInput = oldContainer._fullInput;
+    if(newFullInput && newFullInput !== newContainer) newContainer = newFullInput;
+    if(oldFullInput && oldFullInput !== oldContainer) oldContainer = oldFullInput;
+
+    for(key in oldContainer) {
+        // short-circuit based on previous calls or previous keys that already maximized the pathway
+        if(flags.calc) return;
+
+        var oldVal = oldContainer[key];
+        var newVal = newContainer[key];
+
+        if(key.charAt(0) === '_' || typeof oldVal === 'function' || oldVal === newVal) continue;
+
+        // FIXME: ax.tick0 and dtick get filled in during plotting, and unlike other auto values
+        // they don't make it back into the input, so newContainer won't have them.
+        // similar for axis ranges for 3D
+        // contourcarpet doesn't HAVE zmin/zmax, they're just auto-added. It needs them.
+        if(key === 'tick0' || key === 'dtick') {
+            var tickMode = newContainer.tickmode;
+            if(tickMode === 'auto' || tickMode === 'array' || !tickMode) continue;
+        }
+        if(key === 'range' && newContainer.autorange) continue;
+        if((key === 'zmin' || key === 'zmax') && newContainer.type === 'contourcarpet') continue;
+
+        var parts = outerparts.concat(key);
+        valObject = getValObject(parts);
+
+        // in case type changed, we may not even *have* a valObject.
+        if(!valObject) continue;
+
+        var valType = valObject.valType;
+        var i;
+
+        var canBeDataArray = valObjectCanBeDataArray(valObject);
+        var wasArray = Array.isArray(oldVal);
+        var nowArray = Array.isArray(newVal);
+
+        // hack for traces that modify the data in supplyDefaults, like
+        // converting 1D to 2D arrays, which will always create new objects
+        if(wasArray && nowArray) {
+            var inputKey = '_input_' + key;
+            var oldValIn = oldContainer[inputKey];
+            var newValIn = newContainer[inputKey];
+            if(Array.isArray(oldValIn) && oldValIn === newValIn) continue;
+        }
+
+        if(newVal === undefined) {
+            if(canBeDataArray && wasArray) flags.calc = true;
+            else changed();
+        }
+        else if(valObject._isLinkedToArray) {
+            var arrayEditIndices = [];
+            var extraIndices = false;
+            if(!inArray) flags.arrays[key] = arrayEditIndices;
+
+            var minLen = Math.min(oldVal.length, newVal.length);
+            var maxLen = Math.max(oldVal.length, newVal.length);
+            if(minLen !== maxLen) {
+                if(valObject.editType === 'arraydraw') {
+                    extraIndices = true;
+                }
+                else {
+                    changed();
+                    continue;
+                }
+            }
+
+            for(i = 0; i < minLen; i++) {
+                getDiffFlags(oldVal[i], newVal[i], parts.concat(i),
+                    // add array indices, but not if we're already in an array
+                    Lib.extendFlat({inArray: key, arrayIndex: i}, opts));
+            }
+
+            // put this at the end so that we know our collected array indices are sorted
+            // but the check for length changes happens up front so we can short-circuit
+            // diffing if appropriate
+            if(extraIndices) {
+                for(i = minLen; i < maxLen; i++) {
+                    arrayEditIndices.push(i);
+                }
+            }
+        }
+        else if(!valType && Lib.isPlainObject(oldVal)) {
+            getDiffFlags(oldVal, newVal, parts, opts);
+        }
+        else if(canBeDataArray) {
+            if(wasArray && nowArray) {
+
+                // don't try to diff two data arrays. If immutable we know the data changed,
+                // if not, assume it didn't and let `layout.datarevision` tell us if it did
+                if(immutable) {
+                    flags.calc = true;
+                }
+            }
+            else if(wasArray !== nowArray) {
+                flags.calc = true;
+            }
+            else changed();
+        }
+        else if(wasArray && nowArray) {
+            // info array, colorscale, 'any' - these are short, just stringify.
+            // I don't *think* that covers up any real differences post-validation, does it?
+            // otherwise we need to dive in 1 (info_array) or 2 (colorscale) levels and compare
+            // all elements.
+            if(oldVal.length !== newVal.length || String(oldVal) !== String(newVal)) {
+                changed();
+            }
+        }
+        else {
+            changed();
+        }
+    }
+
+    for(key in newContainer) {
+        if(!(key in oldContainer)) {
+            valObject = getValObject(outerparts.concat(key));
+
+            if(valObjectCanBeDataArray(valObject) && Array.isArray(newContainer[key])) {
+                flags.calc = true;
+                return;
+            }
+            else changed();
+        }
+    }
+}
+
+/*
+ * simple diff for config - for now, just treat all changes as equivalent
+ */
+function diffConfig(oldConfig, newConfig) {
+    var key;
+
+    for(key in oldConfig) {
+        var oldVal = oldConfig[key];
+        var newVal = newConfig[key];
+        if(oldVal !== newVal) {
+            if(Lib.isPlainObject(oldVal) && Lib.isPlainObject(newVal)) {
+                if(diffConfig(oldVal, newVal)) {
+                    return true;
+                }
+            }
+            else if(Array.isArray(oldVal) && Array.isArray(newVal)) {
+                if(oldVal.length !== newVal.length) {
+                    return true;
+                }
+                for(var i = 0; i < oldVal.length; i++) {
+                    if(oldVal[i] !== newVal[i]) {
+                        if(Lib.isPlainObject(oldVal[i]) && Lib.isPlainObject(newVal[i])) {
+                            if(diffConfig(oldVal[i], newVal[i])) {
+                                return true;
+                            }
+                        }
+                        else {
+                            return true;
+                        }
+                    }
+                }
+            }
+            else {
+                return true;
+            }
+        }
+    }
+}
+
 /**
  * Animate to a frame, sequence of frame, frame group, or frame definition
  *
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index 07c09c6eafc..01f892f748f 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -1817,17 +1817,6 @@ axes.doTicks = function(gd, axid, skipTitle) {
         }
     }
 
-    // make sure we only have allowed options for exponents
-    // (others can make confusing errors)
-    if(!ax.tickformat) {
-        if(['none', 'e', 'E', 'power', 'SI', 'B'].indexOf(ax.exponentformat) === -1) {
-            ax.exponentformat = 'e';
-        }
-        if(['all', 'first', 'last', 'none'].indexOf(ax.showexponent) === -1) {
-            ax.showexponent = 'all';
-        }
-    }
-
     // set scaling to pixels
     ax.setScale();
 
diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js
index 779703a2ce4..a56d7befae1 100644
--- a/src/plots/cartesian/graph_interact.js
+++ b/src/plots/cartesian/graph_interact.js
@@ -9,6 +9,8 @@
 
 'use strict';
 
+var d3 = require('d3');
+
 var Fx = require('../../components/fx');
 var dragElement = require('../../components/dragelement');
 
@@ -18,7 +20,13 @@ var makeDragBox = require('./dragbox').makeDragBox;
 module.exports = function initInteractions(gd) {
     var fullLayout = gd._fullLayout;
 
-    if((!fullLayout._has('cartesian') && !fullLayout._has('gl2d')) || gd._context.staticPlot) return;
+    if(gd._context.staticPlot) {
+        // this sweeps up more than just cartesian drag elements...
+        d3.select(gd).selectAll('.drag').remove();
+        return;
+    }
+
+    if(!fullLayout._has('cartesian') && !fullLayout._has('gl2d')) return;
 
     var subplots = Object.keys(fullLayout._plots || {}).sort(function(a, b) {
         // sort overlays last, then by x axis number, then y axis number
diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js
index 01db927a7b9..342d5900b2e 100644
--- a/src/plots/cartesian/set_convert.js
+++ b/src/plots/cartesian/set_convert.js
@@ -398,15 +398,16 @@ module.exports = function setConvert(ax, fullLayout) {
     // in case the expected data isn't there, make a list of
     // integers based on the opposite data
     ax.makeCalcdata = function(trace, axLetter) {
-        var arrayIn, arrayOut, i;
+        var arrayIn, arrayOut, i, len;
 
         var cal = ax.type === 'date' && trace[axLetter + 'calendar'];
 
         if(axLetter in trace) {
             arrayIn = trace[axLetter];
-            arrayOut = new Array(arrayIn.length);
+            len = trace._length || arrayIn.length;
+            arrayOut = new Array(len);
 
-            for(i = 0; i < arrayIn.length; i++) {
+            for(i = 0; i < len; i++) {
                 arrayOut[i] = ax.d2c(arrayIn[i], 0, cal);
             }
         }
@@ -418,9 +419,10 @@ module.exports = function setConvert(ax, fullLayout) {
 
             // the opposing data, for size if we have x and dx etc
             arrayIn = trace[{x: 'y', y: 'x'}[axLetter]];
-            arrayOut = new Array(arrayIn.length);
+            len = trace._length || arrayIn.length;
+            arrayOut = new Array(len);
 
-            for(i = 0; i < arrayIn.length; i++) arrayOut[i] = v0 + i * dv;
+            for(i = 0; i < len; i++) arrayOut[i] = v0 + i * dv;
         }
         return arrayOut;
     };
diff --git a/src/plots/gl3d/layout/tick_marks.js b/src/plots/gl3d/layout/tick_marks.js
index 9cdb2299687..97b4e20fed5 100644
--- a/src/plots/gl3d/layout/tick_marks.js
+++ b/src/plots/gl3d/layout/tick_marks.js
@@ -50,6 +50,7 @@ function computeTickMarks(scene) {
         if(Math.abs(axes._length) === Infinity) {
             ticks[i] = [];
         } else {
+            axes._input_range = axes.range.slice();
             axes.range[0] = (glRange[i].lo) / scene.dataScale[i];
             axes.range[1] = (glRange[i].hi) / scene.dataScale[i];
             axes._m = 1.0 / (scene.dataScale[i] * glRange[i].pixelsPerDataUnit);
diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js
index 2ee604891f0..8a4a2453cfb 100644
--- a/src/plots/gl3d/scene.js
+++ b/src/plots/gl3d/scene.js
@@ -306,9 +306,15 @@ proto.recoverContext = function() {
 
 var axisProperties = [ 'xaxis', 'yaxis', 'zaxis' ];
 
-function coordinateBound(axis, coord, d, bounds, calendar) {
+function coordinateBound(axis, coord, len, d, bounds, calendar) {
     var x;
-    for(var i = 0; i < coord.length; ++i) {
+    if(!Array.isArray(coord)) {
+        bounds[0][d] = Math.min(bounds[0][d], 0);
+        bounds[1][d] = Math.max(bounds[1][d], len - 1);
+        return;
+    }
+
+    for(var i = 0; i < (len || coord.length); ++i) {
         if(Array.isArray(coord[i])) {
             for(var j = 0; j < coord[i].length; ++j) {
                 x = axis.d2l(coord[i][j], 0, calendar);
@@ -330,9 +336,9 @@ function coordinateBound(axis, coord, d, bounds, calendar) {
 
 function computeTraceBounds(scene, trace, bounds) {
     var sceneLayout = scene.fullSceneLayout;
-    coordinateBound(sceneLayout.xaxis, trace.x, 0, bounds, trace.xcalendar);
-    coordinateBound(sceneLayout.yaxis, trace.y, 1, bounds, trace.ycalendar);
-    coordinateBound(sceneLayout.zaxis, trace.z, 2, bounds, trace.zcalendar);
+    coordinateBound(sceneLayout.xaxis, trace.x, trace._xlength, 0, bounds, trace.xcalendar);
+    coordinateBound(sceneLayout.yaxis, trace.y, trace._ylength, 1, bounds, trace.ycalendar);
+    coordinateBound(sceneLayout.zaxis, trace.z, trace._zlength, 2, bounds, trace.zcalendar);
 }
 
 proto.plot = function(sceneData, fullLayout, layout) {
diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js
index 2cf5fac6172..1dfee32e8f7 100644
--- a/src/plots/layout_attributes.js
+++ b/src/plots/layout_attributes.js
@@ -182,4 +182,18 @@ module.exports = {
         editType: 'calc',
         description: 'Sets the default trace colors.'
     },
+    datarevision: {
+        valType: 'any',
+        role: 'info',
+        editType: 'calc',
+        description: [
+            'If provided, a changed value tells `Plotly.react` that',
+            'one or more data arrays has changed. This way you can modify',
+            'arrays in-place rather than making a complete new copy for an',
+            'incremental change.',
+            'If NOT provided, `Plotly.react` assumes that data arrays are',
+            'being treated as immutable, thus any data array with a',
+            'different identity from its predecessor contains new data.'
+        ].join(' ')
+    }
 };
diff --git a/src/plots/mapbox/index.js b/src/plots/mapbox/index.js
index eb1eeed7c0f..0e7b12d57d8 100644
--- a/src/plots/mapbox/index.js
+++ b/src/plots/mapbox/index.js
@@ -62,9 +62,6 @@ exports.plot = function plotMapbox(gd) {
             opts = fullLayout[id],
             mapbox = opts._subplot;
 
-        // copy access token to fullLayout (to handle the context case)
-        opts.accesstoken = accessToken;
-
         if(!mapbox) {
             mapbox = createMapbox({
                 gd: gd,
@@ -136,24 +133,17 @@ function findAccessToken(gd, mapboxIds) {
     // special case for Mapbox Atlas users
     if(context.mapboxAccessToken === '') return '';
 
-    // first look for access token in context
-    var accessToken = context.mapboxAccessToken;
-
-    // allow mapbox layout options to override it
+    // Take the first token we find in a mapbox subplot.
+    // These default to the context value but may be overridden.
     for(var i = 0; i < mapboxIds.length; i++) {
         var opts = fullLayout[mapboxIds[i]];
 
         if(opts.accesstoken) {
-            accessToken = opts.accesstoken;
-            break;
+            return opts.accesstoken;
         }
     }
 
-    if(!accessToken) {
-        throw new Error(constants.noAccessTokenErrorMsg);
-    }
-
-    return accessToken;
+    throw new Error(constants.noAccessTokenErrorMsg);
 }
 
 exports.updateFx = function(fullLayout) {
diff --git a/src/plots/mapbox/layout_defaults.js b/src/plots/mapbox/layout_defaults.js
index f14a3744583..7c381dd9144 100644
--- a/src/plots/mapbox/layout_defaults.js
+++ b/src/plots/mapbox/layout_defaults.js
@@ -20,12 +20,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
         type: 'mapbox',
         attributes: layoutAttributes,
         handleDefaults: handleDefaults,
-        partition: 'y'
+        partition: 'y',
+        accessToken: layoutOut._mapboxAccessToken
     });
 };
 
-function handleDefaults(containerIn, containerOut, coerce) {
-    coerce('accesstoken');
+function handleDefaults(containerIn, containerOut, coerce, opts) {
+    coerce('accesstoken', opts.accessToken);
     coerce('style');
     coerce('center.lon');
     coerce('center.lat');
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 32c16ac1f3e..ae049d78d5c 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -281,13 +281,21 @@ var extraFormatKeys = [
 //   is a list of all the transform modules invoked.
 //
 plots.supplyDefaults = function(gd) {
-    var oldFullLayout = gd._fullLayout || {},
-        newFullLayout = gd._fullLayout = {},
-        newLayout = gd.layout || {};
+    var oldFullLayout = gd._fullLayout || {};
 
-    var oldFullData = gd._fullData || [],
-        newFullData = gd._fullData = [],
-        newData = gd.data || [];
+    if(oldFullLayout._skipDefaults) {
+        delete oldFullLayout._skipDefaults;
+        return;
+    }
+
+    var newFullLayout = gd._fullLayout = {};
+    var newLayout = gd.layout || {};
+
+    var oldFullData = gd._fullData || [];
+    var newFullData = gd._fullData = [];
+    var newData = gd.data || [];
+
+    var context = gd._context || {};
 
     var i;
 
@@ -316,6 +324,9 @@ plots.supplyDefaults = function(gd) {
 
     var formatObj = getFormatObj(gd, d3FormatKeys);
 
+    // stash the token from context so mapbox subplots can use it as default
+    newFullLayout._mapboxAccessToken = context.mapboxAccessToken;
+
     // first fill in what we can of layout without looking at data
     // because fullData needs a few things from layout
 
@@ -337,7 +348,7 @@ plots.supplyDefaults = function(gd) {
 
         var missingWidthOrHeight = (!newLayout.width || !newLayout.height),
             autosize = newFullLayout.autosize,
-            autosizable = gd._context && gd._context.autosizable,
+            autosizable = context.autosizable,
             initialAutoSize = missingWidthOrHeight && (autosize || autosizable);
 
         if(initialAutoSize) plots.plotAutoSize(gd, newLayout, newFullLayout);
@@ -1246,6 +1257,8 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) {
 
     coerce('colorway');
 
+    coerce('datarevision');
+
     Registry.getComponentMethod(
         'calendars',
         'handleDefaults'
diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js
index e6dc76406f4..9e4d49ad15e 100644
--- a/src/plots/polar/layout_defaults.js
+++ b/src/plots/polar/layout_defaults.js
@@ -87,6 +87,7 @@ function handleDefaults(contIn, contOut, coerce, opts) {
         switch(axName) {
             case 'radialaxis':
                 var autoRange = coerceAxis('autorange', !axOut.isValidRange(axIn.range));
+                axIn.autorange = autoRange;
                 if(autoRange) coerceAxis('rangemode');
                 if(autoRange === 'reversed') axOut._m = -1;
 
diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js
index 3a2829b7211..0a87098ea42 100644
--- a/src/traces/carpet/calc.js
+++ b/src/traces/carpet/calc.js
@@ -33,13 +33,13 @@ module.exports = function calc(gd, trace) {
     if(trace._cheater) {
         var avals = aax.cheatertype === 'index' ? a.length : a;
         var bvals = bax.cheatertype === 'index' ? b.length : b;
-        trace.x = x = cheaterBasis(avals, bvals, trace.cheaterslope);
+        x = cheaterBasis(avals, bvals, trace.cheaterslope);
     } else {
         x = trace.x;
     }
 
-    trace._x = trace.x = x = clean2dArray(x);
-    trace._y = trace.y = y = clean2dArray(y);
+    trace._x = x = clean2dArray(x);
+    trace._y = y = clean2dArray(y);
 
     // Fill in any undefined values with elliptic smoothing. This doesn't take
     // into account the spacing of the values. That is, the derivatives should
diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js
index 66f8456ab37..93c6021e81b 100644
--- a/src/traces/carpet/calc_gridlines.js
+++ b/src/traces/carpet/calc_gridlines.js
@@ -27,10 +27,7 @@ module.exports = function calcGridlines(trace, cd, axisLetter, crossAxisLetter)
     var crossAxis = trace[crossAxisLetter + 'axis'];
 
     if(axis.tickmode === 'array') {
-        axis.tickvals = [];
-        for(i = 0; i < data.length; i++) {
-            axis.tickvals.push(data[i]);
-        }
+        axis.tickvals = data.slice();
     }
 
     var xcp = trace.xctrl;
@@ -42,6 +39,9 @@ module.exports = function calcGridlines(trace, cd, axisLetter, crossAxisLetter)
 
     Axes.calcTicks(axis);
 
+    // don't leave tickvals in axis looking like an attribute
+    if(axis.tickmode === 'array') delete axis.tickvals;
+
     // The default is an empty array that will cause the join to remove the gridline if
     // it's just disappeared:
     // axis._startline = axis._endline = [];
diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js
index fc01f772d4a..f47cb0c52a3 100644
--- a/src/traces/carpet/set_convert.js
+++ b/src/traces/carpet/set_convert.js
@@ -65,8 +65,8 @@ module.exports = function setConvert(trace) {
     bax.c2p = function(v) { return v; };
 
     trace.setScale = function() {
-        var x = trace.x;
-        var y = trace.y;
+        var x = trace._x;
+        var y = trace._y;
 
         // This is potentially a very expensive step! It does the bulk of the work of constructing
         // an expanded basis of control points. Note in particular that it overwrites the existing
diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js
index 0e8d2f200e1..162d8f94e6e 100644
--- a/src/traces/heatmap/convert_column_xyz.js
+++ b/src/traces/heatmap/convert_column_xyz.js
@@ -70,9 +70,13 @@ module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name,
         }
     }
 
+    // hack for Plotly.react - save the input arrays for diffing purposes
+    trace['_input_' + var1Name] = trace[var1Name];
+    trace['_input_' + var2Name] = trace[var2Name];
     trace[var1Name] = col1vals;
     trace[var2Name] = col2vals;
     for(j = 0; j < arrayVarNames.length; j++) {
+        trace['_input_' + arrayVarNames[j]] = trace[arrayVarNames[j]];
         trace[arrayVarNames[j]] = newArrays[j];
     }
     if(hasColumnText) trace.text = text;
diff --git a/src/traces/histogram/bin_defaults.js b/src/traces/histogram/bin_defaults.js
index 97ddf0aabd0..77259579edd 100644
--- a/src/traces/histogram/bin_defaults.js
+++ b/src/traces/histogram/bin_defaults.js
@@ -23,8 +23,9 @@ module.exports = function handleBinDefaults(traceIn, traceOut, coerce, binDirect
         coerce(binDirection + 'bins.start');
         coerce(binDirection + 'bins.end');
         coerce(binDirection + 'bins.size');
-        coerce('autobin' + binDirection);
-        coerce('nbins' + binDirection);
+
+        var autobin = coerce('autobin' + binDirection);
+        if(autobin !== false) coerce('nbins' + binDirection);
     });
 
     return traceOut;
diff --git a/src/traces/histogram/clean_bins.js b/src/traces/histogram/clean_bins.js
index c3cb7e5e3d0..dc322d7401d 100644
--- a/src/traces/histogram/clean_bins.js
+++ b/src/traces/histogram/clean_bins.js
@@ -65,11 +65,14 @@ module.exports = function cleanBins(trace, ax, binDirection) {
     var autoBinAttr = 'autobin' + binDirection;
 
     if(typeof trace[autoBinAttr] !== 'boolean') {
-        trace[autoBinAttr] = !(
+        trace[autoBinAttr] = trace._fullInput[autoBinAttr] = trace._input[autoBinAttr] = !(
             (bins.start || bins.start === 0) &&
             (bins.end || bins.end === 0)
         );
     }
 
-    if(!trace[autoBinAttr]) delete trace['nbins' + binDirection];
+    if(!trace[autoBinAttr]) {
+        delete trace['nbins' + binDirection];
+        delete trace._fullInput['nbins' + binDirection];
+    }
 };
diff --git a/src/traces/ohlc/transform.js b/src/traces/ohlc/transform.js
index d249ca85ba3..c569c6ede3d 100644
--- a/src/traces/ohlc/transform.js
+++ b/src/traces/ohlc/transform.js
@@ -213,6 +213,7 @@ exports.calcTransform = function calcTransform(gd, trace, opts) {
     trace.x = x;
     trace.y = y;
     trace.text = textOut;
+    trace._length = x.length;
 };
 
 function convertTickWidth(gd, xa, trace) {
diff --git a/src/traces/pie/calc.js b/src/traces/pie/calc.js
index 6333373790f..a8f4baaaa34 100644
--- a/src/traces/pie/calc.js
+++ b/src/traces/pie/calc.js
@@ -18,7 +18,7 @@ module.exports = function calc(gd, trace) {
     var vals = trace.values;
     var hasVals = Array.isArray(vals) && vals.length;
     var labels = trace.labels;
-    var colors = trace.marker.colors;
+    var colors = trace.marker.colors || [];
     var cd = [];
     var fullLayout = gd._fullLayout;
     var colorWay = fullLayout.colorway;
diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js
index 5881fde486a..a13e89497cc 100644
--- a/src/traces/pie/defaults.js
+++ b/src/traces/pie/defaults.js
@@ -34,8 +34,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
     var lineWidth = coerce('marker.line.width');
     if(lineWidth) coerce('marker.line.color');
 
-    var colors = coerce('marker.colors');
-    if(!Array.isArray(colors)) traceOut.marker.colors = [];
+    coerce('marker.colors');
 
     coerce('scalegroup');
     // TODO: hole needs to be coerced to the same value within a scaleegroup
diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js
index 49f3105f9e9..c720b0fcf02 100644
--- a/src/traces/scatter/calc.js
+++ b/src/traces/scatter/calc.js
@@ -24,15 +24,12 @@ function calc(gd, trace) {
     var ya = Axes.getFromId(gd, trace.yaxis || 'y');
     var x = xa.makeCalcdata(trace, 'x');
     var y = ya.makeCalcdata(trace, 'y');
-    var serieslen = Math.min(x.length, y.length);
+    var serieslen = trace._length;
 
     // cancel minimum tick spacings (only applies to bars and boxes)
     xa._minDtick = 0;
     ya._minDtick = 0;
 
-    if(x.length > serieslen) x.splice(serieslen, x.length - serieslen);
-    if(y.length > serieslen) y.splice(serieslen, y.length - serieslen);
-
     // check whether bounds should be tight, padded, extended to zero...
     // most cases both should be padded on both ends, so start with that.
     var xOptions = {padded: true};
@@ -120,9 +117,13 @@ function calcMarkerSize(trace, serieslen) {
         Axes.setConvert(ax);
 
         var s = ax.makeCalcdata(trace.marker, 'size');
-        if(s.length > serieslen) s.splice(serieslen, s.length - serieslen);
 
-        return s.map(markerTrans);
+        var sizeOut = new Array(serieslen);
+        for(var i = 0; i < serieslen; i++) {
+            sizeOut[i] = markerTrans(s[i]);
+        }
+        return sizeOut;
+
     } else {
         return markerTrans(marker.size);
     }
diff --git a/src/traces/scatter/xy_defaults.js b/src/traces/scatter/xy_defaults.js
index 8fbce7402ff..27a987af471 100644
--- a/src/traces/scatter/xy_defaults.js
+++ b/src/traces/scatter/xy_defaults.js
@@ -23,13 +23,6 @@ module.exports = function handleXYDefaults(traceIn, traceOut, layout, coerce) {
     if(x) {
         if(y) {
             len = Math.min(x.length, y.length);
-            // TODO: not sure we should do this here... but I think
-            // the way it works in calc is wrong, because it'll delete data
-            // which could be a problem eg in streaming / editing if x and y
-            // come in at different times
-            // so we need to revisit calc before taking this out
-            if(len < x.length) traceOut.x = x.slice(0, len);
-            if(len < y.length) traceOut.y = y.slice(0, len);
         }
         else {
             len = x.length;
@@ -44,5 +37,8 @@ module.exports = function handleXYDefaults(traceIn, traceOut, layout, coerce) {
         coerce('x0');
         coerce('dx');
     }
+
+    traceOut._length = len;
+
     return len;
 };
diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js
index 6f302fd177d..050a2dcfed3 100644
--- a/src/traces/scatter3d/defaults.js
+++ b/src/traces/scatter3d/defaults.js
@@ -78,10 +78,9 @@ function handleXYZDefaults(traceIn, traceOut, coerce, layout) {
     handleCalendarDefaults(traceIn, traceOut, ['x', 'y', 'z'], layout);
 
     if(x && y && z) {
+        // TODO: what happens if one is missing?
         len = Math.min(x.length, y.length, z.length);
-        if(len < x.length) traceOut.x = x.slice(0, len);
-        if(len < y.length) traceOut.y = y.slice(0, len);
-        if(len < z.length) traceOut.z = z.slice(0, len);
+        traceOut._xlength = traceOut._ylength = traceOut._zlength = len;
     }
 
     return len;
diff --git a/src/traces/scattergeo/calc.js b/src/traces/scattergeo/calc.js
index 5a187fe3cdd..af3af25236b 100644
--- a/src/traces/scattergeo/calc.js
+++ b/src/traces/scattergeo/calc.js
@@ -20,7 +20,7 @@ var _ = require('../../lib')._;
 
 module.exports = function calc(gd, trace) {
     var hasLocationData = Array.isArray(trace.locations);
-    var len = hasLocationData ? trace.locations.length : trace.lon.length;
+    var len = hasLocationData ? trace.locations.length : trace._length;
     var calcTrace = new Array(len);
 
     for(var i = 0; i < len; i++) {
diff --git a/src/traces/scattergeo/defaults.js b/src/traces/scattergeo/defaults.js
index 55b54ab8d6d..5e09810f39f 100644
--- a/src/traces/scattergeo/defaults.js
+++ b/src/traces/scattergeo/defaults.js
@@ -69,9 +69,7 @@ function handleLonLatLocDefaults(traceIn, traceOut, coerce) {
     lon = coerce('lon') || [];
     lat = coerce('lat') || [];
     len = Math.min(lon.length, lat.length);
-
-    if(len < lon.length) traceOut.lon = lon.slice(0, len);
-    if(len < lat.length) traceOut.lat = lat.slice(0, len);
+    traceOut._length = len;
 
     return len;
 }
diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js
index 4692c377ba6..71e36b627b2 100644
--- a/src/traces/scattergl/index.js
+++ b/src/traces/scattergl/index.js
@@ -53,7 +53,7 @@ function calc(container, trace) {
     var x = xaxis.type === 'linear' ? trace.x : xaxis.makeCalcdata(trace, 'x');
     var y = yaxis.type === 'linear' ? trace.y : yaxis.makeCalcdata(trace, 'y');
 
-    var count = (x || y).length, i, l, xx, yy;
+    var count = trace._length, i, xx, yy;
 
     if(!x) {
         x = Array(count);
@@ -69,38 +69,19 @@ function calc(container, trace) {
     }
 
     // get log converted positions
-    var rawx, rawy;
-    if(xaxis.type === 'log') {
-        rawx = Array(x.length);
-        for(i = 0, l = x.length; i < l; i++) {
-            rawx[i] = x[i];
-            x[i] = xaxis.d2l(x[i]);
-        }
-    }
-    else {
-        rawx = x;
-        for(i = 0, l = x.length; i < l; i++) {
-            x[i] = parseFloat(x[i]);
-        }
-    }
-    if(yaxis.type === 'log') {
-        rawy = Array(y.length);
-        for(i = 0, l = y.length; i < l; i++) {
-            rawy[i] = y[i];
-            y[i] = yaxis.d2l(y[i]);
-        }
-    }
-    else {
-        rawy = y;
-        for(i = 0, l = y.length; i < l; i++) {
-            y[i] = parseFloat(y[i]);
-        }
-    }
+    var rawx = (xaxis.type === 'log' || x.length > count) ? x.slice(0, count) : x;
+    var rawy = (yaxis.type === 'log' || y.length > count) ? y.slice(0, count) : y;
+
+    var convertX = (xaxis.type === 'log') ? xaxis.d2l : parseFloat;
+    var convertY = (yaxis.type === 'log') ? yaxis.d2l : parseFloat;
 
     // we need hi-precision for scatter2d
     positions = new Array(count * 2);
 
     for(i = 0; i < count; i++) {
+        x[i] = convertX(x[i]);
+        y[i] = convertY(y[i]);
+
         // if no x defined, we are creating simple int sequence (API)
         // we use parseFloat because it gives NaN (we need that for empty values to avoid drawing lines) and it is incredibly fast
         xx = isNumeric(x[i]) ? +x[i] : NaN;
diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js
index 3d8bb73348a..210f9e1d47b 100644
--- a/src/traces/scattermapbox/defaults.js
+++ b/src/traces/scattermapbox/defaults.js
@@ -69,9 +69,7 @@ function handleLonLatDefaults(traceIn, traceOut, coerce) {
     var lon = coerce('lon') || [];
     var lat = coerce('lat') || [];
     var len = Math.min(lon.length, lat.length);
-
-    if(len < lon.length) traceOut.lon = lon.slice(0, len);
-    if(len < lat.length) traceOut.lat = lat.slice(0, len);
+    traceOut._length = len;
 
     return len;
 }
diff --git a/src/traces/scatterpolar/calc.js b/src/traces/scatterpolar/calc.js
index 18a7f3b12bc..4c600509cff 100644
--- a/src/traces/scatterpolar/calc.js
+++ b/src/traces/scatterpolar/calc.js
@@ -26,7 +26,7 @@ module.exports = function calc(gd, trace) {
     var angularAxis = fullLayout[subplotId].angularaxis;
     var rArray = radialAxis.makeCalcdata(trace, 'r');
     var thetaArray = angularAxis.makeCalcdata(trace, 'theta');
-    var len = rArray.length;
+    var len = trace._length;
     var cd = new Array(len);
 
     function c2rad(v) {
@@ -53,6 +53,7 @@ module.exports = function calc(gd, trace) {
     if(angularAxis.type !== 'linear') {
         angularAxis.autorange = true;
         Axes.expand(angularAxis, thetaArray);
+        delete angularAxis.autorange;
     }
 
     calcColorscale(trace);
diff --git a/src/traces/scatterpolar/defaults.js b/src/traces/scatterpolar/defaults.js
index 294553ecb6e..5fe19016b48 100644
--- a/src/traces/scatterpolar/defaults.js
+++ b/src/traces/scatterpolar/defaults.js
@@ -34,8 +34,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
         return;
     }
 
-    if(len < r.length) traceOut.r = r.slice(0, len);
-    if(len < theta.length) traceOut.theta = theta.slice(0, len);
+    traceOut._length = len;
 
     coerce('thetaunit');
     coerce('mode', len < PTS_LINESONLY ? 'lines+markers' : 'lines');
diff --git a/src/traces/scatterpolargl/defaults.js b/src/traces/scatterpolargl/defaults.js
index f34e1e5bce4..1c6a5cae2c3 100644
--- a/src/traces/scatterpolargl/defaults.js
+++ b/src/traces/scatterpolargl/defaults.js
@@ -32,8 +32,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
         return;
     }
 
-    if(len < r.length) traceOut.r = r.slice(0, len);
-    if(len < theta.length) traceOut.theta = theta.slice(0, len);
+    traceOut._length = len;
 
     coerce('thetaunit');
     coerce('mode', len < PTS_LINESONLY ? 'lines+markers' : 'lines');
diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js
index 6a7a4a1b94a..8bd78e52393 100644
--- a/src/traces/scatterpolargl/index.js
+++ b/src/traces/scatterpolargl/index.js
@@ -26,6 +26,9 @@ function calc(container, trace) {
     var thetaArray = angularAxis.makeCalcdata(trace, 'theta');
     var stash = {};
 
+    if(trace._length < rArray.length) rArray = rArray.slice(0, trace._length);
+    if(trace._length < thetaArray.length) thetaArray = thetaArray.slice(0, trace._length);
+
     calcColorscales(trace);
 
     stash.r = rArray;
@@ -36,6 +39,7 @@ function calc(container, trace) {
     if(angularAxis.type !== 'linear') {
         angularAxis.autorange = true;
         Axes.expand(angularAxis, thetaArray);
+        delete angularAxis.autorange;
     }
 
     return [{x: false, y: false, t: stash, trace: trace}];
diff --git a/src/traces/scatterternary/calc.js b/src/traces/scatterternary/calc.js
index f7bcc43cbef..e7a4be1b52e 100644
--- a/src/traces/scatterternary/calc.js
+++ b/src/traces/scatterternary/calc.js
@@ -20,34 +20,35 @@ var dataArrays = ['a', 'b', 'c'];
 var arraysToFill = {a: ['b', 'c'], b: ['a', 'c'], c: ['a', 'b']};
 
 module.exports = function calc(gd, trace) {
-    var ternary = gd._fullLayout[trace.subplot],
-        displaySum = ternary.sum,
-        normSum = trace.sum || displaySum;
+    var ternary = gd._fullLayout[trace.subplot];
+    var displaySum = ternary.sum;
+    var normSum = trace.sum || displaySum;
+    var arrays = {a: trace.a, b: trace.b, c: trace.c};
 
     var i, j, dataArray, newArray, fillArray1, fillArray2;
 
     // fill in one missing component
     for(i = 0; i < dataArrays.length; i++) {
         dataArray = dataArrays[i];
-        if(trace[dataArray]) continue;
+        if(arrays[dataArray]) continue;
 
-        fillArray1 = trace[arraysToFill[dataArray][0]];
-        fillArray2 = trace[arraysToFill[dataArray][1]];
+        fillArray1 = arrays[arraysToFill[dataArray][0]];
+        fillArray2 = arrays[arraysToFill[dataArray][1]];
         newArray = new Array(fillArray1.length);
         for(j = 0; j < fillArray1.length; j++) {
             newArray[j] = normSum - fillArray1[j] - fillArray2[j];
         }
-        trace[dataArray] = newArray;
+        arrays[dataArray] = newArray;
     }
 
     // make the calcdata array
-    var serieslen = trace.a.length;
+    var serieslen = trace._length;
     var cd = new Array(serieslen);
     var a, b, c, norm, x, y;
     for(i = 0; i < serieslen; i++) {
-        a = trace.a[i];
-        b = trace.b[i];
-        c = trace.c[i];
+        a = arrays.a[i];
+        b = arrays.b[i];
+        c = arrays.c[i];
         if(isNumeric(a) && isNumeric(b) && isNumeric(c)) {
             a = +a;
             b = +b;
diff --git a/src/traces/scatterternary/defaults.js b/src/traces/scatterternary/defaults.js
index cb982c46a03..a648bef144b 100644
--- a/src/traces/scatterternary/defaults.js
+++ b/src/traces/scatterternary/defaults.js
@@ -55,10 +55,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
         return;
     }
 
-    // cut all data arrays down to same length
-    if(a && len < a.length) traceOut.a = a.slice(0, len);
-    if(b && len < b.length) traceOut.b = b.slice(0, len);
-    if(c && len < c.length) traceOut.c = c.slice(0, len);
+    traceOut._length = len;
 
     coerce('sum');
 
diff --git a/src/traces/surface/convert.js b/src/traces/surface/convert.js
index 1881235686e..3c1650b3d08 100644
--- a/src/traces/surface/convert.js
+++ b/src/traces/surface/convert.js
@@ -45,12 +45,17 @@ proto.handlePick = function(selection) {
         ];
         var traceCoordinate = [0, 0, 0];
 
-        if(Array.isArray(this.data.x[0])) {
+        if(!Array.isArray(this.data.x)) {
+            traceCoordinate[0] = selectIndex[0];
+        } else if(Array.isArray(this.data.x[0])) {
             traceCoordinate[0] = this.data.x[selectIndex[1]][selectIndex[0]];
         } else {
             traceCoordinate[0] = this.data.x[selectIndex[0]];
         }
-        if(Array.isArray(this.data.y[0])) {
+
+        if(!Array.isArray(this.data.y)) {
+            traceCoordinate[1] = selectIndex[1];
+        } else if(Array.isArray(this.data.y[0])) {
             traceCoordinate[1] = this.data.y[selectIndex[1]][selectIndex[0]];
         } else {
             traceCoordinate[1] = this.data.y[selectIndex[1]];
@@ -196,7 +201,7 @@ proto.update = function(data) {
         zaxis = sceneLayout.zaxis,
         scaleFactor = scene.dataScale,
         xlen = z[0].length,
-        ylen = z.length,
+        ylen = data._ylength,
         coords = [
             ndarray(new Float32Array(xlen * ylen), [xlen, ylen]),
             ndarray(new Float32Array(xlen * ylen), [xlen, ylen]),
@@ -226,7 +231,11 @@ proto.update = function(data) {
     });
 
     // coords x
-    if(Array.isArray(x[0])) {
+    if(!Array.isArray(x)) {
+        fill(xc, function(row) {
+            return xaxis.d2l(row, 0, xcalendar) * scaleFactor[0];
+        });
+    } else if(Array.isArray(x[0])) {
         fill(xc, function(row, col) {
             return xaxis.d2l(x[col][row], 0, xcalendar) * scaleFactor[0];
         });
@@ -238,7 +247,11 @@ proto.update = function(data) {
     }
 
     // coords y
-    if(Array.isArray(y[0])) {
+    if(!Array.isArray(x)) {
+        fill(yc, function(row, col) {
+            return yaxis.d2l(col, 0, xcalendar) * scaleFactor[1];
+        });
+    } else if(Array.isArray(y[0])) {
         fill(yc, function(row, col) {
             return yaxis.d2l(y[col][row], 0, ycalendar) * scaleFactor[1];
         });
diff --git a/src/traces/surface/defaults.js b/src/traces/surface/defaults.js
index 434349de623..e0a41136038 100644
--- a/src/traces/surface/defaults.js
+++ b/src/traces/surface/defaults.js
@@ -29,30 +29,16 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
         return;
     }
 
-    var xlen = z[0].length;
-    var ylen = z.length;
-
-    coerce('x');
+    var x = coerce('x');
     coerce('y');
 
+    traceOut._xlength = (Array.isArray(x) && Array.isArray(x[0])) ? z.length : z[0].length;
+    traceOut._ylength = z.length;
+
     var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults');
     handleCalendarDefaults(traceIn, traceOut, ['x', 'y', 'z'], layout);
 
-    if(!Array.isArray(traceOut.x)) {
-        // build a linearly scaled x
-        traceOut.x = [];
-        for(i = 0; i < xlen; ++i) {
-            traceOut.x[i] = i;
-        }
-    }
-
     coerce('text');
-    if(!Array.isArray(traceOut.y)) {
-        traceOut.y = [];
-        for(i = 0; i < ylen; ++i) {
-            traceOut.y[i] = i;
-        }
-    }
 
     // Coerce remaining properties
     [
diff --git a/src/transforms/filter.js b/src/transforms/filter.js
index 5f9350590b5..3b5533b4e82 100644
--- a/src/transforms/filter.js
+++ b/src/transforms/filter.js
@@ -219,6 +219,7 @@ exports.calcTransform = function(gd, trace, opts) {
     }
 
     opts._indexToPoints = indexToPoints;
+    trace._length = index;
 };
 
 function getFilterFunc(opts, d2c, targetCalendar) {
diff --git a/test/image/baselines/range_slider_multiple.png b/test/image/baselines/range_slider_multiple.png
index 14740cbb86c..23af1953709 100644
Binary files a/test/image/baselines/range_slider_multiple.png and b/test/image/baselines/range_slider_multiple.png differ
diff --git a/test/image/mocks/geo_first.json b/test/image/mocks/geo_first.json
index fb2c2ac4e03..0cb9151c3e7 100644
--- a/test/image/mocks/geo_first.json
+++ b/test/image/mocks/geo_first.json
@@ -9,7 +9,8 @@
         {
             "type": "choropleth",
             "locations": ["USA", "CAN", "RUS"],
-            "z": [0, 5, 10]
+            "z": [0, 5, 10],
+            "autocolorscale": true
         }
     ],
     "layout": {
diff --git a/test/image/mocks/range_slider_multiple.json b/test/image/mocks/range_slider_multiple.json
index 4885806a707..09f7490bd17 100644
--- a/test/image/mocks/range_slider_multiple.json
+++ b/test/image/mocks/range_slider_multiple.json
@@ -26,7 +26,7 @@
       "anchor": "y2",
       "domain": [ 0.55, 1 ],
       "rangeslider": {
-        "range": [ -1, 4 ]
+        "range": [ -2, 4 ]
       }
     },
     "yaxis": {
diff --git a/test/jasmine/tests/legend_scroll_test.js b/test/jasmine/tests/legend_scroll_test.js
index 64087fd14a1..89d7ed77f9a 100644
--- a/test/jasmine/tests/legend_scroll_test.js
+++ b/test/jasmine/tests/legend_scroll_test.js
@@ -79,7 +79,7 @@ describe('The legend', function() {
             var legend = getLegend(),
                 scrollBox = getScrollBox(),
                 legendHeight = getLegendHeight(gd),
-                scrollBoxYMax = gd._fullLayout.legend.height - legendHeight,
+                scrollBoxYMax = gd._fullLayout.legend._height - legendHeight,
                 scrollBarYMax = legendHeight -
                     constants.scrollBarHeight -
                     2 * constants.scrollBarMargin,
diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js
index c9700131069..c91bb568de0 100644
--- a/test/jasmine/tests/lib_test.js
+++ b/test/jasmine/tests/lib_test.js
@@ -1292,21 +1292,21 @@ describe('Test lib.js:', function() {
             expect(this.array).toBe(out);
         });
 
-        it('should ignore falsy items', function() {
+        it('should ignore falsy items except 0', function() {
             Lib.pushUnique(this.array, false);
             expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]);
 
             Lib.pushUnique(this.array, undefined);
             expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]);
 
-            Lib.pushUnique(this.array, 0);
-            expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]);
-
             Lib.pushUnique(this.array, null);
             expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]);
 
             Lib.pushUnique(this.array, '');
             expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]);
+
+            Lib.pushUnique(this.array, 0);
+            expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }, 0]);
         });
 
         it('should ignore item already in array', function() {
diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js
index 6974f8ff63a..b783f3d89ad 100644
--- a/test/jasmine/tests/plot_api_test.js
+++ b/test/jasmine/tests/plot_api_test.js
@@ -10,6 +10,8 @@ var pkg = require('../../../package.json');
 var subroutines = require('@src/plot_api/subroutines');
 var helpers = require('@src/plot_api/helpers');
 var editTypes = require('@src/plot_api/edit_types');
+var annotations = require('@src/components/annotations');
+var images = require('@src/components/images');
 
 var d3 = require('d3');
 var createGraphDiv = require('../assets/create_graph_div');
@@ -18,7 +20,6 @@ var fail = require('../assets/fail_test');
 var checkTicks = require('../assets/custom_assertions').checkTicks;
 var supplyAllDefaults = require('../assets/supply_defaults');
 
-
 describe('Test plot api', function() {
     'use strict';
 
@@ -2171,6 +2172,449 @@ describe('Test plot api', function() {
             });
         });
     });
+
+    describe('Plotly.react', function() {
+        var mockedMethods = [
+            'doTraceStyle',
+            'doColorBars',
+            'doLegend',
+            'layoutStyles',
+            'doTicksRelayout',
+            'doModeBar',
+            'doCamera'
+        ];
+
+        var gd;
+        var plotCalls;
+
+        beforeEach(function() {
+            gd = createGraphDiv();
+
+            mockedMethods.forEach(function(m) {
+                spyOn(subroutines, m).and.callThrough();
+                subroutines[m].calls.reset();
+            });
+
+            spyOn(annotations, 'drawOne').and.callThrough();
+            spyOn(annotations, 'draw').and.callThrough();
+            spyOn(images, 'draw').and.callThrough();
+        });
+
+        afterEach(destroyGraphDiv);
+
+        function countPlots() {
+            plotCalls = 0;
+
+            gd.on('plotly_afterplot', function() { plotCalls++; });
+            subroutines.layoutStyles.calls.reset();
+            annotations.draw.calls.reset();
+            annotations.drawOne.calls.reset();
+            images.draw.calls.reset();
+        }
+
+        function countCalls(counts) {
+            var callsFinal = Lib.extendFlat({}, counts);
+            // TODO: do we really need to do layoutStyles twice in Plotly.plot?
+            // We get one from drawFramework and another directly from Plotly.plot.
+            callsFinal.layoutStyles = (counts.layoutStyles || 0) + 2 * (counts.plot || 0);
+
+            mockedMethods.forEach(function(m) {
+                expect(subroutines[m]).toHaveBeenCalledTimes(callsFinal[m] || 0);
+                subroutines[m].calls.reset();
+            });
+
+            expect(plotCalls).toBe(counts.plot || 0, 'calls to Plotly.plot');
+            plotCalls = 0;
+
+            // only consider annotation and image draw calls if we *don't* do a full plot.
+            if(!counts.plot) {
+                expect(annotations.draw).toHaveBeenCalledTimes(counts.annotationDraw || 0);
+                expect(annotations.drawOne).toHaveBeenCalledTimes(counts.annotationDrawOne || 0);
+                expect(images.draw).toHaveBeenCalledTimes(counts.imageDraw || 0);
+            }
+            annotations.draw.calls.reset();
+            annotations.drawOne.calls.reset();
+            images.draw.calls.reset();
+        }
+
+        it('should notice new data by ===, without layout.datarevision', function(done) {
+            var data = [{y: [1, 2, 3], mode: 'markers'}];
+            var layout = {};
+
+            Plotly.newPlot(gd, data, layout)
+            .then(countPlots)
+            .then(function() {
+                expect(d3.selectAll('.point').size()).toBe(3);
+
+                data[0].y.push(4);
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                // didn't pick it up, as we modified in place!!!
+                expect(d3.selectAll('.point').size()).toBe(3);
+
+                data[0].y = [1, 2, 3, 4, 5];
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                // new object, we picked it up!
+                expect(d3.selectAll('.point').size()).toBe(5);
+
+                countCalls({plot: 1});
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('should notice new layout.datarevision', function(done) {
+            var data = [{y: [1, 2, 3], mode: 'markers'}];
+            var layout = {datarevision: 1};
+
+            Plotly.newPlot(gd, data, layout)
+            .then(countPlots)
+            .then(function() {
+                expect(d3.selectAll('.point').size()).toBe(3);
+
+                data[0].y.push(4);
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                // didn't pick it up, as we didn't modify datarevision
+                expect(d3.selectAll('.point').size()).toBe(3);
+
+                data[0].y.push(5);
+                layout.datarevision = 'bananas';
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                // new revision, we picked it up!
+                expect(d3.selectAll('.point').size()).toBe(5);
+
+                countCalls({plot: 1});
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('picks up partial redraws', function(done) {
+            var data = [{y: [1, 2, 3], mode: 'markers'}];
+            var layout = {};
+
+            Plotly.newPlot(gd, data, layout)
+            .then(countPlots)
+            .then(function() {
+                layout.title = 'XXXXX';
+                layout.hovermode = 'closest';
+                data[0].marker = {color: 'rgb(0, 100, 200)'};
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                countCalls({layoutStyles: 1, doTraceStyle: 1, doModeBar: 1});
+                expect(d3.select('.gtitle').text()).toBe('XXXXX');
+                var points = d3.selectAll('.point');
+                expect(points.size()).toBe(3);
+                points.each(function() {
+                    expect(window.getComputedStyle(this).fill).toBe('rgb(0, 100, 200)');
+                });
+
+                layout.showlegend = true;
+                layout.xaxis.tick0 = 0.1;
+                layout.xaxis.dtick = 0.3;
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                // legend and ticks get called initially, but then plot gets added during automargin
+                countCalls({doLegend: 1, doTicksRelayout: 1, plot: 1});
+
+                data = [{z: [[1, 2], [3, 4]], type: 'surface'}];
+                layout = {};
+
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                // we get an extra call to layoutStyles from marginPushersAgain due to the colorbar.
+                // Really need to simplify that pipeline...
+                countCalls({plot: 1, layoutStyles: 1});
+
+                layout.scene.camera = {up: {x: 1, y: -1, z: 0}};
+
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                countCalls({doCamera: 1});
+
+                data[0].type = 'heatmap';
+                delete layout.scene;
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                countCalls({plot: 1});
+
+                // ideally we'd just do this with `surface` but colorbar attrs have editType 'calc' there
+                // TODO: can we drop them to type: 'colorbars' even for the 3D types?
+                data[0].colorbar = {len: 0.6};
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                countCalls({doColorBars: 1, plot: 1});
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('redraws annotations one at a time', function(done) {
+            var data = [{y: [1, 2, 3], mode: 'markers'}];
+            var layout = {};
+            var ymax;
+
+            Plotly.newPlot(gd, data, layout)
+            .then(countPlots)
+            .then(function() {
+                ymax = layout.yaxis.range[1];
+
+                layout.annotations = [{
+                    x: 1,
+                    y: 4,
+                    text: 'Way up high',
+                    showarrow: false
+                }, {
+                    x: 1,
+                    y: 2,
+                    text: 'On the data',
+                    showarrow: false
+                }];
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                // autoranged - so we get a full replot
+                countCalls({plot: 1});
+                expect(d3.selectAll('.annotation').size()).toBe(2);
+
+                layout.annotations[1].bgcolor = 'rgb(200, 100, 0)';
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                countCalls({annotationDrawOne: 1});
+                expect(window.getComputedStyle(d3.select('.annotation[data-index="1"] .bg').node()).fill)
+                    .toBe('rgb(200, 100, 0)');
+                expect(layout.yaxis.range[1]).not.toBeCloseTo(ymax, 0);
+
+                layout.annotations[0].font = {color: 'rgb(0, 255, 0)'};
+                layout.annotations[1].bgcolor = 'rgb(0, 0, 255)';
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                countCalls({annotationDrawOne: 2});
+                expect(window.getComputedStyle(d3.select('.annotation[data-index="0"] text').node()).fill)
+                    .toBe('rgb(0, 255, 0)');
+                expect(window.getComputedStyle(d3.select('.annotation[data-index="1"] .bg').node()).fill)
+                    .toBe('rgb(0, 0, 255)');
+
+                Lib.extendFlat(layout.annotations[0], {yref: 'paper', y: 0.8});
+
+                return Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                countCalls({plot: 1});
+                expect(layout.yaxis.range[1]).toBeCloseTo(ymax, 0);
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('redraws images all at once', function(done) {
+            var data = [{y: [1, 2, 3], mode: 'markers'}];
+            var layout = {};
+            var jsLogo = 'https://images.plot.ly/language-icons/api-home/js-logo.png';
+
+            var x, y, height, width;
+
+            Plotly.newPlot(gd, data, layout)
+            .then(countPlots)
+            .then(function() {
+                layout.images = [{
+                    source: jsLogo,
+                    xref: 'paper',
+                    yref: 'paper',
+                    x: 0.1,
+                    y: 0.1,
+                    sizex: 0.2,
+                    sizey: 0.2
+                }, {
+                    source: jsLogo,
+                    xref: 'x',
+                    yref: 'y',
+                    x: 1,
+                    y: 2,
+                    sizex: 1,
+                    sizey: 1
+                }];
+                Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                countCalls({imageDraw: 1});
+                expect(d3.selectAll('image').size()).toBe(2);
+
+                var n = d3.selectAll('image').node();
+                x = n.attributes.x.value;
+                y = n.attributes.y.value;
+                height = n.attributes.height.value;
+                width = n.attributes.width.value;
+
+                layout.images[0].y = 0.8;
+                layout.images[0].sizey = 0.4;
+                Plotly.react(gd, data, layout);
+            })
+            .then(function() {
+                countCalls({imageDraw: 1});
+                var n = d3.selectAll('image').node();
+                expect(n.attributes.x.value).toBe(x);
+                expect(n.attributes.width.value).toBe(width);
+                expect(n.attributes.y.value).not.toBe(y);
+                expect(n.attributes.height.value).not.toBe(height);
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('can change config, and always redraws', function(done) {
+            var data = [{y: [1, 2, 3]}];
+            var layout = {};
+
+            Plotly.newPlot(gd, data, layout)
+            .then(countPlots)
+            .then(function() {
+                expect(d3.selectAll('.drag').size()).toBe(11);
+                expect(d3.selectAll('.gtitle').size()).toBe(0);
+
+                return Plotly.react(gd, data, layout, {editable: true});
+            })
+            .then(function() {
+                expect(d3.selectAll('.drag').size()).toBe(11);
+                expect(d3.selectAll('.gtitle').text()).toBe('Click to enter Plot title');
+                countCalls({plot: 1});
+
+                return Plotly.react(gd, data, layout, {staticPlot: true});
+            })
+            .then(function() {
+                expect(d3.selectAll('.drag').size()).toBe(0);
+                expect(d3.selectAll('.gtitle').size()).toBe(0);
+                countCalls({plot: 1});
+
+                return Plotly.react(gd, data, layout, {});
+            })
+            .then(function() {
+                expect(d3.selectAll('.drag').size()).toBe(11);
+                expect(d3.selectAll('.gtitle').size()).toBe(0);
+                countCalls({plot: 1});
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('can put polar plots into staticPlot mode', function(done) {
+            // tested separately since some of the relevant code is actually
+            // in cartesian/graph_interact... hopefully we'll fix that
+            // sometime and the test will still pass.
+            var data = [{r: [1, 2, 3], theta: [0, 120, 240], type: 'scatterpolar'}];
+            var layout = {};
+
+            Plotly.newPlot(gd, data, layout)
+            .then(countPlots)
+            .then(function() {
+                expect(d3.select(gd).selectAll('.drag').size()).toBe(3);
+
+                return Plotly.react(gd, data, layout, {staticPlot: true});
+            })
+            .then(function() {
+                expect(d3.select(gd).selectAll('.drag').size()).toBe(0);
+
+                return Plotly.react(gd, data, layout, {});
+            })
+            .then(function() {
+                expect(d3.select(gd).selectAll('.drag').size()).toBe(3);
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        it('can change frames without redrawing', function(done) {
+            var data = [{y: [1, 2, 3]}];
+            var layout = {};
+            var frames = [{name: 'frame1'}];
+
+            Plotly.newPlot(gd, {data: data, layout: layout, frames: frames})
+            .then(countPlots)
+            .then(function() {
+                var frameData = gd._transitionData._frames;
+                expect(frameData.length).toBe(1);
+                expect(frameData[0].name).toBe('frame1');
+
+                frames[0].name = 'frame2';
+                return Plotly.react(gd, {data: data, layout: layout, frames: frames});
+            })
+            .then(function() {
+                countCalls({});
+                var frameData = gd._transitionData._frames;
+                expect(frameData.length).toBe(1);
+                expect(frameData[0].name).toBe('frame2');
+            })
+            .catch(fail)
+            .then(done);
+        });
+
+        var mockList = [
+            ['1', require('@mocks/1.json')],
+            ['4', require('@mocks/4.json')],
+            ['5', require('@mocks/5.json')],
+            ['10', require('@mocks/10.json')],
+            ['11', require('@mocks/11.json')],
+            ['17', require('@mocks/17.json')],
+            ['21', require('@mocks/21.json')],
+            ['22', require('@mocks/22.json')],
+            ['airfoil', require('@mocks/airfoil.json')],
+            ['annotations-autorange', require('@mocks/annotations-autorange.json')],
+            ['axes_enumerated_ticks', require('@mocks/axes_enumerated_ticks.json')],
+            ['axes_visible-false', require('@mocks/axes_visible-false.json')],
+            ['bar_and_histogram', require('@mocks/bar_and_histogram.json')],
+            ['binding', require('@mocks/binding.json')],
+            ['cheater_smooth', require('@mocks/cheater_smooth.json')],
+            ['finance_style', require('@mocks/finance_style.json')],
+            ['geo_first', require('@mocks/geo_first.json')],
+            ['gl2d_line_dash', require('@mocks/gl2d_line_dash.json')],
+            ['gl2d_parcoords_2', require('@mocks/gl2d_parcoords_2.json')],
+            ['gl2d_pointcloud-basic', require('@mocks/gl2d_pointcloud-basic.json')],
+            ['gl3d_world-cals', require('@mocks/gl3d_world-cals.json')],
+            ['gl3d_set-ranges', require('@mocks/gl3d_set-ranges.json')],
+            ['glpolar_style', require('@mocks/glpolar_style.json')],
+            ['layout_image', require('@mocks/layout_image.json')],
+            ['layout-colorway', require('@mocks/layout-colorway.json')],
+            ['polar_categories', require('@mocks/polar_categories.json')],
+            ['polar_direction', require('@mocks/polar_direction.json')],
+            ['range_selector_style', require('@mocks/range_selector_style.json')],
+            ['range_slider_multiple', require('@mocks/range_slider_multiple.json')],
+            ['sankey_energy', require('@mocks/sankey_energy.json')],
+            ['table_wrapped_birds', require('@mocks/table_wrapped_birds.json')],
+            ['ternary_fill', require('@mocks/ternary_fill.json')],
+            ['text_chart_arrays', require('@mocks/text_chart_arrays.json')],
+            ['updatemenus', require('@mocks/updatemenus.json')],
+            ['violins', require('@mocks/violins.json')],
+            ['world-cals', require('@mocks/world-cals.json')]
+        ];
+
+        mockList.forEach(function(mockSpec) {
+            it('can redraw "' + mockSpec[0] + '" with no changes as a noop', function(done) {
+                var mock = mockSpec[1];
+
+                Plotly.newPlot(gd, mock)
+                .then(countPlots)
+                .then(function() { return Plotly.react(gd, mock); })
+                .then(function() { countCalls({}); })
+                .catch(fail)
+                .then(done);
+            });
+        });
+    });
 });
 
 describe('plot_api helpers', function() {
diff --git a/test/jasmine/tests/range_selector_test.js b/test/jasmine/tests/range_selector_test.js
index 9070d9c0eca..3a29cabac23 100644
--- a/test/jasmine/tests/range_selector_test.js
+++ b/test/jasmine/tests/range_selector_test.js
@@ -472,7 +472,7 @@ describe('range selector interactions:', function() {
 
     function checkActiveButton(activeIndex, msg) {
         d3.selectAll('.button').each(function(d, i) {
-            expect(d.isActive).toBe(activeIndex === i, msg + ': button #' + i);
+            expect(d._isActive).toBe(activeIndex === i, msg + ': button #' + i);
         });
     }
 
@@ -481,7 +481,7 @@ describe('range selector interactions:', function() {
             var rect = d3.select(this).select('rect');
 
             expect(rect.node().style.fill).toEqual(
-                d.isActive ? activeColor : bgColor
+                d._isActive ? activeColor : bgColor
             );
         });
     }
diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js
index 94ca4160da6..d88987fde43 100644
--- a/test/jasmine/tests/range_slider_test.js
+++ b/test/jasmine/tests/range_slider_test.js
@@ -445,7 +445,7 @@ describe('the range slider', function() {
         it('should not mutate layoutIn', function() {
             var layoutIn = { xaxis: { rangeslider: { visible: true }} },
                 layoutOut = { xaxis: { rangeslider: {}} },
-                expected = { xaxis: { rangeslider: { visible: true }} };
+                expected = { xaxis: { rangeslider: { visible: true}} };
 
             _supply(layoutIn, layoutOut, 'xaxis');
             expect(layoutIn).toEqual(expected);
@@ -457,7 +457,6 @@ describe('the range slider', function() {
                 expected = {
                     visible: true,
                     autorange: true,
-                    range: [-1, 6],
                     thickness: 0.15,
                     bgcolor: '#fff',
                     borderwidth: 0,
@@ -475,7 +474,6 @@ describe('the range slider', function() {
                 expected = {
                     visible: true,
                     autorange: true,
-                    range: [-1, 6],
                     thickness: 0.15,
                     bgcolor: '#fff',
                     borderwidth: 0,
@@ -507,7 +505,6 @@ describe('the range slider', function() {
                 expected = {
                     visible: true,
                     autorange: true,
-                    range: [-1, 6],
                     thickness: 0.15,
                     bgcolor: '#fff',
                     borderwidth: 0,
@@ -519,34 +516,12 @@ describe('the range slider', function() {
             expect(layoutOut.xaxis.rangeslider).toEqual(expected);
         });
 
-        it('should expand the rangeslider range to axis range', function() {
-            var layoutIn = { xaxis: { rangeslider: { range: [5, 6] } } },
-                layoutOut = { xaxis: { range: [1, 10], type: 'linear'} },
-                expected = {
-                    visible: true,
-                    autorange: false,
-                    range: [1, 10],
-                    thickness: 0.15,
-                    bgcolor: '#fff',
-                    borderwidth: 0,
-                    bordercolor: '#444',
-                    _input: layoutIn.xaxis.rangeslider
-                };
-
-            _supply(layoutIn, layoutOut, 'xaxis');
-
-            // don't compare the whole layout, because we had to run setConvert which
-            // attaches all sorts of other stuff to xaxis
-            expect(layoutOut.xaxis.rangeslider).toEqual(expected);
-        });
-
         it('should set autorange to true when range input is invalid', function() {
             var layoutIn = { xaxis: { rangeslider: { range: 'not-gonna-work'}} },
                 layoutOut = { xaxis: {} },
                 expected = {
                     visible: true,
                     autorange: true,
-                    range: [-1, 6],
                     thickness: 0.15,
                     bgcolor: '#fff',
                     borderwidth: 0,
@@ -729,6 +704,17 @@ describe('the range slider', function() {
             .then(function() {
                 assertRange([-0.26, 4.26], [-0.26, 4.26]);
 
+                // smaller than xaxis.range - won't be accepted
+                return Plotly.relayout(gd, {'xaxis.rangeslider.range': [0, 2]});
+            })
+            .then(function() {
+                assertRange([-0.26, 4.26], [-0.26, 4.26]);
+
+                // will be accepted (and autorange is disabled by impliedEdits)
+                return Plotly.relayout(gd, {'xaxis.rangeslider.range': [-2, 12]});
+            })
+            .then(function() {
+                assertRange([-0.26, 4.26], [-2, 12]);
             })
             .then(done);
         });
diff --git a/test/jasmine/tests/scattergeo_test.js b/test/jasmine/tests/scattergeo_test.js
index 1c9e107e020..b7368d1aa8c 100644
--- a/test/jasmine/tests/scattergeo_test.js
+++ b/test/jasmine/tests/scattergeo_test.js
@@ -26,18 +26,21 @@ describe('Test scattergeo defaults', function() {
         traceOut = {};
     });
 
-    it('should slice lat if it it longer than lon', function() {
+    it('should not slice lat if it it longer than lon', function() {
+        // this is handled at the calc step now via _length.
         traceIn = {
             lon: [-75],
             lat: [45, 45, 45]
         };
 
         ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
-        expect(traceOut.lat).toEqual([45]);
+        expect(traceOut.lat).toEqual([45, 45, 45]);
         expect(traceOut.lon).toEqual([-75]);
+        expect(traceOut._length).toBe(1);
     });
 
     it('should slice lon if it it longer than lat', function() {
+        // this is handled at the calc step now via _length.
         traceIn = {
             lon: [-75, -75, -75],
             lat: [45]
@@ -45,7 +48,8 @@ describe('Test scattergeo defaults', function() {
 
         ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout);
         expect(traceOut.lat).toEqual([45]);
-        expect(traceOut.lon).toEqual([-75]);
+        expect(traceOut.lon).toEqual([-75, -75, -75]);
+        expect(traceOut._length).toBe(1);
     });
 
     it('should not coerce lat and lon if locations is valid', function() {
diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js
index ccc6dbef587..bb4874239fd 100644
--- a/test/jasmine/tests/scattermapbox_test.js
+++ b/test/jasmine/tests/scattermapbox_test.js
@@ -38,24 +38,28 @@ describe('scattermapbox defaults', function() {
         return traceOut;
     }
 
-    it('should truncate \'lon\' if longer than \'lat\'', function() {
+    it('should not truncate \'lon\' if longer than \'lat\'', function() {
+        // this is handled at the calc step now via _length.
         var fullTrace = _supply({
             lon: [1, 2, 3],
             lat: [2, 3]
         });
 
-        expect(fullTrace.lon).toEqual([1, 2]);
+        expect(fullTrace.lon).toEqual([1, 2, 3]);
         expect(fullTrace.lat).toEqual([2, 3]);
+        expect(fullTrace._length).toBe(2);
     });
 
-    it('should truncate \'lat\' if longer than \'lon\'', function() {
+    it('should not truncate \'lat\' if longer than \'lon\'', function() {
+        // this is handled at the calc step now via _length.
         var fullTrace = _supply({
             lon: [1, 2, 3],
             lat: [2, 3, 3, 5]
         });
 
         expect(fullTrace.lon).toEqual([1, 2, 3]);
-        expect(fullTrace.lat).toEqual([2, 3, 3]);
+        expect(fullTrace.lat).toEqual([2, 3, 3, 5]);
+        expect(fullTrace._length).toBe(3);
     });
 
     it('should set \'visible\' to false if \'lat\' and/or \'lon\' has zero length', function() {
diff --git a/test/jasmine/tests/scatterpolar_test.js b/test/jasmine/tests/scatterpolar_test.js
index 1b46f59d9c4..58477040103 100644
--- a/test/jasmine/tests/scatterpolar_test.js
+++ b/test/jasmine/tests/scatterpolar_test.js
@@ -18,24 +18,28 @@ describe('Test scatterpolar trace defaults:', function() {
         ScatterPolar.supplyDefaults(traceIn, traceOut, '#444', layout || {});
     }
 
-    it('should truncate *r* when longer than *theta*', function() {
+    it('should not truncate *r* when longer than *theta*', function() {
+        // this is handled at the calc step now via _length.
         _supply({
             r: [1, 2, 3, 4, 5],
             theta: [1, 2, 3]
         });
 
-        expect(traceOut.r).toEqual([1, 2, 3]);
+        expect(traceOut.r).toEqual([1, 2, 3, 4, 5]);
         expect(traceOut.theta).toEqual([1, 2, 3]);
+        expect(traceOut._length).toBe(3);
     });
 
-    it('should truncate *theta* when longer than *r*', function() {
+    it('should not truncate *theta* when longer than *r*', function() {
+        // this is handled at the calc step now via _length.
         _supply({
             r: [1, 2, 3],
             theta: [1, 2, 3, 4, 5]
         });
 
         expect(traceOut.r).toEqual([1, 2, 3]);
-        expect(traceOut.theta).toEqual([1, 2, 3]);
+        expect(traceOut.theta).toEqual([1, 2, 3, 4, 5]);
+        expect(traceOut._length).toBe(3);
     });
 });
 
diff --git a/test/jasmine/tests/scatterternary_test.js b/test/jasmine/tests/scatterternary_test.js
index bbd1d6b7003..be24bbb5814 100644
--- a/test/jasmine/tests/scatterternary_test.js
+++ b/test/jasmine/tests/scatterternary_test.js
@@ -101,7 +101,8 @@ describe('scatterternary defaults', function() {
         expect(traceOut.visible).toBe(false);
     });
 
-    it('should truncate data arrays to the same length (\'c\' is shortest case)', function() {
+    it('should not truncate data arrays to the same length (\'c\' is shortest case)', function() {
+        // this is handled at the calc step now via _length.
         traceIn = {
             a: [1, 2, 3],
             b: [1, 2],
@@ -109,12 +110,14 @@ describe('scatterternary defaults', function() {
         };
 
         supplyDefaults(traceIn, traceOut, defaultColor, layout);
-        expect(traceOut.a).toEqual([1]);
-        expect(traceOut.b).toEqual([1]);
+        expect(traceOut.a).toEqual([1, 2, 3]);
+        expect(traceOut.b).toEqual([1, 2]);
         expect(traceOut.c).toEqual([1]);
+        expect(traceOut._length).toBe(1);
     });
 
-    it('should truncate data arrays to the same length (\'a\' is shortest case)', function() {
+    it('should not truncate data arrays to the same length (\'a\' is shortest case)', function() {
+        // this is handled at the calc step now via _length.
         traceIn = {
             a: [1],
             b: [1, 2, 3],
@@ -123,11 +126,13 @@ describe('scatterternary defaults', function() {
 
         supplyDefaults(traceIn, traceOut, defaultColor, layout);
         expect(traceOut.a).toEqual([1]);
-        expect(traceOut.b).toEqual([1]);
-        expect(traceOut.c).toEqual([1]);
+        expect(traceOut.b).toEqual([1, 2, 3]);
+        expect(traceOut.c).toEqual([1, 2]);
+        expect(traceOut._length).toBe(1);
     });
 
-    it('should truncate data arrays to the same length (\'a\' is shortest case)', function() {
+    it('should not truncate data arrays to the same length (\'a\' is shortest case)', function() {
+        // this is handled at the calc step now via _length.
         traceIn = {
             a: [1, 2],
             b: [1],
@@ -135,9 +140,10 @@ describe('scatterternary defaults', function() {
         };
 
         supplyDefaults(traceIn, traceOut, defaultColor, layout);
-        expect(traceOut.a).toEqual([1]);
+        expect(traceOut.a).toEqual([1, 2]);
         expect(traceOut.b).toEqual([1]);
-        expect(traceOut.c).toEqual([1]);
+        expect(traceOut.c).toEqual([1, 2, 3]);
+        expect(traceOut._length).toBe(1);
     });
 
     it('should include \'name\' in \'hoverinfo\' default if multi trace graph', function() {
@@ -218,32 +224,39 @@ describe('scatterternary calc', function() {
 
         trace = {
             subplot: 'ternary',
-            sum: 1
+            sum: 1,
+            _length: 3
         };
     });
 
+    function get(cd, component) {
+        return cd.map(function(v) {
+            return v[component];
+        });
+    }
+
     it('should fill in missing component (case \'c\')', function() {
         trace.a = [0.1, 0.3, 0.6];
         trace.b = [0.3, 0.6, 0.1];
 
-        calc(gd, trace);
-        expect(trace.c).toBeCloseToArray([0.6, 0.1, 0.3]);
+        cd = calc(gd, trace);
+        expect(get(cd, 'c')).toBeCloseToArray([0.6, 0.1, 0.3]);
     });
 
     it('should fill in missing component (case \'b\')', function() {
         trace.a = [0.1, 0.3, 0.6];
         trace.c = [0.1, 0.3, 0.2];
 
-        calc(gd, trace);
-        expect(trace.b).toBeCloseToArray([0.8, 0.4, 0.2]);
+        cd = calc(gd, trace);
+        expect(get(cd, 'b')).toBeCloseToArray([0.8, 0.4, 0.2]);
     });
 
     it('should fill in missing component (case \'a\')', function() {
         trace.b = [0.1, 0.3, 0.6];
         trace.c = [0.8, 0.4, 0.1];
 
-        calc(gd, trace);
-        expect(trace.a).toBeCloseToArray([0.1, 0.3, 0.3]);
+        cd = calc(gd, trace);
+        expect(get(cd, 'a')).toBeCloseToArray([0.1, 0.3, 0.3]);
     });
 
     it('should skip over non-numeric values', function() {
diff --git a/test/jasmine/tests/surface_test.js b/test/jasmine/tests/surface_test.js
index b7d23716bc7..00cfe59ff05 100644
--- a/test/jasmine/tests/surface_test.js
+++ b/test/jasmine/tests/surface_test.js
@@ -25,14 +25,15 @@ describe('Test surface', function() {
             expect(traceOut.visible).toBe(false);
         });
 
-        it('should fill \'x\' and \'y\' if not provided', function() {
+        it('should NOT fill \'x\' and \'y\' if not provided', function() {
+            // this happens later on now
             traceIn = {
                 z: [[1, 2, 3], [2, 1, 2]]
             };
 
             supplyDefaults(traceIn, traceOut, defaultColor, layout);
-            expect(traceOut.x).toEqual([0, 1, 2]);
-            expect(traceOut.y).toEqual([0, 1]);
+            expect(traceOut.x).toBeUndefined();
+            expect(traceOut.y).toBeUndefined();
         });
 
         it('should coerce \'project\' if contours or highlight lines are enabled', function() {
diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js
index 3f1181a4c26..40e5a4601a9 100644
--- a/test/jasmine/tests/toimage_test.js
+++ b/test/jasmine/tests/toimage_test.js
@@ -149,7 +149,7 @@ describe('Plotly.toImage', function() {
         .then(function() { return Plotly.toImage(gd, {format: 'png', imageDataOnly: true}); })
         .then(function(d) {
             expect(d.indexOf('data:image/')).toBe(-1);
-            expect(d.length).toBeWithin(50000, 5e3, 'png image length');
+            expect(d.length).toBeWithin(52500, 7500, 'png image length');
         })
         .then(function() { return Plotly.toImage(gd, {format: 'jpeg', imageDataOnly: true}); })
         .then(function(d) {