diff --git a/lib/bar.js b/lib/bar.js
index 64283813cb1..37a62cfa50e 100644
--- a/lib/bar.js
+++ b/lib/bar.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/bar');
+"use strict";
+module.exports = require("../src/traces/bar");
diff --git a/lib/box.js b/lib/box.js
index f3fde994b63..6ef50340d61 100644
--- a/lib/box.js
+++ b/lib/box.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/box');
+"use strict";
+module.exports = require("../src/traces/box");
diff --git a/lib/calendars.js b/lib/calendars.js
index 8ad06f1d6da..86eb1b1fb0d 100644
--- a/lib/calendars.js
+++ b/lib/calendars.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/components/calendars');
+"use strict";
+module.exports = require("../src/components/calendars");
diff --git a/lib/candlestick.js b/lib/candlestick.js
index c608d5b6755..a4c16de27f6 100644
--- a/lib/candlestick.js
+++ b/lib/candlestick.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/candlestick');
+"use strict";
+module.exports = require("../src/traces/candlestick");
diff --git a/lib/choropleth.js b/lib/choropleth.js
index 4e1f61186c5..37045b2e42c 100644
--- a/lib/choropleth.js
+++ b/lib/choropleth.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/choropleth');
+"use strict";
+module.exports = require("../src/traces/choropleth");
diff --git a/lib/contour.js b/lib/contour.js
index 703eb8d311b..25cd26592d2 100644
--- a/lib/contour.js
+++ b/lib/contour.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/contour');
+"use strict";
+module.exports = require("../src/traces/contour");
diff --git a/lib/contourgl.js b/lib/contourgl.js
index 74901df14b4..47f7f4d8b32 100644
--- a/lib/contourgl.js
+++ b/lib/contourgl.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/contourgl');
+"use strict";
+module.exports = require("../src/traces/contourgl");
diff --git a/lib/core.js b/lib/core.js
index afec96d280d..46b80c73a1f 100644
--- a/lib/core.js
+++ b/lib/core.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/core');
+"use strict";
+module.exports = require("../src/core");
diff --git a/lib/filter.js b/lib/filter.js
index 14ffd87f7e1..4112f572a32 100644
--- a/lib/filter.js
+++ b/lib/filter.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/transforms/filter');
+"use strict";
+module.exports = require("../src/transforms/filter");
diff --git a/lib/groupby.js b/lib/groupby.js
index 8ef1dd1f6c2..f0cc35899f4 100644
--- a/lib/groupby.js
+++ b/lib/groupby.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/transforms/groupby');
+"use strict";
+module.exports = require("../src/transforms/groupby");
diff --git a/lib/heatmap.js b/lib/heatmap.js
index 0560a47d95a..9aded18216d 100644
--- a/lib/heatmap.js
+++ b/lib/heatmap.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/heatmap');
+"use strict";
+module.exports = require("../src/traces/heatmap");
diff --git a/lib/heatmapgl.js b/lib/heatmapgl.js
index 26352a58216..d36c77033c3 100644
--- a/lib/heatmapgl.js
+++ b/lib/heatmapgl.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/heatmapgl');
+"use strict";
+module.exports = require("../src/traces/heatmapgl");
diff --git a/lib/histogram.js b/lib/histogram.js
index f704db7fa70..dcb27108677 100644
--- a/lib/histogram.js
+++ b/lib/histogram.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/histogram');
+"use strict";
+module.exports = require("../src/traces/histogram");
diff --git a/lib/histogram2d.js b/lib/histogram2d.js
index 69d2f691c03..872ab1eb287 100644
--- a/lib/histogram2d.js
+++ b/lib/histogram2d.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/histogram2d');
+"use strict";
+module.exports = require("../src/traces/histogram2d");
diff --git a/lib/histogram2dcontour.js b/lib/histogram2dcontour.js
index 133617a9961..f3094de2f0c 100644
--- a/lib/histogram2dcontour.js
+++ b/lib/histogram2dcontour.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/histogram2dcontour');
+"use strict";
+module.exports = require("../src/traces/histogram2dcontour");
diff --git a/lib/index-basic.js b/lib/index-basic.js
index 4c837e413ad..80f5838f296 100644
--- a/lib/index-basic.js
+++ b/lib/index-basic.js
@@ -5,14 +5,9 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Plotly = require("./core");
 
-'use strict';
-
-var Plotly = require('./core');
-
-Plotly.register([
-    require('./bar'),
-    require('./pie')
-]);
+Plotly.register([require("./bar"), require("./pie")]);
 
 module.exports = Plotly;
diff --git a/lib/index-cartesian.js b/lib/index-cartesian.js
index 4d07f5f5f09..c0243480edd 100644
--- a/lib/index-cartesian.js
+++ b/lib/index-cartesian.js
@@ -5,21 +5,19 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var Plotly = require('./core');
+"use strict";
+var Plotly = require("./core");
 
 Plotly.register([
-    require('./bar'),
-    require('./box'),
-    require('./heatmap'),
-    require('./histogram'),
-    require('./histogram2d'),
-    require('./histogram2dcontour'),
-    require('./pie'),
-    require('./contour'),
-    require('./scatterternary')
+  require("./bar"),
+  require("./box"),
+  require("./heatmap"),
+  require("./histogram"),
+  require("./histogram2d"),
+  require("./histogram2dcontour"),
+  require("./pie"),
+  require("./contour"),
+  require("./scatterternary")
 ]);
 
 module.exports = Plotly;
diff --git a/lib/index-finance.js b/lib/index-finance.js
index 4759344b760..486e3ec999e 100644
--- a/lib/index-finance.js
+++ b/lib/index-finance.js
@@ -5,17 +5,15 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var Plotly = require('./core');
+"use strict";
+var Plotly = require("./core");
 
 Plotly.register([
-    require('./bar'),
-    require('./histogram'),
-    require('./pie'),
-    require('./ohlc'),
-    require('./candlestick')
+  require("./bar"),
+  require("./histogram"),
+  require("./pie"),
+  require("./ohlc"),
+  require("./candlestick")
 ]);
 
 module.exports = Plotly;
diff --git a/lib/index-geo.js b/lib/index-geo.js
index 2283f1f0489..aa7aea199f9 100644
--- a/lib/index-geo.js
+++ b/lib/index-geo.js
@@ -5,14 +5,9 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Plotly = require("./core");
 
-'use strict';
-
-var Plotly = require('./core');
-
-Plotly.register([
-    require('./scattergeo'),
-    require('./choropleth')
-]);
+Plotly.register([require("./scattergeo"), require("./choropleth")]);
 
 module.exports = Plotly;
diff --git a/lib/index-gl2d.js b/lib/index-gl2d.js
index a7738a91744..cfceda4cbf5 100644
--- a/lib/index-gl2d.js
+++ b/lib/index-gl2d.js
@@ -5,16 +5,14 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var Plotly = require('./core');
+"use strict";
+var Plotly = require("./core");
 
 Plotly.register([
-    require('./scattergl'),
-    require('./pointcloud'),
-    require('./heatmapgl'),
-    require('./contourgl')
+  require("./scattergl"),
+  require("./pointcloud"),
+  require("./heatmapgl"),
+  require("./contourgl")
 ]);
 
 module.exports = Plotly;
diff --git a/lib/index-gl3d.js b/lib/index-gl3d.js
index 7134846cb7d..69464017f61 100644
--- a/lib/index-gl3d.js
+++ b/lib/index-gl3d.js
@@ -5,15 +5,13 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var Plotly = require('./core');
+"use strict";
+var Plotly = require("./core");
 
 Plotly.register([
-    require('./scatter3d'),
-    require('./surface'),
-    require('./mesh3d')
+  require("./scatter3d"),
+  require("./surface"),
+  require("./mesh3d")
 ]);
 
 module.exports = Plotly;
diff --git a/lib/index-mapbox.js b/lib/index-mapbox.js
index 17b075b5f49..234182976ad 100644
--- a/lib/index-mapbox.js
+++ b/lib/index-mapbox.js
@@ -5,13 +5,9 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Plotly = require("./core");
 
-'use strict';
-
-var Plotly = require('./core');
-
-Plotly.register([
-    require('./scattermapbox')
-]);
+Plotly.register([require("./scattermapbox")]);
 
 module.exports = Plotly;
diff --git a/lib/index.js b/lib/index.js
index 1cbbb8bba08..79f107f9b22 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -5,38 +5,31 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var Plotly = require('./core');
+"use strict";
+var Plotly = require("./core");
 
 // traces
 Plotly.register([
-    require('./bar'),
-    require('./box'),
-    require('./heatmap'),
-    require('./histogram'),
-    require('./histogram2d'),
-    require('./histogram2dcontour'),
-    require('./pie'),
-    require('./contour'),
-    require('./scatterternary'),
-
-    require('./scatter3d'),
-    require('./surface'),
-    require('./mesh3d'),
-
-    require('./scattergeo'),
-    require('./choropleth'),
-
-    require('./scattergl'),
-    require('./pointcloud'),
-    require('./heatmapgl'),
-
-    require('./scattermapbox'),
-
-    require('./ohlc'),
-    require('./candlestick')
+  require("./bar"),
+  require("./box"),
+  require("./heatmap"),
+  require("./histogram"),
+  require("./histogram2d"),
+  require("./histogram2dcontour"),
+  require("./pie"),
+  require("./contour"),
+  require("./scatterternary"),
+  require("./scatter3d"),
+  require("./surface"),
+  require("./mesh3d"),
+  require("./scattergeo"),
+  require("./choropleth"),
+  require("./scattergl"),
+  require("./pointcloud"),
+  require("./heatmapgl"),
+  require("./scattermapbox"),
+  require("./ohlc"),
+  require("./candlestick")
 ]);
 
 // transforms
@@ -49,14 +42,9 @@ Plotly.register([
 // For more info, see:
 // https://github.com/plotly/plotly.js/pull/978#pullrequestreview-2403353
 //
-Plotly.register([
-    require('./filter'),
-    require('./groupby')
-]);
+Plotly.register([require("./filter"), require("./groupby")]);
 
 // components
-Plotly.register([
-    require('./calendars')
-]);
+Plotly.register([require("./calendars")]);
 
 module.exports = Plotly;
diff --git a/lib/mesh3d.js b/lib/mesh3d.js
index 13453614a45..e6c99f2f39d 100644
--- a/lib/mesh3d.js
+++ b/lib/mesh3d.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/mesh3d');
+"use strict";
+module.exports = require("../src/traces/mesh3d");
diff --git a/lib/ohlc.js b/lib/ohlc.js
index 40537cfd011..086c85e64de 100644
--- a/lib/ohlc.js
+++ b/lib/ohlc.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/ohlc');
+"use strict";
+module.exports = require("../src/traces/ohlc");
diff --git a/lib/pie.js b/lib/pie.js
index 08f1d09ab76..099234fc945 100644
--- a/lib/pie.js
+++ b/lib/pie.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/pie');
+"use strict";
+module.exports = require("../src/traces/pie");
diff --git a/lib/pointcloud.js b/lib/pointcloud.js
index 4d6c7d286e0..edbaa5cebb6 100644
--- a/lib/pointcloud.js
+++ b/lib/pointcloud.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/pointcloud');
+"use strict";
+module.exports = require("../src/traces/pointcloud");
diff --git a/lib/scatter.js b/lib/scatter.js
index 589c7bd8b0c..876e9d0045e 100644
--- a/lib/scatter.js
+++ b/lib/scatter.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/scatter');
+"use strict";
+module.exports = require("../src/traces/scatter");
diff --git a/lib/scatter3d.js b/lib/scatter3d.js
index c4ee1a63465..cbb8d1ca233 100644
--- a/lib/scatter3d.js
+++ b/lib/scatter3d.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/scatter3d');
+"use strict";
+module.exports = require("../src/traces/scatter3d");
diff --git a/lib/scattergeo.js b/lib/scattergeo.js
index 90f892115bb..02421046954 100644
--- a/lib/scattergeo.js
+++ b/lib/scattergeo.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/scattergeo');
+"use strict";
+module.exports = require("../src/traces/scattergeo");
diff --git a/lib/scattergl.js b/lib/scattergl.js
index 7c5b6f2ba79..72006b1cc34 100644
--- a/lib/scattergl.js
+++ b/lib/scattergl.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/scattergl');
+"use strict";
+module.exports = require("../src/traces/scattergl");
diff --git a/lib/scattermapbox.js b/lib/scattermapbox.js
index 5391bc32630..11933be948c 100644
--- a/lib/scattermapbox.js
+++ b/lib/scattermapbox.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/scattermapbox');
+"use strict";
+module.exports = require("../src/traces/scattermapbox");
diff --git a/lib/scatterternary.js b/lib/scatterternary.js
index 2196d3c41ae..0fd0aee69bc 100644
--- a/lib/scatterternary.js
+++ b/lib/scatterternary.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/scatterternary');
+"use strict";
+module.exports = require("../src/traces/scatterternary");
diff --git a/lib/surface.js b/lib/surface.js
index d4a3197a2e8..2fc1f1c0bac 100644
--- a/lib/surface.js
+++ b/lib/surface.js
@@ -5,7 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-module.exports = require('../src/traces/surface');
+"use strict";
+module.exports = require("../src/traces/surface");
diff --git a/package.json b/package.json
index f4fbc318a68..e587ce21ec3 100644
--- a/package.json
+++ b/package.json
@@ -28,8 +28,8 @@
     "build": "npm run preprocess && npm run bundle && npm run header && npm run stats",
     "cibuild": "npm run preprocess && node tasks/cibundle.js",
     "watch": "node tasks/watch.js",
-    "lint": "eslint --version && eslint . || true",
-    "lint-fix": "eslint . --fix",
+    "lint": "prettier --write \"src/**/*.js\" \"test/**/*.js\" \"lib/**/*.js\"",
+    "lint-fix": "prettier --write \"src/**/*.js\" \"test/**/*.js\" \"lib/**/*.js\"",
     "docker": "node tasks/docker.js",
     "pretest": "node tasks/pretest.js",
     "test-jasmine": "karma start test/jasmine/karma.conf.js",
@@ -117,6 +117,7 @@
     "npm-link-check": "^1.2.0",
     "open": "0.0.5",
     "prepend-file": "^1.3.1",
+    "prettier": "^0.16.0",
     "prettysize": "0.0.3",
     "requirejs": "^2.3.1",
     "through2": "^2.0.3",
diff --git a/src/assets/geo_assets.js b/src/assets/geo_assets.js
index 5222e524f44..8b137891791 100644
--- a/src/assets/geo_assets.js
+++ b/src/assets/geo_assets.js
@@ -1,17 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-var saneTopojson = require('sane-topojson');
-
-
-// package version injected by `npm run preprocess`
-exports.version = '1.23.1';
-
-exports.topojson = saneTopojson;
diff --git a/src/components/annotations/annotation_defaults.js b/src/components/annotations/annotation_defaults.js
index 7df91978c59..8b137891791 100644
--- a/src/components/annotations/annotation_defaults.js
+++ b/src/components/annotations/annotation_defaults.js
@@ -1,107 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var Lib = require('../../lib');
-var Color = require('../color');
-var Axes = require('../../plots/cartesian/axes');
-
-var attributes = require('./attributes');
-
-
-module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, opts, itemOpts) {
-    opts = opts || {};
-    itemOpts = itemOpts || {};
-
-    function coerce(attr, dflt) {
-        return Lib.coerce(annIn, annOut, attributes, attr, dflt);
-    }
-
-    var visible = coerce('visible', !itemOpts.itemIsNotPlainObject);
-    var clickToShow = coerce('clicktoshow');
-
-    if(!(visible || clickToShow)) return annOut;
-
-    coerce('opacity');
-    coerce('align');
-    coerce('bgcolor');
-
-    var borderColor = coerce('bordercolor'),
-        borderOpacity = Color.opacity(borderColor);
-
-    coerce('borderpad');
-
-    var borderWidth = coerce('borderwidth');
-    var showArrow = coerce('showarrow');
-
-    coerce('text', showArrow ? ' ' : 'new text');
-    coerce('textangle');
-    Lib.coerceFont(coerce, 'font', fullLayout.font);
-
-    // positioning
-    var axLetters = ['x', 'y'],
-        arrowPosDflt = [-10, -30],
-        gdMock = {_fullLayout: fullLayout};
-    for(var i = 0; i < 2; i++) {
-        var axLetter = axLetters[i];
-
-        // xref, yref
-        var axRef = Axes.coerceRef(annIn, annOut, gdMock, axLetter, '', 'paper');
-
-        // x, y
-        Axes.coercePosition(annOut, gdMock, coerce, axRef, axLetter, 0.5);
-
-        if(showArrow) {
-            var arrowPosAttr = 'a' + axLetter,
-                // axref, ayref
-                aaxRef = Axes.coerceRef(annIn, annOut, gdMock, arrowPosAttr, 'pixel');
-
-            // for now the arrow can only be on the same axis or specified as pixels
-            // TODO: sometime it might be interesting to allow it to be on *any* axis
-            // but that would require updates to drawing & autorange code and maybe more
-            if(aaxRef !== 'pixel' && aaxRef !== axRef) {
-                aaxRef = annOut[arrowPosAttr] = 'pixel';
-            }
-
-            // ax, ay
-            var aDflt = (aaxRef === 'pixel') ? arrowPosDflt[i] : 0.4;
-            Axes.coercePosition(annOut, gdMock, coerce, aaxRef, arrowPosAttr, aDflt);
-        }
-
-        // xanchor, yanchor
-        coerce(axLetter + 'anchor');
-    }
-
-    // if you have one coordinate you should have both
-    Lib.noneOrAll(annIn, annOut, ['x', 'y']);
-
-    if(showArrow) {
-        coerce('arrowcolor', borderOpacity ? annOut.bordercolor : Color.defaultLine);
-        coerce('arrowhead');
-        coerce('arrowsize');
-        coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2);
-        coerce('standoff');
-
-        // if you have one part of arrow length you should have both
-        Lib.noneOrAll(annIn, annOut, ['ax', 'ay']);
-    }
-
-    if(clickToShow) {
-        var xClick = coerce('xclick');
-        var yClick = coerce('yclick');
-
-        // put the actual click data to bind to into private attributes
-        // so we don't have to do this little bit of logic on every hover event
-        annOut._xclick = (xClick === undefined) ? annOut.x : xClick;
-        annOut._yclick = (yClick === undefined) ? annOut.y : yClick;
-    }
-
-    return annOut;
-};
diff --git a/src/components/annotations/arrow_paths.js b/src/components/annotations/arrow_paths.js
index 3f27bbaf83a..8b137891791 100644
--- a/src/components/annotations/arrow_paths.js
+++ b/src/components/annotations/arrow_paths.js
@@ -1,63 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-/**
- * centerx is a center of scaling tuned for maximum scalability of
- * the arrowhead ie throughout mag=0.3..3 the head is joined smoothly
- * to the line, but the endpoint moves.
- * backoff is the distance to move the arrowhead, and the end of the
- * line, in order to end at the right place
- *
- * TODO: option to have the pointed-to  point a little in front of the
- * end of the line, as people tend to want a bit of a gap there...
- */
-
-module.exports = [
-    // no arrow
-    {
-        path: '',
-        backoff: 0
-    },
-    // wide with flat back
-    {
-        path: 'M-2.4,-3V3L0.6,0Z',
-        backoff: 0.6
-    },
-    // narrower with flat back
-    {
-        path: 'M-3.7,-2.5V2.5L1.3,0Z',
-        backoff: 1.3
-    },
-    // barbed
-    {
-        path: 'M-4.45,-3L-1.65,-0.2V0.2L-4.45,3L1.55,0Z',
-        backoff: 1.55
-    },
-    // wide line-drawn
-    {
-        path: 'M-2.2,-2.2L-0.2,-0.2V0.2L-2.2,2.2L-1.4,3L1.6,0L-1.4,-3Z',
-        backoff: 1.6
-    },
-    // narrower line-drawn
-    {
-        path: 'M-4.4,-2.1L-0.6,-0.2V0.2L-4.4,2.1L-4,3L2,0L-4,-3Z',
-        backoff: 2
-    },
-    // circle
-    {
-        path: 'M2,0A2,2 0 1,1 0,-2A2,2 0 0,1 2,0Z',
-        backoff: 0
-    },
-    // square
-    {
-        path: 'M2,2V-2H-2V2Z',
-        backoff: 0
-    }
-];
diff --git a/src/components/annotations/attributes.js b/src/components/annotations/attributes.js
index cdaccd8bc73..8b137891791 100644
--- a/src/components/annotations/attributes.js
+++ b/src/components/annotations/attributes.js
@@ -1,359 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-var ARROWPATHS = require('./arrow_paths');
-var fontAttrs = require('../../plots/font_attributes');
-var cartesianConstants = require('../../plots/cartesian/constants');
-var extendFlat = require('../../lib/extend').extendFlat;
-
-
-module.exports = {
-    _isLinkedToArray: 'annotation',
-
-    visible: {
-        valType: 'boolean',
-        role: 'info',
-        dflt: true,
-        description: [
-            'Determines whether or not this annotation is visible.'
-        ].join(' ')
-    },
-
-    text: {
-        valType: 'string',
-        role: 'info',
-        description: [
-            'Sets the text associated with this annotation.',
-            'Plotly uses a subset of HTML tags to do things like',
-            'newline (
), bold (), italics (),',
-            'hyperlinks (). Tags , , ',
-            ' are also supported.'
-        ].join(' ')
-    },
-    textangle: {
-        valType: 'angle',
-        dflt: 0,
-        role: 'style',
-        description: [
-            'Sets the angle at which the `text` is drawn',
-            'with respect to the horizontal.'
-        ].join(' ')
-    },
-    font: extendFlat({}, fontAttrs, {
-        description: 'Sets the annotation text font.'
-    }),
-    opacity: {
-        valType: 'number',
-        min: 0,
-        max: 1,
-        dflt: 1,
-        role: 'style',
-        description: 'Sets the opacity of the annotation (text + arrow).'
-    },
-    align: {
-        valType: 'enumerated',
-        values: ['left', 'center', 'right'],
-        dflt: 'center',
-        role: 'style',
-        description: [
-            'Sets the vertical alignment of the `text` with',
-            'respect to the set `x` and `y` position.',
-            'Has only an effect if `text` spans more two or more lines',
-            '(i.e. `text` contains one or more 
 HTML tags).'
-        ].join(' ')
-    },
-    bgcolor: {
-        valType: 'color',
-        dflt: 'rgba(0,0,0,0)',
-        role: 'style',
-        description: 'Sets the background color of the annotation.'
-    },
-    bordercolor: {
-        valType: 'color',
-        dflt: 'rgba(0,0,0,0)',
-        role: 'style',
-        description: [
-            'Sets the color of the border enclosing the annotation `text`.'
-        ].join(' ')
-    },
-    borderpad: {
-        valType: 'number',
-        min: 0,
-        dflt: 1,
-        role: 'style',
-        description: [
-            'Sets the padding (in px) between the `text`',
-            'and the enclosing border.'
-        ].join(' ')
-    },
-    borderwidth: {
-        valType: 'number',
-        min: 0,
-        dflt: 1,
-        role: 'style',
-        description: [
-            'Sets the width (in px) of the border enclosing',
-            'the annotation `text`.'
-        ].join(' ')
-    },
-    // arrow
-    showarrow: {
-        valType: 'boolean',
-        dflt: true,
-        role: 'style',
-        description: [
-            'Determines whether or not the annotation is drawn with an arrow.',
-            'If *true*, `text` is placed near the arrow\'s tail.',
-            'If *false*, `text` lines up with the `x` and `y` provided.'
-        ].join(' ')
-    },
-    arrowcolor: {
-        valType: 'color',
-        role: 'style',
-        description: 'Sets the color of the annotation arrow.'
-    },
-    arrowhead: {
-        valType: 'integer',
-        min: 0,
-        max: ARROWPATHS.length,
-        dflt: 1,
-        role: 'style',
-        description: 'Sets the annotation arrow head style.'
-    },
-    arrowsize: {
-        valType: 'number',
-        min: 0.3,
-        dflt: 1,
-        role: 'style',
-        description: 'Sets the size (in px) of annotation arrow head.'
-    },
-    arrowwidth: {
-        valType: 'number',
-        min: 0.1,
-        role: 'style',
-        description: 'Sets the width (in px) of annotation arrow.'
-    },
-    standoff: {
-        valType: 'number',
-        min: 0,
-        dflt: 0,
-        role: 'style',
-        description: [
-            'Sets a distance, in pixels, to move the arrowhead away from the',
-            'position it is pointing at, for example to point at the edge of',
-            'a marker independent of zoom.'
-        ].join(' ')
-    },
-    ax: {
-        valType: 'any',
-        role: 'info',
-        description: [
-            'Sets the x component of the arrow tail about the arrow head.',
-            'If `axref` is `pixel`, a positive (negative) ',
-            'component corresponds to an arrow pointing',
-            'from right to left (left to right).',
-            'If `axref` is an axis, this is an absolute value on that axis,',
-            'like `x`, NOT a relative value.'
-        ].join(' ')
-    },
-    ay: {
-        valType: 'any',
-        role: 'info',
-        description: [
-            'Sets the y component of the arrow tail about the arrow head.',
-            'If `ayref` is `pixel`, a positive (negative) ',
-            'component corresponds to an arrow pointing',
-            'from bottom to top (top to bottom).',
-            'If `ayref` is an axis, this is an absolute value on that axis,',
-            'like `y`, NOT a relative value.'
-        ].join(' ')
-    },
-    axref: {
-        valType: 'enumerated',
-        dflt: 'pixel',
-        values: [
-            'pixel',
-            cartesianConstants.idRegex.x.toString()
-        ],
-        role: 'info',
-        description: [
-            'Indicates in what terms the tail of the annotation (ax,ay) ',
-            'is specified. If `pixel`, `ax` is a relative offset in pixels ',
-            'from `x`. If set to an x axis id (e.g. *x* or *x2*), `ax` is ',
-            'specified in the same terms as that axis. This is useful ',
-            'for trendline annotations which should continue to indicate ',
-            'the correct trend when zoomed.'
-        ].join(' ')
-    },
-    ayref: {
-        valType: 'enumerated',
-        dflt: 'pixel',
-        values: [
-            'pixel',
-            cartesianConstants.idRegex.y.toString()
-        ],
-        role: 'info',
-        description: [
-            'Indicates in what terms the tail of the annotation (ax,ay) ',
-            'is specified. If `pixel`, `ay` is a relative offset in pixels ',
-            'from `y`. If set to a y axis id (e.g. *y* or *y2*), `ay` is ',
-            'specified in the same terms as that axis. This is useful ',
-            'for trendline annotations which should continue to indicate ',
-            'the correct trend when zoomed.'
-        ].join(' ')
-    },
-    // positioning
-    xref: {
-        valType: 'enumerated',
-        values: [
-            'paper',
-            cartesianConstants.idRegex.x.toString()
-        ],
-        role: 'info',
-        description: [
-            'Sets the annotation\'s x coordinate axis.',
-            'If set to an x axis id (e.g. *x* or *x2*), the `x` position',
-            'refers to an x coordinate',
-            'If set to *paper*, the `x` position refers to the distance from',
-            'the left side of the plotting area in normalized coordinates',
-            'where 0 (1) corresponds to the left (right) side.'
-        ].join(' ')
-    },
-    x: {
-        valType: 'any',
-        role: 'info',
-        description: [
-            'Sets the annotation\'s x position.',
-            'If the axis `type` is *log*, then you must take the',
-            'log of your desired range.',
-            'If the axis `type` is *date*, it should be date strings,',
-            'like date data, though Date objects and unix milliseconds',
-            'will be accepted and converted to strings.',
-            'If the axis `type` is *category*, it should be numbers,',
-            'using the scale where each category is assigned a serial',
-            'number from zero in the order it appears.'
-        ].join(' ')
-    },
-    xanchor: {
-        valType: 'enumerated',
-        values: ['auto', 'left', 'center', 'right'],
-        dflt: 'auto',
-        role: 'info',
-        description: [
-            'Sets the text box\'s horizontal position anchor',
-            'This anchor binds the `x` position to the *left*, *center*',
-            'or *right* of the annotation.',
-            'For example, if `x` is set to 1, `xref` to *paper* and',
-            '`xanchor` to *right* then the right-most portion of the',
-            'annotation lines up with the right-most edge of the',
-            'plotting area.',
-            'If *auto*, the anchor is equivalent to *center* for',
-            'data-referenced annotations or if there is an arrow,',
-            'whereas for paper-referenced with no arrow, the anchor picked',
-            'corresponds to the closest side.'
-        ].join(' ')
-    },
-    yref: {
-        valType: 'enumerated',
-        values: [
-            'paper',
-            cartesianConstants.idRegex.y.toString()
-        ],
-        role: 'info',
-        description: [
-            'Sets the annotation\'s y coordinate axis.',
-            'If set to an y axis id (e.g. *y* or *y2*), the `y` position',
-            'refers to an y coordinate',
-            'If set to *paper*, the `y` position refers to the distance from',
-            'the bottom of the plotting area in normalized coordinates',
-            'where 0 (1) corresponds to the bottom (top).'
-        ].join(' ')
-    },
-    y: {
-        valType: 'any',
-        role: 'info',
-        description: [
-            'Sets the annotation\'s y position.',
-            'If the axis `type` is *log*, then you must take the',
-            'log of your desired range.',
-            'If the axis `type` is *date*, it should be date strings,',
-            'like date data, though Date objects and unix milliseconds',
-            'will be accepted and converted to strings.',
-            'If the axis `type` is *category*, it should be numbers,',
-            'using the scale where each category is assigned a serial',
-            'number from zero in the order it appears.'
-        ].join(' ')
-    },
-    yanchor: {
-        valType: 'enumerated',
-        values: ['auto', 'top', 'middle', 'bottom'],
-        dflt: 'auto',
-        role: 'info',
-        description: [
-            'Sets the text box\'s vertical position anchor',
-            'This anchor binds the `y` position to the *top*, *middle*',
-            'or *bottom* of the annotation.',
-            'For example, if `y` is set to 1, `yref` to *paper* and',
-            '`yanchor` to *top* then the top-most portion of the',
-            'annotation lines up with the top-most edge of the',
-            'plotting area.',
-            'If *auto*, the anchor is equivalent to *middle* for',
-            'data-referenced annotations or if there is an arrow,',
-            'whereas for paper-referenced with no arrow, the anchor picked',
-            'corresponds to the closest side.'
-        ].join(' ')
-    },
-    clicktoshow: {
-        valType: 'enumerated',
-        values: [false, 'onoff', 'onout'],
-        dflt: false,
-        role: 'style',
-        description: [
-            'Makes this annotation respond to clicks on the plot.',
-            'If you click a data point that exactly matches the `x` and `y`',
-            'values of this annotation, and it is hidden (visible: false),',
-            'it will appear. In *onoff* mode, you must click the same point',
-            'again to make it disappear, so if you click multiple points,',
-            'you can show multiple annotations. In *onout* mode, a click',
-            'anywhere else in the plot (on another data point or not) will',
-            'hide this annotation.',
-            'If you need to show/hide this annotation in response to different',
-            '`x` or `y` values, you can set `xclick` and/or `yclick`. This is',
-            'useful for example to label the side of a bar. To label markers',
-            'though, `standoff` is preferred over `xclick` and `yclick`.'
-        ].join(' ')
-    },
-    xclick: {
-        valType: 'any',
-        role: 'info',
-        description: [
-            'Toggle this annotation when clicking a data point whose `x` value',
-            'is `xclick` rather than the annotation\'s `x` value.'
-        ].join(' ')
-    },
-    yclick: {
-        valType: 'any',
-        role: 'info',
-        description: [
-            'Toggle this annotation when clicking a data point whose `y` value',
-            'is `yclick` rather than the annotation\'s `y` value.'
-        ].join(' ')
-    },
-
-    _deprecated: {
-        ref: {
-            valType: 'string',
-            role: 'info',
-            description: [
-                'Obsolete. Set `xref` and `yref` separately instead.'
-            ].join(' ')
-        }
-    }
-};
diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js
index f68ea537c63..8b137891791 100644
--- a/src/components/annotations/calc_autorange.js
+++ b/src/components/annotations/calc_autorange.js
@@ -1,93 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var Lib = require('../../lib');
-var Axes = require('../../plots/cartesian/axes');
-
-var draw = require('./draw').draw;
-
-
-module.exports = function calcAutorange(gd) {
-    var fullLayout = gd._fullLayout,
-        annotationList = Lib.filterVisible(fullLayout.annotations);
-
-    if(!annotationList.length || !gd._fullData.length) return;
-
-    var annotationAxes = {};
-    annotationList.forEach(function(ann) {
-        annotationAxes[ann.xref] = true;
-        annotationAxes[ann.yref] = true;
-    });
-
-    var autorangedAnnos = Axes.list(gd).filter(function(ax) {
-        return ax.autorange && annotationAxes[ax._id];
-    });
-    if(!autorangedAnnos.length) return;
-
-    return Lib.syncOrAsync([
-        draw,
-        annAutorange
-    ], gd);
-};
-
-function annAutorange(gd) {
-    var fullLayout = gd._fullLayout;
-
-    // find the bounding boxes for each of these annotations'
-    // relative to their anchor points
-    // use the arrow and the text bg rectangle,
-    // as the whole anno may include hidden text in its bbox
-    Lib.filterVisible(fullLayout.annotations).forEach(function(ann) {
-        var xa = Axes.getFromId(gd, ann.xref),
-            ya = Axes.getFromId(gd, ann.yref),
-            headSize = 3 * ann.arrowsize * ann.arrowwidth || 0;
-
-        if(xa && xa.autorange) {
-            if(ann.axref === ann.xref) {
-                // expand for the arrowhead (padded by arrowhead)
-                Axes.expand(xa, [xa.r2c(ann.x)], {
-                    ppadplus: headSize,
-                    ppadminus: headSize
-                });
-                // again for the textbox (padded by textbox)
-                Axes.expand(xa, [xa.r2c(ann.ax)], {
-                    ppadplus: ann._xpadplus,
-                    ppadminus: ann._xpadminus
-                });
-            }
-            else {
-                Axes.expand(xa, [xa.r2c(ann.x)], {
-                    ppadplus: Math.max(ann._xpadplus, headSize),
-                    ppadminus: Math.max(ann._xpadminus, headSize)
-                });
-            }
-        }
-
-        if(ya && ya.autorange) {
-            if(ann.ayref === ann.yref) {
-                Axes.expand(ya, [ya.r2c(ann.y)], {
-                    ppadplus: headSize,
-                    ppadminus: headSize
-                });
-                Axes.expand(ya, [ya.r2c(ann.ay)], {
-                    ppadplus: ann._ypadplus,
-                    ppadminus: ann._ypadminus
-                });
-            }
-            else {
-                Axes.expand(ya, [ya.r2c(ann.y)], {
-                    ppadplus: Math.max(ann._ypadplus, headSize),
-                    ppadminus: Math.max(ann._ypadminus, headSize)
-                });
-            }
-        }
-    });
-}
diff --git a/src/components/annotations/click.js b/src/components/annotations/click.js
index 8fe77ce8286..8b137891791 100644
--- a/src/components/annotations/click.js
+++ b/src/components/annotations/click.js
@@ -1,121 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var Plotly = require('../../plotly');
-
-
-module.exports = {
-    hasClickToShow: hasClickToShow,
-    onClick: onClick
-};
-
-/*
- * hasClickToShow: does the given hoverData have ANY annotations which will
- * turn ON if we click here? (used by hover events to set cursor)
- *
- * gd: graphDiv
- * hoverData: a hoverData array, as included with the *plotly_hover* or
- *     *plotly_click* events in the `points` attribute
- *
- * returns: boolean
- */
-function hasClickToShow(gd, hoverData) {
-    var sets = getToggleSets(gd, hoverData);
-    return sets.on.length > 0 || sets.explicitOff.length > 0;
-}
-
-/*
- * onClick: perform the toggling (via Plotly.update) implied by clicking
- * at this hoverData
- *
- * gd: graphDiv
- * hoverData: a hoverData array, as included with the *plotly_hover* or
- *     *plotly_click* events in the `points` attribute
- *
- * returns: Promise that the update is complete
- */
-function onClick(gd, hoverData) {
-    var toggleSets = getToggleSets(gd, hoverData),
-        onSet = toggleSets.on,
-        offSet = toggleSets.off.concat(toggleSets.explicitOff),
-        update = {},
-        i;
-
-    if(!(onSet.length || offSet.length)) return;
-
-    for(i = 0; i < onSet.length; i++) {
-        update['annotations[' + onSet[i] + '].visible'] = true;
-    }
-
-    for(i = 0; i < offSet.length; i++) {
-        update['annotations[' + offSet[i] + '].visible'] = false;
-    }
-
-    return Plotly.update(gd, {}, update);
-}
-
-/*
- * getToggleSets: find the annotations which will turn on or off at this
- * hoverData
- *
- * gd: graphDiv
- * hoverData: a hoverData array, as included with the *plotly_hover* or
- *     *plotly_click* events in the `points` attribute
- *
- * returns: {
- *   on: Array (indices of annotations to turn on),
- *   off: Array (indices to turn off because you're not hovering on them),
- *   explicitOff: Array (indices to turn off because you *are* hovering on them)
- * }
- */
-function getToggleSets(gd, hoverData) {
-    var annotations = gd._fullLayout.annotations,
-        onSet = [],
-        offSet = [],
-        explicitOffSet = [],
-        hoverLen = (hoverData || []).length;
-
-    var i, j, anni, showMode, pointj, toggleType;
-
-    for(i = 0; i < annotations.length; i++) {
-        anni = annotations[i];
-        showMode = anni.clicktoshow;
-        if(showMode) {
-            for(j = 0; j < hoverLen; j++) {
-                pointj = hoverData[j];
-                if(pointj.x === anni._xclick && pointj.y === anni._yclick &&
-                        pointj.xaxis._id === anni.xref &&
-                        pointj.yaxis._id === anni.yref) {
-                    // match! toggle this annotation
-                    // regardless of its clicktoshow mode
-                    // but if it's onout mode, off is implicit
-                    if(anni.visible) {
-                        if(showMode === 'onout') toggleType = offSet;
-                        else toggleType = explicitOffSet;
-                    }
-                    else {
-                        toggleType = onSet;
-                    }
-                    toggleType.push(i);
-                    break;
-                }
-            }
-
-            if(j === hoverLen) {
-                // no match - only turn this annotation OFF, and only if
-                // showmode is 'onout'
-                if(anni.visible && showMode === 'onout') offSet.push(i);
-            }
-        }
-    }
-
-    return {on: onSet, off: offSet, explicitOff: explicitOffSet};
-}
diff --git a/src/components/annotations/defaults.js b/src/components/annotations/defaults.js
index a4e9b9b45df..8b137891791 100644
--- a/src/components/annotations/defaults.js
+++ b/src/components/annotations/defaults.js
@@ -1,23 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var handleArrayContainerDefaults = require('../../plots/array_container_defaults');
-var handleAnnotationDefaults = require('./annotation_defaults');
-
-
-module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) {
-    var opts = {
-        name: 'annotations',
-        handleItemDefaults: handleAnnotationDefaults
-    };
-
-    handleArrayContainerDefaults(layoutIn, layoutOut, opts);
-};
diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js
index f6cb37b8515..8b137891791 100644
--- a/src/components/annotations/draw.js
+++ b/src/components/annotations/draw.js
@@ -1,775 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var d3 = require('d3');
-var isNumeric = require('fast-isnumeric');
-
-var Plotly = require('../../plotly');
-var Plots = require('../../plots/plots');
-var Lib = require('../../lib');
-var Axes = require('../../plots/cartesian/axes');
-var Color = require('../color');
-var Drawing = require('../drawing');
-var svgTextUtils = require('../../lib/svg_text_utils');
-var setCursor = require('../../lib/setcursor');
-var dragElement = require('../dragelement');
-
-var handleAnnotationDefaults = require('./annotation_defaults');
-var supplyLayoutDefaults = require('./defaults');
-var drawArrowHead = require('./draw_arrow_head');
-
-
-// Annotations are stored in gd.layout.annotations, an array of objects
-// index can point to one item in this array,
-//  or non-numeric to simply add a new one
-//  or -1 to modify all existing
-// opt can be the full options object, or one key (to be set to value)
-//  or undefined to simply redraw
-// if opt is blank, val can be 'add' or a full options object to add a new
-//  annotation at that point in the array, or 'remove' to delete this one
-
-module.exports = {
-    draw: draw,
-    drawOne: drawOne
-};
-
-function draw(gd) {
-    var fullLayout = gd._fullLayout;
-
-    fullLayout._infolayer.selectAll('.annotation').remove();
-
-    for(var i = 0; i < fullLayout.annotations.length; i++) {
-        if(fullLayout.annotations[i].visible) {
-            drawOne(gd, i);
-        }
-    }
-
-    return Plots.previousPromises(gd);
-}
-
-function drawOne(gd, index, opt, value) {
-    var layout = gd.layout,
-        fullLayout = gd._fullLayout,
-        i;
-
-    if(!isNumeric(index) || index === -1) {
-
-        // no index provided - we're operating on ALL annotations
-        if(!index && Array.isArray(value)) {
-            // a whole annotation array is passed in
-            // (as in, redo of delete all)
-            layout.annotations = value;
-            supplyLayoutDefaults(layout, fullLayout);
-            draw(gd);
-            return;
-        }
-        else if(value === 'remove') {
-            // delete all
-            delete layout.annotations;
-            fullLayout.annotations = [];
-            draw(gd);
-            return;
-        }
-        else if(opt && value !== 'add') {
-            // make the same change to all annotations
-            for(i = 0; i < fullLayout.annotations.length; i++) {
-                drawOne(gd, i, opt, value);
-            }
-            return;
-        }
-        else {
-            // add a new empty annotation
-            index = fullLayout.annotations.length;
-            fullLayout.annotations.push({});
-        }
-    }
-
-    if(!opt && value) {
-        if(value === 'remove') {
-            fullLayout._infolayer.selectAll('.annotation[data-index="' + index + '"]')
-                .remove();
-            fullLayout.annotations.splice(index, 1);
-            layout.annotations.splice(index, 1);
-            for(i = index; i < fullLayout.annotations.length; i++) {
-                fullLayout._infolayer
-                    .selectAll('.annotation[data-index="' + (i + 1) + '"]')
-                    .attr('data-index', String(i));
-
-                // redraw all annotations past the removed one,
-                // so they bind to the right events
-                drawOne(gd, i);
-            }
-            return;
-        }
-        else if(value === 'add' || Lib.isPlainObject(value)) {
-            fullLayout.annotations.splice(index, 0, {});
-
-            var rule = Lib.isPlainObject(value) ?
-                    Lib.extendFlat({}, value) :
-                    {text: 'New text'};
-
-            if(layout.annotations) {
-                layout.annotations.splice(index, 0, rule);
-            } else {
-                layout.annotations = [rule];
-            }
-
-            for(i = fullLayout.annotations.length - 1; i > index; i--) {
-                fullLayout._infolayer
-                    .selectAll('.annotation[data-index="' + (i - 1) + '"]')
-                    .attr('data-index', String(i));
-                drawOne(gd, i);
-            }
-        }
-    }
-
-    // remove the existing annotation if there is one
-    fullLayout._infolayer.selectAll('.annotation[data-index="' + index + '"]').remove();
-
-    // remember a few things about what was already there,
-    var optionsIn = layout.annotations[index],
-        oldPrivate = fullLayout.annotations[index];
-
-    // not sure how we're getting here... but C12 is seeing a bug
-    // where we fail here when they add/remove annotations
-    if(!optionsIn) return;
-
-    // alter the input annotation as requested
-    var optionsEdit = {};
-    if(typeof opt === 'string' && opt) optionsEdit[opt] = value;
-    else if(Lib.isPlainObject(opt)) optionsEdit = opt;
-
-    var optionKeys = Object.keys(optionsEdit);
-    for(i = 0; i < optionKeys.length; i++) {
-        var k = optionKeys[i];
-        Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]);
-    }
-
-    // return early in visible: false updates
-    if(optionsIn.visible === false) return;
-
-    var gs = fullLayout._size;
-    var oldRef = {xref: optionsIn.xref, yref: optionsIn.yref};
-
-    var axLetters = ['x', 'y'];
-    for(i = 0; i < 2; i++) {
-        var axLetter = axLetters[i];
-        // if we don't have an explicit position already,
-        // don't set one just because we're changing references
-        // or axis type.
-        // the defaults will be consistent most of the time anyway,
-        // except in log/linear changes
-        if(optionsEdit[axLetter] !== undefined ||
-                optionsIn[axLetter] === undefined) {
-            continue;
-        }
-
-        var axOld = Axes.getFromId(gd, Axes.coerceRef(oldRef, {}, gd, axLetter, '', 'paper')),
-            axNew = Axes.getFromId(gd, Axes.coerceRef(optionsIn, {}, gd, axLetter, '', 'paper')),
-            position = optionsIn[axLetter],
-            axTypeOld = oldPrivate['_' + axLetter + 'type'];
-
-        if(optionsEdit[axLetter + 'ref'] !== undefined) {
-
-            // TODO: include ax / ay / axref / ayref here if not 'pixel'
-            // or even better, move all of this machinery out of here and into
-            // streambed as extra attributes to a regular relayout call
-            // we should do this after v2.0 when it can work equivalently for
-            // annotations, shapes, and images.
-
-            var autoAnchor = optionsIn[axLetter + 'anchor'] === 'auto',
-                plotSize = (axLetter === 'x' ? gs.w : gs.h),
-                halfSizeFrac = (oldPrivate['_' + axLetter + 'size'] || 0) /
-                    (2 * plotSize);
-            if(axOld && axNew) { // data -> different data
-                // go to the same fraction of the axis length
-                // whether or not these axes share a domain
-
-                position = axNew.fraction2r(axOld.r2fraction(position));
-            }
-            else if(axOld) { // data -> paper
-                // first convert to fraction of the axis
-                position = axOld.r2fraction(position);
-
-                // next scale the axis to the whole plot
-                position = axOld.domain[0] +
-                    position * (axOld.domain[1] - axOld.domain[0]);
-
-                // finally see if we need to adjust auto alignment
-                // because auto always means middle / center alignment for data,
-                // but it changes for page alignment based on the closest side
-                if(autoAnchor) {
-                    var posPlus = position + halfSizeFrac,
-                        posMinus = position - halfSizeFrac;
-                    if(position + posMinus < 2 / 3) position = posMinus;
-                    else if(position + posPlus > 4 / 3) position = posPlus;
-                }
-            }
-            else if(axNew) { // paper -> data
-                // first see if we need to adjust auto alignment
-                if(autoAnchor) {
-                    if(position < 1 / 3) position += halfSizeFrac;
-                    else if(position > 2 / 3) position -= halfSizeFrac;
-                }
-
-                // next convert to fraction of the axis
-                position = (position - axNew.domain[0]) /
-                    (axNew.domain[1] - axNew.domain[0]);
-
-                // finally convert to data coordinates
-                position = axNew.fraction2r(position);
-            }
-        }
-
-        if(axNew && axNew === axOld && axTypeOld) {
-            if(axTypeOld === 'log' && axNew.type !== 'log') {
-                position = Math.pow(10, position);
-            }
-            else if(axTypeOld !== 'log' && axNew.type === 'log') {
-                position = (position > 0) ?
-                    Math.log(position) / Math.LN10 : undefined;
-            }
-        }
-
-        optionsIn[axLetter] = position;
-    }
-
-    var options = {};
-    handleAnnotationDefaults(optionsIn, options, fullLayout);
-    fullLayout.annotations[index] = options;
-
-    var xa = Axes.getFromId(gd, options.xref),
-        ya = Axes.getFromId(gd, options.yref),
-
-        // calculated pixel positions
-        // x & y each will get text, head, and tail as appropriate
-        annPosPx = {x: {}, y: {}},
-        textangle = +options.textangle || 0;
-
-    // create the components
-    // made a single group to contain all, so opacity can work right
-    // with border/arrow together this could handle a whole bunch of
-    // cleanup at this point, but works for now
-    var annGroup = fullLayout._infolayer.append('g')
-        .classed('annotation', true)
-        .attr('data-index', String(index))
-        .style('opacity', options.opacity)
-        .on('click', function() {
-            gd._dragging = false;
-            gd.emit('plotly_clickannotation', {
-                index: index,
-                annotation: optionsIn,
-                fullAnnotation: options
-            });
-        });
-
-    // another group for text+background so that they can rotate together
-    var annTextGroup = annGroup.append('g')
-        .classed('annotation-text-g', true)
-        .attr('data-index', String(index));
-
-    var annTextGroupInner = annTextGroup.append('g');
-
-    var borderwidth = options.borderwidth,
-        borderpad = options.borderpad,
-        borderfull = borderwidth + borderpad;
-
-    var annTextBG = annTextGroupInner.append('rect')
-        .attr('class', 'bg')
-        .style('stroke-width', borderwidth + 'px')
-        .call(Color.stroke, options.bordercolor)
-        .call(Color.fill, options.bgcolor);
-
-    var font = options.font;
-
-    var annText = annTextGroupInner.append('text')
-        .classed('annotation', true)
-        .attr('data-unformatted', options.text)
-        .text(options.text);
-
-    function textLayout(s) {
-        s.call(Drawing.font, font)
-        .attr({
-            'text-anchor': {
-                left: 'start',
-                right: 'end'
-            }[options.align] || 'middle'
-        });
-
-        svgTextUtils.convertToTspans(s, drawGraphicalElements);
-        return s;
-    }
-
-    function drawGraphicalElements() {
-
-        // make sure lines are aligned the way they will be
-        // at the end, even if their position changes
-        annText.selectAll('tspan.line').attr({y: 0, x: 0});
-
-        var mathjaxGroup = annTextGroupInner.select('.annotation-math-group'),
-            hasMathjax = !mathjaxGroup.empty(),
-            anntextBB = Drawing.bBox(
-                (hasMathjax ? mathjaxGroup : annText).node()),
-            annwidth = anntextBB.width,
-            annheight = anntextBB.height,
-            outerwidth = Math.round(annwidth + 2 * borderfull),
-            outerheight = Math.round(annheight + 2 * borderfull);
-
-
-        // save size in the annotation object for use by autoscale
-        options._w = annwidth;
-        options._h = annheight;
-
-        function shiftFraction(v, anchor) {
-            if(anchor === 'auto') {
-                if(v < 1 / 3) anchor = 'left';
-                else if(v > 2 / 3) anchor = 'right';
-                else anchor = 'center';
-            }
-            return {
-                center: 0,
-                middle: 0,
-                left: 0.5,
-                bottom: -0.5,
-                right: -0.5,
-                top: 0.5
-            }[anchor];
-        }
-
-        var annotationIsOffscreen = false;
-        ['x', 'y'].forEach(function(axLetter) {
-            var axRef = options[axLetter + 'ref'] || axLetter,
-                tailRef = options['a' + axLetter + 'ref'],
-                ax = Axes.getFromId(gd, axRef),
-                dimAngle = (textangle + (axLetter === 'x' ? 0 : -90)) * Math.PI / 180,
-                // note that these two can be either positive or negative
-                annSizeFromWidth = outerwidth * Math.cos(dimAngle),
-                annSizeFromHeight = outerheight * Math.sin(dimAngle),
-                // but this one is the positive total size
-                annSize = Math.abs(annSizeFromWidth) + Math.abs(annSizeFromHeight),
-                anchor = options[axLetter + 'anchor'],
-                posPx = annPosPx[axLetter],
-                basePx,
-                textPadShift,
-                alignPosition,
-                autoAlignFraction,
-                textShift;
-
-            /*
-             * calculate the *primary* pixel position
-             * which is the arrowhead if there is one,
-             * otherwise the text anchor point
-             */
-            if(ax) {
-                /*
-                 * hide the annotation if it's pointing outside the visible plot
-                 * as long as the axis isn't autoranged - then we need to draw it
-                 * anyway to get its bounding box. When we're dragging, an axis can
-                 * still look autoranged even though it won't be when the drag finishes.
-                 */
-                var posFraction = ax.r2fraction(options[axLetter]);
-                if((gd._dragging || !ax.autorange) && (posFraction < 0 || posFraction > 1)) {
-                    if(tailRef === axRef) {
-                        posFraction = ax.r2fraction(options['a' + axLetter]);
-                        if(posFraction < 0 || posFraction > 1) {
-                            annotationIsOffscreen = true;
-                        }
-                    }
-                    else {
-                        annotationIsOffscreen = true;
-                    }
-
-                    if(annotationIsOffscreen) return;
-                }
-                basePx = ax._offset + ax.r2p(options[axLetter]);
-                autoAlignFraction = 0.5;
-            }
-            else {
-                if(axLetter === 'x') {
-                    alignPosition = options[axLetter];
-                    basePx = gs.l + gs.w * alignPosition;
-                }
-                else {
-                    alignPosition = 1 - options[axLetter];
-                    basePx = gs.t + gs.h * alignPosition;
-                }
-                autoAlignFraction = options.showarrow ? 0.5 : alignPosition;
-            }
-
-            // now translate this into pixel positions of head, tail, and text
-            // as well as paddings for autorange
-            if(options.showarrow) {
-                posPx.head = basePx;
-
-                var arrowLength = options['a' + axLetter];
-
-                // with an arrow, the text rotates around the anchor point
-                textShift = annSizeFromWidth * shiftFraction(0.5, options.xanchor) -
-                    annSizeFromHeight * shiftFraction(0.5, options.yanchor);
-
-                if(tailRef === axRef) {
-                    posPx.tail = ax._offset + ax.r2p(arrowLength);
-                    // tail is data-referenced: autorange pads the text in px from the tail
-                    textPadShift = textShift;
-                }
-                else {
-                    posPx.tail = basePx + arrowLength;
-                    // tail is specified in px from head, so autorange also pads vs head
-                    textPadShift = textShift + arrowLength;
-                }
-
-                posPx.text = posPx.tail + textShift;
-
-                // constrain pixel/paper referenced so the draggers are at least
-                // partially visible
-                var maxPx = fullLayout[(axLetter === 'x') ? 'width' : 'height'];
-                if(axRef === 'paper') {
-                    posPx.head = Lib.constrain(posPx.head, 1, maxPx - 1);
-                }
-                if(tailRef === 'pixel') {
-                    var shiftPlus = -Math.max(posPx.tail - 3, posPx.text),
-                        shiftMinus = Math.min(posPx.tail + 3, posPx.text) - maxPx;
-                    if(shiftPlus > 0) {
-                        posPx.tail += shiftPlus;
-                        posPx.text += shiftPlus;
-                    }
-                    else if(shiftMinus > 0) {
-                        posPx.tail -= shiftMinus;
-                        posPx.text -= shiftMinus;
-                    }
-                }
-            }
-            else {
-                // with no arrow, the text rotates and *then* we put the anchor
-                // relative to the new bounding box
-                textShift = annSize * shiftFraction(autoAlignFraction, anchor);
-                textPadShift = textShift;
-                posPx.text = basePx + textShift;
-            }
-
-            options['_' + axLetter + 'padplus'] = (annSize / 2) + textPadShift;
-            options['_' + axLetter + 'padminus'] = (annSize / 2) - textPadShift;
-
-            // save the current axis type for later log/linear changes
-            options['_' + axLetter + 'type'] = ax && ax.type;
-        });
-
-        if(annotationIsOffscreen) {
-            annTextGroupInner.remove();
-            return;
-        }
-
-        if(hasMathjax) {
-            mathjaxGroup.select('svg').attr({x: borderfull - 1, y: borderfull});
-        }
-        else {
-            var texty = borderfull - anntextBB.top,
-                textx = borderfull - anntextBB.left;
-            annText.attr({x: textx, y: texty});
-            annText.selectAll('tspan.line').attr({y: texty, x: textx});
-        }
-
-        annTextBG.call(Drawing.setRect, borderwidth / 2, borderwidth / 2,
-            outerwidth - borderwidth, outerheight - borderwidth);
-
-        annTextGroupInner.call(Drawing.setTranslate,
-            Math.round(annPosPx.x.text - outerwidth / 2),
-            Math.round(annPosPx.y.text - outerheight / 2));
-
-        /*
-         * rotate text and background
-         * we already calculated the text center position *as rotated*
-         * because we needed that for autoranging anyway, so now whether
-         * we have an arrow or not, we rotate about the text center.
-         */
-        annTextGroup.attr({transform: 'rotate(' + textangle + ',' +
-                            annPosPx.x.text + ',' + annPosPx.y.text + ')'});
-
-        var annbase = 'annotations[' + index + ']';
-
-        /*
-         * add the arrow
-         * uses options[arrowwidth,arrowcolor,arrowhead] for styling
-         * dx and dy are normally zero, but when you are dragging the textbox
-         * while the head stays put, dx and dy are the pixel offsets
-         */
-        var drawArrow = function(dx, dy) {
-            d3.select(gd)
-                .selectAll('.annotation-arrow-g[data-index="' + index + '"]')
-                .remove();
-
-            var headX = annPosPx.x.head,
-                headY = annPosPx.y.head,
-                tailX = annPosPx.x.tail + dx,
-                tailY = annPosPx.y.tail + dy,
-                textX = annPosPx.x.text + dx,
-                textY = annPosPx.y.text + dy,
-
-                // find the edge of the text box, where we'll start the arrow:
-                // create transform matrix to rotate the text box corners
-                transform = Lib.rotationXYMatrix(textangle, textX, textY),
-                applyTransform = Lib.apply2DTransform(transform),
-                applyTransform2 = Lib.apply2DTransform2(transform),
-
-                // calculate and transform bounding box
-                width = +annTextBG.attr('width'),
-                height = +annTextBG.attr('height'),
-                xLeft = textX - 0.5 * width,
-                xRight = xLeft + width,
-                yTop = textY - 0.5 * height,
-                yBottom = yTop + height,
-                edges = [
-                    [xLeft, yTop, xLeft, yBottom],
-                    [xLeft, yBottom, xRight, yBottom],
-                    [xRight, yBottom, xRight, yTop],
-                    [xRight, yTop, xLeft, yTop]
-                ].map(applyTransform2);
-
-            // Remove the line if it ends inside the box.  Use ray
-            // casting for rotated boxes: see which edges intersect a
-            // line from the arrowhead to far away and reduce with xor
-            // to get the parity of the number of intersections.
-            if(edges.reduce(function(a, x) {
-                return a ^
-                    !!lineIntersect(headX, headY, headX + 1e6, headY + 1e6,
-                            x[0], x[1], x[2], x[3]);
-            }, false)) {
-                // no line or arrow - so quit drawArrow now
-                return;
-            }
-
-            edges.forEach(function(x) {
-                var p = lineIntersect(tailX, tailY, headX, headY,
-                            x[0], x[1], x[2], x[3]);
-                if(p) {
-                    tailX = p.x;
-                    tailY = p.y;
-                }
-            });
-
-            var strokewidth = options.arrowwidth,
-                arrowColor = options.arrowcolor;
-
-            var arrowGroup = annGroup.append('g')
-                .style({opacity: Color.opacity(arrowColor)})
-                .classed('annotation-arrow-g', true)
-                .attr('data-index', String(index));
-
-            var arrow = arrowGroup.append('path')
-                .attr('d', 'M' + tailX + ',' + tailY + 'L' + headX + ',' + headY)
-                .style('stroke-width', strokewidth + 'px')
-                .call(Color.stroke, Color.rgb(arrowColor));
-
-            drawArrowHead(arrow, options.arrowhead, 'end', options.arrowsize, options.standoff);
-
-            // the arrow dragger is a small square right at the head, then a line to the tail,
-            // all expanded by a stroke width of 6px plus the arrow line width
-            if(gd._context.editable && arrow.node().parentNode) {
-                var arrowDragHeadX = headX;
-                var arrowDragHeadY = headY;
-                if(options.standoff) {
-                    var arrowLength = Math.sqrt(Math.pow(headX - tailX, 2) + Math.pow(headY - tailY, 2));
-                    arrowDragHeadX += options.standoff * (tailX - headX) / arrowLength;
-                    arrowDragHeadY += options.standoff * (tailY - headY) / arrowLength;
-                }
-                var arrowDrag = arrowGroup.append('path')
-                    .classed('annotation', true)
-                    .classed('anndrag', true)
-                    .attr({
-                        'data-index': String(index),
-                        d: 'M3,3H-3V-3H3ZM0,0L' + (tailX - arrowDragHeadX) + ',' + (tailY - arrowDragHeadY),
-                        transform: 'translate(' + arrowDragHeadX + ',' + arrowDragHeadY + ')'
-                    })
-                    .style('stroke-width', (strokewidth + 6) + 'px')
-                    .call(Color.stroke, 'rgba(0,0,0,0)')
-                    .call(Color.fill, 'rgba(0,0,0,0)');
-
-                var update,
-                    annx0,
-                    anny0;
-
-                // dragger for the arrow & head: translates the whole thing
-                // (head/tail/text) all together
-                dragElement.init({
-                    element: arrowDrag.node(),
-                    prepFn: function() {
-                        var pos = Drawing.getTranslate(annTextGroupInner);
-
-                        annx0 = pos.x;
-                        anny0 = pos.y;
-                        update = {};
-                        if(xa && xa.autorange) {
-                            update[xa._name + '.autorange'] = true;
-                        }
-                        if(ya && ya.autorange) {
-                            update[ya._name + '.autorange'] = true;
-                        }
-                    },
-                    moveFn: function(dx, dy) {
-                        var annxy0 = applyTransform(annx0, anny0),
-                            xcenter = annxy0[0] + dx,
-                            ycenter = annxy0[1] + dy;
-                        annTextGroupInner.call(Drawing.setTranslate, xcenter, ycenter);
-
-                        update[annbase + '.x'] = xa ?
-                            xa.p2r(xa.r2p(options.x) + dx) :
-                            ((headX + dx - gs.l) / gs.w);
-                        update[annbase + '.y'] = ya ?
-                            ya.p2r(ya.r2p(options.y) + dy) :
-                            (1 - ((headY + dy - gs.t) / gs.h));
-
-                        if(options.axref === options.xref) {
-                            update[annbase + '.ax'] = xa ?
-                                xa.p2r(xa.r2p(options.ax) + dx) :
-                                ((headX + dx - gs.l) / gs.w);
-                        }
-
-                        if(options.ayref === options.yref) {
-                            update[annbase + '.ay'] = ya ?
-                                ya.p2r(ya.r2p(options.ay) + dy) :
-                                (1 - ((headY + dy - gs.t) / gs.h));
-                        }
-
-                        arrowGroup.attr('transform', 'translate(' + dx + ',' + dy + ')');
-                        annTextGroup.attr({
-                            transform: 'rotate(' + textangle + ',' +
-                                   xcenter + ',' + ycenter + ')'
-                        });
-                    },
-                    doneFn: function(dragged) {
-                        if(dragged) {
-                            Plotly.relayout(gd, update);
-                            var notesBox = document.querySelector('.js-notes-box-panel');
-                            if(notesBox) notesBox.redraw(notesBox.selectedObj);
-                        }
-                    }
-                });
-            }
-        };
-
-        if(options.showarrow) drawArrow(0, 0);
-
-        // user dragging the annotation (text, not arrow)
-        if(gd._context.editable) {
-            var update,
-                baseTextTransform;
-
-            // dragger for the textbox: if there's an arrow, just drag the
-            // textbox and tail, leave the head untouched
-            dragElement.init({
-                element: annTextGroupInner.node(),
-                prepFn: function() {
-                    baseTextTransform = annTextGroup.attr('transform');
-                    update = {};
-                },
-                moveFn: function(dx, dy) {
-                    var csr = 'pointer';
-                    if(options.showarrow) {
-                        if(options.axref === options.xref) {
-                            update[annbase + '.ax'] = xa.p2r(xa.r2p(options.ax) + dx);
-                        } else {
-                            update[annbase + '.ax'] = options.ax + dx;
-                        }
-
-                        if(options.ayref === options.yref) {
-                            update[annbase + '.ay'] = ya.p2r(ya.r2p(options.ay) + dy);
-                        } else {
-                            update[annbase + '.ay'] = options.ay + dy;
-                        }
-
-                        drawArrow(dx, dy);
-                    }
-                    else {
-                        if(xa) update[annbase + '.x'] = options.x + dx / xa._m;
-                        else {
-                            var widthFraction = options._xsize / gs.w,
-                                xLeft = options.x + options._xshift / gs.w - widthFraction / 2;
-
-                            update[annbase + '.x'] = dragElement.align(xLeft + dx / gs.w,
-                                widthFraction, 0, 1, options.xanchor);
-                        }
-
-                        if(ya) update[annbase + '.y'] = options.y + dy / ya._m;
-                        else {
-                            var heightFraction = options._ysize / gs.h,
-                                yBottom = options.y - options._yshift / gs.h - heightFraction / 2;
-
-                            update[annbase + '.y'] = dragElement.align(yBottom - dy / gs.h,
-                                heightFraction, 0, 1, options.yanchor);
-                        }
-                        if(!xa || !ya) {
-                            csr = dragElement.getCursor(
-                                xa ? 0.5 : update[annbase + '.x'],
-                                ya ? 0.5 : update[annbase + '.y'],
-                                options.xanchor, options.yanchor
-                            );
-                        }
-                    }
-
-                    annTextGroup.attr({
-                        transform: 'translate(' + dx + ',' + dy + ')' + baseTextTransform
-                    });
-
-                    setCursor(annTextGroupInner, csr);
-                },
-                doneFn: function(dragged) {
-                    setCursor(annTextGroupInner);
-                    if(dragged) {
-                        Plotly.relayout(gd, update);
-                        var notesBox = document.querySelector('.js-notes-box-panel');
-                        if(notesBox) notesBox.redraw(notesBox.selectedObj);
-                    }
-                }
-            });
-        }
-    }
-
-    if(gd._context.editable) {
-        annText.call(svgTextUtils.makeEditable, annTextGroupInner)
-            .call(textLayout)
-            .on('edit', function(_text) {
-                options.text = _text;
-                this.attr({'data-unformatted': options.text});
-                this.call(textLayout);
-                var update = {};
-                update['annotations[' + index + '].text'] = options.text;
-                if(xa && xa.autorange) {
-                    update[xa._name + '.autorange'] = true;
-                }
-                if(ya && ya.autorange) {
-                    update[ya._name + '.autorange'] = true;
-                }
-                Plotly.relayout(gd, update);
-            });
-    }
-    else annText.call(textLayout);
-}
-
-// look for intersection of two line segments
-//   (1->2 and 3->4) - returns array [x,y] if they do, null if not
-function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
-    var a = x2 - x1,
-        b = x3 - x1,
-        c = x4 - x3,
-        d = y2 - y1,
-        e = y3 - y1,
-        f = y4 - y3,
-        det = a * f - c * d;
-    // parallel lines? intersection is undefined
-    // ignore the case where they are colinear
-    if(det === 0) return null;
-    var t = (b * f - c * e) / det,
-        u = (b * d - a * e) / det;
-    // segments do not intersect?
-    if(u < 0 || u > 1 || t < 0 || t > 1) return null;
-
-    return {x: x1 + a * t, y: y1 + d * t};
-}
diff --git a/src/components/annotations/draw_arrow_head.js b/src/components/annotations/draw_arrow_head.js
index 69e5181914c..8b137891791 100644
--- a/src/components/annotations/draw_arrow_head.js
+++ b/src/components/annotations/draw_arrow_head.js
@@ -1,132 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var d3 = require('d3');
-var isNumeric = require('fast-isnumeric');
-
-var Color = require('../color');
-var Drawing = require('../drawing');
-
-var ARROWPATHS = require('./arrow_paths');
-
-// add arrowhead(s) to a path or line d3 element el3
-// style: 1-6, first 5 are pointers, 6 is circle, 7 is square, 8 is none
-// ends is 'start', 'end' (default), 'start+end'
-// mag is magnification vs. default (default 1)
-
-module.exports = function drawArrowHead(el3, style, ends, mag, standoff) {
-    if(!isNumeric(mag)) mag = 1;
-    var el = el3.node(),
-        headStyle = ARROWPATHS[style||0];
-
-    if(typeof ends !== 'string' || !ends) ends = 'end';
-
-    var scale = (Drawing.getPx(el3, 'stroke-width') || 1) * mag,
-        stroke = el3.style('stroke') || Color.defaultLine,
-        opacity = el3.style('stroke-opacity') || 1,
-        doStart = ends.indexOf('start') >= 0,
-        doEnd = ends.indexOf('end') >= 0,
-        backOff = headStyle.backoff * scale + standoff,
-        start,
-        end,
-        startRot,
-        endRot;
-
-    if(el.nodeName === 'line') {
-        start = {x: +el3.attr('x1'), y: +el3.attr('y1')};
-        end = {x: +el3.attr('x2'), y: +el3.attr('y2')};
-
-        var dx = start.x - end.x,
-            dy = start.y - end.y;
-
-        startRot = Math.atan2(dy, dx);
-        endRot = startRot + Math.PI;
-        if(backOff) {
-            if(backOff * backOff > dx * dx + dy * dy) {
-                hideLine();
-                return;
-            }
-            var backOffX = backOff * Math.cos(startRot),
-                backOffY = backOff * Math.sin(startRot);
-
-            if(doStart) {
-                start.x -= backOffX;
-                start.y -= backOffY;
-                el3.attr({x1: start.x, y1: start.y});
-            }
-            if(doEnd) {
-                end.x += backOffX;
-                end.y += backOffY;
-                el3.attr({x2: end.x, y2: end.y});
-            }
-        }
-    }
-    else if(el.nodeName === 'path') {
-        var pathlen = el.getTotalLength(),
-            // using dash to hide the backOff region of the path.
-            // if we ever allow dash for the arrow we'll have to
-            // do better than this hack... maybe just manually
-            // combine the two
-            dashArray = '';
-
-        if(pathlen < backOff) {
-            hideLine();
-            return;
-        }
-
-        if(doStart) {
-            var start0 = el.getPointAtLength(0),
-                dstart = el.getPointAtLength(0.1);
-            startRot = Math.atan2(start0.y - dstart.y, start0.x - dstart.x);
-            start = el.getPointAtLength(Math.min(backOff, pathlen));
-            if(backOff) dashArray = '0px,' + backOff + 'px,';
-        }
-
-        if(doEnd) {
-            var end0 = el.getPointAtLength(pathlen),
-                dend = el.getPointAtLength(pathlen - 0.1);
-            endRot = Math.atan2(end0.y - dend.y, end0.x - dend.x);
-            end = el.getPointAtLength(Math.max(0, pathlen - backOff));
-
-            if(backOff) {
-                var shortening = dashArray ? 2 * backOff : backOff;
-                dashArray += (pathlen - shortening) + 'px,' + pathlen + 'px';
-            }
-        }
-        else if(dashArray) dashArray += pathlen + 'px';
-
-        if(dashArray) el3.style('stroke-dasharray', dashArray);
-    }
-
-    function hideLine() { el3.style('stroke-dasharray', '0px,100px'); }
-
-    function drawhead(p, rot) {
-        if(!headStyle.path) return;
-        if(style > 5) rot = 0; // don't rotate square or circle
-        d3.select(el.parentElement).append('path')
-            .attr({
-                'class': el3.attr('class'),
-                d: headStyle.path,
-                transform:
-                    'translate(' + p.x + ',' + p.y + ')' +
-                    'rotate(' + (rot * 180 / Math.PI) + ')' +
-                    'scale(' + scale + ')'
-            })
-            .style({
-                fill: stroke,
-                opacity: opacity,
-                'stroke-width': 0
-            });
-    }
-
-    if(doStart) drawhead(start, startRot);
-    if(doEnd) drawhead(end, endRot);
-};
diff --git a/src/components/annotations/index.js b/src/components/annotations/index.js
index bb32b6b69df..8b137891791 100644
--- a/src/components/annotations/index.js
+++ b/src/components/annotations/index.js
@@ -1,28 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var drawModule = require('./draw');
-var clickModule = require('./click');
-
-module.exports = {
-    moduleType: 'component',
-    name: 'annotations',
-
-    layoutAttributes: require('./attributes'),
-    supplyLayoutDefaults: require('./defaults'),
-
-    calcAutorange: require('./calc_autorange'),
-    draw: drawModule.draw,
-    drawOne: drawModule.drawOne,
-
-    hasClickToShow: clickModule.hasClickToShow,
-    onClick: clickModule.onClick
-};
diff --git a/src/components/calendars/calendars.js b/src/components/calendars/calendars.js
index 2f2a1dec9d5..8b137891791 100644
--- a/src/components/calendars/calendars.js
+++ b/src/components/calendars/calendars.js
@@ -1,31 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-// a trimmed down version of:
-// https://github.com/alexcjohnson/world-calendars/blob/master/dist/index.js
-
-module.exports = require('world-calendars/dist/main');
-
-require('world-calendars/dist/plus');
-
-require('world-calendars/dist/calendars/chinese');
-require('world-calendars/dist/calendars/coptic');
-require('world-calendars/dist/calendars/discworld');
-require('world-calendars/dist/calendars/ethiopian');
-require('world-calendars/dist/calendars/hebrew');
-require('world-calendars/dist/calendars/islamic');
-require('world-calendars/dist/calendars/julian');
-require('world-calendars/dist/calendars/mayan');
-require('world-calendars/dist/calendars/nanakshahi');
-require('world-calendars/dist/calendars/nepali');
-require('world-calendars/dist/calendars/persian');
-require('world-calendars/dist/calendars/taiwan');
-require('world-calendars/dist/calendars/thai');
-require('world-calendars/dist/calendars/ummalqura');
diff --git a/src/components/calendars/index.js b/src/components/calendars/index.js
index eca51c1ac8a..8b137891791 100644
--- a/src/components/calendars/index.js
+++ b/src/components/calendars/index.js
@@ -1,257 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-var calendars = require('./calendars');
-
-var Lib = require('../../lib');
-var constants = require('../../constants/numerical');
-
-var EPOCHJD = constants.EPOCHJD;
-var ONEDAY = constants.ONEDAY;
-
-var attributes = {
-    valType: 'enumerated',
-    values: Object.keys(calendars.calendars),
-    role: 'info',
-    dflt: 'gregorian'
-};
-
-var handleDefaults = function(contIn, contOut, attr, dflt) {
-    var attrs = {};
-    attrs[attr] = attributes;
-
-    return Lib.coerce(contIn, contOut, attrs, attr, dflt);
-};
-
-var handleTraceDefaults = function(traceIn, traceOut, coords, layout) {
-    for(var i = 0; i < coords.length; i++) {
-        handleDefaults(traceIn, traceOut, coords[i] + 'calendar', layout.calendar);
-    }
-};
-
-// each calendar needs its own default canonical tick. I would love to use
-// 2000-01-01 (or even 0000-01-01) for them all but they don't necessarily
-// all support either of those dates. Instead I'll use the most significant
-// number they *do* support, biased toward the present day.
-var CANONICAL_TICK = {
-    chinese: '2000-01-01',
-    coptic: '2000-01-01',
-    discworld: '2000-01-01',
-    ethiopian: '2000-01-01',
-    hebrew: '5000-01-01',
-    islamic: '1000-01-01',
-    julian: '2000-01-01',
-    mayan: '5000-01-01',
-    nanakshahi: '1000-01-01',
-    nepali: '2000-01-01',
-    persian: '1000-01-01',
-    jalali: '1000-01-01',
-    taiwan: '1000-01-01',
-    thai: '2000-01-01',
-    ummalqura: '1400-01-01'
-};
-
-// Start on a Sunday - for week ticks
-// Discworld and Mayan calendars don't have 7-day weeks but we're going to give them
-// 7-day week ticks so start on our Sundays.
-// If anyone really cares we can customize the auto tick spacings for these calendars.
-var CANONICAL_SUNDAY = {
-    chinese: '2000-01-02',
-    coptic: '2000-01-03',
-    discworld: '2000-01-03',
-    ethiopian: '2000-01-05',
-    hebrew: '5000-01-01',
-    islamic: '1000-01-02',
-    julian: '2000-01-03',
-    mayan: '5000-01-01',
-    nanakshahi: '1000-01-05',
-    nepali: '2000-01-05',
-    persian: '1000-01-01',
-    jalali: '1000-01-01',
-    taiwan: '1000-01-04',
-    thai: '2000-01-04',
-    ummalqura: '1400-01-06'
-};
-
-var DFLTRANGE = {
-    chinese: ['2000-01-01', '2001-01-01'],
-    coptic: ['1700-01-01', '1701-01-01'],
-    discworld: ['1800-01-01', '1801-01-01'],
-    ethiopian: ['2000-01-01', '2001-01-01'],
-    hebrew: ['5700-01-01', '5701-01-01'],
-    islamic: ['1400-01-01', '1401-01-01'],
-    julian: ['2000-01-01', '2001-01-01'],
-    mayan: ['5200-01-01', '5201-01-01'],
-    nanakshahi: ['0500-01-01', '0501-01-01'],
-    nepali: ['2000-01-01', '2001-01-01'],
-    persian: ['1400-01-01', '1401-01-01'],
-    jalali: ['1400-01-01', '1401-01-01'],
-    taiwan: ['0100-01-01', '0101-01-01'],
-    thai: ['2500-01-01', '2501-01-01'],
-    ummalqura: ['1400-01-01', '1401-01-01']
-};
-
-/*
- * convert d3 templates to world-calendars templates, so our users only need
- * to know d3's specifiers. Map space padding to no padding, and unknown fields
- * to an ugly placeholder
- */
-var UNKNOWN = '##';
-var d3ToWorldCalendars = {
-    'd': {'0': 'dd', '-': 'd'}, // 2-digit or unpadded day of month
-    'e': {'0': 'd', '-': 'd'}, // alternate, always unpadded day of month
-    'a': {'0': 'D', '-': 'D'}, // short weekday name
-    'A': {'0': 'DD', '-': 'DD'}, // full weekday name
-    'j': {'0': 'oo', '-': 'o'}, // 3-digit or unpadded day of the year
-    'W': {'0': 'ww', '-': 'w'}, // 2-digit or unpadded week of the year (Monday first)
-    'm': {'0': 'mm', '-': 'm'}, // 2-digit or unpadded month number
-    'b': {'0': 'M', '-': 'M'}, // short month name
-    'B': {'0': 'MM', '-': 'MM'}, // full month name
-    'y': {'0': 'yy', '-': 'yy'}, // 2-digit year (map unpadded to zero-padded)
-    'Y': {'0': 'yyyy', '-': 'yyyy'}, // 4-digit year (map unpadded to zero-padded)
-    'U': UNKNOWN, // Sunday-first week of the year
-    'w': UNKNOWN, // day of the week [0(sunday),6]
-    // combined format, we replace the date part with the world-calendar version
-    // and the %X stays there for d3 to handle with time parts
-    'c': {'0': 'D M d %X yyyy', '-': 'D M d %X yyyy'},
-    'x': {'0': 'mm/dd/yyyy', '-': 'mm/dd/yyyy'}
-};
-
-function worldCalFmt(fmt, x, calendar) {
-    var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD,
-        cDate = getCal(calendar).fromJD(dateJD),
-        i = 0,
-        modifier, directive, directiveLen, directiveObj, replacementPart;
-    while((i = fmt.indexOf('%', i)) !== -1) {
-        modifier = fmt.charAt(i + 1);
-        if(modifier === '0' || modifier === '-' || modifier === '_') {
-            directiveLen = 3;
-            directive = fmt.charAt(i + 2);
-            if(modifier === '_') modifier = '-';
-        }
-        else {
-            directive = modifier;
-            modifier = '0';
-            directiveLen = 2;
-        }
-        directiveObj = d3ToWorldCalendars[directive];
-        if(!directiveObj) {
-            i += directiveLen;
-        }
-        else {
-            // code is recognized as a date part but world-calendars doesn't support it
-            if(directiveObj === UNKNOWN) replacementPart = UNKNOWN;
-
-            // format the cDate according to the translated directive
-            else replacementPart = cDate.formatDate(directiveObj[modifier]);
-
-            fmt = fmt.substr(0, i) + replacementPart + fmt.substr(i + directiveLen);
-            i += replacementPart.length;
-        }
-    }
-    return fmt;
-}
-
-// cache world calendars, so we don't have to reinstantiate
-// during each date-time conversion
-var allCals = {};
-function getCal(calendar) {
-    var calendarObj = allCals[calendar];
-    if(calendarObj) return calendarObj;
-
-    calendarObj = allCals[calendar] = calendars.instance(calendar);
-    return calendarObj;
-}
-
-function makeAttrs(description) {
-    return Lib.extendFlat({}, attributes, { description: description });
-}
-
-function makeTraceAttrsDescription(coord) {
-    return 'Sets the calendar system to use with `' + coord + '` date data.';
-}
-
-var xAttrs = {
-    xcalendar: makeAttrs(makeTraceAttrsDescription('x'))
-};
-
-var xyAttrs = Lib.extendFlat({}, xAttrs, {
-    ycalendar: makeAttrs(makeTraceAttrsDescription('y'))
-});
-
-var xyzAttrs = Lib.extendFlat({}, xyAttrs, {
-    zcalendar: makeAttrs(makeTraceAttrsDescription('z'))
-});
-
-var axisAttrs = makeAttrs([
-    'Sets the calendar system to use for `range` and `tick0`',
-    'if this is a date axis. This does not set the calendar for',
-    'interpreting data on this axis, that\'s specified in the trace',
-    'or via the global `layout.calendar`'
-].join(' '));
-
-module.exports = {
-    moduleType: 'component',
-    name: 'calendars',
-
-    schema: {
-        traces: {
-            scatter: xyAttrs,
-            bar: xyAttrs,
-            heatmap: xyAttrs,
-            contour: xyAttrs,
-            histogram: xyAttrs,
-            histogram2d: xyAttrs,
-            histogram2dcontour: xyAttrs,
-            scatter3d: xyzAttrs,
-            surface: xyzAttrs,
-            mesh3d: xyzAttrs,
-            scattergl: xyAttrs,
-            ohlc: xAttrs,
-            candlestick: xAttrs
-        },
-        layout: {
-            calendar: makeAttrs([
-                'Sets the default calendar system to use for interpreting and',
-                'displaying dates throughout the plot.'
-            ].join(' ')),
-            'xaxis.calendar': axisAttrs,
-            'yaxis.calendar': axisAttrs,
-            'scene.xaxis.calendar': axisAttrs,
-            'scene.yaxis.calendar': axisAttrs,
-            'scene.zaxis.calendar': axisAttrs
-        },
-        transforms: {
-            filter: {
-                valuecalendar: makeAttrs([
-                    'Sets the calendar system to use for `value`, if it is a date.'
-                ].join(' ')),
-                targetcalendar: makeAttrs([
-                    'Sets the calendar system to use for `target`, if it is an',
-                    'array of dates. If `target` is a string (eg *x*) we use the',
-                    'corresponding trace attribute (eg `xcalendar`) if it exists,',
-                    'even if `targetcalendar` is provided.'
-                ].join(' '))
-            }
-        }
-    },
-
-    layoutAttributes: attributes,
-
-    handleDefaults: handleDefaults,
-    handleTraceDefaults: handleTraceDefaults,
-
-    CANONICAL_SUNDAY: CANONICAL_SUNDAY,
-    CANONICAL_TICK: CANONICAL_TICK,
-    DFLTRANGE: DFLTRANGE,
-
-    getCal: getCal,
-    worldCalFmt: worldCalFmt
-};
diff --git a/src/components/color/attributes.js b/src/components/color/attributes.js
index e4a5c6d2c35..8b137891791 100644
--- a/src/components/color/attributes.js
+++ b/src/components/color/attributes.js
@@ -1,38 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-
-// IMPORTANT - default colors should be in hex for compatibility
-exports.defaults = [
-    '#1f77b4',  // muted blue
-    '#ff7f0e',  // safety orange
-    '#2ca02c',  // cooked asparagus green
-    '#d62728',  // brick red
-    '#9467bd',  // muted purple
-    '#8c564b',  // chestnut brown
-    '#e377c2',  // raspberry yogurt pink
-    '#7f7f7f',  // middle gray
-    '#bcbd22',  // curry yellow-green
-    '#17becf'   // blue-teal
-];
-
-exports.defaultLine = '#444';
-
-exports.lightLine = '#eee';
-
-exports.background = '#fff';
-
-exports.borderLine = '#BEC8D9';
-
-// with axis.color and Color.interp we aren't using lightLine
-// itself anymore, instead interpolating between axis.color
-// and the background color using tinycolor.mix. lightFraction
-// gives back exactly lightLine if the other colors are defaults.
-exports.lightFraction = 100 * (0xe - 0x4) / (0xf - 0x4);
diff --git a/src/components/color/index.js b/src/components/color/index.js
index 714d5a05cad..8b137891791 100644
--- a/src/components/color/index.js
+++ b/src/components/color/index.js
@@ -1,155 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var tinycolor = require('tinycolor2');
-var isNumeric = require('fast-isnumeric');
-
-var color = module.exports = {};
-
-var colorAttrs = require('./attributes');
-color.defaults = colorAttrs.defaults;
-color.defaultLine = colorAttrs.defaultLine;
-color.lightLine = colorAttrs.lightLine;
-color.background = colorAttrs.background;
-
-color.tinyRGB = function(tc) {
-    var c = tc.toRgb();
-    return 'rgb(' + Math.round(c.r) + ', ' +
-        Math.round(c.g) + ', ' + Math.round(c.b) + ')';
-};
-
-color.rgb = function(cstr) { return color.tinyRGB(tinycolor(cstr)); };
-
-color.opacity = function(cstr) { return cstr ? tinycolor(cstr).getAlpha() : 0; };
-
-color.addOpacity = function(cstr, op) {
-    var c = tinycolor(cstr).toRgb();
-    return 'rgba(' + Math.round(c.r) + ', ' +
-        Math.round(c.g) + ', ' + Math.round(c.b) + ', ' + op + ')';
-};
-
-// combine two colors into one apparent color
-// if back has transparency or is missing,
-// color.background is assumed behind it
-color.combine = function(front, back) {
-    var fc = tinycolor(front).toRgb();
-    if(fc.a === 1) return tinycolor(front).toRgbString();
-
-    var bc = tinycolor(back || color.background).toRgb(),
-        bcflat = bc.a === 1 ? bc : {
-            r: 255 * (1 - bc.a) + bc.r * bc.a,
-            g: 255 * (1 - bc.a) + bc.g * bc.a,
-            b: 255 * (1 - bc.a) + bc.b * bc.a
-        },
-        fcflat = {
-            r: bcflat.r * (1 - fc.a) + fc.r * fc.a,
-            g: bcflat.g * (1 - fc.a) + fc.g * fc.a,
-            b: bcflat.b * (1 - fc.a) + fc.b * fc.a
-        };
-    return tinycolor(fcflat).toRgbString();
-};
-
-color.contrast = function(cstr, lightAmount, darkAmount) {
-    var tc = tinycolor(cstr);
-
-    var newColor = tc.isLight() ?
-        tc.darken(darkAmount) :
-        tc.lighten(lightAmount);
-
-    return newColor.toString();
-};
-
-color.stroke = function(s, c) {
-    var tc = tinycolor(c);
-    s.style({'stroke': color.tinyRGB(tc), 'stroke-opacity': tc.getAlpha()});
-};
-
-color.fill = function(s, c) {
-    var tc = tinycolor(c);
-    s.style({
-        'fill': color.tinyRGB(tc),
-        'fill-opacity': tc.getAlpha()
-    });
-};
-
-// search container for colors with the deprecated rgb(fractions) format
-// and convert them to rgb(0-255 values)
-color.clean = function(container) {
-    if(!container || typeof container !== 'object') return;
-
-    var keys = Object.keys(container),
-        i,
-        j,
-        key,
-        val;
-
-    for(i = 0; i < keys.length; i++) {
-        key = keys[i];
-        val = container[key];
-
-        // only sanitize keys that end in "color" or "colorscale"
-        if(key.substr(key.length - 5) === 'color') {
-            if(Array.isArray(val)) {
-                for(j = 0; j < val.length; j++) val[j] = cleanOne(val[j]);
-            }
-            else container[key] = cleanOne(val);
-        }
-        else if(key.substr(key.length - 10) === 'colorscale' && Array.isArray(val)) {
-            // colorscales have the format [[0, color1], [frac, color2], ... [1, colorN]]
-            for(j = 0; j < val.length; j++) {
-                if(Array.isArray(val[j])) val[j][1] = cleanOne(val[j][1]);
-            }
-        }
-        // recurse into arrays of objects, and plain objects
-        else if(Array.isArray(val)) {
-            var el0 = val[0];
-            if(!Array.isArray(el0) && el0 && typeof el0 === 'object') {
-                for(j = 0; j < val.length; j++) color.clean(val[j]);
-            }
-        }
-        else if(val && typeof val === 'object') color.clean(val);
-    }
-};
-
-function cleanOne(val) {
-    if(isNumeric(val) || typeof val !== 'string') return val;
-
-    var valTrim = val.trim();
-    if(valTrim.substr(0, 3) !== 'rgb') return val;
-
-    var match = valTrim.match(/^rgba?\s*\(([^()]*)\)$/);
-    if(!match) return val;
-
-    var parts = match[1].trim().split(/\s*[\s,]\s*/),
-        rgba = valTrim.charAt(3) === 'a' && parts.length === 4;
-    if(!rgba && parts.length !== 3) return val;
-
-    for(var i = 0; i < parts.length; i++) {
-        if(!parts[i].length) return val;
-        parts[i] = Number(parts[i]);
-
-        // all parts must be non-negative numbers
-        if(!(parts[i] >= 0)) return val;
-        // alpha>1 gets clipped to 1
-        if(i === 3) {
-            if(parts[i] > 1) parts[i] = 1;
-        }
-        // r, g, b must be < 1 (ie 1 itself is not allowed)
-        else if(parts[i] >= 1) return val;
-    }
-
-    var rgbStr = Math.round(parts[0] * 255) + ', ' +
-        Math.round(parts[1] * 255) + ', ' +
-        Math.round(parts[2] * 255);
-
-    if(rgba) return 'rgba(' + rgbStr + ', ' + parts[3] + ')';
-    return 'rgb(' + rgbStr + ')';
-}
diff --git a/src/components/colorbar/attributes.js b/src/components/colorbar/attributes.js
index 62f1e031ff8..8b137891791 100644
--- a/src/components/colorbar/attributes.js
+++ b/src/components/colorbar/attributes.js
@@ -1,194 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-var axesAttrs = require('../../plots/cartesian/layout_attributes');
-var fontAttrs = require('../../plots/font_attributes');
-var extendFlat = require('../../lib/extend').extendFlat;
-
-
-module.exports = {
-// TODO: only right is supported currently
-//     orient: {
-//         valType: 'enumerated',
-//         role: 'info',
-//         values: ['left', 'right', 'top', 'bottom'],
-//         dflt: 'right',
-//         description: [
-//             'Determines which side are the labels on',
-//             '(so left and right make vertical bars, etc.)'
-//         ].join(' ')
-//     },
-    thicknessmode: {
-        valType: 'enumerated',
-        values: ['fraction', 'pixels'],
-        role: 'style',
-        dflt: 'pixels',
-        description: [
-            'Determines whether this color bar\'s thickness',
-            '(i.e. the measure in the constant color direction)',
-            'is set in units of plot *fraction* or in *pixels*.',
-            'Use `thickness` to set the value.'
-        ].join(' ')
-    },
-    thickness: {
-        valType: 'number',
-        role: 'style',
-        min: 0,
-        dflt: 30,
-        description: [
-            'Sets the thickness of the color bar',
-            'This measure excludes the size of the padding, ticks and labels.'
-        ].join(' ')
-    },
-    lenmode: {
-        valType: 'enumerated',
-        values: ['fraction', 'pixels'],
-        role: 'info',
-        dflt: 'fraction',
-        description: [
-            'Determines whether this color bar\'s length',
-            '(i.e. the measure in the color variation direction)',
-            'is set in units of plot *fraction* or in *pixels.',
-            'Use `len` to set the value.'
-        ].join(' ')
-    },
-    len: {
-        valType: 'number',
-        min: 0,
-        dflt: 1,
-        role: 'style',
-        description: [
-            'Sets the length of the color bar',
-            'This measure excludes the padding of both ends.',
-            'That is, the color bar length is this length minus the',
-            'padding on both ends.'
-        ].join(' ')
-    },
-    x: {
-        valType: 'number',
-        dflt: 1.02,
-        min: -2,
-        max: 3,
-        role: 'style',
-        description: [
-            'Sets the x position of the color bar (in plot fraction).'
-        ].join(' ')
-    },
-    xanchor: {
-        valType: 'enumerated',
-        values: ['left', 'center', 'right'],
-        dflt: 'left',
-        role: 'style',
-        description: [
-            'Sets this color bar\'s horizontal position anchor.',
-            'This anchor binds the `x` position to the *left*, *center*',
-            'or *right* of the color bar.'
-        ].join(' ')
-    },
-    xpad: {
-        valType: 'number',
-        role: 'style',
-        min: 0,
-        dflt: 10,
-        description: 'Sets the amount of padding (in px) along the x direction.'
-    },
-    y: {
-        valType: 'number',
-        role: 'style',
-        dflt: 0.5,
-        min: -2,
-        max: 3,
-        description: [
-            'Sets the y position of the color bar (in plot fraction).'
-        ].join(' ')
-    },
-    yanchor: {
-        valType: 'enumerated',
-        values: ['top', 'middle', 'bottom'],
-        role: 'style',
-        dflt: 'middle',
-        description: [
-            'Sets this color bar\'s vertical position anchor',
-            'This anchor binds the `y` position to the *top*, *middle*',
-            'or *bottom* of the color bar.'
-        ].join(' ')
-    },
-    ypad: {
-        valType: 'number',
-        role: 'style',
-        min: 0,
-        dflt: 10,
-        description: 'Sets the amount of padding (in px) along the y direction.'
-    },
-    // a possible line around the bar itself
-    outlinecolor: axesAttrs.linecolor,
-    outlinewidth: axesAttrs.linewidth,
-    // Should outlinewidth have {dflt: 0} ?
-    // another possible line outside the padding and tick labels
-    bordercolor: axesAttrs.linecolor,
-    borderwidth: {
-        valType: 'number',
-        role: 'style',
-        min: 0,
-        dflt: 0,
-        description: [
-            'Sets the width (in px) or the border enclosing this color bar.'
-        ].join(' ')
-    },
-    bgcolor: {
-        valType: 'color',
-        role: 'style',
-        dflt: 'rgba(0,0,0,0)',
-        description: 'Sets the color of padded area.'
-    },
-    // tick and title properties named and function exactly as in axes
-    tickmode: axesAttrs.tickmode,
-    nticks: axesAttrs.nticks,
-    tick0: axesAttrs.tick0,
-    dtick: axesAttrs.dtick,
-    tickvals: axesAttrs.tickvals,
-    ticktext: axesAttrs.ticktext,
-    ticks: extendFlat({}, axesAttrs.ticks, {dflt: ''}),
-    ticklen: axesAttrs.ticklen,
-    tickwidth: axesAttrs.tickwidth,
-    tickcolor: axesAttrs.tickcolor,
-    showticklabels: axesAttrs.showticklabels,
-    tickfont: axesAttrs.tickfont,
-    tickangle: axesAttrs.tickangle,
-    tickformat: axesAttrs.tickformat,
-    tickprefix: axesAttrs.tickprefix,
-    showtickprefix: axesAttrs.showtickprefix,
-    ticksuffix: axesAttrs.ticksuffix,
-    showticksuffix: axesAttrs.showticksuffix,
-    separatethousands: axesAttrs.separatethousands,
-    exponentformat: axesAttrs.exponentformat,
-    showexponent: axesAttrs.showexponent,
-    title: {
-        valType: 'string',
-        role: 'info',
-        dflt: 'Click to enter colorscale title',
-        description: 'Sets the title of the color bar.'
-    },
-    titlefont: extendFlat({}, fontAttrs, {
-        description: [
-            'Sets this color bar\'s title font.'
-        ].join(' ')
-    }),
-    titleside: {
-        valType: 'enumerated',
-        values: ['right', 'top', 'bottom'],
-        role: 'style',
-        dflt: 'top',
-        description: [
-            'Determines the location of the colorbar title',
-            'with respect to the color bar.'
-        ].join(' ')
-    }
-};
diff --git a/src/components/colorbar/defaults.js b/src/components/colorbar/defaults.js
index be767c67524..8b137891791 100644
--- a/src/components/colorbar/defaults.js
+++ b/src/components/colorbar/defaults.js
@@ -1,65 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var Lib = require('../../lib');
-var handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults');
-var handleTickMarkDefaults = require('../../plots/cartesian/tick_mark_defaults');
-var handleTickLabelDefaults = require('../../plots/cartesian/tick_label_defaults');
-
-var attributes = require('./attributes');
-
-
-module.exports = function colorbarDefaults(containerIn, containerOut, layout) {
-    var colorbarOut = containerOut.colorbar = {},
-        colorbarIn = containerIn.colorbar || {};
-
-    function coerce(attr, dflt) {
-        return Lib.coerce(colorbarIn, colorbarOut, attributes, attr, dflt);
-    }
-
-    var thicknessmode = coerce('thicknessmode');
-    coerce('thickness', (thicknessmode === 'fraction') ?
-        30 / (layout.width - layout.margin.l - layout.margin.r) :
-        30
-    );
-
-    var lenmode = coerce('lenmode');
-    coerce('len', (lenmode === 'fraction') ?
-        1 :
-        layout.height - layout.margin.t - layout.margin.b
-    );
-
-    coerce('x');
-    coerce('xanchor');
-    coerce('xpad');
-    coerce('y');
-    coerce('yanchor');
-    coerce('ypad');
-    Lib.noneOrAll(colorbarIn, colorbarOut, ['x', 'y']);
-
-    coerce('outlinecolor');
-    coerce('outlinewidth');
-    coerce('bordercolor');
-    coerce('borderwidth');
-    coerce('bgcolor');
-
-    handleTickValueDefaults(colorbarIn, colorbarOut, coerce, 'linear');
-
-    handleTickLabelDefaults(colorbarIn, colorbarOut, coerce, 'linear',
-        {outerTicks: false, font: layout.font, noHover: true});
-
-    handleTickMarkDefaults(colorbarIn, colorbarOut, coerce, 'linear',
-        {outerTicks: false, font: layout.font, noHover: true});
-
-    coerce('title');
-    Lib.coerceFont(coerce, 'titlefont', layout.font);
-    coerce('titleside');
-};
diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js
index b7eef65140c..8b137891791 100644
--- a/src/components/colorbar/draw.js
+++ b/src/components/colorbar/draw.js
@@ -1,631 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var d3 = require('d3');
-var tinycolor = require('tinycolor2');
-
-var Plotly = require('../../plotly');
-var Plots = require('../../plots/plots');
-var Registry = require('../../registry');
-var Axes = require('../../plots/cartesian/axes');
-var dragElement = require('../dragelement');
-var Lib = require('../../lib');
-var extendFlat = require('../../lib/extend').extendFlat;
-var setCursor = require('../../lib/setcursor');
-var Drawing = require('../drawing');
-var Color = require('../color');
-var Titles = require('../titles');
-
-var handleAxisDefaults = require('../../plots/cartesian/axis_defaults');
-var handleAxisPositionDefaults = require('../../plots/cartesian/position_defaults');
-var axisLayoutAttrs = require('../../plots/cartesian/layout_attributes');
-
-var attributes = require('./attributes');
-
-
-module.exports = function draw(gd, id) {
-    // opts: options object, containing everything from attributes
-    // plus a few others that are the equivalent of the colorbar "data"
-    var opts = {};
-    Object.keys(attributes).forEach(function(k) {
-        opts[k] = null;
-    });
-    // fillcolor can be a d3 scale, domain is z values, range is colors
-    // or leave it out for no fill,
-    // or set to a string constant for single-color fill
-    opts.fillcolor = null;
-    // line.color has the same options as fillcolor
-    opts.line = {color: null, width: null, dash: null};
-    // levels of lines to draw.
-    // note that this DOES NOT determine the extent of the bar
-    // that's given by the domain of fillcolor
-    // (or line.color if no fillcolor domain)
-    opts.levels = {start: null, end: null, size: null};
-    // separate fill levels (for example, heatmap coloring of a
-    // contour map) if this is omitted, fillcolors will be
-    // evaluated halfway between levels
-    opts.filllevels = null;
-
-    function component() {
-        var fullLayout = gd._fullLayout,
-            gs = fullLayout._size;
-        if((typeof opts.fillcolor !== 'function') &&
-                (typeof opts.line.color !== 'function')) {
-            fullLayout._infolayer.selectAll('g.' + id).remove();
-            return;
-        }
-        var zrange = d3.extent(((typeof opts.fillcolor === 'function') ?
-                opts.fillcolor : opts.line.color).domain()),
-            linelevels = [],
-            filllevels = [],
-            l,
-            linecolormap = typeof opts.line.color === 'function' ?
-                opts.line.color : function() { return opts.line.color; },
-            fillcolormap = typeof opts.fillcolor === 'function' ?
-                opts.fillcolor : function() { return opts.fillcolor; };
-
-        var l0 = opts.levels.end + opts.levels.size / 100,
-            ls = opts.levels.size,
-            zr0 = (1.001 * zrange[0] - 0.001 * zrange[1]),
-            zr1 = (1.001 * zrange[1] - 0.001 * zrange[0]);
-        for(l = opts.levels.start; (l - l0) * ls < 0; l += ls) {
-            if(l > zr0 && l < zr1) linelevels.push(l);
-        }
-
-        if(typeof opts.fillcolor === 'function') {
-            if(opts.filllevels) {
-                l0 = opts.filllevels.end + opts.filllevels.size / 100;
-                ls = opts.filllevels.size;
-                for(l = opts.filllevels.start; (l - l0) * ls < 0; l += ls) {
-                    if(l > zrange[0] && l < zrange[1]) filllevels.push(l);
-                }
-            }
-            else {
-                filllevels = linelevels.map(function(v) {
-                    return v - opts.levels.size / 2;
-                });
-                filllevels.push(filllevels[filllevels.length - 1] +
-                    opts.levels.size);
-            }
-        }
-        else if(opts.fillcolor && typeof opts.fillcolor === 'string') {
-            // doesn't matter what this value is, with a single value
-            // we'll make a single fill rect covering the whole bar
-            filllevels = [0];
-        }
-
-        if(opts.levels.size < 0) {
-            linelevels.reverse();
-            filllevels.reverse();
-        }
-
-        // now make a Plotly Axes object to scale with and draw ticks
-        // TODO: does not support orientation other than right
-
-        // we calculate pixel sizes based on the specified graph size,
-        // not the actual (in case something pushed the margins around)
-        // which is a little odd but avoids an odd iterative effect
-        // when the colorbar itself is pushing the margins.
-        // but then the fractional size is calculated based on the
-        // actual graph size, so that the axes will size correctly.
-        var originalPlotHeight = fullLayout.height - fullLayout.margin.t - fullLayout.margin.b,
-            originalPlotWidth = fullLayout.width - fullLayout.margin.l - fullLayout.margin.r,
-            thickPx = Math.round(opts.thickness *
-                (opts.thicknessmode === 'fraction' ? originalPlotWidth : 1)),
-            thickFrac = thickPx / gs.w,
-            lenPx = Math.round(opts.len *
-                (opts.lenmode === 'fraction' ? originalPlotHeight : 1)),
-            lenFrac = lenPx / gs.h,
-            xpadFrac = opts.xpad / gs.w,
-            yExtraPx = (opts.borderwidth + opts.outlinewidth) / 2,
-            ypadFrac = opts.ypad / gs.h,
-
-            // x positioning: do it initially just for left anchor,
-            // then fix at the end (since we don't know the width yet)
-            xLeft = Math.round(opts.x * gs.w + opts.xpad),
-            // for dragging... this is getting a little muddled...
-            xLeftFrac = opts.x - thickFrac *
-                ({middle: 0.5, right: 1}[opts.xanchor]||0),
-
-            // y positioning we can do correctly from the start
-            yBottomFrac = opts.y + lenFrac *
-                (({top: -0.5, bottom: 0.5}[opts.yanchor] || 0) - 0.5),
-            yBottomPx = Math.round(gs.h * (1 - yBottomFrac)),
-            yTopPx = yBottomPx - lenPx,
-            titleEl,
-            cbAxisIn = {
-                type: 'linear',
-                range: zrange,
-                tickmode: opts.tickmode,
-                nticks: opts.nticks,
-                tick0: opts.tick0,
-                dtick: opts.dtick,
-                tickvals: opts.tickvals,
-                ticktext: opts.ticktext,
-                ticks: opts.ticks,
-                ticklen: opts.ticklen,
-                tickwidth: opts.tickwidth,
-                tickcolor: opts.tickcolor,
-                showticklabels: opts.showticklabels,
-                tickfont: opts.tickfont,
-                tickangle: opts.tickangle,
-                tickformat: opts.tickformat,
-                exponentformat: opts.exponentformat,
-                separatethousands: opts.separatethousands,
-                showexponent: opts.showexponent,
-                showtickprefix: opts.showtickprefix,
-                tickprefix: opts.tickprefix,
-                showticksuffix: opts.showticksuffix,
-                ticksuffix: opts.ticksuffix,
-                title: opts.title,
-                titlefont: opts.titlefont,
-                anchor: 'free',
-                position: 1
-            },
-            cbAxisOut = {},
-            axisOptions = {
-                letter: 'y',
-                font: fullLayout.font,
-                noHover: true,
-                calendar: fullLayout.calendar  // not really necessary (yet?)
-            };
-
-        // Coerce w.r.t. Axes layoutAttributes:
-        // re-use axes.js logic without updating _fullData
-        function coerce(attr, dflt) {
-            return Lib.coerce(cbAxisIn, cbAxisOut, axisLayoutAttrs, attr, dflt);
-        }
-
-        // Prepare the Plotly axis object
-        handleAxisDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions);
-        handleAxisPositionDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions);
-
-        cbAxisOut._id = 'y' + id;
-        cbAxisOut._gd = gd;
-
-        // position can't go in through supplyDefaults
-        // because that restricts it to [0,1]
-        cbAxisOut.position = opts.x + xpadFrac + thickFrac;
-
-        // save for other callers to access this axis
-        component.axis = cbAxisOut;
-
-        if(['top', 'bottom'].indexOf(opts.titleside) !== -1) {
-            cbAxisOut.titleside = opts.titleside;
-            cbAxisOut.titlex = opts.x + xpadFrac;
-            cbAxisOut.titley = yBottomFrac +
-                (opts.titleside === 'top' ? lenFrac - ypadFrac : ypadFrac);
-        }
-
-        if(opts.line.color && opts.tickmode === 'auto') {
-            cbAxisOut.tickmode = 'linear';
-            cbAxisOut.tick0 = opts.levels.start;
-            var dtick = opts.levels.size;
-            // expand if too many contours, so we don't get too many ticks
-            var autoNtick = Lib.constrain(
-                    (yBottomPx - yTopPx) / 50, 4, 15) + 1,
-                dtFactor = (zrange[1] - zrange[0]) /
-                    ((opts.nticks || autoNtick) * dtick);
-            if(dtFactor > 1) {
-                var dtexp = Math.pow(10, Math.floor(
-                    Math.log(dtFactor) / Math.LN10));
-                dtick *= dtexp * Lib.roundUp(dtFactor / dtexp, [2, 5, 10]);
-                // if the contours are at round multiples, reset tick0
-                // so they're still at round multiples. Otherwise,
-                // keep the first label on the first contour level
-                if((Math.abs(opts.levels.start) /
-                        opts.levels.size + 1e-6) % 1 < 2e-6) {
-                    cbAxisOut.tick0 = 0;
-                }
-            }
-            cbAxisOut.dtick = dtick;
-        }
-
-        // set domain after init, because we may want to
-        // allow it outside [0,1]
-        cbAxisOut.domain = [
-            yBottomFrac + ypadFrac,
-            yBottomFrac + lenFrac - ypadFrac
-        ];
-        cbAxisOut.setScale();
-
-        // now draw the elements
-        var container = fullLayout._infolayer.selectAll('g.' + id).data([0]);
-        container.enter().append('g').classed(id, true)
-            .each(function() {
-                var s = d3.select(this);
-                s.append('rect').classed('cbbg', true);
-                s.append('g').classed('cbfills', true);
-                s.append('g').classed('cblines', true);
-                s.append('g').classed('cbaxis', true).classed('crisp', true);
-                s.append('g').classed('cbtitleunshift', true)
-                    .append('g').classed('cbtitle', true);
-                s.append('rect').classed('cboutline', true);
-                s.select('.cbtitle').datum(0);
-            });
-        container.attr('transform', 'translate(' + Math.round(gs.l) +
-            ',' + Math.round(gs.t) + ')');
-        // TODO: this opposite transform is a hack until we make it
-        // more rational which items get this offset
-        var titleCont = container.select('.cbtitleunshift')
-            .attr('transform', 'translate(-' +
-                Math.round(gs.l) + ',-' +
-                Math.round(gs.t) + ')');
-
-        cbAxisOut._axislayer = container.select('.cbaxis');
-        var titleHeight = 0;
-        if(['top', 'bottom'].indexOf(opts.titleside) !== -1) {
-            // draw the title so we know how much room it needs
-            // when we squish the axis. This one only applies to
-            // top or bottom titles, not right side.
-            var x = gs.l + (opts.x + xpadFrac) * gs.w,
-                fontSize = cbAxisOut.titlefont.size,
-                y;
-
-            if(opts.titleside === 'top') {
-                y = (1 - (yBottomFrac + lenFrac - ypadFrac)) * gs.h +
-                    gs.t + 3 + fontSize * 0.75;
-            }
-            else {
-                y = (1 - (yBottomFrac + ypadFrac)) * gs.h +
-                    gs.t - 3 - fontSize * 0.25;
-            }
-            drawTitle(cbAxisOut._id + 'title', {
-                attributes: {x: x, y: y, 'text-anchor': 'start'}
-            });
-        }
-
-        function drawAxis() {
-            if(['top', 'bottom'].indexOf(opts.titleside) !== -1) {
-                // squish the axis top to make room for the title
-                var titleGroup = container.select('.cbtitle'),
-                    titleText = titleGroup.select('text'),
-                    titleTrans =
-                        [-opts.outlinewidth / 2, opts.outlinewidth / 2],
-                    mathJaxNode = titleGroup
-                        .select('.h' + cbAxisOut._id + 'title-math-group')
-                        .node(),
-                    lineSize = 15.6;
-                if(titleText.node()) {
-                    lineSize =
-                        parseInt(titleText.style('font-size'), 10) * 1.3;
-                }
-                if(mathJaxNode) {
-                    titleHeight = Drawing.bBox(mathJaxNode).height;
-                    if(titleHeight > lineSize) {
-                        // not entirely sure how mathjax is doing
-                        // vertical alignment, but this seems to work.
-                        titleTrans[1] -= (titleHeight - lineSize) / 2;
-                    }
-                }
-                else if(titleText.node() &&
-                        !titleText.classed('js-placeholder')) {
-                    titleHeight = Drawing.bBox(
-                        titleGroup.node()).height;
-                }
-                if(titleHeight) {
-                    // buffer btwn colorbar and title
-                    // TODO: configurable
-                    titleHeight += 5;
-
-                    if(opts.titleside === 'top') {
-                        cbAxisOut.domain[1] -= titleHeight / gs.h;
-                        titleTrans[1] *= -1;
-                    }
-                    else {
-                        cbAxisOut.domain[0] += titleHeight / gs.h;
-                        var nlines = Math.max(1,
-                            titleText.selectAll('tspan.line').size());
-                        titleTrans[1] += (1 - nlines) * lineSize;
-                    }
-
-                    titleGroup.attr('transform',
-                        'translate(' + titleTrans + ')');
-
-                    cbAxisOut.setScale();
-                }
-            }
-
-            container.selectAll('.cbfills,.cblines,.cbaxis')
-                .attr('transform', 'translate(0,' +
-                    Math.round(gs.h * (1 - cbAxisOut.domain[1])) + ')');
-
-            var fills = container.select('.cbfills')
-                .selectAll('rect.cbfill')
-                    .data(filllevels);
-            fills.enter().append('rect')
-                .classed('cbfill', true)
-                .style('stroke', 'none');
-            fills.exit().remove();
-            fills.each(function(d, i) {
-                var z = [
-                    (i === 0) ? zrange[0] :
-                        (filllevels[i] + filllevels[i - 1]) / 2,
-                    (i === filllevels.length - 1) ? zrange[1] :
-                        (filllevels[i] + filllevels[i + 1]) / 2
-                ]
-                .map(cbAxisOut.c2p)
-                .map(Math.round);
-
-                // offset the side adjoining the next rectangle so they
-                // overlap, to prevent antialiasing gaps
-                if(i !== filllevels.length - 1) {
-                    z[1] += (z[1] > z[0]) ? 1 : -1;
-                }
-
-
-                // Tinycolor can't handle exponents and
-                // at this scale, removing it makes no difference.
-                var colorString = fillcolormap(d).replace('e-', ''),
-                    opaqueColor = tinycolor(colorString).toHexString();
-
-                // Colorbar cannot currently support opacities so we
-                // use an opaque fill even when alpha channels present
-                d3.select(this).attr({
-                    x: xLeft,
-                    width: Math.max(thickPx, 2),
-                    y: d3.min(z),
-                    height: Math.max(d3.max(z) - d3.min(z), 2),
-                    fill: opaqueColor
-                });
-            });
-
-            var lines = container.select('.cblines')
-                .selectAll('path.cbline')
-                    .data(opts.line.color && opts.line.width ?
-                        linelevels : []);
-            lines.enter().append('path')
-                .classed('cbline', true);
-            lines.exit().remove();
-            lines.each(function(d) {
-                d3.select(this)
-                    .attr('d', 'M' + xLeft + ',' +
-                        (Math.round(cbAxisOut.c2p(d)) + (opts.line.width / 2) % 1) +
-                        'h' + thickPx)
-                    .call(Drawing.lineGroupStyle,
-                        opts.line.width, linecolormap(d), opts.line.dash);
-            });
-
-            // force full redraw of labels and ticks
-            cbAxisOut._axislayer.selectAll('g.' + cbAxisOut._id + 'tick,path')
-                .remove();
-
-            cbAxisOut._pos = xLeft + thickPx +
-                (opts.outlinewidth||0) / 2 - (opts.ticks === 'outside' ? 1 : 0);
-            cbAxisOut.side = 'right';
-
-            // separate out axis and title drawing,
-            // so we don't need such complicated logic in Titles.draw
-            // if title is on the top or bottom, we've already drawn it
-            // this title call only handles side=right
-            return Lib.syncOrAsync([
-                function() {
-                    return Axes.doTicks(gd, cbAxisOut, true);
-                },
-                function() {
-                    if(['top', 'bottom'].indexOf(opts.titleside) === -1) {
-                        var fontSize = cbAxisOut.titlefont.size,
-                            y = cbAxisOut._offset + cbAxisOut._length / 2,
-                            x = gs.l + (cbAxisOut.position || 0) * gs.w + ((cbAxisOut.side === 'right') ?
-                                10 + fontSize * ((cbAxisOut.showticklabels ? 1 : 0.5)) :
-                                -10 - fontSize * ((cbAxisOut.showticklabels ? 0.5 : 0)));
-
-                        // the 'h' + is a hack to get around the fact that
-                        // convertToTspans rotates any 'y...' class by 90 degrees.
-                        // TODO: find a better way to control this.
-                        drawTitle('h' + cbAxisOut._id + 'title', {
-                            avoid: {
-                                selection: d3.select(gd).selectAll('g.' + cbAxisOut._id + 'tick'),
-                                side: opts.titleside,
-                                offsetLeft: gs.l,
-                                offsetTop: gs.t,
-                                maxShift: fullLayout.width
-                            },
-                            attributes: {x: x, y: y, 'text-anchor': 'middle'},
-                            transform: {rotate: '-90', offset: 0}
-                        });
-                    }
-                }]);
-        }
-
-        function drawTitle(titleClass, titleOpts) {
-            var trace = getTrace(),
-                propName;
-            if(Registry.traceIs(trace, 'markerColorscale')) {
-                propName = 'marker.colorbar.title';
-            }
-            else propName = 'colorbar.title';
-
-            var dfltTitleOpts = {
-                propContainer: cbAxisOut,
-                propName: propName,
-                traceIndex: trace.index,
-                dfltName: 'colorscale',
-                containerGroup: container.select('.cbtitle')
-            };
-
-            // this class-to-rotate thing with convertToTspans is
-            // getting hackier and hackier... delete groups with the
-            // wrong class (in case earlier the colorbar was drawn on
-            // a different side, I think?)
-            var otherClass = titleClass.charAt(0) === 'h' ?
-                titleClass.substr(1) : ('h' + titleClass);
-            container.selectAll('.' + otherClass + ',.' + otherClass + '-math-group')
-                .remove();
-
-            Titles.draw(gd, titleClass,
-                extendFlat(dfltTitleOpts, titleOpts || {}));
-        }
-
-        function positionCB() {
-            // wait for the axis & title to finish rendering before
-            // continuing positioning
-            // TODO: why are we redrawing multiple times now with this?
-            // I guess autoMargin doesn't like being post-promise?
-            var innerWidth = thickPx + opts.outlinewidth / 2 +
-                    Drawing.bBox(cbAxisOut._axislayer.node()).width;
-            titleEl = titleCont.select('text');
-            if(titleEl.node() && !titleEl.classed('js-placeholder')) {
-                var mathJaxNode = titleCont
-                        .select('.h' + cbAxisOut._id + 'title-math-group')
-                        .node(),
-                    titleWidth;
-                if(mathJaxNode &&
-                        ['top', 'bottom'].indexOf(opts.titleside) !== -1) {
-                    titleWidth = Drawing.bBox(mathJaxNode).width;
-                }
-                else {
-                    // note: the formula below works for all titlesides,
-                    // (except for top/bottom mathjax, above)
-                    // but the weird gs.l is because the titleunshift
-                    // transform gets removed by Drawing.bBox
-                    titleWidth =
-                        Drawing.bBox(titleCont.node()).right -
-                        xLeft - gs.l;
-                }
-                innerWidth = Math.max(innerWidth, titleWidth);
-            }
-
-            var outerwidth = 2 * opts.xpad + innerWidth +
-                    opts.borderwidth + opts.outlinewidth / 2,
-                outerheight = yBottomPx - yTopPx;
-
-            container.select('.cbbg').attr({
-                x: xLeft - opts.xpad -
-                    (opts.borderwidth + opts.outlinewidth) / 2,
-                y: yTopPx - yExtraPx,
-                width: Math.max(outerwidth, 2),
-                height: Math.max(outerheight + 2 * yExtraPx, 2)
-            })
-            .call(Color.fill, opts.bgcolor)
-            .call(Color.stroke, opts.bordercolor)
-            .style({'stroke-width': opts.borderwidth});
-
-            container.selectAll('.cboutline').attr({
-                x: xLeft,
-                y: yTopPx + opts.ypad +
-                    (opts.titleside === 'top' ? titleHeight : 0),
-                width: Math.max(thickPx, 2),
-                height: Math.max(outerheight - 2 * opts.ypad - titleHeight, 2)
-            })
-            .call(Color.stroke, opts.outlinecolor)
-            .style({
-                fill: 'None',
-                'stroke-width': opts.outlinewidth
-            });
-
-            // fix positioning for xanchor!='left'
-            var xoffset = ({center: 0.5, right: 1}[opts.xanchor] || 0) *
-                outerwidth;
-            container.attr('transform',
-                'translate(' + (gs.l - xoffset) + ',' + gs.t + ')');
-
-            // auto margin adjustment
-            Plots.autoMargin(gd, id, {
-                x: opts.x,
-                y: opts.y,
-                l: outerwidth * ({right: 1, center: 0.5}[opts.xanchor] || 0),
-                r: outerwidth * ({left: 1, center: 0.5}[opts.xanchor] || 0),
-                t: outerheight * ({bottom: 1, middle: 0.5}[opts.yanchor] || 0),
-                b: outerheight * ({top: 1, middle: 0.5}[opts.yanchor] || 0)
-            });
-        }
-
-        var cbDone = Lib.syncOrAsync([
-            Plots.previousPromises,
-            drawAxis,
-            Plots.previousPromises,
-            positionCB
-        ], gd);
-
-        if(cbDone && cbDone.then) (gd._promises || []).push(cbDone);
-
-        // dragging...
-        if(gd._context.editable) {
-            var t0,
-                xf,
-                yf;
-
-            dragElement.init({
-                element: container.node(),
-                prepFn: function() {
-                    t0 = container.attr('transform');
-                    setCursor(container);
-                },
-                moveFn: function(dx, dy) {
-                    container.attr('transform',
-                        t0 + ' ' + 'translate(' + dx + ',' + dy + ')');
-
-                    xf = dragElement.align(xLeftFrac + (dx / gs.w), thickFrac,
-                        0, 1, opts.xanchor);
-                    yf = dragElement.align(yBottomFrac - (dy / gs.h), lenFrac,
-                        0, 1, opts.yanchor);
-
-                    var csr = dragElement.getCursor(xf, yf,
-                        opts.xanchor, opts.yanchor);
-                    setCursor(container, csr);
-                },
-                doneFn: function(dragged) {
-                    setCursor(container);
-
-                    if(dragged && xf !== undefined && yf !== undefined) {
-                        Plotly.restyle(gd,
-                            {'colorbar.x': xf, 'colorbar.y': yf},
-                            getTrace().index);
-                    }
-                }
-            });
-        }
-        return cbDone;
-    }
-
-    function getTrace() {
-        var idNum = id.substr(2),
-            i,
-            trace;
-        for(i = 0; i < gd._fullData.length; i++) {
-            trace = gd._fullData[i];
-            if(trace.uid === idNum) return trace;
-        }
-    }
-
-    // setter/getters for every item defined in opts
-    Object.keys(opts).forEach(function(name) {
-        component[name] = function(v) {
-            // getter
-            if(!arguments.length) return opts[name];
-
-            // setter - for multi-part properties,
-            // set only the parts that are provided
-            opts[name] = Lib.isPlainObject(opts[name]) ?
-                 Lib.extendFlat(opts[name], v) :
-                 v;
-
-            return component;
-        };
-    });
-
-    // or use .options to set multiple options at once via a dictionary
-    component.options = function(o) {
-        Object.keys(o).forEach(function(name) {
-            // in case something random comes through
-            // that's not an option, ignore it
-            if(typeof component[name] === 'function') {
-                component[name](o[name]);
-            }
-        });
-        return component;
-    };
-
-    component._opts = opts;
-
-    return component;
-};
diff --git a/src/components/colorbar/has_colorbar.js b/src/components/colorbar/has_colorbar.js
index fb32bc8b6cc..8b137891791 100644
--- a/src/components/colorbar/has_colorbar.js
+++ b/src/components/colorbar/has_colorbar.js
@@ -1,17 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var Lib = require('../../lib');
-
-
-module.exports = function hasColorbar(container) {
-    return Lib.isPlainObject(container.colorbar);
-};
diff --git a/src/components/colorbar/index.js b/src/components/colorbar/index.js
index c0960b78f7c..8b137891791 100644
--- a/src/components/colorbar/index.js
+++ b/src/components/colorbar/index.js
@@ -1,19 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-
-exports.attributes = require('./attributes');
-
-exports.supplyDefaults = require('./defaults');
-
-exports.draw = require('./draw');
-
-exports.hasColorbar = require('./has_colorbar');
diff --git a/src/components/colorscale/attributes.js b/src/components/colorscale/attributes.js
index bbbfc60e9ff..8b137891791 100644
--- a/src/components/colorscale/attributes.js
+++ b/src/components/colorscale/attributes.js
@@ -1,71 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-module.exports = {
-    zauto: {
-        valType: 'boolean',
-        role: 'info',
-        dflt: true,
-        description: [
-            'Determines the whether or not the color domain is computed',
-            'with respect to the input data.'
-        ].join(' ')
-    },
-    zmin: {
-        valType: 'number',
-        role: 'info',
-        dflt: null,
-        description: 'Sets the lower bound of color domain.'
-    },
-    zmax: {
-        valType: 'number',
-        role: 'info',
-        dflt: null,
-        description: 'Sets the upper bound of color domain.'
-    },
-    colorscale: {
-        valType: 'colorscale',
-        role: 'style',
-        description: [
-            'Sets the colorscale.',
-            'The colorscale must be an array containing',
-            'arrays mapping a normalized value to an',
-            'rgb, rgba, hex, hsl, hsv, or named color string.',
-            'At minimum, a mapping for the lowest (0) and highest (1)',
-            'values are required. For example,',
-            '`[[0, \'rgb(0,0,255)\', [1, \'rgb(255,0,0)\']]`.',
-            'To control the bounds of the colorscale in z space,',
-            'use zmin and zmax'
-        ].join(' ')
-    },
-    autocolorscale: {
-        valType: 'boolean',
-        role: 'style',
-        dflt: true,  // gets overrode in 'heatmap' & 'surface' for backwards comp.
-        description: [
-            'Determines whether or not the colorscale is picked using the sign of',
-            'the input z values.'
-        ].join(' ')
-    },
-    reversescale: {
-        valType: 'boolean',
-        role: 'style',
-        dflt: false,
-        description: 'Reverses the colorscale.'
-    },
-    showscale: {
-        valType: 'boolean',
-        role: 'info',
-        dflt: true,
-        description: [
-            'Determines whether or not a colorbar is displayed for this trace.'
-        ].join(' ')
-    }
-};
diff --git a/src/components/colorscale/calc.js b/src/components/colorscale/calc.js
index 8d095e8642d..8b137891791 100644
--- a/src/components/colorscale/calc.js
+++ b/src/components/colorscale/calc.js
@@ -1,77 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var Lib = require('../../lib');
-
-var scales = require('./scales');
-var flipScale = require('./flip_scale');
-
-
-module.exports = function calc(trace, vals, containerStr, cLetter) {
-    var container, inputContainer;
-
-    if(containerStr) {
-        container = Lib.nestedProperty(trace, containerStr).get();
-        inputContainer = Lib.nestedProperty(trace._input, containerStr).get();
-    }
-    else {
-        container = trace;
-        inputContainer = trace._input;
-    }
-
-    var autoAttr = cLetter + 'auto',
-        minAttr = cLetter + 'min',
-        maxAttr = cLetter + 'max',
-        auto = container[autoAttr],
-        min = container[minAttr],
-        max = container[maxAttr],
-        scl = container.colorscale;
-
-    if(auto !== false || min === undefined) {
-        min = Lib.aggNums(Math.min, null, vals);
-    }
-
-    if(auto !== false || max === undefined) {
-        max = Lib.aggNums(Math.max, null, vals);
-    }
-
-    if(min === max) {
-        min -= 0.5;
-        max += 0.5;
-    }
-
-    container[minAttr] = min;
-    container[maxAttr] = max;
-
-    inputContainer[minAttr] = min;
-    inputContainer[maxAttr] = max;
-
-    /*
-     * If auto was explicitly false but min or max was missing,
-     * we filled in the missing piece here but later the trace does
-     * not look auto.
-     * Otherwise make sure the trace still looks auto as far as later
-     * changes are concerned.
-     */
-    inputContainer[autoAttr] = (auto !== false ||
-        (min === undefined && max === undefined));
-
-    if(container.autocolorscale) {
-        if(min * max < 0) scl = scales.RdBu;
-        else if(min >= 0) scl = scales.Reds;
-        else scl = scales.Blues;
-
-        // reversescale is handled at the containerOut level
-        inputContainer.colorscale = scl;
-        if(container.reversescale) scl = flipScale(scl);
-        container.colorscale = scl;
-    }
-};
diff --git a/src/components/colorscale/color_attributes.js b/src/components/colorscale/color_attributes.js
index 9c8f6cdb065..8b137891791 100644
--- a/src/components/colorscale/color_attributes.js
+++ b/src/components/colorscale/color_attributes.js
@@ -1,88 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-var colorScaleAttributes = require('./attributes');
-var extendDeep = require('../../lib/extend').extendDeep;
-var palettes = require('./scales.js');
-
-module.exports = function makeColorScaleAttributes(context) {
-    return {
-        color: {
-            valType: 'color',
-            arrayOk: true,
-            role: 'style',
-            description: [
-                'Sets the ', context, ' color. It accepts either a specific color',
-                ' or an array of numbers that are mapped to the colorscale',
-                ' relative to the max and min values of the array or relative to',
-                ' `cmin` and `cmax` if set.'
-            ].join('')
-        },
-        colorscale: extendDeep({}, colorScaleAttributes.colorscale, {
-            description: [
-                'Sets the colorscale and only has an effect',
-                ' if `', context, '.color` is set to a numerical array.',
-                ' The colorscale must be an array containing',
-                ' arrays mapping a normalized value to an',
-                ' rgb, rgba, hex, hsl, hsv, or named color string.',
-                ' At minimum, a mapping for the lowest (0) and highest (1)',
-                ' values are required. For example,',
-                ' `[[0, \'rgb(0,0,255)\', [1, \'rgb(255,0,0)\']]`.',
-                ' To control the bounds of the colorscale in color space,',
-                ' use `', context, '.cmin` and `', context, '.cmax`.',
-                ' Alternatively, `colorscale` may be a palette name string',
-                ' of the following list: '
-            ].join('').concat(Object.keys(palettes).join(', '))
-        }),
-        cauto: extendDeep({}, colorScaleAttributes.zauto, {
-            description: [
-                'Has an effect only if `', context, '.color` is set to a numerical array',
-                ' and `cmin`, `cmax` are set by the user. In this case,',
-                ' it controls whether the range of colors in `colorscale` is mapped to',
-                ' the range of values in the `color` array (`cauto: true`), or the `cmin`/`cmax`',
-                ' values (`cauto: false`).',
-                ' Defaults to `false` when `cmin`, `cmax` are set by the user.'
-            ].join('')
-        }),
-        cmax: extendDeep({}, colorScaleAttributes.zmax, {
-            description: [
-                'Has an effect only if `', context, '.color` is set to a numerical array.',
-                ' Sets the upper bound of the color domain.',
-                ' Value should be associated to the `', context, '.color` array index,',
-                ' and if set, `', context, '.cmin` must be set as well.'
-            ].join('')
-        }),
-        cmin: extendDeep({}, colorScaleAttributes.zmin, {
-            description: [
-                'Has an effect only if `', context, '.color` is set to a numerical array.',
-                ' Sets the lower bound of the color domain.',
-                ' Value should be associated to the `', context, '.color` array index,',
-                ' and if set, `', context, '.cmax` must be set as well.'
-            ].join('')
-        }),
-        autocolorscale: extendDeep({}, colorScaleAttributes.autocolorscale, {
-            description: [
-                'Has an effect only if `', context, '.color` is set to a numerical array.',
-                ' Determines whether the colorscale is a default palette (`autocolorscale: true`)',
-                ' or the palette determined by `', context, '.colorscale`.',
-                ' In case `colorscale` is unspecified or `autocolorscale` is true, the default ',
-                ' palette will be chosen according to whether numbers in the `color` array are',
-                ' all positive, all negative or mixed.'
-            ].join('')
-        }),
-        reversescale: extendDeep({}, colorScaleAttributes.reversescale, {
-            description: [
-                'Has an effect only if `', context, '.color` is set to a numerical array.',
-                ' Reverses the color mapping if true (`cmin` will correspond to the last color',
-                ' in the array and `cmax` will correspond to the first color).'
-            ].join('')
-        })
-    };
-};
diff --git a/src/components/colorscale/default_scale.js b/src/components/colorscale/default_scale.js
index 286663dac37..8b137891791 100644
--- a/src/components/colorscale/default_scale.js
+++ b/src/components/colorscale/default_scale.js
@@ -1,14 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-var scales = require('./scales');
-
-
-module.exports = scales.RdBu;
diff --git a/src/components/colorscale/defaults.js b/src/components/colorscale/defaults.js
index 55444bb4094..8b137891791 100644
--- a/src/components/colorscale/defaults.js
+++ b/src/components/colorscale/defaults.js
@@ -1,62 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-
-var Lib = require('../../lib');
-
-var hasColorbar = require('../colorbar/has_colorbar');
-var colorbarDefaults = require('../colorbar/defaults');
-var isValidScale = require('./is_valid_scale');
-var flipScale = require('./flip_scale');
-
-
-module.exports = function colorScaleDefaults(traceIn, traceOut, layout, coerce, opts) {
-    var prefix = opts.prefix,
-        cLetter = opts.cLetter,
-        containerStr = prefix.slice(0, prefix.length - 1),
-        containerIn = prefix ?
-            Lib.nestedProperty(traceIn, containerStr).get() || {} :
-            traceIn,
-        containerOut = prefix ?
-            Lib.nestedProperty(traceOut, containerStr).get() || {} :
-            traceOut,
-        minIn = containerIn[cLetter + 'min'],
-        maxIn = containerIn[cLetter + 'max'],
-        sclIn = containerIn.colorscale;
-
-    var validMinMax = isNumeric(minIn) && isNumeric(maxIn) && (minIn < maxIn);
-    coerce(prefix + cLetter + 'auto', !validMinMax);
-    coerce(prefix + cLetter + 'min');
-    coerce(prefix + cLetter + 'max');
-
-    // handles both the trace case (autocolorscale is false by default) and
-    // the marker and marker.line case (autocolorscale is true by default)
-    var autoColorscaleDftl;
-    if(sclIn !== undefined) autoColorscaleDftl = !isValidScale(sclIn);
-    coerce(prefix + 'autocolorscale', autoColorscaleDftl);
-    var sclOut = coerce(prefix + 'colorscale');
-
-    // reversescale is handled at the containerOut level
-    var reverseScale = coerce(prefix + 'reversescale');
-    if(reverseScale) containerOut.colorscale = flipScale(sclOut);
-
-    // ... until Scatter.colorbar can handle marker line colorbars
-    if(prefix === 'marker.line.') return;
-
-    // handle both the trace case where the dflt is listed in attributes and
-    // the marker case where the dflt is determined by hasColorbar
-    var showScaleDftl;
-    if(prefix) showScaleDftl = hasColorbar(containerIn);
-    var showScale = coerce(prefix + 'showscale', showScaleDftl);
-
-    if(showScale) colorbarDefaults(containerIn, containerOut, layout);
-};
diff --git a/src/components/colorscale/extract_scale.js b/src/components/colorscale/extract_scale.js
index d1e3c83d4ff..8b137891791 100644
--- a/src/components/colorscale/extract_scale.js
+++ b/src/components/colorscale/extract_scale.js
@@ -1,35 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-/**
- * Extract colorscale into numeric domain and color range.
- *
- * @param {array} scl colorscale array of arrays
- * @param {number} cmin minimum color value (used to clamp scale)
- * @param {number} cmax maximum color value (used to clamp scale)
- */
-module.exports = function extractScale(scl, cmin, cmax) {
-    var N = scl.length,
-        domain = new Array(N),
-        range = new Array(N);
-
-    for(var i = 0; i < N; i++) {
-        var si = scl[i];
-
-        domain[i] = cmin + si[0] * (cmax - cmin);
-        range[i] = si[1];
-    }
-
-    return {
-        domain: domain,
-        range: range
-    };
-};
diff --git a/src/components/colorscale/flip_scale.js b/src/components/colorscale/flip_scale.js
index 5e974846ea6..8b137891791 100644
--- a/src/components/colorscale/flip_scale.js
+++ b/src/components/colorscale/flip_scale.js
@@ -1,23 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-module.exports = function flipScale(scl) {
-    var N = scl.length,
-        sclNew = new Array(N),
-        si;
-
-    for(var i = N - 1, j = 0; i >= 0; i--, j++) {
-        si = scl[i];
-        sclNew[j] = [1 - si[0], si[1]];
-    }
-
-    return sclNew;
-};
diff --git a/src/components/colorscale/get_scale.js b/src/components/colorscale/get_scale.js
index 1f88c328a42..8b137891791 100644
--- a/src/components/colorscale/get_scale.js
+++ b/src/components/colorscale/get_scale.js
@@ -1,38 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var scales = require('./scales');
-var defaultScale = require('./default_scale');
-var isValidScaleArray = require('./is_valid_scale_array');
-
-
-module.exports = function getScale(scl, dflt) {
-    if(!dflt) dflt = defaultScale;
-    if(!scl) return dflt;
-
-    function parseScale() {
-        try {
-            scl = scales[scl] || JSON.parse(scl);
-        }
-        catch(e) {
-            scl = dflt;
-        }
-    }
-
-    if(typeof scl === 'string') {
-        parseScale();
-        // occasionally scl is double-JSON encoded...
-        if(typeof scl === 'string') parseScale();
-    }
-
-    if(!isValidScaleArray(scl)) return dflt;
-    return scl;
-};
diff --git a/src/components/colorscale/has_colorscale.js b/src/components/colorscale/has_colorscale.js
index 2744e956442..8b137891791 100644
--- a/src/components/colorscale/has_colorscale.js
+++ b/src/components/colorscale/has_colorscale.js
@@ -1,44 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-
-var Lib = require('../../lib');
-
-var isValidScale = require('./is_valid_scale');
-
-
-module.exports = function hasColorscale(trace, containerStr) {
-    var container = containerStr ?
-            Lib.nestedProperty(trace, containerStr).get() || {} :
-            trace,
-        color = container.color,
-        isArrayWithOneNumber = false;
-
-    if(Array.isArray(color)) {
-        for(var i = 0; i < color.length; i++) {
-            if(isNumeric(color[i])) {
-                isArrayWithOneNumber = true;
-                break;
-            }
-        }
-    }
-
-    return (
-        Lib.isPlainObject(container) && (
-            isArrayWithOneNumber ||
-            container.showscale === true ||
-            (isNumeric(container.cmin) && isNumeric(container.cmax)) ||
-            isValidScale(container.colorscale) ||
-            Lib.isPlainObject(container.colorbar)
-        )
-    );
-};
diff --git a/src/components/colorscale/index.js b/src/components/colorscale/index.js
index 0e07e23c32c..8b137891791 100644
--- a/src/components/colorscale/index.js
+++ b/src/components/colorscale/index.js
@@ -1,32 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-exports.scales = require('./scales');
-
-exports.defaultScale = require('./default_scale');
-
-exports.attributes = require('./attributes');
-
-exports.handleDefaults = require('./defaults');
-
-exports.calc = require('./calc');
-
-exports.hasColorscale = require('./has_colorscale');
-
-exports.isValidScale = require('./is_valid_scale');
-
-exports.getScale = require('./get_scale');
-
-exports.flipScale = require('./flip_scale');
-
-exports.extractScale = require('./extract_scale');
-
-exports.makeColorScaleFunc = require('./make_color_scale_func');
diff --git a/src/components/colorscale/is_valid_scale.js b/src/components/colorscale/is_valid_scale.js
index f3137486694..8b137891791 100644
--- a/src/components/colorscale/is_valid_scale.js
+++ b/src/components/colorscale/is_valid_scale.js
@@ -1,19 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var scales = require('./scales');
-var isValidScaleArray = require('./is_valid_scale_array');
-
-
-module.exports = function isValidScale(scl) {
-    if(scales[scl] !== undefined) return true;
-    else return isValidScaleArray(scl);
-};
diff --git a/src/components/colorscale/is_valid_scale_array.js b/src/components/colorscale/is_valid_scale_array.js
index 324b576b50f..8b137891791 100644
--- a/src/components/colorscale/is_valid_scale_array.js
+++ b/src/components/colorscale/is_valid_scale_array.js
@@ -1,35 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var tinycolor = require('tinycolor2');
-
-
-module.exports = function isValidScaleArray(scl) {
-    var highestVal = 0;
-
-    if(!Array.isArray(scl) || scl.length < 2) return false;
-
-    if(!scl[0] || !scl[scl.length - 1]) return false;
-
-    if(+scl[0][0] !== 0 || +scl[scl.length - 1][0] !== 1) return false;
-
-    for(var i = 0; i < scl.length; i++) {
-        var si = scl[i];
-
-        if(si.length !== 2 || +si[0] < highestVal || !tinycolor(si[1]).isValid()) {
-            return false;
-        }
-
-        highestVal = +si[0];
-    }
-
-    return true;
-};
diff --git a/src/components/colorscale/make_color_scale_func.js b/src/components/colorscale/make_color_scale_func.js
index 562e104b00a..8b137891791 100644
--- a/src/components/colorscale/make_color_scale_func.js
+++ b/src/components/colorscale/make_color_scale_func.js
@@ -1,94 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var d3 = require('d3');
-var tinycolor = require('tinycolor2');
-var isNumeric = require('fast-isnumeric');
-
-var Color = require('../color');
-
-/**
- * General colorscale function generator.
- *
- * @param {object} specs output of Colorscale.extractScale or precomputed domain, range.
- *  - domain {array}
- *  - range {array}
- *
- * @param {object} opts
- *  - noNumericCheck {boolean} if true, scale func bypasses numeric checks
- *  - returnArray {boolean} if true, scale func return 4-item array instead of color strings
- *
- * @return {function}
- */
-module.exports = function makeColorScaleFunc(specs, opts) {
-    opts = opts || {};
-
-    var domain = specs.domain,
-        range = specs.range,
-        N = range.length,
-        _range = new Array(N);
-
-    for(var i = 0; i < N; i++) {
-        var rgba = tinycolor(range[i]).toRgb();
-        _range[i] = [rgba.r, rgba.g, rgba.b, rgba.a];
-    }
-
-    var _sclFunc = d3.scale.linear()
-        .domain(domain)
-        .range(_range)
-        .clamp(true);
-
-    var noNumericCheck = opts.noNumericCheck,
-        returnArray = opts.returnArray,
-        sclFunc;
-
-    if(noNumericCheck && returnArray) {
-        sclFunc = _sclFunc;
-    }
-    else if(noNumericCheck) {
-        sclFunc = function(v) {
-            return colorArray2rbga(_sclFunc(v));
-        };
-    }
-    else if(returnArray) {
-        sclFunc = function(v) {
-            if(isNumeric(v)) return _sclFunc(v);
-            else if(tinycolor(v).isValid()) return v;
-            else return Color.defaultLine;
-        };
-    }
-    else {
-        sclFunc = function(v) {
-            if(isNumeric(v)) return colorArray2rbga(_sclFunc(v));
-            else if(tinycolor(v).isValid()) return v;
-            else return Color.defaultLine;
-        };
-    }
-
-    // colorbar draw looks into the d3 scale closure for domain and range
-
-    sclFunc.domain = _sclFunc.domain;
-
-    sclFunc.range = function() { return range; };
-
-    return sclFunc;
-};
-
-function colorArray2rbga(colorArray) {
-    var colorObj = {
-        r: colorArray[0],
-        g: colorArray[1],
-        b: colorArray[2],
-        a: colorArray[3]
-    };
-
-    return tinycolor(colorObj).toRgbString();
-}
diff --git a/src/components/colorscale/scales.js b/src/components/colorscale/scales.js
index 1993b937264..8b137891791 100644
--- a/src/components/colorscale/scales.js
+++ b/src/components/colorscale/scales.js
@@ -1,129 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-'use strict';
-
-
-module.exports = {
-    'Greys': [
-        [0, 'rgb(0,0,0)'], [1, 'rgb(255,255,255)']
-    ],
-
-    'YlGnBu': [
-        [0, 'rgb(8,29,88)'], [0.125, 'rgb(37,52,148)'],
-        [0.25, 'rgb(34,94,168)'], [0.375, 'rgb(29,145,192)'],
-        [0.5, 'rgb(65,182,196)'], [0.625, 'rgb(127,205,187)'],
-        [0.75, 'rgb(199,233,180)'], [0.875, 'rgb(237,248,217)'],
-        [1, 'rgb(255,255,217)']
-    ],
-
-    'Greens': [
-        [0, 'rgb(0,68,27)'], [0.125, 'rgb(0,109,44)'],
-        [0.25, 'rgb(35,139,69)'], [0.375, 'rgb(65,171,93)'],
-        [0.5, 'rgb(116,196,118)'], [0.625, 'rgb(161,217,155)'],
-        [0.75, 'rgb(199,233,192)'], [0.875, 'rgb(229,245,224)'],
-        [1, 'rgb(247,252,245)']
-    ],
-
-    'YlOrRd': [
-        [0, 'rgb(128,0,38)'], [0.125, 'rgb(189,0,38)'],
-        [0.25, 'rgb(227,26,28)'], [0.375, 'rgb(252,78,42)'],
-        [0.5, 'rgb(253,141,60)'], [0.625, 'rgb(254,178,76)'],
-        [0.75, 'rgb(254,217,118)'], [0.875, 'rgb(255,237,160)'],
-        [1, 'rgb(255,255,204)']
-    ],
-
-    'Bluered': [
-        [0, 'rgb(0,0,255)'], [1, 'rgb(255,0,0)']
-    ],
-
-    // modified RdBu based on
-    // www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf
-    'RdBu': [
-        [0, 'rgb(5,10,172)'], [0.35, 'rgb(106,137,247)'],
-        [0.5, 'rgb(190,190,190)'], [0.6, 'rgb(220,170,132)'],
-        [0.7, 'rgb(230,145,90)'], [1, 'rgb(178,10,28)']
-    ],
-
-    // Scale for non-negative numeric values
-    'Reds': [
-        [0, 'rgb(220,220,220)'], [0.2, 'rgb(245,195,157)'],
-        [0.4, 'rgb(245,160,105)'], [1, 'rgb(178,10,28)']
-    ],
-
-    // Scale for non-positive numeric values
-    'Blues': [
-        [0, 'rgb(5,10,172)'], [0.35, 'rgb(40,60,190)'],
-        [0.5, 'rgb(70,100,245)'], [0.6, 'rgb(90,120,245)'],
-        [0.7, 'rgb(106,137,247)'], [1, 'rgb(220,220,220)']
-    ],
-
-    'Picnic': [
-        [0, 'rgb(0,0,255)'], [0.1, 'rgb(51,153,255)'],
-        [0.2, 'rgb(102,204,255)'], [0.3, 'rgb(153,204,255)'],
-        [0.4, 'rgb(204,204,255)'], [0.5, 'rgb(255,255,255)'],
-        [0.6, 'rgb(255,204,255)'], [0.7, 'rgb(255,153,255)'],
-        [0.8, 'rgb(255,102,204)'], [0.9, 'rgb(255,102,102)'],
-        [1, 'rgb(255,0,0)']
-    ],
-
-    'Rainbow': [
-        [0, 'rgb(150,0,90)'], [0.125, 'rgb(0,0,200)'],
-        [0.25, 'rgb(0,25,255)'], [0.375, 'rgb(0,152,255)'],
-        [0.5, 'rgb(44,255,150)'], [0.625, 'rgb(151,255,0)'],
-        [0.75, 'rgb(255,234,0)'], [0.875, 'rgb(255,111,0)'],
-        [1, 'rgb(255,0,0)']
-    ],
-
-    'Portland': [
-        [0, 'rgb(12,51,131)'], [0.25, 'rgb(10,136,186)'],
-        [0.5, 'rgb(242,211,56)'], [0.75, 'rgb(242,143,56)'],
-        [1, 'rgb(217,30,30)']
-    ],
-
-    'Jet': [
-        [0, 'rgb(0,0,131)'], [0.125, 'rgb(0,60,170)'],
-        [0.375, 'rgb(5,255,255)'], [0.625, 'rgb(255,255,0)'],
-        [0.875, 'rgb(250,0,0)'], [1, 'rgb(128,0,0)']
-    ],
-
-    'Hot': [
-        [0, 'rgb(0,0,0)'], [0.3, 'rgb(230,0,0)'],
-        [0.6, 'rgb(255,210,0)'], [1, 'rgb(255,255,255)']
-    ],
-
-    'Blackbody': [
-        [0, 'rgb(0,0,0)'], [0.2, 'rgb(230,0,0)'],
-        [0.4, 'rgb(230,210,0)'], [0.7, 'rgb(255,255,255)'],
-        [1, 'rgb(160,200,255)']
-    ],
-
-    'Earth': [
-        [0, 'rgb(0,0,130)'], [0.1, 'rgb(0,180,180)'],
-        [0.2, 'rgb(40,210,40)'], [0.4, 'rgb(230,230,50)'],
-        [0.6, 'rgb(120,70,20)'], [1, 'rgb(255,255,255)']
-    ],
-
-    'Electric': [
-        [0, 'rgb(0,0,0)'], [0.15, 'rgb(30,0,100)'],
-        [0.4, 'rgb(120,0,100)'], [0.6, 'rgb(160,90,0)'],
-        [0.8, 'rgb(230,200,0)'], [1, 'rgb(255,250,220)']
-    ],
-
-    'Viridis': [
-        [0, '#440154'], [0.06274509803921569, '#48186a'],
-        [0.12549019607843137, '#472d7b'], [0.18823529411764706, '#424086'],
-        [0.25098039215686274, '#3b528b'], [0.3137254901960784, '#33638d'],
-        [0.3764705882352941, '#2c728e'], [0.4392156862745098, '#26828e'],
-        [0.5019607843137255, '#21918c'], [0.5647058823529412, '#1fa088'],
-        [0.6274509803921569, '#28ae80'], [0.6901960784313725, '#3fbc73'],
-        [0.7529411764705882, '#5ec962'], [0.8156862745098039, '#84d44b'],
-        [0.8784313725490196, '#addc30'], [0.9411764705882353, '#d8e219'],
-        [1, '#fde725']
-    ]
-};
diff --git a/src/components/dragelement/align.js b/src/components/dragelement/align.js
index 9503473ed6c..8b137891791 100644
--- a/src/components/dragelement/align.js
+++ b/src/components/dragelement/align.js
@@ -1,31 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-
-// for automatic alignment on dragging, <1/3 means left align,
-// >2/3 means right, and between is center. Pick the right fraction
-// based on where you are, and return the fraction corresponding to
-// that position on the object
-module.exports = function align(v, dv, v0, v1, anchor) {
-    var vmin = (v - v0) / (v1 - v0),
-        vmax = vmin + dv / (v1 - v0),
-        vc = (vmin + vmax) / 2;
-
-    // explicitly specified anchor
-    if(anchor === 'left' || anchor === 'bottom') return vmin;
-    if(anchor === 'center' || anchor === 'middle') return vc;
-    if(anchor === 'right' || anchor === 'top') return vmax;
-
-    // automatic based on position
-    if(vmin < (2 / 3) - vc) return vmin;
-    if(vmax > (4 / 3) - vc) return vmax;
-    return vc;
-};
diff --git a/src/components/dragelement/cursor.js b/src/components/dragelement/cursor.js
index 5601c8aaa30..8b137891791 100644
--- a/src/components/dragelement/cursor.js
+++ b/src/components/dragelement/cursor.js
@@ -1,36 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var Lib = require('../../lib');
-
-
-// set cursors pointing toward the closest corner/side,
-// to indicate alignment
-// x and y are 0-1, fractions of the plot area
-var cursorset = [
-    ['sw-resize', 's-resize', 'se-resize'],
-    ['w-resize', 'move', 'e-resize'],
-    ['nw-resize', 'n-resize', 'ne-resize']
-];
-
-module.exports = function getCursor(x, y, xanchor, yanchor) {
-    if(xanchor === 'left') x = 0;
-    else if(xanchor === 'center') x = 1;
-    else if(xanchor === 'right') x = 2;
-    else x = Lib.constrain(Math.floor(x * 3), 0, 2);
-
-    if(yanchor === 'bottom') y = 0;
-    else if(yanchor === 'middle') y = 1;
-    else if(yanchor === 'top') y = 2;
-    else y = Lib.constrain(Math.floor(y * 3), 0, 2);
-
-    return cursorset[y][x];
-};
diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js
index 8b531a1b5a8..8b137891791 100644
--- a/src/components/dragelement/index.js
+++ b/src/components/dragelement/index.js
@@ -1,185 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var Plotly = require('../../plotly');
-var Lib = require('../../lib');
-
-var constants = require('../../plots/cartesian/constants');
-
-
-var dragElement = module.exports = {};
-
-dragElement.align = require('./align');
-dragElement.getCursor = require('./cursor');
-
-var unhover = require('./unhover');
-dragElement.unhover = unhover.wrapped;
-dragElement.unhoverRaw = unhover.raw;
-
-/**
- * Abstracts click & drag interactions
- * @param {object} options with keys:
- *      element (required) the DOM element to drag
- *      prepFn (optional) function(event, startX, startY)
- *          executed on mousedown
- *          startX and startY are the clientX and clientY pixel position
- *          of the mousedown event
- *      moveFn (optional) function(dx, dy, dragged)
- *          executed on move
- *          dx and dy are the net pixel offset of the drag,
- *          dragged is true/false, has the mouse moved enough to
- *          constitute a drag
- *      doneFn (optional) function(dragged, numClicks)
- *          executed on mouseup, or mouseout of window since
- *          we don't get events after that
- *          dragged is as in moveFn
- *          numClicks is how many clicks we've registered within
- *          a doubleclick time
- *      setCursor (optional) function(event)
- *          executed on mousemove before mousedown
- *          the purpose of this callback is to update the mouse cursor before
- *          the click & drag interaction has been initiated
- */
-dragElement.init = function init(options) {
-    var gd = Lib.getPlotDiv(options.element) || {},
-        numClicks = 1,
-        DBLCLICKDELAY = constants.DBLCLICKDELAY,
-        startX,
-        startY,
-        newMouseDownTime,
-        dragCover,
-        initialTarget,
-        initialOnMouseMove;
-
-    if(!gd._mouseDownTime) gd._mouseDownTime = 0;
-
-    function onStart(e) {
-        // disable call to options.setCursor(evt)
-        options.element.onmousemove = initialOnMouseMove;
-
-        // make dragging and dragged into properties of gd
-        // so that others can look at and modify them
-        gd._dragged = false;
-        gd._dragging = true;
-        startX = e.clientX;
-        startY = e.clientY;
-        initialTarget = e.target;
-
-        newMouseDownTime = (new Date()).getTime();
-        if(newMouseDownTime - gd._mouseDownTime < DBLCLICKDELAY) {
-            // in a click train
-            numClicks += 1;
-        }
-        else {
-            // new click train
-            numClicks = 1;
-            gd._mouseDownTime = newMouseDownTime;
-        }
-
-        if(options.prepFn) options.prepFn(e, startX, startY);
-
-        dragCover = coverSlip();
-
-        dragCover.onmousemove = onMove;
-        dragCover.onmouseup = onDone;
-        dragCover.onmouseout = onDone;
-
-        dragCover.style.cursor = window.getComputedStyle(options.element).cursor;
-
-        return Lib.pauseEvent(e);
-    }
-
-    function onMove(e) {
-        var dx = e.clientX - startX,
-            dy = e.clientY - startY,
-            minDrag = options.minDrag || constants.MINDRAG;
-
-        if(Math.abs(dx) < minDrag) dx = 0;
-        if(Math.abs(dy) < minDrag) dy = 0;
-        if(dx || dy) {
-            gd._dragged = true;
-            dragElement.unhover(gd);
-        }
-
-        if(options.moveFn) options.moveFn(dx, dy, gd._dragged);
-
-        return Lib.pauseEvent(e);
-    }
-
-    function onDone(e) {
-        // re-enable call to options.setCursor(evt)
-        initialOnMouseMove = options.element.onmousemove;
-        if(options.setCursor) options.element.onmousemove = options.setCursor;
-
-        dragCover.onmousemove = null;
-        dragCover.onmouseup = null;
-        dragCover.onmouseout = null;
-        Lib.removeElement(dragCover);
-
-        if(!gd._dragging) {
-            gd._dragged = false;
-            return;
-        }
-        gd._dragging = false;
-
-        // don't count as a dblClick unless the mouseUp is also within
-        // the dblclick delay
-        if((new Date()).getTime() - gd._mouseDownTime > DBLCLICKDELAY) {
-            numClicks = Math.max(numClicks - 1, 1);
-        }
-
-        if(options.doneFn) options.doneFn(gd._dragged, numClicks);
-
-        if(!gd._dragged) {
-            var e2 = document.createEvent('MouseEvents');
-            e2.initEvent('click', true, true);
-            initialTarget.dispatchEvent(e2);
-        }
-
-        finishDrag(gd);
-
-        gd._dragged = false;
-
-        return Lib.pauseEvent(e);
-    }
-
-    // enable call to options.setCursor(evt)
-    initialOnMouseMove = options.element.onmousemove;
-    if(options.setCursor) options.element.onmousemove = options.setCursor;
-
-    options.element.onmousedown = onStart;
-    options.element.style.pointerEvents = 'all';
-};
-
-function coverSlip() {
-    var cover = document.createElement('div');
-
-    cover.className = 'dragcover';
-    var cStyle = cover.style;
-    cStyle.position = 'fixed';
-    cStyle.left = 0;
-    cStyle.right = 0;
-    cStyle.top = 0;
-    cStyle.bottom = 0;
-    cStyle.zIndex = 999999999;
-    cStyle.background = 'none';
-
-    document.body.appendChild(cover);
-
-    return cover;
-}
-
-dragElement.coverSlip = coverSlip;
-
-function finishDrag(gd) {
-    gd._dragging = false;
-    if(gd._replotPending) Plotly.plot(gd);
-}
diff --git a/src/components/dragelement/unhover.js b/src/components/dragelement/unhover.js
index 61b602ac6e6..8b137891791 100644
--- a/src/components/dragelement/unhover.js
+++ b/src/components/dragelement/unhover.js
@@ -1,49 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-
-var Events = require('../../lib/events');
-
-
-var unhover = module.exports = {};
-
-
-unhover.wrapped = function(gd, evt, subplot) {
-    if(typeof gd === 'string') gd = document.getElementById(gd);
-
-    // Important, clear any queued hovers
-    if(gd._hoverTimer) {
-        clearTimeout(gd._hoverTimer);
-        gd._hoverTimer = undefined;
-    }
-
-    unhover.raw(gd, evt, subplot);
-};
-
-
-// remove hover effects on mouse out, and emit unhover event
-unhover.raw = function unhoverRaw(gd, evt) {
-    var fullLayout = gd._fullLayout;
-
-    if(!evt) evt = {};
-    if(evt.target &&
-       Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) {
-        return;
-    }
-
-    fullLayout._hoverlayer.selectAll('g').remove();
-
-    if(evt.target && gd._hoverdata) {
-        gd.emit('plotly_unhover', {points: gd._hoverdata});
-    }
-
-    gd._hoverdata = undefined;
-};
diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js
index fa995b0641e..8b137891791 100644
--- a/src/components/drawing/index.js
+++ b/src/components/drawing/index.js
@@ -1,680 +1 @@
-/**
-* Copyright 2012-2017, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
 
-
-'use strict';
-
-var d3 = require('d3');
-var isNumeric = require('fast-isnumeric');
-
-var Registry = require('../../registry');
-var Color = require('../color');
-var Colorscale = require('../colorscale');
-var Lib = require('../../lib');
-var svgTextUtils = require('../../lib/svg_text_utils');
-
-var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
-var subTypes = require('../../traces/scatter/subtypes');
-var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func');
-
-var drawing = module.exports = {};
-
-// -----------------------------------------------------
-// styling functions for plot elements
-// -----------------------------------------------------
-
-drawing.font = function(s, family, size, color) {
-    // also allow the form font(s, {family, size, color})
-    if(family && family.family) {
-        color = family.color;
-        size = family.size;
-        family = family.family;
-    }
-    if(family) s.style('font-family', family);
-    if(size + 1) s.style('font-size', size + 'px');
-    if(color) s.call(Color.fill, color);
-};
-
-drawing.setPosition = function(s, x, y) { s.attr('x', x).attr('y', y); };
-drawing.setSize = function(s, w, h) { s.attr('width', w).attr('height', h); };
-drawing.setRect = function(s, x, y, w, h) {
-    s.call(drawing.setPosition, x, y).call(drawing.setSize, w, h);
-};
-
-drawing.translatePoint = function(d, sel, xa, ya) {
-    // put xp and yp into d if pixel scaling is already done
-    var x = d.xp || xa.c2p(d.x),
-        y = d.yp || ya.c2p(d.y);
-
-    if(isNumeric(x) && isNumeric(y) && sel.node()) {
-        // for multiline text this works better
-        if(sel.node().nodeName === 'text') {
-            sel.attr('x', x).attr('y', y);
-        } else {
-            sel.attr('transform', 'translate(' + x + ',' + y + ')');
-        }
-    }
-    else sel.remove();
-};
-
-drawing.translatePoints = function(s, xa, ya, trace) {
-    s.each(function(d) {
-        var sel = d3.select(this);
-        drawing.translatePoint(d, sel, xa, ya, trace);
-    });
-};
-
-drawing.getPx = function(s, styleAttr) {
-    // helper to pull out a px value from a style that may contain px units
-    // s is a d3 selection (will pull from the first one)
-    return Number(s.style(styleAttr).replace(/px$/, ''));
-};
-
-drawing.crispRound = function(gd, lineWidth, dflt) {
-    // for lines that disable antialiasing we want to
-    // make sure the width is an integer, and at least 1 if it's nonzero
-
-    if(!lineWidth || !isNumeric(lineWidth)) return dflt || 0;
-
-    // but not for static plots - these don't get antialiased anyway.
-    if(gd._context.staticPlot) return lineWidth;
-
-    if(lineWidth < 1) return 1;
-    return Math.round(lineWidth);
-};
-
-drawing.singleLineStyle = function(d, s, lw, lc, ld) {
-    s.style('fill', 'none');
-    var line = (((d || [])[0] || {}).trace || {}).line || {},
-        lw1 = lw || line.width||0,
-        dash = ld || line.dash || '';
-
-    Color.stroke(s, lc || line.color);
-    drawing.dashLine(s, dash, lw1);
-};
-
-drawing.lineGroupStyle = function(s, lw, lc, ld) {
-    s.style('fill', 'none')
-    .each(function(d) {
-        var line = (((d || [])[0] || {}).trace || {}).line || {},
-            lw1 = lw || line.width||0,
-            dash = ld || line.dash || '';
-
-        d3.select(this)
-            .call(Color.stroke, lc || line.color)
-            .call(drawing.dashLine, dash, lw1);
-    });
-};
-
-drawing.dashLine = function(s, dash, lineWidth) {
-    lineWidth = +lineWidth || 0;
-    var dlw = Math.max(lineWidth, 3);
-
-    if(dash === 'solid') dash = '';
-    else if(dash === 'dot') dash = dlw + 'px,' + dlw + 'px';
-    else if(dash === 'dash') dash = (3 * dlw) + 'px,' + (3 * dlw) + 'px';
-    else if(dash === 'longdash') dash = (5 * dlw) + 'px,' + (5 * dlw) + 'px';
-    else if(dash === 'dashdot') {
-        dash = (3 * dlw) + 'px,' + dlw + 'px,' + dlw + 'px,' + dlw + 'px';
-    }
-    else if(dash === 'longdashdot') {
-        dash = (5 * dlw) + 'px,' + (2 * dlw) + 'px,' + dlw + 'px,' + (2 * dlw) + 'px';
-    }
-    // otherwise user wrote the dasharray themselves - leave it be
-
-    s.style({
-        'stroke-dasharray': dash,
-        'stroke-width': lineWidth + 'px'
-    });
-};
-
-drawing.fillGroupStyle = function(s) {
-    s.style('stroke-width', 0)
-    .each(function(d) {
-        var shape = d3.select(this);
-        try {
-            shape.call(Color.fill, d[0].trace.fillcolor);
-        }
-        catch(e) {
-            Lib.error(e, s);
-            shape.remove();
-        }
-    });
-};
-
-var SYMBOLDEFS = require('./symbol_defs');
-
-drawing.symbolNames = [];
-drawing.symbolFuncs = [];
-drawing.symbolNeedLines = {};
-drawing.symbolNoDot = {};
-drawing.symbolList = [];
-
-Object.keys(SYMBOLDEFS).forEach(function(k) {
-    var symDef = SYMBOLDEFS[k];
-    drawing.symbolList = drawing.symbolList.concat(
-        [symDef.n, k, symDef.n + 100, k + '-open']);
-    drawing.symbolNames[symDef.n] = k;
-    drawing.symbolFuncs[symDef.n] = symDef.f;
-    if(symDef.needLine) {
-        drawing.symbolNeedLines[symDef.n] = true;
-    }
-    if(symDef.noDot) {
-        drawing.symbolNoDot[symDef.n] = true;
-    }
-    else {
-        drawing.symbolList = drawing.symbolList.concat(
-            [symDef.n + 200, k + '-dot', symDef.n + 300, k + '-open-dot']);
-    }
-});
-var MAXSYMBOL = drawing.symbolNames.length,
-    // add a dot in the middle of the symbol
-    DOTPATH = 'M0,0.5L0.5,0L0,-0.5L-0.5,0Z';
-
-drawing.symbolNumber = function(v) {
-    if(typeof v === 'string') {
-        var vbase = 0;
-        if(v.indexOf('-open') > 0) {
-            vbase = 100;
-            v = v.replace('-open', '');
-        }
-        if(v.indexOf('-dot') > 0) {
-            vbase += 200;
-            v = v.replace('-dot', '');
-        }
-        v = drawing.symbolNames.indexOf(v);
-        if(v >= 0) { v += vbase; }
-    }
-    if((v % 100 >= MAXSYMBOL) || v >= 400) { return 0; }
-    return Math.floor(Math.max(v, 0));
-};
-
-function singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerLine) {
-    // only scatter & box plots get marker path and opacity
-    // bars, histograms don't
-    if(Registry.traceIs(trace, 'symbols')) {
-        var sizeFn = makeBubbleSizeFn(trace);
-
-        sel.attr('d', function(d) {
-            var r;
-
-            // handle multi-trace graph edit case
-            if(d.ms === 'various' || marker.size === 'various') r = 3;
-            else {
-                r = subTypes.isBubble(trace) ?
-                        sizeFn(d.ms) : (marker.size || 6) / 2;
-            }
-
-            // store the calculated size so hover can use it
-            d.mrc = r;
-
-            // turn the symbol into a sanitized number
-            var x = drawing.symbolNumber(d.mx || marker.symbol) || 0,
-                xBase = x % 100;
-
-            // save if this marker is open
-            // because that impacts how to handle colors
-            d.om = x % 200 >= 100;
-
-            return drawing.symbolFuncs[xBase](r) +
-                (x >= 200 ? DOTPATH : '');
-        })
-        .style('opacity', function(d) {
-            return (d.mo + 1 || marker.opacity + 1) - 1;
-        });
-    }
-
-    // 'so' is suspected outliers, for box plots
-    var fillColor,
-        lineColor,
-        lineWidth;
-    if(d.so) {
-        lineWidth = markerLine.outlierwidth;
-        lineColor = markerLine.outliercolor;
-        fillColor = marker.outliercolor;
-    }
-    else {
-        lineWidth = (d.mlw + 1 || markerLine.width + 1 ||
-            // TODO: we need the latter for legends... can we get rid of it?
-            (d.trace ? d.trace.marker.line.width : 0) + 1) - 1;
-
-        if('mlc' in d) lineColor = d.mlcc = lineScale(d.mlc);
-        // weird case: array wasn't long enough to apply to every point
-        else if(Array.isArray(markerLine.color)) lineColor = Color.defaultLine;
-        else lineColor = markerLine.color;
-
-        if('mc' in d) fillColor = d.mcc = markerScale(d.mc);
-        else if(Array.isArray(marker.color)) fillColor = Color.defaultLine;
-        else fillColor = marker.color || 'rgba(0,0,0,0)';
-    }
-
-    if(d.om) {
-        // open markers can't have zero linewidth, default to 1px,
-        // and use fill color as stroke color
-        sel.call(Color.stroke, fillColor)
-            .style({
-                'stroke-width': (lineWidth || 1) + 'px',
-                fill: 'none'
-            });
-    }
-    else {
-        sel.style('stroke-width', lineWidth + 'px')
-            .call(Color.fill, fillColor);
-        if(lineWidth) {
-            sel.call(Color.stroke, lineColor);
-        }
-    }
-}
-
-drawing.singlePointStyle = function(d, sel, trace) {
-    var marker = trace.marker,
-        markerLine = marker.line;
-
-    // allow array marker and marker line colors to be
-    // scaled by given max and min to colorscales
-    var markerScale = drawing.tryColorscale(marker, ''),
-        lineScale = drawing.tryColorscale(marker, 'line');
-
-    singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerLine);
-
-};
-
-drawing.pointStyle = function(s, trace) {
-    if(!s.size()) return;
-
-    // allow array marker and marker line colors to be
-    // scaled by given max and min to colorscales
-    var marker = trace.marker;
-    var markerScale = drawing.tryColorscale(marker, ''),
-        lineScale = drawing.tryColorscale(marker, 'line');
-
-    s.each(function(d) {
-        drawing.singlePointStyle(d, d3.select(this), trace, markerScale, lineScale);
-    });
-};
-
-drawing.tryColorscale = function(marker, prefix) {
-    var cont = prefix ? Lib.nestedProperty(marker, prefix).get() : marker,
-        scl = cont.colorscale,
-        colorArray = cont.color;
-
-    if(scl && Array.isArray(colorArray)) {
-        return Colorscale.makeColorScaleFunc(
-            Colorscale.extractScale(scl, cont.cmin, cont.cmax)
-        );
-    }
-    else return Lib.identity;
-};
-
-// draw text at points
-var TEXTOFFSETSIGN = {start: 1, end: -1, middle: 0, bottom: 1, top: -1},
-    LINEEXPAND = 1.3;
-drawing.textPointStyle = function(s, trace) {
-    s.each(function(d) {
-        var p = d3.select(this),
-            text = d.tx || trace.text;
-
-        if(!text || Array.isArray(text)) {
-            // isArray test handles the case of (intentionally) missing
-            // or empty text within a text array
-            p.remove();
-            return;
-        }
-
-        var pos = d.tp || trace.textposition,
-            v = pos.indexOf('top') !== -1 ? 'top' :
-                pos.indexOf('bottom') !== -1 ? 'bottom' : 'middle',
-            h = pos.indexOf('left') !== -1 ? 'end' :
-                pos.indexOf('right') !== -1 ? 'start' : 'middle',
-            fontSize = d.ts || trace.textfont.size,
-            // if markers are shown, offset a little more than
-            // the nominal marker size
-            // ie 2/1.6 * nominal, bcs some markers are a bit bigger
-            r = d.mrc ? (d.mrc / 0.8 + 1) : 0;
-
-        fontSize = (isNumeric(fontSize) && fontSize > 0) ? fontSize : 0;
-
-        p.call(drawing.font,
-                d.tf || trace.textfont.family,
-                fontSize,
-                d.tc || trace.textfont.color)
-            .attr('text-anchor', h)
-            .text(text)
-            .call(svgTextUtils.convertToTspans);
-        var pgroup = d3.select(this.parentNode),
-            tspans = p.selectAll('tspan.line'),
-            numLines = ((tspans[0].length || 1) - 1) * LINEEXPAND + 1,
-            dx = TEXTOFFSETSIGN[h] * r,
-            dy = fontSize * 0.75 + TEXTOFFSETSIGN[v] * r +
-                (TEXTOFFSETSIGN[v] - 1) * numLines * fontSize / 2;
-
-        // fix the overall text group position
-        pgroup.attr('transform', 'translate(' + dx + ',' + dy + ')');
-
-        // then fix multiline text
-        if(numLines > 1) {
-            tspans.attr({ x: p.attr('x'), y: p.attr('y') });
-        }
-    });
-};
-
-// generalized Catmull-Rom splines, per
-// http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf
-var CatmullRomExp = 0.5;
-drawing.smoothopen = function(pts, smoothness) {
-    if(pts.length < 3) { return 'M' + pts.join('L');}
-    var path = 'M' + pts[0],
-        tangents = [], i;
-    for(i = 1; i < pts.length - 1; i++) {
-        tangents.push(makeTangent(pts[i - 1], pts[i], pts[i + 1], smoothness));
-    }
-    path += 'Q' + tangents[0][0] + ' ' + pts[1];
-    for(i = 2; i < pts.length - 1; i++) {
-        path += 'C' + tangents[i - 2][1] + ' ' + tangents[i - 1][0] + ' ' + pts[i];
-    }
-    path += 'Q' + tangents[pts.length - 3][1] + ' ' + pts[pts.length - 1];
-    return path;
-};
-
-drawing.smoothclosed = function(pts, smoothness) {
-    if(pts.length < 3) { return 'M' + pts.join('L') + 'Z'; }
-    var path = 'M' + pts[0],
-        pLast = pts.length - 1,
-        tangents = [makeTangent(pts[pLast],
-                        pts[0], pts[1], smoothness)],
-        i;
-    for(i = 1; i < pLast; i++) {
-        tangents.push(makeTangent(pts[i - 1], pts[i], pts[i + 1], smoothness));
-    }
-    tangents.push(
-        makeTangent(pts[pLast - 1], pts[pLast], pts[0], smoothness)
-    );
-
-    for(i = 1; i <= pLast; i++) {
-        path += 'C' + tangents[i - 1][1] + ' ' + tangents[i][0] + ' ' + pts[i];
-    }
-    path += 'C' + tangents[pLast][1] + ' ' + tangents[0][0] + ' ' + pts[0] + 'Z';
-    return path;
-};
-
-function makeTangent(prevpt, thispt, nextpt, smoothness) {
-    var d1x = prevpt[0] - thispt[0],
-        d1y = prevpt[1] - thispt[1],
-        d2x = nextpt[0] - thispt[0],
-        d2y = nextpt[1] - thispt[1],
-        d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2),
-        d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2),
-        numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness,
-        numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness,
-        denom1 = 3 * d2a * (d1a + d2a),
-        denom2 = 3 * d1a * (d1a + d2a);
-    return [
-        [
-            d3.round(thispt[0] + (denom1 && numx / denom1), 2),
-            d3.round(thispt[1] + (denom1 && numy / denom1), 2)
-        ], [
-            d3.round(thispt[0] - (denom2 && numx / denom2), 2),
-            d3.round(thispt[1] - (denom2 && numy / denom2), 2)
-        ]
-    ];
-}
-
-// step paths - returns a generator function for paths
-// with the given step shape
-var STEPPATH = {
-    hv: function(p0, p1) {
-        return 'H' + d3.round(p1[0], 2) + 'V' + d3.round(p1[1], 2);
-    },
-    vh: function(p0, p1) {
-        return 'V' + d3.round(p1[1], 2) + 'H' + d3.round(p1[0], 2);
-    },
-    hvh: function(p0, p1) {
-        return 'H' + d3.round((p0[0] + p1[0]) / 2, 2) + 'V' +
-            d3.round(p1[1], 2) + 'H' + d3.round(p1[0], 2);
-    },
-    vhv: function(p0, p1) {
-        return 'V' + d3.round((p0[1] + p1[1]) / 2, 2) + 'H' +
-            d3.round(p1[0], 2) + 'V' + d3.round(p1[1], 2);
-    }
-};
-var STEPLINEAR = function(p0, p1) {
-    return 'L' + d3.round(p1[0], 2) + ',' + d3.round(p1[1], 2);
-};
-drawing.steps = function(shape) {
-    var onestep = STEPPATH[shape] || STEPLINEAR;
-    return function(pts) {
-        var path = 'M' + d3.round(pts[0][0], 2) + ',' + d3.round(pts[0][1], 2);
-        for(var i = 1; i < pts.length; i++) {
-            path += onestep(pts[i - 1], pts[i]);
-        }
-        return path;
-    };
-};
-
-// off-screen svg render testing element, shared by the whole page
-// uses the id 'js-plotly-tester' and stores it in gd._tester
-// makes a hash of cached text items in tester.node()._cache
-// so we can add references to rendered text (including all info
-// needed to fully determine its bounding rect)
-drawing.makeTester = function(gd) {
-    var tester = d3.select('body')
-        .selectAll('#js-plotly-tester')
-        .data([0]);
-
-    tester.enter().append('svg')
-        .attr('id', 'js-plotly-tester')
-        .attr(xmlnsNamespaces.svgAttrs)
-        .style({
-            position: 'absolute',
-            left: '-10000px',
-            top: '-10000px',
-            width: '9000px',
-            height: '9000px',
-            'z-index': '1'
-        });
-
-    // browsers differ on how they describe the bounding rect of
-    // the svg if its contents spill over... so make a 1x1px
-    // reference point we can measure off of.
-    var testref = tester.selectAll('.js-reference-point').data([0]);
-    testref.enter().append('path')
-        .classed('js-reference-point', true)
-        .attr('d', 'M0,0H1V1H0Z')
-        .style({
-            'stroke-width': 0,
-            fill: 'black'
-        });
-
-    if(!tester.node()._cache) {
-        tester.node()._cache = {};
-    }
-
-    gd._tester = tester;
-    gd._testref = testref;
-};
-
-// use our offscreen tester to get a clientRect for an element,
-// in a reference frame where it isn't translated and its anchor
-// point is at (0,0)
-// always returns a copy of the bbox, so the caller can modify it safely
-var savedBBoxes = [],
-    maxSavedBBoxes = 10000;
-drawing.bBox = function(node) {
-    // cache elements we've already measured so we don't have to
-    // remeasure the same thing many times
-    var saveNum = node.attributes['data-bb'];
-    if(saveNum && saveNum.value) {
-        return Lib.extendFlat({}, savedBBoxes[saveNum.value]);
-    }
-
-    var test3 = d3.select('#js-plotly-tester'),
-        tester = test3.node();
-
-    // copy the node to test into the tester
-    var testNode = node.cloneNode(true);
-    tester.appendChild(testNode);
-    // standardize its position... do we really want to do this?
-    d3.select(testNode).attr({
-        x: 0,
-        y: 0,
-        transform: ''
-    });
-
-    var testRect = testNode.getBoundingClientRect(),
-        refRect = test3.select('.js-reference-point')
-            .node().getBoundingClientRect();
-
-    tester.removeChild(testNode);
-
-    var bb = {
-        height: testRect.height,
-        width: testRect.width,
-        left: testRect.left - refRect.left,
-        top: testRect.top - refRect.top,
-        right: testRect.right - refRect.left,
-        bottom: testRect.bottom - refRect.top
-    };
-
-    // make sure we don't have too many saved boxes,
-    // or a long session could overload on memory
-    // by saving boxes for long-gone elements
-    if(savedBBoxes.length >= maxSavedBBoxes) {
-        d3.selectAll('[data-bb]').attr('data-bb', null);
-        savedBBoxes = [];
-    }
-
-    // cache this bbox
-    node.setAttribute('data-bb', savedBBoxes.length);
-    savedBBoxes.push(bb);
-
-    return Lib.extendFlat({}, bb);
-};
-
-/*
- * make a robust clipPath url from a local id
- * note! We'd better not be exporting from a page
- * with a  or the svg will not be portable!
- */
-drawing.setClipUrl = function(s, localId) {
-    if(!localId) {
-        s.attr('clip-path', null);
-        return;
-    }
-
-    var url = '#' + localId,
-        base = d3.select('base');
-
-    // add id to location href w/o hashes if any)
-    if(base.size() && base.attr('href')) {
-        url = window.location.href.split('#')[0] + url;
-    }
-
-    s.attr('clip-path', 'url(' + url + ')');
-};
-
-drawing.getTranslate = function(element) {
-    // Note the separator [^\d] between x and y in this regex
-    // We generally use ',' but IE will convert it to ' '
-    var re = /.*\btranslate\((-?\d*\.?\d*)[^-\d]*(-?\d*\.?\d*)[^\d].*/,
-        getter = element.attr ? 'attr' : 'getAttribute',
-        transform = element[getter]('transform') || '';
-
-    var translate = transform.replace(re, function(match, p1, p2) {
-        return [p1, p2].join(' ');
-    })
-    .split(' ');
-
-    return {
-        x: +translate[0] || 0,
-        y: +translate[1] || 0
-    };
-};
-
-drawing.setTranslate = function(element, x, y) {
-
-    var re = /(\btranslate\(.*?\);?)/,
-        getter = element.attr ? 'attr' : 'getAttribute',
-        setter = element.attr ? 'attr' : 'setAttribute',
-        transform = element[getter]('transform') || '';
-
-    x = x || 0;
-    y = y || 0;
-
-    transform = transform.replace(re, '').trim();
-    transform += ' translate(' + x + ', ' + y + ')';
-    transform = transform.trim();
-
-    element[setter]('transform', transform);
-
-    return transform;
-};
-
-drawing.getScale = function(element) {
-
-    var re = /.*\bscale\((\d*\.?\d*)[^\d]*(\d*\.?\d*)[^\d].*/,
-        getter = element.attr ? 'attr' : 'getAttribute',
-        transform = element[getter]('transform') || '';
-
-    var translate = transform.replace(re, function(match, p1, p2) {
-        return [p1, p2].join(' ');
-    })
-    .split(' ');
-
-    return {
-        x: +translate[0] || 1,
-        y: +translate[1] || 1
-    };
-};
-
-drawing.setScale = function(element, x, y) {
-
-    var re = /(\bscale\(.*?\);?)/,
-        getter = element.attr ? 'attr' : 'getAttribute',
-        setter = element.attr ? 'attr' : 'setAttribute',
-        transform = element[getter]('transform') || '';
-
-    x = x || 1;
-    y = y || 1;
-
-    transform = transform.replace(re, '').trim();
-    transform += ' scale(' + x + ', ' + y + ')';
-    transform = transform.trim();
-
-    element[setter]('transform', transform);
-
-    return transform;
-};
-
-drawing.setPointGroupScale = function(selection, x, y) {
-    var t, scale, re;
-
-    x = x || 1;
-    y = y || 1;
-
-    if(x === 1 && y === 1) {
-        scale = '';
-    } else {
-        // The same scale transform for every point:
-        scale = ' scale(' + x + ',' + y + ')';
-    }
-
-    // A regex to strip any existing scale:
-    re = /\s*sc.*/;
-
-    selection.each(function() {
-        // Get the transform:
-        t = (this.getAttribute('transform') || '').replace(re, '');
-        t += scale;
-        t = t.trim();
-
-        // Append the scale transform
-        this.setAttribute('transform', t);
-    });
-
-    return scale;
-};
diff --git a/src/components/drawing/symbol_defs.js b/src/components/drawing/symbol_defs.js
index 548a8c5c307..46e44801d72 100644
--- a/src/components/drawing/symbol_defs.js
+++ b/src/components/drawing/symbol_defs.js
@@ -5,11 +5,8 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var d3 = require('d3');
+"use strict";
+var d3 = require("d3");
 
 /** Marker symbol definitions
  * users can specify markers either by number or name
@@ -18,457 +15,931 @@ var d3 = require('d3');
  * add 200 (or '-dot') and you get a dot in the middle
  * add both and you get both
  */
-
 module.exports = {
-    circle: {
-        n: 0,
-        f: function(r) {
-            var rs = d3.round(r, 2);
-            return 'M' + rs + ',0A' + rs + ',' + rs + ' 0 1,1 0,-' + rs +
-                'A' + rs + ',' + rs + ' 0 0,1 ' + rs + ',0Z';
-        }
-    },
-    square: {
-        n: 1,
-        f: function(r) {
-            var rs = d3.round(r, 2);
-            return 'M' + rs + ',' + rs + 'H-' + rs + 'V-' + rs + 'H' + rs + 'Z';
-        }
-    },
-    diamond: {
-        n: 2,
-        f: function(r) {
-            var rd = d3.round(r * 1.3, 2);
-            return 'M' + rd + ',0L0,' + rd + 'L-' + rd + ',0L0,-' + rd + 'Z';
-        }
-    },
-    cross: {
-        n: 3,
-        f: function(r) {
-            var rc = d3.round(r * 0.4, 2),
-                rc2 = d3.round(r * 1.2, 2);
-            return 'M' + rc2 + ',' + rc + 'H' + rc + 'V' + rc2 + 'H-' + rc +
-                'V' + rc + 'H-' + rc2 + 'V-' + rc + 'H-' + rc + 'V-' + rc2 +
-                'H' + rc + 'V-' + rc + 'H' + rc2 + 'Z';
-        }
-    },
-    x: {
-        n: 4,
-        f: function(r) {
-            var rx = d3.round(r * 0.8 / Math.sqrt(2), 2),
-                ne = 'l' + rx + ',' + rx,
-                se = 'l' + rx + ',-' + rx,
-                sw = 'l-' + rx + ',-' + rx,
-                nw = 'l-' + rx + ',' + rx;
-            return 'M0,' + rx + ne + se + sw + se + sw + nw + sw + nw + ne + nw + ne + 'Z';
-        }
-    },
-    'triangle-up': {
-        n: 5,
-        f: function(r) {
-            var rt = d3.round(r * 2 / Math.sqrt(3), 2),
-                r2 = d3.round(r / 2, 2),
-                rs = d3.round(r, 2);
-            return 'M-' + rt + ',' + r2 + 'H' + rt + 'L0,-' + rs + 'Z';
-        }
-    },
-    'triangle-down': {
-        n: 6,
-        f: function(r) {
-            var rt = d3.round(r * 2 / Math.sqrt(3), 2),
-                r2 = d3.round(r / 2, 2),
-                rs = d3.round(r, 2);
-            return 'M-' + rt + ',-' + r2 + 'H' + rt + 'L0,' + rs + 'Z';
-        }
-    },
-    'triangle-left': {
-        n: 7,
-        f: function(r) {
-            var rt = d3.round(r * 2 / Math.sqrt(3), 2),
-                r2 = d3.round(r / 2, 2),
-                rs = d3.round(r, 2);
-            return 'M' + r2 + ',-' + rt + 'V' + rt + 'L-' + rs + ',0Z';
-        }
-    },
-    'triangle-right': {
-        n: 8,
-        f: function(r) {
-            var rt = d3.round(r * 2 / Math.sqrt(3), 2),
-                r2 = d3.round(r / 2, 2),
-                rs = d3.round(r, 2);
-            return 'M-' + r2 + ',-' + rt + 'V' + rt + 'L' + rs + ',0Z';
-        }
-    },
-    'triangle-ne': {
-        n: 9,
-        f: function(r) {
-            var r1 = d3.round(r * 0.6, 2),
-                r2 = d3.round(r * 1.2, 2);
-            return 'M-' + r2 + ',-' + r1 + 'H' + r1 + 'V' + r2 + 'Z';
-        }
-    },
-    'triangle-se': {
-        n: 10,
-        f: function(r) {
-            var r1 = d3.round(r * 0.6, 2),
-                r2 = d3.round(r * 1.2, 2);
-            return 'M' + r1 + ',-' + r2 + 'V' + r1 + 'H-' + r2 + 'Z';
-        }
-    },
-    'triangle-sw': {
-        n: 11,
-        f: function(r) {
-            var r1 = d3.round(r * 0.6, 2),
-                r2 = d3.round(r * 1.2, 2);
-            return 'M' + r2 + ',' + r1 + 'H-' + r1 + 'V-' + r2 + 'Z';
-        }
-    },
-    'triangle-nw': {
-        n: 12,
-        f: function(r) {
-            var r1 = d3.round(r * 0.6, 2),
-                r2 = d3.round(r * 1.2, 2);
-            return 'M-' + r1 + ',' + r2 + 'V-' + r1 + 'H' + r2 + 'Z';
-        }
-    },
-    pentagon: {
-        n: 13,
-        f: function(r) {
-            var x1 = d3.round(r * 0.951, 2),
-                x2 = d3.round(r * 0.588, 2),
-                y0 = d3.round(-r, 2),
-                y1 = d3.round(r * -0.309, 2),
-                y2 = d3.round(r * 0.809, 2);
-            return 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2 + 'H-' + x2 +
-                'L-' + x1 + ',' + y1 + 'L0,' + y0 + 'Z';
-        }
-    },
-    hexagon: {
-        n: 14,
-        f: function(r) {
-            var y0 = d3.round(r, 2),
-                y1 = d3.round(r / 2, 2),
-                x = d3.round(r * Math.sqrt(3) / 2, 2);
-            return 'M' + x + ',-' + y1 + 'V' + y1 + 'L0,' + y0 +
-                'L-' + x + ',' + y1 + 'V-' + y1 + 'L0,-' + y0 + 'Z';
-        }
-    },
-    hexagon2: {
-        n: 15,
-        f: function(r) {
-            var x0 = d3.round(r, 2),
-                x1 = d3.round(r / 2, 2),
-                y = d3.round(r * Math.sqrt(3) / 2, 2);
-            return 'M-' + x1 + ',' + y + 'H' + x1 + 'L' + x0 +
-                ',0L' + x1 + ',-' + y + 'H-' + x1 + 'L-' + x0 + ',0Z';
-        }
-    },
-    octagon: {
-        n: 16,
-        f: function(r) {
-            var a = d3.round(r * 0.924, 2),
-                b = d3.round(r * 0.383, 2);
-            return 'M-' + b + ',-' + a + 'H' + b + 'L' + a + ',-' + b + 'V' + b +
-                'L' + b + ',' + a + 'H-' + b + 'L-' + a + ',' + b + 'V-' + b + 'Z';
-        }
-    },
-    star: {
-        n: 17,
-        f: function(r) {
-            var rs = r * 1.4,
-                x1 = d3.round(rs * 0.225, 2),
-                x2 = d3.round(rs * 0.951, 2),
-                x3 = d3.round(rs * 0.363, 2),
-                x4 = d3.round(rs * 0.588, 2),
-                y0 = d3.round(-rs, 2),
-                y1 = d3.round(rs * -0.309, 2),
-                y3 = d3.round(rs * 0.118, 2),
-                y4 = d3.round(rs * 0.809, 2),
-                y5 = d3.round(rs * 0.382, 2);
-            return 'M' + x1 + ',' + y1 + 'H' + x2 + 'L' + x3 + ',' + y3 +
-                'L' + x4 + ',' + y4 + 'L0,' + y5 + 'L-' + x4 + ',' + y4 +
-                'L-' + x3 + ',' + y3 + 'L-' + x2 + ',' + y1 + 'H-' + x1 +
-                'L0,' + y0 + 'Z';
-        }
-    },
-    hexagram: {
-        n: 18,
-        f: function(r) {
-            var y = d3.round(r * 0.66, 2),
-                x1 = d3.round(r * 0.38, 2),
-                x2 = d3.round(r * 0.76, 2);
-            return 'M-' + x2 + ',0l-' + x1 + ',-' + y + 'h' + x2 +
-                'l' + x1 + ',-' + y + 'l' + x1 + ',' + y + 'h' + x2 +
-                'l-' + x1 + ',' + y + 'l' + x1 + ',' + y + 'h-' + x2 +
-                'l-' + x1 + ',' + y + 'l-' + x1 + ',-' + y + 'h-' + x2 + 'Z';
-        }
-    },
-    'star-triangle-up': {
-        n: 19,
-        f: function(r) {
-            var x = d3.round(r * Math.sqrt(3) * 0.8, 2),
-                y1 = d3.round(r * 0.8, 2),
-                y2 = d3.round(r * 1.6, 2),
-                rc = d3.round(r * 4, 2),
-                aPart = 'A ' + rc + ',' + rc + ' 0 0 1 ';
-            return 'M-' + x + ',' + y1 + aPart + x + ',' + y1 +
-                aPart + '0,-' + y2 + aPart + '-' + x + ',' + y1 + 'Z';
-        }
-    },
-    'star-triangle-down': {
-        n: 20,
-        f: function(r) {
-            var x = d3.round(r * Math.sqrt(3) * 0.8, 2),
-                y1 = d3.round(r * 0.8, 2),
-                y2 = d3.round(r * 1.6, 2),
-                rc = d3.round(r * 4, 2),
-                aPart = 'A ' + rc + ',' + rc + ' 0 0 1 ';
-            return 'M' + x + ',-' + y1 + aPart + '-' + x + ',-' + y1 +
-                aPart + '0,' + y2 + aPart + x + ',-' + y1 + 'Z';
-        }
-    },
-    'star-square': {
-        n: 21,
-        f: function(r) {
-            var rp = d3.round(r * 1.1, 2),
-                rc = d3.round(r * 2, 2),
-                aPart = 'A ' + rc + ',' + rc + ' 0 0 1 ';
-            return 'M-' + rp + ',-' + rp + aPart + '-' + rp + ',' + rp +
-                aPart + rp + ',' + rp + aPart + rp + ',-' + rp +
-                aPart + '-' + rp + ',-' + rp + 'Z';
-        }
-    },
-    'star-diamond': {
-        n: 22,
-        f: function(r) {
-            var rp = d3.round(r * 1.4, 2),
-                rc = d3.round(r * 1.9, 2),
-                aPart = 'A ' + rc + ',' + rc + ' 0 0 1 ';
-            return 'M-' + rp + ',0' + aPart + '0,' + rp +
-                aPart + rp + ',0' + aPart + '0,-' + rp +
-                aPart + '-' + rp + ',0' + 'Z';
-        }
-    },
-    'diamond-tall': {
-        n: 23,
-        f: function(r) {
-            var x = d3.round(r * 0.7, 2),
-                y = d3.round(r * 1.4, 2);
-            return 'M0,' + y + 'L' + x + ',0L0,-' + y + 'L-' + x + ',0Z';
-        }
-    },
-    'diamond-wide': {
-        n: 24,
-        f: function(r) {
-            var x = d3.round(r * 1.4, 2),
-                y = d3.round(r * 0.7, 2);
-            return 'M0,' + y + 'L' + x + ',0L0,-' + y + 'L-' + x + ',0Z';
-        }
-    },
-    hourglass: {
-        n: 25,
-        f: function(r) {
-            var rs = d3.round(r, 2);
-            return 'M' + rs + ',' + rs + 'H-' + rs + 'L' + rs + ',-' + rs + 'H-' + rs + 'Z';
-        },
-        noDot: true
-    },
-    bowtie: {
-        n: 26,
-        f: function(r) {
-            var rs = d3.round(r, 2);
-            return 'M' + rs + ',' + rs + 'V-' + rs + 'L-' + rs + ',' + rs + 'V-' + rs + 'Z';
-        },
-        noDot: true
-    },
-    'circle-cross': {
-        n: 27,
-        f: function(r) {
-            var rs = d3.round(r, 2);
-            return 'M0,' + rs + 'V-' + rs + 'M' + rs + ',0H-' + rs +
-                'M' + rs + ',0A' + rs + ',' + rs + ' 0 1,1 0,-' + rs +
-                'A' + rs + ',' + rs + ' 0 0,1 ' + rs + ',0Z';
-        },
-        needLine: true,
-        noDot: true
-    },
-    'circle-x': {
-        n: 28,
-        f: function(r) {
-            var rs = d3.round(r, 2),
-                rc = d3.round(r / Math.sqrt(2), 2);
-            return 'M' + rc + ',' + rc + 'L-' + rc + ',-' + rc +
-                'M' + rc + ',-' + rc + 'L-' + rc + ',' + rc +
-                'M' + rs + ',0A' + rs + ',' + rs + ' 0 1,1 0,-' + rs +
-                'A' + rs + ',' + rs + ' 0 0,1 ' + rs + ',0Z';
-        },
-        needLine: true,
-        noDot: true
-    },
-    'square-cross': {
-        n: 29,
-        f: function(r) {
-            var rs = d3.round(r, 2);
-            return 'M0,' + rs + 'V-' + rs + 'M' + rs + ',0H-' + rs +
-                'M' + rs + ',' + rs + 'H-' + rs + 'V-' + rs + 'H' + rs + 'Z';
-        },
-        needLine: true,
-        noDot: true
-    },
-    'square-x': {
-        n: 30,
-        f: function(r) {
-            var rs = d3.round(r, 2);
-            return 'M' + rs + ',' + rs + 'L-' + rs + ',-' + rs +
-                'M' + rs + ',-' + rs + 'L-' + rs + ',' + rs +
-                'M' + rs + ',' + rs + 'H-' + rs + 'V-' + rs + 'H' + rs + 'Z';
-        },
-        needLine: true,
-        noDot: true
-    },
-    'diamond-cross': {
-        n: 31,
-        f: function(r) {
-            var rd = d3.round(r * 1.3, 2);
-            return 'M' + rd + ',0L0,' + rd + 'L-' + rd + ',0L0,-' + rd + 'Z' +
-                'M0,-' + rd + 'V' + rd + 'M-' + rd + ',0H' + rd;
-        },
-        needLine: true,
-        noDot: true
-    },
-    'diamond-x': {
-        n: 32,
-        f: function(r) {
-            var rd = d3.round(r * 1.3, 2),
-                r2 = d3.round(r * 0.65, 2);
-            return 'M' + rd + ',0L0,' + rd + 'L-' + rd + ',0L0,-' + rd + 'Z' +
-                'M-' + r2 + ',-' + r2 + 'L' + r2 + ',' + r2 +
-                'M-' + r2 + ',' + r2 + 'L' + r2 + ',-' + r2;
-        },
-        needLine: true,
-        noDot: true
-    },
-    'cross-thin': {
-        n: 33,
-        f: function(r) {
-            var rc = d3.round(r * 1.4, 2);
-            return 'M0,' + rc + 'V-' + rc + 'M' + rc + ',0H-' + rc;
-        },
-        needLine: true,
-        noDot: true
-    },
-    'x-thin': {
-        n: 34,
-        f: function(r) {
-            var rx = d3.round(r, 2);
-            return 'M' + rx + ',' + rx + 'L-' + rx + ',-' + rx +
-                'M' + rx + ',-' + rx + 'L-' + rx + ',' + rx;
-        },
-        needLine: true,
-        noDot: true
-    },
-    asterisk: {
-        n: 35,
-        f: function(r) {
-            var rc = d3.round(r * 1.2, 2);
-            var rs = d3.round(r * 0.85, 2);
-            return 'M0,' + rc + 'V-' + rc + 'M' + rc + ',0H-' + rc +
-                'M' + rs + ',' + rs + 'L-' + rs + ',-' + rs +
-                'M' + rs + ',-' + rs + 'L-' + rs + ',' + rs;
-        },
-        needLine: true,
-        noDot: true
-    },
-    hash: {
-        n: 36,
-        f: function(r) {
-            var r1 = d3.round(r / 2, 2),
-                r2 = d3.round(r, 2);
-            return 'M' + r1 + ',' + r2 + 'V-' + r2 +
-                'm-' + r2 + ',0V' + r2 +
-                'M' + r2 + ',' + r1 + 'H-' + r2 +
-                'm0,-' + r2 + 'H' + r2;
-        },
-        needLine: true
-    },
-    'y-up': {
-        n: 37,
-        f: function(r) {
-            var x = d3.round(r * 1.2, 2),
-                y0 = d3.round(r * 1.6, 2),
-                y1 = d3.round(r * 0.8, 2);
-            return 'M-' + x + ',' + y1 + 'L0,0M' + x + ',' + y1 + 'L0,0M0,-' + y0 + 'L0,0';
-        },
-        needLine: true,
-        noDot: true
-    },
-    'y-down': {
-        n: 38,
-        f: function(r) {
-            var x = d3.round(r * 1.2, 2),
-                y0 = d3.round(r * 1.6, 2),
-                y1 = d3.round(r * 0.8, 2);
-            return 'M-' + x + ',-' + y1 + 'L0,0M' + x + ',-' + y1 + 'L0,0M0,' + y0 + 'L0,0';
-        },
-        needLine: true,
-        noDot: true
-    },
-    'y-left': {
-        n: 39,
-        f: function(r) {
-            var y = d3.round(r * 1.2, 2),
-                x0 = d3.round(r * 1.6, 2),
-                x1 = d3.round(r * 0.8, 2);
-            return 'M' + x1 + ',' + y + 'L0,0M' + x1 + ',-' + y + 'L0,0M-' + x0 + ',0L0,0';
-        },
-        needLine: true,
-        noDot: true
-    },
-    'y-right': {
-        n: 40,
-        f: function(r) {
-            var y = d3.round(r * 1.2, 2),
-                x0 = d3.round(r * 1.6, 2),
-                x1 = d3.round(r * 0.8, 2);
-            return 'M-' + x1 + ',' + y + 'L0,0M-' + x1 + ',-' + y + 'L0,0M' + x0 + ',0L0,0';
-        },
-        needLine: true,
-        noDot: true
-    },
-    'line-ew': {
-        n: 41,
-        f: function(r) {
-            var rc = d3.round(r * 1.4, 2);
-            return 'M' + rc + ',0H-' + rc;
-        },
-        needLine: true,
-        noDot: true
-    },
-    'line-ns': {
-        n: 42,
-        f: function(r) {
-            var rc = d3.round(r * 1.4, 2);
-            return 'M0,' + rc + 'V-' + rc;
-        },
-        needLine: true,
-        noDot: true
-    },
-    'line-ne': {
-        n: 43,
-        f: function(r) {
-            var rx = d3.round(r, 2);
-            return 'M' + rx + ',-' + rx + 'L-' + rx + ',' + rx;
-        },
-        needLine: true,
-        noDot: true
-    },
-    'line-nw': {
-        n: 44,
-        f: function(r) {
-            var rx = d3.round(r, 2);
-            return 'M' + rx + ',' + rx + 'L-' + rx + ',-' + rx;
-        },
-        needLine: true,
-        noDot: true
+  circle: {
+    n: 0,
+    f: function(r) {
+      var rs = d3.round(r, 2);
+      return "M" +
+        rs +
+        ",0A" +
+        rs +
+        "," +
+        rs +
+        " 0 1,1 0,-" +
+        rs +
+        "A" +
+        rs +
+        "," +
+        rs +
+        " 0 0,1 " +
+        rs +
+        ",0Z";
+    }
+  },
+  square: {
+    n: 1,
+    f: function(r) {
+      var rs = d3.round(r, 2);
+      return "M" + rs + "," + rs + "H-" + rs + "V-" + rs + "H" + rs + "Z";
+    }
+  },
+  diamond: {
+    n: 2,
+    f: function(r) {
+      var rd = d3.round(r * 1.3, 2);
+      return "M" + rd + ",0L0," + rd + "L-" + rd + ",0L0,-" + rd + "Z";
+    }
+  },
+  cross: {
+    n: 3,
+    f: function(r) {
+      var rc = d3.round(r * 0.4, 2), rc2 = d3.round(r * 1.2, 2);
+      return "M" +
+        rc2 +
+        "," +
+        rc +
+        "H" +
+        rc +
+        "V" +
+        rc2 +
+        "H-" +
+        rc +
+        "V" +
+        rc +
+        "H-" +
+        rc2 +
+        "V-" +
+        rc +
+        "H-" +
+        rc +
+        "V-" +
+        rc2 +
+        "H" +
+        rc +
+        "V-" +
+        rc +
+        "H" +
+        rc2 +
+        "Z";
+    }
+  },
+  x: {
+    n: 4,
+    f: function(r) {
+      var rx = d3.round(r * 0.8 / Math.sqrt(2), 2),
+        ne = "l" + rx + "," + rx,
+        se = "l" + rx + ",-" + rx,
+        sw = "l-" + rx + ",-" + rx,
+        nw = "l-" + rx + "," + rx;
+      return "M0," +
+        rx +
+        ne +
+        se +
+        sw +
+        se +
+        sw +
+        nw +
+        sw +
+        nw +
+        ne +
+        nw +
+        ne +
+        "Z";
+    }
+  },
+  "triangle-up": {
+    n: 5,
+    f: function(r) {
+      var rt = d3.round(r * 2 / Math.sqrt(3), 2),
+        r2 = d3.round(r / 2, 2),
+        rs = d3.round(r, 2);
+      return "M-" + rt + "," + r2 + "H" + rt + "L0,-" + rs + "Z";
+    }
+  },
+  "triangle-down": {
+    n: 6,
+    f: function(r) {
+      var rt = d3.round(r * 2 / Math.sqrt(3), 2),
+        r2 = d3.round(r / 2, 2),
+        rs = d3.round(r, 2);
+      return "M-" + rt + ",-" + r2 + "H" + rt + "L0," + rs + "Z";
+    }
+  },
+  "triangle-left": {
+    n: 7,
+    f: function(r) {
+      var rt = d3.round(r * 2 / Math.sqrt(3), 2),
+        r2 = d3.round(r / 2, 2),
+        rs = d3.round(r, 2);
+      return "M" + r2 + ",-" + rt + "V" + rt + "L-" + rs + ",0Z";
+    }
+  },
+  "triangle-right": {
+    n: 8,
+    f: function(r) {
+      var rt = d3.round(r * 2 / Math.sqrt(3), 2),
+        r2 = d3.round(r / 2, 2),
+        rs = d3.round(r, 2);
+      return "M-" + r2 + ",-" + rt + "V" + rt + "L" + rs + ",0Z";
+    }
+  },
+  "triangle-ne": {
+    n: 9,
+    f: function(r) {
+      var r1 = d3.round(r * 0.6, 2), r2 = d3.round(r * 1.2, 2);
+      return "M-" + r2 + ",-" + r1 + "H" + r1 + "V" + r2 + "Z";
+    }
+  },
+  "triangle-se": {
+    n: 10,
+    f: function(r) {
+      var r1 = d3.round(r * 0.6, 2), r2 = d3.round(r * 1.2, 2);
+      return "M" + r1 + ",-" + r2 + "V" + r1 + "H-" + r2 + "Z";
+    }
+  },
+  "triangle-sw": {
+    n: 11,
+    f: function(r) {
+      var r1 = d3.round(r * 0.6, 2), r2 = d3.round(r * 1.2, 2);
+      return "M" + r2 + "," + r1 + "H-" + r1 + "V-" + r2 + "Z";
+    }
+  },
+  "triangle-nw": {
+    n: 12,
+    f: function(r) {
+      var r1 = d3.round(r * 0.6, 2), r2 = d3.round(r * 1.2, 2);
+      return "M-" + r1 + "," + r2 + "V-" + r1 + "H" + r2 + "Z";
+    }
+  },
+  pentagon: {
+    n: 13,
+    f: function(r) {
+      var x1 = d3.round(r * 0.951, 2),
+        x2 = d3.round(r * 0.588, 2),
+        y0 = d3.round(-r, 2),
+        y1 = d3.round(r * (-0.309), 2),
+        y2 = d3.round(r * 0.809, 2);
+      return "M" +
+        x1 +
+        "," +
+        y1 +
+        "L" +
+        x2 +
+        "," +
+        y2 +
+        "H-" +
+        x2 +
+        "L-" +
+        x1 +
+        "," +
+        y1 +
+        "L0," +
+        y0 +
+        "Z";
+    }
+  },
+  hexagon: {
+    n: 14,
+    f: function(r) {
+      var y0 = d3.round(r, 2),
+        y1 = d3.round(r / 2, 2),
+        x = d3.round(r * Math.sqrt(3) / 2, 2);
+      return "M" +
+        x +
+        ",-" +
+        y1 +
+        "V" +
+        y1 +
+        "L0," +
+        y0 +
+        "L-" +
+        x +
+        "," +
+        y1 +
+        "V-" +
+        y1 +
+        "L0,-" +
+        y0 +
+        "Z";
+    }
+  },
+  hexagon2: {
+    n: 15,
+    f: function(r) {
+      var x0 = d3.round(r, 2),
+        x1 = d3.round(r / 2, 2),
+        y = d3.round(r * Math.sqrt(3) / 2, 2);
+      return "M-" +
+        x1 +
+        "," +
+        y +
+        "H" +
+        x1 +
+        "L" +
+        x0 +
+        ",0L" +
+        x1 +
+        ",-" +
+        y +
+        "H-" +
+        x1 +
+        "L-" +
+        x0 +
+        ",0Z";
+    }
+  },
+  octagon: {
+    n: 16,
+    f: function(r) {
+      var a = d3.round(r * 0.924, 2), b = d3.round(r * 0.383, 2);
+      return "M-" +
+        b +
+        ",-" +
+        a +
+        "H" +
+        b +
+        "L" +
+        a +
+        ",-" +
+        b +
+        "V" +
+        b +
+        "L" +
+        b +
+        "," +
+        a +
+        "H-" +
+        b +
+        "L-" +
+        a +
+        "," +
+        b +
+        "V-" +
+        b +
+        "Z";
+    }
+  },
+  star: {
+    n: 17,
+    f: function(r) {
+      var rs = r * 1.4,
+        x1 = d3.round(rs * 0.225, 2),
+        x2 = d3.round(rs * 0.951, 2),
+        x3 = d3.round(rs * 0.363, 2),
+        x4 = d3.round(rs * 0.588, 2),
+        y0 = d3.round(-rs, 2),
+        y1 = d3.round(rs * (-0.309), 2),
+        y3 = d3.round(rs * 0.118, 2),
+        y4 = d3.round(rs * 0.809, 2),
+        y5 = d3.round(rs * 0.382, 2);
+      return "M" +
+        x1 +
+        "," +
+        y1 +
+        "H" +
+        x2 +
+        "L" +
+        x3 +
+        "," +
+        y3 +
+        "L" +
+        x4 +
+        "," +
+        y4 +
+        "L0," +
+        y5 +
+        "L-" +
+        x4 +
+        "," +
+        y4 +
+        "L-" +
+        x3 +
+        "," +
+        y3 +
+        "L-" +
+        x2 +
+        "," +
+        y1 +
+        "H-" +
+        x1 +
+        "L0," +
+        y0 +
+        "Z";
+    }
+  },
+  hexagram: {
+    n: 18,
+    f: function(r) {
+      var y = d3.round(r * 0.66, 2),
+        x1 = d3.round(r * 0.38, 2),
+        x2 = d3.round(r * 0.76, 2);
+      return "M-" +
+        x2 +
+        ",0l-" +
+        x1 +
+        ",-" +
+        y +
+        "h" +
+        x2 +
+        "l" +
+        x1 +
+        ",-" +
+        y +
+        "l" +
+        x1 +
+        "," +
+        y +
+        "h" +
+        x2 +
+        "l-" +
+        x1 +
+        "," +
+        y +
+        "l" +
+        x1 +
+        "," +
+        y +
+        "h-" +
+        x2 +
+        "l-" +
+        x1 +
+        "," +
+        y +
+        "l-" +
+        x1 +
+        ",-" +
+        y +
+        "h-" +
+        x2 +
+        "Z";
+    }
+  },
+  "star-triangle-up": {
+    n: 19,
+    f: function(r) {
+      var x = d3.round(r * Math.sqrt(3) * 0.8, 2),
+        y1 = d3.round(r * 0.8, 2),
+        y2 = d3.round(r * 1.6, 2),
+        rc = d3.round(r * 4, 2),
+        aPart = "A " + rc + "," + rc + " 0 0 1 ";
+      return "M-" +
+        x +
+        "," +
+        y1 +
+        aPart +
+        x +
+        "," +
+        y1 +
+        aPart +
+        "0,-" +
+        y2 +
+        aPart +
+        "-" +
+        x +
+        "," +
+        y1 +
+        "Z";
+    }
+  },
+  "star-triangle-down": {
+    n: 20,
+    f: function(r) {
+      var x = d3.round(r * Math.sqrt(3) * 0.8, 2),
+        y1 = d3.round(r * 0.8, 2),
+        y2 = d3.round(r * 1.6, 2),
+        rc = d3.round(r * 4, 2),
+        aPart = "A " + rc + "," + rc + " 0 0 1 ";
+      return "M" +
+        x +
+        ",-" +
+        y1 +
+        aPart +
+        "-" +
+        x +
+        ",-" +
+        y1 +
+        aPart +
+        "0," +
+        y2 +
+        aPart +
+        x +
+        ",-" +
+        y1 +
+        "Z";
+    }
+  },
+  "star-square": {
+    n: 21,
+    f: function(r) {
+      var rp = d3.round(r * 1.1, 2),
+        rc = d3.round(r * 2, 2),
+        aPart = "A " + rc + "," + rc + " 0 0 1 ";
+      return "M-" +
+        rp +
+        ",-" +
+        rp +
+        aPart +
+        "-" +
+        rp +
+        "," +
+        rp +
+        aPart +
+        rp +
+        "," +
+        rp +
+        aPart +
+        rp +
+        ",-" +
+        rp +
+        aPart +
+        "-" +
+        rp +
+        ",-" +
+        rp +
+        "Z";
+    }
+  },
+  "star-diamond": {
+    n: 22,
+    f: function(r) {
+      var rp = d3.round(r * 1.4, 2),
+        rc = d3.round(r * 1.9, 2),
+        aPart = "A " + rc + "," + rc + " 0 0 1 ";
+      return "M-" +
+        rp +
+        ",0" +
+        aPart +
+        "0," +
+        rp +
+        aPart +
+        rp +
+        ",0" +
+        aPart +
+        "0,-" +
+        rp +
+        aPart +
+        "-" +
+        rp +
+        ",0" +
+        "Z";
+    }
+  },
+  "diamond-tall": {
+    n: 23,
+    f: function(r) {
+      var x = d3.round(r * 0.7, 2), y = d3.round(r * 1.4, 2);
+      return "M0," + y + "L" + x + ",0L0,-" + y + "L-" + x + ",0Z";
+    }
+  },
+  "diamond-wide": {
+    n: 24,
+    f: function(r) {
+      var x = d3.round(r * 1.4, 2), y = d3.round(r * 0.7, 2);
+      return "M0," + y + "L" + x + ",0L0,-" + y + "L-" + x + ",0Z";
     }
+  },
+  hourglass: {
+    n: 25,
+    f: function(r) {
+      var rs = d3.round(r, 2);
+      return "M" +
+        rs +
+        "," +
+        rs +
+        "H-" +
+        rs +
+        "L" +
+        rs +
+        ",-" +
+        rs +
+        "H-" +
+        rs +
+        "Z";
+    },
+    noDot: true
+  },
+  bowtie: {
+    n: 26,
+    f: function(r) {
+      var rs = d3.round(r, 2);
+      return "M" +
+        rs +
+        "," +
+        rs +
+        "V-" +
+        rs +
+        "L-" +
+        rs +
+        "," +
+        rs +
+        "V-" +
+        rs +
+        "Z";
+    },
+    noDot: true
+  },
+  "circle-cross": {
+    n: 27,
+    f: function(r) {
+      var rs = d3.round(r, 2);
+      return "M0," +
+        rs +
+        "V-" +
+        rs +
+        "M" +
+        rs +
+        ",0H-" +
+        rs +
+        "M" +
+        rs +
+        ",0A" +
+        rs +
+        "," +
+        rs +
+        " 0 1,1 0,-" +
+        rs +
+        "A" +
+        rs +
+        "," +
+        rs +
+        " 0 0,1 " +
+        rs +
+        ",0Z";
+    },
+    needLine: true,
+    noDot: true
+  },
+  "circle-x": {
+    n: 28,
+    f: function(r) {
+      var rs = d3.round(r, 2), rc = d3.round(r / Math.sqrt(2), 2);
+      return "M" +
+        rc +
+        "," +
+        rc +
+        "L-" +
+        rc +
+        ",-" +
+        rc +
+        "M" +
+        rc +
+        ",-" +
+        rc +
+        "L-" +
+        rc +
+        "," +
+        rc +
+        "M" +
+        rs +
+        ",0A" +
+        rs +
+        "," +
+        rs +
+        " 0 1,1 0,-" +
+        rs +
+        "A" +
+        rs +
+        "," +
+        rs +
+        " 0 0,1 " +
+        rs +
+        ",0Z";
+    },
+    needLine: true,
+    noDot: true
+  },
+  "square-cross": {
+    n: 29,
+    f: function(r) {
+      var rs = d3.round(r, 2);
+      return "M0," +
+        rs +
+        "V-" +
+        rs +
+        "M" +
+        rs +
+        ",0H-" +
+        rs +
+        "M" +
+        rs +
+        "," +
+        rs +
+        "H-" +
+        rs +
+        "V-" +
+        rs +
+        "H" +
+        rs +
+        "Z";
+    },
+    needLine: true,
+    noDot: true
+  },
+  "square-x": {
+    n: 30,
+    f: function(r) {
+      var rs = d3.round(r, 2);
+      return "M" +
+        rs +
+        "," +
+        rs +
+        "L-" +
+        rs +
+        ",-" +
+        rs +
+        "M" +
+        rs +
+        ",-" +
+        rs +
+        "L-" +
+        rs +
+        "," +
+        rs +
+        "M" +
+        rs +
+        "," +
+        rs +
+        "H-" +
+        rs +
+        "V-" +
+        rs +
+        "H" +
+        rs +
+        "Z";
+    },
+    needLine: true,
+    noDot: true
+  },
+  "diamond-cross": {
+    n: 31,
+    f: function(r) {
+      var rd = d3.round(r * 1.3, 2);
+      return "M" +
+        rd +
+        ",0L0," +
+        rd +
+        "L-" +
+        rd +
+        ",0L0,-" +
+        rd +
+        "Z" +
+        "M0,-" +
+        rd +
+        "V" +
+        rd +
+        "M-" +
+        rd +
+        ",0H" +
+        rd;
+    },
+    needLine: true,
+    noDot: true
+  },
+  "diamond-x": {
+    n: 32,
+    f: function(r) {
+      var rd = d3.round(r * 1.3, 2), r2 = d3.round(r * 0.65, 2);
+      return "M" +
+        rd +
+        ",0L0," +
+        rd +
+        "L-" +
+        rd +
+        ",0L0,-" +
+        rd +
+        "Z" +
+        "M-" +
+        r2 +
+        ",-" +
+        r2 +
+        "L" +
+        r2 +
+        "," +
+        r2 +
+        "M-" +
+        r2 +
+        "," +
+        r2 +
+        "L" +
+        r2 +
+        ",-" +
+        r2;
+    },
+    needLine: true,
+    noDot: true
+  },
+  "cross-thin": {
+    n: 33,
+    f: function(r) {
+      var rc = d3.round(r * 1.4, 2);
+      return "M0," + rc + "V-" + rc + "M" + rc + ",0H-" + rc;
+    },
+    needLine: true,
+    noDot: true
+  },
+  "x-thin": {
+    n: 34,
+    f: function(r) {
+      var rx = d3.round(r, 2);
+      return "M" +
+        rx +
+        "," +
+        rx +
+        "L-" +
+        rx +
+        ",-" +
+        rx +
+        "M" +
+        rx +
+        ",-" +
+        rx +
+        "L-" +
+        rx +
+        "," +
+        rx;
+    },
+    needLine: true,
+    noDot: true
+  },
+  asterisk: {
+    n: 35,
+    f: function(r) {
+      var rc = d3.round(r * 1.2, 2);
+      var rs = d3.round(r * 0.85, 2);
+      return "M0," +
+        rc +
+        "V-" +
+        rc +
+        "M" +
+        rc +
+        ",0H-" +
+        rc +
+        "M" +
+        rs +
+        "," +
+        rs +
+        "L-" +
+        rs +
+        ",-" +
+        rs +
+        "M" +
+        rs +
+        ",-" +
+        rs +
+        "L-" +
+        rs +
+        "," +
+        rs;
+    },
+    needLine: true,
+    noDot: true
+  },
+  hash: {
+    n: 36,
+    f: function(r) {
+      var r1 = d3.round(r / 2, 2), r2 = d3.round(r, 2);
+      return "M" +
+        r1 +
+        "," +
+        r2 +
+        "V-" +
+        r2 +
+        "m-" +
+        r2 +
+        ",0V" +
+        r2 +
+        "M" +
+        r2 +
+        "," +
+        r1 +
+        "H-" +
+        r2 +
+        "m0,-" +
+        r2 +
+        "H" +
+        r2;
+    },
+    needLine: true
+  },
+  "y-up": {
+    n: 37,
+    f: function(r) {
+      var x = d3.round(r * 1.2, 2),
+        y0 = d3.round(r * 1.6, 2),
+        y1 = d3.round(r * 0.8, 2);
+      return "M-" +
+        x +
+        "," +
+        y1 +
+        "L0,0M" +
+        x +
+        "," +
+        y1 +
+        "L0,0M0,-" +
+        y0 +
+        "L0,0";
+    },
+    needLine: true,
+    noDot: true
+  },
+  "y-down": {
+    n: 38,
+    f: function(r) {
+      var x = d3.round(r * 1.2, 2),
+        y0 = d3.round(r * 1.6, 2),
+        y1 = d3.round(r * 0.8, 2);
+      return "M-" +
+        x +
+        ",-" +
+        y1 +
+        "L0,0M" +
+        x +
+        ",-" +
+        y1 +
+        "L0,0M0," +
+        y0 +
+        "L0,0";
+    },
+    needLine: true,
+    noDot: true
+  },
+  "y-left": {
+    n: 39,
+    f: function(r) {
+      var y = d3.round(r * 1.2, 2),
+        x0 = d3.round(r * 1.6, 2),
+        x1 = d3.round(r * 0.8, 2);
+      return "M" +
+        x1 +
+        "," +
+        y +
+        "L0,0M" +
+        x1 +
+        ",-" +
+        y +
+        "L0,0M-" +
+        x0 +
+        ",0L0,0";
+    },
+    needLine: true,
+    noDot: true
+  },
+  "y-right": {
+    n: 40,
+    f: function(r) {
+      var y = d3.round(r * 1.2, 2),
+        x0 = d3.round(r * 1.6, 2),
+        x1 = d3.round(r * 0.8, 2);
+      return "M-" +
+        x1 +
+        "," +
+        y +
+        "L0,0M-" +
+        x1 +
+        ",-" +
+        y +
+        "L0,0M" +
+        x0 +
+        ",0L0,0";
+    },
+    needLine: true,
+    noDot: true
+  },
+  "line-ew": {
+    n: 41,
+    f: function(r) {
+      var rc = d3.round(r * 1.4, 2);
+      return "M" + rc + ",0H-" + rc;
+    },
+    needLine: true,
+    noDot: true
+  },
+  "line-ns": {
+    n: 42,
+    f: function(r) {
+      var rc = d3.round(r * 1.4, 2);
+      return "M0," + rc + "V-" + rc;
+    },
+    needLine: true,
+    noDot: true
+  },
+  "line-ne": {
+    n: 43,
+    f: function(r) {
+      var rx = d3.round(r, 2);
+      return "M" + rx + ",-" + rx + "L-" + rx + "," + rx;
+    },
+    needLine: true,
+    noDot: true
+  },
+  "line-nw": {
+    n: 44,
+    f: function(r) {
+      var rx = d3.round(r, 2);
+      return "M" + rx + "," + rx + "L-" + rx + ",-" + rx;
+    },
+    needLine: true,
+    noDot: true
+  }
 };
diff --git a/src/components/errorbars/attributes.js b/src/components/errorbars/attributes.js
index be441d7b364..58a5e6896d8 100644
--- a/src/components/errorbars/attributes.js
+++ b/src/components/errorbars/attributes.js
@@ -5,136 +5,112 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-    visible: {
-        valType: 'boolean',
-        role: 'info',
-        description: [
-            'Determines whether or not this set of error bars is visible.'
-        ].join(' ')
-    },
-    type: {
-        valType: 'enumerated',
-        values: ['percent', 'constant', 'sqrt', 'data'],
-        role: 'info',
-        description: [
-            'Determines the rule used to generate the error bars.',
-
-            'If *constant`, the bar lengths are of a constant value.',
-            'Set this constant in `value`.',
-
-            'If *percent*, the bar lengths correspond to a percentage of',
-            'underlying data. Set this percentage in `value`.',
-
-            'If *sqrt*, the bar lengths correspond to the sqaure of the',
-            'underlying data.',
-
-            'If *array*, the bar lengths are set with data set `array`.'
-        ].join(' ')
-    },
-    symmetric: {
-        valType: 'boolean',
-        role: 'info',
-        description: [
-            'Determines whether or not the error bars have the same length',
-            'in both direction',
-            '(top/bottom for vertical bars, left/right for horizontal bars.'
-        ].join(' ')
-    },
-    array: {
-        valType: 'data_array',
-        description: [
-            'Sets the data corresponding the length of each error bar.',
-            'Values are plotted relative to the underlying data.'
-        ].join(' ')
-    },
-    arrayminus: {
-        valType: 'data_array',
-        description: [
-            'Sets the data corresponding the length of each error bar in the',
-            'bottom (left) direction for vertical (horizontal) bars',
-            'Values are plotted relative to the underlying data.'
-        ].join(' ')
-    },
-    value: {
-        valType: 'number',
-        min: 0,
-        dflt: 10,
-        role: 'info',
-        description: [
-            'Sets the value of either the percentage',
-            '(if `type` is set to *percent*) or the constant',
-            '(if `type` is set to *constant*) corresponding to the lengths of',
-            'the error bars.'
-        ].join(' ')
-    },
-    valueminus: {
-        valType: 'number',
-        min: 0,
-        dflt: 10,
-        role: 'info',
-        description: [
-            'Sets the value of either the percentage',
-            '(if `type` is set to *percent*) or the constant',
-            '(if `type` is set to *constant*) corresponding to the lengths of',
-            'the error bars in the',
-            'bottom (left) direction for vertical (horizontal) bars'
-        ].join(' ')
-    },
-    traceref: {
-        valType: 'integer',
-        min: 0,
-        dflt: 0,
-        role: 'info'
-    },
-    tracerefminus: {
-        valType: 'integer',
-        min: 0,
-        dflt: 0,
-        role: 'info'
-    },
-    copy_ystyle: {
-        valType: 'boolean',
-        role: 'style'
-    },
-    copy_zstyle: {
-        valType: 'boolean',
-        role: 'style'
-    },
-    color: {
-        valType: 'color',
-        role: 'style',
-        description: 'Sets the stoke color of the error bars.'
-    },
-    thickness: {
-        valType: 'number',
-        min: 0,
-        dflt: 2,
-        role: 'style',
-        description: 'Sets the thickness (in px) of the error bars.'
-    },
-    width: {
-        valType: 'number',
-        min: 0,
-        role: 'style',
-        description: [
-            'Sets the width (in px) of the cross-bar at both ends',
-            'of the error bars.'
-        ].join(' ')
-    },
-
-    _deprecated: {
-        opacity: {
-            valType: 'number',
-            role: 'style',
-            description: [
-                'Obsolete.',
-                'Use the alpha channel in error bar `color` to set the opacity.'
-            ].join(' ')
-        }
+  visible: {
+    valType: "boolean",
+    role: "info",
+    description: [
+      "Determines whether or not this set of error bars is visible."
+    ].join(" ")
+  },
+  type: {
+    valType: "enumerated",
+    values: ["percent", "constant", "sqrt", "data"],
+    role: "info",
+    description: [
+      "Determines the rule used to generate the error bars.",
+      "If *constant`, the bar lengths are of a constant value.",
+      "Set this constant in `value`.",
+      "If *percent*, the bar lengths correspond to a percentage of",
+      "underlying data. Set this percentage in `value`.",
+      "If *sqrt*, the bar lengths correspond to the sqaure of the",
+      "underlying data.",
+      "If *array*, the bar lengths are set with data set `array`."
+    ].join(" ")
+  },
+  symmetric: {
+    valType: "boolean",
+    role: "info",
+    description: [
+      "Determines whether or not the error bars have the same length",
+      "in both direction",
+      "(top/bottom for vertical bars, left/right for horizontal bars."
+    ].join(" ")
+  },
+  array: {
+    valType: "data_array",
+    description: [
+      "Sets the data corresponding the length of each error bar.",
+      "Values are plotted relative to the underlying data."
+    ].join(" ")
+  },
+  arrayminus: {
+    valType: "data_array",
+    description: [
+      "Sets the data corresponding the length of each error bar in the",
+      "bottom (left) direction for vertical (horizontal) bars",
+      "Values are plotted relative to the underlying data."
+    ].join(" ")
+  },
+  value: {
+    valType: "number",
+    min: 0,
+    dflt: 10,
+    role: "info",
+    description: [
+      "Sets the value of either the percentage",
+      "(if `type` is set to *percent*) or the constant",
+      "(if `type` is set to *constant*) corresponding to the lengths of",
+      "the error bars."
+    ].join(" ")
+  },
+  valueminus: {
+    valType: "number",
+    min: 0,
+    dflt: 10,
+    role: "info",
+    description: [
+      "Sets the value of either the percentage",
+      "(if `type` is set to *percent*) or the constant",
+      "(if `type` is set to *constant*) corresponding to the lengths of",
+      "the error bars in the",
+      "bottom (left) direction for vertical (horizontal) bars"
+    ].join(" ")
+  },
+  traceref: { valType: "integer", min: 0, dflt: 0, role: "info" },
+  tracerefminus: { valType: "integer", min: 0, dflt: 0, role: "info" },
+  copy_ystyle: { valType: "boolean", role: "style" },
+  copy_zstyle: { valType: "boolean", role: "style" },
+  color: {
+    valType: "color",
+    role: "style",
+    description: "Sets the stoke color of the error bars."
+  },
+  thickness: {
+    valType: "number",
+    min: 0,
+    dflt: 2,
+    role: "style",
+    description: "Sets the thickness (in px) of the error bars."
+  },
+  width: {
+    valType: "number",
+    min: 0,
+    role: "style",
+    description: [
+      "Sets the width (in px) of the cross-bar at both ends",
+      "of the error bars."
+    ].join(" ")
+  },
+  _deprecated: {
+    opacity: {
+      valType: "number",
+      role: "style",
+      description: [
+        "Obsolete.",
+        "Use the alpha channel in error bar `color` to set the opacity."
+      ].join(" ")
     }
+  }
 };
diff --git a/src/components/errorbars/calc.js b/src/components/errorbars/calc.js
index d631758fcb6..cb1295aeea7 100644
--- a/src/components/errorbars/calc.js
+++ b/src/components/errorbars/calc.js
@@ -5,57 +5,51 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var isNumeric = require("fast-isnumeric");
 
+var Registry = require("../../registry");
+var Axes = require("../../plots/cartesian/axes");
 
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-
-var Registry = require('../../registry');
-var Axes = require('../../plots/cartesian/axes');
-
-var makeComputeError = require('./compute_error');
-
+var makeComputeError = require("./compute_error");
 
 module.exports = function calc(gd) {
-    var calcdata = gd.calcdata;
+  var calcdata = gd.calcdata;
 
-    for(var i = 0; i < calcdata.length; i++) {
-        var calcTrace = calcdata[i],
-            trace = calcTrace[0].trace;
+  for (var i = 0; i < calcdata.length; i++) {
+    var calcTrace = calcdata[i], trace = calcTrace[0].trace;
 
-        if(!Registry.traceIs(trace, 'errorBarsOK')) continue;
+    if (!Registry.traceIs(trace, "errorBarsOK")) continue;
 
-        var xa = Axes.getFromId(gd, trace.xaxis),
-            ya = Axes.getFromId(gd, trace.yaxis);
+    var xa = Axes.getFromId(gd, trace.xaxis),
+      ya = Axes.getFromId(gd, trace.yaxis);
 
-        calcOneAxis(calcTrace, trace, xa, 'x');
-        calcOneAxis(calcTrace, trace, ya, 'y');
-    }
+    calcOneAxis(calcTrace, trace, xa, "x");
+    calcOneAxis(calcTrace, trace, ya, "y");
+  }
 };
 
 function calcOneAxis(calcTrace, trace, axis, coord) {
-    var opts = trace['error_' + coord] || {},
-        isVisible = (opts.visible && ['linear', 'log'].indexOf(axis.type) !== -1),
-        vals = [];
+  var opts = trace["error_" + coord] || {},
+    isVisible = opts.visible && ["linear", "log"].indexOf(axis.type) !== -1,
+    vals = [];
 
-    if(!isVisible) return;
+  if (!isVisible) return;
 
-    var computeError = makeComputeError(opts);
+  var computeError = makeComputeError(opts);
 
-    for(var i = 0; i < calcTrace.length; i++) {
-        var calcPt = calcTrace[i],
-            calcCoord = calcPt[coord];
+  for (var i = 0; i < calcTrace.length; i++) {
+    var calcPt = calcTrace[i], calcCoord = calcPt[coord];
 
-        if(!isNumeric(axis.c2l(calcCoord))) continue;
+    if (!isNumeric(axis.c2l(calcCoord))) continue;
 
-        var errors = computeError(calcCoord, i);
-        if(isNumeric(errors[0]) && isNumeric(errors[1])) {
-            var shoe = calcPt[coord + 's'] = calcCoord - errors[0],
-                hat = calcPt[coord + 'h'] = calcCoord + errors[1];
-            vals.push(shoe, hat);
-        }
+    var errors = computeError(calcCoord, i);
+    if (isNumeric(errors[0]) && isNumeric(errors[1])) {
+      var shoe = calcPt[coord + "s"] = calcCoord - errors[0],
+        hat = calcPt[coord + "h"] = calcCoord + errors[1];
+      vals.push(shoe, hat);
     }
+  }
 
-    Axes.expand(axis, vals, {padded: true});
+  Axes.expand(axis, vals, { padded: true });
 }
diff --git a/src/components/errorbars/compute_error.js b/src/components/errorbars/compute_error.js
index dd0b189662d..74cd965e790 100644
--- a/src/components/errorbars/compute_error.js
+++ b/src/components/errorbars/compute_error.js
@@ -5,11 +5,7 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-
+"use strict";
 /**
  * Error bar computing function generator
  *
@@ -26,44 +22,36 @@
  *        - error[1] : " " " " positive "
  */
 module.exports = function makeComputeError(opts) {
-    var type = opts.type,
-        symmetric = opts.symmetric;
+  var type = opts.type, symmetric = opts.symmetric;
 
-    if(type === 'data') {
-        var array = opts.array,
-            arrayminus = opts.arrayminus;
+  if (type === "data") {
+    var array = opts.array, arrayminus = opts.arrayminus;
 
-        if(symmetric || arrayminus === undefined) {
-            return function computeError(dataPt, index) {
-                var val = +(array[index]);
-                return [val, val];
-            };
-        }
-        else {
-            return function computeError(dataPt, index) {
-                return [+arrayminus[index], +array[index]];
-            };
-        }
+    if (symmetric || arrayminus === undefined) {
+      return function computeError(dataPt, index) {
+        var val = +array[index];
+        return [val, val];
+      };
+    } else {
+      return function computeError(dataPt, index) {
+        return [+arrayminus[index], +array[index]];
+      };
     }
-    else {
-        var computeErrorValue = makeComputeErrorValue(type, opts.value),
-            computeErrorValueMinus = makeComputeErrorValue(type, opts.valueminus);
+  } else {
+    var computeErrorValue = makeComputeErrorValue(type, opts.value),
+      computeErrorValueMinus = makeComputeErrorValue(type, opts.valueminus);
 
-        if(symmetric || opts.valueminus === undefined) {
-            return function computeError(dataPt) {
-                var val = computeErrorValue(dataPt);
-                return [val, val];
-            };
-        }
-        else {
-            return function computeError(dataPt) {
-                return [
-                    computeErrorValueMinus(dataPt),
-                    computeErrorValue(dataPt)
-                ];
-            };
-        }
+    if (symmetric || opts.valueminus === undefined) {
+      return function computeError(dataPt) {
+        var val = computeErrorValue(dataPt);
+        return [val, val];
+      };
+    } else {
+      return function computeError(dataPt) {
+        return [computeErrorValueMinus(dataPt), computeErrorValue(dataPt)];
+      };
     }
+  }
 };
 
 /**
@@ -76,19 +64,19 @@ module.exports = function makeComputeError(opts) {
  *      @param {numeric} dataPt
  */
 function makeComputeErrorValue(type, value) {
-    if(type === 'percent') {
-        return function(dataPt) {
-            return Math.abs(dataPt * value / 100);
-        };
-    }
-    if(type === 'constant') {
-        return function() {
-            return Math.abs(value);
-        };
-    }
-    if(type === 'sqrt') {
-        return function(dataPt) {
-            return Math.sqrt(Math.abs(dataPt));
-        };
-    }
+  if (type === "percent") {
+    return function(dataPt) {
+      return Math.abs(dataPt * value / 100);
+    };
+  }
+  if (type === "constant") {
+    return function() {
+      return Math.abs(value);
+    };
+  }
+  if (type === "sqrt") {
+    return function(dataPt) {
+      return Math.sqrt(Math.abs(dataPt));
+    };
+  }
 }
diff --git a/src/components/errorbars/defaults.js b/src/components/errorbars/defaults.js
index 433f585352b..4957cf43278 100644
--- a/src/components/errorbars/defaults.js
+++ b/src/components/errorbars/defaults.js
@@ -5,71 +5,70 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var isNumeric = require("fast-isnumeric");
 
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-
-var Registry = require('../../registry');
-var Lib = require('../../lib');
-
-var attributes = require('./attributes');
+var Registry = require("../../registry");
+var Lib = require("../../lib");
 
+var attributes = require("./attributes");
 
 module.exports = function(traceIn, traceOut, defaultColor, opts) {
-    var objName = 'error_' + opts.axis,
-        containerOut = traceOut[objName] = {},
-        containerIn = traceIn[objName] || {};
+  var objName = "error_" + opts.axis,
+    containerOut = traceOut[objName] = {},
+    containerIn = traceIn[objName] || {};
 
-    function coerce(attr, dflt) {
-        return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
-    }
+  function coerce(attr, dflt) {
+    return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
+  }
 
-    var hasErrorBars = (
-        containerIn.array !== undefined ||
-        containerIn.value !== undefined ||
-        containerIn.type === 'sqrt'
-    );
+  var hasErrorBars = containerIn.array !== undefined ||
+    containerIn.value !== undefined ||
+    containerIn.type === "sqrt";
 
-    var visible = coerce('visible', hasErrorBars);
+  var visible = coerce("visible", hasErrorBars);
 
-    if(visible === false) return;
+  if (visible === false) return;
 
-    var type = coerce('type', 'array' in containerIn ? 'data' : 'percent'),
-        symmetric = true;
+  var type = coerce("type", "array" in containerIn ? "data" : "percent"),
+    symmetric = true;
 
-    if(type !== 'sqrt') {
-        symmetric = coerce('symmetric',
-            !((type === 'data' ? 'arrayminus' : 'valueminus') in containerIn));
-    }
+  if (type !== "sqrt") {
+    symmetric = coerce(
+      "symmetric",
+      !((type === "data" ? "arrayminus" : "valueminus") in containerIn)
+    );
+  }
 
-    if(type === 'data') {
-        var array = coerce('array');
-        if(!array) containerOut.array = [];
-        coerce('traceref');
-        if(!symmetric) {
-            var arrayminus = coerce('arrayminus');
-            if(!arrayminus) containerOut.arrayminus = [];
-            coerce('tracerefminus');
-        }
-    }
-    else if(type === 'percent' || type === 'constant') {
-        coerce('value');
-        if(!symmetric) coerce('valueminus');
+  if (type === "data") {
+    var array = coerce("array");
+    if (!array) containerOut.array = [];
+    coerce("traceref");
+    if (!symmetric) {
+      var arrayminus = coerce("arrayminus");
+      if (!arrayminus) containerOut.arrayminus = [];
+      coerce("tracerefminus");
     }
+  } else if (type === "percent" || type === "constant") {
+    coerce("value");
+    if (!symmetric) coerce("valueminus");
+  }
 
-    var copyAttr = 'copy_' + opts.inherit + 'style';
-    if(opts.inherit) {
-        var inheritObj = traceOut['error_' + opts.inherit];
-        if((inheritObj || {}).visible) {
-            coerce(copyAttr, !(containerIn.color ||
-                               isNumeric(containerIn.thickness) ||
-                               isNumeric(containerIn.width)));
-        }
-    }
-    if(!opts.inherit || !containerOut[copyAttr]) {
-        coerce('color', defaultColor);
-        coerce('thickness');
-        coerce('width', Registry.traceIs(traceOut, 'gl3d') ? 0 : 4);
+  var copyAttr = "copy_" + opts.inherit + "style";
+  if (opts.inherit) {
+    var inheritObj = traceOut["error_" + opts.inherit];
+    if ((inheritObj || {}).visible) {
+      coerce(
+        copyAttr,
+        !(containerIn.color ||
+          isNumeric(containerIn.thickness) ||
+          isNumeric(containerIn.width))
+      );
     }
+  }
+  if (!opts.inherit || !containerOut[copyAttr]) {
+    coerce("color", defaultColor);
+    coerce("thickness");
+    coerce("width", Registry.traceIs(traceOut, "gl3d") ? 0 : 4);
+  }
 };
diff --git a/src/components/errorbars/index.js b/src/components/errorbars/index.js
index a27378ad6e7..4ab3e45d7d6 100644
--- a/src/components/errorbars/index.js
+++ b/src/components/errorbars/index.js
@@ -5,53 +5,46 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 var errorBars = module.exports = {};
 
-errorBars.attributes = require('./attributes');
+errorBars.attributes = require("./attributes");
 
-errorBars.supplyDefaults = require('./defaults');
+errorBars.supplyDefaults = require("./defaults");
 
-errorBars.calc = require('./calc');
+errorBars.calc = require("./calc");
 
 errorBars.calcFromTrace = function(trace, layout) {
-    var x = trace.x || [],
-        y = trace.y,
-        len = x.length || y.length;
+  var x = trace.x || [], y = trace.y, len = x.length || y.length;
 
-    var calcdataMock = new Array(len);
+  var calcdataMock = new Array(len);
 
-    for(var i = 0; i < len; i++) {
-        calcdataMock[i] = {
-            x: x[i],
-            y: y[i]
-        };
-    }
+  for (var i = 0; i < len; i++) {
+    calcdataMock[i] = { x: x[i], y: y[i] };
+  }
 
-    calcdataMock[0].trace = trace;
+  calcdataMock[0].trace = trace;
 
-    errorBars.calc({
-        calcdata: [calcdataMock],
-        _fullLayout: layout
-    });
+  errorBars.calc({ calcdata: [calcdataMock], _fullLayout: layout });
 
-    return calcdataMock;
+  return calcdataMock;
 };
 
-errorBars.plot = require('./plot');
+errorBars.plot = require("./plot");
 
-errorBars.style = require('./style');
+errorBars.style = require("./style");
 
 errorBars.hoverInfo = function(calcPoint, trace, hoverPoint) {
-    if((trace.error_y || {}).visible) {
-        hoverPoint.yerr = calcPoint.yh - calcPoint.y;
-        if(!trace.error_y.symmetric) hoverPoint.yerrneg = calcPoint.y - calcPoint.ys;
+  if ((trace.error_y || {}).visible) {
+    hoverPoint.yerr = calcPoint.yh - calcPoint.y;
+    if (!trace.error_y.symmetric) {
+      hoverPoint.yerrneg = calcPoint.y - calcPoint.ys;
     }
-    if((trace.error_x || {}).visible) {
-        hoverPoint.xerr = calcPoint.xh - calcPoint.x;
-        if(!trace.error_x.symmetric) hoverPoint.xerrneg = calcPoint.x - calcPoint.xs;
+  }
+  if ((trace.error_x || {}).visible) {
+    hoverPoint.xerr = calcPoint.xh - calcPoint.x;
+    if (!trace.error_x.symmetric) {
+      hoverPoint.xerrneg = calcPoint.x - calcPoint.xs;
     }
+  }
 };
diff --git a/src/components/errorbars/plot.js b/src/components/errorbars/plot.js
index 84bc05504bf..57e8fb7309e 100644
--- a/src/components/errorbars/plot.js
+++ b/src/components/errorbars/plot.js
@@ -5,158 +5,171 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
+var isNumeric = require("fast-isnumeric");
 
-
-'use strict';
-
-var d3 = require('d3');
-var isNumeric = require('fast-isnumeric');
-
-var subTypes = require('../../traces/scatter/subtypes');
+var subTypes = require("../../traces/scatter/subtypes");
 
 module.exports = function plot(traces, plotinfo, transitionOpts) {
-    var isNew;
-
-    var xa = plotinfo.xaxis,
-        ya = plotinfo.yaxis;
-
-    var hasAnimation = transitionOpts && transitionOpts.duration > 0;
-
-    traces.each(function(d) {
-        var trace = d[0].trace,
-            // || {} is in case the trace (specifically scatterternary)
-            // doesn't support error bars at all, but does go through
-            // the scatter.plot mechanics, which calls ErrorBars.plot
-            // internally
-            xObj = trace.error_x || {},
-            yObj = trace.error_y || {};
-
-        var keyFunc;
-
-        if(trace.ids) {
-            keyFunc = function(d) {return d.id;};
-        }
-
-        var sparse = (
-            subTypes.hasMarkers(trace) &&
-            trace.marker.maxdisplayed > 0
-        );
-
-        if(!yObj.visible && !xObj.visible) return;
-
-        var errorbars = d3.select(this).selectAll('g.errorbar')
-            .data(d, keyFunc);
-
-        errorbars.exit().remove();
+  var isNew;
 
-        errorbars.style('opacity', 1);
+  var xa = plotinfo.xaxis, ya = plotinfo.yaxis;
 
-        var enter = errorbars.enter().append('g')
-            .classed('errorbar', true);
-
-        if(hasAnimation) {
-            enter.style('opacity', 0).transition()
-                .duration(transitionOpts.duration)
-                .style('opacity', 1);
-        }
-
-        errorbars.each(function(d) {
-            var errorbar = d3.select(this);
-            var coords = errorCoords(d, xa, ya);
-
-            if(sparse && !d.vis) return;
-
-            var path;
-
-            if(yObj.visible && isNumeric(coords.x) &&
-                    isNumeric(coords.yh) &&
-                    isNumeric(coords.ys)) {
-                var yw = yObj.width;
-
-                path = 'M' + (coords.x - yw) + ',' +
-                    coords.yh + 'h' + (2 * yw) + // hat
-                    'm-' + yw + ',0V' + coords.ys; // bar
+  var hasAnimation = transitionOpts && transitionOpts.duration > 0;
 
+  traces.each(function(d) {
+    var trace = d[0].trace,
+      // || {} is in case the trace (specifically scatterternary)
+      // doesn't support error bars at all, but does go through
+      // the scatter.plot mechanics, which calls ErrorBars.plot
+      // internally
+      xObj = trace.error_x || {},
+      yObj = trace.error_y || {};
 
-                if(!coords.noYS) path += 'm-' + yw + ',0h' + (2 * yw); // shoe
+    var keyFunc;
 
-                var yerror = errorbar.select('path.yerror');
+    if (trace.ids) {
+      keyFunc = function(d) {
+        return d.id;
+      };
+    }
 
-                isNew = !yerror.size();
+    var sparse = subTypes.hasMarkers(trace) && trace.marker.maxdisplayed > 0;
 
-                if(isNew) {
-                    yerror = errorbar.append('path')
-                        .classed('yerror', true);
-                } else if(hasAnimation) {
-                    yerror = yerror
-                        .transition()
-                            .duration(transitionOpts.duration)
-                            .ease(transitionOpts.easing);
-                }
+    if (!yObj.visible && !xObj.visible) return;
 
-                yerror.attr('d', path);
-            }
+    var errorbars = d3.select(this).selectAll("g.errorbar").data(d, keyFunc);
 
-            if(xObj.visible && isNumeric(coords.y) &&
-                    isNumeric(coords.xh) &&
-                    isNumeric(coords.xs)) {
-                var xw = (xObj.copy_ystyle ? yObj : xObj).width;
+    errorbars.exit().remove();
 
-                path = 'M' + coords.xh + ',' +
-                    (coords.y - xw) + 'v' + (2 * xw) + // hat
-                    'm0,-' + xw + 'H' + coords.xs; // bar
+    errorbars.style("opacity", 1);
 
-                if(!coords.noXS) path += 'm0,-' + xw + 'v' + (2 * xw); // shoe
+    var enter = errorbars.enter().append("g").classed("errorbar", true);
 
-                var xerror = errorbar.select('path.xerror');
+    if (hasAnimation) {
+      enter
+        .style("opacity", 0)
+        .transition()
+        .duration(transitionOpts.duration)
+        .style("opacity", 1);
+    }
 
-                isNew = !xerror.size();
+    errorbars.each(function(d) {
+      var errorbar = d3.select(this);
+      var coords = errorCoords(d, xa, ya);
+
+      if (sparse && !d.vis) return;
+
+      var path;
+
+      if (
+        yObj.visible &&
+          isNumeric(coords.x) &&
+          isNumeric(coords.yh) &&
+          isNumeric(coords.ys)
+      ) {
+        var yw = yObj.width;
+
+        path = "M" +
+          (coords.x - yw) +
+          "," +
+          coords.yh +
+          "h" +
+          2 * yw + // hat
+          "m-" +
+          yw +
+          ",0V" +
+          coords.ys;
+
+        // bar
+        if (!coords.noYS) path += "m-" + yw + ",0h" + 2 * yw;
+
+        // shoe
+        var yerror = errorbar.select("path.yerror");
+
+        isNew = !yerror.size();
+
+        if (isNew) {
+          yerror = errorbar.append("path").classed("yerror", true);
+        } else if (hasAnimation) {
+          yerror = yerror
+            .transition()
+            .duration(transitionOpts.duration)
+            .ease(transitionOpts.easing);
+        }
 
-                if(isNew) {
-                    xerror = errorbar.append('path')
-                        .classed('xerror', true);
-                } else if(hasAnimation) {
-                    xerror = xerror
-                        .transition()
-                            .duration(transitionOpts.duration)
-                            .ease(transitionOpts.easing);
-                }
+        yerror.attr("d", path);
+      }
+
+      if (
+        xObj.visible &&
+          isNumeric(coords.y) &&
+          isNumeric(coords.xh) &&
+          isNumeric(coords.xs)
+      ) {
+        var xw = (xObj.copy_ystyle ? yObj : xObj).width;
+
+        path = "M" +
+          coords.xh +
+          "," +
+          (coords.y - xw) +
+          "v" +
+          2 * xw + // hat
+          "m0,-" +
+          xw +
+          "H" +
+          coords.xs;
+
+        // bar
+        if (!coords.noXS) path += "m0,-" + xw + "v" + 2 * xw;
+
+        // shoe
+        var xerror = errorbar.select("path.xerror");
+
+        isNew = !xerror.size();
+
+        if (isNew) {
+          xerror = errorbar.append("path").classed("xerror", true);
+        } else if (hasAnimation) {
+          xerror = xerror
+            .transition()
+            .duration(transitionOpts.duration)
+            .ease(transitionOpts.easing);
+        }
 
-                xerror.attr('d', path);
-            }
-        });
+        xerror.attr("d", path);
+      }
     });
+  });
 };
 
 // compute the coordinates of the error-bar objects
 function errorCoords(d, xa, ya) {
-    var out = {
-        x: xa.c2p(d.x),
-        y: ya.c2p(d.y)
-    };
-
-    // calculate the error bar size and hat and shoe locations
-    if(d.yh !== undefined) {
-        out.yh = ya.c2p(d.yh);
-        out.ys = ya.c2p(d.ys);
-
-        // if the shoes go off-scale (ie log scale, error bars past zero)
-        // clip the bar and hide the shoes
-        if(!isNumeric(out.ys)) {
-            out.noYS = true;
-            out.ys = ya.c2p(d.ys, true);
-        }
+  var out = { x: xa.c2p(d.x), y: ya.c2p(d.y) };
+
+  // calculate the error bar size and hat and shoe locations
+  if (d.yh !== undefined) {
+    out.yh = ya.c2p(d.yh);
+    out.ys = ya.c2p(d.ys);
+
+    // if the shoes go off-scale (ie log scale, error bars past zero)
+    // clip the bar and hide the shoes
+    if (!isNumeric(out.ys)) {
+      out.noYS = true;
+      out.ys = ya.c2p(d.ys, true);
     }
+  }
 
-    if(d.xh !== undefined) {
-        out.xh = xa.c2p(d.xh);
-        out.xs = xa.c2p(d.xs);
+  if (d.xh !== undefined) {
+    out.xh = xa.c2p(d.xh);
+    out.xs = xa.c2p(d.xs);
 
-        if(!isNumeric(out.xs)) {
-            out.noXS = true;
-            out.xs = xa.c2p(d.xs, true);
-        }
+    if (!isNumeric(out.xs)) {
+      out.noXS = true;
+      out.xs = xa.c2p(d.xs, true);
     }
+  }
 
-    return out;
+  return out;
 }
diff --git a/src/components/errorbars/style.js b/src/components/errorbars/style.js
index b6c81feb662..fa6b3fe0239 100644
--- a/src/components/errorbars/style.js
+++ b/src/components/errorbars/style.js
@@ -5,31 +5,29 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
 
-
-'use strict';
-
-var d3 = require('d3');
-
-var Color = require('../color');
-
+var Color = require("../color");
 
 module.exports = function style(traces) {
-    traces.each(function(d) {
-        var trace = d[0].trace,
-            yObj = trace.error_y || {},
-            xObj = trace.error_x || {};
+  traces.each(function(d) {
+    var trace = d[0].trace,
+      yObj = trace.error_y || {},
+      xObj = trace.error_x || {};
 
-        var s = d3.select(this);
+    var s = d3.select(this);
 
-        s.selectAll('path.yerror')
-            .style('stroke-width', yObj.thickness + 'px')
-            .call(Color.stroke, yObj.color);
+    s
+      .selectAll("path.yerror")
+      .style("stroke-width", yObj.thickness + "px")
+      .call(Color.stroke, yObj.color);
 
-        if(xObj.copy_ystyle) xObj = yObj;
+    if (xObj.copy_ystyle) xObj = yObj;
 
-        s.selectAll('path.xerror')
-            .style('stroke-width', xObj.thickness + 'px')
-            .call(Color.stroke, xObj.color);
-    });
+    s
+      .selectAll("path.xerror")
+      .style("stroke-width", xObj.thickness + "px")
+      .call(Color.stroke, xObj.color);
+  });
 };
diff --git a/src/components/images/attributes.js b/src/components/images/attributes.js
index 2f15c72f908..4eaa3795d93 100644
--- a/src/components/images/attributes.js
+++ b/src/components/images/attributes.js
@@ -5,163 +5,138 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var cartesianConstants = require('../../plots/cartesian/constants');
-
+"use strict";
+var cartesianConstants = require("../../plots/cartesian/constants");
 
 module.exports = {
-    _isLinkedToArray: 'image',
-
-    visible: {
-        valType: 'boolean',
-        role: 'info',
-        dflt: true,
-        description: [
-            'Determines whether or not this image is visible.'
-        ].join(' ')
-    },
-
-    source: {
-        valType: 'string',
-        role: 'info',
-        description: [
-            'Specifies the URL of the image to be used.',
-            'The URL must be accessible from the domain where the',
-            'plot code is run, and can be either relative or absolute.'
-
-        ].join(' ')
-    },
-
-    layer: {
-        valType: 'enumerated',
-        values: ['below', 'above'],
-        dflt: 'above',
-        role: 'info',
-        description: [
-            'Specifies whether images are drawn below or above traces.',
-            'When `xref` and `yref` are both set to `paper`,',
-            'image is drawn below the entire plot area.'
-        ].join(' ')
-    },
-
-    sizex: {
-        valType: 'number',
-        role: 'info',
-        dflt: 0,
-        description: [
-            'Sets the image container size horizontally.',
-            'The image will be sized based on the `position` value.',
-            'When `xref` is set to `paper`, units are sized relative',
-            'to the plot width.'
-        ].join(' ')
-    },
-
-    sizey: {
-        valType: 'number',
-        role: 'info',
-        dflt: 0,
-        description: [
-            'Sets the image container size vertically.',
-            'The image will be sized based on the `position` value.',
-            'When `yref` is set to `paper`, units are sized relative',
-            'to the plot height.'
-        ].join(' ')
-    },
-
-    sizing: {
-        valType: 'enumerated',
-        values: ['fill', 'contain', 'stretch'],
-        dflt: 'contain',
-        role: 'info',
-        description: [
-            'Specifies which dimension of the image to constrain.'
-        ].join(' ')
-    },
-
-    opacity: {
-        valType: 'number',
-        role: 'info',
-        min: 0,
-        max: 1,
-        dflt: 1,
-        description: 'Sets the opacity of the image.'
-    },
-
-    x: {
-        valType: 'any',
-        role: 'info',
-        dflt: 0,
-        description: [
-            'Sets the image\'s x position.',
-            'When `xref` is set to `paper`, units are sized relative',
-            'to the plot height.',
-            'See `xref` for more info'
-        ].join(' ')
-    },
-
-    y: {
-        valType: 'any',
-        role: 'info',
-        dflt: 0,
-        description: [
-            'Sets the image\'s y position.',
-            'When `yref` is set to `paper`, units are sized relative',
-            'to the plot height.',
-            'See `yref` for more info'
-        ].join(' ')
-    },
-
-    xanchor: {
-        valType: 'enumerated',
-        values: ['left', 'center', 'right'],
-        dflt: 'left',
-        role: 'info',
-        description: 'Sets the anchor for the x position'
-    },
-
-    yanchor: {
-        valType: 'enumerated',
-        values: ['top', 'middle', 'bottom'],
-        dflt: 'top',
-        role: 'info',
-        description: 'Sets the anchor for the y position.'
-    },
-
-    xref: {
-        valType: 'enumerated',
-        values: [
-            'paper',
-            cartesianConstants.idRegex.x.toString()
-        ],
-        dflt: 'paper',
-        role: 'info',
-        description: [
-            'Sets the images\'s x coordinate axis.',
-            'If set to a x axis id (e.g. *x* or *x2*), the `x` position',
-            'refers to an x data coordinate',
-            'If set to *paper*, the `x` position refers to the distance from',
-            'the left of plot in normalized coordinates',
-            'where *0* (*1*) corresponds to the left (right).'
-        ].join(' ')
-    },
-
-    yref: {
-        valType: 'enumerated',
-        values: [
-            'paper',
-            cartesianConstants.idRegex.y.toString()
-        ],
-        dflt: 'paper',
-        role: 'info',
-        description: [
-            'Sets the images\'s y coordinate axis.',
-            'If set to a y axis id (e.g. *y* or *y2*), the `y` position',
-            'refers to a y data coordinate.',
-            'If set to *paper*, the `y` position refers to the distance from',
-            'the bottom of the plot in normalized coordinates',
-            'where *0* (*1*) corresponds to the bottom (top).'
-        ].join(' ')
-    }
+  _isLinkedToArray: "image",
+  visible: {
+    valType: "boolean",
+    role: "info",
+    dflt: true,
+    description: ["Determines whether or not this image is visible."].join(" ")
+  },
+  source: {
+    valType: "string",
+    role: "info",
+    description: [
+      "Specifies the URL of the image to be used.",
+      "The URL must be accessible from the domain where the",
+      "plot code is run, and can be either relative or absolute."
+    ].join(" ")
+  },
+  layer: {
+    valType: "enumerated",
+    values: ["below", "above"],
+    dflt: "above",
+    role: "info",
+    description: [
+      "Specifies whether images are drawn below or above traces.",
+      "When `xref` and `yref` are both set to `paper`,",
+      "image is drawn below the entire plot area."
+    ].join(" ")
+  },
+  sizex: {
+    valType: "number",
+    role: "info",
+    dflt: 0,
+    description: [
+      "Sets the image container size horizontally.",
+      "The image will be sized based on the `position` value.",
+      "When `xref` is set to `paper`, units are sized relative",
+      "to the plot width."
+    ].join(" ")
+  },
+  sizey: {
+    valType: "number",
+    role: "info",
+    dflt: 0,
+    description: [
+      "Sets the image container size vertically.",
+      "The image will be sized based on the `position` value.",
+      "When `yref` is set to `paper`, units are sized relative",
+      "to the plot height."
+    ].join(" ")
+  },
+  sizing: {
+    valType: "enumerated",
+    values: ["fill", "contain", "stretch"],
+    dflt: "contain",
+    role: "info",
+    description: ["Specifies which dimension of the image to constrain."].join(
+      " "
+    )
+  },
+  opacity: {
+    valType: "number",
+    role: "info",
+    min: 0,
+    max: 1,
+    dflt: 1,
+    description: "Sets the opacity of the image."
+  },
+  x: {
+    valType: "any",
+    role: "info",
+    dflt: 0,
+    description: [
+      "Sets the image's x position.",
+      "When `xref` is set to `paper`, units are sized relative",
+      "to the plot height.",
+      "See `xref` for more info"
+    ].join(" ")
+  },
+  y: {
+    valType: "any",
+    role: "info",
+    dflt: 0,
+    description: [
+      "Sets the image's y position.",
+      "When `yref` is set to `paper`, units are sized relative",
+      "to the plot height.",
+      "See `yref` for more info"
+    ].join(" ")
+  },
+  xanchor: {
+    valType: "enumerated",
+    values: ["left", "center", "right"],
+    dflt: "left",
+    role: "info",
+    description: "Sets the anchor for the x position"
+  },
+  yanchor: {
+    valType: "enumerated",
+    values: ["top", "middle", "bottom"],
+    dflt: "top",
+    role: "info",
+    description: "Sets the anchor for the y position."
+  },
+  xref: {
+    valType: "enumerated",
+    values: ["paper", cartesianConstants.idRegex.x.toString()],
+    dflt: "paper",
+    role: "info",
+    description: [
+      "Sets the images's x coordinate axis.",
+      "If set to a x axis id (e.g. *x* or *x2*), the `x` position",
+      "refers to an x data coordinate",
+      "If set to *paper*, the `x` position refers to the distance from",
+      "the left of plot in normalized coordinates",
+      "where *0* (*1*) corresponds to the left (right)."
+    ].join(" ")
+  },
+  yref: {
+    valType: "enumerated",
+    values: ["paper", cartesianConstants.idRegex.y.toString()],
+    dflt: "paper",
+    role: "info",
+    description: [
+      "Sets the images's y coordinate axis.",
+      "If set to a y axis id (e.g. *y* or *y2*), the `y` position",
+      "refers to a y data coordinate.",
+      "If set to *paper*, the `y` position refers to the distance from",
+      "the bottom of the plot in normalized coordinates",
+      "where *0* (*1*) corresponds to the bottom (top)."
+    ].join(" ")
+  }
 };
diff --git a/src/components/images/defaults.js b/src/components/images/defaults.js
index 0c6c5b32c93..7705519adde 100644
--- a/src/components/images/defaults.js
+++ b/src/components/images/defaults.js
@@ -5,54 +5,48 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Lib = require("../../lib");
+var Axes = require("../../plots/cartesian/axes");
+var handleArrayContainerDefaults = require(
+  "../../plots/array_container_defaults"
+);
 
-'use strict';
-
-var Lib = require('../../lib');
-var Axes = require('../../plots/cartesian/axes');
-var handleArrayContainerDefaults = require('../../plots/array_container_defaults');
-
-var attributes = require('./attributes');
-var name = 'images';
+var attributes = require("./attributes");
+var name = "images";
 
 module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) {
-    var opts = {
-        name: name,
-        handleItemDefaults: imageDefaults
-    };
+  var opts = { name: name, handleItemDefaults: imageDefaults };
 
-    handleArrayContainerDefaults(layoutIn, layoutOut, opts);
+  handleArrayContainerDefaults(layoutIn, layoutOut, opts);
 };
 
-
 function imageDefaults(imageIn, imageOut, fullLayout) {
+  function coerce(attr, dflt) {
+    return Lib.coerce(imageIn, imageOut, attributes, attr, dflt);
+  }
 
-    function coerce(attr, dflt) {
-        return Lib.coerce(imageIn, imageOut, attributes, attr, dflt);
-    }
-
-    var source = coerce('source');
-    var visible = coerce('visible', !!source);
+  var source = coerce("source");
+  var visible = coerce("visible", !!source);
 
-    if(!visible) return imageOut;
+  if (!visible) return imageOut;
 
-    coerce('layer');
-    coerce('x');
-    coerce('y');
-    coerce('xanchor');
-    coerce('yanchor');
-    coerce('sizex');
-    coerce('sizey');
-    coerce('sizing');
-    coerce('opacity');
+  coerce("layer");
+  coerce("x");
+  coerce("y");
+  coerce("xanchor");
+  coerce("yanchor");
+  coerce("sizex");
+  coerce("sizey");
+  coerce("sizing");
+  coerce("opacity");
 
-    var gdMock = { _fullLayout: fullLayout },
-        axLetters = ['x', 'y'];
+  var gdMock = { _fullLayout: fullLayout }, axLetters = ["x", "y"];
 
-    for(var i = 0; i < 2; i++) {
-        // 'paper' is the fallback axref
-        Axes.coerceRef(imageIn, imageOut, gdMock, axLetters[i], 'paper');
-    }
+  for (var i = 0; i < 2; i++) {
+    // 'paper' is the fallback axref
+    Axes.coerceRef(imageIn, imageOut, gdMock, axLetters[i], "paper");
+  }
 
-    return imageOut;
+  return imageOut;
 }
diff --git a/src/components/images/draw.js b/src/components/images/draw.js
index 228916d3de4..7680e3b5836 100644
--- a/src/components/images/draw.js
+++ b/src/components/images/draw.js
@@ -5,174 +5,172 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var d3 = require('d3');
-var Drawing = require('../drawing');
-var Axes = require('../../plots/cartesian/axes');
-var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
+"use strict";
+var d3 = require("d3");
+var Drawing = require("../drawing");
+var Axes = require("../../plots/cartesian/axes");
+var xmlnsNamespaces = require("../../constants/xmlns_namespaces");
 
 module.exports = function draw(gd) {
-    var fullLayout = gd._fullLayout,
-        imageDataAbove = [],
-        imageDataSubplot = [],
-        imageDataBelow = [];
-
-    // Sort into top, subplot, and bottom layers
-    for(var i = 0; i < fullLayout.images.length; i++) {
-        var img = fullLayout.images[i];
-
-        if(img.visible) {
-            if(img.layer === 'below' && img.xref !== 'paper' && img.yref !== 'paper') {
-                imageDataSubplot.push(img);
-            } else if(img.layer === 'above') {
-                imageDataAbove.push(img);
-            } else {
-                imageDataBelow.push(img);
-            }
-        }
+  var fullLayout = gd._fullLayout,
+    imageDataAbove = [],
+    imageDataSubplot = [],
+    imageDataBelow = [];
+
+  // Sort into top, subplot, and bottom layers
+  for (var i = 0; i < fullLayout.images.length; i++) {
+    var img = fullLayout.images[i];
+
+    if (img.visible) {
+      if (
+        img.layer === "below" && img.xref !== "paper" && img.yref !== "paper"
+      ) {
+        imageDataSubplot.push(img);
+      } else if (img.layer === "above") {
+        imageDataAbove.push(img);
+      } else {
+        imageDataBelow.push(img);
+      }
     }
+  }
+
+  var anchors = {
+    x: {
+      left: { sizing: "xMin", offset: 0 },
+      center: { sizing: "xMid", offset: (-1) / 2 },
+      right: { sizing: "xMax", offset: -1 }
+    },
+    y: {
+      top: { sizing: "YMin", offset: 0 },
+      middle: { sizing: "YMid", offset: (-1) / 2 },
+      bottom: { sizing: "YMax", offset: -1 }
+    }
+  };
 
+  // Images must be converted to dataURL's for exporting.
+  function setImage(d) {
+    var thisImage = d3.select(this);
 
-    var anchors = {
-        x: {
-            left: { sizing: 'xMin', offset: 0 },
-            center: { sizing: 'xMid', offset: -1 / 2 },
-            right: { sizing: 'xMax', offset: -1 }
-        },
-        y: {
-            top: { sizing: 'YMin', offset: 0 },
-            middle: { sizing: 'YMid', offset: -1 / 2 },
-            bottom: { sizing: 'YMax', offset: -1 }
-        }
-    };
-
-
-    // Images must be converted to dataURL's for exporting.
-    function setImage(d) {
-        var thisImage = d3.select(this);
-
-        if(this.img && this.img.src === d.source) {
-            return;
-        }
-
-        thisImage.attr('xmlns', xmlnsNamespaces.svg);
-
-        var imagePromise = new Promise(function(resolve) {
-
-            var img = new Image();
-            this.img = img;
-
-            // If not set, a `tainted canvas` error is thrown
-            img.setAttribute('crossOrigin', 'anonymous');
-            img.onerror = errorHandler;
-            img.onload = function() {
-                var canvas = document.createElement('canvas');
-                canvas.width = this.width;
-                canvas.height = this.height;
-
-                var ctx = canvas.getContext('2d');
-                ctx.drawImage(this, 0, 0);
-
-                var dataURL = canvas.toDataURL('image/png');
-
-                thisImage.attr('xlink:href', dataURL);
-            };
+    if (this.img && this.img.src === d.source) {
+      return;
+    }
 
+    thisImage.attr("xmlns", xmlnsNamespaces.svg);
 
-            thisImage.on('error', errorHandler);
-            thisImage.on('load', resolve);
+    var imagePromise = new Promise(
+      (function(resolve) {
+        var img = new Image();
+        this.img = img;
 
-            img.src = d.source;
+        // If not set, a `tainted canvas` error is thrown
+        img.setAttribute("crossOrigin", "anonymous");
+        img.onerror = errorHandler;
+        img.onload = function() {
+          var canvas = document.createElement("canvas");
+          canvas.width = this.width;
+          canvas.height = this.height;
 
-            function errorHandler() {
-                thisImage.remove();
-                resolve();
-            }
-        }.bind(this));
+          var ctx = canvas.getContext("2d");
+          ctx.drawImage(this, 0, 0);
 
-        gd._promises.push(imagePromise);
-    }
+          var dataURL = canvas.toDataURL("image/png");
 
-    function applyAttributes(d) {
-        var thisImage = d3.select(this);
+          thisImage.attr("xlink:href", dataURL);
+        };
 
-        // Axes if specified
-        var xa = Axes.getFromId(gd, d.xref),
-            ya = Axes.getFromId(gd, d.yref);
+        thisImage.on("error", errorHandler);
+        thisImage.on("load", resolve);
 
-        var size = fullLayout._size,
-            width = xa ? Math.abs(xa.l2p(d.sizex) - xa.l2p(0)) : d.sizex * size.w,
-            height = ya ? Math.abs(ya.l2p(d.sizey) - ya.l2p(0)) : d.sizey * size.h;
+        img.src = d.source;
 
-        // Offsets for anchor positioning
-        var xOffset = width * anchors.x[d.xanchor].offset,
-            yOffset = height * anchors.y[d.yanchor].offset;
+        function errorHandler() {
+          thisImage.remove();
+          resolve();
+        }
+      }).bind(this)
+    );
 
-        var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing;
+    gd._promises.push(imagePromise);
+  }
 
-        // Final positions
-        var xPos = (xa ? xa.r2p(d.x) + xa._offset : d.x * size.w + size.l) + xOffset,
-            yPos = (ya ? ya.r2p(d.y) + ya._offset : size.h - d.y * size.h + size.t) + yOffset;
+  function applyAttributes(d) {
+    var thisImage = d3.select(this);
 
+    // Axes if specified
+    var xa = Axes.getFromId(gd, d.xref), ya = Axes.getFromId(gd, d.yref);
 
-        // Construct the proper aspectRatio attribute
-        switch(d.sizing) {
-            case 'fill':
-                sizing += ' slice';
-                break;
+    var size = fullLayout._size,
+      width = xa ? Math.abs(xa.l2p(d.sizex) - xa.l2p(0)) : d.sizex * size.w,
+      height = ya ? Math.abs(ya.l2p(d.sizey) - ya.l2p(0)) : d.sizey * size.h;
 
-            case 'stretch':
-                sizing = 'none';
-                break;
-        }
+    // Offsets for anchor positioning
+    var xOffset = width * anchors.x[d.xanchor].offset,
+      yOffset = height * anchors.y[d.yanchor].offset;
 
-        thisImage.attr({
-            x: xPos,
-            y: yPos,
-            width: width,
-            height: height,
-            preserveAspectRatio: sizing,
-            opacity: d.opacity
-        });
+    var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing;
 
+    // Final positions
+    var xPos = (xa ? xa.r2p(d.x) + xa._offset : d.x * size.w + size.l) +
+      xOffset,
+      yPos = (ya ? ya.r2p(d.y) + ya._offset : size.h - d.y * size.h + size.t) +
+        yOffset;
 
-        // Set proper clipping on images
-        var xId = xa ? xa._id : '',
-            yId = ya ? ya._id : '',
-            clipAxes = xId + yId;
+    // Construct the proper aspectRatio attribute
+    switch (d.sizing) {
+      case "fill":
+        sizing += " slice";
+        break;
 
-        if(clipAxes) {
-            thisImage.call(Drawing.setClipUrl, 'clip' + fullLayout._uid + clipAxes);
-        }
+      case "stretch":
+        sizing = "none";
+        break;
     }
 
-    var imagesBelow = fullLayout._imageLowerLayer.selectAll('image')
-            .data(imageDataBelow),
-        imagesSubplot = fullLayout._imageSubplotLayer.selectAll('image')
-            .data(imageDataSubplot),
-        imagesAbove = fullLayout._imageUpperLayer.selectAll('image')
-            .data(imageDataAbove);
-
-    imagesBelow.enter().append('image');
-    imagesSubplot.enter().append('image');
-    imagesAbove.enter().append('image');
+    thisImage.attr({
+      x: xPos,
+      y: yPos,
+      width: width,
+      height: height,
+      preserveAspectRatio: sizing,
+      opacity: d.opacity
+    });
 
-    imagesBelow.exit().remove();
-    imagesSubplot.exit().remove();
-    imagesAbove.exit().remove();
+    // Set proper clipping on images
+    var xId = xa ? xa._id : "", yId = ya ? ya._id : "", clipAxes = xId + yId;
 
-    imagesBelow.each(function(d) {
-        setImage.bind(this)(d);
-        applyAttributes.bind(this)(d);
-    });
-    imagesSubplot.each(function(d) {
-        setImage.bind(this)(d);
-        applyAttributes.bind(this)(d);
-    });
-    imagesAbove.each(function(d) {
-        setImage.bind(this)(d);
-        applyAttributes.bind(this)(d);
-    });
+    if (clipAxes) {
+      thisImage.call(Drawing.setClipUrl, "clip" + fullLayout._uid + clipAxes);
+    }
+  }
+
+  var imagesBelow = fullLayout._imageLowerLayer
+    .selectAll("image")
+    .data(imageDataBelow),
+    imagesSubplot = fullLayout._imageSubplotLayer
+      .selectAll("image")
+      .data(imageDataSubplot),
+    imagesAbove = fullLayout._imageUpperLayer
+      .selectAll("image")
+      .data(imageDataAbove);
+
+  imagesBelow.enter().append("image");
+  imagesSubplot.enter().append("image");
+  imagesAbove.enter().append("image");
+
+  imagesBelow.exit().remove();
+  imagesSubplot.exit().remove();
+  imagesAbove.exit().remove();
+
+  imagesBelow.each(function(d) {
+    setImage.bind(this)(d);
+    applyAttributes.bind(this)(d);
+  });
+  imagesSubplot.each(function(d) {
+    setImage.bind(this)(d);
+    applyAttributes.bind(this)(d);
+  });
+  imagesAbove.each(function(d) {
+    setImage.bind(this)(d);
+    applyAttributes.bind(this)(d);
+  });
 };
diff --git a/src/components/images/index.js b/src/components/images/index.js
index d7ce308ae28..61363f699a7 100644
--- a/src/components/images/index.js
+++ b/src/components/images/index.js
@@ -5,15 +5,11 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 module.exports = {
-    moduleType: 'component',
-    name: 'images',
-
-    layoutAttributes: require('./attributes'),
-    supplyLayoutDefaults: require('./defaults'),
-
-    draw: require('./draw')
+  moduleType: "component",
+  name: "images",
+  layoutAttributes: require("./attributes"),
+  supplyLayoutDefaults: require("./defaults"),
+  draw: require("./draw")
 };
diff --git a/src/components/legend/anchor_utils.js b/src/components/legend/anchor_utils.js
index 2dcc0161538..5975e6d623b 100644
--- a/src/components/legend/anchor_utils.js
+++ b/src/components/legend/anchor_utils.js
@@ -5,11 +5,7 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-
+"use strict";
 /**
  * Determine the position anchor property of x/y xanchor/yanchor components.
  *
@@ -19,29 +15,20 @@
  */
 
 exports.isRightAnchor = function isRightAnchor(opts) {
-    return (
-        opts.xanchor === 'right' ||
-        (opts.xanchor === 'auto' && opts.x >= 2 / 3)
-    );
+  return opts.xanchor === "right" || opts.xanchor === "auto" && opts.x >= 2 / 3;
 };
 
 exports.isCenterAnchor = function isCenterAnchor(opts) {
-    return (
-        opts.xanchor === 'center' ||
-        (opts.xanchor === 'auto' && opts.x > 1 / 3 && opts.x < 2 / 3)
-    );
+  return opts.xanchor === "center" ||
+    opts.xanchor === "auto" && opts.x > 1 / 3 && opts.x < 2 / 3;
 };
 
 exports.isBottomAnchor = function isBottomAnchor(opts) {
-    return (
-        opts.yanchor === 'bottom' ||
-        (opts.yanchor === 'auto' && opts.y <= 1 / 3)
-    );
+  return opts.yanchor === "bottom" ||
+    opts.yanchor === "auto" && opts.y <= 1 / 3;
 };
 
 exports.isMiddleAnchor = function isMiddleAnchor(opts) {
-    return (
-        opts.yanchor === 'middle' ||
-        (opts.yanchor === 'auto' && opts.y > 1 / 3 && opts.y < 2 / 3)
-    );
+  return opts.yanchor === "middle" ||
+    opts.yanchor === "auto" && opts.y > 1 / 3 && opts.y < 2 / 3;
 };
diff --git a/src/components/legend/attributes.js b/src/components/legend/attributes.js
index 8ae61ac29be..77a1825dbb1 100644
--- a/src/components/legend/attributes.js
+++ b/src/components/legend/attributes.js
@@ -5,109 +5,102 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var fontAttrs = require('../../plots/font_attributes');
-var colorAttrs = require('../color/attributes');
-var extendFlat = require('../../lib/extend').extendFlat;
-
+"use strict";
+var fontAttrs = require("../../plots/font_attributes");
+var colorAttrs = require("../color/attributes");
+var extendFlat = require("../../lib/extend").extendFlat;
 
 module.exports = {
-    bgcolor: {
-        valType: 'color',
-        role: 'style',
-        description: 'Sets the legend background color.'
-    },
-    bordercolor: {
-        valType: 'color',
-        dflt: colorAttrs.defaultLine,
-        role: 'style',
-        description: 'Sets the color of the border enclosing the legend.'
-    },
-    borderwidth: {
-        valType: 'number',
-        min: 0,
-        dflt: 0,
-        role: 'style',
-        description: 'Sets the width (in px) of the border enclosing the legend.'
-    },
-    font: extendFlat({}, fontAttrs, {
-        description: 'Sets the font used to text the legend items.'
-    }),
-    orientation: {
-        valType: 'enumerated',
-        values: ['v', 'h'],
-        dflt: 'v',
-        role: 'info',
-        description: 'Sets the orientation of the legend.'
-    },
-    traceorder: {
-        valType: 'flaglist',
-        flags: ['reversed', 'grouped'],
-        extras: ['normal'],
-        role: 'style',
-        description: [
-            'Determines the order at which the legend items are displayed.',
-
-            'If *normal*, the items are displayed top-to-bottom in the same',
-            'order as the input data.',
-
-            'If *reversed*, the items are displayed in the opposite order',
-            'as *normal*.',
-
-            'If *grouped*, the items are displayed in groups',
-            '(when a trace `legendgroup` is provided).',
-
-            'if *grouped+reversed*, the items are displayed in the opposite order',
-            'as *grouped*.'
-        ].join(' ')
-    },
-    tracegroupgap: {
-        valType: 'number',
-        min: 0,
-        dflt: 10,
-        role: 'style',
-        description: [
-            'Sets the amount of vertical space (in px) between legend groups.'
-        ].join(' ')
-    },
-    x: {
-        valType: 'number',
-        min: -2,
-        max: 3,
-        dflt: 1.02,
-        role: 'style',
-        description: 'Sets the x position (in normalized coordinates) of the legend.'
-    },
-    xanchor: {
-        valType: 'enumerated',
-        values: ['auto', 'left', 'center', 'right'],
-        dflt: 'left',
-        role: 'info',
-        description: [
-            'Sets the legend\'s horizontal position anchor.',
-            'This anchor binds the `x` position to the *left*, *center*',
-            'or *right* of the legend.'
-        ].join(' ')
-    },
-    y: {
-        valType: 'number',
-        min: -2,
-        max: 3,
-        dflt: 1,
-        role: 'style',
-        description: 'Sets the y position (in normalized coordinates) of the legend.'
-    },
-    yanchor: {
-        valType: 'enumerated',
-        values: ['auto', 'top', 'middle', 'bottom'],
-        dflt: 'auto',
-        role: 'info',
-        description: [
-            'Sets the legend\'s vertical position anchor',
-            'This anchor binds the `y` position to the *top*, *middle*',
-            'or *bottom* of the legend.'
-        ].join(' ')
-    }
+  bgcolor: {
+    valType: "color",
+    role: "style",
+    description: "Sets the legend background color."
+  },
+  bordercolor: {
+    valType: "color",
+    dflt: colorAttrs.defaultLine,
+    role: "style",
+    description: "Sets the color of the border enclosing the legend."
+  },
+  borderwidth: {
+    valType: "number",
+    min: 0,
+    dflt: 0,
+    role: "style",
+    description: "Sets the width (in px) of the border enclosing the legend."
+  },
+  font: extendFlat({}, fontAttrs, {
+    description: "Sets the font used to text the legend items."
+  }),
+  orientation: {
+    valType: "enumerated",
+    values: ["v", "h"],
+    dflt: "v",
+    role: "info",
+    description: "Sets the orientation of the legend."
+  },
+  traceorder: {
+    valType: "flaglist",
+    flags: ["reversed", "grouped"],
+    extras: ["normal"],
+    role: "style",
+    description: [
+      "Determines the order at which the legend items are displayed.",
+      "If *normal*, the items are displayed top-to-bottom in the same",
+      "order as the input data.",
+      "If *reversed*, the items are displayed in the opposite order",
+      "as *normal*.",
+      "If *grouped*, the items are displayed in groups",
+      "(when a trace `legendgroup` is provided).",
+      "if *grouped+reversed*, the items are displayed in the opposite order",
+      "as *grouped*."
+    ].join(" ")
+  },
+  tracegroupgap: {
+    valType: "number",
+    min: 0,
+    dflt: 10,
+    role: "style",
+    description: [
+      "Sets the amount of vertical space (in px) between legend groups."
+    ].join(" ")
+  },
+  x: {
+    valType: "number",
+    min: -2,
+    max: 3,
+    dflt: 1.02,
+    role: "style",
+    description: "Sets the x position (in normalized coordinates) of the legend."
+  },
+  xanchor: {
+    valType: "enumerated",
+    values: ["auto", "left", "center", "right"],
+    dflt: "left",
+    role: "info",
+    description: [
+      "Sets the legend's horizontal position anchor.",
+      "This anchor binds the `x` position to the *left*, *center*",
+      "or *right* of the legend."
+    ].join(" ")
+  },
+  y: {
+    valType: "number",
+    min: -2,
+    max: 3,
+    dflt: 1,
+    role: "style",
+    description: "Sets the y position (in normalized coordinates) of the legend."
+  },
+  yanchor: {
+    valType: "enumerated",
+    values: ["auto", "top", "middle", "bottom"],
+    dflt: "auto",
+    role: "info",
+    description: [
+      "Sets the legend's vertical position anchor",
+      "This anchor binds the `y` position to the *top*, *middle*",
+      "or *bottom* of the legend."
+    ].join(" ")
+  }
 };
diff --git a/src/components/legend/constants.js b/src/components/legend/constants.js
index 527fa7ba190..344c828f6bf 100644
--- a/src/components/legend/constants.js
+++ b/src/components/legend/constants.js
@@ -5,12 +5,10 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 module.exports = {
-    scrollBarWidth: 4,
-    scrollBarHeight: 20,
-    scrollBarColor: '#808BA4',
-    scrollBarMargin: 4
+  scrollBarWidth: 4,
+  scrollBarHeight: 20,
+  scrollBarColor: "#808BA4",
+  scrollBarMargin: 4
 };
diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js
index a094d5799ba..b430f38e6ea 100644
--- a/src/components/legend/defaults.js
+++ b/src/components/legend/defaults.js
@@ -5,87 +5,90 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Registry = require("../../registry");
+var Lib = require("../../lib");
 
-
-'use strict';
-
-var Registry = require('../../registry');
-var Lib = require('../../lib');
-
-var attributes = require('./attributes');
-var basePlotLayoutAttributes = require('../../plots/layout_attributes');
-var helpers = require('./helpers');
-
+var attributes = require("./attributes");
+var basePlotLayoutAttributes = require("../../plots/layout_attributes");
+var helpers = require("./helpers");
 
 module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
-    var containerIn = layoutIn.legend || {},
-        containerOut = layoutOut.legend = {};
-
-    var visibleTraces = 0,
-        defaultOrder = 'normal',
-        defaultX,
-        defaultY,
-        defaultXAnchor,
-        defaultYAnchor;
-
-    for(var i = 0; i < fullData.length; i++) {
-        var trace = fullData[i];
-
-        if(helpers.legendGetsTrace(trace)) {
-            visibleTraces++;
-            // always show the legend by default if there's a pie
-            if(Registry.traceIs(trace, 'pie')) visibleTraces++;
-        }
-
-        if((Registry.traceIs(trace, 'bar') && layoutOut.barmode === 'stack') ||
-                ['tonextx', 'tonexty'].indexOf(trace.fill) !== -1) {
-            defaultOrder = helpers.isGrouped({traceorder: defaultOrder}) ?
-                'grouped+reversed' : 'reversed';
-        }
-
-        if(trace.legendgroup !== undefined && trace.legendgroup !== '') {
-            defaultOrder = helpers.isReversed({traceorder: defaultOrder}) ?
-                'reversed+grouped' : 'grouped';
-        }
+  var containerIn = layoutIn.legend || {}, containerOut = layoutOut.legend = {};
+
+  var visibleTraces = 0,
+    defaultOrder = "normal",
+    defaultX,
+    defaultY,
+    defaultXAnchor,
+    defaultYAnchor;
+
+  for (var i = 0; i < fullData.length; i++) {
+    var trace = fullData[i];
+
+    if (helpers.legendGetsTrace(trace)) {
+      visibleTraces++;
+      // always show the legend by default if there's a pie
+      if (Registry.traceIs(trace, "pie")) visibleTraces++;
     }
 
-    function coerce(attr, dflt) {
-        return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
+    if (
+      Registry.traceIs(trace, "bar") && layoutOut.barmode === "stack" ||
+        ["tonextx", "tonexty"].indexOf(trace.fill) !== -1
+    ) {
+      defaultOrder = helpers.isGrouped({ traceorder: defaultOrder })
+        ? "grouped+reversed"
+        : "reversed";
     }
 
-    var showLegend = Lib.coerce(layoutIn, layoutOut,
-        basePlotLayoutAttributes, 'showlegend', visibleTraces > 1);
-
-    if(showLegend === false) return;
-
-    coerce('bgcolor', layoutOut.paper_bgcolor);
-    coerce('bordercolor');
-    coerce('borderwidth');
-    Lib.coerceFont(coerce, 'font', layoutOut.font);
-
-    coerce('orientation');
-    if(containerOut.orientation === 'h') {
-        var xaxis = layoutIn.xaxis;
-        if(xaxis && xaxis.rangeslider && xaxis.rangeslider.visible) {
-            defaultX = 0;
-            defaultXAnchor = 'left';
-            defaultY = 1.1;
-            defaultYAnchor = 'bottom';
-        }
-        else {
-            defaultX = 0;
-            defaultXAnchor = 'left';
-            defaultY = -0.1;
-            defaultYAnchor = 'top';
-        }
+    if (trace.legendgroup !== undefined && trace.legendgroup !== "") {
+      defaultOrder = helpers.isReversed({ traceorder: defaultOrder })
+        ? "reversed+grouped"
+        : "grouped";
+    }
+  }
+
+  function coerce(attr, dflt) {
+    return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
+  }
+
+  var showLegend = Lib.coerce(
+    layoutIn,
+    layoutOut,
+    basePlotLayoutAttributes,
+    "showlegend",
+    visibleTraces > 1
+  );
+
+  if (showLegend === false) return;
+
+  coerce("bgcolor", layoutOut.paper_bgcolor);
+  coerce("bordercolor");
+  coerce("borderwidth");
+  Lib.coerceFont(coerce, "font", layoutOut.font);
+
+  coerce("orientation");
+  if (containerOut.orientation === "h") {
+    var xaxis = layoutIn.xaxis;
+    if (xaxis && xaxis.rangeslider && xaxis.rangeslider.visible) {
+      defaultX = 0;
+      defaultXAnchor = "left";
+      defaultY = 1.1;
+      defaultYAnchor = "bottom";
+    } else {
+      defaultX = 0;
+      defaultXAnchor = "left";
+      defaultY = -0.1;
+      defaultYAnchor = "top";
     }
+  }
 
-    coerce('traceorder', defaultOrder);
-    if(helpers.isGrouped(layoutOut.legend)) coerce('tracegroupgap');
+  coerce("traceorder", defaultOrder);
+  if (helpers.isGrouped(layoutOut.legend)) coerce("tracegroupgap");
 
-    coerce('x', defaultX);
-    coerce('xanchor', defaultXAnchor);
-    coerce('y', defaultY);
-    coerce('yanchor', defaultYAnchor);
-    Lib.noneOrAll(containerIn, containerOut, ['x', 'y']);
+  coerce("x", defaultX);
+  coerce("xanchor", defaultXAnchor);
+  coerce("y", defaultY);
+  coerce("yanchor", defaultYAnchor);
+  Lib.noneOrAll(containerIn, containerOut, ["x", "y"]);
 };
diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js
index d6c836c14e1..d6cf15ea24a 100644
--- a/src/components/legend/draw.js
+++ b/src/components/legend/draw.js
@@ -5,710 +5,677 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
+
+var Plotly = require("../../plotly");
+var Lib = require("../../lib");
+var Plots = require("../../plots/plots");
+var Registry = require("../../registry");
+var dragElement = require("../dragelement");
+var Drawing = require("../drawing");
+var Color = require("../color");
+var svgTextUtils = require("../../lib/svg_text_utils");
+
+var constants = require("./constants");
+var getLegendData = require("./get_legend_data");
+var style = require("./style");
+var helpers = require("./helpers");
+var anchorUtils = require("./anchor_utils");
 
+module.exports = function draw(gd) {
+  var fullLayout = gd._fullLayout;
+  var clipId = "legend" + fullLayout._uid;
 
-'use strict';
-
-var d3 = require('d3');
-
-var Plotly = require('../../plotly');
-var Lib = require('../../lib');
-var Plots = require('../../plots/plots');
-var Registry = require('../../registry');
-var dragElement = require('../dragelement');
-var Drawing = require('../drawing');
-var Color = require('../color');
-var svgTextUtils = require('../../lib/svg_text_utils');
+  if (!fullLayout._infolayer || !gd.calcdata) return;
 
-var constants = require('./constants');
-var getLegendData = require('./get_legend_data');
-var style = require('./style');
-var helpers = require('./helpers');
-var anchorUtils = require('./anchor_utils');
+  var opts = fullLayout.legend,
+    legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts),
+    hiddenSlices = fullLayout.hiddenlabels || [];
 
+  if (!fullLayout.showlegend || !legendData.length) {
+    fullLayout._infolayer.selectAll(".legend").remove();
+    fullLayout._topdefs.select("#" + clipId).remove();
 
-module.exports = function draw(gd) {
-    var fullLayout = gd._fullLayout;
-    var clipId = 'legend' + fullLayout._uid;
+    Plots.autoMargin(gd, "legend");
+    return;
+  }
 
-    if(!fullLayout._infolayer || !gd.calcdata) return;
+  var legend = fullLayout._infolayer.selectAll("g.legend").data([0]);
 
-    var opts = fullLayout.legend,
-        legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts),
-        hiddenSlices = fullLayout.hiddenlabels || [];
+  legend.enter().append("g").attr({ class: "legend", "pointer-events": "all" });
 
-    if(!fullLayout.showlegend || !legendData.length) {
-        fullLayout._infolayer.selectAll('.legend').remove();
-        fullLayout._topdefs.select('#' + clipId).remove();
+  var clipPath = fullLayout._topdefs.selectAll("#" + clipId).data([0]);
 
-        Plots.autoMargin(gd, 'legend');
-        return;
-    }
+  clipPath.enter().append("clipPath").attr("id", clipId).append("rect");
 
-    var legend = fullLayout._infolayer.selectAll('g.legend')
-        .data([0]);
+  var bg = legend.selectAll("rect.bg").data([0]);
 
-    legend.enter().append('g')
-        .attr({
-            'class': 'legend',
-            'pointer-events': 'all'
-        });
+  bg
+    .enter()
+    .append("rect")
+    .attr({ class: "bg", "shape-rendering": "crispEdges" });
 
-    var clipPath = fullLayout._topdefs.selectAll('#' + clipId)
-        .data([0]);
+  bg.call(Color.stroke, opts.bordercolor);
+  bg.call(Color.fill, opts.bgcolor);
+  bg.style("stroke-width", opts.borderwidth + "px");
 
-    clipPath.enter().append('clipPath')
-        .attr('id', clipId)
-        .append('rect');
+  var scrollBox = legend.selectAll("g.scrollbox").data([0]);
 
-    var bg = legend.selectAll('rect.bg')
-        .data([0]);
+  scrollBox.enter().append("g").attr("class", "scrollbox");
 
-    bg.enter().append('rect').attr({
-        'class': 'bg',
-        'shape-rendering': 'crispEdges'
-    });
+  var scrollBar = legend.selectAll("rect.scrollbar").data([0]);
 
-    bg.call(Color.stroke, opts.bordercolor);
-    bg.call(Color.fill, opts.bgcolor);
-    bg.style('stroke-width', opts.borderwidth + 'px');
-
-    var scrollBox = legend.selectAll('g.scrollbox')
-        .data([0]);
-
-    scrollBox.enter().append('g')
-        .attr('class', 'scrollbox');
-
-    var scrollBar = legend.selectAll('rect.scrollbar')
-        .data([0]);
-
-    scrollBar.enter().append('rect')
-        .attr({
-            'class': 'scrollbar',
-            'rx': 20,
-            'ry': 2,
-            'width': 0,
-            'height': 0
-        })
-        .call(Color.fill, '#808BA4');
-
-    var groups = scrollBox.selectAll('g.groups')
-        .data(legendData);
-
-    groups.enter().append('g')
-        .attr('class', 'groups');
-
-    groups.exit().remove();
-
-    var traces = groups.selectAll('g.traces')
-        .data(Lib.identity);
-
-    traces.enter().append('g').attr('class', 'traces');
-    traces.exit().remove();
-
-    traces.call(style)
-        .style('opacity', function(d) {
-            var trace = d[0].trace;
-            if(Registry.traceIs(trace, 'pie')) {
-                return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1;
-            } else {
-                return trace.visible === 'legendonly' ? 0.5 : 1;
-            }
-        })
-        .each(function() {
-            d3.select(this)
-                .call(drawTexts, gd)
-                .call(setupTraceToggle, gd);
-        });
-
-    var firstRender = legend.enter().size() !== 0;
-    if(firstRender) {
-        computeLegendDimensions(gd, groups, traces);
-        expandMargin(gd);
-    }
+  scrollBar
+    .enter()
+    .append("rect")
+    .attr({ class: "scrollbar", rx: 20, ry: 2, width: 0, height: 0 })
+    .call(Color.fill, "#808BA4");
 
-    // Position and size the legend
-    var lxMin = 0,
-        lxMax = fullLayout.width,
-        lyMin = 0,
-        lyMax = fullLayout.height;
+  var groups = scrollBox.selectAll("g.groups").data(legendData);
 
-    computeLegendDimensions(gd, groups, traces);
+  groups.enter().append("g").attr("class", "groups");
 
-    if(opts.height > lyMax) {
-        // If the legend doesn't fit in the plot area,
-        // do not expand the vertical margins.
-        expandHorizontalMargin(gd);
-    } else {
-        expandMargin(gd);
-    }
+  groups.exit().remove();
 
-    // Scroll section must be executed after repositionLegend.
-    // It requires the legend width, height, x and y to position the scrollbox
-    // and these values are mutated in repositionLegend.
-    var gs = fullLayout._size,
-        lx = gs.l + gs.w * opts.x,
-        ly = gs.t + gs.h * (1 - opts.y);
+  var traces = groups.selectAll("g.traces").data(Lib.identity);
 
-    if(anchorUtils.isRightAnchor(opts)) {
-        lx -= opts.width;
-    }
-    else if(anchorUtils.isCenterAnchor(opts)) {
-        lx -= opts.width / 2;
-    }
+  traces.enter().append("g").attr("class", "traces");
+  traces.exit().remove();
 
-    if(anchorUtils.isBottomAnchor(opts)) {
-        ly -= opts.height;
-    }
-    else if(anchorUtils.isMiddleAnchor(opts)) {
-        ly -= opts.height / 2;
-    }
-
-    // Make sure the legend left and right sides are visible
-    var legendWidth = opts.width,
-        legendWidthMax = gs.w;
+  traces
+    .call(style)
+    .style("opacity", function(d) {
+      var trace = d[0].trace;
+      if (Registry.traceIs(trace, "pie")) {
+        return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1;
+      } else {
+        return trace.visible === "legendonly" ? 0.5 : 1;
+      }
+    })
+    .each(function() {
+      d3.select(this).call(drawTexts, gd).call(setupTraceToggle, gd);
+    });
 
-    if(legendWidth > legendWidthMax) {
-        lx = gs.l;
-        legendWidth = legendWidthMax;
-    }
-    else {
-        if(lx + legendWidth > lxMax) lx = lxMax - legendWidth;
-        if(lx < lxMin) lx = lxMin;
-        legendWidth = Math.min(lxMax - lx, opts.width);
-    }
+  var firstRender = legend.enter().size() !== 0;
+  if (firstRender) {
+    computeLegendDimensions(gd, groups, traces);
+    expandMargin(gd);
+  }
+
+  // Position and size the legend
+  var lxMin = 0, lxMax = fullLayout.width, lyMin = 0, lyMax = fullLayout.height;
+
+  computeLegendDimensions(gd, groups, traces);
+
+  if (opts.height > lyMax) {
+    // If the legend doesn't fit in the plot area,
+    // do not expand the vertical margins.
+    expandHorizontalMargin(gd);
+  } else {
+    expandMargin(gd);
+  }
+
+  // Scroll section must be executed after repositionLegend.
+  // It requires the legend width, height, x and y to position the scrollbox
+  // and these values are mutated in repositionLegend.
+  var gs = fullLayout._size,
+    lx = gs.l + gs.w * opts.x,
+    ly = gs.t + gs.h * (1 - opts.y);
+
+  if (anchorUtils.isRightAnchor(opts)) {
+    lx -= opts.width;
+  } else if (anchorUtils.isCenterAnchor(opts)) {
+    lx -= opts.width / 2;
+  }
+
+  if (anchorUtils.isBottomAnchor(opts)) {
+    ly -= opts.height;
+  } else if (anchorUtils.isMiddleAnchor(opts)) {
+    ly -= opts.height / 2;
+  }
+
+  // Make sure the legend left and right sides are visible
+  var legendWidth = opts.width, legendWidthMax = gs.w;
+
+  if (legendWidth > legendWidthMax) {
+    lx = gs.l;
+    legendWidth = legendWidthMax;
+  } else {
+    if (lx + legendWidth > lxMax) lx = lxMax - legendWidth;
+    if (lx < lxMin) lx = lxMin;
+    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, legendHeightMax = gs.h;
+
+  if (legendHeight > legendHeightMax) {
+    ly = gs.t;
+    legendHeight = legendHeightMax;
+  } else {
+    if (ly + legendHeight > lyMax) ly = lyMax - legendHeight;
+    if (ly < lyMin) ly = lyMin;
+    legendHeight = Math.min(lyMax - ly, opts.height);
+  }
+
+  // Set size and position of all the elements that make up a legend:
+  // legend, background and border, scroll box and scroll bar
+  Drawing.setTranslate(legend, lx, ly);
+
+  var scrollBarYMax = legendHeight -
+    constants.scrollBarHeight -
+    2 * constants.scrollBarMargin,
+    scrollBoxYMax = opts.height - legendHeight,
+    scrollBarY,
+    scrollBoxY;
+
+  if (opts.height <= legendHeight || gd._context.staticPlot) {
+    // if scrollbar should not be shown.
+    bg.attr({
+      width: legendWidth - opts.borderwidth,
+      height: legendHeight - opts.borderwidth,
+      x: opts.borderwidth / 2,
+      y: opts.borderwidth / 2
+    });
 
-    // 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,
-        legendHeightMax = gs.h;
+    Drawing.setTranslate(scrollBox, 0, 0);
 
-    if(legendHeight > legendHeightMax) {
-        ly = gs.t;
-        legendHeight = legendHeightMax;
-    }
-    else {
-        if(ly + legendHeight > lyMax) ly = lyMax - legendHeight;
-        if(ly < lyMin) ly = lyMin;
-        legendHeight = Math.min(lyMax - ly, opts.height);
-    }
+    clipPath.select("rect").attr({
+      width: legendWidth - 2 * opts.borderwidth,
+      height: legendHeight - 2 * opts.borderwidth,
+      x: opts.borderwidth,
+      y: opts.borderwidth
+    });
 
-    // Set size and position of all the elements that make up a legend:
-    // legend, background and border, scroll box and scroll bar
-    Drawing.setTranslate(legend, lx, ly);
-
-    var scrollBarYMax = legendHeight -
-            constants.scrollBarHeight -
-            2 * constants.scrollBarMargin,
-        scrollBoxYMax = opts.height - legendHeight,
-        scrollBarY,
-        scrollBoxY;
-
-    if(opts.height <= legendHeight || gd._context.staticPlot) {
-        // if scrollbar should not be shown.
-        bg.attr({
-            width: legendWidth - opts.borderwidth,
-            height: legendHeight - opts.borderwidth,
-            x: opts.borderwidth / 2,
-            y: opts.borderwidth / 2
-        });
-
-        Drawing.setTranslate(scrollBox, 0, 0);
-
-        clipPath.select('rect').attr({
-            width: legendWidth - 2 * opts.borderwidth,
-            height: legendHeight - 2 * opts.borderwidth,
-            x: opts.borderwidth,
-            y: opts.borderwidth
-        });
-
-        scrollBox.call(Drawing.setClipUrl, clipId);
-    }
-    else {
-        scrollBarY = constants.scrollBarMargin,
-        scrollBoxY = scrollBox.attr('data-scroll') || 0;
-
-        // increase the background and clip-path width
-        // by the scrollbar width and margin
-        bg.attr({
-            width: legendWidth -
-                2 * opts.borderwidth +
-                constants.scrollBarWidth +
-                constants.scrollBarMargin,
-            height: legendHeight - opts.borderwidth,
-            x: opts.borderwidth / 2,
-            y: opts.borderwidth / 2
-        });
-
-        clipPath.select('rect').attr({
-            width: legendWidth -
-                2 * opts.borderwidth +
-                constants.scrollBarWidth +
-                constants.scrollBarMargin,
-            height: legendHeight - 2 * opts.borderwidth,
-            x: opts.borderwidth,
-            y: opts.borderwidth - scrollBoxY
-        });
-
-        scrollBox.call(Drawing.setClipUrl, clipId);
-
-        if(firstRender) scrollHandler(scrollBarY, scrollBoxY);
-
-        legend.on('wheel', null);  // to be safe, remove previous listeners
-        legend.on('wheel', function() {
-            scrollBoxY = Lib.constrain(
-                scrollBox.attr('data-scroll') -
-                    d3.event.deltaY / scrollBarYMax * scrollBoxYMax,
-                -scrollBoxYMax, 0);
-            scrollBarY = constants.scrollBarMargin -
-                scrollBoxY / scrollBoxYMax * scrollBarYMax;
-            scrollHandler(scrollBarY, scrollBoxY);
-            d3.event.preventDefault();
-        });
-
-        // to be safe, remove previous listeners
-        scrollBar.on('.drag', null);
-        scrollBox.on('.drag', null);
-
-        var drag = d3.behavior.drag().on('drag', function() {
-            scrollBarY = Lib.constrain(
-                d3.event.y - constants.scrollBarHeight / 2,
-                constants.scrollBarMargin,
-                constants.scrollBarMargin + scrollBarYMax);
-            scrollBoxY = - (scrollBarY - constants.scrollBarMargin) /
-                scrollBarYMax * scrollBoxYMax;
-            scrollHandler(scrollBarY, scrollBoxY);
-        });
-
-        scrollBar.call(drag);
-        scrollBox.call(drag);
-    }
+    scrollBox.call(Drawing.setClipUrl, clipId);
+  } else {
+    scrollBarY = constants.scrollBarMargin, scrollBoxY = scrollBox.attr(
+      "data-scroll"
+    ) ||
+      0;
+
+    // increase the background and clip-path width
+    // by the scrollbar width and margin
+    bg.attr({
+      width: (
+        legendWidth -
+          2 * opts.borderwidth +
+          constants.scrollBarWidth +
+          constants.scrollBarMargin
+      ),
+      height: legendHeight - opts.borderwidth,
+      x: opts.borderwidth / 2,
+      y: opts.borderwidth / 2
+    });
 
+    clipPath.select("rect").attr({
+      width: (
+        legendWidth -
+          2 * opts.borderwidth +
+          constants.scrollBarWidth +
+          constants.scrollBarMargin
+      ),
+      height: legendHeight - 2 * opts.borderwidth,
+      x: opts.borderwidth,
+      y: opts.borderwidth - scrollBoxY
+    });
 
-    function scrollHandler(scrollBarY, scrollBoxY) {
-        scrollBox
-            .attr('data-scroll', scrollBoxY)
-            .call(Drawing.setTranslate, 0, scrollBoxY);
+    scrollBox.call(Drawing.setClipUrl, clipId);
+
+    if (firstRender) scrollHandler(scrollBarY, scrollBoxY);
+
+    legend.on("wheel", null);
+    // to be safe, remove previous listeners
+    legend.on("wheel", function() {
+      scrollBoxY = Lib.constrain(
+        scrollBox.attr("data-scroll") -
+          d3.event.deltaY / scrollBarYMax * scrollBoxYMax,
+        -scrollBoxYMax,
+        0
+      );
+      scrollBarY = constants.scrollBarMargin -
+        scrollBoxY / scrollBoxYMax * scrollBarYMax;
+      scrollHandler(scrollBarY, scrollBoxY);
+      d3.event.preventDefault();
+    });
 
-        scrollBar.call(
-            Drawing.setRect,
-            legendWidth,
-            scrollBarY,
-            constants.scrollBarWidth,
-            constants.scrollBarHeight
-        );
-        clipPath.select('rect').attr({
-            y: opts.borderwidth - scrollBoxY
-        });
-    }
+    // to be safe, remove previous listeners
+    scrollBar.on(".drag", null);
+    scrollBox.on(".drag", null);
+
+    var drag = d3.behavior.drag().on("drag", function() {
+      scrollBarY = Lib.constrain(
+        d3.event.y - constants.scrollBarHeight / 2,
+        constants.scrollBarMargin,
+        constants.scrollBarMargin + scrollBarYMax
+      );
+      scrollBoxY = (-(scrollBarY - constants.scrollBarMargin)) /
+        scrollBarYMax *
+        scrollBoxYMax;
+      scrollHandler(scrollBarY, scrollBoxY);
+    });
 
-    if(gd._context.editable) {
-        var xf, yf, x0, y0;
-
-        legend.classed('cursor-move', true);
-
-        dragElement.init({
-            element: legend.node(),
-            prepFn: function() {
-                var transform = Drawing.getTranslate(legend);
-
-                x0 = transform.x;
-                y0 = transform.y;
-            },
-            moveFn: function(dx, dy) {
-                var newX = x0 + dx,
-                    newY = y0 + dy;
-
-                Drawing.setTranslate(legend, newX, newY);
-
-                xf = dragElement.align(newX, 0, gs.l, gs.l + gs.w, opts.xanchor);
-                yf = dragElement.align(newY, 0, gs.t + gs.h, gs.t, opts.yanchor);
-            },
-            doneFn: function(dragged) {
-                if(dragged && xf !== undefined && yf !== undefined) {
-                    Plotly.relayout(gd, {'legend.x': xf, 'legend.y': yf});
-                }
-            }
-        });
-    }
+    scrollBar.call(drag);
+    scrollBox.call(drag);
+  }
+
+  function scrollHandler(scrollBarY, scrollBoxY) {
+    scrollBox
+      .attr("data-scroll", scrollBoxY)
+      .call(Drawing.setTranslate, 0, scrollBoxY);
+
+    scrollBar.call(
+      Drawing.setRect,
+      legendWidth,
+      scrollBarY,
+      constants.scrollBarWidth,
+      constants.scrollBarHeight
+    );
+    clipPath.select("rect").attr({ y: opts.borderwidth - scrollBoxY });
+  }
+
+  if (gd._context.editable) {
+    var xf, yf, x0, y0;
+
+    legend.classed("cursor-move", true);
+
+    dragElement.init({
+      element: legend.node(),
+      prepFn: function() {
+        var transform = Drawing.getTranslate(legend);
+
+        x0 = transform.x;
+        y0 = transform.y;
+      },
+      moveFn: function(dx, dy) {
+        var newX = x0 + dx, newY = y0 + dy;
+
+        Drawing.setTranslate(legend, newX, newY);
+
+        xf = dragElement.align(newX, 0, gs.l, gs.l + gs.w, opts.xanchor);
+        yf = dragElement.align(newY, 0, gs.t + gs.h, gs.t, opts.yanchor);
+      },
+      doneFn: function(dragged) {
+        if (dragged && xf !== undefined && yf !== undefined) {
+          Plotly.relayout(gd, { "legend.x": xf, "legend.y": yf });
+        }
+      }
+    });
+  }
 };
 
 function drawTexts(g, gd) {
-    var legendItem = g.data()[0][0],
-        fullLayout = gd._fullLayout,
-        trace = legendItem.trace,
-        isPie = Registry.traceIs(trace, 'pie'),
-        traceIndex = trace.index,
-        name = isPie ? legendItem.label : trace.name;
-
-    var text = g.selectAll('text.legendtext')
-        .data([0]);
-    text.enter().append('text').classed('legendtext', true);
-    text.attr({
-        x: 40,
-        y: 0,
-        'data-unformatted': name
-    })
-    .style('text-anchor', 'start')
-    .classed('user-select-none', true)
+  var legendItem = g.data()[0][0],
+    fullLayout = gd._fullLayout,
+    trace = legendItem.trace,
+    isPie = Registry.traceIs(trace, "pie"),
+    traceIndex = trace.index,
+    name = isPie ? legendItem.label : trace.name;
+
+  var text = g.selectAll("text.legendtext").data([0]);
+  text.enter().append("text").classed("legendtext", true);
+  text
+    .attr({ x: 40, y: 0, "data-unformatted": name })
+    .style("text-anchor", "start")
+    .classed("user-select-none", true)
     .call(Drawing.font, fullLayout.legend.font)
     .text(name);
 
-    function textLayout(s) {
-        svgTextUtils.convertToTspans(s, function() {
-            s.selectAll('tspan.line').attr({x: s.attr('x')});
-            g.call(computeTextDimensions, gd);
-        });
-    }
+  function textLayout(s) {
+    svgTextUtils.convertToTspans(s, function() {
+      s.selectAll("tspan.line").attr({ x: s.attr("x") });
+      g.call(computeTextDimensions, gd);
+    });
+  }
 
-    if(gd._context.editable && !isPie) {
-        text.call(svgTextUtils.makeEditable)
-            .call(textLayout)
-            .on('edit', function(text) {
-                this.attr({'data-unformatted': text});
+  if (gd._context.editable && !isPie) {
+    text
+      .call(svgTextUtils.makeEditable)
+      .call(textLayout)
+      .on("edit", function(text) {
+        this.attr({ "data-unformatted": text });
 
-                this.text(text)
-                    .call(textLayout);
+        this.text(text).call(textLayout);
 
-                if(!this.text()) text = ' \u0020\u0020 ';
+        if (!this.text()) text = "    ";
 
-                var fullInput = legendItem.trace._fullInput || {},
-                    astr;
+        var fullInput = legendItem.trace._fullInput || {}, astr;
 
-                // N.B. this block isn't super clean,
-                // is unfortunately untested at the moment,
-                // and only works for for 'ohlc' and 'candlestick',
-                // but should be generalized for other one-to-many transforms
-                if(['ohlc', 'candlestick'].indexOf(fullInput.type) !== -1) {
-                    var transforms = legendItem.trace.transforms,
-                        direction = transforms[transforms.length - 1].direction;
+        // N.B. this block isn't super clean,
+        // is unfortunately untested at the moment,
+        // and only works for for 'ohlc' and 'candlestick',
+        // but should be generalized for other one-to-many transforms
+        if (["ohlc", "candlestick"].indexOf(fullInput.type) !== -1) {
+          var transforms = legendItem.trace.transforms,
+            direction = transforms[transforms.length - 1].direction;
 
-                    astr = direction + '.name';
-                }
-                else astr = 'name';
+          astr = direction + ".name";
+        } else {
+          astr = "name";
+        }
 
-                Plotly.restyle(gd, astr, text, traceIndex);
-            });
-    }
-    else text.call(textLayout);
+        Plotly.restyle(gd, astr, text, traceIndex);
+      });
+  } else {
+    text.call(textLayout);
+  }
 }
 
 function setupTraceToggle(g, gd) {
-    var hiddenSlices = gd._fullLayout.hiddenlabels ?
-        gd._fullLayout.hiddenlabels.slice() :
-        [];
-
-    var traceToggle = g.selectAll('rect')
-        .data([0]);
-
-    traceToggle.enter().append('rect')
-        .classed('legendtoggle', true)
-        .style('cursor', 'pointer')
-        .attr('pointer-events', 'all')
-        .call(Color.fill, 'rgba(0,0,0,0)');
-
-    traceToggle.on('click', function() {
-        if(gd._dragged) return;
-
-        var legendItem = g.data()[0][0],
-            fullData = gd._fullData,
-            trace = legendItem.trace,
-            legendgroup = trace.legendgroup,
-            traceIndicesInGroup = [],
-            tracei,
-            newVisible;
-
-        if(Registry.traceIs(trace, 'pie')) {
-            var thisLabel = legendItem.label,
-                thisLabelIndex = hiddenSlices.indexOf(thisLabel);
-
-            if(thisLabelIndex === -1) hiddenSlices.push(thisLabel);
-            else hiddenSlices.splice(thisLabelIndex, 1);
-
-            Plotly.relayout(gd, 'hiddenlabels', hiddenSlices);
-        } else {
-            if(legendgroup === '') {
-                traceIndicesInGroup = [trace.index];
-            } else {
-                for(var i = 0; i < fullData.length; i++) {
-                    tracei = fullData[i];
-                    if(tracei.legendgroup === legendgroup) {
-                        traceIndicesInGroup.push(tracei.index);
-                    }
-                }
-            }
-
-            newVisible = trace.visible === true ? 'legendonly' : true;
-            Plotly.restyle(gd, 'visible', newVisible, traceIndicesInGroup);
-        }
-    });
-}
+  var hiddenSlices = gd._fullLayout.hiddenlabels
+    ? gd._fullLayout.hiddenlabels.slice()
+    : [];
+
+  var traceToggle = g.selectAll("rect").data([0]);
+
+  traceToggle
+    .enter()
+    .append("rect")
+    .classed("legendtoggle", true)
+    .style("cursor", "pointer")
+    .attr("pointer-events", "all")
+    .call(Color.fill, "rgba(0,0,0,0)");
+
+  traceToggle.on("click", function() {
+    if (gd._dragged) return;
 
-function computeTextDimensions(g, gd) {
     var legendItem = g.data()[0][0],
-        mathjaxGroup = g.select('g[class*=math-group]'),
-        opts = gd._fullLayout.legend,
-        lineHeight = opts.font.size * 1.3,
-        height,
-        width;
-
-    if(!legendItem.trace.showlegend) {
-        g.remove();
-        return;
-    }
+      fullData = gd._fullData,
+      trace = legendItem.trace,
+      legendgroup = trace.legendgroup,
+      traceIndicesInGroup = [],
+      tracei,
+      newVisible;
 
-    if(mathjaxGroup.node()) {
-        var mathjaxBB = Drawing.bBox(mathjaxGroup.node());
+    if (Registry.traceIs(trace, "pie")) {
+      var thisLabel = legendItem.label,
+        thisLabelIndex = hiddenSlices.indexOf(thisLabel);
 
-        height = mathjaxBB.height;
-        width = mathjaxBB.width;
+      if (thisLabelIndex === -1) hiddenSlices.push(thisLabel);
+      else hiddenSlices.splice(thisLabelIndex, 1);
 
-        Drawing.setTranslate(mathjaxGroup, 0, (height / 4));
-    }
-    else {
-        var text = g.selectAll('.legendtext'),
-            textSpans = g.selectAll('.legendtext>tspan'),
-            textLines = textSpans[0].length || 1;
-
-        height = lineHeight * textLines;
-        width = text.node() && Drawing.bBox(text.node()).width;
-
-        // approximation to height offset to center the font
-        // to avoid getBoundingClientRect
-        var textY = lineHeight * (0.3 + (1 - textLines) / 2);
-        text.attr('y', textY);
-        textSpans.attr('y', textY);
-    }
+      Plotly.relayout(gd, "hiddenlabels", hiddenSlices);
+    } else {
+      if (legendgroup === "") {
+        traceIndicesInGroup = [trace.index];
+      } else {
+        for (var i = 0; i < fullData.length; i++) {
+          tracei = fullData[i];
+          if (tracei.legendgroup === legendgroup) {
+            traceIndicesInGroup.push(tracei.index);
+          }
+        }
+      }
 
-    height = Math.max(height, 16) + 3;
+      newVisible = trace.visible === true ? "legendonly" : true;
+      Plotly.restyle(gd, "visible", newVisible, traceIndicesInGroup);
+    }
+  });
+}
 
-    legendItem.height = height;
-    legendItem.width = width;
+function computeTextDimensions(g, gd) {
+  var legendItem = g.data()[0][0],
+    mathjaxGroup = g.select("g[class*=math-group]"),
+    opts = gd._fullLayout.legend,
+    lineHeight = opts.font.size * 1.3,
+    height,
+    width;
+
+  if (!legendItem.trace.showlegend) {
+    g.remove();
+    return;
+  }
+
+  if (mathjaxGroup.node()) {
+    var mathjaxBB = Drawing.bBox(mathjaxGroup.node());
+
+    height = mathjaxBB.height;
+    width = mathjaxBB.width;
+
+    Drawing.setTranslate(mathjaxGroup, 0, height / 4);
+  } else {
+    var text = g.selectAll(".legendtext"),
+      textSpans = g.selectAll(".legendtext>tspan"),
+      textLines = textSpans[0].length || 1;
+
+    height = lineHeight * textLines;
+    width = text.node() && Drawing.bBox(text.node()).width;
+
+    // approximation to height offset to center the font
+    // to avoid getBoundingClientRect
+    var textY = lineHeight * (0.3 + (1 - textLines) / 2);
+    text.attr("y", textY);
+    textSpans.attr("y", textY);
+  }
+
+  height = Math.max(height, 16) + 3;
+
+  legendItem.height = height;
+  legendItem.width = width;
 }
 
 function computeLegendDimensions(gd, groups, traces) {
-    var fullLayout = gd._fullLayout,
-        opts = fullLayout.legend,
-        borderwidth = opts.borderwidth,
-        isGrouped = helpers.isGrouped(opts);
-
-    if(helpers.isVertical(opts)) {
-        if(isGrouped) {
-            groups.each(function(d, i) {
-                Drawing.setTranslate(this, 0, i * opts.tracegroupgap);
-            });
-        }
-
-        opts.width = 0;
-        opts.height = 0;
+  var fullLayout = gd._fullLayout,
+    opts = fullLayout.legend,
+    borderwidth = opts.borderwidth,
+    isGrouped = helpers.isGrouped(opts);
+
+  if (helpers.isVertical(opts)) {
+    if (isGrouped) {
+      groups.each(function(d, i) {
+        Drawing.setTranslate(this, 0, i * opts.tracegroupgap);
+      });
+    }
 
-        traces.each(function(d) {
-            var legendItem = d[0],
-                textHeight = legendItem.height,
-                textWidth = legendItem.width;
+    opts.width = 0;
+    opts.height = 0;
 
-            Drawing.setTranslate(this,
-                borderwidth,
-                (5 + borderwidth + opts.height + textHeight / 2));
+    traces.each(function(d) {
+      var legendItem = d[0],
+        textHeight = legendItem.height,
+        textWidth = legendItem.width;
 
-            opts.height += textHeight;
-            opts.width = Math.max(opts.width, textWidth);
-        });
+      Drawing.setTranslate(
+        this,
+        borderwidth,
+        5 + borderwidth + opts.height + textHeight / 2
+      );
 
-        opts.width += 45 + borderwidth * 2;
-        opts.height += 10 + borderwidth * 2;
+      opts.height += textHeight;
+      opts.width = Math.max(opts.width, textWidth);
+    });
 
-        if(isGrouped) {
-            opts.height += (opts._lgroupsLength - 1) * opts.tracegroupgap;
-        }
+    opts.width += 45 + 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);
-
-        traces.each(function(d) {
-            var legendItem = d[0],
-                bg = d3.select(this).select('.legendtoggle');
-
-            bg.call(Drawing.setRect,
-                0,
-                -legendItem.height / 2,
-                (gd._context.editable ? 0 : opts.width) + 40,
-                legendItem.height
-            );
-        });
+    if (isGrouped) {
+      opts.height += (opts._lgroupsLength - 1) * opts.tracegroupgap;
     }
-    else if(isGrouped) {
-        opts.width = 0;
-        opts.height = 0;
 
-        var groupXOffsets = [opts.width],
-            groupData = groups.data();
+    // make sure we're only getting full pixels
+    opts.width = Math.ceil(opts.width);
+    opts.height = Math.ceil(opts.height);
 
-        for(var i = 0, n = groupData.length; i < n; i++) {
-            var textWidths = groupData[i].map(function(legendItemArray) {
-                return legendItemArray[0].width;
-            });
+    traces.each(function(d) {
+      var legendItem = d[0], bg = d3.select(this).select(".legendtoggle");
 
-            var groupWidth = 40 + Math.max.apply(null, textWidths);
+      bg.call(
+        Drawing.setRect,
+        0,
+        (-legendItem.height) / 2,
+        (gd._context.editable ? 0 : opts.width) + 40,
+        legendItem.height
+      );
+    });
+  } else if (isGrouped) {
+    opts.width = 0;
+    opts.height = 0;
 
-            opts.width += opts.tracegroupgap + groupWidth;
+    var groupXOffsets = [opts.width], groupData = groups.data();
 
-            groupXOffsets.push(opts.width);
-        }
+    for (var i = 0, n = groupData.length; i < n; i++) {
+      var textWidths = groupData[i].map(function(legendItemArray) {
+        return legendItemArray[0].width;
+      });
 
-        groups.each(function(d, i) {
-            Drawing.setTranslate(this, groupXOffsets[i], 0);
-        });
+      var groupWidth = 40 + Math.max.apply(null, textWidths);
 
-        groups.each(function() {
-            var group = d3.select(this),
-                groupTraces = group.selectAll('g.traces'),
-                groupHeight = 0;
+      opts.width += opts.tracegroupgap + groupWidth;
 
-            groupTraces.each(function(d) {
-                var legendItem = d[0],
-                    textHeight = legendItem.height;
+      groupXOffsets.push(opts.width);
+    }
 
-                Drawing.setTranslate(this,
-                    0,
-                    (5 + borderwidth + groupHeight + textHeight / 2));
+    groups.each(function(d, i) {
+      Drawing.setTranslate(this, groupXOffsets[i], 0);
+    });
 
-                groupHeight += textHeight;
-            });
+    groups.each(function() {
+      var group = d3.select(this),
+        groupTraces = group.selectAll("g.traces"),
+        groupHeight = 0;
 
-            opts.height = Math.max(opts.height, groupHeight);
-        });
+      groupTraces.each(function(d) {
+        var legendItem = d[0], textHeight = legendItem.height;
 
-        opts.height += 10 + borderwidth * 2;
-        opts.width += borderwidth * 2;
+        Drawing.setTranslate(
+          this,
+          0,
+          5 + borderwidth + groupHeight + textHeight / 2
+        );
 
-        // make sure we're only getting full pixels
-        opts.width = Math.ceil(opts.width);
-        opts.height = Math.ceil(opts.height);
+        groupHeight += textHeight;
+      });
 
-        traces.each(function(d) {
-            var legendItem = d[0],
-                bg = d3.select(this).select('.legendtoggle');
+      opts.height = Math.max(opts.height, groupHeight);
+    });
 
-            bg.call(Drawing.setRect,
-                0,
-                -legendItem.height / 2,
-                (gd._context.editable ? 0 : opts.width),
-                legendItem.height
-            );
-        });
-    }
-    else {
-        opts.width = 0;
-        opts.height = 0;
-        var rowHeight = 0,
-            maxTraceHeight = 0,
-            maxTraceWidth = 0,
-            offsetX = 0;
-
-        // calculate largest width for traces and use for width of all legend items
-        traces.each(function(d) {
-            maxTraceWidth = Math.max(40 + d[0].width, maxTraceWidth);
-        });
-
-        traces.each(function(d) {
-            var legendItem = d[0],
-                traceWidth = maxTraceWidth,
-                traceGap = opts.tracegroupgap || 5;
-
-            if((borderwidth + offsetX + traceGap + traceWidth) > (fullLayout.width - (fullLayout.margin.r + fullLayout.margin.l))) {
-                offsetX = 0;
-                rowHeight = rowHeight + maxTraceHeight;
-                opts.height = opts.height + maxTraceHeight;
-                // reset for next row
-                maxTraceHeight = 0;
-            }
-
-            Drawing.setTranslate(this,
-                (borderwidth + offsetX),
-                (5 + borderwidth + legendItem.height / 2) + rowHeight);
-
-            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;
-
-        // make sure we're only getting full pixels
-        opts.width = Math.ceil(opts.width);
-        opts.height = Math.ceil(opts.height);
-
-        traces.each(function(d) {
-            var legendItem = d[0],
-                bg = d3.select(this).select('.legendtoggle');
-
-            bg.call(Drawing.setRect,
-                0,
-                -legendItem.height / 2,
-                (gd._context.editable ? 0 : opts.width),
-                legendItem.height
-            );
-        });
-    }
-}
+    opts.height += 10 + borderwidth * 2;
+    opts.width += borderwidth * 2;
 
-function expandMargin(gd) {
-    var fullLayout = gd._fullLayout,
-        opts = fullLayout.legend;
+    // make sure we're only getting full pixels
+    opts.width = Math.ceil(opts.width);
+    opts.height = Math.ceil(opts.height);
 
-    var xanchor = 'left';
-    if(anchorUtils.isRightAnchor(opts)) {
-        xanchor = 'right';
-    }
-    else if(anchorUtils.isCenterAnchor(opts)) {
-        xanchor = 'center';
-    }
+    traces.each(function(d) {
+      var legendItem = d[0], bg = d3.select(this).select(".legendtoggle");
 
-    var yanchor = 'top';
-    if(anchorUtils.isBottomAnchor(opts)) {
-        yanchor = 'bottom';
-    }
-    else if(anchorUtils.isMiddleAnchor(opts)) {
-        yanchor = 'middle';
-    }
+      bg.call(
+        Drawing.setRect,
+        0,
+        (-legendItem.height) / 2,
+        gd._context.editable ? 0 : opts.width,
+        legendItem.height
+      );
+    });
+  } else {
+    opts.width = 0;
+    opts.height = 0;
+    var rowHeight = 0, maxTraceHeight = 0, maxTraceWidth = 0, offsetX = 0;
+
+    // calculate largest width for traces and use for width of all legend items
+    traces.each(function(d) {
+      maxTraceWidth = Math.max(40 + d[0].width, maxTraceWidth);
+    });
 
-    // lastly check if the margin auto-expand has changed
-    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)
+    traces.each(function(d) {
+      var legendItem = d[0],
+        traceWidth = maxTraceWidth,
+        traceGap = opts.tracegroupgap || 5;
+
+      if (
+        borderwidth + offsetX + traceGap + traceWidth >
+          fullLayout.width - (fullLayout.margin.r + fullLayout.margin.l)
+      ) {
+        offsetX = 0;
+        rowHeight = rowHeight + maxTraceHeight;
+        opts.height = opts.height + maxTraceHeight;
+        // reset for next row
+        maxTraceHeight = 0;
+      }
+
+      Drawing.setTranslate(
+        this,
+        borderwidth + offsetX,
+        5 + borderwidth + legendItem.height / 2 + rowHeight
+      );
+
+      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);
     });
-}
 
-function expandHorizontalMargin(gd) {
-    var fullLayout = gd._fullLayout,
-        opts = fullLayout.legend;
+    opts.width += borderwidth * 2;
+    opts.height += 10 + borderwidth * 2;
 
-    var xanchor = 'left';
-    if(anchorUtils.isRightAnchor(opts)) {
-        xanchor = 'right';
-    }
-    else if(anchorUtils.isCenterAnchor(opts)) {
-        xanchor = 'center';
-    }
+    // make sure we're only getting full pixels
+    opts.width = Math.ceil(opts.width);
+    opts.height = Math.ceil(opts.height);
+
+    traces.each(function(d) {
+      var legendItem = d[0], bg = d3.select(this).select(".legendtoggle");
 
-    // lastly check if the margin auto-expand has changed
-    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),
-        b: 0,
-        t: 0
+      bg.call(
+        Drawing.setRect,
+        0,
+        (-legendItem.height) / 2,
+        gd._context.editable ? 0 : opts.width,
+        legendItem.height
+      );
     });
+  }
+}
+
+function expandMargin(gd) {
+  var fullLayout = gd._fullLayout, opts = fullLayout.legend;
+
+  var xanchor = "left";
+  if (anchorUtils.isRightAnchor(opts)) {
+    xanchor = "right";
+  } else if (anchorUtils.isCenterAnchor(opts)) {
+    xanchor = "center";
+  }
+
+  var yanchor = "top";
+  if (anchorUtils.isBottomAnchor(opts)) {
+    yanchor = "bottom";
+  } else if (anchorUtils.isMiddleAnchor(opts)) {
+    yanchor = "middle";
+  }
+
+  // lastly check if the margin auto-expand has changed
+  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)
+  });
+}
+
+function expandHorizontalMargin(gd) {
+  var fullLayout = gd._fullLayout, opts = fullLayout.legend;
+
+  var xanchor = "left";
+  if (anchorUtils.isRightAnchor(opts)) {
+    xanchor = "right";
+  } else if (anchorUtils.isCenterAnchor(opts)) {
+    xanchor = "center";
+  }
+
+  // lastly check if the margin auto-expand has changed
+  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),
+    b: 0,
+    t: 0
+  });
 }
diff --git a/src/components/legend/get_legend_data.js b/src/components/legend/get_legend_data.js
index 4ad4636d442..2522570ee03 100644
--- a/src/components/legend/get_legend_data.js
+++ b/src/components/legend/get_legend_data.js
@@ -5,99 +5,95 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var Registry = require('../../registry');
-var helpers = require('./helpers');
-
+"use strict";
+var Registry = require("../../registry");
+var helpers = require("./helpers");
 
 module.exports = function getLegendData(calcdata, opts) {
-    var lgroupToTraces = {},
-        lgroups = [],
-        hasOneNonBlankGroup = false,
-        slicesShown = {},
-        lgroupi = 0;
-
-    var i, j;
-
-    function addOneItem(legendGroup, legendItem) {
-        // each '' legend group is treated as a separate group
-        if(legendGroup === '' || !helpers.isGrouped(opts)) {
-            var uniqueGroup = '~~i' + lgroupi; // TODO: check this against fullData legendgroups?
-
-            lgroups.push(uniqueGroup);
-            lgroupToTraces[uniqueGroup] = [[legendItem]];
-            lgroupi++;
-        }
-        else if(lgroups.indexOf(legendGroup) === -1) {
-            lgroups.push(legendGroup);
-            hasOneNonBlankGroup = true;
-            lgroupToTraces[legendGroup] = [[legendItem]];
-        }
-        else lgroupToTraces[legendGroup].push([legendItem]);
+  var lgroupToTraces = {},
+    lgroups = [],
+    hasOneNonBlankGroup = false,
+    slicesShown = {},
+    lgroupi = 0;
+
+  var i, j;
+
+  function addOneItem(legendGroup, legendItem) {
+    // each '' legend group is treated as a separate group
+    if (legendGroup === "" || !helpers.isGrouped(opts)) {
+      var uniqueGroup = "~~i" + lgroupi;
+
+      // TODO: check this against fullData legendgroups?
+      lgroups.push(uniqueGroup);
+      lgroupToTraces[uniqueGroup] = [[legendItem]];
+      lgroupi++;
+    } else if (lgroups.indexOf(legendGroup) === -1) {
+      lgroups.push(legendGroup);
+      hasOneNonBlankGroup = true;
+      lgroupToTraces[legendGroup] = [[legendItem]];
+    } else {
+      lgroupToTraces[legendGroup].push([legendItem]);
     }
+  }
 
-    // build an { legendgroup: [cd0, cd0], ... } object
-    for(i = 0; i < calcdata.length; i++) {
-        var cd = calcdata[i],
-            cd0 = cd[0],
-            trace = cd0.trace,
-            lgroup = trace.legendgroup;
+  // build an { legendgroup: [cd0, cd0], ... } object
+  for (i = 0; i < calcdata.length; i++) {
+    var cd = calcdata[i],
+      cd0 = cd[0],
+      trace = cd0.trace,
+      lgroup = trace.legendgroup;
 
-        if(!helpers.legendGetsTrace(trace) || !trace.showlegend) continue;
+    if (!helpers.legendGetsTrace(trace) || !trace.showlegend) continue;
 
-        if(Registry.traceIs(trace, 'pie')) {
-            if(!slicesShown[lgroup]) slicesShown[lgroup] = {};
+    if (Registry.traceIs(trace, "pie")) {
+      if (!slicesShown[lgroup]) slicesShown[lgroup] = {};
 
-            for(j = 0; j < cd.length; j++) {
-                var labelj = cd[j].label;
+      for (j = 0; j < cd.length; j++) {
+        var labelj = cd[j].label;
 
-                if(!slicesShown[lgroup][labelj]) {
-                    addOneItem(lgroup, {
-                        label: labelj,
-                        color: cd[j].color,
-                        i: cd[j].i,
-                        trace: trace
-                    });
+        if (!slicesShown[lgroup][labelj]) {
+          addOneItem(lgroup, {
+            label: labelj,
+            color: cd[j].color,
+            i: cd[j].i,
+            trace: trace
+          });
 
-                    slicesShown[lgroup][labelj] = true;
-                }
-            }
+          slicesShown[lgroup][labelj] = true;
         }
-
-        else addOneItem(lgroup, cd0);
+      }
+    } else {
+      addOneItem(lgroup, cd0);
     }
+  }
 
-    // won't draw a legend in this case
-    if(!lgroups.length) return [];
+  // won't draw a legend in this case
+  if (!lgroups.length) return [];
 
-    // rearrange lgroupToTraces into a d3-friendly array of arrays
-    var lgroupsLength = lgroups.length,
-        ltraces,
-        legendData;
+  // rearrange lgroupToTraces into a d3-friendly array of arrays
+  var lgroupsLength = lgroups.length, ltraces, legendData;
 
-    if(hasOneNonBlankGroup && helpers.isGrouped(opts)) {
-        legendData = new Array(lgroupsLength);
+  if (hasOneNonBlankGroup && helpers.isGrouped(opts)) {
+    legendData = new Array(lgroupsLength);
 
-        for(i = 0; i < lgroupsLength; i++) {
-            ltraces = lgroupToTraces[lgroups[i]];
-            legendData[i] = helpers.isReversed(opts) ? ltraces.reverse() : ltraces;
-        }
+    for (i = 0; i < lgroupsLength; i++) {
+      ltraces = lgroupToTraces[lgroups[i]];
+      legendData[i] = helpers.isReversed(opts) ? ltraces.reverse() : ltraces;
     }
-    else {
-        // collapse all groups into one if all groups are blank
-        legendData = [new Array(lgroupsLength)];
-
-        for(i = 0; i < lgroupsLength; i++) {
-            ltraces = lgroupToTraces[lgroups[i]][0];
-            legendData[0][helpers.isReversed(opts) ? lgroupsLength - i - 1 : i] = ltraces;
-        }
-        lgroupsLength = 1;
+  } else {
+    // collapse all groups into one if all groups are blank
+    legendData = [new Array(lgroupsLength)];
+
+    for (i = 0; i < lgroupsLength; i++) {
+      ltraces = lgroupToTraces[lgroups[i]][0];
+      legendData[0][
+        helpers.isReversed(opts) ? lgroupsLength - i - 1 : i
+      ] = ltraces;
     }
+    lgroupsLength = 1;
+  }
 
-    // needed in repositionLegend
-    opts._lgroupsLength = lgroupsLength;
-    return legendData;
+  // needed in repositionLegend
+  opts._lgroupsLength = lgroupsLength;
+  return legendData;
 };
diff --git a/src/components/legend/helpers.js b/src/components/legend/helpers.js
index 140fa6d7f5f..4117b548124 100644
--- a/src/components/legend/helpers.js
+++ b/src/components/legend/helpers.js
@@ -5,25 +5,21 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var Registry = require('../../registry');
-
+"use strict";
+var Registry = require("../../registry");
 
 exports.legendGetsTrace = function legendGetsTrace(trace) {
-    return trace.visible && Registry.traceIs(trace, 'showLegend');
+  return trace.visible && Registry.traceIs(trace, "showLegend");
 };
 
 exports.isGrouped = function isGrouped(legendLayout) {
-    return (legendLayout.traceorder || '').indexOf('grouped') !== -1;
+  return (legendLayout.traceorder || "").indexOf("grouped") !== -1;
 };
 
 exports.isVertical = function isVertical(legendLayout) {
-    return legendLayout.orientation !== 'h';
+  return legendLayout.orientation !== "h";
 };
 
 exports.isReversed = function isReversed(legendLayout) {
-    return (legendLayout.traceorder || '').indexOf('reversed') !== -1;
+  return (legendLayout.traceorder || "").indexOf("reversed") !== -1;
 };
diff --git a/src/components/legend/index.js b/src/components/legend/index.js
index 71e45a0b723..600db0132b0 100644
--- a/src/components/legend/index.js
+++ b/src/components/legend/index.js
@@ -5,18 +5,12 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-    moduleType: 'component',
-    name: 'legend',
-
-    layoutAttributes: require('./attributes'),
-    supplyLayoutDefaults: require('./defaults'),
-
-    draw: require('./draw'),
-    style: require('./style')
+  moduleType: "component",
+  name: "legend",
+  layoutAttributes: require("./attributes"),
+  supplyLayoutDefaults: require("./defaults"),
+  draw: require("./draw"),
+  style: require("./style")
 };
diff --git a/src/components/legend/style.js b/src/components/legend/style.js
index c3c2101e62e..1ebc5b9e271 100644
--- a/src/components/legend/style.js
+++ b/src/components/legend/style.js
@@ -5,53 +5,41 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
 
+var Registry = require("../../registry");
+var Lib = require("../../lib");
+var Drawing = require("../drawing");
+var Color = require("../color");
 
-'use strict';
+var subTypes = require("../../traces/scatter/subtypes");
+var stylePie = require("../../traces/pie/style_one");
 
-var d3 = require('d3');
+module.exports = function style(s) {
+  s
+    .each(function(d) {
+      var traceGroup = d3.select(this);
 
-var Registry = require('../../registry');
-var Lib = require('../../lib');
-var Drawing = require('../drawing');
-var Color = require('../color');
+      var layers = traceGroup.selectAll("g.layers").data([0]);
+      layers.enter().append("g").classed("layers", true);
+      layers.style("opacity", d[0].trace.opacity);
 
-var subTypes = require('../../traces/scatter/subtypes');
-var stylePie = require('../../traces/pie/style_one');
+      var fill = layers.selectAll("g.legendfill").data([d]);
+      fill.enter().append("g").classed("legendfill", true);
 
+      var line = layers.selectAll("g.legendlines").data([d]);
+      line.enter().append("g").classed("legendlines", true);
 
-module.exports = function style(s) {
-    s.each(function(d) {
-        var traceGroup = d3.select(this);
-
-        var layers = traceGroup.selectAll('g.layers')
-            .data([0]);
-        layers.enter().append('g')
-            .classed('layers', true);
-        layers.style('opacity', d[0].trace.opacity);
-
-        var fill = layers
-            .selectAll('g.legendfill')
-                .data([d]);
-        fill.enter().append('g')
-            .classed('legendfill', true);
-
-        var line = layers
-            .selectAll('g.legendlines')
-                .data([d]);
-        line.enter().append('g')
-            .classed('legendlines', true);
-
-        var symbol = layers
-            .selectAll('g.legendsymbols')
-                .data([d]);
-        symbol.enter().append('g')
-            .classed('legendsymbols', true);
-
-        symbol.selectAll('g.legendpoints')
-            .data([d])
-          .enter().append('g')
-            .classed('legendpoints', true);
+      var symbol = layers.selectAll("g.legendsymbols").data([d]);
+      symbol.enter().append("g").classed("legendsymbols", true);
+
+      symbol
+        .selectAll("g.legendpoints")
+        .data([d])
+        .enter()
+        .append("g")
+        .classed("legendpoints", true);
     })
     .each(styleBars)
     .each(styleBoxes)
@@ -61,166 +49,180 @@ module.exports = function style(s) {
 };
 
 function styleLines(d) {
-    var trace = d[0].trace,
-        showFill = trace.visible && trace.fill && trace.fill !== 'none',
-        showLine = subTypes.hasLines(trace);
-
-    var fill = d3.select(this).select('.legendfill').selectAll('path')
-        .data(showFill ? [d] : []);
-    fill.enter().append('path').classed('js-fill', true);
-    fill.exit().remove();
-    fill.attr('d', 'M5,0h30v6h-30z')
-        .call(Drawing.fillGroupStyle);
-
-    var line = d3.select(this).select('.legendlines').selectAll('path')
-        .data(showLine ? [d] : []);
-    line.enter().append('path').classed('js-line', true)
-        .attr('d', 'M5,0h30');
-    line.exit().remove();
-    line.call(Drawing.lineGroupStyle);
+  var trace = d[0].trace,
+    showFill = trace.visible && trace.fill && trace.fill !== "none",
+    showLine = subTypes.hasLines(trace);
+
+  var fill = d3
+    .select(this)
+    .select(".legendfill")
+    .selectAll("path")
+    .data(showFill ? [d] : []);
+  fill.enter().append("path").classed("js-fill", true);
+  fill.exit().remove();
+  fill.attr("d", "M5,0h30v6h-30z").call(Drawing.fillGroupStyle);
+
+  var line = d3
+    .select(this)
+    .select(".legendlines")
+    .selectAll("path")
+    .data(showLine ? [d] : []);
+  line.enter().append("path").classed("js-line", true).attr("d", "M5,0h30");
+  line.exit().remove();
+  line.call(Drawing.lineGroupStyle);
 }
 
 function stylePoints(d) {
-    var d0 = d[0],
-        trace = d0.trace,
-        showMarkers = subTypes.hasMarkers(trace),
-        showText = subTypes.hasText(trace),
-        showLines = subTypes.hasLines(trace);
-
-    var dMod, tMod;
-
-    // 'scatter3d' and 'scattergeo' don't use gd.calcdata yet;
-    // use d0.trace to infer arrayOk attributes
-
-    function boundVal(attrIn, arrayToValFn, bounds) {
-        var valIn = Lib.nestedProperty(trace, attrIn).get(),
-            valToBound = (Array.isArray(valIn) && arrayToValFn) ?
-                arrayToValFn(valIn) : valIn;
-
-        if(bounds) {
-            if(valToBound < bounds[0]) return bounds[0];
-            else if(valToBound > bounds[1]) return bounds[1];
-        }
-        return valToBound;
+  var d0 = d[0],
+    trace = d0.trace,
+    showMarkers = subTypes.hasMarkers(trace),
+    showText = subTypes.hasText(trace),
+    showLines = subTypes.hasLines(trace);
+
+  var dMod, tMod;
+
+  // 'scatter3d' and 'scattergeo' don't use gd.calcdata yet;
+  // use d0.trace to infer arrayOk attributes
+  function boundVal(attrIn, arrayToValFn, bounds) {
+    var valIn = Lib.nestedProperty(trace, attrIn).get(),
+      valToBound = Array.isArray(valIn) && arrayToValFn
+        ? arrayToValFn(valIn)
+        : valIn;
+
+    if (bounds) {
+      if (valToBound < bounds[0]) return bounds[0];
+      else if (valToBound > bounds[1]) return bounds[1];
+    }
+    return valToBound;
+  }
+
+  function pickFirst(array) {
+    return array[0];
+  }
+
+  // constrain text, markers, etc so they'll fit on the legend
+  if (showMarkers || showText || showLines) {
+    var dEdit = {}, tEdit = {};
+
+    if (showMarkers) {
+      dEdit.mc = boundVal("marker.color", pickFirst);
+      dEdit.mo = boundVal("marker.opacity", Lib.mean, [0.2, 1]);
+      dEdit.ms = boundVal("marker.size", Lib.mean, [2, 16]);
+      dEdit.mlc = boundVal("marker.line.color", pickFirst);
+      dEdit.mlw = boundVal("marker.line.width", Lib.mean, [0, 5]);
+      tEdit.marker = { sizeref: 1, sizemin: 1, sizemode: "diameter" };
     }
 
-    function pickFirst(array) { return array[0]; }
-
-    // constrain text, markers, etc so they'll fit on the legend
-    if(showMarkers || showText || showLines) {
-        var dEdit = {},
-            tEdit = {};
-
-        if(showMarkers) {
-            dEdit.mc = boundVal('marker.color', pickFirst);
-            dEdit.mo = boundVal('marker.opacity', Lib.mean, [0.2, 1]);
-            dEdit.ms = boundVal('marker.size', Lib.mean, [2, 16]);
-            dEdit.mlc = boundVal('marker.line.color', pickFirst);
-            dEdit.mlw = boundVal('marker.line.width', Lib.mean, [0, 5]);
-            tEdit.marker = {
-                sizeref: 1,
-                sizemin: 1,
-                sizemode: 'diameter'
-            };
-        }
-
-        if(showLines) {
-            tEdit.line = {
-                width: boundVal('line.width', pickFirst, [0, 10])
-            };
-        }
-
-        if(showText) {
-            dEdit.tx = 'Aa';
-            dEdit.tp = boundVal('textposition', pickFirst);
-            dEdit.ts = 10;
-            dEdit.tc = boundVal('textfont.color', pickFirst);
-            dEdit.tf = boundVal('textfont.family', pickFirst);
-        }
-
-        dMod = [Lib.minExtend(d0, dEdit)];
-        tMod = Lib.minExtend(trace, tEdit);
+    if (showLines) {
+      tEdit.line = { width: boundVal("line.width", pickFirst, [0, 10]) };
     }
 
-    var ptgroup = d3.select(this).select('g.legendpoints');
-
-    var pts = ptgroup.selectAll('path.scatterpts')
-        .data(showMarkers ? dMod : []);
-    pts.enter().append('path').classed('scatterpts', true)
-        .attr('transform', 'translate(20,0)');
-    pts.exit().remove();
-    pts.call(Drawing.pointStyle, tMod);
-
-    // 'mrc' is set in pointStyle and used in textPointStyle:
-    // constrain it here
-    if(showMarkers) dMod[0].mrc = 3;
-
-    var txt = ptgroup.selectAll('g.pointtext')
-        .data(showText ? dMod : []);
-    txt.enter()
-        .append('g').classed('pointtext', true)
-            .append('text').attr('transform', 'translate(20,0)');
-    txt.exit().remove();
-    txt.selectAll('text').call(Drawing.textPointStyle, tMod);
+    if (showText) {
+      dEdit.tx = "Aa";
+      dEdit.tp = boundVal("textposition", pickFirst);
+      dEdit.ts = 10;
+      dEdit.tc = boundVal("textfont.color", pickFirst);
+      dEdit.tf = boundVal("textfont.family", pickFirst);
+    }
+
+    dMod = [Lib.minExtend(d0, dEdit)];
+    tMod = Lib.minExtend(trace, tEdit);
+  }
+
+  var ptgroup = d3.select(this).select("g.legendpoints");
+
+  var pts = ptgroup.selectAll("path.scatterpts").data(showMarkers ? dMod : []);
+  pts
+    .enter()
+    .append("path")
+    .classed("scatterpts", true)
+    .attr("transform", "translate(20,0)");
+  pts.exit().remove();
+  pts.call(Drawing.pointStyle, tMod);
+
+  // 'mrc' is set in pointStyle and used in textPointStyle:
+  // constrain it here
+  if (showMarkers) dMod[0].mrc = 3;
+
+  var txt = ptgroup.selectAll("g.pointtext").data(showText ? dMod : []);
+  txt
+    .enter()
+    .append("g")
+    .classed("pointtext", true)
+    .append("text")
+    .attr("transform", "translate(20,0)");
+  txt.exit().remove();
+  txt.selectAll("text").call(Drawing.textPointStyle, tMod);
 }
 
 function styleBars(d) {
-    var trace = d[0].trace,
-        marker = trace.marker || {},
-        markerLine = marker.line || {},
-        barpath = d3.select(this).select('g.legendpoints')
-            .selectAll('path.legendbar')
-            .data(Registry.traceIs(trace, 'bar') ? [d] : []);
-    barpath.enter().append('path').classed('legendbar', true)
-        .attr('d', 'M6,6H-6V-6H6Z')
-        .attr('transform', 'translate(20,0)');
-    barpath.exit().remove();
-    barpath.each(function(d) {
-        var p = d3.select(this),
-            d0 = d[0],
-            w = (d0.mlw + 1 || markerLine.width + 1) - 1;
-
-        p.style('stroke-width', w + 'px')
-            .call(Color.fill, d0.mc || marker.color);
-
-        if(w) {
-            p.call(Color.stroke, d0.mlc || markerLine.color);
-        }
-    });
+  var trace = d[0].trace,
+    marker = trace.marker || {},
+    markerLine = marker.line || {},
+    barpath = d3
+      .select(this)
+      .select("g.legendpoints")
+      .selectAll("path.legendbar")
+      .data(Registry.traceIs(trace, "bar") ? [d] : []);
+  barpath
+    .enter()
+    .append("path")
+    .classed("legendbar", true)
+    .attr("d", "M6,6H-6V-6H6Z")
+    .attr("transform", "translate(20,0)");
+  barpath.exit().remove();
+  barpath.each(function(d) {
+    var p = d3.select(this),
+      d0 = d[0],
+      w = (d0.mlw + 1 || markerLine.width + 1) - 1;
+
+    p.style("stroke-width", w + "px").call(Color.fill, d0.mc || marker.color);
+
+    if (w) {
+      p.call(Color.stroke, d0.mlc || markerLine.color);
+    }
+  });
 }
 
 function styleBoxes(d) {
-    var trace = d[0].trace,
-        pts = d3.select(this).select('g.legendpoints')
-            .selectAll('path.legendbox')
-            .data(Registry.traceIs(trace, 'box') && trace.visible ? [d] : []);
-    pts.enter().append('path').classed('legendbox', true)
-        // if we want the median bar, prepend M6,0H-6
-        .attr('d', 'M6,6H-6V-6H6Z')
-        .attr('transform', 'translate(20,0)');
-    pts.exit().remove();
-    pts.each(function() {
-        var w = trace.line.width,
-            p = d3.select(this);
-
-        p.style('stroke-width', w + 'px')
-            .call(Color.fill, trace.fillcolor);
-
-        if(w) {
-            p.call(Color.stroke, trace.line.color);
-        }
-    });
+  var trace = d[0].trace,
+    pts = d3
+      .select(this)
+      .select("g.legendpoints")
+      .selectAll("path.legendbox")
+      .data(Registry.traceIs(trace, "box") && trace.visible ? [d] : []);
+  pts
+    .enter()
+    .append("path")
+    .classed("legendbox", true)
+    .attr("d", "M6,6H-6V-6H6Z")
+    .attr("transform", "translate(20,0)");
+  pts.exit().remove();
+  pts.each(function() {
+    var w = trace.line.width, p = d3.select(this);
+
+    p.style("stroke-width", w + "px").call(Color.fill, trace.fillcolor);
+
+    if (w) {
+      p.call(Color.stroke, trace.line.color);
+    }
+  });
 }
 
 function stylePies(d) {
-    var trace = d[0].trace,
-        pts = d3.select(this).select('g.legendpoints')
-            .selectAll('path.legendpie')
-            .data(Registry.traceIs(trace, 'pie') && trace.visible ? [d] : []);
-    pts.enter().append('path').classed('legendpie', true)
-        .attr('d', 'M6,6H-6V-6H6Z')
-        .attr('transform', 'translate(20,0)');
-    pts.exit().remove();
-
-    if(pts.size()) pts.call(stylePie, d[0], trace);
+  var trace = d[0].trace,
+    pts = d3
+      .select(this)
+      .select("g.legendpoints")
+      .selectAll("path.legendpie")
+      .data(Registry.traceIs(trace, "pie") && trace.visible ? [d] : []);
+  pts
+    .enter()
+    .append("path")
+    .classed("legendpie", true)
+    .attr("d", "M6,6H-6V-6H6Z")
+    .attr("transform", "translate(20,0)");
+  pts.exit().remove();
+
+  if (pts.size()) pts.call(stylePie, d[0], trace);
 }
diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js
index aae0503e368..694e4b98b56 100644
--- a/src/components/modebar/buttons.js
+++ b/src/components/modebar/buttons.js
@@ -5,17 +5,13 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var Plotly = require('../../plotly');
-var Plots = require('../../plots/plots');
-var Axes = require('../../plots/cartesian/axes');
-var Lib = require('../../lib');
-var downloadImage = require('../../snapshot/download');
-var Icons = require('../../../build/ploticon');
-
+"use strict";
+var Plotly = require("../../plotly");
+var Plots = require("../../plots/plots");
+var Axes = require("../../plots/cartesian/axes");
+var Lib = require("../../lib");
+var downloadImage = require("../../snapshot/download");
+var Icons = require("../../../build/ploticon");
 
 var modeBarButtons = module.exports = {};
 
@@ -44,477 +40,466 @@ var modeBarButtons = module.exports = {};
  * @param {boolean} [toggle]
  *      is the button a toggle button?
  */
-
 modeBarButtons.toImage = {
-    name: 'toImage',
-    title: 'Download plot as a png',
-    icon: Icons.camera,
-    click: function(gd) {
-        var format = 'png';
-
-        Lib.notifier('Taking snapshot - this may take a few seconds', 'long');
+  name: "toImage",
+  title: "Download plot as a png",
+  icon: Icons.camera,
+  click: function(gd) {
+    var format = "png";
 
-        if(Lib.isIE()) {
-            Lib.notifier('IE only supports svg.  Changing format to svg.', 'long');
-            format = 'svg';
-        }
+    Lib.notifier("Taking snapshot - this may take a few seconds", "long");
 
-        downloadImage(gd, {'format': format})
-          .then(function(filename) {
-              Lib.notifier('Snapshot succeeded - ' + filename, 'long');
-          })
-          .catch(function() {
-              Lib.notifier('Sorry there was a problem downloading your snapshot!', 'long');
-          });
+    if (Lib.isIE()) {
+      Lib.notifier("IE only supports svg.  Changing format to svg.", "long");
+      format = "svg";
     }
+
+    downloadImage(gd, { format: format })
+      .then(function(filename) {
+        Lib.notifier("Snapshot succeeded - " + filename, "long");
+      })
+      .catch(function() {
+        Lib.notifier(
+          "Sorry there was a problem downloading your snapshot!",
+          "long"
+        );
+      });
+  }
 };
 
 modeBarButtons.sendDataToCloud = {
-    name: 'sendDataToCloud',
-    title: 'Save and edit plot in cloud',
-    icon: Icons.disk,
-    click: function(gd) {
-        Plots.sendDataToCloud(gd);
-    }
+  name: "sendDataToCloud",
+  title: "Save and edit plot in cloud",
+  icon: Icons.disk,
+  click: function(gd) {
+    Plots.sendDataToCloud(gd);
+  }
 };
 
 modeBarButtons.zoom2d = {
-    name: 'zoom2d',
-    title: 'Zoom',
-    attr: 'dragmode',
-    val: 'zoom',
-    icon: Icons.zoombox,
-    click: handleCartesian
+  name: "zoom2d",
+  title: "Zoom",
+  attr: "dragmode",
+  val: "zoom",
+  icon: Icons.zoombox,
+  click: handleCartesian
 };
 
 modeBarButtons.pan2d = {
-    name: 'pan2d',
-    title: 'Pan',
-    attr: 'dragmode',
-    val: 'pan',
-    icon: Icons.pan,
-    click: handleCartesian
+  name: "pan2d",
+  title: "Pan",
+  attr: "dragmode",
+  val: "pan",
+  icon: Icons.pan,
+  click: handleCartesian
 };
 
 modeBarButtons.select2d = {
-    name: 'select2d',
-    title: 'Box Select',
-    attr: 'dragmode',
-    val: 'select',
-    icon: Icons.selectbox,
-    click: handleCartesian
+  name: "select2d",
+  title: "Box Select",
+  attr: "dragmode",
+  val: "select",
+  icon: Icons.selectbox,
+  click: handleCartesian
 };
 
 modeBarButtons.lasso2d = {
-    name: 'lasso2d',
-    title: 'Lasso Select',
-    attr: 'dragmode',
-    val: 'lasso',
-    icon: Icons.lasso,
-    click: handleCartesian
+  name: "lasso2d",
+  title: "Lasso Select",
+  attr: "dragmode",
+  val: "lasso",
+  icon: Icons.lasso,
+  click: handleCartesian
 };
 
 modeBarButtons.zoomIn2d = {
-    name: 'zoomIn2d',
-    title: 'Zoom in',
-    attr: 'zoom',
-    val: 'in',
-    icon: Icons.zoom_plus,
-    click: handleCartesian
+  name: "zoomIn2d",
+  title: "Zoom in",
+  attr: "zoom",
+  val: "in",
+  icon: Icons.zoom_plus,
+  click: handleCartesian
 };
 
 modeBarButtons.zoomOut2d = {
-    name: 'zoomOut2d',
-    title: 'Zoom out',
-    attr: 'zoom',
-    val: 'out',
-    icon: Icons.zoom_minus,
-    click: handleCartesian
+  name: "zoomOut2d",
+  title: "Zoom out",
+  attr: "zoom",
+  val: "out",
+  icon: Icons.zoom_minus,
+  click: handleCartesian
 };
 
 modeBarButtons.autoScale2d = {
-    name: 'autoScale2d',
-    title: 'Autoscale',
-    attr: 'zoom',
-    val: 'auto',
-    icon: Icons.autoscale,
-    click: handleCartesian
+  name: "autoScale2d",
+  title: "Autoscale",
+  attr: "zoom",
+  val: "auto",
+  icon: Icons.autoscale,
+  click: handleCartesian
 };
 
 modeBarButtons.resetScale2d = {
-    name: 'resetScale2d',
-    title: 'Reset axes',
-    attr: 'zoom',
-    val: 'reset',
-    icon: Icons.home,
-    click: handleCartesian
+  name: "resetScale2d",
+  title: "Reset axes",
+  attr: "zoom",
+  val: "reset",
+  icon: Icons.home,
+  click: handleCartesian
 };
 
 modeBarButtons.hoverClosestCartesian = {
-    name: 'hoverClosestCartesian',
-    title: 'Show closest data on hover',
-    attr: 'hovermode',
-    val: 'closest',
-    icon: Icons.tooltip_basic,
-    gravity: 'ne',
-    click: handleCartesian
+  name: "hoverClosestCartesian",
+  title: "Show closest data on hover",
+  attr: "hovermode",
+  val: "closest",
+  icon: Icons.tooltip_basic,
+  gravity: "ne",
+  click: handleCartesian
 };
 
 modeBarButtons.hoverCompareCartesian = {
-    name: 'hoverCompareCartesian',
-    title: 'Compare data on hover',
-    attr: 'hovermode',
-    val: function(gd) {
-        return gd._fullLayout._isHoriz ? 'y' : 'x';
-    },
-    icon: Icons.tooltip_compare,
-    gravity: 'ne',
-    click: handleCartesian
+  name: "hoverCompareCartesian",
+  title: "Compare data on hover",
+  attr: "hovermode",
+  val: function(gd) {
+    return gd._fullLayout._isHoriz ? "y" : "x";
+  },
+  icon: Icons.tooltip_compare,
+  gravity: "ne",
+  click: handleCartesian
 };
 
 function handleCartesian(gd, ev) {
-    var button = ev.currentTarget,
-        astr = button.getAttribute('data-attr'),
-        val = button.getAttribute('data-val') || true,
-        fullLayout = gd._fullLayout,
-        aobj = {};
-
-    if(astr === 'zoom') {
-        var mag = (val === 'in') ? 0.5 : 2,
-            r0 = (1 + mag) / 2,
-            r1 = (1 - mag) / 2,
-            axList = Axes.list(gd, null, true);
-
-        var ax, axName;
-
-        for(var i = 0; i < axList.length; i++) {
-            ax = axList[i];
-
-            if(!ax.fixedrange) {
-                axName = ax._name;
-                if(val === 'auto') aobj[axName + '.autorange'] = true;
-                else if(val === 'reset') {
-                    if(ax._rangeInitial === undefined) {
-                        aobj[axName + '.autorange'] = true;
-                    }
-                    else {
-                        var rangeInitial = ax._rangeInitial.slice();
-                        aobj[axName + '.range[0]'] = rangeInitial[0];
-                        aobj[axName + '.range[1]'] = rangeInitial[1];
-                    }
-                }
-                else {
-                    var rangeNow = [
-                        ax.r2l(ax.range[0]),
-                        ax.r2l(ax.range[1]),
-                    ];
-
-                    var rangeNew = [
-                        r0 * rangeNow[0] + r1 * rangeNow[1],
-                        r0 * rangeNow[1] + r1 * rangeNow[0]
-                    ];
-
-                    aobj[axName + '.range[0]'] = ax.l2r(rangeNew[0]);
-                    aobj[axName + '.range[1]'] = ax.l2r(rangeNew[1]);
-                }
-            }
+  var button = ev.currentTarget,
+    astr = button.getAttribute("data-attr"),
+    val = button.getAttribute("data-val") || true,
+    fullLayout = gd._fullLayout,
+    aobj = {};
+
+  if (astr === "zoom") {
+    var mag = val === "in" ? 0.5 : 2,
+      r0 = (1 + mag) / 2,
+      r1 = (1 - mag) / 2,
+      axList = Axes.list(gd, null, true);
+
+    var ax, axName;
+
+    for (var i = 0; i < axList.length; i++) {
+      ax = axList[i];
+
+      if (!ax.fixedrange) {
+        axName = ax._name;
+        if (val === "auto") {
+          aobj[axName + ".autorange"] = true;
+        } else if (val === "reset") {
+          if (ax._rangeInitial === undefined) {
+            aobj[axName + ".autorange"] = true;
+          } else {
+            var rangeInitial = ax._rangeInitial.slice();
+            aobj[axName + ".range[0]"] = rangeInitial[0];
+            aobj[axName + ".range[1]"] = rangeInitial[1];
+          }
+        } else {
+          var rangeNow = [ax.r2l(ax.range[0]), ax.r2l(ax.range[1])];
+
+          var rangeNew = [
+            r0 * rangeNow[0] + r1 * rangeNow[1],
+            r0 * rangeNow[1] + r1 * rangeNow[0]
+          ];
+
+          aobj[axName + ".range[0]"] = ax.l2r(rangeNew[0]);
+          aobj[axName + ".range[1]"] = ax.l2r(rangeNew[1]);
         }
+      }
     }
-    else {
-        // if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y'
-        if(astr === 'hovermode' && (val === 'x' || val === 'y')) {
-            val = fullLayout._isHoriz ? 'y' : 'x';
-            button.setAttribute('data-val', val);
-        }
-
-        aobj[astr] = val;
+  } else {
+    // if ALL traces have orientation 'h', 'hovermode': 'x' otherwise: 'y'
+    if (astr === "hovermode" && (val === "x" || val === "y")) {
+      val = fullLayout._isHoriz ? "y" : "x";
+      button.setAttribute("data-val", val);
     }
 
-    Plotly.relayout(gd, aobj);
+    aobj[astr] = val;
+  }
+
+  Plotly.relayout(gd, aobj);
 }
 
 modeBarButtons.zoom3d = {
-    name: 'zoom3d',
-    title: 'Zoom',
-    attr: 'scene.dragmode',
-    val: 'zoom',
-    icon: Icons.zoombox,
-    click: handleDrag3d
+  name: "zoom3d",
+  title: "Zoom",
+  attr: "scene.dragmode",
+  val: "zoom",
+  icon: Icons.zoombox,
+  click: handleDrag3d
 };
 
 modeBarButtons.pan3d = {
-    name: 'pan3d',
-    title: 'Pan',
-    attr: 'scene.dragmode',
-    val: 'pan',
-    icon: Icons.pan,
-    click: handleDrag3d
+  name: "pan3d",
+  title: "Pan",
+  attr: "scene.dragmode",
+  val: "pan",
+  icon: Icons.pan,
+  click: handleDrag3d
 };
 
 modeBarButtons.orbitRotation = {
-    name: 'orbitRotation',
-    title: 'orbital rotation',
-    attr: 'scene.dragmode',
-    val: 'orbit',
-    icon: Icons['3d_rotate'],
-    click: handleDrag3d
+  name: "orbitRotation",
+  title: "orbital rotation",
+  attr: "scene.dragmode",
+  val: "orbit",
+  icon: Icons["3d_rotate"],
+  click: handleDrag3d
 };
 
 modeBarButtons.tableRotation = {
-    name: 'tableRotation',
-    title: 'turntable rotation',
-    attr: 'scene.dragmode',
-    val: 'turntable',
-    icon: Icons['z-axis'],
-    click: handleDrag3d
+  name: "tableRotation",
+  title: "turntable rotation",
+  attr: "scene.dragmode",
+  val: "turntable",
+  icon: Icons["z-axis"],
+  click: handleDrag3d
 };
 
 function handleDrag3d(gd, ev) {
-    var button = ev.currentTarget,
-        attr = button.getAttribute('data-attr'),
-        val = button.getAttribute('data-val') || true,
-        fullLayout = gd._fullLayout,
-        sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'),
-        layoutUpdate = {};
+  var button = ev.currentTarget,
+    attr = button.getAttribute("data-attr"),
+    val = button.getAttribute("data-val") || true,
+    fullLayout = gd._fullLayout,
+    sceneIds = Plots.getSubplotIds(fullLayout, "gl3d"),
+    layoutUpdate = {};
 
-    var parts = attr.split('.');
+  var parts = attr.split(".");
 
-    for(var i = 0; i < sceneIds.length; i++) {
-        layoutUpdate[sceneIds[i] + '.' + parts[1]] = val;
-    }
+  for (var i = 0; i < sceneIds.length; i++) {
+    layoutUpdate[sceneIds[i] + "." + parts[1]] = val;
+  }
 
-    Plotly.relayout(gd, layoutUpdate);
+  Plotly.relayout(gd, layoutUpdate);
 }
 
 modeBarButtons.resetCameraDefault3d = {
-    name: 'resetCameraDefault3d',
-    title: 'Reset camera to default',
-    attr: 'resetDefault',
-    icon: Icons.home,
-    click: handleCamera3d
+  name: "resetCameraDefault3d",
+  title: "Reset camera to default",
+  attr: "resetDefault",
+  icon: Icons.home,
+  click: handleCamera3d
 };
 
 modeBarButtons.resetCameraLastSave3d = {
-    name: 'resetCameraLastSave3d',
-    title: 'Reset camera to last save',
-    attr: 'resetLastSave',
-    icon: Icons.movie,
-    click: handleCamera3d
+  name: "resetCameraLastSave3d",
+  title: "Reset camera to last save",
+  attr: "resetLastSave",
+  icon: Icons.movie,
+  click: handleCamera3d
 };
 
 function handleCamera3d(gd, ev) {
-    var button = ev.currentTarget,
-        attr = button.getAttribute('data-attr'),
-        fullLayout = gd._fullLayout,
-        sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'),
-        aobj = {};
-
-    for(var i = 0; i < sceneIds.length; i++) {
-        var sceneId = sceneIds[i],
-            key = sceneId + '.camera',
-            scene = fullLayout[sceneId]._scene;
-
-        if(attr === 'resetDefault') {
-            aobj[key] = null;
-        }
-        else if(attr === 'resetLastSave') {
-            aobj[key] = Lib.extendDeep({}, scene.cameraInitial);
-        }
+  var button = ev.currentTarget,
+    attr = button.getAttribute("data-attr"),
+    fullLayout = gd._fullLayout,
+    sceneIds = Plots.getSubplotIds(fullLayout, "gl3d"),
+    aobj = {};
+
+  for (var i = 0; i < sceneIds.length; i++) {
+    var sceneId = sceneIds[i],
+      key = sceneId + ".camera",
+      scene = fullLayout[sceneId]._scene;
+
+    if (attr === "resetDefault") {
+      aobj[key] = null;
+    } else if (attr === "resetLastSave") {
+      aobj[key] = Lib.extendDeep({}, scene.cameraInitial);
     }
+  }
 
-    Plotly.relayout(gd, aobj);
+  Plotly.relayout(gd, aobj);
 }
 
 modeBarButtons.hoverClosest3d = {
-    name: 'hoverClosest3d',
-    title: 'Toggle show closest data on hover',
-    attr: 'hovermode',
-    val: null,
-    toggle: true,
-    icon: Icons.tooltip_basic,
-    gravity: 'ne',
-    click: handleHover3d
+  name: "hoverClosest3d",
+  title: "Toggle show closest data on hover",
+  attr: "hovermode",
+  val: null,
+  toggle: true,
+  icon: Icons.tooltip_basic,
+  gravity: "ne",
+  click: handleHover3d
 };
 
 function handleHover3d(gd, ev) {
-    var button = ev.currentTarget,
-        val = button._previousVal || false,
-        layout = gd.layout,
-        fullLayout = gd._fullLayout,
-        sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d');
-
-    var axes = ['xaxis', 'yaxis', 'zaxis'],
-        spikeAttrs = ['showspikes', 'spikesides', 'spikethickness', 'spikecolor'];
-
-    // initialize 'current spike' object to be stored in the DOM
-    var currentSpikes = {},
-        axisSpikes = {},
-        layoutUpdate = {};
-
-    if(val) {
-        layoutUpdate = Lib.extendDeep(layout, val);
-        button._previousVal = null;
-    }
-    else {
-        layoutUpdate = {
-            'allaxes.showspikes': false
-        };
-
-        for(var i = 0; i < sceneIds.length; i++) {
-            var sceneId = sceneIds[i],
-                sceneLayout = fullLayout[sceneId],
-                sceneSpikes = currentSpikes[sceneId] = {};
-
-            sceneSpikes.hovermode = sceneLayout.hovermode;
-            layoutUpdate[sceneId + '.hovermode'] = false;
-
-            // copy all the current spike attrs
-            for(var j = 0; j < 3; j++) {
-                var axis = axes[j];
-                axisSpikes = sceneSpikes[axis] = {};
-
-                for(var k = 0; k < spikeAttrs.length; k++) {
-                    var spikeAttr = spikeAttrs[k];
-                    axisSpikes[spikeAttr] = sceneLayout[axis][spikeAttr];
-                }
-            }
+  var button = ev.currentTarget,
+    val = button._previousVal || false,
+    layout = gd.layout,
+    fullLayout = gd._fullLayout,
+    sceneIds = Plots.getSubplotIds(fullLayout, "gl3d");
+
+  var axes = ["xaxis", "yaxis", "zaxis"],
+    spikeAttrs = ["showspikes", "spikesides", "spikethickness", "spikecolor"];
+
+  // initialize 'current spike' object to be stored in the DOM
+  var currentSpikes = {}, axisSpikes = {}, layoutUpdate = {};
+
+  if (val) {
+    layoutUpdate = Lib.extendDeep(layout, val);
+    button._previousVal = null;
+  } else {
+    layoutUpdate = { "allaxes.showspikes": false };
+
+    for (var i = 0; i < sceneIds.length; i++) {
+      var sceneId = sceneIds[i],
+        sceneLayout = fullLayout[sceneId],
+        sceneSpikes = currentSpikes[sceneId] = {};
+
+      sceneSpikes.hovermode = sceneLayout.hovermode;
+      layoutUpdate[sceneId + ".hovermode"] = false;
+
+      // copy all the current spike attrs
+      for (var j = 0; j < 3; j++) {
+        var axis = axes[j];
+        axisSpikes = sceneSpikes[axis] = {};
+
+        for (var k = 0; k < spikeAttrs.length; k++) {
+          var spikeAttr = spikeAttrs[k];
+          axisSpikes[spikeAttr] = sceneLayout[axis][spikeAttr];
         }
-
-        button._previousVal = Lib.extendDeep({}, currentSpikes);
+      }
     }
 
-    Plotly.relayout(gd, layoutUpdate);
+    button._previousVal = Lib.extendDeep({}, currentSpikes);
+  }
+
+  Plotly.relayout(gd, layoutUpdate);
 }
 
 modeBarButtons.zoomInGeo = {
-    name: 'zoomInGeo',
-    title: 'Zoom in',
-    attr: 'zoom',
-    val: 'in',
-    icon: Icons.zoom_plus,
-    click: handleGeo
+  name: "zoomInGeo",
+  title: "Zoom in",
+  attr: "zoom",
+  val: "in",
+  icon: Icons.zoom_plus,
+  click: handleGeo
 };
 
 modeBarButtons.zoomOutGeo = {
-    name: 'zoomOutGeo',
-    title: 'Zoom out',
-    attr: 'zoom',
-    val: 'out',
-    icon: Icons.zoom_minus,
-    click: handleGeo
+  name: "zoomOutGeo",
+  title: "Zoom out",
+  attr: "zoom",
+  val: "out",
+  icon: Icons.zoom_minus,
+  click: handleGeo
 };
 
 modeBarButtons.resetGeo = {
-    name: 'resetGeo',
-    title: 'Reset',
-    attr: 'reset',
-    val: null,
-    icon: Icons.autoscale,
-    click: handleGeo
+  name: "resetGeo",
+  title: "Reset",
+  attr: "reset",
+  val: null,
+  icon: Icons.autoscale,
+  click: handleGeo
 };
 
 modeBarButtons.hoverClosestGeo = {
-    name: 'hoverClosestGeo',
-    title: 'Toggle show closest data on hover',
-    attr: 'hovermode',
-    val: null,
-    toggle: true,
-    icon: Icons.tooltip_basic,
-    gravity: 'ne',
-    click: toggleHover
+  name: "hoverClosestGeo",
+  title: "Toggle show closest data on hover",
+  attr: "hovermode",
+  val: null,
+  toggle: true,
+  icon: Icons.tooltip_basic,
+  gravity: "ne",
+  click: toggleHover
 };
 
 function handleGeo(gd, ev) {
-    var button = ev.currentTarget,
-        attr = button.getAttribute('data-attr'),
-        val = button.getAttribute('data-val') || true,
-        fullLayout = gd._fullLayout,
-        geoIds = Plots.getSubplotIds(fullLayout, 'geo');
-
-    for(var i = 0; i < geoIds.length; i++) {
-        var geo = fullLayout[geoIds[i]]._subplot;
-
-        if(attr === 'zoom') {
-            var scale = geo.projection.scale();
-            var newScale = (val === 'in') ? 2 * scale : 0.5 * scale;
-            geo.projection.scale(newScale);
-            geo.zoom.scale(newScale);
-            geo.render();
-        }
-        else if(attr === 'reset') geo.zoomReset();
-    }
+  var button = ev.currentTarget,
+    attr = button.getAttribute("data-attr"),
+    val = button.getAttribute("data-val") || true,
+    fullLayout = gd._fullLayout,
+    geoIds = Plots.getSubplotIds(fullLayout, "geo");
+
+  for (var i = 0; i < geoIds.length; i++) {
+    var geo = fullLayout[geoIds[i]]._subplot;
+
+    if (attr === "zoom") {
+      var scale = geo.projection.scale();
+      var newScale = val === "in" ? 2 * scale : 0.5 * scale;
+      geo.projection.scale(newScale);
+      geo.zoom.scale(newScale);
+      geo.render();
+    } else if (attr === "reset") geo.zoomReset();
+  }
 }
 
 modeBarButtons.hoverClosestGl2d = {
-    name: 'hoverClosestGl2d',
-    title: 'Toggle show closest data on hover',
-    attr: 'hovermode',
-    val: null,
-    toggle: true,
-    icon: Icons.tooltip_basic,
-    gravity: 'ne',
-    click: toggleHover
+  name: "hoverClosestGl2d",
+  title: "Toggle show closest data on hover",
+  attr: "hovermode",
+  val: null,
+  toggle: true,
+  icon: Icons.tooltip_basic,
+  gravity: "ne",
+  click: toggleHover
 };
 
 modeBarButtons.hoverClosestPie = {
-    name: 'hoverClosestPie',
-    title: 'Toggle show closest data on hover',
-    attr: 'hovermode',
-    val: 'closest',
-    icon: Icons.tooltip_basic,
-    gravity: 'ne',
-    click: toggleHover
+  name: "hoverClosestPie",
+  title: "Toggle show closest data on hover",
+  attr: "hovermode",
+  val: "closest",
+  icon: Icons.tooltip_basic,
+  gravity: "ne",
+  click: toggleHover
 };
 
 function toggleHover(gd) {
-    var fullLayout = gd._fullLayout;
+  var fullLayout = gd._fullLayout;
 
-    var onHoverVal;
-    if(fullLayout._has('cartesian')) {
-        onHoverVal = fullLayout._isHoriz ? 'y' : 'x';
-    }
-    else onHoverVal = 'closest';
+  var onHoverVal;
+  if (fullLayout._has("cartesian")) {
+    onHoverVal = fullLayout._isHoriz ? "y" : "x";
+  } else {
+    onHoverVal = "closest";
+  }
 
-    var newHover = gd._fullLayout.hovermode ? false : onHoverVal;
+  var newHover = gd._fullLayout.hovermode ? false : onHoverVal;
 
-    Plotly.relayout(gd, 'hovermode', newHover);
+  Plotly.relayout(gd, "hovermode", newHover);
 }
 
 // buttons when more then one plot types are present
-
 modeBarButtons.toggleHover = {
-    name: 'toggleHover',
-    title: 'Toggle show closest data on hover',
-    attr: 'hovermode',
-    val: null,
-    toggle: true,
-    icon: Icons.tooltip_basic,
-    gravity: 'ne',
-    click: function(gd, ev) {
-        toggleHover(gd);
-
-        // the 3d hovermode update must come
-        // last so that layout.hovermode update does not
-        // override scene?.hovermode?.layout.
-        handleHover3d(gd, ev);
-    }
+  name: "toggleHover",
+  title: "Toggle show closest data on hover",
+  attr: "hovermode",
+  val: null,
+  toggle: true,
+  icon: Icons.tooltip_basic,
+  gravity: "ne",
+  click: function(gd, ev) {
+    toggleHover(gd);
+
+    // the 3d hovermode update must come
+    // last so that layout.hovermode update does not
+    // override scene?.hovermode?.layout.
+    handleHover3d(gd, ev);
+  }
 };
 
 modeBarButtons.resetViews = {
-    name: 'resetViews',
-    title: 'Reset views',
-    icon: Icons.home,
-    click: function(gd, ev) {
-        var button = ev.currentTarget;
-
-        button.setAttribute('data-attr', 'zoom');
-        button.setAttribute('data-val', 'reset');
-        handleCartesian(gd, ev);
-
-        button.setAttribute('data-attr', 'resetLastSave');
-        handleCamera3d(gd, ev);
-
-        // N.B handleCamera3d also triggers a replot for
-        // geo subplots.
-    }
+  name: "resetViews",
+  title: "Reset views",
+  icon: Icons.home,
+  click: function(gd, ev) {
+    var button = ev.currentTarget;
+
+    button.setAttribute("data-attr", "zoom");
+    button.setAttribute("data-val", "reset");
+    handleCartesian(gd, ev);
+
+    button.setAttribute("data-attr", "resetLastSave");
+    handleCamera3d(gd, ev);
+    // N.B handleCamera3d also triggers a replot for
+    // geo subplots.
+  }
 };
diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js
index 787fa706d4b..bf310fa1aeb 100644
--- a/src/components/modebar/index.js
+++ b/src/components/modebar/index.js
@@ -5,8 +5,5 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-exports.manage = require('./manage');
+"use strict";
+exports.manage = require("./manage");
diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js
index 57e5e2d17b3..e1d7473c8dc 100644
--- a/src/components/modebar/manage.js
+++ b/src/components/modebar/manage.js
@@ -5,15 +5,12 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Axes = require("../../plots/cartesian/axes");
+var scatterSubTypes = require("../../traces/scatter/subtypes");
 
-
-'use strict';
-
-var Axes = require('../../plots/cartesian/axes');
-var scatterSubTypes = require('../../traces/scatter/subtypes');
-
-var createModeBar = require('./modebar');
-var modeBarButtons = require('./buttons');
+var createModeBar = require("./modebar");
+var modeBarButtons = require("./buttons");
 
 /**
  * ModeBar wrapper around 'create' and 'update',
@@ -24,203 +21,205 @@ var modeBarButtons = require('./buttons');
  *
  */
 module.exports = function manageModeBar(gd) {
-    var fullLayout = gd._fullLayout,
-        context = gd._context,
-        modeBar = fullLayout._modeBar;
-
-    if(!context.displayModeBar) {
-        if(modeBar) {
-            modeBar.destroy();
-            delete fullLayout._modeBar;
-        }
-        return;
-    }
-
-    if(!Array.isArray(context.modeBarButtonsToRemove)) {
-        throw new Error([
-            '*modeBarButtonsToRemove* configuration options',
-            'must be an array.'
-        ].join(' '));
-    }
-
-    if(!Array.isArray(context.modeBarButtonsToAdd)) {
-        throw new Error([
-            '*modeBarButtonsToAdd* configuration options',
-            'must be an array.'
-        ].join(' '));
-    }
-
-    var customButtons = context.modeBarButtons;
-    var buttonGroups;
-
-    if(Array.isArray(customButtons) && customButtons.length) {
-        buttonGroups = fillCustomButton(customButtons);
-    }
-    else {
-        buttonGroups = getButtonGroups(
-            gd,
-            context.modeBarButtonsToRemove,
-            context.modeBarButtonsToAdd
-        );
-    }
-
-    if(modeBar) modeBar.update(gd, buttonGroups);
-    else fullLayout._modeBar = createModeBar(gd, buttonGroups);
+  var fullLayout = gd._fullLayout,
+    context = gd._context,
+    modeBar = fullLayout._modeBar;
+
+  if (!context.displayModeBar) {
+    if (modeBar) {
+      modeBar.destroy();
+      delete fullLayout._modeBar;
+    }
+    return;
+  }
+
+  if (!Array.isArray(context.modeBarButtonsToRemove)) {
+    throw new Error(
+      [
+        "*modeBarButtonsToRemove* configuration options",
+        "must be an array."
+      ].join(" ")
+    );
+  }
+
+  if (!Array.isArray(context.modeBarButtonsToAdd)) {
+    throw new Error(
+      ["*modeBarButtonsToAdd* configuration options", "must be an array."].join(
+        " "
+      )
+    );
+  }
+
+  var customButtons = context.modeBarButtons;
+  var buttonGroups;
+
+  if (Array.isArray(customButtons) && customButtons.length) {
+    buttonGroups = fillCustomButton(customButtons);
+  } else {
+    buttonGroups = getButtonGroups(
+      gd,
+      context.modeBarButtonsToRemove,
+      context.modeBarButtonsToAdd
+    );
+  }
+
+  if (modeBar) modeBar.update(gd, buttonGroups);
+  else fullLayout._modeBar = createModeBar(gd, buttonGroups);
 };
 
 // logic behind which buttons are displayed by default
 function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) {
-    var fullLayout = gd._fullLayout,
-        fullData = gd._fullData;
+  var fullLayout = gd._fullLayout, fullData = gd._fullData;
 
-    var hasCartesian = fullLayout._has('cartesian'),
-        hasGL3D = fullLayout._has('gl3d'),
-        hasGeo = fullLayout._has('geo'),
-        hasPie = fullLayout._has('pie'),
-        hasGL2D = fullLayout._has('gl2d'),
-        hasTernary = fullLayout._has('ternary');
+  var hasCartesian = fullLayout._has("cartesian"),
+    hasGL3D = fullLayout._has("gl3d"),
+    hasGeo = fullLayout._has("geo"),
+    hasPie = fullLayout._has("pie"),
+    hasGL2D = fullLayout._has("gl2d"),
+    hasTernary = fullLayout._has("ternary");
 
-    var groups = [];
+  var groups = [];
 
-    function addGroup(newGroup) {
-        var out = [];
+  function addGroup(newGroup) {
+    var out = [];
 
-        for(var i = 0; i < newGroup.length; i++) {
-            var button = newGroup[i];
-            if(buttonsToRemove.indexOf(button) !== -1) continue;
-            out.push(modeBarButtons[button]);
-        }
-
-        groups.push(out);
+    for (var i = 0; i < newGroup.length; i++) {
+      var button = newGroup[i];
+      if (buttonsToRemove.indexOf(button) !== -1) continue;
+      out.push(modeBarButtons[button]);
     }
 
-    // buttons common to all plot types
-    addGroup(['toImage', 'sendDataToCloud']);
+    groups.push(out);
+  }
 
-    // graphs with more than one plot types get 'union buttons'
-    // which reset the view or toggle hover labels across all subplots.
-    if((hasCartesian || hasGL2D || hasPie || hasTernary) + hasGeo + hasGL3D > 1) {
-        addGroup(['resetViews', 'toggleHover']);
-        return appendButtonsToGroups(groups, buttonsToAdd);
-    }
-
-    if(hasGL3D) {
-        addGroup(['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']);
-        addGroup(['resetCameraDefault3d', 'resetCameraLastSave3d']);
-        addGroup(['hoverClosest3d']);
-    }
-
-    if(hasGeo) {
-        addGroup(['zoomInGeo', 'zoomOutGeo', 'resetGeo']);
-        addGroup(['hoverClosestGeo']);
-    }
-
-    var allAxesFixed = areAllAxesFixed(fullLayout),
-        dragModeGroup = [];
-
-    if(((hasCartesian || hasGL2D) && !allAxesFixed) || hasTernary) {
-        dragModeGroup = ['zoom2d', 'pan2d'];
-    }
-    if((hasCartesian || hasTernary) && isSelectable(fullData)) {
-        dragModeGroup.push('select2d');
-        dragModeGroup.push('lasso2d');
-    }
-    if(dragModeGroup.length) addGroup(dragModeGroup);
-
-    if((hasCartesian || hasGL2D) && !allAxesFixed && !hasTernary) {
-        addGroup(['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']);
-    }
-
-    if(hasCartesian && hasPie) {
-        addGroup(['toggleHover']);
-    }
-    else if(hasGL2D) {
-        addGroup(['hoverClosestGl2d']);
-    }
-    else if(hasCartesian) {
-        addGroup(['hoverClosestCartesian', 'hoverCompareCartesian']);
-    }
-    else if(hasPie) {
-        addGroup(['hoverClosestPie']);
-    }
+  // buttons common to all plot types
+  addGroup(["toImage", "sendDataToCloud"]);
 
+  // graphs with more than one plot types get 'union buttons'
+  // which reset the view or toggle hover labels across all subplots.
+  if (
+    (hasCartesian || hasGL2D || hasPie || hasTernary) + hasGeo + hasGL3D > 1
+  ) {
+    addGroup(["resetViews", "toggleHover"]);
     return appendButtonsToGroups(groups, buttonsToAdd);
+  }
+
+  if (hasGL3D) {
+    addGroup(["zoom3d", "pan3d", "orbitRotation", "tableRotation"]);
+    addGroup(["resetCameraDefault3d", "resetCameraLastSave3d"]);
+    addGroup(["hoverClosest3d"]);
+  }
+
+  if (hasGeo) {
+    addGroup(["zoomInGeo", "zoomOutGeo", "resetGeo"]);
+    addGroup(["hoverClosestGeo"]);
+  }
+
+  var allAxesFixed = areAllAxesFixed(fullLayout), dragModeGroup = [];
+
+  if ((hasCartesian || hasGL2D) && !allAxesFixed || hasTernary) {
+    dragModeGroup = ["zoom2d", "pan2d"];
+  }
+  if ((hasCartesian || hasTernary) && isSelectable(fullData)) {
+    dragModeGroup.push("select2d");
+    dragModeGroup.push("lasso2d");
+  }
+  if (dragModeGroup.length) addGroup(dragModeGroup);
+
+  if ((hasCartesian || hasGL2D) && !allAxesFixed && !hasTernary) {
+    addGroup(["zoomIn2d", "zoomOut2d", "autoScale2d", "resetScale2d"]);
+  }
+
+  if (hasCartesian && hasPie) {
+    addGroup(["toggleHover"]);
+  } else if (hasGL2D) {
+    addGroup(["hoverClosestGl2d"]);
+  } else if (hasCartesian) {
+    addGroup(["hoverClosestCartesian", "hoverCompareCartesian"]);
+  } else if (hasPie) {
+    addGroup(["hoverClosestPie"]);
+  }
+
+  return appendButtonsToGroups(groups, buttonsToAdd);
 }
 
 function areAllAxesFixed(fullLayout) {
-    var axList = Axes.list({_fullLayout: fullLayout}, null, true);
-    var allFixed = true;
+  var axList = Axes.list({ _fullLayout: fullLayout }, null, true);
+  var allFixed = true;
 
-    for(var i = 0; i < axList.length; i++) {
-        if(!axList[i].fixedrange) {
-            allFixed = false;
-            break;
-        }
+  for (var i = 0; i < axList.length; i++) {
+    if (!axList[i].fixedrange) {
+      allFixed = false;
+      break;
     }
+  }
 
-    return allFixed;
+  return allFixed;
 }
 
 // look for traces that support selection
 // to be updated as we add more selectPoints handlers
 function isSelectable(fullData) {
-    var selectable = false;
+  var selectable = false;
 
-    for(var i = 0; i < fullData.length; i++) {
-        if(selectable) break;
+  for (var i = 0; i < fullData.length; i++) {
+    if (selectable) break;
 
-        var trace = fullData[i];
+    var trace = fullData[i];
 
-        if(!trace._module || !trace._module.selectPoints) continue;
+    if (!trace._module || !trace._module.selectPoints) continue;
 
-        if(trace.type === 'scatter' || trace.type === 'scatterternary') {
-            if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) {
-                selectable = true;
-            }
-        }
-        // assume that in general if the trace module has selectPoints,
-        // then it's selectable. Scatter is an exception to this because it must
-        // have markers or text, not just be a scatter type.
-        else selectable = true;
+    if (trace.type === "scatter" || trace.type === "scatterternary") {
+      if (scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) {
+        selectable = true;
+      }
+    } else {
+      // assume that in general if the trace module has selectPoints,
+      // then it's selectable. Scatter is an exception to this because it must
+      // have markers or text, not just be a scatter type.
+      selectable = true;
     }
+  }
 
-    return selectable;
+  return selectable;
 }
 
 function appendButtonsToGroups(groups, buttons) {
-    if(buttons.length) {
-        if(Array.isArray(buttons[0])) {
-            for(var i = 0; i < buttons.length; i++) {
-                groups.push(buttons[i]);
-            }
-        }
-        else groups.push(buttons);
+  if (buttons.length) {
+    if (Array.isArray(buttons[0])) {
+      for (var i = 0; i < buttons.length; i++) {
+        groups.push(buttons[i]);
+      }
+    } else {
+      groups.push(buttons);
     }
+  }
 
-    return groups;
+  return groups;
 }
 
 // fill in custom buttons referring to default mode bar buttons
 function fillCustomButton(customButtons) {
-    for(var i = 0; i < customButtons.length; i++) {
-        var buttonGroup = customButtons[i];
-
-        for(var j = 0; j < buttonGroup.length; j++) {
-            var button = buttonGroup[j];
-
-            if(typeof button === 'string') {
-                if(modeBarButtons[button] !== undefined) {
-                    customButtons[i][j] = modeBarButtons[button];
-                }
-                else {
-                    throw new Error([
-                        '*modeBarButtons* configuration options',
-                        'invalid button name'
-                    ].join(' '));
-                }
-            }
+  for (var i = 0; i < customButtons.length; i++) {
+    var buttonGroup = customButtons[i];
+
+    for (var j = 0; j < buttonGroup.length; j++) {
+      var button = buttonGroup[j];
+
+      if (typeof button === "string") {
+        if (modeBarButtons[button] !== undefined) {
+          customButtons[i][j] = modeBarButtons[button];
+        } else {
+          throw new Error(
+            [
+              "*modeBarButtons* configuration options",
+              "invalid button name"
+            ].join(" ")
+          );
         }
+      }
     }
+  }
 
-    return customButtons;
+  return customButtons;
 }
diff --git a/src/components/modebar/modebar.js b/src/components/modebar/modebar.js
index 054a8a91928..25dfcf8448c 100644
--- a/src/components/modebar/modebar.js
+++ b/src/components/modebar/modebar.js
@@ -5,15 +5,11 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
 
-
-'use strict';
-
-var d3 = require('d3');
-
-var Lib = require('../../lib');
-var Icons = require('../../../build/ploticon');
-
+var Lib = require("../../lib");
+var Icons = require("../../../build/ploticon");
 
 /**
  * UI controller for interactive plots
@@ -24,12 +20,12 @@ var Icons = require('../../../build/ploticon');
  * @Param {object} opts.graphInfo  primary plot object containing data and layout
  */
 function ModeBar(opts) {
-    this.container = opts.container;
-    this.element = document.createElement('div');
+  this.container = opts.container;
+  this.element = document.createElement("div");
 
-    this.update(opts.graphInfo, opts.buttons);
+  this.update(opts.graphInfo, opts.buttons);
 
-    this.container.appendChild(this.element);
+  this.container.appendChild(this.element);
 }
 
 var proto = ModeBar.prototype;
@@ -42,60 +38,61 @@ var proto = ModeBar.prototype;
  *
  */
 proto.update = function(graphInfo, buttons) {
-    this.graphInfo = graphInfo;
+  this.graphInfo = graphInfo;
 
-    var context = this.graphInfo._context;
+  var context = this.graphInfo._context;
 
-    if(context.displayModeBar === 'hover') {
-        this.element.className = 'modebar modebar--hover';
-    }
-    else this.element.className = 'modebar';
+  if (context.displayModeBar === "hover") {
+    this.element.className = "modebar modebar--hover";
+  } else {
+    this.element.className = "modebar";
+  }
 
-    // if buttons or logo have changed, redraw modebar interior
-    var needsNewButtons = !this.hasButtons(buttons),
-        needsNewLogo = (this.hasLogo !== context.displaylogo);
+  // if buttons or logo have changed, redraw modebar interior
+  var needsNewButtons = !this.hasButtons(buttons),
+    needsNewLogo = this.hasLogo !== context.displaylogo;
 
-    if(needsNewButtons || needsNewLogo) {
-        this.removeAllButtons();
+  if (needsNewButtons || needsNewLogo) {
+    this.removeAllButtons();
 
-        this.updateButtons(buttons);
+    this.updateButtons(buttons);
 
-        if(context.displaylogo) {
-            this.element.appendChild(this.getLogo());
-            this.hasLogo = true;
-        }
+    if (context.displaylogo) {
+      this.element.appendChild(this.getLogo());
+      this.hasLogo = true;
     }
+  }
 
-    this.updateActiveButton();
+  this.updateActiveButton();
 };
 
 proto.updateButtons = function(buttons) {
-    var _this = this;
-
-    this.buttons = buttons;
-    this.buttonElements = [];
-    this.buttonsNames = [];
-
-    this.buttons.forEach(function(buttonGroup) {
-        var group = _this.createGroup();
-
-        buttonGroup.forEach(function(buttonConfig) {
-            var buttonName = buttonConfig.name;
-            if(!buttonName) {
-                throw new Error('must provide button \'name\' in button config');
-            }
-            if(_this.buttonsNames.indexOf(buttonName) !== -1) {
-                throw new Error('button name \'' + buttonName + '\' is taken');
-            }
-            _this.buttonsNames.push(buttonName);
-
-            var button = _this.createButton(buttonConfig);
-            _this.buttonElements.push(button);
-            group.appendChild(button);
-        });
-
-        _this.element.appendChild(group);
+  var _this = this;
+
+  this.buttons = buttons;
+  this.buttonElements = [];
+  this.buttonsNames = [];
+
+  this.buttons.forEach(function(buttonGroup) {
+    var group = _this.createGroup();
+
+    buttonGroup.forEach(function(buttonConfig) {
+      var buttonName = buttonConfig.name;
+      if (!buttonName) {
+        throw new Error("must provide button 'name' in button config");
+      }
+      if (_this.buttonsNames.indexOf(buttonName) !== -1) {
+        throw new Error("button name '" + buttonName + "' is taken");
+      }
+      _this.buttonsNames.push(buttonName);
+
+      var button = _this.createButton(buttonConfig);
+      _this.buttonElements.push(button);
+      group.appendChild(button);
     });
+
+    _this.element.appendChild(group);
+  });
 };
 
 /**
@@ -103,10 +100,10 @@ proto.updateButtons = function(buttons) {
  * @Return {HTMLelement}
  */
 proto.createGroup = function() {
-    var group = document.createElement('div');
-    group.className = 'modebar-group';
+  var group = document.createElement("div");
+  group.className = "modebar-group";
 
-    return group;
+  return group;
 };
 
 /**
@@ -115,44 +112,42 @@ proto.createGroup = function() {
  * @Return {HTMLelement}
  */
 proto.createButton = function(config) {
-    var _this = this,
-        button = document.createElement('a');
+  var _this = this, button = document.createElement("a");
 
-    button.setAttribute('rel', 'tooltip');
-    button.className = 'modebar-btn';
+  button.setAttribute("rel", "tooltip");
+  button.className = "modebar-btn";
 
-    var title = config.title;
-    if(title === undefined) title = config.name;
-    if(title || title === 0) button.setAttribute('data-title', title);
+  var title = config.title;
+  if (title === undefined) title = config.name;
+  if (title || title === 0) button.setAttribute("data-title", title);
 
-    if(config.attr !== undefined) button.setAttribute('data-attr', config.attr);
+  if (config.attr !== undefined) button.setAttribute("data-attr", config.attr);
 
-    var val = config.val;
-    if(val !== undefined) {
-        if(typeof val === 'function') val = val(this.graphInfo);
-        button.setAttribute('data-val', val);
-    }
+  var val = config.val;
+  if (val !== undefined) {
+    if (typeof val === "function") val = val(this.graphInfo);
+    button.setAttribute("data-val", val);
+  }
 
-    var click = config.click;
-    if(typeof click !== 'function') {
-        throw new Error('must provide button \'click\' function in button config');
-    }
-    else {
-        button.addEventListener('click', function(ev) {
-            config.click(_this.graphInfo, ev);
+  var click = config.click;
+  if (typeof click !== "function") {
+    throw new Error("must provide button 'click' function in button config");
+  } else {
+    button.addEventListener("click", function(ev) {
+      config.click(_this.graphInfo, ev);
 
-            // only needed for 'hoverClosestGeo' which does not call relayout
-            _this.updateActiveButton(ev.currentTarget);
-        });
-    }
+      // only needed for 'hoverClosestGeo' which does not call relayout
+      _this.updateActiveButton(ev.currentTarget);
+    });
+  }
 
-    button.setAttribute('data-toggle', config.toggle || false);
-    if(config.toggle) d3.select(button).classed('active', true);
+  button.setAttribute("data-toggle", config.toggle || false);
+  if (config.toggle) d3.select(button).classed("active", true);
 
-    button.appendChild(this.createIcon(config.icon || Icons.question));
-    button.setAttribute('data-gravity', config.gravity || 'n');
+  button.appendChild(this.createIcon(config.icon || Icons.question));
+  button.setAttribute("data-gravity", config.gravity || "n");
 
-    return button;
+  return button;
 };
 
 /**
@@ -163,20 +158,20 @@ proto.createButton = function(config) {
  * @Return {HTMLelement}
  */
 proto.createIcon = function(thisIcon) {
-    var iconHeight = thisIcon.ascent - thisIcon.descent,
-        svgNS = 'http://www.w3.org/2000/svg',
-        icon = document.createElementNS(svgNS, 'svg'),
-        path = document.createElementNS(svgNS, 'path');
+  var iconHeight = thisIcon.ascent - thisIcon.descent,
+    svgNS = "http://www.w3.org/2000/svg",
+    icon = document.createElementNS(svgNS, "svg"),
+    path = document.createElementNS(svgNS, "path");
 
-    icon.setAttribute('height', '1em');
-    icon.setAttribute('width', (thisIcon.width / iconHeight) + 'em');
-    icon.setAttribute('viewBox', [0, 0, thisIcon.width, iconHeight].join(' '));
+  icon.setAttribute("height", "1em");
+  icon.setAttribute("width", thisIcon.width / iconHeight + "em");
+  icon.setAttribute("viewBox", [0, 0, thisIcon.width, iconHeight].join(" "));
 
-    path.setAttribute('d', thisIcon.path);
-    path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')');
-    icon.appendChild(path);
+  path.setAttribute("d", thisIcon.path);
+  path.setAttribute("transform", "matrix(1 0 0 -1 0 " + thisIcon.ascent + ")");
+  icon.appendChild(path);
 
-    return icon;
+  return icon;
 };
 
 /**
@@ -185,33 +180,31 @@ proto.createIcon = function(thisIcon) {
  * @Return {HTMLelement}
  */
 proto.updateActiveButton = function(buttonClicked) {
-    var fullLayout = this.graphInfo._fullLayout,
-        dataAttrClicked = (buttonClicked !== undefined) ?
-            buttonClicked.getAttribute('data-attr') :
-            null;
-
-    this.buttonElements.forEach(function(button) {
-        var thisval = button.getAttribute('data-val') || true,
-            dataAttr = button.getAttribute('data-attr'),
-            isToggleButton = (button.getAttribute('data-toggle') === 'true'),
-            button3 = d3.select(button);
-
-        // Use 'data-toggle' and 'buttonClicked' to toggle buttons
-        // that have no one-to-one equivalent in fullLayout
-        if(isToggleButton) {
-            if(dataAttr === dataAttrClicked) {
-                button3.classed('active', !button3.classed('active'));
-            }
-        }
-        else {
-            var val = (dataAttr === null) ?
-                dataAttr :
-                Lib.nestedProperty(fullLayout, dataAttr).get();
-
-            button3.classed('active', val === thisval);
-        }
-
-    });
+  var fullLayout = this.graphInfo._fullLayout,
+    dataAttrClicked = buttonClicked !== undefined
+      ? buttonClicked.getAttribute("data-attr")
+      : null;
+
+  this.buttonElements.forEach(function(button) {
+    var thisval = button.getAttribute("data-val") || true,
+      dataAttr = button.getAttribute("data-attr"),
+      isToggleButton = button.getAttribute("data-toggle") === "true",
+      button3 = d3.select(button);
+
+    // Use 'data-toggle' and 'buttonClicked' to toggle buttons
+    // that have no one-to-one equivalent in fullLayout
+    if (isToggleButton) {
+      if (dataAttr === dataAttrClicked) {
+        button3.classed("active", !button3.classed("active"));
+      }
+    } else {
+      var val = dataAttr === null
+        ? dataAttr
+        : Lib.nestedProperty(fullLayout, dataAttr).get();
+
+      button3.classed("active", val === thisval);
+    }
+  });
 };
 
 /**
@@ -221,68 +214,69 @@ proto.updateActiveButton = function(buttonClicked) {
  * @Return {boolean}
  */
 proto.hasButtons = function(buttons) {
-    var currentButtons = this.buttons;
+  var currentButtons = this.buttons;
 
-    if(!currentButtons) return false;
+  if (!currentButtons) return false;
 
-    if(buttons.length !== currentButtons.length) return false;
+  if (buttons.length !== currentButtons.length) return false;
 
-    for(var i = 0; i < buttons.length; ++i) {
-        if(buttons[i].length !== currentButtons[i].length) return false;
-        for(var j = 0; j < buttons[i].length; j++) {
-            if(buttons[i][j].name !== currentButtons[i][j].name) return false;
-        }
+  for (var i = 0; i < buttons.length; ++i) {
+    if (buttons[i].length !== currentButtons[i].length) return false;
+    for (var j = 0; j < buttons[i].length; j++) {
+      if (buttons[i][j].name !== currentButtons[i][j].name) return false;
     }
+  }
 
-    return true;
+  return true;
 };
 
 /**
  * @return {HTMLDivElement} The logo image wrapped in a group
  */
 proto.getLogo = function() {
-    var group = this.createGroup(),
-        a = document.createElement('a');
+  var group = this.createGroup(), a = document.createElement("a");
 
-    a.href = 'https://plot.ly/';
-    a.target = '_blank';
-    a.setAttribute('data-title', 'Produced with Plotly');
-    a.className = 'modebar-btn plotlyjsicon modebar-btn--logo';
+  a.href = "https://plot.ly/";
+  a.target = "_blank";
+  a.setAttribute("data-title", "Produced with Plotly");
+  a.className = "modebar-btn plotlyjsicon modebar-btn--logo";
 
-    a.appendChild(this.createIcon(Icons.plotlylogo));
+  a.appendChild(this.createIcon(Icons.plotlylogo));
 
-    group.appendChild(a);
-    return group;
+  group.appendChild(a);
+  return group;
 };
 
 proto.removeAllButtons = function() {
-    while(this.element.firstChild) {
-        this.element.removeChild(this.element.firstChild);
-    }
+  while (this.element.firstChild) {
+    this.element.removeChild(this.element.firstChild);
+  }
 
-    this.hasLogo = false;
+  this.hasLogo = false;
 };
 
 proto.destroy = function() {
-    Lib.removeElement(this.container.querySelector('.modebar'));
+  Lib.removeElement(this.container.querySelector(".modebar"));
 };
 
 function createModeBar(gd, buttons) {
-    var fullLayout = gd._fullLayout;
-
-    var modeBar = new ModeBar({
-        graphInfo: gd,
-        container: fullLayout._paperdiv.node(),
-        buttons: buttons
-    });
-
-    if(fullLayout._privateplot) {
-        d3.select(modeBar.element).append('span')
-            .classed('badge-private float--left', true)
-            .text('PRIVATE');
-    }
-
-    return modeBar;
+  var fullLayout = gd._fullLayout;
+
+  var modeBar = new ModeBar({
+    graphInfo: gd,
+    container: fullLayout._paperdiv.node(),
+    buttons: buttons
+  });
+
+  if (fullLayout._privateplot) {
+    d3
+      .select(modeBar.element)
+      .append("span")
+      .classed("badge-private float--left", true)
+      .text("PRIVATE");
+  }
+
+  return modeBar;
 }
 
 module.exports = createModeBar;
diff --git a/src/components/rangeselector/attributes.js b/src/components/rangeselector/attributes.js
index 390afe3e200..449c6b3cec3 100644
--- a/src/components/rangeselector/attributes.js
+++ b/src/components/rangeselector/attributes.js
@@ -5,99 +5,92 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var fontAttrs = require('../../plots/font_attributes');
-var colorAttrs = require('../color/attributes');
-var extendFlat = require('../../lib/extend').extendFlat;
-var buttonAttrs = require('./button_attributes');
+"use strict";
+var fontAttrs = require("../../plots/font_attributes");
+var colorAttrs = require("../color/attributes");
+var extendFlat = require("../../lib/extend").extendFlat;
+var buttonAttrs = require("./button_attributes");
 
 buttonAttrs = extendFlat(buttonAttrs, {
-    _isLinkedToArray: 'button',
-
-    description: [
-        'Sets the specifications for each buttons.',
-        'By default, a range selector comes with no buttons.'
-    ].join(' ')
+  _isLinkedToArray: "button",
+  description: [
+    "Sets the specifications for each buttons.",
+    "By default, a range selector comes with no buttons."
+  ].join(" ")
 });
 
 module.exports = {
-    visible: {
-        valType: 'boolean',
-        role: 'info',
-        description: [
-            'Determines whether or not this range selector is visible.',
-            'Note that range selectors are only available for x axes of',
-            '`type` set to or auto-typed to *date*.'
-        ].join(' ')
-    },
-
-    buttons: buttonAttrs,
-
-    x: {
-        valType: 'number',
-        min: -2,
-        max: 3,
-        role: 'style',
-        description: 'Sets the x position (in normalized coordinates) of the range selector.'
-    },
-    xanchor: {
-        valType: 'enumerated',
-        values: ['auto', 'left', 'center', 'right'],
-        dflt: 'left',
-        role: 'info',
-        description: [
-            'Sets the range selector\'s horizontal position anchor.',
-            'This anchor binds the `x` position to the *left*, *center*',
-            'or *right* of the range selector.'
-        ].join(' ')
-    },
-    y: {
-        valType: 'number',
-        min: -2,
-        max: 3,
-        role: 'style',
-        description: 'Sets the y position (in normalized coordinates) of the range selector.'
-    },
-    yanchor: {
-        valType: 'enumerated',
-        values: ['auto', 'top', 'middle', 'bottom'],
-        dflt: 'bottom',
-        role: 'info',
-        description: [
-            'Sets the range selector\'s vertical position anchor',
-            'This anchor binds the `y` position to the *top*, *middle*',
-            'or *bottom* of the range selector.'
-        ].join(' ')
-    },
-
-    font: extendFlat({}, fontAttrs, {
-        description: 'Sets the font of the range selector button text.'
-    }),
-
-    bgcolor: {
-        valType: 'color',
-        dflt: colorAttrs.lightLine,
-        role: 'style',
-        description: 'Sets the background color of the range selector buttons.'
-    },
-    activecolor: {
-        valType: 'color',
-        role: 'style',
-        description: 'Sets the background color of the active range selector button.'
-    },
-    bordercolor: {
-        valType: 'color',
-        dflt: colorAttrs.defaultLine,
-        role: 'style',
-        description: 'Sets the color of the border enclosing the range selector.'
-    },
-    borderwidth: {
-        valType: 'number',
-        min: 0,
-        dflt: 0,
-        role: 'style',
-        description: 'Sets the width (in px) of the border enclosing the range selector.'
-    }
+  visible: {
+    valType: "boolean",
+    role: "info",
+    description: [
+      "Determines whether or not this range selector is visible.",
+      "Note that range selectors are only available for x axes of",
+      "`type` set to or auto-typed to *date*."
+    ].join(" ")
+  },
+  buttons: buttonAttrs,
+  x: {
+    valType: "number",
+    min: -2,
+    max: 3,
+    role: "style",
+    description: "Sets the x position (in normalized coordinates) of the range selector."
+  },
+  xanchor: {
+    valType: "enumerated",
+    values: ["auto", "left", "center", "right"],
+    dflt: "left",
+    role: "info",
+    description: [
+      "Sets the range selector's horizontal position anchor.",
+      "This anchor binds the `x` position to the *left*, *center*",
+      "or *right* of the range selector."
+    ].join(" ")
+  },
+  y: {
+    valType: "number",
+    min: -2,
+    max: 3,
+    role: "style",
+    description: "Sets the y position (in normalized coordinates) of the range selector."
+  },
+  yanchor: {
+    valType: "enumerated",
+    values: ["auto", "top", "middle", "bottom"],
+    dflt: "bottom",
+    role: "info",
+    description: [
+      "Sets the range selector's vertical position anchor",
+      "This anchor binds the `y` position to the *top*, *middle*",
+      "or *bottom* of the range selector."
+    ].join(" ")
+  },
+  font: extendFlat({}, fontAttrs, {
+    description: "Sets the font of the range selector button text."
+  }),
+  bgcolor: {
+    valType: "color",
+    dflt: colorAttrs.lightLine,
+    role: "style",
+    description: "Sets the background color of the range selector buttons."
+  },
+  activecolor: {
+    valType: "color",
+    role: "style",
+    description: "Sets the background color of the active range selector button."
+  },
+  bordercolor: {
+    valType: "color",
+    dflt: colorAttrs.defaultLine,
+    role: "style",
+    description: "Sets the color of the border enclosing the range selector."
+  },
+  borderwidth: {
+    valType: "number",
+    min: 0,
+    dflt: 0,
+    role: "style",
+    description: "Sets the width (in px) of the border enclosing the range selector."
+  }
 };
diff --git a/src/components/rangeselector/button_attributes.js b/src/components/rangeselector/button_attributes.js
index 14fd193a0d4..7dc0e5b538a 100644
--- a/src/components/rangeselector/button_attributes.js
+++ b/src/components/rangeselector/button_attributes.js
@@ -5,52 +5,49 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-    step: {
-        valType: 'enumerated',
-        role: 'info',
-        values: ['month', 'year', 'day', 'hour', 'minute', 'second', 'all'],
-        dflt: 'month',
-        description: [
-            'The unit of measurement that the `count` value will set the range by.'
-        ].join(' ')
-    },
-    stepmode: {
-        valType: 'enumerated',
-        role: 'info',
-        values: ['backward', 'todate'],
-        dflt: 'backward',
-        description: [
-            'Sets the range update mode.',
-            'If *backward*, the range update shifts the start of range',
-            'back *count* times *step* milliseconds.',
-            'If *todate*, the range update shifts the start of range',
-            'back to the first timestamp from *count* times',
-            '*step* milliseconds back.',
-            'For example, with `step` set to *year* and `count` set to *1*',
-            'the range update shifts the start of the range back to',
-            'January 01 of the current year.',
-            'Month and year *todate* are currently available only',
-            'for the built-in (Gregorian) calendar.'
-        ].join(' ')
-    },
-    count: {
-        valType: 'number',
-        role: 'info',
-        min: 0,
-        dflt: 1,
-        description: [
-            'Sets the number of steps to take to update the range.',
-            'Use with `step` to specify the update interval.'
-        ].join(' ')
-    },
-    label: {
-        valType: 'string',
-        role: 'info',
-        description: 'Sets the text label to appear on the button.'
-    }
+  step: {
+    valType: "enumerated",
+    role: "info",
+    values: ["month", "year", "day", "hour", "minute", "second", "all"],
+    dflt: "month",
+    description: [
+      "The unit of measurement that the `count` value will set the range by."
+    ].join(" ")
+  },
+  stepmode: {
+    valType: "enumerated",
+    role: "info",
+    values: ["backward", "todate"],
+    dflt: "backward",
+    description: [
+      "Sets the range update mode.",
+      "If *backward*, the range update shifts the start of range",
+      "back *count* times *step* milliseconds.",
+      "If *todate*, the range update shifts the start of range",
+      "back to the first timestamp from *count* times",
+      "*step* milliseconds back.",
+      "For example, with `step` set to *year* and `count` set to *1*",
+      "the range update shifts the start of the range back to",
+      "January 01 of the current year.",
+      "Month and year *todate* are currently available only",
+      "for the built-in (Gregorian) calendar."
+    ].join(" ")
+  },
+  count: {
+    valType: "number",
+    role: "info",
+    min: 0,
+    dflt: 1,
+    description: [
+      "Sets the number of steps to take to update the range.",
+      "Use with `step` to specify the update interval."
+    ].join(" ")
+  },
+  label: {
+    valType: "string",
+    role: "info",
+    description: "Sets the text label to appear on the button."
+  }
 };
diff --git a/src/components/rangeselector/constants.js b/src/components/rangeselector/constants.js
index 202e73a1cc9..973f61c41db 100644
--- a/src/components/rangeselector/constants.js
+++ b/src/components/rangeselector/constants.js
@@ -5,23 +5,16 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-
-    // 'y' position pad above counter axis domain
-    yPad: 0.02,
-
-    // minimum button width (regardless of text size)
-    minButtonWidth: 30,
-
-    // buttons rect radii
-    rx: 3,
-    ry: 3,
-
-    // light fraction used to compute the 'activecolor' default
-    lightAmount: 25,
-    darkAmount: 10
+  // 'y' position pad above counter axis domain
+  yPad: 0.02,
+  // minimum button width (regardless of text size)
+  minButtonWidth: 30,
+  // buttons rect radii
+  rx: 3,
+  ry: 3,
+  // light fraction used to compute the 'activecolor' default
+  lightAmount: 25,
+  darkAmount: 10
 };
diff --git a/src/components/rangeselector/defaults.js b/src/components/rangeselector/defaults.js
index a2523d69621..0a350dedd88 100644
--- a/src/components/rangeselector/defaults.js
+++ b/src/components/rangeselector/defaults.js
@@ -5,93 +5,102 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var Lib = require('../../lib');
-var Color = require('../color');
-
-var attributes = require('./attributes');
-var buttonAttrs = require('./button_attributes');
-var constants = require('./constants');
-
-
-module.exports = function handleDefaults(containerIn, containerOut, layout, counterAxes, calendar) {
-    var selectorIn = containerIn.rangeselector || {},
-        selectorOut = containerOut.rangeselector = {};
-
-    function coerce(attr, dflt) {
-        return Lib.coerce(selectorIn, selectorOut, attributes, attr, dflt);
-    }
-
-    var buttons = buttonsDefaults(selectorIn, selectorOut, calendar);
-
-    var visible = coerce('visible', buttons.length > 0);
-    if(!visible) return;
-
-    var posDflt = getPosDflt(containerOut, layout, counterAxes);
-    coerce('x', posDflt[0]);
-    coerce('y', posDflt[1]);
-    Lib.noneOrAll(containerIn, containerOut, ['x', 'y']);
-
-    coerce('xanchor');
-    coerce('yanchor');
-
-    Lib.coerceFont(coerce, 'font', layout.font);
-
-    var bgColor = coerce('bgcolor');
-    coerce('activecolor', Color.contrast(bgColor, constants.lightAmount, constants.darkAmount));
-    coerce('bordercolor');
-    coerce('borderwidth');
+"use strict";
+var Lib = require("../../lib");
+var Color = require("../color");
+
+var attributes = require("./attributes");
+var buttonAttrs = require("./button_attributes");
+var constants = require("./constants");
+
+module.exports = function handleDefaults(
+  containerIn,
+  containerOut,
+  layout,
+  counterAxes,
+  calendar
+) {
+  var selectorIn = containerIn.rangeselector || {},
+    selectorOut = containerOut.rangeselector = {};
+
+  function coerce(attr, dflt) {
+    return Lib.coerce(selectorIn, selectorOut, attributes, attr, dflt);
+  }
+
+  var buttons = buttonsDefaults(selectorIn, selectorOut, calendar);
+
+  var visible = coerce("visible", buttons.length > 0);
+  if (!visible) return;
+
+  var posDflt = getPosDflt(containerOut, layout, counterAxes);
+  coerce("x", posDflt[0]);
+  coerce("y", posDflt[1]);
+  Lib.noneOrAll(containerIn, containerOut, ["x", "y"]);
+
+  coerce("xanchor");
+  coerce("yanchor");
+
+  Lib.coerceFont(coerce, "font", layout.font);
+
+  var bgColor = coerce("bgcolor");
+  coerce(
+    "activecolor",
+    Color.contrast(bgColor, constants.lightAmount, constants.darkAmount)
+  );
+  coerce("bordercolor");
+  coerce("borderwidth");
 };
 
 function buttonsDefaults(containerIn, containerOut, calendar) {
-    var buttonsIn = containerIn.buttons || [],
-        buttonsOut = containerOut.buttons = [];
-
-    var buttonIn, buttonOut;
-
-    function coerce(attr, dflt) {
-        return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt);
+  var buttonsIn = containerIn.buttons || [],
+    buttonsOut = containerOut.buttons = [];
+
+  var buttonIn, buttonOut;
+
+  function coerce(attr, dflt) {
+    return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt);
+  }
+
+  for (var i = 0; i < buttonsIn.length; i++) {
+    buttonIn = buttonsIn[i];
+    buttonOut = {};
+
+    if (!Lib.isPlainObject(buttonIn)) continue;
+
+    var step = coerce("step");
+    if (step !== "all") {
+      if (
+        calendar &&
+          calendar !== "gregorian" &&
+          (step === "month" || step === "year")
+      ) {
+        buttonOut.stepmode = "backward";
+      } else {
+        coerce("stepmode");
+      }
+
+      coerce("count");
     }
 
-    for(var i = 0; i < buttonsIn.length; i++) {
-        buttonIn = buttonsIn[i];
-        buttonOut = {};
-
-        if(!Lib.isPlainObject(buttonIn)) continue;
-
-        var step = coerce('step');
-        if(step !== 'all') {
-            if(calendar && calendar !== 'gregorian' && (step === 'month' || step === 'year')) {
-                buttonOut.stepmode = 'backward';
-            }
-            else {
-                coerce('stepmode');
-            }
+    coerce("label");
 
-            coerce('count');
-        }
+    buttonOut._index = i;
+    buttonsOut.push(buttonOut);
+  }
 
-        coerce('label');
-
-        buttonOut._index = i;
-        buttonsOut.push(buttonOut);
-    }
-
-    return buttonsOut;
+  return buttonsOut;
 }
 
 function getPosDflt(containerOut, layout, counterAxes) {
-    var anchoredList = counterAxes.filter(function(ax) {
-        return layout[ax].anchor === containerOut._id;
-    });
-
-    var posY = 0;
-    for(var i = 0; i < anchoredList.length; i++) {
-        var domain = layout[anchoredList[i]].domain;
-        if(domain) posY = Math.max(domain[1], posY);
-    }
+  var anchoredList = counterAxes.filter(function(ax) {
+    return layout[ax].anchor === containerOut._id;
+  });
+
+  var posY = 0;
+  for (var i = 0; i < anchoredList.length; i++) {
+    var domain = layout[anchoredList[i]].domain;
+    if (domain) posY = Math.max(domain[1], posY);
+  }
 
-    return [containerOut.domain[0], posY + constants.yPad];
+  return [containerOut.domain[0], posY + constants.yPad];
 }
diff --git a/src/components/rangeselector/draw.js b/src/components/rangeselector/draw.js
index 8dbc8ff4774..1879420a5bc 100644
--- a/src/components/rangeselector/draw.js
+++ b/src/components/rangeselector/draw.js
@@ -5,269 +5,248 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
 
+var Plotly = require("../../plotly");
+var Plots = require("../../plots/plots");
+var Color = require("../color");
+var Drawing = require("../drawing");
+var svgTextUtils = require("../../lib/svg_text_utils");
+var axisIds = require("../../plots/cartesian/axis_ids");
+var anchorUtils = require("../legend/anchor_utils");
 
-'use strict';
-
-var d3 = require('d3');
-
-var Plotly = require('../../plotly');
-var Plots = require('../../plots/plots');
-var Color = require('../color');
-var Drawing = require('../drawing');
-var svgTextUtils = require('../../lib/svg_text_utils');
-var axisIds = require('../../plots/cartesian/axis_ids');
-var anchorUtils = require('../legend/anchor_utils');
-
-var constants = require('./constants');
-var getUpdateObject = require('./get_update_object');
-
+var constants = require("./constants");
+var getUpdateObject = require("./get_update_object");
 
 module.exports = function draw(gd) {
-    var fullLayout = gd._fullLayout;
-
-    var selectors = fullLayout._infolayer.selectAll('.rangeselector')
-        .data(makeSelectorData(gd), selectorKeyFunc);
+  var fullLayout = gd._fullLayout;
 
-    selectors.enter().append('g')
-        .classed('rangeselector', true);
+  var selectors = fullLayout._infolayer
+    .selectAll(".rangeselector")
+    .data(makeSelectorData(gd), selectorKeyFunc);
 
-    selectors.exit().remove();
+  selectors.enter().append("g").classed("rangeselector", true);
 
-    selectors.style({
-        cursor: 'pointer',
-        'pointer-events': 'all'
-    });
-
-    selectors.each(function(d) {
-        var selector = d3.select(this),
-            axisLayout = d,
-            selectorLayout = axisLayout.rangeselector;
+  selectors.exit().remove();
 
-        var buttons = selector.selectAll('g.button')
-            .data(selectorLayout.buttons);
+  selectors.style({ cursor: "pointer", "pointer-events": "all" });
 
-        buttons.enter().append('g')
-            .classed('button', true);
+  selectors.each(function(d) {
+    var selector = d3.select(this),
+      axisLayout = d,
+      selectorLayout = axisLayout.rangeselector;
 
-        buttons.exit().remove();
+    var buttons = selector.selectAll("g.button").data(selectorLayout.buttons);
 
-        buttons.each(function(d) {
-            var button = d3.select(this);
-            var update = getUpdateObject(axisLayout, d);
+    buttons.enter().append("g").classed("button", true);
 
-            d.isActive = isActive(axisLayout, d, update);
+    buttons.exit().remove();
 
-            button.call(drawButtonRect, selectorLayout, d);
-            button.call(drawButtonText, selectorLayout, d);
+    buttons.each(function(d) {
+      var button = d3.select(this);
+      var update = getUpdateObject(axisLayout, d);
 
-            button.on('click', function() {
-                if(gd._dragged) return;
+      d.isActive = isActive(axisLayout, d, update);
 
-                Plotly.relayout(gd, update);
-            });
+      button.call(drawButtonRect, selectorLayout, d);
+      button.call(drawButtonText, selectorLayout, d);
 
-            button.on('mouseover', function() {
-                d.isHovered = true;
-                button.call(drawButtonRect, selectorLayout, d);
-            });
+      button.on("click", function() {
+        if (gd._dragged) return;
 
-            button.on('mouseout', function() {
-                d.isHovered = false;
-                button.call(drawButtonRect, selectorLayout, d);
-            });
-        });
+        Plotly.relayout(gd, update);
+      });
 
-        // N.B. this mutates selectorLayout
-        reposition(gd, buttons, selectorLayout, axisLayout._name);
+      button.on("mouseover", function() {
+        d.isHovered = true;
+        button.call(drawButtonRect, selectorLayout, d);
+      });
 
-        selector.attr('transform', 'translate(' +
-            selectorLayout.lx + ',' + selectorLayout.ly +
-        ')');
+      button.on("mouseout", function() {
+        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 + ")"
+    );
+  });
 };
 
 function makeSelectorData(gd) {
-    var axes = axisIds.list(gd, 'x', true);
-    var data = [];
+  var axes = axisIds.list(gd, "x", true);
+  var data = [];
 
-    for(var i = 0; i < axes.length; i++) {
-        var axis = axes[i];
+  for (var i = 0; i < axes.length; i++) {
+    var axis = axes[i];
 
-        if(axis.rangeselector && axis.rangeselector.visible) {
-            data.push(axis);
-        }
+    if (axis.rangeselector && axis.rangeselector.visible) {
+      data.push(axis);
     }
+  }
 
-    return data;
+  return data;
 }
 
 function selectorKeyFunc(d) {
-    return d._id;
+  return d._id;
 }
 
 function isActive(axisLayout, opts, update) {
-    if(opts.step === 'all') {
-        return axisLayout.autorange === true;
-    }
-    else {
-        var keys = Object.keys(update);
-
-        return (
-            axisLayout.range[0] === update[keys[0]] &&
-            axisLayout.range[1] === update[keys[1]]
-        );
-    }
+  if (opts.step === "all") {
+    return axisLayout.autorange === true;
+  } else {
+    var keys = Object.keys(update);
+
+    return axisLayout.range[0] === update[keys[0]] &&
+      axisLayout.range[1] === update[keys[1]];
+  }
 }
 
 function drawButtonRect(button, selectorLayout, d) {
-    var rect = button.selectAll('rect')
-        .data([0]);
+  var rect = button.selectAll("rect").data([0]);
 
-    rect.enter().append('rect')
-        .classed('selector-rect', true);
+  rect.enter().append("rect").classed("selector-rect", true);
 
-    rect.attr('shape-rendering', 'crispEdges');
+  rect.attr("shape-rendering", "crispEdges");
 
-    rect.attr({
-        'rx': constants.rx,
-        'ry': constants.ry
-    });
+  rect.attr({ rx: constants.rx, ry: constants.ry });
 
-    rect.call(Color.stroke, selectorLayout.bordercolor)
-        .call(Color.fill, getFillColor(selectorLayout, d))
-        .style('stroke-width', selectorLayout.borderwidth + 'px');
+  rect
+    .call(Color.stroke, selectorLayout.bordercolor)
+    .call(Color.fill, getFillColor(selectorLayout, d))
+    .style("stroke-width", selectorLayout.borderwidth + "px");
 }
 
 function getFillColor(selectorLayout, d) {
-    return (d.isActive || d.isHovered) ?
-        selectorLayout.activecolor :
-        selectorLayout.bgcolor;
+  return d.isActive || d.isHovered
+    ? selectorLayout.activecolor
+    : selectorLayout.bgcolor;
 }
 
 function drawButtonText(button, selectorLayout, d) {
-    function textLayout(s) {
-        svgTextUtils.convertToTspans(s);
+  function textLayout(s) {
+    svgTextUtils.convertToTspans(s);
+    // TODO do we need anything else here?
+  }
 
-        // TODO do we need anything else here?
-    }
+  var text = button.selectAll("text").data([0]);
 
-    var text = button.selectAll('text')
-        .data([0]);
+  text
+    .enter()
+    .append("text")
+    .classed("selector-text", true)
+    .classed("user-select-none", true);
 
-    text.enter().append('text')
-        .classed('selector-text', true)
-        .classed('user-select-none', true);
+  text.attr("text-anchor", "middle");
 
-    text.attr('text-anchor', 'middle');
-
-    text.call(Drawing.font, selectorLayout.font)
-        .text(getLabel(d))
-        .call(textLayout);
+  text
+    .call(Drawing.font, selectorLayout.font)
+    .text(getLabel(d))
+    .call(textLayout);
 }
 
 function getLabel(opts) {
-    if(opts.label) return opts.label;
+  if (opts.label) return opts.label;
 
-    if(opts.step === 'all') return 'all';
+  if (opts.step === "all") return "all";
 
-    return opts.count + opts.step.charAt(0);
+  return opts.count + opts.step.charAt(0);
 }
 
 function reposition(gd, buttons, opts, axName) {
-    opts.width = 0;
-    opts.height = 0;
-
-    var borderWidth = opts.borderwidth;
-
-    buttons.each(function() {
-        var button = d3.select(this),
-            text = button.select('.selector-text'),
-            tspans = text.selectAll('tspan');
-
-        var tHeight = opts.font.size * 1.3,
-            tLines = tspans[0].length || 1,
-            hEff = Math.max(tHeight * tLines, 16) + 3;
-
-        opts.height = Math.max(opts.height, hEff);
-    });
-
-    buttons.each(function() {
-        var button = d3.select(this),
-            rect = button.select('.selector-rect'),
-            text = button.select('.selector-text'),
-            tspans = text.selectAll('tspan');
-
-        var tWidth = text.node() && Drawing.bBox(text.node()).width,
-            tHeight = opts.font.size * 1.3,
-            tLines = tspans[0].length || 1;
-
-        var wEff = Math.max(tWidth + 10, constants.minButtonWidth);
-
-        // TODO add MathJax support
-
-        // TODO add buttongap attribute
-
-        button.attr('transform', 'translate(' +
-            (borderWidth + opts.width) + ',' + borderWidth +
-        ')');
-
-        rect.attr({
-            x: 0,
-            y: 0,
-            width: wEff,
-            height: opts.height
-        });
-
-        var textAttrs = {
-            x: wEff / 2,
-            y: opts.height / 2 - ((tLines - 1) * tHeight / 2) + 3
-        };
-
-        text.attr(textAttrs);
-        tspans.attr(textAttrs);
-
-        opts.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 xanchor = 'left';
-    if(anchorUtils.isRightAnchor(opts)) {
-        opts.lx -= opts.width;
-        xanchor = 'right';
-    }
-    if(anchorUtils.isCenterAnchor(opts)) {
-        opts.lx -= opts.width / 2;
-        xanchor = 'center';
-    }
-
-    var yanchor = 'top';
-    if(anchorUtils.isBottomAnchor(opts)) {
-        opts.ly -= opts.height;
-        yanchor = 'bottom';
-    }
-    if(anchorUtils.isMiddleAnchor(opts)) {
-        opts.ly -= opts.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);
-
-    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)
-    });
+  opts.width = 0;
+  opts.height = 0;
+
+  var borderWidth = opts.borderwidth;
+
+  buttons.each(function() {
+    var button = d3.select(this),
+      text = button.select(".selector-text"),
+      tspans = text.selectAll("tspan");
+
+    var tHeight = opts.font.size * 1.3,
+      tLines = tspans[0].length || 1,
+      hEff = Math.max(tHeight * tLines, 16) + 3;
+
+    opts.height = Math.max(opts.height, hEff);
+  });
+
+  buttons.each(function() {
+    var button = d3.select(this),
+      rect = button.select(".selector-rect"),
+      text = button.select(".selector-text"),
+      tspans = text.selectAll("tspan");
+
+    var tWidth = text.node() && Drawing.bBox(text.node()).width,
+      tHeight = opts.font.size * 1.3,
+      tLines = tspans[0].length || 1;
+
+    var wEff = Math.max(tWidth + 10, constants.minButtonWidth);
+
+    // TODO add MathJax support
+    // TODO add buttongap attribute
+    button.attr(
+      "transform",
+      "translate(" + (borderWidth + opts.width) + "," + borderWidth + ")"
+    );
+
+    rect.attr({ x: 0, y: 0, width: wEff, height: opts.height });
+
+    var textAttrs = {
+      x: wEff / 2,
+      y: opts.height / 2 - (tLines - 1) * tHeight / 2 + 3
+    };
+
+    text.attr(textAttrs);
+    tspans.attr(textAttrs);
+
+    opts.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 xanchor = "left";
+  if (anchorUtils.isRightAnchor(opts)) {
+    opts.lx -= opts.width;
+    xanchor = "right";
+  }
+  if (anchorUtils.isCenterAnchor(opts)) {
+    opts.lx -= opts.width / 2;
+    xanchor = "center";
+  }
+
+  var yanchor = "top";
+  if (anchorUtils.isBottomAnchor(opts)) {
+    opts.ly -= opts.height;
+    yanchor = "bottom";
+  }
+  if (anchorUtils.isMiddleAnchor(opts)) {
+    opts.ly -= opts.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);
+
+  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)
+  });
 }
diff --git a/src/components/rangeselector/get_update_object.js b/src/components/rangeselector/get_update_object.js
index e8fa28971c7..eafc64b456c 100644
--- a/src/components/rangeselector/get_update_object.js
+++ b/src/components/rangeselector/get_update_object.js
@@ -5,51 +5,46 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var d3 = require('d3');
+"use strict";
+var d3 = require("d3");
 
 module.exports = function getUpdateObject(axisLayout, buttonLayout) {
-    var axName = axisLayout._name;
-    var update = {};
+  var axName = axisLayout._name;
+  var update = {};
 
-    if(buttonLayout.step === 'all') {
-        update[axName + '.autorange'] = true;
-    }
-    else {
-        var xrange = getXRange(axisLayout, buttonLayout);
+  if (buttonLayout.step === "all") {
+    update[axName + ".autorange"] = true;
+  } else {
+    var xrange = getXRange(axisLayout, buttonLayout);
 
-        update[axName + '.range[0]'] = xrange[0];
-        update[axName + '.range[1]'] = xrange[1];
-    }
+    update[axName + ".range[0]"] = xrange[0];
+    update[axName + ".range[1]"] = xrange[1];
+  }
 
-    return update;
+  return update;
 };
 
 function getXRange(axisLayout, buttonLayout) {
-    var currentRange = axisLayout.range;
-    var base = new Date(axisLayout.r2l(currentRange[1]));
+  var currentRange = axisLayout.range;
+  var base = new Date(axisLayout.r2l(currentRange[1]));
 
-    var step = buttonLayout.step,
-        count = buttonLayout.count;
+  var step = buttonLayout.step, count = buttonLayout.count;
 
-    var range0;
+  var range0;
 
-    switch(buttonLayout.stepmode) {
-        case 'backward':
-            range0 = axisLayout.l2r(+d3.time[step].utc.offset(base, -count));
-            break;
+  switch (buttonLayout.stepmode) {
+    case "backward":
+      range0 = axisLayout.l2r(+d3.time[step].utc.offset(base, -count));
+      break;
 
-        case 'todate':
-            var base2 = d3.time[step].utc.offset(base, -count);
+    case "todate":
+      var base2 = d3.time[step].utc.offset(base, -count);
 
-            range0 = axisLayout.l2r(+d3.time[step].utc.ceil(base2));
-            break;
-    }
+      range0 = axisLayout.l2r(+d3.time[step].utc.ceil(base2));
+      break;
+  }
 
-    var range1 = currentRange[1];
+  var range1 = currentRange[1];
 
-    return [range0, range1];
+  return [range0, range1];
 }
diff --git a/src/components/rangeselector/index.js b/src/components/rangeselector/index.js
index a4e3e7cfd58..945b46bad07 100644
--- a/src/components/rangeselector/index.js
+++ b/src/components/rangeselector/index.js
@@ -5,21 +5,12 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 module.exports = {
-    moduleType: 'component',
-    name: 'rangeselector',
-
-    schema: {
-        layout: {
-            'xaxis.rangeselector': require('./attributes')
-        }
-    },
-
-    layoutAttributes: require('./attributes'),
-    handleDefaults: require('./defaults'),
-
-    draw: require('./draw')
+  moduleType: "component",
+  name: "rangeselector",
+  schema: { layout: { "xaxis.rangeselector": require("./attributes") } },
+  layoutAttributes: require("./attributes"),
+  handleDefaults: require("./defaults"),
+  draw: require("./draw")
 };
diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js
index d81dfbfaecf..f02f1bcb745 100644
--- a/src/components/rangeslider/attributes.js
+++ b/src/components/rangeslider/attributes.js
@@ -5,69 +5,64 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var colorAttributes = require('../color/attributes');
+"use strict";
+var colorAttributes = require("../color/attributes");
 
 module.exports = {
-    bgcolor: {
-        valType: 'color',
-        dflt: colorAttributes.background,
-        role: 'style',
-        description: 'Sets the background color of the range slider.'
-    },
-    bordercolor: {
-        valType: 'color',
-        dflt: colorAttributes.defaultLine,
-        role: 'style',
-        description: 'Sets the border color of the range slider.'
-    },
-    borderwidth: {
-        valType: 'integer',
-        dflt: 0,
-        min: 0,
-        role: 'style',
-        description: 'Sets the border color of the range slider.'
-    },
-    range: {
-        valType: 'info_array',
-        role: 'info',
-        items: [
-            {valType: 'any'},
-            {valType: 'any'}
-        ],
-        description: [
-            'Sets the range of the range slider.',
-            'If not set, defaults to the full xaxis range.',
-            'If the axis `type` is *log*, then you must take the',
-            'log of your desired range.',
-            'If the axis `type` is *date*, it should be date strings,',
-            'like date data, though Date objects and unix milliseconds',
-            'will be accepted and converted to strings.',
-            'If the axis `type` is *category*, it should be numbers,',
-            'using the scale where each category is assigned a serial',
-            'number from zero in the order it appears.'
-        ].join(' ')
-    },
-    thickness: {
-        valType: 'number',
-        dflt: 0.15,
-        min: 0,
-        max: 1,
-        role: 'style',
-        description: [
-            'The height of the range slider as a fraction of the',
-            'total plot area height.'
-        ].join(' ')
-    },
-    visible: {
-        valType: 'boolean',
-        dflt: true,
-        role: 'info',
-        description: [
-            'Determines whether or not the range slider will be visible.',
-            'If visible, perpendicular axes will be set to `fixedrange`'
-        ].join(' ')
-    }
+  bgcolor: {
+    valType: "color",
+    dflt: colorAttributes.background,
+    role: "style",
+    description: "Sets the background color of the range slider."
+  },
+  bordercolor: {
+    valType: "color",
+    dflt: colorAttributes.defaultLine,
+    role: "style",
+    description: "Sets the border color of the range slider."
+  },
+  borderwidth: {
+    valType: "integer",
+    dflt: 0,
+    min: 0,
+    role: "style",
+    description: "Sets the border color of the range slider."
+  },
+  range: {
+    valType: "info_array",
+    role: "info",
+    items: [{ valType: "any" }, { valType: "any" }],
+    description: [
+      "Sets the range of the range slider.",
+      "If not set, defaults to the full xaxis range.",
+      "If the axis `type` is *log*, then you must take the",
+      "log of your desired range.",
+      "If the axis `type` is *date*, it should be date strings,",
+      "like date data, though Date objects and unix milliseconds",
+      "will be accepted and converted to strings.",
+      "If the axis `type` is *category*, it should be numbers,",
+      "using the scale where each category is assigned a serial",
+      "number from zero in the order it appears."
+    ].join(" ")
+  },
+  thickness: {
+    valType: "number",
+    dflt: 0.15,
+    min: 0,
+    max: 1,
+    role: "style",
+    description: [
+      "The height of the range slider as a fraction of the",
+      "total plot area height."
+    ].join(" ")
+  },
+  visible: {
+    valType: "boolean",
+    dflt: true,
+    role: "info",
+    description: [
+      "Determines whether or not the range slider will be visible.",
+      "If visible, perpendicular axes will be set to `fixedrange`"
+    ].join(" ")
+  }
 };
diff --git a/src/components/rangeslider/constants.js b/src/components/rangeslider/constants.js
index 9adc42e6407..ce761a356fa 100644
--- a/src/components/rangeslider/constants.js
+++ b/src/components/rangeslider/constants.js
@@ -5,47 +5,34 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 module.exports = {
-
-    // attribute container name
-    name: 'rangeslider',
-
-    // class names
-
-    containerClassName: 'rangeslider-container',
-    bgClassName: 'rangeslider-bg',
-    rangePlotClassName: 'rangeslider-rangeplot',
-
-    maskMinClassName: 'rangeslider-mask-min',
-    maskMaxClassName: 'rangeslider-mask-max',
-    slideBoxClassName: 'rangeslider-slidebox',
-
-    grabberMinClassName: 'rangeslider-grabber-min',
-    grabAreaMinClassName: 'rangeslider-grabarea-min',
-    handleMinClassName: 'rangeslider-handle-min',
-
-    grabberMaxClassName: 'rangeslider-grabber-max',
-    grabAreaMaxClassName: 'rangeslider-grabarea-max',
-    handleMaxClassName: 'rangeslider-handle-max',
-
-    // style constants
-
-    maskColor: 'rgba(0,0,0,0.4)',
-
-    slideBoxFill: 'transparent',
-    slideBoxCursor: 'ew-resize',
-
-    grabAreaFill: 'transparent',
-    grabAreaCursor: 'col-resize',
-    grabAreaWidth: 10,
-    grabAreaMinOffset: -6,
-    grabAreaMaxOffset: -2,
-
-    handleWidth: 2,
-    handleRadius: 1,
-    handleFill: '#fff',
-    handleStroke: '#666',
+  // attribute container name
+  name: "rangeslider",
+  // class names
+  containerClassName: "rangeslider-container",
+  bgClassName: "rangeslider-bg",
+  rangePlotClassName: "rangeslider-rangeplot",
+  maskMinClassName: "rangeslider-mask-min",
+  maskMaxClassName: "rangeslider-mask-max",
+  slideBoxClassName: "rangeslider-slidebox",
+  grabberMinClassName: "rangeslider-grabber-min",
+  grabAreaMinClassName: "rangeslider-grabarea-min",
+  handleMinClassName: "rangeslider-handle-min",
+  grabberMaxClassName: "rangeslider-grabber-max",
+  grabAreaMaxClassName: "rangeslider-grabarea-max",
+  handleMaxClassName: "rangeslider-handle-max",
+  // style constants
+  maskColor: "rgba(0,0,0,0.4)",
+  slideBoxFill: "transparent",
+  slideBoxCursor: "ew-resize",
+  grabAreaFill: "transparent",
+  grabAreaCursor: "col-resize",
+  grabAreaWidth: 10,
+  grabAreaMinOffset: -6,
+  grabAreaMaxOffset: -2,
+  handleWidth: 2,
+  handleRadius: 1,
+  handleFill: "#fff",
+  handleStroke: "#666"
 };
diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js
index 379a4596b84..21fec917766 100644
--- a/src/components/rangeslider/defaults.js
+++ b/src/components/rangeslider/defaults.js
@@ -5,48 +5,48 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var Lib = require('../../lib');
-var attributes = require('./attributes');
-
+"use strict";
+var Lib = require("../../lib");
+var attributes = require("./attributes");
 
 module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
-    if(!layoutIn[axName].rangeslider) return;
-
-    // not super proud of this (maybe store _ in axis object instead
-    if(!Lib.isPlainObject(layoutIn[axName].rangeslider)) {
-        layoutIn[axName].rangeslider = {};
-    }
-
-    var containerIn = layoutIn[axName].rangeslider,
-        axOut = layoutOut[axName],
-        containerOut = axOut.rangeslider = {};
-
-    function coerce(attr, dflt) {
-        return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
-    }
-
-    coerce('bgcolor', layoutOut.plot_bgcolor);
-    coerce('bordercolor');
-    coerce('borderwidth');
-    coerce('thickness');
-    coerce('visible');
-    coerce('range');
-
-    // Expand slider range to the axis range
-    if(containerOut.range && !axOut.autorange) {
-        // TODO: what if the ranges are reversed?
-        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])));
-    } else {
-        axOut._needsExpand = true;
-    }
-
-    // to map back range slider (auto) range
-    containerOut._input = containerIn;
+  if (!layoutIn[axName].rangeslider) return;
+
+  // not super proud of this (maybe store _ in axis object instead
+  if (!Lib.isPlainObject(layoutIn[axName].rangeslider)) {
+    layoutIn[axName].rangeslider = {};
+  }
+
+  var containerIn = layoutIn[axName].rangeslider,
+    axOut = layoutOut[axName],
+    containerOut = axOut.rangeslider = {};
+
+  function coerce(attr, dflt) {
+    return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
+  }
+
+  coerce("bgcolor", layoutOut.plot_bgcolor);
+  coerce("bordercolor");
+  coerce("borderwidth");
+  coerce("thickness");
+  coerce("visible");
+  coerce("range");
+
+  // Expand slider range to the axis range
+  if (containerOut.range && !axOut.autorange) {
+    // TODO: what if the ranges are reversed?
+    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]))
+    );
+  } else {
+    axOut._needsExpand = true;
+  }
+
+  // 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 ded3198d316..5c7acf9b850 100644
--- a/src/components/rangeslider/draw.js
+++ b/src/components/rangeslider/draw.js
@@ -5,32 +5,29 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
 
-'use strict';
+var Plotly = require("../../plotly");
+var Plots = require("../../plots/plots");
 
-var d3 = require('d3');
+var Lib = require("../../lib");
+var Drawing = require("../drawing");
+var Color = require("../color");
 
-var Plotly = require('../../plotly');
-var Plots = require('../../plots/plots');
+var Cartesian = require("../../plots/cartesian");
+var Axes = require("../../plots/cartesian/axes");
 
-var Lib = require('../../lib');
-var Drawing = require('../drawing');
-var Color = require('../color');
-
-var Cartesian = require('../../plots/cartesian');
-var Axes = require('../../plots/cartesian/axes');
-
-var dragElement = require('../dragelement');
-var setCursor = require('../../lib/setcursor');
-
-var constants = require('./constants');
+var dragElement = require("../dragelement");
+var setCursor = require("../../lib/setcursor");
 
+var constants = require("./constants");
 
 module.exports = function(gd) {
-    var fullLayout = gd._fullLayout,
-        rangeSliderData = makeRangeSliderData(fullLayout);
+  var fullLayout = gd._fullLayout,
+    rangeSliderData = makeRangeSliderData(fullLayout);
 
-    /*
+  /*
      * 
      *  
      *  < .... range plot />
@@ -46,484 +43,489 @@ module.exports = function(gd) {
      *
      *  ...
      */
-
-    function keyFunction(axisOpts) {
-        return axisOpts._name;
+  function keyFunction(axisOpts) {
+    return axisOpts._name;
+  }
+
+  var rangeSliders = fullLayout._infolayer
+    .selectAll("g." + constants.containerClassName)
+    .data(rangeSliderData, keyFunction);
+
+  rangeSliders
+    .enter()
+    .append("g")
+    .classed(constants.containerClassName, true)
+    .attr("pointer-events", "all");
+
+  // remove exiting sliders and their corresponding clip paths
+  rangeSliders.exit().each(function(axisOpts) {
+    var rangeSlider = d3.select(this), opts = axisOpts[constants.name];
+
+    rangeSlider.remove();
+    fullLayout._topdefs.select("#" + opts._clipId).remove();
+  });
+
+  // remove push margin object(s)
+  if (rangeSliders.exit().size()) clearPushMargins(gd);
+
+  // return early if no range slider is visible
+  if (rangeSliderData.length === 0) return;
+
+  // for all present range sliders
+  rangeSliders.each(function(axisOpts) {
+    var rangeSlider = d3.select(this), opts = axisOpts[constants.name];
+
+    // compute new slider range using axis autorange if necessary
+    // copy back range to input range slider container to skip
+    // this step in subsequent draw calls
+    if (!opts.range) {
+      opts._input.range = opts.range = Axes.getAutoRange(axisOpts);
     }
 
-    var rangeSliders = fullLayout._infolayer
-        .selectAll('g.' + constants.containerClassName)
-        .data(rangeSliderData, keyFunction);
-
-    rangeSliders.enter().append('g')
-        .classed(constants.containerClassName, true)
-        .attr('pointer-events', 'all');
-
-    // remove exiting sliders and their corresponding clip paths
-    rangeSliders.exit().each(function(axisOpts) {
-        var rangeSlider = d3.select(this),
-            opts = axisOpts[constants.name];
-
-        rangeSlider.remove();
-        fullLayout._topdefs.select('#' + opts._clipId).remove();
-    });
-
-    // remove push margin object(s)
-    if(rangeSliders.exit().size()) clearPushMargins(gd);
-
-    // return early if no range slider is visible
-    if(rangeSliderData.length === 0) return;
-
-    // for all present range sliders
-    rangeSliders.each(function(axisOpts) {
-        var rangeSlider = d3.select(this),
-            opts = axisOpts[constants.name];
-
-        // compute new slider range using axis autorange if necessary
-        // copy back range to input range slider container to skip
-        // this step in subsequent draw calls
-        if(!opts.range) {
-            opts._input.range = opts.range = Axes.getAutoRange(axisOpts);
-        }
-
-        // update range slider dimensions
-
-        var margin = fullLayout.margin,
-            graphSize = fullLayout._size,
-            domain = axisOpts.domain;
-
-        opts._id = constants.name + axisOpts._id;
-        opts._clipId = opts._id + '-' + fullLayout._uid;
-
-        opts._width = graphSize.w * (domain[1] - domain[0]);
-        opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness;
-        opts._offsetShift = Math.floor(opts.borderwidth / 2);
-
-        var x = margin.l + (graphSize.w * domain[0]),
-            y = fullLayout.height - opts._height - margin.b;
-
-        rangeSlider.attr('transform', 'translate(' + x + ',' + y + ')');
-
-        // update data <--> pixel coordinate conversion methods
+    // update range slider dimensions
+    var margin = fullLayout.margin,
+      graphSize = fullLayout._size,
+      domain = axisOpts.domain;
 
-        var range0 = axisOpts.r2l(opts.range[0]),
-            range1 = axisOpts.r2l(opts.range[1]),
-            dist = range1 - range0;
+    opts._id = constants.name + axisOpts._id;
+    opts._clipId = opts._id + "-" + fullLayout._uid;
 
-        opts.p2d = function(v) {
-            return (v / opts._width) * dist + range0;
-        };
+    opts._width = graphSize.w * (domain[1] - domain[0]);
+    opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness;
+    opts._offsetShift = Math.floor(opts.borderwidth / 2);
 
-        opts.d2p = function(v) {
-            return (v - range0) / dist * opts._width;
-        };
+    var x = margin.l + graphSize.w * domain[0],
+      y = fullLayout.height - opts._height - margin.b;
 
-        opts._rl = [range0, range1];
+    rangeSlider.attr("transform", "translate(" + x + "," + y + ")");
 
-        // update inner nodes
+    // update data <--> pixel coordinate conversion methods
+    var range0 = axisOpts.r2l(opts.range[0]),
+      range1 = axisOpts.r2l(opts.range[1]),
+      dist = range1 - range0;
 
-        rangeSlider
-            .call(drawBg, gd, axisOpts, opts)
-            .call(addClipPath, gd, axisOpts, opts)
-            .call(drawRangePlot, gd, axisOpts, opts)
-            .call(drawMasks, gd, axisOpts, opts)
-            .call(drawSlideBox, gd, axisOpts, opts)
-            .call(drawGrabbers, gd, axisOpts, opts);
-
-        // setup drag element
-        setupDragElement(rangeSlider, gd, axisOpts, opts);
-
-        // update current range
-        setPixelRange(rangeSlider, gd, axisOpts, opts);
-
-        // update margins
+    opts.p2d = function(v) {
+      return v / opts._width * dist + range0;
+    };
 
-        var bb = axisOpts._boundingBox ? axisOpts._boundingBox.height : 0;
+    opts.d2p = function(v) {
+      return (v - range0) / dist * opts._width;
+    };
 
-        Plots.autoMargin(gd, opts._id, {
-            x: 0, y: 0, l: 0, r: 0, t: 0,
-            b: opts._height + fullLayout.margin.b + bb,
-            pad: 15 + opts._offsetShift * 2
-        });
+    opts._rl = [range0, range1];
+
+    // update inner nodes
+    rangeSlider
+      .call(drawBg, gd, axisOpts, opts)
+      .call(addClipPath, gd, axisOpts, opts)
+      .call(drawRangePlot, gd, axisOpts, opts)
+      .call(drawMasks, gd, axisOpts, opts)
+      .call(drawSlideBox, gd, axisOpts, opts)
+      .call(drawGrabbers, gd, axisOpts, opts);
+
+    // setup drag element
+    setupDragElement(rangeSlider, gd, axisOpts, opts);
+
+    // update current range
+    setPixelRange(rangeSlider, gd, axisOpts, opts);
+
+    // update margins
+    var bb = axisOpts._boundingBox ? axisOpts._boundingBox.height : 0;
+
+    Plots.autoMargin(gd, opts._id, {
+      x: 0,
+      y: 0,
+      l: 0,
+      r: 0,
+      t: 0,
+      b: opts._height + fullLayout.margin.b + bb,
+      pad: 15 + opts._offsetShift * 2
     });
+  });
 };
 
 function makeRangeSliderData(fullLayout) {
-    if(!fullLayout.xaxis) return [];
-    if(!fullLayout.xaxis[constants.name]) return [];
-    if(!fullLayout.xaxis[constants.name].visible) return [];
-    if(fullLayout._has('gl2d')) return [];
+  if (!fullLayout.xaxis) return [];
+  if (!fullLayout.xaxis[constants.name]) return [];
+  if (!fullLayout.xaxis[constants.name].visible) return [];
+  if (fullLayout._has("gl2d")) return [];
 
-    return [fullLayout.xaxis];
+  return [fullLayout.xaxis];
 }
 
 function setupDragElement(rangeSlider, gd, axisOpts, opts) {
-    var slideBox = rangeSlider.select('rect.' + constants.slideBoxClassName).node(),
-        grabAreaMin = rangeSlider.select('rect.' + constants.grabAreaMinClassName).node(),
-        grabAreaMax = rangeSlider.select('rect.' + constants.grabAreaMaxClassName).node();
-
-    rangeSlider.on('mousedown', function() {
-        var event = d3.event,
-            target = event.target,
-            startX = event.clientX,
-            offsetX = startX - rangeSlider.node().getBoundingClientRect().left,
-            minVal = opts.d2p(axisOpts._rl[0]),
-            maxVal = opts.d2p(axisOpts._rl[1]);
-
-        var dragCover = dragElement.coverSlip();
-
-        dragCover.addEventListener('mousemove', mouseMove);
-        dragCover.addEventListener('mouseup', mouseUp);
-
-        function mouseMove(e) {
-            var delta = +e.clientX - startX;
-            var pixelMin, pixelMax, cursor;
-
-            switch(target) {
-                case slideBox:
-                    cursor = 'ew-resize';
-                    pixelMin = minVal + delta;
-                    pixelMax = maxVal + delta;
-                    break;
-
-                case grabAreaMin:
-                    cursor = 'col-resize';
-                    pixelMin = minVal + delta;
-                    pixelMax = maxVal;
-                    break;
-
-                case grabAreaMax:
-                    cursor = 'col-resize';
-                    pixelMin = minVal;
-                    pixelMax = maxVal + delta;
-                    break;
-
-                default:
-                    cursor = 'ew-resize';
-                    pixelMin = offsetX;
-                    pixelMax = offsetX + delta;
-                    break;
-            }
-
-            if(pixelMax < pixelMin) {
-                var tmp = pixelMax;
-                pixelMax = pixelMin;
-                pixelMin = tmp;
-            }
-
-            opts._pixelMin = pixelMin;
-            opts._pixelMax = pixelMax;
-
-            setCursor(d3.select(dragCover), cursor);
-            setDataRange(rangeSlider, gd, axisOpts, opts);
-        }
-
-        function mouseUp() {
-            dragCover.removeEventListener('mousemove', mouseMove);
-            dragCover.removeEventListener('mouseup', mouseUp);
-            Lib.removeElement(dragCover);
-        }
-    });
+  var slideBox = rangeSlider
+    .select("rect." + constants.slideBoxClassName)
+    .node(),
+    grabAreaMin = rangeSlider
+      .select("rect." + constants.grabAreaMinClassName)
+      .node(),
+    grabAreaMax = rangeSlider
+      .select("rect." + constants.grabAreaMaxClassName)
+      .node();
+
+  rangeSlider.on("mousedown", function() {
+    var event = d3.event,
+      target = event.target,
+      startX = event.clientX,
+      offsetX = startX - rangeSlider.node().getBoundingClientRect().left,
+      minVal = opts.d2p(axisOpts._rl[0]),
+      maxVal = opts.d2p(axisOpts._rl[1]);
+
+    var dragCover = dragElement.coverSlip();
+
+    dragCover.addEventListener("mousemove", mouseMove);
+    dragCover.addEventListener("mouseup", mouseUp);
+
+    function mouseMove(e) {
+      var delta = +e.clientX - startX;
+      var pixelMin, pixelMax, cursor;
+
+      switch (target) {
+        case slideBox:
+          cursor = "ew-resize";
+          pixelMin = minVal + delta;
+          pixelMax = maxVal + delta;
+          break;
+
+        case grabAreaMin:
+          cursor = "col-resize";
+          pixelMin = minVal + delta;
+          pixelMax = maxVal;
+          break;
+
+        case grabAreaMax:
+          cursor = "col-resize";
+          pixelMin = minVal;
+          pixelMax = maxVal + delta;
+          break;
+
+        default:
+          cursor = "ew-resize";
+          pixelMin = offsetX;
+          pixelMax = offsetX + delta;
+          break;
+      }
+
+      if (pixelMax < pixelMin) {
+        var tmp = pixelMax;
+        pixelMax = pixelMin;
+        pixelMin = tmp;
+      }
+
+      opts._pixelMin = pixelMin;
+      opts._pixelMax = pixelMax;
+
+      setCursor(d3.select(dragCover), cursor);
+      setDataRange(rangeSlider, gd, axisOpts, opts);
+    }
+
+    function mouseUp() {
+      dragCover.removeEventListener("mousemove", mouseMove);
+      dragCover.removeEventListener("mouseup", mouseUp);
+      Lib.removeElement(dragCover);
+    }
+  });
 }
 
 function setDataRange(rangeSlider, gd, axisOpts, opts) {
+  function clamp(v) {
+    return axisOpts.l2r(Lib.constrain(v, opts._rl[0], opts._rl[1]));
+  }
 
-    function clamp(v) {
-        return axisOpts.l2r(Lib.constrain(v, opts._rl[0], opts._rl[1]));
-    }
+  var dataMin = clamp(opts.p2d(opts._pixelMin)),
+    dataMax = clamp(opts.p2d(opts._pixelMax));
 
-    var dataMin = clamp(opts.p2d(opts._pixelMin)),
-        dataMax = clamp(opts.p2d(opts._pixelMax));
-
-    window.requestAnimationFrame(function() {
-        Plotly.relayout(gd, 'xaxis.range', [dataMin, dataMax]);
-    });
+  window.requestAnimationFrame(function() {
+    Plotly.relayout(gd, "xaxis.range", [dataMin, dataMax]);
+  });
 }
 
 function setPixelRange(rangeSlider, gd, axisOpts, opts) {
-
-    function clamp(v) {
-        return Lib.constrain(v, 0, opts._width);
-    }
-
-    var pixelMin = clamp(opts.d2p(axisOpts._rl[0])),
-        pixelMax = clamp(opts.d2p(axisOpts._rl[1]));
-
-    rangeSlider.select('rect.' + constants.slideBoxClassName)
-        .attr('x', pixelMin)
-        .attr('width', pixelMax - pixelMin);
-
-    rangeSlider.select('rect.' + constants.maskMinClassName)
-        .attr('width', pixelMin);
-
-    rangeSlider.select('rect.' + constants.maskMaxClassName)
-        .attr('x', pixelMax)
-        .attr('width', opts._width - pixelMax);
-
-    rangeSlider.select('g.' + constants.grabberMinClassName)
-        .attr('transform', 'translate(' + (pixelMin - constants.handleWidth - 1) + ',0)');
-
-    rangeSlider.select('g.' + constants.grabberMaxClassName)
-        .attr('transform', 'translate(' + pixelMax + ',0)');
+  function clamp(v) {
+    return Lib.constrain(v, 0, opts._width);
+  }
+
+  var pixelMin = clamp(opts.d2p(axisOpts._rl[0])),
+    pixelMax = clamp(opts.d2p(axisOpts._rl[1]));
+
+  rangeSlider
+    .select("rect." + constants.slideBoxClassName)
+    .attr("x", pixelMin)
+    .attr("width", pixelMax - pixelMin);
+
+  rangeSlider
+    .select("rect." + constants.maskMinClassName)
+    .attr("width", pixelMin);
+
+  rangeSlider
+    .select("rect." + constants.maskMaxClassName)
+    .attr("x", pixelMax)
+    .attr("width", opts._width - pixelMax);
+
+  rangeSlider
+    .select("g." + constants.grabberMinClassName)
+    .attr(
+      "transform",
+      "translate(" + (pixelMin - constants.handleWidth - 1) + ",0)"
+    );
+
+  rangeSlider
+    .select("g." + constants.grabberMaxClassName)
+    .attr("transform", "translate(" + pixelMax + ",0)");
 }
 
 function drawBg(rangeSlider, gd, axisOpts, opts) {
-    var bg = rangeSlider.selectAll('rect.' + constants.bgClassName)
-        .data([0]);
-
-    bg.enter().append('rect')
-        .classed(constants.bgClassName, true)
-        .attr({
-            x: 0,
-            y: 0,
-            'shape-rendering': 'crispEdges'
-        });
-
-    var borderCorrect = (opts.borderwidth % 2) === 0 ?
-            opts.borderwidth :
-            opts.borderwidth - 1;
-
-    var offsetShift = -opts._offsetShift;
-
-    bg.attr({
-        width: opts._width + borderCorrect,
-        height: opts._height + borderCorrect,
-        transform: 'translate(' + offsetShift + ',' + offsetShift + ')',
-        fill: opts.bgcolor,
-        stroke: opts.bordercolor,
-        'stroke-width': opts.borderwidth,
-    });
+  var bg = rangeSlider.selectAll("rect." + constants.bgClassName).data([0]);
+
+  bg
+    .enter()
+    .append("rect")
+    .classed(constants.bgClassName, true)
+    .attr({ x: 0, y: 0, "shape-rendering": "crispEdges" });
+
+  var borderCorrect = opts.borderwidth % 2 === 0
+    ? opts.borderwidth
+    : opts.borderwidth - 1;
+
+  var offsetShift = -opts._offsetShift;
+
+  bg.attr({
+    width: opts._width + borderCorrect,
+    height: opts._height + borderCorrect,
+    transform: "translate(" + offsetShift + "," + offsetShift + ")",
+    fill: opts.bgcolor,
+    stroke: opts.bordercolor,
+    "stroke-width": opts.borderwidth
+  });
 }
 
 function addClipPath(rangeSlider, gd, axisOpts, opts) {
-    var fullLayout = gd._fullLayout;
+  var fullLayout = gd._fullLayout;
 
-    var clipPath = fullLayout._topdefs.selectAll('#' + opts._clipId)
-        .data([0]);
+  var clipPath = fullLayout._topdefs.selectAll("#" + opts._clipId).data([0]);
 
-    clipPath.enter().append('clipPath')
-        .attr('id', opts._clipId)
-        .append('rect')
-        .attr({ x: 0, y: 0 });
+  clipPath
+    .enter()
+    .append("clipPath")
+    .attr("id", opts._clipId)
+    .append("rect")
+    .attr({ x: 0, y: 0 });
 
-    clipPath.select('rect').attr({
-        width: opts._width,
-        height: opts._height
-    });
+  clipPath.select("rect").attr({ width: opts._width, height: opts._height });
 }
 
 function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
-    var subplotData = Axes.getSubplots(gd, axisOpts),
-        calcData = gd.calcdata;
-
-    var rangePlots = rangeSlider.selectAll('g.' + constants.rangePlotClassName)
-        .data(subplotData, Lib.identity);
-
-    rangePlots.enter().append('g')
-        .attr('class', function(id) { return constants.rangePlotClassName + ' ' + id; })
-        .call(Drawing.setClipUrl, opts._clipId);
-
-    rangePlots.order();
-
-    rangePlots.exit().remove();
-
-    var mainplotinfo;
-
-    rangePlots.each(function(id, i) {
-        var plotgroup = d3.select(this),
-            isMainPlot = (i === 0);
-
-        var oppAxisOpts = Axes.getFromId(gd, id, 'y'),
-            oppAxisName = oppAxisOpts._name;
-
-        var mockFigure = {
-            data: [],
-            layout: {
-                xaxis: {
-                    type: axisOpts.type,
-                    domain: [0, 1],
-                    range: opts.range.slice(),
-                    calendar: axisOpts.calendar
-                },
-                width: opts._width,
-                height: opts._height,
-                margin: { t: 0, b: 0, l: 0, r: 0 }
-            }
-        };
-
-        mockFigure.layout[oppAxisName] = {
-            domain: [0, 1],
-            range: oppAxisOpts.range.slice(),
-            calendar: oppAxisOpts.calendar
-        };
-
-        Plots.supplyDefaults(mockFigure);
-
-        var xa = mockFigure._fullLayout.xaxis,
-            ya = mockFigure._fullLayout[oppAxisName];
-
-        var plotinfo = {
-            id: id,
-            plotgroup: plotgroup,
-            xaxis: xa,
-            yaxis: ya
-        };
-
-        if(isMainPlot) mainplotinfo = plotinfo;
-        else {
-            plotinfo.mainplot = 'xy';
-            plotinfo.mainplotinfo = mainplotinfo;
-        }
-
-        Cartesian.rangePlot(gd, plotinfo, filterRangePlotCalcData(calcData, id));
-
-        // no need for the bg layer,
-        // drawBg handles coloring the background
-        if(isMainPlot) plotinfo.bg.remove();
-    });
-}
+  var subplotData = Axes.getSubplots(gd, axisOpts), calcData = gd.calcdata;
 
-function filterRangePlotCalcData(calcData, subplotId) {
-    var out = [];
+  var rangePlots = rangeSlider
+    .selectAll("g." + constants.rangePlotClassName)
+    .data(subplotData, Lib.identity);
 
-    for(var i = 0; i < calcData.length; i++) {
-        var calcTrace = calcData[i],
-            trace = calcTrace[0].trace;
+  rangePlots
+    .enter()
+    .append("g")
+    .attr("class", function(id) {
+      return constants.rangePlotClassName + " " + id;
+    })
+    .call(Drawing.setClipUrl, opts._clipId);
 
-        if(trace.xaxis + trace.yaxis === subplotId) {
-            out.push(calcTrace);
-        }
-    }
+  rangePlots.order();
 
-    return out;
-}
+  rangePlots.exit().remove();
 
-function drawMasks(rangeSlider, gd, axisOpts, opts) {
-    var maskMin = rangeSlider.selectAll('rect.' + constants.maskMinClassName)
-        .data([0]);
+  var mainplotinfo;
 
-    maskMin.enter().append('rect')
-        .classed(constants.maskMinClassName, true)
-        .attr({ x: 0, y: 0 });
+  rangePlots.each(function(id, i) {
+    var plotgroup = d3.select(this), isMainPlot = i === 0;
 
-    maskMin
-        .attr('height', opts._height)
-        .call(Color.fill, constants.maskColor);
+    var oppAxisOpts = Axes.getFromId(gd, id, "y"),
+      oppAxisName = oppAxisOpts._name;
 
-    var maskMax = rangeSlider.selectAll('rect.' + constants.maskMaxClassName)
-        .data([0]);
+    var mockFigure = {
+      data: [],
+      layout: {
+        xaxis: {
+          type: axisOpts.type,
+          domain: [0, 1],
+          range: opts.range.slice(),
+          calendar: axisOpts.calendar
+        },
+        width: opts._width,
+        height: opts._height,
+        margin: { t: 0, b: 0, l: 0, r: 0 }
+      }
+    };
+
+    mockFigure.layout[oppAxisName] = {
+      domain: [0, 1],
+      range: oppAxisOpts.range.slice(),
+      calendar: oppAxisOpts.calendar
+    };
 
-    maskMax.enter().append('rect')
-        .classed(constants.maskMaxClassName, true)
-        .attr('y', 0);
+    Plots.supplyDefaults(mockFigure);
+
+    var xa = mockFigure._fullLayout.xaxis,
+      ya = mockFigure._fullLayout[oppAxisName];
+
+    var plotinfo = { id: id, plotgroup: plotgroup, xaxis: xa, yaxis: ya };
+
+    if (isMainPlot) {
+      mainplotinfo = plotinfo;
+    } else {
+      plotinfo.mainplot = "xy";
+      plotinfo.mainplotinfo = mainplotinfo;
+    }
 
-    maskMax
-        .attr('height', opts._height)
-        .call(Color.fill, constants.maskColor);
+    Cartesian.rangePlot(gd, plotinfo, filterRangePlotCalcData(calcData, id));
+
+    // no need for the bg layer,
+    // drawBg handles coloring the background
+    if (isMainPlot) plotinfo.bg.remove();
+  });
 }
 
-function drawSlideBox(rangeSlider, gd, axisOpts, opts) {
-    if(gd._context.staticPlot) return;
+function filterRangePlotCalcData(calcData, subplotId) {
+  var out = [];
 
-    var slideBox = rangeSlider.selectAll('rect.' + constants.slideBoxClassName)
-        .data([0]);
+  for (var i = 0; i < calcData.length; i++) {
+    var calcTrace = calcData[i], trace = calcTrace[0].trace;
 
-    slideBox.enter().append('rect')
-        .classed(constants.slideBoxClassName, true)
-        .attr('y', 0)
-        .attr('cursor', constants.slideBoxCursor);
+    if (trace.xaxis + trace.yaxis === subplotId) {
+      out.push(calcTrace);
+    }
+  }
 
-    slideBox.attr({
-        height: opts._height,
-        fill: constants.slideBoxFill
-    });
+  return out;
 }
 
-function drawGrabbers(rangeSlider, gd, axisOpts, opts) {
+function drawMasks(rangeSlider, gd, axisOpts, opts) {
+  var maskMin = rangeSlider
+    .selectAll("rect." + constants.maskMinClassName)
+    .data([0]);
 
-    // 
+  maskMin
+    .enter()
+    .append("rect")
+    .classed(constants.maskMinClassName, true)
+    .attr({ x: 0, y: 0 });
 
-    var grabberMin = rangeSlider.selectAll('g.' + constants.grabberMinClassName)
-        .data([0]);
-    grabberMin.enter().append('g')
-        .classed(constants.grabberMinClassName, true);
+  maskMin.attr("height", opts._height).call(Color.fill, constants.maskColor);
 
-    var grabberMax = rangeSlider.selectAll('g.' + constants.grabberMaxClassName)
-        .data([0]);
-    grabberMax.enter().append('g')
-        .classed(constants.grabberMaxClassName, true);
+  var maskMax = rangeSlider
+    .selectAll("rect." + constants.maskMaxClassName)
+    .data([0]);
 
-    // 
+  maskMax
+    .enter()
+    .append("rect")
+    .classed(constants.maskMaxClassName, true)
+    .attr("y", 0);
 
-    var handleFixAttrs = {
-        x: 0,
-        width: constants.handleWidth,
-        rx: constants.handleRadius,
-        fill: constants.handleFill,
-        stroke: constants.handleStroke,
-        'shape-rendering': 'crispEdges'
-    };
+  maskMax.attr("height", opts._height).call(Color.fill, constants.maskColor);
+}
 
-    var handleDynamicAttrs = {
-        y: opts._height / 4,
-        height: opts._height / 2,
-    };
+function drawSlideBox(rangeSlider, gd, axisOpts, opts) {
+  if (gd._context.staticPlot) return;
 
-    var handleMin = grabberMin.selectAll('rect.' + constants.handleMinClassName)
-        .data([0]);
-    handleMin.enter().append('rect')
-        .classed(constants.handleMinClassName, true)
-        .attr(handleFixAttrs);
-    handleMin.attr(handleDynamicAttrs);
-
-    var handleMax = grabberMax.selectAll('rect.' + constants.handleMaxClassName)
-        .data([0]);
-    handleMax.enter().append('rect')
-        .classed(constants.handleMaxClassName, true)
-        .attr(handleFixAttrs);
-    handleMax.attr(handleDynamicAttrs);
-
-    // 
-
-    if(gd._context.staticPlot) return;
-
-    var grabAreaFixAttrs = {
-        width: constants.grabAreaWidth,
-        y: 0,
-        fill: constants.grabAreaFill,
-        cursor: constants.grabAreaCursor
-    };
+  var slideBox = rangeSlider
+    .selectAll("rect." + constants.slideBoxClassName)
+    .data([0]);
 
-    var grabAreaMin = grabberMin.selectAll('rect.' + constants.grabAreaMinClassName)
-        .data([0]);
-    grabAreaMin.enter().append('rect')
-        .classed(constants.grabAreaMinClassName, true)
-        .attr(grabAreaFixAttrs);
-    grabAreaMin.attr({
-        x: constants.grabAreaMinOffset,
-        height: opts._height
-    });
+  slideBox
+    .enter()
+    .append("rect")
+    .classed(constants.slideBoxClassName, true)
+    .attr("y", 0)
+    .attr("cursor", constants.slideBoxCursor);
 
-    var grabAreaMax = grabberMax.selectAll('rect.' + constants.grabAreaMaxClassName)
-        .data([0]);
-    grabAreaMax.enter().append('rect')
-        .classed(constants.grabAreaMaxClassName, true)
-        .attr(grabAreaFixAttrs);
-    grabAreaMax.attr({
-        x: constants.grabAreaMaxOffset,
-        height: opts._height
-    });
+  slideBox.attr({ height: opts._height, fill: constants.slideBoxFill });
+}
+
+function drawGrabbers(rangeSlider, gd, axisOpts, opts) {
+  // 
+  var grabberMin = rangeSlider
+    .selectAll("g." + constants.grabberMinClassName)
+    .data([0]);
+  grabberMin.enter().append("g").classed(constants.grabberMinClassName, true);
+
+  var grabberMax = rangeSlider
+    .selectAll("g." + constants.grabberMaxClassName)
+    .data([0]);
+  grabberMax.enter().append("g").classed(constants.grabberMaxClassName, true);
+
+  // 
+  var handleFixAttrs = {
+    x: 0,
+    width: constants.handleWidth,
+    rx: constants.handleRadius,
+    fill: constants.handleFill,
+    stroke: constants.handleStroke,
+    "shape-rendering": "crispEdges"
+  };
+
+  var handleDynamicAttrs = { y: opts._height / 4, height: opts._height / 2 };
+
+  var handleMin = grabberMin
+    .selectAll("rect." + constants.handleMinClassName)
+    .data([0]);
+  handleMin
+    .enter()
+    .append("rect")
+    .classed(constants.handleMinClassName, true)
+    .attr(handleFixAttrs);
+  handleMin.attr(handleDynamicAttrs);
+
+  var handleMax = grabberMax
+    .selectAll("rect." + constants.handleMaxClassName)
+    .data([0]);
+  handleMax
+    .enter()
+    .append("rect")
+    .classed(constants.handleMaxClassName, true)
+    .attr(handleFixAttrs);
+  handleMax.attr(handleDynamicAttrs);
+
+  // 
+  if (gd._context.staticPlot) return;
+
+  var grabAreaFixAttrs = {
+    width: constants.grabAreaWidth,
+    y: 0,
+    fill: constants.grabAreaFill,
+    cursor: constants.grabAreaCursor
+  };
+
+  var grabAreaMin = grabberMin
+    .selectAll("rect." + constants.grabAreaMinClassName)
+    .data([0]);
+  grabAreaMin
+    .enter()
+    .append("rect")
+    .classed(constants.grabAreaMinClassName, true)
+    .attr(grabAreaFixAttrs);
+  grabAreaMin.attr({ x: constants.grabAreaMinOffset, height: opts._height });
+
+  var grabAreaMax = grabberMax
+    .selectAll("rect." + constants.grabAreaMaxClassName)
+    .data([0]);
+  grabAreaMax
+    .enter()
+    .append("rect")
+    .classed(constants.grabAreaMaxClassName, true)
+    .attr(grabAreaFixAttrs);
+  grabAreaMax.attr({ x: constants.grabAreaMaxOffset, height: opts._height });
 }
 
 function clearPushMargins(gd) {
-    var pushMargins = gd._fullLayout._pushmargin || {},
-        keys = Object.keys(pushMargins);
+  var pushMargins = gd._fullLayout._pushmargin || {},
+    keys = Object.keys(pushMargins);
 
-    for(var i = 0; i < keys.length; i++) {
-        var k = keys[i];
+  for (var i = 0; i < keys.length; i++) {
+    var k = keys[i];
 
-        if(k.indexOf(constants.name) !== -1) {
-            Plots.autoMargin(gd, k);
-        }
+    if (k.indexOf(constants.name) !== -1) {
+      Plots.autoMargin(gd, k);
     }
+  }
 }
diff --git a/src/components/rangeslider/index.js b/src/components/rangeslider/index.js
index 2d29e3b16fd..2181ff19749 100644
--- a/src/components/rangeslider/index.js
+++ b/src/components/rangeslider/index.js
@@ -5,21 +5,12 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 module.exports = {
-    moduleType: 'component',
-    name: 'rangeslider',
-
-    schema: {
-        layout: {
-            'xaxis.rangeslider': require('./attributes')
-        }
-    },
-
-    layoutAttributes: require('./attributes'),
-    handleDefaults: require('./defaults'),
-
-    draw: require('./draw')
+  moduleType: "component",
+  name: "rangeslider",
+  schema: { layout: { "xaxis.rangeslider": require("./attributes") } },
+  layoutAttributes: require("./attributes"),
+  handleDefaults: require("./defaults"),
+  draw: require("./draw")
 };
diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js
index a8974f2825e..e2a2dcccaca 100644
--- a/src/components/shapes/attributes.js
+++ b/src/components/shapes/attributes.js
@@ -5,161 +5,142 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var annAttrs = require('../annotations/attributes');
-var scatterAttrs = require('../../traces/scatter/attributes');
-var extendFlat = require('../../lib/extend').extendFlat;
+"use strict";
+var annAttrs = require("../annotations/attributes");
+var scatterAttrs = require("../../traces/scatter/attributes");
+var extendFlat = require("../../lib/extend").extendFlat;
 
 var scatterLineAttrs = scatterAttrs.line;
 
 module.exports = {
-    _isLinkedToArray: 'shape',
-
-    visible: {
-        valType: 'boolean',
-        role: 'info',
-        dflt: true,
-        description: [
-            'Determines whether or not this shape is visible.'
-        ].join(' ')
-    },
-
-    type: {
-        valType: 'enumerated',
-        values: ['circle', 'rect', 'path', 'line'],
-        role: 'info',
-        description: [
-            'Specifies the shape type to be drawn.',
-
-            'If *line*, a line is drawn from (`x0`,`y0`) to (`x1`,`y1`)',
-
-            'If *circle*, a circle is drawn from',
-            '((`x0`+`x1`)/2, (`y0`+`y1`)/2))',
-            'with radius',
-            '(|(`x0`+`x1`)/2 - `x0`|, |(`y0`+`y1`)/2 -`y0`)|)',
-
-            'If *rect*, a rectangle is drawn linking',
-            '(`x0`,`y0`), (`x1`,`y0`), (`x1`,`y1`), (`x0`,`y1`), (`x0`,`y0`)',
-
-            'If *path*, draw a custom SVG path using `path`.'
-        ].join(' ')
-    },
-
-    layer: {
-        valType: 'enumerated',
-        values: ['below', 'above'],
-        dflt: 'above',
-        role: 'info',
-        description: 'Specifies whether shapes are drawn below or above traces.'
-    },
-
-    xref: extendFlat({}, annAttrs.xref, {
-        description: [
-            'Sets the shape\'s x coordinate axis.',
-            'If set to an x axis id (e.g. *x* or *x2*), the `x` position',
-            'refers to an x coordinate',
-            'If set to *paper*, the `x` position refers to the distance from',
-            'the left side of the plotting area in normalized coordinates',
-            'where *0* (*1*) corresponds to the left (right) side.',
-            'If the axis `type` is *log*, then you must take the',
-            'log of your desired range.',
-            'If the axis `type` is *date*, then you must convert',
-            'the date to unix time in milliseconds.'
-        ].join(' ')
-    }),
-    x0: {
-        valType: 'any',
-        role: 'info',
-        description: [
-            'Sets the shape\'s starting x position.',
-            'See `type` for more info.'
-        ].join(' ')
-    },
-    x1: {
-        valType: 'any',
-        role: 'info',
-        description: [
-            'Sets the shape\'s end x position.',
-            'See `type` for more info.'
-        ].join(' ')
-    },
-
-    yref: extendFlat({}, annAttrs.yref, {
-        description: [
-            'Sets the annotation\'s y coordinate axis.',
-            'If set to an y axis id (e.g. *y* or *y2*), the `y` position',
-            'refers to an y coordinate',
-            'If set to *paper*, the `y` position refers to the distance from',
-            'the bottom of the plotting area in normalized coordinates',
-            'where *0* (*1*) corresponds to the bottom (top).'
-        ].join(' ')
-    }),
-    y0: {
-        valType: 'any',
-        role: 'info',
-        description: [
-            'Sets the shape\'s starting y position.',
-            'See `type` for more info.'
-        ].join(' ')
-    },
-    y1: {
-        valType: 'any',
-        role: 'info',
-        description: [
-            'Sets the shape\'s end y position.',
-            'See `type` for more info.'
-        ].join(' ')
-    },
-
-    path: {
-        valType: 'string',
-        role: 'info',
-        description: [
-            'For `type` *path* - a valid SVG path but with the pixel values',
-            'replaced by data values. There are a few restrictions / quirks',
-            'only absolute instructions, not relative. So the allowed segments',
-            'are: M, L, H, V, Q, C, T, S, and Z',
-            'arcs (A) are not allowed because radius rx and ry are relative.',
-
-            'In the future we could consider supporting relative commands,',
-            'but we would have to decide on how to handle date and log axes.',
-            'Note that even as is, Q and C Bezier paths that are smooth on',
-            'linear axes may not be smooth on log, and vice versa.',
-            'no chained "polybezier" commands - specify the segment type for',
-            'each one.',
-
-            'On category axes, values are numbers scaled to the serial numbers',
-            'of categories because using the categories themselves there would',
-            'be no way to describe fractional positions',
-            'On data axes: because space and T are both normal components of path',
-            'strings, we can\'t use either to separate date from time parts.',
-            'Therefore we\'ll use underscore for this purpose:',
-            '2015-02-21_13:45:56.789'
-        ].join(' ')
-    },
-
-    opacity: {
-        valType: 'number',
-        min: 0,
-        max: 1,
-        dflt: 1,
-        role: 'info',
-        description: 'Sets the opacity of the shape.'
-    },
-    line: {
-        color: scatterLineAttrs.color,
-        width: scatterLineAttrs.width,
-        dash: scatterLineAttrs.dash,
-        role: 'info'
-    },
-    fillcolor: {
-        valType: 'color',
-        dflt: 'rgba(0,0,0,0)',
-        role: 'info',
-        description: [
-            'Sets the color filling the shape\'s interior.'
-        ].join(' ')
-    }
+  _isLinkedToArray: "shape",
+  visible: {
+    valType: "boolean",
+    role: "info",
+    dflt: true,
+    description: ["Determines whether or not this shape is visible."].join(" ")
+  },
+  type: {
+    valType: "enumerated",
+    values: ["circle", "rect", "path", "line"],
+    role: "info",
+    description: [
+      "Specifies the shape type to be drawn.",
+      "If *line*, a line is drawn from (`x0`,`y0`) to (`x1`,`y1`)",
+      "If *circle*, a circle is drawn from",
+      "((`x0`+`x1`)/2, (`y0`+`y1`)/2))",
+      "with radius",
+      "(|(`x0`+`x1`)/2 - `x0`|, |(`y0`+`y1`)/2 -`y0`)|)",
+      "If *rect*, a rectangle is drawn linking",
+      "(`x0`,`y0`), (`x1`,`y0`), (`x1`,`y1`), (`x0`,`y1`), (`x0`,`y0`)",
+      "If *path*, draw a custom SVG path using `path`."
+    ].join(" ")
+  },
+  layer: {
+    valType: "enumerated",
+    values: ["below", "above"],
+    dflt: "above",
+    role: "info",
+    description: "Specifies whether shapes are drawn below or above traces."
+  },
+  xref: extendFlat({}, annAttrs.xref, {
+    description: [
+      "Sets the shape's x coordinate axis.",
+      "If set to an x axis id (e.g. *x* or *x2*), the `x` position",
+      "refers to an x coordinate",
+      "If set to *paper*, the `x` position refers to the distance from",
+      "the left side of the plotting area in normalized coordinates",
+      "where *0* (*1*) corresponds to the left (right) side.",
+      "If the axis `type` is *log*, then you must take the",
+      "log of your desired range.",
+      "If the axis `type` is *date*, then you must convert",
+      "the date to unix time in milliseconds."
+    ].join(" ")
+  }),
+  x0: {
+    valType: "any",
+    role: "info",
+    description: [
+      "Sets the shape's starting x position.",
+      "See `type` for more info."
+    ].join(" ")
+  },
+  x1: {
+    valType: "any",
+    role: "info",
+    description: [
+      "Sets the shape's end x position.",
+      "See `type` for more info."
+    ].join(" ")
+  },
+  yref: extendFlat({}, annAttrs.yref, {
+    description: [
+      "Sets the annotation's y coordinate axis.",
+      "If set to an y axis id (e.g. *y* or *y2*), the `y` position",
+      "refers to an y coordinate",
+      "If set to *paper*, the `y` position refers to the distance from",
+      "the bottom of the plotting area in normalized coordinates",
+      "where *0* (*1*) corresponds to the bottom (top)."
+    ].join(" ")
+  }),
+  y0: {
+    valType: "any",
+    role: "info",
+    description: [
+      "Sets the shape's starting y position.",
+      "See `type` for more info."
+    ].join(" ")
+  },
+  y1: {
+    valType: "any",
+    role: "info",
+    description: [
+      "Sets the shape's end y position.",
+      "See `type` for more info."
+    ].join(" ")
+  },
+  path: {
+    valType: "string",
+    role: "info",
+    description: [
+      "For `type` *path* - a valid SVG path but with the pixel values",
+      "replaced by data values. There are a few restrictions / quirks",
+      "only absolute instructions, not relative. So the allowed segments",
+      "are: M, L, H, V, Q, C, T, S, and Z",
+      "arcs (A) are not allowed because radius rx and ry are relative.",
+      "In the future we could consider supporting relative commands,",
+      "but we would have to decide on how to handle date and log axes.",
+      "Note that even as is, Q and C Bezier paths that are smooth on",
+      "linear axes may not be smooth on log, and vice versa.",
+      'no chained "polybezier" commands - specify the segment type for',
+      "each one.",
+      "On category axes, values are numbers scaled to the serial numbers",
+      "of categories because using the categories themselves there would",
+      "be no way to describe fractional positions",
+      "On data axes: because space and T are both normal components of path",
+      "strings, we can't use either to separate date from time parts.",
+      "Therefore we'll use underscore for this purpose:",
+      "2015-02-21_13:45:56.789"
+    ].join(" ")
+  },
+  opacity: {
+    valType: "number",
+    min: 0,
+    max: 1,
+    dflt: 1,
+    role: "info",
+    description: "Sets the opacity of the shape."
+  },
+  line: {
+    color: scatterLineAttrs.color,
+    width: scatterLineAttrs.width,
+    dash: scatterLineAttrs.dash,
+    role: "info"
+  },
+  fillcolor: {
+    valType: "color",
+    dflt: "rgba(0,0,0,0)",
+    role: "info",
+    description: ["Sets the color filling the shape's interior."].join(" ")
+  }
 };
diff --git a/src/components/shapes/calc_autorange.js b/src/components/shapes/calc_autorange.js
index 6f88b4aad96..a115725b2cb 100644
--- a/src/components/shapes/calc_autorange.js
+++ b/src/components/shapes/calc_autorange.js
@@ -5,71 +5,78 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Lib = require("../../lib");
+var Axes = require("../../plots/cartesian/axes");
 
-
-'use strict';
-
-var Lib = require('../../lib');
-var Axes = require('../../plots/cartesian/axes');
-
-var constants = require('./constants');
-var helpers = require('./helpers');
-
+var constants = require("./constants");
+var helpers = require("./helpers");
 
 module.exports = function calcAutorange(gd) {
-    var fullLayout = gd._fullLayout,
-        shapeList = Lib.filterVisible(fullLayout.shapes);
-
-    if(!shapeList.length || !gd._fullData.length) return;
-
-    for(var i = 0; i < shapeList.length; i++) {
-        var shape = shapeList[i],
-            ppad = shape.line.width / 2;
-
-        var ax, bounds;
-
-        if(shape.xref !== 'paper') {
-            ax = Axes.getFromId(gd, shape.xref);
-            bounds = shapeBounds(ax, shape.x0, shape.x1, shape.path, constants.paramIsX);
-            if(bounds) Axes.expand(ax, bounds, {ppad: ppad});
-        }
+  var fullLayout = gd._fullLayout,
+    shapeList = Lib.filterVisible(fullLayout.shapes);
+
+  if (!shapeList.length || !gd._fullData.length) return;
+
+  for (var i = 0; i < shapeList.length; i++) {
+    var shape = shapeList[i], ppad = shape.line.width / 2;
+
+    var ax, bounds;
+
+    if (shape.xref !== "paper") {
+      ax = Axes.getFromId(gd, shape.xref);
+      bounds = shapeBounds(
+        ax,
+        shape.x0,
+        shape.x1,
+        shape.path,
+        constants.paramIsX
+      );
+      if (bounds) Axes.expand(ax, bounds, { ppad: ppad });
+    }
 
-        if(shape.yref !== 'paper') {
-            ax = Axes.getFromId(gd, shape.yref);
-            bounds = shapeBounds(ax, shape.y0, shape.y1, shape.path, constants.paramIsY);
-            if(bounds) Axes.expand(ax, bounds, {ppad: ppad});
-        }
+    if (shape.yref !== "paper") {
+      ax = Axes.getFromId(gd, shape.yref);
+      bounds = shapeBounds(
+        ax,
+        shape.y0,
+        shape.y1,
+        shape.path,
+        constants.paramIsY
+      );
+      if (bounds) Axes.expand(ax, bounds, { ppad: ppad });
     }
+  }
 };
 
 function shapeBounds(ax, v0, v1, path, paramsToUse) {
-    var convertVal = (ax.type === 'category') ? Number : ax.d2c;
-
-    if(v0 !== undefined) return [convertVal(v0), convertVal(v1)];
-    if(!path) return;
-
-    var min = Infinity,
-        max = -Infinity,
-        segments = path.match(constants.segmentRE),
-        i,
-        segment,
-        drawnParam,
-        params,
-        val;
-
-    if(ax.type === 'date') convertVal = helpers.decodeDate(convertVal);
-
-    for(i = 0; i < segments.length; i++) {
-        segment = segments[i];
-        drawnParam = paramsToUse[segment.charAt(0)].drawn;
-        if(drawnParam === undefined) continue;
-
-        params = segments[i].substr(1).match(constants.paramRE);
-        if(!params || params.length < drawnParam) continue;
-
-        val = convertVal(params[drawnParam]);
-        if(val < min) min = val;
-        if(val > max) max = val;
-    }
-    if(max >= min) return [min, max];
+  var convertVal = ax.type === "category" ? Number : ax.d2c;
+
+  if (v0 !== undefined) return [convertVal(v0), convertVal(v1)];
+  if (!path) return;
+
+  var min = Infinity,
+    max = -Infinity,
+    segments = path.match(constants.segmentRE),
+    i,
+    segment,
+    drawnParam,
+    params,
+    val;
+
+  if (ax.type === "date") convertVal = helpers.decodeDate(convertVal);
+
+  for (i = 0; i < segments.length; i++) {
+    segment = segments[i];
+    drawnParam = paramsToUse[segment.charAt(0)].drawn;
+    if (drawnParam === undefined) continue;
+
+    params = segments[i].substr(1).match(constants.paramRE);
+    if (!params || params.length < drawnParam) continue;
+
+    val = convertVal(params[drawnParam]);
+    if (val < min) min = val;
+    if (val > max) max = val;
+  }
+  if (max >= min) return [min, max];
 }
diff --git a/src/components/shapes/constants.js b/src/components/shapes/constants.js
index e0c009ca84e..c83fe912421 100644
--- a/src/components/shapes/constants.js
+++ b/src/components/shapes/constants.js
@@ -5,58 +5,51 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-    segmentRE: /[MLHVQCTSZ][^MLHVQCTSZ]*/g,
-    paramRE: /[^\s,]+/g,
-
-    // which numbers in each path segment are x (or y) values
-    // drawn is which param is a drawn point, as opposed to a
-    // control point (which doesn't count toward autorange.
-    // TODO: this means curved paths could extend beyond the
-    // autorange bounds. This is a bit tricky to get right
-    // unless we revert to bounding boxes, but perhaps there's
-    // a calculation we could do...)
-    paramIsX: {
-        M: {0: true, drawn: 0},
-        L: {0: true, drawn: 0},
-        H: {0: true, drawn: 0},
-        V: {},
-        Q: {0: true, 2: true, drawn: 2},
-        C: {0: true, 2: true, 4: true, drawn: 4},
-        T: {0: true, drawn: 0},
-        S: {0: true, 2: true, drawn: 2},
-        // A: {0: true, 5: true},
-        Z: {}
-    },
-
-    paramIsY: {
-        M: {1: true, drawn: 1},
-        L: {1: true, drawn: 1},
-        H: {},
-        V: {0: true, drawn: 0},
-        Q: {1: true, 3: true, drawn: 3},
-        C: {1: true, 3: true, 5: true, drawn: 5},
-        T: {1: true, drawn: 1},
-        S: {1: true, 3: true, drawn: 5},
-        // A: {1: true, 6: true},
-        Z: {}
-    },
-
-    numParams: {
-        M: 2,
-        L: 2,
-        H: 1,
-        V: 1,
-        Q: 4,
-        C: 6,
-        T: 2,
-        S: 4,
-        // A: 7,
-        Z: 0
-    }
+  segmentRE: /[MLHVQCTSZ][^MLHVQCTSZ]*/g,
+  paramRE: /[^\s,]+/g,
+  // which numbers in each path segment are x (or y) values
+  // drawn is which param is a drawn point, as opposed to a
+  // control point (which doesn't count toward autorange.
+  // TODO: this means curved paths could extend beyond the
+  // autorange bounds. This is a bit tricky to get right
+  // unless we revert to bounding boxes, but perhaps there's
+  // a calculation we could do...)
+  paramIsX: {
+    M: { 0: true, drawn: 0 },
+    L: { 0: true, drawn: 0 },
+    H: { 0: true, drawn: 0 },
+    V: {},
+    Q: { 0: true, 2: true, drawn: 2 },
+    C: { 0: true, 2: true, 4: true, drawn: 4 },
+    T: { 0: true, drawn: 0 },
+    S: { 0: true, 2: true, drawn: 2 },
+    // A: {0: true, 5: true},
+    Z: {}
+  },
+  paramIsY: {
+    M: { 1: true, drawn: 1 },
+    L: { 1: true, drawn: 1 },
+    H: {},
+    V: { 0: true, drawn: 0 },
+    Q: { 1: true, 3: true, drawn: 3 },
+    C: { 1: true, 3: true, 5: true, drawn: 5 },
+    T: { 1: true, drawn: 1 },
+    S: { 1: true, 3: true, drawn: 5 },
+    // A: {1: true, 6: true},
+    Z: {}
+  },
+  numParams: {
+    M: 2,
+    L: 2,
+    H: 1,
+    V: 1,
+    Q: 4,
+    C: 6,
+    T: 2,
+    S: 4,
+    // A: 7,
+    Z: 0
+  }
 };
diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js
index ceb3b4c0a1a..dc0848dfac2 100644
--- a/src/components/shapes/defaults.js
+++ b/src/components/shapes/defaults.js
@@ -5,19 +5,14 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var handleArrayContainerDefaults = require('../../plots/array_container_defaults');
-var handleShapeDefaults = require('./shape_defaults');
-
+"use strict";
+var handleArrayContainerDefaults = require(
+  "../../plots/array_container_defaults"
+);
+var handleShapeDefaults = require("./shape_defaults");
 
 module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) {
-    var opts = {
-        name: 'shapes',
-        handleItemDefaults: handleShapeDefaults
-    };
+  var opts = { name: "shapes", handleItemDefaults: handleShapeDefaults };
 
-    handleArrayContainerDefaults(layoutIn, layoutOut, opts);
+  handleArrayContainerDefaults(layoutIn, layoutOut, opts);
 };
diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js
index 314cd339f76..4437642731c 100644
--- a/src/components/shapes/draw.js
+++ b/src/components/shapes/draw.js
@@ -5,26 +5,22 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var isNumeric = require("fast-isnumeric");
 
+var Plotly = require("../../plotly");
+var Lib = require("../../lib");
+var Axes = require("../../plots/cartesian/axes");
+var Color = require("../color");
+var Drawing = require("../drawing");
 
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-
-var Plotly = require('../../plotly');
-var Lib = require('../../lib');
-var Axes = require('../../plots/cartesian/axes');
-var Color = require('../color');
-var Drawing = require('../drawing');
-
-var dragElement = require('../dragelement');
-var setCursor = require('../../lib/setcursor');
-
-var constants = require('./constants');
-var helpers = require('./helpers');
-var handleShapeDefaults = require('./shape_defaults');
-var supplyLayoutDefaults = require('./defaults');
+var dragElement = require("../dragelement");
+var setCursor = require("../../lib/setcursor");
 
+var constants = require("./constants");
+var helpers = require("./helpers");
+var handleShapeDefaults = require("./shape_defaults");
+var supplyLayoutDefaults = require("./defaults");
 
 // Shapes are stored in gd.layout.shapes, an array of objects
 // index can point to one item in this array,
@@ -34,532 +30,554 @@ var supplyLayoutDefaults = require('./defaults');
 //  or undefined to simply redraw
 // if opt is blank, val can be 'add' or a full options object to add a new
 //  annotation at that point in the array, or 'remove' to delete this one
-
-module.exports = {
-    draw: draw,
-    drawOne: drawOne
-};
+module.exports = { draw: draw, drawOne: drawOne };
 
 function draw(gd) {
-    var fullLayout = gd._fullLayout;
+  var fullLayout = gd._fullLayout;
 
-    // Remove previous shapes before drawing new in shapes in fullLayout.shapes
-    fullLayout._shapeUpperLayer.selectAll('path').remove();
-    fullLayout._shapeLowerLayer.selectAll('path').remove();
-    fullLayout._shapeSubplotLayer.selectAll('path').remove();
+  // Remove previous shapes before drawing new in shapes in fullLayout.shapes
+  fullLayout._shapeUpperLayer.selectAll("path").remove();
+  fullLayout._shapeLowerLayer.selectAll("path").remove();
+  fullLayout._shapeSubplotLayer.selectAll("path").remove();
 
-    for(var i = 0; i < fullLayout.shapes.length; i++) {
-        if(fullLayout.shapes[i].visible) {
-            drawOne(gd, i);
-        }
+  for (var i = 0; i < fullLayout.shapes.length; i++) {
+    if (fullLayout.shapes[i].visible) {
+      drawOne(gd, i);
     }
-
-    // may need to resurrect this if we put text (LaTeX) in shapes
-    // return Plots.previousPromises(gd);
+  }
+  // may need to resurrect this if we put text (LaTeX) in shapes
+  // return Plots.previousPromises(gd);
 }
 
 function drawOne(gd, index, opt, value) {
-    if(!isNumeric(index) || index === -1) {
-
-        // no index provided - we're operating on ALL shapes
-        if(!index && Array.isArray(value)) {
-            replaceAllShapes(gd, value);
-            return;
-        }
-        else if(value === 'remove') {
-            deleteAllShapes(gd);
-            return;
-        }
-        else if(opt && value !== 'add') {
-            updateAllShapes(gd, opt, value);
-            return;
-        }
-        else {
-            // add a new empty annotation
-            index = gd._fullLayout.shapes.length;
-            gd._fullLayout.shapes.push({});
-        }
+  if (!isNumeric(index) || index === -1) {
+    // no index provided - we're operating on ALL shapes
+    if (!index && Array.isArray(value)) {
+      replaceAllShapes(gd, value);
+      return;
+    } else if (value === "remove") {
+      deleteAllShapes(gd);
+      return;
+    } else if (opt && value !== "add") {
+      updateAllShapes(gd, opt, value);
+      return;
+    } else {
+      // add a new empty annotation
+      index = gd._fullLayout.shapes.length;
+      gd._fullLayout.shapes.push({});
     }
-
-    if(!opt && value) {
-        if(value === 'remove') {
-            deleteShape(gd, index);
-            return;
-        }
-        else if(value === 'add' || Lib.isPlainObject(value)) {
-            insertShape(gd, index, value);
-        }
+  }
+
+  if (!opt && value) {
+    if (value === "remove") {
+      deleteShape(gd, index);
+      return;
+    } else if (value === "add" || Lib.isPlainObject(value)) {
+      insertShape(gd, index, value);
     }
+  }
 
-    updateShape(gd, index, opt, value);
+  updateShape(gd, index, opt, value);
 }
 
 function replaceAllShapes(gd, newShapes) {
-    gd.layout.shapes = newShapes;
-    supplyLayoutDefaults(gd.layout, gd._fullLayout);
-    draw(gd);
+  gd.layout.shapes = newShapes;
+  supplyLayoutDefaults(gd.layout, gd._fullLayout);
+  draw(gd);
 }
 
 function deleteAllShapes(gd) {
-    delete gd.layout.shapes;
-    gd._fullLayout.shapes = [];
-    draw(gd);
+  delete gd.layout.shapes;
+  gd._fullLayout.shapes = [];
+  draw(gd);
 }
 
 function updateAllShapes(gd, opt, value) {
-    for(var i = 0; i < gd._fullLayout.shapes.length; i++) {
-        drawOne(gd, i, opt, value);
-    }
+  for (var i = 0; i < gd._fullLayout.shapes.length; i++) {
+    drawOne(gd, i, opt, value);
+  }
 }
 
 function deleteShape(gd, index) {
-    getShapeLayer(gd, index)
-        .selectAll('[data-index="' + index + '"]')
-        .remove();
+  getShapeLayer(gd, index).selectAll('[data-index="' + index + '"]').remove();
 
-    gd._fullLayout.shapes.splice(index, 1);
+  gd._fullLayout.shapes.splice(index, 1);
 
-    gd.layout.shapes.splice(index, 1);
+  gd.layout.shapes.splice(index, 1);
 
-    for(var i = index; i < gd._fullLayout.shapes.length; i++) {
-        // redraw all shapes past the removed one,
-        // so they bind to the right events
-        getShapeLayer(gd, i)
-            .selectAll('[data-index="' + (i + 1) + '"]')
-            .attr('data-index', i);
-        drawOne(gd, i);
-    }
+  for (var i = index; i < gd._fullLayout.shapes.length; i++) {
+    // redraw all shapes past the removed one,
+    // so they bind to the right events
+    getShapeLayer(gd, i)
+      .selectAll('[data-index="' + (i + 1) + '"]')
+      .attr("data-index", i);
+    drawOne(gd, i);
+  }
 }
 
 function insertShape(gd, index, newShape) {
-    gd._fullLayout.shapes.splice(index, 0, {});
-
-    var rule = Lib.isPlainObject(newShape) ?
-        Lib.extendFlat({}, newShape) :
-        {text: 'New text'};
-
-    if(gd.layout.shapes) {
-        gd.layout.shapes.splice(index, 0, rule);
-    } else {
-        gd.layout.shapes = [rule];
-    }
-
-    // there is no need to call shapes.draw(gd, index),
-    // because updateShape() is called from within shapes.draw()
-
-    for(var i = gd._fullLayout.shapes.length - 1; i > index; i--) {
-        getShapeLayer(gd, i)
-            .selectAll('[data-index="' + (i - 1) + '"]')
-            .attr('data-index', i);
-        drawOne(gd, i);
-    }
+  gd._fullLayout.shapes.splice(index, 0, {});
+
+  var rule = Lib.isPlainObject(newShape)
+    ? Lib.extendFlat({}, newShape)
+    : { text: "New text" };
+
+  if (gd.layout.shapes) {
+    gd.layout.shapes.splice(index, 0, rule);
+  } else {
+    gd.layout.shapes = [rule];
+  }
+
+  // there is no need to call shapes.draw(gd, index),
+  // because updateShape() is called from within shapes.draw()
+  for (var i = gd._fullLayout.shapes.length - 1; i > index; i--) {
+    getShapeLayer(gd, i)
+      .selectAll('[data-index="' + (i - 1) + '"]')
+      .attr("data-index", i);
+    drawOne(gd, i);
+  }
 }
 
 function updateShape(gd, index, opt, value) {
-    var i, n;
-
-    // remove the existing shape if there is one
-    getShapeLayer(gd, index)
-        .selectAll('[data-index="' + index + '"]')
-        .remove();
-
-    // remember a few things about what was already there,
-    var optionsIn = gd.layout.shapes[index];
-
-    // (from annos...) not sure how we're getting here... but C12 is seeing a bug
-    // where we fail here when they add/remove annotations
-    // TODO: clean this up and remove it.
-    if(!optionsIn) return;
-
-    // alter the input shape as requested
-    var optionsEdit = {};
-    if(typeof opt === 'string' && opt) optionsEdit[opt] = value;
-    else if(Lib.isPlainObject(opt)) optionsEdit = opt;
-
-    var optionKeys = Object.keys(optionsEdit);
-    for(i = 0; i < optionKeys.length; i++) {
-        var k = optionKeys[i];
-        Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]);
+  var i, n;
+
+  // remove the existing shape if there is one
+  getShapeLayer(gd, index).selectAll('[data-index="' + index + '"]').remove();
+
+  // remember a few things about what was already there,
+  var optionsIn = gd.layout.shapes[index];
+
+  // (from annos...) not sure how we're getting here... but C12 is seeing a bug
+  // where we fail here when they add/remove annotations
+  // TODO: clean this up and remove it.
+  if (!optionsIn) return;
+
+  // alter the input shape as requested
+  var optionsEdit = {};
+  if (typeof opt === "string" && opt) optionsEdit[opt] = value;
+  else if (Lib.isPlainObject(opt)) optionsEdit = opt;
+
+  var optionKeys = Object.keys(optionsEdit);
+  for (i = 0; i < optionKeys.length; i++) {
+    var k = optionKeys[i];
+    Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]);
+  }
+
+  // return early in visible: false updates
+  if (optionsIn.visible === false) return;
+
+  var oldRef = { xref: optionsIn.xref, yref: optionsIn.yref },
+    posAttrs = ["x0", "x1", "y0", "y1"];
+
+  for (i = 0; i < 4; i++) {
+    var posAttr = posAttrs[i];
+    // if we don't have an explicit position already,
+    // don't set one just because we're changing references
+    // or axis type.
+    // the defaults will be consistent most of the time anyway,
+    // except in log/linear changes
+    if (
+      optionsEdit[posAttr] !== undefined || optionsIn[posAttr] === undefined
+    ) {
+      continue;
     }
 
-    // return early in visible: false updates
-    if(optionsIn.visible === false) return;
-
-    var oldRef = {xref: optionsIn.xref, yref: optionsIn.yref},
-        posAttrs = ['x0', 'x1', 'y0', 'y1'];
-
-    for(i = 0; i < 4; i++) {
-        var posAttr = posAttrs[i];
-        // if we don't have an explicit position already,
-        // don't set one just because we're changing references
-        // or axis type.
-        // the defaults will be consistent most of the time anyway,
-        // except in log/linear changes
-        if(optionsEdit[posAttr] !== undefined ||
-                optionsIn[posAttr] === undefined) {
-            continue;
-        }
-
-        var axLetter = posAttr.charAt(0),
-            axOld = Axes.getFromId(gd,
-                Axes.coerceRef(oldRef, {}, gd, axLetter, '', 'paper')),
-            axNew = Axes.getFromId(gd,
-                Axes.coerceRef(optionsIn, {}, gd, axLetter, '', 'paper')),
-            position = optionsIn[posAttr],
-            rangePosition;
-
-        if(optionsEdit[axLetter + 'ref'] !== undefined) {
-            // first convert to fraction of the axis
-            if(axOld) {
-                rangePosition = helpers.shapePositionToRange(axOld)(position);
-                position = axOld.r2fraction(rangePosition);
-            } else {
-                position = (position - axNew.domain[0]) /
-                    (axNew.domain[1] - axNew.domain[0]);
-            }
-
-            if(axNew) {
-                // then convert to new data coordinates at the same fraction
-                rangePosition = axNew.fraction2r(position);
-                position = helpers.rangeToShapePosition(axNew)(rangePosition);
-            } else {
-                // or scale to the whole plot
-                position = axOld.domain[0] +
-                    position * (axOld.domain[1] - axOld.domain[0]);
-            }
-        }
-
-        optionsIn[posAttr] = position;
+    var axLetter = posAttr.charAt(0),
+      axOld = Axes.getFromId(
+        gd,
+        Axes.coerceRef(oldRef, {}, gd, axLetter, "", "paper")
+      ),
+      axNew = Axes.getFromId(
+        gd,
+        Axes.coerceRef(optionsIn, {}, gd, axLetter, "", "paper")
+      ),
+      position = optionsIn[posAttr],
+      rangePosition;
+
+    if (optionsEdit[axLetter + "ref"] !== undefined) {
+      // first convert to fraction of the axis
+      if (axOld) {
+        rangePosition = helpers.shapePositionToRange(axOld)(position);
+        position = axOld.r2fraction(rangePosition);
+      } else {
+        position = (position - axNew.domain[0]) /
+          (axNew.domain[1] - axNew.domain[0]);
+      }
+
+      if (axNew) {
+        // then convert to new data coordinates at the same fraction
+        rangePosition = axNew.fraction2r(position);
+        position = helpers.rangeToShapePosition(axNew)(rangePosition);
+      } else {
+        // or scale to the whole plot
+        position = axOld.domain[0] +
+          position * (axOld.domain[1] - axOld.domain[0]);
+      }
     }
 
-    var options = {};
-    handleShapeDefaults(optionsIn, options, gd._fullLayout);
-    gd._fullLayout.shapes[index] = options;
-
-    var clipAxes;
-    if(options.layer !== 'below') {
-        clipAxes = (options.xref + options.yref).replace(/paper/g, '');
-        drawShape(gd._fullLayout._shapeUpperLayer);
+    optionsIn[posAttr] = position;
+  }
+
+  var options = {};
+  handleShapeDefaults(optionsIn, options, gd._fullLayout);
+  gd._fullLayout.shapes[index] = options;
+
+  var clipAxes;
+  if (options.layer !== "below") {
+    clipAxes = (options.xref + options.yref).replace(/paper/g, "");
+    drawShape(gd._fullLayout._shapeUpperLayer);
+  } else if (options.xref === "paper" && options.yref === "paper") {
+    clipAxes = "";
+    drawShape(gd._fullLayout._shapeLowerLayer);
+  } else {
+    var plots = gd._fullLayout._plots || {},
+      subplots = Object.keys(plots),
+      plotinfo;
+
+    for (i = 0, n = subplots.length; i < n; i++) {
+      plotinfo = plots[subplots[i]];
+      clipAxes = subplots[i];
+
+      if (isShapeInSubplot(gd, options, plotinfo)) {
+        drawShape(plotinfo.shapelayer);
+      }
     }
-    else if(options.xref === 'paper' && options.yref === 'paper') {
-        clipAxes = '';
-        drawShape(gd._fullLayout._shapeLowerLayer);
-    }
-    else {
-        var plots = gd._fullLayout._plots || {},
-            subplots = Object.keys(plots),
-            plotinfo;
-
-        for(i = 0, n = subplots.length; i < n; i++) {
-            plotinfo = plots[subplots[i]];
-            clipAxes = subplots[i];
-
-            if(isShapeInSubplot(gd, options, plotinfo)) {
-                drawShape(plotinfo.shapelayer);
-            }
-        }
+  }
+
+  function drawShape(shapeLayer) {
+    var attrs = {
+      "data-index": index,
+      "fill-rule": "evenodd",
+      d: getPathString(gd, options)
+    },
+      lineColor = options.line.width ? options.line.color : "rgba(0,0,0,0)";
+
+    var path = shapeLayer
+      .append("path")
+      .attr(attrs)
+      .style("opacity", options.opacity)
+      .call(Color.stroke, lineColor)
+      .call(Color.fill, options.fillcolor)
+      .call(Drawing.dashLine, options.line.dash, options.line.width);
+
+    if (clipAxes) {
+      path.call(Drawing.setClipUrl, "clip" + gd._fullLayout._uid + clipAxes);
     }
 
-    function drawShape(shapeLayer) {
-        var attrs = {
-                'data-index': index,
-                'fill-rule': 'evenodd',
-                d: getPathString(gd, options)
-            },
-            lineColor = options.line.width ?
-                options.line.color : 'rgba(0,0,0,0)';
-
-        var path = shapeLayer.append('path')
-            .attr(attrs)
-            .style('opacity', options.opacity)
-            .call(Color.stroke, lineColor)
-            .call(Color.fill, options.fillcolor)
-            .call(Drawing.dashLine, options.line.dash, options.line.width);
-
-        if(clipAxes) {
-            path.call(Drawing.setClipUrl,
-                'clip' + gd._fullLayout._uid + clipAxes);
-        }
-
-        if(gd._context.editable) setupDragElement(gd, path, options, index);
-    }
+    if (gd._context.editable) setupDragElement(gd, path, options, index);
+  }
 }
 
 function setupDragElement(gd, shapePath, shapeOptions, index) {
-    var MINWIDTH = 10,
-        MINHEIGHT = 10;
-
-    var update;
-    var x0, y0, x1, y1, astrX0, astrY0, astrX1, astrY1;
-    var n0, s0, w0, e0, astrN, astrS, astrW, astrE, optN, optS, optW, optE;
-    var pathIn, astrPath;
-
-    var xa, ya, x2p, y2p, p2x, p2y;
-
-    var dragOptions = {
-            setCursor: updateDragMode,
-            element: shapePath.node(),
-            prepFn: startDrag,
-            doneFn: endDrag
-        },
-        dragBBox = dragOptions.element.getBoundingClientRect(),
-        dragMode;
-
-    dragElement.init(dragOptions);
-
-    function updateDragMode(evt) {
-        // choose 'move' or 'resize'
-        // based on initial position of cursor within the drag element
-        var w = dragBBox.right - dragBBox.left,
-            h = dragBBox.bottom - dragBBox.top,
-            x = evt.clientX - dragBBox.left,
-            y = evt.clientY - dragBBox.top,
-            cursor = (w > MINWIDTH && h > MINHEIGHT && !evt.shiftKey) ?
-                dragElement.getCursor(x / w, 1 - y / h) :
-                'move';
-
-        setCursor(shapePath, cursor);
-
-        // possible values 'move', 'sw', 'w', 'se', 'e', 'ne', 'n', 'nw' and 'w'
-        dragMode = cursor.split('-')[0];
-    }
-
-    function startDrag(evt) {
-        // setup conversion functions
-        xa = Axes.getFromId(gd, shapeOptions.xref);
-        ya = Axes.getFromId(gd, shapeOptions.yref);
-
-        x2p = helpers.getDataToPixel(gd, xa);
-        y2p = helpers.getDataToPixel(gd, ya, true);
-        p2x = helpers.getPixelToData(gd, xa);
-        p2y = helpers.getPixelToData(gd, ya, true);
-
-        // setup update strings and initial values
-        var astr = 'shapes[' + index + ']';
-        if(shapeOptions.type === 'path') {
-            pathIn = shapeOptions.path;
-            astrPath = astr + '.path';
-        }
-        else {
-            x0 = x2p(shapeOptions.x0);
-            y0 = y2p(shapeOptions.y0);
-            x1 = x2p(shapeOptions.x1);
-            y1 = y2p(shapeOptions.y1);
-
-            astrX0 = astr + '.x0';
-            astrY0 = astr + '.y0';
-            astrX1 = astr + '.x1';
-            astrY1 = astr + '.y1';
-        }
-
-        if(x0 < x1) {
-            w0 = x0; astrW = astr + '.x0'; optW = 'x0';
-            e0 = x1; astrE = astr + '.x1'; optE = 'x1';
-        }
-        else {
-            w0 = x1; astrW = astr + '.x1'; optW = 'x1';
-            e0 = x0; astrE = astr + '.x0'; optE = 'x0';
-        }
-        if(y0 < y1) {
-            n0 = y0; astrN = astr + '.y0'; optN = 'y0';
-            s0 = y1; astrS = astr + '.y1'; optS = 'y1';
-        }
-        else {
-            n0 = y1; astrN = astr + '.y1'; optN = 'y1';
-            s0 = y0; astrS = astr + '.y0'; optS = 'y0';
-        }
-
-        update = {};
-
-        // setup dragMode and the corresponding handler
-        updateDragMode(evt);
-        dragOptions.moveFn = (dragMode === 'move') ? moveShape : resizeShape;
+  var MINWIDTH = 10, MINHEIGHT = 10;
+
+  var update;
+  var x0, y0, x1, y1, astrX0, astrY0, astrX1, astrY1;
+  var n0, s0, w0, e0, astrN, astrS, astrW, astrE, optN, optS, optW, optE;
+  var pathIn, astrPath;
+
+  var xa, ya, x2p, y2p, p2x, p2y;
+
+  var dragOptions = {
+    setCursor: updateDragMode,
+    element: shapePath.node(),
+    prepFn: startDrag,
+    doneFn: endDrag
+  },
+    dragBBox = dragOptions.element.getBoundingClientRect(),
+    dragMode;
+
+  dragElement.init(dragOptions);
+
+  function updateDragMode(evt) {
+    // choose 'move' or 'resize'
+    // based on initial position of cursor within the drag element
+    var w = dragBBox.right - dragBBox.left,
+      h = dragBBox.bottom - dragBBox.top,
+      x = evt.clientX - dragBBox.left,
+      y = evt.clientY - dragBBox.top,
+      cursor = w > MINWIDTH && h > MINHEIGHT && !evt.shiftKey
+        ? dragElement.getCursor(x / w, 1 - y / h)
+        : "move";
+
+    setCursor(shapePath, cursor);
+
+    // possible values 'move', 'sw', 'w', 'se', 'e', 'ne', 'n', 'nw' and 'w'
+    dragMode = cursor.split("-")[0];
+  }
+
+  function startDrag(evt) {
+    // setup conversion functions
+    xa = Axes.getFromId(gd, shapeOptions.xref);
+    ya = Axes.getFromId(gd, shapeOptions.yref);
+
+    x2p = helpers.getDataToPixel(gd, xa);
+    y2p = helpers.getDataToPixel(gd, ya, true);
+    p2x = helpers.getPixelToData(gd, xa);
+    p2y = helpers.getPixelToData(gd, ya, true);
+
+    // setup update strings and initial values
+    var astr = "shapes[" + index + "]";
+    if (shapeOptions.type === "path") {
+      pathIn = shapeOptions.path;
+      astrPath = astr + ".path";
+    } else {
+      x0 = x2p(shapeOptions.x0);
+      y0 = y2p(shapeOptions.y0);
+      x1 = x2p(shapeOptions.x1);
+      y1 = y2p(shapeOptions.y1);
+
+      astrX0 = astr + ".x0";
+      astrY0 = astr + ".y0";
+      astrX1 = astr + ".x1";
+      astrY1 = astr + ".y1";
     }
 
-    function endDrag(dragged) {
-        setCursor(shapePath);
-        if(dragged) {
-            Plotly.relayout(gd, update);
-        }
+    if (x0 < x1) {
+      w0 = x0;
+      astrW = astr + ".x0";
+      optW = "x0";
+      e0 = x1;
+      astrE = astr + ".x1";
+      optE = "x1";
+    } else {
+      w0 = x1;
+      astrW = astr + ".x1";
+      optW = "x1";
+      e0 = x0;
+      astrE = astr + ".x0";
+      optE = "x0";
     }
-
-    function moveShape(dx, dy) {
-        if(shapeOptions.type === 'path') {
-            var moveX = function moveX(x) { return p2x(x2p(x) + dx); };
-            if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX);
-
-            var moveY = function moveY(y) { return p2y(y2p(y) + dy); };
-            if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY);
-
-            shapeOptions.path = movePath(pathIn, moveX, moveY);
-            update[astrPath] = shapeOptions.path;
-        }
-        else {
-            update[astrX0] = shapeOptions.x0 = p2x(x0 + dx);
-            update[astrY0] = shapeOptions.y0 = p2y(y0 + dy);
-            update[astrX1] = shapeOptions.x1 = p2x(x1 + dx);
-            update[astrY1] = shapeOptions.y1 = p2y(y1 + dy);
-        }
-
-        shapePath.attr('d', getPathString(gd, shapeOptions));
+    if (y0 < y1) {
+      n0 = y0;
+      astrN = astr + ".y0";
+      optN = "y0";
+      s0 = y1;
+      astrS = astr + ".y1";
+      optS = "y1";
+    } else {
+      n0 = y1;
+      astrN = astr + ".y1";
+      optN = "y1";
+      s0 = y0;
+      astrS = astr + ".y0";
+      optS = "y0";
     }
 
-    function resizeShape(dx, dy) {
-        if(shapeOptions.type === 'path') {
-            // TODO: implement path resize
-            var moveX = function moveX(x) { return p2x(x2p(x) + dx); };
-            if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX);
-
-            var moveY = function moveY(y) { return p2y(y2p(y) + dy); };
-            if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY);
-
-            shapeOptions.path = movePath(pathIn, moveX, moveY);
-            update[astrPath] = shapeOptions.path;
-        }
-        else {
-            var newN = (~dragMode.indexOf('n')) ? n0 + dy : n0,
-                newS = (~dragMode.indexOf('s')) ? s0 + dy : s0,
-                newW = (~dragMode.indexOf('w')) ? w0 + dx : w0,
-                newE = (~dragMode.indexOf('e')) ? e0 + dx : e0;
-
-            if(newS - newN > MINHEIGHT) {
-                update[astrN] = shapeOptions[optN] = p2y(newN);
-                update[astrS] = shapeOptions[optS] = p2y(newS);
-            }
-
-            if(newE - newW > MINWIDTH) {
-                update[astrW] = shapeOptions[optW] = p2x(newW);
-                update[astrE] = shapeOptions[optE] = p2x(newE);
-            }
-        }
-
-        shapePath.attr('d', getPathString(gd, shapeOptions));
-    }
-}
+    update = {};
 
-function getShapeLayer(gd, index) {
-    var shape = gd._fullLayout.shapes[index],
-        shapeLayer = gd._fullLayout._shapeUpperLayer;
+    // setup dragMode and the corresponding handler
+    updateDragMode(evt);
+    dragOptions.moveFn = dragMode === "move" ? moveShape : resizeShape;
+  }
 
-    if(!shape) {
-        Lib.log('getShapeLayer: undefined shape: index', index);
+  function endDrag(dragged) {
+    setCursor(shapePath);
+    if (dragged) {
+      Plotly.relayout(gd, update);
     }
-    else if(shape.layer === 'below') {
-        shapeLayer = (shape.xref === 'paper' && shape.yref === 'paper') ?
-            gd._fullLayout._shapeLowerLayer :
-            gd._fullLayout._shapeSubplotLayer;
+  }
+
+  function moveShape(dx, dy) {
+    if (shapeOptions.type === "path") {
+      var moveX = function moveX(x) {
+        return p2x(x2p(x) + dx);
+      };
+      if (xa && xa.type === "date") moveX = helpers.encodeDate(moveX);
+
+      var moveY = function moveY(y) {
+        return p2y(y2p(y) + dy);
+      };
+      if (ya && ya.type === "date") moveY = helpers.encodeDate(moveY);
+
+      shapeOptions.path = movePath(pathIn, moveX, moveY);
+      update[astrPath] = shapeOptions.path;
+    } else {
+      update[astrX0] = shapeOptions.x0 = p2x(x0 + dx);
+      update[astrY0] = shapeOptions.y0 = p2y(y0 + dy);
+      update[astrX1] = shapeOptions.x1 = p2x(x1 + dx);
+      update[astrY1] = shapeOptions.y1 = p2y(y1 + dy);
     }
 
-    return shapeLayer;
-}
+    shapePath.attr("d", getPathString(gd, shapeOptions));
+  }
 
-function isShapeInSubplot(gd, shape, plotinfo) {
-    var xa = Axes.getFromId(gd, plotinfo.id, 'x')._id,
-        ya = Axes.getFromId(gd, plotinfo.id, 'y')._id,
-        isBelow = shape.layer === 'below',
-        inSuplotAxis = (xa === shape.xref || ya === shape.yref),
-        isNotAnOverlaidSubplot = !!plotinfo.shapelayer;
-    return isBelow && inSuplotAxis && isNotAnOverlaidSubplot;
-}
+  function resizeShape(dx, dy) {
+    if (shapeOptions.type === "path") {
+      // TODO: implement path resize
+      var moveX = function moveX(x) {
+        return p2x(x2p(x) + dx);
+      };
+      if (xa && xa.type === "date") moveX = helpers.encodeDate(moveX);
 
-function getPathString(gd, options) {
-    var type = options.type,
-        xa = Axes.getFromId(gd, options.xref),
-        ya = Axes.getFromId(gd, options.yref),
-        gs = gd._fullLayout._size,
-        x2r,
-        x2p,
-        y2r,
-        y2p;
-
-    if(xa) {
-        x2r = helpers.shapePositionToRange(xa);
-        x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); };
-    }
-    else {
-        x2p = function(v) { return gs.l + gs.w * v; };
-    }
+      var moveY = function moveY(y) {
+        return p2y(y2p(y) + dy);
+      };
+      if (ya && ya.type === "date") moveY = helpers.encodeDate(moveY);
 
-    if(ya) {
-        y2r = helpers.shapePositionToRange(ya);
-        y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); };
-    }
-    else {
-        y2p = function(v) { return gs.t + gs.h * (1 - v); };
+      shapeOptions.path = movePath(pathIn, moveX, moveY);
+      update[astrPath] = shapeOptions.path;
+    } else {
+      var newN = ~dragMode.indexOf("n") ? n0 + dy : n0,
+        newS = ~dragMode.indexOf("s") ? s0 + dy : s0,
+        newW = ~dragMode.indexOf("w") ? w0 + dx : w0,
+        newE = ~dragMode.indexOf("e") ? e0 + dx : e0;
+
+      if (newS - newN > MINHEIGHT) {
+        update[astrN] = shapeOptions[optN] = p2y(newN);
+        update[astrS] = shapeOptions[optS] = p2y(newS);
+      }
+
+      if (newE - newW > MINWIDTH) {
+        update[astrW] = shapeOptions[optW] = p2x(newW);
+        update[astrE] = shapeOptions[optE] = p2x(newE);
+      }
     }
 
-    if(type === 'path') {
-        if(xa && xa.type === 'date') x2p = helpers.decodeDate(x2p);
-        if(ya && ya.type === 'date') y2p = helpers.decodeDate(y2p);
-        return convertPath(options.path, x2p, y2p);
-    }
+    shapePath.attr("d", getPathString(gd, shapeOptions));
+  }
+}
+
+function getShapeLayer(gd, index) {
+  var shape = gd._fullLayout.shapes[index],
+    shapeLayer = gd._fullLayout._shapeUpperLayer;
+
+  if (!shape) {
+    Lib.log("getShapeLayer: undefined shape: index", index);
+  } else if (shape.layer === "below") {
+    shapeLayer = shape.xref === "paper" && shape.yref === "paper"
+      ? gd._fullLayout._shapeLowerLayer
+      : gd._fullLayout._shapeSubplotLayer;
+  }
+
+  return shapeLayer;
+}
 
-    var x0 = x2p(options.x0),
-        x1 = x2p(options.x1),
-        y0 = y2p(options.y0),
-        y1 = y2p(options.y1);
-
-    if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1;
-    if(type === 'rect') return 'M' + x0 + ',' + y0 + 'H' + x1 + 'V' + y1 + 'H' + x0 + 'Z';
-    // circle
-    var cx = (x0 + x1) / 2,
-        cy = (y0 + y1) / 2,
-        rx = Math.abs(cx - x0),
-        ry = Math.abs(cy - y0),
-        rArc = 'A' + rx + ',' + ry,
-        rightPt = (cx + rx) + ',' + cy,
-        topPt = cx + ',' + (cy - ry);
-    return 'M' + rightPt + rArc + ' 0 1,1 ' + topPt +
-        rArc + ' 0 0,1 ' + rightPt + 'Z';
+function isShapeInSubplot(gd, shape, plotinfo) {
+  var xa = Axes.getFromId(gd, plotinfo.id, "x")._id,
+    ya = Axes.getFromId(gd, plotinfo.id, "y")._id,
+    isBelow = shape.layer === "below",
+    inSuplotAxis = xa === shape.xref || ya === shape.yref,
+    isNotAnOverlaidSubplot = !!plotinfo.shapelayer;
+  return isBelow && inSuplotAxis && isNotAnOverlaidSubplot;
 }
 
+function getPathString(gd, options) {
+  var type = options.type,
+    xa = Axes.getFromId(gd, options.xref),
+    ya = Axes.getFromId(gd, options.yref),
+    gs = gd._fullLayout._size,
+    x2r,
+    x2p,
+    y2r,
+    y2p;
+
+  if (xa) {
+    x2r = helpers.shapePositionToRange(xa);
+    x2p = function(v) {
+      return xa._offset + xa.r2p(x2r(v, true));
+    };
+  } else {
+    x2p = function(v) {
+      return gs.l + gs.w * v;
+    };
+  }
+
+  if (ya) {
+    y2r = helpers.shapePositionToRange(ya);
+    y2p = function(v) {
+      return ya._offset + ya.r2p(y2r(v, true));
+    };
+  } else {
+    y2p = function(v) {
+      return gs.t + gs.h * (1 - v);
+    };
+  }
+
+  if (type === "path") {
+    if (xa && xa.type === "date") x2p = helpers.decodeDate(x2p);
+    if (ya && ya.type === "date") y2p = helpers.decodeDate(y2p);
+    return convertPath(options.path, x2p, y2p);
+  }
+
+  var x0 = x2p(options.x0),
+    x1 = x2p(options.x1),
+    y0 = y2p(options.y0),
+    y1 = y2p(options.y1);
+
+  if (type === "line") return "M" + x0 + "," + y0 + "L" + x1 + "," + y1;
+  if (type === "rect") {
+    return "M" + x0 + "," + y0 + "H" + x1 + "V" + y1 + "H" + x0 + "Z";
+  }
+  // circle
+  var cx = (x0 + x1) / 2,
+    cy = (y0 + y1) / 2,
+    rx = Math.abs(cx - x0),
+    ry = Math.abs(cy - y0),
+    rArc = "A" + rx + "," + ry,
+    rightPt = cx + rx + "," + cy,
+    topPt = cx + "," + (cy - ry);
+  return "M" +
+    rightPt +
+    rArc +
+    " 0 1,1 " +
+    topPt +
+    rArc +
+    " 0 0,1 " +
+    rightPt +
+    "Z";
+}
 
 function convertPath(pathIn, x2p, y2p) {
-    // convert an SVG path string from data units to pixels
-    return pathIn.replace(constants.segmentRE, function(segment) {
-        var paramNumber = 0,
-            segmentType = segment.charAt(0),
-            xParams = constants.paramIsX[segmentType],
-            yParams = constants.paramIsY[segmentType],
-            nParams = constants.numParams[segmentType];
-
-        var paramString = segment.substr(1).replace(constants.paramRE, function(param) {
-            if(xParams[paramNumber]) param = x2p(param);
-            else if(yParams[paramNumber]) param = y2p(param);
-            paramNumber++;
-
-            if(paramNumber > nParams) param = 'X';
-            return param;
-        });
-
-        if(paramNumber > nParams) {
-            paramString = paramString.replace(/[\s,]*X.*/, '');
-            Lib.log('Ignoring extra params in segment ' + segment);
-        }
-
-        return segmentType + paramString;
-    });
+  // convert an SVG path string from data units to pixels
+  return pathIn.replace(constants.segmentRE, function(segment) {
+    var paramNumber = 0,
+      segmentType = segment.charAt(0),
+      xParams = constants.paramIsX[segmentType],
+      yParams = constants.paramIsY[segmentType],
+      nParams = constants.numParams[segmentType];
+
+    var paramString = segment
+      .substr(1)
+      .replace(constants.paramRE, function(param) {
+        if (xParams[paramNumber]) param = x2p(param);
+        else if (yParams[paramNumber]) param = y2p(param);
+        paramNumber++;
+
+        if (paramNumber > nParams) param = "X";
+        return param;
+      });
+
+    if (paramNumber > nParams) {
+      paramString = paramString.replace(/[\s,]*X.*/, "");
+      Lib.log("Ignoring extra params in segment " + segment);
+    }
+
+    return segmentType + paramString;
+  });
 }
 
 function movePath(pathIn, moveX, moveY) {
-    return pathIn.replace(constants.segmentRE, function(segment) {
-        var paramNumber = 0,
-            segmentType = segment.charAt(0),
-            xParams = constants.paramIsX[segmentType],
-            yParams = constants.paramIsY[segmentType],
-            nParams = constants.numParams[segmentType];
+  return pathIn.replace(constants.segmentRE, function(segment) {
+    var paramNumber = 0,
+      segmentType = segment.charAt(0),
+      xParams = constants.paramIsX[segmentType],
+      yParams = constants.paramIsY[segmentType],
+      nParams = constants.numParams[segmentType];
 
-        var paramString = segment.substr(1).replace(constants.paramRE, function(param) {
-            if(paramNumber >= nParams) return param;
+    var paramString = segment
+      .substr(1)
+      .replace(constants.paramRE, function(param) {
+        if (paramNumber >= nParams) return param;
 
-            if(xParams[paramNumber]) param = moveX(param);
-            else if(yParams[paramNumber]) param = moveY(param);
+        if (xParams[paramNumber]) param = moveX(param);
+        else if (yParams[paramNumber]) param = moveY(param);
 
-            paramNumber++;
+        paramNumber++;
 
-            return param;
-        });
+        return param;
+      });
 
-        return segmentType + paramString;
-    });
+    return segmentType + paramString;
+  });
 }
diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js
index 15c5337c52a..0d9ab343d71 100644
--- a/src/components/shapes/helpers.js
+++ b/src/components/shapes/helpers.js
@@ -5,10 +5,7 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 // special position conversion functions... category axis positions can't be
 // specified by their data values, because they don't make a continuous mapping.
 // so these have to be specified in terms of the category serial numbers,
@@ -19,61 +16,75 @@
 // removed entirely.
 
 exports.rangeToShapePosition = function(ax) {
-    return (ax.type === 'log') ? ax.r2d : function(v) { return v; };
+  return ax.type === "log"
+    ? ax.r2d
+    : (function(v) {
+        return v;
+      });
 };
 
 exports.shapePositionToRange = function(ax) {
-    return (ax.type === 'log') ? ax.d2r : function(v) { return v; };
+  return ax.type === "log"
+    ? ax.d2r
+    : (function(v) {
+        return v;
+      });
 };
 
 exports.decodeDate = function(convertToPx) {
-    return function(v) {
-        if(v.replace) v = v.replace('_', ' ');
-        return convertToPx(v);
-    };
+  return function(v) {
+    if (v.replace) v = v.replace("_", " ");
+    return convertToPx(v);
+  };
 };
 
 exports.encodeDate = function(convertToDate) {
-    return function(v) { return convertToDate(v).replace(' ', '_'); };
+  return function(v) {
+    return convertToDate(v).replace(" ", "_");
+  };
 };
 
 exports.getDataToPixel = function(gd, axis, isVertical) {
-    var gs = gd._fullLayout._size,
-        dataToPixel;
+  var gs = gd._fullLayout._size, dataToPixel;
 
-    if(axis) {
-        var d2r = exports.shapePositionToRange(axis);
+  if (axis) {
+    var d2r = exports.shapePositionToRange(axis);
 
-        dataToPixel = function(v) {
-            return axis._offset + axis.r2p(d2r(v, true));
-        };
+    dataToPixel = function(v) {
+      return axis._offset + axis.r2p(d2r(v, true));
+    };
 
-        if(axis.type === 'date') dataToPixel = exports.decodeDate(dataToPixel);
-    }
-    else if(isVertical) {
-        dataToPixel = function(v) { return gs.t + gs.h * (1 - v); };
-    }
-    else {
-        dataToPixel = function(v) { return gs.l + gs.w * v; };
-    }
+    if (axis.type === "date") dataToPixel = exports.decodeDate(dataToPixel);
+  } else if (isVertical) {
+    dataToPixel = function(v) {
+      return gs.t + gs.h * (1 - v);
+    };
+  } else {
+    dataToPixel = function(v) {
+      return gs.l + gs.w * v;
+    };
+  }
 
-    return dataToPixel;
+  return dataToPixel;
 };
 
 exports.getPixelToData = function(gd, axis, isVertical) {
-    var gs = gd._fullLayout._size,
-        pixelToData;
+  var gs = gd._fullLayout._size, pixelToData;
 
-    if(axis) {
-        var r2d = exports.rangeToShapePosition(axis);
-        pixelToData = function(p) { return r2d(axis.p2r(p - axis._offset)); };
-    }
-    else if(isVertical) {
-        pixelToData = function(p) { return 1 - (p - gs.t) / gs.h; };
-    }
-    else {
-        pixelToData = function(p) { return (p - gs.l) / gs.w; };
-    }
+  if (axis) {
+    var r2d = exports.rangeToShapePosition(axis);
+    pixelToData = function(p) {
+      return r2d(axis.p2r(p - axis._offset));
+    };
+  } else if (isVertical) {
+    pixelToData = function(p) {
+      return 1 - (p - gs.t) / gs.h;
+    };
+  } else {
+    pixelToData = function(p) {
+      return (p - gs.l) / gs.w;
+    };
+  }
 
-    return pixelToData;
+  return pixelToData;
 };
diff --git a/src/components/shapes/index.js b/src/components/shapes/index.js
index 444ede3cd59..0711a85a820 100644
--- a/src/components/shapes/index.js
+++ b/src/components/shapes/index.js
@@ -5,20 +5,15 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var drawModule = require('./draw');
+"use strict";
+var drawModule = require("./draw");
 
 module.exports = {
-    moduleType: 'component',
-    name: 'shapes',
-
-    layoutAttributes: require('./attributes'),
-    supplyLayoutDefaults: require('./defaults'),
-
-    calcAutorange: require('./calc_autorange'),
-    draw: drawModule.draw,
-    drawOne: drawModule.drawOne
+  moduleType: "component",
+  name: "shapes",
+  layoutAttributes: require("./attributes"),
+  supplyLayoutDefaults: require("./defaults"),
+  calcAutorange: require("./calc_autorange"),
+  draw: drawModule.draw,
+  drawOne: drawModule.drawOne
 };
diff --git a/src/components/shapes/shape_defaults.js b/src/components/shapes/shape_defaults.js
index 44d983fc793..091a1e950cf 100644
--- a/src/components/shapes/shape_defaults.js
+++ b/src/components/shapes/shape_defaults.js
@@ -5,93 +5,95 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var Lib = require('../../lib');
-var Axes = require('../../plots/cartesian/axes');
-
-var attributes = require('./attributes');
-var helpers = require('./helpers');
-
-
-module.exports = function handleShapeDefaults(shapeIn, shapeOut, fullLayout, opts, itemOpts) {
-    opts = opts || {};
-    itemOpts = itemOpts || {};
-
-    function coerce(attr, dflt) {
-        return Lib.coerce(shapeIn, shapeOut, attributes, attr, dflt);
-    }
-
-    var visible = coerce('visible', !itemOpts.itemIsNotPlainObject);
-
-    if(!visible) return shapeOut;
-
-    coerce('layer');
-    coerce('opacity');
-    coerce('fillcolor');
-    coerce('line.color');
-    coerce('line.width');
-    coerce('line.dash');
-
-    var dfltType = shapeIn.path ? 'path' : 'rect',
-        shapeType = coerce('type', dfltType);
-
-    // positioning
-    var axLetters = ['x', 'y'];
-    for(var i = 0; i < 2; i++) {
-        var axLetter = axLetters[i],
-            gdMock = {_fullLayout: fullLayout};
-
-        // xref, yref
-        var axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, '', 'paper');
-
-        if(shapeType !== 'path') {
-            var dflt0 = 0.25,
-                dflt1 = 0.75,
-                ax,
-                pos2r,
-                r2pos;
-
-            if(axRef !== 'paper') {
-                ax = Axes.getFromId(gdMock, axRef);
-                r2pos = helpers.rangeToShapePosition(ax);
-                pos2r = helpers.shapePositionToRange(ax);
-            }
-            else {
-                pos2r = r2pos = Lib.identity;
-            }
-
-            // hack until V2.0 when log has regular range behavior - make it look like other
-            // ranges to send to coerce, then put it back after
-            // this is all to give reasonable default position behavior on log axes, which is
-            // a pretty unimportant edge case so we could just ignore this.
-            var attr0 = axLetter + '0',
-                attr1 = axLetter + '1',
-                in0 = shapeIn[attr0],
-                in1 = shapeIn[attr1];
-            shapeIn[attr0] = pos2r(shapeIn[attr0], true);
-            shapeIn[attr1] = pos2r(shapeIn[attr1], true);
-
-            // x0, x1 (and y0, y1)
-            Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0);
-            Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1);
-
-            // hack part 2
-            shapeOut[attr0] = r2pos(shapeOut[attr0]);
-            shapeOut[attr1] = r2pos(shapeOut[attr1]);
-            shapeIn[attr0] = in0;
-            shapeIn[attr1] = in1;
-        }
+"use strict";
+var Lib = require("../../lib");
+var Axes = require("../../plots/cartesian/axes");
+
+var attributes = require("./attributes");
+var helpers = require("./helpers");
+
+module.exports = function handleShapeDefaults(
+  shapeIn,
+  shapeOut,
+  fullLayout,
+  opts,
+  itemOpts
+) {
+  opts = opts || {};
+  itemOpts = itemOpts || {};
+
+  function coerce(attr, dflt) {
+    return Lib.coerce(shapeIn, shapeOut, attributes, attr, dflt);
+  }
+
+  var visible = coerce("visible", !itemOpts.itemIsNotPlainObject);
+
+  if (!visible) return shapeOut;
+
+  coerce("layer");
+  coerce("opacity");
+  coerce("fillcolor");
+  coerce("line.color");
+  coerce("line.width");
+  coerce("line.dash");
+
+  var dfltType = shapeIn.path ? "path" : "rect",
+    shapeType = coerce("type", dfltType);
+
+  // positioning
+  var axLetters = ["x", "y"];
+  for (var i = 0; i < 2; i++) {
+    var axLetter = axLetters[i], gdMock = { _fullLayout: fullLayout };
+
+    // xref, yref
+    var axRef = Axes.coerceRef(
+      shapeIn,
+      shapeOut,
+      gdMock,
+      axLetter,
+      "",
+      "paper"
+    );
+
+    if (shapeType !== "path") {
+      var dflt0 = 0.25, dflt1 = 0.75, ax, pos2r, r2pos;
+
+      if (axRef !== "paper") {
+        ax = Axes.getFromId(gdMock, axRef);
+        r2pos = helpers.rangeToShapePosition(ax);
+        pos2r = helpers.shapePositionToRange(ax);
+      } else {
+        pos2r = r2pos = Lib.identity;
+      }
+
+      // hack until V2.0 when log has regular range behavior - make it look like other
+      // ranges to send to coerce, then put it back after
+      // this is all to give reasonable default position behavior on log axes, which is
+      // a pretty unimportant edge case so we could just ignore this.
+      var attr0 = axLetter + "0",
+        attr1 = axLetter + "1",
+        in0 = shapeIn[attr0],
+        in1 = shapeIn[attr1];
+      shapeIn[attr0] = pos2r(shapeIn[attr0], true);
+      shapeIn[attr1] = pos2r(shapeIn[attr1], true);
+
+      // x0, x1 (and y0, y1)
+      Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0);
+      Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1);
+
+      // hack part 2
+      shapeOut[attr0] = r2pos(shapeOut[attr0]);
+      shapeOut[attr1] = r2pos(shapeOut[attr1]);
+      shapeIn[attr0] = in0;
+      shapeIn[attr1] = in1;
     }
+  }
 
-    if(shapeType === 'path') {
-        coerce('path');
-    }
-    else {
-        Lib.noneOrAll(shapeIn, shapeOut, ['x0', 'x1', 'y0', 'y1']);
-    }
+  if (shapeType === "path") {
+    coerce("path");
+  } else {
+    Lib.noneOrAll(shapeIn, shapeOut, ["x0", "x1", "y0", "y1"]);
+  }
 
-    return shapeOut;
+  return shapeOut;
 };
diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js
index 8e584a2455f..4fb808df73e 100644
--- a/src/components/sliders/attributes.js
+++ b/src/components/sliders/attributes.js
@@ -5,268 +5,249 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var fontAttrs = require('../../plots/font_attributes');
-var padAttrs = require('../../plots/pad_attributes');
-var extendFlat = require('../../lib/extend').extendFlat;
-var extendDeep = require('../../lib/extend').extendDeep;
-var animationAttrs = require('../../plots/animation_attributes');
-var constants = require('./constants');
+"use strict";
+var fontAttrs = require("../../plots/font_attributes");
+var padAttrs = require("../../plots/pad_attributes");
+var extendFlat = require("../../lib/extend").extendFlat;
+var extendDeep = require("../../lib/extend").extendDeep;
+var animationAttrs = require("../../plots/animation_attributes");
+var constants = require("./constants");
 
 var stepsAttrs = {
-    _isLinkedToArray: 'step',
-
-    method: {
-        valType: 'enumerated',
-        values: ['restyle', 'relayout', 'animate', 'update'],
-        dflt: 'restyle',
-        role: 'info',
-        description: [
-            'Sets the Plotly method to be called when the slider value is changed.'
-        ].join(' ')
-    },
-    args: {
-        valType: 'info_array',
-        role: 'info',
-        freeLength: true,
-        items: [
-            { valType: 'any' },
-            { valType: 'any' },
-            { valType: 'any' }
-        ],
-        description: [
-            'Sets the arguments values to be passed to the Plotly',
-            'method set in `method` on slide.'
-        ].join(' ')
-    },
-    label: {
-        valType: 'string',
-        role: 'info',
-        description: 'Sets the text label to appear on the slider'
-    },
-    value: {
-        valType: 'string',
-        role: 'info',
-        description: [
-            'Sets the value of the slider step, used to refer to the step programatically.',
-            'Defaults to the slider label if not provided.'
-        ].join(' ')
-    }
+  _isLinkedToArray: "step",
+  method: {
+    valType: "enumerated",
+    values: ["restyle", "relayout", "animate", "update"],
+    dflt: "restyle",
+    role: "info",
+    description: [
+      "Sets the Plotly method to be called when the slider value is changed."
+    ].join(" ")
+  },
+  args: {
+    valType: "info_array",
+    role: "info",
+    freeLength: true,
+    items: [{ valType: "any" }, { valType: "any" }, { valType: "any" }],
+    description: [
+      "Sets the arguments values to be passed to the Plotly",
+      "method set in `method` on slide."
+    ].join(" ")
+  },
+  label: {
+    valType: "string",
+    role: "info",
+    description: "Sets the text label to appear on the slider"
+  },
+  value: {
+    valType: "string",
+    role: "info",
+    description: [
+      "Sets the value of the slider step, used to refer to the step programatically.",
+      "Defaults to the slider label if not provided."
+    ].join(" ")
+  }
 };
 
 module.exports = {
-    _isLinkedToArray: 'slider',
-
+  _isLinkedToArray: "slider",
+  visible: {
+    valType: "boolean",
+    role: "info",
+    dflt: true,
+    description: ["Determines whether or not the slider is visible."].join(" ")
+  },
+  active: {
+    valType: "number",
+    role: "info",
+    min: 0,
+    dflt: 0,
+    description: [
+      "Determines which button (by index starting from 0) is",
+      "considered active."
+    ].join(" ")
+  },
+  steps: stepsAttrs,
+  lenmode: {
+    valType: "enumerated",
+    values: ["fraction", "pixels"],
+    role: "info",
+    dflt: "fraction",
+    description: [
+      "Determines whether this slider length",
+      "is set in units of plot *fraction* or in *pixels.",
+      "Use `len` to set the value."
+    ].join(" ")
+  },
+  len: {
+    valType: "number",
+    min: 0,
+    dflt: 1,
+    role: "style",
+    description: [
+      "Sets the length of the slider",
+      "This measure excludes the padding of both ends.",
+      "That is, the slider's length is this length minus the",
+      "padding on both ends."
+    ].join(" ")
+  },
+  x: {
+    valType: "number",
+    min: -2,
+    max: 3,
+    dflt: 0,
+    role: "style",
+    description: "Sets the x position (in normalized coordinates) of the slider."
+  },
+  pad: extendDeep(
+    {},
+    padAttrs,
+    { description: "Set the padding of the slider component along each side." },
+    { t: { dflt: 20 } }
+  ),
+  xanchor: {
+    valType: "enumerated",
+    values: ["auto", "left", "center", "right"],
+    dflt: "left",
+    role: "info",
+    description: [
+      "Sets the slider's horizontal position anchor.",
+      "This anchor binds the `x` position to the *left*, *center*",
+      "or *right* of the range selector."
+    ].join(" ")
+  },
+  y: {
+    valType: "number",
+    min: -2,
+    max: 3,
+    dflt: 0,
+    role: "style",
+    description: "Sets the y position (in normalized coordinates) of the slider."
+  },
+  yanchor: {
+    valType: "enumerated",
+    values: ["auto", "top", "middle", "bottom"],
+    dflt: "top",
+    role: "info",
+    description: [
+      "Sets the slider's vertical position anchor",
+      "This anchor binds the `y` position to the *top*, *middle*",
+      "or *bottom* of the range selector."
+    ].join(" ")
+  },
+  transition: {
+    duration: {
+      valType: "number",
+      role: "info",
+      min: 0,
+      dflt: 150,
+      description: "Sets the duration of the slider transition"
+    },
+    easing: {
+      valType: "enumerated",
+      values: animationAttrs.transition.easing.values,
+      role: "info",
+      dflt: "cubic-in-out",
+      description: "Sets the easing function of the slider transition"
+    }
+  },
+  currentvalue: {
     visible: {
-        valType: 'boolean',
-        role: 'info',
-        dflt: true,
-        description: [
-            'Determines whether or not the slider is visible.'
-        ].join(' ')
+      valType: "boolean",
+      role: "info",
+      dflt: true,
+      description: [
+        "Shows the currently-selected value above the slider."
+      ].join(" ")
     },
-
-    active: {
-        valType: 'number',
-        role: 'info',
-        min: 0,
-        dflt: 0,
-        description: [
-            'Determines which button (by index starting from 0) is',
-            'considered active.'
-        ].join(' ')
-    },
-
-    steps: stepsAttrs,
-
-    lenmode: {
-        valType: 'enumerated',
-        values: ['fraction', 'pixels'],
-        role: 'info',
-        dflt: 'fraction',
-        description: [
-            'Determines whether this slider length',
-            'is set in units of plot *fraction* or in *pixels.',
-            'Use `len` to set the value.'
-        ].join(' ')
-    },
-    len: {
-        valType: 'number',
-        min: 0,
-        dflt: 1,
-        role: 'style',
-        description: [
-            'Sets the length of the slider',
-            'This measure excludes the padding of both ends.',
-            'That is, the slider\'s length is this length minus the',
-            'padding on both ends.'
-        ].join(' ')
-    },
-    x: {
-        valType: 'number',
-        min: -2,
-        max: 3,
-        dflt: 0,
-        role: 'style',
-        description: 'Sets the x position (in normalized coordinates) of the slider.'
-    },
-    pad: extendDeep({}, padAttrs, {
-        description: 'Set the padding of the slider component along each side.'
-    }, {t: {dflt: 20}}),
     xanchor: {
-        valType: 'enumerated',
-        values: ['auto', 'left', 'center', 'right'],
-        dflt: 'left',
-        role: 'info',
-        description: [
-            'Sets the slider\'s horizontal position anchor.',
-            'This anchor binds the `x` position to the *left*, *center*',
-            'or *right* of the range selector.'
-        ].join(' ')
-    },
-    y: {
-        valType: 'number',
-        min: -2,
-        max: 3,
-        dflt: 0,
-        role: 'style',
-        description: 'Sets the y position (in normalized coordinates) of the slider.'
+      valType: "enumerated",
+      values: ["left", "center", "right"],
+      dflt: "left",
+      role: "info",
+      description: [
+        "The alignment of the value readout relative to the length of the slider."
+      ].join(" ")
+    },
+    offset: {
+      valType: "number",
+      dflt: 10,
+      role: "info",
+      description: [
+        "The amount of space, in pixels, between the current value label",
+        "and the slider."
+      ].join(" ")
+    },
+    prefix: {
+      valType: "string",
+      role: "info",
+      description: "When currentvalue.visible is true, this sets the prefix of the label."
+    },
+    suffix: {
+      valType: "string",
+      role: "info",
+      description: "When currentvalue.visible is true, this sets the suffix of the label."
     },
-    yanchor: {
-        valType: 'enumerated',
-        values: ['auto', 'top', 'middle', 'bottom'],
-        dflt: 'top',
-        role: 'info',
-        description: [
-            'Sets the slider\'s vertical position anchor',
-            'This anchor binds the `y` position to the *top*, *middle*',
-            'or *bottom* of the range selector.'
-        ].join(' ')
-    },
-
-    transition: {
-        duration: {
-            valType: 'number',
-            role: 'info',
-            min: 0,
-            dflt: 150,
-            description: 'Sets the duration of the slider transition'
-        },
-        easing: {
-            valType: 'enumerated',
-            values: animationAttrs.transition.easing.values,
-            role: 'info',
-            dflt: 'cubic-in-out',
-            description: 'Sets the easing function of the slider transition'
-        },
-    },
-
-    currentvalue: {
-        visible: {
-            valType: 'boolean',
-            role: 'info',
-            dflt: true,
-            description: [
-                'Shows the currently-selected value above the slider.'
-            ].join(' ')
-        },
-
-        xanchor: {
-            valType: 'enumerated',
-            values: ['left', 'center', 'right'],
-            dflt: 'left',
-            role: 'info',
-            description: [
-                'The alignment of the value readout relative to the length of the slider.'
-            ].join(' ')
-        },
-
-        offset: {
-            valType: 'number',
-            dflt: 10,
-            role: 'info',
-            description: [
-                'The amount of space, in pixels, between the current value label',
-                'and the slider.'
-            ].join(' ')
-        },
-
-        prefix: {
-            valType: 'string',
-            role: 'info',
-            description: 'When currentvalue.visible is true, this sets the prefix of the label.'
-        },
-
-        suffix: {
-            valType: 'string',
-            role: 'info',
-            description: 'When currentvalue.visible is true, this sets the suffix of the label.'
-        },
-
-        font: extendFlat({}, fontAttrs, {
-            description: 'Sets the font of the current value label text.'
-        }),
-    },
-
     font: extendFlat({}, fontAttrs, {
-        description: 'Sets the font of the slider step labels.'
-    }),
-
-    activebgcolor: {
-        valType: 'color',
-        role: 'style',
-        dflt: constants.gripBgActiveColor,
-        description: [
-            'Sets the background color of the slider grip',
-            'while dragging.'
-        ].join(' ')
-    },
-    bgcolor: {
-        valType: 'color',
-        role: 'style',
-        dflt: constants.railBgColor,
-        description: 'Sets the background color of the slider.'
-    },
-    bordercolor: {
-        valType: 'color',
-        dflt: constants.railBorderColor,
-        role: 'style',
-        description: 'Sets the color of the border enclosing the slider.'
-    },
-    borderwidth: {
-        valType: 'number',
-        min: 0,
-        dflt: constants.railBorderWidth,
-        role: 'style',
-        description: 'Sets the width (in px) of the border enclosing the slider.'
-    },
-    ticklen: {
-        valType: 'number',
-        min: 0,
-        dflt: constants.tickLength,
-        role: 'style',
-        description: 'Sets the length in pixels of step tick marks'
-    },
-    tickcolor: {
-        valType: 'color',
-        dflt: constants.tickColor,
-        role: 'style',
-        description: 'Sets the color of the border enclosing the slider.'
-    },
-    tickwidth: {
-        valType: 'number',
-        min: 0,
-        dflt: 1,
-        role: 'style',
-        description: 'Sets the tick width (in px).'
-    },
-    minorticklen: {
-        valType: 'number',
-        min: 0,
-        dflt: constants.minorTickLength,
-        role: 'style',
-        description: 'Sets the length in pixels of minor step tick marks'
-    },
+      description: "Sets the font of the current value label text."
+    })
+  },
+  font: extendFlat({}, fontAttrs, {
+    description: "Sets the font of the slider step labels."
+  }),
+  activebgcolor: {
+    valType: "color",
+    role: "style",
+    dflt: constants.gripBgActiveColor,
+    description: [
+      "Sets the background color of the slider grip",
+      "while dragging."
+    ].join(" ")
+  },
+  bgcolor: {
+    valType: "color",
+    role: "style",
+    dflt: constants.railBgColor,
+    description: "Sets the background color of the slider."
+  },
+  bordercolor: {
+    valType: "color",
+    dflt: constants.railBorderColor,
+    role: "style",
+    description: "Sets the color of the border enclosing the slider."
+  },
+  borderwidth: {
+    valType: "number",
+    min: 0,
+    dflt: constants.railBorderWidth,
+    role: "style",
+    description: "Sets the width (in px) of the border enclosing the slider."
+  },
+  ticklen: {
+    valType: "number",
+    min: 0,
+    dflt: constants.tickLength,
+    role: "style",
+    description: "Sets the length in pixels of step tick marks"
+  },
+  tickcolor: {
+    valType: "color",
+    dflt: constants.tickColor,
+    role: "style",
+    description: "Sets the color of the border enclosing the slider."
+  },
+  tickwidth: {
+    valType: "number",
+    min: 0,
+    dflt: 1,
+    role: "style",
+    description: "Sets the tick width (in px)."
+  },
+  minorticklen: {
+    valType: "number",
+    min: 0,
+    dflt: constants.minorTickLength,
+    role: "style",
+    description: "Sets the length in pixels of minor step tick marks"
+  }
 };
diff --git a/src/components/sliders/constants.js b/src/components/sliders/constants.js
index fedd7c088b5..6004822972e 100644
--- a/src/components/sliders/constants.js
+++ b/src/components/sliders/constants.js
@@ -5,91 +5,70 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-
-    // layout attribute name
-    name: 'sliders',
-
-    // class names
-    containerClassName: 'slider-container',
-    groupClassName: 'slider-group',
-    inputAreaClass: 'slider-input-area',
-    railRectClass: 'slider-rail-rect',
-    railTouchRectClass: 'slider-rail-touch-rect',
-    gripRectClass: 'slider-grip-rect',
-    tickRectClass: 'slider-tick-rect',
-    inputProxyClass: 'slider-input-proxy',
-    labelsClass: 'slider-labels',
-    labelGroupClass: 'slider-label-group',
-    labelClass: 'slider-label',
-    currentValueClass: 'slider-current-value',
-
-    railHeight: 5,
-
-    // DOM attribute name in button group keeping track
-    // of active update menu
-    menuIndexAttrName: 'slider-active-index',
-
-    // id root pass to Plots.autoMargin
-    autoMarginIdRoot: 'slider-',
-
-    // min item width / height
-    minWidth: 30,
-    minHeight: 30,
-
-    // padding around item text
-    textPadX: 40,
-
-    // font size to height scale
-    fontSizeToHeight: 1.3,
-
-    // arrow offset off right edge
-    arrowOffsetX: 4,
-
-    railRadius: 2,
-    railWidth: 5,
-    railBorder: 4,
-    railBorderWidth: 1,
-    railBorderColor: '#bec8d9',
-    railBgColor: '#f8fafc',
-
-    // The distance of the rail from the edge of the touchable area
-    // Slightly less than the step inset because of the curved edges
-    // of the rail
-    railInset: 8,
-
-    // The distance from the extremal tick marks to the edge of the
-    // touchable area. This is basically the same as the grip radius,
-    // but for other styles it wouldn't really need to be.
-    stepInset: 10,
-
-    gripRadius: 10,
-    gripWidth: 20,
-    gripHeight: 20,
-    gripBorder: 20,
-    gripBorderWidth: 1,
-    gripBorderColor: '#bec8d9',
-    gripBgColor: '#f6f8fa',
-    gripBgActiveColor: '#dbdde0',
-
-    labelPadding: 8,
-    labelOffset: 0,
-
-    tickWidth: 1,
-    tickColor: '#333',
-    tickOffset: 25,
-    tickLength: 7,
-
-    minorTickOffset: 25,
-    minorTickColor: '#333',
-    minorTickLength: 4,
-
-    // Extra space below the current value label:
-    currentValuePadding: 8,
-    currentValueInset: 0,
+  // layout attribute name
+  name: "sliders",
+  // class names
+  containerClassName: "slider-container",
+  groupClassName: "slider-group",
+  inputAreaClass: "slider-input-area",
+  railRectClass: "slider-rail-rect",
+  railTouchRectClass: "slider-rail-touch-rect",
+  gripRectClass: "slider-grip-rect",
+  tickRectClass: "slider-tick-rect",
+  inputProxyClass: "slider-input-proxy",
+  labelsClass: "slider-labels",
+  labelGroupClass: "slider-label-group",
+  labelClass: "slider-label",
+  currentValueClass: "slider-current-value",
+  railHeight: 5,
+  // DOM attribute name in button group keeping track
+  // of active update menu
+  menuIndexAttrName: "slider-active-index",
+  // id root pass to Plots.autoMargin
+  autoMarginIdRoot: "slider-",
+  // min item width / height
+  minWidth: 30,
+  minHeight: 30,
+  // padding around item text
+  textPadX: 40,
+  // font size to height scale
+  fontSizeToHeight: 1.3,
+  // arrow offset off right edge
+  arrowOffsetX: 4,
+  railRadius: 2,
+  railWidth: 5,
+  railBorder: 4,
+  railBorderWidth: 1,
+  railBorderColor: "#bec8d9",
+  railBgColor: "#f8fafc",
+  // The distance of the rail from the edge of the touchable area
+  // Slightly less than the step inset because of the curved edges
+  // of the rail
+  railInset: 8,
+  // The distance from the extremal tick marks to the edge of the
+  // touchable area. This is basically the same as the grip radius,
+  // but for other styles it wouldn't really need to be.
+  stepInset: 10,
+  gripRadius: 10,
+  gripWidth: 20,
+  gripHeight: 20,
+  gripBorder: 20,
+  gripBorderWidth: 1,
+  gripBorderColor: "#bec8d9",
+  gripBgColor: "#f6f8fa",
+  gripBgActiveColor: "#dbdde0",
+  labelPadding: 8,
+  labelOffset: 0,
+  tickWidth: 1,
+  tickColor: "#333",
+  tickOffset: 25,
+  tickLength: 7,
+  minorTickOffset: 25,
+  minorTickColor: "#333",
+  minorTickLength: 4,
+  // Extra space below the current value label:
+  currentValuePadding: 8,
+  currentValueInset: 0
 };
diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js
index b2fed316c7b..6d1057a1f18 100644
--- a/src/components/sliders/defaults.js
+++ b/src/components/sliders/defaults.js
@@ -5,107 +5,101 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Lib = require("../../lib");
+var handleArrayContainerDefaults = require(
+  "../../plots/array_container_defaults"
+);
 
-'use strict';
-
-var Lib = require('../../lib');
-var handleArrayContainerDefaults = require('../../plots/array_container_defaults');
-
-var attributes = require('./attributes');
-var constants = require('./constants');
+var attributes = require("./attributes");
+var constants = require("./constants");
 
 var name = constants.name;
 var stepAttrs = attributes.steps;
 
-
 module.exports = function slidersDefaults(layoutIn, layoutOut) {
-    var opts = {
-        name: name,
-        handleItemDefaults: sliderDefaults
-    };
+  var opts = { name: name, handleItemDefaults: sliderDefaults };
 
-    handleArrayContainerDefaults(layoutIn, layoutOut, opts);
+  handleArrayContainerDefaults(layoutIn, layoutOut, opts);
 };
 
 function sliderDefaults(sliderIn, sliderOut, layoutOut) {
+  function coerce(attr, dflt) {
+    return Lib.coerce(sliderIn, sliderOut, attributes, attr, dflt);
+  }
 
-    function coerce(attr, dflt) {
-        return Lib.coerce(sliderIn, sliderOut, attributes, attr, dflt);
-    }
+  var steps = stepsDefaults(sliderIn, sliderOut);
 
-    var steps = stepsDefaults(sliderIn, sliderOut);
+  var visible = coerce("visible", steps.length > 0);
+  if (!visible) return;
 
-    var visible = coerce('visible', steps.length > 0);
-    if(!visible) return;
+  coerce("active");
 
-    coerce('active');
+  coerce("x");
+  coerce("y");
+  Lib.noneOrAll(sliderIn, sliderOut, ["x", "y"]);
 
-    coerce('x');
-    coerce('y');
-    Lib.noneOrAll(sliderIn, sliderOut, ['x', 'y']);
+  coerce("xanchor");
+  coerce("yanchor");
 
-    coerce('xanchor');
-    coerce('yanchor');
+  coerce("len");
+  coerce("lenmode");
 
-    coerce('len');
-    coerce('lenmode');
+  coerce("pad.t");
+  coerce("pad.r");
+  coerce("pad.b");
+  coerce("pad.l");
 
-    coerce('pad.t');
-    coerce('pad.r');
-    coerce('pad.b');
-    coerce('pad.l');
+  Lib.coerceFont(coerce, "font", layoutOut.font);
 
-    Lib.coerceFont(coerce, 'font', layoutOut.font);
+  var currentValueIsVisible = coerce("currentvalue.visible");
 
-    var currentValueIsVisible = coerce('currentvalue.visible');
+  if (currentValueIsVisible) {
+    coerce("currentvalue.xanchor");
+    coerce("currentvalue.prefix");
+    coerce("currentvalue.suffix");
+    coerce("currentvalue.offset");
 
-    if(currentValueIsVisible) {
-        coerce('currentvalue.xanchor');
-        coerce('currentvalue.prefix');
-        coerce('currentvalue.suffix');
-        coerce('currentvalue.offset');
+    Lib.coerceFont(coerce, "currentvalue.font", sliderOut.font);
+  }
 
-        Lib.coerceFont(coerce, 'currentvalue.font', sliderOut.font);
-    }
+  coerce("transition.duration");
+  coerce("transition.easing");
 
-    coerce('transition.duration');
-    coerce('transition.easing');
-
-    coerce('bgcolor');
-    coerce('activebgcolor');
-    coerce('bordercolor');
-    coerce('borderwidth');
-    coerce('ticklen');
-    coerce('tickwidth');
-    coerce('tickcolor');
-    coerce('minorticklen');
+  coerce("bgcolor");
+  coerce("activebgcolor");
+  coerce("bordercolor");
+  coerce("borderwidth");
+  coerce("ticklen");
+  coerce("tickwidth");
+  coerce("tickcolor");
+  coerce("minorticklen");
 }
 
 function stepsDefaults(sliderIn, sliderOut) {
-    var valuesIn = sliderIn.steps || [],
-        valuesOut = sliderOut.steps = [];
+  var valuesIn = sliderIn.steps || [], valuesOut = sliderOut.steps = [];
 
-    var valueIn, valueOut;
+  var valueIn, valueOut;
 
-    function coerce(attr, dflt) {
-        return Lib.coerce(valueIn, valueOut, stepAttrs, attr, dflt);
-    }
+  function coerce(attr, dflt) {
+    return Lib.coerce(valueIn, valueOut, stepAttrs, attr, dflt);
+  }
 
-    for(var i = 0; i < valuesIn.length; i++) {
-        valueIn = valuesIn[i];
-        valueOut = {};
+  for (var i = 0; i < valuesIn.length; i++) {
+    valueIn = valuesIn[i];
+    valueOut = {};
 
-        if(!Lib.isPlainObject(valueIn) || !Array.isArray(valueIn.args)) {
-            continue;
-        }
+    if (!Lib.isPlainObject(valueIn) || !Array.isArray(valueIn.args)) {
+      continue;
+    }
 
-        coerce('method');
-        coerce('args');
-        coerce('label', 'step-' + i);
-        coerce('value', valueOut.label);
+    coerce("method");
+    coerce("args");
+    coerce("label", "step-" + i);
+    coerce("value", valueOut.label);
 
-        valuesOut.push(valueOut);
-    }
+    valuesOut.push(valueOut);
+  }
 
-    return valuesOut;
+  return valuesOut;
 }
diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js
index 50c2ef0f35e..e5ce4991ec4 100644
--- a/src/components/sliders/draw.js
+++ b/src/components/sliders/draw.js
@@ -5,596 +5,704 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
 
+var Plots = require("../../plots/plots");
+var Color = require("../color");
+var Drawing = require("../drawing");
+var svgTextUtils = require("../../lib/svg_text_utils");
+var anchorUtils = require("../legend/anchor_utils");
 
-'use strict';
-
-var d3 = require('d3');
-
-var Plots = require('../../plots/plots');
-var Color = require('../color');
-var Drawing = require('../drawing');
-var svgTextUtils = require('../../lib/svg_text_utils');
-var anchorUtils = require('../legend/anchor_utils');
-
-var constants = require('./constants');
-
+var constants = require("./constants");
 
 module.exports = function draw(gd) {
-    var fullLayout = gd._fullLayout,
-        sliderData = makeSliderData(fullLayout);
-
-    // draw a container for *all* sliders:
-    var sliders = fullLayout._infolayer
-        .selectAll('g.' + constants.containerClassName)
-        .data(sliderData.length > 0 ? [0] : []);
-
-    sliders.enter().append('g')
-        .classed(constants.containerClassName, true)
-        .style('cursor', 'ew-resize');
+  var fullLayout = gd._fullLayout, sliderData = makeSliderData(fullLayout);
 
-    sliders.exit().remove();
+  // draw a container for *all* sliders:
+  var sliders = fullLayout._infolayer
+    .selectAll("g." + constants.containerClassName)
+    .data(sliderData.length > 0 ? [0] : []);
 
-    // If no more sliders, clear the margisn:
-    if(sliders.exit().size()) clearPushMargins(gd);
+  sliders
+    .enter()
+    .append("g")
+    .classed(constants.containerClassName, true)
+    .style("cursor", "ew-resize");
 
-    // Return early if no menus visible:
-    if(sliderData.length === 0) return;
+  sliders.exit().remove();
 
-    var sliderGroups = sliders.selectAll('g.' + constants.groupClassName)
-        .data(sliderData, keyFunction);
+  // If no more sliders, clear the margisn:
+  if (sliders.exit().size()) clearPushMargins(gd);
 
-    sliderGroups.enter().append('g')
-        .classed(constants.groupClassName, true);
+  // Return early if no menus visible:
+  if (sliderData.length === 0) return;
 
-    sliderGroups.exit().each(function(sliderOpts) {
-        d3.select(this).remove();
+  var sliderGroups = sliders
+    .selectAll("g." + constants.groupClassName)
+    .data(sliderData, keyFunction);
 
-        sliderOpts._commandObserver.remove();
-        delete sliderOpts._commandObserver;
+  sliderGroups.enter().append("g").classed(constants.groupClassName, true);
 
-        Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index);
-    });
+  sliderGroups.exit().each(function(sliderOpts) {
+    d3.select(this).remove();
 
-    // Find the dimensions of the sliders:
-    for(var i = 0; i < sliderData.length; i++) {
-        var sliderOpts = sliderData[i];
-        findDimensions(gd, sliderOpts);
-    }
+    sliderOpts._commandObserver.remove();
+    delete sliderOpts._commandObserver;
 
-    sliderGroups.each(function(sliderOpts) {
-        // If it has fewer than two options, it's not really a slider:
-        if(sliderOpts.steps.length < 2) return;
+    Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index);
+  });
 
-        var gSlider = d3.select(this);
+  // Find the dimensions of the sliders:
+  for (var i = 0; i < sliderData.length; i++) {
+    var sliderOpts = sliderData[i];
+    findDimensions(gd, sliderOpts);
+  }
 
-        computeLabelSteps(sliderOpts);
+  sliderGroups.each(function(sliderOpts) {
+    // If it has fewer than two options, it's not really a slider:
+    if (sliderOpts.steps.length < 2) return;
 
-        Plots.manageCommandObserver(gd, sliderOpts, sliderOpts.steps, function(data) {
-            // NB: Same as below. This is *not* always the same as sliderOpts since
-            // if a new set of steps comes in, the reference in this callback would
-            // be invalid. We need to refetch it from the slider group, which is
-            // the join data that creates this slider. So if this slider still exists,
-            // the group should be valid, *to the best of my knowledge.* If not,
-            // we'd have to look it up by d3 data join index/key.
-            var opts = gSlider.data()[0];
+    var gSlider = d3.select(this);
 
-            if(opts.active === data.index) return;
-            if(opts._dragging) return;
+    computeLabelSteps(sliderOpts);
 
-            setActive(gd, gSlider, opts, data.index, false, true);
-        });
+    Plots.manageCommandObserver(gd, sliderOpts, sliderOpts.steps, function(
+      data
+    ) {
+      // NB: Same as below. This is *not* always the same as sliderOpts since
+      // if a new set of steps comes in, the reference in this callback would
+      // be invalid. We need to refetch it from the slider group, which is
+      // the join data that creates this slider. So if this slider still exists,
+      // the group should be valid, *to the best of my knowledge.* If not,
+      // we'd have to look it up by d3 data join index/key.
+      var opts = gSlider.data()[0];
 
-        drawSlider(gd, d3.select(this), sliderOpts);
+      if (opts.active === data.index) return;
+      if (opts._dragging) return;
 
-        // makeInputProxy(gd, d3.select(this), sliderOpts);
+      setActive(gd, gSlider, opts, data.index, false, true);
     });
+
+    drawSlider(gd, d3.select(this), sliderOpts);
+    // makeInputProxy(gd, d3.select(this), sliderOpts);
+  });
 };
 
 /* function makeInputProxy(gd, sliderGroup, sliderOpts) {
     sliderOpts.inputProxy = gd._fullLayout._paperdiv.selectAll('input.' + constants.inputProxyClass)
         .data([0]);
-}*/
-
+} */
 // This really only just filters by visibility:
 function makeSliderData(fullLayout) {
-    var contOpts = fullLayout[constants.name],
-        sliderData = [];
+  var contOpts = fullLayout[constants.name], sliderData = [];
 
-    for(var i = 0; i < contOpts.length; i++) {
-        var item = contOpts[i];
-        if(!item.visible || !item.steps.length) continue;
-        sliderData.push(item);
-    }
+  for (var i = 0; i < contOpts.length; i++) {
+    var item = contOpts[i];
+    if (!item.visible || !item.steps.length) continue;
+    sliderData.push(item);
+  }
 
-    return sliderData;
+  return sliderData;
 }
 
 // This is set in the defaults step:
 function keyFunction(opts) {
-    return opts._index;
+  return opts._index;
 }
 
 // Compute the dimensions (mutates sliderOpts):
 function findDimensions(gd, sliderOpts) {
-    var sliderLabels = gd._tester.selectAll('g.' + constants.labelGroupClass)
-        .data(sliderOpts.steps);
-
-    sliderLabels.enter().append('g')
-        .classed(constants.labelGroupClass, true);
+  var sliderLabels = gd._tester
+    .selectAll("g." + constants.labelGroupClass)
+    .data(sliderOpts.steps);
 
-    // loop over fake buttons to find width / height
-    var maxLabelWidth = 0;
-    var labelHeight = 0;
-    sliderLabels.each(function(stepOpts) {
-        var labelGroup = d3.select(this);
-
-        var text = drawLabel(labelGroup, {step: stepOpts}, sliderOpts);
-
-        var tWidth = (text.node() && Drawing.bBox(text.node()).width) || 0;
-
-        // This just overwrites with the last. Which is fine as long as
-        // the bounding box (probably incorrectly) measures the text *on
-        // a single line*:
-        labelHeight = (text.node() && Drawing.bBox(text.node()).height) || 0;
-
-        maxLabelWidth = Math.max(maxLabelWidth, tWidth);
-    });
+  sliderLabels.enter().append("g").classed(constants.labelGroupClass, true);
 
-    sliderLabels.remove();
+  // loop over fake buttons to find width / height
+  var maxLabelWidth = 0;
+  var labelHeight = 0;
+  sliderLabels.each(function(stepOpts) {
+    var labelGroup = d3.select(this);
 
-    sliderOpts.inputAreaWidth = Math.max(
-        constants.railWidth,
-        constants.gripHeight
-    );
-
-    sliderOpts.currentValueMaxWidth = 0;
-    sliderOpts.currentValueHeight = 0;
-    sliderOpts.currentValueTotalHeight = 0;
-
-    if(sliderOpts.currentvalue.visible) {
-        // Get the dimensions of the current value label:
-        var dummyGroup = gd._tester.append('g');
+    var text = drawLabel(labelGroup, { step: stepOpts }, sliderOpts);
 
-        sliderLabels.each(function(stepOpts) {
-            var curValPrefix = drawCurrentValue(dummyGroup, sliderOpts, stepOpts.label);
-            var curValSize = (curValPrefix.node() && Drawing.bBox(curValPrefix.node())) || {width: 0, height: 0};
-            sliderOpts.currentValueMaxWidth = Math.max(sliderOpts.currentValueMaxWidth, Math.ceil(curValSize.width));
-            sliderOpts.currentValueHeight = Math.max(sliderOpts.currentValueHeight, Math.ceil(curValSize.height));
-        });
+    var tWidth = text.node() && Drawing.bBox(text.node()).width || 0;
 
-        sliderOpts.currentValueTotalHeight = sliderOpts.currentValueHeight + sliderOpts.currentvalue.offset;
+    // This just overwrites with the last. Which is fine as long as
+    // the bounding box (probably incorrectly) measures the text *on
+    // a single line*:
+    labelHeight = text.node() && Drawing.bBox(text.node()).height || 0;
 
-        dummyGroup.remove();
-    }
+    maxLabelWidth = Math.max(maxLabelWidth, tWidth);
+  });
 
-    var graphSize = gd._fullLayout._size;
-    sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x;
-    sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y);
+  sliderLabels.remove();
 
-    if(sliderOpts.lenmode === 'fraction') {
-        // fraction:
-        sliderOpts.outerLength = Math.round(graphSize.w * sliderOpts.len);
-    } else {
-        // pixels:
-        sliderOpts.outerLength = sliderOpts.len;
-    }
+  sliderOpts.inputAreaWidth = Math.max(
+    constants.railWidth,
+    constants.gripHeight
+  );
 
-    // 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);
+  sliderOpts.currentValueMaxWidth = 0;
+  sliderOpts.currentValueHeight = 0;
+  sliderOpts.currentValueTotalHeight = 0;
 
-    // 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);
+  if (sliderOpts.currentvalue.visible) {
+    // Get the dimensions of the current value label:
+    var dummyGroup = gd._tester.append("g");
 
-    var textableInputLength = sliderOpts.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;
-
-    sliderOpts.height = sliderOpts.currentValueTotalHeight + constants.tickOffset + sliderOpts.ticklen + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b;
-
-    var xanchor = 'left';
-    if(anchorUtils.isRightAnchor(sliderOpts)) {
-        sliderOpts.lx -= sliderOpts.outerLength;
-        xanchor = 'right';
-    }
-    if(anchorUtils.isCenterAnchor(sliderOpts)) {
-        sliderOpts.lx -= sliderOpts.outerLength / 2;
-        xanchor = 'center';
-    }
-
-    var yanchor = 'top';
-    if(anchorUtils.isBottomAnchor(sliderOpts)) {
-        sliderOpts.ly -= sliderOpts.height;
-        yanchor = 'bottom';
-    }
-    if(anchorUtils.isMiddleAnchor(sliderOpts)) {
-        sliderOpts.ly -= sliderOpts.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);
-
-    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)
+    sliderLabels.each(function(stepOpts) {
+      var curValPrefix = drawCurrentValue(
+        dummyGroup,
+        sliderOpts,
+        stepOpts.label
+      );
+      var curValSize = curValPrefix.node() &&
+        Drawing.bBox(curValPrefix.node()) ||
+        { width: 0, height: 0 };
+      sliderOpts.currentValueMaxWidth = Math.max(
+        sliderOpts.currentValueMaxWidth,
+        Math.ceil(curValSize.width)
+      );
+      sliderOpts.currentValueHeight = Math.max(
+        sliderOpts.currentValueHeight,
+        Math.ceil(curValSize.height)
+      );
     });
+
+    sliderOpts.currentValueTotalHeight = sliderOpts.currentValueHeight +
+      sliderOpts.currentvalue.offset;
+
+    dummyGroup.remove();
+  }
+
+  var graphSize = gd._fullLayout._size;
+  sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x;
+  sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y);
+
+  if (sliderOpts.lenmode === "fraction") {
+    // fraction:
+    sliderOpts.outerLength = Math.round(graphSize.w * sliderOpts.len);
+  } else {
+    // pixels:
+    sliderOpts.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
+  );
+
+  var textableInputLength = sliderOpts.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;
+
+  sliderOpts.height = sliderOpts.currentValueTotalHeight +
+    constants.tickOffset +
+    sliderOpts.ticklen +
+    constants.labelOffset +
+    sliderOpts.labelHeight +
+    sliderOpts.pad.t +
+    sliderOpts.pad.b;
+
+  var xanchor = "left";
+  if (anchorUtils.isRightAnchor(sliderOpts)) {
+    sliderOpts.lx -= sliderOpts.outerLength;
+    xanchor = "right";
+  }
+  if (anchorUtils.isCenterAnchor(sliderOpts)) {
+    sliderOpts.lx -= sliderOpts.outerLength / 2;
+    xanchor = "center";
+  }
+
+  var yanchor = "top";
+  if (anchorUtils.isBottomAnchor(sliderOpts)) {
+    sliderOpts.ly -= sliderOpts.height;
+    yanchor = "bottom";
+  }
+  if (anchorUtils.isMiddleAnchor(sliderOpts)) {
+    sliderOpts.ly -= sliderOpts.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);
+
+  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)
+  });
 }
 
 function drawSlider(gd, sliderGroup, sliderOpts) {
-    // This is related to the other long notes in this file regarding what happens
-    // when slider steps disappear. This particular fix handles what happens when
-    // the *current* slider step is removed. The drawing functions will error out
-    // when they fail to find it, so the fix for now is that it will just draw the
-    // slider in the first position but will not execute the command.
-    if(sliderOpts.active >= sliderOpts.steps.length) {
-        sliderOpts.active = 0;
-    }
-
-    // These are carefully ordered for proper z-ordering:
-    sliderGroup
-        .call(drawCurrentValue, sliderOpts)
-        .call(drawRail, sliderOpts)
-        .call(drawLabelGroup, sliderOpts)
-        .call(drawTicks, sliderOpts)
-        .call(drawTouchRect, gd, sliderOpts)
-        .call(drawGrip, gd, sliderOpts);
-
-    // Position the rectangle:
-    Drawing.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.pad.l, sliderOpts.ly + sliderOpts.pad.t);
-
-    sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), false);
-    sliderGroup.call(drawCurrentValue, sliderOpts);
-
+  // This is related to the other long notes in this file regarding what happens
+  // when slider steps disappear. This particular fix handles what happens when
+  // the *current* slider step is removed. The drawing functions will error out
+  // when they fail to find it, so the fix for now is that it will just draw the
+  // slider in the first position but will not execute the command.
+  if (sliderOpts.active >= sliderOpts.steps.length) {
+    sliderOpts.active = 0;
+  }
+
+  // These are carefully ordered for proper z-ordering:
+  sliderGroup
+    .call(drawCurrentValue, sliderOpts)
+    .call(drawRail, sliderOpts)
+    .call(drawLabelGroup, sliderOpts)
+    .call(drawTicks, sliderOpts)
+    .call(drawTouchRect, gd, sliderOpts)
+    .call(drawGrip, gd, sliderOpts);
+
+  // Position the rectangle:
+  Drawing.setTranslate(
+    sliderGroup,
+    sliderOpts.lx + sliderOpts.pad.l,
+    sliderOpts.ly + sliderOpts.pad.t
+  );
+
+  sliderGroup.call(
+    setGripPosition,
+    sliderOpts,
+    sliderOpts.active / (sliderOpts.steps.length - 1),
+    false
+  );
+  sliderGroup.call(drawCurrentValue, sliderOpts);
 }
 
 function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) {
-    if(!sliderOpts.currentvalue.visible) return;
-
-    var x0, textAnchor;
-    var text = sliderGroup.selectAll('text')
-        .data([0]);
-
-    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;
-            textAnchor = 'left';
-            break;
-        case 'center':
-            x0 = sliderOpts.inputAreaLength * 0.5;
-            textAnchor = 'middle';
-            break;
-        default:
-            x0 = constants.currentValueInset;
-            textAnchor = 'left';
-    }
-
-    text.enter().append('text')
-        .classed(constants.labelClass, true)
-        .classed('user-select-none', true)
-        .attr('text-anchor', textAnchor);
-
-    var str = sliderOpts.currentvalue.prefix ? sliderOpts.currentvalue.prefix : '';
-
-    if(typeof valueOverride === 'string') {
-        str += valueOverride;
-    } else {
-        var curVal = sliderOpts.steps[sliderOpts.active].label;
-        str += curVal;
-    }
-
-    if(sliderOpts.currentvalue.suffix) {
-        str += sliderOpts.currentvalue.suffix;
-    }
-
-    text.call(Drawing.font, sliderOpts.currentvalue.font)
-        .text(str)
-        .call(svgTextUtils.convertToTspans);
-
-    Drawing.setTranslate(text, x0, sliderOpts.currentValueHeight);
-
-    return text;
+  if (!sliderOpts.currentvalue.visible) return;
+
+  var x0, textAnchor;
+  var text = sliderGroup.selectAll("text").data([0]);
+
+  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;
+      textAnchor = "left";
+      break;
+    case "center":
+      x0 = sliderOpts.inputAreaLength * 0.5;
+      textAnchor = "middle";
+      break;
+    default:
+      x0 = constants.currentValueInset;
+      textAnchor = "left";
+  }
+
+  text
+    .enter()
+    .append("text")
+    .classed(constants.labelClass, true)
+    .classed("user-select-none", true)
+    .attr("text-anchor", textAnchor);
+
+  var str = sliderOpts.currentvalue.prefix
+    ? sliderOpts.currentvalue.prefix
+    : "";
+
+  if (typeof valueOverride === "string") {
+    str += valueOverride;
+  } else {
+    var curVal = sliderOpts.steps[sliderOpts.active].label;
+    str += curVal;
+  }
+
+  if (sliderOpts.currentvalue.suffix) {
+    str += sliderOpts.currentvalue.suffix;
+  }
+
+  text
+    .call(Drawing.font, sliderOpts.currentvalue.font)
+    .text(str)
+    .call(svgTextUtils.convertToTspans);
+
+  Drawing.setTranslate(text, x0, sliderOpts.currentValueHeight);
+
+  return text;
 }
 
 function drawGrip(sliderGroup, gd, sliderOpts) {
-    var grip = sliderGroup.selectAll('rect.' + constants.gripRectClass)
-        .data([0]);
-
-    grip.enter().append('rect')
-        .classed(constants.gripRectClass, true)
-        .call(attachGripEvents, gd, sliderGroup, sliderOpts)
-        .style('pointer-events', 'all');
-
-    grip.attr({
-        width: constants.gripWidth,
-        height: constants.gripHeight,
-        rx: constants.gripRadius,
-        ry: constants.gripRadius,
+  var grip = sliderGroup.selectAll("rect." + constants.gripRectClass).data([0]);
+
+  grip
+    .enter()
+    .append("rect")
+    .classed(constants.gripRectClass, true)
+    .call(attachGripEvents, gd, sliderGroup, sliderOpts)
+    .style("pointer-events", "all");
+
+  grip
+    .attr({
+      width: constants.gripWidth,
+      height: constants.gripHeight,
+      rx: constants.gripRadius,
+      ry: constants.gripRadius
     })
-        .call(Color.stroke, sliderOpts.bordercolor)
-        .call(Color.fill, sliderOpts.bgcolor)
-        .style('stroke-width', sliderOpts.borderwidth + 'px');
+    .call(Color.stroke, sliderOpts.bordercolor)
+    .call(Color.fill, sliderOpts.bgcolor)
+    .style("stroke-width", sliderOpts.borderwidth + "px");
 }
 
 function drawLabel(item, data, sliderOpts) {
-    var text = item.selectAll('text')
-        .data([0]);
+  var text = item.selectAll("text").data([0]);
 
-    text.enter().append('text')
-        .classed(constants.labelClass, true)
-        .classed('user-select-none', true)
-        .attr('text-anchor', 'middle');
+  text
+    .enter()
+    .append("text")
+    .classed(constants.labelClass, true)
+    .classed("user-select-none", true)
+    .attr("text-anchor", "middle");
 
-    text.call(Drawing.font, sliderOpts.font)
-        .text(data.step.label)
-        .call(svgTextUtils.convertToTspans);
+  text
+    .call(Drawing.font, sliderOpts.font)
+    .text(data.step.label)
+    .call(svgTextUtils.convertToTspans);
 
-    return text;
+  return text;
 }
 
 function drawLabelGroup(sliderGroup, sliderOpts) {
-    var labels = sliderGroup.selectAll('g.' + constants.labelsClass)
-        .data([0]);
+  var labels = sliderGroup.selectAll("g." + constants.labelsClass).data([0]);
 
-    labels.enter().append('g')
-        .classed(constants.labelsClass, true);
+  labels.enter().append("g").classed(constants.labelsClass, true);
 
-    var labelItems = labels.selectAll('g.' + constants.labelGroupClass)
-        .data(sliderOpts.labelSteps);
+  var labelItems = labels
+    .selectAll("g." + constants.labelGroupClass)
+    .data(sliderOpts.labelSteps);
 
-    labelItems.enter().append('g')
-        .classed(constants.labelGroupClass, true);
+  labelItems.enter().append("g").classed(constants.labelGroupClass, true);
 
-    labelItems.exit().remove();
+  labelItems.exit().remove();
 
-    labelItems.each(function(d) {
-        var item = d3.select(this);
+  labelItems.each(function(d) {
+    var item = d3.select(this);
 
-        item.call(drawLabel, d, sliderOpts);
-
-        Drawing.setTranslate(item,
-            normalizedValueToPosition(sliderOpts, d.fraction),
-            constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight + constants.labelOffset + sliderOpts.currentValueTotalHeight
-        );
-    });
+    item.call(drawLabel, d, sliderOpts);
 
+    Drawing.setTranslate(
+      item,
+      normalizedValueToPosition(sliderOpts, d.fraction),
+      constants.tickOffset +
+        sliderOpts.ticklen +
+        sliderOpts.labelHeight +
+        constants.labelOffset +
+        sliderOpts.currentValueTotalHeight
+    );
+  });
 }
 
-function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, doTransition) {
-    var quantizedPosition = Math.round(normalizedPosition * (sliderOpts.steps.length - 1));
-
-    if(quantizedPosition !== sliderOpts.active) {
-        setActive(gd, sliderGroup, sliderOpts, quantizedPosition, true, doTransition);
-    }
+function handleInput(
+  gd,
+  sliderGroup,
+  sliderOpts,
+  normalizedPosition,
+  doTransition
+) {
+  var quantizedPosition = Math.round(
+    normalizedPosition * (sliderOpts.steps.length - 1)
+  );
+
+  if (quantizedPosition !== sliderOpts.active) {
+    setActive(
+      gd,
+      sliderGroup,
+      sliderOpts,
+      quantizedPosition,
+      true,
+      doTransition
+    );
+  }
 }
 
-function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) {
-    var previousActive = sliderOpts.active;
-    sliderOpts._input.active = sliderOpts.active = index;
-
-    var step = sliderOpts.steps[sliderOpts.active];
-
-    sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition);
-    sliderGroup.call(drawCurrentValue, sliderOpts);
-
-    gd.emit('plotly_sliderchange', {
-        slider: sliderOpts,
-        step: sliderOpts.steps[sliderOpts.active],
-        interaction: doCallback,
-        previousActive: previousActive
-    });
-
-    if(step && step.method && doCallback) {
-        if(sliderGroup._nextMethod) {
-            // If we've already queued up an update, just overwrite it with the most recent:
-            sliderGroup._nextMethod.step = step;
-            sliderGroup._nextMethod.doCallback = doCallback;
-            sliderGroup._nextMethod.doTransition = doTransition;
-        } else {
-            sliderGroup._nextMethod = {step: step, doCallback: doCallback, doTransition: doTransition};
-            sliderGroup._nextMethodRaf = window.requestAnimationFrame(function() {
-                var _step = sliderGroup._nextMethod.step;
-                if(!_step.method) return;
-
-                Plots.executeAPICommand(gd, _step.method, _step.args);
-
-                sliderGroup._nextMethod = null;
-                sliderGroup._nextMethodRaf = null;
-            });
-        }
+function setActive(
+  gd,
+  sliderGroup,
+  sliderOpts,
+  index,
+  doCallback,
+  doTransition
+) {
+  var previousActive = sliderOpts.active;
+  sliderOpts._input.active = sliderOpts.active = index;
+
+  var step = sliderOpts.steps[sliderOpts.active];
+
+  sliderGroup.call(
+    setGripPosition,
+    sliderOpts,
+    sliderOpts.active / (sliderOpts.steps.length - 1),
+    doTransition
+  );
+  sliderGroup.call(drawCurrentValue, sliderOpts);
+
+  gd.emit("plotly_sliderchange", {
+    slider: sliderOpts,
+    step: sliderOpts.steps[sliderOpts.active],
+    interaction: doCallback,
+    previousActive: previousActive
+  });
+
+  if (step && step.method && doCallback) {
+    if (sliderGroup._nextMethod) {
+      // If we've already queued up an update, just overwrite it with the most recent:
+      sliderGroup._nextMethod.step = step;
+      sliderGroup._nextMethod.doCallback = doCallback;
+      sliderGroup._nextMethod.doTransition = doTransition;
+    } else {
+      sliderGroup._nextMethod = {
+        step: step,
+        doCallback: doCallback,
+        doTransition: doTransition
+      };
+      sliderGroup._nextMethodRaf = window.requestAnimationFrame(function() {
+        var _step = sliderGroup._nextMethod.step;
+        if (!_step.method) return;
+
+        Plots.executeAPICommand(gd, _step.method, _step.args);
+
+        sliderGroup._nextMethod = null;
+        sliderGroup._nextMethodRaf = null;
+      });
     }
+  }
 }
 
 function attachGripEvents(item, gd, sliderGroup) {
-    var node = sliderGroup.node();
-    var $gd = d3.select(gd);
-
-    // NB: This is *not* the same as sliderOpts itself! These callbacks
-    // are in a closure so this array won't actually be correct if the
-    // steps have changed since this was initialized. The sliderGroup,
-    // however, has not changed since that *is* the slider, so it must
-    // be present to receive mouse events.
-    function getSliderOpts() {
-        return sliderGroup.data()[0];
-    }
+  var node = sliderGroup.node();
+  var $gd = d3.select(gd);
+
+  // NB: This is *not* the same as sliderOpts itself! These callbacks
+  // are in a closure so this array won't actually be correct if the
+  // steps have changed since this was initialized. The sliderGroup,
+  // however, has not changed since that *is* the slider, so it must
+  // be present to receive mouse events.
+  function getSliderOpts() {
+    return sliderGroup.data()[0];
+  }
+
+  item.on("mousedown", function() {
+    var sliderOpts = getSliderOpts();
+    gd.emit("plotly_sliderstart", { slider: sliderOpts });
+
+    var grip = sliderGroup.select("." + constants.gripRectClass);
+
+    d3.event.stopPropagation();
+    d3.event.preventDefault();
+    grip.call(Color.fill, sliderOpts.activebgcolor);
+
+    var normalizedPosition = positionToNormalizedValue(
+      sliderOpts,
+      d3.mouse(node)[0]
+    );
+    handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, true);
+    sliderOpts._dragging = true;
+
+    $gd.on("mousemove", function() {
+      var sliderOpts = getSliderOpts();
+      var normalizedPosition = positionToNormalizedValue(
+        sliderOpts,
+        d3.mouse(node)[0]
+      );
+      handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, false);
+    });
+
+    $gd.on("mouseup", function() {
+      var sliderOpts = getSliderOpts();
+      sliderOpts._dragging = false;
+      grip.call(Color.fill, sliderOpts.bgcolor);
+      $gd.on("mouseup", null);
+      $gd.on("mousemove", null);
 
-    item.on('mousedown', function() {
-        var sliderOpts = getSliderOpts();
-        gd.emit('plotly_sliderstart', {slider: sliderOpts});
-
-        var grip = sliderGroup.select('.' + constants.gripRectClass);
-
-        d3.event.stopPropagation();
-        d3.event.preventDefault();
-        grip.call(Color.fill, sliderOpts.activebgcolor);
-
-        var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]);
-        handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, true);
-        sliderOpts._dragging = true;
-
-        $gd.on('mousemove', function() {
-            var sliderOpts = getSliderOpts();
-            var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]);
-            handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, false);
-        });
-
-        $gd.on('mouseup', function() {
-            var sliderOpts = getSliderOpts();
-            sliderOpts._dragging = false;
-            grip.call(Color.fill, sliderOpts.bgcolor);
-            $gd.on('mouseup', null);
-            $gd.on('mousemove', null);
-
-            gd.emit('plotly_sliderend', {
-                slider: sliderOpts,
-                step: sliderOpts.steps[sliderOpts.active]
-            });
-        });
+      gd.emit("plotly_sliderend", {
+        slider: sliderOpts,
+        step: sliderOpts.steps[sliderOpts.active]
+      });
     });
+  });
 }
 
 function drawTicks(sliderGroup, sliderOpts) {
-    var tick = sliderGroup.selectAll('rect.' + constants.tickRectClass)
-        .data(sliderOpts.steps);
+  var tick = sliderGroup
+    .selectAll("rect." + constants.tickRectClass)
+    .data(sliderOpts.steps);
 
-    tick.enter().append('rect')
-        .classed(constants.tickRectClass, true);
+  tick.enter().append("rect").classed(constants.tickRectClass, true);
 
-    tick.exit().remove();
+  tick.exit().remove();
 
-    tick.attr({
-        width: sliderOpts.tickwidth + 'px',
-        'shape-rendering': 'crispEdges'
-    });
-
-    tick.each(function(d, i) {
-        var isMajor = i % sliderOpts.labelStride === 0;
-        var item = d3.select(this);
+  tick.attr({
+    width: sliderOpts.tickwidth + "px",
+    "shape-rendering": "crispEdges"
+  });
 
-        item
-            .attr({height: isMajor ? sliderOpts.ticklen : sliderOpts.minorticklen})
-            .call(Color.fill, isMajor ? sliderOpts.tickcolor : sliderOpts.tickcolor);
+  tick.each(function(d, i) {
+    var isMajor = i % sliderOpts.labelStride === 0;
+    var item = d3.select(this);
 
-        Drawing.setTranslate(item,
-            normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * sliderOpts.tickwidth,
-            (isMajor ? constants.tickOffset : constants.minorTickOffset) + sliderOpts.currentValueTotalHeight
-        );
-    });
+    item
+      .attr({ height: isMajor ? sliderOpts.ticklen : sliderOpts.minorticklen })
+      .call(Color.fill, isMajor ? sliderOpts.tickcolor : sliderOpts.tickcolor);
 
+    Drawing.setTranslate(
+      item,
+      normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) -
+        0.5 * sliderOpts.tickwidth,
+      (isMajor ? constants.tickOffset : constants.minorTickOffset) +
+        sliderOpts.currentValueTotalHeight
+    );
+  });
 }
 
 function computeLabelSteps(sliderOpts) {
-    sliderOpts.labelSteps = [];
-    var i0 = 0;
-    var nsteps = sliderOpts.steps.length;
-
-    for(var i = i0; i < nsteps; i += sliderOpts.labelStride) {
-        sliderOpts.labelSteps.push({
-            fraction: i / (nsteps - 1),
-            step: sliderOpts.steps[i]
-        });
-    }
+  sliderOpts.labelSteps = [];
+  var i0 = 0;
+  var nsteps = sliderOpts.steps.length;
+
+  for (var i = i0; i < nsteps; i += sliderOpts.labelStride) {
+    sliderOpts.labelSteps.push({
+      fraction: i / (nsteps - 1),
+      step: sliderOpts.steps[i]
+    });
+  }
 }
 
 function setGripPosition(sliderGroup, sliderOpts, position, doTransition) {
-    var grip = sliderGroup.select('rect.' + constants.gripRectClass);
-
-    var x = normalizedValueToPosition(sliderOpts, position);
-
-    // If this is true, then *this component* is already invoking its own command
-    // and has triggered its own animation.
-    if(sliderOpts._invokingCommand) return;
-
-    var el = grip;
-    if(doTransition && sliderOpts.transition.duration > 0) {
-        el = el.transition()
-            .duration(sliderOpts.transition.duration)
-            .ease(sliderOpts.transition.easing);
-    }
-
-    // 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) + ')');
+  var grip = sliderGroup.select("rect." + constants.gripRectClass);
+
+  var x = normalizedValueToPosition(sliderOpts, position);
+
+  // If this is true, then *this component* is already invoking its own command
+  // and has triggered its own animation.
+  if (sliderOpts._invokingCommand) return;
+
+  var el = grip;
+  if (doTransition && sliderOpts.transition.duration > 0) {
+    el = el
+      .transition()
+      .duration(sliderOpts.transition.duration)
+      .ease(sliderOpts.transition.easing);
+  }
+
+  // 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 +
+      ")"
+  );
 }
 
 // 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));
+  return sliderOpts.inputAreaStart +
+    constants.stepInset +
+    (sliderOpts.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)));
+  return Math.min(
+    1,
+    Math.max(
+      0,
+      (position - constants.stepInset - sliderOpts.inputAreaStart) /
+        (sliderOpts.inputAreaLength -
+          2 * constants.stepInset -
+          2 * sliderOpts.inputAreaStart)
+    )
+  );
 }
 
 function drawTouchRect(sliderGroup, gd, sliderOpts) {
-    var rect = sliderGroup.selectAll('rect.' + constants.railTouchRectClass)
-        .data([0]);
-
-    rect.enter().append('rect')
-        .classed(constants.railTouchRectClass, true)
-        .call(attachGripEvents, gd, sliderGroup, sliderOpts)
-        .style('pointer-events', 'all');
-
-    rect.attr({
-        width: sliderOpts.inputAreaLength,
-        height: Math.max(sliderOpts.inputAreaWidth, constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight)
+  var rect = sliderGroup
+    .selectAll("rect." + constants.railTouchRectClass)
+    .data([0]);
+
+  rect
+    .enter()
+    .append("rect")
+    .classed(constants.railTouchRectClass, true)
+    .call(attachGripEvents, gd, sliderGroup, sliderOpts)
+    .style("pointer-events", "all");
+
+  rect
+    .attr({
+      width: sliderOpts.inputAreaLength,
+      height: Math.max(
+        sliderOpts.inputAreaWidth,
+        constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight
+      )
     })
-        .call(Color.fill, sliderOpts.bgcolor)
-        .attr('opacity', 0);
+    .call(Color.fill, sliderOpts.bgcolor)
+    .attr("opacity", 0);
 
-    Drawing.setTranslate(rect, 0, sliderOpts.currentValueTotalHeight);
+  Drawing.setTranslate(rect, 0, sliderOpts.currentValueTotalHeight);
 }
 
 function drawRail(sliderGroup, sliderOpts) {
-    var rect = sliderGroup.selectAll('rect.' + constants.railRectClass)
-        .data([0]);
+  var rect = sliderGroup.selectAll("rect." + constants.railRectClass).data([0]);
 
-    rect.enter().append('rect')
-        .classed(constants.railRectClass, true);
+  rect.enter().append("rect").classed(constants.railRectClass, true);
 
-    var computedLength = sliderOpts.inputAreaLength - constants.railInset * 2;
+  var computedLength = sliderOpts.inputAreaLength - constants.railInset * 2;
 
-    rect.attr({
-        width: computedLength,
-        height: constants.railWidth,
-        rx: constants.railRadius,
-        ry: constants.railRadius,
-        'shape-rendering': 'crispEdges'
+  rect
+    .attr({
+      width: computedLength,
+      height: constants.railWidth,
+      rx: constants.railRadius,
+      ry: constants.railRadius,
+      "shape-rendering": "crispEdges"
     })
-        .call(Color.stroke, sliderOpts.bordercolor)
-        .call(Color.fill, sliderOpts.bgcolor)
-        .style('stroke-width', sliderOpts.borderwidth + 'px');
-
-    Drawing.setTranslate(rect,
-        constants.railInset,
-        (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5 + sliderOpts.currentValueTotalHeight
-    );
+    .call(Color.stroke, sliderOpts.bordercolor)
+    .call(Color.fill, sliderOpts.bgcolor)
+    .style("stroke-width", sliderOpts.borderwidth + "px");
+
+  Drawing.setTranslate(
+    rect,
+    constants.railInset,
+    (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5 +
+      sliderOpts.currentValueTotalHeight
+  );
 }
 
 function clearPushMargins(gd) {
-    var pushMargins = gd._fullLayout._pushmargin || {},
-        keys = Object.keys(pushMargins);
+  var pushMargins = gd._fullLayout._pushmargin || {},
+    keys = Object.keys(pushMargins);
 
-    for(var i = 0; i < keys.length; i++) {
-        var k = keys[i];
+  for (var i = 0; i < keys.length; i++) {
+    var k = keys[i];
 
-        if(k.indexOf(constants.autoMarginIdRoot) !== -1) {
-            Plots.autoMargin(gd, k);
-        }
+    if (k.indexOf(constants.autoMarginIdRoot) !== -1) {
+      Plots.autoMargin(gd, k);
     }
+  }
 }
diff --git a/src/components/sliders/index.js b/src/components/sliders/index.js
index 366b9aaa1ad..b44b6e9dab9 100644
--- a/src/components/sliders/index.js
+++ b/src/components/sliders/index.js
@@ -5,17 +5,13 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var constants = require('./constants');
+"use strict";
+var constants = require("./constants");
 
 module.exports = {
-    moduleType: 'component',
-    name: constants.name,
-
-    layoutAttributes: require('./attributes'),
-    supplyLayoutDefaults: require('./defaults'),
-
-    draw: require('./draw')
+  moduleType: "component",
+  name: constants.name,
+  layoutAttributes: require("./attributes"),
+  supplyLayoutDefaults: require("./defaults"),
+  draw: require("./draw")
 };
diff --git a/src/components/titles/index.js b/src/components/titles/index.js
index 6fa0e64c6af..33171b7f68b 100644
--- a/src/components/titles/index.js
+++ b/src/components/titles/index.js
@@ -5,21 +5,17 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var d3 = require('d3');
-var isNumeric = require('fast-isnumeric');
-
-var Plotly = require('../../plotly');
-var Plots = require('../../plots/plots');
-var Lib = require('../../lib');
-var Drawing = require('../drawing');
-var Color = require('../color');
-var svgTextUtils = require('../../lib/svg_text_utils');
-var interactConstants = require('../../constants/interactions');
-
+"use strict";
+var d3 = require("d3");
+var isNumeric = require("fast-isnumeric");
+
+var Plotly = require("../../plotly");
+var Plots = require("../../plots/plots");
+var Lib = require("../../lib");
+var Drawing = require("../drawing");
+var Color = require("../color");
+var svgTextUtils = require("../../lib/svg_text_utils");
+var interactConstants = require("../../constants/interactions");
 
 var Titles = module.exports = {};
 
@@ -52,180 +48,186 @@ var Titles = module.exports = {};
  *          title, include here. Otherwise it will go in fullLayout._infolayer
  */
 Titles.draw = function(gd, titleClass, options) {
-    var cont = options.propContainer,
-        prop = options.propName,
-        traceIndex = options.traceIndex,
-        name = options.dfltName,
-        avoid = options.avoid || {},
-        attributes = options.attributes,
-        transform = options.transform,
-        group = options.containerGroup,
-
-        fullLayout = gd._fullLayout,
-        font = cont.titlefont.family,
-        fontSize = cont.titlefont.size,
-        fontColor = cont.titlefont.color,
-
-        opacity = 1,
-        isplaceholder = false,
-        txt = cont.title.trim();
-    if(txt === '') opacity = 0;
-    if(txt.match(/Click to enter .+ title/)) {
-        opacity = 0.2;
-        isplaceholder = true;
-    }
-
-    if(!group) {
-        group = fullLayout._infolayer.selectAll('.g-' + titleClass)
-            .data([0]);
-        group.enter().append('g')
-            .classed('g-' + titleClass, true);
-    }
-
-    var el = group.selectAll('text')
-        .data([0]);
-    el.enter().append('text');
-    el.text(txt)
-        // this is hacky, but convertToTspans uses the class
-        // to determine whether to rotate mathJax...
-        // so we need to clear out any old class and put the
-        // correct one (only relevant for colorbars, at least
-        // for now) - ie don't use .classed
-        .attr('class', titleClass);
-
-    function titleLayout(titleEl) {
-        Lib.syncOrAsync([drawTitle, scootTitle], titleEl);
+  var cont = options.propContainer,
+    prop = options.propName,
+    traceIndex = options.traceIndex,
+    name = options.dfltName,
+    avoid = options.avoid || {},
+    attributes = options.attributes,
+    transform = options.transform,
+    group = options.containerGroup,
+    fullLayout = gd._fullLayout,
+    font = cont.titlefont.family,
+    fontSize = cont.titlefont.size,
+    fontColor = cont.titlefont.color,
+    opacity = 1,
+    isplaceholder = false,
+    txt = cont.title.trim();
+  if (txt === "") opacity = 0;
+  if (txt.match(/Click to enter .+ title/)) {
+    opacity = 0.2;
+    isplaceholder = true;
+  }
+
+  if (!group) {
+    group = fullLayout._infolayer.selectAll(".g-" + titleClass).data([0]);
+    group.enter().append("g").classed("g-" + titleClass, true);
+  }
+
+  var el = group.selectAll("text").data([0]);
+  el.enter().append("text");
+  el.text(txt).attr("class", titleClass);
+
+  function titleLayout(titleEl) {
+    Lib.syncOrAsync([drawTitle, scootTitle], titleEl);
+  }
+
+  function drawTitle(titleEl) {
+    titleEl.attr(
+      "transform",
+      transform
+        ? "rotate(" +
+            [transform.rotate, attributes.x, attributes.y] +
+            ") translate(0, " +
+            transform.offset +
+            ")"
+        : null
+    );
+
+    titleEl
+      .style({
+        "font-family": font,
+        "font-size": d3.round(fontSize, 2) + "px",
+        fill: Color.rgb(fontColor),
+        opacity: opacity * Color.opacity(fontColor),
+        "font-weight": Plots.fontWeight
+      })
+      .attr(attributes)
+      .call(svgTextUtils.convertToTspans)
+      .attr(attributes);
+
+    titleEl.selectAll("tspan.line").attr(attributes);
+    return Plots.previousPromises(gd);
+  }
+
+  function scootTitle(titleElIn) {
+    var titleGroup = d3.select(titleElIn.node().parentNode);
+
+    if (avoid && avoid.selection && avoid.side && txt) {
+      titleGroup.attr("transform", null);
+
+      // move toward avoid.side (= left, right, top, bottom) if needed
+      // can include pad (pixels, default 2)
+      var shift = 0,
+        backside = ({
+          left: "right",
+          right: "left",
+          top: "bottom",
+          bottom: "top"
+        })[avoid.side],
+        shiftSign = ["left", "top"].indexOf(avoid.side) !== -1 ? -1 : 1,
+        pad = isNumeric(avoid.pad) ? avoid.pad : 2,
+        titlebb = Drawing.bBox(titleGroup.node()),
+        paperbb = {
+          left: 0,
+          top: 0,
+          right: fullLayout.width,
+          bottom: fullLayout.height
+        },
+        maxshift = avoid.maxShift ||
+          (paperbb[avoid.side] - titlebb[avoid.side]) *
+            (avoid.side === "left" || avoid.side === "top" ? -1 : 1);
+      // Prevent the title going off the paper
+      if (maxshift < 0) {
+        shift = maxshift;
+      } else {
+        // so we don't have to offset each avoided element,
+        // give the title the opposite offset
+        var offsetLeft = avoid.offsetLeft || 0,
+          offsetTop = avoid.offsetTop || 0;
+        titlebb.left -= offsetLeft;
+        titlebb.right -= offsetLeft;
+        titlebb.top -= offsetTop;
+        titlebb.bottom -= offsetTop;
+
+        // iterate over a set of elements (avoid.selection)
+        // to avoid collisions with
+        avoid.selection.each(function() {
+          var avoidbb = Drawing.bBox(this);
+
+          if (Lib.bBoxIntersect(titlebb, avoidbb, pad)) {
+            shift = Math.max(
+              shift,
+              shiftSign * (avoidbb[avoid.side] - titlebb[backside]) + pad
+            );
+          }
+        });
+        shift = Math.min(maxshift, shift);
+      }
+      if (shift > 0 || maxshift < 0) {
+        var shiftTemplate = ({
+          left: [-shift, 0],
+          right: [shift, 0],
+          top: [0, -shift],
+          bottom: [0, shift]
+        })[avoid.side];
+        titleGroup.attr("transform", "translate(" + shiftTemplate + ")");
+      }
     }
-
-    function drawTitle(titleEl) {
-        titleEl.attr('transform', transform ?
-            'rotate(' + [transform.rotate, attributes.x, attributes.y] +
-                ') translate(0, ' + transform.offset + ')' :
-            null);
-
-        titleEl.style({
-            'font-family': font,
-            'font-size': d3.round(fontSize, 2) + 'px',
-            fill: Color.rgb(fontColor),
-            opacity: opacity * Color.opacity(fontColor),
-            'font-weight': Plots.fontWeight
-        })
-        .attr(attributes)
-        .call(svgTextUtils.convertToTspans)
-        .attr(attributes);
-
-        titleEl.selectAll('tspan.line')
-            .attr(attributes);
-        return Plots.previousPromises(gd);
-    }
-
-    function scootTitle(titleElIn) {
-        var titleGroup = d3.select(titleElIn.node().parentNode);
-
-        if(avoid && avoid.selection && avoid.side && txt) {
-            titleGroup.attr('transform', null);
-
-            // move toward avoid.side (= left, right, top, bottom) if needed
-            // can include pad (pixels, default 2)
-            var shift = 0,
-                backside = {
-                    left: 'right',
-                    right: 'left',
-                    top: 'bottom',
-                    bottom: 'top'
-                }[avoid.side],
-                shiftSign = (['left', 'top'].indexOf(avoid.side) !== -1) ?
-                    -1 : 1,
-                pad = isNumeric(avoid.pad) ? avoid.pad : 2,
-                titlebb = Drawing.bBox(titleGroup.node()),
-                paperbb = {
-                    left: 0,
-                    top: 0,
-                    right: fullLayout.width,
-                    bottom: fullLayout.height
-                },
-                maxshift = avoid.maxShift || (
-                    (paperbb[avoid.side] - titlebb[avoid.side]) *
-                    ((avoid.side === 'left' || avoid.side === 'top') ? -1 : 1));
-            // Prevent the title going off the paper
-            if(maxshift < 0) shift = maxshift;
-            else {
-                // so we don't have to offset each avoided element,
-                // give the title the opposite offset
-                var offsetLeft = avoid.offsetLeft || 0,
-                    offsetTop = avoid.offsetTop || 0;
-                titlebb.left -= offsetLeft;
-                titlebb.right -= offsetLeft;
-                titlebb.top -= offsetTop;
-                titlebb.bottom -= offsetTop;
-
-                // iterate over a set of elements (avoid.selection)
-                // to avoid collisions with
-                avoid.selection.each(function() {
-                    var avoidbb = Drawing.bBox(this);
-
-                    if(Lib.bBoxIntersect(titlebb, avoidbb, pad)) {
-                        shift = Math.max(shift, shiftSign * (
-                            avoidbb[avoid.side] - titlebb[backside]) + pad);
-                    }
-                });
-                shift = Math.min(maxshift, shift);
-            }
-            if(shift > 0 || maxshift < 0) {
-                var shiftTemplate = {
-                    left: [-shift, 0],
-                    right: [shift, 0],
-                    top: [0, -shift],
-                    bottom: [0, shift]
-                }[avoid.side];
-                titleGroup.attr('transform',
-                    'translate(' + shiftTemplate + ')');
-            }
+  }
+
+  el.attr({ "data-unformatted": txt }).call(titleLayout);
+
+  var placeholderText = "Click to enter " + name + " title";
+
+  function setPlaceholder() {
+    opacity = 0;
+    isplaceholder = true;
+    txt = placeholderText;
+    el
+      .attr({ "data-unformatted": txt })
+      .text(txt)
+      .on("mouseover.opacity", function() {
+        d3
+          .select(this)
+          .transition()
+          .duration(interactConstants.SHOW_PLACEHOLDER)
+          .style("opacity", 1);
+      })
+      .on("mouseout.opacity", function() {
+        d3
+          .select(this)
+          .transition()
+          .duration(interactConstants.HIDE_PLACEHOLDER)
+          .style("opacity", 0);
+      });
+  }
+
+  if (gd._context.editable) {
+    if (!txt) setPlaceholder();
+    else el.on(".opacity", null);
+
+    el
+      .call(svgTextUtils.makeEditable)
+      .on("edit", function(text) {
+        if (traceIndex !== undefined) {
+          Plotly.restyle(gd, prop, text, traceIndex);
+        } else {
+          Plotly.relayout(gd, prop, text);
         }
-    }
-
-    el.attr({'data-unformatted': txt})
-        .call(titleLayout);
-
-    var placeholderText = 'Click to enter ' + name + ' title';
-
-    function setPlaceholder() {
-        opacity = 0;
-        isplaceholder = true;
-        txt = placeholderText;
-        el.attr({'data-unformatted': txt})
-            .text(txt)
-            .on('mouseover.opacity', function() {
-                d3.select(this).transition()
-                    .duration(interactConstants.SHOW_PLACEHOLDER).style('opacity', 1);
-            })
-            .on('mouseout.opacity', function() {
-                d3.select(this).transition()
-                    .duration(interactConstants.HIDE_PLACEHOLDER).style('opacity', 0);
-            });
-    }
-
-    if(gd._context.editable) {
-        if(!txt) setPlaceholder();
-        else el.on('.opacity', null);
-
-        el.call(svgTextUtils.makeEditable)
-            .on('edit', function(text) {
-                if(traceIndex !== undefined) Plotly.restyle(gd, prop, text, traceIndex);
-                else Plotly.relayout(gd, prop, text);
-            })
-            .on('cancel', function() {
-                this.text(this.attr('data-unformatted'))
-                    .call(titleLayout);
-            })
-            .on('input', function(d) {
-                this.text(d || ' ').attr(attributes)
-                    .selectAll('tspan.line')
-                        .attr(attributes);
-            });
-    }
-    else if(!txt || txt.match(/Click to enter .+ title/)) {
-        el.remove();
-    }
-    el.classed('js-placeholder', isplaceholder);
+      })
+      .on("cancel", function() {
+        this.text(this.attr("data-unformatted")).call(titleLayout);
+      })
+      .on("input", function(d) {
+        this
+          .text(d || " ")
+          .attr(attributes)
+          .selectAll("tspan.line")
+          .attr(attributes);
+      });
+  } else if (!txt || txt.match(/Click to enter .+ title/)) {
+    el.remove();
+  }
+  el.classed("js-placeholder", isplaceholder);
 };
diff --git a/src/components/updatemenus/attributes.js b/src/components/updatemenus/attributes.js
index f04465f9ec3..14b36849a96 100644
--- a/src/components/updatemenus/attributes.js
+++ b/src/components/updatemenus/attributes.js
@@ -5,166 +5,147 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var fontAttrs = require('../../plots/font_attributes');
-var colorAttrs = require('../color/attributes');
-var extendFlat = require('../../lib/extend').extendFlat;
-var padAttrs = require('../../plots/pad_attributes');
+"use strict";
+var fontAttrs = require("../../plots/font_attributes");
+var colorAttrs = require("../color/attributes");
+var extendFlat = require("../../lib/extend").extendFlat;
+var padAttrs = require("../../plots/pad_attributes");
 
 var buttonsAttrs = {
-    _isLinkedToArray: 'button',
-
-    method: {
-        valType: 'enumerated',
-        values: ['restyle', 'relayout', 'animate', 'update'],
-        dflt: 'restyle',
-        role: 'info',
-        description: [
-            'Sets the Plotly method to be called on click.'
-        ].join(' ')
-    },
-    args: {
-        valType: 'info_array',
-        role: 'info',
-        freeLength: true,
-        items: [
-            { valType: 'any' },
-            { valType: 'any' },
-            { valType: 'any' }
-        ],
-        description: [
-            'Sets the arguments values to be passed to the Plotly',
-            'method set in `method` on click.'
-        ].join(' ')
-    },
-    label: {
-        valType: 'string',
-        role: 'info',
-        dflt: '',
-        description: 'Sets the text label to appear on the button.'
-    }
+  _isLinkedToArray: "button",
+  method: {
+    valType: "enumerated",
+    values: ["restyle", "relayout", "animate", "update"],
+    dflt: "restyle",
+    role: "info",
+    description: ["Sets the Plotly method to be called on click."].join(" ")
+  },
+  args: {
+    valType: "info_array",
+    role: "info",
+    freeLength: true,
+    items: [{ valType: "any" }, { valType: "any" }, { valType: "any" }],
+    description: [
+      "Sets the arguments values to be passed to the Plotly",
+      "method set in `method` on click."
+    ].join(" ")
+  },
+  label: {
+    valType: "string",
+    role: "info",
+    dflt: "",
+    description: "Sets the text label to appear on the button."
+  }
 };
 
 module.exports = {
-    _isLinkedToArray: 'updatemenu',
-
-    visible: {
-        valType: 'boolean',
-        role: 'info',
-        description: [
-            'Determines whether or not the update menu is visible.'
-        ].join(' ')
-    },
-
-    type: {
-        valType: 'enumerated',
-        values: ['dropdown', 'buttons'],
-        dflt: 'dropdown',
-        role: 'info',
-        description: [
-            'Determines whether the buttons are accessible via a dropdown menu',
-            'or whether the buttons are stacked horizontally or vertically'
-        ].join(' ')
-    },
-
-    direction: {
-        valType: 'enumerated',
-        values: ['left', 'right', 'up', 'down'],
-        dflt: 'down',
-        role: 'info',
-        description: [
-            'Determines the direction in which the buttons are laid out, whether',
-            'in a dropdown menu or a row/column of buttons. For `left` and `up`,',
-            'the buttons will still appear in left-to-right or top-to-bottom order',
-            'respectively.'
-        ].join(' ')
-    },
-
-    active: {
-        valType: 'integer',
-        role: 'info',
-        min: -1,
-        dflt: 0,
-        description: [
-            'Determines which button (by index starting from 0) is',
-            'considered active.'
-        ].join(' ')
-    },
-
-    showactive: {
-        valType: 'boolean',
-        role: 'info',
-        dflt: true,
-        description: 'Highlights active dropdown item or active button if true.'
-    },
-
-    buttons: buttonsAttrs,
-
-    x: {
-        valType: 'number',
-        min: -2,
-        max: 3,
-        dflt: -0.05,
-        role: 'style',
-        description: 'Sets the x position (in normalized coordinates) of the update menu.'
-    },
-    xanchor: {
-        valType: 'enumerated',
-        values: ['auto', 'left', 'center', 'right'],
-        dflt: 'right',
-        role: 'info',
-        description: [
-            'Sets the update menu\'s horizontal position anchor.',
-            'This anchor binds the `x` position to the *left*, *center*',
-            'or *right* of the range selector.'
-        ].join(' ')
-    },
-    y: {
-        valType: 'number',
-        min: -2,
-        max: 3,
-        dflt: 1,
-        role: 'style',
-        description: 'Sets the y position (in normalized coordinates) of the update menu.'
-    },
-    yanchor: {
-        valType: 'enumerated',
-        values: ['auto', 'top', 'middle', 'bottom'],
-        dflt: 'top',
-        role: 'info',
-        description: [
-            'Sets the update menu\'s vertical position anchor',
-            'This anchor binds the `y` position to the *top*, *middle*',
-            'or *bottom* of the range selector.'
-        ].join(' ')
-    },
-
-    pad: extendFlat({}, padAttrs, {
-        description: 'Sets the padding around the buttons or dropdown menu.'
-    }),
-
-    font: extendFlat({}, fontAttrs, {
-        description: 'Sets the font of the update menu button text.'
-    }),
-
-    bgcolor: {
-        valType: 'color',
-        role: 'style',
-        description: 'Sets the background color of the update menu buttons.'
-    },
-    bordercolor: {
-        valType: 'color',
-        dflt: colorAttrs.borderLine,
-        role: 'style',
-        description: 'Sets the color of the border enclosing the update menu.'
-    },
-    borderwidth: {
-        valType: 'number',
-        min: 0,
-        dflt: 1,
-        role: 'style',
-        description: 'Sets the width (in px) of the border enclosing the update menu.'
-    }
+  _isLinkedToArray: "updatemenu",
+  visible: {
+    valType: "boolean",
+    role: "info",
+    description: ["Determines whether or not the update menu is visible."].join(
+      " "
+    )
+  },
+  type: {
+    valType: "enumerated",
+    values: ["dropdown", "buttons"],
+    dflt: "dropdown",
+    role: "info",
+    description: [
+      "Determines whether the buttons are accessible via a dropdown menu",
+      "or whether the buttons are stacked horizontally or vertically"
+    ].join(" ")
+  },
+  direction: {
+    valType: "enumerated",
+    values: ["left", "right", "up", "down"],
+    dflt: "down",
+    role: "info",
+    description: [
+      "Determines the direction in which the buttons are laid out, whether",
+      "in a dropdown menu or a row/column of buttons. For `left` and `up`,",
+      "the buttons will still appear in left-to-right or top-to-bottom order",
+      "respectively."
+    ].join(" ")
+  },
+  active: {
+    valType: "integer",
+    role: "info",
+    min: -1,
+    dflt: 0,
+    description: [
+      "Determines which button (by index starting from 0) is",
+      "considered active."
+    ].join(" ")
+  },
+  showactive: {
+    valType: "boolean",
+    role: "info",
+    dflt: true,
+    description: "Highlights active dropdown item or active button if true."
+  },
+  buttons: buttonsAttrs,
+  x: {
+    valType: "number",
+    min: -2,
+    max: 3,
+    dflt: -0.05,
+    role: "style",
+    description: "Sets the x position (in normalized coordinates) of the update menu."
+  },
+  xanchor: {
+    valType: "enumerated",
+    values: ["auto", "left", "center", "right"],
+    dflt: "right",
+    role: "info",
+    description: [
+      "Sets the update menu's horizontal position anchor.",
+      "This anchor binds the `x` position to the *left*, *center*",
+      "or *right* of the range selector."
+    ].join(" ")
+  },
+  y: {
+    valType: "number",
+    min: -2,
+    max: 3,
+    dflt: 1,
+    role: "style",
+    description: "Sets the y position (in normalized coordinates) of the update menu."
+  },
+  yanchor: {
+    valType: "enumerated",
+    values: ["auto", "top", "middle", "bottom"],
+    dflt: "top",
+    role: "info",
+    description: [
+      "Sets the update menu's vertical position anchor",
+      "This anchor binds the `y` position to the *top*, *middle*",
+      "or *bottom* of the range selector."
+    ].join(" ")
+  },
+  pad: extendFlat({}, padAttrs, {
+    description: "Sets the padding around the buttons or dropdown menu."
+  }),
+  font: extendFlat({}, fontAttrs, {
+    description: "Sets the font of the update menu button text."
+  }),
+  bgcolor: {
+    valType: "color",
+    role: "style",
+    description: "Sets the background color of the update menu buttons."
+  },
+  bordercolor: {
+    valType: "color",
+    dflt: colorAttrs.borderLine,
+    role: "style",
+    description: "Sets the color of the border enclosing the update menu."
+  },
+  borderwidth: {
+    valType: "number",
+    min: 0,
+    dflt: 1,
+    role: "style",
+    description: "Sets the width (in px) of the border enclosing the update menu."
+  }
 };
diff --git a/src/components/updatemenus/constants.js b/src/components/updatemenus/constants.js
index b1c7a2e3ef0..5f0f0f70c74 100644
--- a/src/components/updatemenus/constants.js
+++ b/src/components/updatemenus/constants.js
@@ -5,70 +5,50 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-
-    // layout attribute name
-    name: 'updatemenus',
-
-    // class names
-    containerClassName: 'updatemenu-container',
-    headerGroupClassName: 'updatemenu-header-group',
-    headerClassName: 'updatemenu-header',
-    headerArrowClassName: 'updatemenu-header-arrow',
-    dropdownButtonGroupClassName: 'updatemenu-dropdown-button-group',
-    dropdownButtonClassName: 'updatemenu-dropdown-button',
-    buttonClassName: 'updatemenu-button',
-    itemRectClassName: 'updatemenu-item-rect',
-    itemTextClassName: 'updatemenu-item-text',
-
-    // DOM attribute name in button group keeping track
-    // of active update menu
-    menuIndexAttrName: 'updatemenu-active-index',
-
-    // id root pass to Plots.autoMargin
-    autoMarginIdRoot: 'updatemenu-',
-
-    // options when 'active: -1'
-    blankHeaderOpts: { label: '  ' },
-
-    // min item width / height
-    minWidth: 30,
-    minHeight: 30,
-
-    // padding around item text
-    textPadX: 24,
-    arrowPadX: 16,
-
-    // font size to height scale
-    fontSizeToHeight: 1.3,
-
-    // item rect radii
-    rx: 2,
-    ry: 2,
-
-    // item  text x offset off left edge
-    textOffsetX: 12,
-
-    // item  text y offset (w.r.t. middle)
-    textOffsetY: 3,
-
-    // arrow offset off right edge
-    arrowOffsetX: 4,
-
-    // gap between header and buttons
-    gapButtonHeader: 5,
-
-    // gap between between buttons
-    gapButton: 2,
-
-    // color given to active buttons
-    activeColor: '#F4FAFF',
-
-    // color given to hovered buttons
-    hoverColor: '#F4FAFF'
+  // layout attribute name
+  name: "updatemenus",
+  // class names
+  containerClassName: "updatemenu-container",
+  headerGroupClassName: "updatemenu-header-group",
+  headerClassName: "updatemenu-header",
+  headerArrowClassName: "updatemenu-header-arrow",
+  dropdownButtonGroupClassName: "updatemenu-dropdown-button-group",
+  dropdownButtonClassName: "updatemenu-dropdown-button",
+  buttonClassName: "updatemenu-button",
+  itemRectClassName: "updatemenu-item-rect",
+  itemTextClassName: "updatemenu-item-text",
+  // DOM attribute name in button group keeping track
+  // of active update menu
+  menuIndexAttrName: "updatemenu-active-index",
+  // id root pass to Plots.autoMargin
+  autoMarginIdRoot: "updatemenu-",
+  // options when 'active: -1'
+  blankHeaderOpts: { label: "  " },
+  // min item width / height
+  minWidth: 30,
+  minHeight: 30,
+  // padding around item text
+  textPadX: 24,
+  arrowPadX: 16,
+  // font size to height scale
+  fontSizeToHeight: 1.3,
+  // item rect radii
+  rx: 2,
+  ry: 2,
+  // item  text x offset off left edge
+  textOffsetX: 12,
+  // item  text y offset (w.r.t. middle)
+  textOffsetY: 3,
+  // arrow offset off right edge
+  arrowOffsetX: 4,
+  // gap between header and buttons
+  gapButtonHeader: 5,
+  // gap between between buttons
+  gapButton: 2,
+  // color given to active buttons
+  activeColor: "#F4FAFF",
+  // color given to hovered buttons
+  hoverColor: "#F4FAFF"
 };
diff --git a/src/components/updatemenus/defaults.js b/src/components/updatemenus/defaults.js
index 2d4eeae7faa..f98b8e0b50f 100644
--- a/src/components/updatemenus/defaults.js
+++ b/src/components/updatemenus/defaults.js
@@ -5,88 +5,82 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Lib = require("../../lib");
+var handleArrayContainerDefaults = require(
+  "../../plots/array_container_defaults"
+);
 
-'use strict';
-
-var Lib = require('../../lib');
-var handleArrayContainerDefaults = require('../../plots/array_container_defaults');
-
-var attributes = require('./attributes');
-var constants = require('./constants');
+var attributes = require("./attributes");
+var constants = require("./constants");
 
 var name = constants.name;
 var buttonAttrs = attributes.buttons;
 
-
 module.exports = function updateMenusDefaults(layoutIn, layoutOut) {
-    var opts = {
-        name: name,
-        handleItemDefaults: menuDefaults
-    };
+  var opts = { name: name, handleItemDefaults: menuDefaults };
 
-    handleArrayContainerDefaults(layoutIn, layoutOut, opts);
+  handleArrayContainerDefaults(layoutIn, layoutOut, opts);
 };
 
 function menuDefaults(menuIn, menuOut, layoutOut) {
+  function coerce(attr, dflt) {
+    return Lib.coerce(menuIn, menuOut, attributes, attr, dflt);
+  }
 
-    function coerce(attr, dflt) {
-        return Lib.coerce(menuIn, menuOut, attributes, attr, dflt);
-    }
-
-    var buttons = buttonsDefaults(menuIn, menuOut);
+  var buttons = buttonsDefaults(menuIn, menuOut);
 
-    var visible = coerce('visible', buttons.length > 0);
-    if(!visible) return;
+  var visible = coerce("visible", buttons.length > 0);
+  if (!visible) return;
 
-    coerce('active');
-    coerce('direction');
-    coerce('type');
-    coerce('showactive');
+  coerce("active");
+  coerce("direction");
+  coerce("type");
+  coerce("showactive");
 
-    coerce('x');
-    coerce('y');
-    Lib.noneOrAll(menuIn, menuOut, ['x', 'y']);
+  coerce("x");
+  coerce("y");
+  Lib.noneOrAll(menuIn, menuOut, ["x", "y"]);
 
-    coerce('xanchor');
-    coerce('yanchor');
+  coerce("xanchor");
+  coerce("yanchor");
 
-    coerce('pad.t');
-    coerce('pad.r');
-    coerce('pad.b');
-    coerce('pad.l');
+  coerce("pad.t");
+  coerce("pad.r");
+  coerce("pad.b");
+  coerce("pad.l");
 
-    Lib.coerceFont(coerce, 'font', layoutOut.font);
+  Lib.coerceFont(coerce, "font", layoutOut.font);
 
-    coerce('bgcolor', layoutOut.paper_bgcolor);
-    coerce('bordercolor');
-    coerce('borderwidth');
+  coerce("bgcolor", layoutOut.paper_bgcolor);
+  coerce("bordercolor");
+  coerce("borderwidth");
 }
 
 function buttonsDefaults(menuIn, menuOut) {
-    var buttonsIn = menuIn.buttons || [],
-        buttonsOut = menuOut.buttons = [];
+  var buttonsIn = menuIn.buttons || [], buttonsOut = menuOut.buttons = [];
 
-    var buttonIn, buttonOut;
+  var buttonIn, buttonOut;
 
-    function coerce(attr, dflt) {
-        return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt);
-    }
+  function coerce(attr, dflt) {
+    return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt);
+  }
 
-    for(var i = 0; i < buttonsIn.length; i++) {
-        buttonIn = buttonsIn[i];
-        buttonOut = {};
+  for (var i = 0; i < buttonsIn.length; i++) {
+    buttonIn = buttonsIn[i];
+    buttonOut = {};
 
-        if(!Lib.isPlainObject(buttonIn) || !Array.isArray(buttonIn.args)) {
-            continue;
-        }
+    if (!Lib.isPlainObject(buttonIn) || !Array.isArray(buttonIn.args)) {
+      continue;
+    }
 
-        coerce('method');
-        coerce('args');
-        coerce('label');
+    coerce("method");
+    coerce("args");
+    coerce("label");
 
-        buttonOut._index = i;
-        buttonsOut.push(buttonOut);
-    }
+    buttonOut._index = i;
+    buttonsOut.push(buttonOut);
+  }
 
-    return buttonsOut;
+  return buttonsOut;
 }
diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js
index 3c9c30968b2..ddd70dcf395 100644
--- a/src/components/updatemenus/draw.js
+++ b/src/components/updatemenus/draw.js
@@ -5,26 +5,22 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
 
+var Plots = require("../../plots/plots");
+var Color = require("../color");
+var Drawing = require("../drawing");
+var svgTextUtils = require("../../lib/svg_text_utils");
+var anchorUtils = require("../legend/anchor_utils");
 
-'use strict';
-
-var d3 = require('d3');
-
-var Plots = require('../../plots/plots');
-var Color = require('../color');
-var Drawing = require('../drawing');
-var svgTextUtils = require('../../lib/svg_text_utils');
-var anchorUtils = require('../legend/anchor_utils');
-
-var constants = require('./constants');
-var ScrollBox = require('./scrollbox');
+var constants = require("./constants");
+var ScrollBox = require("./scrollbox");
 
 module.exports = function draw(gd) {
-    var fullLayout = gd._fullLayout,
-        menuData = makeMenuData(fullLayout);
+  var fullLayout = gd._fullLayout, menuData = makeMenuData(fullLayout);
 
-    /* Update menu data is bound to the header-group.
+  /* Update menu data is bound to the header-group.
      * The items in the header group are always present.
      *
      * Upon clicking on a header its corresponding button
@@ -50,104 +46,113 @@ module.exports = function draw(gd) {
      *         
      *         ...
      */
-
-    // draw update menu container
-    var menus = fullLayout._infolayer
-        .selectAll('g.' + constants.containerClassName)
-        .data(menuData.length > 0 ? [0] : []);
-
-    menus.enter().append('g')
-        .classed(constants.containerClassName, true)
-        .style('cursor', 'pointer');
-
-    menus.exit().remove();
-
-    // remove push margin object(s)
-    if(menus.exit().size()) clearPushMargins(gd);
-
-    // return early if no update menus are visible
-    if(menuData.length === 0) return;
-
-    // join header group
-    var headerGroups = menus.selectAll('g.' + constants.headerGroupClassName)
-        .data(menuData, keyFunction);
-
-    headerGroups.enter().append('g')
-        .classed(constants.headerGroupClassName, true);
-
-    // draw dropdown button container
-    var gButton = menus.selectAll('g.' + constants.dropdownButtonGroupClassName)
-        .data([0]);
-
-    gButton.enter().append('g')
-        .classed(constants.dropdownButtonGroupClassName, true)
-        .style('pointer-events', 'all');
-
-    // find dimensions before plotting anything (this mutates menuOpts)
-    for(var i = 0; i < menuData.length; i++) {
-        var menuOpts = menuData[i];
-        findDimensions(gd, menuOpts);
-    }
-
-    // setup scrollbox
-    var scrollBoxId = 'updatemenus' + fullLayout._uid,
-        scrollBox = new ScrollBox(gd, gButton, scrollBoxId);
-
-    // remove exiting header, remove dropped buttons and reset margins
-    if(headerGroups.enter().size()) {
-        gButton
-            .call(removeAllButtons)
-            .attr(constants.menuIndexAttrName, '-1');
-    }
-
-    headerGroups.exit().each(function(menuOpts) {
-        d3.select(this).remove();
-
-        gButton
-            .call(removeAllButtons)
-            .attr(constants.menuIndexAttrName, '-1');
-
-        Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index);
+  // draw update menu container
+  var menus = fullLayout._infolayer
+    .selectAll("g." + constants.containerClassName)
+    .data(menuData.length > 0 ? [0] : []);
+
+  menus
+    .enter()
+    .append("g")
+    .classed(constants.containerClassName, true)
+    .style("cursor", "pointer");
+
+  menus.exit().remove();
+
+  // remove push margin object(s)
+  if (menus.exit().size()) clearPushMargins(gd);
+
+  // return early if no update menus are visible
+  if (menuData.length === 0) return;
+
+  // join header group
+  var headerGroups = menus
+    .selectAll("g." + constants.headerGroupClassName)
+    .data(menuData, keyFunction);
+
+  headerGroups
+    .enter()
+    .append("g")
+    .classed(constants.headerGroupClassName, true);
+
+  // draw dropdown button container
+  var gButton = menus
+    .selectAll("g." + constants.dropdownButtonGroupClassName)
+    .data([0]);
+
+  gButton
+    .enter()
+    .append("g")
+    .classed(constants.dropdownButtonGroupClassName, true)
+    .style("pointer-events", "all");
+
+  // find dimensions before plotting anything (this mutates menuOpts)
+  for (var i = 0; i < menuData.length; i++) {
+    var menuOpts = menuData[i];
+    findDimensions(gd, menuOpts);
+  }
+
+  // setup scrollbox
+  var scrollBoxId = "updatemenus" + fullLayout._uid,
+    scrollBox = new ScrollBox(gd, gButton, scrollBoxId);
+
+  // remove exiting header, remove dropped buttons and reset margins
+  if (headerGroups.enter().size()) {
+    gButton.call(removeAllButtons).attr(constants.menuIndexAttrName, "-1");
+  }
+
+  headerGroups.exit().each(function(menuOpts) {
+    d3.select(this).remove();
+
+    gButton.call(removeAllButtons).attr(constants.menuIndexAttrName, "-1");
+
+    Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index);
+  });
+
+  // draw headers!
+  headerGroups.each(function(menuOpts) {
+    var gHeader = d3.select(this);
+
+    var _gButton = menuOpts.type === "dropdown" ? gButton : null;
+    Plots.manageCommandObserver(gd, menuOpts, menuOpts.buttons, function(data) {
+      setActive(
+        gd,
+        menuOpts,
+        menuOpts.buttons[data.index],
+        gHeader,
+        _gButton,
+        scrollBox,
+        data.index,
+        true
+      );
     });
 
-    // draw headers!
-    headerGroups.each(function(menuOpts) {
-        var gHeader = d3.select(this);
-
-        var _gButton = menuOpts.type === 'dropdown' ? gButton : null;
-        Plots.manageCommandObserver(gd, menuOpts, menuOpts.buttons, function(data) {
-            setActive(gd, menuOpts, menuOpts.buttons[data.index], gHeader, _gButton, scrollBox, data.index, true);
-        });
+    if (menuOpts.type === "dropdown") {
+      drawHeader(gd, gHeader, gButton, scrollBox, menuOpts);
 
-        if(menuOpts.type === 'dropdown') {
-            drawHeader(gd, gHeader, gButton, scrollBox, menuOpts);
-
-            // if this menu is active, update the dropdown container
-            if(isActive(gButton, menuOpts)) {
-                drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
-            }
-        } else {
-            drawButtons(gd, gHeader, null, null, menuOpts);
-        }
-
-    });
+      // if this menu is active, update the dropdown container
+      if (isActive(gButton, menuOpts)) {
+        drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
+      }
+    } else {
+      drawButtons(gd, gHeader, null, null, menuOpts);
+    }
+  });
 };
 
 function makeMenuData(fullLayout) {
-    var contOpts = fullLayout[constants.name],
-        menuData = [];
+  var contOpts = fullLayout[constants.name], menuData = [];
 
-    // Filter visible dropdowns and attach '_index' to each
-    // fullLayout options object to be used for 'object constancy'
-    // in the data join key function.
+  // Filter visible dropdowns and attach '_index' to each
+  // fullLayout options object to be used for 'object constancy'
+  // in the data join key function.
+  for (var i = 0; i < contOpts.length; i++) {
+    var item = contOpts[i];
 
-    for(var i = 0; i < contOpts.length; i++) {
-        var item = contOpts[i];
+    if (item.visible) menuData.push(item);
+  }
 
-        if(item.visible) menuData.push(item);
-    }
-
-    return menuData;
+  return menuData;
 }
 
 // Note that '_index' is set at the default step,
@@ -155,524 +160,547 @@ function makeMenuData(fullLayout) {
 // Because a menu can b set invisible,
 // this is a more 'consistent' field than the index in the menuData.
 function keyFunction(menuOpts) {
-    return menuOpts._index;
+  return menuOpts._index;
 }
 
 function isFolded(gButton) {
-    return +gButton.attr(constants.menuIndexAttrName) === -1;
+  return +gButton.attr(constants.menuIndexAttrName) === -1;
 }
 
 function isActive(gButton, menuOpts) {
-    return +gButton.attr(constants.menuIndexAttrName) === menuOpts._index;
+  return +gButton.attr(constants.menuIndexAttrName) === menuOpts._index;
 }
 
-function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex, isSilentUpdate) {
-    // update 'active' attribute in menuOpts
-    menuOpts._input.active = menuOpts.active = buttonIndex;
-
-    if(menuOpts.type === 'buttons') {
-        drawButtons(gd, gHeader, null, null, menuOpts);
-    }
-    else if(menuOpts.type === 'dropdown') {
-        // fold up buttons and redraw header
-        gButton.attr(constants.menuIndexAttrName, '-1');
-
-        drawHeader(gd, gHeader, gButton, scrollBox, menuOpts);
-
-        if(!isSilentUpdate) {
-            drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
-        }
+function setActive(
+  gd,
+  menuOpts,
+  buttonOpts,
+  gHeader,
+  gButton,
+  scrollBox,
+  buttonIndex,
+  isSilentUpdate
+) {
+  // update 'active' attribute in menuOpts
+  menuOpts._input.active = menuOpts.active = buttonIndex;
+
+  if (menuOpts.type === "buttons") {
+    drawButtons(gd, gHeader, null, null, menuOpts);
+  } else if (menuOpts.type === "dropdown") {
+    // fold up buttons and redraw header
+    gButton.attr(constants.menuIndexAttrName, "-1");
+
+    drawHeader(gd, gHeader, gButton, scrollBox, menuOpts);
+
+    if (!isSilentUpdate) {
+      drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
     }
+  }
 }
 
 function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) {
-    var header = gHeader.selectAll('g.' + constants.headerClassName)
-        .data([0]);
-
-    header.enter().append('g')
-        .classed(constants.headerClassName, true)
-        .style('pointer-events', 'all');
-
-    var active = menuOpts.active,
-        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
-        };
-
-    header
-        .call(drawItem, menuOpts, headerOpts)
-        .call(setItemPosition, menuOpts, posOpts, positionOverrides);
-
-    // draw drop arrow at the right edge
-    var arrow = gHeader.selectAll('text.' + constants.headerArrowClassName)
-        .data([0]);
-
-    arrow.enter().append('text')
-        .classed(constants.headerArrowClassName, true)
-        .classed('user-select-none', true)
-        .attr('text-anchor', 'end')
-        .call(Drawing.font, menuOpts.font)
-        .text('▼');
-
-    arrow.attr({
-        x: menuOpts.headerWidth - constants.arrowOffsetX + menuOpts.pad.l,
-        y: menuOpts.headerHeight / 2 + constants.textOffsetY + menuOpts.pad.t
-    });
-
-    header.on('click', function() {
-        gButton.call(removeAllButtons);
-
-
-        // if this menu is active, fold the dropdown container
-        // otherwise, make this menu active
-        gButton.attr(
-            constants.menuIndexAttrName,
-            isActive(gButton, menuOpts) ?
-                -1 :
-                String(menuOpts._index)
-        );
-
-        drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
-    });
-
-    header.on('mouseover', function() {
-        header.call(styleOnMouseOver);
-    });
-
-    header.on('mouseout', function() {
-        header.call(styleOnMouseOut, menuOpts);
-    });
+  var header = gHeader.selectAll("g." + constants.headerClassName).data([0]);
+
+  header
+    .enter()
+    .append("g")
+    .classed(constants.headerClassName, true)
+    .style("pointer-events", "all");
+
+  var active = menuOpts.active,
+    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
+    };
 
-    // translate header group
-    Drawing.setTranslate(gHeader, menuOpts.lx, menuOpts.ly);
+  header
+    .call(drawItem, menuOpts, headerOpts)
+    .call(setItemPosition, menuOpts, posOpts, positionOverrides);
+
+  // draw drop arrow at the right edge
+  var arrow = gHeader
+    .selectAll("text." + constants.headerArrowClassName)
+    .data([0]);
+
+  arrow
+    .enter()
+    .append("text")
+    .classed(constants.headerArrowClassName, true)
+    .classed("user-select-none", true)
+    .attr("text-anchor", "end")
+    .call(Drawing.font, menuOpts.font)
+    .text("\u25BC");
+
+  arrow.attr({
+    x: menuOpts.headerWidth - constants.arrowOffsetX + menuOpts.pad.l,
+    y: menuOpts.headerHeight / 2 + constants.textOffsetY + menuOpts.pad.t
+  });
+
+  header.on("click", function() {
+    gButton.call(removeAllButtons);
+
+    // if this menu is active, fold the dropdown container
+    // otherwise, make this menu active
+    gButton.attr(
+      constants.menuIndexAttrName,
+      isActive(gButton, menuOpts) ? -1 : String(menuOpts._index)
+    );
+
+    drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
+  });
+
+  header.on("mouseover", function() {
+    header.call(styleOnMouseOver);
+  });
+
+  header.on("mouseout", function() {
+    header.call(styleOnMouseOut, menuOpts);
+  });
+
+  // translate header group
+  Drawing.setTranslate(gHeader, menuOpts.lx, menuOpts.ly);
 }
 
 function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) {
-    // If this is a set of buttons, set pointer events = all since we play
-    // some minor games with which container is which in order to simplify
-    // the drawing of *either* buttons or menus
-    if(!gButton) {
-        gButton = gHeader;
-        gButton.attr('pointer-events', 'all');
-    }
+  // If this is a set of buttons, set pointer events = all since we play
+  // some minor games with which container is which in order to simplify
+  // the drawing of *either* buttons or menus
+  if (!gButton) {
+    gButton = gHeader;
+    gButton.attr("pointer-events", "all");
+  }
 
-    var buttonData = (!isFolded(gButton) || menuOpts.type === 'buttons') ?
-        menuOpts.buttons :
-        [];
+  var buttonData = !isFolded(gButton) || menuOpts.type === "buttons"
+    ? menuOpts.buttons
+    : [];
 
-    var klass = menuOpts.type === 'dropdown' ? constants.dropdownButtonClassName : constants.buttonClassName;
+  var klass = menuOpts.type === "dropdown"
+    ? constants.dropdownButtonClassName
+    : constants.buttonClassName;
 
-    var buttons = gButton.selectAll('g.' + klass)
-        .data(buttonData);
+  var buttons = gButton.selectAll("g." + klass).data(buttonData);
 
-    var enter = buttons.enter().append('g')
-        .classed(klass, true);
+  var enter = buttons.enter().append("g").classed(klass, true);
 
-    var exit = buttons.exit();
+  var exit = buttons.exit();
 
-    if(menuOpts.type === 'dropdown') {
-        enter.attr('opacity', '0')
-            .transition()
-            .attr('opacity', '1');
+  if (menuOpts.type === "dropdown") {
+    enter.attr("opacity", "0").transition().attr("opacity", "1");
 
-        exit.transition()
-            .attr('opacity', '0')
-            .remove();
-    } else {
-        exit.remove();
-    }
+    exit.transition().attr("opacity", "0").remove();
+  } else {
+    exit.remove();
+  }
 
-    var x0 = 0;
-    var y0 = 0;
+  var x0 = 0;
+  var y0 = 0;
 
-    var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1;
+  var isVertical = ["up", "down"].indexOf(menuOpts.direction) !== -1;
 
-    if(menuOpts.type === 'dropdown') {
-        if(isVertical) {
-            y0 = menuOpts.headerHeight + constants.gapButtonHeader;
-        } else {
-            x0 = menuOpts.headerWidth + constants.gapButtonHeader;
-        }
-    }
-
-    if(menuOpts.type === 'dropdown' && menuOpts.direction === 'up') {
-        y0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openHeight;
-    }
-
-    if(menuOpts.type === 'dropdown' && menuOpts.direction === 'left') {
-        x0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openWidth;
+  if (menuOpts.type === "dropdown") {
+    if (isVertical) {
+      y0 = menuOpts.headerHeight + constants.gapButtonHeader;
+    } else {
+      x0 = menuOpts.headerWidth + constants.gapButtonHeader;
     }
-
-    var posOpts = {
-        x: menuOpts.lx + x0 + menuOpts.pad.l,
-        y: menuOpts.ly + y0 + menuOpts.pad.t,
-        yPad: constants.gapButton,
-        xPad: constants.gapButton,
-        index: 0,
-    };
-
-    var scrollBoxPosition = {
-        l: posOpts.x + menuOpts.borderwidth,
-        t: posOpts.y + menuOpts.borderwidth
-    };
-
-    buttons.each(function(buttonOpts, buttonIndex) {
-        var button = d3.select(this);
-
-        button
-            .call(drawItem, menuOpts, buttonOpts)
-            .call(setItemPosition, menuOpts, posOpts);
-
-        button.on('click', function() {
-            // skip `dragend` events
-            if(d3.event.defaultPrevented) return;
-
-            setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex);
-
-            Plots.executeAPICommand(gd, buttonOpts.method, buttonOpts.args);
-
-            gd.emit('plotly_buttonclicked', {menu: menuOpts, button: buttonOpts, active: menuOpts.active});
-        });
-
-        button.on('mouseover', function() {
-            button.call(styleOnMouseOver);
-        });
-
-        button.on('mouseout', function() {
-            button.call(styleOnMouseOut, menuOpts);
-            buttons.call(styleButtons, menuOpts);
-        });
+  }
+
+  if (menuOpts.type === "dropdown" && menuOpts.direction === "up") {
+    y0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openHeight;
+  }
+
+  if (menuOpts.type === "dropdown" && menuOpts.direction === "left") {
+    x0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openWidth;
+  }
+
+  var posOpts = {
+    x: menuOpts.lx + x0 + menuOpts.pad.l,
+    y: menuOpts.ly + y0 + menuOpts.pad.t,
+    yPad: constants.gapButton,
+    xPad: constants.gapButton,
+    index: 0
+  };
+
+  var scrollBoxPosition = {
+    l: posOpts.x + menuOpts.borderwidth,
+    t: posOpts.y + menuOpts.borderwidth
+  };
+
+  buttons.each(function(buttonOpts, buttonIndex) {
+    var button = d3.select(this);
+
+    button
+      .call(drawItem, menuOpts, buttonOpts)
+      .call(setItemPosition, menuOpts, posOpts);
+
+    button.on("click", function() {
+      // skip `dragend` events
+      if (d3.event.defaultPrevented) return;
+
+      setActive(
+        gd,
+        menuOpts,
+        buttonOpts,
+        gHeader,
+        gButton,
+        scrollBox,
+        buttonIndex
+      );
+
+      Plots.executeAPICommand(gd, buttonOpts.method, buttonOpts.args);
+
+      gd.emit("plotly_buttonclicked", {
+        menu: menuOpts,
+        button: buttonOpts,
+        active: menuOpts.active
+      });
     });
 
-    buttons.call(styleButtons, menuOpts);
-
-    if(isVertical) {
-        scrollBoxPosition.w = Math.max(menuOpts.openWidth, menuOpts.headerWidth);
-        scrollBoxPosition.h = posOpts.y - scrollBoxPosition.t;
-    }
-    else {
-        scrollBoxPosition.w = posOpts.x - scrollBoxPosition.l;
-        scrollBoxPosition.h = Math.max(menuOpts.openHeight, menuOpts.headerHeight);
-    }
-
-    scrollBoxPosition.direction = menuOpts.direction;
+    button.on("mouseover", function() {
+      button.call(styleOnMouseOver);
+    });
 
-    if(scrollBox) {
-        if(buttons.size()) {
-            drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, scrollBoxPosition);
-        }
-        else {
-            hideScrollBox(scrollBox);
-        }
+    button.on("mouseout", function() {
+      button.call(styleOnMouseOut, menuOpts);
+      buttons.call(styleButtons, menuOpts);
+    });
+  });
+
+  buttons.call(styleButtons, menuOpts);
+
+  if (isVertical) {
+    scrollBoxPosition.w = Math.max(menuOpts.openWidth, menuOpts.headerWidth);
+    scrollBoxPosition.h = posOpts.y - scrollBoxPosition.t;
+  } else {
+    scrollBoxPosition.w = posOpts.x - scrollBoxPosition.l;
+    scrollBoxPosition.h = Math.max(menuOpts.openHeight, menuOpts.headerHeight);
+  }
+
+  scrollBoxPosition.direction = menuOpts.direction;
+
+  if (scrollBox) {
+    if (buttons.size()) {
+      drawScrollBox(
+        gd,
+        gHeader,
+        gButton,
+        scrollBox,
+        menuOpts,
+        scrollBoxPosition
+      );
+    } else {
+      hideScrollBox(scrollBox);
     }
+  }
 }
 
 function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, position) {
-    // enable the scrollbox
-    var direction = menuOpts.direction,
-        isVertical = (direction === 'up' || direction === 'down');
-
-    var active = menuOpts.active,
-        translateX, translateY,
-        i;
-    if(isVertical) {
-        translateY = 0;
-        for(i = 0; i < active; i++) {
-            translateY += menuOpts.heights[i] + constants.gapButton;
-        }
+  // enable the scrollbox
+  var direction = menuOpts.direction,
+    isVertical = direction === "up" || direction === "down";
+
+  var active = menuOpts.active, translateX, translateY, i;
+  if (isVertical) {
+    translateY = 0;
+    for (i = 0; i < active; i++) {
+      translateY += menuOpts.heights[i] + constants.gapButton;
     }
-    else {
-        translateX = 0;
-        for(i = 0; i < active; i++) {
-            translateX += menuOpts.widths[i] + constants.gapButton;
-        }
+  } else {
+    translateX = 0;
+    for (i = 0; i < active; i++) {
+      translateX += menuOpts.widths[i] + constants.gapButton;
     }
+  }
 
-    scrollBox.enable(position, translateX, translateY);
+  scrollBox.enable(position, translateX, translateY);
 
-    if(scrollBox.hbar) {
-        scrollBox.hbar
-            .attr('opacity', '0')
-            .transition()
-            .attr('opacity', '1');
-    }
+  if (scrollBox.hbar) {
+    scrollBox.hbar.attr("opacity", "0").transition().attr("opacity", "1");
+  }
 
-    if(scrollBox.vbar) {
-        scrollBox.vbar
-            .attr('opacity', '0')
-            .transition()
-            .attr('opacity', '1');
-    }
+  if (scrollBox.vbar) {
+    scrollBox.vbar.attr("opacity", "0").transition().attr("opacity", "1");
+  }
 }
 
 function hideScrollBox(scrollBox) {
-    var hasHBar = !!scrollBox.hbar,
-        hasVBar = !!scrollBox.vbar;
-
-    if(hasHBar) {
-        scrollBox.hbar
-            .transition()
-            .attr('opacity', '0')
-            .each('end', function() {
-                hasHBar = false;
-                if(!hasVBar) scrollBox.disable();
-            });
-    }
+  var hasHBar = !!scrollBox.hbar, hasVBar = !!scrollBox.vbar;
 
-    if(hasVBar) {
-        scrollBox.vbar
-            .transition()
-            .attr('opacity', '0')
-            .each('end', function() {
-                hasVBar = false;
-                if(!hasHBar) scrollBox.disable();
-            });
-    }
+  if (hasHBar) {
+    scrollBox.hbar.transition().attr("opacity", "0").each("end", function() {
+      hasHBar = false;
+      if (!hasVBar) scrollBox.disable();
+    });
+  }
+
+  if (hasVBar) {
+    scrollBox.vbar.transition().attr("opacity", "0").each("end", function() {
+      hasVBar = false;
+      if (!hasHBar) scrollBox.disable();
+    });
+  }
 }
 
 function drawItem(item, menuOpts, itemOpts) {
-    item.call(drawItemRect, menuOpts)
-        .call(drawItemText, menuOpts, itemOpts);
+  item.call(drawItemRect, menuOpts).call(drawItemText, menuOpts, itemOpts);
 }
 
 function drawItemRect(item, menuOpts) {
-    var rect = item.selectAll('rect')
-        .data([0]);
-
-    rect.enter().append('rect')
-        .classed(constants.itemRectClassName, true)
-        .attr({
-            rx: constants.rx,
-            ry: constants.ry,
-            'shape-rendering': 'crispEdges'
-        });
-
-    rect.call(Color.stroke, menuOpts.bordercolor)
-        .call(Color.fill, menuOpts.bgcolor)
-        .style('stroke-width', menuOpts.borderwidth + 'px');
+  var rect = item.selectAll("rect").data([0]);
+
+  rect.enter().append("rect").classed(constants.itemRectClassName, true).attr({
+    rx: constants.rx,
+    ry: constants.ry,
+    "shape-rendering": "crispEdges"
+  });
+
+  rect
+    .call(Color.stroke, menuOpts.bordercolor)
+    .call(Color.fill, menuOpts.bgcolor)
+    .style("stroke-width", menuOpts.borderwidth + "px");
 }
 
 function drawItemText(item, menuOpts, itemOpts) {
-    var text = item.selectAll('text')
-        .data([0]);
-
-    text.enter().append('text')
-        .classed(constants.itemTextClassName, true)
-        .classed('user-select-none', true)
-        .attr('text-anchor', 'start');
-
-    text.call(Drawing.font, menuOpts.font)
-        .text(itemOpts.label)
-        .call(svgTextUtils.convertToTspans);
+  var text = item.selectAll("text").data([0]);
+
+  text
+    .enter()
+    .append("text")
+    .classed(constants.itemTextClassName, true)
+    .classed("user-select-none", true)
+    .attr("text-anchor", "start");
+
+  text
+    .call(Drawing.font, menuOpts.font)
+    .text(itemOpts.label)
+    .call(svgTextUtils.convertToTspans);
 }
 
 function styleButtons(buttons, menuOpts) {
-    var active = menuOpts.active;
+  var active = menuOpts.active;
 
-    buttons.each(function(buttonOpts, i) {
-        var button = d3.select(this);
+  buttons.each(function(buttonOpts, i) {
+    var button = d3.select(this);
 
-        if(i === active && menuOpts.showactive) {
-            button.select('rect.' + constants.itemRectClassName)
-                .call(Color.fill, constants.activeColor);
-        }
-    });
+    if (i === active && menuOpts.showactive) {
+      button
+        .select("rect." + constants.itemRectClassName)
+        .call(Color.fill, constants.activeColor);
+    }
+  });
 }
 
 function styleOnMouseOver(item) {
-    item.select('rect.' + constants.itemRectClassName)
-        .call(Color.fill, constants.hoverColor);
+  item
+    .select("rect." + constants.itemRectClassName)
+    .call(Color.fill, constants.hoverColor);
 }
 
 function styleOnMouseOut(item, menuOpts) {
-    item.select('rect.' + constants.itemRectClassName)
-        .call(Color.fill, menuOpts.bgcolor);
+  item
+    .select("rect." + constants.itemRectClassName)
+    .call(Color.fill, menuOpts.bgcolor);
 }
 
 // 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 fakeButtons = gd._tester.selectAll('g.' + constants.dropdownButtonClassName)
-        .data(menuOpts.buttons);
-
-    fakeButtons.enter().append('g')
-        .classed(constants.dropdownButtonClassName, true);
-
-    var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1;
-
-    // loop over fake buttons to find width / height
-    fakeButtons.each(function(buttonOpts, i) {
-        var button = d3.select(this);
-
-        button.call(drawItem, menuOpts, buttonOpts);
-
-        var text = button.select('.' + constants.itemTextClassName),
-            tspans = text.selectAll('tspan');
-
-        // width is given by max width of all buttons
-        var tWidth = text.node() && Drawing.bBox(text.node()).width,
-            wEff = Math.max(tWidth + constants.textPadX, constants.minWidth);
-
-        // height is determined by item text
-        var tHeight = menuOpts.font.size * constants.fontSizeToHeight,
-            tLines = tspans[0].length || 1,
-            hEff = Math.max(tHeight * tLines, constants.minHeight) + constants.textOffsetY;
-
-        hEff = Math.ceil(hEff);
-        wEff = Math.ceil(wEff);
-
-        // 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;
-
-        // Height and width of individual element:
-        menuOpts.height1 = Math.max(menuOpts.height1, hEff);
-        menuOpts.width1 = Math.max(menuOpts.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;
-        } else {
-            menuOpts.totalWidth += wEff + constants.gapButton;
-            menuOpts.openWidth += wEff + constants.gapButton;
-            menuOpts.totalHeight = Math.max(menuOpts.totalHeight, hEff);
-            menuOpts.openHeight = menuOpts.totalHeight;
-        }
-    });
-
-    if(isVertical) {
-        menuOpts.totalHeight -= constants.gapButton;
+  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 fakeButtons = gd._tester
+    .selectAll("g." + constants.dropdownButtonClassName)
+    .data(menuOpts.buttons);
+
+  fakeButtons
+    .enter()
+    .append("g")
+    .classed(constants.dropdownButtonClassName, true);
+
+  var isVertical = ["up", "down"].indexOf(menuOpts.direction) !== -1;
+
+  // loop over fake buttons to find width / height
+  fakeButtons.each(function(buttonOpts, i) {
+    var button = d3.select(this);
+
+    button.call(drawItem, menuOpts, buttonOpts);
+
+    var text = button.select("." + constants.itemTextClassName),
+      tspans = text.selectAll("tspan");
+
+    // width is given by max width of all buttons
+    var tWidth = text.node() && Drawing.bBox(text.node()).width,
+      wEff = Math.max(tWidth + constants.textPadX, constants.minWidth);
+
+    // height is determined by item text
+    var tHeight = menuOpts.font.size * constants.fontSizeToHeight,
+      tLines = tspans[0].length || 1,
+      hEff = Math.max(tHeight * tLines, constants.minHeight) +
+        constants.textOffsetY;
+
+    hEff = Math.ceil(hEff);
+    wEff = Math.ceil(wEff);
+
+    // 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;
+
+    // Height and width of individual element:
+    menuOpts.height1 = Math.max(menuOpts.height1, hEff);
+    menuOpts.width1 = Math.max(menuOpts.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;
     } else {
-        menuOpts.totalWidth -= constants.gapButton;
-    }
-
-
-    menuOpts.headerWidth = menuOpts.width1 + constants.arrowPadX;
-    menuOpts.headerHeight = menuOpts.height1;
-
-    if(menuOpts.type === 'dropdown') {
-        if(isVertical) {
-            menuOpts.width1 += constants.arrowPadX;
-            menuOpts.totalHeight = menuOpts.height1;
-        } else {
-            menuOpts.totalWidth = menuOpts.width1;
-        }
-        menuOpts.totalWidth += constants.arrowPadX;
+      menuOpts.totalWidth += wEff + constants.gapButton;
+      menuOpts.openWidth += wEff + constants.gapButton;
+      menuOpts.totalHeight = Math.max(menuOpts.totalHeight, hEff);
+      menuOpts.openHeight = menuOpts.totalHeight;
     }
+  });
 
-    fakeButtons.remove();
+  if (isVertical) {
+    menuOpts.totalHeight -= constants.gapButton;
+  } else {
+    menuOpts.totalWidth -= constants.gapButton;
+  }
 
-    var paddedWidth = menuOpts.totalWidth + menuOpts.pad.l + menuOpts.pad.r;
-    var paddedHeight = menuOpts.totalHeight + menuOpts.pad.t + menuOpts.pad.b;
+  menuOpts.headerWidth = menuOpts.width1 + constants.arrowPadX;
+  menuOpts.headerHeight = menuOpts.height1;
 
-    var graphSize = gd._fullLayout._size;
-    menuOpts.lx = graphSize.l + graphSize.w * menuOpts.x;
-    menuOpts.ly = graphSize.t + graphSize.h * (1 - menuOpts.y);
-
-    var xanchor = 'left';
-    if(anchorUtils.isRightAnchor(menuOpts)) {
-        menuOpts.lx -= paddedWidth;
-        xanchor = 'right';
-    }
-    if(anchorUtils.isCenterAnchor(menuOpts)) {
-        menuOpts.lx -= paddedWidth / 2;
-        xanchor = 'center';
-    }
-
-    var yanchor = 'top';
-    if(anchorUtils.isBottomAnchor(menuOpts)) {
-        menuOpts.ly -= paddedHeight;
-        yanchor = 'bottom';
-    }
-    if(anchorUtils.isMiddleAnchor(menuOpts)) {
-        menuOpts.ly -= paddedHeight / 2;
-        yanchor = 'middle';
+  if (menuOpts.type === "dropdown") {
+    if (isVertical) {
+      menuOpts.width1 += constants.arrowPadX;
+      menuOpts.totalHeight = menuOpts.height1;
+    } else {
+      menuOpts.totalWidth = menuOpts.width1;
     }
-
-    menuOpts.totalWidth = Math.ceil(menuOpts.totalWidth);
-    menuOpts.totalHeight = Math.ceil(menuOpts.totalHeight);
-    menuOpts.lx = Math.round(menuOpts.lx);
-    menuOpts.ly = Math.round(menuOpts.ly);
-
-    Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index, {
-        x: menuOpts.x,
-        y: menuOpts.y,
-        l: paddedWidth * ({right: 1, center: 0.5}[xanchor] || 0),
-        r: paddedWidth * ({left: 1, center: 0.5}[xanchor] || 0),
-        b: paddedHeight * ({top: 1, middle: 0.5}[yanchor] || 0),
-        t: paddedHeight * ({bottom: 1, middle: 0.5}[yanchor] || 0)
-    });
+    menuOpts.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 graphSize = gd._fullLayout._size;
+  menuOpts.lx = graphSize.l + graphSize.w * menuOpts.x;
+  menuOpts.ly = graphSize.t + graphSize.h * (1 - menuOpts.y);
+
+  var xanchor = "left";
+  if (anchorUtils.isRightAnchor(menuOpts)) {
+    menuOpts.lx -= paddedWidth;
+    xanchor = "right";
+  }
+  if (anchorUtils.isCenterAnchor(menuOpts)) {
+    menuOpts.lx -= paddedWidth / 2;
+    xanchor = "center";
+  }
+
+  var yanchor = "top";
+  if (anchorUtils.isBottomAnchor(menuOpts)) {
+    menuOpts.ly -= paddedHeight;
+    yanchor = "bottom";
+  }
+  if (anchorUtils.isMiddleAnchor(menuOpts)) {
+    menuOpts.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);
+
+  Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index, {
+    x: menuOpts.x,
+    y: menuOpts.y,
+    l: paddedWidth * (({ right: 1, center: 0.5 })[xanchor] || 0),
+    r: paddedWidth * (({ left: 1, center: 0.5 })[xanchor] || 0),
+    b: paddedHeight * (({ top: 1, middle: 0.5 })[yanchor] || 0),
+    t: paddedHeight * (({ bottom: 1, middle: 0.5 })[yanchor] || 0)
+  });
 }
 
 // set item positions (mutates posOpts)
 function setItemPosition(item, menuOpts, posOpts, overrideOpts) {
-    overrideOpts = overrideOpts || {};
-    var rect = item.select('.' + constants.itemRectClassName),
-        text = item.select('.' + constants.itemTextClassName),
-        tspans = text.selectAll('tspan'),
-        borderWidth = menuOpts.borderwidth,
-        index = posOpts.index;
-
-    Drawing.setTranslate(item, borderWidth + posOpts.x, borderWidth + posOpts.y);
-
-    var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1;
-
-    rect.attr({
-        x: 0,
-        y: 0,
-        width: overrideOpts.width || (isVertical ? menuOpts.width1 : menuOpts.widths[index]),
-        height: overrideOpts.height || (isVertical ? menuOpts.heights[index] : menuOpts.height1)
-    });
-
-    var tHeight = menuOpts.font.size * constants.fontSizeToHeight,
-        tLines = tspans[0].length || 1,
-        spanOffset = ((tLines - 1) * tHeight / 4);
-
-    var textAttrs = {
-        x: constants.textOffsetX,
-        y: menuOpts.heights[index] / 2 - spanOffset + constants.textOffsetY
-    };
-
-    text.attr(textAttrs);
-    tspans.attr(textAttrs);
-
-    if(isVertical) {
-        posOpts.y += menuOpts.heights[index] + posOpts.yPad;
-    } else {
-        posOpts.x += menuOpts.widths[index] + posOpts.xPad;
-    }
-
-    posOpts.index++;
+  overrideOpts = overrideOpts || {};
+  var rect = item.select("." + constants.itemRectClassName),
+    text = item.select("." + constants.itemTextClassName),
+    tspans = text.selectAll("tspan"),
+    borderWidth = menuOpts.borderwidth,
+    index = posOpts.index;
+
+  Drawing.setTranslate(item, borderWidth + posOpts.x, borderWidth + posOpts.y);
+
+  var isVertical = ["up", "down"].indexOf(menuOpts.direction) !== -1;
+
+  rect.attr({
+    x: 0,
+    y: 0,
+    width: (
+      overrideOpts.width ||
+        (isVertical ? menuOpts.width1 : menuOpts.widths[index])
+    ),
+    height: (
+      overrideOpts.height ||
+        (isVertical ? menuOpts.heights[index] : menuOpts.height1)
+    )
+  });
+
+  var tHeight = menuOpts.font.size * constants.fontSizeToHeight,
+    tLines = tspans[0].length || 1,
+    spanOffset = (tLines - 1) * tHeight / 4;
+
+  var textAttrs = {
+    x: constants.textOffsetX,
+    y: menuOpts.heights[index] / 2 - spanOffset + constants.textOffsetY
+  };
+
+  text.attr(textAttrs);
+  tspans.attr(textAttrs);
+
+  if (isVertical) {
+    posOpts.y += menuOpts.heights[index] + posOpts.yPad;
+  } else {
+    posOpts.x += menuOpts.widths[index] + posOpts.xPad;
+  }
+
+  posOpts.index++;
 }
 
 function removeAllButtons(gButton) {
-    gButton.selectAll('g.' + constants.dropdownButtonClassName).remove();
+  gButton.selectAll("g." + constants.dropdownButtonClassName).remove();
 }
 
 function clearPushMargins(gd) {
-    var pushMargins = gd._fullLayout._pushmargin || {},
-        keys = Object.keys(pushMargins);
+  var pushMargins = gd._fullLayout._pushmargin || {},
+    keys = Object.keys(pushMargins);
 
-    for(var i = 0; i < keys.length; i++) {
-        var k = keys[i];
+  for (var i = 0; i < keys.length; i++) {
+    var k = keys[i];
 
-        if(k.indexOf(constants.autoMarginIdRoot) !== -1) {
-            Plots.autoMargin(gd, k);
-        }
+    if (k.indexOf(constants.autoMarginIdRoot) !== -1) {
+      Plots.autoMargin(gd, k);
     }
+  }
 }
diff --git a/src/components/updatemenus/index.js b/src/components/updatemenus/index.js
index 366b9aaa1ad..b44b6e9dab9 100644
--- a/src/components/updatemenus/index.js
+++ b/src/components/updatemenus/index.js
@@ -5,17 +5,13 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var constants = require('./constants');
+"use strict";
+var constants = require("./constants");
 
 module.exports = {
-    moduleType: 'component',
-    name: constants.name,
-
-    layoutAttributes: require('./attributes'),
-    supplyLayoutDefaults: require('./defaults'),
-
-    draw: require('./draw')
+  moduleType: "component",
+  name: constants.name,
+  layoutAttributes: require("./attributes"),
+  supplyLayoutDefaults: require("./defaults"),
+  draw: require("./draw")
 };
diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js
index 6c3431536cf..a1ff5141a27 100644
--- a/src/components/updatemenus/scrollbox.js
+++ b/src/components/updatemenus/scrollbox.js
@@ -5,17 +5,15 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 module.exports = ScrollBox;
 
-var d3 = require('d3');
+var d3 = require("d3");
 
-var Color = require('../color');
-var Drawing = require('../drawing');
+var Color = require("../color");
+var Drawing = require("../drawing");
 
-var Lib = require('../../lib');
+var Lib = require("../../lib");
 
 /**
  * Helper class to setup a scroll box
@@ -26,35 +24,33 @@ var Lib = require('../../lib');
  * @param {string}  id          Id for the clip path to implement the scroll box
  */
 function ScrollBox(gd, container, id) {
-    this.gd = gd;
-    this.container = container;
-    this.id = id;
-
-    // See ScrollBox.prototype.enable for further definition
-    this.position = null;  // scrollbox position
-    this.translateX = null;  // scrollbox horizontal translation
-    this.translateY = null;  // scrollbox vertical translation
-    this.hbar = null;  // horizontal scrollbar D3 selection
-    this.vbar = null;  // vertical scrollbar D3 selection
-
-    //  element to capture pointer events
-    this.bg = this.container.selectAll('rect.scrollbox-bg').data([0]);
-
-    this.bg.exit()
-        .on('.drag', null)
-        .on('wheel', null)
-        .remove();
-
-    this.bg.enter().append('rect')
-        .classed('scrollbox-bg', true)
-        .style('pointer-events', 'all')
-        .attr({
-            opacity: 0,
-            x: 0,
-            y: 0,
-            width: 0,
-            height: 0
-        });
+  this.gd = gd;
+  this.container = container;
+  this.id = id;
+
+  // See ScrollBox.prototype.enable for further definition
+  this.position = null;
+  // scrollbox position
+  this.translateX = null;
+  // scrollbox horizontal translation
+  this.translateY = null;
+  // scrollbox vertical translation
+  this.hbar = null;
+  // horizontal scrollbar D3 selection
+  this.vbar = null;
+
+  // vertical scrollbar D3 selection
+  //  element to capture pointer events
+  this.bg = this.container.selectAll("rect.scrollbox-bg").data([0]);
+
+  this.bg.exit().on(".drag", null).on("wheel", null).remove();
+
+  this.bg
+    .enter()
+    .append("rect")
+    .classed("scrollbox-bg", true)
+    .style("pointer-events", "all")
+    .attr({ opacity: 0, x: 0, y: 0, width: 0, height: 0 });
 }
 
 // scroll bar dimensions
@@ -62,7 +58,7 @@ ScrollBox.barWidth = 2;
 ScrollBox.barLength = 20;
 ScrollBox.barRadius = 2;
 ScrollBox.barPad = 1;
-ScrollBox.barColor = '#808BA4';
+ScrollBox.barColor = "#808BA4";
 
 /**
  * If needed, setup a clip path and scrollbars
@@ -79,238 +75,220 @@ ScrollBox.barColor = '#808BA4';
  * @param {number}  [translateY=0]  Vertical offset (in pixels)
  */
 ScrollBox.prototype.enable = function enable(position, translateX, translateY) {
-    var fullLayout = this.gd._fullLayout,
-        fullWidth = fullLayout.width,
-        fullHeight = fullLayout.height;
-
-    // compute position of scrollbox
-    this.position = position;
-
-    var l = this.position.l,
-        w = this.position.w,
-        t = this.position.t,
-        h = this.position.h,
-        direction = this.position.direction,
-        isDown = (direction === 'down'),
-        isLeft = (direction === 'left'),
-        isRight = (direction === 'right'),
-        isUp = (direction === 'up'),
-        boxW = w,
-        boxH = h,
-        boxL, boxR,
-        boxT, boxB;
-
-    if(!isDown && !isLeft && !isRight && !isUp) {
-        this.position.direction = 'down';
-        isDown = true;
-    }
-
-    var isVertical = isDown || isUp;
-    if(isVertical) {
-        boxL = l;
-        boxR = boxL + boxW;
-
-        if(isDown) {
-            // anchor to top side
-            boxT = t;
-            boxB = Math.min(boxT + boxH, fullHeight);
-            boxH = boxB - boxT;
-        }
-        else {
-            // anchor to bottom side
-            boxB = t + boxH;
-            boxT = Math.max(boxB - boxH, 0);
-            boxH = boxB - boxT;
-        }
+  var fullLayout = this.gd._fullLayout,
+    fullWidth = fullLayout.width,
+    fullHeight = fullLayout.height;
+
+  // compute position of scrollbox
+  this.position = position;
+
+  var l = this.position.l,
+    w = this.position.w,
+    t = this.position.t,
+    h = this.position.h,
+    direction = this.position.direction,
+    isDown = direction === "down",
+    isLeft = direction === "left",
+    isRight = direction === "right",
+    isUp = direction === "up",
+    boxW = w,
+    boxH = h,
+    boxL,
+    boxR,
+    boxT,
+    boxB;
+
+  if (!isDown && !isLeft && !isRight && !isUp) {
+    this.position.direction = "down";
+    isDown = true;
+  }
+
+  var isVertical = isDown || isUp;
+  if (isVertical) {
+    boxL = l;
+    boxR = boxL + boxW;
+
+    if (isDown) {
+      // anchor to top side
+      boxT = t;
+      boxB = Math.min(boxT + boxH, fullHeight);
+      boxH = boxB - boxT;
+    } else {
+      // anchor to bottom side
+      boxB = t + boxH;
+      boxT = Math.max(boxB - boxH, 0);
+      boxH = boxB - boxT;
     }
-    else {
-        boxT = t;
-        boxB = boxT + boxH;
-
-        if(isLeft) {
-            // anchor to right side
-            boxR = l + boxW;
-            boxL = Math.max(boxR - boxW, 0);
-            boxW = boxR - boxL;
-        }
-        else {
-            // anchor to left side
-            boxL = l;
-            boxR = Math.min(boxL + boxW, fullWidth);
-            boxW = boxR - boxL;
-        }
+  } else {
+    boxT = t;
+    boxB = boxT + boxH;
+
+    if (isLeft) {
+      // anchor to right side
+      boxR = l + boxW;
+      boxL = Math.max(boxR - boxW, 0);
+      boxW = boxR - boxL;
+    } else {
+      // anchor to left side
+      boxL = l;
+      boxR = Math.min(boxL + boxW, fullWidth);
+      boxW = boxR - boxL;
     }
-
-    this._box = {
-        l: boxL,
-        t: boxT,
-        w: boxW,
-        h: boxH
-    };
-
-    // compute position of horizontal scroll bar
-    var needsHorizontalScrollBar = (w > boxW),
-        hbarW = ScrollBox.barLength + 2 * ScrollBox.barPad,
-        hbarH = ScrollBox.barWidth + 2 * ScrollBox.barPad,
-        // draw horizontal scrollbar on the bottom side
-        hbarL = l,
-        hbarT = t + h;
-
-    if(hbarT + hbarH > fullHeight) hbarT = fullHeight - hbarH;
-
-    var hbar = this.container.selectAll('rect.scrollbar-horizontal').data(
-            (needsHorizontalScrollBar) ? [0] : []);
-
-    hbar.exit()
-        .on('.drag', null)
-        .remove();
-
-    hbar.enter().append('rect')
-        .classed('scrollbar-horizontal', true)
-        .call(Color.fill, ScrollBox.barColor);
-
-    if(needsHorizontalScrollBar) {
-        this.hbar = hbar.attr({
-            'rx': ScrollBox.barRadius,
-            'ry': ScrollBox.barRadius,
-            'x': hbarL,
-            'y': hbarT,
-            'width': hbarW,
-            'height': hbarH
-        });
-
-        // hbar center moves between hbarXMin and hbarXMin + hbarTranslateMax
-        this._hbarXMin = hbarL + hbarW / 2;
-        this._hbarTranslateMax = boxW - hbarW;
-    }
-    else {
-        delete this.hbar;
-        delete this._hbarXMin;
-        delete this._hbarTranslateMax;
+  }
+
+  this._box = { l: boxL, t: boxT, w: boxW, h: boxH };
+
+  // compute position of horizontal scroll bar
+  var needsHorizontalScrollBar = w > boxW,
+    hbarW = ScrollBox.barLength + 2 * ScrollBox.barPad,
+    hbarH = ScrollBox.barWidth + 2 * ScrollBox.barPad,
+    // draw horizontal scrollbar on the bottom side
+    hbarL = l,
+    hbarT = t + h;
+
+  if (hbarT + hbarH > fullHeight) hbarT = fullHeight - hbarH;
+
+  var hbar = this.container
+    .selectAll("rect.scrollbar-horizontal")
+    .data(needsHorizontalScrollBar ? [0] : []);
+
+  hbar.exit().on(".drag", null).remove();
+
+  hbar
+    .enter()
+    .append("rect")
+    .classed("scrollbar-horizontal", true)
+    .call(Color.fill, ScrollBox.barColor);
+
+  if (needsHorizontalScrollBar) {
+    this.hbar = hbar.attr({
+      rx: ScrollBox.barRadius,
+      ry: ScrollBox.barRadius,
+      x: hbarL,
+      y: hbarT,
+      width: hbarW,
+      height: hbarH
+    });
+
+    // hbar center moves between hbarXMin and hbarXMin + hbarTranslateMax
+    this._hbarXMin = hbarL + hbarW / 2;
+    this._hbarTranslateMax = boxW - hbarW;
+  } else {
+    delete this.hbar;
+    delete this._hbarXMin;
+    delete this._hbarTranslateMax;
+  }
+
+  // compute position of vertical scroll bar
+  var needsVerticalScrollBar = h > boxH,
+    vbarW = ScrollBox.barWidth + 2 * ScrollBox.barPad,
+    vbarH = ScrollBox.barLength + 2 * ScrollBox.barPad,
+    // draw vertical scrollbar on the right side
+    vbarL = l + w,
+    vbarT = t;
+
+  if (vbarL + vbarW > fullWidth) vbarL = fullWidth - vbarW;
+
+  var vbar = this.container
+    .selectAll("rect.scrollbar-vertical")
+    .data(needsVerticalScrollBar ? [0] : []);
+
+  vbar.exit().on(".drag", null).remove();
+
+  vbar
+    .enter()
+    .append("rect")
+    .classed("scrollbar-vertical", true)
+    .call(Color.fill, ScrollBox.barColor);
+
+  if (needsVerticalScrollBar) {
+    this.vbar = vbar.attr({
+      rx: ScrollBox.barRadius,
+      ry: ScrollBox.barRadius,
+      x: vbarL,
+      y: vbarT,
+      width: vbarW,
+      height: vbarH
+    });
+
+    // vbar center moves between vbarYMin and vbarYMin + vbarTranslateMax
+    this._vbarYMin = vbarT + vbarH / 2;
+    this._vbarTranslateMax = boxH - vbarH;
+  } else {
+    delete this.vbar;
+    delete this._vbarYMin;
+    delete this._vbarTranslateMax;
+  }
+
+  // setup a clip path (if scroll bars are needed)
+  var clipId = this.id,
+    clipL = boxL - 0.5,
+    clipR = needsVerticalScrollBar ? boxR + vbarW + 0.5 : boxR + 0.5,
+    clipT = boxT - 0.5,
+    clipB = needsHorizontalScrollBar ? boxB + hbarH + 0.5 : boxB + 0.5;
+
+  var clipPath = fullLayout._topdefs
+    .selectAll("#" + clipId)
+    .data(needsHorizontalScrollBar || needsVerticalScrollBar ? [0] : []);
+
+  clipPath.exit().remove();
+
+  clipPath.enter().append("clipPath").attr("id", clipId).append("rect");
+
+  if (needsHorizontalScrollBar || needsVerticalScrollBar) {
+    this._clipRect = clipPath.select("rect").attr({
+      x: Math.floor(clipL),
+      y: Math.floor(clipT),
+      width: Math.ceil(clipR) - Math.floor(clipL),
+      height: Math.ceil(clipB) - Math.floor(clipT)
+    });
+
+    this.container.call(Drawing.setClipUrl, clipId);
+
+    this.bg.attr({ x: l, y: t, width: w, height: h });
+  } else {
+    this.bg.attr({ width: 0, height: 0 });
+    this.container
+      .on("wheel", null)
+      .on(".drag", null)
+      .call(Drawing.setClipUrl, null);
+    delete this._clipRect;
+  }
+
+  // set up drag listeners (if scroll bars are needed)
+  if (needsHorizontalScrollBar || needsVerticalScrollBar) {
+    var onBoxDrag = d3.behavior
+      .drag()
+      .on("dragstart", function() {
+        d3.event.sourceEvent.preventDefault();
+      })
+      .on("drag", this._onBoxDrag.bind(this));
+
+    this.container
+      .on("wheel", null)
+      .on("wheel", this._onBoxWheel.bind(this))
+      .on(".drag", null)
+      .call(onBoxDrag);
+
+    var onBarDrag = d3.behavior
+      .drag()
+      .on("dragstart", function() {
+        d3.event.sourceEvent.preventDefault();
+        d3.event.sourceEvent.stopPropagation();
+      })
+      .on("drag", this._onBarDrag.bind(this));
+
+    if (needsHorizontalScrollBar) {
+      this.hbar.on(".drag", null).call(onBarDrag);
     }
 
-    // compute position of vertical scroll bar
-    var needsVerticalScrollBar = (h > boxH),
-        vbarW = ScrollBox.barWidth + 2 * ScrollBox.barPad,
-        vbarH = ScrollBox.barLength + 2 * ScrollBox.barPad,
-        // draw vertical scrollbar on the right side
-        vbarL = l + w,
-        vbarT = t;
-
-    if(vbarL + vbarW > fullWidth) vbarL = fullWidth - vbarW;
-
-    var vbar = this.container.selectAll('rect.scrollbar-vertical').data(
-            (needsVerticalScrollBar) ? [0] : []);
-
-    vbar.exit()
-        .on('.drag', null)
-        .remove();
-
-    vbar.enter().append('rect')
-        .classed('scrollbar-vertical', true)
-        .call(Color.fill, ScrollBox.barColor);
-
-    if(needsVerticalScrollBar) {
-        this.vbar = vbar.attr({
-            'rx': ScrollBox.barRadius,
-            'ry': ScrollBox.barRadius,
-            'x': vbarL,
-            'y': vbarT,
-            'width': vbarW,
-            'height': vbarH
-        });
-
-        // vbar center moves between vbarYMin and vbarYMin + vbarTranslateMax
-        this._vbarYMin = vbarT + vbarH / 2;
-        this._vbarTranslateMax = boxH - vbarH;
-    }
-    else {
-        delete this.vbar;
-        delete this._vbarYMin;
-        delete this._vbarTranslateMax;
+    if (needsVerticalScrollBar) {
+      this.vbar.on(".drag", null).call(onBarDrag);
     }
+  }
 
-    // setup a clip path (if scroll bars are needed)
-    var clipId = this.id,
-        clipL = boxL - 0.5,
-        clipR = (needsVerticalScrollBar) ? boxR + vbarW + 0.5 : boxR + 0.5,
-        clipT = boxT - 0.5,
-        clipB = (needsHorizontalScrollBar) ? boxB + hbarH + 0.5 : boxB + 0.5;
-
-    var clipPath = fullLayout._topdefs.selectAll('#' + clipId)
-        .data((needsHorizontalScrollBar || needsVerticalScrollBar) ? [0] : []);
-
-    clipPath.exit().remove();
-
-    clipPath.enter()
-        .append('clipPath').attr('id', clipId)
-        .append('rect');
-
-    if(needsHorizontalScrollBar || needsVerticalScrollBar) {
-        this._clipRect = clipPath.select('rect').attr({
-            x: Math.floor(clipL),
-            y: Math.floor(clipT),
-            width: Math.ceil(clipR) - Math.floor(clipL),
-            height: Math.ceil(clipB) - Math.floor(clipT)
-        });
-
-        this.container.call(Drawing.setClipUrl, clipId);
-
-        this.bg.attr({
-            x: l,
-            y: t,
-            width: w,
-            height: h
-        });
-    }
-    else {
-        this.bg.attr({
-            width: 0,
-            height: 0
-        });
-        this.container
-            .on('wheel', null)
-            .on('.drag', null)
-            .call(Drawing.setClipUrl, null);
-        delete this._clipRect;
-    }
-
-    // set up drag listeners (if scroll bars are needed)
-    if(needsHorizontalScrollBar || needsVerticalScrollBar) {
-        var onBoxDrag = d3.behavior.drag()
-            .on('dragstart', function() {
-                d3.event.sourceEvent.preventDefault();
-            })
-            .on('drag', this._onBoxDrag.bind(this));
-
-        this.container
-            .on('wheel', null)
-            .on('wheel', this._onBoxWheel.bind(this))
-            .on('.drag', null)
-            .call(onBoxDrag);
-
-        var onBarDrag = d3.behavior.drag()
-            .on('dragstart', function() {
-                d3.event.sourceEvent.preventDefault();
-                d3.event.sourceEvent.stopPropagation();
-            })
-            .on('drag', this._onBarDrag.bind(this));
-
-        if(needsHorizontalScrollBar) {
-            this.hbar
-                .on('.drag', null)
-                .call(onBarDrag);
-        }
-
-        if(needsVerticalScrollBar) {
-            this.vbar
-                .on('.drag', null)
-                .call(onBarDrag);
-        }
-    }
-
-    // set scrollbox translation
-    this.setTranslate(translateX, translateY);
+  // set scrollbox translation
+  this.setTranslate(translateX, translateY);
 };
 
 /**
@@ -319,33 +297,30 @@ ScrollBox.prototype.enable = function enable(position, translateX, translateY) {
  * @method
  */
 ScrollBox.prototype.disable = function disable() {
-    if(this.hbar || this.vbar) {
-        this.bg.attr({
-            width: 0,
-            height: 0
-        });
-        this.container
-            .on('wheel', null)
-            .on('.drag', null)
-            .call(Drawing.setClipUrl, null);
-        delete this._clipRect;
-    }
-
-    if(this.hbar) {
-        this.hbar.on('.drag', null);
-        this.hbar.remove();
-        delete this.hbar;
-        delete this._hbarXMin;
-        delete this._hbarTranslateMax;
-    }
-
-    if(this.vbar) {
-        this.vbar.on('.drag', null);
-        this.vbar.remove();
-        delete this.vbar;
-        delete this._vbarYMin;
-        delete this._vbarTranslateMax;
-    }
+  if (this.hbar || this.vbar) {
+    this.bg.attr({ width: 0, height: 0 });
+    this.container
+      .on("wheel", null)
+      .on(".drag", null)
+      .call(Drawing.setClipUrl, null);
+    delete this._clipRect;
+  }
+
+  if (this.hbar) {
+    this.hbar.on(".drag", null);
+    this.hbar.remove();
+    delete this.hbar;
+    delete this._hbarXMin;
+    delete this._hbarTranslateMax;
+  }
+
+  if (this.vbar) {
+    this.vbar.on(".drag", null);
+    this.vbar.remove();
+    delete this.vbar;
+    delete this._vbarYMin;
+    delete this._vbarTranslateMax;
+  }
 };
 
 /**
@@ -354,18 +329,17 @@ ScrollBox.prototype.disable = function disable() {
  * @method
  */
 ScrollBox.prototype._onBoxDrag = function onBarDrag() {
-    var translateX = this.translateX,
-        translateY = this.translateY;
+  var translateX = this.translateX, translateY = this.translateY;
 
-    if(this.hbar) {
-        translateX -= d3.event.dx;
-    }
+  if (this.hbar) {
+    translateX -= d3.event.dx;
+  }
 
-    if(this.vbar) {
-        translateY -= d3.event.dy;
-    }
+  if (this.vbar) {
+    translateY -= d3.event.dy;
+  }
 
-    this.setTranslate(translateX, translateY);
+  this.setTranslate(translateX, translateY);
 };
 
 /**
@@ -374,18 +348,17 @@ ScrollBox.prototype._onBoxDrag = function onBarDrag() {
  * @method
  */
 ScrollBox.prototype._onBoxWheel = function onBarWheel() {
-    var translateX = this.translateX,
-        translateY = this.translateY;
+  var translateX = this.translateX, translateY = this.translateY;
 
-    if(this.hbar) {
-        translateX += d3.event.deltaY;
-    }
+  if (this.hbar) {
+    translateX += d3.event.deltaY;
+  }
 
-    if(this.vbar) {
-        translateY += d3.event.deltaY;
-    }
+  if (this.vbar) {
+    translateY += d3.event.deltaY;
+  }
 
-    this.setTranslate(translateX, translateY);
+  this.setTranslate(translateX, translateY);
 };
 
 /**
@@ -394,32 +367,31 @@ ScrollBox.prototype._onBoxWheel = function onBarWheel() {
  * @method
  */
 ScrollBox.prototype._onBarDrag = function onBarDrag() {
-    var translateX = this.translateX,
-        translateY = this.translateY;
+  var translateX = this.translateX, translateY = this.translateY;
 
-    if(this.hbar) {
-        var xMin = translateX + this._hbarXMin,
-            xMax = xMin + this._hbarTranslateMax,
-            x = Lib.constrain(d3.event.x, xMin, xMax),
-            xf = (x - xMin) / (xMax - xMin);
+  if (this.hbar) {
+    var xMin = translateX + this._hbarXMin,
+      xMax = xMin + this._hbarTranslateMax,
+      x = Lib.constrain(d3.event.x, xMin, xMax),
+      xf = (x - xMin) / (xMax - xMin);
 
-        var translateXMax = this.position.w - this._box.w;
+    var translateXMax = this.position.w - this._box.w;
 
-        translateX = xf * translateXMax;
-    }
+    translateX = xf * translateXMax;
+  }
 
-    if(this.vbar) {
-        var yMin = translateY + this._vbarYMin,
-            yMax = yMin + this._vbarTranslateMax,
-            y = Lib.constrain(d3.event.y, yMin, yMax),
-            yf = (y - yMin) / (yMax - yMin);
+  if (this.vbar) {
+    var yMin = translateY + this._vbarYMin,
+      yMax = yMin + this._vbarTranslateMax,
+      y = Lib.constrain(d3.event.y, yMin, yMax),
+      yf = (y - yMin) / (yMax - yMin);
 
-        var translateYMax = this.position.h - this._box.h;
+    var translateYMax = this.position.h - this._box.h;
 
-        translateY = yf * translateYMax;
-    }
+    translateY = yf * translateYMax;
+  }
 
-    this.setTranslate(translateX, translateY);
+  this.setTranslate(translateX, translateY);
 };
 
 /**
@@ -429,41 +401,50 @@ ScrollBox.prototype._onBarDrag = function onBarDrag() {
  * @param {number}  [translateX=0]  Horizontal offset (in pixels)
  * @param {number}  [translateY=0]  Vertical offset (in pixels)
  */
-ScrollBox.prototype.setTranslate = function setTranslate(translateX, translateY) {
-    // store translateX and translateY (needed by mouse event handlers)
-    var translateXMax = this.position.w - this._box.w,
-        translateYMax = this.position.h - this._box.h;
-
-    translateX = Lib.constrain(translateX || 0, 0, translateXMax);
-    translateY = Lib.constrain(translateY || 0, 0, translateYMax);
-
-    this.translateX = translateX;
-    this.translateY = translateY;
-
-    this.container.call(Drawing.setTranslate,
-        this._box.l - this.position.l - translateX,
-        this._box.t - this.position.t - translateY);
-
-    if(this._clipRect) {
-        this._clipRect.attr({
-            x: Math.floor(this.position.l + translateX - 0.5),
-            y: Math.floor(this.position.t + translateY - 0.5)
-        });
-    }
-
-    if(this.hbar) {
-        var xf = translateX / translateXMax;
-
-        this.hbar.call(Drawing.setTranslate,
-            translateX + xf * this._hbarTranslateMax,
-            translateY);
-    }
-
-    if(this.vbar) {
-        var yf = translateY / translateYMax;
-
-        this.vbar.call(Drawing.setTranslate,
-            translateX,
-            translateY + yf * this._vbarTranslateMax);
-    }
+ScrollBox.prototype.setTranslate = function setTranslate(
+  translateX,
+  translateY
+) {
+  // store translateX and translateY (needed by mouse event handlers)
+  var translateXMax = this.position.w - this._box.w,
+    translateYMax = this.position.h - this._box.h;
+
+  translateX = Lib.constrain(translateX || 0, 0, translateXMax);
+  translateY = Lib.constrain(translateY || 0, 0, translateYMax);
+
+  this.translateX = translateX;
+  this.translateY = translateY;
+
+  this.container.call(
+    Drawing.setTranslate,
+    this._box.l - this.position.l - translateX,
+    this._box.t - this.position.t - translateY
+  );
+
+  if (this._clipRect) {
+    this._clipRect.attr({
+      x: Math.floor(this.position.l + translateX - 0.5),
+      y: Math.floor(this.position.t + translateY - 0.5)
+    });
+  }
+
+  if (this.hbar) {
+    var xf = translateX / translateXMax;
+
+    this.hbar.call(
+      Drawing.setTranslate,
+      translateX + xf * this._hbarTranslateMax,
+      translateY
+    );
+  }
+
+  if (this.vbar) {
+    var yf = translateY / translateYMax;
+
+    this.vbar.call(
+      Drawing.setTranslate,
+      translateX,
+      translateY + yf * this._vbarTranslateMax
+    );
+  }
 };
diff --git a/src/constants/gl2d_dashes.js b/src/constants/gl2d_dashes.js
index 8675739aa56..67d116470c9 100644
--- a/src/constants/gl2d_dashes.js
+++ b/src/constants/gl2d_dashes.js
@@ -5,15 +5,12 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 module.exports = {
-    solid: [1],
-    dot: [1, 1],
-    dash: [4, 1],
-    longdash: [8, 1],
-    dashdot: [4, 1, 1, 1],
-    longdashdot: [8, 1, 1, 1]
+  solid: [1],
+  dot: [1, 1],
+  dash: [4, 1],
+  longdash: [8, 1],
+  dashdot: [4, 1, 1, 1],
+  longdashdot: [8, 1, 1, 1]
 };
diff --git a/src/constants/gl3d_dashes.js b/src/constants/gl3d_dashes.js
index 8e4ac98164a..dee80abd774 100644
--- a/src/constants/gl3d_dashes.js
+++ b/src/constants/gl3d_dashes.js
@@ -5,15 +5,12 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 module.exports = {
-    solid: [[], 0],
-    dot: [[0.5, 1], 200],
-    dash: [[0.5, 1], 50],
-    longdash: [[0.5, 1], 10],
-    dashdot: [[0.5, 0.625, 0.875, 1], 50],
-    longdashdot: [[0.5, 0.7, 0.8, 1], 10]
+  solid: [[], 0],
+  dot: [[0.5, 1], 200],
+  dash: [[0.5, 1], 50],
+  longdash: [[0.5, 1], 10],
+  dashdot: [[0.5, 0.625, 0.875, 1], 50],
+  longdashdot: [[0.5, 0.7, 0.8, 1], 10]
 };
diff --git a/src/constants/gl_markers.js b/src/constants/gl_markers.js
index e10354e84a4..59866362cc7 100644
--- a/src/constants/gl_markers.js
+++ b/src/constants/gl_markers.js
@@ -5,17 +5,14 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 module.exports = {
-    circle: '●',
-    'circle-open': '○',
-    square: '■',
-    'square-open': '□',
-    diamond: '◆',
-    'diamond-open': '◇',
-    cross: '+',
-    x: '❌'
+  circle: "\u25CF",
+  "circle-open": "\u25CB",
+  square: "\u25A0",
+  "square-open": "\u25A1",
+  diamond: "\u25C6",
+  "diamond-open": "\u25C7",
+  cross: "+",
+  x: "\u274C"
 };
diff --git a/src/constants/interactions.js b/src/constants/interactions.js
index 132e9ca4c37..681772f16d1 100644
--- a/src/constants/interactions.js
+++ b/src/constants/interactions.js
@@ -5,14 +5,11 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-    /**
+  /**
      * Timing information for interactive elements
      */
-    SHOW_PLACEHOLDER: 100,
-    HIDE_PLACEHOLDER: 1000
+  SHOW_PLACEHOLDER: 100,
+  HIDE_PLACEHOLDER: 1000
 };
diff --git a/src/constants/numerical.js b/src/constants/numerical.js
index c881daa72c4..35da533fa1f 100644
--- a/src/constants/numerical.js
+++ b/src/constants/numerical.js
@@ -5,42 +5,40 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-    /**
+  /**
      * Standardize all missing data in calcdata to use undefined
      * never null or NaN.
      * That way we can use !==undefined, or !== BADNUM,
      * to test for real data
      */
-    BADNUM: undefined,
-
-    /*
+  BADNUM: undefined,
+  /*
      * Limit certain operations to well below floating point max value
      * to avoid glitches: Make sure that even when you multiply it by the
      * number of pixels on a giant screen it still works
      */
-    FP_SAFE: Number.MAX_VALUE / 10000,
-
-    /*
+  FP_SAFE: (
+    Number.MAX_VALUE / 10000
+  ),
+  /*
      * conversion of date units to milliseconds
      * year and month constants are marked "AVG"
      * to remind us that not all years and months
      * have the same length
      */
-    ONEAVGYEAR: 31557600000, // 365.25 days
-    ONEAVGMONTH: 2629800000, // 1/12 of ONEAVGYEAR
-    ONEDAY: 86400000,
-    ONEHOUR: 3600000,
-    ONEMIN: 60000,
-    ONESEC: 1000,
-
-    /*
+  ONEAVGYEAR: 31557600000,
+  // 365.25 days
+  ONEAVGMONTH: 2629800000,
+  // 1/12 of ONEAVGYEAR
+  ONEDAY: 86400000,
+  ONEHOUR: 3600000,
+  ONEMIN: 60000,
+  ONESEC: 1000,
+  /*
      * For fast conversion btwn world calendars and epoch ms, the Julian Day Number
      * of the unix epoch. From calendars.instance().newDate(1970, 1, 1).toJD()
      */
-    EPOCHJD: 2440587.5
+  EPOCHJD: 2440587.5
 };
diff --git a/src/constants/string_mappings.js b/src/constants/string_mappings.js
index a2760f7b6c0..052735edb7d 100644
--- a/src/constants/string_mappings.js
+++ b/src/constants/string_mappings.js
@@ -5,32 +5,26 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 // N.B. HTML entities are listed without the leading '&' and trailing ';'
 
 module.exports = {
-
-    entityToUnicode: {
-        'mu': 'μ',
-        'amp': '&',
-        'lt': '<',
-        'gt': '>',
-        'nbsp': ' ',
-        'times': '×',
-        'plusmn': '±',
-        'deg': '°'
-    },
-
-    unicodeToEntity: {
-        '&': 'amp',
-        '<': 'lt',
-        '>': 'gt',
-        '"': 'quot',
-        '\'': '#x27',
-        '\/': '#x2F'
-    }
-
+  entityToUnicode: {
+    mu: "\u03BC",
+    amp: "&",
+    lt: "<",
+    gt: ">",
+    nbsp: "\xA0",
+    times: "\xD7",
+    plusmn: "\xB1",
+    deg: "\xB0"
+  },
+  unicodeToEntity: {
+    "&": "amp",
+    "<": "lt",
+    ">": "gt",
+    '"': "quot",
+    "'": "#x27",
+    "/": "#x2F"
+  }
 };
diff --git a/src/constants/xmlns_namespaces.js b/src/constants/xmlns_namespaces.js
index aaeaea827a3..26baebcbc80 100644
--- a/src/constants/xmlns_namespaces.js
+++ b/src/constants/xmlns_namespaces.js
@@ -5,18 +5,11 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-
-exports.xmlns = 'http://www.w3.org/2000/xmlns/';
-exports.svg = 'http://www.w3.org/2000/svg';
-exports.xlink = 'http://www.w3.org/1999/xlink';
+"use strict";
+exports.xmlns = "http://www.w3.org/2000/xmlns/";
+exports.svg = "http://www.w3.org/2000/svg";
+exports.xlink = "http://www.w3.org/1999/xlink";
 
 // the 'old' d3 quirk got fix in v3.5.7
 // https://github.com/mbostock/d3/commit/a6f66e9dd37f764403fc7c1f26be09ab4af24fed
-exports.svgAttrs = {
-    xmlns: exports.svg,
-    'xmlns:xlink': exports.xlink
-};
+exports.svgAttrs = { xmlns: exports.svg, "xmlns:xlink": exports.xlink };
diff --git a/src/core.js b/src/core.js
index 23774702e83..c6420fa69ce 100644
--- a/src/core.js
+++ b/src/core.js
@@ -5,26 +5,24 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 /*
  * Export the plotly.js API methods.
  */
 
-var Plotly = require('./plotly');
+var Plotly = require("./plotly");
 
 // package version injected by `npm run preprocess`
-exports.version = '1.23.1';
+exports.version = "1.23.1";
 
 // inject promise polyfill
-require('es6-promise').polyfill();
+require("es6-promise").polyfill();
 
 // inject plot css
-require('../build/plotcss');
+require("../build/plotcss");
 
 // inject default MathJax config
-require('./fonts/mathjax_config');
+require("./fonts/mathjax_config");
 
 // plot api
 exports.plot = Plotly.plot;
@@ -39,39 +37,39 @@ exports.addTraces = Plotly.addTraces;
 exports.deleteTraces = Plotly.deleteTraces;
 exports.moveTraces = Plotly.moveTraces;
 exports.purge = Plotly.purge;
-exports.setPlotConfig = require('./plot_api/set_plot_config');
-exports.register = require('./plot_api/register');
-exports.toImage = require('./plot_api/to_image');
-exports.downloadImage = require('./snapshot/download');
-exports.validate = require('./plot_api/validate');
+exports.setPlotConfig = require("./plot_api/set_plot_config");
+exports.register = require("./plot_api/register");
+exports.toImage = require("./plot_api/to_image");
+exports.downloadImage = require("./snapshot/download");
+exports.validate = require("./plot_api/validate");
 exports.addFrames = Plotly.addFrames;
 exports.deleteFrames = Plotly.deleteFrames;
 exports.animate = Plotly.animate;
 
 // scatter is the only trace included by default
-exports.register(require('./traces/scatter'));
+exports.register(require("./traces/scatter"));
 
 // register all registrable components modules
 exports.register([
-    require('./components/legend'),
-    require('./components/annotations'),
-    require('./components/shapes'),
-    require('./components/images'),
-    require('./components/updatemenus'),
-    require('./components/sliders'),
-    require('./components/rangeslider'),
-    require('./components/rangeselector')
+  require("./components/legend"),
+  require("./components/annotations"),
+  require("./components/shapes"),
+  require("./components/images"),
+  require("./components/updatemenus"),
+  require("./components/sliders"),
+  require("./components/rangeslider"),
+  require("./components/rangeselector")
 ]);
 
 // plot icons
-exports.Icons = require('../build/ploticon');
+exports.Icons = require("../build/ploticon");
 
 // unofficial 'beta' plot methods, use at your own risk
 exports.Plots = Plotly.Plots;
 exports.Fx = Plotly.Fx;
-exports.Snapshot = require('./snapshot');
-exports.PlotSchema = require('./plot_api/plot_schema');
-exports.Queue = require('./lib/queue');
+exports.Snapshot = require("./snapshot");
+exports.PlotSchema = require("./plot_api/plot_schema");
+exports.Queue = require("./lib/queue");
 
 // export d3 used in the bundle
-exports.d3 = require('d3');
+exports.d3 = require("d3");
diff --git a/src/fonts/mathjax_config.js b/src/fonts/mathjax_config.js
index 8005ad86e13..1799482a286 100644
--- a/src/fonts/mathjax_config.js
+++ b/src/fonts/mathjax_config.js
@@ -5,27 +5,23 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 /* global MathJax:false */
 
 /**
  * Check and configure MathJax
  */
-if(typeof MathJax !== 'undefined') {
-    exports.MathJax = true;
+if (typeof MathJax !== "undefined") {
+  exports.MathJax = true;
 
-    MathJax.Hub.Config({
-        messageStyle: 'none',
-        skipStartupTypeset: true,
-        displayAlign: 'left',
-        tex2jax: {
-            inlineMath: [['$', '$'], ['\\(', '\\)']]
-        }
-    });
+  MathJax.Hub.Config({
+    messageStyle: "none",
+    skipStartupTypeset: true,
+    displayAlign: "left",
+    tex2jax: { inlineMath: [["$", "$"], ["\\(", "\\)"]] }
+  });
 
-    MathJax.Hub.Configured();
+  MathJax.Hub.Configured();
 } else {
-    exports.MathJax = false;
+  exports.MathJax = false;
 }
diff --git a/src/lib/array_to_calc_item.js b/src/lib/array_to_calc_item.js
index 4a968234f0a..bd326d91b07 100644
--- a/src/lib/array_to_calc_item.js
+++ b/src/lib/array_to_calc_item.js
@@ -5,11 +5,8 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 // similar to Lib.mergeArray, but using inside a loop
 module.exports = function arrayToCalcItem(traceAttr, calcItem, calcAttr, i) {
-    if(Array.isArray(traceAttr)) calcItem[calcAttr] = traceAttr[i];
+  if (Array.isArray(traceAttr)) calcItem[calcAttr] = traceAttr[i];
 };
diff --git a/src/lib/clean_number.js b/src/lib/clean_number.js
index 922c2db7e94..26d393b6499 100644
--- a/src/lib/clean_number.js
+++ b/src/lib/clean_number.js
@@ -5,13 +5,10 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var isNumeric = require("fast-isnumeric");
 
-
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-
-var BADNUM = require('../constants/numerical').BADNUM;
+var BADNUM = require("../constants/numerical").BADNUM;
 
 // precompile for speed
 var JUNK = /^['"%,$#\s']+|[, ]|['"%,$#\s']+$/g;
@@ -21,11 +18,11 @@ var JUNK = /^['"%,$#\s']+|[, ]|['"%,$#\s']+$/g;
  * Always returns either a number or BADNUM.
  */
 module.exports = function cleanNumber(v) {
-    if(typeof v === 'string') {
-        v = v.replace(JUNK, '');
-    }
+  if (typeof v === "string") {
+    v = v.replace(JUNK, "");
+  }
 
-    if(isNumeric(v)) return Number(v);
+  if (isNumeric(v)) return Number(v);
 
-    return BADNUM;
+  return BADNUM;
 };
diff --git a/src/lib/coerce.js b/src/lib/coerce.js
index f3ef35d6598..346011173c2 100644
--- a/src/lib/coerce.js
+++ b/src/lib/coerce.js
@@ -5,269 +5,273 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var isNumeric = require("fast-isnumeric");
+var tinycolor = require("tinycolor2");
 
-
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-var tinycolor = require('tinycolor2');
-
-var getColorscale = require('../components/colorscale/get_scale');
-var colorscaleNames = Object.keys(require('../components/colorscale/scales'));
-var nestedProperty = require('./nested_property');
+var getColorscale = require("../components/colorscale/get_scale");
+var colorscaleNames = Object.keys(require("../components/colorscale/scales"));
+var nestedProperty = require("./nested_property");
 
 var ID_REGEX = /^([2-9]|[1-9][0-9]+)$/;
 
 exports.valObjects = {
-    data_array: {
-        // You can use *dflt=[] to force said array to exist though.
-        description: [
-            'An {array} of data.',
-            'The value MUST be an {array}, or we ignore it.'
-        ].join(' '),
-        requiredOpts: [],
-        otherOpts: ['dflt'],
-        coerceFunction: function(v, propOut, dflt) {
-            if(Array.isArray(v)) propOut.set(v);
-            else if(dflt !== undefined) propOut.set(dflt);
-        }
-    },
-    enumerated: {
-        description: [
-            'Enumerated value type. The available values are listed',
-            'in `values`.'
-        ].join(' '),
-        requiredOpts: ['values'],
-        otherOpts: ['dflt', 'coerceNumber', 'arrayOk'],
-        coerceFunction: function(v, propOut, dflt, opts) {
-            if(opts.coerceNumber) v = +v;
-            if(opts.values.indexOf(v) === -1) propOut.set(dflt);
-            else propOut.set(v);
-        }
-    },
-    'boolean': {
-        description: 'A boolean (true/false) value.',
-        requiredOpts: [],
-        otherOpts: ['dflt'],
-        coerceFunction: function(v, propOut, dflt) {
-            if(v === true || v === false) propOut.set(v);
-            else propOut.set(dflt);
-        }
-    },
-    number: {
-        description: [
-            'A number or a numeric value',
-            '(e.g. a number inside a string).',
-            'When applicable, values greater (less) than `max` (`min`)',
-            'are coerced to the `dflt`.'
-        ].join(' '),
-        requiredOpts: [],
-        otherOpts: ['dflt', 'min', 'max', 'arrayOk'],
-        coerceFunction: function(v, propOut, dflt, opts) {
-            if(!isNumeric(v) ||
-                    (opts.min !== undefined && v < opts.min) ||
-                    (opts.max !== undefined && v > opts.max)) {
-                propOut.set(dflt);
-            }
-            else propOut.set(+v);
-        }
-    },
-    integer: {
-        description: [
-            'An integer or an integer inside a string.',
-            'When applicable, values greater (less) than `max` (`min`)',
-            'are coerced to the `dflt`.'
-        ].join(' '),
-        requiredOpts: [],
-        otherOpts: ['dflt', 'min', 'max'],
-        coerceFunction: function(v, propOut, dflt, opts) {
-            if(v % 1 || !isNumeric(v) ||
-                    (opts.min !== undefined && v < opts.min) ||
-                    (opts.max !== undefined && v > opts.max)) {
-                propOut.set(dflt);
-            }
-            else propOut.set(+v);
-        }
-    },
-    string: {
-        description: [
-            'A string value.',
-            'Numbers are converted to strings except for attributes with',
-            '`strict` set to true.'
-        ].join(' '),
-        requiredOpts: [],
-        // TODO 'values shouldn't be in there (edge case: 'dash' in Scatter)
-        otherOpts: ['dflt', 'noBlank', 'strict', 'arrayOk', 'values'],
-        coerceFunction: function(v, propOut, dflt, opts) {
-            if(typeof v !== 'string') {
-                var okToCoerce = (typeof v === 'number');
-
-                if(opts.strict === true || !okToCoerce) propOut.set(dflt);
-                else propOut.set(String(v));
-            }
-            else if(opts.noBlank && !v) propOut.set(dflt);
-            else propOut.set(v);
-        }
-    },
-    color: {
-        description: [
-            'A string describing color.',
-            'Supported formats:',
-            '- hex (e.g. \'#d3d3d3\')',
-            '- rgb (e.g. \'rgb(255, 0, 0)\')',
-            '- rgba (e.g. \'rgb(255, 0, 0, 0.5)\')',
-            '- hsl (e.g. \'hsl(0, 100%, 50%)\')',
-            '- hsv (e.g. \'hsv(0, 100%, 100%)\')',
-            '- named colors (full list: http://www.w3.org/TR/css3-color/#svg-color)'
-        ].join(' '),
-        requiredOpts: [],
-        otherOpts: ['dflt', 'arrayOk'],
-        coerceFunction: function(v, propOut, dflt) {
-            if(tinycolor(v).isValid()) propOut.set(v);
-            else propOut.set(dflt);
-        }
-    },
-    colorscale: {
-        description: [
-            'A Plotly colorscale either picked by a name:',
-            '(any of', colorscaleNames.join(', '), ')',
-            'customized as an {array} of 2-element {arrays} where',
-            'the first element is the normalized color level value',
-            '(starting at *0* and ending at *1*),',
-            'and the second item is a valid color string.'
-        ].join(' '),
-        requiredOpts: [],
-        otherOpts: ['dflt'],
-        coerceFunction: function(v, propOut, dflt) {
-            propOut.set(getColorscale(v, dflt));
-        }
-    },
-    angle: {
-        description: [
-            'A number (in degree) between -180 and 180.'
-        ].join(' '),
-        requiredOpts: [],
-        otherOpts: ['dflt'],
-        coerceFunction: function(v, propOut, dflt) {
-            if(v === 'auto') propOut.set('auto');
-            else if(!isNumeric(v)) propOut.set(dflt);
-            else {
-                if(Math.abs(v) > 180) v -= Math.round(v / 360) * 360;
-                propOut.set(+v);
-            }
-        }
+  data_array: {
+    // You can use *dflt=[] to force said array to exist though.
+    description: [
+      "An {array} of data.",
+      "The value MUST be an {array}, or we ignore it."
+    ].join(" "),
+    requiredOpts: [],
+    otherOpts: ["dflt"],
+    coerceFunction: function(v, propOut, dflt) {
+      if (Array.isArray(v)) propOut.set(v);
+      else if (dflt !== undefined) propOut.set(dflt);
+    }
+  },
+  enumerated: {
+    description: [
+      "Enumerated value type. The available values are listed",
+      "in `values`."
+    ].join(" "),
+    requiredOpts: ["values"],
+    otherOpts: ["dflt", "coerceNumber", "arrayOk"],
+    coerceFunction: function(v, propOut, dflt, opts) {
+      if (opts.coerceNumber) v = +v;
+      if (opts.values.indexOf(v) === -1) propOut.set(dflt);
+      else propOut.set(v);
+    }
+  },
+  boolean: {
+    description: "A boolean (true/false) value.",
+    requiredOpts: [],
+    otherOpts: ["dflt"],
+    coerceFunction: function(v, propOut, dflt) {
+      if (v === true || v === false) propOut.set(v);
+      else propOut.set(dflt);
+    }
+  },
+  number: {
+    description: [
+      "A number or a numeric value",
+      "(e.g. a number inside a string).",
+      "When applicable, values greater (less) than `max` (`min`)",
+      "are coerced to the `dflt`."
+    ].join(" "),
+    requiredOpts: [],
+    otherOpts: ["dflt", "min", "max", "arrayOk"],
+    coerceFunction: function(v, propOut, dflt, opts) {
+      if (
+        !isNumeric(v) ||
+          opts.min !== undefined && v < opts.min ||
+          opts.max !== undefined && v > opts.max
+      ) {
+        propOut.set(dflt);
+      } else {
+        propOut.set(+v);
+      }
+    }
+  },
+  integer: {
+    description: [
+      "An integer or an integer inside a string.",
+      "When applicable, values greater (less) than `max` (`min`)",
+      "are coerced to the `dflt`."
+    ].join(" "),
+    requiredOpts: [],
+    otherOpts: ["dflt", "min", "max"],
+    coerceFunction: function(v, propOut, dflt, opts) {
+      if (
+        v % 1 ||
+          !isNumeric(v) ||
+          opts.min !== undefined && v < opts.min ||
+          opts.max !== undefined && v > opts.max
+      ) {
+        propOut.set(dflt);
+      } else {
+        propOut.set(+v);
+      }
+    }
+  },
+  string: {
+    description: [
+      "A string value.",
+      "Numbers are converted to strings except for attributes with",
+      "`strict` set to true."
+    ].join(" "),
+    requiredOpts: [],
+    // TODO 'values shouldn't be in there (edge case: 'dash' in Scatter)
+    otherOpts: ["dflt", "noBlank", "strict", "arrayOk", "values"],
+    coerceFunction: function(v, propOut, dflt, opts) {
+      if (typeof v !== "string") {
+        var okToCoerce = typeof v === "number";
+
+        if (opts.strict === true || !okToCoerce) propOut.set(dflt);
+        else propOut.set(String(v));
+      } else if (opts.noBlank && !v) propOut.set(dflt);
+      else propOut.set(v);
+    }
+  },
+  color: {
+    description: [
+      "A string describing color.",
+      "Supported formats:",
+      "- hex (e.g. '#d3d3d3')",
+      "- rgb (e.g. 'rgb(255, 0, 0)')",
+      "- rgba (e.g. 'rgb(255, 0, 0, 0.5)')",
+      "- hsl (e.g. 'hsl(0, 100%, 50%)')",
+      "- hsv (e.g. 'hsv(0, 100%, 100%)')",
+      "- named colors (full list: http://www.w3.org/TR/css3-color/#svg-color)"
+    ].join(" "),
+    requiredOpts: [],
+    otherOpts: ["dflt", "arrayOk"],
+    coerceFunction: function(v, propOut, dflt) {
+      if (tinycolor(v).isValid()) propOut.set(v);
+      else propOut.set(dflt);
+    }
+  },
+  colorscale: {
+    description: [
+      "A Plotly colorscale either picked by a name:",
+      "(any of",
+      colorscaleNames.join(", "),
+      ")",
+      "customized as an {array} of 2-element {arrays} where",
+      "the first element is the normalized color level value",
+      "(starting at *0* and ending at *1*),",
+      "and the second item is a valid color string."
+    ].join(" "),
+    requiredOpts: [],
+    otherOpts: ["dflt"],
+    coerceFunction: function(v, propOut, dflt) {
+      propOut.set(getColorscale(v, dflt));
+    }
+  },
+  angle: {
+    description: ["A number (in degree) between -180 and 180."].join(" "),
+    requiredOpts: [],
+    otherOpts: ["dflt"],
+    coerceFunction: function(v, propOut, dflt) {
+      if (v === "auto") {
+        propOut.set("auto");
+      } else if (!isNumeric(v)) {
+        propOut.set(dflt);
+      } else {
+        if (Math.abs(v) > 180) v -= Math.round(v / 360) * 360;
+        propOut.set(+v);
+      }
+    }
+  },
+  subplotid: {
+    description: [
+      "An id string of a subplot type (given by dflt), optionally",
+      "followed by an integer >1. e.g. if dflt='geo', we can have",
+      "'geo', 'geo2', 'geo3', ..."
+    ].join(" "),
+    requiredOpts: ["dflt"],
+    otherOpts: [],
+    coerceFunction: function(v, propOut, dflt) {
+      var dlen = dflt.length;
+      if (
+        typeof v === "string" &&
+          v.substr(0, dlen) === dflt &&
+          ID_REGEX.test(v.substr(dlen))
+      ) {
+        propOut.set(v);
+        return;
+      }
+      propOut.set(dflt);
     },
-    subplotid: {
-        description: [
-            'An id string of a subplot type (given by dflt), optionally',
-            'followed by an integer >1. e.g. if dflt=\'geo\', we can have',
-            '\'geo\', \'geo2\', \'geo3\', ...'
-        ].join(' '),
-        requiredOpts: ['dflt'],
-        otherOpts: [],
-        coerceFunction: function(v, propOut, dflt) {
-            var dlen = dflt.length;
-            if(typeof v === 'string' && v.substr(0, dlen) === dflt &&
-                    ID_REGEX.test(v.substr(dlen))) {
-                propOut.set(v);
-                return;
-            }
-            propOut.set(dflt);
-        },
-        validateFunction: function(v, opts) {
-            var dflt = opts.dflt,
-                dlen = dflt.length;
+    validateFunction: function(v, opts) {
+      var dflt = opts.dflt, dlen = dflt.length;
 
-            if(v === dflt) return true;
-            if(typeof v !== 'string') return false;
-            if(v.substr(0, dlen) === dflt && ID_REGEX.test(v.substr(dlen))) {
-                return true;
-            }
+      if (v === dflt) return true;
+      if (typeof v !== "string") return false;
+      if (v.substr(0, dlen) === dflt && ID_REGEX.test(v.substr(dlen))) {
+        return true;
+      }
 
-            return false;
-        }
-    },
-    flaglist: {
-        description: [
-            'A string representing a combination of flags',
-            '(order does not matter here).',
-            'Combine any of the available `flags` with *+*.',
-            '(e.g. (\'lines+markers\')).',
-            'Values in `extras` cannot be combined.'
-        ].join(' '),
-        requiredOpts: ['flags'],
-        otherOpts: ['dflt', 'extras'],
-        coerceFunction: function(v, propOut, dflt, opts) {
-            if(typeof v !== 'string') {
-                propOut.set(dflt);
-                return;
-            }
-            if((opts.extras || []).indexOf(v) !== -1) {
-                propOut.set(v);
-                return;
-            }
-            var vParts = v.split('+'),
-                i = 0;
-            while(i < vParts.length) {
-                var vi = vParts[i];
-                if(opts.flags.indexOf(vi) === -1 || vParts.indexOf(vi) < i) {
-                    vParts.splice(i, 1);
-                }
-                else i++;
-            }
-            if(!vParts.length) propOut.set(dflt);
-            else propOut.set(vParts.join('+'));
-        }
-    },
-    any: {
-        description: 'Any type.',
-        requiredOpts: [],
-        otherOpts: ['dflt', 'values', 'arrayOk'],
-        coerceFunction: function(v, propOut, dflt) {
-            if(v === undefined) propOut.set(dflt);
-            else propOut.set(v);
+      return false;
+    }
+  },
+  flaglist: {
+    description: [
+      "A string representing a combination of flags",
+      "(order does not matter here).",
+      "Combine any of the available `flags` with *+*.",
+      "(e.g. ('lines+markers')).",
+      "Values in `extras` cannot be combined."
+    ].join(" "),
+    requiredOpts: ["flags"],
+    otherOpts: ["dflt", "extras"],
+    coerceFunction: function(v, propOut, dflt, opts) {
+      if (typeof v !== "string") {
+        propOut.set(dflt);
+        return;
+      }
+      if ((opts.extras || []).indexOf(v) !== -1) {
+        propOut.set(v);
+        return;
+      }
+      var vParts = v.split("+"), i = 0;
+      while (i < vParts.length) {
+        var vi = vParts[i];
+        if (opts.flags.indexOf(vi) === -1 || vParts.indexOf(vi) < i) {
+          vParts.splice(i, 1);
+        } else {
+          i++;
         }
+      }
+      if (!vParts.length) propOut.set(dflt);
+      else propOut.set(vParts.join("+"));
+    }
+  },
+  any: {
+    description: "Any type.",
+    requiredOpts: [],
+    otherOpts: ["dflt", "values", "arrayOk"],
+    coerceFunction: function(v, propOut, dflt) {
+      if (v === undefined) propOut.set(dflt);
+      else propOut.set(v);
+    }
+  },
+  info_array: {
+    description: ["An {array} of plot information."].join(" "),
+    requiredOpts: ["items"],
+    otherOpts: ["dflt", "freeLength"],
+    coerceFunction: function(v, propOut, dflt, opts) {
+      if (!Array.isArray(v)) {
+        propOut.set(dflt);
+        return;
+      }
+
+      var items = opts.items, vOut = [];
+      dflt = Array.isArray(dflt) ? dflt : [];
+
+      for (var i = 0; i < items.length; i++) {
+        exports.coerce(v, vOut, items, "[" + i + "]", dflt[i]);
+      }
+
+      propOut.set(vOut);
     },
-    info_array: {
-        description: [
-            'An {array} of plot information.'
-        ].join(' '),
-        requiredOpts: ['items'],
-        otherOpts: ['dflt', 'freeLength'],
-        coerceFunction: function(v, propOut, dflt, opts) {
-            if(!Array.isArray(v)) {
-                propOut.set(dflt);
-                return;
-            }
-
-            var items = opts.items,
-                vOut = [];
-            dflt = Array.isArray(dflt) ? dflt : [];
-
-            for(var i = 0; i < items.length; i++) {
-                exports.coerce(v, vOut, items, '[' + i + ']', dflt[i]);
-            }
+    validateFunction: function(v, opts) {
+      if (!Array.isArray(v)) return false;
 
-            propOut.set(vOut);
-        },
-        validateFunction: function(v, opts) {
-            if(!Array.isArray(v)) return false;
+      var items = opts.items;
 
-            var items = opts.items;
+      // when free length is off, input and declared lengths must match
+      if (!opts.freeLength && v.length !== items.length) return false;
 
-            // when free length is off, input and declared lengths must match
-            if(!opts.freeLength && v.length !== items.length) return false;
+      // valid when all input items are valid
+      for (var i = 0; i < v.length; i++) {
+        var isItemValid = exports.validate(v[i], opts.items[i]);
 
-            // valid when all input items are valid
-            for(var i = 0; i < v.length; i++) {
-                var isItemValid = exports.validate(v[i], opts.items[i]);
+        if (!isItemValid) return false;
+      }
 
-                if(!isItemValid) return false;
-            }
-
-            return true;
-        }
+      return true;
     }
+  }
 };
 
 /**
@@ -282,28 +286,34 @@ exports.valObjects = {
  *      if dflt is provided as an argument to lib.coerce it takes precedence
  *      as a convenience, returns the value it finally set
  */
-exports.coerce = function(containerIn, containerOut, attributes, attribute, dflt) {
-    var opts = nestedProperty(attributes, attribute).get(),
-        propIn = nestedProperty(containerIn, attribute),
-        propOut = nestedProperty(containerOut, attribute),
-        v = propIn.get();
-
-    if(dflt === undefined) dflt = opts.dflt;
-
-    /**
+exports.coerce = function(
+  containerIn,
+  containerOut,
+  attributes,
+  attribute,
+  dflt
+) {
+  var opts = nestedProperty(attributes, attribute).get(),
+    propIn = nestedProperty(containerIn, attribute),
+    propOut = nestedProperty(containerOut, attribute),
+    v = propIn.get();
+
+  if (dflt === undefined) dflt = opts.dflt;
+
+  /**
      * arrayOk: value MAY be an array, then we do no value checking
      * at this point, because it can be more complicated than the
      * individual form (eg. some array vals can be numbers, even if the
      * single values must be color strings)
      */
-    if(opts.arrayOk && Array.isArray(v)) {
-        propOut.set(v);
-        return v;
-    }
+  if (opts.arrayOk && Array.isArray(v)) {
+    propOut.set(v);
+    return v;
+  }
 
-    exports.valObjects[opts.valType].coerceFunction(v, propOut, dflt, opts);
+  exports.valObjects[opts.valType].coerceFunction(v, propOut, dflt, opts);
 
-    return propOut.get();
+  return propOut.get();
 };
 
 /**
@@ -313,12 +323,24 @@ exports.coerce = function(containerIn, containerOut, attributes, attribute, dflt
  * returns attribute default if user input it not valid or
  * returns false if there is no user input.
  */
-exports.coerce2 = function(containerIn, containerOut, attributes, attribute, dflt) {
-    var propIn = nestedProperty(containerIn, attribute),
-        propOut = exports.coerce(containerIn, containerOut, attributes, attribute, dflt),
-        valIn = propIn.get();
-
-    return (valIn !== undefined && valIn !== null) ? propOut : false;
+exports.coerce2 = function(
+  containerIn,
+  containerOut,
+  attributes,
+  attribute,
+  dflt
+) {
+  var propIn = nestedProperty(containerIn, attribute),
+    propOut = exports.coerce(
+      containerIn,
+      containerOut,
+      attributes,
+      attribute,
+      dflt
+    ),
+    valIn = propIn.get();
+
+  return valIn !== undefined && valIn !== null ? propOut : false;
 };
 
 /*
@@ -327,32 +349,35 @@ exports.coerce2 = function(containerIn, containerOut, attributes, attribute, dfl
  * 'coerce' is a lib.coerce wrapper with implied first three arguments
  */
 exports.coerceFont = function(coerce, attr, dfltObj) {
-    var out = {};
+  var out = {};
 
-    dfltObj = dfltObj || {};
+  dfltObj = dfltObj || {};
 
-    out.family = coerce(attr + '.family', dfltObj.family);
-    out.size = coerce(attr + '.size', dfltObj.size);
-    out.color = coerce(attr + '.color', dfltObj.color);
+  out.family = coerce(attr + ".family", dfltObj.family);
+  out.size = coerce(attr + ".size", dfltObj.size);
+  out.color = coerce(attr + ".color", dfltObj.color);
 
-    return out;
+  return out;
 };
 
 exports.validate = function(value, opts) {
-    var valObject = exports.valObjects[opts.valType];
-
-    if(opts.arrayOk && Array.isArray(value)) return true;
+  var valObject = exports.valObjects[opts.valType];
 
-    if(valObject.validateFunction) {
-        return valObject.validateFunction(value, opts);
-    }
+  if (opts.arrayOk && Array.isArray(value)) return true;
 
-    var failed = {},
-        out = failed,
-        propMock = { set: function(v) { out = v; } };
+  if (valObject.validateFunction) {
+    return valObject.validateFunction(value, opts);
+  }
 
-    // 'failed' just something mutable that won't be === anything else
+  var failed = {},
+    out = failed,
+    propMock = {
+      set: function(v) {
+        out = v;
+      }
+    };
 
-    valObject.coerceFunction(value, propMock, failed, opts);
-    return out !== failed;
+  // 'failed' just something mutable that won't be === anything else
+  valObject.coerceFunction(value, propMock, failed, opts);
+  return out !== failed;
 };
diff --git a/src/lib/dates.js b/src/lib/dates.js
index c5c05fcfda9..dd91ba84ffe 100644
--- a/src/lib/dates.js
+++ b/src/lib/dates.js
@@ -5,17 +5,14 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
+var isNumeric = require("fast-isnumeric");
 
+var logError = require("./loggers").error;
+var mod = require("./mod");
 
-'use strict';
-
-var d3 = require('d3');
-var isNumeric = require('fast-isnumeric');
-
-var logError = require('./loggers').error;
-var mod = require('./mod');
-
-var constants = require('../constants/numerical');
+var constants = require("../constants/numerical");
 var BADNUM = constants.BADNUM;
 var ONEDAY = constants.ONEDAY;
 var ONEHOUR = constants.ONEHOUR;
@@ -23,7 +20,7 @@ var ONEMIN = constants.ONEMIN;
 var ONESEC = constants.ONESEC;
 var EPOCHJD = constants.EPOCHJD;
 
-var Registry = require('../registry');
+var Registry = require("../registry");
 
 var utcFormat = d3.time.format.utc;
 
@@ -35,11 +32,10 @@ var DATETIME_REGEXP_CN = /^\s*(-?\d\d\d\d|\d\d)(-(\d?\di?)(-(\d?\d)([ Tt]([01]?\
 var YFIRST = new Date().getFullYear() - 70;
 
 function isWorldCalendar(calendar) {
-    return (
-        calendar &&
-        Registry.componentsRegistry.calendars &&
-        typeof calendar === 'string' && calendar !== 'gregorian'
-    );
+  return calendar &&
+    Registry.componentsRegistry.calendars &&
+    typeof calendar === "string" &&
+    calendar !== "gregorian";
 }
 
 /*
@@ -48,31 +44,29 @@ function isWorldCalendar(calendar) {
  * bool sunday is for week ticks, shift it to a Sunday.
  */
 exports.dateTick0 = function(calendar, sunday) {
-    if(isWorldCalendar(calendar)) {
-        return sunday ?
-            Registry.getComponentMethod('calendars', 'CANONICAL_SUNDAY')[calendar] :
-            Registry.getComponentMethod('calendars', 'CANONICAL_TICK')[calendar];
-    }
-    else {
-        return sunday ? '2000-01-02' : '2000-01-01';
-    }
+  if (isWorldCalendar(calendar)) {
+    return sunday
+      ? Registry.getComponentMethod("calendars", "CANONICAL_SUNDAY")[calendar]
+      : Registry.getComponentMethod("calendars", "CANONICAL_TICK")[calendar];
+  } else {
+    return sunday ? "2000-01-02" : "2000-01-01";
+  }
 };
 
 /*
  * dfltRange: for each calendar, give a valid default range
  */
 exports.dfltRange = function(calendar) {
-    if(isWorldCalendar(calendar)) {
-        return Registry.getComponentMethod('calendars', 'DFLTRANGE')[calendar];
-    }
-    else {
-        return ['2000-01-01', '2001-01-01'];
-    }
+  if (isWorldCalendar(calendar)) {
+    return Registry.getComponentMethod("calendars", "DFLTRANGE")[calendar];
+  } else {
+    return ["2000-01-01", "2001-01-01"];
+  }
 };
 
 // is an object a javascript date?
 exports.isJSDate = function(v) {
-    return typeof v === 'object' && v !== null && typeof v.getTime === 'function';
+  return typeof v === "object" && v !== null && typeof v.getTime === "function";
 };
 
 // The absolute limits of our date-time system
@@ -134,97 +128,107 @@ var MIN_MS, MAX_MS;
  *   1946-2045
  */
 exports.dateTime2ms = function(s, calendar) {
-    // first check if s is a date object
-    if(exports.isJSDate(s)) {
-        // Convert to the UTC milliseconds that give the same
-        // hours as this date has in the local timezone
-        s = Number(s) - s.getTimezoneOffset() * ONEMIN;
-        if(s >= MIN_MS && s <= MAX_MS) return s;
-        return BADNUM;
+  // first check if s is a date object
+  if (exports.isJSDate(s)) {
+    // Convert to the UTC milliseconds that give the same
+    // hours as this date has in the local timezone
+    s = Number(s) - s.getTimezoneOffset() * ONEMIN;
+    if (s >= MIN_MS && s <= MAX_MS) return s;
+    return BADNUM;
+  }
+  // otherwise only accept strings and numbers
+  if (typeof s !== "string" && typeof s !== "number") return BADNUM;
+
+  s = String(s);
+
+  var isWorld = isWorldCalendar(calendar);
+
+  // to handle out-of-range dates in international calendars, accept
+  // 'G' as a prefix to force the built-in gregorian calendar.
+  var s0 = s.charAt(0);
+  if (isWorld && (s0 === "G" || s0 === "g")) {
+    s = s.substr(1);
+    calendar = "";
+  }
+
+  var isChinese = isWorld && calendar.substr(0, 7) === "chinese";
+
+  var match = s.match(isChinese ? DATETIME_REGEXP_CN : DATETIME_REGEXP);
+  if (!match) return BADNUM;
+  var y = match[1],
+    m = match[3] || "1",
+    d = Number(match[5] || 1),
+    H = Number(match[7] || 0),
+    M = Number(match[9] || 0),
+    S = Number(match[11] || 0);
+
+  if (isWorld) {
+    // disallow 2-digit years for world calendars
+    if (y.length === 2) return BADNUM;
+    y = Number(y);
+
+    var cDate;
+    try {
+      var calInstance = Registry.getComponentMethod("calendars", "getCal")(
+        calendar
+      );
+      if (isChinese) {
+        var isIntercalary = m.charAt(m.length - 1) === "i";
+        m = parseInt(m, 10);
+        cDate = calInstance.newDate(
+          y,
+          calInstance.toMonthIndex(y, m, isIntercalary),
+          d
+        );
+      } else {
+        cDate = calInstance.newDate(y, Number(m), d);
+      }
+    } catch (e) {
+      return BADNUM;
     }
-    // otherwise only accept strings and numbers
-    if(typeof s !== 'string' && typeof s !== 'number') return BADNUM;
 
-    s = String(s);
+    // Invalid ... date
+    if (!cDate) return BADNUM;
 
-    var isWorld = isWorldCalendar(calendar);
+    return (cDate.toJD() - EPOCHJD) * ONEDAY +
+      H * ONEHOUR +
+      M * ONEMIN +
+      S * ONESEC;
+  }
 
-    // to handle out-of-range dates in international calendars, accept
-    // 'G' as a prefix to force the built-in gregorian calendar.
-    var s0 = s.charAt(0);
-    if(isWorld && (s0 === 'G' || s0 === 'g')) {
-        s = s.substr(1);
-        calendar = '';
-    }
-
-    var isChinese = isWorld && calendar.substr(0, 7) === 'chinese';
-
-    var match = s.match(isChinese ? DATETIME_REGEXP_CN : DATETIME_REGEXP);
-    if(!match) return BADNUM;
-    var y = match[1],
-        m = match[3] || '1',
-        d = Number(match[5] || 1),
-        H = Number(match[7] || 0),
-        M = Number(match[9] || 0),
-        S = Number(match[11] || 0);
-
-    if(isWorld) {
-        // disallow 2-digit years for world calendars
-        if(y.length === 2) return BADNUM;
-        y = Number(y);
-
-        var cDate;
-        try {
-            var calInstance = Registry.getComponentMethod('calendars', 'getCal')(calendar);
-            if(isChinese) {
-                var isIntercalary = m.charAt(m.length - 1) === 'i';
-                m = parseInt(m, 10);
-                cDate = calInstance.newDate(y, calInstance.toMonthIndex(y, m, isIntercalary), d);
-            }
-            else {
-                cDate = calInstance.newDate(y, Number(m), d);
-            }
-        }
-        catch(e) { return BADNUM; } // Invalid ... date
-
-        if(!cDate) return BADNUM;
-
-        return ((cDate.toJD() - EPOCHJD) * ONEDAY) +
-            (H * ONEHOUR) + (M * ONEMIN) + (S * ONESEC);
-    }
-
-    if(y.length === 2) {
-        y = (Number(y) + 2000 - YFIRST) % 100 + YFIRST;
-    }
-    else y = Number(y);
+  if (y.length === 2) {
+    y = (Number(y) + 2000 - YFIRST) % 100 + YFIRST;
+  } else {
+    y = Number(y);
+  }
 
-    // new Date uses months from 0; subtract 1 here just so we
-    // don't have to do it again during the validity test below
-    m -= 1;
+  // new Date uses months from 0; subtract 1 here just so we
+  // don't have to do it again during the validity test below
+  m -= 1;
 
-    // javascript takes new Date(0..99,m,d) to mean 1900-1999, so
-    // to support years 0-99 we need to use setFullYear explicitly
-    // Note that 2000 is a leap year.
-    var date = new Date(Date.UTC(2000, m, d, H, M));
-    date.setUTCFullYear(y);
+  // javascript takes new Date(0..99,m,d) to mean 1900-1999, so
+  // to support years 0-99 we need to use setFullYear explicitly
+  // Note that 2000 is a leap year.
+  var date = new Date(Date.UTC(2000, m, d, H, M));
+  date.setUTCFullYear(y);
 
-    if(date.getUTCMonth() !== m) return BADNUM;
-    if(date.getUTCDate() !== d) return BADNUM;
+  if (date.getUTCMonth() !== m) return BADNUM;
+  if (date.getUTCDate() !== d) return BADNUM;
 
-    return date.getTime() + S * ONESEC;
+  return date.getTime() + S * ONESEC;
 };
 
-MIN_MS = exports.MIN_MS = exports.dateTime2ms('-9999');
-MAX_MS = exports.MAX_MS = exports.dateTime2ms('9999-12-31 23:59:59.9999');
+MIN_MS = exports.MIN_MS = exports.dateTime2ms("-9999");
+MAX_MS = exports.MAX_MS = exports.dateTime2ms("9999-12-31 23:59:59.9999");
 
 // is string s a date? (see above)
 exports.isDateTime = function(s, calendar) {
-    return (exports.dateTime2ms(s, calendar) !== BADNUM);
+  return exports.dateTime2ms(s, calendar) !== BADNUM;
 };
 
 // pad a number with zeroes, to given # of digits before the decimal point
 function lpad(val, digits) {
-    return String(val + Math.pow(10, digits)).substr(1);
+  return String(val + Math.pow(10, digits)).substr(1);
 }
 
 /**
@@ -239,58 +243,65 @@ var NINETYDAYS = 90 * ONEDAY;
 var THREEHOURS = 3 * ONEHOUR;
 var FIVEMIN = 5 * ONEMIN;
 exports.ms2DateTime = function(ms, r, calendar) {
-    if(typeof ms !== 'number' || !(ms >= MIN_MS && ms <= MAX_MS)) return BADNUM;
-
-    if(!r) r = 0;
-
-    var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10),
-        msRounded = Math.round(ms - msecTenths / 10),
-        dateStr, h, m, s, msec10, d;
-
-    if(isWorldCalendar(calendar)) {
-        var dateJD = Math.floor(msRounded / ONEDAY) + EPOCHJD,
-            timeMs = Math.floor(mod(ms, ONEDAY));
-        try {
-            dateStr = Registry.getComponentMethod('calendars', 'getCal')(calendar)
-                .fromJD(dateJD).formatDate('yyyy-mm-dd');
-        }
-        catch(e) {
-            // invalid date in this calendar - fall back to Gyyyy-mm-dd
-            dateStr = utcFormat('G%Y-%m-%d')(new Date(msRounded));
-        }
-
-        // yyyy does NOT guarantee 4-digit years. YYYY mostly does, but does
-        // other things for a few calendars, so we can't trust it. Just pad
-        // it manually (after the '-' if there is one)
-        if(dateStr.charAt(0) === '-') {
-            while(dateStr.length < 11) dateStr = '-0' + dateStr.substr(1);
-        }
-        else {
-            while(dateStr.length < 10) dateStr = '0' + dateStr;
-        }
-
-        // TODO: if this is faster, we could use this block for extracting
-        // the time components of regular gregorian too
-        h = (r < NINETYDAYS) ? Math.floor(timeMs / ONEHOUR) : 0;
-        m = (r < NINETYDAYS) ? Math.floor((timeMs % ONEHOUR) / ONEMIN) : 0;
-        s = (r < THREEHOURS) ? Math.floor((timeMs % ONEMIN) / ONESEC) : 0;
-        msec10 = (r < FIVEMIN) ? (timeMs % ONESEC) * 10 + msecTenths : 0;
+  if (typeof ms !== "number" || !(ms >= MIN_MS && ms <= MAX_MS)) return BADNUM;
+
+  if (!r) r = 0;
+
+  var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10),
+    msRounded = Math.round(ms - msecTenths / 10),
+    dateStr,
+    h,
+    m,
+    s,
+    msec10,
+    d;
+
+  if (isWorldCalendar(calendar)) {
+    var dateJD = Math.floor(msRounded / ONEDAY) + EPOCHJD,
+      timeMs = Math.floor(mod(ms, ONEDAY));
+    try {
+      dateStr = Registry.getComponentMethod("calendars", "getCal")(calendar)
+        .fromJD(dateJD)
+        .formatDate("yyyy-mm-dd");
+    } catch (e) {
+      // invalid date in this calendar - fall back to Gyyyy-mm-dd
+      dateStr = utcFormat("G%Y-%m-%d")(new Date(msRounded));
     }
-    else {
-        d = new Date(msRounded);
-
-        dateStr = utcFormat('%Y-%m-%d')(d);
-
-        // <90 days: add hours and minutes - never *only* add hours
-        h = (r < NINETYDAYS) ? d.getUTCHours() : 0;
-        m = (r < NINETYDAYS) ? d.getUTCMinutes() : 0;
-        // <3 hours: add seconds
-        s = (r < THREEHOURS) ? d.getUTCSeconds() : 0;
-        // <5 minutes: add ms (plus one extra digit, this is msec*10)
-        msec10 = (r < FIVEMIN) ? d.getUTCMilliseconds() * 10 + msecTenths : 0;
+
+    // yyyy does NOT guarantee 4-digit years. YYYY mostly does, but does
+    // other things for a few calendars, so we can't trust it. Just pad
+    // it manually (after the '-' if there is one)
+    if (dateStr.charAt(0) === "-") {
+      while (dateStr.length < 11) {
+        dateStr = "-0" + dateStr.substr(1);
+      }
+    } else {
+      while (dateStr.length < 10) {
+        dateStr = "0" + dateStr;
+      }
     }
 
-    return includeTime(dateStr, h, m, s, msec10);
+    // TODO: if this is faster, we could use this block for extracting
+    // the time components of regular gregorian too
+    h = r < NINETYDAYS ? Math.floor(timeMs / ONEHOUR) : 0;
+    m = r < NINETYDAYS ? Math.floor(timeMs % ONEHOUR / ONEMIN) : 0;
+    s = r < THREEHOURS ? Math.floor(timeMs % ONEMIN / ONESEC) : 0;
+    msec10 = r < FIVEMIN ? timeMs % ONESEC * 10 + msecTenths : 0;
+  } else {
+    d = new Date(msRounded);
+
+    dateStr = utcFormat("%Y-%m-%d")(d);
+
+    // <90 days: add hours and minutes - never *only* add hours
+    h = r < NINETYDAYS ? d.getUTCHours() : 0;
+    m = r < NINETYDAYS ? d.getUTCMinutes() : 0;
+    // <3 hours: add seconds
+    s = r < THREEHOURS ? d.getUTCSeconds() : 0;
+    // <5 minutes: add ms (plus one extra digit, this is msec*10)
+    msec10 = r < FIVEMIN ? d.getUTCMilliseconds() * 10 + msecTenths : 0;
+  }
+
+  return includeTime(dateStr, h, m, s, msec10);
 };
 
 // For converting old-style milliseconds to date strings,
@@ -300,67 +311,68 @@ exports.ms2DateTime = function(ms, r, calendar) {
 // Clip one extra day off our date range though so we can't get
 // thrown beyond the range by the timezone shift.
 exports.ms2DateTimeLocal = function(ms) {
-    if(!(ms >= MIN_MS + ONEDAY && ms <= MAX_MS - ONEDAY)) return BADNUM;
+  if (!(ms >= MIN_MS + ONEDAY && ms <= MAX_MS - ONEDAY)) return BADNUM;
 
-    var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10),
-        d = new Date(Math.round(ms - msecTenths / 10)),
-        dateStr = d3.time.format('%Y-%m-%d')(d),
-        h = d.getHours(),
-        m = d.getMinutes(),
-        s = d.getSeconds(),
-        msec10 = d.getUTCMilliseconds() * 10 + msecTenths;
+  var msecTenths = Math.floor(mod(ms + 0.05, 1) * 10),
+    d = new Date(Math.round(ms - msecTenths / 10)),
+    dateStr = d3.time.format("%Y-%m-%d")(d),
+    h = d.getHours(),
+    m = d.getMinutes(),
+    s = d.getSeconds(),
+    msec10 = d.getUTCMilliseconds() * 10 + msecTenths;
 
-    return includeTime(dateStr, h, m, s, msec10);
+  return includeTime(dateStr, h, m, s, msec10);
 };
 
 function includeTime(dateStr, h, m, s, msec10) {
-    // include each part that has nonzero data in or after it
-    if(h || m || s || msec10) {
-        dateStr += ' ' + lpad(h, 2) + ':' + lpad(m, 2);
-        if(s || msec10) {
-            dateStr += ':' + lpad(s, 2);
-            if(msec10) {
-                var digits = 4;
-                while(msec10 % 10 === 0) {
-                    digits -= 1;
-                    msec10 /= 10;
-                }
-                dateStr += '.' + lpad(msec10, digits);
-            }
+  // include each part that has nonzero data in or after it
+  if (h || m || s || msec10) {
+    dateStr += " " + lpad(h, 2) + ":" + lpad(m, 2);
+    if (s || msec10) {
+      dateStr += ":" + lpad(s, 2);
+      if (msec10) {
+        var digits = 4;
+        while (msec10 % 10 === 0) {
+          digits -= 1;
+          msec10 /= 10;
         }
+        dateStr += "." + lpad(msec10, digits);
+      }
     }
-    return dateStr;
+  }
+  return dateStr;
 }
 
 // normalize date format to date string, in case it starts as
 // a Date object or milliseconds
 // optional dflt is the return value if cleaning fails
 exports.cleanDate = function(v, dflt, calendar) {
-    if(exports.isJSDate(v) || typeof v === 'number') {
-        // do not allow milliseconds (old) or jsdate objects (inherently
-        // described as gregorian dates) with world calendars
-        if(isWorldCalendar(calendar)) {
-            logError('JS Dates and milliseconds are incompatible with world calendars', v);
-            return dflt;
-        }
-
-        // NOTE: if someone puts in a year as a number rather than a string,
-        // this will mistakenly convert it thinking it's milliseconds from 1970
-        // that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds
-        v = exports.ms2DateTimeLocal(+v);
-        if(!v && dflt !== undefined) return dflt;
+  if (exports.isJSDate(v) || typeof v === "number") {
+    // do not allow milliseconds (old) or jsdate objects (inherently
+    // described as gregorian dates) with world calendars
+    if (isWorldCalendar(calendar)) {
+      logError(
+        "JS Dates and milliseconds are incompatible with world calendars",
+        v
+      );
+      return dflt;
     }
-    else if(!exports.isDateTime(v, calendar)) {
-        logError('unrecognized date', v);
-        return dflt;
-    }
-    return v;
+
+    // NOTE: if someone puts in a year as a number rather than a string,
+    // this will mistakenly convert it thinking it's milliseconds from 1970
+    // that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds
+    v = exports.ms2DateTimeLocal(+v);
+    if (!v && dflt !== undefined) return dflt;
+  } else if (!exports.isDateTime(v, calendar)) {
+    logError("unrecognized date", v);
+    return dflt;
+  }
+  return v;
 };
 
 /*
  *  Date formatting for ticks and hovertext
  */
-
 /*
  * modDateFormat: Support world calendars, and add one item to
  * d3's vocabulary:
@@ -368,26 +380,30 @@ exports.cleanDate = function(v, dflt, calendar) {
  */
 var fracMatch = /%\d?f/g;
 function modDateFormat(fmt, x, calendar) {
-
-    fmt = fmt.replace(fracMatch, function(match) {
-        var digits = Math.min(+(match.charAt(1)) || 6, 6),
-            fracSecs = ((x / 1000 % 1) + 2)
-                .toFixed(digits)
-                .substr(2).replace(/0+$/, '') || '0';
-        return fracSecs;
-    });
-
-    var d = new Date(Math.floor(x + 0.05));
-
-    if(isWorldCalendar(calendar)) {
-        try {
-            fmt = Registry.getComponentMethod('calendars', 'worldCalFmt')(fmt, x, calendar);
-        }
-        catch(e) {
-            return 'Invalid';
-        }
+  fmt = fmt.replace(fracMatch, function(match) {
+    var digits = Math.min(+match.charAt(1) || 6, 6),
+      fracSecs = (x / 1000 % 1 + 2)
+        .toFixed(digits)
+        .substr(2)
+        .replace(/0+$/, "") ||
+        "0";
+    return fracSecs;
+  });
+
+  var d = new Date(Math.floor(x + 0.05));
+
+  if (isWorldCalendar(calendar)) {
+    try {
+      fmt = Registry.getComponentMethod("calendars", "worldCalFmt")(
+        fmt,
+        x,
+        calendar
+      );
+    } catch (e) {
+      return "Invalid";
     }
-    return utcFormat(fmt)(d);
+  }
+  return utcFormat(fmt)(d);
 }
 
 /*
@@ -398,15 +414,17 @@ function modDateFormat(fmt, x, calendar) {
  */
 var MAXSECONDS = [59, 59.9, 59.99, 59.999, 59.9999];
 function formatTime(x, tr) {
-    var timePart = mod(x + 0.05, ONEDAY);
+  var timePart = mod(x + 0.05, ONEDAY);
 
-    var timeStr = lpad(Math.floor(timePart / ONEHOUR), 2) + ':' +
-        lpad(mod(Math.floor(timePart / ONEMIN), 60), 2);
+  var timeStr = lpad(Math.floor(timePart / ONEHOUR), 2) +
+    ":" +
+    lpad(mod(Math.floor(timePart / ONEMIN), 60), 2);
 
-    if(tr !== 'M') {
-        if(!isNumeric(tr)) tr = 0; // should only be 'S'
+  if (tr !== "M") {
+    if (!isNumeric(tr)) tr = 0;
 
-        /*
+    // should only be 'S'
+    /*
          * this is a weird one - and shouldn't come up unless people
          * monkey with tick0 in weird ways, but we need to do something!
          * IN PARTICULAR we had better not display garbage (see below)
@@ -421,27 +439,35 @@ function formatTime(x, tr) {
          * say we round seconds but floor everything else. BUT that means
          * we need to never round up to 60 seconds, ie 23:59:60
          */
-        var sec = Math.min(mod(x / ONESEC, 60), MAXSECONDS[tr]);
-
-        var secStr = (100 + sec).toFixed(tr).substr(1);
-        if(tr > 0) {
-            secStr = secStr.replace(/0+$/, '').replace(/[\.]$/, '');
-        }
+    var sec = Math.min(mod(x / ONESEC, 60), MAXSECONDS[tr]);
 
-        timeStr += ':' + secStr;
+    var secStr = (100 + sec).toFixed(tr).substr(1);
+    if (tr > 0) {
+      secStr = secStr.replace(/0+$/, "").replace(/[\.]$/, "");
     }
-    return timeStr;
+
+    timeStr += ":" + secStr;
+  }
+  return timeStr;
 }
 
-var yearFormat = utcFormat('%Y'),
-    monthFormat = utcFormat('%b %Y'),
-    dayFormat = utcFormat('%b %-d'),
-    yearMonthDayFormat = utcFormat('%b %-d, %Y');
+var yearFormat = utcFormat("%Y"),
+  monthFormat = utcFormat("%b %Y"),
+  dayFormat = utcFormat("%b %-d"),
+  yearMonthDayFormat = utcFormat("%b %-d, %Y");
 
-function yearFormatWorld(cDate) { return cDate.formatDate('yyyy'); }
-function monthFormatWorld(cDate) { return cDate.formatDate('M yyyy'); }
-function dayFormatWorld(cDate) { return cDate.formatDate('M d'); }
-function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy'); }
+function yearFormatWorld(cDate) {
+  return cDate.formatDate("yyyy");
+}
+function monthFormatWorld(cDate) {
+  return cDate.formatDate("M yyyy");
+}
+function dayFormatWorld(cDate) {
+  return cDate.formatDate("M d");
+}
+function yearMonthDayFormatWorld(cDate) {
+  return cDate.formatDate("M d, yyyy");
+}
 
 /*
  * formatDate: turn a date into tick or hover label text.
@@ -459,48 +485,50 @@ function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy');
  * one tick to the next (as it does with automatic formatting)
  */
 exports.formatDate = function(x, fmt, tr, calendar) {
-    var headStr,
-        dateStr;
-
-    calendar = isWorldCalendar(calendar) && calendar;
-
-    if(fmt) return modDateFormat(fmt, x, calendar);
-
-    if(calendar) {
-        try {
-            var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD,
-                cDate = Registry.getComponentMethod('calendars', 'getCal')(calendar)
-                    .fromJD(dateJD);
-
-            if(tr === 'y') dateStr = yearFormatWorld(cDate);
-            else if(tr === 'm') dateStr = monthFormatWorld(cDate);
-            else if(tr === 'd') {
-                headStr = yearFormatWorld(cDate);
-                dateStr = dayFormatWorld(cDate);
-            }
-            else {
-                headStr = yearMonthDayFormatWorld(cDate);
-                dateStr = formatTime(x, tr);
-            }
-        }
-        catch(e) { return 'Invalid'; }
+  var headStr, dateStr;
+
+  calendar = isWorldCalendar(calendar) && calendar;
+
+  if (fmt) return modDateFormat(fmt, x, calendar);
+
+  if (calendar) {
+    try {
+      var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD,
+        cDate = Registry.getComponentMethod("calendars", "getCal")(
+          calendar
+        ).fromJD(dateJD);
+
+      if (tr === "y") {
+        dateStr = yearFormatWorld(cDate);
+      } else if (tr === "m") {
+        dateStr = monthFormatWorld(cDate);
+      } else if (tr === "d") {
+        headStr = yearFormatWorld(cDate);
+        dateStr = dayFormatWorld(cDate);
+      } else {
+        headStr = yearMonthDayFormatWorld(cDate);
+        dateStr = formatTime(x, tr);
+      }
+    } catch (e) {
+      return "Invalid";
     }
-    else {
-        var d = new Date(Math.floor(x + 0.05));
-
-        if(tr === 'y') dateStr = yearFormat(d);
-        else if(tr === 'm') dateStr = monthFormat(d);
-        else if(tr === 'd') {
-            headStr = yearFormat(d);
-            dateStr = dayFormat(d);
-        }
-        else {
-            headStr = yearMonthDayFormat(d);
-            dateStr = formatTime(x, tr);
-        }
+  } else {
+    var d = new Date(Math.floor(x + 0.05));
+
+    if (tr === "y") {
+      dateStr = yearFormat(d);
+    } else if (tr === "m") {
+      dateStr = monthFormat(d);
+    } else if (tr === "d") {
+      headStr = yearFormat(d);
+      dateStr = dayFormat(d);
+    } else {
+      headStr = yearMonthDayFormat(d);
+      dateStr = formatTime(x, tr);
     }
+  }
 
-    return dateStr + (headStr ? '\n' + headStr : '');
+  return dateStr + (headStr ? "\n" + headStr : "");
 };
 
 /*
@@ -531,33 +559,34 @@ exports.formatDate = function(x, fmt, tr, calendar) {
  */
 var THREEDAYS = 3 * ONEDAY;
 exports.incrementMonth = function(ms, dMonth, calendar) {
-    calendar = isWorldCalendar(calendar) && calendar;
-
-    // pull time out and operate on pure dates, then add time back at the end
-    // this gives maximum precision - not that we *normally* care if we're
-    // incrementing by month, but better to be safe!
-    var timeMs = mod(ms, ONEDAY);
-    ms = Math.round(ms - timeMs);
-
-    if(calendar) {
-        try {
-            var dateJD = Math.round(ms / ONEDAY) + EPOCHJD,
-                calInstance = Registry.getComponentMethod('calendars', 'getCal')(calendar),
-                cDate = calInstance.fromJD(dateJD);
-
-            if(dMonth % 12) calInstance.add(cDate, dMonth, 'm');
-            else calInstance.add(cDate, dMonth / 12, 'y');
-
-            return (cDate.toJD() - EPOCHJD) * ONEDAY + timeMs;
-        }
-        catch(e) {
-            logError('invalid ms ' + ms + ' in calendar ' + calendar);
-            // then keep going in gregorian even though the result will be 'Invalid'
-        }
+  calendar = isWorldCalendar(calendar) && calendar;
+
+  // pull time out and operate on pure dates, then add time back at the end
+  // this gives maximum precision - not that we *normally* care if we're
+  // incrementing by month, but better to be safe!
+  var timeMs = mod(ms, ONEDAY);
+  ms = Math.round(ms - timeMs);
+
+  if (calendar) {
+    try {
+      var dateJD = Math.round(ms / ONEDAY) + EPOCHJD,
+        calInstance = Registry.getComponentMethod("calendars", "getCal")(
+          calendar
+        ),
+        cDate = calInstance.fromJD(dateJD);
+
+      if (dMonth % 12) calInstance.add(cDate, dMonth, "m");
+      else calInstance.add(cDate, dMonth / 12, "y");
+
+      return (cDate.toJD() - EPOCHJD) * ONEDAY + timeMs;
+    } catch (e) {
+      logError("invalid ms " + ms + " in calendar " + calendar);
+      // then keep going in gregorian even though the result will be 'Invalid'
     }
+  }
 
-    var y = new Date(ms + THREEDAYS);
-    return y.setUTCMonth(y.getUTCMonth() + dMonth) + timeMs - THREEDAYS;
+  var y = new Date(ms + THREEDAYS);
+  return y.setUTCMonth(y.getUTCMonth() + dMonth) + timeMs - THREEDAYS;
 };
 
 /*
@@ -567,60 +596,52 @@ exports.incrementMonth = function(ms, dMonth, calendar) {
  * calendar (string) the calendar to test against
  */
 exports.findExactDates = function(data, calendar) {
-    var exactYears = 0,
-        exactMonths = 0,
-        exactDays = 0,
-        blankCount = 0,
-        d,
-        di;
-
-    var calInstance = (
-        isWorldCalendar(calendar) &&
-        Registry.getComponentMethod('calendars', 'getCal')(calendar)
-    );
-
-    for(var i = 0; i < data.length; i++) {
-        di = data[i];
-
-        // not date data at all
-        if(!isNumeric(di)) {
-            blankCount ++;
-            continue;
-        }
+  var exactYears = 0, exactMonths = 0, exactDays = 0, blankCount = 0, d, di;
 
-        // not an exact date
-        if(di % ONEDAY) continue;
-
-        if(calInstance) {
-            try {
-                d = calInstance.fromJD(di / ONEDAY + EPOCHJD);
-                if(d.day() === 1) {
-                    if(d.month() === 1) exactYears++;
-                    else exactMonths++;
-                }
-                else exactDays++;
-            }
-            catch(e) {
-                // invalid date in this calendar - ignore it here.
-            }
-        }
-        else {
-            d = new Date(di);
-            if(d.getUTCDate() === 1) {
-                if(d.getUTCMonth() === 0) exactYears++;
-                else exactMonths++;
-            }
-            else exactDays++;
+  var calInstance = isWorldCalendar(calendar) &&
+    Registry.getComponentMethod("calendars", "getCal")(calendar);
+
+  for (var i = 0; i < data.length; i++) {
+    di = data[i];
+
+    // not date data at all
+    if (!isNumeric(di)) {
+      blankCount++;
+      continue;
+    }
+
+    // not an exact date
+    if (di % ONEDAY) continue;
+
+    if (calInstance) {
+      try {
+        d = calInstance.fromJD(di / ONEDAY + EPOCHJD);
+        if (d.day() === 1) {
+          if (d.month() === 1) exactYears++;
+          else exactMonths++;
+        } else {
+          exactDays++;
         }
+      } catch (e) {
+      }
+    } else {
+      d = new Date(di);
+      if (d.getUTCDate() === 1) {
+        if (d.getUTCMonth() === 0) exactYears++;
+        else exactMonths++;
+      } else {
+        exactDays++;
+      }
     }
-    exactMonths += exactYears;
-    exactDays += exactMonths;
+  }
+  exactMonths += exactYears;
+  exactDays += exactMonths;
 
-    var dataCount = data.length - blankCount;
+  var dataCount = data.length - blankCount;
 
-    return {
-        exactYears: exactYears / dataCount,
-        exactMonths: exactMonths / dataCount,
-        exactDays: exactDays / dataCount
-    };
+  return {
+    exactYears: exactYears / dataCount,
+    exactMonths: exactMonths / dataCount,
+    exactDays: exactDays / dataCount
+  };
 };
diff --git a/src/lib/events.js b/src/lib/events.js
index 8238384242a..6648ea2a76f 100644
--- a/src/lib/events.js
+++ b/src/lib/events.js
@@ -5,35 +5,30 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 /* global jQuery:false */
 
-var EventEmitter = require('events').EventEmitter;
+var EventEmitter = require("events").EventEmitter;
 
 var Events = {
-
-    init: function(plotObj) {
-
-        /*
+  init: function(plotObj) {
+    /*
          * If we have already instantiated an emitter for this plot
          * return early.
          */
-        if(plotObj._ev instanceof EventEmitter) return plotObj;
+    if (plotObj._ev instanceof EventEmitter) return plotObj;
 
-        var ev = new EventEmitter();
-        var internalEv = new EventEmitter();
+    var ev = new EventEmitter();
+    var internalEv = new EventEmitter();
 
-        /*
+    /*
          * Assign to plot._ev while we still live in a land
          * where plot is a DOM element with stuff attached to it.
          * In the future we can make plot the event emitter itself.
          */
-        plotObj._ev = ev;
+    plotObj._ev = ev;
 
-        /*
+    /*
          * Create a second event handler that will manage events *internally*.
          * This allows parts of plotly to respond to thing like relayout without
          * having to use the user-facing event handler. They cannot peacefully
@@ -41,9 +36,9 @@ var Events = {
          * plotObj.removeAllListeners() would detach internal events, breaking
          * plotly.
          */
-        plotObj._internalEv = internalEv;
+    plotObj._internalEv = internalEv;
 
-        /*
+    /*
          * Assign bound methods from the ev to the plot object. These methods
          * will reference the 'this' of plot._ev even though they are methods
          * of plot. This will keep the event machinery away from the plot object
@@ -52,39 +47,42 @@ var Events = {
          * methods have been bound to `plot` as some do not currently add value to
          * the Plotly event API.
          */
-        plotObj.on = ev.on.bind(ev);
-        plotObj.once = ev.once.bind(ev);
-        plotObj.removeListener = ev.removeListener.bind(ev);
-        plotObj.removeAllListeners = ev.removeAllListeners.bind(ev);
+    plotObj.on = ev.on.bind(ev);
+    plotObj.once = ev.once.bind(ev);
+    plotObj.removeListener = ev.removeListener.bind(ev);
+    plotObj.removeAllListeners = ev.removeAllListeners.bind(ev);
 
-        /*
+    /*
          * Create funtions for managing internal events. These are *only* triggered
          * by the mirroring of external events via the emit function.
          */
-        plotObj._internalOn = internalEv.on.bind(internalEv);
-        plotObj._internalOnce = internalEv.once.bind(internalEv);
-        plotObj._removeInternalListener = internalEv.removeListener.bind(internalEv);
-        plotObj._removeAllInternalListeners = internalEv.removeAllListeners.bind(internalEv);
+    plotObj._internalOn = internalEv.on.bind(internalEv);
+    plotObj._internalOnce = internalEv.once.bind(internalEv);
+    plotObj._removeInternalListener = internalEv.removeListener.bind(
+      internalEv
+    );
+    plotObj._removeAllInternalListeners = internalEv.removeAllListeners.bind(
+      internalEv
+    );
 
-        /*
+    /*
          * We must wrap emit to continue to support JQuery events. The idea
          * is to check to see if the user is using JQuery events, if they are
          * we emit JQuery events to trigger user handlers as well as the EventEmitter
          * events.
          */
-        plotObj.emit = function(event, data) {
-            if(typeof jQuery !== 'undefined') {
-                jQuery(plotObj).trigger(event, data);
-            }
-
-            ev.emit(event, data);
-            internalEv.emit(event, data);
-        };
-
-        return plotObj;
-    },
-
-    /*
+    plotObj.emit = function(event, data) {
+      if (typeof jQuery !== "undefined") {
+        jQuery(plotObj).trigger(event, data);
+      }
+
+      ev.emit(event, data);
+      internalEv.emit(event, data);
+    };
+
+    return plotObj;
+  },
+  /*
      * This function behaves like jQueries triggerHandler. It calls
      * all handlers for a particular event and returns the return value
      * of the LAST handler. This function also triggers jQuery's
@@ -94,71 +92,70 @@ var Events = {
      * so the additional behavior of triggerHandler triggering internal events
      * is deliberate excluded in order to avoid reinforcing more usage.
      */
-    triggerHandler: function(plotObj, event, data) {
-        var jQueryHandlerValue;
-        var nodeEventHandlerValue;
-        /*
+  triggerHandler: function(plotObj, event, data) {
+    var jQueryHandlerValue;
+    var nodeEventHandlerValue;
+    /*
          * If Jquery exists run all its handlers for this event and
          * collect the return value of the LAST handler function
          */
-        if(typeof jQuery !== 'undefined') {
-            jQueryHandlerValue = jQuery(plotObj).triggerHandler(event, data);
-        }
+    if (typeof jQuery !== "undefined") {
+      jQueryHandlerValue = jQuery(plotObj).triggerHandler(event, data);
+    }
 
-        /*
+    /*
          * Now run all the node style event handlers
          */
-        var ev = plotObj._ev;
-        if(!ev) return jQueryHandlerValue;
+    var ev = plotObj._ev;
+    if (!ev) return jQueryHandlerValue;
 
-        var handlers = ev._events[event];
-        if(!handlers) return jQueryHandlerValue;
+    var handlers = ev._events[event];
+    if (!handlers) return jQueryHandlerValue;
 
-        /*
+    /*
          * handlers can be function or an array of functions
          */
-        if(typeof handlers === 'function') handlers = [handlers];
-        var lastHandler = handlers.pop();
+    if (typeof handlers === "function") handlers = [handlers];
+    var lastHandler = handlers.pop();
 
-        /*
+    /*
          * Call all the handlers except the last one.
          */
-        for(var i = 0; i < handlers.length; i++) {
-            handlers[i](data);
-        }
+    for (var i = 0; i < handlers.length; i++) {
+      handlers[i](data);
+    }
 
-        /*
+    /*
          * Now call the final handler and collect its value
          */
-        nodeEventHandlerValue = lastHandler(data);
+    nodeEventHandlerValue = lastHandler(data);
 
-        /*
+    /*
          * Return either the jquery handler value if it exists or the
          * nodeEventHandler value. Jquery event value superceeds nodejs
          * events for backwards compatability reasons.
          */
-        return jQueryHandlerValue !== undefined ? jQueryHandlerValue :
-            nodeEventHandlerValue;
-    },
-
-    purge: function(plotObj) {
-        delete plotObj._ev;
-        delete plotObj.on;
-        delete plotObj.once;
-        delete plotObj.removeListener;
-        delete plotObj.removeAllListeners;
-        delete plotObj.emit;
-
-        delete plotObj._ev;
-        delete plotObj._internalEv;
-        delete plotObj._internalOn;
-        delete plotObj._internalOnce;
-        delete plotObj._removeInternalListener;
-        delete plotObj._removeAllInternalListeners;
-
-        return plotObj;
-    }
-
+    return jQueryHandlerValue !== undefined
+      ? jQueryHandlerValue
+      : nodeEventHandlerValue;
+  },
+  purge: function(plotObj) {
+    delete plotObj._ev;
+    delete plotObj.on;
+    delete plotObj.once;
+    delete plotObj.removeListener;
+    delete plotObj.removeAllListeners;
+    delete plotObj.emit;
+
+    delete plotObj._ev;
+    delete plotObj._internalEv;
+    delete plotObj._internalOn;
+    delete plotObj._internalOnce;
+    delete plotObj._removeInternalListener;
+    delete plotObj._removeAllInternalListeners;
+
+    return plotObj;
+  }
 };
 
 module.exports = Events;
diff --git a/src/lib/extend.js b/src/lib/extend.js
index b0591778b64..06f1b77e2d2 100644
--- a/src/lib/extend.js
+++ b/src/lib/extend.js
@@ -5,41 +5,38 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var isPlainObject = require('./is_plain_object.js');
+"use strict";
+var isPlainObject = require("./is_plain_object.js");
 var isArray = Array.isArray;
 
 function primitivesLoopSplice(source, target) {
-    var i, value;
-    for(i = 0; i < source.length; i++) {
-        value = source[i];
-        if(value !== null && typeof(value) === 'object') {
-            return false;
-        }
-        if(value !== void(0)) {
-            target[i] = value;
-        }
+  var i, value;
+  for (i = 0; i < source.length; i++) {
+    value = source[i];
+    if (value !== null && typeof value === "object") {
+      return false;
+    }
+    if (value !== void 0) {
+      target[i] = value;
     }
-    return true;
+  }
+  return true;
 }
 
 exports.extendFlat = function() {
-    return _extend(arguments, false, false, false);
+  return _extend(arguments, false, false, false);
 };
 
 exports.extendDeep = function() {
-    return _extend(arguments, true, false, false);
+  return _extend(arguments, true, false, false);
 };
 
 exports.extendDeepAll = function() {
-    return _extend(arguments, true, true, false);
+  return _extend(arguments, true, true, false);
 };
 
 exports.extendDeepNoArrays = function() {
-    return _extend(arguments, true, false, true);
+  return _extend(arguments, true, false, true);
 };
 
 /*
@@ -60,53 +57,56 @@ exports.extendDeepNoArrays = function() {
  *
  */
 function _extend(inputs, isDeep, keepAllKeys, noArrayCopies) {
-    var target = inputs[0],
-        length = inputs.length;
+  var target = inputs[0], length = inputs.length;
 
-    var input, key, src, copy, copyIsArray, clone, allPrimitives;
+  var input, key, src, copy, copyIsArray, clone, allPrimitives;
 
-    if(length === 2 && isArray(target) && isArray(inputs[1]) && target.length === 0) {
+  if (
+    length === 2 && isArray(target) && isArray(inputs[1]) && target.length === 0
+  ) {
+    allPrimitives = primitivesLoopSplice(inputs[1], target);
 
-        allPrimitives = primitivesLoopSplice(inputs[1], target);
-
-        if(allPrimitives) {
-            return target;
+    if (allPrimitives) {
+      return target;
+    } else {
+      target.splice(0, target.length); // reset target and continue to next block
+    }
+  }
+
+  for (var i = 1; i < length; i++) {
+    input = inputs[i];
+
+    for (key in input) {
+      src = target[key];
+      copy = input[key];
+
+      // Stop early and just transfer the array if array copies are disallowed:
+      if (noArrayCopies && isArray(copy)) {
+        target[key] = copy;
+      } else if (
+        isDeep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))
+      ) {
+        // recurse if we're merging plain objects or arrays
+        if (copyIsArray) {
+          copyIsArray = false;
+          clone = src && isArray(src) ? src : [];
         } else {
-            target.splice(0, target.length); // reset target and continue to next block
+          clone = src && isPlainObject(src) ? src : {};
         }
-    }
-
-    for(var i = 1; i < length; i++) {
-        input = inputs[i];
-
-        for(key in input) {
-            src = target[key];
-            copy = input[key];
-
-            // Stop early and just transfer the array if array copies are disallowed:
-            if(noArrayCopies && isArray(copy)) {
-                target[key] = copy;
-            }
 
-            // recurse if we're merging plain objects or arrays
-            else if(isDeep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) {
-                if(copyIsArray) {
-                    copyIsArray = false;
-                    clone = src && isArray(src) ? src : [];
-                } else {
-                    clone = src && isPlainObject(src) ? src : {};
-                }
-
-                // never move original objects, clone them
-                target[key] = _extend([clone, copy], isDeep, keepAllKeys, noArrayCopies);
-            }
-
-            // don't bring in undefined values, except for extendDeepAll
-            else if(typeof copy !== 'undefined' || keepAllKeys) {
-                target[key] = copy;
-            }
-        }
+        // never move original objects, clone them
+        target[key] = _extend(
+          [clone, copy],
+          isDeep,
+          keepAllKeys,
+          noArrayCopies
+        );
+      } else if (typeof copy !== "undefined" || keepAllKeys) {
+        // don't bring in undefined values, except for extendDeepAll
+        target[key] = copy;
+      }
     }
+  }
 
-    return target;
+  return target;
 }
diff --git a/src/lib/filter_unique.js b/src/lib/filter_unique.js
index 5d035707696..927fa0fb7bc 100644
--- a/src/lib/filter_unique.js
+++ b/src/lib/filter_unique.js
@@ -5,11 +5,7 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-
+"use strict";
 /**
  * Return news array containing only the unique items
  * found in input array.
@@ -32,18 +28,16 @@
  * @return {array} new filtered array
  */
 module.exports = function filterUnique(array) {
-    var seen = {},
-        out = [],
-        j = 0;
+  var seen = {}, out = [], j = 0;
 
-    for(var i = 0; i < array.length; i++) {
-        var item = array[i];
+  for (var i = 0; i < array.length; i++) {
+    var item = array[i];
 
-        if(seen[item] !== 1) {
-            seen[item] = 1;
-            out[j++] = item;
-        }
+    if (seen[item] !== 1) {
+      seen[item] = 1;
+      out[j++] = item;
     }
+  }
 
-    return out;
+  return out;
 };
diff --git a/src/lib/filter_visible.js b/src/lib/filter_visible.js
index fdcf6674de3..3bde55405af 100644
--- a/src/lib/filter_visible.js
+++ b/src/lib/filter_visible.js
@@ -5,10 +5,7 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 /** Filter out object items with visible !== true
  *  insider array container.
  *
@@ -17,13 +14,13 @@
  *
  */
 module.exports = function filterVisible(container) {
-    var out = [];
+  var out = [];
 
-    for(var i = 0; i < container.length; i++) {
-        var item = container[i];
+  for (var i = 0; i < container.length; i++) {
+    var item = container[i];
 
-        if(item.visible === true) out.push(item);
-    }
+    if (item.visible === true) out.push(item);
+  }
 
-    return out;
+  return out;
 };
diff --git a/src/lib/geo_location_utils.js b/src/lib/geo_location_utils.js
index 30795820c37..c12e288f5de 100644
--- a/src/lib/geo_location_utils.js
+++ b/src/lib/geo_location_utils.js
@@ -5,56 +5,54 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var countryRegex = require('country-regex');
-var Lib = require('../lib');
-
+"use strict";
+var countryRegex = require("country-regex");
+var Lib = require("../lib");
 
 // make list of all country iso3 ids from at runtime
 var countryIds = Object.keys(countryRegex);
 
 var locationmodeToIdFinder = {
-    'ISO-3': Lib.identity,
-    'USA-states': Lib.identity,
-    'country names': countryNameToISO3
+  "ISO-3": Lib.identity,
+  "USA-states": Lib.identity,
+  "country names": countryNameToISO3
 };
 
 exports.locationToFeature = function(locationmode, location, features) {
-    var locationId = getLocationId(locationmode, location);
+  var locationId = getLocationId(locationmode, location);
 
-    if(locationId) {
-        for(var i = 0; i < features.length; i++) {
-            var feature = features[i];
+  if (locationId) {
+    for (var i = 0; i < features.length; i++) {
+      var feature = features[i];
 
-            if(feature.id === locationId) return feature;
-        }
-
-        Lib.warn([
-            'Location with id', locationId,
-            'does not have a matching topojson feature at this resolution.'
-        ].join(' '));
+      if (feature.id === locationId) return feature;
     }
 
-    return false;
+    Lib.warn(
+      [
+        "Location with id",
+        locationId,
+        "does not have a matching topojson feature at this resolution."
+      ].join(" ")
+    );
+  }
+
+  return false;
 };
 
 function getLocationId(locationmode, location) {
-    var idFinder = locationmodeToIdFinder[locationmode];
-    return idFinder(location);
+  var idFinder = locationmodeToIdFinder[locationmode];
+  return idFinder(location);
 }
 
 function countryNameToISO3(countryName) {
-    for(var i = 0; i < countryIds.length; i++) {
-        var iso3 = countryIds[i],
-            regex = new RegExp(countryRegex[iso3]);
+  for (var i = 0; i < countryIds.length; i++) {
+    var iso3 = countryIds[i], regex = new RegExp(countryRegex[iso3]);
 
-        if(regex.test(countryName.toLowerCase())) return iso3;
-    }
+    if (regex.test(countryName.toLowerCase())) return iso3;
+  }
 
-    Lib.warn('Unrecognized country name: ' + countryName + '.');
+  Lib.warn("Unrecognized country name: " + countryName + ".");
 
-    return false;
+  return false;
 }
diff --git a/src/lib/geojson_utils.js b/src/lib/geojson_utils.js
index 919472f4b0d..b26e74acbab 100644
--- a/src/lib/geojson_utils.js
+++ b/src/lib/geojson_utils.js
@@ -5,10 +5,7 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 /**
  * Convert calcTrace to GeoJSON 'MultiLineString' coordinate arrays
  *
@@ -21,29 +18,26 @@
  *
  */
 exports.calcTraceToLineCoords = function(calcTrace) {
-    var trace = calcTrace[0].trace,
-        connectgaps = trace.connectgaps;
+  var trace = calcTrace[0].trace, connectgaps = trace.connectgaps;
 
-    var coords = [],
-        lineString = [];
+  var coords = [], lineString = [];
 
-    for(var i = 0; i < calcTrace.length; i++) {
-        var calcPt = calcTrace[i];
+  for (var i = 0; i < calcTrace.length; i++) {
+    var calcPt = calcTrace[i];
 
-        lineString.push(calcPt.lonlat);
+    lineString.push(calcPt.lonlat);
 
-        if(!connectgaps && calcPt.gapAfter && lineString.length > 0) {
-            coords.push(lineString);
-            lineString = [];
-        }
+    if (!connectgaps && calcPt.gapAfter && lineString.length > 0) {
+      coords.push(lineString);
+      lineString = [];
     }
+  }
 
-    coords.push(lineString);
+  coords.push(lineString);
 
-    return coords;
+  return coords;
 };
 
-
 /**
  * Make line ('LineString' or 'MultiLineString') GeoJSON
  *
@@ -57,24 +51,17 @@ exports.calcTraceToLineCoords = function(calcTrace) {
  *
  */
 exports.makeLine = function(coords, trace) {
-    var out = {};
+  var out = {};
 
-    if(coords.length === 1) {
-        out = {
-            type: 'LineString',
-            coordinates: coords[0]
-        };
-    }
-    else {
-        out = {
-            type: 'MultiLineString',
-            coordinates: coords
-        };
-    }
+  if (coords.length === 1) {
+    out = { type: "LineString", coordinates: coords[0] };
+  } else {
+    out = { type: "MultiLineString", coordinates: coords };
+  }
 
-    if(trace) out.trace = trace;
+  if (trace) out.trace = trace;
 
-    return out;
+  return out;
 };
 
 /**
@@ -89,30 +76,23 @@ exports.makeLine = function(coords, trace) {
  *  GeoJSON object
  */
 exports.makePolygon = function(coords, trace) {
-    var out = {};
-
-    if(coords.length === 1) {
-        out = {
-            type: 'Polygon',
-            coordinates: coords
-        };
-    }
-    else {
-        var _coords = new Array(coords.length);
+  var out = {};
 
-        for(var i = 0; i < coords.length; i++) {
-            _coords[i] = [coords[i]];
-        }
+  if (coords.length === 1) {
+    out = { type: "Polygon", coordinates: coords };
+  } else {
+    var _coords = new Array(coords.length);
 
-        out = {
-            type: 'MultiPolygon',
-            coordinates: _coords
-        };
+    for (var i = 0; i < coords.length; i++) {
+      _coords[i] = [coords[i]];
     }
 
-    if(trace) out.trace = trace;
+    out = { type: "MultiPolygon", coordinates: _coords };
+  }
+
+  if (trace) out.trace = trace;
 
-    return out;
+  return out;
 };
 
 /**
@@ -123,8 +103,5 @@ exports.makePolygon = function(coords, trace) {
  *
  */
 exports.makeBlank = function() {
-    return {
-        type: 'Point',
-        coordinates: []
-    };
+  return { type: "Point", coordinates: [] };
 };
diff --git a/src/lib/gl_format_color.js b/src/lib/gl_format_color.js
index ac3f08aeb3e..0804799232d 100644
--- a/src/lib/gl_format_color.js
+++ b/src/lib/gl_format_color.js
@@ -5,77 +5,78 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var tinycolor = require("tinycolor2");
+var isNumeric = require("fast-isnumeric");
 
+var Colorscale = require("../components/colorscale");
+var colorDflt = require("../components/color/attributes").defaultLine;
 
-'use strict';
-
-var tinycolor = require('tinycolor2');
-var isNumeric = require('fast-isnumeric');
-
-var Colorscale = require('../components/colorscale');
-var colorDflt = require('../components/color/attributes').defaultLine;
-
-var str2RgbaArray = require('./str2rgbarray');
+var str2RgbaArray = require("./str2rgbarray");
 
 var opacityDflt = 1;
 
 function calculateColor(colorIn, opacityIn) {
-    var colorOut = str2RgbaArray(colorIn);
-    colorOut[3] *= opacityIn;
-    return colorOut;
+  var colorOut = str2RgbaArray(colorIn);
+  colorOut[3] *= opacityIn;
+  return colorOut;
 }
 
 function validateColor(colorIn) {
-    return tinycolor(colorIn).isValid() ? colorIn : colorDflt;
+  return tinycolor(colorIn).isValid() ? colorIn : colorDflt;
 }
 
 function validateOpacity(opacityIn) {
-    return isNumeric(opacityIn) ? opacityIn : opacityDflt;
+  return isNumeric(opacityIn) ? opacityIn : opacityDflt;
 }
 
 function formatColor(containerIn, opacityIn, len) {
-    var colorIn = containerIn.color,
-        isArrayColorIn = Array.isArray(colorIn),
-        isArrayOpacityIn = Array.isArray(opacityIn),
-        colorOut = [];
-
-    var sclFunc, getColor, getOpacity, colori, opacityi;
-
-    if(containerIn.colorscale !== undefined) {
-        sclFunc = Colorscale.makeColorScaleFunc(
-            Colorscale.extractScale(
-                containerIn.colorscale,
-                containerIn.cmin,
-                containerIn.cmax
-            )
-        );
-    }
-    else sclFunc = validateColor;
-
-    if(isArrayColorIn) {
-        getColor = function(c, i) {
-            return c[i] === undefined ? colorDflt : sclFunc(c[i]);
-        };
-    }
-    else getColor = validateColor;
-
-    if(isArrayOpacityIn) {
-        getOpacity = function(o, i) {
-            return o[i] === undefined ? opacityDflt : validateOpacity(o[i]);
-        };
-    }
-    else getOpacity = validateOpacity;
-
-    if(isArrayColorIn || isArrayOpacityIn) {
-        for(var i = 0; i < len; i++) {
-            colori = getColor(colorIn, i);
-            opacityi = getOpacity(opacityIn, i);
-            colorOut[i] = calculateColor(colori, opacityi);
-        }
+  var colorIn = containerIn.color,
+    isArrayColorIn = Array.isArray(colorIn),
+    isArrayOpacityIn = Array.isArray(opacityIn),
+    colorOut = [];
+
+  var sclFunc, getColor, getOpacity, colori, opacityi;
+
+  if (containerIn.colorscale !== undefined) {
+    sclFunc = Colorscale.makeColorScaleFunc(
+      Colorscale.extractScale(
+        containerIn.colorscale,
+        containerIn.cmin,
+        containerIn.cmax
+      )
+    );
+  } else {
+    sclFunc = validateColor;
+  }
+
+  if (isArrayColorIn) {
+    getColor = function(c, i) {
+      return c[i] === undefined ? colorDflt : sclFunc(c[i]);
+    };
+  } else {
+    getColor = validateColor;
+  }
+
+  if (isArrayOpacityIn) {
+    getOpacity = function(o, i) {
+      return o[i] === undefined ? opacityDflt : validateOpacity(o[i]);
+    };
+  } else {
+    getOpacity = validateOpacity;
+  }
+
+  if (isArrayColorIn || isArrayOpacityIn) {
+    for (var i = 0; i < len; i++) {
+      colori = getColor(colorIn, i);
+      opacityi = getOpacity(opacityIn, i);
+      colorOut[i] = calculateColor(colori, opacityi);
     }
-    else colorOut = calculateColor(colorIn, opacityIn);
+  } else {
+    colorOut = calculateColor(colorIn, opacityIn);
+  }
 
-    return colorOut;
+  return colorOut;
 }
 
 module.exports = formatColor;
diff --git a/src/lib/html2unicode.js b/src/lib/html2unicode.js
index 346ecaaf90f..e184cfb31c5 100644
--- a/src/lib/html2unicode.js
+++ b/src/lib/html2unicode.js
@@ -5,63 +5,57 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var toSuperScript = require('superscript-text');
-var stringMappings = require('../constants/string_mappings');
+"use strict";
+var toSuperScript = require("superscript-text");
+var stringMappings = require("../constants/string_mappings");
 
 function fixSuperScript(x) {
-    var idx = 0;
+  var idx = 0;
 
-    while((idx = x.indexOf('', idx)) >= 0) {
-        var nidx = x.indexOf('', idx);
-        if(nidx < idx) break;
+  while ((idx = x.indexOf("", idx)) >= 0) {
+    var nidx = x.indexOf("", idx);
+    if (nidx < idx) break;
 
-        x = x.slice(0, idx) + toSuperScript(x.slice(idx + 5, nidx)) + x.slice(nidx + 6);
-    }
+    x = x.slice(0, idx) +
+      toSuperScript(x.slice(idx + 5, nidx)) +
+      x.slice(nidx + 6);
+  }
 
-    return x;
+  return x;
 }
 
 function fixBR(x) {
-    return x.replace(/\
/g, '\n');
+  return x.replace(/\
/g, "\n");
 }
 
 function stripTags(x) {
-    return x.replace(/\<.*\>/g, '');
+  return x.replace(/\<.*\>/g, "");
 }
 
 function fixEntities(x) {
-    var entityToUnicode = stringMappings.entityToUnicode;
-    var idx = 0;
-
-    while((idx = x.indexOf('&', idx)) >= 0) {
-        var nidx = x.indexOf(';', idx);
-        if(nidx < idx) {
-            idx += 1;
-            continue;
-        }
+  var entityToUnicode = stringMappings.entityToUnicode;
+  var idx = 0;
+
+  while ((idx = x.indexOf("&", idx)) >= 0) {
+    var nidx = x.indexOf(";", idx);
+    if (nidx < idx) {
+      idx += 1;
+      continue;
+    }
 
-        var entity = entityToUnicode[x.slice(idx + 1, nidx)];
-        if(entity) {
-            x = x.slice(0, idx) + entity + x.slice(nidx + 1);
-        } else {
-            x = x.slice(0, idx) + x.slice(nidx + 1);
-        }
+    var entity = entityToUnicode[x.slice(idx + 1, nidx)];
+    if (entity) {
+      x = x.slice(0, idx) + entity + x.slice(nidx + 1);
+    } else {
+      x = x.slice(0, idx) + x.slice(nidx + 1);
     }
+  }
 
-    return x;
+  return x;
 }
 
 function convertHTMLToUnicode(html) {
-    return '' +
-        fixEntities(
-        stripTags(
-        fixSuperScript(
-        fixBR(
-          html))));
+  return "" + fixEntities(stripTags(fixSuperScript(fixBR(html))));
 }
 
 module.exports = convertHTMLToUnicode;
diff --git a/src/lib/index.js b/src/lib/index.js
index 9544f4b3794..cb7c2fe644a 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -5,27 +5,24 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var d3 = require('d3');
+"use strict";
+var d3 = require("d3");
 
 var lib = module.exports = {};
 
-lib.nestedProperty = require('./nested_property');
-lib.isPlainObject = require('./is_plain_object');
-lib.isArray = require('./is_array');
-lib.mod = require('./mod');
+lib.nestedProperty = require("./nested_property");
+lib.isPlainObject = require("./is_plain_object");
+lib.isArray = require("./is_array");
+lib.mod = require("./mod");
 
-var coerceModule = require('./coerce');
+var coerceModule = require("./coerce");
 lib.valObjects = coerceModule.valObjects;
 lib.coerce = coerceModule.coerce;
 lib.coerce2 = coerceModule.coerce2;
 lib.coerceFont = coerceModule.coerceFont;
 lib.validate = coerceModule.validate;
 
-var datesModule = require('./dates');
+var datesModule = require("./dates");
 lib.dateTime2ms = datesModule.dateTime2ms;
 lib.isDateTime = datesModule.isDateTime;
 lib.ms2DateTime = datesModule.ms2DateTime;
@@ -40,14 +37,14 @@ lib.findExactDates = datesModule.findExactDates;
 lib.MIN_MS = datesModule.MIN_MS;
 lib.MAX_MS = datesModule.MAX_MS;
 
-var searchModule = require('./search');
+var searchModule = require("./search");
 lib.findBin = searchModule.findBin;
 lib.sorterAsc = searchModule.sorterAsc;
 lib.sorterDes = searchModule.sorterDes;
 lib.distinctVals = searchModule.distinctVals;
 lib.roundUp = searchModule.roundUp;
 
-var statsModule = require('./stats');
+var statsModule = require("./stats");
 lib.aggNums = statsModule.aggNums;
 lib.len = statsModule.len;
 lib.mean = statsModule.mean;
@@ -55,7 +52,7 @@ lib.variance = statsModule.variance;
 lib.stdev = statsModule.stdev;
 lib.interp = statsModule.interp;
 
-var matrixModule = require('./matrix');
+var matrixModule = require("./matrix");
 lib.init2dArray = matrixModule.init2dArray;
 lib.transposeRagged = matrixModule.transposeRagged;
 lib.dot = matrixModule.dot;
@@ -65,24 +62,23 @@ lib.rotationXYMatrix = matrixModule.rotationXYMatrix;
 lib.apply2DTransform = matrixModule.apply2DTransform;
 lib.apply2DTransform2 = matrixModule.apply2DTransform2;
 
-var extendModule = require('./extend');
+var extendModule = require("./extend");
 lib.extendFlat = extendModule.extendFlat;
 lib.extendDeep = extendModule.extendDeep;
 lib.extendDeepAll = extendModule.extendDeepAll;
 lib.extendDeepNoArrays = extendModule.extendDeepNoArrays;
 
-var loggersModule = require('./loggers');
+var loggersModule = require("./loggers");
 lib.log = loggersModule.log;
 lib.warn = loggersModule.warn;
 lib.error = loggersModule.error;
 
-lib.notifier = require('./notifier');
+lib.notifier = require("./notifier");
 
-lib.filterUnique = require('./filter_unique');
-lib.filterVisible = require('./filter_visible');
+lib.filterUnique = require("./filter_unique");
+lib.filterVisible = require("./filter_visible");
 
-
-lib.cleanNumber = require('./clean_number');
+lib.cleanNumber = require("./clean_number");
 
 /**
  * swap x and y of the same attribute in container cont
@@ -90,16 +86,16 @@ lib.cleanNumber = require('./clean_number');
  * you can also swap other things than x/y by providing part1 and part2
  */
 lib.swapAttrs = function(cont, attrList, part1, part2) {
-    if(!part1) part1 = 'x';
-    if(!part2) part2 = 'y';
-    for(var i = 0; i < attrList.length; i++) {
-        var attr = attrList[i],
-            xp = lib.nestedProperty(cont, attr.replace('?', part1)),
-            yp = lib.nestedProperty(cont, attr.replace('?', part2)),
-            temp = xp.get();
-        xp.set(yp.get());
-        yp.set(temp);
-    }
+  if (!part1) part1 = "x";
+  if (!part2) part2 = "y";
+  for (var i = 0; i < attrList.length; i++) {
+    var attr = attrList[i],
+      xp = lib.nestedProperty(cont, attr.replace("?", part1)),
+      yp = lib.nestedProperty(cont, attr.replace("?", part2)),
+      temp = xp.get();
+    xp.set(yp.get());
+    yp.set(temp);
+  }
 };
 
 /**
@@ -110,16 +106,16 @@ lib.swapAttrs = function(cont, attrList, part1, part2) {
  *      return pauseEvent(e);
  */
 lib.pauseEvent = function(e) {
-    if(e.stopPropagation) e.stopPropagation();
-    if(e.preventDefault) e.preventDefault();
-    e.cancelBubble = true;
-    return false;
+  if (e.stopPropagation) e.stopPropagation();
+  if (e.preventDefault) e.preventDefault();
+  e.cancelBubble = true;
+  return false;
 };
 
 // constrain - restrict a number v to be between v0 and v1
 lib.constrain = function(v, v0, v1) {
-    if(v0 > v1) return Math.max(v1, Math.min(v0, v));
-    return Math.max(v0, Math.min(v1, v));
+  if (v0 > v1) return Math.max(v1, Math.min(v0, v));
+  return Math.max(v0, Math.min(v1, v));
 };
 
 /**
@@ -128,15 +124,17 @@ lib.constrain = function(v, v0, v1) {
  * takes optional padding pixels
  */
 lib.bBoxIntersect = function(a, b, pad) {
-    pad = pad || 0;
-    return (a.left <= b.right + pad &&
-            b.left <= a.right + pad &&
-            a.top <= b.bottom + pad &&
-            b.top <= a.bottom + pad);
+  pad = pad || 0;
+  return a.left <= b.right + pad &&
+    b.left <= a.right + pad &&
+    a.top <= b.bottom + pad &&
+    b.top <= a.bottom + pad;
 };
 
 // minor convenience/performance booster for d3...
-lib.identity = function(d) { return d; };
+lib.identity = function(d) {
+  return d;
+};
 
 // minor convenience helper
 lib.noop = function() {};
@@ -151,55 +149,55 @@ lib.noop = function() {};
  * x1, x2: optional extra args
  */
 lib.simpleMap = function(array, func, x1, x2) {
-    var len = array.length,
-        out = new Array(len);
-    for(var i = 0; i < len; i++) out[i] = func(array[i], x1, x2);
-    return out;
+  var len = array.length, out = new Array(len);
+  for (var i = 0; i < len; i++) {
+    out[i] = func(array[i], x1, x2);
+  }
+  return out;
 };
 
 // random string generator
 lib.randstr = function randstr(existing, bits, base) {
-    /*
+  /*
      * Include number of bits, the base of the string you want
      * and an optional array of existing strings to avoid.
      */
-    if(!base) base = 16;
-    if(bits === undefined) bits = 24;
-    if(bits <= 0) return '0';
-
-    var digits = Math.log(Math.pow(2, bits)) / Math.log(base),
-        res = '',
-        i,
-        b,
-        x;
-
-    for(i = 2; digits === Infinity; i *= 2) {
-        digits = Math.log(Math.pow(2, bits / i)) / Math.log(base) * i;
-    }
-
-    var rem = digits - Math.floor(digits);
-
-    for(i = 0; i < Math.floor(digits); i++) {
-        x = Math.floor(Math.random() * base).toString(base);
-        res = x + res;
-    }
-
-    if(rem) {
-        b = Math.pow(base, rem);
-        x = Math.floor(Math.random() * b).toString(base);
-        res = x + res;
-    }
-
-    var parsed = parseInt(res, base);
-    if((existing && (existing.indexOf(res) > -1)) ||
-         (parsed !== Infinity && parsed >= Math.pow(2, bits))) {
-        return randstr(existing, bits, base);
-    }
-    else return res;
+  if (!base) base = 16;
+  if (bits === undefined) bits = 24;
+  if (bits <= 0) return "0";
+
+  var digits = Math.log(Math.pow(2, bits)) / Math.log(base), res = "", i, b, x;
+
+  for (i = 2; digits === Infinity; i *= 2) {
+    digits = Math.log(Math.pow(2, bits / i)) / Math.log(base) * i;
+  }
+
+  var rem = digits - Math.floor(digits);
+
+  for (i = 0; i < Math.floor(digits); i++) {
+    x = Math.floor(Math.random() * base).toString(base);
+    res = x + res;
+  }
+
+  if (rem) {
+    b = Math.pow(base, rem);
+    x = Math.floor(Math.random() * b).toString(base);
+    res = x + res;
+  }
+
+  var parsed = parseInt(res, base);
+  if (
+    existing && existing.indexOf(res) > -1 ||
+      parsed !== Infinity && parsed >= Math.pow(2, bits)
+  ) {
+    return randstr(existing, bits, base);
+  } else {
+    return res;
+  }
 };
 
 lib.OptionControl = function(opt, optname) {
-    /*
+  /*
      * An environment to contain all option setters and
      * getters that collectively modify opts.
      *
@@ -208,20 +206,20 @@ lib.OptionControl = function(opt, optname) {
      *
      * See FitOpts for example of usage
      */
-    if(!opt) opt = {};
-    if(!optname) optname = 'opt';
+  if (!opt) opt = {};
+  if (!optname) optname = "opt";
 
-    var self = {};
-    self.optionList = [];
+  var self = {};
+  self.optionList = [];
 
-    self._newoption = function(optObj) {
-        optObj[optname] = opt;
-        self[optObj.name] = optObj;
-        self.optionList.push(optObj);
-    };
+  self._newoption = function(optObj) {
+    optObj[optname] = opt;
+    self[optObj.name] = optObj;
+    self.optionList.push(optObj);
+  };
 
-    self['_' + optname] = opt;
-    return self;
+  self["_" + optname] = opt;
+  return self;
 };
 
 /**
@@ -230,44 +228,45 @@ lib.OptionControl = function(opt, optname) {
  * bounce the ends in, so the output has the same length as the input
  */
 lib.smooth = function(arrayIn, FWHM) {
-    FWHM = Math.round(FWHM) || 0; // only makes sense for integers
-    if(FWHM < 2) return arrayIn;
-
-    var alen = arrayIn.length,
-        alen2 = 2 * alen,
-        wlen = 2 * FWHM - 1,
-        w = new Array(wlen),
-        arrayOut = new Array(alen),
-        i,
-        j,
-        k,
-        v;
-
-    // first make the window array
-    for(i = 0; i < wlen; i++) {
-        w[i] = (1 - Math.cos(Math.PI * (i + 1) / FWHM)) / (2 * FWHM);
-    }
-
-    // now do the convolution
-    for(i = 0; i < alen; i++) {
-        v = 0;
-        for(j = 0; j < wlen; j++) {
-            k = i + j + 1 - FWHM;
-
-            // multibounce
-            if(k < -alen) k -= alen2 * Math.round(k / alen2);
-            else if(k >= alen2) k -= alen2 * Math.floor(k / alen2);
-
-            // single bounce
-            if(k < 0) k = - 1 - k;
-            else if(k >= alen) k = alen2 - 1 - k;
-
-            v += arrayIn[k] * w[j];
-        }
-        arrayOut[i] = v;
+  FWHM = Math.round(FWHM) || 0;
+  // only makes sense for integers
+  if (FWHM < 2) return arrayIn;
+
+  var alen = arrayIn.length,
+    alen2 = 2 * alen,
+    wlen = 2 * FWHM - 1,
+    w = new Array(wlen),
+    arrayOut = new Array(alen),
+    i,
+    j,
+    k,
+    v;
+
+  // first make the window array
+  for (i = 0; i < wlen; i++) {
+    w[i] = (1 - Math.cos(Math.PI * (i + 1) / FWHM)) / (2 * FWHM);
+  }
+
+  // now do the convolution
+  for (i = 0; i < alen; i++) {
+    v = 0;
+    for (j = 0; j < wlen; j++) {
+      k = i + j + 1 - FWHM;
+
+      // multibounce
+      if (k < -alen) k -= alen2 * Math.round(k / alen2);
+      else if (k >= alen2) k -= alen2 * Math.floor(k / alen2);
+
+      // single bounce
+      if (k < 0) k = -1 - k;
+      else if (k >= alen) k = alen2 - 1 - k;
+
+      v += arrayIn[k] * w[j];
     }
+    arrayOut[i] = v;
+  }
 
-    return arrayOut;
+  return arrayOut;
 };
 
 /**
@@ -282,59 +281,54 @@ lib.smooth = function(arrayIn, FWHM) {
  * that it gets reported
  */
 lib.syncOrAsync = function(sequence, arg, finalStep) {
-    var ret, fni;
+  var ret, fni;
 
-    function continueAsync() {
-        return lib.syncOrAsync(sequence, arg, finalStep);
-    }
+  function continueAsync() {
+    return lib.syncOrAsync(sequence, arg, finalStep);
+  }
 
-    while(sequence.length) {
-        fni = sequence.splice(0, 1)[0];
-        ret = fni(arg);
+  while (sequence.length) {
+    fni = sequence.splice(0, 1)[0];
+    ret = fni(arg);
 
-        if(ret && ret.then) {
-            return ret.then(continueAsync)
-                .then(undefined, lib.promiseError);
-        }
+    if (ret && ret.then) {
+      return ret.then(continueAsync).then(undefined, lib.promiseError);
     }
+  }
 
-    return finalStep && finalStep(arg);
+  return finalStep && finalStep(arg);
 };
 
-
 /**
  * Helper to strip trailing slash, from
  * http://stackoverflow.com/questions/6680825/return-string-without-trailing-slash
  */
 lib.stripTrailingSlash = function(str) {
-    if(str.substr(-1) === '/') return str.substr(0, str.length - 1);
-    return str;
+  if (str.substr(-1) === "/") return str.substr(0, str.length - 1);
+  return str;
 };
 
 lib.noneOrAll = function(containerIn, containerOut, attrList) {
-    /**
+  /**
      * some attributes come together, so if you have one of them
      * in the input, you should copy the default values of the others
      * to the input as well.
      */
-    if(!containerIn) return;
+  if (!containerIn) return;
 
-    var hasAny = false,
-        hasAll = true,
-        i,
-        val;
+  var hasAny = false, hasAll = true, i, val;
 
-    for(i = 0; i < attrList.length; i++) {
-        val = containerIn[attrList[i]];
-        if(val !== undefined && val !== null) hasAny = true;
-        else hasAll = false;
-    }
+  for (i = 0; i < attrList.length; i++) {
+    val = containerIn[attrList[i]];
+    if (val !== undefined && val !== null) hasAny = true;
+    else hasAll = false;
+  }
 
-    if(hasAny && !hasAll) {
-        for(i = 0; i < attrList.length; i++) {
-            containerIn[attrList[i]] = containerOut[attrList[i]];
-        }
+  if (hasAny && !hasAll) {
+    for (i = 0; i < attrList.length; i++) {
+      containerIn[attrList[i]] = containerOut[attrList[i]];
     }
+  }
 };
 
 /**
@@ -349,16 +343,18 @@ lib.noneOrAll = function(containerIn, containerOut, attrList) {
  *
  */
 lib.pushUnique = function(array, item) {
-    if(item && array.indexOf(item) === -1) array.push(item);
+  if (item && array.indexOf(item) === -1) array.push(item);
 
-    return array;
+  return array;
 };
 
 lib.mergeArray = function(traceAttr, cd, cdAttr) {
-    if(Array.isArray(traceAttr)) {
-        var imax = Math.min(traceAttr.length, cd.length);
-        for(var i = 0; i < imax; i++) cd[i][cdAttr] = traceAttr[i];
+  if (Array.isArray(traceAttr)) {
+    var imax = Math.min(traceAttr.length, cd.length);
+    for (var i = 0; i < imax; i++) {
+      cd[i][cdAttr] = traceAttr[i];
     }
+  }
 };
 
 /**
@@ -368,63 +364,67 @@ lib.mergeArray = function(traceAttr, cd, cdAttr) {
  * obj2 is assumed to already be clean of these things (including no arrays)
  */
 lib.minExtend = function(obj1, obj2) {
-    var objOut = {};
-    if(typeof obj2 !== 'object') obj2 = {};
-    var arrayLen = 3,
-        keys = Object.keys(obj1),
-        i,
-        k,
-        v;
-    for(i = 0; i < keys.length; i++) {
-        k = keys[i];
-        v = obj1[k];
-        if(k.charAt(0) === '_' || typeof v === 'function') continue;
-        else if(k === 'module') objOut[k] = v;
-        else if(Array.isArray(v)) objOut[k] = v.slice(0, arrayLen);
-        else if(v && (typeof v === 'object')) objOut[k] = lib.minExtend(obj1[k], obj2[k]);
-        else objOut[k] = v;
+  var objOut = {};
+  if (typeof obj2 !== "object") obj2 = {};
+  var arrayLen = 3, keys = Object.keys(obj1), i, k, v;
+  for (i = 0; i < keys.length; i++) {
+    k = keys[i];
+    v = obj1[k];
+    if (k.charAt(0) === "_" || typeof v === "function") {
+      continue;
+    } else if (k === "module") {
+      objOut[k] = v;
+    } else if (Array.isArray(v)) {
+      objOut[k] = v.slice(0, arrayLen);
+    } else if (v && typeof v === "object") {
+      objOut[k] = lib.minExtend(obj1[k], obj2[k]);
+    } else {
+      objOut[k] = v;
     }
-
-    keys = Object.keys(obj2);
-    for(i = 0; i < keys.length; i++) {
-        k = keys[i];
-        v = obj2[k];
-        if(typeof v !== 'object' || !(k in objOut) || typeof objOut[k] !== 'object') {
-            objOut[k] = v;
-        }
+  }
+
+  keys = Object.keys(obj2);
+  for (i = 0; i < keys.length; i++) {
+    k = keys[i];
+    v = obj2[k];
+    if (
+      typeof v !== "object" || !(k in objOut) || typeof objOut[k] !== "object"
+    ) {
+      objOut[k] = v;
     }
+  }
 
-    return objOut;
+  return objOut;
 };
 
 lib.titleCase = function(s) {
-    return s.charAt(0).toUpperCase() + s.substr(1);
+  return s.charAt(0).toUpperCase() + s.substr(1);
 };
 
 lib.containsAny = function(s, fragments) {
-    for(var i = 0; i < fragments.length; i++) {
-        if(s.indexOf(fragments[i]) !== -1) return true;
-    }
-    return false;
+  for (var i = 0; i < fragments.length; i++) {
+    if (s.indexOf(fragments[i]) !== -1) return true;
+  }
+  return false;
 };
 
 // get the parent Plotly plot of any element. Whoo jquery-free tree climbing!
 lib.getPlotDiv = function(el) {
-    for(; el && el.removeAttribute; el = el.parentNode) {
-        if(lib.isPlotDiv(el)) return el;
-    }
+  for (; el && el.removeAttribute; el = el.parentNode) {
+    if (lib.isPlotDiv(el)) return el;
+  }
 };
 
 lib.isPlotDiv = function(el) {
-    var el3 = d3.select(el);
-    return el3.node() instanceof HTMLElement &&
-        el3.size() &&
-        el3.classed('js-plotly-plot');
+  var el3 = d3.select(el);
+  return el3.node() instanceof HTMLElement &&
+    el3.size() &&
+    el3.classed("js-plotly-plot");
 };
 
 lib.removeElement = function(el) {
-    var elParent = el && el.parentNode;
-    if(elParent) elParent.removeChild(el);
+  var elParent = el && el.parentNode;
+  if (elParent) elParent.removeChild(el);
 };
 
 /**
@@ -433,26 +433,26 @@ lib.removeElement = function(el) {
  * by all calls to this function
  */
 lib.addStyleRule = function(selector, styleString) {
-    if(!lib.styleSheet) {
-        var style = document.createElement('style');
-        // WebKit hack :(
-        style.appendChild(document.createTextNode(''));
-        document.head.appendChild(style);
-        lib.styleSheet = style.sheet;
-    }
-    var styleSheet = lib.styleSheet;
-
-    if(styleSheet.insertRule) {
-        styleSheet.insertRule(selector + '{' + styleString + '}', 0);
-    }
-    else if(styleSheet.addRule) {
-        styleSheet.addRule(selector, styleString, 0);
-    }
-    else lib.warn('addStyleRule failed');
+  if (!lib.styleSheet) {
+    var style = document.createElement("style");
+    // WebKit hack :(
+    style.appendChild(document.createTextNode(""));
+    document.head.appendChild(style);
+    lib.styleSheet = style.sheet;
+  }
+  var styleSheet = lib.styleSheet;
+
+  if (styleSheet.insertRule) {
+    styleSheet.insertRule(selector + "{" + styleString + "}", 0);
+  } else if (styleSheet.addRule) {
+    styleSheet.addRule(selector, styleString, 0);
+  } else {
+    lib.warn("addStyleRule failed");
+  }
 };
 
 lib.isIE = function() {
-    return typeof window.navigator.msSaveBlob !== 'undefined';
+  return typeof window.navigator.msSaveBlob !== "undefined";
 };
 
 /**
@@ -460,10 +460,9 @@ lib.isIE = function() {
  * because it doesn't handle instanceof like modern browsers
  */
 lib.isD3Selection = function(obj) {
-    return obj && (typeof obj.classed === 'function');
+  return obj && typeof obj.classed === "function";
 };
 
-
 /**
  * Converts a string path to an object.
  *
@@ -480,42 +479,39 @@ lib.isD3Selection = function(obj) {
  * @return {Object} the constructed object with a full nested path
  */
 lib.objectFromPath = function(path, value) {
-    var keys = path.split('.'),
-        tmpObj,
-        obj = tmpObj = {};
+  var keys = path.split("."), tmpObj, obj = tmpObj = {};
 
-    for(var i = 0; i < keys.length; i++) {
-        var key = keys[i];
-        var el = null;
+  for (var i = 0; i < keys.length; i++) {
+    var key = keys[i];
+    var el = null;
 
-        var parts = keys[i].match(/(.*)\[([0-9]+)\]/);
+    var parts = keys[i].match(/(.*)\[([0-9]+)\]/);
 
-        if(parts) {
-            key = parts[1];
-            el = parts[2];
+    if (parts) {
+      key = parts[1];
+      el = parts[2];
 
-            tmpObj = tmpObj[key] = [];
+      tmpObj = tmpObj[key] = [];
 
-            if(i === keys.length - 1) {
-                tmpObj[el] = value;
-            } else {
-                tmpObj[el] = {};
-            }
+      if (i === keys.length - 1) {
+        tmpObj[el] = value;
+      } else {
+        tmpObj[el] = {};
+      }
 
-            tmpObj = tmpObj[el];
-        } else {
+      tmpObj = tmpObj[el];
+    } else {
+      if (i === keys.length - 1) {
+        tmpObj[key] = value;
+      } else {
+        tmpObj[key] = {};
+      }
 
-            if(i === keys.length - 1) {
-                tmpObj[key] = value;
-            } else {
-                tmpObj[key] = {};
-            }
-
-            tmpObj = tmpObj[key];
-        }
+      tmpObj = tmpObj[key];
     }
+  }
 
-    return obj;
+  return obj;
 };
 
 /**
@@ -541,7 +537,6 @@ lib.objectFromPath = function(path, value) {
  *   lib.expandObjectPaths({'marker[1].range[1]': 5, 'marker[1].range[0]': 4})
  *     => { marker: [null, {range: 4}] }
  */
-
 // Store this to avoid recompiling regex on *every* prop since this may happen many
 // many times for animations. Could maybe be inside the function. Not sure about
 // scoping vs. recompilation tradeoff, but at least it's not just inlining it into
@@ -550,59 +545,65 @@ var dottedPropertyRegex = /^([^\[\.]+)\.(.+)?/;
 var indexedPropertyRegex = /^([^\.]+)\[([0-9]+)\](\.)?(.+)?/;
 
 lib.expandObjectPaths = function(data) {
-    var match, key, prop, datum, idx, dest, trailingPath;
-    if(typeof data === 'object' && !Array.isArray(data)) {
-        for(key in data) {
-            if(data.hasOwnProperty(key)) {
-                if((match = key.match(dottedPropertyRegex))) {
-                    datum = data[key];
-                    prop = match[1];
-
-                    delete data[key];
-
-                    data[prop] = lib.extendDeepNoArrays(data[prop] || {}, lib.objectFromPath(key, lib.expandObjectPaths(datum))[prop]);
-                } else if((match = key.match(indexedPropertyRegex))) {
-                    datum = data[key];
-
-                    prop = match[1];
-                    idx = parseInt(match[2]);
-
-                    delete data[key];
-
-                    data[prop] = data[prop] || [];
-
-                    if(match[3] === '.') {
-                        // This is the case where theere are subsequent properties into which
-                        // we must recurse, e.g. transforms[0].value
-                        trailingPath = match[4];
-                        dest = data[prop][idx] = data[prop][idx] || {};
-
-                        // NB: Extend deep no arrays prevents this from working on multiple
-                        // nested properties in the same object, e.g.
-                        //
-                        // {
-                        //   foo[0].bar[1].range
-                        //   foo[0].bar[0].range
-                        // }
-                        //
-                        // In this case, the extendDeepNoArrays will overwrite one array with
-                        // the other, so that both properties *will not* be present in the
-                        // result. Fixing this would require a more intelligent tracking
-                        // of changes and merging than extendDeepNoArrays currently accomplishes.
-                        lib.extendDeepNoArrays(dest, lib.objectFromPath(trailingPath, lib.expandObjectPaths(datum)));
-                    } else {
-                        // This is the case where this property is the end of the line,
-                        // e.g. xaxis.range[0]
-                        data[prop][idx] = lib.expandObjectPaths(datum);
-                    }
-                } else {
-                    data[key] = lib.expandObjectPaths(data[key]);
-                }
-            }
+  var match, key, prop, datum, idx, dest, trailingPath;
+  if (typeof data === "object" && !Array.isArray(data)) {
+    for (key in data) {
+      if (data.hasOwnProperty(key)) {
+        if (match = key.match(dottedPropertyRegex)) {
+          datum = data[key];
+          prop = match[1];
+
+          delete data[key];
+
+          data[prop] = lib.extendDeepNoArrays(
+            data[prop] || {},
+            lib.objectFromPath(key, lib.expandObjectPaths(datum))[prop]
+          );
+        } else if (match = key.match(indexedPropertyRegex)) {
+          datum = data[key];
+
+          prop = match[1];
+          idx = parseInt(match[2]);
+
+          delete data[key];
+
+          data[prop] = data[prop] || [];
+
+          if (match[3] === ".") {
+            // This is the case where theere are subsequent properties into which
+            // we must recurse, e.g. transforms[0].value
+            trailingPath = match[4];
+            dest = data[prop][idx] = data[prop][idx] || {};
+
+            // NB: Extend deep no arrays prevents this from working on multiple
+            // nested properties in the same object, e.g.
+            //
+            // {
+            //   foo[0].bar[1].range
+            //   foo[0].bar[0].range
+            // }
+            //
+            // In this case, the extendDeepNoArrays will overwrite one array with
+            // the other, so that both properties *will not* be present in the
+            // result. Fixing this would require a more intelligent tracking
+            // of changes and merging than extendDeepNoArrays currently accomplishes.
+            lib.extendDeepNoArrays(
+              dest,
+              lib.objectFromPath(trailingPath, lib.expandObjectPaths(datum))
+            );
+          } else {
+            // This is the case where this property is the end of the line,
+            // e.g. xaxis.range[0]
+            data[prop][idx] = lib.expandObjectPaths(datum);
+          }
+        } else {
+          data[key] = lib.expandObjectPaths(data[key]);
         }
+      }
     }
+  }
 
-    return data;
+  return data;
 };
 
 /**
@@ -627,30 +628,30 @@ lib.expandObjectPaths = function(data) {
  * @return  {string}    the value that has been separated
  */
 lib.numSeparate = function(value, separators, separatethousands) {
-    if(!separatethousands) separatethousands = false;
+  if (!separatethousands) separatethousands = false;
 
-    if(typeof separators !== 'string' || separators.length === 0) {
-        throw new Error('Separator string required for formatting!');
-    }
+  if (typeof separators !== "string" || separators.length === 0) {
+    throw new Error("Separator string required for formatting!");
+  }
 
-    if(typeof value === 'number') {
-        value = String(value);
-    }
+  if (typeof value === "number") {
+    value = String(value);
+  }
 
-    var thousandsRe = /(\d+)(\d{3})/,
-        decimalSep = separators.charAt(0),
-        thouSep = separators.charAt(1);
+  var thousandsRe = /(\d+)(\d{3})/,
+    decimalSep = separators.charAt(0),
+    thouSep = separators.charAt(1);
 
-    var x = value.split('.'),
-        x1 = x[0],
-        x2 = x.length > 1 ? decimalSep + x[1] : '';
+  var x = value.split("."),
+    x1 = x[0],
+    x2 = x.length > 1 ? decimalSep + x[1] : "";
 
-    // Years are ignored for thousands separators
-    if(thouSep && (x.length > 1 || x1.length > 4 || separatethousands)) {
-        while(thousandsRe.test(x1)) {
-            x1 = x1.replace(thousandsRe, '$1' + thouSep + '$2');
-        }
+  // Years are ignored for thousands separators
+  if (thouSep && (x.length > 1 || x1.length > 4 || separatethousands)) {
+    while (thousandsRe.test(x1)) {
+      x1 = x1.replace(thousandsRe, "$1" + thouSep + "$2");
     }
+  }
 
-    return x1 + x2;
+  return x1 + x2;
 };
diff --git a/src/lib/is_array.js b/src/lib/is_array.js
index cda78eeb627..c0b7362fd6a 100644
--- a/src/lib/is_array.js
+++ b/src/lib/is_array.js
@@ -5,18 +5,20 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 /**
  * Return true for arrays, whether they're untyped or not.
  */
 
 // IE9 fallback
-var ab = (typeof ArrayBuffer === 'undefined' || !ArrayBuffer.isView) ?
-    {isView: function() { return false; }} :
-    ArrayBuffer;
+var ab = typeof ArrayBuffer === "undefined" || !ArrayBuffer.isView
+  ? {
+      isView: function() {
+        return false;
+      }
+    }
+  : ArrayBuffer;
 
 module.exports = function isArray(a) {
-    return Array.isArray(a) || ab.isView(a);
+  return Array.isArray(a) || ab.isView(a);
 };
diff --git a/src/lib/is_plain_object.js b/src/lib/is_plain_object.js
index d114e022d2f..b22eee1faa2 100644
--- a/src/lib/is_plain_object.js
+++ b/src/lib/is_plain_object.js
@@ -5,23 +5,17 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 // more info: http://stackoverflow.com/questions/18531624/isplainobject-thing
 module.exports = function isPlainObject(obj) {
+  // We need to be a little less strict in the `imagetest` container because
+  // of how async image requests are handled.
+  //
+  // N.B. isPlainObject(new Constructor()) will return true in `imagetest`
+  if (window && window.process && window.process.versions) {
+    return Object.prototype.toString.call(obj) === "[object Object]";
+  }
 
-    // We need to be a little less strict in the `imagetest` container because
-    // of how async image requests are handled.
-    //
-    // N.B. isPlainObject(new Constructor()) will return true in `imagetest`
-    if(window && window.process && window.process.versions) {
-        return Object.prototype.toString.call(obj) === '[object Object]';
-    }
-
-    return (
-        Object.prototype.toString.call(obj) === '[object Object]' &&
-        Object.getPrototypeOf(obj) === Object.prototype
-    );
+  return Object.prototype.toString.call(obj) === "[object Object]" &&
+    Object.getPrototypeOf(obj) === Object.prototype;
 };
diff --git a/src/lib/loggers.js b/src/lib/loggers.js
index 428f053e000..ff51d598c98 100644
--- a/src/lib/loggers.js
+++ b/src/lib/loggers.js
@@ -5,12 +5,10 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 /* eslint-disable no-console */
 
-var config = require('../plot_api/plot_config');
+var config = require("../plot_api/plot_config");
 
 var loggers = module.exports = {};
 
@@ -19,41 +17,40 @@ var loggers = module.exports = {};
  * debugging tools
  * ------------------------------------------
  */
-
 loggers.log = function() {
-    if(config.logging > 1) {
-        var messages = ['LOG:'];
+  if (config.logging > 1) {
+    var messages = ["LOG:"];
 
-        for(var i = 0; i < arguments.length; i++) {
-            messages.push(arguments[i]);
-        }
-
-        apply(console.trace || console.log, messages);
+    for (var i = 0; i < arguments.length; i++) {
+      messages.push(arguments[i]);
     }
+
+    apply(console.trace || console.log, messages);
+  }
 };
 
 loggers.warn = function() {
-    if(config.logging > 0) {
-        var messages = ['WARN:'];
+  if (config.logging > 0) {
+    var messages = ["WARN:"];
 
-        for(var i = 0; i < arguments.length; i++) {
-            messages.push(arguments[i]);
-        }
-
-        apply(console.trace || console.log, messages);
+    for (var i = 0; i < arguments.length; i++) {
+      messages.push(arguments[i]);
     }
+
+    apply(console.trace || console.log, messages);
+  }
 };
 
 loggers.error = function() {
-    if(config.logging > 0) {
-        var messages = ['ERROR:'];
-
-        for(var i = 0; i < arguments.length; i++) {
-            messages.push(arguments[i]);
-        }
+  if (config.logging > 0) {
+    var messages = ["ERROR:"];
 
-        apply(console.error, messages);
+    for (var i = 0; i < arguments.length; i++) {
+      messages.push(arguments[i]);
     }
+
+    apply(console.error, messages);
+  }
 };
 
 /*
@@ -61,12 +58,11 @@ loggers.error = function() {
  * apply like other functions do
  */
 function apply(f, args) {
-    if(f.apply) {
-        f.apply(f, args);
-    }
-    else {
-        for(var i = 0; i < args.length; i++) {
-            f(args[i]);
-        }
+  if (f.apply) {
+    f.apply(f, args);
+  } else {
+    for (var i = 0; i < args.length; i++) {
+      f(args[i]);
     }
+  }
 }
diff --git a/src/lib/matrix.js b/src/lib/matrix.js
index 2429195de05..84968de207c 100644
--- a/src/lib/matrix.js
+++ b/src/lib/matrix.js
@@ -5,15 +5,13 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-
+"use strict";
 exports.init2dArray = function(rowLength, colLength) {
-    var array = new Array(rowLength);
-    for(var i = 0; i < rowLength; i++) array[i] = new Array(colLength);
-    return array;
+  var array = new Array(rowLength);
+  for (var i = 0; i < rowLength; i++) {
+    array[i] = new Array(colLength);
+  }
+  return array;
 };
 
 /**
@@ -22,87 +20,93 @@ exports.init2dArray = function(rowLength, colLength) {
  * transposing-a-2d-array-in-javascript
  */
 exports.transposeRagged = function(z) {
-    var maxlen = 0,
-        zlen = z.length,
-        i,
-        j;
-    // Maximum row length:
-    for(i = 0; i < zlen; i++) maxlen = Math.max(maxlen, z[i].length);
-
-    var t = new Array(maxlen);
-    for(i = 0; i < maxlen; i++) {
-        t[i] = new Array(zlen);
-        for(j = 0; j < zlen; j++) t[i][j] = z[j][i];
+  var maxlen = 0, zlen = z.length, i, j;
+  // Maximum row length:
+  for (i = 0; i < zlen; i++) {
+    maxlen = Math.max(maxlen, z[i].length);
+  }
+
+  var t = new Array(maxlen);
+  for (i = 0; i < maxlen; i++) {
+    t[i] = new Array(zlen);
+    for (j = 0; j < zlen; j++) {
+      t[i][j] = z[j][i];
     }
+  }
 
-    return t;
+  return t;
 };
 
 // our own dot function so that we don't need to include numeric
 exports.dot = function(x, y) {
-    if(!(x.length && y.length) || x.length !== y.length) return null;
+  if (!(x.length && y.length) || x.length !== y.length) return null;
 
-    var len = x.length,
-        out,
-        i;
+  var len = x.length, out, i;
 
-    if(x[0].length) {
-        // mat-vec or mat-mat
-        out = new Array(len);
-        for(i = 0; i < len; i++) out[i] = exports.dot(x[i], y);
+  if (x[0].length) {
+    // mat-vec or mat-mat
+    out = new Array(len);
+    for (i = 0; i < len; i++) {
+      out[i] = exports.dot(x[i], y);
     }
-    else if(y[0].length) {
-        // vec-mat
-        var yTranspose = exports.transposeRagged(y);
-        out = new Array(yTranspose.length);
-        for(i = 0; i < yTranspose.length; i++) out[i] = exports.dot(x, yTranspose[i]);
+  } else if (y[0].length) {
+    // vec-mat
+    var yTranspose = exports.transposeRagged(y);
+    out = new Array(yTranspose.length);
+    for (i = 0; i < yTranspose.length; i++) {
+      out[i] = exports.dot(x, yTranspose[i]);
     }
-    else {
-        // vec-vec
-        out = 0;
-        for(i = 0; i < len; i++) out += x[i] * y[i];
+  } else {
+    // vec-vec
+    out = 0;
+    for (i = 0; i < len; i++) {
+      out += x[i] * y[i];
     }
+  }
 
-    return out;
+  return out;
 };
 
 // translate by (x,y)
 exports.translationMatrix = function(x, y) {
-    return [[1, 0, x], [0, 1, y], [0, 0, 1]];
+  return [[1, 0, x], [0, 1, y], [0, 0, 1]];
 };
 
 // rotate by alpha around (0,0)
 exports.rotationMatrix = function(alpha) {
-    var a = alpha * Math.PI / 180;
-    return [[Math.cos(a), -Math.sin(a), 0],
-            [Math.sin(a), Math.cos(a), 0],
-            [0, 0, 1]];
+  var a = alpha * Math.PI / 180;
+  return [
+    [Math.cos(a), -Math.sin(a), 0],
+    [Math.sin(a), Math.cos(a), 0],
+    [0, 0, 1]
+  ];
 };
 
 // rotate by alpha around (x,y)
 exports.rotationXYMatrix = function(a, x, y) {
-    return exports.dot(
-        exports.dot(exports.translationMatrix(x, y),
-                    exports.rotationMatrix(a)),
-        exports.translationMatrix(-x, -y));
+  return exports.dot(
+    exports.dot(exports.translationMatrix(x, y), exports.rotationMatrix(a)),
+    exports.translationMatrix(-x, -y)
+  );
 };
 
 // applies a 2D transformation matrix to either x and y params or an [x,y] array
 exports.apply2DTransform = function(transform) {
-    return function() {
-        var args = arguments;
-        if(args.length === 3) {
-            args = args[0];
-        }// from map
-        var xy = arguments.length === 1 ? args[0] : [args[0], args[1]];
-        return exports.dot(transform, [xy[0], xy[1], 1]).slice(0, 2);
-    };
+  return function() {
+    var args = arguments;
+    if (args.length === 3) {
+      args = args[0];
+    }
+    // from map
+    var xy = arguments.length === 1 ? args[0] : [args[0], args[1]];
+    return exports.dot(transform, [xy[0], xy[1], 1]).slice(0, 2);
+  };
 };
 
 // applies a 2D transformation matrix to an [x1,y1,x2,y2] array (to transform a segment)
 exports.apply2DTransform2 = function(transform) {
-    var at = exports.apply2DTransform(transform);
-    return function(xys) {
-        return at(xys.slice(0, 2)).concat(at(xys.slice(2, 4)));
-    };
+  var at = exports.apply2DTransform(transform);
+  return function(xys) {
+    return at(xys.slice(0, 2)).concat(at(xys.slice(2, 4)));
+  };
 };
diff --git a/src/lib/mod.js b/src/lib/mod.js
index 6ddf24e2563..c3951c6d6d1 100644
--- a/src/lib/mod.js
+++ b/src/lib/mod.js
@@ -5,14 +5,12 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 /**
  * sanitized modulus function that always returns in the range [0, d)
  * rather than (-d, 0] if v is negative
  */
 module.exports = function mod(v, d) {
-    var out = v % d;
-    return out < 0 ? out + d : out;
+  var out = v % d;
+  return out < 0 ? out + d : out;
 };
diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js
index a00cd17137a..b630cad00df 100644
--- a/src/lib/nested_property.js
+++ b/src/lib/nested_property.js
@@ -5,12 +5,9 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-var isArray = require('./is_array');
+"use strict";
+var isNumeric = require("fast-isnumeric");
+var isArray = require("./is_array");
 
 /**
  * convert a string s (such as 'xaxis.range[0]')
@@ -27,89 +24,85 @@ var isArray = require('./is_array');
  * but you can do nestedProperty(obj, 'arr').set([5, 5, 5])
  */
 module.exports = function nestedProperty(container, propStr) {
-    if(isNumeric(propStr)) propStr = String(propStr);
-    else if(typeof propStr !== 'string' ||
-            propStr.substr(propStr.length - 4) === '[-1]') {
-        throw 'bad property string';
-    }
-
-    var j = 0,
-        propParts = propStr.split('.'),
-        indexed,
-        indices,
-        i;
-
-    // check for parts of the nesting hierarchy that are numbers (ie array elements)
-    while(j < propParts.length) {
-        // look for non-bracket chars, then any number of [##] blocks
-        indexed = String(propParts[j]).match(/^([^\[\]]*)((\[\-?[0-9]*\])+)$/);
-        if(indexed) {
-            if(indexed[1]) propParts[j] = indexed[1];
-            // allow propStr to start with bracketed array indices
-            else if(j === 0) propParts.splice(0, 1);
-            else throw 'bad property string';
-
-            indices = indexed[2]
-                .substr(1, indexed[2].length - 2)
-                .split('][');
-
-            for(i = 0; i < indices.length; i++) {
-                j++;
-                propParts.splice(j, 0, Number(indices[i]));
-            }
-        }
+  if (isNumeric(propStr)) {
+    propStr = String(propStr);
+  } else if (
+    typeof propStr !== "string" || propStr.substr(propStr.length - 4) === "[-1]"
+  ) {
+    throw "bad property string";
+  }
+
+  var j = 0, propParts = propStr.split("."), indexed, indices, i;
+
+  // check for parts of the nesting hierarchy that are numbers (ie array elements)
+  while (j < propParts.length) {
+    // look for non-bracket chars, then any number of [##] blocks
+    indexed = String(propParts[j]).match(/^([^\[\]]*)((\[\-?[0-9]*\])+)$/);
+    if (indexed) {
+      if (indexed[1]) {
+        propParts[j] = indexed[1];
+      } else if (
+        j === 0 // allow propStr to start with bracketed array indices
+      ) {
+        propParts.splice(0, 1);
+      } else {
+        throw "bad property string";
+      }
+
+      indices = indexed[2].substr(1, indexed[2].length - 2).split("][");
+
+      for (i = 0; i < indices.length; i++) {
         j++;
+        propParts.splice(j, 0, Number(indices[i]));
+      }
     }
-
-    if(typeof container !== 'object') {
-        return badContainer(container, propStr, propParts);
-    }
-
-    return {
-        set: npSet(container, propParts),
-        get: npGet(container, propParts),
-        astr: propStr,
-        parts: propParts,
-        obj: container
-    };
+    j++;
+  }
+
+  if (typeof container !== "object") {
+    return badContainer(container, propStr, propParts);
+  }
+
+  return {
+    set: npSet(container, propParts),
+    get: npGet(container, propParts),
+    astr: propStr,
+    parts: propParts,
+    obj: container
+  };
 };
 
 function npGet(cont, parts) {
-    return function() {
-        var curCont = cont,
-            curPart,
-            allSame,
-            out,
-            i,
-            j;
-
-        for(i = 0; i < parts.length - 1; i++) {
-            curPart = parts[i];
-            if(curPart === -1) {
-                allSame = true;
-                out = [];
-                for(j = 0; j < curCont.length; j++) {
-                    out[j] = npGet(curCont[j], parts.slice(i + 1))();
-                    if(out[j] !== out[0]) allSame = false;
-                }
-                return allSame ? out[0] : out;
-            }
-            if(typeof curPart === 'number' && !isArray(curCont)) {
-                return undefined;
-            }
-            curCont = curCont[curPart];
-            if(typeof curCont !== 'object' || curCont === null) {
-                return undefined;
-            }
+  return function() {
+    var curCont = cont, curPart, allSame, out, i, j;
+
+    for (i = 0; i < parts.length - 1; i++) {
+      curPart = parts[i];
+      if (curPart === -1) {
+        allSame = true;
+        out = [];
+        for (j = 0; j < curCont.length; j++) {
+          out[j] = npGet(curCont[j], parts.slice(i + 1))();
+          if (out[j] !== out[0]) allSame = false;
         }
+        return allSame ? out[0] : out;
+      }
+      if (typeof curPart === "number" && !isArray(curCont)) {
+        return undefined;
+      }
+      curCont = curCont[curPart];
+      if (typeof curCont !== "object" || curCont === null) {
+        return undefined;
+      }
+    }
 
-        // only hit this if parts.length === 1
-        if(typeof curCont !== 'object' || curCont === null) return undefined;
+    // only hit this if parts.length === 1
+    if (typeof curCont !== "object" || curCont === null) return undefined;
 
-        out = curCont[parts[i]];
-        if(out === null) return undefined;
-        return out;
-    };
+    out = curCont[parts[i]];
+    if (out === null) return undefined;
+    return out;
+  };
 }
 
 /*
@@ -119,77 +112,77 @@ function npGet(cont, parts) {
  * AND the replacement value is an array.
  */
 function isDataArray(val, key) {
+  var containers = ["annotations", "shapes", "range", "domain", "buttons"],
+    isNotAContainer = containers.indexOf(key) === -1;
 
-    var containers = ['annotations', 'shapes', 'range', 'domain', 'buttons'],
-        isNotAContainer = containers.indexOf(key) === -1;
-
-    return isArray(val) && isNotAContainer;
+  return isArray(val) && isNotAContainer;
 }
 
 function npSet(cont, parts) {
-    return function(val) {
-        var curCont = cont,
-            containerLevels = [cont],
-            toDelete = emptyObj(val) && !isDataArray(val, parts[parts.length - 1]),
-            curPart,
-            i;
-
-        for(i = 0; i < parts.length - 1; i++) {
-            curPart = parts[i];
-
-            if(typeof curPart === 'number' && !isArray(curCont)) {
-                throw 'array index but container is not an array';
-            }
-
-            // handle special -1 array index
-            if(curPart === -1) {
-                toDelete = !setArrayAll(curCont, parts.slice(i + 1), val);
-                if(toDelete) break;
-                else return;
-            }
-
-            if(!checkNewContainer(curCont, curPart, parts[i + 1], toDelete)) {
-                break;
-            }
-
-            curCont = curCont[curPart];
-
-            if(typeof curCont !== 'object' || curCont === null) {
-                throw 'container is not an object';
-            }
-
-            containerLevels.push(curCont);
-        }
+  return function(val) {
+    var curCont = cont,
+      containerLevels = [cont],
+      toDelete = emptyObj(val) && !isDataArray(val, parts[parts.length - 1]),
+      curPart,
+      i;
+
+    for (i = 0; i < parts.length - 1; i++) {
+      curPart = parts[i];
+
+      if (typeof curPart === "number" && !isArray(curCont)) {
+        throw "array index but container is not an array";
+      }
+
+      // handle special -1 array index
+      if (curPart === -1) {
+        toDelete = !setArrayAll(curCont, parts.slice(i + 1), val);
+        if (toDelete) break;
+        else return;
+      }
+
+      if (!checkNewContainer(curCont, curPart, parts[i + 1], toDelete)) {
+        break;
+      }
+
+      curCont = curCont[curPart];
+
+      if (typeof curCont !== "object" || curCont === null) {
+        throw "container is not an object";
+      }
+
+      containerLevels.push(curCont);
+    }
 
-        if(toDelete) {
-            if(i === parts.length - 1) delete curCont[parts[i]];
-            pruneContainers(containerLevels);
-        }
-        else curCont[parts[i]] = val;
-    };
+    if (toDelete) {
+      if (i === parts.length - 1) delete curCont[parts[i]];
+      pruneContainers(containerLevels);
+    } else {
+      curCont[parts[i]] = val;
+    }
+  };
 }
 
 // handle special -1 array index
 function setArrayAll(containerArray, innerParts, val) {
-    var arrayVal = isArray(val),
-        allSet = true,
-        thisVal = val,
-        deleteThis = arrayVal ? false : emptyObj(val),
-        firstPart = innerParts[0],
-        i;
-
-    for(i = 0; i < containerArray.length; i++) {
-        if(arrayVal) {
-            thisVal = val[i % val.length];
-            deleteThis = emptyObj(thisVal);
-        }
-        if(deleteThis) allSet = false;
-        if(!checkNewContainer(containerArray, i, firstPart, deleteThis)) {
-            continue;
-        }
-        npSet(containerArray[i], innerParts)(thisVal);
+  var arrayVal = isArray(val),
+    allSet = true,
+    thisVal = val,
+    deleteThis = arrayVal ? false : emptyObj(val),
+    firstPart = innerParts[0],
+    i;
+
+  for (i = 0; i < containerArray.length; i++) {
+    if (arrayVal) {
+      thisVal = val[i % val.length];
+      deleteThis = emptyObj(thisVal);
     }
-    return allSet;
+    if (deleteThis) allSet = false;
+    if (!checkNewContainer(containerArray, i, firstPart, deleteThis)) {
+      continue;
+    }
+    npSet(containerArray[i], innerParts)(thisVal);
+  }
+  return allSet;
 }
 
 /**
@@ -198,58 +191,63 @@ function setArrayAll(containerArray, innerParts, val) {
  * because we're only deleting an attribute
  */
 function checkNewContainer(container, part, nextPart, toDelete) {
-    if(container[part] === undefined) {
-        if(toDelete) return false;
+  if (container[part] === undefined) {
+    if (toDelete) return false;
 
-        if(typeof nextPart === 'number') container[part] = [];
-        else container[part] = {};
-    }
-    return true;
+    if (typeof nextPart === "number") container[part] = [];
+    else container[part] = {};
+  }
+  return true;
 }
 
 function pruneContainers(containerLevels) {
-    var i,
-        j,
-        curCont,
-        keys,
-        remainingKeys;
-    for(i = containerLevels.length - 1; i >= 0; i--) {
-        curCont = containerLevels[i];
-        remainingKeys = false;
-        if(isArray(curCont)) {
-            for(j = curCont.length - 1; j >= 0; j--) {
-                if(emptyObj(curCont[j])) {
-                    if(remainingKeys) curCont[j] = undefined;
-                    else curCont.pop();
-                }
-                else remainingKeys = true;
-            }
+  var i, j, curCont, keys, remainingKeys;
+  for (i = containerLevels.length - 1; i >= 0; i--) {
+    curCont = containerLevels[i];
+    remainingKeys = false;
+    if (isArray(curCont)) {
+      for (j = curCont.length - 1; j >= 0; j--) {
+        if (emptyObj(curCont[j])) {
+          if (remainingKeys) curCont[j] = undefined;
+          else curCont.pop();
+        } else {
+          remainingKeys = true;
         }
-        else if(typeof curCont === 'object' && curCont !== null) {
-            keys = Object.keys(curCont);
-            remainingKeys = false;
-            for(j = keys.length - 1; j >= 0; j--) {
-                if(emptyObj(curCont[keys[j]]) && !isDataArray(curCont[keys[j]], keys[j])) delete curCont[keys[j]];
-                else remainingKeys = true;
-            }
+      }
+    } else if (typeof curCont === "object" && curCont !== null) {
+      keys = Object.keys(curCont);
+      remainingKeys = false;
+      for (j = keys.length - 1; j >= 0; j--) {
+        if (
+          emptyObj(curCont[keys[j]]) && !isDataArray(curCont[keys[j]], keys[j])
+        ) {
+          delete curCont[keys[j]];
+        } else {
+          remainingKeys = true;
         }
-        if(remainingKeys) return;
+      }
     }
+    if (remainingKeys) return;
+  }
 }
 
 function emptyObj(obj) {
-    if(obj === undefined || obj === null) return true;
-    if(typeof obj !== 'object') return false; // any plain value
-    if(isArray(obj)) return !obj.length; // []
-    return !Object.keys(obj).length; // {}
+  if (obj === undefined || obj === null) return true;
+  if (typeof obj !== "object") return false;
+  // any plain value
+  if (isArray(obj)) return !obj.length;
+  // []
+  return !Object.keys(obj).length; // {}
 }
 
 function badContainer(container, propStr, propParts) {
-    return {
-        set: function() { throw 'bad container'; },
-        get: function() {},
-        astr: propStr,
-        parts: propParts,
-        obj: container
-    };
+  return {
+    set: function() {
+      throw "bad container";
+    },
+    get: function() {},
+    astr: propStr,
+    parts: propParts,
+    obj: container
+  };
 }
diff --git a/src/lib/notifier.js b/src/lib/notifier.js
index e7443afd7a0..25187c6ec36 100644
--- a/src/lib/notifier.js
+++ b/src/lib/notifier.js
@@ -5,12 +5,9 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var d3 = require('d3');
-var isNumeric = require('fast-isnumeric');
+"use strict";
+var d3 = require("d3");
+var isNumeric = require("fast-isnumeric");
 
 var NOTEDATA = [];
 
@@ -22,59 +19,62 @@ var NOTEDATA = [];
  * @return {undefined} this function does not return a value
  */
 module.exports = function(text, displayLength) {
-    if(NOTEDATA.indexOf(text) !== -1) return;
+  if (NOTEDATA.indexOf(text) !== -1) return;
 
-    NOTEDATA.push(text);
+  NOTEDATA.push(text);
 
-    var ts = 1000;
-    if(isNumeric(displayLength)) ts = displayLength;
-    else if(displayLength === 'long') ts = 3000;
+  var ts = 1000;
+  if (isNumeric(displayLength)) ts = displayLength;
+  else if (displayLength === "long") ts = 3000;
 
-    var notifierContainer = d3.select('body')
-        .selectAll('.plotly-notifier')
-        .data([0]);
-    notifierContainer.enter()
-        .append('div')
-        .classed('plotly-notifier', true);
+  var notifierContainer = d3
+    .select("body")
+    .selectAll(".plotly-notifier")
+    .data([0]);
+  notifierContainer.enter().append("div").classed("plotly-notifier", true);
 
-    var notes = notifierContainer.selectAll('.notifier-note').data(NOTEDATA);
+  var notes = notifierContainer.selectAll(".notifier-note").data(NOTEDATA);
 
-    function killNote(transition) {
-        transition
-            .duration(700)
-            .style('opacity', 0)
-            .each('end', function(thisText) {
-                var thisIndex = NOTEDATA.indexOf(thisText);
-                if(thisIndex !== -1) NOTEDATA.splice(thisIndex, 1);
-                d3.select(this).remove();
-            });
-    }
+  function killNote(transition) {
+    transition
+      .duration(700)
+      .style("opacity", 0)
+      .each("end", function(thisText) {
+        var thisIndex = NOTEDATA.indexOf(thisText);
+        if (thisIndex !== -1) NOTEDATA.splice(thisIndex, 1);
+        d3.select(this).remove();
+      });
+  }
 
-    notes.enter().append('div')
-        .classed('notifier-note', true)
-        .style('opacity', 0)
-        .each(function(thisText) {
-            var note = d3.select(this);
+  notes
+    .enter()
+    .append("div")
+    .classed("notifier-note", true)
+    .style("opacity", 0)
+    .each(function(thisText) {
+      var note = d3.select(this);
 
-            note.append('button')
-                .classed('notifier-close', true)
-                .html('×')
-                .on('click', function() {
-                    note.transition().call(killNote);
-                });
+      note
+        .append("button")
+        .classed("notifier-close", true)
+        .html("×")
+        .on("click", function() {
+          note.transition().call(killNote);
+        });
 
-            var p = note.append('p');
-            var lines = thisText.split(/
/g);
-            for(var i = 0; i < lines.length; i++) {
-                if(i) p.append('br');
-                p.append('span').text(lines[i]);
-            }
+      var p = note.append("p");
+      var lines = thisText.split(/
/g);
+      for (var i = 0; i < lines.length; i++) {
+        if (i) p.append("br");
+        p.append("span").text(lines[i]);
+      }
 
-            note.transition()
-                    .duration(700)
-                    .style('opacity', 1)
-                .transition()
-                    .delay(ts)
-                    .call(killNote);
-        });
+      note
+        .transition()
+        .duration(700)
+        .style("opacity", 1)
+        .transition()
+        .delay(ts)
+        .call(killNote);
+    });
 };
diff --git a/src/lib/override_cursor.js b/src/lib/override_cursor.js
index ebbd290951e..27d25df7ae9 100644
--- a/src/lib/override_cursor.js
+++ b/src/lib/override_cursor.js
@@ -5,14 +5,11 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var setCursor = require("./setcursor");
 
-
-'use strict';
-
-var setCursor = require('./setcursor');
-
-var STASHATTR = 'data-savedcursor';
-var NO_CURSOR = '!!';
+var STASHATTR = "data-savedcursor";
+var NO_CURSOR = "!!";
 
 /*
  * works with our CSS cursor classes (see css/_cursor.scss)
@@ -21,27 +18,25 @@ var NO_CURSOR = '!!';
  * omit cursor to revert to the previously set value.
  */
 module.exports = function overrideCursor(el3, csr) {
-    var savedCursor = el3.attr(STASHATTR);
-    if(csr) {
-        if(!savedCursor) {
-            var classes = (el3.attr('class') || '').split(' ');
-            for(var i = 0; i < classes.length; i++) {
-                var cls = classes[i];
-                if(cls.indexOf('cursor-') === 0) {
-                    el3.attr(STASHATTR, cls.substr(7))
-                        .classed(cls, false);
-                }
-            }
-            if(!el3.attr(STASHATTR)) {
-                el3.attr(STASHATTR, NO_CURSOR);
-            }
+  var savedCursor = el3.attr(STASHATTR);
+  if (csr) {
+    if (!savedCursor) {
+      var classes = (el3.attr("class") || "").split(" ");
+      for (var i = 0; i < classes.length; i++) {
+        var cls = classes[i];
+        if (cls.indexOf("cursor-") === 0) {
+          el3.attr(STASHATTR, cls.substr(7)).classed(cls, false);
         }
-        setCursor(el3, csr);
+      }
+      if (!el3.attr(STASHATTR)) {
+        el3.attr(STASHATTR, NO_CURSOR);
+      }
     }
-    else if(savedCursor) {
-        el3.attr(STASHATTR, null);
+    setCursor(el3, csr);
+  } else if (savedCursor) {
+    el3.attr(STASHATTR, null);
 
-        if(savedCursor === NO_CURSOR) setCursor(el3);
-        else setCursor(el3, savedCursor);
-    }
+    if (savedCursor === NO_CURSOR) setCursor(el3);
+    else setCursor(el3, savedCursor);
+  }
 };
diff --git a/src/lib/polygon.js b/src/lib/polygon.js
index befd593e275..59a94cd29ca 100644
--- a/src/lib/polygon.js
+++ b/src/lib/polygon.js
@@ -5,11 +5,8 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var dot = require('./matrix').dot;
+"use strict";
+var dot = require("./matrix").dot;
 
 var polygon = module.exports = {};
 
@@ -30,133 +27,139 @@ var polygon = module.exports = {};
  *          returns boolean: is pt inside the polygon (including on its edges)
  */
 polygon.tester = function tester(ptsIn) {
-    var pts = ptsIn.slice(),
-        xmin = pts[0][0],
-        xmax = xmin,
-        ymin = pts[0][1],
-        ymax = ymin;
-
-    pts.push(pts[0]);
-    for(var i = 1; i < pts.length; i++) {
-        xmin = Math.min(xmin, pts[i][0]);
-        xmax = Math.max(xmax, pts[i][0]);
-        ymin = Math.min(ymin, pts[i][1]);
-        ymax = Math.max(ymax, pts[i][1]);
+  var pts = ptsIn.slice(),
+    xmin = pts[0][0],
+    xmax = xmin,
+    ymin = pts[0][1],
+    ymax = ymin;
+
+  pts.push(pts[0]);
+  for (var i = 1; i < pts.length; i++) {
+    xmin = Math.min(xmin, pts[i][0]);
+    xmax = Math.max(xmax, pts[i][0]);
+    ymin = Math.min(ymin, pts[i][1]);
+    ymax = Math.max(ymax, pts[i][1]);
+  }
+
+  // do we have a rectangle? Handle this here, so we can use the same
+  // tester for the rectangular case without sacrificing speed
+  var isRect = false, rectFirstEdgeTest;
+
+  if (pts.length === 5) {
+    if (pts[0][0] === pts[1][0]) {
+      // vert, horz, vert, horz
+      if (
+        pts[2][0] === pts[3][0] &&
+          pts[0][1] === pts[3][1] &&
+          pts[1][1] === pts[2][1]
+      ) {
+        isRect = true;
+        rectFirstEdgeTest = function(pt) {
+          return pt[0] === pts[0][0];
+        };
+      }
+    } else if (pts[0][1] === pts[1][1]) {
+      // horz, vert, horz, vert
+      if (
+        pts[2][1] === pts[3][1] &&
+          pts[0][0] === pts[3][0] &&
+          pts[1][0] === pts[2][0]
+      ) {
+        isRect = true;
+        rectFirstEdgeTest = function(pt) {
+          return pt[1] === pts[0][1];
+        };
+      }
     }
+  }
 
-    // do we have a rectangle? Handle this here, so we can use the same
-    // tester for the rectangular case without sacrificing speed
+  function rectContains(pt, omitFirstEdge) {
+    var x = pt[0], y = pt[1];
 
-    var isRect = false,
-        rectFirstEdgeTest;
-
-    if(pts.length === 5) {
-        if(pts[0][0] === pts[1][0]) { // vert, horz, vert, horz
-            if(pts[2][0] === pts[3][0] &&
-                    pts[0][1] === pts[3][1] &&
-                    pts[1][1] === pts[2][1]) {
-                isRect = true;
-                rectFirstEdgeTest = function(pt) { return pt[0] === pts[0][0]; };
-            }
-        }
-        else if(pts[0][1] === pts[1][1]) { // horz, vert, horz, vert
-            if(pts[2][1] === pts[3][1] &&
-                    pts[0][0] === pts[3][0] &&
-                    pts[1][0] === pts[2][0]) {
-                isRect = true;
-                rectFirstEdgeTest = function(pt) { return pt[1] === pts[0][1]; };
-            }
-        }
+    if (x < xmin || x > xmax || y < ymin || y > ymax) {
+      // pt is outside the bounding box of polygon
+      return false;
     }
+    if (omitFirstEdge && rectFirstEdgeTest(pt)) return false;
 
-    function rectContains(pt, omitFirstEdge) {
-        var x = pt[0],
-            y = pt[1];
+    return true;
+  }
 
-        if(x < xmin || x > xmax || y < ymin || y > ymax) {
-            // pt is outside the bounding box of polygon
-            return false;
-        }
-        if(omitFirstEdge && rectFirstEdgeTest(pt)) return false;
+  function contains(pt, omitFirstEdge) {
+    var x = pt[0], y = pt[1];
 
-        return true;
+    if (x < xmin || x > xmax || y < ymin || y > ymax) {
+      // pt is outside the bounding box of polygon
+      return false;
     }
 
-    function contains(pt, omitFirstEdge) {
-        var x = pt[0],
-            y = pt[1];
-
-        if(x < xmin || x > xmax || y < ymin || y > ymax) {
-            // pt is outside the bounding box of polygon
-            return false;
+    var imax = pts.length,
+      x1 = pts[0][0],
+      y1 = pts[0][1],
+      crossings = 0,
+      i,
+      x0,
+      y0,
+      xmini,
+      ycross;
+
+    for (i = 1; i < imax; i++) {
+      // find all crossings of a vertical line upward from pt with
+      // polygon segments
+      // crossings exactly at xmax don't count, unless the point is
+      // exactly on the segment, then it counts as inside.
+      x0 = x1;
+      y0 = y1;
+      x1 = pts[i][0];
+      y1 = pts[i][1];
+      xmini = Math.min(x0, x1);
+
+      // outside the bounding box of this segment, it's only a crossing
+      // if it's below the box.
+      if (x < xmini || x > Math.max(x0, x1) || y > Math.max(y0, y1)) {
+        continue;
+      } else if (y < Math.min(y0, y1)) {
+        // don't count the left-most point of the segment as a crossing
+        // because we don't want to double-count adjacent crossings
+        // UNLESS the polygon turns past vertical at exactly this x
+        // Note that this is repeated below, but we can't factor it out
+        // because
+        if (x !== xmini) crossings++;
+      } else {
+        // inside the bounding box, check the actual line intercept
+        // vertical segment - we know already that the point is exactly
+        // on the segment, so mark the crossing as exactly at the point.
+        if (x1 === x0) {
+          ycross = y;
+        } else {
+          // any other angle
+          ycross = y0 + (x - x0) * (y1 - y0) / (x1 - x0);
         }
 
-        var imax = pts.length,
-            x1 = pts[0][0],
-            y1 = pts[0][1],
-            crossings = 0,
-            i,
-            x0,
-            y0,
-            xmini,
-            ycross;
-
-        for(i = 1; i < imax; i++) {
-            // find all crossings of a vertical line upward from pt with
-            // polygon segments
-            // crossings exactly at xmax don't count, unless the point is
-            // exactly on the segment, then it counts as inside.
-            x0 = x1;
-            y0 = y1;
-            x1 = pts[i][0];
-            y1 = pts[i][1];
-            xmini = Math.min(x0, x1);
-
-            // outside the bounding box of this segment, it's only a crossing
-            // if it's below the box.
-            if(x < xmini || x > Math.max(x0, x1) || y > Math.max(y0, y1)) {
-                continue;
-            }
-            else if(y < Math.min(y0, y1)) {
-                // don't count the left-most point of the segment as a crossing
-                // because we don't want to double-count adjacent crossings
-                // UNLESS the polygon turns past vertical at exactly this x
-                // Note that this is repeated below, but we can't factor it out
-                // because
-                if(x !== xmini) crossings++;
-            }
-            // inside the bounding box, check the actual line intercept
-            else {
-                // vertical segment - we know already that the point is exactly
-                // on the segment, so mark the crossing as exactly at the point.
-                if(x1 === x0) ycross = y;
-                // any other angle
-                else ycross = y0 + (x - x0) * (y1 - y0) / (x1 - x0);
-
-                // exactly on the edge: counts as inside the polygon, unless it's the
-                // first edge and we're omitting it.
-                if(y === ycross) {
-                    if(i === 1 && omitFirstEdge) return false;
-                    return true;
-                }
-
-                if(y <= ycross && x !== xmini) crossings++;
-            }
+        // exactly on the edge: counts as inside the polygon, unless it's the
+        // first edge and we're omitting it.
+        if (y === ycross) {
+          if (i === 1 && omitFirstEdge) return false;
+          return true;
         }
 
-        // if we've gotten this far, odd crossings means inside, even is outside
-        return crossings % 2 === 1;
+        if (y <= ycross && x !== xmini) crossings++;
+      }
     }
 
-    return {
-        xmin: xmin,
-        xmax: xmax,
-        ymin: ymin,
-        ymax: ymax,
-        pts: pts,
-        contains: isRect ? rectContains : contains,
-        isRect: isRect
-    };
+    // if we've gotten this far, odd crossings means inside, even is outside
+    return crossings % 2 === 1;
+  }
+
+  return {
+    xmin: xmin,
+    xmax: xmax,
+    ymin: ymin,
+    ymax: ymax,
+    pts: pts,
+    contains: isRect ? rectContains : contains,
+    isRect: isRect
+  };
 };
 
 /**
@@ -169,24 +172,34 @@ polygon.tester = function tester(ptsIn) {
  *      before the line counts as bent
  * @returns boolean: true means this segment is bent, false means straight
  */
-var isBent = polygon.isSegmentBent = function isBent(pts, start, end, tolerance) {
-    var startPt = pts[start],
-        segment = [pts[end][0] - startPt[0], pts[end][1] - startPt[1]],
-        segmentSquared = dot(segment, segment),
-        segmentLen = Math.sqrt(segmentSquared),
-        unitPerp = [-segment[1] / segmentLen, segment[0] / segmentLen],
-        i,
-        part,
-        partParallel;
-
-    for(i = start + 1; i < end; i++) {
-        part = [pts[i][0] - startPt[0], pts[i][1] - startPt[1]];
-        partParallel = dot(part, segment);
-
-        if(partParallel < 0 || partParallel > segmentSquared ||
-            Math.abs(dot(part, unitPerp)) > tolerance) return true;
+var isBent = polygon.isSegmentBent = function isBent(
+  pts,
+  start,
+  end,
+  tolerance
+) {
+  var startPt = pts[start],
+    segment = [pts[end][0] - startPt[0], pts[end][1] - startPt[1]],
+    segmentSquared = dot(segment, segment),
+    segmentLen = Math.sqrt(segmentSquared),
+    unitPerp = [(-segment[1]) / segmentLen, segment[0] / segmentLen],
+    i,
+    part,
+    partParallel;
+
+  for (i = start + 1; i < end; i++) {
+    part = [pts[i][0] - startPt[0], pts[i][1] - startPt[1]];
+    partParallel = dot(part, segment);
+
+    if (
+      partParallel < 0 ||
+        partParallel > segmentSquared ||
+        Math.abs(dot(part, unitPerp)) > tolerance
+    ) {
+      return true;
     }
-    return false;
+  }
+  return false;
 };
 
 /**
@@ -203,36 +216,29 @@ var isBent = polygon.isSegmentBent = function isBent(pts, start, end, tolerance)
  *      filtered is the resulting filtered Array of [x, y] pairs
  */
 polygon.filter = function filter(pts, tolerance) {
-    var ptsFiltered = [pts[0]],
-        doneRawIndex = 0,
-        doneFilteredIndex = 0;
-
-    function addPt(pt) {
-        pts.push(pt);
-        var prevFilterLen = ptsFiltered.length,
-            iLast = doneRawIndex;
-        ptsFiltered.splice(doneFilteredIndex + 1);
-
-        for(var i = iLast + 1; i < pts.length; i++) {
-            if(i === pts.length - 1 || isBent(pts, iLast, i + 1, tolerance)) {
-                ptsFiltered.push(pts[i]);
-                if(ptsFiltered.length < prevFilterLen - 2) {
-                    doneRawIndex = i;
-                    doneFilteredIndex = ptsFiltered.length - 1;
-                }
-                iLast = i;
-            }
+  var ptsFiltered = [pts[0]], doneRawIndex = 0, doneFilteredIndex = 0;
+
+  function addPt(pt) {
+    pts.push(pt);
+    var prevFilterLen = ptsFiltered.length, iLast = doneRawIndex;
+    ptsFiltered.splice(doneFilteredIndex + 1);
+
+    for (var i = iLast + 1; i < pts.length; i++) {
+      if (i === pts.length - 1 || isBent(pts, iLast, i + 1, tolerance)) {
+        ptsFiltered.push(pts[i]);
+        if (ptsFiltered.length < prevFilterLen - 2) {
+          doneRawIndex = i;
+          doneFilteredIndex = ptsFiltered.length - 1;
         }
+        iLast = i;
+      }
     }
+  }
 
-    if(pts.length > 1) {
-        var lastPt = pts.pop();
-        addPt(lastPt);
-    }
+  if (pts.length > 1) {
+    var lastPt = pts.pop();
+    addPt(lastPt);
+  }
 
-    return {
-        addPt: addPt,
-        raw: pts,
-        filtered: ptsFiltered
-    };
+  return { addPt: addPt, raw: pts, filtered: ptsFiltered };
 };
diff --git a/src/lib/queue.js b/src/lib/queue.js
index 815fd915723..e793887d047 100644
--- a/src/lib/queue.js
+++ b/src/lib/queue.js
@@ -5,13 +5,9 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var Lib = require('../lib');
-var config = require('../plot_api/plot_config');
-
+"use strict";
+var Lib = require("../lib");
+var config = require("../plot_api/plot_config");
 
 /**
  * Copy arg array *without* removing `undefined` values from objects.
@@ -21,34 +17,32 @@ var config = require('../plot_api/plot_config');
  * @returns {Array}
  */
 function copyArgArray(gd, args) {
-    var copy = [];
-    var arg;
-
-    for(var i = 0; i < args.length; i++) {
-        arg = args[i];
-
-        if(arg === gd) copy[i] = arg;
-        else if(typeof arg === 'object') {
-            copy[i] = Array.isArray(arg) ?
-                Lib.extendDeep([], arg) :
-                Lib.extendDeepAll({}, arg);
-        }
-        else copy[i] = arg;
+  var copy = [];
+  var arg;
+
+  for (var i = 0; i < args.length; i++) {
+    arg = args[i];
+
+    if (arg === gd) {
+      copy[i] = arg;
+    } else if (typeof arg === "object") {
+      copy[i] = Array.isArray(arg)
+        ? Lib.extendDeep([], arg)
+        : Lib.extendDeepAll({}, arg);
+    } else {
+      copy[i] = arg;
     }
+  }
 
-    return copy;
+  return copy;
 }
 
-
 // -----------------------------------------------------
 // Undo/Redo queue for plots
 // -----------------------------------------------------
-
-
 var queue = {};
 
 // TODO: disable/enable undo and redo buttons appropriately
-
 /**
  * Add an item to the undoQueue for a graphDiv
  *
@@ -59,42 +53,45 @@ var queue = {};
  * @param redoArgs Args to supply redoFunc with
  */
 queue.add = function(gd, undoFunc, undoArgs, redoFunc, redoArgs) {
-    var queueObj,
-        queueIndex;
-
-    // make sure we have the queue and our position in it
-    gd.undoQueue = gd.undoQueue || {index: 0, queue: [], sequence: false};
-    queueIndex = gd.undoQueue.index;
-
-    // if we're already playing an undo or redo, or if this is an auto operation
-    // (like pane resize... any others?) then we don't save this to the undo queue
-    if(gd.autoplay) {
-        if(!gd.undoQueue.inSequence) gd.autoplay = false;
-        return;
-    }
-
-    // if we're not in a sequence or are just starting, we need a new queue item
-    if(!gd.undoQueue.sequence || gd.undoQueue.beginSequence) {
-        queueObj = {undo: {calls: [], args: []}, redo: {calls: [], args: []}};
-        gd.undoQueue.queue.splice(queueIndex, gd.undoQueue.queue.length - queueIndex, queueObj);
-        gd.undoQueue.index += 1;
-    } else {
-        queueObj = gd.undoQueue.queue[queueIndex - 1];
-    }
-    gd.undoQueue.beginSequence = false;
-
-    // we unshift to handle calls for undo in a forward for loop later
-    if(queueObj) {
-        queueObj.undo.calls.unshift(undoFunc);
-        queueObj.undo.args.unshift(undoArgs);
-        queueObj.redo.calls.push(redoFunc);
-        queueObj.redo.args.push(redoArgs);
-    }
-
-    if(gd.undoQueue.queue.length > config.queueLength) {
-        gd.undoQueue.queue.shift();
-        gd.undoQueue.index--;
-    }
+  var queueObj, queueIndex;
+
+  // make sure we have the queue and our position in it
+  gd.undoQueue = gd.undoQueue || { index: 0, queue: [], sequence: false };
+  queueIndex = gd.undoQueue.index;
+
+  // if we're already playing an undo or redo, or if this is an auto operation
+  // (like pane resize... any others?) then we don't save this to the undo queue
+  if (gd.autoplay) {
+    if (!gd.undoQueue.inSequence) gd.autoplay = false;
+    return;
+  }
+
+  // if we're not in a sequence or are just starting, we need a new queue item
+  if (!gd.undoQueue.sequence || gd.undoQueue.beginSequence) {
+    queueObj = { undo: { calls: [], args: [] }, redo: { calls: [], args: [] } };
+    gd.undoQueue.queue.splice(
+      queueIndex,
+      gd.undoQueue.queue.length - queueIndex,
+      queueObj
+    );
+    gd.undoQueue.index += 1;
+  } else {
+    queueObj = gd.undoQueue.queue[queueIndex - 1];
+  }
+  gd.undoQueue.beginSequence = false;
+
+  // we unshift to handle calls for undo in a forward for loop later
+  if (queueObj) {
+    queueObj.undo.calls.unshift(undoFunc);
+    queueObj.undo.args.unshift(undoArgs);
+    queueObj.redo.calls.push(redoFunc);
+    queueObj.redo.args.push(redoArgs);
+  }
+
+  if (gd.undoQueue.queue.length > config.queueLength) {
+    gd.undoQueue.queue.shift();
+    gd.undoQueue.index--;
+  }
 };
 
 /**
@@ -103,9 +100,9 @@ queue.add = function(gd, undoFunc, undoArgs, redoFunc, redoArgs) {
  * @param gd
  */
 queue.startSequence = function(gd) {
-    gd.undoQueue = gd.undoQueue || {index: 0, queue: [], sequence: false};
-    gd.undoQueue.sequence = true;
-    gd.undoQueue.beginSequence = true;
+  gd.undoQueue = gd.undoQueue || { index: 0, queue: [], sequence: false };
+  gd.undoQueue.sequence = true;
+  gd.undoQueue.beginSequence = true;
 };
 
 /**
@@ -116,9 +113,9 @@ queue.startSequence = function(gd) {
  * @param gd
  */
 queue.stopSequence = function(gd) {
-    gd.undoQueue = gd.undoQueue || {index: 0, queue: [], sequence: false};
-    gd.undoQueue.sequence = false;
-    gd.undoQueue.beginSequence = false;
+  gd.undoQueue = gd.undoQueue || { index: 0, queue: [], sequence: false };
+  gd.undoQueue.sequence = false;
+  gd.undoQueue.beginSequence = false;
 };
 
 /**
@@ -127,31 +124,33 @@ queue.stopSequence = function(gd) {
  * @param gd
  */
 queue.undo = function undo(gd) {
-    var queueObj, i;
-
-    if(gd.framework && gd.framework.isPolar) {
-        gd.framework.undo();
-        return;
-    }
-    if(gd.undoQueue === undefined ||
-            isNaN(gd.undoQueue.index) ||
-            gd.undoQueue.index <= 0) {
-        return;
-    }
-
-    // index is pointing to next *forward* queueObj, point to the one we're undoing
-    gd.undoQueue.index--;
-
-    // get the queueObj for instructions on how to undo
-    queueObj = gd.undoQueue.queue[gd.undoQueue.index];
-
-    // this sequence keeps things from adding to the queue during undo/redo
-    gd.undoQueue.inSequence = true;
-    for(i = 0; i < queueObj.undo.calls.length; i++) {
-        queue.plotDo(gd, queueObj.undo.calls[i], queueObj.undo.args[i]);
-    }
-    gd.undoQueue.inSequence = false;
-    gd.autoplay = false;
+  var queueObj, i;
+
+  if (gd.framework && gd.framework.isPolar) {
+    gd.framework.undo();
+    return;
+  }
+  if (
+    gd.undoQueue === undefined ||
+      isNaN(gd.undoQueue.index) ||
+      gd.undoQueue.index <= 0
+  ) {
+    return;
+  }
+
+  // index is pointing to next *forward* queueObj, point to the one we're undoing
+  gd.undoQueue.index--;
+
+  // get the queueObj for instructions on how to undo
+  queueObj = gd.undoQueue.queue[gd.undoQueue.index];
+
+  // this sequence keeps things from adding to the queue during undo/redo
+  gd.undoQueue.inSequence = true;
+  for (i = 0; i < queueObj.undo.calls.length; i++) {
+    queue.plotDo(gd, queueObj.undo.calls[i], queueObj.undo.args[i]);
+  }
+  gd.undoQueue.inSequence = false;
+  gd.autoplay = false;
 };
 
 /**
@@ -160,31 +159,33 @@ queue.undo = function undo(gd) {
  * @param gd
  */
 queue.redo = function redo(gd) {
-    var queueObj, i;
-
-    if(gd.framework && gd.framework.isPolar) {
-        gd.framework.redo();
-        return;
-    }
-    if(gd.undoQueue === undefined ||
-            isNaN(gd.undoQueue.index) ||
-            gd.undoQueue.index >= gd.undoQueue.queue.length) {
-        return;
-    }
-
-    // get the queueObj for instructions on how to undo
-    queueObj = gd.undoQueue.queue[gd.undoQueue.index];
-
-    // this sequence keeps things from adding to the queue during undo/redo
-    gd.undoQueue.inSequence = true;
-    for(i = 0; i < queueObj.redo.calls.length; i++) {
-        queue.plotDo(gd, queueObj.redo.calls[i], queueObj.redo.args[i]);
-    }
-    gd.undoQueue.inSequence = false;
-    gd.autoplay = false;
-
-    // index is pointing to the thing we just redid, move it
-    gd.undoQueue.index++;
+  var queueObj, i;
+
+  if (gd.framework && gd.framework.isPolar) {
+    gd.framework.redo();
+    return;
+  }
+  if (
+    gd.undoQueue === undefined ||
+      isNaN(gd.undoQueue.index) ||
+      gd.undoQueue.index >= gd.undoQueue.queue.length
+  ) {
+    return;
+  }
+
+  // get the queueObj for instructions on how to undo
+  queueObj = gd.undoQueue.queue[gd.undoQueue.index];
+
+  // this sequence keeps things from adding to the queue during undo/redo
+  gd.undoQueue.inSequence = true;
+  for (i = 0; i < queueObj.redo.calls.length; i++) {
+    queue.plotDo(gd, queueObj.redo.calls[i], queueObj.redo.args[i]);
+  }
+  gd.undoQueue.inSequence = false;
+  gd.autoplay = false;
+
+  // index is pointing to the thing we just redid, move it
+  gd.undoQueue.index++;
 };
 
 /**
@@ -197,13 +198,13 @@ queue.redo = function redo(gd) {
  * @param args
  */
 queue.plotDo = function(gd, func, args) {
-    gd.autoplay = true;
+  gd.autoplay = true;
 
-    // this *won't* copy gd and it preserves `undefined` properties!
-    args = copyArgArray(gd, args);
+  // this *won't* copy gd and it preserves `undefined` properties!
+  args = copyArgArray(gd, args);
 
-    // call the supplied function
-    func.apply(null, args);
+  // call the supplied function
+  func.apply(null, args);
 };
 
 module.exports = queue;
diff --git a/src/lib/search.js b/src/lib/search.js
index 8cb4275f1f3..e779a81106f 100644
--- a/src/lib/search.js
+++ b/src/lib/search.js
@@ -5,13 +5,9 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-var loggers = require('./loggers');
-
+"use strict";
+var isNumeric = require("fast-isnumeric");
+var loggers = require("./loggers");
 
 /**
  * findBin - find the bin for val - note that it can return outside the
@@ -24,40 +20,47 @@ var loggers = require('./loggers');
  * the lower bin rather than the default upper bin
  */
 exports.findBin = function(val, bins, linelow) {
-    if(isNumeric(bins.start)) {
-        return linelow ?
-            Math.ceil((val - bins.start) / bins.size) - 1 :
-            Math.floor((val - bins.start) / bins.size);
+  if (isNumeric(bins.start)) {
+    return linelow
+      ? Math.ceil((val - bins.start) / bins.size) - 1
+      : Math.floor((val - bins.start) / bins.size);
+  } else {
+    var n1 = 0, n2 = bins.length, c = 0, n, test;
+    if (bins[bins.length - 1] >= bins[0]) {
+      test = linelow ? lessThan : lessOrEqual;
+    } else {
+      test = linelow ? greaterOrEqual : greaterThan;
     }
-    else {
-        var n1 = 0,
-            n2 = bins.length,
-            c = 0,
-            n,
-            test;
-        if(bins[bins.length - 1] >= bins[0]) {
-            test = linelow ? lessThan : lessOrEqual;
-        } else {
-            test = linelow ? greaterOrEqual : greaterThan;
-        }
-        // c is just to avoid infinite loops if there's an error
-        while(n1 < n2 && c++ < 100) {
-            n = Math.floor((n1 + n2) / 2);
-            if(test(bins[n], val)) n1 = n + 1;
-            else n2 = n;
-        }
-        if(c > 90) loggers.log('Long binary search...');
-        return n1 - 1;
+    // c is just to avoid infinite loops if there's an error
+    while (n1 < n2 && c++ < 100) {
+      n = Math.floor((n1 + n2) / 2);
+      if (test(bins[n], val)) n1 = n + 1;
+      else n2 = n;
     }
+    if (c > 90) loggers.log("Long binary search...");
+    return n1 - 1;
+  }
 };
 
-function lessThan(a, b) { return a < b; }
-function lessOrEqual(a, b) { return a <= b; }
-function greaterThan(a, b) { return a > b; }
-function greaterOrEqual(a, b) { return a >= b; }
+function lessThan(a, b) {
+  return a < b;
+}
+function lessOrEqual(a, b) {
+  return a <= b;
+}
+function greaterThan(a, b) {
+  return a > b;
+}
+function greaterOrEqual(a, b) {
+  return a >= b;
+}
 
-exports.sorterAsc = function(a, b) { return a - b; };
-exports.sorterDes = function(a, b) { return b - a; };
+exports.sorterAsc = function(a, b) {
+  return a - b;
+};
+exports.sorterDes = function(a, b) {
+  return b - a;
+};
 
 /**
  * find distinct values in an array, lumping together ones that appear to
@@ -65,23 +68,24 @@ exports.sorterDes = function(a, b) { return b - a; };
  * return the distinct values and the minimum difference between any two
  */
 exports.distinctVals = function(valsIn) {
-    var vals = valsIn.slice();  // otherwise we sort the original array...
-    vals.sort(exports.sorterAsc);
+  var vals = valsIn.slice();
+  // otherwise we sort the original array...
+  vals.sort(exports.sorterAsc);
 
-    var l = vals.length - 1,
-        minDiff = (vals[l] - vals[0]) || 1,
-        errDiff = minDiff / (l || 1) / 10000,
-        v2 = [vals[0]];
+  var l = vals.length - 1,
+    minDiff = vals[l] - vals[0] || 1,
+    errDiff = minDiff / (l || 1) / 10000,
+    v2 = [vals[0]];
 
-    for(var i = 0; i < l; i++) {
-        // make sure values aren't just off by a rounding error
-        if(vals[i + 1] > vals[i] + errDiff) {
-            minDiff = Math.min(minDiff, vals[i + 1] - vals[i]);
-            v2.push(vals[i + 1]);
-        }
+  for (var i = 0; i < l; i++) {
+    // make sure values aren't just off by a rounding error
+    if (vals[i + 1] > vals[i] + errDiff) {
+      minDiff = Math.min(minDiff, vals[i + 1] - vals[i]);
+      v2.push(vals[i + 1]);
     }
+  }
 
-    return {vals: v2, minDiff: minDiff};
+  return { vals: v2, minDiff: minDiff };
 };
 
 /**
@@ -92,18 +96,18 @@ exports.distinctVals = function(valsIn) {
  * binary search is probably overkill here...
  */
 exports.roundUp = function(val, arrayIn, reverse) {
-    var low = 0,
-        high = arrayIn.length - 1,
-        mid,
-        c = 0,
-        dlow = reverse ? 0 : 1,
-        dhigh = reverse ? 1 : 0,
-        rounded = reverse ? Math.ceil : Math.floor;
-    // c is just to avoid infinite loops if there's an error
-    while(low < high && c++ < 100) {
-        mid = rounded((low + high) / 2);
-        if(arrayIn[mid] <= val) low = mid + dlow;
-        else high = mid - dhigh;
-    }
-    return arrayIn[low];
+  var low = 0,
+    high = arrayIn.length - 1,
+    mid,
+    c = 0,
+    dlow = reverse ? 0 : 1,
+    dhigh = reverse ? 1 : 0,
+    rounded = reverse ? Math.ceil : Math.floor;
+  // c is just to avoid infinite loops if there's an error
+  while (low < high && c++ < 100) {
+    mid = rounded((low + high) / 2);
+    if (arrayIn[mid] <= val) low = mid + dlow;
+    else high = mid - dhigh;
+  }
+  return arrayIn[low];
 };
diff --git a/src/lib/setcursor.js b/src/lib/setcursor.js
index ef70880a1d5..007605d775c 100644
--- a/src/lib/setcursor.js
+++ b/src/lib/setcursor.js
@@ -5,17 +5,14 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 // works with our CSS cursor classes (see css/_cursor.scss)
 // to apply cursors to d3 single-element selections.
 // omit cursor to revert to the default.
 module.exports = function setCursor(el3, csr) {
-    (el3.attr('class') || '').split(' ').forEach(function(cls) {
-        if(cls.indexOf('cursor-') === 0) el3.classed(cls, false);
-    });
+  (el3.attr("class") || "").split(" ").forEach(function(cls) {
+    if (cls.indexOf("cursor-") === 0) el3.classed(cls, false);
+  });
 
-    if(csr) el3.classed('cursor-' + csr, true);
+  if (csr) el3.classed("cursor-" + csr, true);
 };
diff --git a/src/lib/show_no_webgl_msg.js b/src/lib/show_no_webgl_msg.js
index 40b84c680bf..a952b1ac2e6 100644
--- a/src/lib/show_no_webgl_msg.js
+++ b/src/lib/show_no_webgl_msg.js
@@ -5,15 +5,11 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var Color = require('../components/color');
+"use strict";
+var Color = require("../components/color");
 
 var noop = function() {};
 
-
 /**
  * Prints a no webgl error message into the scene container
  * @param {scene instance} scene
@@ -22,26 +18,26 @@ var noop = function() {};
  *
  */
 module.exports = function showWebGlMsg(scene) {
-    for(var prop in scene) {
-        if(typeof scene[prop] === 'function') scene[prop] = noop;
-    }
-
-    scene.destroy = function() {
-        scene.container.parentNode.removeChild(scene.container);
-    };
-
-    var div = document.createElement('div');
-    div.textContent = 'Webgl is not supported by your browser - visit http://get.webgl.org for more info';
-    div.style.cursor = 'pointer';
-    div.style.fontSize = '24px';
-    div.style.color = Color.defaults[0];
-
-    scene.container.appendChild(div);
-    scene.container.style.background = '#FFFFFF';
-    scene.container.onclick = function() {
-        window.open('http://get.webgl.org');
-    };
-
-    // return before setting up camera and onrender methods
-    return false;
+  for (var prop in scene) {
+    if (typeof scene[prop] === "function") scene[prop] = noop;
+  }
+
+  scene.destroy = function() {
+    scene.container.parentNode.removeChild(scene.container);
+  };
+
+  var div = document.createElement("div");
+  div.textContent = "Webgl is not supported by your browser - visit http://get.webgl.org for more info";
+  div.style.cursor = "pointer";
+  div.style.fontSize = "24px";
+  div.style.color = Color.defaults[0];
+
+  scene.container.appendChild(div);
+  scene.container.style.background = "#FFFFFF";
+  scene.container.onclick = function() {
+    window.open("http://get.webgl.org");
+  };
+
+  // return before setting up camera and onrender methods
+  return false;
 };
diff --git a/src/lib/stats.js b/src/lib/stats.js
index a365d4ce33f..37e2718354c 100644
--- a/src/lib/stats.js
+++ b/src/lib/stats.js
@@ -5,12 +5,8 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-
+"use strict";
+var isNumeric = require("fast-isnumeric");
 
 /**
  * aggNums() returns the result of an aggregate function applied to an array of
@@ -26,21 +22,22 @@ var isNumeric = require('fast-isnumeric');
  * @return {Number} - result of f applied to a starting from v
  */
 exports.aggNums = function(f, v, a, len) {
-    var i,
-        b;
-    if(!len) len = a.length;
-    if(!isNumeric(v)) v = false;
-    if(Array.isArray(a[0])) {
-        b = new Array(len);
-        for(i = 0; i < len; i++) b[i] = exports.aggNums(f, v, a[i]);
-        a = b;
+  var i, b;
+  if (!len) len = a.length;
+  if (!isNumeric(v)) v = false;
+  if (Array.isArray(a[0])) {
+    b = new Array(len);
+    for (i = 0; i < len; i++) {
+      b[i] = exports.aggNums(f, v, a[i]);
     }
+    a = b;
+  }
 
-    for(i = 0; i < len; i++) {
-        if(!isNumeric(v)) v = a[i];
-        else if(isNumeric(a[i])) v = f(+v, +a[i]);
-    }
-    return v;
+  for (i = 0; i < len; i++) {
+    if (!isNumeric(v)) v = a[i];
+    else if (isNumeric(a[i])) v = f(+v, +a[i]);
+  }
+  return v;
 };
 
 /**
@@ -48,25 +45,43 @@ exports.aggNums = function(f, v, a, len) {
  * even need to use aggNums instead of .length, to toss out non-numerics
  */
 exports.len = function(data) {
-    return exports.aggNums(function(a) { return a + 1; }, 0, data);
+  return exports.aggNums(
+    function(a) {
+      return a + 1;
+    },
+    0,
+    data
+  );
 };
 
 exports.mean = function(data, len) {
-    if(!len) len = exports.len(data);
-    return exports.aggNums(function(a, b) { return a + b; }, 0, data) / len;
+  if (!len) len = exports.len(data);
+  return exports.aggNums(
+    function(a, b) {
+      return a + b;
+    },
+    0,
+    data
+  ) /
+    len;
 };
 
 exports.variance = function(data, len, mean) {
-    if(!len) len = exports.len(data);
-    if(!isNumeric(mean)) mean = exports.mean(data, len);
+  if (!len) len = exports.len(data);
+  if (!isNumeric(mean)) mean = exports.mean(data, len);
 
-    return exports.aggNums(function(a, b) {
-        return a + Math.pow(b - mean, 2);
-    }, 0, data) / len;
+  return exports.aggNums(
+    function(a, b) {
+      return a + Math.pow(b - mean, 2);
+    },
+    0,
+    data
+  ) /
+    len;
 };
 
 exports.stdev = function(data, len, mean) {
-    return Math.sqrt(exports.variance(data, len, mean));
+  return Math.sqrt(exports.variance(data, len, mean));
 };
 
 /**
@@ -85,10 +100,10 @@ exports.stdev = function(data, len, mean) {
  * @return {Number} - percentile
  */
 exports.interp = function(arr, n) {
-    if(!isNumeric(n)) throw 'n should be a finite number';
-    n = n * arr.length - 0.5;
-    if(n < 0) return arr[0];
-    if(n > arr.length - 1) return arr[arr.length - 1];
-    var frac = n % 1;
-    return frac * arr[Math.ceil(n)] + (1 - frac) * arr[Math.floor(n)];
+  if (!isNumeric(n)) throw "n should be a finite number";
+  n = n * arr.length - 0.5;
+  if (n < 0) return arr[0];
+  if (n > arr.length - 1) return arr[arr.length - 1];
+  var frac = n % 1;
+  return frac * arr[Math.ceil(n)] + (1 - frac) * arr[Math.floor(n)];
 };
diff --git a/src/lib/str2rgbarray.js b/src/lib/str2rgbarray.js
index 0dd3c027fce..f35fa8ecb80 100644
--- a/src/lib/str2rgbarray.js
+++ b/src/lib/str2rgbarray.js
@@ -5,16 +5,13 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var tinycolor = require('tinycolor2');
-var arrtools = require('arraytools');
+"use strict";
+var tinycolor = require("tinycolor2");
+var arrtools = require("arraytools");
 
 function str2RgbaArray(color) {
-    color = tinycolor(color);
-    return arrtools.str2RgbaArray(color.toRgbString());
+  color = tinycolor(color);
+  return arrtools.str2RgbaArray(color.toRgbString());
 }
 
 module.exports = str2RgbaArray;
diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js
index a55334d5157..9aa02e19aa2 100644
--- a/src/lib/svg_text_utils.js
+++ b/src/lib/svg_text_utils.js
@@ -5,298 +5,324 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 /* global MathJax:false */
 
-var d3 = require('d3');
+var d3 = require("d3");
 
-var Lib = require('../lib');
-var xmlnsNamespaces = require('../constants/xmlns_namespaces');
-var stringMappings = require('../constants/string_mappings');
+var Lib = require("../lib");
+var xmlnsNamespaces = require("../constants/xmlns_namespaces");
+var stringMappings = require("../constants/string_mappings");
 
 // Append SVG
-
 d3.selection.prototype.appendSVG = function(_svgString) {
-    var skeleton = [
-        ''
-    ].join('');
-
-    var dom = new DOMParser().parseFromString(skeleton, 'application/xml'),
-        childNode = dom.documentElement.firstChild;
-
-    while(childNode) {
-        this.node().appendChild(this.node().ownerDocument.importNode(childNode, true));
-        childNode = childNode.nextSibling;
-    }
-    if(dom.querySelector('parsererror')) {
-        Lib.log(dom.querySelector('parsererror div').textContent);
-        return null;
-    }
-    return d3.select(this.node().lastChild);
+  var skeleton = [
+    '"
+  ].join("");
+
+  var dom = new DOMParser().parseFromString(skeleton, "application/xml"),
+    childNode = dom.documentElement.firstChild;
+
+  while (childNode) {
+    this
+      .node()
+      .appendChild(this.node().ownerDocument.importNode(childNode, true));
+    childNode = childNode.nextSibling;
+  }
+  if (dom.querySelector("parsererror")) {
+    Lib.log(dom.querySelector("parsererror div").textContent);
+    return null;
+  }
+  return d3.select(this.node().lastChild);
 };
 
 // Text utilities
-
 exports.html_entity_decode = function(s) {
-    var hiddenDiv = d3.select('body').append('div').style({display: 'none'}).html('');
-    var replaced = s.replace(/(&[^;]*;)/gi, function(d) {
-        if(d === '<') { return '<'; } // special handling for brackets
-        if(d === '&rt;') { return '>'; }
-        if(d.indexOf('<') !== -1 || d.indexOf('>') !== -1) { return ''; }
-        return hiddenDiv.html(d).text(); // everything else, let the browser decode it to unicode
-    });
-    hiddenDiv.remove();
-    return replaced;
+  var hiddenDiv = d3
+    .select("body")
+    .append("div")
+    .style({ display: "none" })
+    .html("");
+  var replaced = s.replace(/(&[^;]*;)/gi, function(d) {
+    if (d === "<") {
+      return "<";
+    }
+    // special handling for brackets
+    if (d === "&rt;") {
+      return ">";
+    }
+    if (d.indexOf("<") !== -1 || d.indexOf(">") !== -1) {
+      return "";
+    }
+    return hiddenDiv.html(d).text(); // everything else, let the browser decode it to unicode
+  });
+  hiddenDiv.remove();
+  return replaced;
 };
 
 exports.xml_entity_encode = function(str) {
-    return str.replace(/&(?!\w+;|\#[0-9]+;| \#x[0-9A-F]+;)/g, '&');
+  return str.replace(/&(?!\w+;|\#[0-9]+;| \#x[0-9A-F]+;)/g, "&");
 };
 
 // text converter
-
 function getSize(_selection, _dimension) {
-    return _selection.node().getBoundingClientRect()[_dimension];
+  return _selection.node().getBoundingClientRect()[_dimension];
 }
 
 exports.convertToTspans = function(_context, _callback) {
-    var str = _context.text();
-    var converted = convertToSVG(str);
-    var that = _context;
-
-    // Until we get tex integrated more fully (so it can be used along with non-tex)
-    // allow some elements to prohibit it by attaching 'data-notex' to the original
-    var tex = (!that.attr('data-notex')) && converted.match(/([^$]*)([$]+[^$]*[$]+)([^$]*)/);
-    var result = str;
-    var parent = d3.select(that.node().parentNode);
-    if(parent.empty()) return;
-    var svgClass = (that.attr('class')) ? that.attr('class').split(' ')[0] : 'text';
-    svgClass += '-math';
-    parent.selectAll('svg.' + svgClass).remove();
-    parent.selectAll('g.' + svgClass + '-group').remove();
-    _context.style({visibility: null});
-    for(var up = _context.node(); up && up.removeAttribute; up = up.parentNode) {
-        up.removeAttribute('data-bb');
+  var str = _context.text();
+  var converted = convertToSVG(str);
+  var that = _context;
+
+  // Until we get tex integrated more fully (so it can be used along with non-tex)
+  // allow some elements to prohibit it by attaching 'data-notex' to the original
+  var tex = !that.attr("data-notex") &&
+    converted.match(/([^$]*)([$]+[^$]*[$]+)([^$]*)/);
+  var result = str;
+  var parent = d3.select(that.node().parentNode);
+  if (parent.empty()) return;
+  var svgClass = that.attr("class") ? that.attr("class").split(" ")[0] : "text";
+  svgClass += "-math";
+  parent.selectAll("svg." + svgClass).remove();
+  parent.selectAll("g." + svgClass + "-group").remove();
+  _context.style({ visibility: null });
+  for (var up = _context.node(); up && up.removeAttribute; up = up.parentNode) {
+    up.removeAttribute("data-bb");
+  }
+
+  function showText() {
+    if (!parent.empty()) {
+      svgClass = that.attr("class") + "-math";
+      parent.select("svg." + svgClass).remove();
+    }
+    _context.text("").style({ visibility: "inherit", "white-space": "pre" });
+
+    result = _context.appendSVG(converted);
+
+    if (!result) _context.text(str);
+
+    if (_context.select("a").size()) {
+      // at least in Chrome, pointer-events does not seem
+      // to be honored in children of  elements
+      // so if we have an anchor, we have to make the
+      // whole element respond
+      _context.style("pointer-events", "all");
     }
 
-    function showText() {
-        if(!parent.empty()) {
-            svgClass = that.attr('class') + '-math';
-            parent.select('svg.' + svgClass).remove();
+    if (_callback) _callback.call(that);
+  }
+
+  if (tex) {
+    var gd = Lib.getPlotDiv(that.node());
+    (gd && gd._promises || []).push(new Promise(function(resolve) {
+      that.style({ visibility: "hidden" });
+      var config = { fontSize: parseInt(that.style("font-size"), 10) };
+
+      texToSVG(tex[2], config, function(_svgEl, _glyphDefs, _svgBBox) {
+        parent.selectAll("svg." + svgClass).remove();
+        parent.selectAll("g." + svgClass + "-group").remove();
+
+        var newSvg = _svgEl && _svgEl.select("svg");
+        if (!newSvg || !newSvg.node()) {
+          showText();
+          resolve();
+          return;
         }
-        _context.text('')
-            .style({
-                visibility: 'inherit',
-                'white-space': 'pre'
-            });
-
-        result = _context.appendSVG(converted);
-
-        if(!result) _context.text(str);
-
-        if(_context.select('a').size()) {
-            // at least in Chrome, pointer-events does not seem
-            // to be honored in children of  elements
-            // so if we have an anchor, we have to make the
-            // whole element respond
-            _context.style('pointer-events', 'all');
+
+        var mathjaxGroup = parent
+          .append("g")
+          .classed(svgClass + "-group", true)
+          .attr({ "pointer-events": "none" });
+
+        mathjaxGroup.node().appendChild(newSvg.node());
+
+        // stitch the glyph defs
+        if (_glyphDefs && _glyphDefs.node()) {
+          newSvg
+            .node()
+            .insertBefore(
+              _glyphDefs.node().cloneNode(true),
+              newSvg.node().firstChild
+            );
         }
 
-        if(_callback) _callback.call(that);
-    }
+        newSvg
+          .attr({
+            class: svgClass,
+            height: _svgBBox.height,
+            preserveAspectRatio: "xMinYMin meet"
+          })
+          .style({ overflow: "visible", "pointer-events": "none" });
+
+        var fill = that.style("fill") || "black";
+        newSvg.select("g").attr({ fill: fill, stroke: fill });
+
+        var newSvgW = getSize(newSvg, "width"),
+          newSvgH = getSize(newSvg, "height"),
+          newX = +that.attr("x") -
+            newSvgW *
+              ({ start: 0, middle: 0.5, end: 1 })[
+                that.attr("text-anchor") || "start"
+              ],
+          // font baseline is about 1/4 fontSize below centerline
+          textHeight = parseInt(that.style("font-size"), 10) ||
+            getSize(that, "height"),
+          dy = (-textHeight) / 4;
+
+        if (svgClass[0] === "y") {
+          mathjaxGroup.attr({
+            transform: (
+              "rotate(" +
+                [-90, +that.attr("x"), +that.attr("y")] +
+                ") translate(" +
+                [(-newSvgW) / 2, dy - newSvgH / 2] +
+                ")"
+            )
+          });
+          newSvg.attr({ x: +that.attr("x"), y: +that.attr("y") });
+        } else if (svgClass[0] === "l") {
+          newSvg.attr({ x: that.attr("x"), y: dy - newSvgH / 2 });
+        } else if (svgClass[0] === "a") {
+          newSvg.attr({ x: 0, y: dy });
+        } else {
+          newSvg.attr({ x: newX, y: +that.attr("y") + dy - newSvgH / 2 });
+        }
 
-    if(tex) {
-        var gd = Lib.getPlotDiv(that.node());
-        ((gd && gd._promises) || []).push(new Promise(function(resolve) {
-            that.style({visibility: 'hidden'});
-            var config = {fontSize: parseInt(that.style('font-size'), 10)};
-
-            texToSVG(tex[2], config, function(_svgEl, _glyphDefs, _svgBBox) {
-                parent.selectAll('svg.' + svgClass).remove();
-                parent.selectAll('g.' + svgClass + '-group').remove();
-
-                var newSvg = _svgEl && _svgEl.select('svg');
-                if(!newSvg || !newSvg.node()) {
-                    showText();
-                    resolve();
-                    return;
-                }
-
-                var mathjaxGroup = parent.append('g')
-                    .classed(svgClass + '-group', true)
-                    .attr({'pointer-events': 'none'});
-
-                mathjaxGroup.node().appendChild(newSvg.node());
-
-                // stitch the glyph defs
-                if(_glyphDefs && _glyphDefs.node()) {
-                    newSvg.node().insertBefore(_glyphDefs.node().cloneNode(true),
-                                               newSvg.node().firstChild);
-                }
-
-                newSvg.attr({
-                    'class': svgClass,
-                    height: _svgBBox.height,
-                    preserveAspectRatio: 'xMinYMin meet'
-                })
-                .style({overflow: 'visible', 'pointer-events': 'none'});
-
-                var fill = that.style('fill') || 'black';
-                newSvg.select('g').attr({fill: fill, stroke: fill});
-
-                var newSvgW = getSize(newSvg, 'width'),
-                    newSvgH = getSize(newSvg, 'height'),
-                    newX = +that.attr('x') - newSvgW *
-                        {start: 0, middle: 0.5, end: 1}[that.attr('text-anchor') || 'start'],
-                    // font baseline is about 1/4 fontSize below centerline
-                    textHeight = parseInt(that.style('font-size'), 10) ||
-                        getSize(that, 'height'),
-                    dy = -textHeight / 4;
-
-                if(svgClass[0] === 'y') {
-                    mathjaxGroup.attr({
-                        transform: 'rotate(' + [-90, +that.attr('x'), +that.attr('y')] +
-                        ') translate(' + [-newSvgW / 2, dy - newSvgH / 2] + ')'
-                    });
-                    newSvg.attr({x: +that.attr('x'), y: +that.attr('y')});
-                }
-                else if(svgClass[0] === 'l') {
-                    newSvg.attr({x: that.attr('x'), y: dy - (newSvgH / 2)});
-                }
-                else if(svgClass[0] === 'a') {
-                    newSvg.attr({x: 0, y: dy});
-                }
-                else {
-                    newSvg.attr({x: newX, y: (+that.attr('y') + dy - newSvgH / 2)});
-                }
-
-                if(_callback) _callback.call(that, mathjaxGroup);
-                resolve(mathjaxGroup);
-            });
-        }));
-    }
-    else showText();
+        if (_callback) _callback.call(that, mathjaxGroup);
+        resolve(mathjaxGroup);
+      });
+    }));
+  } else {
+    showText();
+  }
 
-    return _context;
+  return _context;
 };
 
-
 // MathJax
-
 function cleanEscapesForTex(s) {
-    return s.replace(/(<|<|<)/g, '\\lt ')
-        .replace(/(>|>|>)/g, '\\gt ');
+  return s
+    .replace(/(<|<|<)/g, "\\lt ")
+    .replace(/(>|>|>)/g, "\\gt ");
 }
 
 function texToSVG(_texString, _config, _callback) {
-    var randomID = 'math-output-' + Lib.randstr([], 64);
-    var tmpDiv = d3.select('body').append('div')
-        .attr({id: randomID})
-        .style({visibility: 'hidden', position: 'absolute'})
-        .style({'font-size': _config.fontSize + 'px'})
-        .text(cleanEscapesForTex(_texString));
-
-    MathJax.Hub.Queue(['Typeset', MathJax.Hub, tmpDiv.node()], function() {
-        var glyphDefs = d3.select('body').select('#MathJax_SVG_glyphs');
-
-        if(tmpDiv.select('.MathJax_SVG').empty() || !tmpDiv.select('svg').node()) {
-            Lib.log('There was an error in the tex syntax.', _texString);
-            _callback();
-        }
-        else {
-            var svgBBox = tmpDiv.select('svg').node().getBoundingClientRect();
-            _callback(tmpDiv.select('.MathJax_SVG'), glyphDefs, svgBBox);
-        }
+  var randomID = "math-output-" + Lib.randstr([], 64);
+  var tmpDiv = d3
+    .select("body")
+    .append("div")
+    .attr({ id: randomID })
+    .style({ visibility: "hidden", position: "absolute" })
+    .style({ "font-size": _config.fontSize + "px" })
+    .text(cleanEscapesForTex(_texString));
+
+  MathJax.Hub.Queue(["Typeset", MathJax.Hub, tmpDiv.node()], function() {
+    var glyphDefs = d3.select("body").select("#MathJax_SVG_glyphs");
+
+    if (tmpDiv.select(".MathJax_SVG").empty() || !tmpDiv.select("svg").node()) {
+      Lib.log("There was an error in the tex syntax.", _texString);
+      _callback();
+    } else {
+      var svgBBox = tmpDiv.select("svg").node().getBoundingClientRect();
+      _callback(tmpDiv.select(".MathJax_SVG"), glyphDefs, svgBBox);
+    }
 
-        tmpDiv.remove();
-    });
+    tmpDiv.remove();
+  });
 }
 
 var TAG_STYLES = {
-    // would like to use baseline-shift but FF doesn't support it yet
-    // so we need to use dy along with the uber hacky shift-back-to
-    // baseline below
-    sup: 'font-size:70%" dy="-0.6em',
-    sub: 'font-size:70%" dy="0.3em',
-    b: 'font-weight:bold',
-    i: 'font-style:italic',
-    a: '',
-    span: '',
-    br: '',
-    em: 'font-style:italic;font-weight:bold'
+  // would like to use baseline-shift but FF doesn't support it yet
+  // so we need to use dy along with the uber hacky shift-back-to
+  // baseline below
+  sup: 'font-size:70%" dy="-0.6em',
+  sub: 'font-size:70%" dy="0.3em',
+  b: "font-weight:bold",
+  i: "font-style:italic",
+  a: "",
+  span: "",
+  br: "",
+  em: "font-style:italic;font-weight:bold"
 };
 
-var PROTOCOLS = ['http:', 'https:', 'mailto:'];
+var PROTOCOLS = ["http:", "https:", "mailto:"];
 
-var STRIP_TAGS = new RegExp('?(' + Object.keys(TAG_STYLES).join('|') + ')( [^>]*)?/?>', 'g');
+var STRIP_TAGS = new RegExp(
+  "?(" + Object.keys(TAG_STYLES).join("|") + ")( [^>]*)?/?>",
+  "g"
+);
 
-var ENTITY_TO_UNICODE = Object.keys(stringMappings.entityToUnicode).map(function(k) {
-    return {
-        regExp: new RegExp('&' + k + ';', 'g'),
-        sub: stringMappings.entityToUnicode[k]
-    };
+var ENTITY_TO_UNICODE = Object.keys(
+  stringMappings.entityToUnicode
+).map(function(k) {
+  return {
+    regExp: new RegExp("&" + k + ";", "g"),
+    sub: stringMappings.entityToUnicode[k]
+  };
 });
 
-var UNICODE_TO_ENTITY = Object.keys(stringMappings.unicodeToEntity).map(function(k) {
-    return {
-        regExp: new RegExp(k, 'g'),
-        sub: '&' + stringMappings.unicodeToEntity[k] + ';'
-    };
+var UNICODE_TO_ENTITY = Object.keys(
+  stringMappings.unicodeToEntity
+).map(function(k) {
+  return {
+    regExp: new RegExp(k, "g"),
+    sub: "&" + stringMappings.unicodeToEntity[k] + ";"
+  };
 });
 
 var NEWLINES = /(\r\n?|\n)/g;
 
 exports.plainText = function(_str) {
-    // strip out our pseudo-html so we have a readable
-    // version to put into text fields
-    return (_str || '').replace(STRIP_TAGS, ' ');
+  // strip out our pseudo-html so we have a readable
+  // version to put into text fields
+  return (_str || "").replace(STRIP_TAGS, " ");
 };
 
 function replaceFromMapObject(_str, list) {
-    var out = _str || '';
+  var out = _str || "";
 
-    for(var i = 0; i < list.length; i++) {
-        var item = list[i];
-        out = out.replace(item.regExp, item.sub);
-    }
+  for (var i = 0; i < list.length; i++) {
+    var item = list[i];
+    out = out.replace(item.regExp, item.sub);
+  }
 
-    return out;
+  return out;
 }
 
 function convertEntities(_str) {
-    return replaceFromMapObject(_str, ENTITY_TO_UNICODE);
+  return replaceFromMapObject(_str, ENTITY_TO_UNICODE);
 }
 
 function encodeForHTML(_str) {
-    return replaceFromMapObject(_str, UNICODE_TO_ENTITY);
+  return replaceFromMapObject(_str, UNICODE_TO_ENTITY);
 }
 
 function convertToSVG(_str) {
-    _str = convertEntities(_str);
-
-    // normalize behavior between IE and others wrt newlines and whitespace:pre
-    // this combination makes IE barf https://github.com/plotly/plotly.js/issues/746
-    // Chrome and FF display \n, \r, or \r\n as a space in this mode.
-    // I feel like at some point we turned these into 
 but currently we don't so
-    // I'm just going to cement what we do now in Chrome and FF
-    _str = _str.replace(NEWLINES, ' ');
-
-    var result = _str
-        .split(/(<[^<>]*>)/).map(function(d) {
-            var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i),
-                tag = match && match[2].toLowerCase(),
-                style = TAG_STYLES[tag];
-
-            if(style !== undefined) {
-                var close = match[1],
-                    extra = match[3],
-                    /**
+  _str = convertEntities(_str);
+
+  // normalize behavior between IE and others wrt newlines and whitespace:pre
+  // this combination makes IE barf https://github.com/plotly/plotly.js/issues/746
+  // Chrome and FF display \n, \r, or \r\n as a space in this mode.
+  // I feel like at some point we turned these into 
 but currently we don't so
+  // I'm just going to cement what we do now in Chrome and FF
+  _str = _str.replace(NEWLINES, " ");
+
+  var result = _str.split(/(<[^<>]*>)/).map(function(d) {
+    var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i),
+      tag = match && match[2].toLowerCase(),
+      style = TAG_STYLES[tag];
+
+    if (style !== undefined) {
+      var close = match[1],
+        extra = match[3],
+        /**
                      * extraStyle: any random extra css (that's supported by svg)
                      * use this like  to change font in the middle
                      *
@@ -304,232 +330,265 @@ function convertToSVG(_str) {
                      * valid HTML anymore and we dropped it accidentally for many months, we will not
                      * resurrect it.
                      */
-                    extraStyle = extra.match(/^style\s*=\s*"([^"]+)"\s*/i);
-
-                // anchor and br are the only ones that don't turn into a tspan
-                if(tag === 'a') {
-                    if(close) return '';
-                    else if(extra.substr(0, 4).toLowerCase() !== 'href') return '';
-                    else {
-                        // remove quotes, leading '=', replace '&' with '&'
-                        var href = extra.substr(4)
-                            .replace(/["']/g, '')
-                            .replace(/=/, '');
-
-                        // check protocol
-                        var dummyAnchor = document.createElement('a');
-                        dummyAnchor.href = href;
-                        if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return '';
-
-                        return '';
-                    }
-                }
-                else if(tag === 'br') return '
';
-                else if(close) {
-                    // closing tag
-
-                    // sub/sup: extra tspan with zero-width space to get back to the right baseline
-                    if(tag === 'sup') return '';
-                    if(tag === 'sub') return '';
-                    else return '';
-                }
-                else {
-                    var tspanStart = '';
-                }
-            }
-            else {
-                return exports.xml_entity_encode(d).replace(/";
+        } else if (extra.substr(0, 4).toLowerCase() !== "href") {
+          return "";
+        } else {
+          // remove quotes, leading '=', replace '&' with '&'
+          var href = extra.substr(4).replace(/["']/g, "").replace(/=/, "");
+
+          // check protocol
+          var dummyAnchor = document.createElement("a");
+          dummyAnchor.href = href;
+          if (PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return "";
+
+          return '';
+        }
+      } else if (tag === "br") {
+        return "
";
+      } else if (close) {
+        // closing tag
+        // sub/sup: extra tspan with zero-width space to get back to the right baseline
+        if (tag === "sup") return '';
+        if (tag === "sub") {
+          return '';
+        } else {
+          return "";
+        }
+      } else {
+        var tspanStart = "'); index > 0; index = result.indexOf('
', index + 1)) {
-        indices.push(index);
-    }
-    var count = 0;
-    indices.forEach(function(d) {
-        var brIndex = d + count;
-        var search = result.slice(0, brIndex);
-        var previousOpenTag = '';
-        for(var i2 = search.length - 1; i2 >= 0; i2--) {
-            var isTag = search[i2].match(/<(\/?).*>/i);
-            if(isTag && search[i2] !== '
') {
-                if(!isTag[1]) previousOpenTag = search[i2];
-                break;
-            }
+        if (tag === "sup" || tag === "sub") {
+          // sub/sup: extra zero-width space, fixes problem if new line starts with sub/sup
+          tspanStart = "" + tspanStart;
         }
-        if(previousOpenTag) {
-            result.splice(brIndex + 1, 0, previousOpenTag);
-            result.splice(brIndex, 0, '');
-            count += 2;
+
+        if (extraStyle) {
+          // most of the svg css users will care about is just like html,
+          // but font color is different. Let our users ignore this.
+          extraStyle = extraStyle[1].replace(/(^|;)\s*color:/, "$1 fill:");
+          style = encodeForHTML(extraStyle) + (style ? ";" + style : "");
         }
-    });
 
-    var joined = result.join('');
-    var splitted = joined.split(/
/gi);
-    if(splitted.length > 1) {
-        result = splitted.map(function(d, i) {
-            // TODO: figure out max font size of this line and alter dy
-            // this requires either:
-            // 1) bringing the base font size into convertToTspans, or
-            // 2) only allowing relative percentage font sizes.
-            // I think #2 is the way to go
-            return '' + d + '';
-        });
+        return tspanStart + (style ? ' style="' + style + '"' : "") + ">";
+      }
+    } else {
+      return exports.xml_entity_encode(d).replace(/");
+    index > 0;
+    index = result.indexOf("
", index + 1)
+  ) {
+    indices.push(index);
+  }
+  var count = 0;
+  indices.forEach(function(d) {
+    var brIndex = d + count;
+    var search = result.slice(0, brIndex);
+    var previousOpenTag = "";
+    for (var i2 = search.length - 1; i2 >= 0; i2--) {
+      var isTag = search[i2].match(/<(\/?).*>/i);
+      if (isTag && search[i2] !== "
") {
+        if (!isTag[1]) previousOpenTag = search[i2];
+        break;
+      }
     }
+    if (previousOpenTag) {
+      result.splice(brIndex + 1, 0, previousOpenTag);
+      result.splice(brIndex, 0, "");
+      count += 2;
+    }
+  });
+
+  var joined = result.join("");
+  var splitted = joined.split(/
/gi);
+  if (splitted.length > 1) {
+    result = splitted.map(function(d, i) {
+      // TODO: figure out max font size of this line and alter dy
+      // this requires either:
+      // 1) bringing the base font size into convertToTspans, or
+      // 2) only allowing relative percentage font sizes.
+      // I think #2 is the way to go
+      return '' + d + "";
+    });
+  }
 
-    return result.join('');
+  return result.join("");
 }
 
 function alignHTMLWith(_base, container, options) {
-    var alignH = options.horizontalAlign,
-        alignV = options.verticalAlign || 'top',
-        bRect = _base.node().getBoundingClientRect(),
-        cRect = container.node().getBoundingClientRect(),
-        thisRect,
-        getTop,
-        getLeft;
-
-    if(alignV === 'bottom') {
-        getTop = function() { return bRect.bottom - thisRect.height; };
-    } else if(alignV === 'middle') {
-        getTop = function() { return bRect.top + (bRect.height - thisRect.height) / 2; };
-    } else { // default: top
-        getTop = function() { return bRect.top; };
-    }
-
-    if(alignH === 'right') {
-        getLeft = function() { return bRect.right - thisRect.width; };
-    } else if(alignH === 'center') {
-        getLeft = function() { return bRect.left + (bRect.width - thisRect.width) / 2; };
-    } else { // default: left
-        getLeft = function() { return bRect.left; };
-    }
+  var alignH = options.horizontalAlign,
+    alignV = options.verticalAlign || "top",
+    bRect = _base.node().getBoundingClientRect(),
+    cRect = container.node().getBoundingClientRect(),
+    thisRect,
+    getTop,
+    getLeft;
+
+  if (alignV === "bottom") {
+    getTop = function() {
+      return bRect.bottom - thisRect.height;
+    };
+  } else if (alignV === "middle") {
+    getTop = function() {
+      return bRect.top + (bRect.height - thisRect.height) / 2;
+    };
+  } else {
+    // default: top
+    getTop = function() {
+      return bRect.top;
+    };
+  }
 
-    return function() {
-        thisRect = this.node().getBoundingClientRect();
-        this.style({
-            top: (getTop() - cRect.top) + 'px',
-            left: (getLeft() - cRect.left) + 'px',
-            'z-index': 1000
-        });
-        return this;
+  if (alignH === "right") {
+    getLeft = function() {
+      return bRect.right - thisRect.width;
+    };
+  } else if (alignH === "center") {
+    getLeft = function() {
+      return bRect.left + (bRect.width - thisRect.width) / 2;
     };
+  } else {
+    // default: left
+    getLeft = function() {
+      return bRect.left;
+    };
+  }
+
+  return function() {
+    thisRect = this.node().getBoundingClientRect();
+    this.style({
+      top: getTop() - cRect.top + "px",
+      left: getLeft() - cRect.left + "px",
+      "z-index": 1000
+    });
+    return this;
+  };
 }
 
 // Editable title
-
 exports.makeEditable = function(context, _delegate, options) {
-    if(!options) options = {};
-    var that = this;
-    var dispatch = d3.dispatch('edit', 'input', 'cancel');
-    var textSelection = d3.select(this.node())
-        .style({'pointer-events': 'all'});
-
-    var handlerElement = _delegate || textSelection;
-    if(_delegate) textSelection.style({'pointer-events': 'none'});
-
-    function handleClick() {
-        appendEditable();
-        that.style({opacity: 0});
-        // also hide any mathjax svg
-        var svgClass = handlerElement.attr('class'),
-            mathjaxClass;
-        if(svgClass) mathjaxClass = '.' + svgClass.split(' ')[0] + '-math-group';
-        else mathjaxClass = '[class*=-math-group]';
-        if(mathjaxClass) {
-            d3.select(that.node().parentNode).select(mathjaxClass).style({opacity: 0});
-        }
+  if (!options) options = {};
+  var that = this;
+  var dispatch = d3.dispatch("edit", "input", "cancel");
+  var textSelection = d3.select(this.node()).style({ "pointer-events": "all" });
+
+  var handlerElement = _delegate || textSelection;
+  if (_delegate) textSelection.style({ "pointer-events": "none" });
+
+  function handleClick() {
+    appendEditable();
+    that.style({ opacity: 0 });
+    // also hide any mathjax svg
+    var svgClass = handlerElement.attr("class"), mathjaxClass;
+    if (svgClass) mathjaxClass = "." + svgClass.split(" ")[0] + "-math-group";
+    else mathjaxClass = "[class*=-math-group]";
+    if (mathjaxClass) {
+      d3
+        .select(that.node().parentNode)
+        .select(mathjaxClass)
+        .style({ opacity: 0 });
     }
-
-    function selectElementContents(_el) {
-        var el = _el.node();
-        var range = document.createRange();
-        range.selectNodeContents(el);
-        var sel = window.getSelection();
-        sel.removeAllRanges();
-        sel.addRange(range);
-        el.focus();
-    }
-
-    function appendEditable() {
-        var plotDiv = d3.select(Lib.getPlotDiv(that.node())),
-            container = plotDiv.select('.svg-container'),
-            div = container.append('div');
-        div.classed('plugin-editable editable', true)
-            .style({
-                position: 'absolute',
-                'font-family': that.style('font-family') || 'Arial',
-                'font-size': that.style('font-size') || 12,
-                color: options.fill || that.style('fill') || 'black',
-                opacity: 1,
-                'background-color': options.background || 'transparent',
-                outline: '#ffffff33 1px solid',
-                margin: [-parseFloat(that.style('font-size')) / 8 + 1, 0, 0, -1].join('px ') + 'px',
-                padding: '0',
-                'box-sizing': 'border-box'
-            })
-            .attr({contenteditable: true})
-            .text(options.text || that.attr('data-unformatted'))
-            .call(alignHTMLWith(that, container, options))
-            .on('blur', function() {
-                that.text(this.textContent)
-                    .style({opacity: 1});
-                var svgClass = d3.select(this).attr('class'),
-                    mathjaxClass;
-                if(svgClass) mathjaxClass = '.' + svgClass.split(' ')[0] + '-math-group';
-                else mathjaxClass = '[class*=-math-group]';
-                if(mathjaxClass) {
-                    d3.select(that.node().parentNode).select(mathjaxClass).style({opacity: 0});
-                }
-                var text = this.textContent;
-                d3.select(this).transition().duration(0).remove();
-                d3.select(document).on('mouseup', null);
-                dispatch.edit.call(that, text);
-            })
-            .on('focus', function() {
-                var context = this;
-                d3.select(document).on('mouseup', function() {
-                    if(d3.event.target === context) return false;
-                    if(document.activeElement === div.node()) div.node().blur();
-                });
-            })
-            .on('keyup', function() {
-                if(d3.event.which === 27) {
-                    that.style({opacity: 1});
-                    d3.select(this)
-                        .style({opacity: 0})
-                        .on('blur', function() { return false; })
-                        .transition().remove();
-                    dispatch.cancel.call(that, this.textContent);
-                }
-                else {
-                    dispatch.input.call(that, this.textContent);
-                    d3.select(this).call(alignHTMLWith(that, container, options));
-                }
-            })
-            .on('keydown', function() {
-                if(d3.event.which === 13) this.blur();
+  }
+
+  function selectElementContents(_el) {
+    var el = _el.node();
+    var range = document.createRange();
+    range.selectNodeContents(el);
+    var sel = window.getSelection();
+    sel.removeAllRanges();
+    sel.addRange(range);
+    el.focus();
+  }
+
+  function appendEditable() {
+    var plotDiv = d3.select(Lib.getPlotDiv(that.node())),
+      container = plotDiv.select(".svg-container"),
+      div = container.append("div");
+    div
+      .classed("plugin-editable editable", true)
+      .style({
+        position: "absolute",
+        "font-family": that.style("font-family") || "Arial",
+        "font-size": that.style("font-size") || 12,
+        color: options.fill || that.style("fill") || "black",
+        opacity: 1,
+        "background-color": options.background || "transparent",
+        outline: "#ffffff33 1px solid",
+        margin: (
+          [(-parseFloat(that.style("font-size"))) / 8 + 1, 0, 0, -1].join(
+            "px "
+          ) +
+            "px"
+        ),
+        padding: "0",
+        "box-sizing": "border-box"
+      })
+      .attr({ contenteditable: true })
+      .text(options.text || that.attr("data-unformatted"))
+      .call(alignHTMLWith(that, container, options))
+      .on("blur", function() {
+        that.text(this.textContent).style({ opacity: 1 });
+        var svgClass = d3.select(this).attr("class"), mathjaxClass;
+        if (svgClass) {
+          mathjaxClass = "." + svgClass.split(" ")[0] + "-math-group";
+        } else {
+          mathjaxClass = "[class*=-math-group]";
+        }
+        if (mathjaxClass) {
+          d3
+            .select(that.node().parentNode)
+            .select(mathjaxClass)
+            .style({ opacity: 0 });
+        }
+        var text = this.textContent;
+        d3.select(this).transition().duration(0).remove();
+        d3.select(document).on("mouseup", null);
+        dispatch.edit.call(that, text);
+      })
+      .on("focus", function() {
+        var context = this;
+        d3.select(document).on("mouseup", function() {
+          if (d3.event.target === context) return false;
+          if (document.activeElement === div.node()) div.node().blur();
+        });
+      })
+      .on("keyup", function() {
+        if (d3.event.which === 27) {
+          that.style({ opacity: 1 });
+          d3
+            .select(this)
+            .style({ opacity: 0 })
+            .on("blur", function() {
+              return false;
             })
-            .call(selectElementContents);
-    }
+            .transition()
+            .remove();
+          dispatch.cancel.call(that, this.textContent);
+        } else {
+          dispatch.input.call(that, this.textContent);
+          d3.select(this).call(alignHTMLWith(that, container, options));
+        }
+      })
+      .on("keydown", function() {
+        if (d3.event.which === 13) this.blur();
+      })
+      .call(selectElementContents);
+  }
 
-    if(options.immediate) handleClick();
-    else handlerElement.on('click', handleClick);
+  if (options.immediate) handleClick();
+  else handlerElement.on("click", handleClick);
 
-    return d3.rebind(this, dispatch, 'on');
+  return d3.rebind(this, dispatch, "on");
 };
diff --git a/src/lib/topojson_utils.js b/src/lib/topojson_utils.js
index bdd5d7bf196..67574b3d1f6 100644
--- a/src/lib/topojson_utils.js
+++ b/src/lib/topojson_utils.js
@@ -5,30 +5,28 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
+"use strict";
 var topojsonUtils = module.exports = {};
 
-var locationmodeToLayer = require('../plots/geo/constants').locationmodeToLayer;
-var topojsonFeature = require('topojson-client').feature;
-
+var locationmodeToLayer = require("../plots/geo/constants").locationmodeToLayer;
+var topojsonFeature = require("topojson-client").feature;
 
 topojsonUtils.getTopojsonName = function(geoLayout) {
-    return [
-        geoLayout.scope.replace(/ /g, '-'), '_',
-        geoLayout.resolution.toString(), 'm'
-    ].join('');
+  return [
+    geoLayout.scope.replace(/ /g, "-"),
+    "_",
+    geoLayout.resolution.toString(),
+    "m"
+  ].join("");
 };
 
 topojsonUtils.getTopojsonPath = function(topojsonURL, topojsonName) {
-    return topojsonURL + topojsonName + '.json';
+  return topojsonURL + topojsonName + ".json";
 };
 
 topojsonUtils.getTopojsonFeatures = function(trace, topojson) {
-    var layer = locationmodeToLayer[trace.locationmode],
-        obj = topojson.objects[layer];
+  var layer = locationmodeToLayer[trace.locationmode],
+    obj = topojson.objects[layer];
 
-    return topojsonFeature(topojson, obj).features;
+  return topojsonFeature(topojson, obj).features;
 };
diff --git a/src/lib/typed_array_truncate.js b/src/lib/typed_array_truncate.js
index 3bacc8ebe56..718402f680b 100644
--- a/src/lib/typed_array_truncate.js
+++ b/src/lib/typed_array_truncate.js
@@ -5,19 +5,21 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 function truncateFloat32(arrayIn, len) {
-    var arrayOut = new Float32Array(len);
-    for(var i = 0; i < len; i++) arrayOut[i] = arrayIn[i];
-    return arrayOut;
+  var arrayOut = new Float32Array(len);
+  for (var i = 0; i < len; i++) {
+    arrayOut[i] = arrayIn[i];
+  }
+  return arrayOut;
 }
 
 function truncateFloat64(arrayIn, len) {
-    var arrayOut = new Float64Array(len);
-    for(var i = 0; i < len; i++) arrayOut[i] = arrayIn[i];
-    return arrayOut;
+  var arrayOut = new Float64Array(len);
+  for (var i = 0; i < len; i++) {
+    arrayOut[i] = arrayIn[i];
+  }
+  return arrayOut;
 }
 
 /**
@@ -26,7 +28,7 @@ function truncateFloat64(arrayIn, len) {
  * 2x as long, therefore we aren't checking for its existence
  */
 module.exports = function truncate(arrayIn, len) {
-    if(arrayIn instanceof Float32Array) return truncateFloat32(arrayIn, len);
-    if(arrayIn instanceof Float64Array) return truncateFloat64(arrayIn, len);
-    throw new Error('This array type is not yet supported by `truncate`.');
+  if (arrayIn instanceof Float32Array) return truncateFloat32(arrayIn, len);
+  if (arrayIn instanceof Float64Array) return truncateFloat64(arrayIn, len);
+  throw new Error("This array type is not yet supported by `truncate`.");
 };
diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js
index 62a4b7e38d5..6984f674f63 100644
--- a/src/plot_api/helpers.js
+++ b/src/plot_api/helpers.js
@@ -5,436 +5,456 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var isNumeric = require("fast-isnumeric");
+var m4FromQuat = require("gl-mat4/fromQuat");
 
-
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-var m4FromQuat = require('gl-mat4/fromQuat');
-
-var Registry = require('../registry');
-var Lib = require('../lib');
-var Plots = require('../plots/plots');
-var Axes = require('../plots/cartesian/axes');
-var Color = require('../components/color');
-
+var Registry = require("../registry");
+var Lib = require("../lib");
+var Plots = require("../plots/plots");
+var Axes = require("../plots/cartesian/axes");
+var Color = require("../components/color");
 
 // Get the container div: we store all variables for this plot as
 // properties of this div
 // some callers send this in by DOM element, others by id (string)
 exports.getGraphDiv = function(gd) {
-    var gdElement;
+  var gdElement;
 
-    if(typeof gd === 'string') {
-        gdElement = document.getElementById(gd);
+  if (typeof gd === "string") {
+    gdElement = document.getElementById(gd);
 
-        if(gdElement === null) {
-            throw new Error('No DOM element with id \'' + gd + '\' exists on the page.');
-        }
-
-        return gdElement;
-    }
-    else if(gd === null || gd === undefined) {
-        throw new Error('DOM element provided is null or undefined');
+    if (gdElement === null) {
+      throw new Error(
+        "No DOM element with id '" + gd + "' exists on the page."
+      );
     }
 
-    return gd;  // otherwise assume that gd is a DOM element
+    return gdElement;
+  } else if (gd === null || gd === undefined) {
+    throw new Error("DOM element provided is null or undefined");
+  }
+
+  return gd; // otherwise assume that gd is a DOM element
 };
 
 // clear the promise queue if one of them got rejected
 exports.clearPromiseQueue = function(gd) {
-    if(Array.isArray(gd._promises) && gd._promises.length > 0) {
-        Lib.log('Clearing previous rejected promises from queue.');
-    }
+  if (Array.isArray(gd._promises) && gd._promises.length > 0) {
+    Lib.log("Clearing previous rejected promises from queue.");
+  }
 
-    gd._promises = [];
+  gd._promises = [];
 };
 
 // make a few changes to the layout right away
 // before it gets used for anything
 // backward compatibility and cleanup of nonstandard options
 exports.cleanLayout = function(layout) {
-    var i, j;
-
-    if(!layout) layout = {};
+  var i, j;
+
+  if (!layout) layout = {};
+
+  // cannot have (x|y)axis1, numbering goes axis, axis2, axis3...
+  if (layout.xaxis1) {
+    if (!layout.xaxis) layout.xaxis = layout.xaxis1;
+    delete layout.xaxis1;
+  }
+  if (layout.yaxis1) {
+    if (!layout.yaxis) layout.yaxis = layout.yaxis1;
+    delete layout.yaxis1;
+  }
+
+  var axList = Axes.list({ _fullLayout: layout });
+  for (i = 0; i < axList.length; i++) {
+    var ax = axList[i];
+    if (ax.anchor && ax.anchor !== "free") {
+      ax.anchor = Axes.cleanId(ax.anchor);
+    }
+    if (ax.overlaying) ax.overlaying = Axes.cleanId(ax.overlaying);
 
-    // cannot have (x|y)axis1, numbering goes axis, axis2, axis3...
-    if(layout.xaxis1) {
-        if(!layout.xaxis) layout.xaxis = layout.xaxis1;
-        delete layout.xaxis1;
+    // old method of axis type - isdate and islog (before category existed)
+    if (!ax.type) {
+      if (ax.isdate) ax.type = "date";
+      else if (ax.islog) ax.type = "log";
+      else if (ax.isdate === false && ax.islog === false) ax.type = "linear";
     }
-    if(layout.yaxis1) {
-        if(!layout.yaxis) layout.yaxis = layout.yaxis1;
-        delete layout.yaxis1;
+    if (ax.autorange === "withzero" || ax.autorange === "tozero") {
+      ax.autorange = true;
+      ax.rangemode = "tozero";
     }
-
-    var axList = Axes.list({_fullLayout: layout});
-    for(i = 0; i < axList.length; i++) {
-        var ax = axList[i];
-        if(ax.anchor && ax.anchor !== 'free') {
-            ax.anchor = Axes.cleanId(ax.anchor);
-        }
-        if(ax.overlaying) ax.overlaying = Axes.cleanId(ax.overlaying);
-
-        // old method of axis type - isdate and islog (before category existed)
-        if(!ax.type) {
-            if(ax.isdate) ax.type = 'date';
-            else if(ax.islog) ax.type = 'log';
-            else if(ax.isdate === false && ax.islog === false) ax.type = 'linear';
-        }
-        if(ax.autorange === 'withzero' || ax.autorange === 'tozero') {
-            ax.autorange = true;
-            ax.rangemode = 'tozero';
-        }
-        delete ax.islog;
-        delete ax.isdate;
-        delete ax.categories; // replaced by _categories
-
-        // prune empty domain arrays made before the new nestedProperty
-        if(emptyContainer(ax, 'domain')) delete ax.domain;
-
-        // autotick -> tickmode
-        if(ax.autotick !== undefined) {
-            if(ax.tickmode === undefined) {
-                ax.tickmode = ax.autotick ? 'auto' : 'linear';
-            }
-            delete ax.autotick;
-        }
+    delete ax.islog;
+    delete ax.isdate;
+    delete ax.categories;
+
+    // replaced by _categories
+    // prune empty domain arrays made before the new nestedProperty
+    if (emptyContainer(ax, "domain")) delete ax.domain;
+
+    // autotick -> tickmode
+    if (ax.autotick !== undefined) {
+      if (ax.tickmode === undefined) {
+        ax.tickmode = ax.autotick ? "auto" : "linear";
+      }
+      delete ax.autotick;
     }
-
-    var annotationsLen = Array.isArray(layout.annotations) ? layout.annotations.length : 0;
-    for(i = 0; i < annotationsLen; i++) {
-        var ann = layout.annotations[i];
-
-        if(!Lib.isPlainObject(ann)) continue;
-
-        if(ann.ref) {
-            if(ann.ref === 'paper') {
-                ann.xref = 'paper';
-                ann.yref = 'paper';
-            }
-            else if(ann.ref === 'data') {
-                ann.xref = 'x';
-                ann.yref = 'y';
-            }
-            delete ann.ref;
-        }
-
-        cleanAxRef(ann, 'xref');
-        cleanAxRef(ann, 'yref');
+  }
+
+  var annotationsLen = Array.isArray(layout.annotations)
+    ? layout.annotations.length
+    : 0;
+  for (i = 0; i < annotationsLen; i++) {
+    var ann = layout.annotations[i];
+
+    if (!Lib.isPlainObject(ann)) continue;
+
+    if (ann.ref) {
+      if (ann.ref === "paper") {
+        ann.xref = "paper";
+        ann.yref = "paper";
+      } else if (ann.ref === "data") {
+        ann.xref = "x";
+        ann.yref = "y";
+      }
+      delete ann.ref;
     }
 
-    var shapesLen = Array.isArray(layout.shapes) ? layout.shapes.length : 0;
-    for(i = 0; i < shapesLen; i++) {
-        var shape = layout.shapes[i];
-
-        if(!Lib.isPlainObject(shape)) continue;
-
-        cleanAxRef(shape, 'xref');
-        cleanAxRef(shape, 'yref');
+    cleanAxRef(ann, "xref");
+    cleanAxRef(ann, "yref");
+  }
+
+  var shapesLen = Array.isArray(layout.shapes) ? layout.shapes.length : 0;
+  for (i = 0; i < shapesLen; i++) {
+    var shape = layout.shapes[i];
+
+    if (!Lib.isPlainObject(shape)) continue;
+
+    cleanAxRef(shape, "xref");
+    cleanAxRef(shape, "yref");
+  }
+
+  var legend = layout.legend;
+  if (legend) {
+    // check for old-style legend positioning (x or y is +/- 100)
+    if (legend.x > 3) {
+      legend.x = 1.02;
+      legend.xanchor = "left";
+    } else if (legend.x < -2) {
+      legend.x = -0.02;
+      legend.xanchor = "right";
     }
 
-    var legend = layout.legend;
-    if(legend) {
-        // check for old-style legend positioning (x or y is +/- 100)
-        if(legend.x > 3) {
-            legend.x = 1.02;
-            legend.xanchor = 'left';
-        }
-        else if(legend.x < -2) {
-            legend.x = -0.02;
-            legend.xanchor = 'right';
-        }
-
-        if(legend.y > 3) {
-            legend.y = 1.02;
-            legend.yanchor = 'bottom';
-        }
-        else if(legend.y < -2) {
-            legend.y = -0.02;
-            legend.yanchor = 'top';
-        }
+    if (legend.y > 3) {
+      legend.y = 1.02;
+      legend.yanchor = "bottom";
+    } else if (legend.y < -2) {
+      legend.y = -0.02;
+      legend.yanchor = "top";
     }
+  }
 
-    /*
+  /*
      * Moved from rotate -> orbit for dragmode
      */
-    if(layout.dragmode === 'rotate') layout.dragmode = 'orbit';
+  if (layout.dragmode === "rotate") layout.dragmode = "orbit";
 
-    // cannot have scene1, numbering goes scene, scene2, scene3...
-    if(layout.scene1) {
-        if(!layout.scene) layout.scene = layout.scene1;
-        delete layout.scene1;
-    }
+  // cannot have scene1, numbering goes scene, scene2, scene3...
+  if (layout.scene1) {
+    if (!layout.scene) layout.scene = layout.scene1;
+    delete layout.scene1;
+  }
 
-    /*
+  /*
      * Clean up Scene layouts
      */
-    var sceneIds = Plots.getSubplotIds(layout, 'gl3d');
-    for(i = 0; i < sceneIds.length; i++) {
-        var scene = layout[sceneIds[i]];
-
-        // clean old Camera coords
-        var cameraposition = scene.cameraposition;
-        if(Array.isArray(cameraposition) && cameraposition[0].length === 4) {
-            var rotation = cameraposition[0],
-                center = cameraposition[1],
-                radius = cameraposition[2],
-                mat = m4FromQuat([], rotation),
-                eye = [];
-
-            for(j = 0; j < 3; ++j) {
-                eye[j] = center[i] + radius * mat[2 + 4 * j];
-            }
-
-            scene.camera = {
-                eye: {x: eye[0], y: eye[1], z: eye[2]},
-                center: {x: center[0], y: center[1], z: center[2]},
-                up: {x: mat[1], y: mat[5], z: mat[9]}
-            };
-
-            delete scene.cameraposition;
-        }
+  var sceneIds = Plots.getSubplotIds(layout, "gl3d");
+  for (i = 0; i < sceneIds.length; i++) {
+    var scene = layout[sceneIds[i]];
+
+    // clean old Camera coords
+    var cameraposition = scene.cameraposition;
+    if (Array.isArray(cameraposition) && cameraposition[0].length === 4) {
+      var rotation = cameraposition[0],
+        center = cameraposition[1],
+        radius = cameraposition[2],
+        mat = m4FromQuat([], rotation),
+        eye = [];
+
+      for (j = 0; j < 3; ++j) {
+        eye[j] = center[i] + radius * mat[2 + 4 * j];
+      }
+
+      scene.camera = {
+        eye: { x: eye[0], y: eye[1], z: eye[2] },
+        center: { x: center[0], y: center[1], z: center[2] },
+        up: { x: mat[1], y: mat[5], z: mat[9] }
+      };
+
+      delete scene.cameraposition;
     }
+  }
 
-    // sanitize rgb(fractions) and rgba(fractions) that old tinycolor
-    // supported, but new tinycolor does not because they're not valid css
-    Color.clean(layout);
+  // sanitize rgb(fractions) and rgba(fractions) that old tinycolor
+  // supported, but new tinycolor does not because they're not valid css
+  Color.clean(layout);
 
-    return layout;
+  return layout;
 };
 
 function cleanAxRef(container, attr) {
-    var valIn = container[attr],
-        axLetter = attr.charAt(0);
-    if(valIn && valIn !== 'paper') {
-        container[attr] = Axes.cleanId(valIn, axLetter);
-    }
+  var valIn = container[attr], axLetter = attr.charAt(0);
+  if (valIn && valIn !== "paper") {
+    container[attr] = Axes.cleanId(valIn, axLetter);
+  }
 }
 
 // Make a few changes to the data right away
 // before it gets used for anything
 exports.cleanData = function(data, existingData) {
+  // Enforce unique IDs
+  var suids = [],
+    // seen uids --- so we can weed out incoming repeats
+    uids = data
+      .concat(Array.isArray(existingData) ? existingData : [])
+      .filter(function(trace) {
+        return "uid" in trace;
+      })
+      .map(function(trace) {
+        return trace.uid;
+      });
+
+  for (var tracei = 0; tracei < data.length; tracei++) {
+    var trace = data[tracei];
+    var i;
 
-    // Enforce unique IDs
-    var suids = [], // seen uids --- so we can weed out incoming repeats
-        uids = data.concat(Array.isArray(existingData) ? existingData : [])
-               .filter(function(trace) { return 'uid' in trace; })
-               .map(function(trace) { return trace.uid; });
+    // assign uids to each trace and detect collisions.
+    if (!("uid" in trace) || suids.indexOf(trace.uid) !== -1) {
+      var newUid;
 
-    for(var tracei = 0; tracei < data.length; tracei++) {
-        var trace = data[tracei];
-        var i;
+      for (i = 0; i < 100; i++) {
+        newUid = Lib.randstr(uids);
+        if (suids.indexOf(newUid) === -1) break;
+      }
+      trace.uid = Lib.randstr(uids);
+      uids.push(trace.uid);
+    }
+    // keep track of already seen uids, so that if there are
+    // doubles we force the trace with a repeat uid to
+    // acquire a new one
+    suids.push(trace.uid);
+
+    // BACKWARD COMPATIBILITY FIXES
+    // use xbins to bin data in x, and ybins to bin data in y
+    if (
+      trace.type === "histogramy" && "xbins" in trace && !("ybins" in trace)
+    ) {
+      trace.ybins = trace.xbins;
+      delete trace.xbins;
+    }
 
-        // assign uids to each trace and detect collisions.
-        if(!('uid' in trace) || suids.indexOf(trace.uid) !== -1) {
-            var newUid;
+    // error_y.opacity is obsolete - merge into color
+    if (trace.error_y && "opacity" in trace.error_y) {
+      var dc = Color.defaults,
+        yeColor = trace.error_y.color ||
+          (Registry.traceIs(trace, "bar")
+            ? Color.defaultLine
+            : dc[tracei % dc.length]);
+      trace.error_y.color = Color.addOpacity(
+        Color.rgb(yeColor),
+        Color.opacity(yeColor) * trace.error_y.opacity
+      );
+      delete trace.error_y.opacity;
+    }
 
-            for(i = 0; i < 100; i++) {
-                newUid = Lib.randstr(uids);
-                if(suids.indexOf(newUid) === -1) break;
-            }
-            trace.uid = Lib.randstr(uids);
-            uids.push(trace.uid);
-        }
-        // keep track of already seen uids, so that if there are
-        // doubles we force the trace with a repeat uid to
-        // acquire a new one
-        suids.push(trace.uid);
+    // convert bardir to orientation, and put the data into
+    // the axes it's eventually going to be used with
+    if ("bardir" in trace) {
+      if (
+        trace.bardir === "h" &&
+          (Registry.traceIs(trace, "bar") ||
+            trace.type.substr(0, 9) === "histogram")
+      ) {
+        trace.orientation = "h";
+        exports.swapXYData(trace);
+      }
+      delete trace.bardir;
+    }
 
-        // BACKWARD COMPATIBILITY FIXES
+    // now we have only one 1D histogram type, and whether
+    // it uses x or y data depends on trace.orientation
+    if (trace.type === "histogramy") exports.swapXYData(trace);
+    if (trace.type === "histogramx" || trace.type === "histogramy") {
+      trace.type = "histogram";
+    }
 
-        // use xbins to bin data in x, and ybins to bin data in y
-        if(trace.type === 'histogramy' && 'xbins' in trace && !('ybins' in trace)) {
-            trace.ybins = trace.xbins;
-            delete trace.xbins;
-        }
+    // scl->scale, reversescl->reversescale
+    if ("scl" in trace) {
+      trace.colorscale = trace.scl;
+      delete trace.scl;
+    }
+    if ("reversescl" in trace) {
+      trace.reversescale = trace.reversescl;
+      delete trace.reversescl;
+    }
 
-        // error_y.opacity is obsolete - merge into color
-        if(trace.error_y && 'opacity' in trace.error_y) {
-            var dc = Color.defaults,
-                yeColor = trace.error_y.color ||
-                (Registry.traceIs(trace, 'bar') ? Color.defaultLine : dc[tracei % dc.length]);
-            trace.error_y.color = Color.addOpacity(
-                Color.rgb(yeColor),
-                Color.opacity(yeColor) * trace.error_y.opacity);
-            delete trace.error_y.opacity;
-        }
+    // axis ids x1 -> x, y1-> y
+    if (trace.xaxis) trace.xaxis = Axes.cleanId(trace.xaxis, "x");
+    if (trace.yaxis) trace.yaxis = Axes.cleanId(trace.yaxis, "y");
 
-        // convert bardir to orientation, and put the data into
-        // the axes it's eventually going to be used with
-        if('bardir' in trace) {
-            if(trace.bardir === 'h' && (Registry.traceIs(trace, 'bar') ||
-                     trace.type.substr(0, 9) === 'histogram')) {
-                trace.orientation = 'h';
-                exports.swapXYData(trace);
-            }
-            delete trace.bardir;
-        }
+    // scene ids scene1 -> scene
+    if (Registry.traceIs(trace, "gl3d") && trace.scene) {
+      trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene);
+    }
 
-        // now we have only one 1D histogram type, and whether
-        // it uses x or y data depends on trace.orientation
-        if(trace.type === 'histogramy') exports.swapXYData(trace);
-        if(trace.type === 'histogramx' || trace.type === 'histogramy') {
-            trace.type = 'histogram';
-        }
+    if (!Registry.traceIs(trace, "pie") && !Registry.traceIs(trace, "bar")) {
+      if (Array.isArray(trace.textposition)) {
+        trace.textposition = trace.textposition.map(cleanTextPosition);
+      } else if (trace.textposition) {
+        trace.textposition = cleanTextPosition(trace.textposition);
+      }
+    }
 
-        // scl->scale, reversescl->reversescale
-        if('scl' in trace) {
-            trace.colorscale = trace.scl;
-            delete trace.scl;
-        }
-        if('reversescl' in trace) {
-            trace.reversescale = trace.reversescl;
-            delete trace.reversescl;
-        }
+    // fix typo in colorscale definition
+    if (Registry.traceIs(trace, "2dMap")) {
+      if (trace.colorscale === "YIGnBu") trace.colorscale = "YlGnBu";
+      if (trace.colorscale === "YIOrRd") trace.colorscale = "YlOrRd";
+    }
+    if (Registry.traceIs(trace, "markerColorscale") && trace.marker) {
+      var cont = trace.marker;
+      if (cont.colorscale === "YIGnBu") cont.colorscale = "YlGnBu";
+      if (cont.colorscale === "YIOrRd") cont.colorscale = "YlOrRd";
+    }
 
-        // axis ids x1 -> x, y1-> y
-        if(trace.xaxis) trace.xaxis = Axes.cleanId(trace.xaxis, 'x');
-        if(trace.yaxis) trace.yaxis = Axes.cleanId(trace.yaxis, 'y');
+    // fix typo in surface 'highlight*' definitions
+    if (trace.type === "surface" && Lib.isPlainObject(trace.contours)) {
+      var dims = ["x", "y", "z"];
 
-        // scene ids scene1 -> scene
-        if(Registry.traceIs(trace, 'gl3d') && trace.scene) {
-            trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene);
-        }
+      for (i = 0; i < dims.length; i++) {
+        var opts = trace.contours[dims[i]];
 
-        if(!Registry.traceIs(trace, 'pie') && !Registry.traceIs(trace, 'bar')) {
-            if(Array.isArray(trace.textposition)) {
-                trace.textposition = trace.textposition.map(cleanTextPosition);
-            }
-            else if(trace.textposition) {
-                trace.textposition = cleanTextPosition(trace.textposition);
-            }
-        }
+        if (!Lib.isPlainObject(opts)) continue;
 
-        // fix typo in colorscale definition
-        if(Registry.traceIs(trace, '2dMap')) {
-            if(trace.colorscale === 'YIGnBu') trace.colorscale = 'YlGnBu';
-            if(trace.colorscale === 'YIOrRd') trace.colorscale = 'YlOrRd';
+        if (opts.highlightColor) {
+          opts.highlightcolor = opts.highlightColor;
+          delete opts.highlightColor;
         }
-        if(Registry.traceIs(trace, 'markerColorscale') && trace.marker) {
-            var cont = trace.marker;
-            if(cont.colorscale === 'YIGnBu') cont.colorscale = 'YlGnBu';
-            if(cont.colorscale === 'YIOrRd') cont.colorscale = 'YlOrRd';
-        }
-
-        // fix typo in surface 'highlight*' definitions
-        if(trace.type === 'surface' && Lib.isPlainObject(trace.contours)) {
-            var dims = ['x', 'y', 'z'];
-
-            for(i = 0; i < dims.length; i++) {
-                var opts = trace.contours[dims[i]];
 
-                if(!Lib.isPlainObject(opts)) continue;
-
-                if(opts.highlightColor) {
-                    opts.highlightcolor = opts.highlightColor;
-                    delete opts.highlightColor;
-                }
-
-                if(opts.highlightWidth) {
-                    opts.highlightwidth = opts.highlightWidth;
-                    delete opts.highlightWidth;
-                }
-            }
+        if (opts.highlightWidth) {
+          opts.highlightwidth = opts.highlightWidth;
+          delete opts.highlightWidth;
         }
+      }
+    }
 
-        // transforms backward compatibility fixes
-        if(Array.isArray(trace.transforms)) {
-            var transforms = trace.transforms;
+    // transforms backward compatibility fixes
+    if (Array.isArray(trace.transforms)) {
+      var transforms = trace.transforms;
 
-            for(i = 0; i < transforms.length; i++) {
-                var transform = transforms[i];
+      for (i = 0; i < transforms.length; i++) {
+        var transform = transforms[i];
 
-                if(!Lib.isPlainObject(transform)) continue;
+        if (!Lib.isPlainObject(transform)) continue;
 
-                if(transform.type === 'filter') {
-                    if(transform.filtersrc) {
-                        transform.target = transform.filtersrc;
-                        delete transform.filtersrc;
-                    }
+        if (transform.type === "filter") {
+          if (transform.filtersrc) {
+            transform.target = transform.filtersrc;
+            delete transform.filtersrc;
+          }
 
-                    if(transform.calendar) {
-                        if(!transform.valuecalendar) {
-                            transform.valuecalendar = transform.calendar;
-                        }
-                        delete transform.calendar;
-                    }
-                }
+          if (transform.calendar) {
+            if (!transform.valuecalendar) {
+              transform.valuecalendar = transform.calendar;
             }
+            delete transform.calendar;
+          }
         }
+      }
+    }
 
-        // prune empty containers made before the new nestedProperty
-        if(emptyContainer(trace, 'line')) delete trace.line;
-        if('marker' in trace) {
-            if(emptyContainer(trace.marker, 'line')) delete trace.marker.line;
-            if(emptyContainer(trace, 'marker')) delete trace.marker;
-        }
-
-        // sanitize rgb(fractions) and rgba(fractions) that old tinycolor
-        // supported, but new tinycolor does not because they're not valid css
-        Color.clean(trace);
+    // prune empty containers made before the new nestedProperty
+    if (emptyContainer(trace, "line")) delete trace.line;
+    if ("marker" in trace) {
+      if (emptyContainer(trace.marker, "line")) delete trace.marker.line;
+      if (emptyContainer(trace, "marker")) delete trace.marker;
     }
+
+    // sanitize rgb(fractions) and rgba(fractions) that old tinycolor
+    // supported, but new tinycolor does not because they're not valid css
+    Color.clean(trace);
+  }
 };
 
 // textposition - support partial attributes (ie just 'top')
 // and incorrect use of middle / center etc.
 function cleanTextPosition(textposition) {
-    var posY = 'middle',
-        posX = 'center';
-    if(textposition.indexOf('top') !== -1) posY = 'top';
-    else if(textposition.indexOf('bottom') !== -1) posY = 'bottom';
+  var posY = "middle", posX = "center";
+  if (textposition.indexOf("top") !== -1) posY = "top";
+  else if (textposition.indexOf("bottom") !== -1) posY = "bottom";
 
-    if(textposition.indexOf('left') !== -1) posX = 'left';
-    else if(textposition.indexOf('right') !== -1) posX = 'right';
+  if (textposition.indexOf("left") !== -1) posX = "left";
+  else if (textposition.indexOf("right") !== -1) posX = "right";
 
-    return posY + ' ' + posX;
+  return posY + " " + posX;
 }
 
 function emptyContainer(outer, innerStr) {
-    return (innerStr in outer) &&
-        (typeof outer[innerStr] === 'object') &&
-        (Object.keys(outer[innerStr]).length === 0);
+  return innerStr in outer &&
+    typeof outer[innerStr] === "object" &&
+    Object.keys(outer[innerStr]).length === 0;
 }
 
-
 // swap all the data and data attributes associated with x and y
 exports.swapXYData = function(trace) {
-    var i;
-    Lib.swapAttrs(trace, ['?', '?0', 'd?', '?bins', 'nbins?', 'autobin?', '?src', 'error_?']);
-    if(Array.isArray(trace.z) && Array.isArray(trace.z[0])) {
-        if(trace.transpose) delete trace.transpose;
-        else trace.transpose = true;
+  var i;
+  Lib.swapAttrs(trace, [
+    "?",
+    "?0",
+    "d?",
+    "?bins",
+    "nbins?",
+    "autobin?",
+    "?src",
+    "error_?"
+  ]);
+  if (Array.isArray(trace.z) && Array.isArray(trace.z[0])) {
+    if (trace.transpose) delete trace.transpose;
+    else trace.transpose = true;
+  }
+  if (trace.error_x && trace.error_y) {
+    var errorY = trace.error_y,
+      copyYstyle = "copy_ystyle" in errorY
+        ? errorY.copy_ystyle
+        : !(errorY.color || errorY.thickness || errorY.width);
+    Lib.swapAttrs(trace, ["error_?.copy_ystyle"]);
+    if (copyYstyle) {
+      Lib.swapAttrs(trace, [
+        "error_?.color",
+        "error_?.thickness",
+        "error_?.width"
+      ]);
     }
-    if(trace.error_x && trace.error_y) {
-        var errorY = trace.error_y,
-            copyYstyle = ('copy_ystyle' in errorY) ? errorY.copy_ystyle :
-                !(errorY.color || errorY.thickness || errorY.width);
-        Lib.swapAttrs(trace, ['error_?.copy_ystyle']);
-        if(copyYstyle) {
-            Lib.swapAttrs(trace, ['error_?.color', 'error_?.thickness', 'error_?.width']);
-        }
-    }
-    if(trace.hoverinfo) {
-        var hoverInfoParts = trace.hoverinfo.split('+');
-        for(i = 0; i < hoverInfoParts.length; i++) {
-            if(hoverInfoParts[i] === 'x') hoverInfoParts[i] = 'y';
-            else if(hoverInfoParts[i] === 'y') hoverInfoParts[i] = 'x';
-        }
-        trace.hoverinfo = hoverInfoParts.join('+');
+  }
+  if (trace.hoverinfo) {
+    var hoverInfoParts = trace.hoverinfo.split("+");
+    for (i = 0; i < hoverInfoParts.length; i++) {
+      if (hoverInfoParts[i] === "x") hoverInfoParts[i] = "y";
+      else if (hoverInfoParts[i] === "y") hoverInfoParts[i] = "x";
     }
+    trace.hoverinfo = hoverInfoParts.join("+");
+  }
 };
 
 // coerce traceIndices input to array of trace indices
 exports.coerceTraceIndices = function(gd, traceIndices) {
-    if(isNumeric(traceIndices)) {
-        return [traceIndices];
-    }
-    else if(!Array.isArray(traceIndices) || !traceIndices.length) {
-        return gd.data.map(function(_, i) { return i; });
-    }
-
-    return traceIndices;
+  if (isNumeric(traceIndices)) {
+    return [traceIndices];
+  } else if (!Array.isArray(traceIndices) || !traceIndices.length) {
+    return gd.data.map(function(_, i) {
+      return i;
+    });
+  }
+
+  return traceIndices;
 };
 
 /**
@@ -450,37 +470,31 @@ exports.coerceTraceIndices = function(gd, traceIndices) {
  *
  */
 exports.manageArrayContainers = function(np, newVal, undoit) {
-    var obj = np.obj,
-        parts = np.parts,
-        pLength = parts.length,
-        pLast = parts[pLength - 1];
-
-    var pLastIsNumber = isNumeric(pLast);
-
-    // delete item
-    if(pLastIsNumber && newVal === null) {
-
-        // Clear item in array container when new value is null
-        var contPath = parts.slice(0, pLength - 1).join('.'),
-            cont = Lib.nestedProperty(obj, contPath).get();
-        cont.splice(pLast, 1);
-
-        // Note that nested property clears null / undefined at end of
-        // array container, but not within them.
-    }
+  var obj = np.obj,
+    parts = np.parts,
+    pLength = parts.length,
+    pLast = parts[pLength - 1];
+
+  var pLastIsNumber = isNumeric(pLast);
+
+  // delete item
+  if (pLastIsNumber && newVal === null) {
+    // Clear item in array container when new value is null
+    var contPath = parts.slice(0, pLength - 1).join("."),
+      cont = Lib.nestedProperty(obj, contPath).get();
+    cont.splice(pLast, 1);
+    // Note that nested property clears null / undefined at end of
+    // array container, but not within them.
+  } else if (pLastIsNumber && np.get() === undefined) {
     // create item
-    else if(pLastIsNumber && np.get() === undefined) {
-
-        // When adding a new item, make sure undo command will remove it
-        if(np.get() === undefined) undoit[np.astr] = null;
+    // When adding a new item, make sure undo command will remove it
+    if (np.get() === undefined) undoit[np.astr] = null;
 
-        np.set(newVal);
-    }
+    np.set(newVal);
+  } else {
     // update item
-    else {
-
-        // If the last part of attribute string isn't a number,
-        // np.set is all we need.
-        np.set(newVal);
-    }
+    // If the last part of attribute string isn't a number,
+    // np.set is all we need.
+    np.set(newVal);
+  }
 };
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index 3f5f80444d5..7a6d6062308 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -5,32 +5,27 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var d3 = require("d3");
+var isNumeric = require("fast-isnumeric");
 
+var Plotly = require("../plotly");
+var Lib = require("../lib");
+var Events = require("../lib/events");
+var Queue = require("../lib/queue");
 
-'use strict';
+var Registry = require("../registry");
+var Plots = require("../plots/plots");
+var Fx = require("../plots/cartesian/graph_interact");
+var Polar = require("../plots/polar");
 
+var Drawing = require("../components/drawing");
+var ErrorBars = require("../components/errorbars");
+var xmlnsNamespaces = require("../constants/xmlns_namespaces");
+var svgTextUtils = require("../lib/svg_text_utils");
 
-var d3 = require('d3');
-var isNumeric = require('fast-isnumeric');
-
-var Plotly = require('../plotly');
-var Lib = require('../lib');
-var Events = require('../lib/events');
-var Queue = require('../lib/queue');
-
-var Registry = require('../registry');
-var Plots = require('../plots/plots');
-var Fx = require('../plots/cartesian/graph_interact');
-var Polar = require('../plots/polar');
-
-var Drawing = require('../components/drawing');
-var ErrorBars = require('../components/errorbars');
-var xmlnsNamespaces = require('../constants/xmlns_namespaces');
-var svgTextUtils = require('../lib/svg_text_utils');
-
-var helpers = require('./helpers');
-var subroutines = require('./subroutines');
-
+var helpers = require("./helpers");
+var subroutines = require("./subroutines");
 
 /**
  * Main plot-creation function
@@ -47,465 +42,468 @@ var subroutines = require('./subroutines');
  *
  */
 Plotly.plot = function(gd, data, layout, config) {
-    var frames;
-
-    gd = helpers.getGraphDiv(gd);
-
-    // Events.init is idempotent and bails early if gd has already been init'd
-    Events.init(gd);
-
-    if(Lib.isPlainObject(data)) {
-        var obj = data;
-        data = obj.data;
-        layout = obj.layout;
-        config = obj.config;
-        frames = obj.frames;
-    }
-
-    var okToPlot = Events.triggerHandler(gd, 'plotly_beforeplot', [data, layout, config]);
-    if(okToPlot === false) return Promise.reject();
-
-    // if there's no data or layout, and this isn't yet a plotly plot
-    // container, log a warning to help plotly.js users debug
-    if(!data && !layout && !Lib.isPlotDiv(gd)) {
-        Lib.warn('Calling Plotly.plot as if redrawing ' +
-            'but this container doesn\'t yet have a plot.', gd);
-    }
+  var frames;
+
+  gd = helpers.getGraphDiv(gd);
+
+  // Events.init is idempotent and bails early if gd has already been init'd
+  Events.init(gd);
+
+  if (Lib.isPlainObject(data)) {
+    var obj = data;
+    data = obj.data;
+    layout = obj.layout;
+    config = obj.config;
+    frames = obj.frames;
+  }
+
+  var okToPlot = Events.triggerHandler(gd, "plotly_beforeplot", [
+    data,
+    layout,
+    config
+  ]);
+  if (okToPlot === false) return Promise.reject();
+
+  // if there's no data or layout, and this isn't yet a plotly plot
+  // container, log a warning to help plotly.js users debug
+  if (!data && !layout && !Lib.isPlotDiv(gd)) {
+    Lib.warn(
+      "Calling Plotly.plot as if redrawing " +
+        "but this container doesn't yet have a plot.",
+      gd
+    );
+  }
 
-    function addFrames() {
-        if(frames) {
-            return Plotly.addFrames(gd, frames);
-        }
+  function addFrames() {
+    if (frames) {
+      return Plotly.addFrames(gd, frames);
     }
+  }
 
-    // transfer configuration options to gd until we move over to
-    // a more OO like model
-    setPlotContext(gd, config);
+  // transfer configuration options to gd until we move over to
+  // a more OO like model
+  setPlotContext(gd, config);
 
-    if(!layout) layout = {};
+  if (!layout) layout = {};
 
-    // hook class for plots main container (in case of plotly.js
-    // this won't be #embedded-graph or .js-tab-contents)
-    d3.select(gd).classed('js-plotly-plot', true);
+  // hook class for plots main container (in case of plotly.js
+  // this won't be #embedded-graph or .js-tab-contents)
+  d3.select(gd).classed("js-plotly-plot", true);
 
-    // off-screen getBoundingClientRect testing space,
-    // in #js-plotly-tester (and stored as gd._tester)
-    // so we can share cached text across tabs
-    Drawing.makeTester(gd);
+  // off-screen getBoundingClientRect testing space,
+  // in #js-plotly-tester (and stored as gd._tester)
+  // so we can share cached text across tabs
+  Drawing.makeTester(gd);
 
-    // collect promises for any async actions during plotting
-    // any part of the plotting code can push to gd._promises, then
-    // before we move to the next step, we check that they're all
-    // complete, and empty out the promise list again.
-    gd._promises = [];
+  // collect promises for any async actions during plotting
+  // any part of the plotting code can push to gd._promises, then
+  // before we move to the next step, we check that they're all
+  // complete, and empty out the promise list again.
+  gd._promises = [];
 
-    var graphWasEmpty = ((gd.data || []).length === 0 && Array.isArray(data));
+  var graphWasEmpty = (gd.data || []).length === 0 && Array.isArray(data);
 
-    // if there is already data on the graph, append the new data
-    // if you only want to redraw, pass a non-array for data
-    if(Array.isArray(data)) {
-        helpers.cleanData(data, gd.data);
+  // if there is already data on the graph, append the new data
+  // if you only want to redraw, pass a non-array for data
+  if (Array.isArray(data)) {
+    helpers.cleanData(data, gd.data);
 
-        if(graphWasEmpty) gd.data = data;
-        else gd.data.push.apply(gd.data, data);
-
-        // for routines outside graph_obj that want a clean tab
-        // (rather than appending to an existing one) gd.empty
-        // is used to determine whether to make a new tab
-        gd.empty = false;
-    }
+    if (graphWasEmpty) gd.data = data;
+    else gd.data.push.apply(gd.data, data);
 
-    if(!gd.layout || graphWasEmpty) gd.layout = helpers.cleanLayout(layout);
+    // for routines outside graph_obj that want a clean tab
+    // (rather than appending to an existing one) gd.empty
+    // is used to determine whether to make a new tab
+    gd.empty = false;
+  }
 
-    // if the user is trying to drag the axes, allow new data and layout
-    // to come in but don't allow a replot.
-    if(gd._dragging && !gd._transitioning) {
-        // signal to drag handler that after everything else is done
-        // we need to replot, because something has changed
-        gd._replotPending = true;
-        return Promise.reject();
-    } else {
-        // we're going ahead with a replot now
-        gd._replotPending = false;
-    }
+  if (!gd.layout || graphWasEmpty) gd.layout = helpers.cleanLayout(layout);
 
-    Plots.supplyDefaults(gd);
+  // if the user is trying to drag the axes, allow new data and layout
+  // to come in but don't allow a replot.
+  if (gd._dragging && !gd._transitioning) {
+    // signal to drag handler that after everything else is done
+    // we need to replot, because something has changed
+    gd._replotPending = true;
+    return Promise.reject();
+  } else {
+    // we're going ahead with a replot now
+    gd._replotPending = false;
+  }
 
-    // Polar plots
-    if(data && data[0] && data[0].r) return plotPolar(gd, data, layout);
+  Plots.supplyDefaults(gd);
 
-    // so we don't try to re-call Plotly.plot from inside
-    // legend and colorbar, if margins changed
-    gd._replotting = true;
+  // Polar plots
+  if (data && data[0] && data[0].r) return plotPolar(gd, data, layout);
 
-    // make or remake the framework if we need to
-    if(graphWasEmpty) makePlotFramework(gd);
+  // so we don't try to re-call Plotly.plot from inside
+  // legend and colorbar, if margins changed
+  gd._replotting = true;
 
-    // polar need a different framework
-    if(gd.framework !== makePlotFramework) {
-        gd.framework = makePlotFramework;
-        makePlotFramework(gd);
-    }
+  // make or remake the framework if we need to
+  if (graphWasEmpty) makePlotFramework(gd);
 
-    // save initial axis range once per graph
-    if(graphWasEmpty) Plotly.Axes.saveRangeInitial(gd);
+  // polar need a different framework
+  if (gd.framework !== makePlotFramework) {
+    gd.framework = makePlotFramework;
+    makePlotFramework(gd);
+  }
 
-    var fullLayout = gd._fullLayout;
+  // save initial axis range once per graph
+  if (graphWasEmpty) Plotly.Axes.saveRangeInitial(gd);
 
-    // prepare the data and find the autorange
+  var fullLayout = gd._fullLayout;
 
-    // generate calcdata, if we need to
-    // to force redoing calcdata, just delete it before calling Plotly.plot
-    var recalc = !gd.calcdata || gd.calcdata.length !== (gd._fullData || []).length;
-    if(recalc) Plots.doCalcdata(gd);
+  // prepare the data and find the autorange
+  // generate calcdata, if we need to
+  // to force redoing calcdata, just delete it before calling Plotly.plot
+  var recalc = !gd.calcdata ||
+    gd.calcdata.length !== (gd._fullData || []).length;
+  if (recalc) Plots.doCalcdata(gd);
 
-    // in case it has changed, attach fullData traces to calcdata
-    for(var i = 0; i < gd.calcdata.length; i++) {
-        gd.calcdata[i][0].trace = gd._fullData[i];
-    }
+  // in case it has changed, attach fullData traces to calcdata
+  for (var i = 0; i < gd.calcdata.length; i++) {
+    gd.calcdata[i][0].trace = gd._fullData[i];
+  }
 
-    /*
+  /*
      * start async-friendly code - now we're actually drawing things
      */
+  var oldmargins = JSON.stringify(fullLayout._size);
+
+  // draw framework first so that margin-pushing
+  // components can position themselves correctly
+  function drawFramework() {
+    var basePlotModules = fullLayout._basePlotModules;
+
+    for (var i = 0; i < basePlotModules.length; i++) {
+      if (basePlotModules[i].drawFramework) {
+        basePlotModules[i].drawFramework(gd);
+      }
+    }
+
+    return Lib.syncOrAsync([subroutines.layoutStyles, drawAxes, Fx.init], gd);
+  }
+
+  // draw anything that can affect margins.
+  // currently this is legend and colorbars
+  function marginPushers() {
+    var calcdata = gd.calcdata;
+    var i, cd, trace;
+
+    Registry.getComponentMethod("legend", "draw")(gd);
+    Registry.getComponentMethod("rangeselector", "draw")(gd);
+    Registry.getComponentMethod("sliders", "draw")(gd);
+    Registry.getComponentMethod("updatemenus", "draw")(gd);
+
+    for (i = 0; i < calcdata.length; i++) {
+      cd = calcdata[i];
+      trace = cd[0].trace;
+      if (trace.visible !== true || !trace._module.colorbar) {
+        Plots.autoMargin(gd, "cb" + trace.uid);
+      } else {
+        trace._module.colorbar(gd, cd);
+      }
+    }
+
+    Plots.doAutoMargin(gd);
+    return Plots.previousPromises(gd);
+  }
+
+  // in case the margins changed, draw margin pushers again
+  function marginPushersAgain() {
+    var seq = JSON.stringify(fullLayout._size) === oldmargins
+      ? []
+      : [marginPushers, subroutines.layoutStyles];
+
+    // re-initialize cartesian interaction,
+    // which are sometimes cleared during marginPushers
+    seq = seq.concat(Fx.init);
+
+    return Lib.syncOrAsync(seq, gd);
+  }
+
+  function positionAndAutorange() {
+    if (!recalc) return;
+
+    var subplots = Plots.getSubplotIds(fullLayout, "cartesian"),
+      modules = fullLayout._modules;
+
+    // position and range calculations for traces that
+    // depend on each other ie bars (stacked or grouped)
+    // and boxes (grouped) push each other out of the way
+    var subplotInfo, _module;
+
+    for (var i = 0; i < subplots.length; i++) {
+      subplotInfo = fullLayout._plots[subplots[i]];
+
+      for (var j = 0; j < modules.length; j++) {
+        _module = modules[j];
+        if (_module.setPositions) _module.setPositions(gd, subplotInfo);
+      }
+    }
+
+    // calc and autorange for errorbars
+    ErrorBars.calc(gd);
+
+    // TODO: autosize extra for text markers
+    return Lib.syncOrAsync(
+      [
+        Registry.getComponentMethod("shapes", "calcAutorange"),
+        Registry.getComponentMethod("annotations", "calcAutorange"),
+        doAutoRange
+      ],
+      gd
+    );
+  }
 
-    var oldmargins = JSON.stringify(fullLayout._size);
-
-    // draw framework first so that margin-pushing
-    // components can position themselves correctly
-    function drawFramework() {
-        var basePlotModules = fullLayout._basePlotModules;
-
-        for(var i = 0; i < basePlotModules.length; i++) {
-            if(basePlotModules[i].drawFramework) {
-                basePlotModules[i].drawFramework(gd);
-            }
-        }
-
-        return Lib.syncOrAsync([
-            subroutines.layoutStyles,
-            drawAxes,
-            Fx.init
-        ], gd);
-    }
-
-    // draw anything that can affect margins.
-    // currently this is legend and colorbars
-    function marginPushers() {
-        var calcdata = gd.calcdata;
-        var i, cd, trace;
-
-        Registry.getComponentMethod('legend', 'draw')(gd);
-        Registry.getComponentMethod('rangeselector', 'draw')(gd);
-        Registry.getComponentMethod('sliders', 'draw')(gd);
-        Registry.getComponentMethod('updatemenus', 'draw')(gd);
-
-        for(i = 0; i < calcdata.length; i++) {
-            cd = calcdata[i];
-            trace = cd[0].trace;
-            if(trace.visible !== true || !trace._module.colorbar) {
-                Plots.autoMargin(gd, 'cb' + trace.uid);
-            }
-            else trace._module.colorbar(gd, cd);
-        }
-
-        Plots.doAutoMargin(gd);
-        return Plots.previousPromises(gd);
-    }
-
-    // in case the margins changed, draw margin pushers again
-    function marginPushersAgain() {
-        var seq = JSON.stringify(fullLayout._size) === oldmargins ?
-            [] :
-            [marginPushers, subroutines.layoutStyles];
-
-        // re-initialize cartesian interaction,
-        // which are sometimes cleared during marginPushers
-        seq = seq.concat(Fx.init);
+  function doAutoRange() {
+    if (gd._transitioning) return;
 
-        return Lib.syncOrAsync(seq, gd);
+    var axList = Plotly.Axes.list(gd, "", true);
+    for (var i = 0; i < axList.length; i++) {
+      Plotly.Axes.doAutoRange(axList[i]);
     }
+  }
 
-    function positionAndAutorange() {
-        if(!recalc) return;
+  // draw ticks, titles, and calculate axis scaling (._b, ._m)
+  function drawAxes() {
+    return Plotly.Axes.doTicks(gd, "redraw");
+  }
 
-        var subplots = Plots.getSubplotIds(fullLayout, 'cartesian'),
-            modules = fullLayout._modules;
+  // Now plot the data
+  function drawData() {
+    var calcdata = gd.calcdata, i;
 
-        // position and range calculations for traces that
-        // depend on each other ie bars (stacked or grouped)
-        // and boxes (grouped) push each other out of the way
-
-        var subplotInfo, _module;
-
-        for(var i = 0; i < subplots.length; i++) {
-            subplotInfo = fullLayout._plots[subplots[i]];
-
-            for(var j = 0; j < modules.length; j++) {
-                _module = modules[j];
-                if(_module.setPositions) _module.setPositions(gd, subplotInfo);
-            }
-        }
+    // in case of traces that were heatmaps or contour maps
+    // previously, remove them and their colorbars explicitly
+    for (i = 0; i < calcdata.length; i++) {
+      var trace = calcdata[i][0].trace,
+        isVisible = trace.visible === true,
+        uid = trace.uid;
 
-        // calc and autorange for errorbars
-        ErrorBars.calc(gd);
+      if (!isVisible || !Registry.traceIs(trace, "2dMap")) {
+        fullLayout._paper
+          .selectAll(".hm" + uid + ",.contour" + uid + ",#clip" + uid)
+          .remove();
+      }
 
-        // TODO: autosize extra for text markers
-        return Lib.syncOrAsync([
-            Registry.getComponentMethod('shapes', 'calcAutorange'),
-            Registry.getComponentMethod('annotations', 'calcAutorange'),
-            doAutoRange
-        ], gd);
+      if (!isVisible || !trace._module.colorbar) {
+        fullLayout._infolayer.selectAll(".cb" + uid).remove();
+      }
     }
 
-    function doAutoRange() {
-        if(gd._transitioning) return;
-
-        var axList = Plotly.Axes.list(gd, '', true);
-        for(var i = 0; i < axList.length; i++) {
-            Plotly.Axes.doAutoRange(axList[i]);
-        }
+    // loop over the base plot modules present on graph
+    var basePlotModules = fullLayout._basePlotModules;
+    for (i = 0; i < basePlotModules.length; i++) {
+      basePlotModules[i].plot(gd);
     }
 
-    // draw ticks, titles, and calculate axis scaling (._b, ._m)
-    function drawAxes() {
-        return Plotly.Axes.doTicks(gd, 'redraw');
-    }
+    // keep reference to shape layers in subplots
+    var layerSubplot = fullLayout._paper.selectAll(".layer-subplot");
+    fullLayout._imageSubplotLayer = layerSubplot.selectAll(".imagelayer");
+    fullLayout._shapeSubplotLayer = layerSubplot.selectAll(".shapelayer");
 
-    // Now plot the data
-    function drawData() {
-        var calcdata = gd.calcdata,
-            i;
-
-        // in case of traces that were heatmaps or contour maps
-        // previously, remove them and their colorbars explicitly
-        for(i = 0; i < calcdata.length; i++) {
-            var trace = calcdata[i][0].trace,
-                isVisible = (trace.visible === true),
-                uid = trace.uid;
-
-            if(!isVisible || !Registry.traceIs(trace, '2dMap')) {
-                fullLayout._paper.selectAll(
-                    '.hm' + uid +
-                    ',.contour' + uid +
-                    ',#clip' + uid
-                ).remove();
-            }
+    // styling separate from drawing
+    Plots.style(gd);
 
-            if(!isVisible || !trace._module.colorbar) {
-                fullLayout._infolayer.selectAll('.cb' + uid).remove();
-            }
-        }
+    // show annotations and shapes
+    Registry.getComponentMethod("shapes", "draw")(gd);
+    Registry.getComponentMethod("annotations", "draw")(gd);
 
-        // loop over the base plot modules present on graph
-        var basePlotModules = fullLayout._basePlotModules;
-        for(i = 0; i < basePlotModules.length; i++) {
-            basePlotModules[i].plot(gd);
-        }
-
-        // keep reference to shape layers in subplots
-        var layerSubplot = fullLayout._paper.selectAll('.layer-subplot');
-        fullLayout._imageSubplotLayer = layerSubplot.selectAll('.imagelayer');
-        fullLayout._shapeSubplotLayer = layerSubplot.selectAll('.shapelayer');
-
-        // styling separate from drawing
-        Plots.style(gd);
-
-        // show annotations and shapes
-        Registry.getComponentMethod('shapes', 'draw')(gd);
-        Registry.getComponentMethod('annotations', 'draw')(gd);
-
-        // source links
-        Plots.addLinks(gd);
-
-        // Mark the first render as complete
-        gd._replotting = false;
-
-        return Plots.previousPromises(gd);
-    }
-
-    // An initial paint must be completed before these components can be
-    // correctly sized and the whole plot re-margined. gd._replotting must
-    // be set to false before these will work properly.
-    function finalDraw() {
-        Registry.getComponentMethod('shapes', 'draw')(gd);
-        Registry.getComponentMethod('images', 'draw')(gd);
-        Registry.getComponentMethod('annotations', 'draw')(gd);
-        Registry.getComponentMethod('legend', 'draw')(gd);
-        Registry.getComponentMethod('rangeslider', 'draw')(gd);
-        Registry.getComponentMethod('rangeselector', 'draw')(gd);
-        Registry.getComponentMethod('sliders', 'draw')(gd);
-        Registry.getComponentMethod('updatemenus', 'draw')(gd);
-    }
+    // source links
+    Plots.addLinks(gd);
 
-    Lib.syncOrAsync([
-        Plots.previousPromises,
-        addFrames,
-        drawFramework,
-        marginPushers,
-        marginPushersAgain,
-        positionAndAutorange,
-        subroutines.layoutStyles,
-        drawAxes,
-        drawData,
-        finalDraw
-    ], gd);
-
-    // even if everything we did was synchronous, return a promise
-    // so that the caller doesn't care which route we took
-    return Promise.all(gd._promises).then(function() {
-        gd.emit('plotly_afterplot');
-        return gd;
-    });
+    // Mark the first render as complete
+    gd._replotting = false;
+
+    return Plots.previousPromises(gd);
+  }
+
+  // An initial paint must be completed before these components can be
+  // correctly sized and the whole plot re-margined. gd._replotting must
+  // be set to false before these will work properly.
+  function finalDraw() {
+    Registry.getComponentMethod("shapes", "draw")(gd);
+    Registry.getComponentMethod("images", "draw")(gd);
+    Registry.getComponentMethod("annotations", "draw")(gd);
+    Registry.getComponentMethod("legend", "draw")(gd);
+    Registry.getComponentMethod("rangeslider", "draw")(gd);
+    Registry.getComponentMethod("rangeselector", "draw")(gd);
+    Registry.getComponentMethod("sliders", "draw")(gd);
+    Registry.getComponentMethod("updatemenus", "draw")(gd);
+  }
+
+  Lib.syncOrAsync(
+    [
+      Plots.previousPromises,
+      addFrames,
+      drawFramework,
+      marginPushers,
+      marginPushersAgain,
+      positionAndAutorange,
+      subroutines.layoutStyles,
+      drawAxes,
+      drawData,
+      finalDraw
+    ],
+    gd
+  );
+
+  // even if everything we did was synchronous, return a promise
+  // so that the caller doesn't care which route we took
+  return Promise.all(gd._promises).then(function() {
+    gd.emit("plotly_afterplot");
+    return gd;
+  });
 };
 
-
 function opaqueSetBackground(gd, bgColor) {
-    gd._fullLayout._paperdiv.style('background', 'white');
-    Plotly.defaultConfig.setBackground(gd, bgColor);
+  gd._fullLayout._paperdiv.style("background", "white");
+  Plotly.defaultConfig.setBackground(gd, bgColor);
 }
 
 function setPlotContext(gd, config) {
-    if(!gd._context) gd._context = Lib.extendFlat({}, Plotly.defaultConfig);
-    var context = gd._context;
-
-    if(config) {
-        Object.keys(config).forEach(function(key) {
-            if(key in context) {
-                if(key === 'setBackground' && config[key] === 'opaque') {
-                    context[key] = opaqueSetBackground;
-                }
-                else context[key] = config[key];
-            }
-        });
-
-        // map plot3dPixelRatio to plotGlPixelRatio for backward compatibility
-        if(config.plot3dPixelRatio && !context.plotGlPixelRatio) {
-            context.plotGlPixelRatio = context.plot3dPixelRatio;
+  if (!gd._context) gd._context = Lib.extendFlat({}, Plotly.defaultConfig);
+  var context = gd._context;
+
+  if (config) {
+    Object.keys(config).forEach(function(key) {
+      if (key in context) {
+        if (key === "setBackground" && config[key] === "opaque") {
+          context[key] = opaqueSetBackground;
+        } else {
+          context[key] = config[key];
         }
-    }
+      }
+    });
 
-    // staticPlot forces a bunch of others:
-    if(context.staticPlot) {
-        context.editable = false;
-        context.autosizable = false;
-        context.scrollZoom = false;
-        context.doubleClick = false;
-        context.showTips = false;
-        context.showLink = false;
-        context.displayModeBar = false;
-    }
+    // map plot3dPixelRatio to plotGlPixelRatio for backward compatibility
+    if (config.plot3dPixelRatio && !context.plotGlPixelRatio) {
+      context.plotGlPixelRatio = context.plot3dPixelRatio;
+    }
+  }
+
+  // staticPlot forces a bunch of others:
+  if (context.staticPlot) {
+    context.editable = false;
+    context.autosizable = false;
+    context.scrollZoom = false;
+    context.doubleClick = false;
+    context.showTips = false;
+    context.showLink = false;
+    context.displayModeBar = false;
+  }
 }
 
 function plotPolar(gd, data, layout) {
-    // build or reuse the container skeleton
-    var plotContainer = d3.select(gd).selectAll('.plot-container')
-        .data([0]);
-    plotContainer.enter()
-        .insert('div', ':first-child')
-        .classed('plot-container plotly', true);
-    var paperDiv = plotContainer.selectAll('.svg-container')
-        .data([0]);
-    paperDiv.enter().append('div')
-        .classed('svg-container', true)
-        .style('position', 'relative');
-
-    // empty it everytime for now
-    paperDiv.html('');
-
-    // fulfill gd requirements
-    if(data) gd.data = data;
-    if(layout) gd.layout = layout;
-    Polar.manager.fillLayout(gd);
-
-    // resize canvas
-    paperDiv.style({
-        width: gd._fullLayout.width + 'px',
-        height: gd._fullLayout.height + 'px'
-    });
-
-    // instantiate framework
-    gd.framework = Polar.manager.framework(gd);
-
-    // plot
-    gd.framework({data: gd.data, layout: gd.layout}, paperDiv.node());
-
-    // set undo point
-    gd.framework.setUndoPoint();
-
-    // get the resulting svg for extending it
-    var polarPlotSVG = gd.framework.svg();
-
-    // editable title
-    var opacity = 1;
-    var txt = gd._fullLayout.title;
-    if(txt === '' || !txt) opacity = 0;
-    var placeholderText = 'Click to enter title';
+  // build or reuse the container skeleton
+  var plotContainer = d3.select(gd).selectAll(".plot-container").data([0]);
+  plotContainer
+    .enter()
+    .insert("div", ":first-child")
+    .classed("plot-container plotly", true);
+  var paperDiv = plotContainer.selectAll(".svg-container").data([0]);
+  paperDiv
+    .enter()
+    .append("div")
+    .classed("svg-container", true)
+    .style("position", "relative");
+
+  // empty it everytime for now
+  paperDiv.html("");
+
+  // fulfill gd requirements
+  if (data) gd.data = data;
+  if (layout) gd.layout = layout;
+  Polar.manager.fillLayout(gd);
+
+  // resize canvas
+  paperDiv.style({
+    width: gd._fullLayout.width + "px",
+    height: gd._fullLayout.height + "px"
+  });
+
+  // instantiate framework
+  gd.framework = Polar.manager.framework(gd);
+
+  // plot
+  gd.framework({ data: gd.data, layout: gd.layout }, paperDiv.node());
+
+  // set undo point
+  gd.framework.setUndoPoint();
+
+  // get the resulting svg for extending it
+  var polarPlotSVG = gd.framework.svg();
+
+  // editable title
+  var opacity = 1;
+  var txt = gd._fullLayout.title;
+  if (txt === "" || !txt) opacity = 0;
+  var placeholderText = "Click to enter title";
+
+  var titleLayout = function() {
+    this.call(svgTextUtils.convertToTspans);
+    // TODO: html/mathjax
+    // TODO: center title
+  };
+
+  var title = polarPlotSVG.select(".title-group text").call(titleLayout);
+
+  if (gd._context.editable) {
+    title.attr({ "data-unformatted": txt });
+    if (!txt || txt === placeholderText) {
+      opacity = 0.2;
+      title
+        .attr({ "data-unformatted": placeholderText })
+        .text(placeholderText)
+        .style({ opacity: opacity })
+        .on("mouseover.opacity", function() {
+          d3.select(this).transition().duration(100).style("opacity", 1);
+        })
+        .on("mouseout.opacity", function() {
+          d3.select(this).transition().duration(1000).style("opacity", 0);
+        });
+    }
 
-    var titleLayout = function() {
-        this.call(svgTextUtils.convertToTspans);
-        // TODO: html/mathjax
-        // TODO: center title
+    var setContenteditable = function() {
+      this
+        .call(svgTextUtils.makeEditable)
+        .on("edit", function(text) {
+          gd.framework({ layout: { title: text } });
+          this.attr({ "data-unformatted": text }).text(text).call(titleLayout);
+          this.call(setContenteditable);
+        })
+        .on("cancel", function() {
+          var txt = this.attr("data-unformatted");
+          this.text(txt).call(titleLayout);
+        });
     };
+    title.call(setContenteditable);
+  }
 
-    var title = polarPlotSVG.select('.title-group text')
-        .call(titleLayout);
-
-    if(gd._context.editable) {
-        title.attr({'data-unformatted': txt});
-        if(!txt || txt === placeholderText) {
-            opacity = 0.2;
-            title.attr({'data-unformatted': placeholderText})
-                .text(placeholderText)
-                .style({opacity: opacity})
-                .on('mouseover.opacity', function() {
-                    d3.select(this).transition().duration(100)
-                        .style('opacity', 1);
-                })
-                .on('mouseout.opacity', function() {
-                    d3.select(this).transition().duration(1000)
-                        .style('opacity', 0);
-                });
-        }
-
-        var setContenteditable = function() {
-            this.call(svgTextUtils.makeEditable)
-                .on('edit', function(text) {
-                    gd.framework({layout: {title: text}});
-                    this.attr({'data-unformatted': text})
-                        .text(text)
-                        .call(titleLayout);
-                    this.call(setContenteditable);
-                })
-                .on('cancel', function() {
-                    var txt = this.attr('data-unformatted');
-                    this.text(txt).call(titleLayout);
-                });
-        };
-        title.call(setContenteditable);
-    }
-
-    gd._context.setBackground(gd, gd._fullLayout.paper_bgcolor);
-    Plots.addLinks(gd);
+  gd._context.setBackground(gd, gd._fullLayout.paper_bgcolor);
+  Plots.addLinks(gd);
 
-    return Promise.resolve();
+  return Promise.resolve();
 }
 
 // convenience function to force a full redraw, mostly for use by plotly.js
 Plotly.redraw = function(gd) {
-    gd = helpers.getGraphDiv(gd);
+  gd = helpers.getGraphDiv(gd);
 
-    if(!Lib.isPlotDiv(gd)) {
-        throw new Error('This element is not a Plotly plot: ' + gd);
-    }
+  if (!Lib.isPlotDiv(gd)) {
+    throw new Error("This element is not a Plotly plot: " + gd);
+  }
 
-    helpers.cleanData(gd.data, gd.data);
-    helpers.cleanLayout(gd.layout);
+  helpers.cleanData(gd.data, gd.data);
+  helpers.cleanLayout(gd.layout);
 
-    gd.calcdata = undefined;
-    return Plotly.plot(gd).then(function() {
-        gd.emit('plotly_redraw');
-        return gd;
-    });
+  gd.calcdata = undefined;
+  return Plotly.plot(gd).then(function() {
+    gd.emit("plotly_redraw");
+    return gd;
+  });
 };
 
 /**
@@ -517,13 +515,13 @@ Plotly.redraw = function(gd) {
  * @param {Object} config
  */
 Plotly.newPlot = function(gd, data, layout, config) {
-    gd = helpers.getGraphDiv(gd);
+  gd = helpers.getGraphDiv(gd);
 
-    // remove gl contexts
-    Plots.cleanPlot([], {}, gd._fullData || {}, gd._fullLayout || {});
+  // remove gl contexts
+  Plots.cleanPlot([], {}, gd._fullData || {}, gd._fullLayout || {});
 
-    Plots.purge(gd);
-    return Plotly.plot(gd, data, layout, config);
+  Plots.purge(gd);
+  return Plotly.plot(gd, data, layout, config);
 };
 
 /**
@@ -533,20 +531,17 @@ Plotly.newPlot = function(gd, data, layout, config) {
  * @param {Number} maxIndex The maximum index allowable (arr.length - 1)
  */
 function positivifyIndices(indices, maxIndex) {
-    var parentLength = maxIndex + 1,
-        positiveIndices = [],
-        i,
-        index;
-
-    for(i = 0; i < indices.length; i++) {
-        index = indices[i];
-        if(index < 0) {
-            positiveIndices.push(parentLength + index);
-        } else {
-            positiveIndices.push(index);
-        }
+  var parentLength = maxIndex + 1, positiveIndices = [], i, index;
+
+  for (i = 0; i < indices.length; i++) {
+    index = indices[i];
+    if (index < 0) {
+      positiveIndices.push(parentLength + index);
+    } else {
+      positiveIndices.push(index);
     }
-    return positiveIndices;
+  }
+  return positiveIndices;
 }
 
 /**
@@ -559,29 +554,30 @@ function positivifyIndices(indices, maxIndex) {
  * @param arrayName
  */
 function assertIndexArray(gd, indices, arrayName) {
-    var i,
-        index;
+  var i, index;
 
-    for(i = 0; i < indices.length; i++) {
-        index = indices[i];
+  for (i = 0; i < indices.length; i++) {
+    index = indices[i];
 
-        // validate that indices are indeed integers
-        if(index !== parseInt(index, 10)) {
-            throw new Error('all values in ' + arrayName + ' must be integers');
-        }
+    // validate that indices are indeed integers
+    if (index !== parseInt(index, 10)) {
+      throw new Error("all values in " + arrayName + " must be integers");
+    }
 
-        // check that all indices are in bounds for given gd.data array length
-        if(index >= gd.data.length || index < -gd.data.length) {
-            throw new Error(arrayName + ' must be valid indices for gd.data.');
-        }
+    // check that all indices are in bounds for given gd.data array length
+    if (index >= gd.data.length || index < -gd.data.length) {
+      throw new Error(arrayName + " must be valid indices for gd.data.");
+    }
 
-        // check that indices aren't repeated
-        if(indices.indexOf(index, i + 1) > -1 ||
-                index >= 0 && indices.indexOf(-gd.data.length + index) > -1 ||
-                index < 0 && indices.indexOf(gd.data.length + index) > -1) {
-            throw new Error('each index in ' + arrayName + ' must be unique.');
-        }
+    // check that indices aren't repeated
+    if (
+      indices.indexOf(index, i + 1) > -1 ||
+        index >= 0 && indices.indexOf(-gd.data.length + index) > -1 ||
+        index < 0 && indices.indexOf(gd.data.length + index) > -1
+    ) {
+      throw new Error("each index in " + arrayName + " must be unique.");
     }
+  }
 }
 
 /**
@@ -592,33 +588,34 @@ function assertIndexArray(gd, indices, arrayName) {
  * @param newIndices
  */
 function checkMoveTracesArgs(gd, currentIndices, newIndices) {
-
-    // check that gd has attribute 'data' and 'data' is array
-    if(!Array.isArray(gd.data)) {
-        throw new Error('gd.data must be an array.');
-    }
-
-    // validate currentIndices array
-    if(typeof currentIndices === 'undefined') {
-        throw new Error('currentIndices is a required argument.');
-    } else if(!Array.isArray(currentIndices)) {
-        currentIndices = [currentIndices];
-    }
-    assertIndexArray(gd, currentIndices, 'currentIndices');
-
-    // validate newIndices array if it exists
-    if(typeof newIndices !== 'undefined' && !Array.isArray(newIndices)) {
-        newIndices = [newIndices];
-    }
-    if(typeof newIndices !== 'undefined') {
-        assertIndexArray(gd, newIndices, 'newIndices');
-    }
-
-    // check currentIndices and newIndices are the same length if newIdices exists
-    if(typeof newIndices !== 'undefined' && currentIndices.length !== newIndices.length) {
-        throw new Error('current and new indices must be of equal length.');
-    }
-
+  // check that gd has attribute 'data' and 'data' is array
+  if (!Array.isArray(gd.data)) {
+    throw new Error("gd.data must be an array.");
+  }
+
+  // validate currentIndices array
+  if (typeof currentIndices === "undefined") {
+    throw new Error("currentIndices is a required argument.");
+  } else if (!Array.isArray(currentIndices)) {
+    currentIndices = [currentIndices];
+  }
+  assertIndexArray(gd, currentIndices, "currentIndices");
+
+  // validate newIndices array if it exists
+  if (typeof newIndices !== "undefined" && !Array.isArray(newIndices)) {
+    newIndices = [newIndices];
+  }
+  if (typeof newIndices !== "undefined") {
+    assertIndexArray(gd, newIndices, "newIndices");
+  }
+
+  // check currentIndices and newIndices are the same length if newIdices exists
+  if (
+    typeof newIndices !== "undefined" &&
+      currentIndices.length !== newIndices.length
+  ) {
+    throw new Error("current and new indices must be of equal length.");
+  }
 }
 /**
  * A private function to reduce the type checking clutter in addTraces.
@@ -628,40 +625,42 @@ function checkMoveTracesArgs(gd, currentIndices, newIndices) {
  * @param newIndices
  */
 function checkAddTracesArgs(gd, traces, newIndices) {
-    var i, value;
-
-    // check that gd has attribute 'data' and 'data' is array
-    if(!Array.isArray(gd.data)) {
-        throw new Error('gd.data must be an array.');
-    }
-
-    // make sure traces exists
-    if(typeof traces === 'undefined') {
-        throw new Error('traces must be defined.');
-    }
-
-    // make sure traces is an array
-    if(!Array.isArray(traces)) {
-        traces = [traces];
-    }
-
-    // make sure each value in traces is an object
-    for(i = 0; i < traces.length; i++) {
-        value = traces[i];
-        if(typeof value !== 'object' || (Array.isArray(value) || value === null)) {
-            throw new Error('all values in traces array must be non-array objects');
-        }
-    }
-
-    // make sure we have an index for each trace
-    if(typeof newIndices !== 'undefined' && !Array.isArray(newIndices)) {
-        newIndices = [newIndices];
-    }
-    if(typeof newIndices !== 'undefined' && newIndices.length !== traces.length) {
-        throw new Error(
-            'if indices is specified, traces.length must equal indices.length'
-        );
-    }
+  var i, value;
+
+  // check that gd has attribute 'data' and 'data' is array
+  if (!Array.isArray(gd.data)) {
+    throw new Error("gd.data must be an array.");
+  }
+
+  // make sure traces exists
+  if (typeof traces === "undefined") {
+    throw new Error("traces must be defined.");
+  }
+
+  // make sure traces is an array
+  if (!Array.isArray(traces)) {
+    traces = [traces];
+  }
+
+  // make sure each value in traces is an object
+  for (i = 0; i < traces.length; i++) {
+    value = traces[i];
+    if (typeof value !== "object" || (Array.isArray(value) || value === null)) {
+      throw new Error("all values in traces array must be non-array objects");
+    }
+  }
+
+  // make sure we have an index for each trace
+  if (typeof newIndices !== "undefined" && !Array.isArray(newIndices)) {
+    newIndices = [newIndices];
+  }
+  if (
+    typeof newIndices !== "undefined" && newIndices.length !== traces.length
+  ) {
+    throw new Error(
+      "if indices is specified, traces.length must equal indices.length"
+    );
+  }
 }
 
 /**
@@ -675,42 +674,49 @@ function checkAddTracesArgs(gd, traces, newIndices) {
  * @param maxPoints
  */
 function assertExtendTracesArgs(gd, update, indices, maxPoints) {
+  var maxPointsIsObject = Lib.isPlainObject(maxPoints);
 
-    var maxPointsIsObject = Lib.isPlainObject(maxPoints);
-
-    if(!Array.isArray(gd.data)) {
-        throw new Error('gd.data must be an array');
-    }
-    if(!Lib.isPlainObject(update)) {
-        throw new Error('update must be a key:value object');
-    }
-
-    if(typeof indices === 'undefined') {
-        throw new Error('indices must be an integer or array of integers');
-    }
+  if (!Array.isArray(gd.data)) {
+    throw new Error("gd.data must be an array");
+  }
+  if (!Lib.isPlainObject(update)) {
+    throw new Error("update must be a key:value object");
+  }
 
-    assertIndexArray(gd, indices, 'indices');
+  if (typeof indices === "undefined") {
+    throw new Error("indices must be an integer or array of integers");
+  }
 
-    for(var key in update) {
+  assertIndexArray(gd, indices, "indices");
 
-        /*
+  for (var key in update) {
+    /*
          * Verify that the attribute to be updated contains as many trace updates
          * as indices. Failure must result in throw and no-op
          */
-        if(!Array.isArray(update[key]) || update[key].length !== indices.length) {
-            throw new Error('attribute ' + key + ' must be an array of length equal to indices array length');
-        }
+    if (!Array.isArray(update[key]) || update[key].length !== indices.length) {
+      throw new Error(
+        "attribute " +
+          key +
+          " must be an array of length equal to indices array length"
+      );
+    }
 
-        /*
+    /*
          * if maxPoints is an object it must match keys and array lengths of 'update' 1:1
          */
-        if(maxPointsIsObject &&
-            (!(key in maxPoints) || !Array.isArray(maxPoints[key]) ||
-            maxPoints[key].length !== update[key].length)) {
-            throw new Error('when maxPoints is set as a key:value object it must contain a 1:1 ' +
-                            'corrispondence with the keys and number of traces in the update object');
-        }
-    }
+    if (
+      maxPointsIsObject &&
+        (!(key in maxPoints) ||
+          !Array.isArray(maxPoints[key]) ||
+          maxPoints[key].length !== update[key].length)
+    ) {
+      throw new Error(
+        "when maxPoints is set as a key:value object it must contain a 1:1 " +
+          "corrispondence with the keys and number of traces in the update object"
+      );
+    }
+  }
 }
 
 /**
@@ -723,68 +729,66 @@ function assertExtendTracesArgs(gd, update, indices, maxPoints) {
  * @return {Object[]}
  */
 function getExtendProperties(gd, update, indices, maxPoints) {
+  var maxPointsIsObject = Lib.isPlainObject(maxPoints), updateProps = [];
+  var trace, target, prop, insert, maxp;
 
-    var maxPointsIsObject = Lib.isPlainObject(maxPoints),
-        updateProps = [];
-    var trace, target, prop, insert, maxp;
+  // allow scalar index to represent a single trace position
+  if (!Array.isArray(indices)) indices = [indices];
 
-    // allow scalar index to represent a single trace position
-    if(!Array.isArray(indices)) indices = [indices];
+  // negative indices are wrapped around to their positive value. Equivalent to python indexing.
+  indices = positivifyIndices(indices, gd.data.length - 1);
 
-    // negative indices are wrapped around to their positive value. Equivalent to python indexing.
-    indices = positivifyIndices(indices, gd.data.length - 1);
-
-    // loop through all update keys and traces and harvest validated data.
-    for(var key in update) {
-
-        for(var j = 0; j < indices.length; j++) {
-
-            /*
+  // loop through all update keys and traces and harvest validated data.
+  for (var key in update) {
+    for (var j = 0; j < indices.length; j++) {
+      /*
              * Choose the trace indexed by the indices map argument and get the prop setter-getter
              * instance that references the key and value for this particular trace.
              */
-            trace = gd.data[indices[j]];
-            prop = Lib.nestedProperty(trace, key);
+      trace = gd.data[indices[j]];
+      prop = Lib.nestedProperty(trace, key);
 
-            /*
+      /*
              * Target is the existing gd.data.trace.dataArray value like "x" or "marker.size"
              * Target must exist as an Array to allow the extend operation to be performed.
              */
-            target = prop.get();
-            insert = update[key][j];
+      target = prop.get();
+      insert = update[key][j];
 
-            if(!Array.isArray(insert)) {
-                throw new Error('attribute: ' + key + ' index: ' + j + ' must be an array');
-            }
-            if(!Array.isArray(target)) {
-                throw new Error('cannot extend missing or non-array attribute: ' + key);
-            }
+      if (!Array.isArray(insert)) {
+        throw new Error(
+          "attribute: " + key + " index: " + j + " must be an array"
+        );
+      }
+      if (!Array.isArray(target)) {
+        throw new Error("cannot extend missing or non-array attribute: " + key);
+      }
 
-            /*
+      /*
              * maxPoints may be an object map or a scalar. If object select the key:value, else
              * Use the scalar maxPoints for all key and trace combinations.
              */
-            maxp = maxPointsIsObject ? maxPoints[key][j] : maxPoints;
+      maxp = maxPointsIsObject ? maxPoints[key][j] : maxPoints;
 
-            // could have chosen null here, -1 just tells us to not take a window
-            if(!isNumeric(maxp)) maxp = -1;
+      // could have chosen null here, -1 just tells us to not take a window
+      if (!isNumeric(maxp)) maxp = -1;
 
-            /*
+      /*
              * Wrap the nestedProperty in an object containing required data
              * for lengthening and windowing this particular trace - key combination.
              * Flooring maxp mirrors the behaviour of floats in the Array.slice JSnative function.
              */
-            updateProps.push({
-                prop: prop,
-                target: target,
-                insert: insert,
-                maxp: Math.floor(maxp)
-            });
-        }
+      updateProps.push({
+        prop: prop,
+        target: target,
+        insert: insert,
+        maxp: Math.floor(maxp)
+      });
     }
+  }
 
-    // all target and insertion data now validated
-    return updateProps;
+  // all target and insertion data now validated
+  return updateProps;
 }
 
 /**
@@ -798,58 +802,65 @@ function getExtendProperties(gd, update, indices, maxPoints) {
  * @param {Function} spliceArray
  * @return {Object}
  */
-function spliceTraces(gd, update, indices, maxPoints, lengthenArray, spliceArray) {
-
-    assertExtendTracesArgs(gd, update, indices, maxPoints);
-
-    var updateProps = getExtendProperties(gd, update, indices, maxPoints),
-        remainder = [],
-        undoUpdate = {},
-        undoPoints = {};
-    var target, prop, maxp;
-
-    for(var i = 0; i < updateProps.length; i++) {
-
-        /*
+function spliceTraces(
+  gd,
+  update,
+  indices,
+  maxPoints,
+  lengthenArray,
+  spliceArray
+) {
+  assertExtendTracesArgs(gd, update, indices, maxPoints);
+
+  var updateProps = getExtendProperties(gd, update, indices, maxPoints),
+    remainder = [],
+    undoUpdate = {},
+    undoPoints = {};
+  var target, prop, maxp;
+
+  for (var i = 0; i < updateProps.length; i++) {
+    /*
          * prop is the object returned by Lib.nestedProperties
          */
-        prop = updateProps[i].prop;
-        maxp = updateProps[i].maxp;
+    prop = updateProps[i].prop;
+    maxp = updateProps[i].maxp;
 
-        target = lengthenArray(updateProps[i].target, updateProps[i].insert);
+    target = lengthenArray(updateProps[i].target, updateProps[i].insert);
 
-        /*
+    /*
          * If maxp is set within post-extension trace.length, splice to maxp length.
          * Otherwise skip function call as splice op will have no effect anyway.
          */
-        if(maxp >= 0 && maxp < target.length) remainder = spliceArray(target, maxp);
+    if (maxp >= 0 && maxp < target.length) {
+      remainder = spliceArray(target, maxp);
+    }
 
-        /*
+    /*
          * to reverse this operation we need the size of the original trace as the reverse
          * operation will need to window out any lengthening operation performed in this pass.
          */
-        maxp = updateProps[i].target.length;
+    maxp = updateProps[i].target.length;
 
-        /*
+    /*
          * Magic happens here! update gd.data.trace[key] with new array data.
          */
-        prop.set(target);
+    prop.set(target);
 
-        if(!Array.isArray(undoUpdate[prop.astr])) undoUpdate[prop.astr] = [];
-        if(!Array.isArray(undoPoints[prop.astr])) undoPoints[prop.astr] = [];
+    if (!Array.isArray(undoUpdate[prop.astr])) undoUpdate[prop.astr] = [];
+    if (!Array.isArray(undoPoints[prop.astr])) undoPoints[prop.astr] = [];
 
-        /*
+    /*
          * build the inverse update object for the undo operation
          */
-        undoUpdate[prop.astr].push(remainder);
+    undoUpdate[prop.astr].push(remainder);
 
-        /*
+    /*
          * build the matching maxPoints undo object containing original trace lengths.
          */
-        undoPoints[prop.astr].push(maxp);
-    }
+    undoPoints[prop.astr].push(maxp);
+  }
 
-    return {update: undoUpdate, maxPoints: undoPoints};
+  return { update: undoUpdate, maxPoints: undoPoints };
 }
 
 /**
@@ -870,57 +881,63 @@ function spliceTraces(gd, update, indices, maxPoints, lengthenArray, spliceArray
  *
  */
 Plotly.extendTraces = function extendTraces(gd, update, indices, maxPoints) {
-    gd = helpers.getGraphDiv(gd);
+  gd = helpers.getGraphDiv(gd);
 
-    var undo = spliceTraces(gd, update, indices, maxPoints,
-
-                           /*
+  var undo = spliceTraces(
+    gd,
+    update,
+    indices,
+    maxPoints,
+    /*
                             * The Lengthen operation extends trace from end with insert
                             */
-                            function(target, insert) {
-                                return target.concat(insert);
-                            },
-
-                            /*
+    function(target, insert) {
+      return target.concat(insert);
+    },
+    /*
                              * Window the trace keeping maxPoints, counting back from the end
                              */
-                            function(target, maxPoints) {
-                                return target.splice(0, target.length - maxPoints);
-                            });
+    function(target, maxPoints) {
+      return target.splice(0, target.length - maxPoints);
+    }
+  );
 
-    var promise = Plotly.redraw(gd);
+  var promise = Plotly.redraw(gd);
 
-    var undoArgs = [gd, undo.update, indices, undo.maxPoints];
-    Queue.add(gd, Plotly.prependTraces, undoArgs, extendTraces, arguments);
+  var undoArgs = [gd, undo.update, indices, undo.maxPoints];
+  Queue.add(gd, Plotly.prependTraces, undoArgs, extendTraces, arguments);
 
-    return promise;
+  return promise;
 };
 
 Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) {
-    gd = helpers.getGraphDiv(gd);
+  gd = helpers.getGraphDiv(gd);
 
-    var undo = spliceTraces(gd, update, indices, maxPoints,
-
-                           /*
+  var undo = spliceTraces(
+    gd,
+    update,
+    indices,
+    maxPoints,
+    /*
                             * The Lengthen operation extends trace by appending insert to start
                             */
-                            function(target, insert) {
-                                return insert.concat(target);
-                            },
-
-                            /*
+    function(target, insert) {
+      return insert.concat(target);
+    },
+    /*
                              * Window the trace keeping maxPoints, counting forward from the start
                              */
-                            function(target, maxPoints) {
-                                return target.splice(maxPoints, target.length);
-                            });
+    function(target, maxPoints) {
+      return target.splice(maxPoints, target.length);
+    }
+  );
 
-    var promise = Plotly.redraw(gd);
+  var promise = Plotly.redraw(gd);
 
-    var undoArgs = [gd, undo.update, indices, undo.maxPoints];
-    Queue.add(gd, Plotly.extendTraces, undoArgs, prependTraces, arguments);
+  var undoArgs = [gd, undo.update, indices, undo.maxPoints];
+  Queue.add(gd, Plotly.extendTraces, undoArgs, prependTraces, arguments);
 
-    return promise;
+  return promise;
 };
 
 /**
@@ -933,73 +950,71 @@ Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) {
  *
  */
 Plotly.addTraces = function addTraces(gd, traces, newIndices) {
-    gd = helpers.getGraphDiv(gd);
-
-    var currentIndices = [],
-        undoFunc = Plotly.deleteTraces,
-        redoFunc = addTraces,
-        undoArgs = [gd, currentIndices],
-        redoArgs = [gd, traces],  // no newIndices here
-        i,
-        promise;
-
-    // all validation is done elsewhere to remove clutter here
-    checkAddTracesArgs(gd, traces, newIndices);
-
-    // make sure traces is an array
-    if(!Array.isArray(traces)) {
-        traces = [traces];
-    }
-
-    // make sure traces do not repeat existing ones
-    traces = traces.map(function(trace) {
-        return Lib.extendFlat({}, trace);
-    });
-
-    helpers.cleanData(traces, gd.data);
-
-    // add the traces to gd.data (no redrawing yet!)
-    for(i = 0; i < traces.length; i++) {
-        gd.data.push(traces[i]);
-    }
-
-    // to continue, we need to call moveTraces which requires currentIndices
-    for(i = 0; i < traces.length; i++) {
-        currentIndices.push(-traces.length + i);
-    }
-
-    // if the user didn't define newIndices, they just want the traces appended
-    // i.e., we can simply redraw and be done
-    if(typeof newIndices === 'undefined') {
-        promise = Plotly.redraw(gd);
-        Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
-        return promise;
-    }
-
-    // make sure indices is property defined
-    if(!Array.isArray(newIndices)) {
-        newIndices = [newIndices];
-    }
-
-    try {
-
-        // this is redundant, but necessary to not catch later possible errors!
-        checkMoveTracesArgs(gd, currentIndices, newIndices);
-    }
-    catch(error) {
-
-        // something went wrong, reset gd to be safe and rethrow error
-        gd.data.splice(gd.data.length - traces.length, traces.length);
-        throw error;
-    }
-
-    // if we're here, the user has defined specific places to place the new traces
-    // this requires some extra work that moveTraces will do
-    Queue.startSequence(gd);
+  gd = helpers.getGraphDiv(gd);
+
+  var currentIndices = [],
+    undoFunc = Plotly.deleteTraces,
+    redoFunc = addTraces,
+    undoArgs = [gd, currentIndices],
+    redoArgs = [gd, traces],
+    // no newIndices here
+    i,
+    promise;
+
+  // all validation is done elsewhere to remove clutter here
+  checkAddTracesArgs(gd, traces, newIndices);
+
+  // make sure traces is an array
+  if (!Array.isArray(traces)) {
+    traces = [traces];
+  }
+
+  // make sure traces do not repeat existing ones
+  traces = traces.map(function(trace) {
+    return Lib.extendFlat({}, trace);
+  });
+
+  helpers.cleanData(traces, gd.data);
+
+  // add the traces to gd.data (no redrawing yet!)
+  for (i = 0; i < traces.length; i++) {
+    gd.data.push(traces[i]);
+  }
+
+  // to continue, we need to call moveTraces which requires currentIndices
+  for (i = 0; i < traces.length; i++) {
+    currentIndices.push(-traces.length + i);
+  }
+
+  // if the user didn't define newIndices, they just want the traces appended
+  // i.e., we can simply redraw and be done
+  if (typeof newIndices === "undefined") {
+    promise = Plotly.redraw(gd);
     Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
-    promise = Plotly.moveTraces(gd, currentIndices, newIndices);
-    Queue.stopSequence(gd);
     return promise;
+  }
+
+  // make sure indices is property defined
+  if (!Array.isArray(newIndices)) {
+    newIndices = [newIndices];
+  }
+
+  try {
+    // this is redundant, but necessary to not catch later possible errors!
+    checkMoveTracesArgs(gd, currentIndices, newIndices);
+  } catch (error) {
+    // something went wrong, reset gd to be safe and rethrow error
+    gd.data.splice(gd.data.length - traces.length, traces.length);
+    throw error;
+  }
+
+  // if we're here, the user has defined specific places to place the new traces
+  // this requires some extra work that moveTraces will do
+  Queue.startSequence(gd);
+  Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
+  promise = Plotly.moveTraces(gd, currentIndices, newIndices);
+  Queue.stopSequence(gd);
+  return promise;
 };
 
 /**
@@ -1010,38 +1025,38 @@ Plotly.addTraces = function addTraces(gd, traces, newIndices) {
  * @param {Number|Number[]} indices The indices
  */
 Plotly.deleteTraces = function deleteTraces(gd, indices) {
-    gd = helpers.getGraphDiv(gd);
-
-    var traces = [],
-        undoFunc = Plotly.addTraces,
-        redoFunc = deleteTraces,
-        undoArgs = [gd, traces, indices],
-        redoArgs = [gd, indices],
-        i,
-        deletedTrace;
-
-    // make sure indices are defined
-    if(typeof indices === 'undefined') {
-        throw new Error('indices must be an integer or array of integers.');
-    } else if(!Array.isArray(indices)) {
-        indices = [indices];
-    }
-    assertIndexArray(gd, indices, 'indices');
-
-    // convert negative indices to positive indices
-    indices = positivifyIndices(indices, gd.data.length - 1);
-
-    // we want descending here so that splicing later doesn't affect indexing
-    indices.sort(Lib.sorterDes);
-    for(i = 0; i < indices.length; i += 1) {
-        deletedTrace = gd.data.splice(indices[i], 1)[0];
-        traces.push(deletedTrace);
-    }
-
-    var promise = Plotly.redraw(gd);
-    Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
-
-    return promise;
+  gd = helpers.getGraphDiv(gd);
+
+  var traces = [],
+    undoFunc = Plotly.addTraces,
+    redoFunc = deleteTraces,
+    undoArgs = [gd, traces, indices],
+    redoArgs = [gd, indices],
+    i,
+    deletedTrace;
+
+  // make sure indices are defined
+  if (typeof indices === "undefined") {
+    throw new Error("indices must be an integer or array of integers.");
+  } else if (!Array.isArray(indices)) {
+    indices = [indices];
+  }
+  assertIndexArray(gd, indices, "indices");
+
+  // convert negative indices to positive indices
+  indices = positivifyIndices(indices, gd.data.length - 1);
+
+  // we want descending here so that splicing later doesn't affect indexing
+  indices.sort(Lib.sorterDes);
+  for (i = 0; i < indices.length; i += 1) {
+    deletedTrace = gd.data.splice(indices[i], 1)[0];
+    traces.push(deletedTrace);
+  }
+
+  var promise = Plotly.redraw(gd);
+  Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
+
+  return promise;
 };
 
 /**
@@ -1076,70 +1091,73 @@ Plotly.deleteTraces = function deleteTraces(gd, indices) {
  *      Plotly.moveTraces(gd, [b, d, e, a, c])  // same as 'move to end'
  */
 Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) {
-    gd = helpers.getGraphDiv(gd);
-
-    var newData = [],
-        movingTraceMap = [],
-        undoFunc = moveTraces,
-        redoFunc = moveTraces,
-        undoArgs = [gd, newIndices, currentIndices],
-        redoArgs = [gd, currentIndices, newIndices],
-        i;
-
-    // to reduce complexity here, check args elsewhere
-    // this throws errors where appropriate
-    checkMoveTracesArgs(gd, currentIndices, newIndices);
-
-    // make sure currentIndices is an array
-    currentIndices = Array.isArray(currentIndices) ? currentIndices : [currentIndices];
-
-    // if undefined, define newIndices to point to the end of gd.data array
-    if(typeof newIndices === 'undefined') {
-        newIndices = [];
-        for(i = 0; i < currentIndices.length; i++) {
-            newIndices.push(-currentIndices.length + i);
-        }
-    }
-
-    // make sure newIndices is an array if it's user-defined
-    newIndices = Array.isArray(newIndices) ? newIndices : [newIndices];
-
-    // convert negative indices to positive indices (they're the same length)
-    currentIndices = positivifyIndices(currentIndices, gd.data.length - 1);
-    newIndices = positivifyIndices(newIndices, gd.data.length - 1);
-
-    // at this point, we've coerced the index arrays into predictable forms
-
-    // get the traces that aren't being moved around
-    for(i = 0; i < gd.data.length; i++) {
-
-        // if index isn't in currentIndices, include it in ignored!
-        if(currentIndices.indexOf(i) === -1) {
-            newData.push(gd.data[i]);
-        }
-    }
-
-    // get a mapping of indices to moving traces
-    for(i = 0; i < currentIndices.length; i++) {
-        movingTraceMap.push({newIndex: newIndices[i], trace: gd.data[currentIndices[i]]});
-    }
-
-    // reorder this mapping by newIndex, ascending
-    movingTraceMap.sort(function(a, b) {
-        return a.newIndex - b.newIndex;
+  gd = helpers.getGraphDiv(gd);
+
+  var newData = [],
+    movingTraceMap = [],
+    undoFunc = moveTraces,
+    redoFunc = moveTraces,
+    undoArgs = [gd, newIndices, currentIndices],
+    redoArgs = [gd, currentIndices, newIndices],
+    i;
+
+  // to reduce complexity here, check args elsewhere
+  // this throws errors where appropriate
+  checkMoveTracesArgs(gd, currentIndices, newIndices);
+
+  // make sure currentIndices is an array
+  currentIndices = Array.isArray(currentIndices)
+    ? currentIndices
+    : [currentIndices];
+
+  // if undefined, define newIndices to point to the end of gd.data array
+  if (typeof newIndices === "undefined") {
+    newIndices = [];
+    for (i = 0; i < currentIndices.length; i++) {
+      newIndices.push(-currentIndices.length + i);
+    }
+  }
+
+  // make sure newIndices is an array if it's user-defined
+  newIndices = Array.isArray(newIndices) ? newIndices : [newIndices];
+
+  // convert negative indices to positive indices (they're the same length)
+  currentIndices = positivifyIndices(currentIndices, gd.data.length - 1);
+  newIndices = positivifyIndices(newIndices, gd.data.length - 1);
+
+  // at this point, we've coerced the index arrays into predictable forms
+  // get the traces that aren't being moved around
+  for (i = 0; i < gd.data.length; i++) {
+    // if index isn't in currentIndices, include it in ignored!
+    if (currentIndices.indexOf(i) === -1) {
+      newData.push(gd.data[i]);
+    }
+  }
+
+  // get a mapping of indices to moving traces
+  for (i = 0; i < currentIndices.length; i++) {
+    movingTraceMap.push({
+      newIndex: newIndices[i],
+      trace: gd.data[currentIndices[i]]
     });
+  }
 
-    // now, add the moving traces back in, in order!
-    for(i = 0; i < movingTraceMap.length; i += 1) {
-        newData.splice(movingTraceMap[i].newIndex, 0, movingTraceMap[i].trace);
-    }
+  // reorder this mapping by newIndex, ascending
+  movingTraceMap.sort(function(a, b) {
+    return a.newIndex - b.newIndex;
+  });
 
-    gd.data = newData;
+  // now, add the moving traces back in, in order!
+  for (i = 0; i < movingTraceMap.length; i += 1) {
+    newData.splice(movingTraceMap[i].newIndex, 0, movingTraceMap[i].trace);
+  }
 
-    var promise = Plotly.redraw(gd);
-    Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
+  gd.data = newData;
 
-    return promise;
+  var promise = Plotly.redraw(gd);
+  Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
+
+  return promise;
 };
 
 /**
@@ -1173,497 +1191,625 @@ Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) {
  * style files that want to specify cyclical default values).
  */
 Plotly.restyle = function restyle(gd, astr, val, traces) {
-    gd = helpers.getGraphDiv(gd);
-    helpers.clearPromiseQueue(gd);
-
-    var aobj = {};
-    if(typeof astr === 'string') aobj[astr] = val;
-    else if(Lib.isPlainObject(astr)) {
-        // the 3-arg form
-        aobj = astr;
-        if(traces === undefined) traces = val;
-    }
-    else {
-        Lib.warn('Restyle fail.', astr, val, traces);
-        return Promise.reject();
-    }
+  gd = helpers.getGraphDiv(gd);
+  helpers.clearPromiseQueue(gd);
 
-    if(Object.keys(aobj).length) gd.changed = true;
+  var aobj = {};
+  if (typeof astr === "string") {
+    aobj[astr] = val;
+  } else if (Lib.isPlainObject(astr)) {
+    // the 3-arg form
+    aobj = astr;
+    if (traces === undefined) traces = val;
+  } else {
+    Lib.warn("Restyle fail.", astr, val, traces);
+    return Promise.reject();
+  }
 
-    var specs = _restyle(gd, aobj, traces),
-        flags = specs.flags;
+  if (Object.keys(aobj).length) gd.changed = true;
 
-    // clear calcdata if required
-    if(flags.clearCalc) gd.calcdata = undefined;
+  var specs = _restyle(gd, aobj, traces), flags = specs.flags;
 
-    // fill in redraw sequence
-    var seq = [];
+  // clear calcdata if required
+  if (flags.clearCalc) gd.calcdata = undefined;
 
-    if(flags.fullReplot) {
-        seq.push(Plotly.plot);
-    }
-    else {
-        seq.push(Plots.previousPromises);
+  // fill in redraw sequence
+  var seq = [];
 
-        Plots.supplyDefaults(gd);
+  if (flags.fullReplot) {
+    seq.push(Plotly.plot);
+  } else {
+    seq.push(Plots.previousPromises);
 
-        if(flags.dostyle) seq.push(subroutines.doTraceStyle);
-        if(flags.docolorbars) seq.push(subroutines.doColorBars);
-    }
+    Plots.supplyDefaults(gd);
 
-    Queue.add(gd,
-        restyle, [gd, specs.undoit, specs.traces],
-        restyle, [gd, specs.redoit, specs.traces]
-    );
+    if (flags.dostyle) seq.push(subroutines.doTraceStyle);
+    if (flags.docolorbars) seq.push(subroutines.doColorBars);
+  }
 
-    var plotDone = Lib.syncOrAsync(seq, gd);
-    if(!plotDone || !plotDone.then) plotDone = Promise.resolve();
+  Queue.add(gd, restyle, [gd, specs.undoit, specs.traces], restyle, [
+    gd,
+    specs.redoit,
+    specs.traces
+  ]);
 
-    return plotDone.then(function() {
-        gd.emit('plotly_restyle', specs.eventData);
-        return gd;
-    });
+  var plotDone = Lib.syncOrAsync(seq, gd);
+  if (!plotDone || !plotDone.then) plotDone = Promise.resolve();
+
+  return plotDone.then(function() {
+    gd.emit("plotly_restyle", specs.eventData);
+    return gd;
+  });
 };
 
 function _restyle(gd, aobj, _traces) {
-    var fullLayout = gd._fullLayout,
-        fullData = gd._fullData,
-        data = gd.data,
-        i;
-
-    var traces = helpers.coerceTraceIndices(gd, _traces);
-
-    // initialize flags
-    var flags = {
-        docalc: false,
-        docalcAutorange: false,
-        doplot: false,
-        dostyle: false,
-        docolorbars: false,
-        autorangeOn: false,
-        clearCalc: false,
-        fullReplot: false
-    };
-
-    // copies of the change (and previous values of anything affected)
-    // for the undo / redo queue
-    var redoit = {},
-        undoit = {},
-        axlist,
-        flagAxForDelete = {};
-
-    // recalcAttrs attributes need a full regeneration of calcdata
-    // as well as a replot, because the right objects may not exist,
-    // or autorange may need recalculating
-    // in principle we generally shouldn't need to redo ALL traces... that's
-    // harder though.
-    var recalcAttrs = [
-        'mode', 'visible', 'type', 'orientation', 'fill',
-        'histfunc', 'histnorm', 'text',
-        'x', 'y', 'z',
-        'a', 'b', 'c',
-        'open', 'high', 'low', 'close',
-        'base', 'width', 'offset',
-        'xtype', 'x0', 'dx', 'ytype', 'y0', 'dy', 'xaxis', 'yaxis',
-        'line.width',
-        'connectgaps', 'transpose', 'zsmooth',
-        'showscale', 'marker.showscale',
-        'zauto', 'marker.cauto',
-        'autocolorscale', 'marker.autocolorscale',
-        'colorscale', 'marker.colorscale',
-        'reversescale', 'marker.reversescale',
-        'autobinx', 'nbinsx', 'xbins', 'xbins.start', 'xbins.end', 'xbins.size',
-        'autobiny', 'nbinsy', 'ybins', 'ybins.start', 'ybins.end', 'ybins.size',
-        'autocontour', 'ncontours', 'contours', 'contours.coloring',
-        'error_y', 'error_y.visible', 'error_y.value', 'error_y.type',
-        'error_y.traceref', 'error_y.array', 'error_y.symmetric',
-        'error_y.arrayminus', 'error_y.valueminus', 'error_y.tracerefminus',
-        'error_x', 'error_x.visible', 'error_x.value', 'error_x.type',
-        'error_x.traceref', 'error_x.array', 'error_x.symmetric',
-        'error_x.arrayminus', 'error_x.valueminus', 'error_x.tracerefminus',
-        'swapxy', 'swapxyaxes', 'orientationaxes',
-        'marker.colors', 'values', 'labels', 'label0', 'dlabel', 'sort',
-        'textinfo', 'textposition', 'textfont.size', 'textfont.family', 'textfont.color',
-        'insidetextfont.size', 'insidetextfont.family', 'insidetextfont.color',
-        'outsidetextfont.size', 'outsidetextfont.family', 'outsidetextfont.color',
-        'hole', 'scalegroup', 'domain', 'domain.x', 'domain.y',
-        'domain.x[0]', 'domain.x[1]', 'domain.y[0]', 'domain.y[1]',
-        'tilt', 'tiltaxis', 'depth', 'direction', 'rotation', 'pull',
-        'line.showscale', 'line.cauto', 'line.autocolorscale', 'line.reversescale',
-        'marker.line.showscale', 'marker.line.cauto', 'marker.line.autocolorscale', 'marker.line.reversescale',
-        'xcalendar', 'ycalendar',
-        'cumulative', 'cumulative.enabled', 'cumulative.direction', 'cumulative.currentbin'
-    ];
-
-    for(i = 0; i < traces.length; i++) {
-        if(Registry.traceIs(fullData[traces[i]], 'box')) {
-            recalcAttrs.push('name');
-            break;
-        }
-    }
-
-    // autorangeAttrs attributes need a full redo of calcdata
-    // only if an axis is autoranged,
-    // because .calc() is where the autorange gets determined
-    // TODO: could we break this out as well?
-    var autorangeAttrs = [
-        'marker', 'marker.size', 'textfont',
-        'boxpoints', 'jitter', 'pointpos', 'whiskerwidth', 'boxmean',
-        'tickwidth'
-    ];
-
-    // replotAttrs attributes need a replot (because different
-    // objects need to be made) but not a recalc
-    var replotAttrs = [
-        'zmin', 'zmax', 'zauto',
-        'xgap', 'ygap',
-        'marker.cmin', 'marker.cmax', 'marker.cauto',
-        'line.cmin', 'line.cmax',
-        'marker.line.cmin', 'marker.line.cmax',
-        'contours.start', 'contours.end', 'contours.size',
-        'contours.showlines',
-        'line', 'line.smoothing', 'line.shape',
-        'error_y.width', 'error_x.width', 'error_x.copy_ystyle',
-        'marker.maxdisplayed'
-    ];
-
-    // these ones may alter the axis type
-    // (at least if the first trace is involved)
-    var axtypeAttrs = [
-        'type', 'x', 'y', 'x0', 'y0', 'orientation', 'xaxis', 'yaxis'
-    ];
-
-    var zscl = ['zmin', 'zmax'],
-        xbins = ['xbins.start', 'xbins.end', 'xbins.size'],
-        ybins = ['ybins.start', 'ybins.end', 'ybins.size'],
-        contourAttrs = ['contours.start', 'contours.end', 'contours.size'];
-
-    // At the moment, only cartesian, pie and ternary plot types can afford
-    // to not go through a full replot
-    var doPlotWhiteList = ['cartesian', 'pie', 'ternary'];
-    fullLayout._basePlotModules.forEach(function(_module) {
-        if(doPlotWhiteList.indexOf(_module.name) === -1) flags.docalc = true;
-    });
-
-    // make a new empty vals array for undoit
-    function a0() { return traces.map(function() { return undefined; }); }
-
-    // for autoranging multiple axes
-    function addToAxlist(axid) {
-        var axName = Plotly.Axes.id2name(axid);
-        if(axlist.indexOf(axName) === -1) axlist.push(axName);
-    }
-
-    function autorangeAttr(axName) { return 'LAYOUT' + axName + '.autorange'; }
-
-    function rangeAttr(axName) { return 'LAYOUT' + axName + '.range'; }
-
-    // for attrs that interact (like scales & autoscales), save the
-    // old vals before making the change
-    // val=undefined will not set a value, just record what the value was.
-    // val=null will delete the attribute
-    // attr can be an array to set several at once (all to the same val)
-    function doextra(attr, val, i) {
-        if(Array.isArray(attr)) {
-            attr.forEach(function(a) { doextra(a, val, i); });
-            return;
-        }
-        // quit if explicitly setting this elsewhere
-        if(attr in aobj) return;
-
-        var extraparam;
-        if(attr.substr(0, 6) === 'LAYOUT') {
-            extraparam = Lib.nestedProperty(gd.layout, attr.replace('LAYOUT', ''));
-        } else {
-            extraparam = Lib.nestedProperty(data[traces[i]], attr);
-        }
-
-        if(!(attr in undoit)) {
-            undoit[attr] = a0();
-        }
-        if(undoit[attr][i] === undefined) {
-            undoit[attr][i] = extraparam.get();
-        }
-        if(val !== undefined) {
-            extraparam.set(val);
-        }
-    }
-
-    // now make the changes to gd.data (and occasionally gd.layout)
-    // and figure out what kind of graphics update we need to do
-    for(var ai in aobj) {
-        var vi = aobj[ai],
-            cont,
-            contFull,
-            param,
-            oldVal,
-            newVal;
-
-        redoit[ai] = vi;
-
-        if(ai.substr(0, 6) === 'LAYOUT') {
-            param = Lib.nestedProperty(gd.layout, ai.replace('LAYOUT', ''));
-            undoit[ai] = [param.get()];
-            // since we're allowing val to be an array, allow it here too,
-            // even though that's meaningless
-            param.set(Array.isArray(vi) ? vi[0] : vi);
-            // ironically, the layout attrs in restyle only require replot,
-            // not relayout
-            flags.docalc = true;
-            continue;
-        }
-
-        // set attribute in gd.data
-        undoit[ai] = a0();
-        for(i = 0; i < traces.length; i++) {
-            cont = data[traces[i]];
-            contFull = fullData[traces[i]];
-            param = Lib.nestedProperty(cont, ai);
-            oldVal = param.get();
-            newVal = Array.isArray(vi) ? vi[i % vi.length] : vi;
-
-            if(newVal === undefined) continue;
-
-            // setting bin or z settings should turn off auto
-            // and setting auto should save bin or z settings
-            if(zscl.indexOf(ai) !== -1) {
-                doextra('zauto', false, i);
-            }
-            else if(ai === 'colorscale') {
-                doextra('autocolorscale', false, i);
-            }
-            else if(ai === 'autocolorscale') {
-                doextra('colorscale', undefined, i);
-            }
-            else if(ai === 'marker.colorscale') {
-                doextra('marker.autocolorscale', false, i);
-            }
-            else if(ai === 'marker.autocolorscale') {
-                doextra('marker.colorscale', undefined, i);
-            }
-            else if(ai === 'zauto') {
-                doextra(zscl, undefined, i);
-            }
-            else if(xbins.indexOf(ai) !== -1) {
-                doextra('autobinx', false, i);
-            }
-            else if(ai === 'autobinx') {
-                doextra(xbins, undefined, i);
-            }
-            else if(ybins.indexOf(ai) !== -1) {
-                doextra('autobiny', false, i);
-            }
-            else if(ai === 'autobiny') {
-                doextra(ybins, undefined, i);
-            }
-            else if(contourAttrs.indexOf(ai) !== -1) {
-                doextra('autocontour', false, i);
-            }
-            else if(ai === 'autocontour') {
-                doextra(contourAttrs, undefined, i);
-            }
-            // heatmaps: setting x0 or dx, y0 or dy,
-            // should turn xtype/ytype to 'scaled' if 'array'
-            else if(['x0', 'dx'].indexOf(ai) !== -1 &&
-                    contFull.x && contFull.xtype !== 'scaled') {
-                doextra('xtype', 'scaled', i);
-            }
-            else if(['y0', 'dy'].indexOf(ai) !== -1 &&
-                    contFull.y && contFull.ytype !== 'scaled') {
-                doextra('ytype', 'scaled', i);
-            }
-            // changing colorbar size modes,
-            // make the resulting size not change
-            // note that colorbar fractional sizing is based on the
-            // original plot size, before anything (like a colorbar)
-            // increases the margins
-            else if(ai === 'colorbar.thicknessmode' && param.get() !== newVal &&
-                        ['fraction', 'pixels'].indexOf(newVal) !== -1 &&
-                        contFull.colorbar) {
-                var thicknorm =
-                    ['top', 'bottom'].indexOf(contFull.colorbar.orient) !== -1 ?
-                        (fullLayout.height - fullLayout.margin.t - fullLayout.margin.b) :
-                        (fullLayout.width - fullLayout.margin.l - fullLayout.margin.r);
-                doextra('colorbar.thickness', contFull.colorbar.thickness *
-                    (newVal === 'fraction' ? 1 / thicknorm : thicknorm), i);
-            }
-            else if(ai === 'colorbar.lenmode' && param.get() !== newVal &&
-                        ['fraction', 'pixels'].indexOf(newVal) !== -1 &&
-                        contFull.colorbar) {
-                var lennorm =
-                    ['top', 'bottom'].indexOf(contFull.colorbar.orient) !== -1 ?
-                        (fullLayout.width - fullLayout.margin.l - fullLayout.margin.r) :
-                        (fullLayout.height - fullLayout.margin.t - fullLayout.margin.b);
-                doextra('colorbar.len', contFull.colorbar.len *
-                    (newVal === 'fraction' ? 1 / lennorm : lennorm), i);
-            }
-            else if(ai === 'colorbar.tick0' || ai === 'colorbar.dtick') {
-                doextra('colorbar.tickmode', 'linear', i);
-            }
-            else if(ai === 'colorbar.tickmode') {
-                doextra(['colorbar.tick0', 'colorbar.dtick'], undefined, i);
-            }
-
-
-            if(ai === 'type' && (newVal === 'pie') !== (oldVal === 'pie')) {
-                var labelsTo = 'x',
-                    valuesTo = 'y';
-                if((newVal === 'bar' || oldVal === 'bar') && cont.orientation === 'h') {
-                    labelsTo = 'y';
-                    valuesTo = 'x';
-                }
-                Lib.swapAttrs(cont, ['?', '?src'], 'labels', labelsTo);
-                Lib.swapAttrs(cont, ['d?', '?0'], 'label', labelsTo);
-                Lib.swapAttrs(cont, ['?', '?src'], 'values', valuesTo);
-
-                if(oldVal === 'pie') {
-                    Lib.nestedProperty(cont, 'marker.color')
-                        .set(Lib.nestedProperty(cont, 'marker.colors').get());
-
-                    // super kludgy - but if all pies are gone we won't remove them otherwise
-                    fullLayout._pielayer.selectAll('g.trace').remove();
-                } 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;
-                }
-            }
-
-            undoit[ai][i] = oldVal;
-            // set the new value - if val is an array, it's one el per trace
-            // first check for attributes that get more complex alterations
-            var swapAttrs = [
-                'swapxy', 'swapxyaxes', 'orientation', 'orientationaxes'
-            ];
-            if(swapAttrs.indexOf(ai) !== -1) {
-                // setting an orientation: make sure it's changing
-                // before we swap everything else
-                if(ai === 'orientation') {
-                    param.set(newVal);
-                    if(param.get() === undoit[ai][i]) continue;
-                }
-                // orientationaxes has no value,
-                // it flips everything and the axes
-                else if(ai === 'orientationaxes') {
-                    cont.orientation =
-                        {v: 'h', h: 'v'}[contFull.orientation];
-                }
-                helpers.swapXYData(cont);
-            }
-            else if(Plots.dataArrayContainers.indexOf(param.parts[0]) !== -1) {
-                helpers.manageArrayContainers(param, newVal, undoit);
-                flags.docalc = true;
-            }
-            // all the other ones, just modify that one attribute
-            else param.set(newVal);
-
-        }
-
-        // swap the data attributes of the relevant x and y axes?
-        if(['swapxyaxes', 'orientationaxes'].indexOf(ai) !== -1) {
-            Plotly.Axes.swap(gd, traces);
-        }
-
-        // swap hovermode if set to "compare x/y data"
-        if(ai === 'orientationaxes') {
-            var hovermode = Lib.nestedProperty(gd.layout, 'hovermode');
-            if(hovermode.get() === 'x') {
-                hovermode.set('y');
-            } else if(hovermode.get() === 'y') {
-                hovermode.set('x');
-            }
-        }
-
-        // check if we need to call axis type
-        if((traces.indexOf(0) !== -1) && (axtypeAttrs.indexOf(ai) !== -1)) {
-            Plotly.Axes.clearTypes(gd, traces);
-            flags.docalc = true;
-        }
-
-        // switching from auto to manual binning or z scaling doesn't
-        // actually do anything but change what you see in the styling
-        // box. everything else at least needs to apply styles
-        if((['autobinx', 'autobiny', 'zauto'].indexOf(ai) === -1) ||
-                newVal !== false) {
-            flags.dostyle = true;
-        }
-        if(['colorbar', 'line'].indexOf(param.parts[0]) !== -1 ||
-            param.parts[0] === 'marker' && param.parts[1] === 'colorbar') {
-            flags.docolorbars = true;
-        }
-
-        var aiArrayStart = ai.indexOf('['),
-            aiAboveArray = aiArrayStart === -1 ? ai : ai.substr(0, aiArrayStart);
-
-        if(recalcAttrs.indexOf(aiAboveArray) !== -1) {
-            // major enough changes deserve autoscale, autobin, and
-            // non-reversed axes so people don't get confused
-            if(['orientation', 'type'].indexOf(ai) !== -1) {
-                axlist = [];
-                for(i = 0; i < traces.length; i++) {
-                    var trace = data[traces[i]];
-
-                    if(Registry.traceIs(trace, 'cartesian')) {
-                        addToAxlist(trace.xaxis || 'x');
-                        addToAxlist(trace.yaxis || 'y');
-
-                        if(ai === 'type') {
-                            doextra(['autobinx', 'autobiny'], true, i);
-                        }
-                    }
-                }
-
-                doextra(axlist.map(autorangeAttr), true, 0);
-                doextra(axlist.map(rangeAttr), [0, 1], 0);
-            }
-            flags.docalc = true;
-        }
-        else if(replotAttrs.indexOf(aiAboveArray) !== -1) flags.doplot = true;
-        else if(autorangeAttrs.indexOf(aiAboveArray) !== -1) flags.docalcAutorange = true;
-    }
-
-    // do we need to force a recalc?
-    Plotly.Axes.list(gd).forEach(function(ax) {
-        if(ax.autorange) flags.autorangeOn = true;
+  var fullLayout = gd._fullLayout, fullData = gd._fullData, data = gd.data, i;
+
+  var traces = helpers.coerceTraceIndices(gd, _traces);
+
+  // initialize flags
+  var flags = {
+    docalc: false,
+    docalcAutorange: false,
+    doplot: false,
+    dostyle: false,
+    docolorbars: false,
+    autorangeOn: false,
+    clearCalc: false,
+    fullReplot: false
+  };
+
+  // copies of the change (and previous values of anything affected)
+  // for the undo / redo queue
+  var redoit = {}, undoit = {}, axlist, flagAxForDelete = {};
+
+  // recalcAttrs attributes need a full regeneration of calcdata
+  // as well as a replot, because the right objects may not exist,
+  // or autorange may need recalculating
+  // in principle we generally shouldn't need to redo ALL traces... that's
+  // harder though.
+  var recalcAttrs = [
+    "mode",
+    "visible",
+    "type",
+    "orientation",
+    "fill",
+    "histfunc",
+    "histnorm",
+    "text",
+    "x",
+    "y",
+    "z",
+    "a",
+    "b",
+    "c",
+    "open",
+    "high",
+    "low",
+    "close",
+    "base",
+    "width",
+    "offset",
+    "xtype",
+    "x0",
+    "dx",
+    "ytype",
+    "y0",
+    "dy",
+    "xaxis",
+    "yaxis",
+    "line.width",
+    "connectgaps",
+    "transpose",
+    "zsmooth",
+    "showscale",
+    "marker.showscale",
+    "zauto",
+    "marker.cauto",
+    "autocolorscale",
+    "marker.autocolorscale",
+    "colorscale",
+    "marker.colorscale",
+    "reversescale",
+    "marker.reversescale",
+    "autobinx",
+    "nbinsx",
+    "xbins",
+    "xbins.start",
+    "xbins.end",
+    "xbins.size",
+    "autobiny",
+    "nbinsy",
+    "ybins",
+    "ybins.start",
+    "ybins.end",
+    "ybins.size",
+    "autocontour",
+    "ncontours",
+    "contours",
+    "contours.coloring",
+    "error_y",
+    "error_y.visible",
+    "error_y.value",
+    "error_y.type",
+    "error_y.traceref",
+    "error_y.array",
+    "error_y.symmetric",
+    "error_y.arrayminus",
+    "error_y.valueminus",
+    "error_y.tracerefminus",
+    "error_x",
+    "error_x.visible",
+    "error_x.value",
+    "error_x.type",
+    "error_x.traceref",
+    "error_x.array",
+    "error_x.symmetric",
+    "error_x.arrayminus",
+    "error_x.valueminus",
+    "error_x.tracerefminus",
+    "swapxy",
+    "swapxyaxes",
+    "orientationaxes",
+    "marker.colors",
+    "values",
+    "labels",
+    "label0",
+    "dlabel",
+    "sort",
+    "textinfo",
+    "textposition",
+    "textfont.size",
+    "textfont.family",
+    "textfont.color",
+    "insidetextfont.size",
+    "insidetextfont.family",
+    "insidetextfont.color",
+    "outsidetextfont.size",
+    "outsidetextfont.family",
+    "outsidetextfont.color",
+    "hole",
+    "scalegroup",
+    "domain",
+    "domain.x",
+    "domain.y",
+    "domain.x[0]",
+    "domain.x[1]",
+    "domain.y[0]",
+    "domain.y[1]",
+    "tilt",
+    "tiltaxis",
+    "depth",
+    "direction",
+    "rotation",
+    "pull",
+    "line.showscale",
+    "line.cauto",
+    "line.autocolorscale",
+    "line.reversescale",
+    "marker.line.showscale",
+    "marker.line.cauto",
+    "marker.line.autocolorscale",
+    "marker.line.reversescale",
+    "xcalendar",
+    "ycalendar",
+    "cumulative",
+    "cumulative.enabled",
+    "cumulative.direction",
+    "cumulative.currentbin"
+  ];
+
+  for (i = 0; i < traces.length; i++) {
+    if (Registry.traceIs(fullData[traces[i]], "box")) {
+      recalcAttrs.push("name");
+      break;
+    }
+  }
+
+  // autorangeAttrs attributes need a full redo of calcdata
+  // only if an axis is autoranged,
+  // because .calc() is where the autorange gets determined
+  // TODO: could we break this out as well?
+  var autorangeAttrs = [
+    "marker",
+    "marker.size",
+    "textfont",
+    "boxpoints",
+    "jitter",
+    "pointpos",
+    "whiskerwidth",
+    "boxmean",
+    "tickwidth"
+  ];
+
+  // replotAttrs attributes need a replot (because different
+  // objects need to be made) but not a recalc
+  var replotAttrs = [
+    "zmin",
+    "zmax",
+    "zauto",
+    "xgap",
+    "ygap",
+    "marker.cmin",
+    "marker.cmax",
+    "marker.cauto",
+    "line.cmin",
+    "line.cmax",
+    "marker.line.cmin",
+    "marker.line.cmax",
+    "contours.start",
+    "contours.end",
+    "contours.size",
+    "contours.showlines",
+    "line",
+    "line.smoothing",
+    "line.shape",
+    "error_y.width",
+    "error_x.width",
+    "error_x.copy_ystyle",
+    "marker.maxdisplayed"
+  ];
+
+  // these ones may alter the axis type
+  // (at least if the first trace is involved)
+  var axtypeAttrs = [
+    "type",
+    "x",
+    "y",
+    "x0",
+    "y0",
+    "orientation",
+    "xaxis",
+    "yaxis"
+  ];
+
+  var zscl = ["zmin", "zmax"],
+    xbins = ["xbins.start", "xbins.end", "xbins.size"],
+    ybins = ["ybins.start", "ybins.end", "ybins.size"],
+    contourAttrs = ["contours.start", "contours.end", "contours.size"];
+
+  // At the moment, only cartesian, pie and ternary plot types can afford
+  // to not go through a full replot
+  var doPlotWhiteList = ["cartesian", "pie", "ternary"];
+  fullLayout._basePlotModules.forEach(function(_module) {
+    if (doPlotWhiteList.indexOf(_module.name) === -1) flags.docalc = true;
+  });
+
+  // make a new empty vals array for undoit
+  function a0() {
+    return traces.map(function() {
+      return undefined;
     });
-
-    // 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.docalc || (flags.docalcAutorange && flags.autorangeOn)) {
-        flags.clearCalc = true;
-    }
-    if(flags.docalc || flags.doplot || flags.docalcAutorange) {
-        flags.fullReplot = true;
-    }
-
-    return {
-        flags: flags,
-        undoit: undoit,
-        redoit: redoit,
-        traces: traces,
-        eventData: Lib.extendDeepNoArrays([], [redoit, traces])
-    };
+  }
+
+  // for autoranging multiple axes
+  function addToAxlist(axid) {
+    var axName = Plotly.Axes.id2name(axid);
+    if (axlist.indexOf(axName) === -1) axlist.push(axName);
+  }
+
+  function autorangeAttr(axName) {
+    return "LAYOUT" + axName + ".autorange";
+  }
+
+  function rangeAttr(axName) {
+    return "LAYOUT" + axName + ".range";
+  }
+
+  // for attrs that interact (like scales & autoscales), save the
+  // old vals before making the change
+  // val=undefined will not set a value, just record what the value was.
+  // val=null will delete the attribute
+  // attr can be an array to set several at once (all to the same val)
+  function doextra(attr, val, i) {
+    if (Array.isArray(attr)) {
+      attr.forEach(function(a) {
+        doextra(a, val, i);
+      });
+      return;
+    }
+    // quit if explicitly setting this elsewhere
+    if (attr in aobj) return;
+
+    var extraparam;
+    if (attr.substr(0, 6) === "LAYOUT") {
+      extraparam = Lib.nestedProperty(gd.layout, attr.replace("LAYOUT", ""));
+    } else {
+      extraparam = Lib.nestedProperty(data[traces[i]], attr);
+    }
+
+    if (!(attr in undoit)) {
+      undoit[attr] = a0();
+    }
+    if (undoit[attr][i] === undefined) {
+      undoit[attr][i] = extraparam.get();
+    }
+    if (val !== undefined) {
+      extraparam.set(val);
+    }
+  }
+
+  // now make the changes to gd.data (and occasionally gd.layout)
+  // and figure out what kind of graphics update we need to do
+  for (var ai in aobj) {
+    var vi = aobj[ai], cont, contFull, param, oldVal, newVal;
+
+    redoit[ai] = vi;
+
+    if (ai.substr(0, 6) === "LAYOUT") {
+      param = Lib.nestedProperty(gd.layout, ai.replace("LAYOUT", ""));
+      undoit[ai] = [param.get()];
+      // since we're allowing val to be an array, allow it here too,
+      // even though that's meaningless
+      param.set(Array.isArray(vi) ? vi[0] : vi);
+      // ironically, the layout attrs in restyle only require replot,
+      // not relayout
+      flags.docalc = true;
+      continue;
+    }
+
+    // set attribute in gd.data
+    undoit[ai] = a0();
+    for (i = 0; i < traces.length; i++) {
+      cont = data[traces[i]];
+      contFull = fullData[traces[i]];
+      param = Lib.nestedProperty(cont, ai);
+      oldVal = param.get();
+      newVal = Array.isArray(vi) ? vi[i % vi.length] : vi;
+
+      if (newVal === undefined) continue;
+
+      // setting bin or z settings should turn off auto
+      // and setting auto should save bin or z settings
+      if (zscl.indexOf(ai) !== -1) {
+        doextra("zauto", false, i);
+      } else if (ai === "colorscale") {
+        doextra("autocolorscale", false, i);
+      } else if (ai === "autocolorscale") {
+        doextra("colorscale", undefined, i);
+      } else if (ai === "marker.colorscale") {
+        doextra("marker.autocolorscale", false, i);
+      } else if (ai === "marker.autocolorscale") {
+        doextra("marker.colorscale", undefined, i);
+      } else if (ai === "zauto") {
+        doextra(zscl, undefined, i);
+      } else if (xbins.indexOf(ai) !== -1) {
+        doextra("autobinx", false, i);
+      } else if (ai === "autobinx") {
+        doextra(xbins, undefined, i);
+      } else if (ybins.indexOf(ai) !== -1) {
+        doextra("autobiny", false, i);
+      } else if (ai === "autobiny") {
+        doextra(ybins, undefined, i);
+      } else if (contourAttrs.indexOf(ai) !== -1) {
+        doextra("autocontour", false, i);
+      } else if (ai === "autocontour") {
+        doextra(contourAttrs, undefined, i);
+      } else if (
+        ["x0", "dx"].indexOf(ai) !== -1 &&
+          contFull.x &&
+          contFull.xtype !== "scaled"
+      ) {
+        // heatmaps: setting x0 or dx, y0 or dy,
+        // should turn xtype/ytype to 'scaled' if 'array'
+        doextra("xtype", "scaled", i);
+      } else if (
+        ["y0", "dy"].indexOf(ai) !== -1 &&
+          contFull.y &&
+          contFull.ytype !== "scaled"
+      ) {
+        doextra("ytype", "scaled", i);
+      } else if (
+        ai === "colorbar.thicknessmode" &&
+          param.get() !== newVal &&
+          ["fraction", "pixels"].indexOf(newVal) !== -1 &&
+          contFull.colorbar
+      ) {
+        // changing colorbar size modes,
+        // make the resulting size not change
+        // note that colorbar fractional sizing is based on the
+        // original plot size, before anything (like a colorbar)
+        // increases the margins
+        var thicknorm = ["top", "bottom"].indexOf(contFull.colorbar.orient) !==
+          -1
+          ? fullLayout.height - fullLayout.margin.t - fullLayout.margin.b
+          : fullLayout.width - fullLayout.margin.l - fullLayout.margin.r;
+        doextra(
+          "colorbar.thickness",
+          contFull.colorbar.thickness *
+            (newVal === "fraction" ? 1 / thicknorm : thicknorm),
+          i
+        );
+      } else if (
+        ai === "colorbar.lenmode" &&
+          param.get() !== newVal &&
+          ["fraction", "pixels"].indexOf(newVal) !== -1 &&
+          contFull.colorbar
+      ) {
+        var lennorm = ["top", "bottom"].indexOf(contFull.colorbar.orient) !== -1
+          ? fullLayout.width - fullLayout.margin.l - fullLayout.margin.r
+          : fullLayout.height - fullLayout.margin.t - fullLayout.margin.b;
+        doextra(
+          "colorbar.len",
+          contFull.colorbar.len *
+            (newVal === "fraction" ? 1 / lennorm : lennorm),
+          i
+        );
+      } else if (ai === "colorbar.tick0" || ai === "colorbar.dtick") {
+        doextra("colorbar.tickmode", "linear", i);
+      } else if (ai === "colorbar.tickmode") {
+        doextra(["colorbar.tick0", "colorbar.dtick"], undefined, i);
+      }
+
+      if (ai === "type" && newVal === "pie" !== (oldVal === "pie")) {
+        var labelsTo = "x", valuesTo = "y";
+        if (
+          (newVal === "bar" || oldVal === "bar") && cont.orientation === "h"
+        ) {
+          labelsTo = "y";
+          valuesTo = "x";
+        }
+        Lib.swapAttrs(cont, ["?", "?src"], "labels", labelsTo);
+        Lib.swapAttrs(cont, ["d?", "?0"], "label", labelsTo);
+        Lib.swapAttrs(cont, ["?", "?src"], "values", valuesTo);
+
+        if (oldVal === "pie") {
+          Lib.nestedProperty(cont, "marker.color").set(
+            Lib.nestedProperty(cont, "marker.colors").get()
+          );
+
+          // super kludgy - but if all pies are gone we won't remove them otherwise
+          fullLayout._pielayer.selectAll("g.trace").remove();
+        } 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;
+        }
+      }
+
+      undoit[ai][i] = oldVal;
+      // set the new value - if val is an array, it's one el per trace
+      // first check for attributes that get more complex alterations
+      var swapAttrs = [
+        "swapxy",
+        "swapxyaxes",
+        "orientation",
+        "orientationaxes"
+      ];
+      if (swapAttrs.indexOf(ai) !== -1) {
+        // setting an orientation: make sure it's changing
+        // before we swap everything else
+        if (ai === "orientation") {
+          param.set(newVal);
+          if (param.get() === undoit[ai][i]) continue;
+        } else if (ai === "orientationaxes") {
+          // orientationaxes has no value,
+          // it flips everything and the axes
+          cont.orientation = ({ v: "h", h: "v" })[contFull.orientation];
+        }
+        helpers.swapXYData(cont);
+      } else if (Plots.dataArrayContainers.indexOf(param.parts[0]) !== -1) {
+        helpers.manageArrayContainers(param, newVal, undoit);
+        flags.docalc = true;
+      } else {
+        // all the other ones, just modify that one attribute
+        param.set(newVal);
+      }
+    }
+
+    // swap the data attributes of the relevant x and y axes?
+    if (["swapxyaxes", "orientationaxes"].indexOf(ai) !== -1) {
+      Plotly.Axes.swap(gd, traces);
+    }
+
+    // swap hovermode if set to "compare x/y data"
+    if (ai === "orientationaxes") {
+      var hovermode = Lib.nestedProperty(gd.layout, "hovermode");
+      if (hovermode.get() === "x") {
+        hovermode.set("y");
+      } else if (hovermode.get() === "y") {
+        hovermode.set("x");
+      }
+    }
+
+    // check if we need to call axis type
+    if (traces.indexOf(0) !== -1 && axtypeAttrs.indexOf(ai) !== -1) {
+      Plotly.Axes.clearTypes(gd, traces);
+      flags.docalc = true;
+    }
+
+    // switching from auto to manual binning or z scaling doesn't
+    // actually do anything but change what you see in the styling
+    // box. everything else at least needs to apply styles
+    if (
+      ["autobinx", "autobiny", "zauto"].indexOf(ai) === -1 || newVal !== false
+    ) {
+      flags.dostyle = true;
+    }
+    if (
+      ["colorbar", "line"].indexOf(param.parts[0]) !== -1 ||
+        param.parts[0] === "marker" && param.parts[1] === "colorbar"
+    ) {
+      flags.docolorbars = true;
+    }
+
+    var aiArrayStart = ai.indexOf("["),
+      aiAboveArray = aiArrayStart === -1 ? ai : ai.substr(0, aiArrayStart);
+
+    if (recalcAttrs.indexOf(aiAboveArray) !== -1) {
+      // major enough changes deserve autoscale, autobin, and
+      // non-reversed axes so people don't get confused
+      if (["orientation", "type"].indexOf(ai) !== -1) {
+        axlist = [];
+        for (i = 0; i < traces.length; i++) {
+          var trace = data[traces[i]];
+
+          if (Registry.traceIs(trace, "cartesian")) {
+            addToAxlist(trace.xaxis || "x");
+            addToAxlist(trace.yaxis || "y");
+
+            if (ai === "type") {
+              doextra(["autobinx", "autobiny"], true, i);
+            }
+          }
+        }
+
+        doextra(axlist.map(autorangeAttr), true, 0);
+        doextra(axlist.map(rangeAttr), [0, 1], 0);
+      }
+      flags.docalc = true;
+    } else if (replotAttrs.indexOf(aiAboveArray) !== -1) {
+      flags.doplot = true;
+    } else if (autorangeAttrs.indexOf(aiAboveArray) !== -1) {
+      flags.docalcAutorange = true;
+    }
+  }
+
+  // do we need to force a recalc?
+  Plotly.Axes.list(gd).forEach(function(ax) {
+    if (ax.autorange) flags.autorangeOn = true;
+  });
+
+  // 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.docalc || flags.docalcAutorange && flags.autorangeOn) {
+    flags.clearCalc = true;
+  }
+  if (flags.docalc || flags.doplot || flags.docalcAutorange) {
+    flags.fullReplot = true;
+  }
+
+  return {
+    flags: flags,
+    undoit: undoit,
+    redoit: redoit,
+    traces: traces,
+    eventData: Lib.extendDeepNoArrays([], [redoit, traces])
+  };
 }
 
 /**
@@ -1687,373 +1833,365 @@ function _restyle(gd, aobj, _traces) {
  *  allows setting multiple attributes simultaneously
  */
 Plotly.relayout = function relayout(gd, astr, val) {
-    gd = helpers.getGraphDiv(gd);
-    helpers.clearPromiseQueue(gd);
+  gd = helpers.getGraphDiv(gd);
+  helpers.clearPromiseQueue(gd);
 
-    if(gd.framework && gd.framework.isPolar) {
-        return Promise.resolve(gd);
-    }
+  if (gd.framework && gd.framework.isPolar) {
+    return Promise.resolve(gd);
+  }
 
-    var aobj = {};
-    if(typeof astr === 'string') aobj[astr] = val;
-    else if(Lib.isPlainObject(astr)) aobj = astr;
-    else {
-        Lib.warn('Relayout fail.', astr, val);
-        return Promise.reject();
-    }
+  var aobj = {};
+  if (typeof astr === "string") {
+    aobj[astr] = val;
+  } else if (Lib.isPlainObject(astr)) {
+    aobj = astr;
+  } else {
+    Lib.warn("Relayout fail.", astr, val);
+    return Promise.reject();
+  }
 
-    if(Object.keys(aobj).length) gd.changed = true;
+  if (Object.keys(aobj).length) gd.changed = true;
 
-    var specs = _relayout(gd, aobj),
-        flags = specs.flags;
+  var specs = _relayout(gd, aobj), flags = specs.flags;
 
-    // clear calcdata if required
-    if(flags.docalc) gd.calcdata = undefined;
+  // clear calcdata if required
+  if (flags.docalc) gd.calcdata = undefined;
 
-    // fill in redraw sequence
-    var seq = [];
+  // fill in redraw sequence
+  var seq = [];
 
-    if(flags.layoutReplot) {
-        seq.push(subroutines.layoutReplot);
-    }
-    else if(Object.keys(aobj).length) {
-        seq.push(Plots.previousPromises);
-        Plots.supplyDefaults(gd);
-
-        if(flags.dolegend) seq.push(subroutines.doLegend);
-        if(flags.dolayoutstyle) seq.push(subroutines.layoutStyles);
-        if(flags.doticks) seq.push(subroutines.doTicksRelayout);
-        if(flags.domodebar) seq.push(subroutines.doModeBar);
-        if(flags.docamera) seq.push(subroutines.doCamera);
-    }
+  if (flags.layoutReplot) {
+    seq.push(subroutines.layoutReplot);
+  } else if (Object.keys(aobj).length) {
+    seq.push(Plots.previousPromises);
+    Plots.supplyDefaults(gd);
 
-    Queue.add(gd,
-        relayout, [gd, specs.undoit],
-        relayout, [gd, specs.redoit]
-    );
+    if (flags.dolegend) seq.push(subroutines.doLegend);
+    if (flags.dolayoutstyle) seq.push(subroutines.layoutStyles);
+    if (flags.doticks) seq.push(subroutines.doTicksRelayout);
+    if (flags.domodebar) seq.push(subroutines.doModeBar);
+    if (flags.docamera) seq.push(subroutines.doCamera);
+  }
 
-    var plotDone = Lib.syncOrAsync(seq, gd);
-    if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd);
+  Queue.add(gd, relayout, [gd, specs.undoit], relayout, [gd, specs.redoit]);
 
-    return plotDone.then(function() {
-        gd.emit('plotly_relayout', specs.eventData);
-        return gd;
-    });
+  var plotDone = Lib.syncOrAsync(seq, gd);
+  if (!plotDone || !plotDone.then) plotDone = Promise.resolve(gd);
+
+  return plotDone.then(function() {
+    gd.emit("plotly_relayout", specs.eventData);
+    return gd;
+  });
 };
 
 function _relayout(gd, aobj) {
-    var layout = gd.layout,
-        fullLayout = gd._fullLayout,
-        keys = Object.keys(aobj),
-        axes = Plotly.Axes.list(gd),
-        i;
-
-    // look for 'allaxes', split out into all axes
-    // in case of 3D the axis are nested within a scene which is held in _id
-    for(i = 0; i < keys.length; i++) {
-        if(keys[i].indexOf('allaxes') === 0) {
-            for(var j = 0; j < axes.length; j++) {
-                var scene = axes[j]._id.substr(1),
-                    axisAttr = (scene.indexOf('scene') !== -1) ? (scene + '.') : '',
-                    newkey = keys[i].replace('allaxes', axisAttr + axes[j]._name);
-
-                if(!aobj[newkey]) aobj[newkey] = aobj[keys[i]];
-            }
-
-            delete aobj[keys[i]];
-        }
-    }
-
-    // initialize flags
-    var flags = {
-        dolegend: false,
-        doticks: false,
-        dolayoutstyle: false,
-        doplot: false,
-        docalc: false,
-        domodebar: false,
-        docamera: false,
-        layoutReplot: false
-    };
-
-    // copies of the change (and previous values of anything affected)
-    // for the undo / redo queue
-    var redoit = {},
-        undoit = {};
-
-    // for attrs that interact (like scales & autoscales), save the
-    // old vals before making the change
-    // val=undefined will not set a value, just record what the value was.
-    // attr can be an array to set several at once (all to the same val)
-    function doextra(attr, val) {
-        if(Array.isArray(attr)) {
-            attr.forEach(function(a) { doextra(a, val); });
-            return;
-        }
-        // quit if explicitly setting this elsewhere
-        if(attr in aobj) return;
-
-        var p = Lib.nestedProperty(layout, attr);
-        if(!(attr in undoit)) undoit[attr] = p.get();
-        if(val !== undefined) p.set(val);
-    }
-
-    // for editing annotations or shapes - is it on autoscaled axes?
-    function refAutorange(obj, axletter) {
-        var axName = Plotly.Axes.id2name(obj[axletter + 'ref'] || axletter);
-        return (fullLayout[axName] || {}).autorange;
-    }
-
-    // alter gd.layout
-    for(var ai in aobj) {
-        var p = Lib.nestedProperty(layout, ai),
-            vi = aobj[ai],
-            plen = p.parts.length,
-            // p.parts may end with an index integer if the property is an array
-            pend = typeof p.parts[plen - 1] === 'string' ? (plen - 1) : (plen - 2),
-            // last property in chain (leaf node)
-            pleaf = p.parts[pend],
-            // leaf plus immediate parent
-            pleafPlus = p.parts[pend - 1] + '.' + pleaf,
-            // trunk nodes (everything except the leaf)
-            ptrunk = p.parts.slice(0, pend).join('.'),
-            parentIn = Lib.nestedProperty(gd.layout, ptrunk).get(),
-            parentFull = Lib.nestedProperty(fullLayout, ptrunk).get();
-
-        if(vi === undefined) continue;
-
-        redoit[ai] = vi;
-
-        // axis reverse is special - it is its own inverse
-        // op and has no flag.
-        undoit[ai] = (pleaf === 'reverse') ? vi : p.get();
-
-        // Setting width or height to null must reset the graph's width / height
-        // back to its initial value as computed during the first pass in Plots.plotAutoSize.
-        //
-        // To do so, we must manually set them back here using the _initialAutoSize cache.
-        if(['width', 'height'].indexOf(ai) !== -1 && vi === null) {
-            gd._fullLayout[ai] = gd._initialAutoSize[ai];
-        }
-        // check autorange vs range
-        else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) {
-            doextra(ptrunk + '.autorange', false);
-        }
-        else if(pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) {
-            doextra([ptrunk + '.range[0]', ptrunk + '.range[1]'],
-                undefined);
-        }
-        else if(pleafPlus.match(/^aspectratio\.[xyz]$/)) {
-            doextra(p.parts[0] + '.aspectmode', 'manual');
-        }
-        else if(pleafPlus.match(/^aspectmode$/)) {
-            doextra([ptrunk + '.x', ptrunk + '.y', ptrunk + '.z'], undefined);
-        }
-        else if(pleaf === 'tick0' || pleaf === 'dtick') {
-            doextra(ptrunk + '.tickmode', 'linear');
-        }
-        else if(pleaf === 'tickmode') {
-            doextra([ptrunk + '.tick0', ptrunk + '.dtick'], undefined);
-        }
-        else if(/[xy]axis[0-9]*?$/.test(pleaf) && !Object.keys(vi || {}).length) {
-            flags.docalc = true;
-        }
-        else if(/[xy]axis[0-9]*\.categoryorder$/.test(pleafPlus)) {
-            flags.docalc = true;
-        }
-        else if(/[xy]axis[0-9]*\.categoryarray/.test(pleafPlus)) {
-            flags.docalc = true;
-        }
-
-        if(pleafPlus.indexOf('rangeslider') !== -1) {
-            flags.docalc = true;
-        }
-
-        // toggling log without autorange: need to also recalculate ranges
-        // logical XOR (ie are we toggling log)
-        if(pleaf === 'type' && ((parentFull.type === 'log') !== (vi === 'log'))) {
-            var ax = parentIn;
-
-            if(!ax || !ax.range) {
-                doextra(ptrunk + '.autorange', true);
-            }
-            else if(!parentFull.autorange) {
-                var r0 = ax.range[0],
-                    r1 = ax.range[1];
-                if(vi === 'log') {
-                    // if both limits are negative, autorange
-                    if(r0 <= 0 && r1 <= 0) {
-                        doextra(ptrunk + '.autorange', true);
-                    }
-                    // if one is negative, set it 6 orders below the other.
-                    if(r0 <= 0) r0 = r1 / 1e6;
-                    else if(r1 <= 0) r1 = r0 / 1e6;
-                    // now set the range values as appropriate
-                    doextra(ptrunk + '.range[0]', Math.log(r0) / Math.LN10);
-                    doextra(ptrunk + '.range[1]', Math.log(r1) / Math.LN10);
-                }
-                else {
-                    doextra(ptrunk + '.range[0]', Math.pow(10, r0));
-                    doextra(ptrunk + '.range[1]', Math.pow(10, r1));
-                }
-            }
-            else if(vi === 'log') {
-                // just make sure the range is positive and in the right
-                // order, it'll get recalculated later
-                ax.range = (ax.range[1] > ax.range[0]) ? [1, 2] : [2, 1];
-            }
-        }
-
-        // handle axis reversal explicitly, as there's no 'reverse' flag
-        if(pleaf === 'reverse') {
-            if(parentIn.range) parentIn.range.reverse();
-            else {
-                doextra(ptrunk + '.autorange', true);
-                parentIn.range = [1, 0];
-            }
-
-            if(parentFull.autorange) flags.docalc = true;
-            else flags.doplot = true;
-        }
-        // send annotation and shape mods one-by-one through Annotations.draw(),
-        // don't set via nestedProperty
-        // that's because add and remove are special
-        else if(p.parts[0] === 'annotations' || p.parts[0] === 'shapes') {
-            var objNum = p.parts[1],
-                objType = p.parts[0],
-                objList = layout[objType] || [],
-                obji = objList[objNum] || {};
-
-            // if p.parts is just an annotation number, and val is either
-            // 'add' or an entire annotation to add, the undo is 'remove'
-            // if val is 'remove' then undo is the whole annotation object
-            if(p.parts.length === 2) {
-
-                // new API, remove annotation / shape with `null`
-                if(vi === null) aobj[ai] = 'remove';
-
-                if(aobj[ai] === 'add' || Lib.isPlainObject(aobj[ai])) {
-                    undoit[ai] = 'remove';
-                }
-                else if(aobj[ai] === 'remove') {
-                    if(objNum === -1) {
-                        undoit[objType] = objList;
-                        delete undoit[ai];
-                    }
-                    else undoit[ai] = obji;
-                }
-                else Lib.log('???', aobj);
-            }
-
-            if((refAutorange(obji, 'x') || refAutorange(obji, 'y')) &&
-                    !Lib.containsAny(ai, ['color', 'opacity', 'align', 'dash'])) {
-                flags.docalc = true;
-            }
-
-            // TODO: combine all edits to a given annotation / shape into one call
-            // as it is we get separate calls for x and y (or ax and ay) on move
-
-            var drawOne = Registry.getComponentMethod(objType, 'drawOne');
-            drawOne(gd, objNum, p.parts.slice(2).join('.'), aobj[ai]);
-            delete aobj[ai];
-        }
-        else if(
-            Plots.layoutArrayContainers.indexOf(p.parts[0]) !== -1 ||
-            (p.parts[0] === 'mapbox' && p.parts[1] === 'layers')
-        ) {
-            helpers.manageArrayContainers(p, vi, undoit);
-            flags.doplot = true;
-        }
-        // alter gd.layout
-        else {
-            var pp1 = String(p.parts[1] || '');
-            // check whether we can short-circuit a full redraw
-            // 3d or geo at this point just needs to redraw.
-            if(p.parts[0].indexOf('scene') === 0) {
-                if(p.parts[1] === 'camera') flags.docamera = true;
-                else flags.doplot = true;
-            }
-            else if(p.parts[0].indexOf('geo') === 0) flags.doplot = true;
-            else if(p.parts[0].indexOf('ternary') === 0) flags.doplot = true;
-            else if(ai === 'paper_bgcolor') flags.doplot = true;
-            else if(fullLayout._has('gl2d') &&
-                (ai.indexOf('axis') !== -1 || p.parts[0] === 'plot_bgcolor')
-            ) flags.doplot = true;
-            else if(ai === 'hiddenlabels') flags.docalc = true;
-            else if(p.parts[0].indexOf('legend') !== -1) flags.dolegend = true;
-            else if(ai.indexOf('title') !== -1) flags.doticks = true;
-            else if(p.parts[0].indexOf('bgcolor') !== -1) flags.dolayoutstyle = true;
-            else if(p.parts.length > 1 &&
-                    Lib.containsAny(pp1, ['tick', 'exponent', 'grid', 'zeroline'])) {
-                flags.doticks = true;
-            }
-            else if(ai.indexOf('.linewidth') !== -1 &&
-                    ai.indexOf('axis') !== -1) {
-                flags.doticks = flags.dolayoutstyle = true;
-            }
-            else if(p.parts.length > 1 && pp1.indexOf('line') !== -1) {
-                flags.dolayoutstyle = true;
-            }
-            else if(p.parts.length > 1 && pp1 === 'mirror') {
-                flags.doticks = flags.dolayoutstyle = true;
-            }
-            else if(ai === 'margin.pad') {
-                flags.doticks = flags.dolayoutstyle = true;
-            }
-            else if(p.parts[0] === 'margin' ||
-                    p.parts[1] === 'autorange' ||
-                    p.parts[1] === 'rangemode' ||
-                    p.parts[1] === 'type' ||
-                    p.parts[1] === 'domain' ||
-                    ai.indexOf('calendar') !== -1 ||
-                    ai.match(/^(bar|box|font)/)) {
-                flags.docalc = true;
-            }
-            /*
+  var layout = gd.layout,
+    fullLayout = gd._fullLayout,
+    keys = Object.keys(aobj),
+    axes = Plotly.Axes.list(gd),
+    i;
+
+  // look for 'allaxes', split out into all axes
+  // in case of 3D the axis are nested within a scene which is held in _id
+  for (i = 0; i < keys.length; i++) {
+    if (keys[i].indexOf("allaxes") === 0) {
+      for (var j = 0; j < axes.length; j++) {
+        var scene = axes[j]._id.substr(1),
+          axisAttr = scene.indexOf("scene") !== -1 ? scene + "." : "",
+          newkey = keys[i].replace("allaxes", axisAttr + axes[j]._name);
+
+        if (!aobj[newkey]) aobj[newkey] = aobj[keys[i]];
+      }
+
+      delete aobj[keys[i]];
+    }
+  }
+
+  // initialize flags
+  var flags = {
+    dolegend: false,
+    doticks: false,
+    dolayoutstyle: false,
+    doplot: false,
+    docalc: false,
+    domodebar: false,
+    docamera: false,
+    layoutReplot: false
+  };
+
+  // copies of the change (and previous values of anything affected)
+  // for the undo / redo queue
+  var redoit = {}, undoit = {};
+
+  // for attrs that interact (like scales & autoscales), save the
+  // old vals before making the change
+  // val=undefined will not set a value, just record what the value was.
+  // attr can be an array to set several at once (all to the same val)
+  function doextra(attr, val) {
+    if (Array.isArray(attr)) {
+      attr.forEach(function(a) {
+        doextra(a, val);
+      });
+      return;
+    }
+    // quit if explicitly setting this elsewhere
+    if (attr in aobj) return;
+
+    var p = Lib.nestedProperty(layout, attr);
+    if (!(attr in undoit)) undoit[attr] = p.get();
+    if (val !== undefined) p.set(val);
+  }
+
+  // for editing annotations or shapes - is it on autoscaled axes?
+  function refAutorange(obj, axletter) {
+    var axName = Plotly.Axes.id2name(obj[axletter + "ref"] || axletter);
+    return (fullLayout[axName] || {}).autorange;
+  }
+
+  // alter gd.layout
+  for (var ai in aobj) {
+    var p = Lib.nestedProperty(layout, ai),
+      vi = aobj[ai],
+      plen = p.parts.length,
+      // p.parts may end with an index integer if the property is an array
+      pend = typeof p.parts[plen - 1] === "string" ? plen - 1 : plen - 2,
+      // last property in chain (leaf node)
+      pleaf = p.parts[pend],
+      // leaf plus immediate parent
+      pleafPlus = p.parts[pend - 1] + "." + pleaf,
+      // trunk nodes (everything except the leaf)
+      ptrunk = p.parts.slice(0, pend).join("."),
+      parentIn = Lib.nestedProperty(gd.layout, ptrunk).get(),
+      parentFull = Lib.nestedProperty(fullLayout, ptrunk).get();
+
+    if (vi === undefined) continue;
+
+    redoit[ai] = vi;
+
+    // axis reverse is special - it is its own inverse
+    // op and has no flag.
+    undoit[ai] = pleaf === "reverse" ? vi : p.get();
+
+    // Setting width or height to null must reset the graph's width / height
+    // back to its initial value as computed during the first pass in Plots.plotAutoSize.
+    //
+    // To do so, we must manually set them back here using the _initialAutoSize cache.
+    if (["width", "height"].indexOf(ai) !== -1 && vi === null) {
+      gd._fullLayout[ai] = gd._initialAutoSize[ai];
+    } else if (pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) {
+      // check autorange vs range
+      doextra(ptrunk + ".autorange", false);
+    } else if (pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) {
+      doextra([ptrunk + ".range[0]", ptrunk + ".range[1]"], undefined);
+    } else if (pleafPlus.match(/^aspectratio\.[xyz]$/)) {
+      doextra(p.parts[0] + ".aspectmode", "manual");
+    } else if (pleafPlus.match(/^aspectmode$/)) {
+      doextra([ptrunk + ".x", ptrunk + ".y", ptrunk + ".z"], undefined);
+    } else if (pleaf === "tick0" || pleaf === "dtick") {
+      doextra(ptrunk + ".tickmode", "linear");
+    } else if (pleaf === "tickmode") {
+      doextra([ptrunk + ".tick0", ptrunk + ".dtick"], undefined);
+    } else if (
+      /[xy]axis[0-9]*?$/.test(pleaf) && !Object.keys(vi || {}).length
+    ) {
+      flags.docalc = true;
+    } else if (/[xy]axis[0-9]*\.categoryorder$/.test(pleafPlus)) {
+      flags.docalc = true;
+    } else if (/[xy]axis[0-9]*\.categoryarray/.test(pleafPlus)) {
+      flags.docalc = true;
+    }
+
+    if (pleafPlus.indexOf("rangeslider") !== -1) {
+      flags.docalc = true;
+    }
+
+    // toggling log without autorange: need to also recalculate ranges
+    // logical XOR (ie are we toggling log)
+    if (pleaf === "type" && parentFull.type === "log" !== (vi === "log")) {
+      var ax = parentIn;
+
+      if (!ax || !ax.range) {
+        doextra(ptrunk + ".autorange", true);
+      } else if (!parentFull.autorange) {
+        var r0 = ax.range[0], r1 = ax.range[1];
+        if (vi === "log") {
+          // if both limits are negative, autorange
+          if (r0 <= 0 && r1 <= 0) {
+            doextra(ptrunk + ".autorange", true);
+          }
+          // if one is negative, set it 6 orders below the other.
+          if (r0 <= 0) r0 = r1 / 1e6;
+          else if (r1 <= 0) r1 = r0 / 1e6;
+          // now set the range values as appropriate
+          doextra(ptrunk + ".range[0]", Math.log(r0) / Math.LN10);
+          doextra(ptrunk + ".range[1]", Math.log(r1) / Math.LN10);
+        } else {
+          doextra(ptrunk + ".range[0]", Math.pow(10, r0));
+          doextra(ptrunk + ".range[1]", Math.pow(10, r1));
+        }
+      } else if (vi === "log") {
+        // just make sure the range is positive and in the right
+        // order, it'll get recalculated later
+        ax.range = ax.range[1] > ax.range[0] ? [1, 2] : [2, 1];
+      }
+    }
+
+    // handle axis reversal explicitly, as there's no 'reverse' flag
+    if (pleaf === "reverse") {
+      if (parentIn.range) {
+        parentIn.range.reverse();
+      } else {
+        doextra(ptrunk + ".autorange", true);
+        parentIn.range = [1, 0];
+      }
+
+      if (parentFull.autorange) flags.docalc = true;
+      else flags.doplot = true;
+    } else if (p.parts[0] === "annotations" || p.parts[0] === "shapes") {
+      // send annotation and shape mods one-by-one through Annotations.draw(),
+      // don't set via nestedProperty
+      // that's because add and remove are special
+      var objNum = p.parts[1],
+        objType = p.parts[0],
+        objList = layout[objType] || [],
+        obji = objList[objNum] || {};
+
+      // if p.parts is just an annotation number, and val is either
+      // 'add' or an entire annotation to add, the undo is 'remove'
+      // if val is 'remove' then undo is the whole annotation object
+      if (p.parts.length === 2) {
+        // new API, remove annotation / shape with `null`
+        if (vi === null) aobj[ai] = "remove";
+
+        if (aobj[ai] === "add" || Lib.isPlainObject(aobj[ai])) {
+          undoit[ai] = "remove";
+        } else if (aobj[ai] === "remove") {
+          if (objNum === -1) {
+            undoit[objType] = objList;
+            delete undoit[ai];
+          } else {
+            undoit[ai] = obji;
+          }
+        } else {
+          Lib.log("???", aobj);
+        }
+      }
+
+      if (
+        (refAutorange(obji, "x") || refAutorange(obji, "y")) &&
+          !Lib.containsAny(ai, ["color", "opacity", "align", "dash"])
+      ) {
+        flags.docalc = true;
+      }
+
+      // TODO: combine all edits to a given annotation / shape into one call
+      // as it is we get separate calls for x and y (or ax and ay) on move
+      var drawOne = Registry.getComponentMethod(objType, "drawOne");
+      drawOne(gd, objNum, p.parts.slice(2).join("."), aobj[ai]);
+      delete aobj[ai];
+    } else if (
+      Plots.layoutArrayContainers.indexOf(p.parts[0]) !== -1 ||
+        p.parts[0] === "mapbox" && p.parts[1] === "layers"
+    ) {
+      helpers.manageArrayContainers(p, vi, undoit);
+      flags.doplot = true;
+    } else {
+      // alter gd.layout
+      var pp1 = String(p.parts[1] || "");
+      // check whether we can short-circuit a full redraw
+      // 3d or geo at this point just needs to redraw.
+      if (p.parts[0].indexOf("scene") === 0) {
+        if (p.parts[1] === "camera") flags.docamera = true;
+        else flags.doplot = true;
+      } else if (p.parts[0].indexOf("geo") === 0) {
+        flags.doplot = true;
+      } else if (p.parts[0].indexOf("ternary") === 0) {
+        flags.doplot = true;
+      } else if (ai === "paper_bgcolor") {
+        flags.doplot = true;
+      } else if (
+        fullLayout._has("gl2d") &&
+          (ai.indexOf("axis") !== -1 || p.parts[0] === "plot_bgcolor")
+      ) {
+        flags.doplot = true;
+      } else if (ai === "hiddenlabels") {
+        flags.docalc = true;
+      } else if (p.parts[0].indexOf("legend") !== -1) {
+        flags.dolegend = true;
+      } else if (ai.indexOf("title") !== -1) {
+        flags.doticks = true;
+      } else if (p.parts[0].indexOf("bgcolor") !== -1) {
+        flags.dolayoutstyle = true;
+      } else if (
+        p.parts.length > 1 &&
+          Lib.containsAny(pp1, ["tick", "exponent", "grid", "zeroline"])
+      ) {
+        flags.doticks = true;
+      } else if (ai.indexOf(".linewidth") !== -1 && ai.indexOf("axis") !== -1) {
+        flags.doticks = flags.dolayoutstyle = true;
+      } else if (p.parts.length > 1 && pp1.indexOf("line") !== -1) {
+        flags.dolayoutstyle = true;
+      } else if (p.parts.length > 1 && pp1 === "mirror") {
+        flags.doticks = flags.dolayoutstyle = true;
+      } else if (ai === "margin.pad") {
+        flags.doticks = flags.dolayoutstyle = true;
+      } else if (
+        p.parts[0] === "margin" ||
+          p.parts[1] === "autorange" ||
+          p.parts[1] === "rangemode" ||
+          p.parts[1] === "type" ||
+          p.parts[1] === "domain" ||
+          ai.indexOf("calendar") !== -1 ||
+          ai.match(/^(bar|box|font)/)
+      ) {
+        flags.docalc = true;
+      } else if (["hovermode", "dragmode"].indexOf(ai) !== -1) {
+        /*
              * hovermode and dragmode don't need any redrawing, since they just
              * affect reaction to user input, everything else, assume full replot.
              * height, width, autosize get dealt with below. Except for the case of
              * of subplots - scenes - which require scene.updateFx to be called.
              */
-            else if(['hovermode', 'dragmode'].indexOf(ai) !== -1) flags.domodebar = true;
-            else if(['hovermode', 'dragmode', 'height',
-                'width', 'autosize'].indexOf(ai) === -1) {
-                flags.doplot = true;
-            }
-
-            p.set(vi);
-        }
-    }
-
-    var oldWidth = gd._fullLayout.width,
-        oldHeight = gd._fullLayout.height;
-
-    // coerce the updated layout
-    Plots.supplyDefaults(gd);
-
-    // calculate autosizing
-    if(gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, gd._fullLayout);
-
-    // avoid unnecessary redraws
-    var hasSizechanged = aobj.height || aobj.width ||
-        (gd._fullLayout.width !== oldWidth) ||
-        (gd._fullLayout.height !== oldHeight);
-
-    if(hasSizechanged) flags.docalc = true;
-
-    if(flags.doplot || flags.docalc) {
-        flags.layoutReplot = true;
-    }
-
-    // now all attribute mods are done, as are
-    // redo and undo so we can save them
-
-    return {
-        flags: flags,
-        undoit: undoit,
-        redoit: redoit,
-        eventData: Lib.extendDeep({}, redoit)
-    };
+        flags.domodebar = true;
+      } else if (
+        ["hovermode", "dragmode", "height", "width", "autosize"].indexOf(ai) ===
+          -1
+      ) {
+        flags.doplot = true;
+      }
+
+      p.set(vi);
+    }
+  }
+
+  var oldWidth = gd._fullLayout.width, oldHeight = gd._fullLayout.height;
+
+  // coerce the updated layout
+  Plots.supplyDefaults(gd);
+
+  // calculate autosizing
+  if (gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, gd._fullLayout);
+
+  // avoid unnecessary redraws
+  var hasSizechanged = aobj.height ||
+    aobj.width ||
+    gd._fullLayout.width !== oldWidth ||
+    gd._fullLayout.height !== oldHeight;
+
+  if (hasSizechanged) flags.docalc = true;
+
+  if (flags.doplot || flags.docalc) {
+    flags.layoutReplot = true;
+  }
+
+  // now all attribute mods are done, as are
+  // redo and undo so we can save them
+  return {
+    flags: flags,
+    undoit: undoit,
+    redoit: redoit,
+    eventData: Lib.extendDeep({}, redoit)
+  };
 }
 
 /**
@@ -2072,77 +2210,78 @@ function _relayout(gd, aobj) {
  *
  */
 Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) {
-    gd = helpers.getGraphDiv(gd);
-    helpers.clearPromiseQueue(gd);
-
-    if(gd.framework && gd.framework.isPolar) {
-        return Promise.resolve(gd);
-    }
+  gd = helpers.getGraphDiv(gd);
+  helpers.clearPromiseQueue(gd);
 
-    if(!Lib.isPlainObject(traceUpdate)) traceUpdate = {};
-    if(!Lib.isPlainObject(layoutUpdate)) layoutUpdate = {};
+  if (gd.framework && gd.framework.isPolar) {
+    return Promise.resolve(gd);
+  }
 
-    if(Object.keys(traceUpdate).length) gd.changed = true;
-    if(Object.keys(layoutUpdate).length) gd.changed = true;
+  if (!Lib.isPlainObject(traceUpdate)) traceUpdate = {};
+  if (!Lib.isPlainObject(layoutUpdate)) layoutUpdate = {};
 
-    var restyleSpecs = _restyle(gd, traceUpdate, traces),
-        restyleFlags = restyleSpecs.flags;
+  if (Object.keys(traceUpdate).length) gd.changed = true;
+  if (Object.keys(layoutUpdate).length) gd.changed = true;
 
-    var relayoutSpecs = _relayout(gd, layoutUpdate),
-        relayoutFlags = relayoutSpecs.flags;
+  var restyleSpecs = _restyle(gd, traceUpdate, traces),
+    restyleFlags = restyleSpecs.flags;
 
-    // clear calcdata if required
-    if(restyleFlags.clearCalc || relayoutFlags.docalc) gd.calcdata = undefined;
+  var relayoutSpecs = _relayout(gd, layoutUpdate),
+    relayoutFlags = relayoutSpecs.flags;
 
-    // fill in redraw sequence
-    var seq = [];
+  // clear calcdata if required
+  if (restyleFlags.clearCalc || relayoutFlags.docalc) gd.calcdata = undefined;
 
-    if(restyleFlags.fullReplot && relayoutFlags.layoutReplot) {
-        var data = gd.data,
-            layout = gd.layout;
-
-        // clear existing data/layout on gd
-        // so that Plotly.plot doesn't try to extend them
-        gd.data = undefined;
-        gd.layout = undefined;
-
-        seq.push(function() { return Plotly.plot(gd, data, layout); });
-    }
-    else if(restyleFlags.fullReplot) {
-        seq.push(Plotly.plot);
-    }
-    else if(relayoutFlags.layoutReplot) {
-        seq.push(subroutines.layoutReplot);
-    }
-    else {
-        seq.push(Plots.previousPromises);
-        Plots.supplyDefaults(gd);
-
-        if(restyleFlags.dostyle) seq.push(subroutines.doTraceStyle);
-        if(restyleFlags.docolorbars) seq.push(subroutines.doColorBars);
-        if(relayoutFlags.dolegend) seq.push(subroutines.doLegend);
-        if(relayoutFlags.dolayoutstyle) seq.push(subroutines.layoutStyles);
-        if(relayoutFlags.doticks) seq.push(subroutines.doTicksRelayout);
-        if(relayoutFlags.domodebar) seq.push(subroutines.doModeBar);
-        if(relayoutFlags.doCamera) seq.push(subroutines.doCamera);
-    }
+  // fill in redraw sequence
+  var seq = [];
 
-    Queue.add(gd,
-        update, [gd, restyleSpecs.undoit, relayoutSpecs.undoit, restyleSpecs.traces],
-        update, [gd, restyleSpecs.redoit, relayoutSpecs.redoit, restyleSpecs.traces]
-    );
+  if (restyleFlags.fullReplot && relayoutFlags.layoutReplot) {
+    var data = gd.data, layout = gd.layout;
 
-    var plotDone = Lib.syncOrAsync(seq, gd);
-    if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd);
+    // clear existing data/layout on gd
+    // so that Plotly.plot doesn't try to extend them
+    gd.data = undefined;
+    gd.layout = undefined;
 
-    return plotDone.then(function() {
-        gd.emit('plotly_update', {
-            data: restyleSpecs.eventData,
-            layout: relayoutSpecs.eventData
-        });
+    seq.push(function() {
+      return Plotly.plot(gd, data, layout);
+    });
+  } else if (restyleFlags.fullReplot) {
+    seq.push(Plotly.plot);
+  } else if (relayoutFlags.layoutReplot) {
+    seq.push(subroutines.layoutReplot);
+  } else {
+    seq.push(Plots.previousPromises);
+    Plots.supplyDefaults(gd);
 
-        return gd;
+    if (restyleFlags.dostyle) seq.push(subroutines.doTraceStyle);
+    if (restyleFlags.docolorbars) seq.push(subroutines.doColorBars);
+    if (relayoutFlags.dolegend) seq.push(subroutines.doLegend);
+    if (relayoutFlags.dolayoutstyle) seq.push(subroutines.layoutStyles);
+    if (relayoutFlags.doticks) seq.push(subroutines.doTicksRelayout);
+    if (relayoutFlags.domodebar) seq.push(subroutines.doModeBar);
+    if (relayoutFlags.doCamera) seq.push(subroutines.doCamera);
+  }
+
+  Queue.add(
+    gd,
+    update,
+    [gd, restyleSpecs.undoit, relayoutSpecs.undoit, restyleSpecs.traces],
+    update,
+    [gd, restyleSpecs.redoit, relayoutSpecs.redoit, restyleSpecs.traces]
+  );
+
+  var plotDone = Lib.syncOrAsync(seq, gd);
+  if (!plotDone || !plotDone.then) plotDone = Promise.resolve(gd);
+
+  return plotDone.then(function() {
+    gd.emit("plotly_update", {
+      data: restyleSpecs.eventData,
+      layout: relayoutSpecs.eventData
     });
+
+    return gd;
+  });
 };
 
 /**
@@ -2173,348 +2312,364 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) {
  *      configuration for the animation
  */
 Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
-    gd = helpers.getGraphDiv(gd);
-
-    if(!Lib.isPlotDiv(gd)) {
-        throw new Error(
-            'This element is not a Plotly plot: ' + gd + '. It\'s likely that you\'ve failed ' +
-            'to create a plot before animating it. For more details, see ' +
-            'https://plot.ly/javascript/animations/'
-        );
-    }
-
-    var trans = gd._transitionData;
-
-    // This is the queue of frames that will be animated as soon as possible. They
-    // are popped immediately upon the *start* of a transition:
-    if(!trans._frameQueue) {
-        trans._frameQueue = [];
+  gd = helpers.getGraphDiv(gd);
+
+  if (!Lib.isPlotDiv(gd)) {
+    throw new Error(
+      "This element is not a Plotly plot: " +
+        gd +
+        ". It's likely that you've failed " +
+        "to create a plot before animating it. For more details, see " +
+        "https://plot.ly/javascript/animations/"
+    );
+  }
+
+  var trans = gd._transitionData;
+
+  // This is the queue of frames that will be animated as soon as possible. They
+  // are popped immediately upon the *start* of a transition:
+  if (!trans._frameQueue) {
+    trans._frameQueue = [];
+  }
+
+  animationOpts = Plots.supplyAnimationDefaults(animationOpts);
+  var transitionOpts = animationOpts.transition;
+  var frameOpts = animationOpts.frame;
+
+  // Since frames are popped immediately, an empty queue only means all frames have
+  // *started* to transition, not that the animation is complete. To solve that,
+  // track a separate counter that increments at the same time as frames are added
+  // to the queue, but decrements only when the transition is complete.
+  if (trans._frameWaitingCnt === undefined) {
+    trans._frameWaitingCnt = 0;
+  }
+
+  function getTransitionOpts(i) {
+    if (Array.isArray(transitionOpts)) {
+      if (i >= transitionOpts.length) {
+        return transitionOpts[0];
+      } else {
+        return transitionOpts[i];
+      }
+    } else {
+      return transitionOpts;
     }
+  }
 
-    animationOpts = Plots.supplyAnimationDefaults(animationOpts);
-    var transitionOpts = animationOpts.transition;
-    var frameOpts = animationOpts.frame;
-
-    // Since frames are popped immediately, an empty queue only means all frames have
-    // *started* to transition, not that the animation is complete. To solve that,
-    // track a separate counter that increments at the same time as frames are added
-    // to the queue, but decrements only when the transition is complete.
-    if(trans._frameWaitingCnt === undefined) {
-        trans._frameWaitingCnt = 0;
-    }
+  function getFrameOpts(i) {
+    if (Array.isArray(frameOpts)) {
+      if (i >= frameOpts.length) {
+        return frameOpts[0];
+      } else {
+        return frameOpts[i];
+      }
+    } else {
+      return frameOpts;
+    }
+  }
+
+  // Execute a callback after the wrapper function has been called n times.
+  // This is used to defer the resolution until a transition has resovled *and*
+  // the frame has completed. If it's not done this way, then we get a race
+  // condition in which the animation might resolve before a transition is complete
+  // or vice versa.
+  function callbackOnNthTime(cb, n) {
+    var cnt = 0;
+    return function() {
+      if (cb && ++cnt === n) {
+        return cb();
+      }
+    };
+  }
 
-    function getTransitionOpts(i) {
-        if(Array.isArray(transitionOpts)) {
-            if(i >= transitionOpts.length) {
-                return transitionOpts[0];
-            } else {
-                return transitionOpts[i];
-            }
-        } else {
-            return transitionOpts;
-        }
-    }
+  return new Promise(function(resolve, reject) {
+    function discardExistingFrames() {
+      if (trans._frameQueue.length === 0) {
+        return;
+      }
 
-    function getFrameOpts(i) {
-        if(Array.isArray(frameOpts)) {
-            if(i >= frameOpts.length) {
-                return frameOpts[0];
-            } else {
-                return frameOpts[i];
-            }
-        } else {
-            return frameOpts;
+      while (trans._frameQueue.length) {
+        var next = trans._frameQueue.pop();
+        if (next.onInterrupt) {
+          next.onInterrupt();
         }
-    }
+      }
 
-    // Execute a callback after the wrapper function has been called n times.
-    // This is used to defer the resolution until a transition has resovled *and*
-    // the frame has completed. If it's not done this way, then we get a race
-    // condition in which the animation might resolve before a transition is complete
-    // or vice versa.
-    function callbackOnNthTime(cb, n) {
-        var cnt = 0;
-        return function() {
-            if(cb && ++cnt === n) {
-                return cb();
-            }
-        };
+      gd.emit("plotly_animationinterrupted", []);
     }
 
-    return new Promise(function(resolve, reject) {
-        function discardExistingFrames() {
-            if(trans._frameQueue.length === 0) {
-                return;
-            }
+    function queueFrames(frameList) {
+      if (frameList.length === 0) return;
 
-            while(trans._frameQueue.length) {
-                var next = trans._frameQueue.pop();
-                if(next.onInterrupt) {
-                    next.onInterrupt();
-                }
-            }
+      for (var i = 0; i < frameList.length; i++) {
+        var computedFrame;
 
-            gd.emit('plotly_animationinterrupted', []);
-        }
-
-        function queueFrames(frameList) {
-            if(frameList.length === 0) return;
-
-            for(var i = 0; i < frameList.length; i++) {
-                var computedFrame;
-
-                if(frameList[i].type === 'byname') {
-                    // If it's a named frame, compute it:
-                    computedFrame = Plots.computeFrame(gd, frameList[i].name);
-                } else {
-                    // Otherwise we must have been given a simple object, so treat
-                    // the input itself as the computed frame.
-                    computedFrame = frameList[i].data;
-                }
-
-                var frameOpts = getFrameOpts(i);
-                var transitionOpts = getTransitionOpts(i);
-
-                // It doesn't make much sense for the transition duration to be greater than
-                // the frame duration, so limit it:
-                transitionOpts.duration = Math.min(transitionOpts.duration, frameOpts.duration);
-
-                var nextFrame = {
-                    frame: computedFrame,
-                    name: frameList[i].name,
-                    frameOpts: frameOpts,
-                    transitionOpts: transitionOpts,
-                };
-                if(i === frameList.length - 1) {
-                    // The last frame in this .animate call stores the promise resolve
-                    // and reject callbacks. This is how we ensure that the animation
-                    // loop (which may exist as a result of a *different* .animate call)
-                    // still resolves or rejecdts this .animate call's promise. once it's
-                    // complete.
-                    nextFrame.onComplete = callbackOnNthTime(resolve, 2);
-                    nextFrame.onInterrupt = reject;
-                }
-
-                trans._frameQueue.push(nextFrame);
-            }
-
-            // Set it as never having transitioned to a frame. This will cause the animation
-            // loop to immediately transition to the next frame (which, for immediate mode,
-            // is the first frame in the list since all others would have been discarded
-            // below)
-            if(animationOpts.mode === 'immediate') {
-                trans._lastFrameAt = -Infinity;
-            }
-
-            // Only it's not already running, start a RAF loop. This could be avoided in the
-            // case that there's only one frame, but it significantly complicated the logic
-            // and only sped things up by about 5% or so for a lorenz attractor simulation.
-            // It would be a fine thing to implement, but the benefit of that optimization
-            // doesn't seem worth the extra complexity.
-            if(!trans._animationRaf) {
-                beginAnimationLoop();
-            }
-        }
-
-        function stopAnimationLoop() {
-            gd.emit('plotly_animated');
-
-            // Be sure to unset also since it's how we know whether a loop is already running:
-            window.cancelAnimationFrame(trans._animationRaf);
-            trans._animationRaf = null;
-        }
-
-        function nextFrame() {
-            if(trans._currentFrame && trans._currentFrame.onComplete) {
-                // Execute the callback and unset it to ensure it doesn't
-                // accidentally get called twice
-                trans._currentFrame.onComplete();
-            }
-
-            var newFrame = trans._currentFrame = trans._frameQueue.shift();
-
-            if(newFrame) {
-                // Since it's sometimes necessary to do deep digging into frame data,
-                // we'll consider it not 100% impossible for nulls or numbers to sneak through,
-                // so check when casting the name, just to be absolutely certain:
-                var stringName = newFrame.name ? newFrame.name.toString() : null;
-                gd._fullLayout._currentFrame = stringName;
-
-                trans._lastFrameAt = Date.now();
-                trans._timeToNext = newFrame.frameOpts.duration;
-
-                // This is simply called and it's left to .transition to decide how to manage
-                // interrupting current transitions. That means we don't need to worry about
-                // how it resolves or what happens after this:
-                Plots.transition(gd,
-                    newFrame.frame.data,
-                    newFrame.frame.layout,
-                    helpers.coerceTraceIndices(gd, newFrame.frame.traces),
-                    newFrame.frameOpts,
-                    newFrame.transitionOpts
-                ).then(function() {
-                    if(newFrame.onComplete) {
-                        newFrame.onComplete();
-                    }
-
-                });
-
-                gd.emit('plotly_animatingframe', {
-                    name: stringName,
-                    frame: newFrame.frame,
-                    animation: {
-                        frame: newFrame.frameOpts,
-                        transition: newFrame.transitionOpts,
-                    }
-                });
-            } else {
-                // If there are no more frames, then stop the RAF loop:
-                stopAnimationLoop();
-            }
+        if (frameList[i].type === "byname") {
+          // If it's a named frame, compute it:
+          computedFrame = Plots.computeFrame(gd, frameList[i].name);
+        } else {
+          // Otherwise we must have been given a simple object, so treat
+          // the input itself as the computed frame.
+          computedFrame = frameList[i].data;
         }
 
-        function beginAnimationLoop() {
-            gd.emit('plotly_animating');
+        var frameOpts = getFrameOpts(i);
+        var transitionOpts = getTransitionOpts(i);
 
-            // If no timer is running, then set last frame = long ago so that the next
-            // frame is immediately transitioned:
-            trans._lastFrameAt = -Infinity;
-            trans._timeToNext = 0;
-            trans._runningTransitions = 0;
-            trans._currentFrame = null;
-
-            var doFrame = function() {
-                // This *must* be requested before nextFrame since nextFrame may decide
-                // to cancel it if there's nothing more to animated:
-                trans._animationRaf = window.requestAnimationFrame(doFrame);
-
-                // Check if we're ready for a new frame:
-                if(Date.now() - trans._lastFrameAt > trans._timeToNext) {
-                    nextFrame();
-                }
-            };
+        // It doesn't make much sense for the transition duration to be greater than
+        // the frame duration, so limit it:
+        transitionOpts.duration = Math.min(
+          transitionOpts.duration,
+          frameOpts.duration
+        );
 
-            doFrame();
-        }
+        var nextFrame = {
+          frame: computedFrame,
+          name: frameList[i].name,
+          frameOpts: frameOpts,
+          transitionOpts: transitionOpts
+        };
+        if (i === frameList.length - 1) {
+          // The last frame in this .animate call stores the promise resolve
+          // and reject callbacks. This is how we ensure that the animation
+          // loop (which may exist as a result of a *different* .animate call)
+          // still resolves or rejecdts this .animate call's promise. once it's
+          // complete.
+          nextFrame.onComplete = callbackOnNthTime(resolve, 2);
+          nextFrame.onInterrupt = reject;
+        }
+
+        trans._frameQueue.push(nextFrame);
+      }
+
+      // Set it as never having transitioned to a frame. This will cause the animation
+      // loop to immediately transition to the next frame (which, for immediate mode,
+      // is the first frame in the list since all others would have been discarded
+      // below)
+      if (animationOpts.mode === "immediate") {
+        trans._lastFrameAt = -Infinity;
+      }
+
+      // Only it's not already running, start a RAF loop. This could be avoided in the
+      // case that there's only one frame, but it significantly complicated the logic
+      // and only sped things up by about 5% or so for a lorenz attractor simulation.
+      // It would be a fine thing to implement, but the benefit of that optimization
+      // doesn't seem worth the extra complexity.
+      if (!trans._animationRaf) {
+        beginAnimationLoop();
+      }
+    }
+
+    function stopAnimationLoop() {
+      gd.emit("plotly_animated");
+
+      // Be sure to unset also since it's how we know whether a loop is already running:
+      window.cancelAnimationFrame(trans._animationRaf);
+      trans._animationRaf = null;
+    }
+
+    function nextFrame() {
+      if (trans._currentFrame && trans._currentFrame.onComplete) {
+        // Execute the callback and unset it to ensure it doesn't
+        // accidentally get called twice
+        trans._currentFrame.onComplete();
+      }
+
+      var newFrame = trans._currentFrame = trans._frameQueue.shift();
+
+      if (newFrame) {
+        // Since it's sometimes necessary to do deep digging into frame data,
+        // we'll consider it not 100% impossible for nulls or numbers to sneak through,
+        // so check when casting the name, just to be absolutely certain:
+        var stringName = newFrame.name ? newFrame.name.toString() : null;
+        gd._fullLayout._currentFrame = stringName;
+
+        trans._lastFrameAt = Date.now();
+        trans._timeToNext = newFrame.frameOpts.duration;
+
+        // This is simply called and it's left to .transition to decide how to manage
+        // interrupting current transitions. That means we don't need to worry about
+        // how it resolves or what happens after this:
+        Plots.transition(
+          gd,
+          newFrame.frame.data,
+          newFrame.frame.layout,
+          helpers.coerceTraceIndices(gd, newFrame.frame.traces),
+          newFrame.frameOpts,
+          newFrame.transitionOpts
+        ).then(function() {
+          if (newFrame.onComplete) {
+            newFrame.onComplete();
+          }
+        });
 
-        // This is an animate-local counter that helps match up option input list
-        // items with the particular frame.
-        var configCounter = 0;
-        function setTransitionConfig(frame) {
-            if(Array.isArray(transitionOpts)) {
-                if(configCounter >= transitionOpts.length) {
-                    frame.transitionOpts = transitionOpts[configCounter];
-                } else {
-                    frame.transitionOpts = transitionOpts[0];
-                }
-            } else {
-                frame.transitionOpts = transitionOpts;
-            }
-            configCounter++;
-            return frame;
-        }
+        gd.emit("plotly_animatingframe", {
+          name: stringName,
+          frame: newFrame.frame,
+          animation: {
+            frame: newFrame.frameOpts,
+            transition: newFrame.transitionOpts
+          }
+        });
+      } else {
+        // If there are no more frames, then stop the RAF loop:
+        stopAnimationLoop();
+      }
+    }
 
-        // Disambiguate what's sort of frames have been received
-        var i, frame;
-        var frameList = [];
-        var allFrames = frameOrGroupNameOrFrameList === undefined || frameOrGroupNameOrFrameList === null;
-        var isFrameArray = Array.isArray(frameOrGroupNameOrFrameList);
-        var isSingleFrame = !allFrames && !isFrameArray && Lib.isPlainObject(frameOrGroupNameOrFrameList);
-
-        if(isSingleFrame) {
-            // In this case, a simple object has been passed to animate.
-            frameList.push({
-                type: 'object',
-                data: setTransitionConfig(Lib.extendFlat({}, frameOrGroupNameOrFrameList))
-            });
-        } else if(allFrames || ['string', 'number'].indexOf(typeof frameOrGroupNameOrFrameList) !== -1) {
-            // In this case, null or undefined has been passed so that we want to
-            // animate *all* currently defined frames
-            for(i = 0; i < trans._frames.length; i++) {
-                frame = trans._frames[i];
-
-                if(!frame) continue;
-
-                if(allFrames || String(frame.group) === String(frameOrGroupNameOrFrameList)) {
-                    frameList.push({
-                        type: 'byname',
-                        name: String(frame.name),
-                        data: setTransitionConfig({name: frame.name})
-                    });
-                }
-            }
-        } else if(isFrameArray) {
-            for(i = 0; i < frameOrGroupNameOrFrameList.length; i++) {
-                var frameOrName = frameOrGroupNameOrFrameList[i];
-                if(['number', 'string'].indexOf(typeof frameOrName) !== -1) {
-                    frameOrName = String(frameOrName);
-                    // In this case, there's an array and this frame is a string name:
-                    frameList.push({
-                        type: 'byname',
-                        name: frameOrName,
-                        data: setTransitionConfig({name: frameOrName})
-                    });
-                } else if(Lib.isPlainObject(frameOrName)) {
-                    frameList.push({
-                        type: 'object',
-                        data: setTransitionConfig(Lib.extendFlat({}, frameOrName))
-                    });
-                }
-            }
-        }
+    function beginAnimationLoop() {
+      gd.emit("plotly_animating");
 
-        // Verify that all of these frames actually exist; return and reject if not:
-        for(i = 0; i < frameList.length; i++) {
-            frame = frameList[i];
-            if(frame.type === 'byname' && !trans._frameHash[frame.data.name]) {
-                Lib.warn('animate failure: frame not found: "' + frame.data.name + '"');
-                reject();
-                return;
-            }
-        }
+      // If no timer is running, then set last frame = long ago so that the next
+      // frame is immediately transitioned:
+      trans._lastFrameAt = -Infinity;
+      trans._timeToNext = 0;
+      trans._runningTransitions = 0;
+      trans._currentFrame = null;
 
-        // If the mode is either next or immediate, then all currently queued frames must
-        // be dumped and the corresponding .animate promises rejected.
-        if(['next', 'immediate'].indexOf(animationOpts.mode) !== -1) {
-            discardExistingFrames();
-        }
+      var doFrame = function() {
+        // This *must* be requested before nextFrame since nextFrame may decide
+        // to cancel it if there's nothing more to animated:
+        trans._animationRaf = window.requestAnimationFrame(doFrame);
 
-        if(animationOpts.direction === 'reverse') {
-            frameList.reverse();
+        // Check if we're ready for a new frame:
+        if (Date.now() - trans._lastFrameAt > trans._timeToNext) {
+          nextFrame();
         }
+      };
 
-        var currentFrame = gd._fullLayout._currentFrame;
-        if(currentFrame && animationOpts.fromcurrent) {
-            var idx = -1;
-            for(i = 0; i < frameList.length; i++) {
-                frame = frameList[i];
-                if(frame.type === 'byname' && frame.name === currentFrame) {
-                    idx = i;
-                    break;
-                }
-            }
-
-            if(idx > 0 && idx < frameList.length - 1) {
-                var filteredFrameList = [];
-                for(i = 0; i < frameList.length; i++) {
-                    frame = frameList[i];
-                    if(frameList[i].type !== 'byname' || i > idx) {
-                        filteredFrameList.push(frame);
-                    }
-                }
-                frameList = filteredFrameList;
-            }
-        }
+      doFrame();
+    }
 
-        if(frameList.length > 0) {
-            queueFrames(frameList);
+    // This is an animate-local counter that helps match up option input list
+    // items with the particular frame.
+    var configCounter = 0;
+    function setTransitionConfig(frame) {
+      if (Array.isArray(transitionOpts)) {
+        if (configCounter >= transitionOpts.length) {
+          frame.transitionOpts = transitionOpts[configCounter];
         } else {
-            // This is the case where there were simply no frames. It's a little strange
-            // since there's not much to do:
-            gd.emit('plotly_animated');
-            resolve();
-        }
-    });
+          frame.transitionOpts = transitionOpts[0];
+        }
+      } else {
+        frame.transitionOpts = transitionOpts;
+      }
+      configCounter++;
+      return frame;
+    }
+
+    // Disambiguate what's sort of frames have been received
+    var i, frame;
+    var frameList = [];
+    var allFrames = frameOrGroupNameOrFrameList === undefined ||
+      frameOrGroupNameOrFrameList === null;
+    var isFrameArray = Array.isArray(frameOrGroupNameOrFrameList);
+    var isSingleFrame = !allFrames &&
+      !isFrameArray &&
+      Lib.isPlainObject(frameOrGroupNameOrFrameList);
+
+    if (isSingleFrame) {
+      // In this case, a simple object has been passed to animate.
+      frameList.push({
+        type: "object",
+        data: setTransitionConfig(
+          Lib.extendFlat({}, frameOrGroupNameOrFrameList)
+        )
+      });
+    } else if (
+      allFrames ||
+        ["string", "number"].indexOf(typeof frameOrGroupNameOrFrameList) !== -1
+    ) {
+      // In this case, null or undefined has been passed so that we want to
+      // animate *all* currently defined frames
+      for (i = 0; i < trans._frames.length; i++) {
+        frame = trans._frames[i];
+
+        if (!frame) continue;
+
+        if (
+          allFrames ||
+            String(frame.group) === String(frameOrGroupNameOrFrameList)
+        ) {
+          frameList.push({
+            type: "byname",
+            name: String(frame.name),
+            data: setTransitionConfig({ name: frame.name })
+          });
+        }
+      }
+    } else if (isFrameArray) {
+      for (i = 0; i < frameOrGroupNameOrFrameList.length; i++) {
+        var frameOrName = frameOrGroupNameOrFrameList[i];
+        if (["number", "string"].indexOf(typeof frameOrName) !== -1) {
+          frameOrName = String(frameOrName);
+          // In this case, there's an array and this frame is a string name:
+          frameList.push({
+            type: "byname",
+            name: frameOrName,
+            data: setTransitionConfig({ name: frameOrName })
+          });
+        } else if (Lib.isPlainObject(frameOrName)) {
+          frameList.push({
+            type: "object",
+            data: setTransitionConfig(Lib.extendFlat({}, frameOrName))
+          });
+        }
+      }
+    }
+
+    // Verify that all of these frames actually exist; return and reject if not:
+    for (i = 0; i < frameList.length; i++) {
+      frame = frameList[i];
+      if (frame.type === "byname" && !trans._frameHash[frame.data.name]) {
+        Lib.warn('animate failure: frame not found: "' + frame.data.name + '"');
+        reject();
+        return;
+      }
+    }
+
+    // If the mode is either next or immediate, then all currently queued frames must
+    // be dumped and the corresponding .animate promises rejected.
+    if (["next", "immediate"].indexOf(animationOpts.mode) !== -1) {
+      discardExistingFrames();
+    }
+
+    if (animationOpts.direction === "reverse") {
+      frameList.reverse();
+    }
+
+    var currentFrame = gd._fullLayout._currentFrame;
+    if (currentFrame && animationOpts.fromcurrent) {
+      var idx = -1;
+      for (i = 0; i < frameList.length; i++) {
+        frame = frameList[i];
+        if (frame.type === "byname" && frame.name === currentFrame) {
+          idx = i;
+          break;
+        }
+      }
+
+      if (idx > 0 && idx < frameList.length - 1) {
+        var filteredFrameList = [];
+        for (i = 0; i < frameList.length; i++) {
+          frame = frameList[i];
+          if (frameList[i].type !== "byname" || i > idx) {
+            filteredFrameList.push(frame);
+          }
+        }
+        frameList = filteredFrameList;
+      }
+    }
+
+    if (frameList.length > 0) {
+      queueFrames(frameList);
+    } else {
+      // This is the case where there were simply no frames. It's a little strange
+      // since there's not much to do:
+      gd.emit("plotly_animated");
+      resolve();
+    }
+  });
 };
 
 /**
@@ -2537,118 +2692,133 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
  *      will be overwritten.
  */
 Plotly.addFrames = function(gd, frameList, indices) {
-    gd = helpers.getGraphDiv(gd);
-
-    var numericNameWarningCount = 0;
-
-    if(frameList === null || frameList === undefined) {
-        return Promise.resolve();
-    }
-
-    if(!Lib.isPlotDiv(gd)) {
-        throw new Error(
-            'This element is not a Plotly plot: ' + gd + '. It\'s likely that you\'ve failed ' +
-            'to create a plot before adding frames. For more details, see ' +
-            'https://plot.ly/javascript/animations/'
-        );
-    }
+  gd = helpers.getGraphDiv(gd);
 
-    var i, frame, j, idx;
-    var _frames = gd._transitionData._frames;
-    var _hash = gd._transitionData._frameHash;
+  var numericNameWarningCount = 0;
 
+  if (frameList === null || frameList === undefined) {
+    return Promise.resolve();
+  }
+
+  if (!Lib.isPlotDiv(gd)) {
+    throw new Error(
+      "This element is not a Plotly plot: " +
+        gd +
+        ". It's likely that you've failed " +
+        "to create a plot before adding frames. For more details, see " +
+        "https://plot.ly/javascript/animations/"
+    );
+  }
 
-    if(!Array.isArray(frameList)) {
-        throw new Error('addFrames failure: frameList must be an Array of frame definitions' + frameList);
-    }
-
-    // Create a sorted list of insertions since we run into lots of problems if these
-    // aren't in ascending order of index:
-    //
-    // Strictly for sorting. Make sure this is guaranteed to never collide with any
-    // already-exisisting indices:
-    var bigIndex = _frames.length + frameList.length * 2;
-
-    var insertions = [];
-    for(i = frameList.length - 1; i >= 0; i--) {
-        if(!Lib.isPlainObject(frameList[i])) continue;
-
-        var name = (_hash[frameList[i].name] || {}).name;
-        var newName = frameList[i].name;
-
-        if(name && newName && typeof newName === 'number' && _hash[name]) {
-            numericNameWarningCount++;
-
-            Lib.warn('addFrames: overwriting frame "' + _hash[name].name +
-                '" with a frame whose name of type "number" also equates to "' +
-                name + '". This is valid but may potentially lead to unexpected ' +
-                'behavior since all plotly.js frame names are stored internally ' +
-                'as strings.');
-
-            if(numericNameWarningCount > 5) {
-                Lib.warn('addFrames: This API call has yielded too many warnings. ' +
-                    'For the rest of this call, further warnings about numeric frame ' +
-                    'names will be suppressed.');
-            }
-        }
+  var i, frame, j, idx;
+  var _frames = gd._transitionData._frames;
+  var _hash = gd._transitionData._frameHash;
 
-        insertions.push({
-            frame: Plots.supplyFrameDefaults(frameList[i]),
-            index: (indices && indices[i] !== undefined && indices[i] !== null) ? indices[i] : bigIndex + i
-        });
+  if (!Array.isArray(frameList)) {
+    throw new Error(
+      "addFrames failure: frameList must be an Array of frame definitions" +
+        frameList
+    );
+  }
+
+  // Create a sorted list of insertions since we run into lots of problems if these
+  // aren't in ascending order of index:
+  //
+  // Strictly for sorting. Make sure this is guaranteed to never collide with any
+  // already-exisisting indices:
+  var bigIndex = _frames.length + frameList.length * 2;
+
+  var insertions = [];
+  for (i = frameList.length - 1; i >= 0; i--) {
+    if (!Lib.isPlainObject(frameList[i])) continue;
+
+    var name = (_hash[frameList[i].name] || {}).name;
+    var newName = frameList[i].name;
+
+    if (name && newName && typeof newName === "number" && _hash[name]) {
+      numericNameWarningCount++;
+
+      Lib.warn(
+        'addFrames: overwriting frame "' +
+          _hash[name].name +
+          '" with a frame whose name of type "number" also equates to "' +
+          name +
+          '". This is valid but may potentially lead to unexpected ' +
+          "behavior since all plotly.js frame names are stored internally " +
+          "as strings."
+      );
+
+      if (numericNameWarningCount > 5) {
+        Lib.warn(
+          "addFrames: This API call has yielded too many warnings. " +
+            "For the rest of this call, further warnings about numeric frame " +
+            "names will be suppressed."
+        );
+      }
     }
 
-    // Sort this, taking note that undefined insertions end up at the end:
-    insertions.sort(function(a, b) {
-        if(a.index > b.index) return -1;
-        if(a.index < b.index) return 1;
-        return 0;
+    insertions.push({
+      frame: Plots.supplyFrameDefaults(frameList[i]),
+      index: (
+        indices && indices[i] !== undefined && indices[i] !== null
+          ? indices[i]
+          : bigIndex + i
+      )
     });
+  }
+
+  // Sort this, taking note that undefined insertions end up at the end:
+  insertions.sort(function(a, b) {
+    if (a.index > b.index) return -1;
+    if (a.index < b.index) return 1;
+    return 0;
+  });
+
+  var ops = [];
+  var revops = [];
+  var frameCount = _frames.length;
+
+  for (i = insertions.length - 1; i >= 0; i--) {
+    frame = insertions[i].frame;
+
+    if (typeof frame.name === "number") {
+      Lib.warn(
+        "Warning: addFrames accepts frames with numeric names, but the numbers are" +
+          "implicitly cast to strings"
+      );
+    }
+
+    if (!frame.name) {
+      // Repeatedly assign a default name, incrementing the counter each time until
+      // we get a name that's not in the hashed lookup table:
+      while (_hash[frame.name = "frame " + gd._transitionData._counter++]);
+    }
+
+    if (_hash[frame.name]) {
+      // If frame is present, overwrite its definition:
+      for (j = 0; j < _frames.length; j++) {
+        if ((_frames[j] || {}).name === frame.name) break;
+      }
+      ops.push({ type: "replace", index: j, value: frame });
+      revops.unshift({ type: "replace", index: j, value: _frames[j] });
+    } else {
+      // Otherwise insert it at the end of the list:
+      idx = Math.max(0, Math.min(insertions[i].index, frameCount));
 
-    var ops = [];
-    var revops = [];
-    var frameCount = _frames.length;
-
-    for(i = insertions.length - 1; i >= 0; i--) {
-        frame = insertions[i].frame;
-
-        if(typeof frame.name === 'number') {
-            Lib.warn('Warning: addFrames accepts frames with numeric names, but the numbers are' +
-                'implicitly cast to strings');
-
-        }
-
-        if(!frame.name) {
-            // Repeatedly assign a default name, incrementing the counter each time until
-            // we get a name that's not in the hashed lookup table:
-            while(_hash[(frame.name = 'frame ' + gd._transitionData._counter++)]);
-        }
-
-        if(_hash[frame.name]) {
-            // If frame is present, overwrite its definition:
-            for(j = 0; j < _frames.length; j++) {
-                if((_frames[j] || {}).name === frame.name) break;
-            }
-            ops.push({type: 'replace', index: j, value: frame});
-            revops.unshift({type: 'replace', index: j, value: _frames[j]});
-        } else {
-            // Otherwise insert it at the end of the list:
-            idx = Math.max(0, Math.min(insertions[i].index, frameCount));
-
-            ops.push({type: 'insert', index: idx, value: frame});
-            revops.unshift({type: 'delete', index: idx});
-            frameCount++;
-        }
+      ops.push({ type: "insert", index: idx, value: frame });
+      revops.unshift({ type: "delete", index: idx });
+      frameCount++;
     }
+  }
 
-    var undoFunc = Plots.modifyFrames,
-        redoFunc = Plots.modifyFrames,
-        undoArgs = [gd, revops],
-        redoArgs = [gd, ops];
+  var undoFunc = Plots.modifyFrames,
+    redoFunc = Plots.modifyFrames,
+    undoArgs = [gd, revops],
+    redoArgs = [gd, ops];
 
-    if(Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
+  if (Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
 
-    return Plots.modifyFrames(gd, ops);
+  return Plots.modifyFrames(gd, ops);
 };
 
 /**
@@ -2661,34 +2831,34 @@ Plotly.addFrames = function(gd, frameList, indices) {
  *      list of integer indices of frames to be deleted
  */
 Plotly.deleteFrames = function(gd, frameList) {
-    gd = helpers.getGraphDiv(gd);
+  gd = helpers.getGraphDiv(gd);
 
-    if(!Lib.isPlotDiv(gd)) {
-        throw new Error('This element is not a Plotly plot: ' + gd);
-    }
+  if (!Lib.isPlotDiv(gd)) {
+    throw new Error("This element is not a Plotly plot: " + gd);
+  }
 
-    var i, idx;
-    var _frames = gd._transitionData._frames;
-    var ops = [];
-    var revops = [];
+  var i, idx;
+  var _frames = gd._transitionData._frames;
+  var ops = [];
+  var revops = [];
 
-    frameList = frameList.slice(0);
-    frameList.sort();
+  frameList = frameList.slice(0);
+  frameList.sort();
 
-    for(i = frameList.length - 1; i >= 0; i--) {
-        idx = frameList[i];
-        ops.push({type: 'delete', index: idx});
-        revops.unshift({type: 'insert', index: idx, value: _frames[idx]});
-    }
+  for (i = frameList.length - 1; i >= 0; i--) {
+    idx = frameList[i];
+    ops.push({ type: "delete", index: idx });
+    revops.unshift({ type: "insert", index: idx, value: _frames[idx] });
+  }
 
-    var undoFunc = Plots.modifyFrames,
-        redoFunc = Plots.modifyFrames,
-        undoArgs = [gd, revops],
-        redoArgs = [gd, ops];
+  var undoFunc = Plots.modifyFrames,
+    redoFunc = Plots.modifyFrames,
+    undoArgs = [gd, revops],
+    redoArgs = [gd, ops];
 
-    if(Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
+  if (Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
 
-    return Plots.modifyFrames(gd, ops);
+  return Plots.modifyFrames(gd, ops);
 };
 
 /**
@@ -2698,131 +2868,157 @@ Plotly.deleteFrames = function(gd, frameList) {
  *      the id or DOM element of the graph container div
  */
 Plotly.purge = function purge(gd) {
-    gd = helpers.getGraphDiv(gd);
+  gd = helpers.getGraphDiv(gd);
 
-    var fullLayout = gd._fullLayout || {},
-        fullData = gd._fullData || [];
+  var fullLayout = gd._fullLayout || {}, fullData = gd._fullData || [];
 
-    // remove gl contexts
-    Plots.cleanPlot([], {}, fullData, fullLayout);
+  // remove gl contexts
+  Plots.cleanPlot([], {}, fullData, fullLayout);
 
-    // purge properties
-    Plots.purge(gd);
+  // purge properties
+  Plots.purge(gd);
 
-    // purge event emitter methods
-    Events.purge(gd);
+  // purge event emitter methods
+  Events.purge(gd);
 
-    // remove plot container
-    if(fullLayout._container) fullLayout._container.remove();
+  // remove plot container
+  if (fullLayout._container) fullLayout._container.remove();
 
-    delete gd._context;
-    delete gd._replotPending;
-    delete gd._mouseDownTime;
-    delete gd._hmpixcount;
-    delete gd._hmlumcount;
+  delete gd._context;
+  delete gd._replotPending;
+  delete gd._mouseDownTime;
+  delete gd._hmpixcount;
+  delete gd._hmlumcount;
 
-    return gd;
+  return gd;
 };
 
 // -------------------------------------------------------
 // makePlotFramework: Create the plot container and axes
 // -------------------------------------------------------
 function makePlotFramework(gd) {
-    var gd3 = d3.select(gd),
-        fullLayout = gd._fullLayout;
-
-    // Plot container
-    fullLayout._container = gd3.selectAll('.plot-container').data([0]);
-    fullLayout._container.enter().insert('div', ':first-child')
-        .classed('plot-container', true)
-        .classed('plotly', true);
-
-    // Make the svg container
-    fullLayout._paperdiv = fullLayout._container.selectAll('.svg-container').data([0]);
-    fullLayout._paperdiv.enter().append('div')
-        .classed('svg-container', true)
-        .style('position', 'relative');
-
-    // Make the graph containers
-    // start fresh each time we get here, so we know the order comes out
-    // right, rather than enter/exit which can muck up the order
-    // TODO: sort out all the ordering so we don't have to
-    // explicitly delete anything
-    fullLayout._glcontainer = fullLayout._paperdiv.selectAll('.gl-container')
-        .data([0]);
-    fullLayout._glcontainer.enter().append('div')
-        .classed('gl-container', true);
-
-    fullLayout._geocontainer = fullLayout._paperdiv.selectAll('.geo-container')
-        .data([0]);
-    fullLayout._geocontainer.enter().append('div')
-        .classed('geo-container', true);
-
-    fullLayout._paperdiv.selectAll('.main-svg').remove();
-
-    fullLayout._paper = fullLayout._paperdiv.insert('svg', ':first-child')
-        .classed('main-svg', true);
-
-    fullLayout._toppaper = fullLayout._paperdiv.append('svg')
-        .classed('main-svg', true);
-
-    if(!fullLayout._uid) {
-        var otherUids = [];
-        d3.selectAll('defs').each(function() {
-            if(this.id) otherUids.push(this.id.split('-')[1]);
-        });
-        fullLayout._uid = Lib.randstr(otherUids);
-    }
-
-    fullLayout._paperdiv.selectAll('.main-svg')
-        .attr(xmlnsNamespaces.svgAttrs);
-
-    fullLayout._defs = fullLayout._paper.append('defs')
-        .attr('id', 'defs-' + fullLayout._uid);
-
-    fullLayout._topdefs = fullLayout._toppaper.append('defs')
-        .attr('id', 'topdefs-' + fullLayout._uid);
-
-    fullLayout._draggers = fullLayout._paper.append('g')
-        .classed('draglayer', true);
-
-    // lower shape layer
-    // (only for shapes to be drawn below the whole plot)
-    var layerBelow = fullLayout._paper.append('g')
-        .classed('layer-below', true);
-    fullLayout._imageLowerLayer = layerBelow.append('g')
-        .classed('imagelayer', true);
-    fullLayout._shapeLowerLayer = layerBelow.append('g')
-        .classed('shapelayer', true);
-
-    // single cartesian layer for the whole plot
-    fullLayout._cartesianlayer = fullLayout._paper.append('g').classed('cartesianlayer', true);
-
-    // single ternary layer for the whole plot
-    fullLayout._ternarylayer = fullLayout._paper.append('g').classed('ternarylayer', true);
-
-    // upper shape layer
-    // (only for shapes to be drawn above the whole plot, including subplots)
-    var layerAbove = fullLayout._paper.append('g')
-        .classed('layer-above', true);
-    fullLayout._imageUpperLayer = layerAbove.append('g')
-        .classed('imagelayer', true);
-    fullLayout._shapeUpperLayer = layerAbove.append('g')
-        .classed('shapelayer', true);
-
-    // single pie layer for the whole plot
-    fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true);
-
-    // fill in image server scrape-svg
-    fullLayout._glimages = fullLayout._paper.append('g').classed('glimages', true);
-    fullLayout._geoimages = fullLayout._paper.append('g').classed('geoimages', true);
-
-    // lastly info (legend, annotations) and hover layers go on top
-    // these are in a different svg element normally, but get collapsed into a single
-    // svg when exporting (after inserting 3D)
-    fullLayout._infolayer = fullLayout._toppaper.append('g').classed('infolayer', true);
-    fullLayout._zoomlayer = fullLayout._toppaper.append('g').classed('zoomlayer', true);
-    fullLayout._hoverlayer = fullLayout._toppaper.append('g').classed('hoverlayer', true);
-
-    gd.emit('plotly_framework');
+  var gd3 = d3.select(gd), fullLayout = gd._fullLayout;
+
+  // Plot container
+  fullLayout._container = gd3.selectAll(".plot-container").data([0]);
+  fullLayout._container
+    .enter()
+    .insert("div", ":first-child")
+    .classed("plot-container", true)
+    .classed("plotly", true);
+
+  // Make the svg container
+  fullLayout._paperdiv = fullLayout._container
+    .selectAll(".svg-container")
+    .data([0]);
+  fullLayout._paperdiv
+    .enter()
+    .append("div")
+    .classed("svg-container", true)
+    .style("position", "relative");
+
+  // Make the graph containers
+  // start fresh each time we get here, so we know the order comes out
+  // right, rather than enter/exit which can muck up the order
+  // TODO: sort out all the ordering so we don't have to
+  // explicitly delete anything
+  fullLayout._glcontainer = fullLayout._paperdiv
+    .selectAll(".gl-container")
+    .data([0]);
+  fullLayout._glcontainer.enter().append("div").classed("gl-container", true);
+
+  fullLayout._geocontainer = fullLayout._paperdiv
+    .selectAll(".geo-container")
+    .data([0]);
+  fullLayout._geocontainer.enter().append("div").classed("geo-container", true);
+
+  fullLayout._paperdiv.selectAll(".main-svg").remove();
+
+  fullLayout._paper = fullLayout._paperdiv
+    .insert("svg", ":first-child")
+    .classed("main-svg", true);
+
+  fullLayout._toppaper = fullLayout._paperdiv
+    .append("svg")
+    .classed("main-svg", true);
+
+  if (!fullLayout._uid) {
+    var otherUids = [];
+    d3.selectAll("defs").each(function() {
+      if (this.id) otherUids.push(this.id.split("-")[1]);
+    });
+    fullLayout._uid = Lib.randstr(otherUids);
+  }
+
+  fullLayout._paperdiv.selectAll(".main-svg").attr(xmlnsNamespaces.svgAttrs);
+
+  fullLayout._defs = fullLayout._paper
+    .append("defs")
+    .attr("id", "defs-" + fullLayout._uid);
+
+  fullLayout._topdefs = fullLayout._toppaper
+    .append("defs")
+    .attr("id", "topdefs-" + fullLayout._uid);
+
+  fullLayout._draggers = fullLayout._paper
+    .append("g")
+    .classed("draglayer", true);
+
+  // lower shape layer
+  // (only for shapes to be drawn below the whole plot)
+  var layerBelow = fullLayout._paper.append("g").classed("layer-below", true);
+  fullLayout._imageLowerLayer = layerBelow
+    .append("g")
+    .classed("imagelayer", true);
+  fullLayout._shapeLowerLayer = layerBelow
+    .append("g")
+    .classed("shapelayer", true);
+
+  // single cartesian layer for the whole plot
+  fullLayout._cartesianlayer = fullLayout._paper
+    .append("g")
+    .classed("cartesianlayer", true);
+
+  // single ternary layer for the whole plot
+  fullLayout._ternarylayer = fullLayout._paper
+    .append("g")
+    .classed("ternarylayer", true);
+
+  // upper shape layer
+  // (only for shapes to be drawn above the whole plot, including subplots)
+  var layerAbove = fullLayout._paper.append("g").classed("layer-above", true);
+  fullLayout._imageUpperLayer = layerAbove
+    .append("g")
+    .classed("imagelayer", true);
+  fullLayout._shapeUpperLayer = layerAbove
+    .append("g")
+    .classed("shapelayer", true);
+
+  // single pie layer for the whole plot
+  fullLayout._pielayer = fullLayout._paper
+    .append("g")
+    .classed("pielayer", true);
+
+  // fill in image server scrape-svg
+  fullLayout._glimages = fullLayout._paper
+    .append("g")
+    .classed("glimages", true);
+  fullLayout._geoimages = fullLayout._paper
+    .append("g")
+    .classed("geoimages", true);
+
+  // lastly info (legend, annotations) and hover layers go on top
+  // these are in a different svg element normally, but get collapsed into a single
+  // svg when exporting (after inserting 3D)
+  fullLayout._infolayer = fullLayout._toppaper
+    .append("g")
+    .classed("infolayer", true);
+  fullLayout._zoomlayer = fullLayout._toppaper
+    .append("g")
+    .classed("zoomlayer", true);
+  fullLayout._hoverlayer = fullLayout._toppaper
+    .append("g")
+    .classed("hoverlayer", true);
+
+  gd.emit("plotly_framework");
 }
diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js
index d54dd1a05a6..f382090aa12 100644
--- a/src/plot_api/plot_config.js
+++ b/src/plot_api/plot_config.js
@@ -5,9 +5,7 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 /* eslint-disable no-console */
 
 /**
@@ -19,100 +17,75 @@
  */
 
 module.exports = {
-
-    // no interactivity, for export or image generation
-    staticPlot: false,
-
-    // we can edit titles, move annotations, etc
-    editable: false,
-
-    // DO autosize once regardless of layout.autosize
-    // (use default width or height values otherwise)
-    autosizable: false,
-
-    // set the length of the undo/redo queue
-    queueLength: 0,
-
-    // if we DO autosize, do we fill the container or the screen?
-    fillFrame: false,
-
-    // if we DO autosize, set the frame margins in percents of plot size
-    frameMargins: 0,
-
-    // mousewheel or two-finger scroll zooms the plot
-    scrollZoom: false,
-
-    // double click interaction (false, 'reset', 'autosize' or 'reset+autosize')
-    doubleClick: 'reset+autosize',
-
-    // new users see some hints about interactivity
-    showTips: true,
-
-    // link to open this plot in plotly
-    showLink: false,
-
-    // if we show a link, does it contain data or just link to a plotly file?
-    sendData: true,
-
-    // text appearing in the sendData link
-    linkText: 'Edit chart',
-
-    // false or function adding source(s) to linkText 
-    showSources: false,
-
-    // display the mode bar (true, false, or 'hover')
-    displayModeBar: 'hover',
-
-    // remove mode bar button by name
-    // (see ./components/modebar/buttons.js for the list of names)
-    modeBarButtonsToRemove: [],
-
-    // add mode bar button using config objects
-    // (see ./components/modebar/buttons.js for list of arguments)
-    modeBarButtonsToAdd: [],
-
-    // fully custom mode bar buttons as nested array,
-    // where the outer arrays represents button groups, and
-    // the inner arrays have buttons config objects or names of default buttons
-    // (see ./components/modebar/buttons.js for more info)
-    modeBarButtons: false,
-
-    // add the plotly logo on the end of the mode bar
-    displaylogo: true,
-
-    // increase the pixel ratio for Gl plot images
-    plotGlPixelRatio: 2,
-
-    // function to add the background color to a different container
-    // or 'opaque' to ensure there's white behind it
-    setBackground: defaultSetBackground,
-
-    // URL to topojson files used in geo charts
-    topojsonURL: 'https://cdn.plot.ly/',
-
-    // Mapbox access token (required to plot mapbox trace types)
-    // If using an Mapbox Atlas server, set this option to '',
-    // so that plotly.js won't attempt to authenticate to the public Mapbox server.
-    mapboxAccessToken: null,
-
-    // Turn all console logging on or off (errors will be thrown)
-    // This should ONLY be set via Plotly.setPlotConfig
-    logging: false,
-
-    // Set global transform to be applied to all traces with no
-    // specification needed
-    globalTransforms: []
+  // no interactivity, for export or image generation
+  staticPlot: false,
+  // we can edit titles, move annotations, etc
+  editable: false,
+  // DO autosize once regardless of layout.autosize
+  // (use default width or height values otherwise)
+  autosizable: false,
+  // set the length of the undo/redo queue
+  queueLength: 0,
+  // if we DO autosize, do we fill the container or the screen?
+  fillFrame: false,
+  // if we DO autosize, set the frame margins in percents of plot size
+  frameMargins: 0,
+  // mousewheel or two-finger scroll zooms the plot
+  scrollZoom: false,
+  // double click interaction (false, 'reset', 'autosize' or 'reset+autosize')
+  doubleClick: "reset+autosize",
+  // new users see some hints about interactivity
+  showTips: true,
+  // link to open this plot in plotly
+  showLink: false,
+  // if we show a link, does it contain data or just link to a plotly file?
+  sendData: true,
+  // text appearing in the sendData link
+  linkText: "Edit chart",
+  // false or function adding source(s) to linkText 
+  showSources: false,
+  // display the mode bar (true, false, or 'hover')
+  displayModeBar: "hover",
+  // remove mode bar button by name
+  // (see ./components/modebar/buttons.js for the list of names)
+  modeBarButtonsToRemove: [],
+  // add mode bar button using config objects
+  // (see ./components/modebar/buttons.js for list of arguments)
+  modeBarButtonsToAdd: [],
+  // fully custom mode bar buttons as nested array,
+  // where the outer arrays represents button groups, and
+  // the inner arrays have buttons config objects or names of default buttons
+  // (see ./components/modebar/buttons.js for more info)
+  modeBarButtons: false,
+  // add the plotly logo on the end of the mode bar
+  displaylogo: true,
+  // increase the pixel ratio for Gl plot images
+  plotGlPixelRatio: 2,
+  // function to add the background color to a different container
+  // or 'opaque' to ensure there's white behind it
+  setBackground: defaultSetBackground,
+  // URL to topojson files used in geo charts
+  topojsonURL: "https://cdn.plot.ly/",
+  // Mapbox access token (required to plot mapbox trace types)
+  // If using an Mapbox Atlas server, set this option to '',
+  // so that plotly.js won't attempt to authenticate to the public Mapbox server.
+  mapboxAccessToken: null,
+  // Turn all console logging on or off (errors will be thrown)
+  // This should ONLY be set via Plotly.setPlotConfig
+  logging: false,
+  // Set global transform to be applied to all traces with no
+  // specification needed
+  globalTransforms: []
 };
 
 // where and how the background gets set can be overridden by context
 // so we define the default (plotly.js) behavior here
 function defaultSetBackground(gd, bgColor) {
-    try {
-        gd._fullLayout._paper.style('background', bgColor);
-    }
-    catch(e) {
-        if(module.exports.logging > 0) {
-            console.error(e);
-        }
+  try {
+    gd._fullLayout._paper.style("background", bgColor);
+  } catch (e) {
+    if (module.exports.logging > 0) {
+      console.error(e);
     }
+  }
 }
diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js
index 2075674108a..2cd76726ed6 100644
--- a/src/plot_api/plot_schema.js
+++ b/src/plot_api/plot_schema.js
@@ -5,28 +5,25 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Registry = require("../registry");
+var Lib = require("../lib");
 
-
-'use strict';
-
-var Registry = require('../registry');
-var Lib = require('../lib');
-
-var baseAttributes = require('../plots/attributes');
-var baseLayoutAttributes = require('../plots/layout_attributes');
-var frameAttributes = require('../plots/frame_attributes');
-var animationAttributes = require('../plots/animation_attributes');
+var baseAttributes = require("../plots/attributes");
+var baseLayoutAttributes = require("../plots/layout_attributes");
+var frameAttributes = require("../plots/frame_attributes");
+var animationAttributes = require("../plots/animation_attributes");
 
 // polar attributes are not part of the Registry yet
-var polarAreaAttrs = require('../plots/polar/area_attributes');
-var polarAxisAttrs = require('../plots/polar/axis_attributes');
+var polarAreaAttrs = require("../plots/polar/area_attributes");
+var polarAxisAttrs = require("../plots/polar/axis_attributes");
 
 var extendFlat = Lib.extendFlat;
 var extendDeep = Lib.extendDeep;
 
-var IS_SUBPLOT_OBJ = '_isSubplotObj';
-var IS_LINKED_TO_ARRAY = '_isLinkedToArray';
-var DEPRECATED = '_deprecated';
+var IS_SUBPLOT_OBJ = "_isSubplotObj";
+var IS_LINKED_TO_ARRAY = "_isLinkedToArray";
+var DEPRECATED = "_deprecated";
 var UNDERSCORE_ATTRS = [IS_SUBPLOT_OBJ, IS_LINKED_TO_ARRAY, DEPRECATED];
 
 exports.IS_SUBPLOT_OBJ = IS_SUBPLOT_OBJ;
@@ -46,32 +43,29 @@ exports.UNDERSCORE_ATTRS = UNDERSCORE_ATTRS;
  *  - config (coming soon ...)
  */
 exports.get = function() {
-    var traces = {};
-
-    Registry.allTypes.concat('area').forEach(function(type) {
-        traces[type] = getTraceAttributes(type);
-    });
-
-    var transforms = {};
-
-    Object.keys(Registry.transformsRegistry).forEach(function(type) {
-        transforms[type] = getTransformAttributes(type);
-    });
-
-    return {
-        defs: {
-            valObjects: Lib.valObjects,
-            metaKeys: UNDERSCORE_ATTRS.concat(['description', 'role'])
-        },
-
-        traces: traces,
-        layout: getLayoutAttributes(),
-
-        transforms: transforms,
-
-        frames: getFramesAttributes(),
-        animation: formatAttributes(animationAttributes)
-    };
+  var traces = {};
+
+  Registry.allTypes.concat("area").forEach(function(type) {
+    traces[type] = getTraceAttributes(type);
+  });
+
+  var transforms = {};
+
+  Object.keys(Registry.transformsRegistry).forEach(function(type) {
+    transforms[type] = getTransformAttributes(type);
+  });
+
+  return {
+    defs: {
+      valObjects: Lib.valObjects,
+      metaKeys: UNDERSCORE_ATTRS.concat(["description", "role"])
+    },
+    traces: traces,
+    layout: getLayoutAttributes(),
+    transforms: transforms,
+    frames: getFramesAttributes(),
+    animation: formatAttributes(animationAttributes)
+  };
 };
 
 /**
@@ -98,19 +92,19 @@ exports.get = function() {
  *  copy of transformIn that contains attribute defaults
  */
 exports.crawl = function(attrs, callback, specifiedLevel) {
-    var level = specifiedLevel || 0;
+  var level = specifiedLevel || 0;
 
-    Object.keys(attrs).forEach(function(attrName) {
-        var attr = attrs[attrName];
+  Object.keys(attrs).forEach(function(attrName) {
+    var attr = attrs[attrName];
 
-        if(UNDERSCORE_ATTRS.indexOf(attrName) !== -1) return;
+    if (UNDERSCORE_ATTRS.indexOf(attrName) !== -1) return;
 
-        callback(attr, attrName, attrs, level);
+    callback(attr, attrName, attrs, level);
 
-        if(exports.isValObject(attr)) return;
+    if (exports.isValObject(attr)) return;
 
-        if(Lib.isPlainObject(attr)) exports.crawl(attr, callback, level + 1);
-    });
+    if (Lib.isPlainObject(attr)) exports.crawl(attr, callback, level + 1);
+  });
 };
 
 /** Is object a value object (or a container object)?
@@ -121,7 +115,7 @@ exports.crawl = function(attrs, callback, specifiedLevel) {
  *  false for tree nodes in the attribute hierarchy
  */
 exports.isValObject = function(obj) {
-    return obj && obj.valType !== undefined;
+  return obj && obj.valType !== undefined;
 };
 
 /**
@@ -135,272 +129,267 @@ exports.isValObject = function(obj) {
  *  list of array attributes for the given trace
  */
 exports.findArrayAttributes = function(trace) {
-    var arrayAttributes = [],
-        stack = [];
+  var arrayAttributes = [], stack = [];
 
-    function callback(attr, attrName, attrs, level) {
-        stack = stack.slice(0, level).concat([attrName]);
+  function callback(attr, attrName, attrs, level) {
+    stack = stack.slice(0, level).concat([attrName]);
 
-        var splittableAttr = attr && (attr.valType === 'data_array' || attr.arrayOk === true);
-        if(!splittableAttr) return;
+    var splittableAttr = attr &&
+      (attr.valType === "data_array" || attr.arrayOk === true);
+    if (!splittableAttr) return;
 
-        var astr = toAttrString(stack);
-        var val = Lib.nestedProperty(trace, astr).get();
-        if(!Array.isArray(val)) return;
+    var astr = toAttrString(stack);
+    var val = Lib.nestedProperty(trace, astr).get();
+    if (!Array.isArray(val)) return;
 
-        arrayAttributes.push(astr);
-    }
+    arrayAttributes.push(astr);
+  }
 
-    function toAttrString(stack) {
-        return stack.join('.');
-    }
+  function toAttrString(stack) {
+    return stack.join(".");
+  }
 
-    exports.crawl(trace._module.attributes, callback);
+  exports.crawl(trace._module.attributes, callback);
 
-    if(trace.transforms) {
-        var transforms = trace.transforms;
+  if (trace.transforms) {
+    var transforms = trace.transforms;
 
-        for(var i = 0; i < transforms.length; i++) {
-            var transform = transforms[i];
+    for (var i = 0; i < transforms.length; i++) {
+      var transform = transforms[i];
 
-            stack = ['transforms[' + i + ']'];
+      stack = ["transforms[" + i + "]"];
 
-            exports.crawl(transform._module.attributes, callback, 1);
-        }
+      exports.crawl(transform._module.attributes, callback, 1);
     }
+  }
 
-    // Look into the fullInput module attributes for array attributes
-    // to make sure that 'custom' array attributes are detected.
-    //
-    // At the moment, we need this block to make sure that
-    // ohlc and candlestick 'open', 'high', 'low', 'close' can be
-    // used with filter ang groupby transforms.
-    if(trace._fullInput) {
-        exports.crawl(trace._fullInput._module.attributes, callback);
+  // Look into the fullInput module attributes for array attributes
+  // to make sure that 'custom' array attributes are detected.
+  //
+  // At the moment, we need this block to make sure that
+  // ohlc and candlestick 'open', 'high', 'low', 'close' can be
+  // used with filter ang groupby transforms.
+  if (trace._fullInput) {
+    exports.crawl(trace._fullInput._module.attributes, callback);
 
-        arrayAttributes = Lib.filterUnique(arrayAttributes);
-    }
+    arrayAttributes = Lib.filterUnique(arrayAttributes);
+  }
 
-    return arrayAttributes;
+  return arrayAttributes;
 };
 
 function getTraceAttributes(type) {
-    var _module, basePlotModule;
-
-    if(type === 'area') {
-        _module = { attributes: polarAreaAttrs };
-        basePlotModule = {};
-    }
-    else {
-        _module = Registry.modules[type]._module,
-        basePlotModule = _module.basePlotModule;
-    }
-
-    var attributes = {};
-
-    // make 'type' the first attribute in the object
-    attributes.type = null;
-
-    // base attributes (same for all trace types)
-    extendDeep(attributes, baseAttributes);
-
-    // module attributes
-    extendDeep(attributes, _module.attributes);
-
-    // subplot attributes
-    if(basePlotModule.attributes) {
-        extendDeep(attributes, basePlotModule.attributes);
+  var _module, basePlotModule;
+
+  if (type === "area") {
+    _module = { attributes: polarAreaAttrs };
+    basePlotModule = {};
+  } else {
+    _module = Registry.modules[
+      type
+    ]._module, basePlotModule = _module.basePlotModule;
+  }
+
+  var attributes = {};
+
+  // make 'type' the first attribute in the object
+  attributes.type = null;
+
+  // base attributes (same for all trace types)
+  extendDeep(attributes, baseAttributes);
+
+  // module attributes
+  extendDeep(attributes, _module.attributes);
+
+  // subplot attributes
+  if (basePlotModule.attributes) {
+    extendDeep(attributes, basePlotModule.attributes);
+  }
+
+  // add registered components trace attributes
+  Object.keys(Registry.componentsRegistry).forEach(function(k) {
+    var _module = Registry.componentsRegistry[k];
+
+    if (
+      _module.schema && _module.schema.traces && _module.schema.traces[type]
+    ) {
+      Object.keys(_module.schema.traces[type]).forEach(function(v) {
+        insertAttrs(attributes, _module.schema.traces[type][v], v);
+      });
     }
+  });
 
-    // add registered components trace attributes
-    Object.keys(Registry.componentsRegistry).forEach(function(k) {
-        var _module = Registry.componentsRegistry[k];
+  // 'type' gets overwritten by baseAttributes; reset it here
+  attributes.type = type;
 
-        if(_module.schema && _module.schema.traces && _module.schema.traces[type]) {
-            Object.keys(_module.schema.traces[type]).forEach(function(v) {
-                insertAttrs(attributes, _module.schema.traces[type][v], v);
-            });
-        }
-    });
+  var out = {
+    meta: _module.meta || {},
+    attributes: formatAttributes(attributes)
+  };
 
-    // 'type' gets overwritten by baseAttributes; reset it here
-    attributes.type = type;
-
-    var out = {
-        meta: _module.meta || {},
-        attributes: formatAttributes(attributes),
-    };
-
-    // trace-specific layout attributes
-    if(_module.layoutAttributes) {
-        var layoutAttributes = {};
+  // trace-specific layout attributes
+  if (_module.layoutAttributes) {
+    var layoutAttributes = {};
 
-        extendDeep(layoutAttributes, _module.layoutAttributes);
-        out.layoutAttributes = formatAttributes(layoutAttributes);
-    }
+    extendDeep(layoutAttributes, _module.layoutAttributes);
+    out.layoutAttributes = formatAttributes(layoutAttributes);
+  }
 
-    return out;
+  return out;
 }
 
 function getLayoutAttributes() {
-    var layoutAttributes = {};
+  var layoutAttributes = {};
 
-    // global layout attributes
-    extendDeep(layoutAttributes, baseLayoutAttributes);
+  // global layout attributes
+  extendDeep(layoutAttributes, baseLayoutAttributes);
 
-    // add base plot module layout attributes
-    Object.keys(Registry.subplotsRegistry).forEach(function(k) {
-        var _module = Registry.subplotsRegistry[k];
+  // add base plot module layout attributes
+  Object.keys(Registry.subplotsRegistry).forEach(function(k) {
+    var _module = Registry.subplotsRegistry[k];
 
-        if(!_module.layoutAttributes) return;
+    if (!_module.layoutAttributes) return;
 
-        if(_module.name === 'cartesian') {
-            handleBasePlotModule(layoutAttributes, _module, 'xaxis');
-            handleBasePlotModule(layoutAttributes, _module, 'yaxis');
-        }
-        else {
-            var astr = _module.attr === 'subplot' ? _module.name : _module.attr;
+    if (_module.name === "cartesian") {
+      handleBasePlotModule(layoutAttributes, _module, "xaxis");
+      handleBasePlotModule(layoutAttributes, _module, "yaxis");
+    } else {
+      var astr = _module.attr === "subplot" ? _module.name : _module.attr;
 
-            handleBasePlotModule(layoutAttributes, _module, astr);
-        }
-    });
+      handleBasePlotModule(layoutAttributes, _module, astr);
+    }
+  });
 
-    // polar layout attributes
-    layoutAttributes = assignPolarLayoutAttrs(layoutAttributes);
+  // polar layout attributes
+  layoutAttributes = assignPolarLayoutAttrs(layoutAttributes);
 
-    // add registered components layout attributes
-    Object.keys(Registry.componentsRegistry).forEach(function(k) {
-        var _module = Registry.componentsRegistry[k];
+  // add registered components layout attributes
+  Object.keys(Registry.componentsRegistry).forEach(function(k) {
+    var _module = Registry.componentsRegistry[k];
 
-        if(!_module.layoutAttributes) return;
+    if (!_module.layoutAttributes) return;
 
-        if(_module.schema && _module.schema.layout) {
-            Object.keys(_module.schema.layout).forEach(function(v) {
-                insertAttrs(layoutAttributes, _module.schema.layout[v], v);
-            });
-        }
-        else {
-            insertAttrs(layoutAttributes, _module.layoutAttributes, _module.name);
-        }
-    });
+    if (_module.schema && _module.schema.layout) {
+      Object.keys(_module.schema.layout).forEach(function(v) {
+        insertAttrs(layoutAttributes, _module.schema.layout[v], v);
+      });
+    } else {
+      insertAttrs(layoutAttributes, _module.layoutAttributes, _module.name);
+    }
+  });
 
-    return {
-        layoutAttributes: formatAttributes(layoutAttributes)
-    };
+  return { layoutAttributes: formatAttributes(layoutAttributes) };
 }
 
 function getTransformAttributes(type) {
-    var _module = Registry.transformsRegistry[type];
-    var attributes = extendDeep({}, _module.attributes);
-
-    // add registered components transform attributes
-    Object.keys(Registry.componentsRegistry).forEach(function(k) {
-        var _module = Registry.componentsRegistry[k];
-
-        if(_module.schema && _module.schema.transforms && _module.schema.transforms[type]) {
-            Object.keys(_module.schema.transforms[type]).forEach(function(v) {
-                insertAttrs(attributes, _module.schema.transforms[type][v], v);
-            });
-        }
-    });
+  var _module = Registry.transformsRegistry[type];
+  var attributes = extendDeep({}, _module.attributes);
+
+  // add registered components transform attributes
+  Object.keys(Registry.componentsRegistry).forEach(function(k) {
+    var _module = Registry.componentsRegistry[k];
+
+    if (
+      _module.schema &&
+        _module.schema.transforms &&
+        _module.schema.transforms[type]
+    ) {
+      Object.keys(_module.schema.transforms[type]).forEach(function(v) {
+        insertAttrs(attributes, _module.schema.transforms[type][v], v);
+      });
+    }
+  });
 
-    return {
-        attributes: formatAttributes(attributes)
-    };
+  return { attributes: formatAttributes(attributes) };
 }
 
 function getFramesAttributes() {
-    var attrs = {
-        frames: Lib.extendDeep({}, frameAttributes)
-    };
+  var attrs = { frames: Lib.extendDeep({}, frameAttributes) };
 
-    formatAttributes(attrs);
+  formatAttributes(attrs);
 
-    return attrs.frames;
+  return attrs.frames;
 }
 
 function formatAttributes(attrs) {
-    mergeValTypeAndRole(attrs);
-    formatArrayContainers(attrs);
+  mergeValTypeAndRole(attrs);
+  formatArrayContainers(attrs);
 
-    return attrs;
+  return attrs;
 }
 
 function mergeValTypeAndRole(attrs) {
-
-    function makeSrcAttr(attrName) {
-        return {
-            valType: 'string',
-            role: 'info',
-            description: [
-                'Sets the source reference on plot.ly for ',
-                attrName, '.'
-            ].join(' ')
-        };
-    }
-
-    function callback(attr, attrName, attrs) {
-        if(exports.isValObject(attr)) {
-            if(attr.valType === 'data_array') {
-                // all 'data_array' attrs have role 'data'
-                attr.role = 'data';
-                // all 'data_array' attrs have a corresponding 'src' attr
-                attrs[attrName + 'src'] = makeSrcAttr(attrName);
-            }
-            else if(attr.arrayOk === true) {
-                // all 'arrayOk' attrs have a corresponding 'src' attr
-                attrs[attrName + 'src'] = makeSrcAttr(attrName);
-            }
-        }
-        else if(Lib.isPlainObject(attr)) {
-            // all attrs container objects get role 'object'
-            attr.role = 'object';
-        }
+  function makeSrcAttr(attrName) {
+    return {
+      valType: "string",
+      role: "info",
+      description: [
+        "Sets the source reference on plot.ly for ",
+        attrName,
+        "."
+      ].join(" ")
+    };
+  }
+
+  function callback(attr, attrName, attrs) {
+    if (exports.isValObject(attr)) {
+      if (attr.valType === "data_array") {
+        // all 'data_array' attrs have role 'data'
+        attr.role = "data";
+        // all 'data_array' attrs have a corresponding 'src' attr
+        attrs[attrName + "src"] = makeSrcAttr(attrName);
+      } else if (attr.arrayOk === true) {
+        // all 'arrayOk' attrs have a corresponding 'src' attr
+        attrs[attrName + "src"] = makeSrcAttr(attrName);
+      }
+    } else if (Lib.isPlainObject(attr)) {
+      // all attrs container objects get role 'object'
+      attr.role = "object";
     }
+  }
 
-    exports.crawl(attrs, callback);
+  exports.crawl(attrs, callback);
 }
 
 function formatArrayContainers(attrs) {
+  function callback(attr, attrName, attrs) {
+    if (!attr) return;
 
-    function callback(attr, attrName, attrs) {
-        if(!attr) return;
+    var itemName = attr[IS_LINKED_TO_ARRAY];
 
-        var itemName = attr[IS_LINKED_TO_ARRAY];
+    if (!itemName) return;
 
-        if(!itemName) return;
+    delete attr[IS_LINKED_TO_ARRAY];
 
-        delete attr[IS_LINKED_TO_ARRAY];
-
-        attrs[attrName] = { items: {} };
-        attrs[attrName].items[itemName] = attr;
-        attrs[attrName].role = 'object';
-    }
+    attrs[attrName] = { items: {} };
+    attrs[attrName].items[itemName] = attr;
+    attrs[attrName].role = "object";
+  }
 
-    exports.crawl(attrs, callback);
+  exports.crawl(attrs, callback);
 }
 
 function assignPolarLayoutAttrs(layoutAttributes) {
-    extendFlat(layoutAttributes, {
-        radialaxis: polarAxisAttrs.radialaxis,
-        angularaxis: polarAxisAttrs.angularaxis
-    });
+  extendFlat(layoutAttributes, {
+    radialaxis: polarAxisAttrs.radialaxis,
+    angularaxis: polarAxisAttrs.angularaxis
+  });
 
-    extendFlat(layoutAttributes, polarAxisAttrs.layout);
+  extendFlat(layoutAttributes, polarAxisAttrs.layout);
 
-    return layoutAttributes;
+  return layoutAttributes;
 }
 
 function handleBasePlotModule(layoutAttributes, _module, astr) {
-    var np = Lib.nestedProperty(layoutAttributes, astr),
-        attrs = extendDeep({}, _module.layoutAttributes);
+  var np = Lib.nestedProperty(layoutAttributes, astr),
+    attrs = extendDeep({}, _module.layoutAttributes);
 
-    attrs[IS_SUBPLOT_OBJ] = true;
-    np.set(attrs);
+  attrs[IS_SUBPLOT_OBJ] = true;
+  np.set(attrs);
 }
 
 function insertAttrs(baseAttrs, newAttrs, astr) {
-    var np = Lib.nestedProperty(baseAttrs, astr);
+  var np = Lib.nestedProperty(baseAttrs, astr);
 
-    np.set(extendDeep(np.get() || {}, newAttrs));
+  np.set(extendDeep(np.get() || {}, newAttrs));
 }
diff --git a/src/plot_api/register.js b/src/plot_api/register.js
index 87dae2e49b2..7f578029b5a 100644
--- a/src/plot_api/register.js
+++ b/src/plot_api/register.js
@@ -5,93 +5,97 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var Registry = require('../registry');
-var Lib = require('../lib');
-
+"use strict";
+var Registry = require("../registry");
+var Lib = require("../lib");
 
 module.exports = function register(_modules) {
-    if(!_modules) {
-        throw new Error('No argument passed to Plotly.register.');
-    }
-    else if(_modules && !Array.isArray(_modules)) {
-        _modules = [_modules];
-    }
+  if (!_modules) {
+    throw new Error("No argument passed to Plotly.register.");
+  } else if (_modules && !Array.isArray(_modules)) {
+    _modules = [_modules];
+  }
 
-    for(var i = 0; i < _modules.length; i++) {
-        var newModule = _modules[i];
+  for (var i = 0; i < _modules.length; i++) {
+    var newModule = _modules[i];
 
-        if(!newModule) {
-            throw new Error('Invalid module was attempted to be registered!');
-        }
+    if (!newModule) {
+      throw new Error("Invalid module was attempted to be registered!");
+    }
 
-        switch(newModule.moduleType) {
-            case 'trace':
-                registerTraceModule(newModule);
-                break;
+    switch (newModule.moduleType) {
+      case "trace":
+        registerTraceModule(newModule);
+        break;
 
-            case 'transform':
-                registerTransformModule(newModule);
-                break;
+      case "transform":
+        registerTransformModule(newModule);
+        break;
 
-            case 'component':
-                registerComponentModule(newModule);
-                break;
+      case "component":
+        registerComponentModule(newModule);
+        break;
 
-            default:
-                throw new Error('Invalid module was attempted to be registered!');
-        }
+      default:
+        throw new Error("Invalid module was attempted to be registered!");
     }
+  }
 };
 
 function registerTraceModule(newModule) {
-    Registry.register(newModule, newModule.name, newModule.categories, newModule.meta);
-
-    if(!Registry.subplotsRegistry[newModule.basePlotModule.name]) {
-        Registry.registerSubplot(newModule.basePlotModule);
-    }
+  Registry.register(
+    newModule,
+    newModule.name,
+    newModule.categories,
+    newModule.meta
+  );
+
+  if (!Registry.subplotsRegistry[newModule.basePlotModule.name]) {
+    Registry.registerSubplot(newModule.basePlotModule);
+  }
 }
 
 function registerTransformModule(newModule) {
-    if(typeof newModule.name !== 'string') {
-        throw new Error('Transform module *name* must be a string.');
-    }
-
-    var prefix = 'Transform module ' + newModule.name;
-
-    var hasTransform = typeof newModule.transform === 'function',
-        hasCalcTransform = typeof newModule.calcTransform === 'function';
-
-
-    if(!hasTransform && !hasCalcTransform) {
-        throw new Error(prefix + ' is missing a *transform* or *calcTransform* method.');
-    }
-
-    if(hasTransform && hasCalcTransform) {
-        Lib.log([
-            prefix + ' has both a *transform* and *calcTransform* methods.',
-            'Please note that all *transform* methods are executed',
-            'before all *calcTransform* methods.'
-        ].join(' '));
-    }
-
-    if(!Lib.isPlainObject(newModule.attributes)) {
-        Lib.log(prefix + ' registered without an *attributes* object.');
-    }
-
-    if(typeof newModule.supplyDefaults !== 'function') {
-        Lib.log(prefix + ' registered without a *supplyDefaults* method.');
-    }
-
-    Registry.transformsRegistry[newModule.name] = newModule;
+  if (typeof newModule.name !== "string") {
+    throw new Error("Transform module *name* must be a string.");
+  }
+
+  var prefix = "Transform module " + newModule.name;
+
+  var hasTransform = typeof newModule.transform === "function",
+    hasCalcTransform = typeof newModule.calcTransform === "function";
+
+  if (!hasTransform && !hasCalcTransform) {
+    throw new Error(
+      prefix + " is missing a *transform* or *calcTransform* method."
+    );
+  }
+
+  if (hasTransform && hasCalcTransform) {
+    Lib.log(
+      [
+        prefix + " has both a *transform* and *calcTransform* methods.",
+        "Please note that all *transform* methods are executed",
+        "before all *calcTransform* methods."
+      ].join(" ")
+    );
+  }
+
+  if (!Lib.isPlainObject(newModule.attributes)) {
+    Lib.log(prefix + " registered without an *attributes* object.");
+  }
+
+  if (typeof newModule.supplyDefaults !== "function") {
+    Lib.log(prefix + " registered without a *supplyDefaults* method.");
+  }
+
+  Registry.transformsRegistry[newModule.name] = newModule;
 }
 
 function registerComponentModule(newModule) {
-    if(typeof newModule.name !== 'string') {
-        throw new Error('Component module *name* must be a string.');
-    }
+  if (typeof newModule.name !== "string") {
+    throw new Error("Component module *name* must be a string.");
+  }
 
-    Registry.registerComponent(newModule);
+  Registry.registerComponent(newModule);
 }
diff --git a/src/plot_api/set_plot_config.js b/src/plot_api/set_plot_config.js
index e4f9e058ca4..316c79a776e 100644
--- a/src/plot_api/set_plot_config.js
+++ b/src/plot_api/set_plot_config.js
@@ -5,12 +5,9 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var Plotly = require('../plotly');
-var Lib = require('../lib');
+"use strict";
+var Plotly = require("../plotly");
+var Lib = require("../lib");
 
 /**
  * Extends the plot config
@@ -20,5 +17,5 @@ var Lib = require('../lib');
  *
  */
 module.exports = function setPlotConfig(configObj) {
-    return Lib.extendFlat(Plotly.defaultConfig, configObj);
+  return Lib.extendFlat(Plotly.defaultConfig, configObj);
 };
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index ceb2a4dbf64..0a97354fd96 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -5,225 +5,221 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Plotly = require("../plotly");
+var Registry = require("../registry");
+var Plots = require("../plots/plots");
+var Lib = require("../lib");
 
+var Color = require("../components/color");
+var Drawing = require("../components/drawing");
+var Titles = require("../components/titles");
+var ModeBar = require("../components/modebar");
 
-'use strict';
+exports.layoutStyles = function(gd) {
+  return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd);
+};
 
-var Plotly = require('../plotly');
-var Registry = require('../registry');
-var Plots = require('../plots/plots');
-var Lib = require('../lib');
+exports.lsInner = function(gd) {
+  var fullLayout = gd._fullLayout,
+    gs = fullLayout._size,
+    axList = Plotly.Axes.list(gd),
+    i;
+
+  // clear axis line positions, to be set in the subplot loop below
+  for (i = 0; i < axList.length; i++) {
+    axList[i]._linepositions = {};
+  }
+
+  fullLayout._paperdiv
+    .style({ width: fullLayout.width + "px", height: fullLayout.height + "px" })
+    .selectAll(".main-svg")
+    .call(Drawing.setSize, fullLayout.width, fullLayout.height);
+
+  gd._context.setBackground(gd, fullLayout.paper_bgcolor);
+
+  var freefinished = [];
+  fullLayout._paper.selectAll("g.subplot").each(function(subplot) {
+    var plotinfo = fullLayout._plots[subplot],
+      xa = Plotly.Axes.getFromId(gd, subplot, "x"),
+      ya = Plotly.Axes.getFromId(gd, subplot, "y");
+
+    xa.setScale();
+    // this may already be done... not sure
+    ya.setScale();
+
+    if (plotinfo.bg) {
+      plotinfo.bg
+        .call(
+          Drawing.setRect,
+          xa._offset - gs.p,
+          ya._offset - gs.p,
+          xa._length + 2 * gs.p,
+          ya._length + 2 * gs.p
+        )
+        .call(Color.fill, fullLayout.plot_bgcolor);
+    }
 
-var Color = require('../components/color');
-var Drawing = require('../components/drawing');
-var Titles = require('../components/titles');
-var ModeBar = require('../components/modebar');
+    // Clip so that data only shows up on the plot area.
+    plotinfo.clipId = "clip" + fullLayout._uid + subplot + "plot";
+
+    var plotClip = fullLayout._defs
+      .selectAll("g.clips")
+      .selectAll("#" + plotinfo.clipId)
+      .data([0]);
+
+    plotClip
+      .enter()
+      .append("clipPath")
+      .attr({ class: "plotclip", id: plotinfo.clipId })
+      .append("rect");
+
+    plotClip.selectAll("rect").attr({ width: xa._length, height: ya._length });
+
+    plotinfo.plot.call(Drawing.setTranslate, xa._offset, ya._offset);
+    plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId);
+
+    var xlw = Drawing.crispRound(gd, xa.linewidth, 1),
+      ylw = Drawing.crispRound(gd, ya.linewidth, 1),
+      xp = gs.p + ylw,
+      xpathPrefix = "M" + (-xp) + ",",
+      xpathSuffix = "h" + (xa._length + 2 * xp),
+      showfreex = xa.anchor === "free" && freefinished.indexOf(xa._id) === -1,
+      freeposx = gs.h * (1 - (xa.position || 0)) + xlw / 2 % 1,
+      showbottom = xa.anchor === ya._id && (xa.mirror || xa.side !== "top") ||
+        xa.mirror === "all" ||
+        xa.mirror === "allticks" ||
+        xa.mirrors && xa.mirrors[ya._id + "bottom"],
+      bottompos = ya._length + gs.p + xlw / 2,
+      showtop = xa.anchor === ya._id && (xa.mirror || xa.side === "top") ||
+        xa.mirror === "all" ||
+        xa.mirror === "allticks" ||
+        xa.mirrors && xa.mirrors[ya._id + "top"],
+      toppos = -gs.p - xlw / 2,
+      // shorten y axis lines so they don't overlap x axis lines
+      yp = gs.p,
+      // except where there's no x line
+      // TODO: this gets more complicated with multiple x and y axes
+      ypbottom = showbottom ? 0 : xlw,
+      yptop = showtop ? 0 : xlw,
+      ypathSuffix = "," +
+        (-yp - yptop) +
+        "v" +
+        (ya._length + 2 * yp + yptop + ypbottom),
+      showfreey = ya.anchor === "free" && freefinished.indexOf(ya._id) === -1,
+      freeposy = gs.w * (ya.position || 0) + ylw / 2 % 1,
+      showleft = ya.anchor === xa._id && (ya.mirror || ya.side !== "right") ||
+        ya.mirror === "all" ||
+        ya.mirror === "allticks" ||
+        ya.mirrors && ya.mirrors[xa._id + "left"],
+      leftpos = -gs.p - ylw / 2,
+      showright = ya.anchor === xa._id && (ya.mirror || ya.side === "right") ||
+        ya.mirror === "all" ||
+        ya.mirror === "allticks" ||
+        ya.mirrors && ya.mirrors[xa._id + "right"],
+      rightpos = xa._length + gs.p + ylw / 2;
+
+    // save axis line positions for ticks, draggers, etc to reference
+    // each subplot gets an entry:
+    //    [left or bottom, right or top, free, main]
+    // main is the position at which to draw labels and draggers, if any
+    xa._linepositions[subplot] = [
+      showbottom ? bottompos : undefined,
+      showtop ? toppos : undefined,
+      showfreex ? freeposx : undefined
+    ];
+    if (xa.anchor === ya._id) {
+      xa._linepositions[subplot][3] = xa.side === "top" ? toppos : bottompos;
+    } else if (showfreex) {
+      xa._linepositions[subplot][3] = freeposx;
+    }
 
+    ya._linepositions[subplot] = [
+      showleft ? leftpos : undefined,
+      showright ? rightpos : undefined,
+      showfreey ? freeposy : undefined
+    ];
+    if (ya.anchor === xa._id) {
+      ya._linepositions[subplot][3] = ya.side === "right" ? rightpos : leftpos;
+    } else if (showfreey) {
+      ya._linepositions[subplot][3] = freeposy;
+    }
 
-exports.layoutStyles = function(gd) {
-    return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd);
-};
+    // translate all the extra stuff to have the
+    // same origin as the plot area or axes
+    var origin = "translate(" + xa._offset + "," + ya._offset + ")",
+      originx = origin,
+      originy = origin;
+    if (showfreex) {
+      originx = "translate(" + xa._offset + "," + gs.t + ")";
+      toppos += ya._offset - gs.t;
+      bottompos += ya._offset - gs.t;
+    }
+    if (showfreey) {
+      originy = "translate(" + gs.l + "," + ya._offset + ")";
+      leftpos += xa._offset - gs.l;
+      rightpos += xa._offset - gs.l;
+    }
 
-exports.lsInner = function(gd) {
-    var fullLayout = gd._fullLayout,
-        gs = fullLayout._size,
-        axList = Plotly.Axes.list(gd),
-        i;
-
-    // clear axis line positions, to be set in the subplot loop below
-    for(i = 0; i < axList.length; i++) axList[i]._linepositions = {};
-
-    fullLayout._paperdiv
-        .style({
-            width: fullLayout.width + 'px',
-            height: fullLayout.height + 'px'
-        })
-        .selectAll('.main-svg')
-            .call(Drawing.setSize, fullLayout.width, fullLayout.height);
-
-    gd._context.setBackground(gd, fullLayout.paper_bgcolor);
-
-    var freefinished = [];
-    fullLayout._paper.selectAll('g.subplot').each(function(subplot) {
-        var plotinfo = fullLayout._plots[subplot],
-            xa = Plotly.Axes.getFromId(gd, subplot, 'x'),
-            ya = Plotly.Axes.getFromId(gd, subplot, 'y');
-
-        xa.setScale(); // this may already be done... not sure
-        ya.setScale();
-
-        if(plotinfo.bg) {
-            plotinfo.bg
-                .call(Drawing.setRect,
-                    xa._offset - gs.p, ya._offset - gs.p,
-                    xa._length + 2 * gs.p, ya._length + 2 * gs.p)
-                .call(Color.fill, fullLayout.plot_bgcolor);
-        }
-
-        // Clip so that data only shows up on the plot area.
-        plotinfo.clipId = 'clip' + fullLayout._uid + subplot + 'plot';
-
-        var plotClip = fullLayout._defs.selectAll('g.clips')
-            .selectAll('#' + plotinfo.clipId)
-            .data([0]);
-
-        plotClip.enter().append('clipPath')
-            .attr({
-                'class': 'plotclip',
-                'id': plotinfo.clipId
-            })
-            .append('rect');
-
-        plotClip.selectAll('rect')
-            .attr({
-                'width': xa._length,
-                'height': ya._length
-            });
-
-
-        plotinfo.plot.call(Drawing.setTranslate, xa._offset, ya._offset);
-        plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId);
-
-        var xlw = Drawing.crispRound(gd, xa.linewidth, 1),
-            ylw = Drawing.crispRound(gd, ya.linewidth, 1),
-            xp = gs.p + ylw,
-            xpathPrefix = 'M' + (-xp) + ',',
-            xpathSuffix = 'h' + (xa._length + 2 * xp),
-            showfreex = xa.anchor === 'free' &&
-                freefinished.indexOf(xa._id) === -1,
-            freeposx = gs.h * (1 - (xa.position||0)) + ((xlw / 2) % 1),
-            showbottom =
-                (xa.anchor === ya._id && (xa.mirror || xa.side !== 'top')) ||
-                xa.mirror === 'all' || xa.mirror === 'allticks' ||
-                (xa.mirrors && xa.mirrors[ya._id + 'bottom']),
-            bottompos = ya._length + gs.p + xlw / 2,
-            showtop =
-                (xa.anchor === ya._id && (xa.mirror || xa.side === 'top')) ||
-                xa.mirror === 'all' || xa.mirror === 'allticks' ||
-                (xa.mirrors && xa.mirrors[ya._id + 'top']),
-            toppos = -gs.p - xlw / 2,
-
-            // shorten y axis lines so they don't overlap x axis lines
-            yp = gs.p,
-            // except where there's no x line
-            // TODO: this gets more complicated with multiple x and y axes
-            ypbottom = showbottom ? 0 : xlw,
-            yptop = showtop ? 0 : xlw,
-            ypathSuffix = ',' + (-yp - yptop) +
-                'v' + (ya._length + 2 * yp + yptop + ypbottom),
-            showfreey = ya.anchor === 'free' &&
-                freefinished.indexOf(ya._id) === -1,
-            freeposy = gs.w * (ya.position||0) + ((ylw / 2) % 1),
-            showleft =
-                (ya.anchor === xa._id && (ya.mirror || ya.side !== 'right')) ||
-                ya.mirror === 'all' || ya.mirror === 'allticks' ||
-                (ya.mirrors && ya.mirrors[xa._id + 'left']),
-            leftpos = -gs.p - ylw / 2,
-            showright =
-                (ya.anchor === xa._id && (ya.mirror || ya.side === 'right')) ||
-                ya.mirror === 'all' || ya.mirror === 'allticks' ||
-                (ya.mirrors && ya.mirrors[xa._id + 'right']),
-            rightpos = xa._length + gs.p + ylw / 2;
-
-        // save axis line positions for ticks, draggers, etc to reference
-        // each subplot gets an entry:
-        //    [left or bottom, right or top, free, main]
-        // main is the position at which to draw labels and draggers, if any
-        xa._linepositions[subplot] = [
-            showbottom ? bottompos : undefined,
-            showtop ? toppos : undefined,
-            showfreex ? freeposx : undefined
-        ];
-        if(xa.anchor === ya._id) {
-            xa._linepositions[subplot][3] = xa.side === 'top' ?
-                toppos : bottompos;
-        }
-        else if(showfreex) {
-            xa._linepositions[subplot][3] = freeposx;
-        }
-
-        ya._linepositions[subplot] = [
-            showleft ? leftpos : undefined,
-            showright ? rightpos : undefined,
-            showfreey ? freeposy : undefined
-        ];
-        if(ya.anchor === xa._id) {
-            ya._linepositions[subplot][3] = ya.side === 'right' ?
-                rightpos : leftpos;
-        }
-        else if(showfreey) {
-            ya._linepositions[subplot][3] = freeposy;
-        }
-
-        // translate all the extra stuff to have the
-        // same origin as the plot area or axes
-        var origin = 'translate(' + xa._offset + ',' + ya._offset + ')',
-            originx = origin,
-            originy = origin;
-        if(showfreex) {
-            originx = 'translate(' + xa._offset + ',' + gs.t + ')';
-            toppos += ya._offset - gs.t;
-            bottompos += ya._offset - gs.t;
-        }
-        if(showfreey) {
-            originy = 'translate(' + gs.l + ',' + ya._offset + ')';
-            leftpos += xa._offset - gs.l;
-            rightpos += xa._offset - gs.l;
-        }
-
-        plotinfo.xlines
-            .attr('transform', originx)
-            .attr('d', (
-                (showbottom ? (xpathPrefix + bottompos + xpathSuffix) : '') +
-                (showtop ? (xpathPrefix + toppos + xpathSuffix) : '') +
-                (showfreex ? (xpathPrefix + freeposx + xpathSuffix) : '')) ||
-                // so it doesn't barf with no lines shown
-                'M0,0')
-            .style('stroke-width', xlw + 'px')
-            .call(Color.stroke, xa.showline ?
-                xa.linecolor : 'rgba(0,0,0,0)');
-        plotinfo.ylines
-            .attr('transform', originy)
-            .attr('d', (
-                (showleft ? ('M' + leftpos + ypathSuffix) : '') +
-                (showright ? ('M' + rightpos + ypathSuffix) : '') +
-                (showfreey ? ('M' + freeposy + ypathSuffix) : '')) ||
-                'M0,0')
-            .attr('stroke-width', ylw + 'px')
-            .call(Color.stroke, ya.showline ?
-                ya.linecolor : 'rgba(0,0,0,0)');
-
-        plotinfo.xaxislayer.attr('transform', originx);
-        plotinfo.yaxislayer.attr('transform', originy);
-        plotinfo.gridlayer.attr('transform', origin);
-        plotinfo.zerolinelayer.attr('transform', origin);
-        plotinfo.draglayer.attr('transform', origin);
-
-        // mark free axes as displayed, so we don't draw them again
-        if(showfreex) { freefinished.push(xa._id); }
-        if(showfreey) { freefinished.push(ya._id); }
-    });
-
-    Plotly.Axes.makeClipPaths(gd);
-    exports.drawMainTitle(gd);
-    ModeBar.manage(gd);
-
-    return gd._promises.length && Promise.all(gd._promises);
+    plotinfo.xlines
+      .attr("transform", originx)
+      .attr(
+        "d",
+        (showbottom ? xpathPrefix + bottompos + xpathSuffix : "") +
+          (showtop ? xpathPrefix + toppos + xpathSuffix : "") +
+          (showfreex ? xpathPrefix + freeposx + xpathSuffix : "") ||
+          // so it doesn't barf with no lines shown
+          "M0,0"
+      )
+      .style("stroke-width", xlw + "px")
+      .call(Color.stroke, xa.showline ? xa.linecolor : "rgba(0,0,0,0)");
+    plotinfo.ylines
+      .attr("transform", originy)
+      .attr(
+        "d",
+        (showleft ? "M" + leftpos + ypathSuffix : "") +
+          (showright ? "M" + rightpos + ypathSuffix : "") +
+          (showfreey ? "M" + freeposy + ypathSuffix : "") ||
+          "M0,0"
+      )
+      .attr("stroke-width", ylw + "px")
+      .call(Color.stroke, ya.showline ? ya.linecolor : "rgba(0,0,0,0)");
+
+    plotinfo.xaxislayer.attr("transform", originx);
+    plotinfo.yaxislayer.attr("transform", originy);
+    plotinfo.gridlayer.attr("transform", origin);
+    plotinfo.zerolinelayer.attr("transform", origin);
+    plotinfo.draglayer.attr("transform", origin);
+
+    // mark free axes as displayed, so we don't draw them again
+    if (showfreex) {
+      freefinished.push(xa._id);
+    }
+    if (showfreey) {
+      freefinished.push(ya._id);
+    }
+  });
+
+  Plotly.Axes.makeClipPaths(gd);
+  exports.drawMainTitle(gd);
+  ModeBar.manage(gd);
+
+  return gd._promises.length && Promise.all(gd._promises);
 };
 
 exports.drawMainTitle = function(gd) {
-    var fullLayout = gd._fullLayout;
-
-    Titles.draw(gd, 'gtitle', {
-        propContainer: fullLayout,
-        propName: 'title',
-        dfltName: 'Plot',
-        attributes: {
-            x: fullLayout.width / 2,
-            y: fullLayout._size.t / 2,
-            'text-anchor': 'middle'
-        }
-    });
+  var fullLayout = gd._fullLayout;
+
+  Titles.draw(gd, "gtitle", {
+    propContainer: fullLayout,
+    propName: "title",
+    dfltName: "Plot",
+    attributes: {
+      x: fullLayout.width / 2,
+      y: fullLayout._size.t / 2,
+      "text-anchor": "middle"
+    }
+  });
 };
 
 // First, see if we need to do arraysToCalcdata
@@ -231,98 +227,98 @@ exports.drawMainTitle = function(gd) {
 // supplyDefaults brought in an array that was already
 // in gd.data but not in gd._fullData previously
 exports.doTraceStyle = function(gd) {
-    for(var i = 0; i < gd.calcdata.length; i++) {
-        var cdi = gd.calcdata[i],
-            _module = ((cdi[0] || {}).trace || {})._module || {},
-            arraysToCalcdata = _module.arraysToCalcdata;
+  for (var i = 0; i < gd.calcdata.length; i++) {
+    var cdi = gd.calcdata[i],
+      _module = ((cdi[0] || {}).trace || {})._module || {},
+      arraysToCalcdata = _module.arraysToCalcdata;
 
-        if(arraysToCalcdata) arraysToCalcdata(cdi, cdi[0].trace);
-    }
+    if (arraysToCalcdata) arraysToCalcdata(cdi, cdi[0].trace);
+  }
 
-    Plots.style(gd);
-    Registry.getComponentMethod('legend', 'draw')(gd);
+  Plots.style(gd);
+  Registry.getComponentMethod("legend", "draw")(gd);
 
-    return Plots.previousPromises(gd);
+  return Plots.previousPromises(gd);
 };
 
 exports.doColorBars = function(gd) {
-    for(var i = 0; i < gd.calcdata.length; i++) {
-        var cdi0 = gd.calcdata[i][0];
-
-        if((cdi0.t || {}).cb) {
-            var trace = cdi0.trace,
-                cb = cdi0.t.cb;
-
-            if(Registry.traceIs(trace, 'contour')) {
-                cb.line({
-                    width: trace.contours.showlines !== false ?
-                        trace.line.width : 0,
-                    dash: trace.line.dash,
-                    color: trace.contours.coloring === 'line' ?
-                        cb._opts.line.color : trace.line.color
-                });
-            }
-            if(Registry.traceIs(trace, 'markerColorscale')) {
-                cb.options(trace.marker.colorbar)();
-            }
-            else cb.options(trace.colorbar)();
-        }
+  for (var i = 0; i < gd.calcdata.length; i++) {
+    var cdi0 = gd.calcdata[i][0];
+
+    if ((cdi0.t || {}).cb) {
+      var trace = cdi0.trace, cb = cdi0.t.cb;
+
+      if (Registry.traceIs(trace, "contour")) {
+        cb.line({
+          width: trace.contours.showlines !== false ? trace.line.width : 0,
+          dash: trace.line.dash,
+          color: (
+            trace.contours.coloring === "line"
+              ? cb._opts.line.color
+              : trace.line.color
+          )
+        });
+      }
+      if (Registry.traceIs(trace, "markerColorscale")) {
+        cb.options(trace.marker.colorbar)();
+      } else {
+        cb.options(trace.colorbar)();
+      }
     }
+  }
 
-    return Plots.previousPromises(gd);
+  return Plots.previousPromises(gd);
 };
 
 // force plot() to redo the layout and replot with the modified layout
 exports.layoutReplot = function(gd) {
-    var layout = gd.layout;
-    gd.layout = undefined;
-    return Plotly.plot(gd, '', layout);
+  var layout = gd.layout;
+  gd.layout = undefined;
+  return Plotly.plot(gd, "", layout);
 };
 
 exports.doLegend = function(gd) {
-    Registry.getComponentMethod('legend', 'draw')(gd);
-    return Plots.previousPromises(gd);
+  Registry.getComponentMethod("legend", "draw")(gd);
+  return Plots.previousPromises(gd);
 };
 
 exports.doTicksRelayout = function(gd) {
-    Plotly.Axes.doTicks(gd, 'redraw');
-    exports.drawMainTitle(gd);
-    return Plots.previousPromises(gd);
+  Plotly.Axes.doTicks(gd, "redraw");
+  exports.drawMainTitle(gd);
+  return Plots.previousPromises(gd);
 };
 
 exports.doModeBar = function(gd) {
-    var fullLayout = gd._fullLayout;
-    var subplotIds, i;
-
-    ModeBar.manage(gd);
-    Plotly.Fx.init(gd);
-
-    subplotIds = Plots.getSubplotIds(fullLayout, 'gl3d');
-    for(i = 0; i < subplotIds.length; i++) {
-        var scene = fullLayout[subplotIds[i]]._scene;
-        scene.updateFx(fullLayout.dragmode, fullLayout.hovermode);
-    }
-
-    // no need to do this for gl2d subplots,
-    // Plots.linkSubplots takes care of it all.
-
-    subplotIds = Plots.getSubplotIds(fullLayout, 'geo');
-    for(i = 0; i < subplotIds.length; i++) {
-        var geo = fullLayout[subplotIds[i]]._subplot;
-        geo.updateFx(fullLayout.hovermode);
-    }
-
-    return Plots.previousPromises(gd);
+  var fullLayout = gd._fullLayout;
+  var subplotIds, i;
+
+  ModeBar.manage(gd);
+  Plotly.Fx.init(gd);
+
+  subplotIds = Plots.getSubplotIds(fullLayout, "gl3d");
+  for (i = 0; i < subplotIds.length; i++) {
+    var scene = fullLayout[subplotIds[i]]._scene;
+    scene.updateFx(fullLayout.dragmode, fullLayout.hovermode);
+  }
+
+  // no need to do this for gl2d subplots,
+  // Plots.linkSubplots takes care of it all.
+  subplotIds = Plots.getSubplotIds(fullLayout, "geo");
+  for (i = 0; i < subplotIds.length; i++) {
+    var geo = fullLayout[subplotIds[i]]._subplot;
+    geo.updateFx(fullLayout.hovermode);
+  }
+
+  return Plots.previousPromises(gd);
 };
 
 exports.doCamera = function(gd) {
-    var fullLayout = gd._fullLayout,
-        sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d');
+  var fullLayout = gd._fullLayout,
+    sceneIds = Plots.getSubplotIds(fullLayout, "gl3d");
 
-    for(var i = 0; i < sceneIds.length; i++) {
-        var sceneLayout = fullLayout[sceneIds[i]],
-            scene = sceneLayout._scene;
+  for (var i = 0; i < sceneIds.length; i++) {
+    var sceneLayout = fullLayout[sceneIds[i]], scene = sceneLayout._scene;
 
-        scene.setCamera(sceneLayout.camera);
-    }
+    scene.setCamera(sceneLayout.camera);
+  }
 };
diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js
index 6ebcf75b367..a9b3ce975f4 100644
--- a/src/plot_api/to_image.js
+++ b/src/plot_api/to_image.js
@@ -5,18 +5,16 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var isNumeric = require("fast-isnumeric");
 
-'use strict';
+var Plotly = require("../plotly");
+var Lib = require("../lib");
 
-var isNumeric = require('fast-isnumeric');
-
-var Plotly = require('../plotly');
-var Lib = require('../lib');
-
-var helpers = require('../snapshot/helpers');
-var clonePlot = require('../snapshot/cloneplot');
-var toSVG = require('../snapshot/tosvg');
-var svgToImg = require('../snapshot/svgtoimg');
+var helpers = require("../snapshot/helpers");
+var clonePlot = require("../snapshot/cloneplot");
+var toSVG = require("../snapshot/tosvg");
+var svgToImg = require("../snapshot/svgtoimg");
 
 /**
  * @param {object} gd figure Object
@@ -26,83 +24,92 @@ var svgToImg = require('../snapshot/svgtoimg');
  * @param opts.height height of snapshot in px
  */
 function toImage(gd, opts) {
-
-    var promise = new Promise(function(resolve, reject) {
-        // check for undefined opts
-        opts = opts || {};
-        // default to png
-        opts.format = opts.format || 'png';
-
-        var isSizeGood = function(size) {
-            // undefined and null are valid options
-            if(size === undefined || size === null) {
-                return true;
-            }
-
-            if(isNumeric(size) && size > 1) {
-                return true;
-            }
-
-            return false;
-        };
-
-        if(!isSizeGood(opts.width) || !isSizeGood(opts.height)) {
-            reject(new Error('Height and width should be pixel values.'));
-        }
-
-        // first clone the GD so we can operate in a clean environment
-        var clone = clonePlot(gd, {format: 'png', height: opts.height, width: opts.width});
-        var clonedGd = clone.gd;
-
-        // put the cloned div somewhere off screen before attaching to DOM
-        clonedGd.style.position = 'absolute';
-        clonedGd.style.left = '-5000px';
-        document.body.appendChild(clonedGd);
-
-        function wait() {
-            var delay = helpers.getDelay(clonedGd._fullLayout);
-
-            return new Promise(function(resolve, reject) {
-                setTimeout(function() {
-                    var svg = toSVG(clonedGd);
-
-                    var canvas = document.createElement('canvas');
-                    canvas.id = Lib.randstr();
-
-                    svgToImg({
-                        format: opts.format,
-                        width: clonedGd._fullLayout.width,
-                        height: clonedGd._fullLayout.height,
-                        canvas: canvas,
-                        svg: svg,
-                        // ask svgToImg to return a Promise
-                        //  rather than EventEmitter
-                        //  leave EventEmitter for backward
-                        //  compatibility
-                        promise: true
-                    }).then(function(url) {
-                        if(clonedGd) document.body.removeChild(clonedGd);
-                        resolve(url);
-                    }).catch(function(err) {
-                        reject(err);
-                    });
-
-                }, delay);
-            });
-        }
-
-        var redrawFunc = helpers.getRedrawFunc(clonedGd);
-
-        Plotly.plot(clonedGd, clone.data, clone.layout, clone.config)
-            .then(redrawFunc)
-            .then(wait)
-            .then(function(url) { resolve(url); })
-            .catch(function(err) {
-                reject(err);
-            });
+  var promise = new Promise(function(resolve, reject) {
+    // check for undefined opts
+    opts = opts || {};
+    // default to png
+    opts.format = opts.format || "png";
+
+    var isSizeGood = function(size) {
+      // undefined and null are valid options
+      if (size === undefined || size === null) {
+        return true;
+      }
+
+      if (isNumeric(size) && size > 1) {
+        return true;
+      }
+
+      return false;
+    };
+
+    if (!isSizeGood(opts.width) || !isSizeGood(opts.height)) {
+      reject(new Error("Height and width should be pixel values."));
+    }
+
+    // first clone the GD so we can operate in a clean environment
+    var clone = clonePlot(gd, {
+      format: "png",
+      height: opts.height,
+      width: opts.width
     });
-
-    return promise;
+    var clonedGd = clone.gd;
+
+    // put the cloned div somewhere off screen before attaching to DOM
+    clonedGd.style.position = "absolute";
+    clonedGd.style.left = "-5000px";
+    document.body.appendChild(clonedGd);
+
+    function wait() {
+      var delay = helpers.getDelay(clonedGd._fullLayout);
+
+      return new Promise(function(resolve, reject) {
+        setTimeout(
+          function() {
+            var svg = toSVG(clonedGd);
+
+            var canvas = document.createElement("canvas");
+            canvas.id = Lib.randstr();
+
+            svgToImg({
+              format: opts.format,
+              width: clonedGd._fullLayout.width,
+              height: clonedGd._fullLayout.height,
+              canvas: canvas,
+              svg: svg,
+              // ask svgToImg to return a Promise
+              //  rather than EventEmitter
+              //  leave EventEmitter for backward
+              //  compatibility
+              promise: true
+            })
+              .then(function(url) {
+                if (clonedGd) document.body.removeChild(clonedGd);
+                resolve(url);
+              })
+              .catch(function(err) {
+                reject(err);
+              });
+          },
+          delay
+        );
+      });
+    }
+
+    var redrawFunc = helpers.getRedrawFunc(clonedGd);
+
+    Plotly.plot(clonedGd, clone.data, clone.layout, clone.config)
+      .then(redrawFunc)
+      .then(wait)
+      .then(function(url) {
+        resolve(url);
+      })
+      .catch(function(err) {
+        reject(err);
+      });
+  });
+
+  return promise;
 }
 
 module.exports = toImage;
diff --git a/src/plot_api/validate.js b/src/plot_api/validate.js
index b06e5506f4d..e4759202133 100644
--- a/src/plot_api/validate.js
+++ b/src/plot_api/validate.js
@@ -5,19 +5,14 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-
-var Lib = require('../lib');
-var Plots = require('../plots/plots');
-var PlotSchema = require('./plot_schema');
+"use strict";
+var Lib = require("../lib");
+var Plots = require("../plots/plots");
+var PlotSchema = require("./plot_schema");
 
 var isPlainObject = Lib.isPlainObject;
 var isArray = Array.isArray;
 
-
 /**
  * Validate a data array and layout object.
  *
@@ -40,329 +35,310 @@ var isArray = Array.isArray;
  *      error message (shown in console in logger config argument is enable)
  */
 module.exports = function valiate(data, layout) {
-    var schema = PlotSchema.get(),
-        errorList = [],
-        gd = {};
+  var schema = PlotSchema.get(), errorList = [], gd = {};
+
+  var dataIn, layoutIn;
+
+  if (isArray(data)) {
+    gd.data = Lib.extendDeep([], data);
+    dataIn = data;
+  } else {
+    gd.data = [];
+    dataIn = [];
+    errorList.push(format("array", "data"));
+  }
+
+  if (isPlainObject(layout)) {
+    gd.layout = Lib.extendDeep({}, layout);
+    layoutIn = layout;
+  } else {
+    gd.layout = {};
+    layoutIn = {};
+    if (arguments.length > 1) {
+      errorList.push(format("object", "layout"));
+    }
+  }
 
-    var dataIn, layoutIn;
+  // N.B. dataIn and layoutIn are in general not the same as
+  // gd.data and gd.layout after supplyDefaults as some attributes
+  // in gd.data and gd.layout (still) get mutated during this step.
+  Plots.supplyDefaults(gd);
 
-    if(isArray(data)) {
-        gd.data = Lib.extendDeep([], data);
-        dataIn = data;
-    }
-    else {
-        gd.data = [];
-        dataIn = [];
-        errorList.push(format('array', 'data'));
-    }
+  var dataOut = gd._fullData, len = dataIn.length;
 
-    if(isPlainObject(layout)) {
-        gd.layout = Lib.extendDeep({}, layout);
-        layoutIn = layout;
-    }
-    else {
-        gd.layout = {};
-        layoutIn = {};
-        if(arguments.length > 1) {
-            errorList.push(format('object', 'layout'));
-        }
+  for (var i = 0; i < len; i++) {
+    var traceIn = dataIn[i], base = ["data", i];
+
+    if (!isPlainObject(traceIn)) {
+      errorList.push(format("object", base));
+      continue;
     }
 
-    // N.B. dataIn and layoutIn are in general not the same as
-    // gd.data and gd.layout after supplyDefaults as some attributes
-    // in gd.data and gd.layout (still) get mutated during this step.
+    var traceOut = dataOut[i],
+      traceType = traceOut.type,
+      traceSchema = schema.traces[traceType].attributes;
+
+    // PlotSchema does something fancy with trace 'type', reset it here
+    // to make the trace schema compatible with Lib.validate.
+    traceSchema.type = { valType: "enumerated", values: [traceType] };
 
-    Plots.supplyDefaults(gd);
+    if (traceOut.visible === false && traceIn.visible !== false) {
+      errorList.push(format("invisible", base));
+    }
 
-    var dataOut = gd._fullData,
-        len = dataIn.length;
+    crawl(traceIn, traceOut, traceSchema, errorList, base);
 
-    for(var i = 0; i < len; i++) {
-        var traceIn = dataIn[i],
-            base = ['data', i];
+    var transformsIn = traceIn.transforms, transformsOut = traceOut.transforms;
 
-        if(!isPlainObject(traceIn)) {
-            errorList.push(format('object', base));
-            continue;
-        }
+    if (transformsIn) {
+      if (!isArray(transformsIn)) {
+        errorList.push(format("array", base, ["transforms"]));
+      }
 
-        var traceOut = dataOut[i],
-            traceType = traceOut.type,
-            traceSchema = schema.traces[traceType].attributes;
+      base.push("transforms");
 
-        // PlotSchema does something fancy with trace 'type', reset it here
-        // to make the trace schema compatible with Lib.validate.
-        traceSchema.type = {
-            valType: 'enumerated',
-            values: [traceType]
-        };
+      for (var j = 0; j < transformsIn.length; j++) {
+        var path = ["transforms", j], transformType = transformsIn[j].type;
 
-        if(traceOut.visible === false && traceIn.visible !== false) {
-            errorList.push(format('invisible', base));
+        if (!isPlainObject(transformsIn[j])) {
+          errorList.push(format("object", base, path));
+          continue;
         }
 
-        crawl(traceIn, traceOut, traceSchema, errorList, base);
+        var transformSchema = schema.transforms[transformType]
+          ? schema.transforms[transformType].attributes
+          : {};
 
-        var transformsIn = traceIn.transforms,
-            transformsOut = traceOut.transforms;
+        // add 'type' to transform schema to validate the transform type
+        transformSchema.type = {
+          valType: "enumerated",
+          values: Object.keys(schema.transforms)
+        };
 
-        if(transformsIn) {
-            if(!isArray(transformsIn)) {
-                errorList.push(format('array', base, ['transforms']));
-            }
+        crawl(
+          transformsIn[j],
+          transformsOut[j],
+          transformSchema,
+          errorList,
+          base,
+          path
+        );
+      }
+    }
+  }
 
-            base.push('transforms');
+  var layoutOut = gd._fullLayout,
+    layoutSchema = fillLayoutSchema(schema, dataOut);
 
-            for(var j = 0; j < transformsIn.length; j++) {
-                var path = ['transforms', j],
-                    transformType = transformsIn[j].type;
+  crawl(layoutIn, layoutOut, layoutSchema, errorList, "layout");
 
-                if(!isPlainObject(transformsIn[j])) {
-                    errorList.push(format('object', base, path));
-                    continue;
-                }
+  // return undefined if no validation errors were found
+  return errorList.length === 0 ? void 0 : errorList;
+};
 
-                var transformSchema = schema.transforms[transformType] ?
-                    schema.transforms[transformType].attributes :
-                    {};
+function crawl(objIn, objOut, schema, list, base, path) {
+  path = path || [];
 
-                // add 'type' to transform schema to validate the transform type
-                transformSchema.type = {
-                    valType: 'enumerated',
-                    values: Object.keys(schema.transforms)
-                };
+  var keys = Object.keys(objIn);
 
-                crawl(transformsIn[j], transformsOut[j], transformSchema, errorList, base, path);
-            }
-        }
-    }
+  for (var i = 0; i < keys.length; i++) {
+    var k = keys[i];
 
-    var layoutOut = gd._fullLayout,
-        layoutSchema = fillLayoutSchema(schema, dataOut);
+    // transforms are handled separately
+    if (k === "transforms") continue;
 
-    crawl(layoutIn, layoutOut, layoutSchema, errorList, 'layout');
+    var p = path.slice();
+    p.push(k);
 
-    // return undefined if no validation errors were found
-    return (errorList.length === 0) ? void(0) : errorList;
-};
+    var valIn = objIn[k], valOut = objOut[k];
 
-function crawl(objIn, objOut, schema, list, base, path) {
-    path = path || [];
+    var nestedSchema = getNestedSchema(schema, k),
+      isInfoArray = (nestedSchema || {}).valType === "info_array";
 
-    var keys = Object.keys(objIn);
+    if (!isInSchema(schema, k)) {
+      list.push(format("schema", base, p));
+    } else if (isPlainObject(valIn) && isPlainObject(valOut)) {
+      crawl(valIn, valOut, nestedSchema, list, base, p);
+    } else if (nestedSchema.items && !isInfoArray && isArray(valIn)) {
+      var items = nestedSchema.items,
+        _nestedSchema = items[Object.keys(items)[0]],
+        indexList = [];
 
-    for(var i = 0; i < keys.length; i++) {
-        var k = keys[i];
+      var j, _p;
 
-        // transforms are handled separately
-        if(k === 'transforms') continue;
+      // loop over valOut items while keeping track of their
+      // corresponding input container index (given by _index)
+      for (j = 0; j < valOut.length; j++) {
+        var _index = valOut[j]._index || j;
 
-        var p = path.slice();
-        p.push(k);
+        _p = p.slice();
+        _p.push(_index);
 
-        var valIn = objIn[k],
-            valOut = objOut[k];
+        if (isPlainObject(valIn[_index]) && isPlainObject(valOut[j])) {
+          indexList.push(_index);
+          crawl(valIn[_index], valOut[j], _nestedSchema, list, base, _p);
+        }
+      }
 
-        var nestedSchema = getNestedSchema(schema, k),
-            isInfoArray = (nestedSchema || {}).valType === 'info_array';
+      // loop over valIn to determine where it went wrong for some items
+      for (j = 0; j < valIn.length; j++) {
+        _p = p.slice();
+        _p.push(j);
 
-        if(!isInSchema(schema, k)) {
-            list.push(format('schema', base, p));
-        }
-        else if(isPlainObject(valIn) && isPlainObject(valOut)) {
-            crawl(valIn, valOut, nestedSchema, list, base, p);
-        }
-        else if(nestedSchema.items && !isInfoArray && isArray(valIn)) {
-            var items = nestedSchema.items,
-                _nestedSchema = items[Object.keys(items)[0]],
-                indexList = [];
-
-            var j, _p;
-
-            // loop over valOut items while keeping track of their
-            // corresponding input container index (given by _index)
-            for(j = 0; j < valOut.length; j++) {
-                var _index = valOut[j]._index || j;
-
-                _p = p.slice();
-                _p.push(_index);
-
-                if(isPlainObject(valIn[_index]) && isPlainObject(valOut[j])) {
-                    indexList.push(_index);
-                    crawl(valIn[_index], valOut[j], _nestedSchema, list, base, _p);
-                }
-            }
-
-            // loop over valIn to determine where it went wrong for some items
-            for(j = 0; j < valIn.length; j++) {
-                _p = p.slice();
-                _p.push(j);
-
-                if(!isPlainObject(valIn[j])) {
-                    list.push(format('object', base, _p, valIn[j]));
-                }
-                else if(indexList.indexOf(j) === -1) {
-                    list.push(format('unused', base, _p));
-                }
-            }
-        }
-        else if(!isPlainObject(valIn) && isPlainObject(valOut)) {
-            list.push(format('object', base, p, valIn));
-        }
-        else if(!isArray(valIn) && isArray(valOut) && !isInfoArray) {
-            list.push(format('array', base, p, valIn));
-        }
-        else if(!(k in objOut)) {
-            list.push(format('unused', base, p, valIn));
-        }
-        else if(!Lib.validate(valIn, nestedSchema)) {
-            list.push(format('value', base, p, valIn));
+        if (!isPlainObject(valIn[j])) {
+          list.push(format("object", base, _p, valIn[j]));
+        } else if (indexList.indexOf(j) === -1) {
+          list.push(format("unused", base, _p));
         }
+      }
+    } else if (!isPlainObject(valIn) && isPlainObject(valOut)) {
+      list.push(format("object", base, p, valIn));
+    } else if (!isArray(valIn) && isArray(valOut) && !isInfoArray) {
+      list.push(format("array", base, p, valIn));
+    } else if (!(k in objOut)) {
+      list.push(format("unused", base, p, valIn));
+    } else if (!Lib.validate(valIn, nestedSchema)) {
+      list.push(format("value", base, p, valIn));
     }
+  }
 
-    return list;
+  return list;
 }
 
 // the 'full' layout schema depends on the traces types presents
 function fillLayoutSchema(schema, dataOut) {
-    for(var i = 0; i < dataOut.length; i++) {
-        var traceType = dataOut[i].type,
-            traceLayoutAttr = schema.traces[traceType].layoutAttributes;
+  for (var i = 0; i < dataOut.length; i++) {
+    var traceType = dataOut[i].type,
+      traceLayoutAttr = schema.traces[traceType].layoutAttributes;
 
-        if(traceLayoutAttr) {
-            Lib.extendFlat(schema.layout.layoutAttributes, traceLayoutAttr);
-        }
+    if (traceLayoutAttr) {
+      Lib.extendFlat(schema.layout.layoutAttributes, traceLayoutAttr);
     }
+  }
 
-    return schema.layout.layoutAttributes;
+  return schema.layout.layoutAttributes;
 }
 
 // validation error codes
 var code2msgFunc = {
-    object: function(base, astr) {
-        var prefix;
-
-        if(base === 'layout' && astr === '') prefix = 'The layout argument';
-        else if(base[0] === 'data' && astr === '') {
-            prefix = 'Trace ' + base[1] + ' in the data argument';
-        }
-        else prefix = inBase(base) + 'key ' + astr;
-
-        return prefix + ' must be linked to an object container';
-    },
-    array: function(base, astr) {
-        var prefix;
-
-        if(base === 'data') prefix = 'The data argument';
-        else prefix = inBase(base) + 'key ' + astr;
-
-        return prefix + ' must be linked to an array container';
-    },
-    schema: function(base, astr) {
-        return inBase(base) + 'key ' + astr + ' is not part of the schema';
-    },
-    unused: function(base, astr, valIn) {
-        var target = isPlainObject(valIn) ? 'container' : 'key';
-
-        return inBase(base) + target + ' ' + astr + ' did not get coerced';
-    },
-    invisible: function(base) {
-        return 'Trace ' + base[1] + ' got defaulted to be not visible';
-    },
-    value: function(base, astr, valIn) {
-        return [
-            inBase(base) + 'key ' + astr,
-            'is set to an invalid value (' + valIn + ')'
-        ].join(' ');
+  object: function(base, astr) {
+    var prefix;
+
+    if (base === "layout" && astr === "") {
+      prefix = "The layout argument";
+    } else if (base[0] === "data" && astr === "") {
+      prefix = "Trace " + base[1] + " in the data argument";
+    } else {
+      prefix = inBase(base) + "key " + astr;
     }
+
+    return prefix + " must be linked to an object container";
+  },
+  array: function(base, astr) {
+    var prefix;
+
+    if (base === "data") prefix = "The data argument";
+    else prefix = inBase(base) + "key " + astr;
+
+    return prefix + " must be linked to an array container";
+  },
+  schema: function(base, astr) {
+    return inBase(base) + "key " + astr + " is not part of the schema";
+  },
+  unused: function(base, astr, valIn) {
+    var target = isPlainObject(valIn) ? "container" : "key";
+
+    return inBase(base) + target + " " + astr + " did not get coerced";
+  },
+  invisible: function(base) {
+    return "Trace " + base[1] + " got defaulted to be not visible";
+  },
+  value: function(base, astr, valIn) {
+    return [
+      inBase(base) + "key " + astr,
+      "is set to an invalid value (" + valIn + ")"
+    ].join(" ");
+  }
 };
 
 function inBase(base) {
-    if(isArray(base)) return 'In data trace ' + base[1] + ', ';
+  if (isArray(base)) return "In data trace " + base[1] + ", ";
 
-    return 'In ' + base + ', ';
+  return "In " + base + ", ";
 }
 
 function format(code, base, path, valIn) {
-    path = path || '';
-
-    var container, trace;
-
-    // container is either 'data' or 'layout
-    // trace is the trace index if 'data', null otherwise
-
-    if(isArray(base)) {
-        container = base[0];
-        trace = base[1];
-    }
-    else {
-        container = base;
-        trace = null;
-    }
-
-    var astr = convertPathToAttributeString(path),
-        msg = code2msgFunc[code](base, astr, valIn);
-
-    // log to console if logger config option is enabled
-    Lib.log(msg);
-
-    return {
-        code: code,
-        container: container,
-        trace: trace,
-        path: path,
-        astr: astr,
-        msg: msg
-    };
+  path = path || "";
+
+  var container, trace;
+
+  // container is either 'data' or 'layout
+  // trace is the trace index if 'data', null otherwise
+  if (isArray(base)) {
+    container = base[0];
+    trace = base[1];
+  } else {
+    container = base;
+    trace = null;
+  }
+
+  var astr = convertPathToAttributeString(path),
+    msg = code2msgFunc[code](base, astr, valIn);
+
+  // log to console if logger config option is enabled
+  Lib.log(msg);
+
+  return {
+    code: code,
+    container: container,
+    trace: trace,
+    path: path,
+    astr: astr,
+    msg: msg
+  };
 }
 
 function isInSchema(schema, key) {
-    var parts = splitKey(key),
-        keyMinusId = parts.keyMinusId,
-        id = parts.id;
+  var parts = splitKey(key), keyMinusId = parts.keyMinusId, id = parts.id;
 
-    if((keyMinusId in schema) && schema[keyMinusId]._isSubplotObj && id) {
-        return true;
-    }
+  if (keyMinusId in schema && schema[keyMinusId]._isSubplotObj && id) {
+    return true;
+  }
 
-    return (key in schema);
+  return key in schema;
 }
 
 function getNestedSchema(schema, key) {
-    var parts = splitKey(key);
+  var parts = splitKey(key);
 
-    return schema[parts.keyMinusId];
+  return schema[parts.keyMinusId];
 }
 
 function splitKey(key) {
-    var idRegex = /([2-9]|[1-9][0-9]+)$/;
+  var idRegex = /([2-9]|[1-9][0-9]+)$/;
 
-    var keyMinusId = key.split(idRegex)[0],
-        id = key.substr(keyMinusId.length, key.length);
+  var keyMinusId = key.split(idRegex)[0],
+    id = key.substr(keyMinusId.length, key.length);
 
-    return {
-        keyMinusId: keyMinusId,
-        id: id
-    };
+  return { keyMinusId: keyMinusId, id: id };
 }
 
 function convertPathToAttributeString(path) {
-    if(!isArray(path)) return String(path);
+  if (!isArray(path)) return String(path);
 
-    var astr = '';
+  var astr = "";
 
-    for(var i = 0; i < path.length; i++) {
-        var p = path[i];
+  for (var i = 0; i < path.length; i++) {
+    var p = path[i];
 
-        if(typeof p === 'number') {
-            astr = astr.substr(0, astr.length - 1) + '[' + p + ']';
-        }
-        else {
-            astr += p;
-        }
-
-        if(i < path.length - 1) astr += '.';
+    if (typeof p === "number") {
+      astr = astr.substr(0, astr.length - 1) + "[" + p + "]";
+    } else {
+      astr += p;
     }
 
-    return astr;
+    if (i < path.length - 1) astr += ".";
+  }
+
+  return astr;
 }
diff --git a/src/plotly.js b/src/plotly.js
index 042166b05bc..adeba58a286 100644
--- a/src/plotly.js
+++ b/src/plotly.js
@@ -5,9 +5,7 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 /*
  * Pack internal modules unto an object.
  *
@@ -19,13 +17,13 @@
  */
 
 // configuration
-exports.defaultConfig = require('./plot_api/plot_config');
+exports.defaultConfig = require("./plot_api/plot_config");
 
 // plots
-exports.Plots = require('./plots/plots');
-exports.Axes = require('./plots/cartesian/axes');
-exports.Fx = require('./plots/cartesian/graph_interact');
-exports.ModeBar = require('./components/modebar');
+exports.Plots = require("./plots/plots");
+exports.Axes = require("./plots/cartesian/axes");
+exports.Fx = require("./plots/cartesian/graph_interact");
+exports.ModeBar = require("./components/modebar");
 
 // plot api
-require('./plot_api/plot_api');
+require("./plot_api/plot_api");
diff --git a/src/plots/animation_attributes.js b/src/plots/animation_attributes.js
index 8b7316270cc..48ccd51d340 100644
--- a/src/plots/animation_attributes.js
+++ b/src/plots/animation_attributes.js
@@ -5,118 +5,116 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
+"use strict";
 module.exports = {
-    mode: {
-        valType: 'enumerated',
-        dflt: 'afterall',
-        role: 'info',
-        values: ['immediate', 'next', 'afterall'],
-        description: [
-            'Describes how a new animate call interacts with currently-running',
-            'animations. If `immediate`, current animations are interrupted and',
-            'the new animation is started. If `next`, the current frame is allowed',
-            'to complete, after which the new animation is started. If `afterall`',
-            'all existing frames are animated to completion before the new animation',
-            'is started.'
-        ].join(' ')
+  mode: {
+    valType: "enumerated",
+    dflt: "afterall",
+    role: "info",
+    values: ["immediate", "next", "afterall"],
+    description: [
+      "Describes how a new animate call interacts with currently-running",
+      "animations. If `immediate`, current animations are interrupted and",
+      "the new animation is started. If `next`, the current frame is allowed",
+      "to complete, after which the new animation is started. If `afterall`",
+      "all existing frames are animated to completion before the new animation",
+      "is started."
+    ].join(" ")
+  },
+  direction: {
+    valType: "enumerated",
+    role: "info",
+    values: ["forward", "reverse"],
+    dflt: "forward",
+    description: [
+      "The direction in which to play the frames triggered by the animation call"
+    ].join(" ")
+  },
+  fromcurrent: {
+    valType: "boolean",
+    dflt: false,
+    role: "info",
+    description: [
+      "Play frames starting at the current frame instead of the beginning."
+    ].join(" ")
+  },
+  frame: {
+    duration: {
+      valType: "number",
+      role: "info",
+      min: 0,
+      dflt: 500,
+      description: [
+        "The duration in milliseconds of each frame. If greater than the frame",
+        "duration, it will be limited to the frame duration."
+      ].join(" ")
     },
-    direction: {
-        valType: 'enumerated',
-        role: 'info',
-        values: ['forward', 'reverse'],
-        dflt: 'forward',
-        description: [
-            'The direction in which to play the frames triggered by the animation call'
-        ].join(' ')
-    },
-    fromcurrent: {
-        valType: 'boolean',
-        dflt: false,
-        role: 'info',
-        description: [
-            'Play frames starting at the current frame instead of the beginning.'
-        ].join(' ')
-    },
-    frame: {
-        duration: {
-            valType: 'number',
-            role: 'info',
-            min: 0,
-            dflt: 500,
-            description: [
-                'The duration in milliseconds of each frame. If greater than the frame',
-                'duration, it will be limited to the frame duration.'
-            ].join(' ')
-        },
-        redraw: {
-            valType: 'boolean',
-            role: 'info',
-            dflt: true,
-            description: [
-                'Redraw the plot at completion of the transition. This is desirable',
-                'for transitions that include properties that cannot be transitioned,',
-                'but may significantly slow down updates that do not require a full',
-                'redraw of the plot'
-            ].join(' ')
-        },
+    redraw: {
+      valType: "boolean",
+      role: "info",
+      dflt: true,
+      description: [
+        "Redraw the plot at completion of the transition. This is desirable",
+        "for transitions that include properties that cannot be transitioned,",
+        "but may significantly slow down updates that do not require a full",
+        "redraw of the plot"
+      ].join(" ")
+    }
+  },
+  transition: {
+    duration: {
+      valType: "number",
+      role: "info",
+      min: 0,
+      dflt: 500,
+      description: [
+        "The duration of the transition, in milliseconds. If equal to zero,",
+        "updates are synchronous."
+      ].join(" ")
     },
-    transition: {
-        duration: {
-            valType: 'number',
-            role: 'info',
-            min: 0,
-            dflt: 500,
-            description: [
-                'The duration of the transition, in milliseconds. If equal to zero,',
-                'updates are synchronous.'
-            ].join(' ')
-        },
-        easing: {
-            valType: 'enumerated',
-            dflt: 'cubic-in-out',
-            values: [
-                'linear',
-                'quad',
-                'cubic',
-                'sin',
-                'exp',
-                'circle',
-                'elastic',
-                'back',
-                'bounce',
-                'linear-in',
-                'quad-in',
-                'cubic-in',
-                'sin-in',
-                'exp-in',
-                'circle-in',
-                'elastic-in',
-                'back-in',
-                'bounce-in',
-                'linear-out',
-                'quad-out',
-                'cubic-out',
-                'sin-out',
-                'exp-out',
-                'circle-out',
-                'elastic-out',
-                'back-out',
-                'bounce-out',
-                'linear-in-out',
-                'quad-in-out',
-                'cubic-in-out',
-                'sin-in-out',
-                'exp-in-out',
-                'circle-in-out',
-                'elastic-in-out',
-                'back-in-out',
-                'bounce-in-out'
-            ],
-            role: 'info',
-            description: 'The easing function used for the transition'
-        },
+    easing: {
+      valType: "enumerated",
+      dflt: "cubic-in-out",
+      values: [
+        "linear",
+        "quad",
+        "cubic",
+        "sin",
+        "exp",
+        "circle",
+        "elastic",
+        "back",
+        "bounce",
+        "linear-in",
+        "quad-in",
+        "cubic-in",
+        "sin-in",
+        "exp-in",
+        "circle-in",
+        "elastic-in",
+        "back-in",
+        "bounce-in",
+        "linear-out",
+        "quad-out",
+        "cubic-out",
+        "sin-out",
+        "exp-out",
+        "circle-out",
+        "elastic-out",
+        "back-out",
+        "bounce-out",
+        "linear-in-out",
+        "quad-in-out",
+        "cubic-in-out",
+        "sin-in-out",
+        "exp-in-out",
+        "circle-in-out",
+        "elastic-in-out",
+        "back-in-out",
+        "bounce-in-out"
+      ],
+      role: "info",
+      description: "The easing function used for the transition"
     }
+  }
 };
diff --git a/src/plots/array_container_defaults.js b/src/plots/array_container_defaults.js
index 2754ed613c2..539f7391fde 100644
--- a/src/plots/array_container_defaults.js
+++ b/src/plots/array_container_defaults.js
@@ -5,11 +5,8 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-var Lib = require('../lib');
-
+"use strict";
+var Lib = require("../lib");
 
 /** Convenience wrapper for making array container logic DRY and consistent
  *
@@ -41,27 +38,29 @@ var Lib = require('../lib');
  *    links to supplementary data (e.g. fullData for layout components)
  *
  */
-module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut, opts) {
-    var name = opts.name;
+module.exports = function handleArrayContainerDefaults(
+  parentObjIn,
+  parentObjOut,
+  opts
+) {
+  var name = opts.name;
 
-    var contIn = Array.isArray(parentObjIn[name]) ? parentObjIn[name] : [],
-        contOut = parentObjOut[name] = [];
+  var contIn = Array.isArray(parentObjIn[name]) ? parentObjIn[name] : [],
+    contOut = parentObjOut[name] = [];
 
-    for(var i = 0; i < contIn.length; i++) {
-        var itemIn = contIn[i],
-            itemOut = {},
-            itemOpts = {};
+  for (var i = 0; i < contIn.length; i++) {
+    var itemIn = contIn[i], itemOut = {}, itemOpts = {};
 
-        if(!Lib.isPlainObject(itemIn)) {
-            itemOpts.itemIsNotPlainObject = true;
-            itemIn = {};
-        }
+    if (!Lib.isPlainObject(itemIn)) {
+      itemOpts.itemIsNotPlainObject = true;
+      itemIn = {};
+    }
 
-        opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts);
+    opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts);
 
-        itemOut._input = itemIn;
-        itemOut._index = i;
+    itemOut._input = itemIn;
+    itemOut._index = i;
 
-        contOut.push(itemOut);
-    }
+    contOut.push(itemOut);
+  }
 };
diff --git a/src/plots/attributes.js b/src/plots/attributes.js
index 594fba13a6d..b5a1c44dd4b 100644
--- a/src/plots/attributes.js
+++ b/src/plots/attributes.js
@@ -5,104 +5,98 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-    type: {
-        valType: 'enumerated',
-        role: 'info',
-        values: [],     // listed dynamically
-        dflt: 'scatter'
+  type: {
+    valType: "enumerated",
+    role: "info",
+    values: [],
+    // listed dynamically
+    dflt: "scatter"
+  },
+  visible: {
+    valType: "enumerated",
+    values: [true, false, "legendonly"],
+    role: "info",
+    dflt: true,
+    description: [
+      "Determines whether or not this trace is visible.",
+      "If *legendonly*, the trace is not drawn,",
+      "but can appear as a legend item",
+      "(provided that the legend itself is visible)."
+    ].join(" ")
+  },
+  showlegend: {
+    valType: "boolean",
+    role: "info",
+    dflt: true,
+    description: [
+      "Determines whether or not an item corresponding to this",
+      "trace is shown in the legend."
+    ].join(" ")
+  },
+  legendgroup: {
+    valType: "string",
+    role: "info",
+    dflt: "",
+    description: [
+      "Sets the legend group for this trace.",
+      "Traces part of the same legend group hide/show at the same time",
+      "when toggling legend items."
+    ].join(" ")
+  },
+  opacity: {
+    valType: "number",
+    role: "style",
+    min: 0,
+    max: 1,
+    dflt: 1,
+    description: "Sets the opacity of the trace."
+  },
+  name: {
+    valType: "string",
+    role: "info",
+    description: [
+      "Sets the trace name.",
+      "The trace name appear as the legend item and on hover."
+    ].join(" ")
+  },
+  uid: { valType: "string", role: "info", dflt: "" },
+  hoverinfo: {
+    valType: "flaglist",
+    role: "info",
+    flags: ["x", "y", "z", "text", "name"],
+    extras: ["all", "none", "skip"],
+    dflt: "all",
+    description: [
+      "Determines which trace information appear on hover.",
+      "If `none` or `skip` are set, no information is displayed upon hovering.",
+      "But, if `none` is set, click and hover events are still fired."
+    ].join(" ")
+  },
+  stream: {
+    token: {
+      valType: "string",
+      noBlank: true,
+      strict: true,
+      role: "info",
+      description: [
+        "The stream id number links a data trace on a plot with a stream.",
+        "See https://plot.ly/settings for more details."
+      ].join(" ")
     },
-    visible: {
-        valType: 'enumerated',
-        values: [true, false, 'legendonly'],
-        role: 'info',
-        dflt: true,
-        description: [
-            'Determines whether or not this trace is visible.',
-            'If *legendonly*, the trace is not drawn,',
-            'but can appear as a legend item',
-            '(provided that the legend itself is visible).'
-        ].join(' ')
-    },
-    showlegend: {
-        valType: 'boolean',
-        role: 'info',
-        dflt: true,
-        description: [
-            'Determines whether or not an item corresponding to this',
-            'trace is shown in the legend.'
-        ].join(' ')
-    },
-    legendgroup: {
-        valType: 'string',
-        role: 'info',
-        dflt: '',
-        description: [
-            'Sets the legend group for this trace.',
-            'Traces part of the same legend group hide/show at the same time',
-            'when toggling legend items.'
-        ].join(' ')
-    },
-    opacity: {
-        valType: 'number',
-        role: 'style',
-        min: 0,
-        max: 1,
-        dflt: 1,
-        description: 'Sets the opacity of the trace.'
-    },
-    name: {
-        valType: 'string',
-        role: 'info',
-        description: [
-            'Sets the trace name.',
-            'The trace name appear as the legend item and on hover.'
-        ].join(' ')
-    },
-    uid: {
-        valType: 'string',
-        role: 'info',
-        dflt: ''
-    },
-    hoverinfo: {
-        valType: 'flaglist',
-        role: 'info',
-        flags: ['x', 'y', 'z', 'text', 'name'],
-        extras: ['all', 'none', 'skip'],
-        dflt: 'all',
-        description: [
-            'Determines which trace information appear on hover.',
-            'If `none` or `skip` are set, no information is displayed upon hovering.',
-            'But, if `none` is set, click and hover events are still fired.'
-        ].join(' ')
-    },
-    stream: {
-        token: {
-            valType: 'string',
-            noBlank: true,
-            strict: true,
-            role: 'info',
-            description: [
-                'The stream id number links a data trace on a plot with a stream.',
-                'See https://plot.ly/settings for more details.'
-            ].join(' ')
-        },
-        maxpoints: {
-            valType: 'number',
-            min: 0,
-            max: 10000,
-            dflt: 500,
-            role: 'info',
-            description: [
-                'Sets the maximum number of points to keep on the plots from an',
-                'incoming stream.',
-                'If `maxpoints` is set to *50*, only the newest 50 points will',
-                'be displayed on the plot.'
-            ].join(' ')
-        }
+    maxpoints: {
+      valType: "number",
+      min: 0,
+      max: 10000,
+      dflt: 500,
+      role: "info",
+      description: [
+        "Sets the maximum number of points to keep on the plots from an",
+        "incoming stream.",
+        "If `maxpoints` is set to *50*, only the newest 50 points will",
+        "be displayed on the plot."
+      ].join(" ")
     }
+  }
 };
diff --git a/src/plots/cartesian/attributes.js b/src/plots/cartesian/attributes.js
index a472892edc6..40c1f408b2d 100644
--- a/src/plots/cartesian/attributes.js
+++ b/src/plots/cartesian/attributes.js
@@ -5,33 +5,30 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-    xaxis: {
-        valType: 'subplotid',
-        role: 'info',
-        dflt: 'x',
-        description: [
-            'Sets a reference between this trace\'s x coordinates and',
-            'a 2D cartesian x axis.',
-            'If *x* (the default value), the x coordinates refer to',
-            '`layout.xaxis`.',
-            'If *x2*, the x coordinates refer to `layout.xaxis2`, and so on.'
-        ].join(' ')
-    },
-    yaxis: {
-        valType: 'subplotid',
-        role: 'info',
-        dflt: 'y',
-        description: [
-            'Sets a reference between this trace\'s y coordinates and',
-            'a 2D cartesian y axis.',
-            'If *y* (the default value), the y coordinates refer to',
-            '`layout.yaxis`.',
-            'If *y2*, the y coordinates refer to `layout.xaxis2`, and so on.'
-        ].join(' ')
-    }
+  xaxis: {
+    valType: "subplotid",
+    role: "info",
+    dflt: "x",
+    description: [
+      "Sets a reference between this trace's x coordinates and",
+      "a 2D cartesian x axis.",
+      "If *x* (the default value), the x coordinates refer to",
+      "`layout.xaxis`.",
+      "If *x2*, the x coordinates refer to `layout.xaxis2`, and so on."
+    ].join(" ")
+  },
+  yaxis: {
+    valType: "subplotid",
+    role: "info",
+    dflt: "y",
+    description: [
+      "Sets a reference between this trace's y coordinates and",
+      "a 2D cartesian y axis.",
+      "If *y* (the default value), the y coordinates refer to",
+      "`layout.yaxis`.",
+      "If *y2*, the y coordinates refer to `layout.xaxis2`, and so on."
+    ].join(" ")
+  }
 };
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index 603a933d548..f3c02d80eb4 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -5,21 +5,18 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var d3 = require('d3');
-var isNumeric = require('fast-isnumeric');
-
-var Registry = require('../../registry');
-var Lib = require('../../lib');
-var svgTextUtils = require('../../lib/svg_text_utils');
-var Titles = require('../../components/titles');
-var Color = require('../../components/color');
-var Drawing = require('../../components/drawing');
-
-var constants = require('../../constants/numerical');
+"use strict";
+var d3 = require("d3");
+var isNumeric = require("fast-isnumeric");
+
+var Registry = require("../../registry");
+var Lib = require("../../lib");
+var svgTextUtils = require("../../lib/svg_text_utils");
+var Titles = require("../../components/titles");
+var Color = require("../../components/color");
+var Drawing = require("../../components/drawing");
+
+var constants = require("../../constants/numerical");
 var FP_SAFE = constants.FP_SAFE;
 var ONEAVGYEAR = constants.ONEAVGYEAR;
 var ONEAVGMONTH = constants.ONEAVGMONTH;
@@ -29,15 +26,14 @@ var ONEMIN = constants.ONEMIN;
 var ONESEC = constants.ONESEC;
 var BADNUM = constants.BADNUM;
 
-
 var axes = module.exports = {};
 
-axes.layoutAttributes = require('./layout_attributes');
-axes.supplyLayoutDefaults = require('./layout_defaults');
+axes.layoutAttributes = require("./layout_attributes");
+axes.supplyLayoutDefaults = require("./layout_defaults");
 
-axes.setConvert = require('./set_convert');
+axes.setConvert = require("./set_convert");
 
-var axisIds = require('./axis_ids');
+var axisIds = require("./axis_ids");
 axes.id2name = axisIds.id2name;
 axes.cleanId = axisIds.cleanId;
 axes.list = axisIds.list;
@@ -45,7 +41,6 @@ axes.listIds = axisIds.listIds;
 axes.getFromId = axisIds.getFromId;
 axes.getFromTrace = axisIds.getFromTrace;
 
-
 /*
  * find the list of possible axes to reference with an xref or yref attribute
  * and coerce it to that list
@@ -57,25 +52,31 @@ axes.getFromTrace = axisIds.getFromTrace;
  * extraOption: aside from existing axes with this letter, what non-axis value is allowed?
  *     Only required if it's different from `dflt`
  */
-axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption) {
-    var axLetter = attr.charAt(attr.length - 1),
-        axlist = axes.listIds(gd, axLetter),
-        refAttr = attr + 'ref',
-        attrDef = {};
-
-    if(!dflt) dflt = axlist[0] || extraOption;
-    if(!extraOption) extraOption = dflt;
-
-    // data-ref annotations are not supported in gl2d yet
-
-    attrDef[refAttr] = {
-        valType: 'enumerated',
-        values: axlist.concat(extraOption ? [extraOption] : []),
-        dflt: dflt
-    };
-
-    // xref, yref
-    return Lib.coerce(containerIn, containerOut, attrDef, refAttr);
+axes.coerceRef = function(
+  containerIn,
+  containerOut,
+  gd,
+  attr,
+  dflt,
+  extraOption
+) {
+  var axLetter = attr.charAt(attr.length - 1),
+    axlist = axes.listIds(gd, axLetter),
+    refAttr = attr + "ref",
+    attrDef = {};
+
+  if (!dflt) dflt = axlist[0] || extraOption;
+  if (!extraOption) extraOption = dflt;
+
+  // data-ref annotations are not supported in gl2d yet
+  attrDef[refAttr] = {
+    valType: "enumerated",
+    values: axlist.concat(extraOption ? [extraOption] : []),
+    dflt: dflt
+  };
+
+  // xref, yref
+  return Lib.coerce(containerIn, containerOut, attrDef, refAttr);
 };
 
 /*
@@ -101,54 +102,53 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption
  * - for other types: coerce them to numbers
  */
 axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) {
-    var pos,
-        newPos;
+  var pos, newPos;
 
-    if(axRef === 'paper' || axRef === 'pixel') {
-        pos = coerce(attr, dflt);
-    }
-    else {
-        var ax = axes.getFromId(gd, axRef);
+  if (axRef === "paper" || axRef === "pixel") {
+    pos = coerce(attr, dflt);
+  } else {
+    var ax = axes.getFromId(gd, axRef);
 
-        dflt = ax.fraction2r(dflt);
-        pos = coerce(attr, dflt);
+    dflt = ax.fraction2r(dflt);
+    pos = coerce(attr, dflt);
 
-        if(ax.type === 'category') {
-            // if position is given as a category name, convert it to a number
-            if(typeof pos === 'string' && (ax._categories || []).length) {
-                newPos = ax._categories.indexOf(pos);
-                containerOut[attr] = (newPos === -1) ? dflt : newPos;
-                return;
-            }
-        }
-        else if(ax.type === 'date') {
-            containerOut[attr] = Lib.cleanDate(pos, BADNUM, ax.calendar);
-            return;
-        }
-    }
-    // finally make sure we have a number (unless date type already returned a string)
-    containerOut[attr] = isNumeric(pos) ? Number(pos) : dflt;
+    if (ax.type === "category") {
+      // if position is given as a category name, convert it to a number
+      if (typeof pos === "string" && (ax._categories || []).length) {
+        newPos = ax._categories.indexOf(pos);
+        containerOut[attr] = newPos === -1 ? dflt : newPos;
+        return;
+      }
+    } else if (ax.type === "date") {
+      containerOut[attr] = Lib.cleanDate(pos, BADNUM, ax.calendar);
+      return;
+    }
+  }
+  // finally make sure we have a number (unless date type already returned a string)
+  containerOut[attr] = isNumeric(pos) ? Number(pos) : dflt;
 };
 
 // empty out types for all axes containing these traces
 // so we auto-set them again
 axes.clearTypes = function(gd, traces) {
-    if(!Array.isArray(traces) || !traces.length) {
-        traces = (gd._fullData).map(function(d, i) { return i; });
-    }
-    traces.forEach(function(tracenum) {
-        var trace = gd.data[tracenum];
-        delete (axes.getFromId(gd, trace.xaxis) || {}).type;
-        delete (axes.getFromId(gd, trace.yaxis) || {}).type;
+  if (!Array.isArray(traces) || !traces.length) {
+    traces = gd._fullData.map(function(d, i) {
+      return i;
     });
+  }
+  traces.forEach(function(tracenum) {
+    var trace = gd.data[tracenum];
+    delete (axes.getFromId(gd, trace.xaxis) || {}).type;
+    delete (axes.getFromId(gd, trace.yaxis) || {}).type;
+  });
 };
 
 // get counteraxis letter for this axis (name or id)
 // this can also be used as the id for default counter axis
 axes.counterLetter = function(id) {
-    var axLetter = id.charAt(0);
-    if(axLetter === 'x') return 'y';
-    if(axLetter === 'y') return 'x';
+  var axLetter = id.charAt(0);
+  if (axLetter === "x") return "y";
+  if (axLetter === "y") return "x";
 };
 
 // incorporate a new minimum difference and first tick into
@@ -156,35 +156,34 @@ axes.counterLetter = function(id) {
 // note that _forceTick0 is linearized, so needs to be turned into
 // a range value for setting tick0
 axes.minDtick = function(ax, newDiff, newFirst, allow) {
-    // doesn't make sense to do forced min dTick on log or category axes,
-    // and the plot itself may decide to cancel (ie non-grouped bars)
-    if(['log', 'category'].indexOf(ax.type) !== -1 || !allow) {
-        ax._minDtick = 0;
-    }
+  // doesn't make sense to do forced min dTick on log or category axes,
+  // and the plot itself may decide to cancel (ie non-grouped bars)
+  if (["log", "category"].indexOf(ax.type) !== -1 || !allow) {
+    ax._minDtick = 0;
+  } else if (ax._minDtick === undefined) {
     // undefined means there's nothing there yet
-    else if(ax._minDtick === undefined) {
-        ax._minDtick = newDiff;
-        ax._forceTick0 = newFirst;
-    }
-    else if(ax._minDtick) {
-        // existing minDtick is an integer multiple of newDiff
-        // (within rounding err)
-        // and forceTick0 can be shifted to newFirst
-        if((ax._minDtick / newDiff + 1e-6) % 1 < 2e-6 &&
-                (((newFirst - ax._forceTick0) / newDiff % 1) +
-                    1.000001) % 1 < 2e-6) {
-            ax._minDtick = newDiff;
-            ax._forceTick0 = newFirst;
-        }
-        // if the converse is true (newDiff is a multiple of minDtick and
-        // newFirst can be shifted to forceTick0) then do nothing - same
-        // forcing stands. Otherwise, cancel forced minimum
-        else if((newDiff / ax._minDtick + 1e-6) % 1 > 2e-6 ||
-                (((newFirst - ax._forceTick0) / ax._minDtick % 1) +
-                    1.000001) % 1 > 2e-6) {
-            ax._minDtick = 0;
-        }
-    }
+    ax._minDtick = newDiff;
+    ax._forceTick0 = newFirst;
+  } else if (ax._minDtick) {
+    // existing minDtick is an integer multiple of newDiff
+    // (within rounding err)
+    // and forceTick0 can be shifted to newFirst
+    if (
+      (ax._minDtick / newDiff + 1e-6) % 1 < 2e-6 &&
+        ((newFirst - ax._forceTick0) / newDiff % 1 + 1.000001) % 1 < 2e-6
+    ) {
+      ax._minDtick = newDiff;
+      ax._forceTick0 = newFirst;
+    } else if (
+      (newDiff / ax._minDtick + 1e-6) % 1 > 2e-6 ||
+        ((newFirst - ax._forceTick0) / ax._minDtick % 1 + 1.000001) % 1 > 2e-6
+    ) {
+      // if the converse is true (newDiff is a multiple of minDtick and
+      // newFirst can be shifted to forceTick0) then do nothing - same
+      // forcing stands. Otherwise, cancel forced minimum
+      ax._minDtick = 0;
+    }
+  }
 };
 
 // Find the autorange for this axis
@@ -201,168 +200,152 @@ axes.minDtick = function(ax, newDiff, newFirst, allow) {
 // though, because otherwise values between categories (or outside all categories)
 // would be impossible.
 axes.getAutoRange = function(ax) {
-    var newRange = [];
-
-    var minmin = ax._min[0].val,
-        maxmax = ax._max[0].val,
-        i;
-
-    for(i = 1; i < ax._min.length; i++) {
-        if(minmin !== maxmax) break;
-        minmin = Math.min(minmin, ax._min[i].val);
-    }
-    for(i = 1; i < ax._max.length; i++) {
-        if(minmin !== maxmax) break;
-        maxmax = Math.max(maxmax, ax._max[i].val);
-    }
-
-    var j, minpt, maxpt, minbest, maxbest, dp, dv,
-        mbest = 0,
-        axReverse = false;
-
-    if(ax.range) {
-        var rng = Lib.simpleMap(ax.range, ax.r2l);
-        axReverse = rng[1] < rng[0];
-    }
-
-    // one-time setting to easily reverse the axis
-    // when plotting from code
-    if(ax.autorange === 'reversed') {
-        axReverse = true;
-        ax.autorange = true;
-    }
-
-    for(i = 0; i < ax._min.length; i++) {
-        minpt = ax._min[i];
-        for(j = 0; j < ax._max.length; j++) {
-            maxpt = ax._max[j];
-            dv = maxpt.val - minpt.val;
-            dp = ax._length - minpt.pad - maxpt.pad;
-            if(dv > 0 && dp > 0 && dv / dp > mbest) {
-                minbest = minpt;
-                maxbest = maxpt;
-                mbest = dv / dp;
-            }
-        }
-    }
-
-    if(minmin === maxmax) {
-        var lower = minmin - 1;
-        var upper = minmin + 1;
-        if(ax.rangemode === 'tozero') {
-            newRange = minmin < 0 ? [lower, 0] : [0, upper];
-        }
-        else if(ax.rangemode === 'nonnegative') {
-            newRange = [Math.max(0, lower), Math.max(0, upper)];
-        }
-        else {
-            newRange = [lower, upper];
-        }
-    }
-    else if(mbest) {
-        if(ax.type === 'linear' || ax.type === '-') {
-            if(ax.rangemode === 'tozero') {
-                if(minbest.val >= 0) {
-                    minbest = {val: 0, pad: 0};
-                }
-                if(maxbest.val <= 0) {
-                    maxbest = {val: 0, pad: 0};
-                }
-            }
-            else if(ax.rangemode === 'nonnegative') {
-                if(minbest.val - mbest * minbest.pad < 0) {
-                    minbest = {val: 0, pad: 0};
-                }
-                if(maxbest.val < 0) {
-                    maxbest = {val: 1, pad: 0};
-                }
-            }
-
-            // in case it changed again...
-            mbest = (maxbest.val - minbest.val) /
-                (ax._length - minbest.pad - maxbest.pad);
+  var newRange = [];
 
-        }
+  var minmin = ax._min[0].val, maxmax = ax._max[0].val, i;
 
-        newRange = [
-            minbest.val - mbest * minbest.pad,
-            maxbest.val + mbest * maxbest.pad
-        ];
-    }
+  for (i = 1; i < ax._min.length; i++) {
+    if (minmin !== maxmax) break;
+    minmin = Math.min(minmin, ax._min[i].val);
+  }
+  for (i = 1; i < ax._max.length; i++) {
+    if (minmin !== maxmax) break;
+    maxmax = Math.max(maxmax, ax._max[i].val);
+  }
 
-    // don't let axis have zero size, while still respecting tozero and nonnegative
-    if(newRange[0] === newRange[1]) {
-        if(ax.rangemode === 'tozero') {
-            if(newRange[0] < 0) {
-                newRange = [newRange[0], 0];
-            }
-            else if(newRange[0] > 0) {
-                newRange = [0, newRange[0]];
-            }
-            else {
-                newRange = [0, 1];
-            }
-        }
-        else {
-            newRange = [newRange[0] - 1, newRange[0] + 1];
-            if(ax.rangemode === 'nonnegative') {
-                newRange[0] = Math.max(0, newRange[0]);
-            }
-        }
-    }
+  var j, minpt, maxpt, minbest, maxbest, dp, dv, mbest = 0, axReverse = false;
 
-    // maintain reversal
-    if(axReverse) newRange.reverse();
-
-    return Lib.simpleMap(newRange, ax.l2r || Number);
+  if (ax.range) {
+    var rng = Lib.simpleMap(ax.range, ax.r2l);
+    axReverse = rng[1] < rng[0];
+  }
+
+  // one-time setting to easily reverse the axis
+  // when plotting from code
+  if (ax.autorange === "reversed") {
+    axReverse = true;
+    ax.autorange = true;
+  }
+
+  for (i = 0; i < ax._min.length; i++) {
+    minpt = ax._min[i];
+    for (j = 0; j < ax._max.length; j++) {
+      maxpt = ax._max[j];
+      dv = maxpt.val - minpt.val;
+      dp = ax._length - minpt.pad - maxpt.pad;
+      if (dv > 0 && dp > 0 && dv / dp > mbest) {
+        minbest = minpt;
+        maxbest = maxpt;
+        mbest = dv / dp;
+      }
+    }
+  }
+
+  if (minmin === maxmax) {
+    var lower = minmin - 1;
+    var upper = minmin + 1;
+    if (ax.rangemode === "tozero") {
+      newRange = minmin < 0 ? [lower, 0] : [0, upper];
+    } else if (ax.rangemode === "nonnegative") {
+      newRange = [Math.max(0, lower), Math.max(0, upper)];
+    } else {
+      newRange = [lower, upper];
+    }
+  } else if (mbest) {
+    if (ax.type === "linear" || ax.type === "-") {
+      if (ax.rangemode === "tozero") {
+        if (minbest.val >= 0) {
+          minbest = { val: 0, pad: 0 };
+        }
+        if (maxbest.val <= 0) {
+          maxbest = { val: 0, pad: 0 };
+        }
+      } else if (ax.rangemode === "nonnegative") {
+        if (minbest.val - mbest * minbest.pad < 0) {
+          minbest = { val: 0, pad: 0 };
+        }
+        if (maxbest.val < 0) {
+          maxbest = { val: 1, pad: 0 };
+        }
+      }
+
+      // in case it changed again...
+      mbest = (maxbest.val - minbest.val) /
+        (ax._length - minbest.pad - maxbest.pad);
+    }
+
+    newRange = [
+      minbest.val - mbest * minbest.pad,
+      maxbest.val + mbest * maxbest.pad
+    ];
+  }
+
+  // don't let axis have zero size, while still respecting tozero and nonnegative
+  if (newRange[0] === newRange[1]) {
+    if (ax.rangemode === "tozero") {
+      if (newRange[0] < 0) {
+        newRange = [newRange[0], 0];
+      } else if (newRange[0] > 0) {
+        newRange = [0, newRange[0]];
+      } else {
+        newRange = [0, 1];
+      }
+    } else {
+      newRange = [newRange[0] - 1, newRange[0] + 1];
+      if (ax.rangemode === "nonnegative") {
+        newRange[0] = Math.max(0, newRange[0]);
+      }
+    }
+  }
+
+  // maintain reversal
+  if (axReverse) newRange.reverse();
+
+  return Lib.simpleMap(newRange, ax.l2r || Number);
 };
 
 axes.doAutoRange = function(ax) {
-    if(!ax._length) ax.setScale();
+  if (!ax._length) ax.setScale();
 
-    // TODO do we really need this?
-    var hasDeps = (ax._min && ax._max && ax._min.length && ax._max.length);
+  // TODO do we really need this?
+  var hasDeps = ax._min && ax._max && ax._min.length && ax._max.length;
 
-    if(ax.autorange && hasDeps) {
-        ax.range = axes.getAutoRange(ax);
+  if (ax.autorange && hasDeps) {
+    ax.range = axes.getAutoRange(ax);
 
-        // doAutoRange will get called on fullLayout,
-        // but we want to report its results back to layout
-        var axIn = ax._gd.layout[ax._name];
+    // doAutoRange will get called on fullLayout,
+    // but we want to report its results back to layout
+    var axIn = ax._gd.layout[ax._name];
 
-        if(!axIn) ax._gd.layout[ax._name] = axIn = {};
+    if (!axIn) ax._gd.layout[ax._name] = axIn = {};
 
-        if(axIn !== ax) {
-            axIn.range = ax.range.slice();
-            axIn.autorange = ax.autorange;
-        }
+    if (axIn !== ax) {
+      axIn.range = ax.range.slice();
+      axIn.autorange = ax.autorange;
     }
+  }
 };
 
 // save a copy of the initial axis ranges in fullLayout
 // use them in mode bar and dblclick events
 axes.saveRangeInitial = function(gd, overwrite) {
-    var axList = axes.list(gd, '', true),
-        hasOneAxisChanged = false;
-
-    for(var i = 0; i < axList.length; i++) {
-        var ax = axList[i];
-
-        var isNew = (ax._rangeInitial === undefined);
-        var hasChanged = (
-            isNew || !(
-                ax.range[0] === ax._rangeInitial[0] &&
-                ax.range[1] === ax._rangeInitial[1]
-            )
-        );
-
-        if((isNew && ax.autorange === false) || (overwrite && hasChanged)) {
-            ax._rangeInitial = ax.range.slice();
-            hasOneAxisChanged = true;
-        }
+  var axList = axes.list(gd, "", true), hasOneAxisChanged = false;
+
+  for (var i = 0; i < axList.length; i++) {
+    var ax = axList[i];
+
+    var isNew = ax._rangeInitial === undefined;
+    var hasChanged = isNew ||
+      !(ax.range[0] === ax._rangeInitial[0] &&
+        ax.range[1] === ax._rangeInitial[1]);
+
+    if (isNew && ax.autorange === false || overwrite && hasChanged) {
+      ax._rangeInitial = ax.range.slice();
+      hasOneAxisChanged = true;
     }
+  }
 
-    return hasOneAxisChanged;
+  return hasOneAxisChanged;
 };
 
 // axes.expand: if autoranging, include new data in the outer limits
@@ -378,435 +361,457 @@ axes.saveRangeInitial = function(gd, overwrite) {
 //      tozero: (boolean) make sure to include zero if axis is linear,
 //          and make it a tight bound if possible
 axes.expand = function(ax, data, options) {
-    if(!(ax.autorange || ax._needsExpand) || !data) return;
-    if(!ax._min) ax._min = [];
-    if(!ax._max) ax._max = [];
-    if(!options) options = {};
-    if(!ax._m) ax.setScale();
-
-    var len = data.length,
-        extrappad = options.padded ? ax._length * 0.05 : 0,
-        tozero = options.tozero && (ax.type === 'linear' || ax.type === '-'),
-        i, j, v, di, dmin, dmax,
-        ppadiplus, ppadiminus, includeThis, vmin, vmax;
-
-    function getPad(item) {
-        if(Array.isArray(item)) {
-            return function(i) { return Math.max(Number(item[i]||0), 0); };
-        }
-        else {
-            var v = Math.max(Number(item||0), 0);
-            return function() { return v; };
-        }
-    }
-    var ppadplus = getPad((ax._m > 0 ?
-            options.ppadplus : options.ppadminus) || options.ppad || 0),
-        ppadminus = getPad((ax._m > 0 ?
-            options.ppadminus : options.ppadplus) || options.ppad || 0),
-        vpadplus = getPad(options.vpadplus || options.vpad),
-        vpadminus = getPad(options.vpadminus || options.vpad);
-
-    function addItem(i) {
-        di = data[i];
-        if(!isNumeric(di)) return;
-        ppadiplus = ppadplus(i) + extrappad;
-        ppadiminus = ppadminus(i) + extrappad;
-        vmin = di - vpadminus(i);
-        vmax = di + vpadplus(i);
-        // special case for log axes: if vpad makes this object span
-        // more than an order of mag, clip it to one order. This is so
-        // we don't have non-positive errors or absurdly large lower
-        // range due to rounding errors
-        if(ax.type === 'log' && vmin < vmax / 10) { vmin = vmax / 10; }
-
-        dmin = ax.c2l(vmin);
-        dmax = ax.c2l(vmax);
-
-        if(tozero) {
-            dmin = Math.min(0, dmin);
-            dmax = Math.max(0, dmax);
-        }
-
-        // In order to stop overflow errors, don't consider points
-        // too close to the limits of js floating point
-        function goodNumber(v) {
-            return isNumeric(v) && Math.abs(v) < FP_SAFE;
-        }
-
-        if(goodNumber(dmin)) {
-            includeThis = true;
-            // take items v from ax._min and compare them to the
-            // presently active point:
-            // - if the item supercedes the new point, set includethis false
-            // - if the new pt supercedes the item, delete it from ax._min
-            for(j = 0; j < ax._min.length && includeThis; j++) {
-                v = ax._min[j];
-                if(v.val <= dmin && v.pad >= ppadiminus) {
-                    includeThis = false;
-                }
-                else if(v.val >= dmin && v.pad <= ppadiminus) {
-                    ax._min.splice(j, 1);
-                    j--;
-                }
-            }
-            if(includeThis) {
-                ax._min.push({
-                    val: dmin,
-                    pad: (tozero && dmin === 0) ? 0 : ppadiminus
-                });
-            }
-        }
-
-        if(goodNumber(dmax)) {
-            includeThis = true;
-            for(j = 0; j < ax._max.length && includeThis; j++) {
-                v = ax._max[j];
-                if(v.val >= dmax && v.pad >= ppadiplus) {
-                    includeThis = false;
-                }
-                else if(v.val <= dmax && v.pad <= ppadiplus) {
-                    ax._max.splice(j, 1);
-                    j--;
-                }
-            }
-            if(includeThis) {
-                ax._max.push({
-                    val: dmax,
-                    pad: (tozero && dmax === 0) ? 0 : ppadiplus
-                });
-            }
-        }
-    }
-
-    // For efficiency covering monotonic or near-monotonic data,
-    // check a few points at both ends first and then sweep
-    // through the middle
-    for(i = 0; i < 6; i++) addItem(i);
-    for(i = len - 1; i > 5; i--) addItem(i);
-
+  if (!(ax.autorange || ax._needsExpand) || !data) return;
+  if (!ax._min) ax._min = [];
+  if (!ax._max) ax._max = [];
+  if (!options) options = {};
+  if (!ax._m) ax.setScale();
+
+  var len = data.length,
+    extrappad = options.padded ? ax._length * 0.05 : 0,
+    tozero = options.tozero && (ax.type === "linear" || ax.type === "-"),
+    i,
+    j,
+    v,
+    di,
+    dmin,
+    dmax,
+    ppadiplus,
+    ppadiminus,
+    includeThis,
+    vmin,
+    vmax;
+
+  function getPad(item) {
+    if (Array.isArray(item)) {
+      return function(i) {
+        return Math.max(Number(item[i] || 0), 0);
+      };
+    } else {
+      var v = Math.max(Number(item || 0), 0);
+      return function() {
+        return v;
+      };
+    }
+  }
+  var ppadplus = getPad(
+    (ax._m > 0 ? options.ppadplus : options.ppadminus) || options.ppad || 0
+  ),
+    ppadminus = getPad(
+      (ax._m > 0 ? options.ppadminus : options.ppadplus) || options.ppad || 0
+    ),
+    vpadplus = getPad(options.vpadplus || options.vpad),
+    vpadminus = getPad(options.vpadminus || options.vpad);
+
+  function addItem(i) {
+    di = data[i];
+    if (!isNumeric(di)) return;
+    ppadiplus = ppadplus(i) + extrappad;
+    ppadiminus = ppadminus(i) + extrappad;
+    vmin = di - vpadminus(i);
+    vmax = di + vpadplus(i);
+    // special case for log axes: if vpad makes this object span
+    // more than an order of mag, clip it to one order. This is so
+    // we don't have non-positive errors or absurdly large lower
+    // range due to rounding errors
+    if (ax.type === "log" && vmin < vmax / 10) {
+      vmin = vmax / 10;
+    }
+
+    dmin = ax.c2l(vmin);
+    dmax = ax.c2l(vmax);
+
+    if (tozero) {
+      dmin = Math.min(0, dmin);
+      dmax = Math.max(0, dmax);
+    }
+
+    // In order to stop overflow errors, don't consider points
+    // too close to the limits of js floating point
+    function goodNumber(v) {
+      return isNumeric(v) && Math.abs(v) < FP_SAFE;
+    }
+
+    if (goodNumber(dmin)) {
+      includeThis = true;
+      // take items v from ax._min and compare them to the
+      // presently active point:
+      // - if the item supercedes the new point, set includethis false
+      // - if the new pt supercedes the item, delete it from ax._min
+      for (j = 0; j < ax._min.length && includeThis; j++) {
+        v = ax._min[j];
+        if (v.val <= dmin && v.pad >= ppadiminus) {
+          includeThis = false;
+        } else if (v.val >= dmin && v.pad <= ppadiminus) {
+          ax._min.splice(j, 1);
+          j--;
+        }
+      }
+      if (includeThis) {
+        ax._min.push({ val: dmin, pad: tozero && dmin === 0 ? 0 : ppadiminus });
+      }
+    }
+
+    if (goodNumber(dmax)) {
+      includeThis = true;
+      for (j = 0; j < ax._max.length && includeThis; j++) {
+        v = ax._max[j];
+        if (v.val >= dmax && v.pad >= ppadiplus) {
+          includeThis = false;
+        } else if (v.val <= dmax && v.pad <= ppadiplus) {
+          ax._max.splice(j, 1);
+          j--;
+        }
+      }
+      if (includeThis) {
+        ax._max.push({ val: dmax, pad: tozero && dmax === 0 ? 0 : ppadiplus });
+      }
+    }
+  }
+
+  // For efficiency covering monotonic or near-monotonic data,
+  // check a few points at both ends first and then sweep
+  // through the middle
+  for (i = 0; i < 6; i++) {
+    addItem(i);
+  }
+  for (i = len - 1; i > 5; i--) {
+    addItem(i);
+  }
 };
 
 axes.autoBin = function(data, ax, nbins, is2d, calendar) {
-    var dataMin = Lib.aggNums(Math.min, null, data),
-        dataMax = Lib.aggNums(Math.max, null, data);
-
-    if(!calendar) calendar = ax.calendar;
-
-    if(ax.type === 'category') {
-        return {
-            start: dataMin - 0.5,
-            end: dataMax + 0.5,
-            size: 1
-        };
-    }
-
-    var size0;
-    if(nbins) size0 = ((dataMax - dataMin) / nbins);
-    else {
-        // totally auto: scale off std deviation so the highest bin is
-        // somewhat taller than the total number of bins, but don't let
-        // the size get smaller than the 'nice' rounded down minimum
-        // difference between values
-        var distinctData = Lib.distinctVals(data),
-            msexp = Math.pow(10, Math.floor(
-                Math.log(distinctData.minDiff) / Math.LN10)),
-            minSize = msexp * Lib.roundUp(
-                distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true);
-        size0 = Math.max(minSize, 2 * Lib.stdev(data) /
-            Math.pow(data.length, is2d ? 0.25 : 0.4));
-
-        // fallback if ax.d2c output BADNUMs
-        // e.g. when user try to plot categorical bins
-        // on a layout.xaxis.type: 'linear'
-        if(!isNumeric(size0)) size0 = 1;
-    }
-
-    // piggyback off autotick code to make "nice" bin sizes
-    var dummyAx;
-    if(ax.type === 'log') {
-        dummyAx = {
-            type: 'linear',
-            range: [dataMin, dataMax]
-        };
-    }
-    else {
-        dummyAx = {
-            type: ax.type,
-            range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar),
-            calendar: calendar
-        };
-    }
-    axes.setConvert(dummyAx);
-
-    axes.autoTicks(dummyAx, size0);
-    var binStart = axes.tickIncrement(
-            axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse', calendar),
-        binEnd;
-
-    // check for too many data points right at the edges of bins
-    // (>50% within 1% of bin edges) or all data points integral
-    // and offset the bins accordingly
-    if(typeof dummyAx.dtick === 'number') {
-        binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax);
-
-        var bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick);
-        binEnd = binStart + bincount * dummyAx.dtick;
-    }
-    else {
-        // month ticks - should be the only nonlinear kind we have at this point.
-        // dtick (as supplied by axes.autoTick) only has nonlinear values on
-        // date and log axes, but even if you display a histogram on a log axis
-        // we bin it on a linear axis (which one could argue against, but that's
-        // a separate issue)
-        if(dummyAx.dtick.charAt(0) === 'M') {
-            binStart = autoShiftMonthBins(binStart, data, dummyAx.dtick, dataMin, calendar);
-        }
-
-        // calculate the endpoint for nonlinear ticks - you have to
-        // just increment until you're done
-        binEnd = binStart;
-        while(binEnd <= dataMax) {
-            binEnd = axes.tickIncrement(binEnd, dummyAx.dtick, false, calendar);
-        }
-    }
+  var dataMin = Lib.aggNums(Math.min, null, data),
+    dataMax = Lib.aggNums(Math.max, null, data);
+
+  if (!calendar) calendar = ax.calendar;
+
+  if (ax.type === "category") {
+    return { start: dataMin - 0.5, end: dataMax + 0.5, size: 1 };
+  }
+
+  var size0;
+  if (nbins) {
+    size0 = (dataMax - dataMin) / nbins;
+  } else {
+    // totally auto: scale off std deviation so the highest bin is
+    // somewhat taller than the total number of bins, but don't let
+    // the size get smaller than the 'nice' rounded down minimum
+    // difference between values
+    var distinctData = Lib.distinctVals(data),
+      msexp = Math.pow(
+        10,
+        Math.floor(Math.log(distinctData.minDiff) / Math.LN10)
+      ),
+      minSize = msexp *
+        Lib.roundUp(distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true);
+    size0 = Math.max(
+      minSize,
+      2 * Lib.stdev(data) / Math.pow(data.length, is2d ? 0.25 : 0.4)
+    );
 
-    return {
-        start: ax.c2r(binStart, 0, calendar),
-        end: ax.c2r(binEnd, 0, calendar),
-        size: dummyAx.dtick
+    // fallback if ax.d2c output BADNUMs
+    // e.g. when user try to plot categorical bins
+    // on a layout.xaxis.type: 'linear'
+    if (!isNumeric(size0)) size0 = 1;
+  }
+
+  // piggyback off autotick code to make "nice" bin sizes
+  var dummyAx;
+  if (ax.type === "log") {
+    dummyAx = { type: "linear", range: [dataMin, dataMax] };
+  } else {
+    dummyAx = {
+      type: ax.type,
+      range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar),
+      calendar: calendar
     };
+  }
+  axes.setConvert(dummyAx);
+
+  axes.autoTicks(dummyAx, size0);
+  var binStart = axes.tickIncrement(
+    axes.tickFirst(dummyAx),
+    dummyAx.dtick,
+    "reverse",
+    calendar
+  ),
+    binEnd;
+
+  // check for too many data points right at the edges of bins
+  // (>50% within 1% of bin edges) or all data points integral
+  // and offset the bins accordingly
+  if (typeof dummyAx.dtick === "number") {
+    binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax);
+
+    var bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick);
+    binEnd = binStart + bincount * dummyAx.dtick;
+  } else {
+    // month ticks - should be the only nonlinear kind we have at this point.
+    // dtick (as supplied by axes.autoTick) only has nonlinear values on
+    // date and log axes, but even if you display a histogram on a log axis
+    // we bin it on a linear axis (which one could argue against, but that's
+    // a separate issue)
+    if (dummyAx.dtick.charAt(0) === "M") {
+      binStart = autoShiftMonthBins(
+        binStart,
+        data,
+        dummyAx.dtick,
+        dataMin,
+        calendar
+      );
+    }
+
+    // calculate the endpoint for nonlinear ticks - you have to
+    // just increment until you're done
+    binEnd = binStart;
+    while (binEnd <= dataMax) {
+      binEnd = axes.tickIncrement(binEnd, dummyAx.dtick, false, calendar);
+    }
+  }
+
+  return {
+    start: ax.c2r(binStart, 0, calendar),
+    end: ax.c2r(binEnd, 0, calendar),
+    size: dummyAx.dtick
+  };
 };
 
-
 function autoShiftNumericBins(binStart, data, ax, dataMin, dataMax) {
-    var edgecount = 0,
-        midcount = 0,
-        intcount = 0,
-        blankCount = 0;
-
-    function nearEdge(v) {
-        // is a value within 1% of a bin edge?
-        return (1 + (v - binStart) * 100 / ax.dtick) % 100 < 2;
-    }
-
-    for(var i = 0; i < data.length; i++) {
-        if(data[i] % 1 === 0) intcount++;
-        else if(!isNumeric(data[i])) blankCount++;
-
-        if(nearEdge(data[i])) edgecount++;
-        if(nearEdge(data[i] + ax.dtick / 2)) midcount++;
-    }
-    var dataCount = data.length - blankCount;
-
-    if(intcount === dataCount && ax.type !== 'date') {
-        // all integers: if bin size is <1, it's because
-        // that was specifically requested (large nbins)
-        // so respect that... but center the bins containing
-        // integers on those integers
-        if(ax.dtick < 1) {
-            binStart = dataMin - 0.5 * ax.dtick;
-        }
-        // otherwise start half an integer down regardless of
-        // the bin size, just enough to clear up endpoint
-        // ambiguity about which integers are in which bins.
-        else {
-            binStart -= 0.5;
-            if(binStart + ax.dtick < dataMin) binStart += ax.dtick;
-        }
-    }
-    else if(midcount < dataCount * 0.1) {
-        if(edgecount > dataCount * 0.3 ||
-                nearEdge(dataMin) || nearEdge(dataMax)) {
-            // lots of points at the edge, not many in the middle
-            // shift half a bin
-            var binshift = ax.dtick / 2;
-            binStart += (binStart + binshift < dataMin) ? binshift : -binshift;
-        }
-    }
-    return binStart;
+  var edgecount = 0, midcount = 0, intcount = 0, blankCount = 0;
+
+  function nearEdge(v) {
+    // is a value within 1% of a bin edge?
+    return (1 + (v - binStart) * 100 / ax.dtick) % 100 < 2;
+  }
+
+  for (var i = 0; i < data.length; i++) {
+    if (data[i] % 1 === 0) intcount++;
+    else if (!isNumeric(data[i])) blankCount++;
+
+    if (nearEdge(data[i])) edgecount++;
+    if (nearEdge(data[i] + ax.dtick / 2)) midcount++;
+  }
+  var dataCount = data.length - blankCount;
+
+  if (intcount === dataCount && ax.type !== "date") {
+    // all integers: if bin size is <1, it's because
+    // that was specifically requested (large nbins)
+    // so respect that... but center the bins containing
+    // integers on those integers
+    if (ax.dtick < 1) {
+      binStart = dataMin - 0.5 * ax.dtick;
+    } else {
+      // otherwise start half an integer down regardless of
+      // the bin size, just enough to clear up endpoint
+      // ambiguity about which integers are in which bins.
+      binStart -= 0.5;
+      if (binStart + ax.dtick < dataMin) binStart += ax.dtick;
+    }
+  } else if (midcount < dataCount * 0.1) {
+    if (edgecount > dataCount * 0.3 || nearEdge(dataMin) || nearEdge(dataMax)) {
+      // lots of points at the edge, not many in the middle
+      // shift half a bin
+      var binshift = ax.dtick / 2;
+      binStart += binStart + binshift < dataMin ? binshift : -binshift;
+    }
+  }
+  return binStart;
 }
 
-
 function autoShiftMonthBins(binStart, data, dtick, dataMin, calendar) {
-    var stats = Lib.findExactDates(data, calendar);
-    // number of data points that needs to be an exact value
-    // to shift that increment to (near) the bin center
-    var threshold = 0.8;
-
-    if(stats.exactDays > threshold) {
-        var numMonths = Number(dtick.substr(1));
-
-        if((stats.exactYears > threshold) && (numMonths % 12 === 0)) {
-            // The exact middle of a non-leap-year is 1.5 days into July
-            // so if we start the bins here, all but leap years will
-            // get hover-labeled as exact years.
-            binStart = axes.tickIncrement(binStart, 'M6', 'reverse') + ONEDAY * 1.5;
-        }
-        else if(stats.exactMonths > threshold) {
-            // Months are not as clean, but if we shift half the *longest*
-            // month (31/2 days) then 31-day months will get labeled exactly
-            // and shorter months will get labeled with the correct month
-            // but shifted 12-36 hours into it.
-            binStart = axes.tickIncrement(binStart, 'M1', 'reverse') + ONEDAY * 15.5;
-        }
-        else {
-            // Shifting half a day is exact, but since these are month bins it
-            // will always give a somewhat odd-looking label, until we do something
-            // smarter like showing the bin boundaries (or the bounds of the actual
-            // data in each bin)
-            binStart -= ONEDAY / 2;
-        }
-        var nextBinStart = axes.tickIncrement(binStart, dtick);
-
-        if(nextBinStart <= dataMin) return nextBinStart;
-    }
-    return binStart;
+  var stats = Lib.findExactDates(data, calendar);
+  // number of data points that needs to be an exact value
+  // to shift that increment to (near) the bin center
+  var threshold = 0.8;
+
+  if (stats.exactDays > threshold) {
+    var numMonths = Number(dtick.substr(1));
+
+    if (stats.exactYears > threshold && numMonths % 12 === 0) {
+      // The exact middle of a non-leap-year is 1.5 days into July
+      // so if we start the bins here, all but leap years will
+      // get hover-labeled as exact years.
+      binStart = axes.tickIncrement(binStart, "M6", "reverse") + ONEDAY * 1.5;
+    } else if (stats.exactMonths > threshold) {
+      // Months are not as clean, but if we shift half the *longest*
+      // month (31/2 days) then 31-day months will get labeled exactly
+      // and shorter months will get labeled with the correct month
+      // but shifted 12-36 hours into it.
+      binStart = axes.tickIncrement(binStart, "M1", "reverse") + ONEDAY * 15.5;
+    } else {
+      // Shifting half a day is exact, but since these are month bins it
+      // will always give a somewhat odd-looking label, until we do something
+      // smarter like showing the bin boundaries (or the bounds of the actual
+      // data in each bin)
+      binStart -= ONEDAY / 2;
+    }
+    var nextBinStart = axes.tickIncrement(binStart, dtick);
+
+    if (nextBinStart <= dataMin) return nextBinStart;
+  }
+  return binStart;
 }
 
 // ----------------------------------------------------
 // Ticks and grids
 // ----------------------------------------------------
-
 // calculate the ticks: text, values, positioning
 // if ticks are set to automatic, determine the right values (tick0,dtick)
 // in any case, set tickround to # of digits to round tick labels to,
 // or codes to this effect for log and date scales
 axes.calcTicks = function calcTicks(ax) {
-    var rng = Lib.simpleMap(ax.range, ax.r2l);
-
-    // calculate max number of (auto) ticks to display based on plot size
-    if(ax.tickmode === 'auto' || !ax.dtick) {
-        var nt = ax.nticks,
-            minPx;
-        if(!nt) {
-            if(ax.type === 'category') {
-                minPx = ax.tickfont ? (ax.tickfont.size || 12) * 1.2 : 15;
-                nt = ax._length / minPx;
-            }
-            else {
-                minPx = ax._id.charAt(0) === 'y' ? 40 : 80;
-                nt = Lib.constrain(ax._length / minPx, 4, 9) + 1;
-            }
-        }
-
-        // add a couple of extra digits for filling in ticks when we
-        // have explicit tickvals without tick text
-        if(ax.tickmode === 'array') nt *= 100;
-
-        axes.autoTicks(ax, Math.abs(rng[1] - rng[0]) / nt);
-        // check for a forced minimum dtick
-        if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) {
-            ax.dtick = ax._minDtick;
-            ax.tick0 = ax.l2r(ax._forceTick0);
-        }
-    }
-
-    // check for missing tick0
-    if(!ax.tick0) {
-        ax.tick0 = (ax.type === 'date') ? '2000-01-01' : 0;
-    }
-
-    // now figure out rounding of tick values
-    autoTickRound(ax);
-
-    // now that we've figured out the auto values for formatting
-    // in case we're missing some ticktext, we can break out for array ticks
-    if(ax.tickmode === 'array') return arrayTicks(ax);
-
-    // find the first tick
-    ax._tmin = axes.tickFirst(ax);
-
-    // check for reversed axis
-    var axrev = (rng[1] < rng[0]);
+  var rng = Lib.simpleMap(ax.range, ax.r2l);
+
+  // calculate max number of (auto) ticks to display based on plot size
+  if (ax.tickmode === "auto" || !ax.dtick) {
+    var nt = ax.nticks, minPx;
+    if (!nt) {
+      if (ax.type === "category") {
+        minPx = ax.tickfont ? (ax.tickfont.size || 12) * 1.2 : 15;
+        nt = ax._length / minPx;
+      } else {
+        minPx = ax._id.charAt(0) === "y" ? 40 : 80;
+        nt = Lib.constrain(ax._length / minPx, 4, 9) + 1;
+      }
+    }
+
+    // add a couple of extra digits for filling in ticks when we
+    // have explicit tickvals without tick text
+    if (ax.tickmode === "array") nt *= 100;
+
+    axes.autoTicks(ax, Math.abs(rng[1] - rng[0]) / nt);
+    // check for a forced minimum dtick
+    if (ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) {
+      ax.dtick = ax._minDtick;
+      ax.tick0 = ax.l2r(ax._forceTick0);
+    }
+  }
+
+  // check for missing tick0
+  if (!ax.tick0) {
+    ax.tick0 = ax.type === "date" ? "2000-01-01" : 0;
+  }
+
+  // now figure out rounding of tick values
+  autoTickRound(ax);
+
+  // now that we've figured out the auto values for formatting
+  // in case we're missing some ticktext, we can break out for array ticks
+  if (ax.tickmode === "array") return arrayTicks(ax);
+
+  // find the first tick
+  ax._tmin = axes.tickFirst(ax);
+
+  // check for reversed axis
+  var axrev = rng[1] < rng[0];
+
+  // return the full set of tick vals
+  var vals = [],
+    // add a tiny bit so we get ticks which may have rounded out
+    endtick = rng[1] * 1.0001 - rng[0] * 0.0001;
+  if (ax.type === "category") {
+    endtick = axrev
+      ? Math.max(-0.5, endtick)
+      : Math.min(ax._categories.length - 0.5, endtick);
+  }
+  for (
+    var x = ax._tmin;
+    axrev ? x >= endtick : x <= endtick;
+    x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar)
+  ) {
+    vals.push(x);
 
-    // return the full set of tick vals
-    var vals = [],
-        // add a tiny bit so we get ticks which may have rounded out
-        endtick = rng[1] * 1.0001 - rng[0] * 0.0001;
-    if(ax.type === 'category') {
-        endtick = (axrev) ? Math.max(-0.5, endtick) :
-            Math.min(ax._categories.length - 0.5, endtick);
-    }
-    for(var x = ax._tmin;
-            (axrev) ? (x >= endtick) : (x <= endtick);
-            x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar)) {
-        vals.push(x);
-
-        // prevent infinite loops
-        if(vals.length > 1000) break;
-    }
+    // prevent infinite loops
+    if (vals.length > 1000) break;
+  }
 
-    // save the last tick as well as first, so we can
-    // show the exponent only on the last one
-    ax._tmax = vals[vals.length - 1];
+  // save the last tick as well as first, so we can
+  // show the exponent only on the last one
+  ax._tmax = vals[vals.length - 1];
 
-    // for showing the rest of a date when the main tick label is only the
-    // latter part: ax._prevDateHead holds what we showed most recently.
-    // Start with it cleared and mark that we're in calcTicks (ie calculating a
-    // whole string of these so we should care what the previous date head was!)
-    ax._prevDateHead = '';
-    ax._inCalcTicks = true;
+  // for showing the rest of a date when the main tick label is only the
+  // latter part: ax._prevDateHead holds what we showed most recently.
+  // Start with it cleared and mark that we're in calcTicks (ie calculating a
+  // whole string of these so we should care what the previous date head was!)
+  ax._prevDateHead = "";
+  ax._inCalcTicks = true;
 
-    var ticksOut = new Array(vals.length);
-    for(var i = 0; i < vals.length; i++) ticksOut[i] = axes.tickText(ax, vals[i]);
+  var ticksOut = new Array(vals.length);
+  for (var i = 0; i < vals.length; i++) {
+    ticksOut[i] = axes.tickText(ax, vals[i]);
+  }
 
-    ax._inCalcTicks = false;
+  ax._inCalcTicks = false;
 
-    return ticksOut;
+  return ticksOut;
 };
 
 function arrayTicks(ax) {
-    var vals = ax.tickvals,
-        text = ax.ticktext,
-        ticksOut = new Array(vals.length),
-        rng = Lib.simpleMap(ax.range, ax.r2l),
-        r0expanded = rng[0] * 1.0001 - rng[1] * 0.0001,
-        r1expanded = rng[1] * 1.0001 - rng[0] * 0.0001,
-        tickMin = Math.min(r0expanded, r1expanded),
-        tickMax = Math.max(r0expanded, r1expanded),
-        vali,
-        i,
-        j = 0;
-
-    // without a text array, just format the given values as any other ticks
-    // except with more precision to the numbers
-    if(!Array.isArray(text)) text = [];
-
-    // make sure showing ticks doesn't accidentally add new categories
-    var tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l;
-
-    // array ticks on log axes always show the full number
-    // (if no explicit ticktext overrides it)
-    if(ax.type === 'log' && String(ax.dtick).charAt(0) !== 'L') {
-        ax.dtick = 'L' + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1);
-    }
-
-    for(i = 0; i < vals.length; i++) {
-        vali = tickVal2l(vals[i]);
-        if(vali > tickMin && vali < tickMax) {
-            if(text[i] === undefined) ticksOut[j] = axes.tickText(ax, vali);
-            else ticksOut[j] = tickTextObj(ax, vali, String(text[i]));
-            j++;
-        }
-    }
-
-    if(j < vals.length) ticksOut.splice(j, vals.length - j);
-
-    return ticksOut;
+  var vals = ax.tickvals,
+    text = ax.ticktext,
+    ticksOut = new Array(vals.length),
+    rng = Lib.simpleMap(ax.range, ax.r2l),
+    r0expanded = rng[0] * 1.0001 - rng[1] * 0.0001,
+    r1expanded = rng[1] * 1.0001 - rng[0] * 0.0001,
+    tickMin = Math.min(r0expanded, r1expanded),
+    tickMax = Math.max(r0expanded, r1expanded),
+    vali,
+    i,
+    j = 0;
+
+  // without a text array, just format the given values as any other ticks
+  // except with more precision to the numbers
+  if (!Array.isArray(text)) text = [];
+
+  // make sure showing ticks doesn't accidentally add new categories
+  var tickVal2l = ax.type === "category" ? ax.d2l_noadd : ax.d2l;
+
+  // array ticks on log axes always show the full number
+  // (if no explicit ticktext overrides it)
+  if (ax.type === "log" && String(ax.dtick).charAt(0) !== "L") {
+    ax.dtick = "L" +
+      Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1);
+  }
+
+  for (i = 0; i < vals.length; i++) {
+    vali = tickVal2l(vals[i]);
+    if (vali > tickMin && vali < tickMax) {
+      if (text[i] === undefined) ticksOut[j] = axes.tickText(ax, vali);
+      else ticksOut[j] = tickTextObj(ax, vali, String(text[i]));
+      j++;
+    }
+  }
+
+  if (j < vals.length) ticksOut.splice(j, vals.length - j);
+
+  return ticksOut;
 }
 
 var roundBase10 = [2, 5, 10],
-    roundBase24 = [1, 2, 3, 6, 12],
-    roundBase60 = [1, 2, 5, 10, 15, 30],
-    // 2&3 day ticks are weird, but need something btwn 1&7
-    roundDays = [1, 2, 3, 7, 14],
-    // approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2)
-    // these don't have to be exact, just close enough to round to the right value
-    roundLog1 = [-0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1],
-    roundLog2 = [-0.301, 0, 0.301, 0.699, 1];
+  roundBase24 = [1, 2, 3, 6, 12],
+  roundBase60 = [1, 2, 5, 10, 15, 30],
+  // 2&3 day ticks are weird, but need something btwn 1&7
+  roundDays = [1, 2, 3, 7, 14],
+  // approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2)
+  // these don't have to be exact, just close enough to round to the right value
+  roundLog1 = [
+    -0.046,
+    0,
+    0.301,
+    0.477,
+    0.602,
+    0.699,
+    0.778,
+    0.845,
+    0.903,
+    0.954,
+    1
+  ],
+  roundLog2 = [-0.301, 0, 0.301, 0.699, 1];
 
 function roundDTick(roughDTick, base, roundingSet) {
-    return base * Lib.roundUp(roughDTick / base, roundingSet);
+  return base * Lib.roundUp(roughDTick / base, roundingSet);
 }
 
 // autoTicks: calculate best guess at pleasant ticks for this axis
@@ -826,90 +831,78 @@ function roundDTick(roughDTick, base, roundingSet) {
 //      log showing powers plus some intermediates:
 //          D1 shows all digits, D2 shows 2 and 5
 axes.autoTicks = function(ax, roughDTick) {
-    var base;
-
-    if(ax.type === 'date') {
-        ax.tick0 = Lib.dateTick0(ax.calendar);
-        // the criteria below are all based on the rough spacing we calculate
-        // being > half of the final unit - so precalculate twice the rough val
-        var roughX2 = 2 * roughDTick;
-
-        if(roughX2 > ONEAVGYEAR) {
-            roughDTick /= ONEAVGYEAR;
-            base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
-            ax.dtick = 'M' + (12 * roundDTick(roughDTick, base, roundBase10));
-        }
-        else if(roughX2 > ONEAVGMONTH) {
-            roughDTick /= ONEAVGMONTH;
-            ax.dtick = 'M' + roundDTick(roughDTick, 1, roundBase24);
-        }
-        else if(roughX2 > ONEDAY) {
-            ax.dtick = roundDTick(roughDTick, ONEDAY, roundDays);
-            // get week ticks on sunday
-            // this will also move the base tick off 2000-01-01 if dtick is
-            // 2 or 3 days... but that's a weird enough case that we'll ignore it.
-            ax.tick0 = Lib.dateTick0(ax.calendar, true);
-        }
-        else if(roughX2 > ONEHOUR) {
-            ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24);
-        }
-        else if(roughX2 > ONEMIN) {
-            ax.dtick = roundDTick(roughDTick, ONEMIN, roundBase60);
-        }
-        else if(roughX2 > ONESEC) {
-            ax.dtick = roundDTick(roughDTick, ONESEC, roundBase60);
-        }
-        else {
-            // milliseconds
-            base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
-            ax.dtick = roundDTick(roughDTick, base, roundBase10);
-        }
-    }
-    else if(ax.type === 'log') {
-        ax.tick0 = 0;
-        var rng = Lib.simpleMap(ax.range, ax.r2l);
-
-        if(roughDTick > 0.7) {
-            // only show powers of 10
-            ax.dtick = Math.ceil(roughDTick);
-        }
-        else if(Math.abs(rng[1] - rng[0]) < 1) {
-            // span is less than one power of 10
-            var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick);
-
-            // ticks on a linear scale, labeled fully
-            roughDTick = Math.abs(Math.pow(10, rng[1]) -
-                Math.pow(10, rng[0])) / nt;
-            base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
-            ax.dtick = 'L' + roundDTick(roughDTick, base, roundBase10);
-        }
-        else {
-            // include intermediates between powers of 10,
-            // labeled with small digits
-            // ax.dtick = "D2" (show 2 and 5) or "D1" (show all digits)
-            ax.dtick = (roughDTick > 0.3) ? 'D2' : 'D1';
-        }
-    }
-    else if(ax.type === 'category') {
-        ax.tick0 = 0;
-        ax.dtick = Math.ceil(Math.max(roughDTick, 1));
-    }
-    else {
-        // auto ticks always start at 0
-        ax.tick0 = 0;
-        base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
-        ax.dtick = roundDTick(roughDTick, base, roundBase10);
-    }
-
-    // prevent infinite loops
-    if(ax.dtick === 0) ax.dtick = 1;
+  var base;
+
+  if (ax.type === "date") {
+    ax.tick0 = Lib.dateTick0(ax.calendar);
+    // the criteria below are all based on the rough spacing we calculate
+    // being > half of the final unit - so precalculate twice the rough val
+    var roughX2 = 2 * roughDTick;
+
+    if (roughX2 > ONEAVGYEAR) {
+      roughDTick /= ONEAVGYEAR;
+      base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
+      ax.dtick = "M" + 12 * roundDTick(roughDTick, base, roundBase10);
+    } else if (roughX2 > ONEAVGMONTH) {
+      roughDTick /= ONEAVGMONTH;
+      ax.dtick = "M" + roundDTick(roughDTick, 1, roundBase24);
+    } else if (roughX2 > ONEDAY) {
+      ax.dtick = roundDTick(roughDTick, ONEDAY, roundDays);
+      // get week ticks on sunday
+      // this will also move the base tick off 2000-01-01 if dtick is
+      // 2 or 3 days... but that's a weird enough case that we'll ignore it.
+      ax.tick0 = Lib.dateTick0(ax.calendar, true);
+    } else if (roughX2 > ONEHOUR) {
+      ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24);
+    } else if (roughX2 > ONEMIN) {
+      ax.dtick = roundDTick(roughDTick, ONEMIN, roundBase60);
+    } else if (roughX2 > ONESEC) {
+      ax.dtick = roundDTick(roughDTick, ONESEC, roundBase60);
+    } else {
+      // milliseconds
+      base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
+      ax.dtick = roundDTick(roughDTick, base, roundBase10);
+    }
+  } else if (ax.type === "log") {
+    ax.tick0 = 0;
+    var rng = Lib.simpleMap(ax.range, ax.r2l);
 
-    // TODO: this is from log axis histograms with autorange off
-    if(!isNumeric(ax.dtick) && typeof ax.dtick !== 'string') {
-        var olddtick = ax.dtick;
-        ax.dtick = 1;
-        throw 'ax.dtick error: ' + String(olddtick);
-    }
+    if (roughDTick > 0.7) {
+      // only show powers of 10
+      ax.dtick = Math.ceil(roughDTick);
+    } else if (Math.abs(rng[1] - rng[0]) < 1) {
+      // span is less than one power of 10
+      var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick);
+
+      // ticks on a linear scale, labeled fully
+      roughDTick = Math.abs(Math.pow(10, rng[1]) - Math.pow(10, rng[0])) / nt;
+      base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
+      ax.dtick = "L" + roundDTick(roughDTick, base, roundBase10);
+    } else {
+      // include intermediates between powers of 10,
+      // labeled with small digits
+      // ax.dtick = "D2" (show 2 and 5) or "D1" (show all digits)
+      ax.dtick = roughDTick > 0.3 ? "D2" : "D1";
+    }
+  } else if (ax.type === "category") {
+    ax.tick0 = 0;
+    ax.dtick = Math.ceil(Math.max(roughDTick, 1));
+  } else {
+    // auto ticks always start at 0
+    ax.tick0 = 0;
+    base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
+    ax.dtick = roundDTick(roughDTick, base, roundBase10);
+  }
+
+  // prevent infinite loops
+  if (ax.dtick === 0) ax.dtick = 1;
+
+  // TODO: this is from log axis histograms with autorange off
+  if (!isNumeric(ax.dtick) && typeof ax.dtick !== "string") {
+    var olddtick = ax.dtick;
+    ax.dtick = 1;
+    throw "ax.dtick error: " + String(olddtick);
+  }
 };
 
 // after dtick is already known, find tickround = precision
@@ -918,61 +911,67 @@ axes.autoTicks = function(ax, roughDTick) {
 //   for date ticks, the last date part to show (y,m,d,H,M,S)
 //      or an integer # digits past seconds
 function autoTickRound(ax) {
-    var dtick = ax.dtick;
-
-    ax._tickexponent = 0;
-    if(!isNumeric(dtick) && typeof dtick !== 'string') {
-        dtick = 1;
-    }
-
-    if(ax.type === 'category') {
-        ax._tickround = null;
-    }
-    if(ax.type === 'date') {
-        // If tick0 is unusual, give tickround a bit more information
-        // not necessarily *all* the information in tick0 though, if it's really odd
-        // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19
-        // take off a leading minus (year < 0) and i (intercalary month) so length is consistent
-        var tick0ms = ax.r2l(ax.tick0),
-            tick0str = ax.l2r(tick0ms).replace(/(^-|i)/g, ''),
-            tick0len = tick0str.length;
-
-        if(String(dtick).charAt(0) === 'M') {
-            // any tick0 more specific than a year: alway show the full date
-            if(tick0len > 10 || tick0str.substr(5) !== '01-01') ax._tickround = 'd';
-            // show the month unless ticks are full multiples of a year
-            else ax._tickround = (+(dtick.substr(1)) % 12 === 0) ? 'y' : 'm';
-        }
-        else if((dtick >= ONEDAY && tick0len <= 10) || (dtick >= ONEDAY * 15)) ax._tickround = 'd';
-        else if((dtick >= ONEMIN && tick0len <= 16) || (dtick >= ONEHOUR)) ax._tickround = 'M';
-        else if((dtick >= ONESEC && tick0len <= 19) || (dtick >= ONEMIN)) ax._tickround = 'S';
-        else {
-            // tickround is a number of digits of fractional seconds
-            // of any two adjacent ticks, at least one will have the maximum fractional digits
-            // of all possible ticks - so take the max. length of tick0 and the next one
-            var tick1len = ax.l2r(tick0ms + dtick).replace(/^-/, '').length;
-            ax._tickround = Math.max(tick0len, tick1len) - 20;
-        }
-    }
-    else if(isNumeric(dtick) || dtick.charAt(0) === 'L') {
-        // linear or log (except D1, D2)
-        var rng = ax.range.map(ax.r2d || Number);
-        if(!isNumeric(dtick)) dtick = Number(dtick.substr(1));
-        // 2 digits past largest digit of dtick
-        ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01);
-
-        var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1]));
-
-        var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01);
-        if(Math.abs(rangeexp) > 3) {
-            if(ax.exponentformat === 'SI' || ax.exponentformat === 'B') {
-                ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3);
-            }
-            else ax._tickexponent = rangeexp;
-        }
-    }
+  var dtick = ax.dtick;
+
+  ax._tickexponent = 0;
+  if (!isNumeric(dtick) && typeof dtick !== "string") {
+    dtick = 1;
+  }
+
+  if (ax.type === "category") {
+    ax._tickround = null;
+  }
+  if (ax.type === "date") {
+    // If tick0 is unusual, give tickround a bit more information
+    // not necessarily *all* the information in tick0 though, if it's really odd
+    // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19
+    // take off a leading minus (year < 0) and i (intercalary month) so length is consistent
+    var tick0ms = ax.r2l(ax.tick0),
+      tick0str = ax.l2r(tick0ms).replace(/(^-|i)/g, ""),
+      tick0len = tick0str.length;
+
+    if (String(dtick).charAt(0) === "M") {
+      // any tick0 more specific than a year: alway show the full date
+      if (tick0len > 10 || tick0str.substr(5) !== "01-01") {
+        ax._tickround = "d";
+      } else {
+        // show the month unless ticks are full multiples of a year
+        ax._tickround = (+dtick.substr(1)) % 12 === 0 ? "y" : "m";
+      }
+    } else if (dtick >= ONEDAY && tick0len <= 10 || dtick >= ONEDAY * 15) {
+      ax._tickround = "d";
+    } else if (dtick >= ONEMIN && tick0len <= 16 || dtick >= ONEHOUR) {
+      ax._tickround = "M";
+    } else if (dtick >= ONESEC && tick0len <= 19 || dtick >= ONEMIN) {
+      ax._tickround = "S";
+    } else {
+      // tickround is a number of digits of fractional seconds
+      // of any two adjacent ticks, at least one will have the maximum fractional digits
+      // of all possible ticks - so take the max. length of tick0 and the next one
+      var tick1len = ax.l2r(tick0ms + dtick).replace(/^-/, "").length;
+      ax._tickround = Math.max(tick0len, tick1len) - 20;
+    }
+  } else if (isNumeric(dtick) || dtick.charAt(0) === "L") {
+    // linear or log (except D1, D2)
+    var rng = ax.range.map(ax.r2d || Number);
+    if (!isNumeric(dtick)) dtick = Number(dtick.substr(1));
+    // 2 digits past largest digit of dtick
+    ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01);
+
+    var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1]));
+
+    var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01);
+    if (Math.abs(rangeexp) > 3) {
+      if (ax.exponentformat === "SI" || ax.exponentformat === "B") {
+        ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3);
+      } else {
+        ax._tickexponent = rangeexp;
+      }
+    }
+  } else {
     // D1 or D2 (log)
-    else ax._tickround = null;
+    ax._tickround = null;
+  }
 }
 
 // months and years don't have constant millisecond values
@@ -982,98 +981,99 @@ function autoTickRound(ax) {
 // numeric ticks always have constant differences, other datetime ticks
 // can all be calculated as constant number of milliseconds
 axes.tickIncrement = function(x, dtick, axrev, calendar) {
-    var axSign = axrev ? -1 : 1;
-
-    // includes linear, all dates smaller than month, and pure 10^n in log
-    if(isNumeric(dtick)) return x + axSign * dtick;
-
-    // everything else is a string, one character plus a number
-    var tType = dtick.charAt(0),
-        dtSigned = axSign * Number(dtick.substr(1));
-
-    // Dates: months (or years - see Lib.incrementMonth)
-    if(tType === 'M') return Lib.incrementMonth(x, dtSigned, calendar);
-
-    // Log scales: Linear, Digits
-    else if(tType === 'L') return Math.log(Math.pow(10, x) + dtSigned) / Math.LN10;
-
+  var axSign = axrev ? -1 : 1;
+
+  // includes linear, all dates smaller than month, and pure 10^n in log
+  if (isNumeric(dtick)) return x + axSign * dtick;
+
+  // everything else is a string, one character plus a number
+  var tType = dtick.charAt(0), dtSigned = axSign * Number(dtick.substr(1));
+
+  // Dates: months (or years - see Lib.incrementMonth)
+  if (tType === "M") {
+    return Lib.incrementMonth(x, dtSigned, calendar);
+  } else if (
+    tType === "L" // Log scales: Linear, Digits
+  ) {
+    return Math.log(Math.pow(10, x) + dtSigned) / Math.LN10;
+  } else if (tType === "D") {
     // log10 of 2,5,10, or all digits (logs just have to be
     // close enough to round)
-    else if(tType === 'D') {
-        var tickset = (dtick === 'D2') ? roundLog2 : roundLog1,
-            x2 = x + axSign * 0.01,
-            frac = Lib.roundUp(Lib.mod(x2, 1), tickset, axrev);
-
-        return Math.floor(x2) +
-            Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10;
-    }
-    else throw 'unrecognized dtick ' + String(dtick);
+    var tickset = dtick === "D2" ? roundLog2 : roundLog1,
+      x2 = x + axSign * 0.01,
+      frac = Lib.roundUp(Lib.mod(x2, 1), tickset, axrev);
+
+    return Math.floor(x2) +
+      Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10;
+  } else {
+    throw "unrecognized dtick " + String(dtick);
+  }
 };
 
 // calculate the first tick on an axis
 axes.tickFirst = function(ax) {
-    var r2l = ax.r2l || Number,
-        rng = Lib.simpleMap(ax.range, r2l),
-        axrev = rng[1] < rng[0],
-        sRound = axrev ? Math.floor : Math.ceil,
-        // add a tiny extra bit to make sure we get ticks
-        // that may have been rounded out
-        r0 = rng[0] * 1.0001 - rng[1] * 0.0001,
-        dtick = ax.dtick,
-        tick0 = r2l(ax.tick0);
-
-    if(isNumeric(dtick)) {
-        var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0;
-
-        // make sure no ticks outside the category list
-        if(ax.type === 'category') {
-            tmin = Lib.constrain(tmin, 0, ax._categories.length - 1);
-        }
-        return tmin;
-    }
-
-    var tType = dtick.charAt(0),
-        dtNum = Number(dtick.substr(1));
-
-    // Dates: months (or years)
-    if(tType === 'M') {
-        var cnt = 0,
-            t0 = tick0,
-            t1,
-            mult,
-            newDTick;
-
-        // This algorithm should work for *any* nonlinear (but close to linear!)
-        // tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3.
-        while(cnt < 10) {
-            t1 = axes.tickIncrement(t0, dtick, axrev, ax.calendar);
-            if((t1 - r0) * (t0 - r0) <= 0) {
-                // t1 and t0 are on opposite sides of r0! we've succeeded!
-                if(axrev) return Math.min(t0, t1);
-                return Math.max(t0, t1);
-            }
-            mult = (r0 - ((t0 + t1) / 2)) / (t1 - t0);
-            newDTick = tType + ((Math.abs(Math.round(mult)) || 1) * dtNum);
-            t0 = axes.tickIncrement(t0, newDTick, mult < 0 ? !axrev : axrev, ax.calendar);
-            cnt++;
-        }
-        Lib.error('tickFirst did not converge', ax);
-        return t0;
-    }
-
+  var r2l = ax.r2l || Number,
+    rng = Lib.simpleMap(ax.range, r2l),
+    axrev = rng[1] < rng[0],
+    sRound = axrev ? Math.floor : Math.ceil,
+    // add a tiny extra bit to make sure we get ticks
+    // that may have been rounded out
+    r0 = rng[0] * 1.0001 - rng[1] * 0.0001,
+    dtick = ax.dtick,
+    tick0 = r2l(ax.tick0);
+
+  if (isNumeric(dtick)) {
+    var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0;
+
+    // make sure no ticks outside the category list
+    if (ax.type === "category") {
+      tmin = Lib.constrain(tmin, 0, ax._categories.length - 1);
+    }
+    return tmin;
+  }
+
+  var tType = dtick.charAt(0), dtNum = Number(dtick.substr(1));
+
+  // Dates: months (or years)
+  if (tType === "M") {
+    var cnt = 0, t0 = tick0, t1, mult, newDTick;
+
+    // This algorithm should work for *any* nonlinear (but close to linear!)
+    // tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3.
+    while (cnt < 10) {
+      t1 = axes.tickIncrement(t0, dtick, axrev, ax.calendar);
+      if ((t1 - r0) * (t0 - r0) <= 0) {
+        // t1 and t0 are on opposite sides of r0! we've succeeded!
+        if (axrev) return Math.min(t0, t1);
+        return Math.max(t0, t1);
+      }
+      mult = (r0 - (t0 + t1) / 2) / (t1 - t0);
+      newDTick = tType + (Math.abs(Math.round(mult)) || 1) * dtNum;
+      t0 = axes.tickIncrement(
+        t0,
+        newDTick,
+        mult < 0 ? !axrev : axrev,
+        ax.calendar
+      );
+      cnt++;
+    }
+    Lib.error("tickFirst did not converge", ax);
+    return t0;
+  } else if (tType === "L") {
     // Log scales: Linear, Digits
-    else if(tType === 'L') {
-        return Math.log(sRound(
-            (Math.pow(10, r0) - tick0) / dtNum) * dtNum + tick0) / Math.LN10;
-    }
-    else if(tType === 'D') {
-        var tickset = (dtick === 'D2') ? roundLog2 : roundLog1,
-            frac = Lib.roundUp(Lib.mod(r0, 1), tickset, axrev);
-
-        return Math.floor(r0) +
-            Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10;
-    }
-    else throw 'unrecognized dtick ' + String(dtick);
+    return Math.log(
+      sRound((Math.pow(10, r0) - tick0) / dtNum) * dtNum + tick0
+    ) /
+      Math.LN10;
+  } else if (tType === "D") {
+    var tickset = dtick === "D2" ? roundLog2 : roundLog1,
+      frac = Lib.roundUp(Lib.mod(r0, 1), tickset, axrev);
+
+    return Math.floor(r0) +
+      Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10;
+  } else {
+    throw "unrecognized dtick " + String(dtick);
+  }
 };
 
 // draw the text for one tick.
@@ -1083,301 +1083,305 @@ axes.tickFirst = function(ax) {
 // hover is a (truthy) flag for whether to show numbers with a bit
 // more precision for hovertext
 axes.tickText = function(ax, x, hover) {
-    var out = tickTextObj(ax, x),
-        hideexp,
-        arrayMode = ax.tickmode === 'array',
-        extraPrecision = hover || arrayMode,
-        i,
-        tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l;
-
-    if(arrayMode && Array.isArray(ax.ticktext)) {
-        var rng = Lib.simpleMap(ax.range, ax.r2l),
-            minDiff = Math.abs(rng[1] - rng[0]) / 10000;
-        for(i = 0; i < ax.ticktext.length; i++) {
-            if(Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break;
-        }
-        if(i < ax.ticktext.length) {
-            out.text = String(ax.ticktext[i]);
-            return out;
-        }
-    }
-
-    function isHidden(showAttr) {
-        var first_or_last;
-
-        if(showAttr === undefined) return true;
-        if(hover) return showAttr === 'none';
-
-        first_or_last = {
-            first: ax._tmin,
-            last: ax._tmax
-        }[showAttr];
-
-        return showAttr !== 'all' && x !== first_or_last;
-    }
-
-    hideexp = ax.exponentformat !== 'none' && isHidden(ax.showexponent) ? 'hide' : '';
-
-    if(ax.type === 'date') formatDate(ax, out, hover, extraPrecision);
-    else if(ax.type === 'log') formatLog(ax, out, hover, extraPrecision, hideexp);
-    else if(ax.type === 'category') formatCategory(ax, out);
-    else formatLinear(ax, out, hover, extraPrecision, hideexp);
-
-    // add prefix and suffix
-    if(ax.tickprefix && !isHidden(ax.showtickprefix)) out.text = ax.tickprefix + out.text;
-    if(ax.ticksuffix && !isHidden(ax.showticksuffix)) out.text += ax.ticksuffix;
-
-    return out;
+  var out = tickTextObj(ax, x),
+    hideexp,
+    arrayMode = ax.tickmode === "array",
+    extraPrecision = hover || arrayMode,
+    i,
+    tickVal2l = ax.type === "category" ? ax.d2l_noadd : ax.d2l;
+
+  if (arrayMode && Array.isArray(ax.ticktext)) {
+    var rng = Lib.simpleMap(ax.range, ax.r2l),
+      minDiff = Math.abs(rng[1] - rng[0]) / 10000;
+    for (i = 0; i < ax.ticktext.length; i++) {
+      if (Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break;
+    }
+    if (i < ax.ticktext.length) {
+      out.text = String(ax.ticktext[i]);
+      return out;
+    }
+  }
+
+  function isHidden(showAttr) {
+    var first_or_last;
+
+    if (showAttr === undefined) return true;
+    if (hover) return showAttr === "none";
+
+    first_or_last = ({ first: ax._tmin, last: ax._tmax })[showAttr];
+
+    return showAttr !== "all" && x !== first_or_last;
+  }
+
+  hideexp = ax.exponentformat !== "none" && isHidden(ax.showexponent)
+    ? "hide"
+    : "";
+
+  if (ax.type === "date") {
+    formatDate(ax, out, hover, extraPrecision);
+  } else if (ax.type === "log") {
+    formatLog(ax, out, hover, extraPrecision, hideexp);
+  } else if (ax.type === "category") formatCategory(ax, out);
+  else formatLinear(ax, out, hover, extraPrecision, hideexp);
+
+  // add prefix and suffix
+  if (ax.tickprefix && !isHidden(ax.showtickprefix)) {
+    out.text = ax.tickprefix + out.text;
+  }
+  if (ax.ticksuffix && !isHidden(ax.showticksuffix)) out.text += ax.ticksuffix;
+
+  return out;
 };
 
 function tickTextObj(ax, x, text) {
-    var tf = ax.tickfont || ax._gd._fullLayout.font;
-
-    return {
-        x: x,
-        dx: 0,
-        dy: 0,
-        text: text || '',
-        fontSize: tf.size,
-        font: tf.family,
-        fontColor: tf.color
-    };
+  var tf = ax.tickfont || ax._gd._fullLayout.font;
+
+  return {
+    x: x,
+    dx: 0,
+    dy: 0,
+    text: text || "",
+    fontSize: tf.size,
+    font: tf.family,
+    fontColor: tf.color
+  };
 }
 
 function formatDate(ax, out, hover, extraPrecision) {
-    var tr = ax._tickround,
-        fmt = (hover && ax.hoverformat) || ax.tickformat;
-
-    if(extraPrecision) {
-        // second or sub-second precision: extra always shows max digits.
-        // for other fields, extra precision just adds one field.
-        if(isNumeric(tr)) tr = 4;
-        else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr];
-    }
-
-    var dateStr = Lib.formatDate(out.x, fmt, tr, ax.calendar),
-        headStr;
-
-    var splitIndex = dateStr.indexOf('\n');
-    if(splitIndex !== -1) {
-        headStr = dateStr.substr(splitIndex + 1);
-        dateStr = dateStr.substr(0, splitIndex);
-    }
-
-    if(extraPrecision) {
-        // if extraPrecision led to trailing zeros, strip them off
-        // actually, this can lead to removing even more zeros than
-        // in the original rounding, but that's fine because in these
-        // contexts uniformity is not so important (if there's even
-        // anything to be uniform with!)
-
-        // can we remove the whole time part?
-        if(dateStr === '00:00:00' || dateStr === '00:00') {
-            dateStr = headStr;
-            headStr = '';
-        }
-        else if(dateStr.length === 8) {
-            // strip off seconds if they're zero (zero fractional seconds
-            // are already omitted)
-            // but we never remove minutes and leave just hours
-            dateStr = dateStr.replace(/:00$/, '');
-        }
-    }
-
-    if(headStr) {
-        if(hover) {
-            // hover puts it all on one line, so headPart works best up front
-            // except for year headPart: turn this into "Jan 1, 2000" etc.
-            if(tr === 'd') dateStr += ', ' + headStr;
-            else dateStr = headStr + (dateStr ? ', ' + dateStr : '');
-        }
-        else if(!ax._inCalcTicks || (headStr !== ax._prevDateHead)) {
-            dateStr += '
' + headStr;
-            ax._prevDateHead = headStr;
-        }
-    }
-
-    out.text = dateStr;
+  var tr = ax._tickround, fmt = hover && ax.hoverformat || ax.tickformat;
+
+  if (extraPrecision) {
+    // second or sub-second precision: extra always shows max digits.
+    // for other fields, extra precision just adds one field.
+    if (isNumeric(tr)) tr = 4;
+    else tr = ({ y: "m", m: "d", d: "M", M: "S", S: 4 })[tr];
+  }
+
+  var dateStr = Lib.formatDate(out.x, fmt, tr, ax.calendar), headStr;
+
+  var splitIndex = dateStr.indexOf("\n");
+  if (splitIndex !== -1) {
+    headStr = dateStr.substr(splitIndex + 1);
+    dateStr = dateStr.substr(0, splitIndex);
+  }
+
+  if (extraPrecision) {
+    // if extraPrecision led to trailing zeros, strip them off
+    // actually, this can lead to removing even more zeros than
+    // in the original rounding, but that's fine because in these
+    // contexts uniformity is not so important (if there's even
+    // anything to be uniform with!)
+    // can we remove the whole time part?
+    if (dateStr === "00:00:00" || dateStr === "00:00") {
+      dateStr = headStr;
+      headStr = "";
+    } else if (dateStr.length === 8) {
+      // strip off seconds if they're zero (zero fractional seconds
+      // are already omitted)
+      // but we never remove minutes and leave just hours
+      dateStr = dateStr.replace(/:00$/, "");
+    }
+  }
+
+  if (headStr) {
+    if (hover) {
+      // hover puts it all on one line, so headPart works best up front
+      // except for year headPart: turn this into "Jan 1, 2000" etc.
+      if (tr === "d") dateStr += ", " + headStr;
+      else dateStr = headStr + (dateStr ? ", " + dateStr : "");
+    } else if (!ax._inCalcTicks || headStr !== ax._prevDateHead) {
+      dateStr += "
" + headStr;
+      ax._prevDateHead = headStr;
+    }
+  }
+
+  out.text = dateStr;
 }
 
 function formatLog(ax, out, hover, extraPrecision, hideexp) {
-    var dtick = ax.dtick,
-        x = out.x;
-    if(extraPrecision && ((typeof dtick !== 'string') || dtick.charAt(0) !== 'L')) dtick = 'L3';
-
-    if(ax.tickformat || (typeof dtick === 'string' && dtick.charAt(0) === 'L')) {
-        out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision);
-    }
-    else if(isNumeric(dtick) || ((dtick.charAt(0) === 'D') && (Lib.mod(x + 0.01, 1) < 0.1))) {
-        if(['e', 'E', 'power'].indexOf(ax.exponentformat) !== -1) {
-            var p = Math.round(x);
-            if(p === 0) out.text = 1;
-            else if(p === 1) out.text = '10';
-            else if(p > 1) out.text = '10' + p + '';
-            else out.text = '10\u2212' + -p + '';
-
-            out.fontSize *= 1.25;
-        }
-        else {
-            out.text = numFormat(Math.pow(10, x), ax, '', 'fakehover');
-            if(dtick === 'D1' && ax._id.charAt(0) === 'y') {
-                out.dy -= out.fontSize / 6;
-            }
-        }
-    }
-    else if(dtick.charAt(0) === 'D') {
-        out.text = String(Math.round(Math.pow(10, Lib.mod(x, 1))));
-        out.fontSize *= 0.75;
-    }
-    else throw 'unrecognized dtick ' + String(dtick);
-
-    // if 9's are printed on log scale, move the 10's away a bit
-    if(ax.dtick === 'D1') {
-        var firstChar = String(out.text).charAt(0);
-        if(firstChar === '0' || firstChar === '1') {
-            if(ax._id.charAt(0) === 'y') {
-                out.dx -= out.fontSize / 4;
-            }
-            else {
-                out.dy += out.fontSize / 2;
-                out.dx += (ax.range[1] > ax.range[0] ? 1 : -1) *
-                    out.fontSize * (x < 0 ? 0.5 : 0.25);
-            }
-        }
-    }
+  var dtick = ax.dtick, x = out.x;
+  if (
+    extraPrecision && (typeof dtick !== "string" || dtick.charAt(0) !== "L")
+  ) {
+    dtick = "L3";
+  }
+
+  if (ax.tickformat || typeof dtick === "string" && dtick.charAt(0) === "L") {
+    out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision);
+  } else if (
+    isNumeric(dtick) || dtick.charAt(0) === "D" && Lib.mod(x + 0.01, 1) < 0.1
+  ) {
+    if (["e", "E", "power"].indexOf(ax.exponentformat) !== -1) {
+      var p = Math.round(x);
+      if (p === 0) out.text = 1;
+      else if (p === 1) out.text = "10";
+      else if (p > 1) out.text = "10" + p + "";
+      else out.text = "10\u2212" + (-p) + "";
+
+      out.fontSize *= 1.25;
+    } else {
+      out.text = numFormat(Math.pow(10, x), ax, "", "fakehover");
+      if (dtick === "D1" && ax._id.charAt(0) === "y") {
+        out.dy -= out.fontSize / 6;
+      }
+    }
+  } else if (dtick.charAt(0) === "D") {
+    out.text = String(Math.round(Math.pow(10, Lib.mod(x, 1))));
+    out.fontSize *= 0.75;
+  } else {
+    throw "unrecognized dtick " + String(dtick);
+  }
+
+  // if 9's are printed on log scale, move the 10's away a bit
+  if (ax.dtick === "D1") {
+    var firstChar = String(out.text).charAt(0);
+    if (firstChar === "0" || firstChar === "1") {
+      if (ax._id.charAt(0) === "y") {
+        out.dx -= out.fontSize / 4;
+      } else {
+        out.dy += out.fontSize / 2;
+        out.dx += (ax.range[1] > ax.range[0] ? 1 : -1) *
+          out.fontSize *
+          (x < 0 ? 0.5 : 0.25);
+      }
+    }
+  }
 }
 
 function formatCategory(ax, out) {
-    var tt = ax._categories[Math.round(out.x)];
-    if(tt === undefined) tt = '';
-    out.text = String(tt);
+  var tt = ax._categories[Math.round(out.x)];
+  if (tt === undefined) tt = "";
+  out.text = String(tt);
 }
 
 function formatLinear(ax, out, hover, extraPrecision, hideexp) {
-    // don't add an exponent to zero if we're showing all exponents
-    // so the only reason you'd show an exponent on zero is if it's the
-    // ONLY tick to get an exponent (first or last)
-    if(ax.showexponent === 'all' && Math.abs(out.x / ax.dtick) < 1e-6) {
-        hideexp = 'hide';
-    }
-    out.text = numFormat(out.x, ax, hideexp, extraPrecision);
+  // don't add an exponent to zero if we're showing all exponents
+  // so the only reason you'd show an exponent on zero is if it's the
+  // ONLY tick to get an exponent (first or last)
+  if (ax.showexponent === "all" && Math.abs(out.x / ax.dtick) < 1e-6) {
+    hideexp = "hide";
+  }
+  out.text = numFormat(out.x, ax, hideexp, extraPrecision);
 }
 
 // format a number (tick value) according to the axis settings
 // new, more reliable procedure than d3.round or similar:
 // add half the rounding increment, then stringify and truncate
 // also automatically switch to sci. notation
-var SIPREFIXES = ['f', 'p', 'n', 'μ', 'm', '', 'k', 'M', 'G', 'T'];
+var SIPREFIXES = ["f", "p", "n", "\u03BC", "m", "", "k", "M", "G", "T"];
 
 function numFormat(v, ax, fmtoverride, hover) {
-        // negative?
-    var isNeg = v < 0,
-        // max number of digits past decimal point to show
-        tickRound = ax._tickround,
-        exponentFormat = fmtoverride || ax.exponentformat || 'B',
-        exponent = ax._tickexponent,
-        tickformat = ax.tickformat,
-        separatethousands = ax.separatethousands;
-
-    // special case for hover: set exponent just for this value, and
-    // add a couple more digits of precision over tick labels
-    if(hover) {
-        // make a dummy axis obj to get the auto rounding and exponent
-        var ah = {
-            exponentformat: ax.exponentformat,
-            dtick: ax.showexponent === 'none' ? ax.dtick :
-                (isNumeric(v) ? Math.abs(v) || 1 : 1),
-            // if not showing any exponents, don't change the exponent
-            // from what we calculate
-            range: ax.showexponent === 'none' ? ax.range.map(ax.r2d) : [0, v || 1]
-        };
-        autoTickRound(ah);
-        tickRound = (Number(ah._tickround) || 0) + 4;
-        exponent = ah._tickexponent;
-        if(ax.hoverformat) tickformat = ax.hoverformat;
-    }
-
-    if(tickformat) return d3.format(tickformat)(v).replace(/-/g, '\u2212');
-
-    // 'epsilon' - rounding increment
-    var e = Math.pow(10, -tickRound) / 2;
-
-    // exponentFormat codes:
-    // 'e' (1.2e+6, default)
-    // 'E' (1.2E+6)
-    // 'SI' (1.2M)
-    // 'B' (same as SI except 10^9=B not G)
-    // 'none' (1200000)
-    // 'power' (1.2x10^6)
-    // 'hide' (1.2, use 3rd argument=='hide' to eg
-    //      only show exponent on last tick)
-    if(exponentFormat === 'none') exponent = 0;
-
-    // take the sign out, put it back manually at the end
-    // - makes cases easier
-    v = Math.abs(v);
-    if(v < e) {
-        // 0 is just 0, but may get exponent if it's the last tick
-        v = '0';
-        isNeg = false;
-    }
-    else {
-        v += e;
-        // take out a common exponent, if any
-        if(exponent) {
-            v *= Math.pow(10, -exponent);
-            tickRound += exponent;
-        }
-        // round the mantissa
-        if(tickRound === 0) v = String(Math.floor(v));
-        else if(tickRound < 0) {
-            v = String(Math.round(v));
-            v = v.substr(0, v.length + tickRound);
-            for(var i = tickRound; i < 0; i++) v += '0';
-        }
-        else {
-            v = String(v);
-            var dp = v.indexOf('.') + 1;
-            if(dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, '');
-        }
-        // insert appropriate decimal point and thousands separator
-        v = Lib.numSeparate(v, ax._gd._fullLayout.separators, separatethousands);
-    }
-
-    // add exponent
-    if(exponent && exponentFormat !== 'hide') {
-        var signedExponent;
-        if(exponent < 0) signedExponent = '\u2212' + -exponent;
-        else if(exponentFormat !== 'power') signedExponent = '+' + exponent;
-        else signedExponent = String(exponent);
-
-        if(exponentFormat === 'e' ||
-                ((exponentFormat === 'SI' || exponentFormat === 'B') &&
-                 (exponent > 12 || exponent < -15))) {
-            v += 'e' + signedExponent;
-        }
-        else if(exponentFormat === 'E') {
-            v += 'E' + signedExponent;
-        }
-        else if(exponentFormat === 'power') {
-            v += '×10' + signedExponent + '';
-        }
-        else if(exponentFormat === 'B' && exponent === 9) {
-            v += 'B';
-        }
-        else if(exponentFormat === 'SI' || exponentFormat === 'B') {
-            v += SIPREFIXES[exponent / 3 + 5];
-        }
-    }
-
-    // put sign back in and return
-    // replace standard minus character (which is technically a hyphen)
-    // with a true minus sign
-    if(isNeg) return '\u2212' + v;
-    return v;
+  // negative?
+  var isNeg = v < 0,
+    // max number of digits past decimal point to show
+    tickRound = ax._tickround,
+    exponentFormat = fmtoverride || ax.exponentformat || "B",
+    exponent = ax._tickexponent,
+    tickformat = ax.tickformat,
+    separatethousands = ax.separatethousands;
+
+  // special case for hover: set exponent just for this value, and
+  // add a couple more digits of precision over tick labels
+  if (hover) {
+    // make a dummy axis obj to get the auto rounding and exponent
+    var ah = {
+      exponentformat: ax.exponentformat,
+      dtick: (
+        ax.showexponent === "none"
+          ? ax.dtick
+          : isNumeric(v) ? Math.abs(v) || 1 : 1
+      ),
+      // if not showing any exponents, don't change the exponent
+      // from what we calculate
+      range: (
+        ax.showexponent === "none" ? ax.range.map(ax.r2d) : [0, v || 1]
+      )
+    };
+    autoTickRound(ah);
+    tickRound = (Number(ah._tickround) || 0) + 4;
+    exponent = ah._tickexponent;
+    if (ax.hoverformat) tickformat = ax.hoverformat;
+  }
+
+  if (tickformat) return d3.format(tickformat)(v).replace(/-/g, "\u2212");
+
+  // 'epsilon' - rounding increment
+  var e = Math.pow(10, -tickRound) / 2;
+
+  // exponentFormat codes:
+  // 'e' (1.2e+6, default)
+  // 'E' (1.2E+6)
+  // 'SI' (1.2M)
+  // 'B' (same as SI except 10^9=B not G)
+  // 'none' (1200000)
+  // 'power' (1.2x10^6)
+  // 'hide' (1.2, use 3rd argument=='hide' to eg
+  //      only show exponent on last tick)
+  if (exponentFormat === "none") exponent = 0;
+
+  // take the sign out, put it back manually at the end
+  // - makes cases easier
+  v = Math.abs(v);
+  if (v < e) {
+    // 0 is just 0, but may get exponent if it's the last tick
+    v = "0";
+    isNeg = false;
+  } else {
+    v += e;
+    // take out a common exponent, if any
+    if (exponent) {
+      v *= Math.pow(10, -exponent);
+      tickRound += exponent;
+    }
+    // round the mantissa
+    if (tickRound === 0) {
+      v = String(Math.floor(v));
+    } else if (tickRound < 0) {
+      v = String(Math.round(v));
+      v = v.substr(0, v.length + tickRound);
+      for (var i = tickRound; i < 0; i++) {
+        v += "0";
+      }
+    } else {
+      v = String(v);
+      var dp = v.indexOf(".") + 1;
+      if (dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, "");
+    }
+    // insert appropriate decimal point and thousands separator
+    v = Lib.numSeparate(v, ax._gd._fullLayout.separators, separatethousands);
+  }
+
+  // add exponent
+  if (exponent && exponentFormat !== "hide") {
+    var signedExponent;
+    if (exponent < 0) signedExponent = "\u2212" + (-exponent);
+    else if (exponentFormat !== "power") signedExponent = "+" + exponent;
+    else signedExponent = String(exponent);
+
+    if (
+      exponentFormat === "e" ||
+        (exponentFormat === "SI" || exponentFormat === "B") &&
+          (exponent > 12 || exponent < -15)
+    ) {
+      v += "e" + signedExponent;
+    } else if (exponentFormat === "E") {
+      v += "E" + signedExponent;
+    } else if (exponentFormat === "power") {
+      v += "\xD710" + signedExponent + "";
+    } else if (exponentFormat === "B" && exponent === 9) {
+      v += "B";
+    } else if (exponentFormat === "SI" || exponentFormat === "B") {
+      v += SIPREFIXES[exponent / 3 + 5];
+    }
+  }
+
+  // put sign back in and return
+  // replace standard minus character (which is technically a hyphen)
+  // with a true minus sign
+  if (isNeg) return "\u2212" + v;
+  return v;
 }
 
-
 axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/;
 
 // getSubplots - extract all combinations of axes we need to make plots for
@@ -1387,153 +1391,155 @@ axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/;
 // looks both for combinations of x and y found in the data
 // and at axes and their anchors
 axes.getSubplots = function(gd, ax) {
-    var subplots = [];
-    var i, j, sp;
+  var subplots = [];
+  var i, j, sp;
 
-    // look for subplots in the data
-    var data = gd._fullData || gd.data || [];
+  // look for subplots in the data
+  var data = gd._fullData || gd.data || [];
 
-    for(i = 0; i < data.length; i++) {
-        var trace = data[i];
+  for (i = 0; i < data.length; i++) {
+    var trace = data[i];
 
-        if(trace.visible === false || trace.visible === 'legendonly' ||
-            !(Registry.traceIs(trace, 'cartesian') || Registry.traceIs(trace, 'gl2d'))
-        ) continue;
+    if (
+      trace.visible === false ||
+        trace.visible === "legendonly" ||
+        !(Registry.traceIs(trace, "cartesian") ||
+          Registry.traceIs(trace, "gl2d"))
+    ) {
+      continue;
+    }
 
-        var xId = trace.xaxis || 'x',
-            yId = trace.yaxis || 'y';
-        sp = xId + yId;
+    var xId = trace.xaxis || "x", yId = trace.yaxis || "y";
+    sp = xId + yId;
 
-        if(subplots.indexOf(sp) === -1) subplots.push(sp);
-    }
+    if (subplots.indexOf(sp) === -1) subplots.push(sp);
+  }
 
-    // look for subplots in the axes/anchors, so that we at least draw all axes
-    var axesList = axes.list(gd, '', true);
+  // look for subplots in the axes/anchors, so that we at least draw all axes
+  var axesList = axes.list(gd, "", true);
 
-    function hasAx2(sp, ax2) {
-        return sp.indexOf(ax2._id) !== -1;
-    }
+  function hasAx2(sp, ax2) {
+    return sp.indexOf(ax2._id) !== -1;
+  }
 
-    for(i = 0; i < axesList.length; i++) {
-        var ax2 = axesList[i],
-            ax2Letter = ax2._id.charAt(0),
-            ax3Id = (ax2.anchor === 'free') ?
-                ((ax2Letter === 'x') ? 'y' : 'x') :
-                ax2.anchor,
-            ax3 = axes.getFromId(gd, ax3Id);
+  for (i = 0; i < axesList.length; i++) {
+    var ax2 = axesList[i],
+      ax2Letter = ax2._id.charAt(0),
+      ax3Id = ax2.anchor === "free"
+        ? ax2Letter === "x" ? "y" : "x"
+        : ax2.anchor,
+      ax3 = axes.getFromId(gd, ax3Id);
 
-        // look if ax2 is already represented in the data
-        var foundAx2 = false;
-        for(j = 0; j < subplots.length; j++) {
-            if(hasAx2(subplots[j], ax2)) {
-                foundAx2 = true;
-                break;
-            }
-        }
+    // look if ax2 is already represented in the data
+    var foundAx2 = false;
+    for (j = 0; j < subplots.length; j++) {
+      if (hasAx2(subplots[j], ax2)) {
+        foundAx2 = true;
+        break;
+      }
+    }
 
-        // ignore free axes that already represented in the data
-        if(ax2.anchor === 'free' && foundAx2) continue;
+    // ignore free axes that already represented in the data
+    if (ax2.anchor === "free" && foundAx2) continue;
 
-        // ignore anchor-less axes
-        if(!ax3) continue;
+    // ignore anchor-less axes
+    if (!ax3) continue;
 
-        sp = (ax2Letter === 'x') ?
-            ax2._id + ax3._id :
-            ax3._id + ax2._id;
+    sp = ax2Letter === "x" ? ax2._id + ax3._id : ax3._id + ax2._id;
 
-        if(subplots.indexOf(sp) === -1) subplots.push(sp);
-    }
+    if (subplots.indexOf(sp) === -1) subplots.push(sp);
+  }
 
-    // filter invalid subplots
-    var spMatch = axes.subplotMatch,
-        allSubplots = [];
+  // filter invalid subplots
+  var spMatch = axes.subplotMatch, allSubplots = [];
 
-    for(i = 0; i < subplots.length; i++) {
-        sp = subplots[i];
-        if(spMatch.test(sp)) allSubplots.push(sp);
-    }
+  for (i = 0; i < subplots.length; i++) {
+    sp = subplots[i];
+    if (spMatch.test(sp)) allSubplots.push(sp);
+  }
 
-    // sort the subplot ids
-    allSubplots.sort(function(a, b) {
-        var aMatch = a.match(spMatch),
-            bMatch = b.match(spMatch);
+  // sort the subplot ids
+  allSubplots.sort(function(a, b) {
+    var aMatch = a.match(spMatch), bMatch = b.match(spMatch);
 
-        if(aMatch[1] === bMatch[1]) {
-            return +(aMatch[2] || 1) - (bMatch[2] || 1);
-        }
+    if (aMatch[1] === bMatch[1]) {
+      return +(aMatch[2] || 1) - (bMatch[2] || 1);
+    }
 
-        return +(aMatch[1]||0) - (bMatch[1]||0);
-    });
+    return +(aMatch[1] || 0) - (bMatch[1] || 0);
+  });
 
-    if(ax) return axes.findSubplotsWithAxis(allSubplots, ax);
-    return allSubplots;
+  if (ax) return axes.findSubplotsWithAxis(allSubplots, ax);
+  return allSubplots;
 };
 
 // find all subplots with axis 'ax'
 axes.findSubplotsWithAxis = function(subplots, ax) {
-    var axMatch = new RegExp(
-        (ax._id.charAt(0) === 'x') ? ('^' + ax._id + 'y') : (ax._id + '$')
-    );
-    var subplotsWithAxis = [];
+  var axMatch = new RegExp(
+    (ax._id.charAt(0) === "x" ? "^" + ax._id + "y" : ax._id + "$")
+  );
+  var subplotsWithAxis = [];
 
-    for(var i = 0; i < subplots.length; i++) {
-        var sp = subplots[i];
-        if(axMatch.test(sp)) subplotsWithAxis.push(sp);
-    }
+  for (var i = 0; i < subplots.length; i++) {
+    var sp = subplots[i];
+    if (axMatch.test(sp)) subplotsWithAxis.push(sp);
+  }
 
-    return subplotsWithAxis;
+  return subplotsWithAxis;
 };
 
 // makeClipPaths: prepare clipPaths for all single axes and all possible xy pairings
 axes.makeClipPaths = function(gd) {
-    var fullLayout = gd._fullLayout,
-        defs = fullLayout._defs,
-        fullWidth = {_offset: 0, _length: fullLayout.width, _id: ''},
-        fullHeight = {_offset: 0, _length: fullLayout.height, _id: ''},
-        xaList = axes.list(gd, 'x', true),
-        yaList = axes.list(gd, 'y', true),
-        clipList = [],
-        i,
-        j;
-
-    for(i = 0; i < xaList.length; i++) {
-        clipList.push({x: xaList[i], y: fullHeight});
-        for(j = 0; j < yaList.length; j++) {
-            if(i === 0) clipList.push({x: fullWidth, y: yaList[j]});
-            clipList.push({x: xaList[i], y: yaList[j]});
-        }
-    }
-
-    var defGroup = defs.selectAll('g.clips')
-        .data([0]);
-
-    defGroup.enter().append('g')
-        .classed('clips', true);
-
-    // selectors don't work right with camelCase tags,
-    // have to use class instead
-    // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I
-    var axClips = defGroup.selectAll('.axesclip')
-        .data(clipList, function(d) { return d.x._id + d.y._id; });
-
-    axClips.enter().append('clipPath')
-        .classed('axesclip', true)
-        .attr('id', function(d) { return 'clip' + fullLayout._uid + d.x._id + d.y._id; })
-      .append('rect');
-
-    axClips.exit().remove();
-
-    axClips.each(function(d) {
-        d3.select(this).select('rect').attr({
-            x: d.x._offset || 0,
-            y: d.y._offset || 0,
-            width: d.x._length || 1,
-            height: d.y._length || 1
-        });
+  var fullLayout = gd._fullLayout,
+    defs = fullLayout._defs,
+    fullWidth = { _offset: 0, _length: fullLayout.width, _id: "" },
+    fullHeight = { _offset: 0, _length: fullLayout.height, _id: "" },
+    xaList = axes.list(gd, "x", true),
+    yaList = axes.list(gd, "y", true),
+    clipList = [],
+    i,
+    j;
+
+  for (i = 0; i < xaList.length; i++) {
+    clipList.push({ x: xaList[i], y: fullHeight });
+    for (j = 0; j < yaList.length; j++) {
+      if (i === 0) clipList.push({ x: fullWidth, y: yaList[j] });
+      clipList.push({ x: xaList[i], y: yaList[j] });
+    }
+  }
+
+  var defGroup = defs.selectAll("g.clips").data([0]);
+
+  defGroup.enter().append("g").classed("clips", true);
+
+  // selectors don't work right with camelCase tags,
+  // have to use class instead
+  // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I
+  var axClips = defGroup.selectAll(".axesclip").data(clipList, function(d) {
+    return d.x._id + d.y._id;
+  });
+
+  axClips
+    .enter()
+    .append("clipPath")
+    .classed("axesclip", true)
+    .attr("id", function(d) {
+      return "clip" + fullLayout._uid + d.x._id + d.y._id;
+    })
+    .append("rect");
+
+  axClips.exit().remove();
+
+  axClips.each(function(d) {
+    d3.select(this).select("rect").attr({
+      x: d.x._offset || 0,
+      y: d.y._offset || 0,
+      width: d.x._length || 1,
+      height: d.y._length || 1
     });
+  });
 };
 
-
 // doTicks: draw ticks, grids, and tick labels
 // axid: 'x', 'y', 'x2' etc,
 //     blank to do all,
@@ -1542,663 +1548,737 @@ axes.makeClipPaths = function(gd) {
 //          ax._rl (stored linearized range for use by zoom/pan)
 //     or can pass in an axis object directly
 axes.doTicks = function(gd, axid, skipTitle) {
-    var fullLayout = gd._fullLayout,
-        ax,
-        independent = false;
-
-    // allow passing an independent axis object instead of id
-    if(typeof axid === 'object') {
-        ax = axid;
-        axid = ax._id;
-        independent = true;
-    }
-    else {
-        ax = axes.getFromId(gd, axid);
-
-        if(axid === 'redraw') {
-            fullLayout._paper.selectAll('g.subplot').each(function(subplot) {
-                var plotinfo = fullLayout._plots[subplot],
-                    xa = plotinfo.xaxis,
-                    ya = plotinfo.yaxis;
-
-                plotinfo.xaxislayer
-                    .selectAll('.' + xa._id + 'tick').remove();
-                plotinfo.yaxislayer
-                    .selectAll('.' + ya._id + 'tick').remove();
-                plotinfo.gridlayer
-                    .selectAll('path').remove();
-                plotinfo.zerolinelayer
-                    .selectAll('path').remove();
-            });
-        }
-
-        if(!axid || axid === 'redraw') {
-            return Lib.syncOrAsync(axes.list(gd, '', true).map(function(ax) {
-                return function() {
-                    if(!ax._id) return;
-                    var axDone = axes.doTicks(gd, ax._id);
-                    if(axid === 'redraw') {
-                        ax._r = ax.range.slice();
-                        ax._rl = Lib.simpleMap(ax._r, ax.r2l);
-                    }
-                    return axDone;
-                };
-            }));
-        }
-    }
-
-    // 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();
-
-    var axletter = axid.charAt(0),
-        counterLetter = axes.counterLetter(axid),
-        vals = axes.calcTicks(ax),
-        datafn = function(d) { return [d.text, d.x, ax.mirror].join('_'); },
-        tcls = axid + 'tick',
-        gcls = axid + 'grid',
-        zcls = axid + 'zl',
-        pad = (ax.linewidth || 1) / 2,
-        labelStandoff =
-            (ax.ticks === 'outside' ? ax.ticklen : 1) + (ax.linewidth || 0),
-        labelShift = 0,
-        gridWidth = Drawing.crispRound(gd, ax.gridwidth, 1),
-        zeroLineWidth = Drawing.crispRound(gd, ax.zerolinewidth, gridWidth),
-        tickWidth = Drawing.crispRound(gd, ax.tickwidth, 1),
-        sides, transfn, tickpathfn,
-        i;
-
-    if(ax._counterangle && ax.ticks === 'outside') {
-        var caRad = ax._counterangle * Math.PI / 180;
-        labelStandoff = ax.ticklen * Math.cos(caRad) + (ax.linewidth || 0);
-        labelShift = ax.ticklen * Math.sin(caRad);
-    }
-
-    // positioning arguments for x vs y axes
-    if(axletter === 'x') {
-        sides = ['bottom', 'top'];
-        transfn = function(d) {
-            return 'translate(' + ax.l2p(d.x) + ',0)';
-        };
-        tickpathfn = function(shift, len) {
-            if(ax._counterangle) {
-                var caRad = ax._counterangle * Math.PI / 180;
-                return 'M0,' + shift + 'l' + (Math.sin(caRad) * len) + ',' + (Math.cos(caRad) * len);
-            }
-            else return 'M0,' + shift + 'v' + len;
-        };
-    }
-    else if(axletter === 'y') {
-        sides = ['left', 'right'];
-        transfn = function(d) {
-            return 'translate(0,' + ax.l2p(d.x) + ')';
-        };
-        tickpathfn = function(shift, len) {
-            if(ax._counterangle) {
-                var caRad = ax._counterangle * Math.PI / 180;
-                return 'M' + shift + ',0l' + (Math.cos(caRad) * len) + ',' + (-Math.sin(caRad) * len);
+  var fullLayout = gd._fullLayout, ax, independent = false;
+
+  // allow passing an independent axis object instead of id
+  if (typeof axid === "object") {
+    ax = axid;
+    axid = ax._id;
+    independent = true;
+  } else {
+    ax = axes.getFromId(gd, axid);
+
+    if (axid === "redraw") {
+      fullLayout._paper.selectAll("g.subplot").each(function(subplot) {
+        var plotinfo = fullLayout._plots[subplot],
+          xa = plotinfo.xaxis,
+          ya = plotinfo.yaxis;
+
+        plotinfo.xaxislayer.selectAll("." + xa._id + "tick").remove();
+        plotinfo.yaxislayer.selectAll("." + ya._id + "tick").remove();
+        plotinfo.gridlayer.selectAll("path").remove();
+        plotinfo.zerolinelayer.selectAll("path").remove();
+      });
+    }
+
+    if (!axid || axid === "redraw") {
+      return Lib.syncOrAsync(
+        axes.list(gd, "", true).map(function(ax) {
+          return function() {
+            if (!ax._id) return;
+            var axDone = axes.doTicks(gd, ax._id);
+            if (axid === "redraw") {
+              ax._r = ax.range.slice();
+              ax._rl = Lib.simpleMap(ax._r, ax.r2l);
             }
-            else return 'M' + shift + ',0h' + len;
-        };
-    }
-    else {
-        Lib.warn('Unrecognized doTicks axis:', axid);
-        return;
-    }
-    var axside = ax.side || sides[0],
+            return axDone;
+          };
+        })
+      );
+    }
+  }
+
+  // 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();
+
+  var axletter = axid.charAt(0),
+    counterLetter = axes.counterLetter(axid),
+    vals = axes.calcTicks(ax),
+    datafn = function(d) {
+      return [d.text, d.x, ax.mirror].join("_");
+    },
+    tcls = axid + "tick",
+    gcls = axid + "grid",
+    zcls = axid + "zl",
+    pad = (ax.linewidth || 1) / 2,
+    labelStandoff = (ax.ticks === "outside" ? ax.ticklen : 1) +
+      (ax.linewidth || 0),
+    labelShift = 0,
+    gridWidth = Drawing.crispRound(gd, ax.gridwidth, 1),
+    zeroLineWidth = Drawing.crispRound(gd, ax.zerolinewidth, gridWidth),
+    tickWidth = Drawing.crispRound(gd, ax.tickwidth, 1),
+    sides,
+    transfn,
+    tickpathfn,
+    i;
+
+  if (ax._counterangle && ax.ticks === "outside") {
+    var caRad = ax._counterangle * Math.PI / 180;
+    labelStandoff = ax.ticklen * Math.cos(caRad) + (ax.linewidth || 0);
+    labelShift = ax.ticklen * Math.sin(caRad);
+  }
+
+  // positioning arguments for x vs y axes
+  if (axletter === "x") {
+    sides = ["bottom", "top"];
+    transfn = function(d) {
+      return "translate(" + ax.l2p(d.x) + ",0)";
+    };
+    tickpathfn = function(shift, len) {
+      if (ax._counterangle) {
+        var caRad = ax._counterangle * Math.PI / 180;
+        return "M0," +
+          shift +
+          "l" +
+          Math.sin(caRad) * len +
+          "," +
+          Math.cos(caRad) * len;
+      } else {
+        return "M0," + shift + "v" + len;
+      }
+    };
+  } else if (axletter === "y") {
+    sides = ["left", "right"];
+    transfn = function(d) {
+      return "translate(0," + ax.l2p(d.x) + ")";
+    };
+    tickpathfn = function(shift, len) {
+      if (ax._counterangle) {
+        var caRad = ax._counterangle * Math.PI / 180;
+        return "M" +
+          shift +
+          ",0l" +
+          Math.cos(caRad) * len +
+          "," +
+          (-Math.sin(caRad)) * len;
+      } else {
+        return "M" + shift + ",0h" + len;
+      }
+    };
+  } else {
+    Lib.warn("Unrecognized doTicks axis:", axid);
+    return;
+  }
+  var axside = ax.side || sides[0],
     // which direction do the side[0], side[1], and free ticks go?
     // then we flip if outside XOR y axis
-        ticksign = [-1, 1, axside === sides[1] ? 1 : -1];
-    if((ax.ticks !== 'inside') === (axletter === 'x')) {
-        ticksign = ticksign.map(function(v) { return -v; });
-    }
-
-    // remove zero lines, grid lines, and inside ticks if they're within
-    // 1 pixel of the end
-    // The key case here is removing zero lines when the axis bound is zero.
-    function clipEnds(d) {
-        var p = ax.l2p(d.x);
-        return (p > 1 && p < ax._length - 1);
-    }
-    var valsClipped = vals.filter(clipEnds);
-
-    function drawTicks(container, tickpath) {
-        var ticks = container.selectAll('path.' + tcls)
-            .data(ax.ticks === 'inside' ? valsClipped : vals, datafn);
-        if(tickpath && ax.ticks) {
-            ticks.enter().append('path').classed(tcls, 1).classed('ticks', 1)
-                .classed('crisp', 1)
-                .call(Color.stroke, ax.tickcolor)
-                .style('stroke-width', tickWidth + 'px')
-                .attr('d', tickpath);
-            ticks.attr('transform', transfn);
-            ticks.exit().remove();
-        }
-        else ticks.remove();
-    }
-
-    function drawLabels(container, position) {
-        // tick labels - for now just the main labels.
-        // TODO: mirror labels, esp for subplots
-        var tickLabels = container.selectAll('g.' + tcls).data(vals, datafn);
-        if(!ax.showticklabels || !isNumeric(position)) {
-            tickLabels.remove();
-            drawAxTitle(axid);
-            return;
-        }
-
-        var labelx, labely, labelanchor, labelpos0, flipit;
-        if(axletter === 'x') {
-            flipit = (axside === 'bottom') ? 1 : -1;
-            labelx = function(d) { return d.dx + labelShift * flipit; };
-            labelpos0 = position + (labelStandoff + pad) * flipit;
-            labely = function(d) {
-                return d.dy + labelpos0 + d.fontSize *
-                    ((axside === 'bottom') ? 1 : -0.5);
-            };
-            labelanchor = function(angle) {
-                if(!isNumeric(angle) || angle === 0 || angle === 180) {
-                    return 'middle';
-                }
-                return (angle * flipit < 0) ? 'end' : 'start';
-            };
-        }
-        else {
-            flipit = (axside === 'right') ? 1 : -1;
-            labely = function(d) { return d.dy + d.fontSize / 2 - labelShift * flipit; };
-            labelx = function(d) {
-                return d.dx + position + (labelStandoff + pad +
-                    ((Math.abs(ax.tickangle) === 90) ? d.fontSize / 2 : 0)) * flipit;
-            };
-            labelanchor = function(angle) {
-                if(isNumeric(angle) && Math.abs(angle) === 90) {
-                    return 'middle';
-                }
-                return axside === 'right' ? 'start' : 'end';
-            };
-        }
-        var maxFontSize = 0,
-            autoangle = 0,
-            labelsReady = [];
-        tickLabels.enter().append('g').classed(tcls, 1)
-            .append('text')
-                // only so tex has predictable alignment that we can
-                // alter later
-                .attr('text-anchor', 'middle')
-                .each(function(d) {
-                    var thisLabel = d3.select(this),
-                        newPromise = gd._promises.length;
-                    thisLabel
-                        .call(Drawing.setPosition, labelx(d), labely(d))
-                        .call(Drawing.font, d.font, d.fontSize, d.fontColor)
-                        .text(d.text)
-                        .call(svgTextUtils.convertToTspans);
-                    newPromise = gd._promises[newPromise];
-                    if(newPromise) {
-                        // if we have an async label, we'll deal with that
-                        // all here so take it out of gd._promises and
-                        // instead position the label and promise this in
-                        // labelsReady
-                        labelsReady.push(gd._promises.pop().then(function() {
-                            positionLabels(thisLabel, ax.tickangle);
-                        }));
-                    }
-                    else {
-                        // sync label: just position it now.
-                        positionLabels(thisLabel, ax.tickangle);
-                    }
-                });
-        tickLabels.exit().remove();
+    ticksign = [-1, 1, axside === sides[1] ? 1 : -1];
+  if (ax.ticks !== "inside" === (axletter === "x")) {
+    ticksign = ticksign.map(function(v) {
+      return -v;
+    });
+  }
+
+  // remove zero lines, grid lines, and inside ticks if they're within
+  // 1 pixel of the end
+  // The key case here is removing zero lines when the axis bound is zero.
+  function clipEnds(d) {
+    var p = ax.l2p(d.x);
+    return p > 1 && p < ax._length - 1;
+  }
+  var valsClipped = vals.filter(clipEnds);
+
+  function drawTicks(container, tickpath) {
+    var ticks = container
+      .selectAll("path." + tcls)
+      .data(ax.ticks === "inside" ? valsClipped : vals, datafn);
+    if (tickpath && ax.ticks) {
+      ticks
+        .enter()
+        .append("path")
+        .classed(tcls, 1)
+        .classed("ticks", 1)
+        .classed("crisp", 1)
+        .call(Color.stroke, ax.tickcolor)
+        .style("stroke-width", tickWidth + "px")
+        .attr("d", tickpath);
+      ticks.attr("transform", transfn);
+      ticks.exit().remove();
+    } else {
+      ticks.remove();
+    }
+  }
+
+  function drawLabels(container, position) {
+    // tick labels - for now just the main labels.
+    // TODO: mirror labels, esp for subplots
+    var tickLabels = container.selectAll("g." + tcls).data(vals, datafn);
+    if (!ax.showticklabels || !isNumeric(position)) {
+      tickLabels.remove();
+      drawAxTitle(axid);
+      return;
+    }
+
+    var labelx, labely, labelanchor, labelpos0, flipit;
+    if (axletter === "x") {
+      flipit = axside === "bottom" ? 1 : -1;
+      labelx = function(d) {
+        return d.dx + labelShift * flipit;
+      };
+      labelpos0 = position + (labelStandoff + pad) * flipit;
+      labely = function(d) {
+        return d.dy + labelpos0 + d.fontSize * (axside === "bottom" ? 1 : -0.5);
+      };
+      labelanchor = function(angle) {
+        if (!isNumeric(angle) || angle === 0 || angle === 180) {
+          return "middle";
+        }
+        return angle * flipit < 0 ? "end" : "start";
+      };
+    } else {
+      flipit = axside === "right" ? 1 : -1;
+      labely = function(d) {
+        return d.dy + d.fontSize / 2 - labelShift * flipit;
+      };
+      labelx = function(d) {
+        return d.dx +
+          position +
+          (labelStandoff +
+            pad +
+            (Math.abs(ax.tickangle) === 90 ? d.fontSize / 2 : 0)) *
+            flipit;
+      };
+      labelanchor = function(angle) {
+        if (isNumeric(angle) && Math.abs(angle) === 90) {
+          return "middle";
+        }
+        return axside === "right" ? "start" : "end";
+      };
+    }
+    var maxFontSize = 0, autoangle = 0, labelsReady = [];
+    tickLabels
+      .enter()
+      .append("g")
+      .classed(tcls, 1)
+      .append("text")
+      .attr("text-anchor", "middle")
+      .each(function(d) {
+        var thisLabel = d3.select(this), newPromise = gd._promises.length;
+        thisLabel
+          .call(Drawing.setPosition, labelx(d), labely(d))
+          .call(Drawing.font, d.font, d.fontSize, d.fontColor)
+          .text(d.text)
+          .call(svgTextUtils.convertToTspans);
+        newPromise = gd._promises[newPromise];
+        if (newPromise) {
+          // if we have an async label, we'll deal with that
+          // all here so take it out of gd._promises and
+          // instead position the label and promise this in
+          // labelsReady
+          labelsReady.push(
+            gd._promises.pop().then(function() {
+              positionLabels(thisLabel, ax.tickangle);
+            })
+          );
+        } else {
+          // sync label: just position it now.
+          positionLabels(thisLabel, ax.tickangle);
+        }
+      });
+    tickLabels.exit().remove();
+
+    tickLabels.each(function(d) {
+      maxFontSize = Math.max(maxFontSize, d.fontSize);
+    });
 
+    function positionLabels(s, angle) {
+      s.each(function(d) {
+        var anchor = labelanchor(angle);
+        var thisLabel = d3.select(this),
+          mathjaxGroup = thisLabel.select(".text-math-group"),
+          transform = transfn(d) +
+            (isNumeric(angle) && +angle !== 0
+              ? " rotate(" +
+                  angle +
+                  "," +
+                  labelx(d) +
+                  "," +
+                  (labely(d) - d.fontSize / 2) +
+                  ")"
+              : "");
+        if (mathjaxGroup.empty()) {
+          var txt = thisLabel
+            .select("text")
+            .attr({ transform: transform, "text-anchor": anchor });
+
+          if (!txt.empty()) {
+            txt
+              .selectAll("tspan.line")
+              .attr({ x: txt.attr("x"), y: txt.attr("y") });
+          }
+        } else {
+          var mjShift = Drawing.bBox(mathjaxGroup.node()).width *
+            ({ end: -0.5, start: 0.5 })[anchor];
+          mathjaxGroup.attr(
+            "transform",
+            transform + (mjShift ? "translate(" + mjShift + ",0)" : "")
+          );
+        }
+      });
+    }
+
+    // make sure all labels are correctly positioned at their base angle
+    // the positionLabels call above is only for newly drawn labels.
+    // do this without waiting, using the last calculated angle to
+    // minimize flicker, then do it again when we know all labels are
+    // there, putting back the prescribed angle to check for overlaps.
+    positionLabels(tickLabels, ax._lastangle || ax.tickangle);
+
+    function allLabelsReady() {
+      return labelsReady.length && Promise.all(labelsReady);
+    }
+
+    function fixLabelOverlaps() {
+      positionLabels(tickLabels, ax.tickangle);
+
+      // check for auto-angling if x labels overlap
+      // don't auto-angle at all for log axes with
+      // base and digit format
+      if (
+        axletter === "x" &&
+          !isNumeric(ax.tickangle) &&
+          (ax.type !== "log" || String(ax.dtick).charAt(0) !== "D")
+      ) {
+        var lbbArray = [];
         tickLabels.each(function(d) {
-            maxFontSize = Math.max(maxFontSize, d.fontSize);
+          var s = d3.select(this),
+            thisLabel = s.select(".text-math-group"),
+            x = ax.l2p(d.x);
+          if (thisLabel.empty()) thisLabel = s.select("text");
+
+          var bb = Drawing.bBox(thisLabel.node());
+
+          lbbArray.push({
+            // ignore about y, just deal with x overlaps
+            top: 0,
+            bottom: 10,
+            height: 10,
+            left: x - bb.width / 2,
+            // impose a 2px gap
+            right: (
+              x + bb.width / 2 + 2
+            ),
+            width: bb.width + 2
+          });
         });
-
-        function positionLabels(s, angle) {
-            s.each(function(d) {
-                var anchor = labelanchor(angle);
-                var thisLabel = d3.select(this),
-                    mathjaxGroup = thisLabel.select('.text-math-group'),
-                    transform = transfn(d) +
-                        ((isNumeric(angle) && +angle !== 0) ?
-                        (' rotate(' + angle + ',' + labelx(d) + ',' +
-                            (labely(d) - d.fontSize / 2) + ')') :
-                        '');
-                if(mathjaxGroup.empty()) {
-                    var txt = thisLabel.select('text').attr({
-                        transform: transform,
-                        'text-anchor': anchor
-                    });
-
-                    if(!txt.empty()) {
-                        txt.selectAll('tspan.line').attr({
-                            x: txt.attr('x'),
-                            y: txt.attr('y')
-                        });
-                    }
-                }
-                else {
-                    var mjShift =
-                        Drawing.bBox(mathjaxGroup.node()).width *
-                            {end: -0.5, start: 0.5}[anchor];
-                    mathjaxGroup.attr('transform', transform +
-                        (mjShift ? 'translate(' + mjShift + ',0)' : ''));
-                }
-            });
-        }
-
-        // make sure all labels are correctly positioned at their base angle
-        // the positionLabels call above is only for newly drawn labels.
-        // do this without waiting, using the last calculated angle to
-        // minimize flicker, then do it again when we know all labels are
-        // there, putting back the prescribed angle to check for overlaps.
-        positionLabels(tickLabels, ax._lastangle || ax.tickangle);
-
-        function allLabelsReady() {
-            return labelsReady.length && Promise.all(labelsReady);
-        }
-
-        function fixLabelOverlaps() {
-            positionLabels(tickLabels, ax.tickangle);
-
-            // check for auto-angling if x labels overlap
-            // don't auto-angle at all for log axes with
-            // base and digit format
-            if(axletter === 'x' && !isNumeric(ax.tickangle) &&
-                    (ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')) {
-                var lbbArray = [];
-                tickLabels.each(function(d) {
-                    var s = d3.select(this),
-                        thisLabel = s.select('.text-math-group'),
-                        x = ax.l2p(d.x);
-                    if(thisLabel.empty()) thisLabel = s.select('text');
-
-                    var bb = Drawing.bBox(thisLabel.node());
-
-                    lbbArray.push({
-                        // ignore about y, just deal with x overlaps
-                        top: 0,
-                        bottom: 10,
-                        height: 10,
-                        left: x - bb.width / 2,
-                        // impose a 2px gap
-                        right: x + bb.width / 2 + 2,
-                        width: bb.width + 2
-                    });
-                });
-                for(i = 0; i < lbbArray.length - 1; i++) {
-                    if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1])) {
-                        // any overlap at all - set 30 degrees
-                        autoangle = 30;
-                        break;
-                    }
-                }
-                if(autoangle) {
-                    var tickspacing = Math.abs(
-                            (vals[vals.length - 1].x - vals[0].x) * ax._m
-                        ) / (vals.length - 1);
-                    if(tickspacing < maxFontSize * 2.5) {
-                        autoangle = 90;
-                    }
-                    positionLabels(tickLabels, autoangle);
-                }
-                ax._lastangle = autoangle;
+        for (i = 0; i < lbbArray.length - 1; i++) {
+          if (Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1])) {
+            // any overlap at all - set 30 degrees
+            autoangle = 30;
+            break;
+          }
+        }
+        if (autoangle) {
+          var tickspacing = Math.abs(
+            (vals[vals.length - 1].x - vals[0].x) * ax._m
+          ) /
+            (vals.length - 1);
+          if (tickspacing < maxFontSize * 2.5) {
+            autoangle = 90;
+          }
+          positionLabels(tickLabels, autoangle);
+        }
+        ax._lastangle = autoangle;
+      }
+
+      // update the axis title
+      // (so it can move out of the way if needed)
+      // TODO: separate out scoot so we don't need to do
+      // a full redraw of the title (mostly relevant for MathJax)
+      drawAxTitle(axid);
+      return axid + " done";
+    }
+
+    function calcBoundingBox() {
+      ax._boundingBox = container.node().getBoundingClientRect();
+    }
+
+    var done = Lib.syncOrAsync([
+      allLabelsReady,
+      fixLabelOverlaps,
+      calcBoundingBox
+    ]);
+    if (done && done.then) gd._promises.push(done);
+    return done;
+  }
+
+  function drawAxTitle(axid) {
+    if (skipTitle) return;
+
+    // now this only applies to regular cartesian axes; colorbars and
+    // others ALWAYS call doTicks with skipTitle=true so they can
+    // configure their own titles.
+    var ax = axisIds.getFromId(gd, axid),
+      avoidSelection = d3.select(gd).selectAll("g." + axid + "tick"),
+      avoid = { selection: avoidSelection, side: ax.side },
+      axLetter = axid.charAt(0),
+      gs = gd._fullLayout._size,
+      offsetBase = 1.5,
+      fontSize = ax.titlefont.size,
+      transform,
+      counterAxis,
+      x,
+      y;
+    if (avoidSelection.size()) {
+      var translation = Drawing.getTranslate(avoidSelection.node().parentNode);
+      avoid.offsetLeft = translation.x;
+      avoid.offsetTop = translation.y;
+    }
+
+    if (axLetter === "x") {
+      counterAxis = ax.anchor === "free"
+        ? { _offset: gs.t + (1 - (ax.position || 0)) * gs.h, _length: 0 }
+        : axisIds.getFromId(gd, ax.anchor);
+
+      x = ax._offset + ax._length / 2;
+      y = counterAxis._offset +
+        (ax.side === "top"
+          ? -10 - fontSize * (offsetBase + (ax.showticklabels ? 1 : 0))
+          : counterAxis._length +
+              10 +
+              fontSize * (offsetBase + (ax.showticklabels ? 1.5 : 0.5)));
+
+      if (ax.rangeslider && ax.rangeslider.visible && ax._boundingBox) {
+        y += (fullLayout.height - fullLayout.margin.b - fullLayout.margin.t) *
+          ax.rangeslider.thickness +
+          ax._boundingBox.height;
+      }
+
+      if (!avoid.side) avoid.side = "bottom";
+    } else {
+      counterAxis = ax.anchor === "free"
+        ? { _offset: gs.l + (ax.position || 0) * gs.w, _length: 0 }
+        : axisIds.getFromId(gd, ax.anchor);
+
+      y = ax._offset + ax._length / 2;
+      x = counterAxis._offset +
+        (ax.side === "right"
+          ? counterAxis._length +
+              10 +
+              fontSize * (offsetBase + (ax.showticklabels ? 1 : 0.5))
+          : -10 - fontSize * (offsetBase + (ax.showticklabels ? 0.5 : 0)));
+
+      transform = { rotate: "-90", offset: 0 };
+      if (!avoid.side) avoid.side = "left";
+    }
+
+    Titles.draw(gd, axid + "title", {
+      propContainer: ax,
+      propName: ax._name + ".title",
+      dfltName: axLetter.toUpperCase() + " axis",
+      avoid: avoid,
+      transform: transform,
+      attributes: { x: x, y: y, "text-anchor": "middle" }
+    });
+  }
+
+  function traceHasBarsOrFill(trace, subplot) {
+    if (trace.visible !== true || trace.xaxis + trace.yaxis !== subplot) {
+      return false;
+    }
+    if (
+      Registry.traceIs(trace, "bar") &&
+        trace.orientation === ({ x: "h", y: "v" })[axletter]
+    ) {
+      return true;
+    }
+    return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axletter;
+  }
+
+  function drawGrid(plotinfo, counteraxis, subplot) {
+    var gridcontainer = plotinfo.gridlayer,
+      zlcontainer = plotinfo.zerolinelayer,
+      gridvals = plotinfo["hidegrid" + axletter] ? [] : valsClipped,
+      gridpath = ax._gridpath ||
+        "M0,0" + (axletter === "x" ? "v" : "h") + counteraxis._length,
+      grid = gridcontainer
+        .selectAll("path." + gcls)
+        .data(ax.showgrid === false ? [] : gridvals, datafn);
+    grid
+      .enter()
+      .append("path")
+      .classed(gcls, 1)
+      .classed("crisp", 1)
+      .attr("d", gridpath)
+      .each(function(d) {
+        if (
+          ax.zeroline &&
+            (ax.type === "linear" || ax.type === "-") &&
+            Math.abs(d.x) < ax.dtick / 100
+        ) {
+          d3.select(this).remove();
+        }
+      });
+    grid
+      .attr("transform", transfn)
+      .call(Color.stroke, ax.gridcolor || "#ddd")
+      .style("stroke-width", gridWidth + "px");
+    grid.exit().remove();
+
+    // zero line
+    if (zlcontainer) {
+      var hasBarsOrFill = false;
+      for (var i = 0; i < gd._fullData.length; i++) {
+        if (traceHasBarsOrFill(gd._fullData[i], subplot)) {
+          hasBarsOrFill = true;
+          break;
+        }
+      }
+      var rng = Lib.simpleMap(ax.range, ax.r2l),
+        showZl = rng[0] * rng[1] <= 0 &&
+          ax.zeroline &&
+          (ax.type === "linear" || ax.type === "-") &&
+          gridvals.length &&
+          (hasBarsOrFill || clipEnds({ x: 0 }) || !ax.showline);
+      var zl = zlcontainer
+        .selectAll("path." + zcls)
+        .data(showZl ? [{ x: 0 }] : []);
+      zl
+        .enter()
+        .append("path")
+        .classed(zcls, 1)
+        .classed("zl", 1)
+        .classed("crisp", 1)
+        .attr("d", gridpath);
+      zl
+        .attr("transform", transfn)
+        .call(Color.stroke, ax.zerolinecolor || Color.defaultLine)
+        .style("stroke-width", zeroLineWidth + "px");
+      zl.exit().remove();
+    }
+  }
+
+  if (independent) {
+    drawTicks(
+      ax._axislayer,
+      tickpathfn(ax._pos + pad * ticksign[2], ticksign[2] * ax.ticklen)
+    );
+    if (ax._counteraxis) {
+      var fictionalPlotinfo = {
+        gridlayer: ax._gridlayer,
+        zerolinelayer: ax._zerolinelayer
+      };
+      drawGrid(fictionalPlotinfo, ax._counteraxis);
+    }
+    return drawLabels(ax._axislayer, ax._pos);
+  } else {
+    var alldone = axes
+      .getSubplots(gd, ax)
+      .map(function(subplot) {
+        var plotinfo = fullLayout._plots[subplot];
+
+        if (!fullLayout._has("cartesian")) return;
+
+        var container = plotinfo[axletter + "axislayer"],
+          // [bottom or left, top or right, free, main]
+          linepositions = ax._linepositions[subplot] || [],
+          counteraxis = plotinfo[counterLetter + "axis"],
+          mainSubplot = counteraxis._id === ax.anchor,
+          ticksides = [false, false, false],
+          tickpath = "";
+
+        // ticks
+        if (ax.mirror === "allticks") {
+          ticksides = [true, true, false];
+        } else if (mainSubplot) {
+          if (ax.mirror === "ticks") ticksides = [true, true, false];
+          else ticksides[sides.indexOf(axside)] = true;
+        }
+        if (ax.mirrors) {
+          for (i = 0; i < 2; i++) {
+            var thisMirror = ax.mirrors[counteraxis._id + sides[i]];
+            if (thisMirror === "ticks" || thisMirror === "labels") {
+              ticksides[i] = true;
             }
-
-            // update the axis title
-            // (so it can move out of the way if needed)
-            // TODO: separate out scoot so we don't need to do
-            // a full redraw of the title (mostly relevant for MathJax)
-            drawAxTitle(axid);
-            return axid + ' done';
+          }
         }
 
-        function calcBoundingBox() {
-            ax._boundingBox = container.node().getBoundingClientRect();
-        }
+        // free axis ticks
+        if (linepositions[2] !== undefined) ticksides[2] = true;
 
-        var done = Lib.syncOrAsync([
-            allLabelsReady,
-            fixLabelOverlaps,
-            calcBoundingBox
-        ]);
-        if(done && done.then) gd._promises.push(done);
-        return done;
-    }
-
-    function drawAxTitle(axid) {
-        if(skipTitle) return;
-
-        // now this only applies to regular cartesian axes; colorbars and
-        // others ALWAYS call doTicks with skipTitle=true so they can
-        // configure their own titles.
-        var ax = axisIds.getFromId(gd, axid),
-            avoidSelection = d3.select(gd).selectAll('g.' + axid + 'tick'),
-            avoid = {
-                selection: avoidSelection,
-                side: ax.side
-            },
-            axLetter = axid.charAt(0),
-            gs = gd._fullLayout._size,
-            offsetBase = 1.5,
-            fontSize = ax.titlefont.size,
-            transform,
-            counterAxis,
-            x,
-            y;
-        if(avoidSelection.size()) {
-            var translation = Drawing.getTranslate(avoidSelection.node().parentNode);
-            avoid.offsetLeft = translation.x;
-            avoid.offsetTop = translation.y;
-        }
-
-        if(axLetter === 'x') {
-            counterAxis = (ax.anchor === 'free') ?
-                {_offset: gs.t + (1 - (ax.position || 0)) * gs.h, _length: 0} :
-                axisIds.getFromId(gd, ax.anchor);
-
-            x = ax._offset + ax._length / 2;
-            y = counterAxis._offset + ((ax.side === 'top') ?
-                -10 - fontSize * (offsetBase + (ax.showticklabels ? 1 : 0)) :
-                counterAxis._length + 10 +
-                    fontSize * (offsetBase + (ax.showticklabels ? 1.5 : 0.5)));
-
-            if(ax.rangeslider && ax.rangeslider.visible && ax._boundingBox) {
-                y += (fullLayout.height - fullLayout.margin.b - fullLayout.margin.t) *
-                    ax.rangeslider.thickness + ax._boundingBox.height;
-            }
-
-            if(!avoid.side) avoid.side = 'bottom';
-        }
-        else {
-            counterAxis = (ax.anchor === 'free') ?
-                {_offset: gs.l + (ax.position || 0) * gs.w, _length: 0} :
-                axisIds.getFromId(gd, ax.anchor);
-
-            y = ax._offset + ax._length / 2;
-            x = counterAxis._offset + ((ax.side === 'right') ?
-                counterAxis._length + 10 +
-                    fontSize * (offsetBase + (ax.showticklabels ? 1 : 0.5)) :
-                -10 - fontSize * (offsetBase + (ax.showticklabels ? 0.5 : 0)));
-
-            transform = {rotate: '-90', offset: 0};
-            if(!avoid.side) avoid.side = 'left';
-        }
-
-        Titles.draw(gd, axid + 'title', {
-            propContainer: ax,
-            propName: ax._name + '.title',
-            dfltName: axLetter.toUpperCase() + ' axis',
-            avoid: avoid,
-            transform: transform,
-            attributes: {x: x, y: y, 'text-anchor': 'middle'}
+        ticksides.forEach(function(showside, sidei) {
+          var pos = linepositions[sidei], tsign = ticksign[sidei];
+          if (showside && isNumeric(pos)) {
+            tickpath += tickpathfn(pos + pad * tsign, tsign * ax.ticklen);
+          }
         });
-    }
-
-    function traceHasBarsOrFill(trace, subplot) {
-        if(trace.visible !== true || trace.xaxis + trace.yaxis !== subplot) return false;
-        if(Registry.traceIs(trace, 'bar') && trace.orientation === {x: 'h', y: 'v'}[axletter]) return true;
-        return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axletter;
-    }
-
-    function drawGrid(plotinfo, counteraxis, subplot) {
-        var gridcontainer = plotinfo.gridlayer,
-            zlcontainer = plotinfo.zerolinelayer,
-            gridvals = plotinfo['hidegrid' + axletter] ? [] : valsClipped,
-            gridpath = ax._gridpath ||
-                'M0,0' + ((axletter === 'x') ? 'v' : 'h') + counteraxis._length,
-            grid = gridcontainer.selectAll('path.' + gcls)
-                .data((ax.showgrid === false) ? [] : gridvals, datafn);
-        grid.enter().append('path').classed(gcls, 1)
-            .classed('crisp', 1)
-            .attr('d', gridpath)
-            .each(function(d) {
-                if(ax.zeroline && (ax.type === 'linear' || ax.type === '-') &&
-                        Math.abs(d.x) < ax.dtick / 100) {
-                    d3.select(this).remove();
-                }
-            });
-        grid.attr('transform', transfn)
-            .call(Color.stroke, ax.gridcolor || '#ddd')
-            .style('stroke-width', gridWidth + 'px');
-        grid.exit().remove();
-
-        // zero line
-        if(zlcontainer) {
-            var hasBarsOrFill = false;
-            for(var i = 0; i < gd._fullData.length; i++) {
-                if(traceHasBarsOrFill(gd._fullData[i], subplot)) {
-                    hasBarsOrFill = true;
-                    break;
-                }
-            }
-            var rng = Lib.simpleMap(ax.range, ax.r2l),
-                showZl = (rng[0] * rng[1] <= 0) && ax.zeroline &&
-                (ax.type === 'linear' || ax.type === '-') && gridvals.length &&
-                (hasBarsOrFill || clipEnds({x: 0}) || !ax.showline);
-            var zl = zlcontainer.selectAll('path.' + zcls)
-                .data(showZl ? [{x: 0}] : []);
-            zl.enter().append('path').classed(zcls, 1).classed('zl', 1)
-                .classed('crisp', 1)
-                .attr('d', gridpath);
-            zl.attr('transform', transfn)
-                .call(Color.stroke, ax.zerolinecolor || Color.defaultLine)
-                .style('stroke-width', zeroLineWidth + 'px');
-            zl.exit().remove();
-        }
-    }
-
-    if(independent) {
-        drawTicks(ax._axislayer, tickpathfn(ax._pos + pad * ticksign[2], ticksign[2] * ax.ticklen));
-        if(ax._counteraxis) {
-            var fictionalPlotinfo = {
-                gridlayer: ax._gridlayer,
-                zerolinelayer: ax._zerolinelayer
-            };
-            drawGrid(fictionalPlotinfo, ax._counteraxis);
-        }
-        return drawLabels(ax._axislayer, ax._pos);
-    }
-    else {
-        var alldone = axes.getSubplots(gd, ax).map(function(subplot) {
-            var plotinfo = fullLayout._plots[subplot];
 
-            if(!fullLayout._has('cartesian')) return;
-
-            var container = plotinfo[axletter + 'axislayer'],
-
-                // [bottom or left, top or right, free, main]
-                linepositions = ax._linepositions[subplot] || [],
-                counteraxis = plotinfo[counterLetter + 'axis'],
-                mainSubplot = counteraxis._id === ax.anchor,
-                ticksides = [false, false, false],
-                tickpath = '';
-
-            // ticks
-            if(ax.mirror === 'allticks') ticksides = [true, true, false];
-            else if(mainSubplot) {
-                if(ax.mirror === 'ticks') ticksides = [true, true, false];
-                else ticksides[sides.indexOf(axside)] = true;
-            }
-            if(ax.mirrors) {
-                for(i = 0; i < 2; i++) {
-                    var thisMirror = ax.mirrors[counteraxis._id + sides[i]];
-                    if(thisMirror === 'ticks' || thisMirror === 'labels') {
-                        ticksides[i] = true;
-                    }
-                }
-            }
+        drawTicks(container, tickpath);
+        drawGrid(plotinfo, counteraxis, subplot);
+        return drawLabels(container, linepositions[3]);
+      })
+      .filter(function(onedone) {
+        return onedone && onedone.then;
+      });
 
-            // free axis ticks
-            if(linepositions[2] !== undefined) ticksides[2] = true;
-
-            ticksides.forEach(function(showside, sidei) {
-                var pos = linepositions[sidei],
-                    tsign = ticksign[sidei];
-                if(showside && isNumeric(pos)) {
-                    tickpath += tickpathfn(pos + pad * tsign, tsign * ax.ticklen);
-                }
-            });
-
-            drawTicks(container, tickpath);
-            drawGrid(plotinfo, counteraxis, subplot);
-            return drawLabels(container, linepositions[3]);
-        }).filter(function(onedone) { return onedone && onedone.then; });
-
-        return alldone.length ? Promise.all(alldone) : 0;
-    }
+    return alldone.length ? Promise.all(alldone) : 0;
+  }
 };
 
 // swap all the presentation attributes of the axes showing these traces
 axes.swap = function(gd, traces) {
-    var axGroups = makeAxisGroups(gd, traces);
+  var axGroups = makeAxisGroups(gd, traces);
 
-    for(var i = 0; i < axGroups.length; i++) {
-        swapAxisGroup(gd, axGroups[i].x, axGroups[i].y);
-    }
+  for (var i = 0; i < axGroups.length; i++) {
+    swapAxisGroup(gd, axGroups[i].x, axGroups[i].y);
+  }
 };
 
 function makeAxisGroups(gd, traces) {
-    var groups = [],
-        i,
-        j;
-
-    for(i = 0; i < traces.length; i++) {
-        var groupsi = [],
-            xi = gd._fullData[traces[i]].xaxis,
-            yi = gd._fullData[traces[i]].yaxis;
-        if(!xi || !yi) continue; // not a 2D cartesian trace?
-
-        for(j = 0; j < groups.length; j++) {
-            if(groups[j].x.indexOf(xi) !== -1 || groups[j].y.indexOf(yi) !== -1) {
-                groupsi.push(j);
-            }
-        }
+  var groups = [], i, j;
 
-        if(!groupsi.length) {
-            groups.push({x: [xi], y: [yi]});
-            continue;
-        }
+  for (i = 0; i < traces.length; i++) {
+    var groupsi = [],
+      xi = gd._fullData[traces[i]].xaxis,
+      yi = gd._fullData[traces[i]].yaxis;
+    if (!xi || !yi) continue;
 
-        var group0 = groups[groupsi[0]],
-            groupj;
+    // not a 2D cartesian trace?
+    for (j = 0; j < groups.length; j++) {
+      if (groups[j].x.indexOf(xi) !== -1 || groups[j].y.indexOf(yi) !== -1) {
+        groupsi.push(j);
+      }
+    }
 
-        if(groupsi.length > 1) {
-            for(j = 1; j < groupsi.length; j++) {
-                groupj = groups[groupsi[j]];
-                mergeAxisGroups(group0.x, groupj.x);
-                mergeAxisGroups(group0.y, groupj.y);
-            }
-        }
-        mergeAxisGroups(group0.x, [xi]);
-        mergeAxisGroups(group0.y, [yi]);
+    if (!groupsi.length) {
+      groups.push({ x: [xi], y: [yi] });
+      continue;
+    }
+
+    var group0 = groups[groupsi[0]], groupj;
+
+    if (groupsi.length > 1) {
+      for (j = 1; j < groupsi.length; j++) {
+        groupj = groups[groupsi[j]];
+        mergeAxisGroups(group0.x, groupj.x);
+        mergeAxisGroups(group0.y, groupj.y);
+      }
     }
+    mergeAxisGroups(group0.x, [xi]);
+    mergeAxisGroups(group0.y, [yi]);
+  }
 
-    return groups;
+  return groups;
 }
 
 function mergeAxisGroups(intoSet, fromSet) {
-    for(var i = 0; i < fromSet.length; i++) {
-        if(intoSet.indexOf(fromSet[i]) === -1) intoSet.push(fromSet[i]);
-    }
+  for (var i = 0; i < fromSet.length; i++) {
+    if (intoSet.indexOf(fromSet[i]) === -1) intoSet.push(fromSet[i]);
+  }
 }
 
 function swapAxisGroup(gd, xIds, yIds) {
-    var i,
-        j,
-        xFullAxes = [],
-        yFullAxes = [],
-        layout = gd.layout;
-
-    for(i = 0; i < xIds.length; i++) xFullAxes.push(axes.getFromId(gd, xIds[i]));
-    for(i = 0; i < yIds.length; i++) yFullAxes.push(axes.getFromId(gd, yIds[i]));
-
-    var allAxKeys = Object.keys(xFullAxes[0]),
-        noSwapAttrs = [
-            'anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle'
-        ],
-        numericTypes = ['linear', 'log'];
-
-    for(i = 0; i < allAxKeys.length; i++) {
-        var keyi = allAxKeys[i],
-            xVal = xFullAxes[0][keyi],
-            yVal = yFullAxes[0][keyi],
-            allEqual = true,
-            coerceLinearX = false,
-            coerceLinearY = false;
-        if(keyi.charAt(0) === '_' || typeof xVal === 'function' ||
-                noSwapAttrs.indexOf(keyi) !== -1) {
-            continue;
-        }
-        for(j = 1; j < xFullAxes.length && allEqual; j++) {
-            var xVali = xFullAxes[j][keyi];
-            if(keyi === 'type' && numericTypes.indexOf(xVal) !== -1 &&
-                    numericTypes.indexOf(xVali) !== -1 && xVal !== xVali) {
-                // type is special - if we find a mixture of linear and log,
-                // coerce them all to linear on flipping
-                coerceLinearX = true;
-            }
-            else if(xVali !== xVal) allEqual = false;
-        }
-        for(j = 1; j < yFullAxes.length && allEqual; j++) {
-            var yVali = yFullAxes[j][keyi];
-            if(keyi === 'type' && numericTypes.indexOf(yVal) !== -1 &&
-                    numericTypes.indexOf(yVali) !== -1 && yVal !== yVali) {
-                // type is special - if we find a mixture of linear and log,
-                // coerce them all to linear on flipping
-                coerceLinearY = true;
-            }
-            else if(yFullAxes[j][keyi] !== yVal) allEqual = false;
-        }
-        if(allEqual) {
-            if(coerceLinearX) layout[xFullAxes[0]._name].type = 'linear';
-            if(coerceLinearY) layout[yFullAxes[0]._name].type = 'linear';
-            swapAxisAttrs(layout, keyi, xFullAxes, yFullAxes);
-        }
-    }
-
-    // now swap x&y for any annotations anchored to these x & y
-    for(i = 0; i < gd._fullLayout.annotations.length; i++) {
-        var ann = gd._fullLayout.annotations[i];
-        if(xIds.indexOf(ann.xref) !== -1 &&
-                yIds.indexOf(ann.yref) !== -1) {
-            Lib.swapAttrs(layout.annotations[i], ['?']);
-        }
-    }
+  var i, j, xFullAxes = [], yFullAxes = [], layout = gd.layout;
+
+  for (i = 0; i < xIds.length; i++) {
+    xFullAxes.push(axes.getFromId(gd, xIds[i]));
+  }
+  for (i = 0; i < yIds.length; i++) {
+    yFullAxes.push(axes.getFromId(gd, yIds[i]));
+  }
+
+  var allAxKeys = Object.keys(xFullAxes[0]),
+    noSwapAttrs = [
+      "anchor",
+      "domain",
+      "overlaying",
+      "position",
+      "side",
+      "tickangle"
+    ],
+    numericTypes = ["linear", "log"];
+
+  for (i = 0; i < allAxKeys.length; i++) {
+    var keyi = allAxKeys[i],
+      xVal = xFullAxes[0][keyi],
+      yVal = yFullAxes[0][keyi],
+      allEqual = true,
+      coerceLinearX = false,
+      coerceLinearY = false;
+    if (
+      keyi.charAt(0) === "_" ||
+        typeof xVal === "function" ||
+        noSwapAttrs.indexOf(keyi) !== -1
+    ) {
+      continue;
+    }
+    for (j = 1; j < xFullAxes.length && allEqual; j++) {
+      var xVali = xFullAxes[j][keyi];
+      if (
+        keyi === "type" &&
+          numericTypes.indexOf(xVal) !== -1 &&
+          numericTypes.indexOf(xVali) !== -1 &&
+          xVal !== xVali
+      ) {
+        // type is special - if we find a mixture of linear and log,
+        // coerce them all to linear on flipping
+        coerceLinearX = true;
+      } else if (xVali !== xVal) allEqual = false;
+    }
+    for (j = 1; j < yFullAxes.length && allEqual; j++) {
+      var yVali = yFullAxes[j][keyi];
+      if (
+        keyi === "type" &&
+          numericTypes.indexOf(yVal) !== -1 &&
+          numericTypes.indexOf(yVali) !== -1 &&
+          yVal !== yVali
+      ) {
+        // type is special - if we find a mixture of linear and log,
+        // coerce them all to linear on flipping
+        coerceLinearY = true;
+      } else if (yFullAxes[j][keyi] !== yVal) allEqual = false;
+    }
+    if (allEqual) {
+      if (coerceLinearX) layout[xFullAxes[0]._name].type = "linear";
+      if (coerceLinearY) layout[yFullAxes[0]._name].type = "linear";
+      swapAxisAttrs(layout, keyi, xFullAxes, yFullAxes);
+    }
+  }
+
+  // now swap x&y for any annotations anchored to these x & y
+  for (i = 0; i < gd._fullLayout.annotations.length; i++) {
+    var ann = gd._fullLayout.annotations[i];
+    if (xIds.indexOf(ann.xref) !== -1 && yIds.indexOf(ann.yref) !== -1) {
+      Lib.swapAttrs(layout.annotations[i], ["?"]);
+    }
+  }
 }
 
 function swapAxisAttrs(layout, key, xFullAxes, yFullAxes) {
-    // in case the value is the default for either axis,
-    // look at the first axis in each list and see if
-    // this key's value is undefined
-    var np = Lib.nestedProperty,
-        xVal = np(layout[xFullAxes[0]._name], key).get(),
-        yVal = np(layout[yFullAxes[0]._name], key).get(),
-        i;
-    if(key === 'title') {
-        // special handling of placeholder titles
-        if(xVal === 'Click to enter X axis title') {
-            xVal = 'Click to enter Y axis title';
-        }
-        if(yVal === 'Click to enter Y axis title') {
-            yVal = 'Click to enter X axis title';
-        }
-    }
-
-    for(i = 0; i < xFullAxes.length; i++) {
-        np(layout, xFullAxes[i]._name + '.' + key).set(yVal);
-    }
-    for(i = 0; i < yFullAxes.length; i++) {
-        np(layout, yFullAxes[i]._name + '.' + key).set(xVal);
-    }
+  // in case the value is the default for either axis,
+  // look at the first axis in each list and see if
+  // this key's value is undefined
+  var np = Lib.nestedProperty,
+    xVal = np(layout[xFullAxes[0]._name], key).get(),
+    yVal = np(layout[yFullAxes[0]._name], key).get(),
+    i;
+  if (key === "title") {
+    // special handling of placeholder titles
+    if (xVal === "Click to enter X axis title") {
+      xVal = "Click to enter Y axis title";
+    }
+    if (yVal === "Click to enter Y axis title") {
+      yVal = "Click to enter X axis title";
+    }
+  }
+
+  for (i = 0; i < xFullAxes.length; i++) {
+    np(layout, xFullAxes[i]._name + "." + key).set(yVal);
+  }
+  for (i = 0; i < yFullAxes.length; i++) {
+    np(layout, yFullAxes[i]._name + "." + key).set(xVal);
+  }
 }
diff --git a/src/plots/cartesian/axis_autotype.js b/src/plots/cartesian/axis_autotype.js
index 476c4ec4bff..dbc0714e9ca 100644
--- a/src/plots/cartesian/axis_autotype.js
+++ b/src/plots/cartesian/axis_autotype.js
@@ -5,32 +5,29 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var isNumeric = require("fast-isnumeric");
 
-
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-
-var Lib = require('../../lib');
-var BADNUM = require('../../constants/numerical').BADNUM;
+var Lib = require("../../lib");
+var BADNUM = require("../../constants/numerical").BADNUM;
 
 module.exports = function autoType(array, calendar) {
-    if(moreDates(array, calendar)) return 'date';
-    if(category(array)) return 'category';
-    if(linearOK(array)) return 'linear';
-    else return '-';
+  if (moreDates(array, calendar)) return "date";
+  if (category(array)) return "category";
+  if (linearOK(array)) return "linear";
+  else return "-";
 };
 
 // is there at least one number in array? If not, we should leave
 // ax.type empty so it can be autoset later
 function linearOK(array) {
-    if(!array) return false;
+  if (!array) return false;
 
-    for(var i = 0; i < array.length; i++) {
-        if(isNumeric(array[i])) return true;
-    }
+  for (var i = 0; i < array.length; i++) {
+    if (isNumeric(array[i])) return true;
+  }
 
-    return false;
+  return false;
 }
 
 // does the array a have mostly dates rather than numbers?
@@ -39,35 +36,35 @@ function linearOK(array) {
 // dates as non-dates, to exclude cases with mostly 2 & 4 digit
 // numbers and a few dates
 function moreDates(a, calendar) {
-    var dcnt = 0,
-        ncnt = 0,
-        // test at most 1000 points, evenly spaced
-        inc = Math.max(1, (a.length - 1) / 1000),
-        ai;
+  var dcnt = 0,
+    ncnt = 0,
+    // test at most 1000 points, evenly spaced
+    inc = Math.max(1, (a.length - 1) / 1000),
+    ai;
 
-    for(var i = 0; i < a.length; i += inc) {
-        ai = a[Math.round(i)];
-        if(Lib.isDateTime(ai, calendar)) dcnt += 1;
-        if(isNumeric(ai)) ncnt += 1;
-    }
+  for (var i = 0; i < a.length; i += inc) {
+    ai = a[Math.round(i)];
+    if (Lib.isDateTime(ai, calendar)) dcnt += 1;
+    if (isNumeric(ai)) ncnt += 1;
+  }
 
-    return (dcnt > ncnt * 2);
+  return dcnt > ncnt * 2;
 }
 
 // are the (x,y)-values in gd.data mostly text?
 // require twice as many categories as numbers
 function category(a) {
-    // test at most 1000 points
-    var inc = Math.max(1, (a.length - 1) / 1000),
-        curvenums = 0,
-        curvecats = 0,
-        ai;
+  // test at most 1000 points
+  var inc = Math.max(1, (a.length - 1) / 1000),
+    curvenums = 0,
+    curvecats = 0,
+    ai;
 
-    for(var i = 0; i < a.length; i += inc) {
-        ai = a[Math.round(i)];
-        if(Lib.cleanNumber(ai) !== BADNUM) curvenums++;
-        else if(typeof ai === 'string' && ai !== '' && ai !== 'None') curvecats++;
-    }
+  for (var i = 0; i < a.length; i += inc) {
+    ai = a[Math.round(i)];
+    if (Lib.cleanNumber(ai) !== BADNUM) curvenums++;
+    else if (typeof ai === "string" && ai !== "" && ai !== "None") curvecats++;
+  }
 
-    return curvecats > curvenums * 2;
+  return curvecats > curvenums * 2;
 }
diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js
index e4e99bf8294..93199e01e18 100644
--- a/src/plots/cartesian/axis_defaults.js
+++ b/src/plots/cartesian/axis_defaults.js
@@ -5,27 +5,23 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var isNumeric = require('fast-isnumeric');
-var colorMix = require('tinycolor2').mix;
-
-var Registry = require('../../registry');
-var Lib = require('../../lib');
-var lightFraction = require('../../components/color/attributes').lightFraction;
-
-var layoutAttributes = require('./layout_attributes');
-var handleTickValueDefaults = require('./tick_value_defaults');
-var handleTickMarkDefaults = require('./tick_mark_defaults');
-var handleTickLabelDefaults = require('./tick_label_defaults');
-var handleCategoryOrderDefaults = require('./category_order_defaults');
-var setConvert = require('./set_convert');
-var orderedCategories = require('./ordered_categories');
-var axisIds = require('./axis_ids');
-var autoType = require('./axis_autotype');
-
+"use strict";
+var isNumeric = require("fast-isnumeric");
+var colorMix = require("tinycolor2").mix;
+
+var Registry = require("../../registry");
+var Lib = require("../../lib");
+var lightFraction = require("../../components/color/attributes").lightFraction;
+
+var layoutAttributes = require("./layout_attributes");
+var handleTickValueDefaults = require("./tick_value_defaults");
+var handleTickMarkDefaults = require("./tick_mark_defaults");
+var handleTickLabelDefaults = require("./tick_label_defaults");
+var handleCategoryOrderDefaults = require("./category_order_defaults");
+var setConvert = require("./set_convert");
+var orderedCategories = require("./ordered_categories");
+var axisIds = require("./axis_ids");
+var autoType = require("./axis_autotype");
 
 /**
  * options: object containing:
@@ -40,193 +36,216 @@ var autoType = require('./axis_autotype');
  *  data: the plot data to use in choosing auto type
  *  bgColor: the plot background color, to calculate default gridline colors
  */
-module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, options) {
-    var letter = options.letter,
-        font = options.font || {},
-        defaultTitle = 'Click to enter ' +
-            (options.title || (letter.toUpperCase() + ' axis')) +
-            ' title';
-
-    function coerce2(attr, dflt) {
-        return Lib.coerce2(containerIn, containerOut, layoutAttributes, attr, dflt);
+module.exports = function handleAxisDefaults(
+  containerIn,
+  containerOut,
+  coerce,
+  options
+) {
+  var letter = options.letter,
+    font = options.font || {},
+    defaultTitle = "Click to enter " +
+      (options.title || letter.toUpperCase() + " axis") +
+      " title";
+
+  function coerce2(attr, dflt) {
+    return Lib.coerce2(containerIn, containerOut, layoutAttributes, attr, dflt);
+  }
+
+  // set up some private properties
+  if (options.name) {
+    containerOut._name = options.name;
+    containerOut._id = axisIds.name2id(options.name);
+  }
+
+  // now figure out type and do some more initialization
+  var axType = coerce("type");
+  if (axType === "-") {
+    setAutoType(containerOut, options.data);
+
+    if (containerOut.type === "-") {
+      containerOut.type = "linear";
+    } else {
+      // copy autoType back to input axis
+      // note that if this object didn't exist
+      // in the input layout, we have to put it in
+      // this happens in the main supplyDefaults function
+      axType = containerIn.type = containerOut.type;
     }
+  }
 
-    // set up some private properties
-    if(options.name) {
-        containerOut._name = options.name;
-        containerOut._id = axisIds.name2id(options.name);
-    }
-
-    // now figure out type and do some more initialization
-    var axType = coerce('type');
-    if(axType === '-') {
-        setAutoType(containerOut, options.data);
-
-        if(containerOut.type === '-') {
-            containerOut.type = 'linear';
-        }
-        else {
-            // copy autoType back to input axis
-            // note that if this object didn't exist
-            // in the input layout, we have to put it in
-            // this happens in the main supplyDefaults function
-            axType = containerIn.type = containerOut.type;
-        }
-    }
-
-    if(axType === 'date') {
-        var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults');
-        handleCalendarDefaults(containerIn, containerOut, 'calendar', options.calendar);
-    }
-
-    setConvert(containerOut);
-
-    var dfltColor = coerce('color');
-    // if axis.color was provided, use it for fonts too; otherwise,
-    // inherit from global font color in case that was provided.
-    var dfltFontColor = (dfltColor === containerIn.color) ? dfltColor : font.color;
-
-    coerce('title', defaultTitle);
-    Lib.coerceFont(coerce, 'titlefont', {
-        family: font.family,
-        size: Math.round(font.size * 1.2),
-        color: dfltFontColor
-    });
-
-    var validRange = (
-        (containerIn.range || []).length === 2 &&
-        isNumeric(containerOut.r2l(containerIn.range[0])) &&
-        isNumeric(containerOut.r2l(containerIn.range[1]))
+  if (axType === "date") {
+    var handleCalendarDefaults = Registry.getComponentMethod(
+      "calendars",
+      "handleDefaults"
+    );
+    handleCalendarDefaults(
+      containerIn,
+      containerOut,
+      "calendar",
+      options.calendar
+    );
+  }
+
+  setConvert(containerOut);
+
+  var dfltColor = coerce("color");
+  // if axis.color was provided, use it for fonts too; otherwise,
+  // inherit from global font color in case that was provided.
+  var dfltFontColor = dfltColor === containerIn.color ? dfltColor : font.color;
+
+  coerce("title", defaultTitle);
+  Lib.coerceFont(coerce, "titlefont", {
+    family: font.family,
+    size: Math.round(font.size * 1.2),
+    color: dfltFontColor
+  });
+
+  var validRange = (containerIn.range || []).length === 2 &&
+    isNumeric(containerOut.r2l(containerIn.range[0])) &&
+    isNumeric(containerOut.r2l(containerIn.range[1]));
+  var autoRange = coerce("autorange", !validRange);
+
+  if (autoRange) coerce("rangemode");
+
+  coerce("range");
+  containerOut.cleanRange();
+
+  handleTickValueDefaults(containerIn, containerOut, coerce, axType);
+  handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options);
+  handleTickMarkDefaults(containerIn, containerOut, coerce, options);
+  handleCategoryOrderDefaults(containerIn, containerOut, coerce);
+
+  var lineColor = coerce2("linecolor", dfltColor),
+    lineWidth = coerce2("linewidth"),
+    showLine = coerce("showline", !!lineColor || !!lineWidth);
+
+  if (!showLine) {
+    delete containerOut.linecolor;
+    delete containerOut.linewidth;
+  }
+
+  if (showLine || containerOut.ticks) coerce("mirror");
+
+  var gridColor = coerce2(
+    "gridcolor",
+    colorMix(dfltColor, options.bgColor, lightFraction).toRgbString()
+  ),
+    gridWidth = coerce2("gridwidth"),
+    showGridLines = coerce(
+      "showgrid",
+      options.showGrid || !!gridColor || !!gridWidth
     );
-    var autoRange = coerce('autorange', !validRange);
-
-    if(autoRange) coerce('rangemode');
-
-    coerce('range');
-    containerOut.cleanRange();
-
-    handleTickValueDefaults(containerIn, containerOut, coerce, axType);
-    handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options);
-    handleTickMarkDefaults(containerIn, containerOut, coerce, options);
-    handleCategoryOrderDefaults(containerIn, containerOut, coerce);
-
-    var lineColor = coerce2('linecolor', dfltColor),
-        lineWidth = coerce2('linewidth'),
-        showLine = coerce('showline', !!lineColor || !!lineWidth);
-
-    if(!showLine) {
-        delete containerOut.linecolor;
-        delete containerOut.linewidth;
-    }
-
-    if(showLine || containerOut.ticks) coerce('mirror');
-
-    var gridColor = coerce2('gridcolor', colorMix(dfltColor, options.bgColor, lightFraction).toRgbString()),
-        gridWidth = coerce2('gridwidth'),
-        showGridLines = coerce('showgrid', options.showGrid || !!gridColor || !!gridWidth);
-
-    if(!showGridLines) {
-        delete containerOut.gridcolor;
-        delete containerOut.gridwidth;
-    }
-
-    var zeroLineColor = coerce2('zerolinecolor', dfltColor),
-        zeroLineWidth = coerce2('zerolinewidth'),
-        showZeroLine = coerce('zeroline', options.showGrid || !!zeroLineColor || !!zeroLineWidth);
 
-    if(!showZeroLine) {
-        delete containerOut.zerolinecolor;
-        delete containerOut.zerolinewidth;
-    }
+  if (!showGridLines) {
+    delete containerOut.gridcolor;
+    delete containerOut.gridwidth;
+  }
 
-    // fill in categories
-    containerOut._initialCategories = axType === 'category' ?
-        orderedCategories(letter, containerOut.categoryorder, containerOut.categoryarray, options.data) :
-        [];
+  var zeroLineColor = coerce2("zerolinecolor", dfltColor),
+    zeroLineWidth = coerce2("zerolinewidth"),
+    showZeroLine = coerce(
+      "zeroline",
+      options.showGrid || !!zeroLineColor || !!zeroLineWidth
+    );
 
-    return containerOut;
+  if (!showZeroLine) {
+    delete containerOut.zerolinecolor;
+    delete containerOut.zerolinewidth;
+  }
+
+  // fill in categories
+  containerOut._initialCategories = axType === "category"
+    ? orderedCategories(
+        letter,
+        containerOut.categoryorder,
+        containerOut.categoryarray,
+        options.data
+      )
+    : [];
+
+  return containerOut;
 };
 
 function setAutoType(ax, data) {
-    // new logic: let people specify any type they want,
-    // only autotype if type is '-'
-    if(ax.type !== '-') return;
-
-    var id = ax._id,
-        axLetter = id.charAt(0);
-
-    // support 3d
-    if(id.indexOf('scene') !== -1) id = axLetter;
-
-    var d0 = getFirstNonEmptyTrace(data, id, axLetter);
-    if(!d0) return;
-
-    // first check for histograms, as the count direction
-    // should always default to a linear axis
-    if(d0.type === 'histogram' &&
-            axLetter === {v: 'y', h: 'x'}[d0.orientation || 'v']) {
-        ax.type = 'linear';
-        return;
+  // new logic: let people specify any type they want,
+  // only autotype if type is '-'
+  if (ax.type !== "-") return;
+
+  var id = ax._id, axLetter = id.charAt(0);
+
+  // support 3d
+  if (id.indexOf("scene") !== -1) id = axLetter;
+
+  var d0 = getFirstNonEmptyTrace(data, id, axLetter);
+  if (!d0) return;
+
+  // first check for histograms, as the count direction
+  // should always default to a linear axis
+  if (
+    d0.type === "histogram" &&
+      axLetter === ({ v: "y", h: "x" })[d0.orientation || "v"]
+  ) {
+    ax.type = "linear";
+    return;
+  }
+
+  var calAttr = axLetter + "calendar", calendar = d0[calAttr];
+
+  // check all boxes on this x axis to see
+  // if they're dates, numbers, or categories
+  if (isBoxWithoutPositionCoords(d0, axLetter)) {
+    var posLetter = getBoxPosLetter(d0), boxPositions = [], trace;
+
+    for (var i = 0; i < data.length; i++) {
+      trace = data[i];
+      if (
+        !Registry.traceIs(trace, "box") ||
+          (trace[axLetter + "axis"] || axLetter) !== id
+      ) {
+        continue;
+      }
+
+      if (trace[posLetter] !== undefined) {
+        boxPositions.push(trace[posLetter][0]);
+      } else if (trace.name !== undefined) boxPositions.push(trace.name);
+      else boxPositions.push("text");
+
+      if (trace[calAttr] !== calendar) calendar = undefined;
     }
 
-    var calAttr = axLetter + 'calendar',
-        calendar = d0[calAttr];
-
-    // check all boxes on this x axis to see
-    // if they're dates, numbers, or categories
-    if(isBoxWithoutPositionCoords(d0, axLetter)) {
-        var posLetter = getBoxPosLetter(d0),
-            boxPositions = [],
-            trace;
-
-        for(var i = 0; i < data.length; i++) {
-            trace = data[i];
-            if(!Registry.traceIs(trace, 'box') ||
-               (trace[axLetter + 'axis'] || axLetter) !== id) continue;
-
-            if(trace[posLetter] !== undefined) boxPositions.push(trace[posLetter][0]);
-            else if(trace.name !== undefined) boxPositions.push(trace.name);
-            else boxPositions.push('text');
-
-            if(trace[calAttr] !== calendar) calendar = undefined;
-        }
-
-        ax.type = autoType(boxPositions, calendar);
-    }
-    else {
-        ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']], calendar);
-    }
+    ax.type = autoType(boxPositions, calendar);
+  } else {
+    ax.type = autoType(d0[axLetter] || [d0[axLetter + "0"]], calendar);
+  }
 }
 
 function getBoxPosLetter(trace) {
-    return {v: 'x', h: 'y'}[trace.orientation || 'v'];
+  return ({ v: "x", h: "y" })[trace.orientation || "v"];
 }
 
 function isBoxWithoutPositionCoords(trace, axLetter) {
-    var posLetter = getBoxPosLetter(trace),
-        isBox = Registry.traceIs(trace, 'box'),
-        isCandlestick = Registry.traceIs(trace._fullInput || {}, 'candlestick');
-
-    return (
-        isBox &&
-        !isCandlestick &&
-        axLetter === posLetter &&
-        trace[posLetter] === undefined &&
-        trace[posLetter + '0'] === undefined
-    );
+  var posLetter = getBoxPosLetter(trace),
+    isBox = Registry.traceIs(trace, "box"),
+    isCandlestick = Registry.traceIs(trace._fullInput || {}, "candlestick");
+
+  return isBox &&
+    !isCandlestick &&
+    axLetter === posLetter &&
+    trace[posLetter] === undefined &&
+    trace[posLetter + "0"] === undefined;
 }
 
 function getFirstNonEmptyTrace(data, id, axLetter) {
-    for(var i = 0; i < data.length; i++) {
-        var trace = data[i];
-
-        if((trace[axLetter + 'axis'] || axLetter) === id) {
-            if(isBoxWithoutPositionCoords(trace, axLetter)) {
-                return trace;
-            }
-            else if((trace[axLetter] || []).length || trace[axLetter + '0']) {
-                return trace;
-            }
-        }
+  for (var i = 0; i < data.length; i++) {
+    var trace = data[i];
+
+    if ((trace[axLetter + "axis"] || axLetter) === id) {
+      if (isBoxWithoutPositionCoords(trace, axLetter)) {
+        return trace;
+      } else if ((trace[axLetter] || []).length || trace[axLetter + "0"]) {
+        return trace;
+      }
     }
+  }
 }
diff --git a/src/plots/cartesian/axis_ids.js b/src/plots/cartesian/axis_ids.js
index 63b9fae6e77..fe7212f6bd0 100644
--- a/src/plots/cartesian/axis_ids.js
+++ b/src/plots/cartesian/axis_ids.js
@@ -5,116 +5,107 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+var Registry = require("../../registry");
+var Plots = require("../plots");
+var Lib = require("../../lib");
 
-'use strict';
-
-var Registry = require('../../registry');
-var Plots = require('../plots');
-var Lib = require('../../lib');
-
-var constants = require('./constants');
-
+var constants = require("./constants");
 
 // convert between axis names (xaxis, xaxis2, etc, elements of gd.layout)
 // and axis id's (x, x2, etc). Would probably have ditched 'xaxis'
 // completely in favor of just 'x' if it weren't ingrained in the API etc.
 exports.id2name = function id2name(id) {
-    if(typeof id !== 'string' || !id.match(constants.AX_ID_PATTERN)) return;
-    var axNum = id.substr(1);
-    if(axNum === '1') axNum = '';
-    return id.charAt(0) + 'axis' + axNum;
+  if (typeof id !== "string" || !id.match(constants.AX_ID_PATTERN)) return;
+  var axNum = id.substr(1);
+  if (axNum === "1") axNum = "";
+  return id.charAt(0) + "axis" + axNum;
 };
 
 exports.name2id = function name2id(name) {
-    if(!name.match(constants.AX_NAME_PATTERN)) return;
-    var axNum = name.substr(5);
-    if(axNum === '1') axNum = '';
-    return name.charAt(0) + axNum;
+  if (!name.match(constants.AX_NAME_PATTERN)) return;
+  var axNum = name.substr(5);
+  if (axNum === "1") axNum = "";
+  return name.charAt(0) + axNum;
 };
 
 exports.cleanId = function cleanId(id, axLetter) {
-    if(!id.match(constants.AX_ID_PATTERN)) return;
-    if(axLetter && id.charAt(0) !== axLetter) return;
+  if (!id.match(constants.AX_ID_PATTERN)) return;
+  if (axLetter && id.charAt(0) !== axLetter) return;
 
-    var axNum = id.substr(1).replace(/^0+/, '');
-    if(axNum === '1') axNum = '';
-    return id.charAt(0) + axNum;
+  var axNum = id.substr(1).replace(/^0+/, "");
+  if (axNum === "1") axNum = "";
+  return id.charAt(0) + axNum;
 };
 
 // get all axis object names
 // optionally restricted to only x or y or z by string axLetter
 // and optionally 2D axes only, not those inside 3D scenes
 function listNames(gd, axLetter, only2d) {
-    var fullLayout = gd._fullLayout;
-    if(!fullLayout) return [];
-
-    function filterAxis(obj, extra) {
-        var keys = Object.keys(obj),
-            axMatch = /^[xyz]axis[0-9]*/,
-            out = [];
+  var fullLayout = gd._fullLayout;
+  if (!fullLayout) return [];
 
-        for(var i = 0; i < keys.length; i++) {
-            var k = keys[i];
-            if(axLetter && k.charAt(0) !== axLetter) continue;
-            if(axMatch.test(k)) out.push(extra + k);
-        }
+  function filterAxis(obj, extra) {
+    var keys = Object.keys(obj), axMatch = /^[xyz]axis[0-9]*/, out = [];
 
-        return out.sort();
+    for (var i = 0; i < keys.length; i++) {
+      var k = keys[i];
+      if (axLetter && k.charAt(0) !== axLetter) continue;
+      if (axMatch.test(k)) out.push(extra + k);
     }
 
-    var names = filterAxis(fullLayout, '');
-    if(only2d) return names;
+    return out.sort();
+  }
 
-    var sceneIds3D = Plots.getSubplotIds(fullLayout, 'gl3d') || [];
-    for(var i = 0; i < sceneIds3D.length; i++) {
-        var sceneId = sceneIds3D[i];
-        names = names.concat(
-            filterAxis(fullLayout[sceneId], sceneId + '.')
-        );
-    }
+  var names = filterAxis(fullLayout, "");
+  if (only2d) return names;
+
+  var sceneIds3D = Plots.getSubplotIds(fullLayout, "gl3d") || [];
+  for (var i = 0; i < sceneIds3D.length; i++) {
+    var sceneId = sceneIds3D[i];
+    names = names.concat(filterAxis(fullLayout[sceneId], sceneId + "."));
+  }
 
-    return names;
+  return names;
 }
 
 // get all axis objects, as restricted in listNames
 exports.list = function(gd, axletter, only2d) {
-    return listNames(gd, axletter, only2d)
-        .map(function(axName) {
-            return Lib.nestedProperty(gd._fullLayout, axName).get();
-        });
+  return listNames(gd, axletter, only2d).map(function(axName) {
+    return Lib.nestedProperty(gd._fullLayout, axName).get();
+  });
 };
 
 // get all axis ids, optionally restricted by letter
 // this only makes sense for 2d axes
 exports.listIds = function(gd, axletter) {
-    return listNames(gd, axletter, true).map(exports.name2id);
+  return listNames(gd, axletter, true).map(exports.name2id);
 };
 
 // get an axis object from its id 'x','x2' etc
 // optionally, id can be a subplot (ie 'x2y3') and type gets x or y from it
 exports.getFromId = function(gd, id, type) {
-    var fullLayout = gd._fullLayout;
+  var fullLayout = gd._fullLayout;
 
-    if(type === 'x') id = id.replace(/y[0-9]*/, '');
-    else if(type === 'y') id = id.replace(/x[0-9]*/, '');
+  if (type === "x") id = id.replace(/y[0-9]*/, "");
+  else if (type === "y") id = id.replace(/x[0-9]*/, "");
 
-    return fullLayout[exports.id2name(id)];
+  return fullLayout[exports.id2name(id)];
 };
 
 // get an axis object of specified type from the containing trace
 exports.getFromTrace = function(gd, fullTrace, type) {
-    var fullLayout = gd._fullLayout;
-    var ax = null;
-
-    if(Registry.traceIs(fullTrace, 'gl3d')) {
-        var scene = fullTrace.scene;
-        if(scene.substr(0, 5) === 'scene') {
-            ax = fullLayout[scene][type + 'axis'];
-        }
-    }
-    else {
-        ax = exports.getFromId(gd, fullTrace[type + 'axis'] || type);
+  var fullLayout = gd._fullLayout;
+  var ax = null;
+
+  if (Registry.traceIs(fullTrace, "gl3d")) {
+    var scene = fullTrace.scene;
+    if (scene.substr(0, 5) === "scene") {
+      ax = fullLayout[scene][type + "axis"];
     }
+  } else {
+    ax = exports.getFromId(gd, fullTrace[type + "axis"] || type);
+  }
 
-    return ax;
+  return ax;
 };
diff --git a/src/plots/cartesian/category_order_defaults.js b/src/plots/cartesian/category_order_defaults.js
index c115dd28c23..e82611c2b9a 100644
--- a/src/plots/cartesian/category_order_defaults.js
+++ b/src/plots/cartesian/category_order_defaults.js
@@ -5,28 +5,28 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
+"use strict";
+module.exports = function handleCategoryOrderDefaults(
+  containerIn,
+  containerOut,
+  coerce
+) {
+  if (containerOut.type !== "category") return;
 
-'use strict';
+  var arrayIn = containerIn.categoryarray, orderDefault;
 
+  var isValidArray = Array.isArray(arrayIn) && arrayIn.length > 0;
 
-module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, coerce) {
-    if(containerOut.type !== 'category') return;
+  // override default 'categoryorder' value when non-empty array is supplied
+  if (isValidArray) orderDefault = "array";
 
-    var arrayIn = containerIn.categoryarray,
-        orderDefault;
+  var order = coerce("categoryorder", orderDefault);
 
-    var isValidArray = (Array.isArray(arrayIn) && arrayIn.length > 0);
+  // coerce 'categoryarray' only in array order case
+  if (order === "array") coerce("categoryarray");
 
-    // override default 'categoryorder' value when non-empty array is supplied
-    if(isValidArray) orderDefault = 'array';
-
-    var order = coerce('categoryorder', orderDefault);
-
-    // coerce 'categoryarray' only in array order case
-    if(order === 'array') coerce('categoryarray');
-
-    // cannot set 'categoryorder' to 'array' with an invalid 'categoryarray'
-    if(!isValidArray && order === 'array') {
-        containerOut.categoryorder = 'trace';
-    }
+  // cannot set 'categoryorder' to 'array' with an invalid 'categoryarray'
+  if (!isValidArray && order === "array") {
+    containerOut.categoryorder = "trace";
+  }
 };
diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js
index 8d7ed20df45..c3643cab011 100644
--- a/src/plots/cartesian/constants.js
+++ b/src/plots/cartesian/constants.js
@@ -5,68 +5,48 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-'use strict';
-
-
+"use strict";
 module.exports = {
-
-    idRegex: {
-        x: /^x([2-9]|[1-9][0-9]+)?$/,
-        y: /^y([2-9]|[1-9][0-9]+)?$/
-    },
-
-    attrRegex: {
-        x: /^xaxis([2-9]|[1-9][0-9]+)?$/,
-        y: /^yaxis([2-9]|[1-9][0-9]+)?$/
-    },
-
-    // axis match regular expression
-    xAxisMatch: /^xaxis[0-9]*$/,
-    yAxisMatch: /^yaxis[0-9]*$/,
-
-    // pattern matching axis ids and names
-    AX_ID_PATTERN: /^[xyz][0-9]*$/,
-    AX_NAME_PATTERN: /^[xyz]axis[0-9]*$/,
-
-    // ms between first mousedown and 2nd mouseup to constitute dblclick...
-    // we don't seem to have access to the system setting
-    DBLCLICKDELAY: 300,
-
-    // pixels to move mouse before you stop clamping to starting point
-    MINDRAG: 8,
-
-    // smallest dimension allowed for a select box
-    MINSELECT: 12,
-
-    // smallest dimension allowed for a zoombox
-    MINZOOM: 20,
-
-    // width of axis drag regions
-    DRAGGERSIZE: 20,
-
-    // max pixels away from mouse to allow a point to highlight
-    MAXDIST: 20,
-
-    // hover labels for multiple horizontal bars get tilted by this angle
-    YANGLE: 60,
-
-    // size and display constants for hover text
-    HOVERARROWSIZE: 6, // pixel size of hover arrows
-    HOVERTEXTPAD: 3, // pixels padding around text
-    HOVERFONTSIZE: 13,
-    HOVERFONT: 'Arial, sans-serif',
-
-    // minimum time (msec) between hover calls
-    HOVERMINTIME: 50,
-
-    // max pixels off straight before a lasso select line counts as bent
-    BENDPX: 1.5,
-
-    // delay before a redraw (relayout) after smooth panning and zooming
-    REDRAWDELAY: 50,
-
-    // last resort axis ranges for x and y axes if we have no data
-    DFLTRANGEX: [-1, 6],
-    DFLTRANGEY: [-1, 4]
+  idRegex: { x: /^x([2-9]|[1-9][0-9]+)?$/, y: /^y([2-9]|[1-9][0-9]+)?$/ },
+  attrRegex: {
+    x: /^xaxis([2-9]|[1-9][0-9]+)?$/,
+    y: /^yaxis([2-9]|[1-9][0-9]+)?$/
+  },
+  // axis match regular expression
+  xAxisMatch: /^xaxis[0-9]*$/,
+  yAxisMatch: /^yaxis[0-9]*$/,
+  // pattern matching axis ids and names
+  AX_ID_PATTERN: /^[xyz][0-9]*$/,
+  AX_NAME_PATTERN: /^[xyz]axis[0-9]*$/,
+  // ms between first mousedown and 2nd mouseup to constitute dblclick...
+  // we don't seem to have access to the system setting
+  DBLCLICKDELAY: 300,
+  // pixels to move mouse before you stop clamping to starting point
+  MINDRAG: 8,
+  // smallest dimension allowed for a select box
+  MINSELECT: 12,
+  // smallest dimension allowed for a zoombox
+  MINZOOM: 20,
+  // width of axis drag regions
+  DRAGGERSIZE: 20,
+  // max pixels away from mouse to allow a point to highlight
+  MAXDIST: 20,
+  // hover labels for multiple horizontal bars get tilted by this angle
+  YANGLE: 60,
+  // size and display constants for hover text
+  HOVERARROWSIZE: 6,
+  // pixel size of hover arrows
+  HOVERTEXTPAD: 3,
+  // pixels padding around text
+  HOVERFONTSIZE: 13,
+  HOVERFONT: "Arial, sans-serif",
+  // minimum time (msec) between hover calls
+  HOVERMINTIME: 50,
+  // max pixels off straight before a lasso select line counts as bent
+  BENDPX: 1.5,
+  // delay before a redraw (relayout) after smooth panning and zooming
+  REDRAWDELAY: 50,
+  // last resort axis ranges for x and y axes if we have no data
+  DFLTRANGEX: [-1, 6],
+  DFLTRANGEY: [-1, 4]
 };
diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js
index 22a253f287e..fcc8808315e 100644
--- a/src/plots/cartesian/dragbox.js
+++ b/src/plots/cartesian/dragbox.js
@@ -5,26 +5,22 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var d3 = require('d3');
-var tinycolor = require('tinycolor2');
-
-var Plotly = require('../../plotly');
-var Registry = require('../../registry');
-var Lib = require('../../lib');
-var svgTextUtils = require('../../lib/svg_text_utils');
-var Color = require('../../components/color');
-var Drawing = require('../../components/drawing');
-var setCursor = require('../../lib/setcursor');
-var dragElement = require('../../components/dragelement');
-
-var Axes = require('./axes');
-var prepSelect = require('./select');
-var constants = require('./constants');
-
+"use strict";
+var d3 = require("d3");
+var tinycolor = require("tinycolor2");
+
+var Plotly = require("../../plotly");
+var Registry = require("../../registry");
+var Lib = require("../../lib");
+var svgTextUtils = require("../../lib/svg_text_utils");
+var Color = require("../../components/color");
+var Drawing = require("../../components/drawing");
+var setCursor = require("../../lib/setcursor");
+var dragElement = require("../../components/dragelement");
+
+var Axes = require("./axes");
+var prepSelect = require("./select");
+var constants = require("./constants");
 
 // flag for showing "doubleclick to zoom out" only at the beginning
 var SHOWZOOMOUTTIP = true;
@@ -39,725 +35,817 @@ var SHOWZOOMOUTTIP = true;
 //          'ns' - top and bottom together, difference unchanged
 //      ew - same for horizontal axis
 module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) {
-    // mouseDown stores ms of first mousedown event in the last
-    // DBLCLICKDELAY ms on the drag bars
-    // numClicks stores how many mousedowns have been seen
-    // within DBLCLICKDELAY so we can check for click or doubleclick events
-    // dragged stores whether a drag has occurred, so we don't have to
-    // redraw unnecessarily, ie if no move bigger than MINDRAG or MINZOOM px
-    var fullLayout = gd._fullLayout,
-        // if we're dragging two axes at once, also drag overlays
-        subplots = [plotinfo].concat((ns && ew) ? plotinfo.overlays : []),
-        xa = [plotinfo.xaxis],
-        ya = [plotinfo.yaxis],
-        pw = xa[0]._length,
-        ph = ya[0]._length,
-        MINDRAG = constants.MINDRAG,
-        MINZOOM = constants.MINZOOM,
-        isMainDrag = (ns + ew === 'nsew');
-
-    for(var i = 1; i < subplots.length; i++) {
-        var subplotXa = subplots[i].xaxis,
-            subplotYa = subplots[i].yaxis;
-        if(xa.indexOf(subplotXa) === -1) xa.push(subplotXa);
-        if(ya.indexOf(subplotYa) === -1) ya.push(subplotYa);
+  // mouseDown stores ms of first mousedown event in the last
+  // DBLCLICKDELAY ms on the drag bars
+  // numClicks stores how many mousedowns have been seen
+  // within DBLCLICKDELAY so we can check for click or doubleclick events
+  // dragged stores whether a drag has occurred, so we don't have to
+  // redraw unnecessarily, ie if no move bigger than MINDRAG or MINZOOM px
+  var fullLayout = gd._fullLayout,
+    // if we're dragging two axes at once, also drag overlays
+    subplots = [plotinfo].concat(ns && ew ? plotinfo.overlays : []),
+    xa = [plotinfo.xaxis],
+    ya = [plotinfo.yaxis],
+    pw = xa[0]._length,
+    ph = ya[0]._length,
+    MINDRAG = constants.MINDRAG,
+    MINZOOM = constants.MINZOOM,
+    isMainDrag = ns + ew === "nsew";
+
+  for (var i = 1; i < subplots.length; i++) {
+    var subplotXa = subplots[i].xaxis, subplotYa = subplots[i].yaxis;
+    if (xa.indexOf(subplotXa) === -1) xa.push(subplotXa);
+    if (ya.indexOf(subplotYa) === -1) ya.push(subplotYa);
+  }
+
+  function isDirectionActive(axList, activeVal) {
+    for (var i = 0; i < axList.length; i++) {
+      if (!axList[i].fixedrange) return activeVal;
     }
-
-    function isDirectionActive(axList, activeVal) {
-        for(var i = 0; i < axList.length; i++) {
-            if(!axList[i].fixedrange) return activeVal;
-        }
-        return '';
+    return "";
+  }
+
+  var allaxes = xa.concat(ya),
+    xActive = isDirectionActive(xa, ew),
+    yActive = isDirectionActive(ya, ns),
+    cursor = getDragCursor(yActive + xActive, fullLayout.dragmode),
+    dragClass = ns + ew + "drag";
+
+  var dragger3 = plotinfo.draglayer.selectAll("." + dragClass).data([0]);
+
+  dragger3
+    .enter()
+    .append("rect")
+    .classed("drag", true)
+    .classed(dragClass, true)
+    .style({ fill: "transparent", "stroke-width": 0 })
+    .attr("data-subplot", plotinfo.id);
+
+  dragger3.call(Drawing.setRect, x, y, w, h).call(setCursor, cursor);
+
+  var dragger = dragger3.node();
+
+  // still need to make the element if the axes are disabled
+  // but nuke its events (except for maindrag which needs them for hover)
+  // and stop there
+  if (!yActive && !xActive && !isSelectOrLasso(fullLayout.dragmode)) {
+    dragger.onmousedown = null;
+    dragger.style.pointerEvents = isMainDrag ? "all" : "none";
+    return dragger;
+  }
+
+  var dragOptions = {
+    element: dragger,
+    gd: gd,
+    plotinfo: plotinfo,
+    xaxes: xa,
+    yaxes: ya,
+    doubleclick: doubleClick,
+    prepFn: function(e, startX, startY) {
+      var dragModeNow = gd._fullLayout.dragmode;
+
+      if (isMainDrag) {
+        // main dragger handles all drag modes, and changes
+        // to pan (or to zoom if it already is pan) on shift
+        if (e.shiftKey) {
+          if (dragModeNow === "pan") dragModeNow = "zoom";
+          else dragModeNow = "pan";
+        }
+      } else {
+        // all other draggers just pan
+        dragModeNow = "pan";
+      }
+
+      if (dragModeNow === "lasso") dragOptions.minDrag = 1;
+      else dragOptions.minDrag = undefined;
+
+      if (dragModeNow === "zoom") {
+        dragOptions.moveFn = zoomMove;
+        dragOptions.doneFn = zoomDone;
+        zoomPrep(e, startX, startY);
+      } else if (dragModeNow === "pan") {
+        dragOptions.moveFn = plotDrag;
+        dragOptions.doneFn = dragDone;
+        clearSelect();
+      } else if (isSelectOrLasso(dragModeNow)) {
+        prepSelect(e, startX, startY, dragOptions, dragModeNow);
+      }
+    }
+  };
+
+  dragElement.init(dragOptions);
+
+  var zoomlayer = gd._fullLayout._zoomlayer,
+    xs = plotinfo.xaxis._offset,
+    ys = plotinfo.yaxis._offset,
+    x0,
+    y0,
+    box,
+    lum,
+    path0,
+    dimmed,
+    zoomMode,
+    zb,
+    corners;
+
+  function recomputeAxisLists() {
+    xa = [plotinfo.xaxis];
+    ya = [plotinfo.yaxis];
+    pw = xa[0]._length;
+    ph = ya[0]._length;
+
+    for (var i = 1; i < subplots.length; i++) {
+      var subplotXa = subplots[i].xaxis, subplotYa = subplots[i].yaxis;
+      if (xa.indexOf(subplotXa) === -1) xa.push(subplotXa);
+      if (ya.indexOf(subplotYa) === -1) ya.push(subplotYa);
+    }
+    allaxes = xa.concat(ya);
+    xActive = isDirectionActive(xa, ew);
+    yActive = isDirectionActive(ya, ns);
+    cursor = getDragCursor(yActive + xActive, fullLayout.dragmode);
+    xs = plotinfo.xaxis._offset;
+    ys = plotinfo.yaxis._offset;
+    dragOptions.xa = xa;
+    dragOptions.ya = ya;
+  }
+
+  function zoomPrep(e, startX, startY) {
+    var dragBBox = dragger.getBoundingClientRect();
+    x0 = startX - dragBBox.left;
+    y0 = startY - dragBBox.top;
+    box = { l: x0, r: x0, w: 0, t: y0, b: y0, h: 0 };
+    lum = gd._hmpixcount
+      ? gd._hmlumcount / gd._hmpixcount
+      : tinycolor(gd._fullLayout.plot_bgcolor).getLuminance();
+    path0 = "M0,0H" + pw + "V" + ph + "H0V0";
+    dimmed = false;
+    zoomMode = "xy";
+
+    zb = zoomlayer
+      .append("path")
+      .attr("class", "zoombox")
+      .style({
+        fill: lum > 0.2 ? "rgba(0,0,0,0)" : "rgba(255,255,255,0)",
+        "stroke-width": 0
+      })
+      .attr("transform", "translate(" + xs + ", " + ys + ")")
+      .attr("d", path0 + "Z");
+
+    corners = zoomlayer
+      .append("path")
+      .attr("class", "zoombox-corners")
+      .style({
+        fill: Color.background,
+        stroke: Color.defaultLine,
+        "stroke-width": 1,
+        opacity: 0
+      })
+      .attr("transform", "translate(" + xs + ", " + ys + ")")
+      .attr("d", "M0,0Z");
+
+    clearSelect();
+  }
+
+  function clearSelect() {
+    // until we get around to persistent selections, remove the outline
+    // here. The selection itself will be removed when the plot redraws
+    // at the end.
+    zoomlayer.selectAll(".select-outline").remove();
+  }
+
+  function zoomMove(dx0, dy0) {
+    if (gd._transitioningWithDuration) {
+      return false;
     }
 
-    var allaxes = xa.concat(ya),
-        xActive = isDirectionActive(xa, ew),
-        yActive = isDirectionActive(ya, ns),
-        cursor = getDragCursor(yActive + xActive, fullLayout.dragmode),
-        dragClass = ns + ew + 'drag';
+    var x1 = Math.max(0, Math.min(pw, dx0 + x0)),
+      y1 = Math.max(0, Math.min(ph, dy0 + y0)),
+      dx = Math.abs(x1 - x0),
+      dy = Math.abs(y1 - y0),
+      clen = Math.floor(Math.min(dy, dx, MINZOOM) / 2);
+
+    box.l = Math.min(x0, x1);
+    box.r = Math.max(x0, x1);
+    box.t = Math.min(y0, y1);
+    box.b = Math.max(y0, y1);
+
+    // look for small drags in one direction or the other,
+    // and only drag the other axis
+    if (!yActive || dy < Math.min(Math.max(dx * 0.6, MINDRAG), MINZOOM)) {
+      if (dx < MINDRAG) {
+        zoomMode = "";
+        box.r = box.l;
+        box.t = box.b;
+        corners.attr("d", "M0,0Z");
+      } else {
+        box.t = 0;
+        box.b = ph;
+        zoomMode = "x";
+        corners.attr(
+          "d",
+          "M" +
+            (box.l - 0.5) +
+            "," +
+            (y0 - MINZOOM - 0.5) +
+            "h-3v" +
+            (2 * MINZOOM + 1) +
+            "h3ZM" +
+            (box.r + 0.5) +
+            "," +
+            (y0 - MINZOOM - 0.5) +
+            "h3v" +
+            (2 * MINZOOM + 1) +
+            "h-3Z"
+        );
+      }
+    } else if (!xActive || dx < Math.min(dy * 0.6, MINZOOM)) {
+      box.l = 0;
+      box.r = pw;
+      zoomMode = "y";
+      corners.attr(
+        "d",
+        "M" +
+          (x0 - MINZOOM - 0.5) +
+          "," +
+          (box.t - 0.5) +
+          "v-3h" +
+          (2 * MINZOOM + 1) +
+          "v3ZM" +
+          (x0 - MINZOOM - 0.5) +
+          "," +
+          (box.b + 0.5) +
+          "v3h" +
+          (2 * MINZOOM + 1) +
+          "v-3Z"
+      );
+    } else {
+      zoomMode = "xy";
+      corners.attr(
+        "d",
+        "M" +
+          (box.l - 3.5) +
+          "," +
+          (box.t - 0.5 + clen) +
+          "h3v" +
+          (-clen) +
+          "h" +
+          clen +
+          "v-3h-" +
+          (clen + 3) +
+          "ZM" +
+          (box.r + 3.5) +
+          "," +
+          (box.t - 0.5 + clen) +
+          "h-3v" +
+          (-clen) +
+          "h" +
+          (-clen) +
+          "v-3h" +
+          (clen + 3) +
+          "ZM" +
+          (box.r + 3.5) +
+          "," +
+          (box.b + 0.5 - clen) +
+          "h-3v" +
+          clen +
+          "h" +
+          (-clen) +
+          "v3h" +
+          (clen + 3) +
+          "ZM" +
+          (box.l - 3.5) +
+          "," +
+          (box.b + 0.5 - clen) +
+          "h3v" +
+          clen +
+          "h" +
+          clen +
+          "v3h-" +
+          (clen + 3) +
+          "Z"
+      );
+    }
+    box.w = box.r - box.l;
+    box.h = box.b - box.t;
+
+    // Not sure about the addition of window.scrollX/Y...
+    // seems to work but doesn't seem robust.
+    zb.attr(
+      "d",
+      path0 +
+        "M" +
+        box.l +
+        "," +
+        box.t +
+        "v" +
+        box.h +
+        "h" +
+        box.w +
+        "v-" +
+        box.h +
+        "h-" +
+        box.w +
+        "Z"
+    );
+    if (!dimmed) {
+      zb
+        .transition()
+        .style("fill", lum > 0.2 ? "rgba(0,0,0,0.4)" : "rgba(255,255,255,0.3)")
+        .duration(200);
+      corners.transition().style("opacity", 1).duration(200);
+      dimmed = true;
+    }
+  }
 
-    var dragger3 = plotinfo.draglayer.selectAll('.' + dragClass).data([0]);
+  function zoomAxRanges(axList, r0Fraction, r1Fraction) {
+    var i, axi, axRangeLinear0, axRangeLinearSpan;
 
-    dragger3.enter().append('rect')
-        .classed('drag', true)
-        .classed(dragClass, true)
-        .style({fill: 'transparent', 'stroke-width': 0})
-        .attr('data-subplot', plotinfo.id);
+    for (i = 0; i < axList.length; i++) {
+      axi = axList[i];
+      if (axi.fixedrange) continue;
 
-    dragger3.call(Drawing.setRect, x, y, w, h)
-        .call(setCursor, cursor);
+      axRangeLinear0 = axi._rl[0];
+      axRangeLinearSpan = axi._rl[1] - axRangeLinear0;
+      axi.range = [
+        axi.l2r(axRangeLinear0 + axRangeLinearSpan * r0Fraction),
+        axi.l2r(axRangeLinear0 + axRangeLinearSpan * r1Fraction)
+      ];
+    }
+  }
 
-    var dragger = dragger3.node();
+  function zoomDone(dragged, numClicks) {
+    if (Math.min(box.h, box.w) < MINDRAG * 2) {
+      if (numClicks === 2) doubleClick();
 
-    // still need to make the element if the axes are disabled
-    // but nuke its events (except for maindrag which needs them for hover)
-    // and stop there
-    if(!yActive && !xActive && !isSelectOrLasso(fullLayout.dragmode)) {
-        dragger.onmousedown = null;
-        dragger.style.pointerEvents = isMainDrag ? 'all' : 'none';
-        return dragger;
+      return removeZoombox(gd);
     }
 
-    var dragOptions = {
-        element: dragger,
-        gd: gd,
-        plotinfo: plotinfo,
-        xaxes: xa,
-        yaxes: ya,
-        doubleclick: doubleClick,
-        prepFn: function(e, startX, startY) {
-            var dragModeNow = gd._fullLayout.dragmode;
-
-            if(isMainDrag) {
-                // main dragger handles all drag modes, and changes
-                // to pan (or to zoom if it already is pan) on shift
-                if(e.shiftKey) {
-                    if(dragModeNow === 'pan') dragModeNow = 'zoom';
-                    else dragModeNow = 'pan';
-                }
-            }
-            // all other draggers just pan
-            else dragModeNow = 'pan';
-
-            if(dragModeNow === 'lasso') dragOptions.minDrag = 1;
-            else dragOptions.minDrag = undefined;
-
-            if(dragModeNow === 'zoom') {
-                dragOptions.moveFn = zoomMove;
-                dragOptions.doneFn = zoomDone;
-                zoomPrep(e, startX, startY);
-            }
-            else if(dragModeNow === 'pan') {
-                dragOptions.moveFn = plotDrag;
-                dragOptions.doneFn = dragDone;
-                clearSelect();
-            }
-            else if(isSelectOrLasso(dragModeNow)) {
-                prepSelect(e, startX, startY, dragOptions, dragModeNow);
-            }
-        }
-    };
-
-    dragElement.init(dragOptions);
-
-    var zoomlayer = gd._fullLayout._zoomlayer,
-        xs = plotinfo.xaxis._offset,
-        ys = plotinfo.yaxis._offset,
-        x0,
-        y0,
-        box,
-        lum,
-        path0,
-        dimmed,
-        zoomMode,
-        zb,
-        corners;
-
-    function recomputeAxisLists() {
-        xa = [plotinfo.xaxis];
-        ya = [plotinfo.yaxis];
-        pw = xa[0]._length;
-        ph = ya[0]._length;
-
-        for(var i = 1; i < subplots.length; i++) {
-            var subplotXa = subplots[i].xaxis,
-                subplotYa = subplots[i].yaxis;
-            if(xa.indexOf(subplotXa) === -1) xa.push(subplotXa);
-            if(ya.indexOf(subplotYa) === -1) ya.push(subplotYa);
-        }
-        allaxes = xa.concat(ya);
-        xActive = isDirectionActive(xa, ew);
-        yActive = isDirectionActive(ya, ns);
-        cursor = getDragCursor(yActive + xActive, fullLayout.dragmode);
-        xs = plotinfo.xaxis._offset;
-        ys = plotinfo.yaxis._offset;
-        dragOptions.xa = xa;
-        dragOptions.ya = ya;
+    if (zoomMode === "xy" || zoomMode === "x") {
+      zoomAxRanges(xa, box.l / pw, box.r / pw);
+    }
+    if (zoomMode === "xy" || zoomMode === "y") {
+      zoomAxRanges(ya, (ph - box.b) / ph, (ph - box.t) / ph);
     }
 
-    function zoomPrep(e, startX, startY) {
-        var dragBBox = dragger.getBoundingClientRect();
-        x0 = startX - dragBBox.left;
-        y0 = startY - dragBBox.top;
-        box = {l: x0, r: x0, w: 0, t: y0, b: y0, h: 0};
-        lum = gd._hmpixcount ?
-            (gd._hmlumcount / gd._hmpixcount) :
-            tinycolor(gd._fullLayout.plot_bgcolor).getLuminance();
-        path0 = 'M0,0H' + pw + 'V' + ph + 'H0V0';
-        dimmed = false;
-        zoomMode = 'xy';
-
-        zb = zoomlayer.append('path')
-            .attr('class', 'zoombox')
-            .style({
-                'fill': lum > 0.2 ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)',
-                'stroke-width': 0
-            })
-            .attr('transform', 'translate(' + xs + ', ' + ys + ')')
-            .attr('d', path0 + 'Z');
-
-        corners = zoomlayer.append('path')
-            .attr('class', 'zoombox-corners')
-            .style({
-                fill: Color.background,
-                stroke: Color.defaultLine,
-                'stroke-width': 1,
-                opacity: 0
-            })
-            .attr('transform', 'translate(' + xs + ', ' + ys + ')')
-            .attr('d', 'M0,0Z');
+    removeZoombox(gd);
+    dragTail(zoomMode);
 
-        clearSelect();
+    if (SHOWZOOMOUTTIP && gd.data && gd._context.showTips) {
+      Lib.notifier("Double-click to
zoom back out", "long");
+      SHOWZOOMOUTTIP = false;
     }
-
-    function clearSelect() {
-        // until we get around to persistent selections, remove the outline
-        // here. The selection itself will be removed when the plot redraws
-        // at the end.
-        zoomlayer.selectAll('.select-outline').remove();
+  }
+
+  function dragDone(dragged, numClicks) {
+    var singleEnd = (ns + ew).length === 1;
+    if (dragged) {
+      dragTail();
+    } else if (numClicks === 2 && !singleEnd) {
+      doubleClick();
+    } else if (numClicks === 1 && singleEnd) {
+      var ax = ns ? ya[0] : xa[0],
+        end = ns === "s" || ew === "w" ? 0 : 1,
+        attrStr = ax._name + ".range[" + end + "]",
+        initialText = getEndText(ax, end),
+        hAlign = "left",
+        vAlign = "middle";
+
+      if (ax.fixedrange) return;
+
+      if (ns) {
+        vAlign = ns === "n" ? "top" : "bottom";
+        if (ax.side === "right") hAlign = "right";
+      } else if (ew === "e") hAlign = "right";
+
+      dragger3
+        .call(svgTextUtils.makeEditable, null, {
+          immediate: true,
+          background: fullLayout.paper_bgcolor,
+          text: String(initialText),
+          fill: ax.tickfont ? ax.tickfont.color : "#444",
+          horizontalAlign: hAlign,
+          verticalAlign: vAlign
+        })
+        .on("edit", function(text) {
+          var v = ax.d2r(text);
+          if (v !== undefined) {
+            Plotly.relayout(gd, attrStr, v);
+          }
+        });
     }
-
-    function zoomMove(dx0, dy0) {
-        if(gd._transitioningWithDuration) {
-            return false;
-        }
-
-        var x1 = Math.max(0, Math.min(pw, dx0 + x0)),
-            y1 = Math.max(0, Math.min(ph, dy0 + y0)),
-            dx = Math.abs(x1 - x0),
-            dy = Math.abs(y1 - y0),
-            clen = Math.floor(Math.min(dy, dx, MINZOOM) / 2);
-
-        box.l = Math.min(x0, x1);
-        box.r = Math.max(x0, x1);
-        box.t = Math.min(y0, y1);
-        box.b = Math.max(y0, y1);
-
-        // look for small drags in one direction or the other,
-        // and only drag the other axis
-        if(!yActive || dy < Math.min(Math.max(dx * 0.6, MINDRAG), MINZOOM)) {
-            if(dx < MINDRAG) {
-                zoomMode = '';
-                box.r = box.l;
-                box.t = box.b;
-                corners.attr('d', 'M0,0Z');
-            }
-            else {
-                box.t = 0;
-                box.b = ph;
-                zoomMode = 'x';
-                corners.attr('d',
-                    'M' + (box.l - 0.5) + ',' + (y0 - MINZOOM - 0.5) +
-                    'h-3v' + (2 * MINZOOM + 1) + 'h3ZM' +
-                    (box.r + 0.5) + ',' + (y0 - MINZOOM - 0.5) +
-                    'h3v' + (2 * MINZOOM + 1) + 'h-3Z');
-            }
-        }
-        else if(!xActive || dx < Math.min(dy * 0.6, MINZOOM)) {
-            box.l = 0;
-            box.r = pw;
-            zoomMode = 'y';
-            corners.attr('d',
-                'M' + (x0 - MINZOOM - 0.5) + ',' + (box.t - 0.5) +
-                'v-3h' + (2 * MINZOOM + 1) + 'v3ZM' +
-                (x0 - MINZOOM - 0.5) + ',' + (box.b + 0.5) +
-                'v3h' + (2 * MINZOOM + 1) + 'v-3Z');
-        }
-        else {
-            zoomMode = 'xy';
-            corners.attr('d',
-                'M' + (box.l - 3.5) + ',' + (box.t - 0.5 + clen) + 'h3v' + (-clen) +
-                        'h' + clen + 'v-3h-' + (clen + 3) + 'ZM' +
-                    (box.r + 3.5) + ',' + (box.t - 0.5 + clen) + 'h-3v' + (-clen) +
-                        'h' + (-clen) + 'v-3h' + (clen + 3) + 'ZM' +
-                    (box.r + 3.5) + ',' + (box.b + 0.5 - clen) + 'h-3v' + clen +
-                        'h' + (-clen) + 'v3h' + (clen + 3) + 'ZM' +
-                    (box.l - 3.5) + ',' + (box.b + 0.5 - clen) + 'h3v' + clen +
-                        'h' + clen + 'v3h-' + (clen + 3) + 'Z');
-        }
-        box.w = box.r - box.l;
-        box.h = box.b - box.t;
-
-        // Not sure about the addition of window.scrollX/Y...
-        // seems to work but doesn't seem robust.
-        zb.attr('d',
-            path0 + 'M' + (box.l) + ',' + (box.t) + 'v' + (box.h) +
-            'h' + (box.w) + 'v-' + (box.h) + 'h-' + (box.w) + 'Z');
-        if(!dimmed) {
-            zb.transition()
-                .style('fill', lum > 0.2 ? 'rgba(0,0,0,0.4)' :
-                    'rgba(255,255,255,0.3)')
-                .duration(200);
-            corners.transition()
-                .style('opacity', 1)
-                .duration(200);
-            dimmed = true;
-        }
+  }
+
+  // scroll zoom, on all draggers except corners
+  var scrollViewBox = [0, 0, pw, ph],
+    // wait a little after scrolling before redrawing
+    redrawTimer = null,
+    REDRAWDELAY = constants.REDRAWDELAY,
+    mainplot = plotinfo.mainplot
+      ? fullLayout._plots[plotinfo.mainplot]
+      : plotinfo;
+
+  function zoomWheel(e) {
+    // deactivate mousewheel scrolling on embedded graphs
+    // devs can override this with layout._enablescrollzoom,
+    // but _ ensures this setting won't leave their page
+    if (!gd._context.scrollZoom && !fullLayout._enablescrollzoom) {
+      return;
     }
 
-    function zoomAxRanges(axList, r0Fraction, r1Fraction) {
-        var i,
-            axi,
-            axRangeLinear0,
-            axRangeLinearSpan;
-
-        for(i = 0; i < axList.length; i++) {
-            axi = axList[i];
-            if(axi.fixedrange) continue;
-
-            axRangeLinear0 = axi._rl[0];
-            axRangeLinearSpan = axi._rl[1] - axRangeLinear0;
-            axi.range = [
-                axi.l2r(axRangeLinear0 + axRangeLinearSpan * r0Fraction),
-                axi.l2r(axRangeLinear0 + axRangeLinearSpan * r1Fraction)
-            ];
-        }
+    // If a transition is in progress, then disable any behavior:
+    if (gd._transitioningWithDuration) {
+      return Lib.pauseEvent(e);
     }
 
-    function zoomDone(dragged, numClicks) {
-        if(Math.min(box.h, box.w) < MINDRAG * 2) {
-            if(numClicks === 2) doubleClick();
+    var pc = gd.querySelector(".plotly");
 
-            return removeZoombox(gd);
-        }
+    recomputeAxisLists();
 
-        if(zoomMode === 'xy' || zoomMode === 'x') zoomAxRanges(xa, box.l / pw, box.r / pw);
-        if(zoomMode === 'xy' || zoomMode === 'y') zoomAxRanges(ya, (ph - box.b) / ph, (ph - box.t) / ph);
+    // if the plot has scrollbars (more than a tiny excess)
+    // disable scrollzoom too.
+    if (
+      pc.scrollHeight - pc.clientHeight > 10 ||
+        pc.scrollWidth - pc.clientWidth > 10
+    ) {
+      return;
+    }
 
-        removeZoombox(gd);
-        dragTail(zoomMode);
+    clearTimeout(redrawTimer);
 
-        if(SHOWZOOMOUTTIP && gd.data && gd._context.showTips) {
-            Lib.notifier('Double-click to
zoom back out', 'long');
-            SHOWZOOMOUTTIP = false;
-        }
+    var wheelDelta = -e.deltaY;
+    if (!isFinite(wheelDelta)) wheelDelta = e.wheelDelta / 10;
+    if (!isFinite(wheelDelta)) {
+      Lib.log("Did not find wheel motion attributes: ", e);
+      return;
     }
 
-    function dragDone(dragged, numClicks) {
-        var singleEnd = (ns + ew).length === 1;
-        if(dragged) dragTail();
-        else if(numClicks === 2 && !singleEnd) doubleClick();
-        else if(numClicks === 1 && singleEnd) {
-            var ax = ns ? ya[0] : xa[0],
-                end = (ns === 's' || ew === 'w') ? 0 : 1,
-                attrStr = ax._name + '.range[' + end + ']',
-                initialText = getEndText(ax, end),
-                hAlign = 'left',
-                vAlign = 'middle';
-
-            if(ax.fixedrange) return;
-
-            if(ns) {
-                vAlign = (ns === 'n') ? 'top' : 'bottom';
-                if(ax.side === 'right') hAlign = 'right';
-            }
-            else if(ew === 'e') hAlign = 'right';
-
-            dragger3
-                .call(svgTextUtils.makeEditable, null, {
-                    immediate: true,
-                    background: fullLayout.paper_bgcolor,
-                    text: String(initialText),
-                    fill: ax.tickfont ? ax.tickfont.color : '#444',
-                    horizontalAlign: hAlign,
-                    verticalAlign: vAlign
-                })
-                .on('edit', function(text) {
-                    var v = ax.d2r(text);
-                    if(v !== undefined) {
-                        Plotly.relayout(gd, attrStr, v);
-                    }
-                });
-        }
+    var zoom = Math.exp((-Math.min(Math.max(wheelDelta, -20), 20)) / 100),
+      gbb = mainplot.draglayer
+        .select(".nsewdrag")
+        .node()
+        .getBoundingClientRect(),
+      xfrac = (e.clientX - gbb.left) / gbb.width,
+      vbx0 = scrollViewBox[0] + scrollViewBox[2] * xfrac,
+      yfrac = (gbb.bottom - e.clientY) / gbb.height,
+      vby0 = scrollViewBox[1] + scrollViewBox[3] * (1 - yfrac),
+      i;
+
+    function zoomWheelOneAxis(ax, centerFraction, zoom) {
+      if (ax.fixedrange) return;
+
+      var axRange = Lib.simpleMap(ax.range, ax.r2l),
+        v0 = axRange[0] + (axRange[1] - axRange[0]) * centerFraction;
+      function doZoom(v) {
+        return ax.l2r(v0 + (v - v0) * zoom);
+      }
+      ax.range = axRange.map(doZoom);
     }
 
-    // scroll zoom, on all draggers except corners
-    var scrollViewBox = [0, 0, pw, ph],
-        // wait a little after scrolling before redrawing
-        redrawTimer = null,
-        REDRAWDELAY = constants.REDRAWDELAY,
-        mainplot = plotinfo.mainplot ?
-            fullLayout._plots[plotinfo.mainplot] : plotinfo;
-
-    function zoomWheel(e) {
-        // deactivate mousewheel scrolling on embedded graphs
-        // devs can override this with layout._enablescrollzoom,
-        // but _ ensures this setting won't leave their page
-        if(!gd._context.scrollZoom && !fullLayout._enablescrollzoom) {
-            return;
-        }
-
-        // If a transition is in progress, then disable any behavior:
-        if(gd._transitioningWithDuration) {
-            return Lib.pauseEvent(e);
-        }
-
-        var pc = gd.querySelector('.plotly');
-
-        recomputeAxisLists();
-
-        // if the plot has scrollbars (more than a tiny excess)
-        // disable scrollzoom too.
-        if(pc.scrollHeight - pc.clientHeight > 10 ||
-                pc.scrollWidth - pc.clientWidth > 10) {
-            return;
-        }
-
-        clearTimeout(redrawTimer);
-
-        var wheelDelta = -e.deltaY;
-        if(!isFinite(wheelDelta)) wheelDelta = e.wheelDelta / 10;
-        if(!isFinite(wheelDelta)) {
-            Lib.log('Did not find wheel motion attributes: ', e);
-            return;
-        }
-
-        var zoom = Math.exp(-Math.min(Math.max(wheelDelta, -20), 20) / 100),
-            gbb = mainplot.draglayer.select('.nsewdrag')
-                .node().getBoundingClientRect(),
-            xfrac = (e.clientX - gbb.left) / gbb.width,
-            vbx0 = scrollViewBox[0] + scrollViewBox[2] * xfrac,
-            yfrac = (gbb.bottom - e.clientY) / gbb.height,
-            vby0 = scrollViewBox[1] + scrollViewBox[3] * (1 - yfrac),
-            i;
-
-        function zoomWheelOneAxis(ax, centerFraction, zoom) {
-            if(ax.fixedrange) return;
-
-            var axRange = Lib.simpleMap(ax.range, ax.r2l),
-                v0 = axRange[0] + (axRange[1] - axRange[0]) * centerFraction;
-            function doZoom(v) { return ax.l2r(v0 + (v - v0) * zoom); }
-            ax.range = axRange.map(doZoom);
-        }
-
-        if(ew) {
-            for(i = 0; i < xa.length; i++) zoomWheelOneAxis(xa[i], xfrac, zoom);
-            scrollViewBox[2] *= zoom;
-            scrollViewBox[0] = vbx0 - scrollViewBox[2] * xfrac;
-        }
-        if(ns) {
-            for(i = 0; i < ya.length; i++) zoomWheelOneAxis(ya[i], yfrac, zoom);
-            scrollViewBox[3] *= zoom;
-            scrollViewBox[1] = vby0 - scrollViewBox[3] * (1 - yfrac);
-        }
-
-        // viewbox redraw at first
-        updateSubplots(scrollViewBox);
-        ticksAndAnnotations(ns, ew);
-
-        // then replot after a delay to make sure
-        // no more scrolling is coming
-        redrawTimer = setTimeout(function() {
-            scrollViewBox = [0, 0, pw, ph];
-            dragTail();
-        }, REDRAWDELAY);
-
-        return Lib.pauseEvent(e);
+    if (ew) {
+      for (i = 0; i < xa.length; i++) {
+        zoomWheelOneAxis(xa[i], xfrac, zoom);
+      }
+      scrollViewBox[2] *= zoom;
+      scrollViewBox[0] = vbx0 - scrollViewBox[2] * xfrac;
     }
-
-    // everything but the corners gets wheel zoom
-    if(ns.length * ew.length !== 1) {
-        // still seems to be some confusion about onwheel vs onmousewheel...
-        if(dragger.onwheel !== undefined) dragger.onwheel = zoomWheel;
-        else if(dragger.onmousewheel !== undefined) dragger.onmousewheel = zoomWheel;
+    if (ns) {
+      for (i = 0; i < ya.length; i++) {
+        zoomWheelOneAxis(ya[i], yfrac, zoom);
+      }
+      scrollViewBox[3] *= zoom;
+      scrollViewBox[1] = vby0 - scrollViewBox[3] * (1 - yfrac);
     }
 
-    // plotDrag: move the plot in response to a drag
-    function plotDrag(dx, dy) {
-        // If a transition is in progress, then disable any behavior:
-        if(gd._transitioningWithDuration) {
-            return;
-        }
-
-        recomputeAxisLists();
-
-        function dragAxList(axList, pix) {
-            for(var i = 0; i < axList.length; i++) {
-                var axi = axList[i];
-                if(!axi.fixedrange) {
-                    axi.range = [
-                        axi.l2r(axi._rl[0] - pix / axi._m),
-                        axi.l2r(axi._rl[1] - pix / axi._m)
-                    ];
-                }
-            }
-        }
-
-        if(xActive === 'ew' || yActive === 'ns') {
-            if(xActive) dragAxList(xa, dx);
-            if(yActive) dragAxList(ya, dy);
-            updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]);
-            ticksAndAnnotations(yActive, xActive);
-            return;
-        }
-
-        // common transform for dragging one end of an axis
-        // d>0 is compressing scale (cursor is over the plot,
-        //  the axis end should move with the cursor)
-        // d<0 is expanding (cursor is off the plot, axis end moves
-        //  nonlinearly so you can expand far)
-        function dZoom(d) {
-            return 1 - ((d >= 0) ? Math.min(d, 0.9) :
-                1 / (1 / Math.max(d, -0.3) + 3.222));
-        }
-
-        // dz: set a new value for one end (0 or 1) of an axis array axArray,
-        // and return a pixel shift for that end for the viewbox
-        // based on pixel drag distance d
-        // TODO: this makes (generally non-fatal) errors when you get
-        // near floating point limits
-        function dz(axArray, end, d) {
-            var otherEnd = 1 - end,
-                movedAx,
-                newLinearizedEnd;
-            for(var i = 0; i < axArray.length; i++) {
-                var axi = axArray[i];
-                if(axi.fixedrange) continue;
-                movedAx = axi;
-                newLinearizedEnd = axi._rl[otherEnd] +
-                    (axi._rl[end] - axi._rl[otherEnd]) / dZoom(d / axi._length);
-                var newEnd = axi.l2r(newLinearizedEnd);
-
-                // if l2r comes back false or undefined, it means we've dragged off
-                // the end of valid ranges - so stop.
-                if(newEnd !== false && newEnd !== undefined) axi.range[end] = newEnd;
-            }
-            return movedAx._length * (movedAx._rl[end] - newLinearizedEnd) /
-                (movedAx._rl[end] - movedAx._rl[otherEnd]);
-        }
-
-        if(xActive === 'w') dx = dz(xa, 0, dx);
-        else if(xActive === 'e') dx = dz(xa, 1, -dx);
-        else if(!xActive) dx = 0;
-
-        if(yActive === 'n') dy = dz(ya, 1, dy);
-        else if(yActive === 's') dy = dz(ya, 0, -dy);
-        else if(!yActive) dy = 0;
-
-        updateSubplots([
-            (xActive === 'w') ? dx : 0,
-            (yActive === 'n') ? dy : 0,
-            pw - dx,
-            ph - dy
-        ]);
-        ticksAndAnnotations(yActive, xActive);
+    // viewbox redraw at first
+    updateSubplots(scrollViewBox);
+    ticksAndAnnotations(ns, ew);
+
+    // then replot after a delay to make sure
+    // no more scrolling is coming
+    redrawTimer = setTimeout(
+      function() {
+        scrollViewBox = [0, 0, pw, ph];
+        dragTail();
+      },
+      REDRAWDELAY
+    );
+
+    return Lib.pauseEvent(e);
+  }
+
+  // everything but the corners gets wheel zoom
+  if (ns.length * ew.length !== 1) {
+    // still seems to be some confusion about onwheel vs onmousewheel...
+    if (dragger.onwheel !== undefined) {
+      dragger.onwheel = zoomWheel;
+    } else if (dragger.onmousewheel !== undefined) {
+      dragger.onmousewheel = zoomWheel;
     }
+  }
 
-    function ticksAndAnnotations(ns, ew) {
-        var activeAxIds = [],
-            i;
-
-        function pushActiveAxIds(axList) {
-            for(i = 0; i < axList.length; i++) {
-                if(!axList[i].fixedrange) activeAxIds.push(axList[i]._id);
-            }
-        }
+  // plotDrag: move the plot in response to a drag
+  function plotDrag(dx, dy) {
+    // If a transition is in progress, then disable any behavior:
+    if (gd._transitioningWithDuration) {
+      return;
+    }
 
-        if(ew) pushActiveAxIds(xa);
-        if(ns) pushActiveAxIds(ya);
+    recomputeAxisLists();
 
-        for(i = 0; i < activeAxIds.length; i++) {
-            Axes.doTicks(gd, activeAxIds[i], true);
+    function dragAxList(axList, pix) {
+      for (var i = 0; i < axList.length; i++) {
+        var axi = axList[i];
+        if (!axi.fixedrange) {
+          axi.range = [
+            axi.l2r(axi._rl[0] - pix / axi._m),
+            axi.l2r(axi._rl[1] - pix / axi._m)
+          ];
         }
+      }
+    }
 
-        function redrawObjs(objArray, method) {
-            for(i = 0; i < objArray.length; i++) {
-                var obji = objArray[i];
+    if (xActive === "ew" || yActive === "ns") {
+      if (xActive) dragAxList(xa, dx);
+      if (yActive) dragAxList(ya, dy);
+      updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]);
+      ticksAndAnnotations(yActive, xActive);
+      return;
+    }
 
-                if((ew && activeAxIds.indexOf(obji.xref) !== -1) ||
-                    (ns && activeAxIds.indexOf(obji.yref) !== -1)) {
-                    method(gd, i);
-                }
-            }
-        }
+    // common transform for dragging one end of an axis
+    // d>0 is compressing scale (cursor is over the plot,
+    //  the axis end should move with the cursor)
+    // d<0 is expanding (cursor is off the plot, axis end moves
+    //  nonlinearly so you can expand far)
+    function dZoom(d) {
+      return 1 -
+        (d >= 0 ? Math.min(d, 0.9) : 1 / (1 / Math.max(d, -0.3) + 3.222));
+    }
 
-        // annotations and shapes 'draw' method is slow,
-        // use the finer-grained 'drawOne' method instead
+    // dz: set a new value for one end (0 or 1) of an axis array axArray,
+    // and return a pixel shift for that end for the viewbox
+    // based on pixel drag distance d
+    // TODO: this makes (generally non-fatal) errors when you get
+    // near floating point limits
+    function dz(axArray, end, d) {
+      var otherEnd = 1 - end, movedAx, newLinearizedEnd;
+      for (var i = 0; i < axArray.length; i++) {
+        var axi = axArray[i];
+        if (axi.fixedrange) continue;
+        movedAx = axi;
+        newLinearizedEnd = axi._rl[otherEnd] +
+          (axi._rl[end] - axi._rl[otherEnd]) / dZoom(d / axi._length);
+        var newEnd = axi.l2r(newLinearizedEnd);
+
+        // if l2r comes back false or undefined, it means we've dragged off
+        // the end of valid ranges - so stop.
+        if (newEnd !== false && newEnd !== undefined) axi.range[end] = newEnd;
+      }
+      return movedAx._length *
+        (movedAx._rl[end] - newLinearizedEnd) /
+        (movedAx._rl[end] - movedAx._rl[otherEnd]);
+    }
 
-        redrawObjs(fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne'));
-        redrawObjs(fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne'));
-        redrawObjs(fullLayout.images || [], Registry.getComponentMethod('images', 'draw'));
+    if (xActive === "w") dx = dz(xa, 0, dx);
+    else if (xActive === "e") dx = dz(xa, 1, -dx);
+    else if (!xActive) dx = 0;
+
+    if (yActive === "n") dy = dz(ya, 1, dy);
+    else if (yActive === "s") dy = dz(ya, 0, -dy);
+    else if (!yActive) dy = 0;
+
+    updateSubplots([
+      xActive === "w" ? dx : 0,
+      yActive === "n" ? dy : 0,
+      pw - dx,
+      ph - dy
+    ]);
+    ticksAndAnnotations(yActive, xActive);
+  }
+
+  function ticksAndAnnotations(ns, ew) {
+    var activeAxIds = [], i;
+
+    function pushActiveAxIds(axList) {
+      for (i = 0; i < axList.length; i++) {
+        if (!axList[i].fixedrange) activeAxIds.push(axList[i]._id);
+      }
     }
 
-    function doubleClick() {
-        if(gd._transitioningWithDuration) return;
+    if (ew) pushActiveAxIds(xa);
+    if (ns) pushActiveAxIds(ya);
 
-        var doubleClickConfig = gd._context.doubleClick,
-            axList = (xActive ? xa : []).concat(yActive ? ya : []),
-            attrs = {};
+    for (i = 0; i < activeAxIds.length; i++) {
+      Axes.doTicks(gd, activeAxIds[i], true);
+    }
 
-        var ax, i, rangeInitial;
+    function redrawObjs(objArray, method) {
+      for (i = 0; i < objArray.length; i++) {
+        var obji = objArray[i];
 
-        if(doubleClickConfig === 'autosize') {
-            for(i = 0; i < axList.length; i++) {
-                ax = axList[i];
-                if(!ax.fixedrange) attrs[ax._name + '.autorange'] = true;
-            }
-        }
-        else if(doubleClickConfig === 'reset') {
-            for(i = 0; i < axList.length; i++) {
-                ax = axList[i];
-
-                if(!ax._rangeInitial) {
-                    attrs[ax._name + '.autorange'] = true;
-                }
-                else {
-                    rangeInitial = ax._rangeInitial.slice();
-                    attrs[ax._name + '.range[0]'] = rangeInitial[0];
-                    attrs[ax._name + '.range[1]'] = rangeInitial[1];
-                }
-            }
+        if (
+          ew && activeAxIds.indexOf(obji.xref) !== -1 ||
+            ns && activeAxIds.indexOf(obji.yref) !== -1
+        ) {
+          method(gd, i);
         }
-        else if(doubleClickConfig === 'reset+autosize') {
-            for(i = 0; i < axList.length; i++) {
-                ax = axList[i];
-
-                if(ax.fixedrange) continue;
-                if(ax._rangeInitial === undefined ||
-                    ax.range[0] === ax._rangeInitial[0] &&
-                    ax.range[1] === ax._rangeInitial[1]
-                ) {
-                    attrs[ax._name + '.autorange'] = true;
-                }
-                else {
-                    rangeInitial = ax._rangeInitial.slice();
-                    attrs[ax._name + '.range[0]'] = rangeInitial[0];
-                    attrs[ax._name + '.range[1]'] = rangeInitial[1];
-                }
-            }
-        }
-
-        gd.emit('plotly_doubleclick', null);
-        Plotly.relayout(gd, attrs);
+      }
     }
 
-    // dragTail - finish a drag event with a redraw
-    function dragTail(zoommode) {
-        var attrs = {};
-        // revert to the previous axis settings, then apply the new ones
-        // through relayout - this lets relayout manage undo/redo
-        for(var i = 0; i < allaxes.length; i++) {
-            var axi = allaxes[i];
-            if(zoommode && zoommode.indexOf(axi._id.charAt(0)) === -1) {
-                continue;
-            }
-            if(axi._r[0] !== axi.range[0]) attrs[axi._name + '.range[0]'] = axi.range[0];
-            if(axi._r[1] !== axi.range[1]) attrs[axi._name + '.range[1]'] = axi.range[1];
-
-            axi.range = axi._r.slice();
-        }
+    // annotations and shapes 'draw' method is slow,
+    // use the finer-grained 'drawOne' method instead
+    redrawObjs(
+      fullLayout.annotations || [],
+      Registry.getComponentMethod("annotations", "drawOne")
+    );
+    redrawObjs(
+      fullLayout.shapes || [],
+      Registry.getComponentMethod("shapes", "drawOne")
+    );
+    redrawObjs(
+      fullLayout.images || [],
+      Registry.getComponentMethod("images", "draw")
+    );
+  }
+
+  function doubleClick() {
+    if (gd._transitioningWithDuration) return;
+
+    var doubleClickConfig = gd._context.doubleClick,
+      axList = (xActive ? xa : []).concat(yActive ? ya : []),
+      attrs = {};
+
+    var ax, i, rangeInitial;
+
+    if (doubleClickConfig === "autosize") {
+      for (i = 0; i < axList.length; i++) {
+        ax = axList[i];
+        if (!ax.fixedrange) attrs[ax._name + ".autorange"] = true;
+      }
+    } else if (doubleClickConfig === "reset") {
+      for (i = 0; i < axList.length; i++) {
+        ax = axList[i];
+
+        if (!ax._rangeInitial) {
+          attrs[ax._name + ".autorange"] = true;
+        } else {
+          rangeInitial = ax._rangeInitial.slice();
+          attrs[ax._name + ".range[0]"] = rangeInitial[0];
+          attrs[ax._name + ".range[1]"] = rangeInitial[1];
+        }
+      }
+    } else if (doubleClickConfig === "reset+autosize") {
+      for (i = 0; i < axList.length; i++) {
+        ax = axList[i];
+
+        if (ax.fixedrange) continue;
+        if (
+          ax._rangeInitial === undefined ||
+            ax.range[0] === ax._rangeInitial[0] &&
+              ax.range[1] === ax._rangeInitial[1]
+        ) {
+          attrs[ax._name + ".autorange"] = true;
+        } else {
+          rangeInitial = ax._rangeInitial.slice();
+          attrs[ax._name + ".range[0]"] = rangeInitial[0];
+          attrs[ax._name + ".range[1]"] = rangeInitial[1];
+        }
+      }
+    }
 
-        updateSubplots([0, 0, pw, ph]);
-        Plotly.relayout(gd, attrs);
+    gd.emit("plotly_doubleclick", null);
+    Plotly.relayout(gd, attrs);
+  }
+
+  // dragTail - finish a drag event with a redraw
+  function dragTail(zoommode) {
+    var attrs = {};
+    // revert to the previous axis settings, then apply the new ones
+    // through relayout - this lets relayout manage undo/redo
+    for (var i = 0; i < allaxes.length; i++) {
+      var axi = allaxes[i];
+      if (zoommode && zoommode.indexOf(axi._id.charAt(0)) === -1) {
+        continue;
+      }
+      if (axi._r[0] !== axi.range[0]) {
+        attrs[axi._name + ".range[0]"] = axi.range[0];
+      }
+      if (axi._r[1] !== axi.range[1]) {
+        attrs[axi._name + ".range[1]"] = axi.range[1];
+      }
+
+      axi.range = axi._r.slice();
     }
 
-    // updateSubplots - find all plot viewboxes that should be
-    // affected by this drag, and update them. look for all plots
-    // sharing an affected axis (including the one being dragged)
-    function updateSubplots(viewBox) {
-        var j;
-        var plotinfos = fullLayout._plots,
-            subplots = Object.keys(plotinfos);
-
-        for(var i = 0; i < subplots.length; i++) {
-
-            var subplot = plotinfos[subplots[i]],
-                xa2 = subplot.xaxis,
-                ya2 = subplot.yaxis,
-                editX = ew && !xa2.fixedrange,
-                editY = ns && !ya2.fixedrange;
-
-            if(editX) {
-                var isInX = false;
-                for(j = 0; j < xa.length; j++) {
-                    if(xa[j]._id === xa2._id) {
-                        isInX = true;
-                        break;
-                    }
-                }
-                editX = editX && isInX;
-            }
-
-            if(editY) {
-                var isInY = false;
-                for(j = 0; j < ya.length; j++) {
-                    if(ya[j]._id === ya2._id) {
-                        isInY = true;
-                        break;
-                    }
-                }
-                editY = editY && isInY;
-            }
-
-            var xScaleFactor = editX ? xa2._length / viewBox[2] : 1,
-                yScaleFactor = editY ? ya2._length / viewBox[3] : 1;
-
-            var clipDx = editX ? viewBox[0] : 0,
-                clipDy = editY ? viewBox[1] : 0;
-
-            var fracDx = editX ? (viewBox[0] / viewBox[2] * xa2._length) : 0,
-                fracDy = editY ? (viewBox[1] / viewBox[3] * ya2._length) : 0;
-
-            var plotDx = xa2._offset - fracDx,
-                plotDy = ya2._offset - fracDy;
-
-            fullLayout._defs.selectAll('#' + subplot.clipId)
-                .call(Drawing.setTranslate, clipDx, clipDy)
-                .call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor);
-
-            subplot.plot
-                .call(Drawing.setTranslate, plotDx, plotDy)
-                .call(Drawing.setScale, xScaleFactor, yScaleFactor)
-
-                // This is specifically directed at scatter traces, applying an inverse
-                // scale to individual points to counteract the scale of the trace
-                // as a whole:
-                .select('.scatterlayer').selectAll('.points').selectAll('.point')
-                    .call(Drawing.setPointGroupScale, 1 / xScaleFactor, 1 / yScaleFactor);
-        }
+    updateSubplots([0, 0, pw, ph]);
+    Plotly.relayout(gd, attrs);
+  }
+
+  // updateSubplots - find all plot viewboxes that should be
+  // affected by this drag, and update them. look for all plots
+  // sharing an affected axis (including the one being dragged)
+  function updateSubplots(viewBox) {
+    var j;
+    var plotinfos = fullLayout._plots, subplots = Object.keys(plotinfos);
+
+    for (var i = 0; i < subplots.length; i++) {
+      var subplot = plotinfos[subplots[i]],
+        xa2 = subplot.xaxis,
+        ya2 = subplot.yaxis,
+        editX = ew && !xa2.fixedrange,
+        editY = ns && !ya2.fixedrange;
+
+      if (editX) {
+        var isInX = false;
+        for (j = 0; j < xa.length; j++) {
+          if (xa[j]._id === xa2._id) {
+            isInX = true;
+            break;
+          }
+        }
+        editX = editX && isInX;
+      }
+
+      if (editY) {
+        var isInY = false;
+        for (j = 0; j < ya.length; j++) {
+          if (ya[j]._id === ya2._id) {
+            isInY = true;
+            break;
+          }
+        }
+        editY = editY && isInY;
+      }
+
+      var xScaleFactor = editX ? xa2._length / viewBox[2] : 1,
+        yScaleFactor = editY ? ya2._length / viewBox[3] : 1;
+
+      var clipDx = editX ? viewBox[0] : 0, clipDy = editY ? viewBox[1] : 0;
+
+      var fracDx = editX ? viewBox[0] / viewBox[2] * xa2._length : 0,
+        fracDy = editY ? viewBox[1] / viewBox[3] * ya2._length : 0;
+
+      var plotDx = xa2._offset - fracDx, plotDy = ya2._offset - fracDy;
+
+      fullLayout._defs
+        .selectAll("#" + subplot.clipId)
+        .call(Drawing.setTranslate, clipDx, clipDy)
+        .call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor);
+
+      subplot.plot
+        .call(Drawing.setTranslate, plotDx, plotDy)
+        .call(Drawing.setScale, xScaleFactor, yScaleFactor)
+        .select(".scatterlayer")
+        .selectAll(".points")
+        .selectAll(".point")
+        .call(Drawing.setPointGroupScale, 1 / xScaleFactor, 1 / yScaleFactor);
     }
+  }
 
-    return dragger;
+  return dragger;
 };
 
 function getEndText(ax, end) {
-    var initialVal = ax.range[end],
-        diff = Math.abs(initialVal - ax.range[1 - end]),
-        dig;
-
-    // TODO: this should basically be ax.r2d but we're doing extra
-    // rounding here... can we clean up at all?
-    if(ax.type === 'date') {
-        return initialVal;
-    }
-    else if(ax.type === 'log') {
-        dig = Math.ceil(Math.max(0, -Math.log(diff) / Math.LN10)) + 3;
-        return d3.format('.' + dig + 'g')(Math.pow(10, initialVal));
-    }
-    else { // linear numeric (or category... but just show numbers here)
-        dig = Math.floor(Math.log(Math.abs(initialVal)) / Math.LN10) -
-            Math.floor(Math.log(diff) / Math.LN10) + 4;
-        return d3.format('.' + String(dig) + 'g')(initialVal);
-    }
+  var initialVal = ax.range[end],
+    diff = Math.abs(initialVal - ax.range[1 - end]),
+    dig;
+
+  // TODO: this should basically be ax.r2d but we're doing extra
+  // rounding here... can we clean up at all?
+  if (ax.type === "date") {
+    return initialVal;
+  } else if (ax.type === "log") {
+    dig = Math.ceil(Math.max(0, (-Math.log(diff)) / Math.LN10)) + 3;
+    return d3.format("." + dig + "g")(Math.pow(10, initialVal));
+  } else {
+    // linear numeric (or category... but just show numbers here)
+    dig = Math.floor(Math.log(Math.abs(initialVal)) / Math.LN10) -
+      Math.floor(Math.log(diff) / Math.LN10) +
+      4;
+    return d3.format("." + String(dig) + "g")(initialVal);
+  }
 }
 
 function getDragCursor(nsew, dragmode) {
-    if(!nsew) return 'pointer';
-    if(nsew === 'nsew') {
-        if(dragmode === 'pan') return 'move';
-        return 'crosshair';
-    }
-    return nsew.toLowerCase() + '-resize';
+  if (!nsew) return "pointer";
+  if (nsew === "nsew") {
+    if (dragmode === "pan") return "move";
+    return "crosshair";
+  }
+  return nsew.toLowerCase() + "-resize";
 }
 
 function removeZoombox(gd) {
-    d3.select(gd)
-        .selectAll('.zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners')
-        .remove();
+  d3
+    .select(gd)
+    .selectAll(
+      ".zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners"
+    )
+    .remove();
 }
 
 function isSelectOrLasso(dragmode) {
-    var modes = ['lasso', 'select'];
+  var modes = ["lasso", "select"];
 
-    return modes.indexOf(dragmode) !== -1;
+  return modes.indexOf(dragmode) !== -1;
 }
diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js
index 7fea9cb893e..ffcd6639b15 100644
--- a/src/plots/cartesian/graph_interact.js
+++ b/src/plots/cartesian/graph_interact.js
@@ -5,28 +5,24 @@
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
-
-
-'use strict';
-
-var d3 = require('d3');
-var tinycolor = require('tinycolor2');
-var isNumeric = require('fast-isnumeric');
-
-var Lib = require('../../lib');
-var Events = require('../../lib/events');
-var svgTextUtils = require('../../lib/svg_text_utils');
-var Color = require('../../components/color');
-var Drawing = require('../../components/drawing');
-var dragElement = require('../../components/dragelement');
-var overrideCursor = require('../../lib/override_cursor');
-var Registry = require('../../registry');
-
-var Axes = require('./axes');
-var constants = require('./constants');
-var dragBox = require('./dragbox');
-var layoutAttributes = require('../layout_attributes');
-
+"use strict";
+var d3 = require("d3");
+var tinycolor = require("tinycolor2");
+var isNumeric = require("fast-isnumeric");
+
+var Lib = require("../../lib");
+var Events = require("../../lib/events");
+var svgTextUtils = require("../../lib/svg_text_utils");
+var Color = require("../../components/color");
+var Drawing = require("../../components/drawing");
+var dragElement = require("../../components/dragelement");
+var overrideCursor = require("../../lib/override_cursor");
+var Registry = require("../../registry");
+
+var Axes = require("./axes");
+var constants = require("./constants");
+var dragBox = require("./dragbox");
+var layoutAttributes = require("../layout_attributes");
 
 var fx = module.exports = {};
 
@@ -35,201 +31,271 @@ var fx = module.exports = {};
 fx.unhover = dragElement.unhover;
 
 fx.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) {
-
-    function coerce(attr, dflt) {
-        return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
-    }
-
-    coerce('dragmode');
-
-    var hovermodeDflt;
-    if(layoutOut._has('cartesian')) {
-        // flag for 'horizontal' plots:
-        // determines the state of the mode bar 'compare' hovermode button
-        var isHoriz = layoutOut._isHoriz = fx.isHoriz(fullData);
-        hovermodeDflt = isHoriz ? 'y' : 'x';
-    }
-    else hovermodeDflt = 'closest';
-
-    coerce('hovermode', hovermodeDflt);
+  function coerce(attr, dflt) {
+    return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
+  }
+
+  coerce("dragmode");
+
+  var hovermodeDflt;
+  if (layoutOut._has("cartesian")) {
+    // flag for 'horizontal' plots:
+    // determines the state of the mode bar 'compare' hovermode button
+    var isHoriz = layoutOut._isHoriz = fx.isHoriz(fullData);
+    hovermodeDflt = isHoriz ? "y" : "x";
+  } else {
+    hovermodeDflt = "closest";
+  }
+
+  coerce("hovermode", hovermodeDflt);
 };
 
 fx.isHoriz = function(fullData) {
-    var isHoriz = true;
+  var isHoriz = true;
 
-    for(var i = 0; i < fullData.length; i++) {
-        var trace = fullData[i];
+  for (var i = 0; i < fullData.length; i++) {
+    var trace = fullData[i];
 
-        if(trace.orientation !== 'h') {
-            isHoriz = false;
-            break;
-        }
+    if (trace.orientation !== "h") {
+      isHoriz = false;
+      break;
     }
+  }
 
-    return isHoriz;
+  return isHoriz;
 };
 
 fx.init = function(gd) {
-    var fullLayout = gd._fullLayout;
-
-    if(!fullLayout._has('cartesian') || gd._context.staticPlot) return;
-
-    var subplots = Object.keys(fullLayout._plots || {}).sort(function(a, b) {
-        // sort overlays last, then by x axis number, then y axis number
-        if((fullLayout._plots[a].mainplot && true) ===
-            (fullLayout._plots[b].mainplot && true)) {
-            var aParts = a.split('y'),
-                bParts = b.split('y');
-            return (aParts[0] === bParts[0]) ?
-                (Number(aParts[1] || 1) - Number(bParts[1] || 1)) :
-                (Number(aParts[0] || 1) - Number(bParts[0] || 1));
-        }
-        return fullLayout._plots[a].mainplot ? 1 : -1;
-    });
-
-    subplots.forEach(function(subplot) {
-        var plotinfo = fullLayout._plots[subplot];
-
-        if(!fullLayout._has('cartesian')) return;
-
-        var xa = plotinfo.xaxis,
-            ya = plotinfo.yaxis,
-
-            // the y position of the main x axis line
-            y0 = (xa._linepositions[subplot] || [])[3],
-
-            // the x position of the main y axis line
-            x0 = (ya._linepositions[subplot] || [])[3];
-
-        var DRAGGERSIZE = constants.DRAGGERSIZE;
-        if(isNumeric(y0) && xa.side === 'top') y0 -= DRAGGERSIZE;
-        if(isNumeric(x0) && ya.side !== 'right') x0 -= DRAGGERSIZE;
-
-        // main and corner draggers need not be repeated for
-        // overlaid subplots - these draggers drag them all
-        if(!plotinfo.mainplot) {
-            // main dragger goes over the grids and data, so we use its
-            // mousemove events for all data hover effects
-            var maindrag = dragBox(gd, plotinfo, 0, 0,
-                xa._length, ya._length, 'ns', 'ew');
-
-            maindrag.onmousemove = function(evt) {
-                fx.hover(gd, evt, subplot);
-                fullLayout._lasthover = maindrag;
-                fullLayout._hoversubplot = subplot;
-            };
-
-            /*
+  var fullLayout = gd._fullLayout;
+
+  if (!fullLayout._has("cartesian") || gd._context.staticPlot) return;
+
+  var subplots = Object.keys(fullLayout._plots || {}).sort(function(a, b) {
+    // sort overlays last, then by x axis number, then y axis number
+    if (
+      (fullLayout._plots[a].mainplot && true) ===
+        (fullLayout._plots[b].mainplot && true)
+    ) {
+      var aParts = a.split("y"), bParts = b.split("y");
+      return aParts[0] === bParts[0]
+        ? Number(aParts[1] || 1) - Number(bParts[1] || 1)
+        : Number(aParts[0] || 1) - Number(bParts[0] || 1);
+    }
+    return fullLayout._plots[a].mainplot ? 1 : -1;
+  });
+
+  subplots.forEach(function(subplot) {
+    var plotinfo = fullLayout._plots[subplot];
+
+    if (!fullLayout._has("cartesian")) return;
+
+    var xa = plotinfo.xaxis,
+      ya = plotinfo.yaxis,
+      // the y position of the main x axis line
+      y0 = (xa._linepositions[subplot] || [])[3],
+      // the x position of the main y axis line
+      x0 = (ya._linepositions[subplot] || [])[3];
+
+    var DRAGGERSIZE = constants.DRAGGERSIZE;
+    if (isNumeric(y0) && xa.side === "top") y0 -= DRAGGERSIZE;
+    if (isNumeric(x0) && ya.side !== "right") x0 -= DRAGGERSIZE;
+
+    // main and corner draggers need not be repeated for
+    // overlaid subplots - these draggers drag them all
+    if (!plotinfo.mainplot) {
+      // main dragger goes over the grids and data, so we use its
+      // mousemove events for all data hover effects
+      var maindrag = dragBox(
+        gd,
+        plotinfo,
+        0,
+        0,
+        xa._length,
+        ya._length,
+        "ns",
+        "ew"
+      );
+
+      maindrag.onmousemove = function(evt) {
+        fx.hover(gd, evt, subplot);
+        fullLayout._lasthover = maindrag;
+        fullLayout._hoversubplot = subplot;
+      };
+
+      /*
              * IMPORTANT:
              * We must check for the presence of the drag cover here.
              * If we don't, a 'mouseout' event is triggered on the
              * maindrag before each 'click' event, which has the effect
              * of clearing the hoverdata; thus, cancelling the click event.
              */
-            maindrag.onmouseout = function(evt) {
-                if(gd._dragging) return;
-
-                dragElement.unhover(gd, evt);
-            };
-
-            maindrag.onclick = function(evt) {
-                fx.click(gd, evt);
-            };
-
-            // corner draggers
-            dragBox(gd, plotinfo, -DRAGGERSIZE, -DRAGGERSIZE,
-                DRAGGERSIZE, DRAGGERSIZE, 'n', 'w');
-            dragBox(gd, plotinfo, xa._length, -DRAGGERSIZE,
-                DRAGGERSIZE, DRAGGERSIZE, 'n', 'e');
-            dragBox(gd, plotinfo, -DRAGGERSIZE, ya._length,
-                DRAGGERSIZE, DRAGGERSIZE, 's', 'w');
-            dragBox(gd, plotinfo, xa._length, ya._length,
-                DRAGGERSIZE, DRAGGERSIZE, 's', 'e');
-        }
+      maindrag.onmouseout = function(evt) {
+        if (gd._dragging) return;
 
-        // x axis draggers - if you have overlaid plots,
-        // these drag each axis separately
-        if(isNumeric(y0)) {
-            if(xa.anchor === 'free') y0 -= fullLayout._size.h * (1 - ya.domain[1]);
-            dragBox(gd, plotinfo, xa._length * 0.1, y0,
-                xa._length * 0.8, DRAGGERSIZE, '', 'ew');
-            dragBox(gd, plotinfo, 0, y0,
-                xa._length * 0.1, DRAGGERSIZE, '', 'w');
-            dragBox(gd, plotinfo, xa._length * 0.9, y0,
-                xa._length * 0.1, DRAGGERSIZE, '', 'e');
-        }
-        // y axis draggers
-        if(isNumeric(x0)) {
-            if(ya.anchor === 'free') x0 -= fullLayout._size.w * xa.domain[0];
-            dragBox(gd, plotinfo, x0, ya._length * 0.1,
-                DRAGGERSIZE, ya._length * 0.8, 'ns', '');
-            dragBox(gd, plotinfo, x0, ya._length * 0.9,
-                DRAGGERSIZE, ya._length * 0.1, 's', '');
-            dragBox(gd, plotinfo, x0, 0,
-                DRAGGERSIZE, ya._length * 0.1, 'n', '');
-        }
-    });
-
-    // In case you mousemove over some hovertext, send it to fx.hover too
-    // we do this so that we can put the hover text in front of everything,
-    // but still be able to interact with everything as if it isn't there
-    var hoverLayer = fullLayout._hoverlayer.node();
+        dragElement.unhover(gd, evt);
+      };
 
-    hoverLayer.onmousemove = function(evt) {
-        evt.target = fullLayout._lasthover;
-        fx.hover(gd, evt, fullLayout._hoversubplot);
-    };
-
-    hoverLayer.onclick = function(evt) {
-        evt.target = fullLayout._lasthover;
+      maindrag.onclick = function(evt) {
         fx.click(gd, evt);
-    };
+      };
+
+      // corner draggers
+      dragBox(
+        gd,
+        plotinfo,
+        -DRAGGERSIZE,
+        -DRAGGERSIZE,
+        DRAGGERSIZE,
+        DRAGGERSIZE,
+        "n",
+        "w"
+      );
+      dragBox(
+        gd,
+        plotinfo,
+        xa._length,
+        -DRAGGERSIZE,
+        DRAGGERSIZE,
+        DRAGGERSIZE,
+        "n",
+        "e"
+      );
+      dragBox(
+        gd,
+        plotinfo,
+        -DRAGGERSIZE,
+        ya._length,
+        DRAGGERSIZE,
+        DRAGGERSIZE,
+        "s",
+        "w"
+      );
+      dragBox(
+        gd,
+        plotinfo,
+        xa._length,
+        ya._length,
+        DRAGGERSIZE,
+        DRAGGERSIZE,
+        "s",
+        "e"
+      );
+    }
 
-    // also delegate mousedowns... TODO: does this actually work?
-    hoverLayer.onmousedown = function(evt) {
-        fullLayout._lasthover.onmousedown(evt);
-    };
+    // x axis draggers - if you have overlaid plots,
+    // these drag each axis separately
+    if (isNumeric(y0)) {
+      if (xa.anchor === "free") y0 -= fullLayout._size.h * (1 - ya.domain[1]);
+      dragBox(
+        gd,
+        plotinfo,
+        xa._length * 0.1,
+        y0,
+        xa._length * 0.8,
+        DRAGGERSIZE,
+        "",
+        "ew"
+      );
+      dragBox(gd, plotinfo, 0, y0, xa._length * 0.1, DRAGGERSIZE, "", "w");
+      dragBox(
+        gd,
+        plotinfo,
+        xa._length * 0.9,
+        y0,
+        xa._length * 0.1,
+        DRAGGERSIZE,
+        "",
+        "e"
+      );
+    }
+    // y axis draggers
+    if (isNumeric(x0)) {
+      if (ya.anchor === "free") x0 -= fullLayout._size.w * xa.domain[0];
+      dragBox(
+        gd,
+        plotinfo,
+        x0,
+        ya._length * 0.1,
+        DRAGGERSIZE,
+        ya._length * 0.8,
+        "ns",
+        ""
+      );
+      dragBox(
+        gd,
+        plotinfo,
+        x0,
+        ya._length * 0.9,
+        DRAGGERSIZE,
+        ya._length * 0.1,
+        "s",
+        ""
+      );
+      dragBox(gd, plotinfo, x0, 0, DRAGGERSIZE, ya._length * 0.1, "n", "");
+    }
+  });
+
+  // In case you mousemove over some hovertext, send it to fx.hover too
+  // we do this so that we can put the hover text in front of everything,
+  // but still be able to interact with everything as if it isn't there
+  var hoverLayer = fullLayout._hoverlayer.node();
+
+  hoverLayer.onmousemove = function(evt) {
+    evt.target = fullLayout._lasthover;
+    fx.hover(gd, evt, fullLayout._hoversubplot);
+  };
+
+  hoverLayer.onclick = function(evt) {
+    evt.target = fullLayout._lasthover;
+    fx.click(gd, evt);
+  };
+
+  // also delegate mousedowns... TODO: does this actually work?
+  hoverLayer.onmousedown = function(evt) {
+    fullLayout._lasthover.onmousedown(evt);
+  };
 };
 
 // hover labels for multiple horizontal bars get tilted by some angle,
 // then need to be offset differently if they overlap
 var YANGLE = constants.YANGLE,
-    YA_RADIANS = Math.PI * YANGLE / 180,
-
-    // expansion of projected height
-    YFACTOR = 1 / Math.sin(YA_RADIANS),
-
-    // to make the appropriate post-rotation x offset,
-    // you need both x and y offsets
-    YSHIFTX = Math.cos(YA_RADIANS),
-    YSHIFTY = Math.sin(YA_RADIANS);
+  YA_RADIANS = Math.PI * YANGLE / 180,
+  // expansion of projected height
+  YFACTOR = 1 / Math.sin(YA_RADIANS),
+  // to make the appropriate post-rotation x offset,
+  // you need both x and y offsets
+  YSHIFTX = Math.cos(YA_RADIANS),
+  YSHIFTY = Math.sin(YA_RADIANS);
 
 // convenience functions for mapping all relevant axes
 function flat(subplots, v) {
-    var out = [];
-    for(var i = subplots.length; i > 0; i--) out.push(v);
-    return out;
+  var out = [];
+  for (var i = subplots.length; i > 0; i--) {
+    out.push(v);
+  }
+  return out;
 }
 
 function p2c(axArray, v) {
-    var out = [];
-    for(var i = 0; i < axArray.length; i++) out.push(axArray[i].p2c(v));
-    return out;
+  var out = [];
+  for (var i = 0; i < axArray.length; i++) {
+    out.push(axArray[i].p2c(v));
+  }
+  return out;
 }
 
 function quadrature(dx, dy) {
-    return function(di) {
-        var x = dx(di),
-            y = dy(di);
-        return Math.sqrt(x * x + y * y);
-    };
+  return function(di) {
+    var x = dx(di), y = dy(di);
+    return Math.sqrt(x * x + y * y);
+  };
 }
 
 // size and display constants for hover text
 var HOVERARROWSIZE = constants.HOVERARROWSIZE,
-    HOVERTEXTPAD = constants.HOVERTEXTPAD,
-    HOVERFONTSIZE = constants.HOVERFONTSIZE,
-    HOVERFONT = constants.HOVERFONT;
+  HOVERTEXTPAD = constants.HOVERTEXTPAD,
+  HOVERFONTSIZE = constants.HOVERFONTSIZE,
+  HOVERFONT = constants.HOVERFONT;
 
 // fx.hover: highlight data on hover
 // evt can be a mousemove event, or an object with data about what points
@@ -256,830 +322,901 @@ var HOVERARROWSIZE = constants.HOVERARROWSIZE,
 // We wrap the hovers in a timer, to limit their frequency.
 // The actual rendering is done by private functions
 // hover() and unhover().
-
 fx.hover = function(gd, evt, subplot) {
-    if(typeof gd === 'string') gd = document.getElementById(gd);
-    if(gd._lastHoverTime === undefined) gd._lastHoverTime = 0;
-
-    // If we have an update queued, discard it now
-    if(gd._hoverTimer !== undefined) {
-        clearTimeout(gd._hoverTimer);
-        gd._hoverTimer = undefined;
-    }
-    // Is it more than 100ms since the last update?  If so, force
-    // an update now (synchronously) and exit
-    if(Date.now() > gd._lastHoverTime + constants.HOVERMINTIME) {
-        hover(gd, evt, subplot);
-        gd._lastHoverTime = Date.now();
-        return;
-    }
-    // Queue up the next hover for 100ms from now (if no further events)
-    gd._hoverTimer = setTimeout(function() {
-        hover(gd, evt, subplot);
-        gd._lastHoverTime = Date.now();
-        gd._hoverTimer = undefined;
-    }, constants.HOVERMINTIME);
+  if (typeof gd === "string") gd = document.getElementById(gd);
+  if (gd._lastHoverTime === undefined) gd._lastHoverTime = 0;
+
+  // If we have an update queued, discard it now
+  if (gd._hoverTimer !== undefined) {
+    clearTimeout(gd._hoverTimer);
+    gd._hoverTimer = undefined;
+  }
+  // Is it more than 100ms since the last update?  If so, force
+  // an update now (synchronously) and exit
+  if (Date.now() > gd._lastHoverTime + constants.HOVERMINTIME) {
+    hover(gd, evt, subplot);
+    gd._lastHoverTime = Date.now();
+    return;
+  }
+  // Queue up the next hover for 100ms from now (if no further events)
+  gd._hoverTimer = setTimeout(
+    function() {
+      hover(gd, evt, subplot);
+      gd._lastHoverTime = Date.now();
+      gd._hoverTimer = undefined;
+    },
+    constants.HOVERMINTIME
+  );
 };
 
 // The actual implementation is here:
-
 function hover(gd, evt, subplot) {
-    if(subplot === 'pie') {
-        gd.emit('plotly_hover', {
-            points: [evt]
-        });
-        return;
-    }
-
-    if(!subplot) subplot = 'xy';
+  if (subplot === "pie") {
+    gd.emit("plotly_hover", { points: [evt] });
+    return;
+  }
 
-    // if the user passed in an array of subplots,
-    // use those instead of finding overlayed plots
-    var subplots = Array.isArray(subplot) ? subplot : [subplot];
+  if (!subplot) subplot = "xy";
 
-    var fullLayout = gd._fullLayout,
-        plots = fullLayout._plots || [],
-        plotinfo = plots[subplot];
+  // if the user passed in an array of subplots,
+  // use those instead of finding overlayed plots
+  var subplots = Array.isArray(subplot) ? subplot : [subplot];
 
-    // list of all overlaid subplots to look at
-    if(plotinfo) {
-        var overlayedSubplots = plotinfo.overlays.map(function(pi) {
-            return pi.id;
-        });
+  var fullLayout = gd._fullLayout,
+    plots = fullLayout._plots || [],
+    plotinfo = plots[subplot];
 
-        subplots = subplots.concat(overlayedSubplots);
-    }
-
-    var len = subplots.length,
-        xaArray = new Array(len),
-        yaArray = new Array(len);
-
-    for(var i = 0; i < len; i++) {
-        var spId = subplots[i];
+  // list of all overlaid subplots to look at
+  if (plotinfo) {
+    var overlayedSubplots = plotinfo.overlays.map(function(pi) {
+      return pi.id;
+    });
 
-        // 'cartesian' case
-        var plotObj = plots[spId];
-        if(plotObj) {
+    subplots = subplots.concat(overlayedSubplots);
+  }
 
-            // TODO make sure that fullLayout_plots axis refs
-            // get updated properly so that we don't have
-            // to use Axes.getFromId in general.
+  var len = subplots.length, xaArray = new Array(len), yaArray = new Array(len);
 
-            xaArray[i] = Axes.getFromId(gd, plotObj.xaxis._id);
-            yaArray[i] = Axes.getFromId(gd, plotObj.yaxis._id);
-            continue;
-        }
+  for (var i = 0; i < len; i++) {
+    var spId = subplots[i];
 
-        // other subplot types
-        var _subplot = fullLayout[spId]._subplot;
-        xaArray[i] = _subplot.xaxis;
-        yaArray[i] = _subplot.yaxis;
+    // 'cartesian' case
+    var plotObj = plots[spId];
+    if (plotObj) {
+      // TODO make sure that fullLayout_plots axis refs
+      // get updated properly so that we don't have
+      // to use Axes.getFromId in general.
+      xaArray[i] = Axes.getFromId(gd, plotObj.xaxis._id);
+      yaArray[i] = Axes.getFromId(gd, plotObj.yaxis._id);
+      continue;
     }
 
-    var hovermode = evt.hovermode || fullLayout.hovermode;
-
-    if(['x', 'y', 'closest'].indexOf(hovermode) === -1 || !gd.calcdata ||
-            gd.querySelector('.zoombox') || gd._dragging) {
-        return dragElement.unhoverRaw(gd, evt);
+    // other subplot types
+    var _subplot = fullLayout[spId]._subplot;
+    xaArray[i] = _subplot.xaxis;
+    yaArray[i] = _subplot.yaxis;
+  }
+
+  var hovermode = evt.hovermode || fullLayout.hovermode;
+
+  if (
+    ["x", "y", "closest"].indexOf(hovermode) === -1 ||
+      !gd.calcdata ||
+      gd.querySelector(".zoombox") ||
+      gd._dragging
+  ) {
+    return dragElement.unhoverRaw(gd, evt);
+  }
+
+  // hoverData: the set of candidate points we've found to highlight
+  var hoverData = [],
+    // searchData: the data to search in. Mostly this is just a copy of
+    // gd.calcdata, filtered to the subplot and overlays we're on
+    // but if a point array is supplied it will be a mapping
+    // of indicated curves
+    searchData = [],
+    // [x|y]valArray: the axis values of the hover event
+    // mapped onto each of the currently selected overlaid subplots
+    xvalArray,
+    yvalArray,
+    // used in loops
+    itemnum,
+    curvenum,
+    cd,
+    trace,
+    subplotId,
+    subploti,
+    mode,
+    xval,
+    yval,
+    pointData,
+    closedataPreviousLength;
+
+  // Figure out what we're hovering on:
+  // mouse location or user-supplied data
+  if (Array.isArray(evt)) {
+    // user specified an array of points to highlight
+    hovermode = "array";
+    for (itemnum = 0; itemnum < evt.length; itemnum++) {
+      cd = gd.calcdata[evt[itemnum].curveNumber || 0];
+      if (cd[0].trace.hoverinfo !== "skip") {
+        searchData.push(cd);
+      }
     }
-
-        // hoverData: the set of candidate points we've found to highlight
-    var hoverData = [],
-
-        // searchData: the data to search in. Mostly this is just a copy of
-        // gd.calcdata, filtered to the subplot and overlays we're on
-        // but if a point array is supplied it will be a mapping
-        // of indicated curves
-        searchData = [],
-
-        // [x|y]valArray: the axis values of the hover event
-        // mapped onto each of the currently selected overlaid subplots
-        xvalArray,
-        yvalArray,
-
-        // used in loops
-        itemnum,
-        curvenum,
-        cd,
-        trace,
-        subplotId,
-        subploti,
-        mode,
-        xval,
-        yval,
-        pointData,
-        closedataPreviousLength;
-
-    // Figure out what we're hovering on:
-    // mouse location or user-supplied data
-
-    if(Array.isArray(evt)) {
-        // user specified an array of points to highlight
-        hovermode = 'array';
-        for(itemnum = 0; itemnum < evt.length; itemnum++) {
-            cd = gd.calcdata[evt[itemnum].curveNumber||0];
-            if(cd[0].trace.hoverinfo !== 'skip') {
-                searchData.push(cd);
-            }
-        }
+  } else {
+    for (curvenum = 0; curvenum < gd.calcdata.length; curvenum++) {
+      cd = gd.calcdata[curvenum];
+      trace = cd[0].trace;
+      if (
+        trace.hoverinfo !== "skip" && subplots.indexOf(getSubplot(trace)) !== -1
+      ) {
+        searchData.push(cd);
+      }
     }
-    else {
-        for(curvenum = 0; curvenum < gd.calcdata.length; curvenum++) {
-            cd = gd.calcdata[curvenum];
-            trace = cd[0].trace;
-            if(trace.hoverinfo !== 'skip' && subplots.indexOf(getSubplot(trace)) !== -1) {
-                searchData.push(cd);
-            }
-        }
-
-        // [x|y]px: the pixels (from top left) of the mouse location
-        // on the currently selected plot area
-        var xpx, ypx;
 
-        // mouse event? ie is there a target element with
-        // clientX and clientY values?
-        if(evt.target && ('clientX' in evt) && ('clientY' in evt)) {
-
-            // fire the beforehover event and quit if it returns false
-            // note that we're only calling this on real mouse events, so
-            // manual calls to fx.hover will always run.
-            if(Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) {
-                return;
-            }
+    // [x|y]px: the pixels (from top left) of the mouse location
+    // on the currently selected plot area
+    var xpx, ypx;
+
+    // mouse event? ie is there a target element with
+    // clientX and clientY values?
+    if (evt.target && "clientX" in evt && "clientY" in evt) {
+      // fire the beforehover event and quit if it returns false
+      // note that we're only calling this on real mouse events, so
+      // manual calls to fx.hover will always run.
+      if (Events.triggerHandler(gd, "plotly_beforehover", evt) === false) {
+        return;
+      }
 
-            var dbb = evt.target.getBoundingClientRect();
+      var dbb = evt.target.getBoundingClientRect();
 
-            xpx = evt.clientX - dbb.left;
-            ypx = evt.clientY - dbb.top;
+      xpx = evt.clientX - dbb.left;
+      ypx = evt.clientY - dbb.top;
 
-            // in case hover was called from mouseout into hovertext,
-            // it's possible you're not actually over the plot anymore
-            if(xpx < 0 || xpx > dbb.width || ypx < 0 || ypx > dbb.height) {
-                return dragElement.unhoverRaw(gd, evt);
-            }
-        }
-        else {
-            if('xpx' in evt) xpx = evt.xpx;
-            else xpx = xaArray[0]._length / 2;
+      // in case hover was called from mouseout into hovertext,
+      // it's possible you're not actually over the plot anymore
+      if (xpx < 0 || xpx > dbb.width || ypx < 0 || ypx > dbb.height) {
+        return dragElement.unhoverRaw(gd, evt);
+      }
+    } else {
+      if ("xpx" in evt) xpx = evt.xpx;
+      else xpx = xaArray[0]._length / 2;
 
-            if('ypx' in evt) ypx = evt.ypx;
-            else ypx = yaArray[0]._length / 2;
-        }
+      if ("ypx" in evt) ypx = evt.ypx;
+      else ypx = yaArray[0]._length / 2;
+    }
 
-        if('xval' in evt) xvalArray = flat(subplots, evt.xval);
-        else xvalArray = p2c(xaArray, xpx);
+    if ("xval" in evt) xvalArray = flat(subplots, evt.xval);
+    else xvalArray = p2c(xaArray, xpx);
 
-        if('yval' in evt) yvalArray = flat(subplots, evt.yval);
-        else yvalArray = p2c(yaArray, ypx);
+    if ("yval" in evt) yvalArray = flat(subplots, evt.yval);
+    else yvalArray = p2c(yaArray, ypx);
 
-        if(!isNumeric(xvalArray[0]) || !isNumeric(yvalArray[0])) {
-            Lib.warn('Fx.hover failed', evt, gd);
-            return dragElement.unhoverRaw(gd, evt);
-        }
+    if (!isNumeric(xvalArray[0]) || !isNumeric(yvalArray[0])) {
+      Lib.warn("Fx.hover failed", evt, gd);
+      return dragElement.unhoverRaw(gd, evt);
     }
+  }
+
+  // the pixel distance to beat as a matching point
+  // in 'x' or 'y' mode this resets for each trace
+  var distance = Infinity;
+
+  // find the closest point in each trace
+  // this is minimum dx and/or dy, depending on mode
+  // and the pixel position for the label (labelXpx, labelYpx)
+  for (curvenum = 0; curvenum < searchData.length; curvenum++) {
+    cd = searchData[curvenum];
+
+    // filter out invisible or broken data
+    if (!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue;
+
+    trace = cd[0].trace;
+    subplotId = getSubplot(trace);
+    subploti = subplots.indexOf(subplotId);
+
+    // within one trace mode can sometimes be overridden
+    mode = hovermode;
+
+    // container for new point, also used to pass info into module.hoverPoints
+    pointData = {
+      // trace properties
+      cd: cd,
+      trace: trace,
+      xa: xaArray[subploti],
+      ya: yaArray[subploti],
+      name: (
+        gd.data.length > 1 || trace.hoverinfo.indexOf("name") !== -1
+          ? trace.name
+          : undefined
+      ),
+      // point properties - override all of these
+      index: false,
+      // point index in trace - only used by plotly.js hoverdata consumers
+      distance: Math.min(distance, constants.MAXDIST),
+      // pixel distance or pseudo-distance
+      color: Color.defaultLine,
+      // trace color
+      x0: undefined,
+      x1: undefined,
+      y0: undefined,
+      y1: undefined,
+      xLabelVal: undefined,
+      yLabelVal: undefined,
+      zLabelVal: undefined,
+      text: undefined
+    };
 
-    // the pixel distance to beat as a matching point
-    // in 'x' or 'y' mode this resets for each trace
-    var distance = Infinity;
-
-    // find the closest point in each trace
-    // this is minimum dx and/or dy, depending on mode
-    // and the pixel position for the label (labelXpx, labelYpx)
-    for(curvenum = 0; curvenum < searchData.length; curvenum++) {
-        cd = searchData[curvenum];
-
-        // filter out invisible or broken data
-        if(!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue;
-
-        trace = cd[0].trace;
-        subplotId = getSubplot(trace);
-        subploti = subplots.indexOf(subplotId);
-
-        // within one trace mode can sometimes be overridden
-        mode = hovermode;
-
-        // container for new point, also used to pass info into module.hoverPoints
-        pointData = {
-            // trace properties
-            cd: cd,
-            trace: trace,
-            xa: xaArray[subploti],
-            ya: yaArray[subploti],
-            name: (gd.data.length > 1 || trace.hoverinfo.indexOf('name') !== -1) ? trace.name : undefined,
-            // point properties - override all of these
-            index: false, // point index in trace - only used by plotly.js hoverdata consumers
-            distance: Math.min(distance, constants.MAXDIST), // pixel distance or pseudo-distance
-            color: Color.defaultLine, // trace color
-            x0: undefined,
-            x1: undefined,
-            y0: undefined,
-            y1: undefined,
-            xLabelVal: undefined,
-            yLabelVal: undefined,
-            zLabelVal: undefined,
-            text: undefined
-        };
-
-        // add ref to subplot object (non-cartesian case)
-        if(fullLayout[subplotId]) {
-            pointData.subplot = fullLayout[subplotId]._subplot;
-        }
-
-        closedataPreviousLength = hoverData.length;
-
-        // for a highlighting array, figure out what
-        // we're searching for with this element
-        if(mode === 'array') {
-            var selection = evt[curvenum];
-            if('pointNumber' in selection) {
-                pointData.index = selection.pointNumber;
-                mode = 'closest';
-            }
-            else {
-                mode = '';
-                if('xval' in selection) {
-                    xval = selection.xval;
-                    mode = 'x';
-                }
-                if('yval' in selection) {
-                    yval = selection.yval;
-                    mode = mode ? 'closest' : 'y';
-                }
-            }
-        }
-        else {
-            xval = xvalArray[subploti];
-            yval = yvalArray[subploti];
-        }
+    // add ref to subplot object (non-cartesian case)
+    if (fullLayout[subplotId]) {
+      pointData.subplot = fullLayout[subplotId]._subplot;
+    }
 
-        // Now find the points.
-        if(trace._module && trace._module.hoverPoints) {
-            var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode);
-            if(newPoints) {
-                var newPoint;
-                for(var newPointNum = 0; newPointNum < newPoints.length; newPointNum++) {
-                    newPoint = newPoints[newPointNum];
-                    if(isNumeric(newPoint.x0) && isNumeric(newPoint.y0)) {
-                        hoverData.push(cleanPoint(newPoint, hovermode));
-                    }
-                }
-            }
+    closedataPreviousLength = hoverData.length;
+
+    // for a highlighting array, figure out what
+    // we're searching for with this element
+    if (mode === "array") {
+      var selection = evt[curvenum];
+      if ("pointNumber" in selection) {
+        pointData.index = selection.pointNumber;
+        mode = "closest";
+      } else {
+        mode = "";
+        if ("xval" in selection) {
+          xval = selection.xval;
+          mode = "x";
         }
-        else {
-            Lib.log('Unrecognized trace type in hover:', trace);
-        }
-
-        // in closest mode, remove any existing (farther) points
-        // and don't look any farther than this latest point (or points, if boxes)
-        if(hovermode === 'closest' && hoverData.length > closedataPreviousLength) {
-            hoverData.splice(0, closedataPreviousLength);
-            distance = hoverData[0].distance;
+        if ("yval" in selection) {
+          yval = selection.yval;
+          mode = mode ? "closest" : "y";
         }
+      }
+    } else {
+      xval = xvalArray[subploti];
+      yval = yvalArray[subploti];
     }
 
-    // nothing left: remove all labels and quit
-    if(hoverData.length === 0) return dragElement.unhoverRaw(gd, evt);
-
-    // if there's more than one horz bar trace,
-    // rotate the labels so they don't overlap
-    var rotateLabels = hovermode === 'y' && searchData.length > 1;
-
-    hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });
-
-    var bgColor = Color.combine(
-        fullLayout.plot_bgcolor || Color.background,
-        fullLayout.paper_bgcolor
-    );
-
-    var labelOpts = {
-        hovermode: hovermode,
-        rotateLabels: rotateLabels,
-        bgColor: bgColor,
-        container: fullLayout._hoverlayer,
-        outerContainer: fullLayout._paperdiv
-    };
-    var hoverLabels = createHoverText(hoverData, labelOpts);
-
-    hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya');
-
-    alignHoverText(hoverLabels, rotateLabels);
-
-    // lastly, emit custom hover/unhover events
-    var oldhoverdata = gd._hoverdata,
-        newhoverdata = [];
-
-    // pull out just the data that's useful to
-    // other people and send it to the event
-    for(itemnum = 0; itemnum < hoverData.length; itemnum++) {
-        var pt = hoverData[itemnum];
-
-        var out = {
-            data: pt.trace._input,
-            fullData: pt.trace,
-            curveNumber: pt.trace.index,
-            pointNumber: pt.index
-        };
-
-        if(pt.trace._module.eventData) out = pt.trace._module.eventData(out, pt);
-        else {
-            out.x = pt.xVal;
-            out.y = pt.yVal;
-            out.xaxis = pt.xa;
-            out.yaxis = pt.ya;
-
-            if(pt.zLabelVal !== undefined) out.z = pt.zLabelVal;
+    // Now find the points.
+    if (trace._module && trace._module.hoverPoints) {
+      var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode);
+      if (newPoints) {
+        var newPoint;
+        for (
+          var newPointNum = 0;
+          newPointNum < newPoints.length;
+          newPointNum++
+        ) {
+          newPoint = newPoints[newPointNum];
+          if (isNumeric(newPoint.x0) && isNumeric(newPoint.y0)) {
+            hoverData.push(cleanPoint(newPoint, hovermode));
+          }
         }
-
-        newhoverdata.push(out);
+      }
+    } else {
+      Lib.log("Unrecognized trace type in hover:", trace);
     }
 
-    gd._hoverdata = newhoverdata;
-
-    // TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
-    // we should improve the "fx" API so other plots can use it without these hack.
-    if(evt.target && evt.target.tagName) {
-        var hasClickToShow = Registry.getComponentMethod('annotations', 'hasClickToShow')(gd, newhoverdata);
-        overrideCursor(d3.select(evt.target), hasClickToShow ? 'pointer' : '');
+    // in closest mode, remove any existing (farther) points
+    // and don't look any farther than this latest point (or points, if boxes)
+    if (hovermode === "closest" && hoverData.length > closedataPreviousLength) {
+      hoverData.splice(0, closedataPreviousLength);
+      distance = hoverData[0].distance;
     }
+  }
+
+  // nothing left: remove all labels and quit
+  if (hoverData.length === 0) return dragElement.unhoverRaw(gd, evt);
+
+  // if there's more than one horz bar trace,
+  // rotate the labels so they don't overlap
+  var rotateLabels = hovermode === "y" && searchData.length > 1;
+
+  hoverData.sort(function(d1, d2) {
+    return d1.distance - d2.distance;
+  });
+
+  var bgColor = Color.combine(
+    fullLayout.plot_bgcolor || Color.background,
+    fullLayout.paper_bgcolor
+  );
+
+  var labelOpts = {
+    hovermode: hovermode,
+    rotateLabels: rotateLabels,
+    bgColor: bgColor,
+    container: fullLayout._hoverlayer,
+    outerContainer: fullLayout._paperdiv
+  };
+  var hoverLabels = createHoverText(hoverData, labelOpts);
+
+  hoverAvoidOverlaps(hoverData, rotateLabels ? "xa" : "ya");
+
+  alignHoverText(hoverLabels, rotateLabels);
+
+  // lastly, emit custom hover/unhover events
+  var oldhoverdata = gd._hoverdata, newhoverdata = [];
+
+  // pull out just the data that's useful to
+  // other people and send it to the event
+  for (itemnum = 0; itemnum < hoverData.length; itemnum++) {
+    var pt = hoverData[itemnum];
+
+    var out = {
+      data: pt.trace._input,
+      fullData: pt.trace,
+      curveNumber: pt.trace.index,
+      pointNumber: pt.index
+    };
 
-    if(!hoverChanged(gd, evt, oldhoverdata)) return;
+    if (pt.trace._module.eventData) {
+      out = pt.trace._module.eventData(out, pt);
+    } else {
+      out.x = pt.xVal;
+      out.y = pt.yVal;
+      out.xaxis = pt.xa;
+      out.yaxis = pt.ya;
 
-    if(oldhoverdata) {
-        gd.emit('plotly_unhover', { points: oldhoverdata });
+      if (pt.zLabelVal !== undefined) out.z = pt.zLabelVal;
     }
 
-    gd.emit('plotly_hover', {
-        points: gd._hoverdata,
-        xaxes: xaArray,
-        yaxes: yaArray,
-        xvals: xvalArray,
-        yvals: yvalArray
-    });
+    newhoverdata.push(out);
+  }
+
+  gd._hoverdata = newhoverdata;
+
+  // TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
+  // we should improve the "fx" API so other plots can use it without these hack.
+  if (evt.target && evt.target.tagName) {
+    var hasClickToShow = Registry.getComponentMethod(
+      "annotations",
+      "hasClickToShow"
+    )(gd, newhoverdata);
+    overrideCursor(d3.select(evt.target), hasClickToShow ? "pointer" : "");
+  }
+
+  if (!hoverChanged(gd, evt, oldhoverdata)) return;
+
+  if (oldhoverdata) {
+    gd.emit("plotly_unhover", { points: oldhoverdata });
+  }
+
+  gd.emit("plotly_hover", {
+    points: gd._hoverdata,
+    xaxes: xaArray,
+    yaxes: yaArray,
+    xvals: xvalArray,
+    yvals: yvalArray
+  });
 }
 
 // look for either .subplot (currently just ternary)
 // or xaxis and yaxis attributes
 function getSubplot(trace) {
-    return trace.subplot || (trace.xaxis + trace.yaxis) || trace.geo;
+  return trace.subplot || trace.xaxis + trace.yaxis || trace.geo;
 }
 
 fx.getDistanceFunction = function(mode, dx, dy, dxy) {
-    if(mode === 'closest') return dxy || quadrature(dx, dy);
-    return mode === 'x' ? dx : dy;
+  if (mode === "closest") return dxy || quadrature(dx, dy);
+  return mode === "x" ? dx : dy;
 };
 
 fx.getClosest = function(cd, distfn, pointData) {
-    // do we already have a point number? (array mode only)
-    if(pointData.index !== false) {
-        if(pointData.index >= 0 && pointData.index < cd.length) {
-            pointData.distance = 0;
-        }
-        else pointData.index = false;
+  // do we already have a point number? (array mode only)
+  if (pointData.index !== false) {
+    if (pointData.index >= 0 && pointData.index < cd.length) {
+      pointData.distance = 0;
+    } else {
+      pointData.index = false;
     }
-    else {
-        // apply the distance function to each data point
-        // this is the longest loop... if this bogs down, we may need
-        // to create pre-sorted data (by x or y), not sure how to
-        // do this for 'closest'
-        for(var i = 0; i < cd.length; i++) {
-            var newDistance = distfn(cd[i]);
-            if(newDistance <= pointData.distance) {
-                pointData.index = i;
-                pointData.distance = newDistance;
-            }
-        }
+  } else {
+    // apply the distance function to each data point
+    // this is the longest loop... if this bogs down, we may need
+    // to create pre-sorted data (by x or y), not sure how to
+    // do this for 'closest'
+    for (var i = 0; i < cd.length; i++) {
+      var newDistance = distfn(cd[i]);
+      if (newDistance <= pointData.distance) {
+        pointData.index = i;
+        pointData.distance = newDistance;
+      }
     }
-    return pointData;
+  }
+  return pointData;
 };
 
 function cleanPoint(d, hovermode) {
-    d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2;
-
-    // then constrain all the positions to be on the plot
-    d.x0 = Lib.constrain(d.x0, 0, d.xa._length);
-    d.x1 = Lib.constrain(d.x1, 0, d.xa._length);
-    d.y0 = Lib.constrain(d.y0, 0, d.ya._length);
-    d.y1 = Lib.constrain(d.y1, 0, d.ya._length);
-
-    // and convert the x and y label values into objects
-    // formatted as text, with font info
-    var logOffScale;
-    if(d.xLabelVal !== undefined) {
-        logOffScale = (d.xa.type === 'log' && d.xLabelVal <= 0);
-        var xLabelObj = Axes.tickText(d.xa,
-                d.xa.c2l(logOffScale ? -d.xLabelVal : d.xLabelVal), 'hover');
-        if(logOffScale) {
-            if(d.xLabelVal === 0) d.xLabel = '0';
-            else d.xLabel = '-' + xLabelObj.text;
-        }
-        // TODO: should we do something special if the axis calendar and
-        // the data calendar are different? Somehow display both dates with
-        // their system names? Right now it will just display in the axis calendar
-        // but users could add the other one as text.
-        else d.xLabel = xLabelObj.text;
-        d.xVal = d.xa.c2d(d.xLabelVal);
+  d.posref = hovermode === "y" ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2;
+
+  // then constrain all the positions to be on the plot
+  d.x0 = Lib.constrain(d.x0, 0, d.xa._length);
+  d.x1 = Lib.constrain(d.x1, 0, d.xa._length);
+  d.y0 = Lib.constrain(d.y0, 0, d.ya._length);
+  d.y1 = Lib.constrain(d.y1, 0, d.ya._length);
+
+  // and convert the x and y label values into objects
+  // formatted as text, with font info
+  var logOffScale;
+  if (d.xLabelVal !== undefined) {
+    logOffScale = d.xa.type === "log" && d.xLabelVal <= 0;
+    var xLabelObj = Axes.tickText(
+      d.xa,
+      d.xa.c2l(logOffScale ? -d.xLabelVal : d.xLabelVal),
+      "hover"
+    );
+    if (logOffScale) {
+      if (d.xLabelVal === 0) d.xLabel = "0";
+      else d.xLabel = "-" + xLabelObj.text;
+    } else {
+      // TODO: should we do something special if the axis calendar and
+      // the data calendar are different? Somehow display both dates with
+      // their system names? Right now it will just display in the axis calendar
+      // but users could add the other one as text.
+      d.xLabel = xLabelObj.text;
     }
-
-    if(d.yLabelVal !== undefined) {
-        logOffScale = (d.ya.type === 'log' && d.yLabelVal <= 0);
-        var yLabelObj = Axes.tickText(d.ya,
-                d.ya.c2l(logOffScale ? -d.yLabelVal : d.yLabelVal), 'hover');
-        if(logOffScale) {
-            if(d.yLabelVal === 0) d.yLabel = '0';
-            else d.yLabel = '-' + yLabelObj.text;
-        }
-        // TODO: see above TODO
-        else d.yLabel = yLabelObj.text;
-        d.yVal = d.ya.c2d(d.yLabelVal);
+    d.xVal = d.xa.c2d(d.xLabelVal);
+  }
+
+  if (d.yLabelVal !== undefined) {
+    logOffScale = d.ya.type === "log" && d.yLabelVal <= 0;
+    var yLabelObj = Axes.tickText(
+      d.ya,
+      d.ya.c2l(logOffScale ? -d.yLabelVal : d.yLabelVal),
+      "hover"
+    );
+    if (logOffScale) {
+      if (d.yLabelVal === 0) d.yLabel = "0";
+      else d.yLabel = "-" + yLabelObj.text;
+    } else {
+      // TODO: see above TODO
+      d.yLabel = yLabelObj.text;
     }
-
-    if(d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal);
-
-    // for box means and error bars, add the range to the label
-    if(!isNaN(d.xerr) && !(d.xa.type === 'log' && d.xerr <= 0)) {
-        var xeText = Axes.tickText(d.xa, d.xa.c2l(d.xerr), 'hover').text;
-        if(d.xerrneg !== undefined) {
-            d.xLabel += ' +' + xeText + ' / -' +
-                Axes.tickText(d.xa, d.xa.c2l(d.xerrneg), 'hover').text;
-        }
-        else d.xLabel += ' ± ' + xeText;
-
-        // small distance penalty for error bars, so that if there are
-        // traces with errors and some without, the error bar label will
-        // hoist up to the point
-        if(hovermode === 'x') d.distance += 1;
+    d.yVal = d.ya.c2d(d.yLabelVal);
+  }
+
+  if (d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal);
+
+  // for box means and error bars, add the range to the label
+  if (!isNaN(d.xerr) && !(d.xa.type === "log" && d.xerr <= 0)) {
+    var xeText = Axes.tickText(d.xa, d.xa.c2l(d.xerr), "hover").text;
+    if (d.xerrneg !== undefined) {
+      d.xLabel += " +" +
+        xeText +
+        " / -" +
+        Axes.tickText(d.xa, d.xa.c2l(d.xerrneg), "hover").text;
+    } else {
+      d.xLabel += " \xB1 " + xeText;
     }
-    if(!isNaN(d.yerr) && !(d.ya.type === 'log' && d.yerr <= 0)) {
-        var yeText = Axes.tickText(d.ya, d.ya.c2l(d.yerr), 'hover').text;
-        if(d.yerrneg !== undefined) {
-            d.yLabel += ' +' + yeText + ' / -' +
-                Axes.tickText(d.ya, d.ya.c2l(d.yerrneg), 'hover').text;
-        }
-        else d.yLabel += ' ± ' + yeText;
 
-        if(hovermode === 'y') d.distance += 1;
+    // small distance penalty for error bars, so that if there are
+    // traces with errors and some without, the error bar label will
+    // hoist up to the point
+    if (hovermode === "x") d.distance += 1;
+  }
+  if (!isNaN(d.yerr) && !(d.ya.type === "log" && d.yerr <= 0)) {
+    var yeText = Axes.tickText(d.ya, d.ya.c2l(d.yerr), "hover").text;
+    if (d.yerrneg !== undefined) {
+      d.yLabel += " +" +
+        yeText +
+        " / -" +
+        Axes.tickText(d.ya, d.ya.c2l(d.yerrneg), "hover").text;
+    } else {
+      d.yLabel += " \xB1 " + yeText;
     }
 
-    var infomode = d.trace.hoverinfo;
-    if(infomode !== 'all') {
-        infomode = infomode.split('+');
-        if(infomode.indexOf('x') === -1) d.xLabel = undefined;
-        if(infomode.indexOf('y') === -1) d.yLabel = undefined;
-        if(infomode.indexOf('z') === -1) d.zLabel = undefined;
-        if(infomode.indexOf('text') === -1) d.text = undefined;
-        if(infomode.indexOf('name') === -1) d.name = undefined;
-    }
+    if (hovermode === "y") d.distance += 1;
+  }
+
+  var infomode = d.trace.hoverinfo;
+  if (infomode !== "all") {
+    infomode = infomode.split("+");
+    if (infomode.indexOf("x") === -1) d.xLabel = undefined;
+    if (infomode.indexOf("y") === -1) d.yLabel = undefined;
+    if (infomode.indexOf("z") === -1) d.zLabel = undefined;
+    if (infomode.indexOf("text") === -1) d.text = undefined;
+    if (infomode.indexOf("name") === -1) d.name = undefined;
+  }
 
-    return d;
+  return d;
 }
 
 fx.loneHover = function(hoverItem, opts) {
-    // draw a single hover item in a pre-existing svg container somewhere
-    // hoverItem should have keys:
-    //    - x and y (or x0, x1, y0, and y1):
-    //      the pixel position to mark, relative to opts.container
-    //    - xLabel, yLabel, zLabel, text, and name:
-    //      info to go in the label
-    //    - color:
-    //      the background color for the label. text & outline color will
-    //      be chosen black or white to contrast with this
-    // opts should have keys:
-    //    - bgColor:
-    //      the background color this is against, used if the trace is
-    //      non-opaque, and for the name, which goes outside the box
-    //    - container:
-    //      a dom