8000 Add support for multiple hatches, edgecolors and linewidths in histograms by Impaler343 · Pull Request #28073 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

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
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions doc/users/next_whats_new/histogram_vectorized_parameters.rst
8000
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Vectorize ``hatch``, ``edgecolor``, ``facecolor``, ``linewidth`` and ``linestyle`` in *hist* methods
----------------------------------------------------------------------------------------------------

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 unique parameters for each histogram that is generated
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: ax1 uses different linewidths, ax2 uses different hatches, ax3 uses different edgecolors, and ax4 uses different facecolors. Each histogram in ax1 and ax3 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()
89 changes: 88 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,93 @@
fig.tight_layout()
plt.show()

# %%
# -----------------------------------
# Setting properties for each dataset
# -----------------------------------
#
# Plotting bar charts with datasets differentiated using:
#
# * edgecolors
# * facecolors
# * hatches
# * linewidths
# * linestyles
#
#
# Edge-Colors
# ...........................

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()

# %%
# Face-Colors
# ...........................

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()

# %%
# Hatches
# .......................

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()

# %%
# Linewidths
# ..........................

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()

# %%
# LineStyles
# ..........................

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
24 changes: 23 additions & 1 deletion lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6937,7 +6937,10 @@ 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*, *linewidths*, *linestyles*, *hatches*.

See Also
--------
Expand Down Expand Up @@ -7210,9 +7213,28 @@ 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]
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)
Expand Down
62 changes: 62 additions & 0 deletions lib/matplotlib/tests/test_axes.py
F438
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)
x = [np.random.randn(n) for n in [2000, 5000, 10000]]

(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(x, bins=10, histtype=histtype, **kwargs)

kw, values = next(iter(kwargs.items()))
for i, (xi, value) in enumerate(zip(x, values)):
axr.hist(xi, bins=bins, histtype=histtype, **{kw: value},
zorder=(len(x)-i)/2)


@pytest.mark.parametrize('kwargs, patch_face, patch_edge',
[({'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)
# '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
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,10 @@ def test_hist_emptydata():
ax.hist([[], range(10), range(10)], histtype="step")


def test_hist_none_patch():
plt.hist([1, 2], label=["First", "Second"])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
plt.hist([1, 2], label=["First", "Second"])
plt.hist([ ], label=["First", "Second"])

None patch is the empty list/array case

Copy link
Member
@story645 story645 Jun 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And what you probably want to test is

assert len(patches) == 0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This too works, basically the None condition is hit when we don't have enough datasets to cover each label. This is the doing of itertools.zip_longest

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should i still change it up? As the previous one seems clearer

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I thought that zip_longest would copy the patch to both labels. I think there needs to be some sort of assert that you're test is doing what you think it is - maybe then checking that len(patches) != len(labels)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is len(lbs) here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The number of labels that have been used in the plot

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant what were the actual numbers/does this test pass.

But yes if this test passes and coverage improves (add a comment that this test is to cover the if not patch case), then I'll let this concern go.

Copy link
Member
@story645 story645 Jul 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But also assert len(patches) < len(labels) too I think?

Copy link
Contributor Author
@Impaler343 Impaler343 Jul 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the tests pass, and yes we can add the length(patches) < len(labels) one as well, does the same comparison.
The actual numbers are 1 < 2



def test_hist_labels():
# test singleton labels OK
fig, ax = plt.subplots()
Expand Down
Loading
0