diff --git a/doc/users/next_whats_new/histogram_vectorized_parameters.rst b/doc/users/next_whats_new/histogram_vectorized_parameters.rst new file mode 100644 index 000000000000..7b9c04e71739 --- /dev/null +++ b/doc/users/next_whats_new/histogram_vectorized_parameters.rst @@ -0,0 +1,46 @@ +Vectorized ``hist`` style parameters +------------------------------------ + +The parameters *hatch*, *edgecolor*, *facecolor*, *linewidth* and *linestyle* +of the `~matplotlib.axes.Axes.hist` method are now vectorized. +This means that you can pass in individual parameters for each histogram +when the input *x* has multiple datasets. + + +.. plot:: + :include-source: true + :alt: Four charts, each displaying stacked histograms of three Poisson distributions. Each chart differentiates the histograms using various parameters: top left uses different linewidths, top right uses different hatches, bottom left uses different edgecolors, and bottom right uses different facecolors. Each histogram on the left side also has a different edgecolor. + + import matplotlib.pyplot as plt + import numpy as np + np.random.seed(19680801) + + fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(9, 9)) + + data1 = np.random.poisson(5, 1000) + data2 = np.random.poisson(7, 1000) + data3 = np.random.poisson(10, 1000) + + labels = ["Data 1", "Data 2", "Data 3"] + + ax1.hist([data1, data2, data3], bins=range(17), histtype="step", stacked=True, + edgecolor=["red", "green", "blue"], linewidth=[1, 2, 3]) + ax1.set_title("Different linewidths") + ax1.legend(labels) + + ax2.hist([data1, data2, data3], bins=range(17), histtype="barstacked", + hatch=["/", ".", "*"]) + ax2.set_title("Different hatch patterns") + ax2.legend(labels) + + ax3.hist([data1, data2, data3], bins=range(17), histtype="bar", fill=False, + edgecolor=["red", "green", "blue"], linestyle=["--", "-.", ":"]) + ax3.set_title("Different linestyles") + ax3.legend(labels) + + ax4.hist([data1, data2, data3], bins=range(17), histtype="barstacked", + facecolor=["red", "green", "blue"]) + ax4.set_title("Different facecolors") + ax4.legend(labels) + + plt.show() diff --git a/galleries/examples/statistics/histogram_multihist.py b/galleries/examples/statistics/histogram_multihist.py index f1957dc38939..b9a9c5f0bf26 100644 --- a/galleries/examples/statistics/histogram_multihist.py +++ b/galleries/examples/statistics/histogram_multihist.py @@ -15,7 +15,7 @@ select these parameters: http://docs.astropy.org/en/stable/visualization/histogram.html """ - +# %% import matplotlib.pyplot as plt import numpy as np @@ -45,6 +45,94 @@ fig.tight_layout() plt.show() +# %% +# ----------------------------------- +# Setting properties for each dataset +# ----------------------------------- +# +# You can style the histograms individually by passing a list of values to the +# following parameters: +# +# * edgecolor +# * facecolor +# * hatch +# * linewidth +# * linestyle +# +# +# edgecolor +# ......... + +fig, ax = plt.subplots() + +edgecolors = ['green', 'red', 'blue'] + +ax.hist(x, n_bins, fill=False, histtype="step", stacked=True, + edgecolor=edgecolors, label=edgecolors) +ax.legend() +ax.set_title('Stacked Steps with Edgecolors') + +plt.show() + +# %% +# facecolor +# ......... + +fig, ax = plt.subplots() + +facecolors = ['green', 'red', 'blue'] + +ax.hist(x, n_bins, histtype="barstacked", facecolor=facecolors, label=facecolors) +ax.legend() +ax.set_title("Bars with different Facecolors") + +plt.show() + +# %% +# hatch +# ..... + +fig, ax = plt.subplots() + +hatches = [".", "o", "x"] + +ax.hist(x, n_bins, histtype="barstacked", hatch=hatches, label=hatches) +ax.legend() +ax.set_title("Hatches on Stacked Bars") + +plt.show() + +# %% +# linewidth +# ......... + +fig, ax = plt.subplots() + +linewidths = [1, 2, 3] +edgecolors = ["green", "red", "blue"] + +ax.hist(x, n_bins, fill=False, histtype="bar", linewidth=linewidths, + edgecolor=edgecolors, label=linewidths) +ax.legend() +ax.set_title("Bars with Linewidths") + +plt.show() + +# %% +# linestyle +# ......... + +fig, ax = plt.subplots() + +linestyles = ['-', ':', '--'] + +ax.hist(x, n_bins, fill=False, histtype='bar', linestyle=linestyles, + edgecolor=edgecolors, label=linestyles) +ax.legend() +ax.set_title('Bars with Linestyles') + +plt.show() + # %% # # .. admonition:: References diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 52c99b125d36..07393b1028d2 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6937,7 +6937,13 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, DATA_PARAMETER_PLACEHOLDER **kwargs - `~matplotlib.patches.Patch` properties + `~matplotlib.patches.Patch` properties. The following properties + additionally accept a sequence of values corresponding to the + datasets in *x*: + *edgecolors*, *facecolors*, *lines*, *linestyles*, *hatches*. + + .. versionadded:: 3.10 + Allowing sequences of values in above listed Patch properties. See Also -------- @@ -7210,15 +7216,35 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, # If None, make all labels None (via zip_longest below); otherwise, # cast each element to str, but keep a single str as it. labels = [] if label is None else np.atleast_1d(np.asarray(label, str)) + + if histtype == "step": + edgecolors = itertools.cycle(np.atleast_1d(kwargs.get('edgecolor', + colors))) + else: + edgecolors = itertools.cycle(np.atleast_1d(kwargs.get("edgecolor", None))) + + facecolors = itertools.cycle(np.atleast_1d(kwargs.get('facecolor', colors))) + hatches = itertools.cycle(np.atleast_1d(kwargs.get('hatch', None))) + linewidths = itertools.cycle(np.atleast_1d(kwargs.get('linewidth', None))) + linestyles = itertools.cycle(np.atleast_1d(kwargs.get('linestyle', None))) + for patch, lbl in itertools.zip_longest(patches, labels): - if patch: - p = patch[0] + if not patch: + continue + p = patch[0] + kwargs.update({ + 'hatch': next(hatches), + 'linewidth': next(linewidths), + 'linestyle': next(linestyles), + 'edgecolor': next(edgecolors), + 'facecolor': next(facecolors), + }) + p._internal_update(kwargs) + if lbl is not None: + p.set_label(lbl) + for p in patch[1:]: p._internal_update(kwargs) - if lbl is not None: - p.set_label(lbl) - for p in patch[1:]: - p._internal_update(kwargs) - p.set_label('_nolegend_') + p.set_label('_nolegend_') if nx == 1: return tops[0], bins, patches[0] diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index dd37d3d8ee80..b1f97b3f855f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4603,6 +4603,64 @@ def test_hist_stacked_bar(): ax.legend(loc='upper right', bbox_to_anchor=(1.0, 1.0), ncols=1) +@pytest.mark.parametrize('kwargs', ({'facecolor': ["b", "g", "r"]}, + {'edgecolor': ["b", "g", "r"]}, + {'hatch': ["/", "\\", "."]}, + {'linestyle': ["-", "--", ":"]}, + {'linewidth': [1, 1.5, 2]}, + {'color': ["b", "g", "r"]})) +@check_figures_equal(extensions=["png"]) +def test_hist_vectorized_params(fig_test, fig_ref, kwargs): + np.random.seed(19680801) + xs = [np.random.randn(n) for n in [20, 50, 100]] + + (axt1, axt2) = fig_test.subplots(2) + (axr1, axr2) = fig_ref.subplots(2) + + for histtype, axt, axr in [("stepfilled", axt1, axr1), ("step", axt2, axr2)]: + _, bins, _ = axt.hist(xs, bins=10, histtype=histtype, **kwargs) + + kw, values = next(iter(kwargs.items())) + for i, (x, value) in enumerate(zip(xs, values)): + axr.hist(x, bins=bins, histtype=histtype, **{kw: value}, + zorder=(len(xs)-i)/2) + + +@pytest.mark.parametrize('kwargs, patch_face, patch_edge', + # 'C0'(blue) stands for the first color of the + # default color cycle as well as the patch.facecolor rcParam + # When the expected edgecolor is 'k'(black), + # it corresponds to the patch.edgecolor rcParam + [({'histtype': 'stepfilled', 'color': 'r', + 'facecolor': 'y', 'edgecolor': 'g'}, 'y', 'g'), + ({'histtype': 'step', 'color': 'r', + 'facecolor': 'y', 'edgecolor': 'g'}, ('y', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r', + 'edgecolor': 'g'}, 'r', 'g'), + ({'histtype': 'step', 'color': 'r', + 'edgecolor': 'g'}, ('r', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r', + 'facecolor': 'y'}, 'y', 'k'), + ({'histtype': 'step', 'color': 'r', + 'facecolor': 'y'}, ('y', 0), 'r'), + ({'histtype': 'stepfilled', + 'facecolor': 'y', 'edgecolor': 'g'}, 'y', 'g'), + ({'histtype': 'step', 'facecolor': 'y', + 'edgecolor': 'g'}, ('y', 0), 'g'), + ({'histtype': 'stepfilled', 'color': 'r'}, 'r', 'k'), + ({'histtype': 'step', 'color': 'r'}, ('r', 0), 'r'), + ({'histtype': 'stepfilled', 'facecolor': 'y'}, 'y', 'k'), + ({'histtype': 'step', 'facecolor': 'y'}, ('y', 0), 'C0'), + ({'histtype': 'stepfilled', 'edgecolor': 'g'}, 'C0', 'g'), + ({'histtype': 'step', 'edgecolor': 'g'}, ('C0', 0), 'g'), + ({'histtype': 'stepfilled'}, 'C0', 'k'), + ({'histtype': 'step'}, ('C0', 0), 'C0')]) +def test_hist_color_semantics(kwargs, patch_face, patch_edge): + _, _, patches = plt.figure().subplots().hist([1, 2, 3], **kwargs) + assert all(mcolors.same_color([p.get_facecolor(), p.get_edgecolor()], + [patch_face, patch_edge]) for p in patches) + + def test_hist_barstacked_bottom_unchanged(): b = np.array([10, 20]) plt.hist([[0, 1], [0, 1]], 2, histtype="barstacked", bottom=b) @@ -4614,6 +4672,15 @@ def test_hist_emptydata(): ax.hist([[], range(10), range(10)], histtype="step") +def test_hist_unused_labels(): + # When a list with one dataset and N elements is provided and N labels, ensure + # that the first label is used for the dataset and all other labels are ignored + fig, ax = plt.subplots() + ax.hist([[1, 2, 3]], label=["values", "unused", "also unused"]) + _, labels = ax.get_legend_handles_labels() + assert labels == ["values"] + + def test_hist_labels(): # test singleton labels OK fig, ax = plt.subplots()