From cd9ddcd24145e96a2ff6f95abaf4efa908b2d7f3 Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Tue, 4 Nov 2025 10:01:07 -0600 Subject: [PATCH 01/17] Enhance grouped_bar: refine hatch handling, type hints, and tests --- lib/matplotlib/axes/_axes.py | 60 ++++++++++++++-- lib/matplotlib/axes/_axes.pyi | 1 + lib/matplotlib/tests/test_axes.py | 115 ++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 8548d16d43de..4eb65eb425aa 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -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. @@ -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 @@ -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) + + # 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 @@ -3331,15 +3374,24 @@ 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)) 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: diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 69d251aa21f7..d1b54f3fd263 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -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( diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 7307951595cb..84dc7bf37c19 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -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_hatch_mixed_orientation(): + """Ensure hatch works correctly for both vertical and horizontal orientations.""" + fig, (ax1, ax2) = plt.subplots(1, 2) + x = np.arange(3) + heights = [np.array([1, 2, 3]), np.array([2, 1, 2])] + hatches = ['//', 'xx'] + + containers_v = ax1.grouped_bar( + heights, positions=x, hatch=hatches, orientation="vertical") + containers_h = ax2.grouped_bar( + heights, positions=x, hatch=hatches, orientation="horizontal") + + for gi, (cv, ch) in enumerate( + zip(containers_v.bar_containers, containers_h.bar_containers)): + for rect in cv: + assert rect.get_hatch() == hatches[gi] + for rect in ch: + assert rect.get_hatch() == hatches[gi] + + +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) From 6e3a7c82d5b443cdcd2a3630e1c40fd147dd5f0a Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Tue, 4 Nov 2025 10:58:56 -0600 Subject: [PATCH 02/17] Sync pyplot boilerplate after grouped_bar signature change --- lib/matplotlib/pyplot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 25aa1a1b2821..c9889004a96a 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -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( From dc94475f934d9543f72b66f860a4c3ca1adc288e Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Tue, 4 Nov 2025 14:57:22 -0600 Subject: [PATCH 03/17] Sync pyplot boilerplate after grouped_bar signature change --- lib/matplotlib/axes/_axes.py | 27 +++++++++++++++++++-------- lib/matplotlib/pyplot.py | 1 + 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 4eb65eb425aa..a675d44d4511 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3196,6 +3196,8 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing - 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. - Single string values (e.g., ``'//'``) are **not supported**. Raises @@ -3334,23 +3336,32 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing 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" + 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 → treat as no hatch. + hatches = itertools.cycle([None]) + elif not all(isinstance(h, str) for h in hatch_list): + raise TypeError("All entries in 'hatch' must be strings") + else: # Sequence of hatch patterns: cycle through them as needed. # Example: hatch=['//', 'xx', '..'] → patterns repeat across datasets. - hatches = itertools.cycle(hatch) + hatches = itertools.cycle(hatch) # TODO: Discussion — # We may later introduce optional strict validation: diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index c9889004a96a..c78ef89fd998 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -3548,6 +3548,7 @@ def grouped_bar( labels=labels, orientation=orientation, colors=colors, + hatch=hatch, **kwargs, ) From b402730894fe6de2971947f4c1a74dc0eb90f09e Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Tue, 4 Nov 2025 20:06:48 -0600 Subject: [PATCH 04/17] Remove ValueError docstring note, drop TODOs --- lib/matplotlib/axes/_axes.py | 17 ----------------- lib/matplotlib/tests/test_axes.py | 20 -------------------- 2 files changed, 37 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index a675d44d4511..c86722a363e0 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3200,11 +3200,6 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing the same pattern is repeated for all 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 @@ -3336,9 +3331,6 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing 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): raise ValueError("'hatch' must be a sequence of strings " @@ -3363,15 +3355,6 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing # Example: hatch=['//', 'xx', '..'] → patterns repeat across datasets. hatches = itertools.cycle(hatch) - # 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 diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 84dc7bf37c19..71bdd064e9c1 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2333,26 +2333,6 @@ def test_grouped_bar_hatch_none(): f"Expected no hatch, got {rect.get_hatch()!r}" -def test_grouped_bar_hatch_mixed_orientation(): - """Ensure hatch works correctly for both vertical and horizontal orientations.""" - fig, (ax1, ax2) = plt.subplots(1, 2) - x = np.arange(3) - heights = [np.array([1, 2, 3]), np.array([2, 1, 2])] - hatches = ['//', 'xx'] - - containers_v = ax1.grouped_bar( - heights, positions=x, hatch=hatches, orientation="vertical") - containers_h = ax2.grouped_bar( - heights, positions=x, hatch=hatches, orientation="horizontal") - - for gi, (cv, ch) in enumerate( - zip(containers_v.bar_containers, containers_h.bar_containers)): - for rect in cv: - assert rect.get_hatch() == hatches[gi] - for rect in ch: - assert rect.get_hatch() == hatches[gi] - - 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() From 90f0a22e94a2e98ec19c49d518b8f1e8b0d1702b Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Wed, 5 Nov 2025 11:09:56 -0600 Subject: [PATCH 05/17] Trigger CI rerun: retry Windows backend timeouts From 0fad966ddfbd93ce28394ebf24934e4383119a48 Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Wed, 5 Nov 2025 12:24:48 -0600 Subject: [PATCH 06/17] Trigger CI rerun: retry WebAgg timeout From 1eb63f9a86979cb95f39db855d1c215bfb346ecc Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Thu, 6 Nov 2025 12:14:01 -0600 Subject: [PATCH 07/17] Trigger CI rerun: retry backend timeout From 960914c3aeefc4e24271249a8d5d4fd40d6e4273 Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Thu, 6 Nov 2025 13:10:04 -0600 Subject: [PATCH 08/17] Trigger CI rerun: retry TkAgg timeout From adf2717e9a338d54fd1c60a0e0ef7cf4edb9e5ea Mon Sep 17 00:00:00 2001 From: ilakk manoharan Date: Fri, 7 Nov 2025 03:31:37 -0600 Subject: [PATCH 09/17] Update lib/matplotlib/axes/_axes.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/axes/_axes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index c86722a363e0..ffdb15c3ebc8 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3198,7 +3198,6 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing the patterns are cycled across datasets. - If the sequence contains a single element (e.g., ``['//']``), the same pattern is repeated for all datasets. - - Single string values (e.g., ``'//'``) are **not supported**. **kwargs : `.Rectangle` properties From a8c8705da61c6080ac5b1ccf3a8d6aa2edeae203 Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Fri, 7 Nov 2025 04:54:40 -0600 Subject: [PATCH 10/17] Allow None entries in hatch list for grouped_bar() --- lib/matplotlib/axes/_axes.py | 4 ++-- lib/matplotlib/tests/test_axes.py | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index ffdb15c3ebc8..2d14796644ad 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3347,8 +3347,8 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing if not hatch_list: # Empty sequence → treat as no hatch. hatches = itertools.cycle([None]) - elif not all(isinstance(h, str) for h in hatch_list): - raise TypeError("All entries in 'hatch' must be strings") + 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. diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 71bdd064e9c1..de0ca27a1506 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2334,15 +2334,24 @@ def test_grouped_bar_hatch_none(): def test_grouped_bar_empty_string_disables_hatch(): - """An empty string in the hatch list should result in no hatch for that dataset.""" + """ + 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])] - hatches = ["", "xx"] + 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] - 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 + # 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_dict_with_labels_forbidden(): From d2f1943aef4dca616783d1cc9726bcc77fed7efb Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Fri, 7 Nov 2025 08:41:27 -0600 Subject: [PATCH 11/17] CI: rerun tests From afb64cb107382fac8656b851348f3eae2a5005b8 Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Fri, 7 Nov 2025 10:31:59 -0600 Subject: [PATCH 12/17] Docstring: clarify 'hatch' parameter type as sequence of :mpltype: or None, optional --- lib/matplotlib/axes/_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 2d14796644ad..256d18a85dc6 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3190,7 +3190,7 @@ 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 : sequence of :mpltype:`hatch` or None, optional Hatch pattern(s) to apply per dataset. - If ``None`` (default), no hatching is applied. From 485ef67aa7fb56c01a9c3382e625b68e3c214c6b Mon Sep 17 00:00:00 2001 From: ilakk manoharan Date: Fri, 7 Nov 2025 11:48:26 -0600 Subject: [PATCH 13/17] Update lib/matplotlib/axes/_axes.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/axes/_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 256d18a85dc6..1a515a766335 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -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, - hatch=None,**kwargs): + hatch=None, **kwargs): """ Make a grouped bar plot. From 9fffd16883d746706b7a8ac5c2dadc5c1b97b197 Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Fri, 7 Nov 2025 13:30:40 -0600 Subject: [PATCH 14/17] Clean up API change entry for grouped_bar hatch behavior From af21213d0d9914bbd31d08f258295e6c330d2b35 Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Mon, 17 Nov 2025 11:20:48 -0600 Subject: [PATCH 15/17] Fix hatch validation: disallow empty lists --- lib/matplotlib/axes/_axes.py | 15 ++++++++++----- lib/matplotlib/tests/test_axes.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 1a515a766335..9b2baf0a61dc 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3345,14 +3345,19 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing "(e.g., ['//']) or None") from None if not hatch_list: - # Empty sequence → treat as no hatch. - hatches = itertools.cycle([None]) + # 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) + # Sequence of hatch patterns: cycle through them as needed. + # Example: hatch=['//', 'xx', '..'] → patterns repeat across datasets. + hatches = itertools.cycle(hatch_list) bar_width = (group_distance / (num_datasets + (num_datasets - 1) * bar_spacing + group_spacing)) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index de0ca27a1506..60d2dcb76ef2 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2354,6 +2354,17 @@ def test_grouped_bar_empty_string_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() From 541bf681bcdbcdac3177f4ad789d74960a2b87ef Mon Sep 17 00:00:00 2001 From: Ilakkuvaselvi Manoharan Date: Mon, 17 Nov 2025 12:11:04 -0600 Subject: [PATCH 16/17] Trigger CI/CD From 324d5781852a44499e024e8cdcc58fd10fb233aa Mon Sep 17 00:00:00 2001 From: ilakk manoharan Date: Fri, 21 Nov 2025 14:09:20 -0600 Subject: [PATCH 17/17] Update lib/matplotlib/axes/_axes.py Co-authored-by: hannah --- lib/matplotlib/axes/_axes.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 9b2baf0a61dc..6a4e6b81ddf8 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3373,12 +3373,7 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing # are handled separately below bar_containers = [] - # 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) ):