From c5cb42c7195d7d11f0d8c7c91d0f4451a074c221 Mon Sep 17 00:00:00 2001
From: alexcjohnson <alex@plot.ly>
Date: Sun, 23 Sep 2018 15:17:19 -0400
Subject: [PATCH 01/10] fix up cleanDate error reporting no error on
 `undefined` but yes on non-finite numbers

---
 src/lib/dates.js                    | 4 +++-
 test/jasmine/tests/lib_date_test.js | 8 +++++---
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/lib/dates.js b/src/lib/dates.js
index 67828b32837..1027a797928 100644
--- a/src/lib/dates.js
+++ b/src/lib/dates.js
@@ -345,7 +345,9 @@ function includeTime(dateStr, h, m, s, msec10) {
 // 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') {
+    // let us use cleanDate to provide a missing default without an error
+    if(v === BADNUM) return dflt;
+    if(exports.isJSDate(v) || (typeof v === 'number' && isFinite(v))) {
         // do not allow milliseconds (old) or jsdate objects (inherently
         // described as gregorian dates) with world calendars
         if(isWorldCalendar(calendar)) {
diff --git a/test/jasmine/tests/lib_date_test.js b/test/jasmine/tests/lib_date_test.js
index 2a0971afd7b..6bccb8a9865 100644
--- a/test/jasmine/tests/lib_date_test.js
+++ b/test/jasmine/tests/lib_date_test.js
@@ -391,20 +391,22 @@ describe('dates', function() {
                 errors.push(msg);
             });
 
-            [
+            var cases = [
                 new Date(-20000, 0, 1),
                 new Date(20000, 0, 1),
                 new Date('fail'),
                 undefined, null, NaN,
                 [], {}, [0], {1: 2}, '',
                 '2001-02-29'  // not a leap year
-            ].forEach(function(v) {
+            ];
+            cases.forEach(function(v) {
                 expect(Lib.cleanDate(v)).toBeUndefined();
                 if(!isNumeric(+v)) expect(Lib.cleanDate(+v)).toBeUndefined();
                 expect(Lib.cleanDate(v, '2000-01-01')).toBe('2000-01-01');
             });
 
-            expect(errors.length).toBe(16);
+            // two errors for each case except `undefined`
+            expect(errors.length).toBe(2 * (cases.length - 1));
         });
 
         it('should not alter valid date strings, even to truncate them', function() {

From e066bb91c6bc291647604878328f2eeaadffb986 Mon Sep 17 00:00:00 2001
From: alexcjohnson <alex@plot.ly>
Date: Sun, 23 Sep 2018 15:19:28 -0400
Subject: [PATCH 02/10] fix `legend.traceorder` when all traces are
 `legendonly`

---
 src/plots/plots.js             |  5 ++++-
 test/jasmine/tests/bar_test.js | 12 ++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/src/plots/plots.js b/src/plots/plots.js
index 4cc227fb3cb..a6907a04041 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -1482,7 +1482,10 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans
     }
 
     // trace module layout defaults
-    var modules = layoutOut._visibleModules;
+    // use _modules rather than _visibleModules so that even
+    // legendonly traces can include settings - eg barmode, which affects
+    // legend.traceorder default value.
+    var modules = layoutOut._modules;
     for(i = 0; i < modules.length; i++) {
         _module = modules[i];
 
diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js
index e7b3f395a1e..dc9ec698751 100644
--- a/test/jasmine/tests/bar_test.js
+++ b/test/jasmine/tests/bar_test.js
@@ -1361,6 +1361,7 @@ describe('bar visibility toggling:', function() {
             spyOn(gd._fullData[0]._module, 'crossTraceCalc').and.callThrough();
 
             _assert('base', [0.5, 3.5], [-2.222, 2.222], 0);
+            expect(gd._fullLayout.legend.traceorder).toBe('normal');
             return Plotly.restyle(gd, 'visible', false, [1]);
         })
         .then(function() {
@@ -1369,6 +1370,11 @@ describe('bar visibility toggling:', function() {
         })
         .then(function() {
             _assert('both invisible', [0.5, 3.5], [0, 2.105], 0);
+            return Plotly.restyle(gd, 'visible', 'legendonly');
+        })
+        .then(function() {
+            _assert('both legendonly', [0.5, 3.5], [0, 2.105], 0);
+            expect(gd._fullLayout.legend.traceorder).toBe('normal');
             return Plotly.restyle(gd, 'visible', true, [1]);
         })
         .then(function() {
@@ -1391,6 +1397,7 @@ describe('bar visibility toggling:', function() {
             spyOn(gd._fullData[0]._module, 'crossTraceCalc').and.callThrough();
 
             _assert('base', [0.5, 3.5], [0, 5.263], 0);
+            expect(gd._fullLayout.legend.traceorder).toBe('reversed');
             return Plotly.restyle(gd, 'visible', false, [1]);
         })
         .then(function() {
@@ -1399,6 +1406,11 @@ describe('bar visibility toggling:', function() {
         })
         .then(function() {
             _assert('both invisible', [0.5, 3.5], [0, 2.105], 0);
+            return Plotly.restyle(gd, 'visible', 'legendonly');
+        })
+        .then(function() {
+            _assert('both legendonly', [0.5, 3.5], [0, 2.105], 0);
+            expect(gd._fullLayout.legend.traceorder).toBe('reversed');
             return Plotly.restyle(gd, 'visible', true, [1]);
         })
         .then(function() {

From ab20fae9a57d7b418accb1af3d93d66f4046a195 Mon Sep 17 00:00:00 2001
From: alexcjohnson <alex@plot.ly>
Date: Sun, 23 Sep 2018 21:21:45 -0400
Subject: [PATCH 03/10] pull out tick0/dtick validation logic for reuse

---
 src/plots/cartesian/clean_ticks.js         | 87 ++++++++++++++++++++++
 src/plots/cartesian/tick_value_defaults.js | 50 ++-----------
 2 files changed, 93 insertions(+), 44 deletions(-)
 create mode 100644 src/plots/cartesian/clean_ticks.js

diff --git a/src/plots/cartesian/clean_ticks.js b/src/plots/cartesian/clean_ticks.js
new file mode 100644
index 00000000000..a6a51bef9a7
--- /dev/null
+++ b/src/plots/cartesian/clean_ticks.js
@@ -0,0 +1,87 @@
+/**
+* Copyright 2012-2018, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+var isNumeric = require('fast-isnumeric');
+var Lib = require('../../lib');
+var ONEDAY = require('../../constants/numerical').ONEDAY;
+
+/**
+ * Return a validated dtick value for this axis
+ *
+ * @param {any} dtick: the candidate dtick. valid values are numbers and strings,
+ *     and further constrained depending on the axis type.
+ * @param {string} axType: the axis type
+ */
+exports.dtick = function(dtick, axType) {
+    var isLog = axType === 'log';
+    var isDate = axType === 'date';
+    var isCat = axType === 'category';
+    var dtickDflt = isDate ? ONEDAY : 1;
+
+    if(!dtick) return dtickDflt;
+
+    if(isNumeric(dtick)) {
+        dtick = Number(dtick);
+        if(dtick <= 0) return dtickDflt;
+        if(isCat) {
+            // category dtick must be positive integers
+            return Math.max(1, Math.round(dtick));
+        }
+        if(isDate) {
+            // date dtick must be at least 0.1ms (our current precision)
+            return Math.max(0.1, dtick);
+        }
+        return dtick;
+    }
+
+    if(typeof dtick !== 'string' || !(isDate || isLog)) {
+        return dtickDflt;
+    }
+
+    var prefix = dtick.charAt(0);
+    var dtickNum = dtick.substr(1);
+    dtickNum = isNumeric(dtickNum) ? Number(dtickNum) : 0;
+
+    if((dtickNum <= 0) || !(
+            // "M<n>" gives ticks every (integer) n months
+            (isDate && prefix === 'M' && dtickNum === Math.round(dtickNum)) ||
+            // "L<f>" gives ticks linearly spaced in data (not in position) every (float) f
+            (isLog && prefix === 'L') ||
+            // "D1" gives powers of 10 with all small digits between, "D2" gives only 2 and 5
+            (isLog && prefix === 'D' && (dtickNum === 1 || dtickNum === 2))
+        )) {
+        return dtickDflt;
+    }
+
+    return dtick;
+};
+
+/**
+ * Return a validated tick0 for this axis
+ *
+ * @param {any} tick0: the candidate tick0. Valid values are numbers and strings,
+ *     further constrained depending on the axis type
+ * @param {string} axType: the axis type
+ * @param {string} calendar: for date axes, the calendar to validate/convert with
+ * @param {any} dtick: an already valid dtick. Only used for D1 and D2 log dticks,
+ *     which do not support tick0 at all.
+ */
+exports.tick0 = function(tick0, axType, calendar, dtick) {
+    if(axType === 'date') {
+        return Lib.cleanDate(tick0, Lib.dateTick0(calendar));
+    }
+    if(dtick === 'D1' || dtick === 'D2') {
+        // D1 and D2 modes ignore tick0 entirely
+        return undefined;
+    }
+    // Aside from date axes, tick0 must be numeric
+    return isNumeric(tick0) ? Number(tick0) : 0;
+};
diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js
index ca3e1aa356d..f5aff20aefa 100644
--- a/src/plots/cartesian/tick_value_defaults.js
+++ b/src/plots/cartesian/tick_value_defaults.js
@@ -9,9 +9,7 @@
 
 'use strict';
 
-var isNumeric = require('fast-isnumeric');
-var Lib = require('../../lib');
-var ONEDAY = require('../../constants/numerical').ONEDAY;
+var cleanTicks = require('./clean_ticks');
 
 
 module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType) {
@@ -33,47 +31,11 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe
     else if(tickmode === 'linear') {
         // dtick is usually a positive number, but there are some
         // special strings available for log or date axes
-        // default is 1 day for dates, otherwise 1
-        var dtickDflt = (axType === 'date') ? ONEDAY : 1;
-        var dtick = coerce('dtick', dtickDflt);
-        if(isNumeric(dtick)) {
-            containerOut.dtick = (dtick > 0) ? Number(dtick) : dtickDflt;
-        }
-        else if(typeof dtick !== 'string') {
-            containerOut.dtick = dtickDflt;
-        }
-        else {
-            // date and log special cases are all one character plus a number
-            var prefix = dtick.charAt(0),
-                dtickNum = dtick.substr(1);
-
-            dtickNum = isNumeric(dtickNum) ? Number(dtickNum) : 0;
-            if((dtickNum <= 0) || !(
-                    // "M<n>" gives ticks every (integer) n months
-                    (axType === 'date' && prefix === 'M' && dtickNum === Math.round(dtickNum)) ||
-                    // "L<f>" gives ticks linearly spaced in data (not in position) every (float) f
-                    (axType === 'log' && prefix === 'L') ||
-                    // "D1" gives powers of 10 with all small digits between, "D2" gives only 2 and 5
-                    (axType === 'log' && prefix === 'D' && (dtickNum === 1 || dtickNum === 2))
-                )) {
-                containerOut.dtick = dtickDflt;
-            }
-        }
-
-        // tick0 can have different valType for different axis types, so
-        // validate that now. Also for dates, change milliseconds to date strings
-        var tick0Dflt = (axType === 'date') ? Lib.dateTick0(containerOut.calendar) : 0;
-        var tick0 = coerce('tick0', tick0Dflt);
-        if(axType === 'date') {
-            containerOut.tick0 = Lib.cleanDate(tick0, tick0Dflt);
-        }
-        // Aside from date axes, dtick must be numeric; D1 and D2 modes ignore tick0 entirely
-        else if(isNumeric(tick0) && dtick !== 'D1' && dtick !== 'D2') {
-            containerOut.tick0 = Number(tick0);
-        }
-        else {
-            containerOut.tick0 = tick0Dflt;
-        }
+        // tick0 also has special logic
+        var dtick = containerOut.dtick = cleanTicks.dtick(
+            containerIn.dtick, axType);
+        containerOut.tick0 = cleanTicks.tick0(
+            containerIn.tick0, axType, containerOut.calendar, dtick);
     }
     else {
         var tickvals = coerce('tickvals');

From a8e48020abfc748713a6c1781518cf4edaa5cad9 Mon Sep 17 00:00:00 2001
From: alexcjohnson <alex@plot.ly>
Date: Sun, 23 Sep 2018 21:30:45 -0400
Subject: [PATCH 04/10] extend Axes.autoBin to find start/end with fixed size

---
 src/plots/cartesian/axes.js | 82 +++++++++++++++++++++----------------
 1 file changed, 47 insertions(+), 35 deletions(-)

diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index f8d1b4e8369..c2352fcdc33 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -21,6 +21,7 @@ var Color = require('../../components/color');
 var Drawing = require('../../components/drawing');
 
 var axAttrs = require('./layout_attributes');
+var cleanTicks = require('./clean_ticks');
 
 var constants = require('../../constants/numerical');
 var ONEAVGYEAR = constants.ONEAVGYEAR;
@@ -280,43 +281,22 @@ axes.saveShowSpikeInitial = function(gd, overwrite) {
     return hasOneAxisChanged;
 };
 
-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;
+axes.autoBin = function(data, ax, nbins, is2d, calendar, size) {
+    var dataMin = Lib.aggNums(Math.min, null, data);
+    var dataMax = Lib.aggNums(Math.max, null, data);
 
     if(ax.type === 'category') {
         return {
             start: dataMin - 0.5,
             end: dataMax + 0.5,
-            size: 1,
+            size: Math.max(1, Math.round(size) || 1),
             _dataSpan: dataMax - dataMin,
         };
     }
 
-    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;
-    }
+    if(!calendar) calendar = ax.calendar;
 
-    // piggyback off autotick code to make "nice" bin sizes
+    // piggyback off tick code to make "nice" bin sizes and edges
     var dummyAx;
     if(ax.type === 'log') {
         dummyAx = {
@@ -333,19 +313,51 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {
     }
     axes.setConvert(dummyAx);
 
-    axes.autoTicks(dummyAx, size0);
+    size = size && cleanTicks.dtick(size, dummyAx.type);
+
+    if(size) {
+        dummyAx.dtick = size;
+        dummyAx.tick0 = cleanTicks.tick0(undefined, dummyAx.type, calendar);
+    }
+    else {
+        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);
+            var msexp = Math.pow(10, Math.floor(
+                Math.log(distinctData.minDiff) / Math.LN10));
+            var 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;
+        }
+
+        axes.autoTicks(dummyAx, size0);
+    }
+
+
+    var finalSize = dummyAx.dtick;
     var binStart = axes.tickIncrement(
-            axes.tickFirst(dummyAx), dummyAx.dtick, 'reverse', calendar);
+            axes.tickFirst(dummyAx), finalSize, 'reverse', calendar);
     var binEnd, bincount;
 
     // 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') {
+    if(typeof finalSize === 'number') {
         binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax);
 
-        bincount = 1 + Math.floor((dataMax - binStart) / dummyAx.dtick);
-        binEnd = binStart + bincount * dummyAx.dtick;
+        bincount = 1 + Math.floor((dataMax - binStart) / finalSize);
+        binEnd = binStart + bincount * finalSize;
     }
     else {
         // month ticks - should be the only nonlinear kind we have at this point.
@@ -354,7 +366,7 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {
         // 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);
+            binStart = autoShiftMonthBins(binStart, data, finalSize, dataMin, calendar);
         }
 
         // calculate the endpoint for nonlinear ticks - you have to
@@ -362,7 +374,7 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {
         binEnd = binStart;
         bincount = 0;
         while(binEnd <= dataMax) {
-            binEnd = axes.tickIncrement(binEnd, dummyAx.dtick, false, calendar);
+            binEnd = axes.tickIncrement(binEnd, finalSize, false, calendar);
             bincount++;
         }
     }
@@ -370,7 +382,7 @@ axes.autoBin = function(data, ax, nbins, is2d, calendar) {
     return {
         start: ax.c2r(binStart, 0, calendar),
         end: ax.c2r(binEnd, 0, calendar),
-        size: dummyAx.dtick,
+        size: finalSize,
         _dataSpan: dataMax - dataMin
     };
 };

From 536ce480be5802c92c8c53d386ae8ce1c6a506ea Mon Sep 17 00:00:00 2001
From: alexcjohnson <alex@plot.ly>
Date: Mon, 24 Sep 2018 16:28:51 -0400
Subject: [PATCH 05/10] new histogram autobin algo

---
 src/plot_api/helpers.js                     |  13 +
 src/plot_api/plot_api.js                    |  33 ++-
 src/plots/plots.js                          |  21 +-
 src/traces/bar/layout_defaults.js           |   2 +-
 src/traces/histogram/attributes.js          |  87 +++----
 src/traces/histogram/bin_defaults.js        |  32 ---
 src/traces/histogram/calc.js                | 253 ++++++++++----------
 src/traces/histogram/clean_bins.js          |  78 ------
 src/traces/histogram/clean_data.js          | 112 +++++++++
 src/traces/histogram/defaults.js            |   4 +-
 src/traces/histogram/index.js               |   1 +
 src/traces/histogram2d/attributes.js        |   8 +-
 src/traces/histogram2d/calc.js              |  59 +++--
 src/traces/histogram2d/clean_data.js        |  93 +++++++
 src/traces/histogram2d/index.js             |   1 +
 src/traces/histogram2d/sample_defaults.js   |   6 +-
 src/traces/histogram2dcontour/attributes.js |   8 +-
 src/traces/histogram2dcontour/index.js      |   1 +
 test/jasmine/tests/axes_test.js             |   2 +-
 test/jasmine/tests/histogram2d_test.js      |  73 +++---
 test/jasmine/tests/histogram_test.js        | 194 +++++++++++----
 test/jasmine/tests/plot_api_test.js         |  25 +-
 22 files changed, 694 insertions(+), 412 deletions(-)
 delete mode 100644 src/traces/histogram/bin_defaults.js
 delete mode 100644 src/traces/histogram/clean_bins.js
 create mode 100644 src/traces/histogram/clean_data.js
 create mode 100644 src/traces/histogram2d/clean_data.js

diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js
index 8a49ae911ae..6a4208e1cde 100644
--- a/src/plot_api/helpers.js
+++ b/src/plot_api/helpers.js
@@ -386,6 +386,19 @@ exports.cleanData = function(data) {
         // sanitize rgb(fractions) and rgba(fractions) that old tinycolor
         // supported, but new tinycolor does not because they're not valid css
         Color.clean(trace);
+
+        // remove obsolete autobin(x|y) attributes, but only if true
+        // if false, this needs to happen in Histogram.calc because it
+        // can be a one-time autobin so we need to know the results before
+        // we can push them back into the trace.
+        if(trace.autobinx) {
+            delete trace.autobinx;
+            delete trace.xbins;
+        }
+        if(trace.autobiny) {
+            delete trace.autobiny;
+            delete trace.ybins;
+        }
     }
 };
 
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index 9bd8007dfc2..97014e69014 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -1434,6 +1434,18 @@ function _restyle(gd, aobj, traces) {
         }
     }
 
+    function allBins(binAttr) {
+        return function(j) {
+            return fullData[j][binAttr];
+        };
+    }
+
+    function arrayBins(binAttr) {
+        return function(vij, j) {
+            return vij === false ? fullData[traces[j]][binAttr] : null;
+        };
+    }
+
     // 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) {
@@ -1449,6 +1461,17 @@ function _restyle(gd, aobj, traces) {
             newVal,
             valObject;
 
+        // Backward compatibility shim for turning histogram autobin on,
+        // or freezing previous autobinned values.
+        // Replace obsolete `autobin(x|y): true` with `(x|y)bins: null`
+        // and `autobin(x|y): false` with the `(x|y)bins` in `fullData`
+        if(ai === 'autobinx' || ai === 'autobiny') {
+            ai = ai.charAt(ai.length - 1) + 'bins';
+            if(Array.isArray(vi)) vi = vi.map(arrayBins(ai));
+            else if(vi === false) vi = traces.map(allBins(ai));
+            else vi = null;
+        }
+
         redoit[ai] = vi;
 
         if(ai.substr(0, 6) === 'LAYOUT') {
@@ -1609,8 +1632,12 @@ function _restyle(gd, aobj, traces) {
             }
         }
 
-        // major enough changes deserve autoscale, autobin, and
+        // Major enough changes deserve autoscale and
         // non-reversed axes so people don't get confused
+        //
+        // Note: autobin (or its new analog bin clearing) is not included here
+        // since we're not pushing bins back to gd.data, so if we have bin
+        // info it was explicitly provided by the user.
         if(['orientation', 'type'].indexOf(ai) !== -1) {
             axlist = [];
             for(i = 0; i < traces.length; i++) {
@@ -1619,10 +1646,6 @@ function _restyle(gd, aobj, traces) {
                 if(Registry.traceIs(trace, 'cartesian')) {
                     addToAxlist(trace.xaxis || 'x');
                     addToAxlist(trace.yaxis || 'y');
-
-                    if(ai === 'type') {
-                        doextra(['autobinx', 'autobiny'], true, i);
-                    }
                 }
             }
 
diff --git a/src/plots/plots.js b/src/plots/plots.js
index a6907a04041..6c10a889201 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -428,13 +428,6 @@ plots.supplyDefaults = function(gd, opts) {
     // attach helper method to check whether a plot type is present on graph
     newFullLayout._has = plots._hasPlotType.bind(newFullLayout);
 
-    // special cases that introduce interactions between traces
-    var _modules = newFullLayout._visibleModules;
-    for(i = 0; i < _modules.length; i++) {
-        var _module = _modules[i];
-        if(_module.cleanData) _module.cleanData(newFullData);
-    }
-
     if(oldFullData.length === newFullData.length) {
         for(i = 0; i < newFullData.length; i++) {
             relinkPrivateKeys(newFullData[i], oldFullData[i]);
@@ -444,6 +437,20 @@ plots.supplyDefaults = function(gd, opts) {
     // finally, fill in the pieces of layout that may need to look at data
     plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData, gd._transitionData);
 
+    // Special cases that introduce interactions between traces.
+    // This is after relinkPrivateKeys so we can use those in cleanData
+    // and after layout module defaults, so we can use eg barmode
+    var _modules = newFullLayout._visibleModules;
+    var cleanDataFuncs = [];
+    for(i = 0; i < _modules.length; i++) {
+        var _module = _modules[i];
+        // some trace types share cleanData (ie histogram2d, histogram2dcontour)
+        if(_module.cleanData) Lib.pushUnique(cleanDataFuncs, _module.cleanData);
+    }
+    for(i = 0; i < cleanDataFuncs.length; i++) {
+        cleanDataFuncs[i](newFullData, newFullLayout);
+    }
+
     // turn on flag to optimize large splom-only graphs
     // mostly by omitting SVG layers during Cartesian.drawFramework
     newFullLayout._hasOnlyLargeSploms = (
diff --git a/src/traces/bar/layout_defaults.js b/src/traces/bar/layout_defaults.js
index 274b0696282..4809ce1d035 100644
--- a/src/traces/bar/layout_defaults.js
+++ b/src/traces/bar/layout_defaults.js
@@ -28,7 +28,7 @@ module.exports = function(layoutIn, layoutOut, fullData) {
 
     for(var i = 0; i < fullData.length; i++) {
         var trace = fullData[i];
-        if(Registry.traceIs(trace, 'bar')) hasBars = true;
+        if(Registry.traceIs(trace, 'bar') && trace.visible) hasBars = true;
         else continue;
 
         // if we have at least 2 grouped bar traces on the same subplot,
diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js
index f15e85b6fc2..bd9e3df112b 100644
--- a/src/traces/histogram/attributes.js
+++ b/src/traces/histogram/attributes.js
@@ -125,24 +125,6 @@ module.exports = {
         },
         editType: 'calc'
     },
-
-    autobinx: {
-        valType: 'boolean',
-        dflt: null,
-        role: 'style',
-        editType: 'calc',
-        impliedEdits: {
-            'xbins.start': undefined,
-            'xbins.end': undefined,
-            'xbins.size': undefined
-        },
-        description: [
-            'Determines whether or not the x axis bin attributes are picked',
-            'by an algorithm. Note that this should be set to false if you',
-            'want to manually set the number of bins using the attributes in',
-            'xbins.'
-        ].join(' ')
-    },
     nbinsx: {
         valType: 'integer',
         min: 0,
@@ -152,28 +134,12 @@ module.exports = {
         description: [
             'Specifies the maximum number of desired bins. This value will be used',
             'in an algorithm that will decide the optimal bin size such that the',
-            'histogram best visualizes the distribution of the data.'
+            'histogram best visualizes the distribution of the data.',
+            'Ignored if `xbins.size` is provided.'
         ].join(' ')
     },
     xbins: makeBinsAttr('x'),
 
-    autobiny: {
-        valType: 'boolean',
-        dflt: null,
-        role: 'style',
-        editType: 'calc',
-        impliedEdits: {
-            'ybins.start': undefined,
-            'ybins.end': undefined,
-            'ybins.size': undefined
-        },
-        description: [
-            'Determines whether or not the y axis bin attributes are picked',
-            'by an algorithm. Note that this should be set to false if you',
-            'want to manually set the number of bins using the attributes in',
-            'ybins.'
-        ].join(' ')
-    },
     nbinsy: {
         valType: 'integer',
         min: 0,
@@ -183,7 +149,8 @@ module.exports = {
         description: [
             'Specifies the maximum number of desired bins. This value will be used',
             'in an algorithm that will decide the optimal bin size such that the',
-            'histogram best visualizes the distribution of the data.'
+            'histogram best visualizes the distribution of the data.',
+            'Ignored if `ybins.size` is provided.'
         ].join(' ')
     },
     ybins: makeBinsAttr('y'),
@@ -194,23 +161,46 @@ module.exports = {
     unselected: barAttrs.unselected,
 
     _deprecated: {
-        bardir: barAttrs._deprecated.bardir
+        bardir: barAttrs._deprecated.bardir,
+        autobinx: {
+            valType: 'boolean',
+            dflt: null,
+            role: 'style',
+            editType: 'calc',
+            impliedEdits: {
+                'xbins.start': undefined,
+                'xbins.end': undefined,
+                'xbins.size': undefined
+            },
+            description: [
+                'Obsolete: since v1.42 each bin',
+                'attribute is auto-determined separately.'
+            ].join(' ')
+        },
+        autobiny: {
+            valType: 'boolean',
+            dflt: null,
+            role: 'style',
+            editType: 'calc',
+            impliedEdits: {
+                'ybins.start': undefined,
+                'ybins.end': undefined,
+                'ybins.size': undefined
+            },
+            description: [
+                'Obsolete: since v1.42 each bin',
+                'attribute is auto-determined separately.'
+            ].join(' ')
+        }
     }
 };
 
 function makeBinsAttr(axLetter) {
-    var impliedEdits = {};
-    impliedEdits['autobin' + axLetter] = false;
-    var impliedEditsInner = {};
-    impliedEditsInner['^autobin' + axLetter] = false;
-
     return {
         start: {
             valType: 'any', // for date axes
-            dflt: null,
             role: 'style',
             editType: 'calc',
-            impliedEdits: impliedEditsInner,
             description: [
                 'Sets the starting value for the', axLetter,
                 'axis bins.'
@@ -218,10 +208,8 @@ function makeBinsAttr(axLetter) {
         },
         end: {
             valType: 'any', // for date axes
-            dflt: null,
             role: 'style',
             editType: 'calc',
-            impliedEdits: impliedEditsInner,
             description: [
                 'Sets the end value for the', axLetter,
                 'axis bins.'
@@ -229,16 +217,13 @@ function makeBinsAttr(axLetter) {
         },
         size: {
             valType: 'any', // for date axes
-            dflt: null,
             role: 'style',
             editType: 'calc',
-            impliedEdits: impliedEditsInner,
             description: [
                 'Sets the step in-between value each', axLetter,
                 'axis bin.'
             ].join(' ')
         },
-        editType: 'calc',
-        impliedEdits: impliedEdits
+        editType: 'calc'
     };
 }
diff --git a/src/traces/histogram/bin_defaults.js b/src/traces/histogram/bin_defaults.js
deleted file mode 100644
index 77259579edd..00000000000
--- a/src/traces/histogram/bin_defaults.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
-* Copyright 2012-2018, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
-
-
-'use strict';
-
-
-module.exports = function handleBinDefaults(traceIn, traceOut, coerce, binDirections) {
-    coerce('histnorm');
-
-    binDirections.forEach(function(binDirection) {
-        /*
-         * Because date axes have string values for start and end,
-         * and string options for size, we cannot validate these attributes
-         * now. We will do this during calc (immediately prior to binning)
-         * in ./clean_bins, and push the cleaned values back to _fullData.
-         */
-        coerce(binDirection + 'bins.start');
-        coerce(binDirection + 'bins.end');
-        coerce(binDirection + 'bins.size');
-
-        var autobin = coerce('autobin' + binDirection);
-        if(autobin !== false) coerce('nbins' + binDirection);
-    });
-
-    return traceOut;
-};
diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js
index 7d4659719c2..5b66465a5f5 100644
--- a/src/traces/histogram/calc.js
+++ b/src/traces/histogram/calc.js
@@ -18,8 +18,6 @@ var arraysToCalcdata = require('../bar/arrays_to_calcdata');
 var binFunctions = require('./bin_functions');
 var normFunctions = require('./norm_functions');
 var doAvg = require('./average');
-var cleanBins = require('./clean_bins');
-var oneMonth = require('../../constants/numerical').ONEAVGMONTH;
 var getBinSpanLabelRound = require('./bin_label_vals');
 
 module.exports = function calc(gd, trace) {
@@ -38,8 +36,6 @@ module.exports = function calc(gd, trace) {
     var cumulativeSpec = trace.cumulative;
     var i;
 
-    cleanBins(trace, pa, mainData);
-
     var binsAndPos = calcAllAutoBins(gd, trace, pa, mainData);
     var binSpec = binsAndPos[0];
     var pos0 = binsAndPos[1];
@@ -217,8 +213,26 @@ module.exports = function calc(gd, trace) {
  */
 function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) {
     var binAttr = mainData + 'bins';
-    var isOverlay = gd._fullLayout.barmode === 'overlay';
-    var i, tracei, calendar, firstManual, pos0;
+    var fullLayout = gd._fullLayout;
+    var isOverlay = fullLayout.barmode === 'overlay';
+    var i, traces, tracei, calendar, pos0, autoVals, cumulativeSpec;
+
+    var cleanBound = (pa.type === 'date') ?
+        function(v) { return (v || v === 0) ? Lib.cleanDate(v, null, pa.calendar) : null; } :
+        function(v) { return isNumeric(v) ? Number(v) : null; };
+
+    function setBound(attr, bins, newBins) {
+        if(bins[attr + 'Found']) {
+            bins[attr] = cleanBound(bins[attr]);
+            if(bins[attr] === null) bins[attr] = newBins[attr];
+        }
+        else {
+            autoVals[attr] = bins[attr] = newBins[attr];
+            Lib.nestedProperty(traces[0], binAttr + '.' + attr).set(newBins[attr]);
+        }
+    }
+
+    var binOpts = fullLayout._histogramBinOpts[trace._groupName];
 
     // all but the first trace in this group has already been marked finished
     // clear this flag, so next time we run calc we will run autobin again
@@ -226,121 +240,131 @@ function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) {
         delete trace._autoBinFinished;
     }
     else {
-        // must be the first trace in the group - do the autobinning on them all
-
-        // find all grouped traces - in overlay mode each trace is independent
-        var traceGroup = isOverlay ? [trace] : getConnectedHistograms(gd, trace);
-        var autoBinnedTraces = [];
-
-        var minSize = Infinity;
-        var minStart = Infinity;
-        var maxEnd = -Infinity;
-
-        var autoBinAttr = 'autobin' + mainData;
-
-        for(i = 0; i < traceGroup.length; i++) {
-            tracei = traceGroup[i];
-
-            // stash pos0 on the trace so we don't need to duplicate this
-            // in the main body of calc
+        traces = binOpts.traces;
+        var sizeFound = binOpts.sizeFound;
+        var allPos = [];
+        autoVals = traces[0]._autoBin = {};
+        // Note: we're including `legendonly` traces here for autobin purposes,
+        // so that showing & hiding from the legend won't affect bins.
+        // But this complicates things a bit since those traces don't `calc`,
+        // hence `isFirstVisible`.
+        var isFirstVisible = true;
+        for(i = 0; i < traces.length; i++) {
+            tracei = traces[i];
             pos0 = tracei._pos0 = pa.makeCalcdata(tracei, mainData);
-            var binSpec = tracei[binAttr];
-
-            if((tracei[autoBinAttr]) || !binSpec ||
-                    binSpec.start === null || binSpec.end === null) {
-                calendar = tracei[mainData + 'calendar'];
-                var cumulativeSpec = tracei.cumulative;
-
-                binSpec = Axes.autoBin(pos0, pa, tracei['nbins' + mainData], false, calendar);
-
-                // Edge case: single-valued histogram overlaying others
-                // Use them all together to calculate the bin size for the single-valued one
-                if(isOverlay && binSpec._dataSpan === 0 && pa.type !== 'category') {
-                    // Several single-valued histograms! Stop infinite recursion,
-                    // just return an extra flag that tells handleSingleValueOverlays
-                    // to sort out this trace too
-                    if(_overlayEdgeCase) return [binSpec, pos0, true];
-
-                    binSpec = handleSingleValueOverlays(gd, trace, pa, mainData, binAttr);
+            allPos = allPos.concat(pos0);
+            delete tracei._autoBinFinished;
+            if(trace.visible === true) {
+                if(isFirstVisible) {
+                    isFirstVisible = false;
                 }
-
-                // adjust for CDF edge cases
-                if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) {
-                    if(cumulativeSpec.direction === 'decreasing') {
-                        minStart = Math.min(minStart, pa.r2c(binSpec.start, 0, calendar) - binSpec.size);
-                    }
-                    else {
-                        maxEnd = Math.max(maxEnd, pa.r2c(binSpec.end, 0, calendar) + binSpec.size);
-                    }
+                else {
+                    delete tracei._autoBin;
+                    tracei._autoBinFinished = 1;
                 }
+            }
+        }
+        calendar = traces[0][mainData + 'calendar'];
+        var newBinSpec = Axes.autoBin(
+            allPos, pa, binOpts.nbins, false, calendar, sizeFound && binOpts.size);
+
+        // Edge case: single-valued histogram overlaying others
+        // Use them all together to calculate the bin size for the single-valued one
+        if(isOverlay && newBinSpec._dataSpan === 0 && pa.type !== 'category') {
+            // Several single-valued histograms! Stop infinite recursion,
+            // just return an extra flag that tells handleSingleValueOverlays
+            // to sort out this trace too
+            if(_overlayEdgeCase) return [newBinSpec, pos0, true];
+
+            newBinSpec = handleSingleValueOverlays(gd, trace, pa, mainData, binAttr);
+        }
 
-                // note that it's possible to get here with an explicit autobin: false
-                // if the bins were not specified. mark this trace for followup
-                autoBinnedTraces.push(tracei);
+        // adjust for CDF edge cases
+        cumulativeSpec = tracei.cumulative;
+        if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) {
+            if(cumulativeSpec.direction === 'decreasing') {
+                newBinSpec.start = pa.c2r(Axes.tickIncrement(
+                    pa.r2c(newBinSpec.start, 0, calendar),
+                    newBinSpec.size, true, calendar
+                ));
             }
-            else if(!firstManual) {
-                // Remember the first manually set binSpec. We'll try to be extra
-                // accommodating of this one, so other bins line up with these.
-                // But if there's more than one manual bin set and they're mutually
-                // inconsistent, then there's not much we can do...
-                firstManual = {
-                    size: binSpec.size,
-                    start: pa.r2c(binSpec.start, 0, calendar),
-                    end: pa.r2c(binSpec.end, 0, calendar)
-                };
+            else {
+                newBinSpec.end = pa.c2r(Axes.tickIncrement(
+                    pa.r2c(newBinSpec.end, 0, calendar),
+                    newBinSpec.size, false, calendar
+                ));
             }
-
-            // Even non-autobinned traces get included here, so we get the greatest extent
-            // and minimum bin size of them all.
-            // But manually binned traces won't be adjusted, even if the auto values
-            // are inconsistent with the manual ones (or the manual ones are inconsistent
-            // with each other).
-            minSize = getMinSize(minSize, binSpec.size);
-            minStart = Math.min(minStart, pa.r2c(binSpec.start, 0, calendar));
-            maxEnd = Math.max(maxEnd, pa.r2c(binSpec.end, 0, calendar));
-
-            // add the flag that lets us abort autobin on later traces
-            if(i) tracei._autoBinFinished = 1;
         }
 
-        // do what we can to match the auto bins to the first manual bins
-        // but only if sizes are all numeric
-        if(firstManual && isNumeric(firstManual.size) && isNumeric(minSize)) {
-            // first need to ensure the bin size is the same as or an integer fraction
-            // of the first manual bin
-            // allow the bin size to increase just under the autobin step size to match,
-            // (which is a factor of 2 or 2.5) otherwise shrink it
-            if(minSize > firstManual.size / 1.9) minSize = firstManual.size;
-            else minSize = firstManual.size / Math.ceil(firstManual.size / minSize);
-
-            // now decrease minStart if needed to make the bin centers line up
-            var adjustedFirstStart = firstManual.start + (firstManual.size - minSize) / 2;
-            minStart = adjustedFirstStart - minSize * Math.ceil((adjustedFirstStart - minStart) / minSize);
+        binOpts.size = newBinSpec.size;
+        if(!sizeFound) {
+            autoVals.size = newBinSpec.size;
+            Lib.nestedProperty(traces[0], binAttr + '.size').set(newBinSpec.size);
         }
 
-        // now go back to the autobinned traces and update their bin specs with the final values
-        for(i = 0; i < autoBinnedTraces.length; i++) {
-            tracei = autoBinnedTraces[i];
-            calendar = tracei[mainData + 'calendar'];
+        setBound('start', binOpts, newBinSpec);
+        setBound('end', binOpts, newBinSpec);
+    }
 
-            tracei._input[binAttr] = tracei[binAttr] = {
-                start: pa.c2r(minStart, 0, calendar),
-                end: pa.c2r(maxEnd, 0, calendar),
-                size: minSize
-            };
+    pos0 = trace._pos0;
+    delete trace._pos0;
 
-            // note that it's possible to get here with an explicit autobin: false
-            // if the bins were not specified.
-            // in that case this will remain in the trace, so that future updates
-            // which would change the autobinning will not do so.
-            tracei._input[autoBinAttr] = tracei[autoBinAttr];
+    // Each trace can specify its own start/end, or if omitted
+    // we ensure they're beyond the bounds of this trace's data,
+    // and we need to make sure start is aligned with the main start
+    var traceInputBins = trace._input[binAttr] || {};
+    var traceBinOptsCalc = Lib.extendFlat({}, binOpts);
+    var mainStart = binOpts.start;
+    var startIn = pa.r2l(traceInputBins.start);
+    var hasStart = startIn !== undefined;
+    if((binOpts.startFound || hasStart) && startIn !== pa.r2l(mainStart)) {
+        // We have an explicit start to reconcile across traces
+        // if this trace has an explicit start, shift it down to a bin edge
+        // if another trace had an explicit start, shift it down to a
+        // bin edge past our data
+        var traceStart = hasStart ?
+            startIn :
+            Lib.aggNums(Math.min, null, pos0);
+
+        var dummyAx = {
+            type: pa.type === 'category' ? 'linear' : pa.type,
+            r2l: pa.r2l,
+            dtick: binOpts.size,
+            tick0: mainStart,
+            calendar: calendar,
+            range: ([traceStart, Axes.tickIncrement(traceStart, binOpts.size, false, calendar)]).map(pa.l2r)
+        };
+        var newStart = Axes.tickFirst(dummyAx);
+        if(newStart > pa.r2l(traceStart)) {
+            newStart = Axes.tickIncrement(newStart, binOpts.size, true, calendar);
         }
+        traceBinOptsCalc.start = pa.l2r(newStart);
+        if(!hasStart) Lib.nestedProperty(trace, binAttr + '.start').set(traceBinOptsCalc.start);
     }
 
-    pos0 = trace._pos0;
-    delete trace._pos0;
+    var mainEnd = binOpts.end;
+    var endIn = pa.r2l(traceInputBins.end);
+    var hasEnd = endIn !== undefined;
+    if((binOpts.endFound || hasEnd) && endIn !== pa.r2l(mainEnd)) {
+        // Reconciling an explicit end is easier, as it doesn't need to
+        // match bin edges
+        var traceEnd = hasEnd ?
+            endIn :
+            Lib.aggNums(Math.max, null, pos0);
+
+        traceBinOptsCalc.end = pa.l2r(traceEnd);
+        if(!hasEnd) Lib.nestedProperty(trace, binAttr + '.start').set(traceBinOptsCalc.end);
+    }
 
-    return [trace[binAttr], pos0];
+    // Backward compatibility for one-time autobinning.
+    // autobin: true is handled in cleanData, but autobin: false
+    // needs to be here where we have determined the values.
+    if(trace._input['autobin' + mainData] === false) {
+        trace._input[binAttr] = Lib.extendFlat({}, trace[binAttr] || {});
+        delete trace._input['autobin' + mainData];
+    }
+
+    return [traceBinOptsCalc, pos0];
 }
 
 /*
@@ -449,25 +473,6 @@ function getConnectedHistograms(gd, trace) {
 }
 
 
-/*
- * getMinSize: find the smallest given that size can be a string code
- * ie 'M6' for 6 months. ('L' wouldn't make sense to compare with numeric sizes)
- */
-function getMinSize(size1, size2) {
-    if(size1 === Infinity) return size2;
-    var sizeNumeric1 = numericSize(size1);
-    var sizeNumeric2 = numericSize(size2);
-    return sizeNumeric2 < sizeNumeric1 ? size2 : size1;
-}
-
-function numericSize(size) {
-    if(isNumeric(size)) return size;
-    if(typeof size === 'string' && size.charAt(0) === 'M') {
-        return oneMonth * +(size.substr(1));
-    }
-    return Infinity;
-}
-
 function cdf(size, direction, currentBin) {
     var i, vi, prevSum;
 
diff --git a/src/traces/histogram/clean_bins.js b/src/traces/histogram/clean_bins.js
deleted file mode 100644
index dc322d7401d..00000000000
--- a/src/traces/histogram/clean_bins.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
-* Copyright 2012-2018, Plotly, Inc.
-* All rights reserved.
-*
-* This source code is licensed under the MIT license found in the
-* LICENSE file in the root directory of this source tree.
-*/
-
-
-'use strict';
-var isNumeric = require('fast-isnumeric');
-var cleanDate = require('../../lib').cleanDate;
-var constants = require('../../constants/numerical');
-var ONEDAY = constants.ONEDAY;
-var BADNUM = constants.BADNUM;
-
-/*
- * cleanBins: validate attributes autobin[xy] and [xy]bins.(start, end, size)
- * Mutates trace so all these attributes are valid.
- *
- * Normally this kind of thing would happen during supplyDefaults, but
- * in this case we need to know the axis type, and axis type isn't set until
- * after trace supplyDefaults are completed. So this gets called during the
- * calc step, when data are inserted into bins.
- */
-module.exports = function cleanBins(trace, ax, binDirection) {
-    var axType = ax.type,
-        binAttr = binDirection + 'bins',
-        bins = trace[binAttr];
-
-    if(!bins) bins = trace[binAttr] = {};
-
-    var cleanBound = (axType === 'date') ?
-        function(v) { return (v || v === 0) ? cleanDate(v, BADNUM, bins.calendar) : null; } :
-        function(v) { return isNumeric(v) ? Number(v) : null; };
-
-    bins.start = cleanBound(bins.start);
-    bins.end = cleanBound(bins.end);
-
-    // logic for bin size is very similar to dtick (cartesian/tick_value_defaults)
-    // but without the extra string options for log axes
-    // ie the only strings we accept are M<n> for months
-    var sizeDflt = (axType === 'date') ? ONEDAY : 1,
-        binSize = bins.size;
-
-    if(isNumeric(binSize)) {
-        bins.size = (binSize > 0) ? Number(binSize) : sizeDflt;
-    }
-    else if(typeof binSize !== 'string') {
-        bins.size = sizeDflt;
-    }
-    else {
-        // date special case: "M<n>" gives bins every (integer) n months
-        var prefix = binSize.charAt(0),
-            sizeNum = binSize.substr(1);
-
-        sizeNum = isNumeric(sizeNum) ? Number(sizeNum) : 0;
-        if((sizeNum <= 0) || !(
-                axType === 'date' && prefix === 'M' && sizeNum === Math.round(sizeNum)
-            )) {
-            bins.size = sizeDflt;
-        }
-    }
-
-    var autoBinAttr = 'autobin' + binDirection;
-
-    if(typeof trace[autoBinAttr] !== 'boolean') {
-        trace[autoBinAttr] = trace._fullInput[autoBinAttr] = trace._input[autoBinAttr] = !(
-            (bins.start || bins.start === 0) &&
-            (bins.end || bins.end === 0)
-        );
-    }
-
-    if(!trace[autoBinAttr]) {
-        delete trace['nbins' + binDirection];
-        delete trace._fullInput['nbins' + binDirection];
-    }
-};
diff --git a/src/traces/histogram/clean_data.js b/src/traces/histogram/clean_data.js
new file mode 100644
index 00000000000..90dfb33b47b
--- /dev/null
+++ b/src/traces/histogram/clean_data.js
@@ -0,0 +1,112 @@
+/**
+* Copyright 2012-2018, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+var Lib = require('../../lib');
+var nestedProperty = Lib.nestedProperty;
+
+var attributes = require('./attributes');
+
+var BINATTRS = {
+    x: [
+        {aStr: 'xbins.start', name: 'start'},
+        {aStr: 'xbins.end', name: 'end'},
+        {aStr: 'xbins.size', name: 'size'},
+        {aStr: 'nbinsx', name: 'nbins'}
+    ],
+    y: [
+        {aStr: 'ybins.start', name: 'start'},
+        {aStr: 'ybins.end', name: 'end'},
+        {aStr: 'ybins.size', name: 'size'},
+        {aStr: 'nbinsy', name: 'nbins'}
+    ]
+};
+
+// handle bin attrs and relink auto-determined values so fullData is complete
+module.exports = function cleanData(fullData, fullLayout) {
+    var allBinOpts = fullLayout._histogramBinOpts = {};
+    var isOverlay = fullLayout.barmode === 'overlay';
+    var i, j, traceOut, traceIn, binDirection, group, binOpts;
+
+    function coerce(attr) {
+        return Lib.coerce(traceOut._input, traceOut, attributes, attr);
+    }
+
+    for(i = 0; i < fullData.length; i++) {
+        traceOut = fullData[i];
+        if(traceOut.type !== 'histogram') continue;
+
+        // TODO: this shouldn't be relinked as it's only used within calc
+        // https://github.com/plotly/plotly.js/issues/749
+        delete traceOut._autoBinFinished;
+
+        binDirection = traceOut.orientation === 'v' ? 'x' : 'y';
+        // in overlay mode make a separate group for each trace
+        // otherwise collect all traces of the same subplot & orientation
+        group = isOverlay ? traceOut.uid : (traceOut.xaxis + traceOut.yaxis + binDirection);
+        traceOut._groupName = group;
+
+        binOpts = allBinOpts[group];
+
+        if(binOpts) {
+            binOpts.traces.push(traceOut);
+        }
+        else {
+            binOpts = allBinOpts[group] = {
+                traces: [traceOut],
+                direction: binDirection
+            };
+        }
+    }
+
+    for(group in allBinOpts) {
+        binOpts = allBinOpts[group];
+        binDirection = binOpts.direction;
+        var attrs = BINATTRS[binDirection];
+        for(j = 0; j < attrs.length; j++) {
+            var attrSpec = attrs[j];
+            var attr = attrSpec.name;
+
+            // nbins(x|y) is moot if we have a size. This depends on
+            // nbins coming after size in binAttrs.
+            if(attr === 'nbins' && binOpts.sizeFound) continue;
+
+            var aStr = attrSpec.aStr;
+            for(i = 0; i < binOpts.traces.length; i++) {
+                traceOut = binOpts.traces[i];
+                traceIn = traceOut._input;
+                if(nestedProperty(traceIn, aStr).get() !== undefined) {
+                    binOpts[attr] = coerce(aStr);
+                    binOpts[attr + 'Found'] = true;
+                    break;
+                }
+                var autoVals = traceOut._autoBin;
+                if(autoVals && autoVals[attr]) {
+                    // if this is the *first* autoval
+                    nestedProperty(traceOut, aStr).set(autoVals[attr]);
+                }
+            }
+            // start and end we need to coerce anyway, after having collected the
+            // first of each into binOpts, in case a trace wants to restrict its
+            // data to a certain range
+            if(attr === 'start' || attr === 'end') {
+                for(; i < binOpts.traces.length; i++) {
+                    traceOut = binOpts.traces[i];
+                    coerce(aStr, (traceOut._autoBin || {})[attr]);
+                }
+            }
+
+            if(attr === 'nbins' && !binOpts.sizeFound && !binOpts.nbinsFound) {
+                traceOut = binOpts.traces[0];
+                binOpts[attr] = coerce(aStr);
+            }
+        }
+    }
+};
diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js
index b56b451311a..296f8834bb6 100644
--- a/src/traces/histogram/defaults.js
+++ b/src/traces/histogram/defaults.js
@@ -13,7 +13,6 @@ var Registry = require('../../registry');
 var Lib = require('../../lib');
 var Color = require('../../components/color');
 
-var handleBinDefaults = require('./bin_defaults');
 var handleStyleDefaults = require('../bar/style_defaults');
 var attributes = require('./attributes');
 
@@ -51,8 +50,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
 
     var hasAggregationData = traceOut[aggLetter];
     if(hasAggregationData) coerce('histfunc');
+    coerce('histnorm');
 
-    handleBinDefaults(traceIn, traceOut, coerce, [sampleLetter]);
+    // Note: bin defaults are now handled in Histogram.cleanData
 
     handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout);
 
diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js
index 25fc27a227e..e1469215b67 100644
--- a/src/traces/histogram/index.js
+++ b/src/traces/histogram/index.js
@@ -28,6 +28,7 @@ var Histogram = {};
 Histogram.attributes = require('./attributes');
 Histogram.layoutAttributes = require('../bar/layout_attributes');
 Histogram.supplyDefaults = require('./defaults');
+Histogram.cleanData = require('./clean_data');
 Histogram.supplyLayoutDefaults = require('../bar/layout_defaults');
 Histogram.calc = require('./calc');
 Histogram.crossTraceCalc = require('../bar/cross_trace_calc').crossTraceCalc;
diff --git a/src/traces/histogram2d/attributes.js b/src/traces/histogram2d/attributes.js
index 7d166f3daf1..c8a208ce5a6 100644
--- a/src/traces/histogram2d/attributes.js
+++ b/src/traces/histogram2d/attributes.js
@@ -36,17 +36,19 @@ module.exports = extendFlat(
 
         histnorm: histogramAttrs.histnorm,
         histfunc: histogramAttrs.histfunc,
-        autobinx: histogramAttrs.autobinx,
         nbinsx: histogramAttrs.nbinsx,
         xbins: histogramAttrs.xbins,
-        autobiny: histogramAttrs.autobiny,
         nbinsy: histogramAttrs.nbinsy,
         ybins: histogramAttrs.ybins,
 
         xgap: heatmapAttrs.xgap,
         ygap: heatmapAttrs.ygap,
         zsmooth: heatmapAttrs.zsmooth,
-        zhoverformat: heatmapAttrs.zhoverformat
+        zhoverformat: heatmapAttrs.zhoverformat,
+        _deprecated: {
+            autobinx: histogramAttrs._deprecated.autobinx,
+            autobiny: histogramAttrs._deprecated.autobiny
+        }
     },
     colorscaleAttrs('', {
         cLetter: 'z',
diff --git a/src/traces/histogram2d/calc.js b/src/traces/histogram2d/calc.js
index bc3c91294fe..c345c904e5f 100644
--- a/src/traces/histogram2d/calc.js
+++ b/src/traces/histogram2d/calc.js
@@ -15,7 +15,6 @@ var Axes = require('../../plots/cartesian/axes');
 var binFunctions = require('../histogram/bin_functions');
 var normFunctions = require('../histogram/norm_functions');
 var doAvg = require('../histogram/average');
-var cleanBins = require('../histogram/clean_bins');
 var getBinSpanLabelRound = require('../histogram/bin_label_vals');
 
 
@@ -38,8 +37,8 @@ module.exports = function calc(gd, trace) {
     if(y.length > serieslen) y.splice(serieslen, y.length - serieslen);
 
     // calculate the bins
-    cleanAndAutobin(trace, 'x', x, xa, xr2c, xc2r, xcalendar);
-    cleanAndAutobin(trace, 'y', y, ya, yr2c, yc2r, ycalendar);
+    doAutoBin(trace, 'x', x, xa, xr2c, xc2r, xcalendar);
+    doAutoBin(trace, 'y', y, ya, yr2c, yc2r, ycalendar);
 
     // make the empty bin array & scale the map
     var z = [];
@@ -182,31 +181,47 @@ module.exports = function calc(gd, trace) {
     };
 };
 
-function cleanAndAutobin(trace, axLetter, data, ax, r2c, c2r, calendar) {
-    var binSpecAttr = axLetter + 'bins';
-    var autoBinAttr = 'autobin' + axLetter;
-    var binSpec = trace[binSpecAttr];
+function doAutoBin(trace, axLetter, data, ax, r2c, c2r, calendar) {
+    var binAttr = axLetter + 'bins';
+    var binSpec = trace[binAttr];
+    if(!binSpec) binSpec = trace[binAttr] = {};
+    var inputBinSpec = trace._input[binAttr] || {};
+    var autoBin = trace._autoBin = {};
 
-    cleanBins(trace, ax, axLetter);
+    // clear out any previously added autobin info
+    if(!inputBinSpec.size) delete binSpec.size;
+    if(inputBinSpec.start === undefined) delete binSpec.start;
+    if(inputBinSpec.end === undefined) delete binSpec.end;
 
-    if(trace[autoBinAttr] || !binSpec || binSpec.start === null || binSpec.end === null) {
-        binSpec = Axes.autoBin(data, ax, trace['nbins' + axLetter], '2d', calendar);
+    var autoSize = !binSpec.size;
+    var autoStart = binSpec.start === undefined;
+    var autoEnd = binSpec.end === undefined;
+
+    if(autoSize || autoStart || autoEnd) {
+        var newBinSpec = Axes.autoBin(data, ax, trace['nbins' + axLetter], '2d', calendar, binSpec.size);
         if(trace.type === 'histogram2dcontour') {
-            // the "true" last argument reverses the tick direction (which we can't
+            // the "true" 2nd argument reverses the tick direction (which we can't
             // just do with a minus sign because of month bins)
-            binSpec.start = c2r(Axes.tickIncrement(
-                r2c(binSpec.start), binSpec.size, true, calendar));
-            binSpec.end = c2r(Axes.tickIncrement(
-                r2c(binSpec.end), binSpec.size, false, calendar));
+            if(autoStart) {
+                newBinSpec.start = c2r(Axes.tickIncrement(
+                    r2c(newBinSpec.start), newBinSpec.size, true, calendar));
+            }
+            if(autoEnd) {
+                newBinSpec.end = c2r(Axes.tickIncrement(
+                    r2c(newBinSpec.end), newBinSpec.size, false, calendar));
+            }
         }
+        if(autoSize) binSpec.size = autoBin.size = newBinSpec.size;
+        if(autoStart) binSpec.start = autoBin.start = newBinSpec.start;
+        if(autoEnd) binSpec.end = autoBin.end = newBinSpec.end;
+    }
 
-        // copy bin info back to the source data.
-        trace._input[binSpecAttr] = trace[binSpecAttr] = binSpec;
-        // note that it's possible to get here with an explicit autobin: false
-        // if the bins were not specified.
-        // in that case this will remain in the trace, so that future updates
-        // which would change the autobinning will not do so.
-        trace._input[autoBinAttr] = trace[autoBinAttr];
+    // Backward compatibility for one-time autobinning.
+    // autobin: true is handled in cleanData, but autobin: false
+    // needs to be here where we have determined the values.
+    if(trace._input['autobin' + axLetter] === false) {
+        trace._input[binAttr] = Lib.extendFlat({}, binSpec);
+        delete trace._input['autobin' + axLetter];
     }
 }
 
diff --git a/src/traces/histogram2d/clean_data.js b/src/traces/histogram2d/clean_data.js
new file mode 100644
index 00000000000..cff8ddf781c
--- /dev/null
+++ b/src/traces/histogram2d/clean_data.js
@@ -0,0 +1,93 @@
+/**
+* Copyright 2012-2018, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+
+'use strict';
+
+
+var isNumeric = require('fast-isnumeric');
+
+var BADNUM = require('../../constants/numerical').BADNUM;
+var axisIds = require('../../plots/cartesian/axis_ids');
+var Lib = require('../../lib');
+
+var attributes = require('./attributes');
+
+var BINDIRECTIONS = ['x', 'y'];
+
+// Handle bin attrs and relink auto-determined values so fullData is complete
+// does not have cross-trace coupling, but moved out here so we have axis types
+// and relinked trace._autoBin
+module.exports = function cleanData(fullData, fullLayout) {
+    var i, j, traceOut, binDirection;
+
+    function coerce(attr) {
+        return Lib.coerce(traceOut._input, traceOut, attributes, attr);
+    }
+
+    for(i = 0; i < fullData.length; i++) {
+        traceOut = fullData[i];
+        var type = traceOut.type;
+        if(type !== 'histogram2d' && type !== 'histogram2dcontour') continue;
+
+        for(j = 0; j < BINDIRECTIONS.length; j++) {
+            binDirection = BINDIRECTIONS[j];
+            var binAttr = binDirection + 'bins';
+            var autoBins = (traceOut._autoBin || {})[binDirection] || {};
+            coerce(binAttr + '.start', autoBins.start);
+            coerce(binAttr + '.end', autoBins.end);
+            coerce(binAttr + '.size', autoBins.size);
+
+            cleanBins(traceOut, binDirection, fullLayout, autoBins);
+
+            if(!(traceOut[binAttr] || {}).size) coerce('nbins' + binDirection);
+        }
+    }
+};
+
+function cleanBins(trace, binDirection, fullLayout, autoBins) {
+    var ax = fullLayout[axisIds.id2name(trace[binDirection + 'axis'])];
+    var axType = ax.type;
+    var binAttr = binDirection + 'bins';
+    var bins = trace[binAttr];
+    var calendar = trace[binDirection + 'calendar'];
+
+    if(!bins) bins = trace[binAttr] = {};
+
+    var cleanBound = (axType === 'date') ?
+        function(v, dflt) { return (v || v === 0) ? Lib.cleanDate(v, BADNUM, calendar) : dflt; } :
+        function(v, dflt) { return isNumeric(v) ? Number(v) : dflt; };
+
+    bins.start = cleanBound(bins.start, autoBins.start);
+    bins.end = cleanBound(bins.end, autoBins.end);
+
+    // logic for bin size is very similar to dtick (cartesian/tick_value_defaults)
+    // but without the extra string options for log axes
+    // ie the only strings we accept are M<n> for months
+    var sizeDflt = autoBins.size;
+    var binSize = bins.size;
+
+    if(isNumeric(binSize)) {
+        bins.size = (binSize > 0) ? Number(binSize) : sizeDflt;
+    }
+    else if(typeof binSize !== 'string') {
+        bins.size = sizeDflt;
+    }
+    else {
+        // date special case: "M<n>" gives bins every (integer) n months
+        var prefix = binSize.charAt(0);
+        var sizeNum = binSize.substr(1);
+
+        sizeNum = isNumeric(sizeNum) ? Number(sizeNum) : 0;
+        if((sizeNum <= 0) || !(
+                axType === 'date' && prefix === 'M' && sizeNum === Math.round(sizeNum)
+            )) {
+            bins.size = sizeDflt;
+        }
+    }
+}
diff --git a/src/traces/histogram2d/index.js b/src/traces/histogram2d/index.js
index af2549a9c18..6f3e49860f7 100644
--- a/src/traces/histogram2d/index.js
+++ b/src/traces/histogram2d/index.js
@@ -13,6 +13,7 @@ var Histogram2D = {};
 
 Histogram2D.attributes = require('./attributes');
 Histogram2D.supplyDefaults = require('./defaults');
+Histogram2D.cleanData = require('./clean_data');
 Histogram2D.calc = require('../heatmap/calc');
 Histogram2D.plot = require('../heatmap/plot');
 Histogram2D.layerName = 'heatmaplayer';
diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js
index 80575051d26..b1262ec82e9 100644
--- a/src/traces/histogram2d/sample_defaults.js
+++ b/src/traces/histogram2d/sample_defaults.js
@@ -10,8 +10,6 @@
 'use strict';
 
 var Registry = require('../../registry');
-var handleBinDefaults = require('../histogram/bin_defaults');
-
 
 module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout) {
     var x = coerce('x');
@@ -34,7 +32,7 @@ module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout
     var hasAggregationData = coerce('z') || coerce('marker.color');
 
     if(hasAggregationData) coerce('histfunc');
+    coerce('histnorm');
 
-    var binDirections = ['x', 'y'];
-    handleBinDefaults(traceIn, traceOut, coerce, binDirections);
+    // Note: bin defaults are now handled in Histogram2D.cleanData
 };
diff --git a/src/traces/histogram2dcontour/attributes.js b/src/traces/histogram2dcontour/attributes.js
index 048a0c481d4..e8c3c402d7b 100644
--- a/src/traces/histogram2dcontour/attributes.js
+++ b/src/traces/histogram2dcontour/attributes.js
@@ -23,10 +23,8 @@ module.exports = extendFlat({
 
     histnorm: histogram2dAttrs.histnorm,
     histfunc: histogram2dAttrs.histfunc,
-    autobinx: histogram2dAttrs.autobinx,
     nbinsx: histogram2dAttrs.nbinsx,
     xbins: histogram2dAttrs.xbins,
-    autobiny: histogram2dAttrs.autobiny,
     nbinsy: histogram2dAttrs.nbinsy,
     ybins: histogram2dAttrs.ybins,
 
@@ -34,7 +32,11 @@ module.exports = extendFlat({
     ncontours: contourAttrs.ncontours,
     contours: contourAttrs.contours,
     line: contourAttrs.line,
-    zhoverformat: histogram2dAttrs.zhoverformat
+    zhoverformat: histogram2dAttrs.zhoverformat,
+    _deprecated: {
+        autobinx: histogram2dAttrs._deprecated.autobinx,
+        autobiny: histogram2dAttrs._deprecated.autobiny
+    }
 },
     colorscaleAttrs('', {
         cLetter: 'z',
diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js
index c16206dff5e..99a3679af0e 100644
--- a/src/traces/histogram2dcontour/index.js
+++ b/src/traces/histogram2dcontour/index.js
@@ -13,6 +13,7 @@ var Histogram2dContour = {};
 
 Histogram2dContour.attributes = require('./attributes');
 Histogram2dContour.supplyDefaults = require('./defaults');
+Histogram2dContour.cleanData = require('../histogram2d/clean_data');
 Histogram2dContour.calc = require('../contour/calc');
 Histogram2dContour.plot = require('../contour/plot').plot;
 Histogram2dContour.layerName = 'contourlayer';
diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js
index bfc15effcc0..e3a2680b343 100644
--- a/test/jasmine/tests/axes_test.js
+++ b/test/jasmine/tests/axes_test.js
@@ -1220,7 +1220,7 @@ describe('Test axes', function() {
                 axOut = {};
                 mockSupplyDefaults(axIn, axOut, 'log');
                 // tick0 gets ignored for D<n>
-                expect(axOut.tick0).toBe(0);
+                expect(axOut.tick0).toBeUndefined(v);
                 expect(axOut.dtick).toBe(v);
             });
 
diff --git a/test/jasmine/tests/histogram2d_test.js b/test/jasmine/tests/histogram2d_test.js
index 3950324aeb5..7893e18c767 100644
--- a/test/jasmine/tests/histogram2d_test.js
+++ b/test/jasmine/tests/histogram2d_test.js
@@ -182,6 +182,16 @@ describe('Test histogram2d', function() {
             .then(done);
         });
 
+        function _assert(xBinsFull, yBinsFull, xBins, yBins) {
+            expect(gd._fullData[0].xbins).toEqual(xBinsFull);
+            expect(gd._fullData[0].ybins).toEqual(yBinsFull);
+            expect(gd._fullData[0].autobinx).toBeUndefined();
+            expect(gd._fullData[0].autobiny).toBeUndefined();
+            expect(gd.data[0].xbins).toEqual(xBins);
+            expect(gd.data[0].ybins).toEqual(yBins);
+            expect(gd.data[0].autobinx).toBeUndefined();
+            expect(gd.data[0].autobiny).toBeUndefined();
+        }
 
         it('handles autobin correctly on restyles', function() {
             var x1 = [
@@ -191,65 +201,64 @@ describe('Test histogram2d', function() {
                 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4,
                 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4];
             Plotly.newPlot(gd, [{type: 'histogram2d', x: x1, y: y1}]);
-            expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1, _dataSpan: 3});
-            expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _dataSpan: 3});
-            expect(gd._fullData[0].autobinx).toBe(true);
-            expect(gd._fullData[0].autobiny).toBe(true);
+            _assert(
+                {start: 0.5, end: 4.5, size: 1},
+                {start: 0.5, end: 4.5, size: 1},
+                undefined, undefined);
 
             // same range but fewer samples increases sizes
             Plotly.restyle(gd, {x: [[1, 3, 4]], y: [[1, 2, 4]]});
-            expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 5.5, size: 2, _dataSpan: 3});
-            expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 5.5, size: 2, _dataSpan: 3});
-            expect(gd._fullData[0].autobinx).toBe(true);
-            expect(gd._fullData[0].autobiny).toBe(true);
+            _assert(
+                {start: -0.5, end: 5.5, size: 2},
+                {start: -0.5, end: 5.5, size: 2},
+                undefined, undefined);
 
             // larger range
             Plotly.restyle(gd, {x: [[10, 30, 40]], y: [[10, 20, 40]]});
-            expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20, _dataSpan: 30});
-            expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20, _dataSpan: 30});
-            expect(gd._fullData[0].autobinx).toBe(true);
-            expect(gd._fullData[0].autobiny).toBe(true);
+            _assert(
+                {start: -0.5, end: 59.5, size: 20},
+                {start: -0.5, end: 59.5, size: 20},
+                undefined, undefined);
 
             // explicit changes to bin settings
             Plotly.restyle(gd, 'xbins.start', 12);
-            expect(gd._fullData[0].xbins).toEqual({start: 12, end: 59.5, size: 20, _dataSpan: 30});
-            expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20, _dataSpan: 30});
-            expect(gd._fullData[0].autobinx).toBe(false);
-            expect(gd._fullData[0].autobiny).toBe(true);
+            _assert(
+                {start: 12, end: 59.5, size: 20},
+                {start: -0.5, end: 59.5, size: 20},
+                {start: 12}, undefined);
 
             Plotly.restyle(gd, {'ybins.end': 12, 'ybins.size': 3});
-            expect(gd._fullData[0].xbins).toEqual({start: 12, end: 59.5, size: 20, _dataSpan: 30});
-            expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 12, size: 3, _dataSpan: 30});
-            expect(gd._fullData[0].autobinx).toBe(false);
-            expect(gd._fullData[0].autobiny).toBe(false);
+            _assert(
+                {start: 12, end: 59.5, size: 20},
+                // with the new autobin algo, start responds to autobin
+                {start: 8.5, end: 12, size: 3},
+                {start: 12},
+                {end: 12, size: 3});
 
             // restart autobin
             Plotly.restyle(gd, {autobinx: true, autobiny: true});
-            expect(gd._fullData[0].xbins).toEqual({start: -0.5, end: 59.5, size: 20, _dataSpan: 30});
-            expect(gd._fullData[0].ybins).toEqual({start: -0.5, end: 59.5, size: 20, _dataSpan: 30});
-            expect(gd._fullData[0].autobinx).toBe(true);
-            expect(gd._fullData[0].autobiny).toBe(true);
+            _assert(
+                {start: -0.5, end: 59.5, size: 20},
+                {start: -0.5, end: 59.5, size: 20},
+                undefined, undefined);
         });
 
         it('respects explicit autobin: false as a one-time autobin', function() {
+            // patched in for backward compat, but there aren't really
+            // autobinx/autobiny attributes anymore
             var x1 = [
                 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4,
                 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4];
             var y1 = [
                 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4,
                 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4];
+            var binSpec = {start: 0.5, end: 4.5, size: 1};
             Plotly.newPlot(gd, [{type: 'histogram2d', x: x1, y: y1, autobinx: false, autobiny: false}]);
-            expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1, _dataSpan: 3});
-            expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _dataSpan: 3});
-            expect(gd._fullData[0].autobinx).toBe(false);
-            expect(gd._fullData[0].autobiny).toBe(false);
+            _assert(binSpec, binSpec, binSpec, binSpec);
 
             // with autobin false this will no longer update the bins.
             Plotly.restyle(gd, {x: [[1, 3, 4]], y: [[1, 2, 4]]});
-            expect(gd._fullData[0].xbins).toEqual({start: 0.5, end: 4.5, size: 1, _dataSpan: 3});
-            expect(gd._fullData[0].ybins).toEqual({start: 0.5, end: 4.5, size: 1, _dataSpan: 3});
-            expect(gd._fullData[0].autobinx).toBe(false);
-            expect(gd._fullData[0].autobiny).toBe(false);
+            _assert(binSpec, binSpec, binSpec, binSpec);
         });
     });
 
diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js
index e39096a7c94..141204b08cc 100644
--- a/test/jasmine/tests/histogram_test.js
+++ b/test/jasmine/tests/histogram_test.js
@@ -187,10 +187,10 @@ describe('Test histogram', function() {
 
 
     describe('calc', function() {
-        function _calc(opts, extraTraces, layout) {
+        function _calc(opts, extraTraces, layout, prependExtras) {
             var base = { type: 'histogram' };
             var trace = Lib.extendFlat({}, base, opts);
-            var gd = { data: [trace] };
+            var gd = { data: prependExtras ? [] : [trace] };
 
             if(layout) gd.layout = layout;
 
@@ -200,8 +200,16 @@ describe('Test histogram', function() {
                 });
             }
 
+            if(prependExtras) gd.data.push(trace);
+
             supplyAllDefaults(gd);
-            var fullTrace = gd._fullData[0];
+            var fullTrace = gd._fullData[prependExtras ? gd._fullData.length - 1 : 0];
+
+            if(prependExtras) {
+                for(var i = 0; i < gd._fullData.length - 1; i++) {
+                    calc(gd, gd._fullData[i]);
+                }
+            }
 
             var out = calc(gd, fullTrace);
             delete out[0].trace;
@@ -408,8 +416,8 @@ describe('Test histogram', function() {
             ]);
         });
 
-        function calcPositions(opts, extraTraces) {
-            return _calc(opts, extraTraces).map(function(v) { return v.p; });
+        function calcPositions(opts, extraTraces, prepend) {
+            return _calc(opts, extraTraces, {}, prepend).map(function(v) { return v.p; });
         }
 
         it('harmonizes autobins when all traces are autobinned', function() {
@@ -420,25 +428,11 @@ describe('Test histogram', function() {
 
             expect(calcPositions(trace2)).toBeCloseToArray([5.5, 6.5], 5);
 
-            expect(calcPositions(trace1, [trace2])).toEqual([1, 2, 3, 4]);
-            // huh, turns out even this one is an example of "unexpected bin positions"
-            // (see another example below) - in this case it's because trace1 gets
-            // autoshifted to keep integers off the bin edges, whereas trace2 doesn't
-            // because there are as many integers as half-integers.
-            // In this case though, it's unexpected but arguably better than the
-            // "expected" result.
-            expect(calcPositions(trace2, [trace1])).toEqual([5, 6, 7]);
-        });
-
-        it('can sometimes give unexpected bin positions', function() {
-            // documenting an edge case that might not be desirable but for now
-            // we've decided to ignore: a larger bin sets the bin start, but then it
-            // doesn't quite make sense with the smaller bin we end up with
-            // we *could* fix this by ensuring that the bin start is based on the
-            // same bin spec that gave the minimum bin size, but incremented down to
-            // include the minimum start... but that would have awkward edge cases
-            // involving month bins so for now we're ignoring it.
+            expect(calcPositions(trace1, [trace2])).toEqual([1, 3, 5]);
+            expect(calcPositions(trace2, [trace1])).toEqual([5, 7]);
+        });
 
+        it('autobins all data as one', function() {
             // all integers, so all autobins should get shifted to start 0.5 lower
             // than they otherwise would.
             var trace1 = {x: [1, 2, 3, 4]};
@@ -450,19 +444,21 @@ describe('Test histogram', function() {
             // {size: 5, start: -5.5}: -5..-1, 0..4, 5..9
             expect(calcPositions(trace2)).toEqual([-3, 2, 7]);
 
-            // unexpected behavior when we put these together,
-            // because 2 and 5 are mutually prime. Normally you could never get
-            // groupings 1&2, 3&4... you'd always get 0&1, 2&3...
-            expect(calcPositions(trace1, [trace2])).toBeCloseToArray([1.5, 3.5], 5);
-            expect(calcPositions(trace2, [trace1])).toBeCloseToArray([
-                -2.5, -0.5, 1.5, 3.5, 5.5, 7.5
-            ], 5);
+            // together bins match the wider trace
+            expect(calcPositions(trace1, [trace2])).toBeCloseToArray([2], 5);
+            expect(calcPositions(trace2, [trace1])).toEqual([-3, 2, 7]);
+
+            // unless we add enough points to shrink the bins
+            expect(calcPositions(trace2, [trace1, trace1, trace1, trace1]))
+                .toBeCloseToArray([-1.5, 0.5, 2.5, 4.5, 6.5], 5);
         });
 
         it('harmonizes autobins with smaller manual bins', function() {
             var trace1 = {x: [1, 2, 3, 4]};
             var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 7.1, size: 0.4}};
 
+            // size is preserved, and start is shifted to be compatible with trace2
+            // (but we can't just use start from trace2 or it would cut off all our data!)
             expect(calcPositions(trace1, [trace2])).toBeCloseToArray([
                 0.9, 1.3, 1.7, 2.1, 2.5, 2.9, 3.3, 3.7, 4.1
             ], 5);
@@ -470,20 +466,73 @@ describe('Test histogram', function() {
 
         it('harmonizes autobins with larger manual bins', function() {
             var trace1 = {x: [1, 2, 3, 4]};
-            var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 15, size: 7}};
+            var trace2 = {x: [5, 6, 7, 8], xbins: {start: 3.7, end: 15, size: 7}};
 
             expect(calcPositions(trace1, [trace2])).toBeCloseToArray([
-                0.8, 2.55, 4.3
+                0.2, 7.2
             ], 5);
         });
 
+        it('ignores incompatible sizes, and harmonizes start values', function() {
+            var trace1 = {x: [1, 2, 3, 4], xbins: {start: 1.7, end: 3.5, size: 0.6}};
+            var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.3, end: 7.1, size: 0.4}};
+
+            // trace1 is first: all its settings are used directly,
+            // and trace2 uses its size and shifts start to harmonize with it.
+            expect(calcPositions(trace1, [trace2])).toBeCloseToArray([
+                2.0, 2.6, 3.2
+            ], 5);
+            expect(calcPositions(trace2, [trace1], true)).toBeCloseToArray([
+                5.0, 5.6, 6.2, 6.8
+            ], 5);
+
+            // switch the order: trace2 values win
+            expect(calcPositions(trace2, [trace1])).toBeCloseToArray([
+                4.9, 5.3, 5.7, 6.1, 6.5, 6.9
+            ], 5);
+            expect(calcPositions(trace1, [trace2], true)).toBeCloseToArray([
+                2.1, 2.5, 2.9
+            ], 5);
+        });
+
+        it('can take size and start from different traces in any order', function() {
+            var trace1 = {x: [1, 2, 3, 4], xbins: {size: 0.6}};
+            var trace2 = {x: [5, 6, 7, 8], xbins: {start: 4.8}};
+
+            [true, false].forEach(function(prepend) {
+                expect(calcPositions(trace1, [trace2], prepend)).toBeCloseToArray([
+                    0.9, 1.5, 2.1, 2.7, 3.3, 3.9
+                ], 5);
+
+                expect(calcPositions(trace2, [trace1], prepend)).toBeCloseToArray([
+                    5.1, 5.7, 6.3, 6.9, 7.5, 8.1
+                ], 5);
+            });
+        });
+
+        it('works with only a size specified', function() {
+            // this used to not just lose the size, but actually errored out.
+            var trace1 = {x: [1, 2, 3, 4], xbins: {size: 0.8}};
+            var trace2 = {x: [5, 6, 7, 8]};
+
+            [true, false].forEach(function(prepend) {
+                expect(calcPositions(trace1, [trace2], prepend)).toBeCloseToArray([
+                    1, 1.8, 2.6, 3.4, 4.2
+                ], 5);
+
+                expect(calcPositions(trace2, [trace1], prepend)).toBeCloseToArray([
+                    5, 5.8, 6.6, 7.4, 8.2
+                ], 5);
+            });
+        });
+
         it('ignores traces on other axes', function() {
             var trace1 = {x: [1, 2, 3, 4]};
             var trace2 = {x: [5, 5.5, 6, 6.5]};
             var trace3 = {x: [1, 1.1, 1.2, 1.3], xaxis: 'x2'};
             var trace4 = {x: [1, 1.2, 1.4, 1.6], yaxis: 'y2'};
 
-            expect(calcPositions(trace1, [trace2, trace3, trace4])).toEqual([1, 2, 3, 4]);
+            expect(calcPositions(trace1, [trace2, trace3, trace4])).toEqual([1, 3, 5]);
             expect(calcPositions(trace3)).toBeCloseToArray([1.1, 1.3], 5);
         });
 
@@ -610,43 +659,59 @@ describe('Test histogram', function() {
             var data1 = [1.5, 2, 2, 3, 3, 3, 4, 4, 5];
             Plotly.plot(gd, [{x: data1, type: 'histogram' }]);
             expect(gd._fullData[0].xbins).toEqual({start: 1, end: 6, size: 1});
-            expect(gd._fullData[0].autobinx).toBe(true);
+            expect(gd._fullData[0].nbinsx).toBe(0);
 
             // same range but fewer samples changes autobin size
             var data2 = [1.5, 5];
             Plotly.restyle(gd, 'x', [data2]);
             expect(gd._fullData[0].xbins).toEqual({start: -2.5, end: 7.5, size: 5});
-            expect(gd._fullData[0].autobinx).toBe(true);
+            expect(gd._fullData[0].nbinsx).toBe(0);
 
             // different range
             var data3 = [10, 20.2, 20, 30, 30, 30, 40, 40, 50];
             Plotly.restyle(gd, 'x', [data3]);
             expect(gd._fullData[0].xbins).toEqual({start: 5, end: 55, size: 10});
-            expect(gd._fullData[0].autobinx).toBe(true);
+            expect(gd._fullData[0].nbinsx).toBe(0);
 
-            // explicit change to a bin attribute clears autobin
+            // explicit change to start does not update anything else
             Plotly.restyle(gd, 'xbins.start', 3);
             expect(gd._fullData[0].xbins).toEqual({start: 3, end: 55, size: 10});
-            expect(gd._fullData[0].autobinx).toBe(false);
+            expect(gd._fullData[0].nbinsx).toBe(0);
 
             // restart autobin
             Plotly.restyle(gd, 'autobinx', true);
             expect(gd._fullData[0].xbins).toEqual({start: 5, end: 55, size: 10});
-            expect(gd._fullData[0].autobinx).toBe(true);
+            expect(gd._fullData[0].nbinsx).toBe(0);
+
+            // explicit end does not update anything else
+            Plotly.restyle(gd, 'xbins.end', 43);
+            expect(gd._fullData[0].xbins).toEqual({start: 5, end: 43, size: 10});
+            expect(gd._fullData[0].nbinsx).toBe(0);
+
+            // nbins would update all three, but explicit end is honored
+            Plotly.restyle(gd, 'nbinsx', 3);
+            expect(gd._fullData[0].xbins).toEqual({start: 0, end: 43, size: 20});
+            expect(gd._fullData[0].nbinsx).toBe(3);
+
+            // explicit size updates auto start *and* end, and moots nbins
+            Plotly.restyle(gd, {'xbins.end': null, 'xbins.size': 2});
+            expect(gd._fullData[0].xbins).toEqual({start: 9, end: 51, size: 2});
+            expect(gd._fullData[0].nbinsx).toBeUndefined();
         });
 
         it('respects explicit autobin: false as a one-time autobin', function() {
             var data1 = [1.5, 2, 2, 3, 3, 3, 4, 4, 5];
             Plotly.plot(gd, [{x: data1, type: 'histogram', autobinx: false }]);
             // we have no bins, so even though autobin is false we have to autobin once
+            // but for backward compat. calc pushes these bins back into gd.data
+            // even though there's no `autobinx` attribute anymore.
             expect(gd._fullData[0].xbins).toEqual({start: 1, end: 6, size: 1});
-            expect(gd._fullData[0].autobinx).toBe(false);
+            expect(gd.data[0].xbins).toEqual({start: 1, end: 6, size: 1});
 
             // since autobin is false, this will not change the bins
             var data2 = [1.5, 5];
             Plotly.restyle(gd, 'x', [data2]);
             expect(gd._fullData[0].xbins).toEqual({start: 1, end: 6, size: 1});
-            expect(gd._fullData[0].autobinx).toBe(false);
         });
 
         it('allows changing axis type with new x data', function() {
@@ -742,6 +807,53 @@ describe('Test histogram', function() {
             .catch(failTest)
             .then(done);
         });
+
+        it('autobins all histograms together except `visible: false`', function(done) {
+            function _assertBinCenters(expectedCenters) {
+                var centers = gd.calcdata.map(function(cd) {
+                    return cd.map(function(cdi) { return cdi.p; });
+                });
+
+                expect(centers).toBeCloseTo2DArray(expectedCenters);
+            }
+
+            var hidden = [undefined];
+
+            Plotly.newPlot(gd, [
+                {type: 'histogram', x: [1]},
+                {type: 'histogram', x: [10, 10.1, 10.2, 10.3]},
+                {type: 'histogram', x: [20, 20, 20, 20, 20, 20, 20, 20, 20, 21]}
+            ])
+            .then(function() {
+                _assertBinCenters([[0], [10], [20]]);
+                return Plotly.restyle(gd, 'visible', 'legendonly', [1, 2]);
+            })
+            .then(function() {
+                _assertBinCenters([[0], hidden, hidden]);
+                return Plotly.restyle(gd, 'visible', false, [1, 2]);
+            })
+            .then(function() {
+                _assertBinCenters([[1], hidden, hidden]);
+                return Plotly.restyle(gd, 'visible', [false, false, true]);
+            })
+            .then(function() {
+                _assertBinCenters([hidden, hidden, [20, 21]]);
+                return Plotly.restyle(gd, 'visible', [false, true, false]);
+            })
+            .then(function() {
+                _assertBinCenters([hidden, [10.1, 10.3], hidden]);
+                // only one trace is visible, despite traces being grouped
+                expect(gd._fullLayout.bargap).toBe(0);
+                return Plotly.restyle(gd, 'visible', ['legendonly', true, 'legendonly']);
+            })
+            .then(function() {
+                _assertBinCenters([hidden, [10], hidden]);
+                // legendonly traces still flip us back to gapped
+                expect(gd._fullLayout.bargap).toBe(0.2);
+            })
+            .catch(failTest)
+            .then(done);
+        });
     });
 });
 
diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js
index 4e38fc1e982..42ad39c79f6 100644
--- a/test/jasmine/tests/plot_api_test.js
+++ b/test/jasmine/tests/plot_api_test.js
@@ -1119,17 +1119,30 @@ describe('Test plot api', function() {
         });
 
         it('turns off autobin when you edit bin specs', function(done) {
+            // test retained (modified) for backward compat with new autobin logic
             var start0 = 0.2;
             var end1 = 6;
             var size1 = 0.5;
 
             function check(auto, msg) {
-                expect(gd.data[0].autobinx).toBe(auto, msg);
-                expect(gd.data[0].xbins.start).negateIf(auto).toBe(start0, msg);
-                expect(gd.data[1].autobinx).toBe(auto, msg);
-                expect(gd.data[1].autobiny).toBe(auto, msg);
-                expect(gd.data[1].xbins.end).negateIf(auto).toBe(end1, msg);
-                expect(gd.data[1].ybins.size).negateIf(auto).toBe(size1, msg);
+                expect(gd.data[0].autobinx).toBeUndefined(msg);
+                expect(gd.data[1].autobinx).toBeUndefined(msg);
+                expect(gd.data[1].autobiny).toBeUndefined(msg);
+
+                if(auto) {
+                    expect(gd.data[0].xbins).toBeUndefined(msg);
+                    expect(gd.data[1].xbins).toBeUndefined(msg);
+                    expect(gd.data[1].ybins).toBeUndefined(msg);
+                }
+                else {
+                    // we can have - and use - partial autobin now
+                    expect(gd.data[0].xbins).toEqual({start: start0});
+                    expect(gd.data[1].xbins).toEqual({end: end1});
+                    expect(gd.data[1].ybins).toEqual({size: size1});
+                    expect(gd._fullData[0].xbins.start).toBe(start0, msg);
+                    expect(gd._fullData[1].xbins.end).toBe(end1, msg);
+                    expect(gd._fullData[1].ybins.size).toBe(size1, msg);
+                }
             }
 
             Plotly.plot(gd, [

From c0b8c6ffe4ca4273bc1b578bdeaab664570615bf Mon Sep 17 00:00:00 2001
From: alexcjohnson <alex@plot.ly>
Date: Thu, 27 Sep 2018 15:58:35 -0400
Subject: [PATCH 06/10] test invisible bars and traceorder

---
 test/jasmine/tests/bar_test.js | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js
index dc9ec698751..609231fffda 100644
--- a/test/jasmine/tests/bar_test.js
+++ b/test/jasmine/tests/bar_test.js
@@ -1423,6 +1423,37 @@ describe('bar visibility toggling:', function() {
         .catch(failTest)
         .then(done);
     });
+
+    it('gets the right legend traceorder if all bars are visible: false', function(done) {
+        function _assert(traceorder, yRange, legendCount) {
+            expect(gd._fullLayout.legend.traceorder).toBe(traceorder);
+            expect(gd._fullLayout.yaxis.range).toBeCloseToArray(yRange, 2);
+            expect(d3.select(gd).selectAll('.legend .traces').size()).toBe(legendCount);
+        }
+        Plotly.newPlot(gd, [
+            {type: 'bar', y: [1, 2, 3]},
+            {type: 'bar', y: [3, 2, 1]},
+            {y: [2, 3, 2]},
+            {y: [3, 2, 3]}
+        ], {
+            barmode: 'stack', width: 400, height: 400
+        })
+        .then(function() {
+            _assert('reversed', [0, 4.211], 4);
+
+            return Plotly.restyle(gd, {visible: false}, [0, 1]);
+        })
+        .then(function() {
+            _assert('normal', [1.922, 3.077], 2);
+
+            return Plotly.restyle(gd, {visible: 'legendonly'}, [0, 1]);
+        })
+        .then(function() {
+            _assert('reversed', [1.922, 3.077], 4);
+        })
+        .catch(failTest)
+        .then(done);
+    });
 });
 
 describe('bar hover', function() {

From 76eee4b992eacbebff1f00ee025c7696eeb7f5d3 Mon Sep 17 00:00:00 2001
From: alexcjohnson <alex@plot.ly>
Date: Thu, 27 Sep 2018 19:52:03 -0400
Subject: [PATCH 07/10] flesh out bin attribute descriptions and let
 autobin(x|y) pass validate

---
 src/traces/histogram/attributes.js          | 99 +++++++--------------
 src/traces/histogram/bin_attributes.js      | 74 +++++++++++++++
 src/traces/histogram/calc.js                |  6 +-
 src/traces/histogram/defaults.js            |  2 +
 src/traces/histogram2d/attributes.js        | 13 ++-
 src/traces/histogram2d/calc.js              |  6 +-
 src/traces/histogram2d/sample_defaults.js   |  3 +
 src/traces/histogram2dcontour/attributes.js |  8 +-
 test/jasmine/tests/validate_test.js         | 69 ++++++++++++++
 9 files changed, 198 insertions(+), 82 deletions(-)
 create mode 100644 src/traces/histogram/bin_attributes.js

diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js
index bd9e3df112b..80bf57e1585 100644
--- a/src/traces/histogram/attributes.js
+++ b/src/traces/histogram/attributes.js
@@ -9,6 +9,7 @@
 'use strict';
 
 var barAttrs = require('../bar/attributes');
+var makeBinAttrs = require('./bin_attributes');
 
 module.exports = {
     x: {
@@ -138,7 +139,7 @@ module.exports = {
             'Ignored if `xbins.size` is provided.'
         ].join(' ')
     },
-    xbins: makeBinsAttr('x'),
+    xbins: makeBinAttrs('x', true),
 
     nbinsy: {
         valType: 'integer',
@@ -153,7 +154,36 @@ module.exports = {
             'Ignored if `ybins.size` is provided.'
         ].join(' ')
     },
-    ybins: makeBinsAttr('y'),
+    ybins: makeBinAttrs('y', true),
+    autobinx: {
+        valType: 'boolean',
+        dflt: null,
+        role: 'style',
+        editType: 'calc',
+        description: [
+            'Obsolete: since v1.42 each bin attribute is auto-determined',
+            'separately and `autobinx` is not needed. However, we accept',
+            '`autobinx: true` or `false` and will update `xbins` accordingly',
+            'before deleting `autobinx` from the trace.'
+        ].join(' ')
+    },
+    autobiny: {
+        valType: 'boolean',
+        dflt: null,
+        role: 'style',
+        editType: 'calc',
+        impliedEdits: {
+            'ybins.start': undefined,
+            'ybins.end': undefined,
+            'ybins.size': undefined
+        },
+        description: [
+            'Obsolete: since v1.42 each bin attribute is auto-determined',
+            'separately and `autobiny` is not needed. However, we accept',
+            '`autobiny: true` or `false` and will update `ybins` accordingly',
+            'before deleting `autobiny` from the trace.'
+        ].join(' ')
+    },
 
     marker: barAttrs.marker,
 
@@ -161,69 +191,6 @@ module.exports = {
     unselected: barAttrs.unselected,
 
     _deprecated: {
-        bardir: barAttrs._deprecated.bardir,
-        autobinx: {
-            valType: 'boolean',
-            dflt: null,
-            role: 'style',
-            editType: 'calc',
-            impliedEdits: {
-                'xbins.start': undefined,
-                'xbins.end': undefined,
-                'xbins.size': undefined
-            },
-            description: [
-                'Obsolete: since v1.42 each bin',
-                'attribute is auto-determined separately.'
-            ].join(' ')
-        },
-        autobiny: {
-            valType: 'boolean',
-            dflt: null,
-            role: 'style',
-            editType: 'calc',
-            impliedEdits: {
-                'ybins.start': undefined,
-                'ybins.end': undefined,
-                'ybins.size': undefined
-            },
-            description: [
-                'Obsolete: since v1.42 each bin',
-                'attribute is auto-determined separately.'
-            ].join(' ')
-        }
+        bardir: barAttrs._deprecated.bardir
     }
 };
-
-function makeBinsAttr(axLetter) {
-    return {
-        start: {
-            valType: 'any', // for date axes
-            role: 'style',
-            editType: 'calc',
-            description: [
-                'Sets the starting value for the', axLetter,
-                'axis bins.'
-            ].join(' ')
-        },
-        end: {
-            valType: 'any', // for date axes
-            role: 'style',
-            editType: 'calc',
-            description: [
-                'Sets the end value for the', axLetter,
-                'axis bins.'
-            ].join(' ')
-        },
-        size: {
-            valType: 'any', // for date axes
-            role: 'style',
-            editType: 'calc',
-            description: [
-                'Sets the step in-between value each', axLetter,
-                'axis bin.'
-            ].join(' ')
-        },
-        editType: 'calc'
-    };
-}
diff --git a/src/traces/histogram/bin_attributes.js b/src/traces/histogram/bin_attributes.js
new file mode 100644
index 00000000000..24c800477b8
--- /dev/null
+++ b/src/traces/histogram/bin_attributes.js
@@ -0,0 +1,74 @@
+/**
+* Copyright 2012-2018, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+module.exports = function makeBinAttrs(axLetter, match) {
+    return {
+        start: {
+            valType: 'any', // for date axes
+            role: 'style',
+            editType: 'calc',
+            description: [
+                'Sets the starting value for the', axLetter,
+                'axis bins. Defaults to the minimum data value,',
+                'shifted down if necessary to make nice round values',
+                'and to remove ambiguous bin edges. For example, if most of the',
+                'data is integers we shift the bin edges 0.5 down, so a `size`',
+                'of 5 would have a default `start` of -0.5, so it is clear',
+                'that 0-4 are in the first bin, 5-9 in the second, but',
+                'continuous data gets a start of 0 and bins [0,5), [5,10) etc.',
+                'Dates behave similarly, and `start` should be a date string.',
+                'For category data, `start` is based on the category serial',
+                'numbers, and defaults to -0.5.',
+                (match ? (
+                    'If multiple non-overlaying histograms share a subplot, ' +
+                    'the first explicit `start` is used exactly and all others ' +
+                    'are shifted down (if necessary) to differ from that one ' +
+                    'by an integer number of bins.'
+                ) : '')
+            ].join(' ')
+        },
+        end: {
+            valType: 'any', // for date axes
+            role: 'style',
+            editType: 'calc',
+            description: [
+                'Sets the end value for the', axLetter,
+                'axis bins. The last bin may not end exactly at this value,',
+                'we increment the bin edge by `size` from `start` until we',
+                'reach or exceed `end`. Defaults to the maximum data value.',
+                'Like `start`, for dates use a date string, and for category',
+                'data `end` is based on the category serial numbers.'
+            ].join(' ')
+        },
+        size: {
+            valType: 'any', // for date axes
+            role: 'style',
+            editType: 'calc',
+            description: [
+                'Sets the size of each', axLetter, 'axis bin.',
+                'Default behavior: If `nbins' + axLetter + '` is 0 or omitted,',
+                'we choose a nice round bin size such that the number of bins',
+                'is about the same as the typical number of samples in each bin.',
+                'If `nbins' + axLetter + '` is provided, we choose a nice round',
+                'bin size giving no more than that many bins.',
+                'For date data, use milliseconds or *M<n>* for months, as in',
+                '`axis.dtick`. For category data, the number of categories to',
+                'bin together (always defaults to 1).',
+                (match ? (
+                    'If multiple non-overlaying histograms share a subplot, ' +
+                    'the first explicit `size` is used and all others discarded. ' +
+                    'If no `size` is provided,the sample data from all traces ' +
+                    'is combined to determine `size` as described above.'
+                ) : '')
+            ].join(' ')
+        },
+        editType: 'calc'
+    };
+};
diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js
index 5b66465a5f5..1a60c9206e9 100644
--- a/src/traces/histogram/calc.js
+++ b/src/traces/histogram/calc.js
@@ -359,9 +359,11 @@ function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) {
     // Backward compatibility for one-time autobinning.
     // autobin: true is handled in cleanData, but autobin: false
     // needs to be here where we have determined the values.
-    if(trace._input['autobin' + mainData] === false) {
+    var autoBinAttr = 'autobin' + mainData;
+    if(trace._input[autoBinAttr] === false) {
         trace._input[binAttr] = Lib.extendFlat({}, trace[binAttr] || {});
-        delete trace._input['autobin' + mainData];
+        delete trace._input[autoBinAttr];
+        delete trace[autoBinAttr];
     }
 
     return [traceBinOptsCalc, pos0];
diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js
index 296f8834bb6..949adacf76f 100644
--- a/src/traces/histogram/defaults.js
+++ b/src/traces/histogram/defaults.js
@@ -53,6 +53,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
     coerce('histnorm');
 
     // Note: bin defaults are now handled in Histogram.cleanData
+    // autobin(x|y) are only included here to appease Plotly.validate
+    coerce('autobin' + sampleLetter);
 
     handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout);
 
diff --git a/src/traces/histogram2d/attributes.js b/src/traces/histogram2d/attributes.js
index c8a208ce5a6..eba5151eb0b 100644
--- a/src/traces/histogram2d/attributes.js
+++ b/src/traces/histogram2d/attributes.js
@@ -9,6 +9,7 @@
 'use strict';
 
 var histogramAttrs = require('../histogram/attributes');
+var makeBinAttrs = require('../histogram/bin_attributes');
 var heatmapAttrs = require('../heatmap/attributes');
 var colorscaleAttrs = require('../../components/colorscale/attributes');
 var colorbarAttrs = require('../../components/colorbar/attributes');
@@ -37,18 +38,16 @@ module.exports = extendFlat(
         histnorm: histogramAttrs.histnorm,
         histfunc: histogramAttrs.histfunc,
         nbinsx: histogramAttrs.nbinsx,
-        xbins: histogramAttrs.xbins,
+        xbins: makeBinAttrs('x'),
         nbinsy: histogramAttrs.nbinsy,
-        ybins: histogramAttrs.ybins,
+        ybins: makeBinAttrs('y'),
+        autobinx: histogramAttrs.autobinx,
+        autobiny: histogramAttrs.autobiny,
 
         xgap: heatmapAttrs.xgap,
         ygap: heatmapAttrs.ygap,
         zsmooth: heatmapAttrs.zsmooth,
-        zhoverformat: heatmapAttrs.zhoverformat,
-        _deprecated: {
-            autobinx: histogramAttrs._deprecated.autobinx,
-            autobiny: histogramAttrs._deprecated.autobiny
-        }
+        zhoverformat: heatmapAttrs.zhoverformat
     },
     colorscaleAttrs('', {
         cLetter: 'z',
diff --git a/src/traces/histogram2d/calc.js b/src/traces/histogram2d/calc.js
index c345c904e5f..7a221a8eb91 100644
--- a/src/traces/histogram2d/calc.js
+++ b/src/traces/histogram2d/calc.js
@@ -219,9 +219,11 @@ function doAutoBin(trace, axLetter, data, ax, r2c, c2r, calendar) {
     // Backward compatibility for one-time autobinning.
     // autobin: true is handled in cleanData, but autobin: false
     // needs to be here where we have determined the values.
-    if(trace._input['autobin' + axLetter] === false) {
+    var autoBinAttr = 'autobin' + axLetter;
+    if(trace._input[autoBinAttr] === false) {
         trace._input[binAttr] = Lib.extendFlat({}, binSpec);
-        delete trace._input['autobin' + axLetter];
+        delete trace._input[autoBinAttr];
+        delete trace[autoBinAttr];
     }
 }
 
diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js
index b1262ec82e9..155f717de6f 100644
--- a/src/traces/histogram2d/sample_defaults.js
+++ b/src/traces/histogram2d/sample_defaults.js
@@ -35,4 +35,7 @@ module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout
     coerce('histnorm');
 
     // Note: bin defaults are now handled in Histogram2D.cleanData
+    // autobin(x|y) are only included here to appease Plotly.validate
+    coerce('autobinx');
+    coerce('autobiny');
 };
diff --git a/src/traces/histogram2dcontour/attributes.js b/src/traces/histogram2dcontour/attributes.js
index e8c3c402d7b..ee60da32e57 100644
--- a/src/traces/histogram2dcontour/attributes.js
+++ b/src/traces/histogram2dcontour/attributes.js
@@ -27,16 +27,14 @@ module.exports = extendFlat({
     xbins: histogram2dAttrs.xbins,
     nbinsy: histogram2dAttrs.nbinsy,
     ybins: histogram2dAttrs.ybins,
+    autobinx: histogram2dAttrs.autobinx,
+    autobiny: histogram2dAttrs.autobiny,
 
     autocontour: contourAttrs.autocontour,
     ncontours: contourAttrs.ncontours,
     contours: contourAttrs.contours,
     line: contourAttrs.line,
-    zhoverformat: histogram2dAttrs.zhoverformat,
-    _deprecated: {
-        autobinx: histogram2dAttrs._deprecated.autobinx,
-        autobiny: histogram2dAttrs._deprecated.autobiny
-    }
+    zhoverformat: histogram2dAttrs.zhoverformat
 },
     colorscaleAttrs('', {
         cLetter: 'z',
diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js
index d3123bd3a1a..0a455f2b4e5 100644
--- a/test/jasmine/tests/validate_test.js
+++ b/test/jasmine/tests/validate_test.js
@@ -568,4 +568,73 @@ describe('Plotly.validate', function() {
             'In layout, container polar3 did not get coerced'
         );
     });
+
+    it('understands histogram bin and autobin attributes', function() {
+        var out = Plotly.validate([{
+            type: 'histogram',
+            x: [1, 2, 3],
+            // allowed by Plotly.validate, even though we get rid of it
+            // in a real plot call
+            autobinx: true,
+            // valid attribute, but not coerced
+            autobiny: false
+        }]);
+        expect(out.length).toBe(1);
+        assertErrorContent(
+            out[0], 'unused', 'data', 0, ['autobiny'], 'autobiny',
+            'In data trace 0, key autobiny did not get coerced'
+        );
+
+        out = Plotly.validate([{
+            type: 'histogram',
+            x: [1, 2, 3],
+            xbins: {start: 1, end: 4, size: 0.5}
+        }]);
+        expect(out).toBeUndefined();
+
+        out = Plotly.validate([{
+            type: 'histogram',
+            x: [1, 2, 3],
+            xbins: {start: 0.8, end: 4, size: 0.5}
+        }, {
+            type: 'histogram',
+            x: [1, 2, 3],
+            // start and end still get coerced, even though start will get modified
+            // during calc. size will not be coerced because trace 0 already has it.
+            xbins: {start: 2, end: 3, size: 1}
+        }]);
+
+        expect(out.length).toBe(1);
+        assertErrorContent(
+            out[0], 'unused', 'data', 1, ['xbins', 'size'], 'xbins.size',
+            'In data trace 1, key xbins.size did not get coerced'
+        );
+    });
+
+    it('understands histogram2d(contour) bin and autobin attributes', function() {
+        var out = Plotly.validate([{
+            type: 'histogram2d',
+            x: [1, 2, 3],
+            y: [1, 2, 3],
+            autobinx: true,
+            autobiny: false,
+            xbins: {start: 5, end: 10},
+            ybins: {size: 2}
+        }, {
+            type: 'histogram2d',
+            x: [1, 2, 3],
+            y: [1, 2, 3],
+            xbins: {start: 0, end: 7, size: 1},
+            ybins: {size: 3}
+        }, {
+            type: 'histogram2dcontour',
+            x: [1, 2, 3],
+            y: [1, 2, 3],
+            autobinx: false,
+            autobiny: false,
+            xbins: {start: 1, end: 5, size: 2},
+            ybins: {size: 4}
+        }]);
+        expect(out).toBeUndefined();
+    });
 });

From 5d930cee2d0df51ad6bbb99a0df17a4eb2fe64dc Mon Sep 17 00:00:00 2001
From: alexcjohnson <alex@plot.ly>
Date: Thu, 27 Sep 2018 21:23:25 -0400
Subject: [PATCH 08/10] _module.cleanData -> crossTraceDefaults

---
 src/plots/plots.js                                 | 14 +++++++-------
 .../{clean_data.js => cross_trace_defaults.js}     |  2 +-
 src/traces/histogram/defaults.js                   |  2 +-
 src/traces/histogram/index.js                      |  2 +-
 .../{clean_data.js => cross_trace_defaults.js}     |  2 +-
 src/traces/histogram2d/index.js                    |  2 +-
 src/traces/histogram2d/sample_defaults.js          |  2 +-
 src/traces/histogram2dcontour/index.js             |  2 +-
 .../{clean_data.js => cross_trace_defaults.js}     |  2 +-
 src/traces/scatter/index.js                        |  2 +-
 src/traces/scattergl/index.js                      |  2 +-
 11 files changed, 17 insertions(+), 17 deletions(-)
 rename src/traces/histogram/{clean_data.js => cross_trace_defaults.js} (98%)
 rename src/traces/histogram2d/{clean_data.js => cross_trace_defaults.js} (97%)
 rename src/traces/scatter/{clean_data.js => cross_trace_defaults.js} (94%)

diff --git a/src/plots/plots.js b/src/plots/plots.js
index 6c10a889201..246f518de3e 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -438,17 +438,17 @@ plots.supplyDefaults = function(gd, opts) {
     plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData, gd._transitionData);
 
     // Special cases that introduce interactions between traces.
-    // This is after relinkPrivateKeys so we can use those in cleanData
+    // This is after relinkPrivateKeys so we can use those in crossTraceDefaults
     // and after layout module defaults, so we can use eg barmode
     var _modules = newFullLayout._visibleModules;
-    var cleanDataFuncs = [];
+    var crossTraceDefaultsFuncs = [];
     for(i = 0; i < _modules.length; i++) {
-        var _module = _modules[i];
-        // some trace types share cleanData (ie histogram2d, histogram2dcontour)
-        if(_module.cleanData) Lib.pushUnique(cleanDataFuncs, _module.cleanData);
+        var funci = _modules[i].crossTraceDefaults;
+        // some trace types share crossTraceDefaults (ie histogram2d, histogram2dcontour)
+        if(funci) Lib.pushUnique(crossTraceDefaultsFuncs, funci);
     }
-    for(i = 0; i < cleanDataFuncs.length; i++) {
-        cleanDataFuncs[i](newFullData, newFullLayout);
+    for(i = 0; i < crossTraceDefaultsFuncs.length; i++) {
+        crossTraceDefaultsFuncs[i](newFullData, newFullLayout);
     }
 
     // turn on flag to optimize large splom-only graphs
diff --git a/src/traces/histogram/clean_data.js b/src/traces/histogram/cross_trace_defaults.js
similarity index 98%
rename from src/traces/histogram/clean_data.js
rename to src/traces/histogram/cross_trace_defaults.js
index 90dfb33b47b..4bffacb4d69 100644
--- a/src/traces/histogram/clean_data.js
+++ b/src/traces/histogram/cross_trace_defaults.js
@@ -30,7 +30,7 @@ var BINATTRS = {
 };
 
 // handle bin attrs and relink auto-determined values so fullData is complete
-module.exports = function cleanData(fullData, fullLayout) {
+module.exports = function crossTraceDefaults(fullData, fullLayout) {
     var allBinOpts = fullLayout._histogramBinOpts = {};
     var isOverlay = fullLayout.barmode === 'overlay';
     var i, j, traceOut, traceIn, binDirection, group, binOpts;
diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js
index 949adacf76f..23ef933ba18 100644
--- a/src/traces/histogram/defaults.js
+++ b/src/traces/histogram/defaults.js
@@ -52,7 +52,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
     if(hasAggregationData) coerce('histfunc');
     coerce('histnorm');
 
-    // Note: bin defaults are now handled in Histogram.cleanData
+    // Note: bin defaults are now handled in Histogram.crossTraceDefaults
     // autobin(x|y) are only included here to appease Plotly.validate
     coerce('autobin' + sampleLetter);
 
diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js
index e1469215b67..b157d73f4be 100644
--- a/src/traces/histogram/index.js
+++ b/src/traces/histogram/index.js
@@ -28,7 +28,7 @@ var Histogram = {};
 Histogram.attributes = require('./attributes');
 Histogram.layoutAttributes = require('../bar/layout_attributes');
 Histogram.supplyDefaults = require('./defaults');
-Histogram.cleanData = require('./clean_data');
+Histogram.crossTraceDefaults = require('./cross_trace_defaults');
 Histogram.supplyLayoutDefaults = require('../bar/layout_defaults');
 Histogram.calc = require('./calc');
 Histogram.crossTraceCalc = require('../bar/cross_trace_calc').crossTraceCalc;
diff --git a/src/traces/histogram2d/clean_data.js b/src/traces/histogram2d/cross_trace_defaults.js
similarity index 97%
rename from src/traces/histogram2d/clean_data.js
rename to src/traces/histogram2d/cross_trace_defaults.js
index cff8ddf781c..a32db267f94 100644
--- a/src/traces/histogram2d/clean_data.js
+++ b/src/traces/histogram2d/cross_trace_defaults.js
@@ -23,7 +23,7 @@ var BINDIRECTIONS = ['x', 'y'];
 // Handle bin attrs and relink auto-determined values so fullData is complete
 // does not have cross-trace coupling, but moved out here so we have axis types
 // and relinked trace._autoBin
-module.exports = function cleanData(fullData, fullLayout) {
+module.exports = function crossTraceDefaults(fullData, fullLayout) {
     var i, j, traceOut, binDirection;
 
     function coerce(attr) {
diff --git a/src/traces/histogram2d/index.js b/src/traces/histogram2d/index.js
index 6f3e49860f7..4423cfb0945 100644
--- a/src/traces/histogram2d/index.js
+++ b/src/traces/histogram2d/index.js
@@ -13,7 +13,7 @@ var Histogram2D = {};
 
 Histogram2D.attributes = require('./attributes');
 Histogram2D.supplyDefaults = require('./defaults');
-Histogram2D.cleanData = require('./clean_data');
+Histogram2D.crossTraceDefaults = require('./cross_trace_defaults');
 Histogram2D.calc = require('../heatmap/calc');
 Histogram2D.plot = require('../heatmap/plot');
 Histogram2D.layerName = 'heatmaplayer';
diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js
index 155f717de6f..aca6acf6595 100644
--- a/src/traces/histogram2d/sample_defaults.js
+++ b/src/traces/histogram2d/sample_defaults.js
@@ -34,7 +34,7 @@ module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout
     if(hasAggregationData) coerce('histfunc');
     coerce('histnorm');
 
-    // Note: bin defaults are now handled in Histogram2D.cleanData
+    // Note: bin defaults are now handled in Histogram2D.crossTraceDefaults
     // autobin(x|y) are only included here to appease Plotly.validate
     coerce('autobinx');
     coerce('autobiny');
diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js
index 99a3679af0e..7e8400d7499 100644
--- a/src/traces/histogram2dcontour/index.js
+++ b/src/traces/histogram2dcontour/index.js
@@ -13,7 +13,7 @@ var Histogram2dContour = {};
 
 Histogram2dContour.attributes = require('./attributes');
 Histogram2dContour.supplyDefaults = require('./defaults');
-Histogram2dContour.cleanData = require('../histogram2d/clean_data');
+Histogram2dContour.crossTraceDefaults = require('../histogram2d/cross_trace_defaults');
 Histogram2dContour.calc = require('../contour/calc');
 Histogram2dContour.plot = require('../contour/plot').plot;
 Histogram2dContour.layerName = 'contourlayer';
diff --git a/src/traces/scatter/clean_data.js b/src/traces/scatter/cross_trace_defaults.js
similarity index 94%
rename from src/traces/scatter/clean_data.js
rename to src/traces/scatter/cross_trace_defaults.js
index b72ab26eb1a..7f079158165 100644
--- a/src/traces/scatter/clean_data.js
+++ b/src/traces/scatter/cross_trace_defaults.js
@@ -11,7 +11,7 @@
 
 
 // remove opacity for any trace that has a fill or is filled to
-module.exports = function cleanData(fullData) {
+module.exports = function crossTraceDefaults(fullData) {
     for(var i = 0; i < fullData.length; i++) {
         var tracei = fullData[i];
         if(tracei.type !== 'scatter') continue;
diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js
index 133b54ae32e..82736acb321 100644
--- a/src/traces/scatter/index.js
+++ b/src/traces/scatter/index.js
@@ -19,7 +19,7 @@ Scatter.isBubble = subtypes.isBubble;
 
 Scatter.attributes = require('./attributes');
 Scatter.supplyDefaults = require('./defaults');
-Scatter.cleanData = require('./clean_data');
+Scatter.crossTraceDefaults = require('./cross_trace_defaults');
 Scatter.calc = require('./calc').calc;
 Scatter.crossTraceCalc = require('./cross_trace_calc');
 Scatter.arraysToCalcdata = require('./arrays_to_calcdata');
diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js
index 14f8b27d021..99657b2c68a 100644
--- a/src/traces/scattergl/index.js
+++ b/src/traces/scattergl/index.js
@@ -952,7 +952,7 @@ module.exports = {
 
     attributes: require('./attributes'),
     supplyDefaults: require('./defaults'),
-    cleanData: require('../scatter/clean_data'),
+    crossTraceDefaults: require('../scatter/cross_trace_defaults'),
     colorbar: require('../scatter/marker_colorbar'),
     calc: calc,
     plot: plot,

From 7514592f34d0a1eb290e97d5817902f0cd2d67f3 Mon Sep 17 00:00:00 2001
From: alexcjohnson <alex@plot.ly>
Date: Thu, 27 Sep 2018 21:26:58 -0400
Subject: [PATCH 09/10] updated test description

---
 test/jasmine/tests/histogram_test.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js
index 141204b08cc..1d86448b260 100644
--- a/test/jasmine/tests/histogram_test.js
+++ b/test/jasmine/tests/histogram_test.js
@@ -808,7 +808,7 @@ describe('Test histogram', function() {
             .then(done);
         });
 
-        it('autobins all histograms together except `visible: false`', function(done) {
+        it('autobins all histograms (on the same subplot) together except `visible: false`', function(done) {
             function _assertBinCenters(expectedCenters) {
                 var centers = gd.calcdata.map(function(cd) {
                     return cd.map(function(cdi) { return cdi.p; });

From 7909f53274f20997104193e136d5f2e939184c00 Mon Sep 17 00:00:00 2001
From: alexcjohnson <alex@plot.ly>
Date: Thu, 27 Sep 2018 21:42:48 -0400
Subject: [PATCH 10/10] new histogram autobin mock

---
 test/image/baselines/hist_multi.png | Bin 0 -> 12999 bytes
 test/image/mocks/hist_multi.json    |  22 ++++++++++++++++++++++
 2 files changed, 22 insertions(+)
 create mode 100644 test/image/baselines/hist_multi.png
 create mode 100644 test/image/mocks/hist_multi.json

diff --git a/test/image/baselines/hist_multi.png b/test/image/baselines/hist_multi.png
new file mode 100644
index 0000000000000000000000000000000000000000..df9598daf235ac47065a55dec0caa7ed64186b4e
GIT binary patch
literal 12999
zcmeHO2{e@L-?o*Z1*yDA)+|L7A~a;ggd%N1Xd%fKhQVN{(Pq!~%GM;6WXTd^4DH!x
zERkglWf|MphOv$L?on@Vz2A1ebN=Ui-*--(<DASq&;8ubegA&fef_TA<<4<4_y&Ga
zehv<f4aP?fpWxu&q=TOr$Xf8p*`j+NI5;FZj1TLd^0OOH<BK`{siZ*UcJ1cg?D%DM
z!PY;-Zm3}5VLOY$L?a>k+?>Y*H*@mAu0J?vahZ?jO)rPsrq}Yj)voG4JkGa;YgzKC
zzv$tDz1TzguA7hZEVxXtn%<L|8HH^<srU)xv!{R32yMDcHlscHMBGsN$>HADH+aJ~
za0!NR@JMomUY9%0hZR9EKl?)taexm)<U>f#Jm3C*h>J%sfXmQ<@7uMU7&C#@YdCl|
zNrsSw6{Jm9$KZrWA_Ca=f&pVVI5Fvmgsiy0*qmS#Yzz1I2k}58={$F<H+}n@3z8=O
z{pOwED*5Aw6IZY25<EnuvnK{_mgMAuKtrVqzt4sP_JsHQ&3xc0zRj_+-)AGJ4gG%e
zkq{1;`Gea^oZn^>LK<dI0{rs(RetZv?*IcC_d8d<A=SSC!JxOqsg$a!<*yQ~7?XtE
z3r%_}Q_%x2yWH!f?n!+|y#oM2H>To!M&&Z$(TSZUE<=0o|MlHG`@4D_1XWV)a=n5M
zj3|Zq_En5bC_~<?-m{-2;d|AXJG*7@X2r$37xu!4*jml*Yd2bRZ7k<#WJUXhTFd?@
z9a>Dl?8ty%jo>QOJ-z)Z(VmgiyfC9EdS-5EQ6TbOXS;pI%>2b|w@Kbl>FH3^BkPi@
zUcIk4lXZ}J%fsgAdvJ-MR=9Rek-j7kXS?_P$evb`%$y%jF~J`}`xAy{kdXs3M7<;x
z^g>-zctybMRsZQ4`2h-Rr8G5oaoCIO(_V8-lkD(P0>b1uUe0y&zPfRv_NbC`AaO=M
zMhYe6(aj=^jh~90h@Cqoj13Q*N%pvpY^t48Z166}Oe_zzH_T`RJ4X7OB^~cdkv~a2
z`S@t8+_C#da?a#lFVM1UPO=LyZq%7L=-gfQVyv^o6uhr2quV?BoL^s<+|qTDImacu
z?|A<m`Sh6k`(5N@=tv_w6xU8j=%JT<=d{O9%<KppPi(IT5A!&8e&%uTe539B;CB7F
zPukAUW$3b|iPi7!2xl2ZimotGy{|$GT{~2>?Fi*%nL{Nm?TeI&9zvHyaYACE$v|yF
z7Cc(Kxx(MO$`y%W;VDcr4UZnBC+PvtB1HF_fPW6)8daZ>Qw#JK^DO6<$98Qr#>cLk
z>$gc;+WBV%4(U+#>E9(OzAC%)aib@za`97sEjW+|I-D{RPUoCi$mt;`)(UCEH2tBm
zJ1>EBh3S>c!lT4??XfNfF@jzke&X1es@0rtU#04dX9UU*F3ygkEby5D3}V`|iO!Oo
zZm+sMx+|H{y3^=CTq%{d0SwxnuOA|3+HJJ-%1$oYBecmlhlcz6R|L2j#F6c%AcX6h
z58S$5){w9Uj-ze4UY*WQY79kecarhQB(=r^+*qfvh7@w>t0wkrh=$Y@4Yzxo+-u{P
zxH_xQRmE;yX9gQm<hN_MP8RASHhjW{bQC#-dxv@7=xWQgk#Bi?EbhgnkK%FOZ!mm?
zSDK`=Ph{QIyUSW(RC&z3F{GNri(wTxH*E|J`93`DO7{*QiO%k9B$Bgu3D;$ZX8Kqa
zoDY3|_62N^n4$9a<^~<<@k`NAxo{DcL=egg8TfTi$!Hyabkj@6x~;5_uRmGZ@3FM?
zNvO%;z$)&OM#vs|BPAHa0fSR<kRVEBP&3ZkNv}1{2<auE7hIw4`k91|%MMzav!jl-
z=O^vE^g+xUqwCV9)B&#RK?(--g^BbxXj0<w6O~>4r$a~ye+ZRtlI#<N9O1yq&@;PS
zru%DF)JCIr=q%dfh$I1#@+H3#Ct|v4&>|w+y5w1Xs#Xi-Z2gX8e9MGwIYJ1@ASLdx
zxw~XigqJPDUnF=?Dp$T*A=lvR7_WIH#JsESwcDYNDOb*4L}Sy!Jw@IJO`s?<@Zqi7
zVU{NjQ%yoJ29lDTm?%=Ty9zR45imOD8h4PT8CBH_*Rc30n%Z=hY0_f;P>FMk(N^Vi
zXMImqdVV!ROQq;I(lB~~^Vgh74??S|8`1)h(n!lv<VdTrb*X#MJ38%p1qf3^Gp&Km
zEGS)KW%AZ^OU6p;nDQft?ZV~jqy!GWTZayDL3UtyUGwX+jHtEuB!4Wg%6jY&N`=uP
zy8laC{y>GhVEJ?d&yURwxB!8kU$^=6RZO0eCd`s+rx+I}pCke!UO-Sy62&T-{x_q;
zshnMk-q}Lsg*zd(d@<J`JUc^3LIJPlYR|EU<omCOB<#iL`j7Wlls|ZM`OfmZqF$ro
z<3HBQARH6^-b#_1B;IA85T)+2y+dYycn&}ons9ox{(&#nAEwrt$ozOuI2B%+grCj9
zkLRH5{Kv*~OM9k?r;*VvKBbY&8uMW3c6&6T!yPP)2_}!{$N?U%O^2uuCUwpW=GOe^
zPVv=GyU!0LWOPC=mOOg&PVd2c@(cl>it5F85Y{1)@C{hST|dX{t<v^;vZfAS-rYHo
zX|y5Rs_5~0u>;3J0LgvIu}lI#;i!$}OH3LW@7Vmv_=L6fNJhlqyDorH8MQn5@VAOw
zdn!8gFpjNHE$o^VE%V~DPd-_6daCKxd5V2-pwSiMD@zj$A`EHnUi4Y8{GRkrOBswn
z;pEdxt~5OygOH)RK0mN$;`58Q;kaPtqFpQPX%Ex;$q0bpk&Gl-)Fip6g)FhQtJEW@
zhk&Xp^ZfeAom{ROK)-@EVLeW9UwlLJjlSsPzw!OqiuM0^TzWW(?9|D<cD+KWaGGD&
zOE<-t!G^dE+co^G@E>!;D;v_GHZR?}<N!7?^z2IA(~hQS9QzQpV=0Wxoc|iVG?YUL
zUK+og&w<H1eCMZV9g;>M)oD?Q0&cX{rEZ)z)1v%829{j*S_W2}$+cb?;@v1V$%0;Z
zgYm+5F+F>umL&ow^;xcn9I!*iKkv|gf-1+^hxB+?DFbF-6-<cOrZ)Je4Be{ea!q5J
z4x1<B6r_mkvEGSZnaOaZlCWL?+bs|1W^h8F64nC|02+VF^Zc4PdDHRElB`P~qgE_w
zYV@~Zl|G|qhC7O$9L>-rqf1-|>yuS4eOQlnpqa%Yhno${eMSdW9J-2}qE9_LW$ZiN
zDGLy8_MWfhb9^9e_Z-;z9s3A)g^4f`JBYNmrVz}sGWqXZ{ubjVL6g0&SGY=-Xn1q5
zBt94t*^jYpp*hPm3jlBP<73;c%C24LuvqvIxt)2MX7kc@>WzeMV43<A(xJBJRx-3!
zyf6-5PB{6*tQpKl<}p&FMr#4D$F<Q7oIr6>1~>n#;1?|4B=b|m{Remek~e*V(7pZB
z)pxn}b^c^Y#6Zg5`A&Z$81QPV!n0P0t(E&v2?sroa#Y?AvrPOOgS24+pY4)OBS{Ie
z`adl%_e;yc695a0!Bl?0ddPSJjy>e2|9Z%PP)N}5?d9?ZjY)>eqcQeJg(3hdO8<=(
zDuP%dO>TOmzFVj7!^gvwbMK?&8lV=|GC~33vxri<w<ZNCkjJ7St47%$actuct^RiI
z4|M0+lilpeGqvU)2F2%8ueK^^`At4JIQfeEF+PMe^U9yaWGtmj*?Twahd>3~*N45T
zkp`H!*hJ8DtljMV9cuz?Dp@N_)Ap=cd#@%#k(#;jE};NA|MCQe=82>wmpw`Iz0YP+
z5T?VH>;6(PyzG-jz(a&lTy#fR-Xv0V|D($d_f@a;9<qArntUqX&h|{c-5#12mBzp$
zClZb0<cyu2vrpxnvkIV7r5aK+8QI-Jm8b)CfEF+f&UQEX8XMn1S1z<>pJb}F0`@RM
z>@TNCRN}bb981xtamomi^B%0%r}|eUHr%8ESkX)P2<2r=eUwNtRmSmPQ<`Ljc-{!D
zLMTmC%j;3`Uug8HrUYb`W3%tXf?56SXglMfFMO7kp>vv2;$q3FKp-&_>1ni;z1QGf
z1Qa9D^Xms=a=DKqo!Yb#Ux_NW_D?~n0-T%?3@A%l5pfb%zY@cyGs^fe)#SYwVE16G
zj}InN*B96Oi*eBuU`4phh>H|uG|kJK2kpT?&ZH{$4PK_~C3anzW$h;pZ@P+_`7`aV
zpT#+U7!uyVMzN#>)Y@_s3(3_}aN$N$394eGuWPKM$Ppl0GXaHUef1e_t0{JFaTxpj
z0s=k4n|{9eeYi*+;B-vx33B2yQoG*#Tj8DYxdkmZ+Z)<8I?r9cmMzz*_Z4^i<z*K6
z`ElR<pM!PPE>`)|X6$U8(pzK^643kZ@>bLzG>U=q9$L&ql;WaoM3l}-DxP^BvoJGM
zS8(BN)Y(dacHV}$)+NhC0kH$UtgeNJ6_GNC7FrFo1Ip-@@u-+G>fj!>4K)XZ05|e1
zr#}x*`?8zSZSi57R+(?A|Hb-;n*$<ji|@<4%l6C(3Qv&7kW>QWPSW7*`4ut<#z&8#
z#x&+}q$s&2XnB6Bx!{6<r)5^D2c>5nc}F)c+Adlbma*R@ajYBHf?l^fX#;i$w(HHG
zr=m%eZT@aN0YZ-tQH3PC%tJ|rf&r-$So8FQR#)5e_UzE{rALnG8t9#)+LXG_IypQs
z&rk;ZIU!lisTLsZax6}V*^^}|GTfG%QtaB%j2;EN)1=U)U3s{xG=KdL?Ng$f?qf5!
ze698z%V-(IQt3K;1cV&r7;_W?g|v~30SAxvxwP#Ey`o(l0^{$HLbQ+4Ac4cpStoL>
z_XB<(sdTQaVC-oipx7(B8GCGA`t=D&=$;c*zamL{+Vv~JiX<6S`JWve`k<PqzQIch
z!F_w`B%1-5Qm61bJrUh!jcNc9;<UacgK&9hF)c-x773_ic<FsT=gzclR20T4Qt|*q
zJ@7dIE0q(Km4K;uVd2Atba{PhttcCDMz3MqkExsJf&|V~xBe0*iEs=B{O0c);8Y9I
zJyQVZ3SR*dMbZ|-+6YJ9696n$Au@3u5G$+m0Pe2rh$7|QF{-%*_*J}-X&`_=rM&>6
zi#NilZHF`a4X6;PNv5nWz~HTa{4(J$TNlgp@`*V_(Pg-gbNh3ByM6EXf=hBDz+s&G
zR;YY`lfr^vfaf{_vA#rgC;>gx?NUV;R&}1qkkyJ-Nel%uaQ?>(l(2Z4s`9SN@}Y!}
zJsZUjDhTK;7gQ`y*R9MI?9fKUjOO7RT{XeF$o^xBiKHh-lckT{SC0i}m*MdETY3Mf
z_acrp_heNass$JN<IEqKXXvhIjSXd>D{Csj;b-)ZN3X}~B%7rku(;a#RBFFVTYM-t
zZ*8Gt-6L>#7J@R^lc<lZq7YD}?26aK)HKU$ptd;^F5ui#fkW>plL=&8O9XhhlsRUR
zX=Dsg@cB91iN|JXBN^D9V&{{?pK_8|%)TxHqeoG*$1hXerLCUj2N>3L4{D-jyp-ZT
zt!+RxiTKA({Q*NZoa>n(rJY@x9krAj2f5(4uRY(M65hGv6CO=F71W+*%baN@0}%Et
zUwV6fX>Pn3kM!84>7L4#4veD3$IXW+h?ZmUir{nI1_IEX0E1t$aH!@kK#d$f7hI6b
zSHtAJyZ!T?fkHGHMk#iV)0?&J8B7oDdFf`8tm%>N`nlkBT_@CTcG7KbtfSDaOJlf)
zfM-pC<KCgO*jd$g?6b@Va^OtEbhS+Mku+_eWukr1>}mfmcjTkCAs?z}V}c*{uV`-k
zIgAjPVAU{M)CY3Y43=G^z#C~p?kMJLyW{KYYYH#E=TFu{uk3@jf-~r^8gS|dS(CBT
zEA0u$K?j%XOe4Fd2kMwbZCbGCX^oRpJ0XONMiy{M`qQ#pIS|B52mZgxK}sV{y*zGd
ze(G!1g|{~gkrQ^#luClgB0yrt`ak!56vGnl?>F_Ob?+)A`*wNSZ@RH|!#$UL!WreN
zWiLHivQzrhlhKKa8MC;gP4IueXJ=s<2mK3M^K5OjeaEP%rQs(TiN7#7+h=ETcdI!y
z#&1)*JTK#oTqKw#DL3cZXgYTkW`Pp}zWemEoq%EAs|;GI1(kXhILve@yb^(uvm>8$
zzRb=vo2t}2Gz_oJ^Fn6e@nxnegw}qao8L;+pY#6(hL(3i;-Fta)vNK!CEAS0;~mO~
z_Q`K0qe;otPmjQ%yiz=mB{pcfcT@AdD20yUB9%H}JrDG`0eOuqJ2_$2PaBytOVc(6
zazV5|vrMrst(7dn1?cGvoOK2isrT4LYFi(KeE&*(ASf0;ZVHIMscQ`INY?L{6brYZ
zF7(tf>zvwp*(IZ@Q2F#Y|IeX7|FSh8UQY2k8?vhZkqG}1$O7=2)wC%CaKv@LU=)D=
z<|}v1K~cKK=+~ElI~TuWBQDqf3gYHJcqM^eeu3Vahl)?{0)o>jhM~zfNosTa`qXWT
zN8Ir@0B*ady?Oc-kfts4Td0@gAPok1_2xs_dY!6MqnH)WD=XrUeOy?lsgf{K(b)uk
z1|K*C_){>PTR^e8MOxeImCi(kjDYU^gJmGqz^VR);i_U-_-j`vAJ9?Spr<1Mj*FjG
z*|R5sPgt%Rw3ZYB7$2hg7ldnU#=;HUw1mKQiOf^aaX{uwpuBWT0ldo~m6W3Cq0C%j
zBmps0UY1_MD6S7ZJa?;g8)IN>!XXcJpiI3zb|7kL@~yz>seL!{Q<0AK!>U^CYBF;v
z6YZ>pB`-j`E&v?4-Y=US#Q`JU0D{ia@_SEn=xqGKmHDq;wY#3|^tR}JEY#kwFO4@)
zgvOAzN1mYa3W(RcW1~8Y@|jaEYl`x*zUqLh_<coRA9K$#N%%gT9%ry7C9voy0DT}~
zK6k*ZG*-Kqha6sOa9|+)^}|yYrm0>-@u>H<Z*JcO9W8zc0n|n(2mwA?GxciB>Z+C5
zd^XTk(}!E$GZSjt+jd`lQ4N+aHni&~I8Mut$!|e!#n|aH?fnO~_|Ny<Vh#<Res(Gr
zh>yml?&SVd@5ZTE2=tPQBqsgE_5PPSAbW<|c1385b7Jzw1b#`L|KZ{M|1Hx0!7YAI
z|9_L&{a;;uJN;0`1Vx3N{7TlvM>dKbkc`}}nFt`k1eAI~J7L7niac1S=aRVbjconb
zhd(n4&VjWgNk|0IW0lSp9C9z~Tgz7afsA6S;nukm9PBWvRiK*`UFl5Ln;;k!LU<#0
z;ZPXp)`Z=T5}Tas@~C5TrfA(o1sE~THogx6<-a^?48-wLTK2{ITcnxii^~f$?~Mt7
zT^jQReVxpi*%{l=mmskm?MnN6XY5mNv+$kW;S0+%#1E>0{Tv*duCu)WY`9C&?NA>?
zB&Uuh3oSJ;a%x!1Zx22(J&W@ikn}fxCIgE8UIq~BOj#>Tww_lLbhFa^Rq3>3EE!vZ
z?oz<(Bu%963bV6*wsfO6+?QeF-9k+Wth2cNHHloHRP7Fkq0SMzdm{k3#1?RxaQPV>
zTA2Z;cMA&@l<(FFoqv8I)l0zae#_U_LMg{4JNM)#4@g_g@qx>)(m_F97sAQ?$l%sS
zulHLl#e_mh-W-2;Ltp}C+p)ZGR}D^>rh%2<9l?M#j-5bu>PAfvn!y7BgBjSDDHQNw
zQ4Gtg0xpmG?8=34zgZ}`Ns7*2O9#tP$L3w2hX#)j)A4<N?%L}$fdWt$w<yvLOP*hb
zE-yUFZWny3kUoM!ggws?Qp54BhowQ0BK)HQ5DK{Vuutv~weR!geQGevGi!d_1%V!D
zx+W}1zvf!>=QdE8ux01f%ESUAo_eK>PcY!XORIXo1x~XiqE!;30A5kOn+F0F>r%Q5
z)SYK+oouz%+KlCu7j@^th)20?RRapr=^y3UUD6HV4Vs)VfsY#D$w0K;{e!$1QeWS>
znZkj+bt&b2J*aoK`~Y=ndhg-4;C0@<Xt;P8llPqe_v`%LoquJpK|elYx6y72NGJmx
zIbo(zCtrylI|SL=wig6HJ*!1|gD$V|XJD_<5L+j^4m6xk5TT4zCHu;Ax(ofnIKXv;
zK+oE&QxGFA2K_aY#cGRv$E|VmU&6D{tQB?{j7jG%iql{xa5U+Lj0On#6s*$i!w_h+
zDswuiD&CF|AVoj;r=bMM6&d(1shkc^KCyMt#vrfu#$n%E>Dpt@$7ts&Q5t3Hl-n{N
z&~qJ5&*oM5R~Ap&3*1Y3G{@71ID0AM)L}E|Z0ir8Oe!SVS50*J$5hiu_p5f*&>9Eu
zP9|9IooOo9J`9<J$gs{Czuy7ix;L$GRc;dBsd_N}B+c<XpwYAzF3NsPdIOg<{ou%T
z(oBd?;Z12!mlg)(-ySrib|`7Uh?&Yjbv-NZq5uvS`5eaGD|X<9ivR^|%U(pRiY{!T
zY*={-z^$vs8kl9<UnqoeC!Yl1wWgUH<iEoA{1+<sF^?6%iX0IDvJLh-kgSfNi?Nf0
zg0^EA>>DEOz}hf21Nt3G=Ku=KU-i=OL4U95za*rH*yF6_A-w_3s)fkF$=AGIG~7@v
zIQjb{)EXy2al#w2?-pCOETY^+<h8r|=N_?4A9n=R<H*dyP(~eShwes_eUbvdZl}a(
zc@I_tPliKvj1&YKPKX4R>DDFH7l2lEC@H}5A1HfNE<8TCJo#2Nc-c>H8nm*TD}zvM
zb?6Z&iNvr@x%oheC0d)ymFYpCDTapBEv*AtNp{ol{wlV2!f0;flWD-**e)RR=~Jyn
zU4Zmi)E37_6MzvfTL=ZL@$m=Udk0_|SczZ4=7M&?bWsNLD4X(t+OF47r?3O-Pdhyn
zNtlkPo}7H0^(+mK=*)WNcU@}I5Kul16gSrLcv6ymu+^EobGv}+#Jr;#v|3GpDP-XK
zI^`Joj6l_t1jl^-^g~a~(oVTN&^$d4(cQ@D=E0@8o)cro*$&hw)8M5pN^O5l9J896
zcjFPZStT01oEbQ5YR`XiOCju=Cutfqv=uo9m9LLBXN7nB52ZO;;#lJz=(wg#_|TpE
zecwGr*Vw)xS0K(P2tesE6^tHK^HU2M^dbS^sn%gto^Req!CL?uymWhxImKt+xP>ng
z04Adtpx-P^v;%Vmk!(5JJ<HAZtstl*eW2=>_bz!$3y)E%?9&5Ac3Bl)iDeyl1VpCh
zQV;uVo6<b<QopH-tSxUr6S1bWa&`~e^Ad2nm^<vg=p}jF0_t_d^Tf@v^eS*d1hL9S
zFBO*KJi3?r1kf=+o}9mN7g#_p`&f45#{sve;sZ-W%QnN2UMwvVIX~IwHJI!a;Hi@>
z(P<YrWoT9CaHq<@5v;3p&?0>b`B8_pJac2hvQjvZ-UKf^|C$TecXK;if@Rw)I)Ea%
zUR3=EsJqcM`DmsKdIQF0s5(}L85Thkg}mI$6J;5_SFO__2n6x+`+Qkh+2k$dc=->1
z?z`jwygpuyT0@_eQk9H>RC3qr%<8-pu)ugU-M$9&WsC+J(`+<`eo<)acM$N=+uf^}
za|%mY$^Ag5os)<@xJb=Z^&ULh0o35-FCtZwyU1{P+SbZj%GX-!4P4!MHs-_Kg{I&*
zy=4&B11ypa-q-DNn6BUCdEDTAm;MyW^5wuI^=Acn1j`>D-9knf#cEqW)j386-EJ97
zaNV9#zg_7;a~;8Ysq<-*w>E6w>Q-fjcnCT<;c>Tha(d0+c3w5IUY|}|E+A;d_$33!
zzBgg=_tVP`@`>3+A2i5Zk9^WehO5&awu2r;wPJq7Ju57}eVB+|@n%gmvzu^wi%rqX
zdKwTh&_mopINo&3X>)dFSfV^V#iP<n+`nH4_y(dV(U$f>pU@lAW{IOcXjafLaKPzx
zdSzgv|JVn!F1{i~S(z)X^V7!RHQxb3Z+Pk4bKxDf(Mn!^;LLUeNyGNuEY#Vruph5y
zlvY7@X@T0j2`cW>ok1cC5E3#k^k1>}LiZ{4vD)3%CRm+tIqL1TjC{7jJ@JvP=UTWW
zMw3iUO$qfAWnVX<dG$(h#71%o8Q6ZtSu9|1TMnAnf(cOU20qCwKVN8=q~RwPqtAB_
zH3OxkrkF(Dufld{90YOYh>j>9HS7-70p>Rlm?+7xl*V{@1X91Fq}@&tWIaxvUav;1
z20c@zbB&TaTY9UyLbQ!Ug9<2q{2`uHW4j@cT7+gq@*opm2URw1jd~^WNv1nOKCvT6
z=L^tzm6zIX#63nqfz9Z+IBM*Lu&L<Y+F1s0oYjew$MSshPv<1Esv|aVL2Ns?cXcBk
zAH?EaxVxQ)4J>i^{HZeHHWh+8im?_6C)1>Lk@_d=68A(urGmIvb~cvxL?+;M)=cDW
z^kwuptuyHLn}w)l+O{cWcp-BT5-42wNpjIn-Q`IGbXqmHz`@?Cs~^{LV%$mGVZklQ
z(->O8(KPLnv7*cJrpRa7;VC+6hQ-}R&7k6yT6gd*<FrvE+lyze$2@gk(}Tv^>q*H4
zPkHb#0F(Sl&ZRAk7U0)i=D9K)&F;Mc|5Kvvq_aVYQkYo?k+hC*eN6A0l{+N-3-vE2
zwkXd5n0aQ@bFNjKx|_-YQ<+|Se&L&a9Lz1<XouCB)!Q~}0sQRW@M>CW)ea6;2pXwf
z$iHfHRPf@05URI_3>d81yF~#eiuyQgSsmqgKlm+HlcVm|s>NL$&@c6o>fNj6@dwqQ
zn3eY6NEBF|k29dU&aJVhznR2ig%42;dpMLNR;ShsOztgDWpY-1>Y>uW7$082c~*CI
zYL7s)Jh6bE;anZX(2N5s;?G?WxH`2XH-HKK_Y1O@`}Yq1{|M1nEN+oxZroZW1xfG+
PDjdd!W{2}&=db@8kqN{5

literal 0
HcmV?d00001

diff --git a/test/image/mocks/hist_multi.json b/test/image/mocks/hist_multi.json
new file mode 100644
index 00000000000..05ef17e5ad3
--- /dev/null
+++ b/test/image/mocks/hist_multi.json
@@ -0,0 +1,22 @@
+{
+    "data":[{
+        "x": [1, 1, 1, 2, 2],
+        "type": "histogram"
+    }, {
+        "x": [20, 20, 21, 21],
+        "type": "histogram"
+    }, {
+        "x": [1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7],
+        "type": "histogram",
+        "xaxis": "x2"
+    }, {
+        "x": [6, 6.1, 6.2],
+        "type": "histogram",
+        "xaxis": "x2"
+    }],
+    "layout": {
+        "height": 400, "width": 500,
+        "barmode": "stack",
+        "grid": {"rows": 1, "columns": 2}
+    }
+}