| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 76e9e26 commit 424de9a
7 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,37 @@ | |||
| 1 | + Add API for composing semantic axes layouts from text or nested lists | ||
| 2 | + --------------------------------------------------------------------- | ||
| 3 | + | ||
| 4 | + The `.Figure` class has a provisional method to generate complex grids | ||
| 5 | + of named `.axes.Axes` based on nested list input or ASCII art: | ||
| 6 | + | ||
| 7 | + .. plot:: | ||
| 8 | + :include-source: True | ||
| 9 | + | ||
| 10 | + axd = plt.figure(constrained_layout=True).subplot_mosaic( | ||
| 11 | + [["Top", "Top", "Edge"], | ||
| 12 | + ["Left", ".", "Edge"]] | ||
| 13 | + ) | ||
| 14 | + for k, ax in axd.items(): | ||
| 15 | + ax.text(0.5, 0.5, k, | ||
| 16 | + ha='center', va='center', fontsize=36, | ||
| 17 | + color='darkgrey') | ||
| 18 | + | ||
| 19 | + or as a string (with single-character Axes labels): | ||
| 20 | + | ||
| 21 | + .. plot:: | ||
| 22 | + :include-source: True | ||
| 23 | + | ||
| 24 | + axd = plt.figure(constrained_layout=True).subplot_mosaic( | ||
| 25 | + """ | ||
| 26 | + TTE | ||
| 27 | + L.E | ||
| 28 | + """) | ||
| 29 | + for k, ax in axd.items(): | ||
| 30 | + ax.text(0.5, 0.5, k, | ||
| 31 | + ha='center', va='center', fontsize=36, | ||
| 32 | + color='darkgrey') | ||
| 33 | + | ||
| 34 | + | ||
| 35 | + | ||
| 36 | + See :ref:`sphx_glr_tutorials_provisional_mosaic.py` for more | ||
| 37 | + details and examples. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -166,6 +166,23 @@ def _make_twin_axes(self, *args, **kwargs): | |||
| 166 | 166 | self._twinned_axes.join(self, twin) | |
| 167 | 167 | return twin | |
| 168 | 168 | ||
| 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 | + | ||
| 169 | 186 | ||
| 170 | 187 | # this here to support cartopy which was using a private part of the | |
| 171 | 188 | # API to register their Axes subclasses. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,6 +8,7 @@ | |||
| 8 | 8 | Control the default spacing between subplots. | |
| 9 | 9 | """ | |
| 10 | 10 | ||
| 11 | + import inspect | ||
| 11 | 12 | import logging | |
| 12 | 13 | from numbers import Integral | |
| 13 | 14 | ||
@@ -1522,6 +1523,204 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, | |||
| 1522 | 1523 | .subplots(sharex=sharex, sharey=sharey, squeeze=squeeze, | |
| 1523 | 1524 | subplot_kw=subplot_kw)) | |
| 1524 | 1525 | ||
| 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 panel', 'A panel', 'edge'], | ||
| 1552 | + ['C panel', '.', 'edge']] | ||
| 1553 | + | ||
| 1554 | + Produces 4 axes: | ||
| 1555 | + | ||
| 1556 | + - 'A panel' 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 panel' 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 | + Any of the entries in the layout can be a list of lists | ||
| 1562 | + of the same form to create nested layouts. | ||
| 1563 | + | ||
| 1564 | + If input is a str, then it must be of the form :: | ||
| 1565 | + | ||
| 1566 | + ''' | ||
| 1567 | + AAE | ||
| 1568 | + C.E | ||
| 1569 | + ''' | ||
| 1570 | + | ||
| 1571 | + where each character is a column and each line is a row. | ||
| 1572 | + This only allows only single character Axes labels and does | ||
| 1573 | + not allow nesting but is very terse. | ||
| 1574 | + | ||
| 1575 | + subplot_kw : dict, optional | ||
| 1576 | + Dictionary with keywords passed to the `.Figure.add_subplot` call | ||
| 1577 | + used to create each subplot. | ||
| 1578 | + | ||
| 1579 | + gridspec_kw : dict, optional | ||
| 1580 | + Dictionary with keywords passed to the `.GridSpec` constructor used | ||
| 1581 | + to create the grid the subplots are placed on. | ||
| 1582 | + | ||
| 1583 | + empty_sentinel : object, optional | ||
| 1584 | + Entry in the layout to mean "leave this space empty". Defaults | ||
| 1585 | + to ``'.'``. Note, if *layout* is a string, it is processed via | ||
| 1586 | + `inspect.cleandoc` to remove leading white space, which may | ||
| 1587 | + interfere with using white-space as the empty sentinel. | ||
| 1588 | + | ||
| 1589 | + Returns | ||
| 1590 | + ------- | ||
| 1591 | + dict[label, Axes] | ||
| 1592 | + A dictionary mapping the labels to the Axes objects. | ||
| 1593 | + | ||
| 1594 | + """ | ||
| 1595 | + subplot_kw = subplot_kw or {} | ||
| 1596 | + gridspec_kw = gridspec_kw or {} | ||
| 1597 | + # special-case string input | ||
| 1598 | + if isinstance(layout, str): | ||
| 1599 | + layout = self._normalize_grid_string(layout) | ||
| 1600 | + | ||
| 1601 | + def _make_array(inp): | ||
| 1602 | + """ | ||
| 1603 | + Convert input into 2D array | ||
| 1604 | + | ||
| 1605 | + We need to have this internal function rather than | ||
| 1606 | + ``np.asarray(..., dtype=object)`` so that a list of lists | ||
| 1607 | + of lists does not get converted to an array of dimension > | ||
| 1608 | + 2 | ||
| 1609 | + | ||
| 1610 | + Returns | ||
| 1611 | + ------- | ||
| 1612 | + 2D object array | ||
| 1613 | + | ||
| 1614 | + """ | ||
| 1615 | + r0, *rest = inp | ||
| 1616 | + for j, r in enumerate(rest, start=1): | ||
| 1617 | + if len(r0) != len(r): | ||
| 1618 | + raise ValueError( | ||
| 1619 | + "All of the rows must be the same length, however " | ||
| 1620 | + f"the first row ({r0!r}) has length {len(r0)} " | ||
| 1621 | + f"and row {j} ({r!r}) has length {len(r)}." | ||
| 1622 | + ) | ||
| 1623 | + out = np.zeros((len(inp), len(r0)), dtype=object) | ||
| 1624 | + for j, r in enumerate(inp): | ||
| 1625 | + for k, v in enumerate(r): | ||
| 1626 | + out[j, k] = v | ||
| 1627 | + return out | ||
| 1628 | + | ||
| 1629 | + def _identify_keys_and_nested(layout): | ||
| 1630 | + """ | ||
| 1631 | + Given a 2D object array, identify unique IDs and nested layouts | ||
| 1632 | + | ||
| 1633 | + Parameters | ||
| 1634 | + ---------- | ||
| 1635 | + layout : 2D numpy object array | ||
| 1636 | + | ||
| 1637 | + Returns | ||
| 1638 | + ------- | ||
| 1639 | + unique_ids : Set[object] | ||
| 1640 | + The unique non-sub layout entries in this layout | ||
| 1641 | + nested : Dict[Tuple[int, int]], 2D object array | ||
| 1642 | + """ | ||
| 1643 | + unique_ids = set() | ||
| 1644 | + nested = {} | ||
| 1645 | + for j, row in enumerate(layout): | ||
| 1646 | + for k, v in enumerate(row): | ||
| 1647 | + if v == empty_sentinel: | ||
| 1648 | + continue | ||
| 1649 | + elif not cbook.is_scalar_or_string(v): | ||
| 1650 | + nested[(j, k)] = _make_array(v) | ||
| 1651 | + else: | ||
| 1652 | + unique_ids.add(v) | ||
| 1653 | + | ||
| 1654 | + return unique_ids, nested | ||
| 1655 | + | ||
| 1656 | + def _do_layout(gs, layout, unique_ids, nested): | ||
| 1657 | + """ | ||
| 1658 | + Recursively do the layout. | ||
| 1659 | + | ||
| 1660 | + Parameters | ||
| 1661 | + ---------- | ||
| 1662 | + gs : GridSpec | ||
| 1663 | + | ||
| 1664 | + layout : 2D object array | ||
| 1665 | + The input converted to a 2D numpy array for this level. | ||
| 1666 | + | ||
| 1667 | + unique_ids : Set[object] | ||
| 1668 | + The identified scalar labels at this level of nesting. | ||
| 1669 | + | ||
| 1670 | + nested : Dict[Tuple[int, int]], 2D object array | ||
| 1671 | + The identified nested layouts if any. | ||
| 1672 | + | ||
| 1673 | + Returns | ||
| 1674 | + ------- | ||
| 1675 | + Dict[label, Axes] | ||
| 1676 | + A flat dict of all of the Axes created. | ||
| 1677 | + """ | ||
| 1678 | + rows, cols = layout.shape | ||
| 1679 | + output = dict() | ||
| 1680 | + | ||
| 1681 | + # create the Axes at this level of nesting | ||
| 1682 | + for name in unique_ids: | ||
| 1683 | + indx = np.argwhere(layout == name) | ||
| 1684 | + start_row, start_col = np.min(indx, axis=0) | ||
| 1685 | + end_row, end_col = np.max(indx, axis=0) + 1 | ||
| 1686 | + slc = (slice(start_row, end_row), slice(start_col, end_col)) | ||
| 1687 | + | ||
| 1688 | + if (layout[slc] != name).any(): | ||
| 1689 | + raise ValueError( | ||
| 1690 | + f"While trying to layout\n{layout!r}\n" | ||
| 1691 | + f"we found that the label {name!r} specifies a " | ||
| 1692 | + "non-rectangular or non-contiguous area.") | ||
| 1693 | + | ||
| 1694 | + ax = self.add_subplot( | ||
| 1695 | + gs[slc], **{'label': str(name), **subplot_kw} | ||
| 1696 | + ) | ||
| 1697 | + output[name] = ax | ||
| 1698 | + | ||
| 1699 | + # do any sub-layouts | ||
| 1700 | + for (j, k), nested_layout in nested.items(): | ||
| 1701 | + rows, cols = nested_layout.shape | ||
| 1702 | + nested_output = _do_layout( | ||
| 1703 | + gs[j, k].subgridspec(rows, cols, **gridspec_kw), | ||
| 1704 | + nested_layout, | ||
| 1705 | + *_identify_keys_and_nested(nested_layout) | ||
| 1706 | + ) | ||
| 1707 | + overlap = set(output) & set(nested_output) | ||
| 1708 | + if overlap: | ||
| 1709 | + raise ValueError(f"There are duplicate keys {overlap} " | ||
| 1710 | + f"between the outer layout\n{layout!r}\n" | ||
| 1711 | + f"and the nested layout\n{nested_layout}") | ||
| 1712 | + output.update(nested_output) | ||
| 1713 | + return output | ||
| 1714 | + | ||
| 1715 | + layout = _make_array(layout) | ||
| 1716 | + rows, cols = layout.shape | ||
| 1717 | + gs = self.add_gridspec(rows, cols, **gridspec_kw) | ||
| 1718 | + ret = _do_layout(gs, layout, *_identify_keys_and_nested(layout)) | ||
| 1719 | + for k, ax in ret.items(): | ||
| 1720 | + if isinstance(k, str): | ||
| 1721 | + ax.set_label(k) | ||
| 1722 | + return ret | ||
| 1723 | + | ||
| 1525 | 1724 | def delaxes(self, ax): | |
| 1526 | 1725 | """ | |
| 1527 | 1726 | Remove the `~.axes.Axes` *ax* from the figure; update the current axes. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1274,6 +1274,87 @@ def subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True, | |||
| 1274 | 1274 | return fig, axs | |
| 1275 | 1275 | ||
| 1276 | 1276 | ||
| 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 ASCII art or nested lists. | ||
| 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 panel', 'A panel', 'edge'], | ||
| 1298 | + ['C panel', '.', 'edge']] | ||
| 1299 | + | ||
| 1300 | + Produces 4 axes: | ||
| 1301 | + | ||
| 1302 | + - 'A panel' 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 panel' 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 | + Any of the entries in the layout can be a list of lists | ||
| 1308 | + of the same form to create nested layouts. | ||
| 1309 | + | ||
| 1310 | + If input is a str, then it must be of the form :: | ||
| 1311 | + | ||
| 1312 | + ''' | ||
| 1313 | + AAE | ||
| 1314 | + C.E | ||
| 1315 | + ''' | ||
| 1316 | + | ||
| 1317 | + where each character is a column and each line is a row. | ||
| 1318 | + This only allows only single character Axes labels and does | ||
| 1319 | + not allow nesting but is very terse. | ||
| 1320 | + | ||
| 1321 | + subplot_kw : dict, optional | ||
| 1322 | + Dictionary with keywords passed to the `.Figure.add_subplot` call | ||
| 1323 | + used to create each subplot. | ||
| 1324 | + | ||
| 1325 | + gridspec_kw : dict, optional | ||
| 1326 | + Dictionary with keywords passed to the `.GridSpec` constructor used | ||
| 1327 | + to create the grid the subplots are placed on. | ||
| 1328 | + | ||
| 1329 | + empty_sentinel : object, optional | ||
| 1330 | + Entry in the layout to mean "leave this space empty". Defaults | ||
| 1331 | + to ``'.'``. Note, if *layout* is a string, it is processed via | ||
| 1332 | + `inspect.cleandoc` to remove leading white space, which may | ||
| 1333 | + interfere with using white-space as the empty sentinel. | ||
| 1334 | + | ||
| 1335 | + **fig_kw | ||
| 1336 | + All additional keyword arguments are passed to the | ||
| 1337 | + `.pyplot.figure` call. | ||
| 1338 | + | ||
| 1339 | + Returns | ||
| 1340 | + ------- | ||
| 1341 | + fig : `~.figure.Figure` | ||
| 1342 | + The new figure | ||
| 1343 | + | ||
| 1344 | + dict[label, Axes] | ||
| 1345 | + A dictionary mapping the labels to the Axes objects. | ||
| 1346 | + | ||
| 1347 | + """ | ||
| 1348 | + fig = figure(**fig_kw) | ||
| 1349 | + ax_dict = fig.subplot_mosaic( | ||
| 1350 | + layout, | ||
| 1351 | + subplot_kw=subplot_kw, | ||
| 1352 | + gridspec_kw=gridspec_kw, | ||
| 1353 | + empty_sentinel=empty_sentinel | ||
| 1354 | + ) | ||
| 1355 | + return fig, ax_dict | ||
| 1356 | + | ||
| 1357 | + | ||
| 1277 | 1358 | def subplot2grid(shape, loc, rowspan=1, colspan=1, fig=None, **kwargs): | |
| 1278 | 1359 | """ | |
| 1279 | 1360 | Create a subplot at a specific location inside a regular grid. | |
| Back | FazBrowse Home | New Git URL |
0 commit comments