diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index ba9d507827a9..6edf7fb46e4f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -36,6 +36,30 @@ from matplotlib.axes._base import _AxesBase, _process_plot_format from matplotlib.axes._secondary_axes import SecondaryAxis +try: + from numpy.lib.histograms import histogram_bin_edges +except ImportError: + # this function is new in np 1.15 + def histogram_bin_edges(arr, bins, range=None, weights=None): + # this in True for 1D arrays, and False for None and str + if np.ndim(bins) == 1: + return bins + + if isinstance(bins, str): + # rather than backporting the internals, just do the full + # computation. If this is too slow for users, they can + # update numpy, or pick a manual number of bins + return np.histogram(arr, bins, range, weights)[1] + else: + if bins is None: + # hard-code numpy's default + bins = 10 + if range is None: + range = np.min(arr), np.max(arr) + + return np.linspace(*range, bins + 1) + + _log = logging.getLogger(__name__) @@ -6649,9 +6673,6 @@ def hist(self, x, bins=None, range=None, density=None, weights=None, if bin_range is not None: bin_range = self.convert_xunits(bin_range) - # Check whether bins or range are given explicitly. - binsgiven = np.iterable(bins) or bin_range is not None - # We need to do to 'weights' what was done to 'x' if weights is not None: w = cbook._reshape_2D(weights, 'weights') @@ -6676,22 +6697,42 @@ def hist(self, x, bins=None, range=None, density=None, weights=None, "sets and %d colors were provided" % (nx, len(color))) raise ValueError(error_message) - # If bins are not specified either explicitly or via range, - # we need to figure out the range required for all datasets, - # and supply that to np.histogram. - if not binsgiven and not input_empty: + hist_kwargs = dict() + + # if the bin_range is not given, compute without nan numpy + # does not do this for us when guessing the range (but will + # happily ignore nans when computing the histogram). + if bin_range is None: xmin = np.inf xmax = -np.inf for xi in x: - if len(xi) > 0: + if len(xi): + # python's min/max ignore nan, + # np.minnan returns nan for all nan input xmin = min(xmin, np.nanmin(xi)) xmax = max(xmax, np.nanmax(xi)) - bin_range = (xmin, xmax) + # make sure we have seen at least one non-nan and finite + # value before we reset the bin range + if not np.isnan([xmin, xmax]).any() and not (xmin > xmax): + bin_range = (xmin, xmax) + + # If bins are not specified either explicitly or via range, + # we need to figure out the range required for all datasets, + # and supply that to np.histogram. + if not input_empty and len(x) > 1: + if weights is not None: + _w = np.concatenate(w) + else: + _w = None + + bins = histogram_bin_edges(np.concatenate(x), + bins, bin_range, _w) + else: + hist_kwargs['range'] = bin_range + density = bool(density) or bool(normed) if density and not stacked: - hist_kwargs = dict(range=bin_range, density=density) - else: - hist_kwargs = dict(range=bin_range) + hist_kwargs = dict(density=density) # List to store all the top coordinates of the histograms tops = [] diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 12aef65beeaf..130a40557426 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6347,3 +6347,23 @@ def test_datetime_masked(): ax.plot(x, m) # these are the default viewlim assert ax.get_xlim() == (730120.0, 733773.0) + + +def test_hist_auto_bins(): + _, bins, _ = plt.hist([[1, 2, 3], [3, 4, 5, 6]], bins='auto') + assert bins[0] <= 1 + assert bins[-1] >= 6 + + +def test_hist_nan_data(): + fig, (ax1, ax2) = plt.subplots(2) + + data = [1, 2, 3] + nan_data = data + [np.nan] + + bins, edges, _ = ax1.hist(data) + with np.errstate(invalid='ignore'): + nanbins, nanedges, _ = ax2.hist(nan_data) + + assert np.allclose(bins, nanbins) + assert np.allclose(edges, nanedges)