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 hatch pattern support to Axes.grouped_bar#30726

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

Open
ilakkmanoharan wants to merge17 commits intomatplotlib:main
base:main
Choose a base branch
Loading
fromilakkmanoharan:enh/grouped-bar-hatch-broadcast-v2
Open
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
17 commits
Select commitHold shift + click to select a range
cd9ddcd
Enhance grouped_bar: refine hatch handling, type hints, and tests
ilakkmanoharanNov 4, 2025
6e3a7c8
Sync pyplot boilerplate after grouped_bar signature change
ilakkmanoharanNov 4, 2025
dc94475
Sync pyplot boilerplate after grouped_bar signature change
ilakkmanoharanNov 4, 2025
b402730
Remove ValueError docstring note, drop TODOs
ilakkmanoharanNov 5, 2025
90f0a22
Trigger CI rerun: retry Windows backend timeouts
ilakkmanoharanNov 5, 2025
0fad966
Trigger CI rerun: retry WebAgg timeout
ilakkmanoharanNov 5, 2025
1eb63f9
Trigger CI rerun: retry backend timeout
ilakkmanoharanNov 6, 2025
960914c
Trigger CI rerun: retry TkAgg timeout
ilakkmanoharanNov 6, 2025
adf2717
Update lib/matplotlib/axes/_axes.py
ilakkmanoharanNov 7, 2025
a8c8705
Allow None entries in hatch list for grouped_bar()
ilakkmanoharanNov 7, 2025
d2f1943
CI: rerun tests
ilakkmanoharanNov 7, 2025
afb64cb
Docstring: clarify 'hatch' parameter type as sequence of :mpltype: or…
ilakkmanoharanNov 7, 2025
485ef67
Update lib/matplotlib/axes/_axes.py
ilakkmanoharanNov 7, 2025
9fffd16
Clean up API change entry for grouped_bar hatch behavior
ilakkmanoharanNov 7, 2025
af21213
Fix hatch validation: disallow empty lists
ilakkmanoharanNov 17, 2025
541bf68
Trigger CI/CD
ilakkmanoharanNov 17, 2025
324d578
Update lib/matplotlib/axes/_axes.py
ilakkmanoharanNov 21, 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
53 changes: 49 additions & 4 deletionslib/matplotlib/axes/_axes.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3050,7 +3050,7 @@
@_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,15 @@

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

hatch : sequence of :mpltype:`hatch` 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.
- If the sequence contains a single element (e.g., ``['//']``),
the same pattern is repeated for all datasets.

**kwargs : `.Rectangle` properties

%(Rectangle:kwdoc)s
Expand DownExpand Up@@ -3318,6 +3327,38 @@
# 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])

elif isinstance(hatch, str):
raise ValueError("'hatch' must be a sequence of strings "
"(e.g., ['//']) or None; "
"a single string like '//' is not allowed."
)

else:
try:
hatch_list = list(hatch)
except TypeError:
raise ValueError("'hatch' must be a sequence of strings"
"(e.g., ['//']) or None") from None

if not hatch_list:
# Empty sequence is invalid → raise instead of treating as no hatch.
raise ValueError(
"'hatch' must be a non-empty sequence of strings or None; "
"use hatch=None for no hatching."
)

elif not all(h is None or isinstance(h, str) for h in hatch_list):
raise TypeError("All entries in 'hatch' must be strings or None")

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

Comment on lines +3332 to +3361
Copy link
Member

Choose a reason for hiding this comment

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

can you condense the whitespace please?

ilakkmanoharan reacted with thumbs up emoji
Copy link
Member

Choose a reason for hiding this comment

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

Also, why not just follow the pattern in hist and allow a string (since we're allowing 1d list) and therefore simplify the error checking - basically bar handles the hatch validation instead of grouped bar.

https://github.com/matplotlib/matplotlib/blob/dedfe9be48ad82cade86766ef89410844ff09b31/lib/matplotlib/axes/_axes.py#L7560C8-L7560C76

ilakkmanoharan reacted with thumbs up emoji

Choose a reason for hiding this comment

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

When hatch is given as a single string (e.g. "//"), we raise a ValueError. This prevents Matplotlib from incorrectly iterating over individual characters ('/', '/') instead of treating the hatch as a single pattern.

Choose a reason for hiding this comment

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

@story645@timhoffm

https://github.com/matplotlib/matplotlib/blob/dedfe9be48ad82cade86766ef89410844ff09b31/lib/matplotlib/axes/_axes.py#L7560C8-L7560C76 --->>>>>> the implementation cycles hatch patterns per patch, which breaks grouped bar semantics because each dataset generates multiple patches. This causes the hatch sequence to repeat incorrectly within the same dataset.
I am wondering if we need to replace the patch-level cycling with a dataset-level normalization instead? Specifically:
determine the number of datasets (num_datasets = len(heights)),
normalize hatch to one pattern per dataset, and
apply that hatch consistently to all patches for that dataset.
This would align hatch behavior with how color and label are already broadcast in grouped_bar, and it also resolves the test failures caused by per-patch cycling.

Copy link
Member

Choose a reason for hiding this comment

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

Specifically:
determine the number of datasets (num_datasets = len(heights)),
normalize hatch to one pattern per dataset, and
apply that hatch consistently to all patches for that dataset.

Sounds good to me, I think would also then allow for easier expansion into nesting (hatch per patch per dataset) if we wanted to go that direction.

ilakkmanoharan reacted with thumbs up emoji

Choose a reason for hiding this comment

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

[MNT]: Discussion : Normalize hatch patterns per dataset instead of per patch in grouped_bar#30789

I’ve created a new issue to address the hatch-normalization behavior in grouped_bar (i.e., switching from per-patch hatch cycling to per-dataset hatch assignment). This allows us to track that behavioral change separately from PR#30726, which is already large and focused on adding hatch support. Splitting this out avoids scope creep, keeps the current PR reviewable, and ensures the semantic change to hatch handling receives dedicated discussion and review.

@timhoffm@story645

Copy link
Member

@story645story645Nov 25, 2025
edited
Loading

Choose a reason for hiding this comment

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

@ilakkmanoharan I closed that issue b/c that behavioral discussion is necessary in this PR to move this PR forward - it's not scope creep b/c it's inherent to how you're handling the hatch argument and which errors to throw. If the current behavior of grouped hatch is one color per dataset, than the behavior of hatch should also be one color per dataset. This is also consistent with stackplot. Also, that's the behavior you've documented:

Image

Until you get better intuition for what would make a good issue, I strongly recommend you ask maintainers if a discussion warrants a stand alone issue before opening a new one.

bar_width = (group_distance /
(num_datasets + (num_datasets - 1) * bar_spacing + group_spacing))
bar_spacing_abs = bar_spacing * bar_width
Expand All@@ -3331,15 +3372,19 @@
# 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)):


Check warning on line 3376 in lib/matplotlib/axes/_axes.py

View workflow job for this annotation

GitHub Actions/ ruff

[rdjson] reported by reviewdog 🐶Blank line contains whitespaceRaw Output:message:"Blank line contains whitespace" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/axes/_axes.py" range:{start:{line:3376 column:1} end:{line:3376 column:8}}} severity:WARNING source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"W293" url:"https://docs.astral.sh/ruff/rules/blank-line-with-whitespace"} suggestions:{range:{start:{line:3376 column:1} end:{line:3376 column:8}}}
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))
if orientation == "vertical":
bc = self.bar(lefts, hs, width=bar_width, align="edge",
label=label, color=color, **kwargs)
label=label, color=color,hatch=hatch_pattern,**kwargs)
else:
bc = self.barh(lefts, hs, height=bar_width, align="edge",
label=label, color=color, **kwargs)
label=label, color=color,hatch=hatch_pattern,**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
115 changes: 115 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,121 @@ 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():
"""
Empty strings or None in the hatch list should result in no hatch
for the corresponding dataset, while valid strings should apply
the hatch pattern normally.
"""
fig, ax = plt.subplots()
x = np.arange(3)
heights = [np.array([1, 2, 3]), np.array([2, 1, 2]), np.array([3, 2, 1])]
hatches = ["", "xx", None]
containers = ax.grouped_bar(heights, positions=x, hatch=hatches)
# Collect the hatch pattern for each bar in each dataset
counts = [[rect.get_hatch() for rect in bc] for bc in containers.bar_containers]
# First dataset: empty string disables hatch
assert all(h in ("", None) for h in counts[0])
# Second dataset: hatch pattern applied
assert all(h == "xx" for h in counts[1])
# Third dataset: None disables hatch
assert all(h in ("", None) for h in counts[2])


def test_grouped_bar_empty_hatch_sequence_raises():
"""An empty hatch sequence 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 non-empty sequence of strings or None"
):
ax.grouped_bar(heights, hatch=[])


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