Skip to content

Commit 65e54af

Browse files
authored
Merge pull request #24604 from tacaswell/enh/extend_mosaic_kwargs
Enh/extend mosaic kwargs
2 parents c9fc6a3 + e56f876 commit 65e54af

File tree

5 files changed

+204
-13
lines changed

5 files changed

+204
-13
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 Axes creation in 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: 57 additions & 6 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='.',
1794+
subplot_kw=None, per_subplot_kw=None, gridspec_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+
The Axes identifiers may be `str` or a non-iterable hashable
1845+
object (e.g. `tuple` s may not be used).
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 identifiers or tuples of identifiers
1874+
to 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+
If *mosaic* is a string, and thus all keys are single characters,
1880+
it is possible to use a single string instead of a tuple as keys;
1881+
i.e. ``"AB"`` is equivalent to ``("A", "B")``.
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,12 @@ 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(per_subplot_kw)
1929+
18851930
# Only accept strict bools to allow a possible future API expansion.
18861931
_api.check_isinstance(bool, sharex=sharex, sharey=sharey)
18871932

@@ -2011,7 +2056,11 @@ def _do_layout(gs, mosaic, unique_ids, nested):
20112056
raise ValueError(f"There are duplicate keys {name} "
20122057
f"in the layout\n{mosaic!r}")
20132058
ax = self.add_subplot(
2014-
gs[slc], **{'label': str(name), **subplot_kw}
2059+
gs[slc], **{
2060+
'label': str(name),
2061+
**subplot_kw,
2062+
**per_subplot_kw.get(name, {})
2063+
}
20152064
)
20162065
output[name] = ax
20172066
elif method == 'nested':
@@ -2048,9 +2097,11 @@ def _do_layout(gs, mosaic, unique_ids, nested):
20482097
if sharey:
20492098
ax.sharey(ax0)
20502099
ax._label_outer_yaxis(check_patch=True)
2051-
for k, ax in ret.items():
2052-
if isinstance(k, str):
2053-
ax.set_label(k)
2100+
if extra := set(per_subplot_kw) - set(ret):
2101+
raise ValueError(
2102+
f"The keys {extra} are in *per_subplot_kw* "
2103+
"but not in the mosaic."
2104+
)
20542105
return ret
20552106

20562107
def _set_artist_props(self, a):

lib/matplotlib/pyplot.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,7 +1492,8 @@ def subplots(nrows=1, ncols=1, *, sharex=False, sharey=False, squeeze=True,
14921492

14931493
def subplot_mosaic(mosaic, *, sharex=False, sharey=False,
14941494
width_ratios=None, height_ratios=None, empty_sentinel='.',
1495-
subplot_kw=None, gridspec_kw=None, **fig_kw):
1495+
subplot_kw=None, gridspec_kw=None,
1496+
per_subplot_kw=None, **fig_kw):
14961497
"""
14971498
Build a layout of Axes based on ASCII art or nested lists.
14981499
@@ -1563,7 +1564,21 @@ def subplot_mosaic(mosaic, *, sharex=False, sharey=False,
15631564
15641565
subplot_kw : dict, optional
15651566
Dictionary with keywords passed to the `.Figure.add_subplot` call
1566-
used to create each subplot.
1567+
used to create each subplot. These values may be overridden by
1568+
values in *per_subplot_kw*.
1569+
1570+
per_subplot_kw : dict, optional
1571+
A dictionary mapping the Axes identifiers or tuples of identifiers
1572+
to a dictionary of keyword arguments to be passed to the
1573+
`.Figure.add_subplot` call used to create each subplot. The values
1574+
in these dictionaries have precedence over the values in
1575+
*subplot_kw*.
1576+
1577+
If *mosaic* is a string, and thus all keys are single characters,
1578+
it is possible to use a single string instead of a tuple as keys;
1579+
i.e. ``"AB"`` is equivalent to ``("A", "B")``.
1580+
1581+
.. versionadded:: 3.7
15671582
15681583
gridspec_kw : dict, optional
15691584
Dictionary with keywords passed to the `.GridSpec` constructor used
@@ -1589,7 +1604,8 @@ def subplot_mosaic(mosaic, *, sharex=False, sharey=False,
15891604
mosaic, sharex=sharex, sharey=sharey,
15901605
height_ratios=height_ratios, width_ratios=width_ratios,
15911606
subplot_kw=subplot_kw, gridspec_kw=gridspec_kw,
1592-
empty_sentinel=empty_sentinel
1607+
empty_sentinel=empty_sentinel,
1608+
per_subplot_kw=per_subplot_kw,
15931609
)
15941610
return fig, ax_dict
15951611

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

291347
###############################################################################
292348
# Nested list input

0 commit comments

Comments
 (0)