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

Skip to content

Commit bbda124

Browse files
tacaswellQuLogicWeatherGodtimhoffm
committed
ENH: API to use ASCII art or nested lists to compose complex figures
See tutorials/provisional/mosaic.py for details. Co-authored-by: Elliott Sales de Andrade <[email protected]> Co-authored-by: Benjamin Root <[email protected]> Co-authored-by: Tim Hoffmann <[email protected]>
1 parent 76e9e26 commit bbda124

File tree

7 files changed

+781
-0
lines changed

7 files changed

+781
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Add patch-work inspired API for generating grids of Axes
2+
--------------------------------------------------------
3+
4+
The `.Figure` class has a provisional method to easily generate
5+
complex grids of `.axes.Axes` based on text or nested list input:
6+
7+
8+
.. plot::
9+
:include-source: True
10+
11+
axd = plt.figure(constrained_layout=True).subplot_mosaic(
12+
"""
13+
AAE
14+
C.E
15+
""")
16+
for k, ax in axd.items():
17+
ax.text(0.5, 0.5, k,
18+
ha='center', va='center', fontsize=48,
19+
color='darkgrey')
20+
21+
22+
23+
See :ref:`sphx_glr_tutorials_provisional_mosaic.py` for more
24+
details and examples.

lib/matplotlib/axes/_subplots.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,23 @@ def _make_twin_axes(self, *args, **kwargs):
166166
self._twinned_axes.join(self, twin)
167167
return twin
168168

169+
def __repr__(self):
170+
fields = []
171+
if self.get_label():
172+
fields += [f"label={self.get_label()!r}"]
173+
titles = []
174+
for k in ["left", "center", "right"]:
175+
title = self.get_title(loc=k)
176+
if title:
177+
titles.append(f"{k!r}:{title!r}")
178+
if titles:
179+
fields += ["title={" + ",".join(titles) + "}"]
180+
if self.get_xlabel():
181+
fields += [f"xlabel={self.get_xlabel()!r}"]
182+
if self.get_ylabel():
183+
fields += [f"ylabel={self.get_ylabel()!r}"]
184+
return f"<{self.__class__.__name__}:" + ", ".join(fields) + ">"
185+
169186

170187
# this here to support cartopy which was using a private part of the
171188
# API to register their Axes subclasses.

lib/matplotlib/figure.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import logging
1212
from numbers import Integral
13+
import inspect
1314

1415
import numpy as np
1516

@@ -1522,6 +1523,187 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False,
15221523
.subplots(sharex=sharex, sharey=sharey, squeeze=squeeze,
15231524
subplot_kw=subplot_kw))
15241525

1526+
@staticmethod
1527+
def _normalize_grid_string(layout):
1528+
layout = inspect.cleandoc(layout)
1529+
return [list(ln) for ln in layout.strip('\n').split('\n')]
1530+
1531+
def subplot_mosaic(self, layout, *, subplot_kw=None, gridspec_kw=None,
1532+
empty_sentinel='.'):
1533+
"""
1534+
Build a layout of Axes based on ASCII art or nested lists.
1535+
1536+
This is a helper function to build complex GridSpec layouts visually.
1537+
1538+
.. note ::
1539+
1540+
This API is provisional and may be revised in the future based on
1541+
early user feedback.
1542+
1543+
1544+
Parameters
1545+
----------
1546+
layout : list of list of {hashable or nested} or str
1547+
1548+
A visual layout of how you want your Axes to be arranged
1549+
labeled as strings. For example ::
1550+
1551+
x = [['A', 'A', 'edge'],
1552+
['C', '.', 'edge']]
1553+
1554+
Produces 4 axes:
1555+
1556+
- 'A' which is 1 row high and spans the first two columns
1557+
- 'edge' which is 2 rows high and is on the right edge
1558+
- 'C' which in 1 row and 1 column wide in the bottom left
1559+
- a blank space 1 row and 1 column wide in the bottom center
1560+
1561+
If input is a str, then it must be of the form ::
1562+
1563+
'''
1564+
AAE
1565+
C.E
1566+
'''
1567+
1568+
where the labels are assumed to be single characters and
1569+
the rows are delimited by new lines. This only allows
1570+
single character axes labels and does not allow nesting,
1571+
but is very terse.
1572+
1573+
Using nested lists any hashable objects can be used to label
1574+
the Axes. In the special case that the objects in the list are
1575+
existing Axes objects they are adjusted to match the requested
1576+
layout.
1577+
1578+
subplot_kw : dict, optional
1579+
Dictionary with keywords passed to the `.Figure.add_subplot` call
1580+
used to create each subplot.
1581+
1582+
gridspec_kw : dict, optional
1583+
Dictionary with keywords passed to the `.GridSpec` constructor used
1584+
to create the grid the subplots are placed on.
1585+
1586+
empty_sentinel : object, optional
1587+
Entry in the layout to mean "leave this space empty". Defaults
1588+
to ``'.'``. Note, if *layout* is a string, it is processed via
1589+
`inspect.cleandoc` to remove leading white space, which may
1590+
interfere with using white-space as the empty sentinel.
1591+
1592+
Returns
1593+
-------
1594+
dict[label, Axes]
1595+
A dictionary mapping the labels to the Axes objects.
1596+
1597+
"""
1598+
subplot_kw = subplot_kw or {}
1599+
gridspec_kw = gridspec_kw or {}
1600+
# special-case string input
1601+
if isinstance(layout, str):
1602+
layout = self._normalize_grid_string(layout)
1603+
1604+
def _make_array(inp):
1605+
"""
1606+
Convert input into 2D array
1607+
1608+
We need to have this internal function rather than
1609+
``np.asarray(..., dtype=object)`` so that a list of lists
1610+
of lists does not get converted to an array of dimension >
1611+
2
1612+
1613+
Returns
1614+
-------
1615+
2D object array
1616+
1617+
"""
1618+
r0, *rest = inp
1619+
for j, r in enumerate(rest, start=1):
1620+
if len(r0) != len(r):
1621+
raise ValueError(
1622+
"All of the rows must be the same length, however "
1623+
f"the first row ({r0!r}) has length {len(r0)} "
1624+
f"and row {j} ({r!r}) has length {len(r)}."
1625+
)
1626+
out = np.zeros((len(inp), len(r0)), dtype=object)
1627+
for j, r in enumerate(inp):
1628+
for k, v in enumerate(r):
1629+
out[j, k] = v
1630+
return out
1631+
1632+
def _identify_keys_and_nested(layout):
1633+
"""
1634+
Given a 2D object array, identify unique IDs and nested layouts
1635+
1636+
Parameters
1637+
----------
1638+
layout : 2D numpy object array
1639+
1640+
Returns
1641+
-------
1642+
unique_ids : Set[object]
1643+
The unique non-sub layout entries in this layout
1644+
nested : Dict[Tuple[int, int]], 2D object array
1645+
"""
1646+
unique_ids = set()
1647+
nested = {}
1648+
for j, row in enumerate(layout):
1649+
for k, v in enumerate(row):
1650+
if v == empty_sentinel:
1651+
continue
1652+
elif not cbook.is_scalar_or_string(v):
1653+
nested[(j, k)] = _make_array(v)
1654+
else:
1655+
unique_ids.add(v)
1656+
1657+
return unique_ids, nested
1658+
1659+
def _do_layout(gs, layout, unique_ids, nested):
1660+
1661+
rows, cols = layout.shape
1662+
output = dict()
1663+
1664+
for name in unique_ids:
1665+
1666+
indx = np.argwhere(layout == name)
1667+
start_row, start_col = np.min(indx, axis=0)
1668+
end_row, end_col = np.max(indx, axis=0) + 1
1669+
slc = (slice(start_row, end_row), slice(start_col, end_col))
1670+
1671+
if (layout[slc] != name).any():
1672+
raise ValueError(
1673+
f"While trying to layout\n{layout!r}\n"
1674+
f"we found that the label {name!r} specifies a "
1675+
"non-rectangular or non-contiguous area.")
1676+
1677+
ax = self.add_subplot(
1678+
gs[slc], **{'label': str(name), **subplot_kw}
1679+
)
1680+
output[name] = ax
1681+
1682+
for (j, k), nested_layout in nested.items():
1683+
rows, cols = nested_layout.shape
1684+
gs_n = gs[j, k].subgridspec(rows, cols, **gridspec_kw)
1685+
nested_output = _do_layout(
1686+
gs_n,
1687+
nested_layout,
1688+
*_identify_keys_and_nested(nested_layout)
1689+
)
1690+
overlap = set(output) & set(nested_output)
1691+
if overlap:
1692+
raise ValueError(f"There are duplicate keys {overlap} "
1693+
f"between the outer layout\n{layout!r}\n"
1694+
f"and the nested layout\n{nested_layout}")
1695+
output.update(nested_output)
1696+
return output
1697+
1698+
layout = _make_array(layout)
1699+
rows, cols = layout.shape
1700+
gs = self.add_gridspec(rows, cols, **gridspec_kw)
1701+
ret = _do_layout(gs, layout, *_identify_keys_and_nested(layout))
1702+
for k, ax in ret.items():
1703+
if isinstance(k, str):
1704+
ax.set_label(k)
1705+
return ret
1706+
15251707
def delaxes(self, ax):
15261708
"""
15271709
Remove the `~.axes.Axes` *ax* from the figure; update the current axes.

lib/matplotlib/pyplot.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,86 @@ def subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True,
12741274
return fig, axs
12751275

12761276

1277+
def subplot_mosaic(layout, *, subplot_kw=None, gridspec_kw=None,
1278+
empty_sentinel='.', **fig_kw):
1279+
"""
1280+
Build a layout of Axes based on 2D layout.
1281+
1282+
This is a helper function to build complex GridSpec layouts visually.
1283+
1284+
.. note ::
1285+
1286+
This API is provisional and may be revised in the future based on
1287+
early user feedback.
1288+
1289+
1290+
Parameters
1291+
----------
1292+
layout : list of list of {hashable or nested} or str
1293+
1294+
A visual layout of how you want your Axes to be arranged
1295+
labeled as strings. For example ::
1296+
1297+
x = [['A', 'A', 'edge'],
1298+
['C', '.', 'edge']]
1299+
1300+
Produces 4 axes:
1301+
1302+
- 'A' which is 1 row high and spans the first two columns
1303+
- 'edge' which is 2 rows high and is on the right edge
1304+
- 'C' which in 1 row and 1 column wide in the bottom left
1305+
- a blank space 1 row and 1 column wide in the bottom center
1306+
1307+
If input is a str, then it must be of the form ::
1308+
1309+
'''
1310+
AAE
1311+
C.E
1312+
'''
1313+
1314+
where the labels are assumed to be single characters and
1315+
the rows are delimited by new lines. This only allows
1316+
single character axes labels and does not allow nesting,
1317+
but is very terse.
1318+
1319+
subplot_kw : dict, optional
1320+
Dictionary with keywords passed to the `.Figure.add_subplot` call
1321+
used to create each subplot.
1322+
1323+
gridspec_kw : dict, optional
1324+
Dictionary with keywords passed to the `.GridSpec` constructor used
1325+
to create the grid the subplots are placed on.
1326+
1327+
empty_sentinel : object, optional
1328+
Entry in the layout to mean "leave this space empty". Defaults
1329+
to ``'.'``. Note, if *layout* is a string, it is processed via
1330+
`inspect.cleandoc` to remove leading white space, which may
1331+
interfere with using white-space as the empty sentinel.
1332+
1333+
**fig_kw
1334+
All additional keyword arguments are passed to the
1335+
`.pyplot.figure` call.
1336+
1337+
1338+
Returns
1339+
-------
1340+
fig : `~.figure.Figure`
1341+
The new figure
1342+
1343+
ax_dict : dict[str, Axes]
1344+
A dictionary mapping the string labels to the new Axes objects.
1345+
1346+
"""
1347+
fig = figure(**fig_kw)
1348+
ax_dict = fig.subplot_mosaic(
1349+
layout,
1350+
subplot_kw=subplot_kw,
1351+
gridspec_kw=gridspec_kw,
1352+
empty_sentinel=empty_sentinel
1353+
)
1354+
return fig, ax_dict
1355+
1356+
12771357
def subplot2grid(shape, loc, rowspan=1, colspan=1, fig=None, **kwargs):
12781358
"""
12791359
Create a subplot at a specific location inside a regular grid.

0 commit comments

Comments
 (0)