|
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.
|
|
0 commit comments