8000 ENH: add ability to pass per-Axes subplot_kw through subplot_mosaic · matplotlib/matplotlib@6ca4fbb · GitHub
[go: up one dir, main page]

Skip to content

Commit 6ca4fbb

Browse files
committed
ENH: add ability to pass per-Axes subplot_kw through subplot_mosaic
1 parent f0f682e commit 6ca4fbb

File tree

4 files changed

+171
-11
lines changed

4 files changed

+171
-11
lines changed

lib/matplotlib/figure.py

Lines changed: 60 additions & 5 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_needs_a_better_name(needs_a_better_name):
1764+
expanded = {}
1765+
for k, v in needs_a_better_name.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:
@@ -1767,11 +1786,12 @@ def _normalize_grid_string(layout):
17671786
else:
17681787
# multi-line string
17691788
layout = inspect.cleandoc(layout)
1770-
return [list(ln) for ln in layout.strip('\n').split('\n')]
1789+
return [list(_) for _ in layout.strip('\n').split('\n')]
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+
needs_a_better_name=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,8 @@ 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 *needs_a_better_name*.
18471871
18481872
gridspec_kw : dict, optional
18491873
Dictionary with keywords passed to the `.GridSpec` constructor used
@@ -1858,6 +1882,19 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False,
18581882
`inspect.cleandoc` to remove leading white space, which may
18591883
interfere with using white-space as the empty sentinel.
18601884
1885+
needs_a_better_name : dict, optional
1886+
A dictionary mapping the Axes idenitfies or tuples of identifies to
1887+
a dictionary of keyword arguments to be passed to the
1888+
`.Figure.add_subplot` call used to create each subplot. The values
1889+
in these dictionaries have precedence over the values in
1890+
*subplot_kw*.
1891+
1892+
In the special case *mosaic* being a string, multi-character keys
1893+
in *needs_a_better_name* will be applied to all of the Axes named
1894+
treating the string as a sequence.
1895+
1896+
.. versionadded:: 3.7
1897+
18611898
Returns
18621899
-------
18631900
dict[label, Axes]
@@ -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+
needs_a_better_name = needs_a_better_name 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+
needs_a_better_name = {
1925+
tuple(k): v for k, v in needs_a_better_name.items()
1926+
}
1927+
1928+
needs_a_better_name = self._norm_needs_a_better_name(
1929+
needs_a_better_name
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+
**needs_a_better_name.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(needs_a_better_name) - set(ret):
2103+
raise ValueError(
2104+
f"The keys {extra} are in *needs_a_better_name* "
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
@@ -1474,7 +1474,8 @@ def subplots(nrows=1, ncols=1, *, sharex=False, sharey=False, squeeze=True,
14741474

14751475
def subplot_mosaic(mosaic, *, sharex=False, sharey=False,
14761476
width_ratios=None, height_ratios=None, empty_sentinel='.',
1477-
subplot_kw=None, gridspec_kw=None, **fig_kw):
1477+
subplot_kw=None, gridspec_kw=None,
1478+
needs_a_better_name=None, **fig_kw):
14781479
"""
14791480
Build a layout of Axes based on ASCII art or nested lists.
14801481
@@ -1571,7 +1572,8 @@ def subplot_mosaic(mosaic, *, sharex=False, sharey=False,
15711572
mosaic, sharex=sharex, sharey=sharey,
15721573
height_ratios=height_ratios, width_ratios=width_ratios,
15731574
subplot_kw=subplot_kw, gridspec_kw=gridspec_kw,
1574-
empty_sentinel=empty_sentinel
1575+
empty_sentinel=empty_sentinel,
1576+
needs_a_better_name=needs_a_better_name,
15751577
)
15761578
return fig, ax_dict
15771579

lib/matplotlib/tests/test_figure.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,12 @@ def test_animated_with_canvas_change(fig_test, fig_ref):
832832
class TestSubplotMosaic:
833833
@check_figures_equal(extensions=["png"])
834834
@pytest.mark.parametrize(
835-
"x", [[["A", "A", "B"], ["C", "D", "B"]], [[1, 1, 2], [3, 4, 2]]]
835+
"x", [
836+
[["A", "A", "B"], ["C", "D", "B"]],
837+
[[1, 1, 2], [3, 4, 2]],
838+
(("A", "A", "B"), ("C", "D", "B")),
839+
((1, 1, 2), (3, 4, 2))
840+
]
836841
)
837842
def test_basic(self, fig_test, fig_ref, x):
838843
grid_axes = fig_test.subplot_mosaic(x)
@@ -982,6 +987,10 @@ def test_fail_list_of_str(self):
982987
plt.subplot_mosaic(['foo', 'bar'])
983988
with pytest.raises(ValueError, match='must be 2D'):
984989
plt.subplot_mosaic(['foo'])
990+
with pytest.raises(ValueError, match='must be 2D'):
991+
plt.subplot_mosaic([['foo', ('bar',)]])
992+
with pytest.raises(ValueError, match='must be 2D'):
993+
plt.subplot_mosaic([['a', 'b'], [('a', 'b'), 'c']])
985994

986995
@check_figures_equal(extensions=["png"])
987996
@pytest.mark.parametrize("subplot_kw", [{}, {"projection": "polar"}, None])
@@ -995,8 +1004,26 @@ def test_subplot_kw(self, fig_test, fig_ref, subplot_kw):
9951004

9961005
axB = fig_ref.add_subplot(gs[0, 1], **subplot_kw)
9971006

1007+
@check_figures_equal(extensions=["png"])
1008+
@pytest.mark.parametrize("multi_value", ['BC', tuple('BC')])
1009+
def test_need_better_name(self, fig_test, fig_ref, multi_value):
1010+
x = 'AB;CD'
1011+
grid_axes = fig_test.subplot_mosaic(
1012+
x,
1013+
subplot_kw={'facecolor': 'red'},
1014+
needs_a_better_name={
1015+
'D': {'facecolor': 'blue'},
1016+
multi_value: {'facecolor': 'green'},
1017+
}
1018+
)
1019+
1020+
gs = fig_ref.add_gridspec(2, 2)
1021+
for color, spec in zip(['red', 'green', 'green', 'blue'], gs):
1022+
fig_ref.add_subplot(spec, facecolor=color)
1023+
9981024
def test_string_parser(self):
9991025
normalize = Figure._normalize_grid_string
1026+
10001027
assert normalize('ABC') == [['A', 'B', 'C']]
10011028
assert normalize('AB;CC') == [['A', 'B'], ['C', 'C']]
10021029
assert normalize('AB;CC;DE') == [['A', 'B'], ['C', 'C'], ['D', 'E']]
@@ -1013,6 +1040,25 @@ def test_string_parser(self):
10131040
DE
10141041
""") == [['A', 'B'], ['C', 'C'], ['D', 'E']]
10151042

1043+
def test_needs_a_better_name_expander(self):
1044+
normalize = Figure._norm_needs_a_better_name
1045+
assert normalize({"A": {}, "B": {}}) == {"A": {}, "B": {}}
1046+
assert normalize({("A", "B"): {}}) == {"A": {}, "B": {}}
1047+
with pytest.raises(
1048+
ValueError, match=f'The key {"B"!r} appears multiple times'
1049+
):
1050+
normalize({("A", "B"): {}, "B": {}})
1051+
with pytest.raises(
1052+
ValueError, match=f'The key {"B"!r} appears multiple times'
1053+
):
1054+
normalize({"B": {}, ("A", "B"): {}})
1055+
1056+
def test_extra_needs_a_better_name(self):
1057+
with pytest.raises(
1058+
ValueError, match=f'The keys {set("B")!r} are in'
1059+
):
1060+
Figure().subplot_mosaic("A", needs_a_better_name={"B": {}})
1061+
10161062
@check_figures_equal(extensions=["png"])
10171063
@pytest.mark.parametrize("str_pattern",
10181064
["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`
@@ -275,15 +275,72 @@ def identify_axes(ax_dict, fontsize=48):
275275

276276

277277
###############################################################################
278+
# Controlling subplot creation
279+
# ============================
280+
278281
# We can also pass through arguments used to create the subplots
279-
# (again, the same as `.Figure.subplots`).
282+
# (again, the same as `.Figure.subplots`) which will apply to all
283+
# of the Axes created.
280284

281285

282286
axd = plt.figure(constrained_layout=True).subplot_mosaic(
283287
"AB", subplot_kw={"projection": "polar"}
284288
)
285289
identify_axes(axd)
286290

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

288345
###############################################################################
289346
# Nested List input

0 commit comments

Comments
 (0)
0