Skip to content

Commit c624ca4

Browse files
Consolidate style parameter handling for plotting methods that call other plotting methods
Co-authored-by: Ilakkuvaselvi Manoharan <ilakkmanoharan@gmail.com>
1 parent 6c2d9e8 commit c624ca4

File tree

7 files changed

+221
-68
lines changed

7 files changed

+221
-68
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Stackplot styling
2+
-----------------
3+
4+
`~.Axes.stackplot` now accepts sequences for the style parameters *facecolor*,
5+
*edgecolor*, *linestyle*, and *linewidth*, similar to how the *hatch* parameter
6+
is already handled.
7+
8+
.. plot::
9+
:include-source: true
10+
:alt: A stackplot showing with two regions. The bottom region is red with black dots and a dotted black outline. The top region is blue with gray stars and a thicker dashed outline.
11+
12+
import matplotlib.pyplot as plt
13+
import numpy as np
14+
15+
x = np.linspace(0, 10, 10)
16+
y1 = 1.0 * x
17+
y2 = 2.0 * x + 1
18+
19+
fig, ax = plt.subplots()
20+
21+
ax.stackplot(x, y1, y2,
22+
facecolor=['tab:red', 'tab:blue'],
23+
edgecolor=['black', 'gray'],
24+
linestyle=[':', '--'],
25+
linewidth=[2, 3],
26+
hatch=['.', '*'])

lib/matplotlib/_style_helpers.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import collections.abc
2+
import itertools
3+
4+
import numpy as np
5+
6+
import matplotlib.cbook as cbook
7+
import matplotlib.colors as mcolors
8+
import matplotlib.lines as mlines
9+
10+
11+
def check_non_empty(key, value):
12+
"""Raise a TypeError if an empty sequence is passed"""
13+
if (not cbook.is_scalar_or_string(value) and
14+
isinstance(value, collections.abc.Sized) and len(value) == 0):
15+
raise TypeError(f'{key} must not be an empty sequence')
16+
17+
18+
def style_generator(kw):
19+
"""
20+
Helper for handling style sequences (e.g. facecolor=['r', 'b', 'k']) within plotting
21+
methods that repeatedly call other plotting methods (e.g. hist, stackplot). Remove
22+
style keywords from the given dictionary. Return the reduced dictionary together
23+
with a generator which provides a series of dictionaries to be used in each call to
24+
the wrapped function.
25+
"""
26+
kw_iterators = {}
27+
remaining_kw = {}
28+
for key, value in kw.items():
29+
if key in ['facecolor', 'edgecolor']:
30+
if value is None or cbook._str_lower_equal(value, 'none'):
31+
kw_iterators[key] = itertools.repeat(value)
32+
else:
33+
check_non_empty(key, value)
34+
kw_iterators[key] = itertools.cycle(mcolors.to_rgba_array(value))
35+
36+
elif key in ['hatch', 'linewidth']:
37+
check_non_empty(key, value)
38+
kw_iterators[key] = itertools.cycle(np.atleast_1d(value))
39+
40+
elif key == 'linestyle':
41+
check_non_empty(key, value)
42+
kw_iterators[key] = itertools.cycle(mlines._get_dash_patterns(value))
43+
44+
else:
45+
remaining_kw[key] = value
46+
47+
def style_gen():
48+
while True:
49+
yield {key: next(val) for key, val in kw_iterators.items()}
50+
51+
return remaining_kw, style_gen()

lib/matplotlib/axes/_axes.py

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import matplotlib.transforms as mtransforms
3333
import matplotlib.tri as mtri
3434
import matplotlib.units as munits
35-
from matplotlib import _api, _docstring, _preprocess_data
35+
from matplotlib import _api, _docstring, _preprocess_data, _style_helpers
3636
from matplotlib.axes._base import (
3737
_AxesBase, _TransformedBoundsLocator, _process_plot_format)
3838
from matplotlib.axes._secondary_axes import SecondaryAxis
@@ -3194,6 +3194,10 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing
31943194
31953195
**kwargs : `.Rectangle` properties
31963196
3197+
The following properties additionally accept a sequence of values
3198+
corresponding to the datasets in *heights*:
3199+
*edgecolor*, *facecolor*, *linewidth*, *linestyle*, *hatch*.
3200+
31973201
%(Rectangle:kwdoc)s
31983202
31993203
Returns
@@ -3320,6 +3324,8 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing
33203324
# TODO: do we want to be more restrictive and check lengths?
33213325
colors = itertools.cycle(colors)
33223326

3327+
kwargs, style_gen = _style_helpers.style_generator(kwargs)
3328+
33233329
bar_width = (group_distance /
33243330
(num_datasets + (num_datasets - 1) * bar_spacing + group_spacing))
33253331
bar_spacing_abs = bar_spacing * bar_width
@@ -3333,15 +3339,16 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing
33333339
# place the bars, but only use numerical positions, categorical tick labels
33343340
# are handled separately below
33353341
bar_containers = []
3336-
for i, (hs, label, color) in enumerate(zip(heights, labels, colors)):
3342+
for i, (hs, label, color, styles) in enumerate(zip(heights, labels, colors,
3343+
style_gen)):
33373344
lefts = (group_centers - 0.5 * group_distance + margin_abs
33383345
+ i * (bar_width + bar_spacing_abs))
33393346
if orientation == "vertical":
33403347
bc = self.bar(lefts, hs, width=bar_width, align="edge",
3341-
label=label, color=color, **kwargs)
3348+
label=label, color=color, **styles, **kwargs)
33423349
else:
33433350
bc = self.barh(lefts, hs, height=bar_width, align="edge",
3344-
label=label, color=color, **kwargs)
3351+
label=label, color=color, **styles, **kwargs)
33453352
bar_containers.append(bc)
33463353

33473354
if tick_labels is not None:
@@ -7632,38 +7639,15 @@ def hist(self, x, bins=None, range=None, density=False, weights=None,
76327639
labels = [] if label is None else np.atleast_1d(np.asarray(label, str))
76337640

76347641
if histtype == "step":
7635-
ec = kwargs.get('edgecolor', colors)
7636-
else:
7637-
ec = kwargs.get('edgecolor', None)
7638-
if ec is None or cbook._str_lower_equal(ec, 'none'):
7639-
edgecolors = itertools.repeat(ec)
7640-
else:
7641-
edgecolors = itertools.cycle(mcolors.to_rgba_array(ec))
7642-
7643-
fc = kwargs.get('facecolor', colors)
7644-
if cbook._str_lower_equal(fc, 'none'):
7645-
facecolors = itertools.repeat(fc)
7646-
else:
7647-
facecolors = itertools.cycle(mcolors.to_rgba_array(fc))
7642+
kwargs.setdefault('edgecolor', colors)
76487643

7649-
hatches = itertools.cycle(np.atleast_1d(kwargs.get('hatch', None)))
7650-
linewidths = itertools.cycle(np.atleast_1d(kwargs.get('linewidth', None)))
7651-
if 'linestyle' in kwargs:
7652-
linestyles = itertools.cycle(mlines._get_dash_patterns(kwargs['linestyle']))
7653-
else:
7654-
linestyles = itertools.repeat(None)
7644+
kwargs, style_gen = _style_helpers.style_generator(kwargs)
76557645

76567646
for patch, lbl in itertools.zip_longest(patches, labels):
76577647
if not patch:
76587648
continue
76597649
p = patch[0]
7660-
kwargs.update({
7661-
'hatch': next(hatches),
7662-
'linewidth': next(linewidths),
7663-
'linestyle': next(linestyles),
7664-
'edgecolor': next(edgecolors),
7665-
'facecolor': next(facecolors),
7666-
})
7650+
kwargs.update(next(style_gen))
76677651
p._internal_update(kwargs)
76687652
if lbl is not None:
76697653
p.set_label(lbl)

lib/matplotlib/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ python_sources = [
1717
'_mathtext.py',
1818
'_mathtext_data.py',
1919
'_pylab_helpers.py',
20+
'_style_helpers.py',
2021
'_text_helpers.py',
2122
'_tight_bbox.py',
2223
'_tight_layout.py',

lib/matplotlib/stackplot.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
(https://stackoverflow.com/users/66549/doug)
77
"""
88

9-
import itertools
109

1110
import numpy as np
1211

13-
from matplotlib import _api
12+
from matplotlib import cbook, collections, _api, _style_helpers
1413

1514
__all__ = ['stackplot']
1615

@@ -71,7 +70,14 @@ def stackplot(axes, x, *args,
7170
DATA_PARAMETER_PLACEHOLDER
7271
7372
**kwargs
74-
All other keyword arguments are passed to `.Axes.fill_between`.
73+
All other keyword arguments are passed to `.Axes.fill_between`. The
74+
following parameters additionally accept a sequence of values
75+
corresponding to the *y* datasets:
76+
*edgecolor(s)*, *facecolor(s)*, *linewidth(s)*, *linestyle(s)*.
77+
78+
.. versionadded:: 3.11
79+
Allowing sequences of values in above listed `.Axes.fill_between`
80+
parameters.
7581
7682
Returns
7783
-------
@@ -83,15 +89,14 @@ def stackplot(axes, x, *args,
8389
y = np.vstack(args)
8490

8591
labels = iter(labels)
86-
if colors is not None:
87-
colors = itertools.cycle(colors)
88-
else:
89-
colors = (axes._get_lines.get_next_color() for _ in y)
92+
if colors is None:
93+
colors = [axes._get_lines.get_next_color() for _ in y]
94+
95+
kwargs['hatch'] = hatch
96+
kwargs = cbook.normalize_kwargs(kwargs, collections.PolyCollection)
97+
kwargs.setdefault('facecolor', colors)
9098

91-
if hatch is None or isinstance(hatch, str):
92-
hatch = itertools.cycle([hatch])
93-
else:
94-
hatch = itertools.cycle(hatch)
99+
kwargs, style_gen = _style_helpers.style_generator(kwargs)
95100

96101
# Assume data passed has not been 'stacked', so stack it here.
97102
# We'll need a float buffer for the upcoming calculations.
@@ -130,18 +135,14 @@ def stackplot(axes, x, *args,
130135

131136
# Color between x = 0 and the first array.
132137
coll = axes.fill_between(x, first_line, stack[0, :],
133-
facecolor=next(colors),
134-
hatch=next(hatch),
135138
label=next(labels, None),
136-
**kwargs)
139+
**next(style_gen), **kwargs)
137140
coll.sticky_edges.y[:] = [0]
138141
r = [coll]
139142

140143
# Color between array i-1 and array i
141144
for i in range(len(y) - 1):
142145
r.append(axes.fill_between(x, stack[i, :], stack[i + 1, :],
143-
facecolor=next(colors),
144-
hatch=next(hatch),
145146
label=next(labels, None),
146-
**kwargs))
147+
**next(style_gen), **kwargs))
147148
return r
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import pytest
2+
3+
import matplotlib.colors as mcolors
4+
from matplotlib.lines import _get_dash_pattern
5+
from matplotlib._style_helpers import style_generator
6+
7+
8+
@pytest.mark.parametrize('key, value', [('facecolor', ["b", "g", "r"]),
9+
('edgecolor', ["b", "g", "r"]),
10+
('hatch', ["/", "\\", "."]),
11+
('linestyle', ["-", "--", ":"]),
12+
('linewidth', [1, 1.5, 2])])
13+
def test_style_generator_list(key, value):
14+
kw = {'foo': 12, key: value}
15+
new_kw, gen = style_generator(kw)
16+
17+
assert new_kw == {'foo': 12}
18+
19+
for v in value * 2: # Result should repeat
20+
style_dict = next(gen)
21+
assert len(style_dict) == 1
22+
if key.endswith('color'):
23+
assert mcolors.same_color(v, style_dict[key])
24+
elif key == 'linestyle':
25+
assert _get_dash_pattern(v) == style_dict[key]
26+
else:
27+
assert v == style_dict[key]
28+
29+
30+
@pytest.mark.parametrize('key, value', [('facecolor', "b"),
31+
('edgecolor', "b"),
32+
('hatch', "/"),
33+
('linestyle', "-"),
34+
('linewidth', 1)])
35+
def test_style_generator_single(key, value):
36+
kw = {'foo': 12, key: value}
37+
new_kw, gen = style_generator(kw)
38+
39+
assert new_kw == {'foo': 12}
40+
for _ in range(2): # Result should repeat
41+
style_dict = next(gen)
42+
if key.endswith('color'):
43+
assert mcolors.same_color(value, style_dict[key])
44+
elif key == 'linestyle':
45+
assert _get_dash_pattern(value) == style_dict[key]
46+
else:
47+
assert value == style_dict[key]
48+
49+
50+
@pytest.mark.parametrize('key', ['facecolor', 'hatch', 'linestyle'])
51+
def test_style_generator_empty(key):
52+
kw = {key: []}
53+
with pytest.raises(TypeError, match=f'{key} must not be an empty sequence'):
54+
style_generator(kw)
55+
56+
57+
def test_style_generator_sequence_type_styles():
58+
kw = {'facecolor': ('r', 0.5),
59+
'edgecolor': [0.5, 0.5, 0.5],
60+
'linestyle': (0, (1, 1))}
61+
62+
_, gen = style_generator(kw)
63+
for _ in range(2): # Result should repeat
64+
style_dict = next(gen)
65+
mcolors.same_color(kw['facecolor'], style_dict['facecolor'])
66+
mcolors.same_color(kw['edgecolor'], style_dict['edgecolor'])
67+
kw['linestyle'] == style_dict['linestyle']
68+
69+
70+
def test_style_generator_none():
71+
kw = {'facecolor': 'none',
72+
'edgecolor': 'none'}
73+
_, gen = style_generator(kw)
74+
for _ in range(2): # Result should repeat
75+
style_dict = next(gen)
76+
assert style_dict['facecolor'] == 'none'
77+
assert style_dict['edgecolor'] == 'none'

0 commit comments

Comments
 (0)