Skip to content

Conversation

@rcomer
Copy link
Member

@rcomer rcomer commented Dec 4, 2025

PR summary

hist, stackplot and grouped_bar all loop through datasets and call another plotting method repeatedly. This means they have a common requirement to support sequences for parameters such as facecolor and hatch.

hist already has handling for this, which I have factored out into a new helper function. I wasn't sure where to put the helper, so for now it is in its own new private module. I tried putting it in _axes, _api and cbook but all those options gave me circular import problems. The tests from #29719 are now reimagined as cheaper tests on the helper.

Using the helper in stackplot closes #30804.

Using the helper in grouped_bar closes #30673, so this would supersede #30726. I copied the grouped_bar test from there so added @ilakkmanoharan as a co-author. Note that, contrary to agreement in #30726, this does support a single hatch string for grouped_bar. I still don't think a single hatch makes sense for any of these methods, but it is already supported for both hist and stackplot and I do not feel strongly enough to deprecate the existing support or to throw a special case exception in grouped_bar.

grouped_bar example usage:

import matplotlib.pyplot as plt

categories = ['A', 'B']
datasets = {
    'dataset 0': [1, 11],
    'dataset 1': [3, 13],
    'dataset 2': [5, 15],
}

fig, ax = plt.subplots()
ax.grouped_bar(datasets, tick_labels=categories, hatch=['/','+', '.'], facecolor='gray', edgecolor=['r', 'b', 'g'])
ax.legend()
plt.show()
image

Note the helper does not handle color because the methods already have bespoke handling for colors. E.g. stackplot maps colors to facecolor, hist with "step" type maps colors to edgecolor. What should happen if color is in **kwargs probably needs individual consideration for each method.

PR checklist

if colors is None:
colors = [axes._get_lines.get_next_color() for _ in y]

kwargs['hatch'] = hatch
Copy link
Member Author

Choose a reason for hiding this comment

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

Unsure whether to do this or to remove hatch from the named parameters.

Copy link
Member

Choose a reason for hiding this comment

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

Not really important, but I'm inclined to remove the explicit hatch parameter. The code is simpler without and there's no reason to name this explicitly, but not other style parameters.

@rcomer rcomer added this to the v3.11.0 milestone Dec 4, 2025
else:
colors = (axes._get_lines.get_next_color() for _ in y)
if colors is None:
colors = [axes._get_lines.get_next_color() for _ in y]
Copy link
Member Author

@rcomer rcomer Dec 4, 2025

Choose a reason for hiding this comment

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

Changed this from a generator to a list because to_rgba_array needs something with a __len__.

…ther plotting methods

Co-authored-by: Ilakkuvaselvi Manoharan <ilakkmanoharan@gmail.com>
@rcomer rcomer marked this pull request as draft December 5, 2025 11:49
@rcomer rcomer marked this pull request as ready for review December 5, 2025 13:53
Comment on lines 3342 to 3347
for i, (hs, label, color) in enumerate(zip(heights, labels, colors)):
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, **next(kwargs_gen))
Copy link
Member

Choose a reason for hiding this comment

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

This pattern is not ideal. It splits the iteration. Part of the advancement is done in for ... in ..., part is done through the next(). The kwargs_gen should go into the zip.

Also: Slight concern but probably ok for now: iterate_styles() is a bit invasive in that it handles also the non-style kwargs. After calling iterate_styles() the kwargs are folded into to generator and kwargs should likely not be used any further. In particular this means (1) all kwarg prepocessing has to be done before iterate_styles(), (2) we could not have another iterate_something() in the future.

The alternative would be something like

style_kwargs, kwargs = _style_helpers.extract_style_kwargs(kwargs)
...
for i, (hs, label, color, style_kw) in enumerate(zip(heights, labels, colors, style_kwargs)):
    ...
    bar(..., color=color, **style_kw, **kwargs)

where we could still decide whether we extract constant style parameters and turn them into an iterator or leave them in kwargs.

Copy link
Member Author

Choose a reason for hiding this comment

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

So more like what I did in my first commit? 🙃

Comment on lines 21 to 26
ax.stackplot(x, y1, y2,
facecolor=['tab:red', 'tab:blue'],
edgecolor=['black', 'gray'],
linestyle=[':', '--'],
linewidth=[2, 3],
hatch=['.', '*'])
Copy link
Member

@timhoffm timhoffm Dec 6, 2025

Choose a reason for hiding this comment

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

While it is structurally reasonable to also iterate edge-related parameters, I don't see a practical use case because the horizontal edges overlap, so that you get no clear picture / artifacts if they are different.

I would only do an example if we can come up with an example that may be actually used in practice.
We also don't have to showcase every aspect of the feature, e.g. different line widths should be extermely rare.

Copy link
Member Author

Choose a reason for hiding this comment

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

I played around a bit, but didn't produce a plot I actually liked, so just removed it.

Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

Looks good. Comments are small suggestions.

Comment on lines +3197 to +3199
The following properties additionally accept a sequence of values
corresponding to the datasets in *heights*:
*edgecolor*, *facecolor*, *linewidth*, *linestyle*, *hatch*.
Copy link
Member

Choose a reason for hiding this comment

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

Should we make this a bit more prominent?

Suggested change
The following properties additionally accept a sequence of values
corresponding to the datasets in *heights*:
*edgecolor*, *facecolor*, *linewidth*, *linestyle*, *hatch*.
Properties applied to all bars. The following properties additionally
accept a sequence of values corresponding to the datasets in
*heights*:
- *edgecolor*
- *facecolor*
- *linewidth*
- *linestyle*
- *hatch*

'edgecolor': next(edgecolors),
'facecolor': next(facecolors),
})
kwargs.update(next(style_gen))
Copy link
Member

Choose a reason for hiding this comment

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

This is the correct translation keeping existing behavior.

Semi-OT: I'm wondering whether the behavior is logically correct: If patches is empty, we do not advance the style. This means depending on data contents the assignment of styles to datasets may vary.

('hatch', ["/", "\\", "."]),
('linestyle', ["-", "--", ":"]),
('linewidth', [1, 1.5, 2])])
def test_style_generator_list(key, value):
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
def test_style_generator_list(key, value):
def test_style_generator_list(key, value):
"""Test that style parameter lists are distributed to the generator."""

or as similar wording.

('hatch', "/"),
('linestyle', "-"),
('linewidth', 1)])
def test_style_generator_single(key, value):
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
def test_style_generator_single(key, value):
def test_style_generator_single(key, value):
"""Test that single-value style parameters are distributed to the generator."""



@pytest.mark.parametrize('key', ['facecolor', 'hatch', 'linestyle'])
def test_style_generator_empty(key):
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
def test_style_generator_empty(key):
def test_style_generator_raises_on_empty_style_parameter_list(key):

quite verbose, but now exactly telling what we test :)



def test_style_generator_sequence_type_styles():
kw = {'facecolor': ('r', 0.5),
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
kw = {'facecolor': ('r', 0.5),
"""
Test that sequence type style values are detected as single value
and passed to a all elements of the generator.
"""
kw = {'facecolor': ('r', 0.5),

Comment on lines +3453 to +3464
fcols = ['r', 'b']

fig, ax = plt.subplots()

colls = ax.stackplot(x, y1, y2, facecolor=fcols, colors=['c', 'm'])
for coll, fcol in zip(colls, fcols):
assert mcolors.same_color(coll.get_facecolor(), fcol)

# Plural alias should also work
colls = ax.stackplot(x, y1, y2, facecolors=fcols, colors=['c', 'm'])
for coll, fcol in zip(colls, fcols):
assert mcolors.same_color(coll.get_facecolor(), fcol)
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
fcols = ['r', 'b']
fig, ax = plt.subplots()
colls = ax.stackplot(x, y1, y2, facecolor=fcols, colors=['c', 'm'])
for coll, fcol in zip(colls, fcols):
assert mcolors.same_color(coll.get_facecolor(), fcol)
# Plural alias should also work
colls = ax.stackplot(x, y1, y2, facecolors=fcols, colors=['c', 'm'])
for coll, fcol in zip(colls, fcols):
assert mcolors.same_color(coll.get_facecolor(), fcol)
facecolors = ['r', 'b']
fig, ax = plt.subplots()
colls = ax.stackplot(x, y1, y2, facecolor=facecolors, colors=['c', 'm'])
for coll, fcol in zip(colls, facecolors):
assert mcolors.same_color(coll.get_facecolor(), facecolors)
# Plural alias should also work
colls = ax.stackplot(x, y1, y2, facecolors=facecolors, colors=['c', 'm'])
for coll, fcol in zip(colls, facecolors):
assert mcolors.same_color(coll.get_facecolor(), facecolors)

We can affort the additional letters, which IMHO makes this easier to read (In particular, fcols is prone to misinterpretation because (1) we have colls here and (2) col(s) is typically used as abbreviation for columns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Stackplot does not pass facecolor(s) correctly to fill_between [ENH]: Add custom hatch styling to grouped_bar

2 participants