Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

ENH: Add broadcasted hatch to grouped_bar#30683

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
19 commits
Select commitHold shift + click to select a range
787375a
grouped_bar supports broadcasted hatch (one hatch per dataset)
ilakkmanoharanOct 23, 2025
c0d13d1
ENH: Add broadcasted hatch support to grouped_bar (final style fixes)
ilakkmanoharanOct 25, 2025
3ceee0c
MAINT: regenerate pyplot.py after adding hatch to grouped_bar
ilakkmanoharanOct 25, 2025
38deb93
Stub: include 'hatch' parameter in Axes.grouped_bar to align with run…
ilakkmanoharanOct 29, 2025
40bac2e
Stub: include 'hatch' parameter in Axes.grouped_bar to align with run…
ilakkmanoharanOct 29, 2025
f9cf38f
Stub: re-add 'hatch' parameter to Axes.grouped_bar for consistency wi…
ilakkmanoharanOct 29, 2025
0e8b15e
DOC: behavior note for grouped_bar hatch broadcasting (30683-IM)
ilakkmanoharanOct 29, 2025
2ca419f
Sync pyplot.py via tools/boilerplate.py after grouped_bar hatch change
ilakkmanoharanOct 30, 2025
91843e4
Docstring: clarify that an empty string disables hatching in Axes.gro…
ilakkmanoharanOct 30, 2025
1fc2493
Tests: add grouped_bar edge cases (empty hatch, dict+labels, non-equi…
ilakkmanoharanOct 30, 2025
3bf57a7
Style: revert lefts assignment to compact upstream format
ilakkmanoharanOct 31, 2025
42a134d
Fix: restore correct bar alignment in grouped_bar (revert accidental …
ilakkmanoharanOct 31, 2025
7f7779e
Update doc/api/next_api_changes/behavior/30683-IM.rst
ilakkmanoharanNov 1, 2025
dc499a1
ENH: align hatch broadcasting with color handling in Axes.grouped_bar
ilakkmanoharanNov 1, 2025
4b255c2
CI: refresh branch to clear stale file diff for test_grouped_bar_hatc…
ilakkmanoharanNov 1, 2025
7a314e4
Update lib/matplotlib/tests/test_axes.py
ilakkmanoharanNov 2, 2025
47c575b
Enhance grouped_bar: refine hatch handling, type hints, and tests
ilakkmanoharanNov 4, 2025
a935895
Sync pyplot boilerplate after grouped_bar signature change
ilakkmanoharanNov 4, 2025
5d8f84c
Refactor grouped_bar orientation logic with common_kwargs and remove …
ilakkmanoharanNov 4, 2025
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
32 changes: 32 additions & 0 deletionsdoc/api/next_api_changes/behavior/30683-IM.rst
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
grouped_bar hatch patterns
~~~~~~~~~~~~~~~~~~~~~~~~~~~

`.Axes.grouped_bar` now accepts a list of strings describing hatch patterns,
which are applied sequentially to the datasets, cycling if fewer patterns are
provided—similar to how colors are handled.

.. plot::

import matplotlib.pyplot as plt
import numpy as np

fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8, 4))

x = np.arange(3)
heights = [
[1, 2, 3],
[2, 1, 2],
[3, 2, 1],
]

ax1.grouped_bar(heights, tick_labels=["A", "B", "C"], hatch="/")
ax2.grouped_bar(heights, tick_labels=["A", "B", "C"], hatch=["/", "\\\\", ".."])

ax1.set_title("hatch='/'")
ax2.set_title("hatch=['/', '\\\\', '..']")

plt.show()

The first plot applies the same hatch to all bars.
The second plot uses a different hatch for each dataset, cycling automatically
if the list of hatches is shorter than the number of datasets.
66 changes: 60 additions & 6 deletionslib/matplotlib/axes/_axes.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3050,7 +3050,7 @@ def broken_barh(self, xranges, yrange, align="bottom", **kwargs):
@_docstring.interpd
def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing=0,
tick_labels=None, labels=None, orientation="vertical", colors=None,
**kwargs):
hatch=None,**kwargs):
"""
Make a grouped bar plot.

Expand DownExpand Up@@ -3190,6 +3190,19 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing

If not specified, the colors from the Axes property cycle will be used.

hatch : sequence of str or None, optional
Hatch pattern(s) to apply per dataset.

- If ``None`` (default), no hatching is applied.
- If a sequence of strings is provided (e.g., ``['//', 'xx', '..']``),
the patterns are cycled across datasets.
- Single string values (e.g., ``'//'``) are **not supported**.

Raises
------
ValueError
If ``hatch`` is a single string or a non-iterable value.

**kwargs : `.Rectangle` properties

%(Rectangle:kwdoc)s
Expand DownExpand Up@@ -3318,6 +3331,36 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing
# TODO: do we want to be more restrictive and check lengths?
colors = itertools.cycle(colors)

if hatch is None:
# No hatch specified: disable hatching entirely by cycling [None].
hatches = itertools.cycle([None])

# TODO: Discussion —
# Should grouped_bar() apply a default hatch pattern (e.g., '//')
# when none is provided ?

elif isinstance(hatch, str) or not hasattr(hatch, "__iter__"):
# Single strings or non-iterable values are not supported here.
# Explicit sequences of hatch patterns are required, ensuring
# predictable one-to-one mapping between datasets and hatches.
raise ValueError(
"'hatch' must be a sequence of strings with one entry per dataset"
)

else:
# Sequence of hatch patterns: cycle through them as needed.
# Example: hatch=['//', 'xx', '..'] → patterns repeat across datasets.
hatches = itertools.cycle(hatch)
Copy link
Member

Choose a reason for hiding this comment

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

If we have enforced the length of the list then I think we do not needitertools.cycle. But why do we enforce the length for hatches when we didn’t for colors?

Copy link
Member

Choose a reason for hiding this comment

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

There are good arguments for and against enforcing length. Since we currently do not do this in most parts of the library, let's stick to that approach for now. Enforcing length is a separate discussion.

Choose a reason for hiding this comment

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

Thank you,@rcomer and@timhoffm, for the helpful comments and feedback! I’ll review them and make the necessary updates.

Choose a reason for hiding this comment

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

@rcomer@timhoffm

If you confirm that we can allow cycling for this PR, I’ll go ahead and make the required changes.

I’ve also opened an issue to discuss this behavior more broadly and reach a consensus, since it could be a useful improvement across style arguments:

[MNT]: DISCUSSION: Should Axes.grouped_bar() enforce hatch list length or allow cycling like colors? (#30712)

Copy link
Member

Choose a reason for hiding this comment

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

@ilakkmanoharan for this PR, please allow cycling and do not enforce the list length.

Choose a reason for hiding this comment

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

@rcomer@timhoffm Okay, I will allow cycling and not enforce the list length for this PR


# TODO: Discussion —
# We may later introduce optional strict validation:
# if len(hatch) != num_datasets:
# raise ValueError(
# f"Expected {num_datasets} hatches, got {len(hatch)}"
# )
# This would enforce a strict 1:1 correspondence between
# datasets and provided hatches, preventing silent cycling.

bar_width = (group_distance /
(num_datasets + (num_datasets - 1) * bar_spacing + group_spacing))
bar_spacing_abs = bar_spacing * bar_width
Expand All@@ -3331,15 +3374,26 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing
# place the bars, but only use numerical positions, categorical tick labels
# are handled separately below
bar_containers = []
for i, (hs, label, color) in enumerate(zip(heights, labels, colors)):

# Both colors and hatches are cycled indefinitely using itertools.cycle.
# heights and labels, however, are finite (length = num_datasets).
# Because zip() stops at the shortest iterable, this loop executes exactly
# num_datasets times even though colors and hatches are infinite.
# This ensures one (color, hatch) pair per dataset
# without explicit length checks.
for i, (hs, label, color, hatch_pattern) in enumerate(
zip(heights, labels, colors, hatches)
):
lefts = (group_centers - 0.5 * group_distance + margin_abs
+ i * (bar_width + bar_spacing_abs))

common_kwargs = dict(
align="edge", label=label, color=color, hatch=hatch_pattern, **kwargs
)
if orientation == "vertical":
bc = self.bar(lefts, hs, width=bar_width, align="edge",
label=label, color=color, **kwargs)
bc = self.bar(lefts, hs, width=bar_width, **common_kwargs, **kwargs)
else:
bc = self.barh(lefts, hs, height=bar_width, align="edge",
label=label, color=color, **kwargs)
bc = self.barh(lefts, hs, height=bar_width, **common_kwargs, **kwargs)
bar_containers.append(bc)

if tick_labels is not None:
Expand Down
1 change: 1 addition & 0 deletionslib/matplotlib/axes/_axes.pyi
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -287,6 +287,7 @@ class Axes(_AxesBase):
bar_spacing: float | None = ...,
orientation: Literal["vertical", "horizontal"] = ...,
colors: Iterable[ColorType] | None = ...,
hatch: Iterable[str] | None = ...,
**kwargs
) -> list[BarContainer]: ...
def stem(
Expand Down
2 changes: 2 additions & 0 deletionslib/matplotlib/pyplot.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3536,6 +3536,7 @@ def grouped_bar(
labels: Sequence[str] | None = None,
orientation: Literal["vertical", "horizontal"] = "vertical",
colors: Iterable[ColorType] | None = None,
hatch: Iterable[str] | None = None,
**kwargs,
) -> list[BarContainer]:
return gca().grouped_bar(
Expand All@@ -3547,6 +3548,7 @@ def grouped_bar(
labels=labels,
orientation=orientation,
colors=colors,
hatch=hatch,
**kwargs,
)

Expand Down
95 changes: 95 additions & 0 deletionslib/matplotlib/tests/test_axes.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2267,6 +2267,101 @@ def test_grouped_bar_return_value():
assert bc not in ax.containers


def test_grouped_bar_single_hatch_str_raises():
"""Passing a single string for hatch should raise a ValueError."""
fig, ax = plt.subplots()
x = np.arange(3)
heights = [np.array([1, 2, 3]), np.array([2, 1, 2])]
with pytest.raises(ValueError, match="must be a sequence of strings"):
ax.grouped_bar(heights, positions=x, hatch='//')


def test_grouped_bar_hatch_non_iterable_raises():
"""Non-iterable hatch values should raise a ValueError."""
fig, ax = plt.subplots()
heights = [np.array([1, 2]), np.array([2, 3])]
with pytest.raises(ValueError, match="must be a sequence of strings"):
ax.grouped_bar(heights, hatch=123) # invalid non-iterable


def test_grouped_bar_hatch_sequence():
"""Each dataset should receive its own hatch pattern when a sequence is passed."""
fig, ax = plt.subplots()
x = np.arange(2)
heights = [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])]
hatches = ['//', 'xx', '..']
containers = ax.grouped_bar(heights, positions=x, hatch=hatches)

# Verify each dataset gets the corresponding hatch
for hatch, c in zip(hatches, containers.bar_containers):
for rect in c:
assert rect.get_hatch() == hatch


def test_grouped_bar_hatch_cycles_when_shorter_than_datasets():
"""When the hatch list is shorter than the number of datasets,
patterns should cycle.
"""

fig, ax = plt.subplots()
x = np.arange(2)
heights = [
np.array([1, 2]),
np.array([2, 3]),
np.array([3, 4]),
]
hatches = ['//', 'xx'] # shorter than number of datasets → should cycle
containers = ax.grouped_bar(heights, positions=x, hatch=hatches)

expected_hatches = ['//', 'xx', '//'] # cycle repeats
for gi, c in enumerate(containers.bar_containers):
for rect in c:
assert rect.get_hatch() == expected_hatches[gi]


def test_grouped_bar_hatch_none():
"""Passing hatch=None should result in bars with no hatch."""
fig, ax = plt.subplots()
x = np.arange(2)
heights = [np.array([1, 2]), np.array([2, 3])]
containers = ax.grouped_bar(heights, positions=x, hatch=None)

# All bars should have no hatch applied
for c in containers.bar_containers:
for rect in c:
assert rect.get_hatch() in (None, ''), \
f"Expected no hatch, got {rect.get_hatch()!r}"


def test_grouped_bar_empty_string_disables_hatch():
"""An empty string in the hatch list should result in no hatch for that dataset."""
fig, ax = plt.subplots()
x = np.arange(3)
heights = [np.array([1, 2, 3]), np.array([2, 1, 2])]
hatches = ["", "xx"]
containers = ax.grouped_bar(heights, positions=x, hatch=hatches)
counts = [[rect.get_hatch() for rect in bc] for bc in containers.bar_containers]
assert all(h == '' or h is None for h in counts[0]) # first dataset: no hatch
assert all(h == 'xx' for h in counts[1]) # second dataset: hatched


def test_grouped_bar_dict_with_labels_forbidden():
"""Passing labels along with dict input should raise an error."""
fig, ax = plt.subplots()
data = {"a": [1, 2], "b": [2, 1]}
with pytest.raises(ValueError, match="cannot be used if 'heights' is a mapping"):
ax.grouped_bar(data, labels=["x", "y"])


def test_grouped_bar_positions_not_equidistant():
"""Passing non-equidistant positions should raise an error."""
fig, ax = plt.subplots()
x = np.array([0, 1, 3])
heights = [np.array([1, 2, 3]), np.array([2, 1, 2])]
with pytest.raises(ValueError, match="must be equidistant"):
ax.grouped_bar(heights, positions=x)


def test_boxplot_dates_pandas(pd):
# smoke test for boxplot and dates in pandas
data = np.random.rand(5, 2)
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp