Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f74fe35

Browse files
committed
ENH: add ability to pass per-Axes subplot_kw through subplot_mosaic
1 parent 7760e5e commit f74fe35

File tree

5 files changed

+192
-10
lines changed

5 files changed

+192
-10
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Per-subplot keyword arguments in ``subplot_mosaic``
2+
----------------------------------------------------
3+
4+
It is now possible to pass keyword arguments through to the creation to each
5+
specific call to ``add_subplot`` in `.Figure.subplot_mosaic` and
6+
`.pyplot.subplot_mosaic` :
7+
8+
.. plot::
9+
:include-source: true
10+
11+
fig, axd = plt.subplot_mosaic(
12+
"AB;CD",
13+
per_subplot_kw={
14+
"A": {"projection": "polar"},
15+
("C", "D"): {"xscale": "log"},
16+
"B": {"projection": "3d"},
17+
},
18+
)
19+
20+
21+
This is particularly useful for creating mosaics with mixed projections, but
22+
any keyword arguments can be passed through.

lib/matplotlib/figure.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,6 +1759,25 @@ def get_tightbbox(self, renderer=None, bbox_extra_artists=None):
17591759

17601760
return _bbox
17611761

1762+
@staticmethod
1763+
def _norm_per_subplot_kw(per_subplot_kw):
1764+
expanded = {}
1765+
for k, v in per_subplot_kw.items():
1766+
if isinstance(k, tuple):
1767+
for sub_key in k:
1768+
if sub_key in expanded:
1769+
raise ValueError(
1770+
f'The key {sub_key!r} appears multiple times.'
1771+
)
1772+
expanded[sub_key] = v
1773+
else:
1774+
if k in expanded:
1775+
raise ValueError(
1776+
f'The key {k!r} appears multiple times.'
1777+
)
1778+
expanded[k] = v
1779+
return expanded
1780+
17621781
@staticmethod
17631782
def _normalize_grid_string(layout):
17641783
if '\n' not in layout:
@@ -1771,7 +1790,8 @@ def _normalize_grid_string(layout):
17711790

17721791
def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False,
17731792
width_ratios=None, height_ratios=None,
1774-
empty_sentinel='.', subplot_kw=None, gridspec_kw=None):
1793+
empty_sentinel='.', subplot_kw=None, gridspec_kw=None,
1794+
per_subplot_kw=None):
17751795
"""
17761796
Build a layout of Axes based on ASCII art or nested lists.
17771797
@@ -1821,6 +1841,9 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False,
18211841
The string notation allows only single character Axes labels and
18221842
does not support nesting but is very terse.
18231843
1844+
Tuples may be used instead of lists and tuples may not be used
1845+
as Axes identifiers.
1846+
18241847
sharex, sharey : bool, default: False
18251848
If True, the x-axis (*sharex*) or y-axis (*sharey*) will be shared
18261849
among all subplots. In that case, tick label visibility and axis
@@ -1843,7 +1866,21 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False,
18431866
18441867
subplot_kw : dict, optional
18451868
Dictionary with keywords passed to the `.Figure.add_subplot` call
1846-
used to create each subplot.
1869+
used to create each subplot. These values may be overridden by
1870+
values in *per_subplot_kw*.
1871+
1872+
per_subplot_kw : dict, optional
1873+
A dictionary mapping the Axes idenitfies or tuples of identifies to
1874+
a dictionary of keyword arguments to be passed to the
1875+
`.Figure.add_subplot` call used to create each subplot. The values
1876+
in these dictionaries have precedence over the values in
1877+
*subplot_kw*.
1878+
1879+
In the special case *mosaic* being a string, multi-character keys
1880+
in *per_subplot_kw* will be applied to all of the Axes named
1881+
treating the string as a sequence.
1882+
1883+
.. versionadded:: 3.7
18471884
18481885
gridspec_kw : dict, optional
18491886
Dictionary with keywords passed to the `.GridSpec` constructor used
@@ -1868,6 +1905,8 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False,
18681905
"""
18691906
subplot_kw = subplot_kw or {}
18701907
gridspec_kw = dict(gridspec_kw or {})
1908+
per_subplot_kw = per_subplot_kw or {}
1909+
18711910
if height_ratios is not None:
18721911
if 'height_ratios' in gridspec_kw:
18731912
raise ValueError("'height_ratios' must not be defined both as "
@@ -1882,6 +1921,14 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False,
18821921
# special-case string input
18831922
if isinstance(mosaic, str):
18841923
mosaic = self._normalize_grid_string(mosaic)
1924+
per_subplot_kw = {
1925+
tuple(k): v for k, v in per_subplot_kw.items()
1926+
}
1927+
1928+
per_subplot_kw = self._norm_per_subplot_kw(
1929+
per_subplot_kw
1930+
)
1931+
18851932
# Only accept strict bools to allow a possible future API expansion.
18861933
_api.check_isinstance(bool, sharex=sharex, sharey=sharey)
18871934

@@ -2011,7 +2058,11 @@ def _do_layout(gs, mosaic, unique_ids, nested):
20112058
raise ValueError(f"There are duplicate keys {name} "
20122059
f"in the layout\n{mosaic!r}")
20132060
ax = self.add_subplot(
2014-
gs[slc], **{'label': str(name), **subplot_kw}
2061+
gs[slc], **{
2062+
'label': str(name),
2063+
**subplot_kw,
2064+
**per_subplot_kw.get(name, {})
2065+
}
20152066
)
20162067
output[name] = ax
20172068
elif method == 'nested':
@@ -2048,7 +2099,11 @@ def _do_layout(gs, mosaic, unique_ids, nested):
20482099
if sharey:
20492100
ax.sharey(ax0)
20502101
ax._label_outer_yaxis(check_patch=True)
2051-
2102+
if extra := set(per_subplot_kw) - set(ret):
2103+
raise ValueError(
2104+
f"The keys {extra} are in *per_subplot_kw* "
2105+
"but not in the mosaic."
2106+
)
20522107
return ret
20532108

20542109
def _set_artist_props(self, a):

lib/matplotlib/pyplot.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,7 +1479,8 @@ def subplots(nrows=1, ncols=1, *, sharex=False, sharey=False, squeeze=True,
14791479

14801480
def subplot_mosaic(mosaic, *, sharex=False, sharey=False,
14811481
width_ratios=None, height_ratios=None, empty_sentinel='.',
1482-
subplot_kw=None, gridspec_kw=None, **fig_kw):
1482+
subplot_kw=None, gridspec_kw=None,
1483+
per_subplot_kw=None, **fig_kw):
14831484
"""
14841485
Build a layout of Axes based on ASCII art or nested lists.
14851486
@@ -1576,7 +1577,8 @@ def subplot_mosaic(mosaic, *, sharex=False, sharey=False,
15761577
mosaic, sharex=sharex, sharey=sharey,
15771578
height_ratios=height_ratios, width_ratios=width_ratios,
15781579
subplot_kw=subplot_kw, gridspec_kw=gridspec_kw,
1579-
empty_sentinel=empty_sentinel
1580+
empty_sentinel=empty_sentinel,
1581+
per_subplot_kw=per_subplot_kw,
15801582
)
15811583
return fig, ax_dict
15821584

lib/matplotlib/tests/test_figure.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,12 @@ def test_animated_with_canvas_change(fig_test, fig_ref):
848848
class TestSubplotMosaic:
849849
@check_figures_equal(extensions=["png"])
850850
@pytest.mark.parametrize(
851-
"x", [[["A", "A", "B"], ["C", "D", "B"]], [[1, 1, 2], [3, 4, 2]]]
851+
"x", [
852+
[["A", "A", "B"], ["C", "D", "B"]],
853+
[[1, 1, 2], [3, 4, 2]],
854+
(("A", "A", "B"), ("C", "D", "B")),
855+
((1, 1, 2), (3, 4, 2))
856+
]
852857
)
853858
def test_basic(self, fig_test, fig_ref, x):
854859
grid_axes = fig_test.subplot_mosaic(x)
@@ -998,6 +1003,10 @@ def test_fail_list_of_str(self):
9981003
plt.subplot_mosaic(['foo', 'bar'])
9991004
with pytest.raises(ValueError, match='must be 2D'):
10001005
plt.subplot_mosaic(['foo'])
1006+
with pytest.raises(ValueError, match='must be 2D'):
1007+
plt.subplot_mosaic([['foo', ('bar',)]])
1008+
with pytest.raises(ValueError, match='must be 2D'):
1009+
plt.subplot_mosaic([['a', 'b'], [('a', 'b'), 'c']])
10011010

10021011
@check_figures_equal(extensions=["png"])
10031012
@pytest.mark.parametrize("subplot_kw", [{}, {"projection": "polar"}, None])
@@ -1011,8 +1020,26 @@ def test_subplot_kw(self, fig_test, fig_ref, subplot_kw):
10111020

10121021
axB = fig_ref.add_subplot(gs[0, 1], **subplot_kw)
10131022

1023+
@check_figures_equal(extensions=["png"])
1024+
@pytest.mark.parametrize("multi_value", ['BC', tuple('BC')])
1025+
def test_per_subplot_kw(self, fig_test, fig_ref, multi_value):
1026+
x = 'AB;CD'
1027+
grid_axes = fig_test.subplot_mosaic(
1028+
x,
1029+
subplot_kw={'facecolor': 'red'},
1030+
per_subplot_kw={
1031+
'D': {'facecolor': 'blue'},
1032+
multi_value: {'facecolor': 'green'},
1033+
}
1034+
)
1035+
1036+
gs = fig_ref.add_gridspec(2, 2)
1037+
for color, spec in zip(['red', 'green', 'green', 'blue'], gs):
1038+
fig_ref.add_subplot(spec, facecolor=color)
1039+
10141040
def test_string_parser(self):
10151041
normalize = Figure._normalize_grid_string
1042+
10161043
assert normalize('ABC') == [['A', 'B', 'C']]
10171044
assert normalize('AB;CC') == [['A', 'B'], ['C', 'C']]
10181045
assert normalize('AB;CC;DE') == [['A', 'B'], ['C', 'C'], ['D', 'E']]
@@ -1029,6 +1056,25 @@ def test_string_parser(self):
10291056
DE
10301057
""") == [['A', 'B'], ['C', 'C'], ['D', 'E']]
10311058

1059+
def test_per_subplot_kw_expander(self):
1060+
normalize = Figure._norm_per_subplot_kw
1061+
assert normalize({"A": {}, "B": {}}) == {"A": {}, "B": {}}
1062+
assert normalize({("A", "B"): {}}) == {"A": {}, "B": {}}
1063+
with pytest.raises(
1064+
ValueError, match=f'The key {"B"!r} appears multiple times'
1065+
):
1066+
normalize({("A", "B"): {}, "B": {}})
1067+
with pytest.raises(
1068+
ValueError, match=f'The key {"B"!r} appears multiple times'
1069+
):
1070+
normalize({"B": {}, ("A", "B"): {}})
1071+
1072+
def test_extra_per_subplot_kw(self):
1073+
with pytest.raises(
1074+
ValueError, match=f'The keys {set("B")!r} are in'
1075+
):
1076+
Figure().subplot_mosaic("A", per_subplot_kw={"B": {}})
1077+
10321078
@check_figures_equal(extensions=["png"])
10331079
@pytest.mark.parametrize("str_pattern",
10341080
["AAA\nBBB", "\nAAA\nBBB\n", "ABC\nDEF"]

tutorials/provisional/mosaic.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,8 @@ def identify_axes(ax_dict, fontsize=48):
202202
# empty sentinel with the string shorthand because it may be stripped
203203
# while processing the input.
204204
#
205-
# Controlling mosaic and subplot creation
206-
# =======================================
205+
# Controlling mosaic creation
206+
# ===========================
207207
#
208208
# This feature is built on top of `.gridspec` and you can pass the
209209
# keyword arguments through to the underlying `.gridspec.GridSpec`
@@ -278,15 +278,72 @@ def identify_axes(ax_dict, fontsize=48):
278278

279279

280280
###############################################################################
281+
# Controlling subplot creation
282+
# ============================
283+
281284
# We can also pass through arguments used to create the subplots
282-
# (again, the same as `.Figure.subplots`).
285+
# (again, the same as `.Figure.subplots`) which will apply to all
286+
# of the Axes created.
283287

284288

285289
axd = plt.figure(constrained_layout=True).subplot_mosaic(
286290
"AB", subplot_kw={"projection": "polar"}
287291
)
288292
identify_axes(axd)
289293

294+
###############################################################################
295+
# Per-Axes subplot keyword arguments
296+
# ----------------------------------
297+
#
298+
#
299+
# If you need to control the parameters passed to each subplot individually use
300+
# *per_subplot_kw* to pass a mapping between the Axes identifiers (or
301+
# tuples of Axes identifiers) to dictionaries of keywords to be passed.
302+
#
303+
# .. versionadded:: 3.7
304+
#
305+
306+
307+
fig, axd = plt.subplot_mosaic(
308+
"AB;CD",
309+
per_subplot_kw={
310+
"A": {"projection": "polar"},
311+
("C", "D"): {"xscale": "log"}
312+
},
313+
)
314+
identify_axes(axd)
315+
316+
###############################################################################
317+
# If the layout is specified with the string short-hand, then we know the
318+
# Axes labels will be one character and can unambiguously interpret longer
319+
# strings in *per_subplot_kw* to specify a set of Axes to apply the
320+
# keywords to:
321+
322+
323+
fig, axd = plt.subplot_mosaic(
324+
"AB;CD",
325+
per_subplot_kw={
326+
"AD": {"projection": "polar"},
327+
"BC": {"facecolor": ".9"}
328+
},
329+
)
330+
identify_axes(axd)
331+
332+
###############################################################################
333+
# if *subplot_kw* and *per_subplot_kw* are used together, then they are
334+
# merged with *per_subplot_kw* taking priority:
335+
336+
337+
axd = plt.figure(constrained_layout=True).subplot_mosaic(
338+
"AB;CD",
339+
subplot_kw={"facecolor": "xkcd:tangerine"},
340+
per_subplot_kw={
341+
"B": {"facecolor": "xkcd:water blue"},
342+
"D": {"projection": "polar", "facecolor": "w"},
343+
}
344+
)
345+
identify_axes(axd)
346+
290347

291348
###############################################################################
292349
# Nested list input

0 commit comments

Comments
 (0)