Skip to content

Add support for multiple hatches, edgecolors and linewidths in histograms #28073

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 20, 2024
46 changes: 46 additions & 0 deletions doc/users/next_whats_new/histogram_vectorized_parameters.rst
Original file line number Diff line number Diff line change
@@ -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()
90 changes: 89 additions & 1 deletion galleries/examples/statistics/histogram_multihist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
42 changes: 34 additions & 8 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------
Expand Down Expand Up @@ -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]
Expand Down
67 changes: 67 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down
Loading