diff --git a/doc/users/next_whats_new/2020-05-tac.rst b/doc/users/next_whats_new/2020-05-tac.rst new file mode 100644 index 000000000000..de3427161951 --- /dev/null +++ b/doc/users/next_whats_new/2020-05-tac.rst @@ -0,0 +1,37 @@ +Add API for composing semantic axes layouts from text or nested lists +--------------------------------------------------------------------- + +The `.Figure` class has a provisional method to generate complex grids +of named `.axes.Axes` based on nested list input or ASCII art: + +.. plot:: + :include-source: True + + axd = plt.figure(constrained_layout=True).subplot_mosaic( + [["Top", "Top", "Edge"], + ["Left", ".", "Edge"]] + ) + for k, ax in axd.items(): + ax.text(0.5, 0.5, k, + ha='center', va='center', fontsize=36, + color='darkgrey') + +or as a string (with single-character Axes labels): + +.. plot:: + :include-source: True + + axd = plt.figure(constrained_layout=True).subplot_mosaic( + """ + TTE + L.E + """) + for k, ax in axd.items(): + ax.text(0.5, 0.5, k, + ha='center', va='center', fontsize=36, + color='darkgrey') + + + +See :ref:`sphx_glr_tutorials_provisional_mosaic.py` for more +details and examples. diff --git a/lib/matplotlib/axes/_subplots.py b/lib/matplotlib/axes/_subplots.py index 3fa33fbdfeaa..1b5b181149c4 100644 --- a/lib/matplotlib/axes/_subplots.py +++ b/lib/matplotlib/axes/_subplots.py @@ -166,6 +166,23 @@ def _make_twin_axes(self, *args, **kwargs): self._twinned_axes.join(self, twin) return twin + def __repr__(self): + fields = [] + if self.get_label(): + fields += [f"label={self.get_label()!r}"] + titles = [] + for k in ["left", "center", "right"]: + title = self.get_title(loc=k) + if title: + titles.append(f"{k!r}:{title!r}") + if titles: + fields += ["title={" + ",".join(titles) + "}"] + if self.get_xlabel(): + fields += [f"xlabel={self.get_xlabel()!r}"] + if self.get_ylabel(): + fields += [f"ylabel={self.get_ylabel()!r}"] + return f"<{self.__class__.__name__}:" + ", ".join(fields) + ">" + # this here to support cartopy which was using a private part of the # API to register their Axes subclasses. diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 8b2cd2f6d815..c456abe5cb30 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -8,6 +8,7 @@ Control the default spacing between subplots. """ +import inspect import logging from numbers import Integral @@ -1522,6 +1523,204 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, .subplots(sharex=sharex, sharey=sharey, squeeze=squeeze, subplot_kw=subplot_kw)) + @staticmethod + def _normalize_grid_string(layout): + layout = inspect.cleandoc(layout) + return [list(ln) for ln in layout.strip('\n').split('\n')] + + def subplot_mosaic(self, layout, *, subplot_kw=None, gridspec_kw=None, + empty_sentinel='.'): + """ + Build a layout of Axes based on ASCII art or nested lists. + + This is a helper function to build complex GridSpec layouts visually. + + .. note :: + + This API is provisional and may be revised in the future based on + early user feedback. + + + Parameters + ---------- + layout : list of list of {hashable or nested} or str + + A visual layout of how you want your Axes to be arranged + labeled as strings. For example :: + + x = [['A panel', 'A panel', 'edge'], + ['C panel', '.', 'edge']] + + Produces 4 axes: + + - 'A panel' which is 1 row high and spans the first two columns + - 'edge' which is 2 rows high and is on the right edge + - 'C panel' which in 1 row and 1 column wide in the bottom left + - a blank space 1 row and 1 column wide in the bottom center + + Any of the entries in the layout can be a list of lists + of the same form to create nested layouts. + + If input is a str, then it must be of the form :: + + ''' + AAE + C.E + ''' + + where each character is a column and each line is a row. + This only allows only single character Axes labels and does + not allow nesting but is very terse. + + subplot_kw : dict, optional + Dictionary with keywords passed to the `.Figure.add_subplot` call + used to create each subplot. + + gridspec_kw : dict, optional + Dictionary with keywords passed to the `.GridSpec` constructor used + to create the grid the subplots are placed on. + + empty_sentinel : object, optional + Entry in the layout to mean "leave this space empty". Defaults + to ``'.'``. Note, if *layout* is a string, it is processed via + `inspect.cleandoc` to remove leading white space, which may + interfere with using white-space as the empty sentinel. + + Returns + ------- + dict[label, Axes] + A dictionary mapping the labels to the Axes objects. + + """ + subplot_kw = subplot_kw or {} + gridspec_kw = gridspec_kw or {} + # special-case string input + if isinstance(layout, str): + layout = self._normalize_grid_string(layout) + + def _make_array(inp): + """ + Convert input into 2D array + + We need to have this internal function rather than + ``np.asarray(..., dtype=object)`` so that a list of lists + of lists does not get converted to an array of dimension > + 2 + + Returns + ------- + 2D object array + + """ + r0, *rest = inp + for j, r in enumerate(rest, start=1): + if len(r0) != len(r): + raise ValueError( + "All of the rows must be the same length, however " + f"the first row ({r0!r}) has length {len(r0)} " + f"and row {j} ({r!r}) has length {len(r)}." + ) + out = np.zeros((len(inp), len(r0)), dtype=object) + for j, r in enumerate(inp): + for k, v in enumerate(r): + out[j, k] = v + return out + + def _identify_keys_and_nested(layout): + """ + Given a 2D object array, identify unique IDs and nested layouts + + Parameters + ---------- + layout : 2D numpy object array + + Returns + ------- + unique_ids : Set[object] + The unique non-sub layout entries in this layout + nested : Dict[Tuple[int, int]], 2D object array + """ + unique_ids = set() + nested = {} + for j, row in enumerate(layout): + for k, v in enumerate(row): + if v == empty_sentinel: + continue + elif not cbook.is_scalar_or_string(v): + nested[(j, k)] = _make_array(v) + else: + unique_ids.add(v) + + return unique_ids, nested + + def _do_layout(gs, layout, unique_ids, nested): + """ + Recursively do the layout. + + Parameters + ---------- + gs : GridSpec + + layout : 2D object array + The input converted to a 2D numpy array for this level. + + unique_ids : Set[object] + The identified scalar labels at this level of nesting. + + nested : Dict[Tuple[int, int]], 2D object array + The identified nested layouts if any. + + Returns + ------- + Dict[label, Axes] + A flat dict of all of the Axes created. + """ + rows, cols = layout.shape + output = dict() + + # create the Axes at this level of nesting + for name in unique_ids: + indx = np.argwhere(layout == name) + start_row, start_col = np.min(indx, axis=0) + end_row, end_col = np.max(indx, axis=0) + 1 + slc = (slice(start_row, end_row), slice(start_col, end_col)) + + if (layout[slc] != name).any(): + raise ValueError( + f"While trying to layout\n{layout!r}\n" + f"we found that the label {name!r} specifies a " + "non-rectangular or non-contiguous area.") + + ax = self.add_subplot( + gs[slc], **{'label': str(name), **subplot_kw} + ) + output[name] = ax + + # do any sub-layouts + for (j, k), nested_layout in nested.items(): + rows, cols = nested_layout.shape + nested_output = _do_layout( + gs[j, k].subgridspec(rows, cols, **gridspec_kw), + nested_layout, + *_identify_keys_and_nested(nested_layout) + ) + overlap = set(output) & set(nested_output) + if overlap: + raise ValueError(f"There are duplicate keys {overlap} " + f"between the outer layout\n{layout!r}\n" + f"and the nested layout\n{nested_layout}") + output.update(nested_output) + return output + + layout = _make_array(layout) + rows, cols = layout.shape + gs = self.add_gridspec(rows, cols, **gridspec_kw) + ret = _do_layout(gs, layout, *_identify_keys_and_nested(layout)) + for k, ax in ret.items(): + if isinstance(k, str): + ax.set_label(k) + return ret + def delaxes(self, ax): """ Remove the `~.axes.Axes` *ax* from the figure; update the current axes. diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 2b7ed189a8ff..1314f055ec5d 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1274,6 +1274,87 @@ def subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True, return fig, axs +def subplot_mosaic(layout, *, subplot_kw=None, gridspec_kw=None, + empty_sentinel='.', **fig_kw): + """ + Build a layout of Axes based on ASCII art or nested lists. + + This is a helper function to build complex GridSpec layouts visually. + + .. note :: + + This API is provisional and may be revised in the future based on + early user feedback. + + + Parameters + ---------- + layout : list of list of {hashable or nested} or str + + A visual layout of how you want your Axes to be arranged + labeled as strings. For example :: + + x = [['A panel', 'A panel', 'edge'], + ['C panel', '.', 'edge']] + + Produces 4 axes: + + - 'A panel' which is 1 row high and spans the first two columns + - 'edge' which is 2 rows high and is on the right edge + - 'C panel' which in 1 row and 1 column wide in the bottom left + - a blank space 1 row and 1 column wide in the bottom center + + Any of the entries in the layout can be a list of lists + of the same form to create nested layouts. + + If input is a str, then it must be of the form :: + + ''' + AAE + C.E + ''' + + where each character is a column and each line is a row. + This only allows only single character Axes labels and does + not allow nesting but is very terse. + + subplot_kw : dict, optional + Dictionary with keywords passed to the `.Figure.add_subplot` call + used to create each subplot. + + gridspec_kw : dict, optional + Dictionary with keywords passed to the `.GridSpec` constructor used + to create the grid the subplots are placed on. + + empty_sentinel : object, optional + Entry in the layout to mean "leave this space empty". Defaults + to ``'.'``. Note, if *layout* is a string, it is processed via + `inspect.cleandoc` to remove leading white space, which may + interfere with using white-space as the empty sentinel. + + **fig_kw + All additional keyword arguments are passed to the + `.pyplot.figure` call. + + Returns + ------- + fig : `~.figure.Figure` + The new figure + + dict[label, Axes] + A dictionary mapping the labels to the Axes objects. + + """ + fig = figure(**fig_kw) + ax_dict = fig.subplot_mosaic( + layout, + subplot_kw=subplot_kw, + gridspec_kw=gridspec_kw, + empty_sentinel=empty_sentinel + ) + return fig, ax_dict + + def subplot2grid(shape, loc, rowspan=1, colspan=1, fig=None, **kwargs): """ Create a subplot at a specific location inside a regular grid. diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 36f0c2f76ca0..9eec9cb97a7c 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -588,3 +588,186 @@ def test_animated_with_canvas_change(fig_test, fig_ref): ax_test = fig_test.subplots() ax_test.plot(range(5), animated=True) + + +class TestSubplotMosaic: + @check_figures_equal(extensions=["png"]) + @pytest.mark.parametrize( + "x", [[["A", "A", "B"], ["C", "D", "B"]], [[1, 1, 2], [3, 4, 2]]] + ) + def test_basic(self, fig_test, fig_ref, x): + grid_axes = fig_test.subplot_mosaic(x) + + for k, ax in grid_axes.items(): + ax.set_title(k) + + labels = sorted(np.unique(x)) + + assert len(labels) == len(grid_axes) + + gs = fig_ref.add_gridspec(2, 3) + axA = fig_ref.add_subplot(gs[:1, :2]) + axA.set_title(labels[0]) + + axB = fig_ref.add_subplot(gs[:, 2]) + axB.set_title(labels[1]) + + axC = fig_ref.add_subplot(gs[1, 0]) + axC.set_title(labels[2]) + + axD = fig_ref.add_subplot(gs[1, 1]) + axD.set_title(labels[3]) + + @check_figures_equal(extensions=["png"]) + def test_all_nested(self, fig_test, fig_ref): + x = [["A", "B"], ["C", "D"]] + y = [["E", "F"], ["G", "H"]] + + fig_ref.set_constrained_layout(True) + fig_test.set_constrained_layout(True) + + grid_axes = fig_test.subplot_mosaic([[x, y]]) + for ax in grid_axes.values(): + ax.set_title(ax.get_label()) + + gs = fig_ref.add_gridspec(1, 2) + gs_left = gs[0, 0].subgridspec(2, 2) + for j, r in enumerate(x): + for k, label in enumerate(r): + fig_ref.add_subplot(gs_left[j, k]).set_title(label) + + gs_right = gs[0, 1].subgridspec(2, 2) + for j, r in enumerate(y): + for k, label in enumerate(r): + fig_ref.add_subplot(gs_right[j, k]).set_title(label) + + @check_figures_equal(extensions=["png"]) + def test_nested(self, fig_test, fig_ref): + + fig_ref.set_constrained_layout(True) + fig_test.set_constrained_layout(True) + + x = [["A", "B"], ["C", "D"]] + + y = [["F"], [x]] + + grid_axes = fig_test.subplot_mosaic(y) + + for k, ax in grid_axes.items(): + ax.set_title(k) + + gs = fig_ref.add_gridspec(2, 1) + + gs_n = gs[1, 0].subgridspec(2, 2) + + axA = fig_ref.add_subplot(gs_n[0, 0]) + axA.set_title("A") + + axB = fig_ref.add_subplot(gs_n[0, 1]) + axB.set_title("B") + + axC = fig_ref.add_subplot(gs_n[1, 0]) + axC.set_title("C") + + axD = fig_ref.add_subplot(gs_n[1, 1]) + axD.set_title("D") + + axF = fig_ref.add_subplot(gs[0, 0]) + axF.set_title("F") + + @check_figures_equal(extensions=["png"]) + def test_nested_tuple(self, fig_test, fig_ref): + x = [["A", "B", "B"], ["C", "C", "D"]] + xt = (("A", "B", "B"), ("C", "C", "D")) + + fig_ref.subplot_mosaic([["F"], [x]]) + fig_test.subplot_mosaic([["F"], [xt]]) + + @check_figures_equal(extensions=["png"]) + @pytest.mark.parametrize( + "x, empty_sentinel", + [ + ([["A", None], [None, "B"]], None), + ([["A", "."], [".", "B"]], "SKIP"), + ([["A", 0], [0, "B"]], 0), + ([[1, None], [None, 2]], None), + ([[1, "."], [".", 2]], "SKIP"), + ([[1, 0], [0, 2]], 0), + ], + ) + def test_empty(self, fig_test, fig_ref, x, empty_sentinel): + if empty_sentinel != "SKIP": + kwargs = {"empty_sentinel": empty_sentinel} + else: + kwargs = {} + grid_axes = fig_test.subplot_mosaic(x, **kwargs) + + for k, ax in grid_axes.items(): + ax.set_title(k) + + labels = sorted( + {name for row in x for name in row} - {empty_sentinel, "."} + ) + + assert len(labels) == len(grid_axes) + + gs = fig_ref.add_gridspec(2, 2) + axA = fig_ref.add_subplot(gs[0, 0]) + axA.set_title(labels[0]) + + axB = fig_ref.add_subplot(gs[1, 1]) + axB.set_title(labels[1]) + + @check_figures_equal(extensions=["png"]) + @pytest.mark.parametrize("subplot_kw", [{}, {"projection": "polar"}, None]) + def test_subplot_kw(self, fig_test, fig_ref, subplot_kw): + x = [[1, 2]] + grid_axes = fig_test.subplot_mosaic(x, subplot_kw=subplot_kw) + subplot_kw = subplot_kw or {} + + gs = fig_ref.add_gridspec(1, 2) + axA = fig_ref.add_subplot(gs[0, 0], **subplot_kw) + + axB = fig_ref.add_subplot(gs[0, 1], **subplot_kw) + + @check_figures_equal(extensions=["png"]) + @pytest.mark.parametrize("str_pattern", + ["AAA\nBBB", "\nAAA\nBBB\n", "ABC\nDEF"] + ) + def test_single_str_input(self, fig_test, fig_ref, str_pattern): + grid_axes = fig_test.subplot_mosaic(str_pattern) + + grid_axes = fig_ref.subplot_mosaic( + [list(ln) for ln in str_pattern.strip().split("\n")] + ) + + @pytest.mark.parametrize( + "x,match", + [ + ( + [["A", "."], [".", "A"]], + ( + "(?m)we found that the label .A. specifies a " + + "non-rectangular or non-contiguous area." + ), + ), + ( + [["A", "B"], [None, [["A", "B"], ["C", "D"]]]], + "There are duplicate keys .* between the outer layout", + ), + ("AAA\nc\nBBB", "All of the rows must be the same length"), + ( + [["A", [["B", "C"], ["D"]]], ["E", "E"]], + "All of the rows must be the same length", + ), + ], + ) + def test_fail(self, x, match): + fig = plt.figure() + with pytest.raises(ValueError, match=match): + fig.subplot_mosaic(x) + + @check_figures_equal(extensions=["png"]) + def test_hashable_keys(self, fig_test, fig_ref): + fig_test.subplot_mosaic([[object(), object()]]) + fig_ref.subplot_mosaic([["A", "B"]]) diff --git a/tutorials/provisional/README.txt b/tutorials/provisional/README.txt new file mode 100644 index 000000000000..cf2296f05369 --- /dev/null +++ b/tutorials/provisional/README.txt @@ -0,0 +1,14 @@ +.. _tutorials-provisional: + +Provisional +----------- + +These tutorials cover proposed APIs of any complexity. These are here +to document features that we have released, but want to get user +feedback on before committing to them. Please have a look, try them +out and give us feedback on `gitter +`__, `discourse +`__, or the `the mailing list +`__! But, +be aware that we may change the APIs without warning in subsequent +versions. diff --git a/tutorials/provisional/mosaic.py b/tutorials/provisional/mosaic.py new file mode 100644 index 000000000000..d142b5686cf9 --- /dev/null +++ b/tutorials/provisional/mosaic.py @@ -0,0 +1,302 @@ +""" +======================================= +Complex and semantic figure composition +======================================= + +.. warning:: + + This tutorial documents experimental / provisional API. + We are releasing this in v3.3 to get user feedback. We may + make breaking changes in future versions with no warning. + + +Laying out Axes in a Figure in a non uniform grid can be both tedious +and verbose. For dense, even grids we have `.Figure.subplots` but for +more complex layouts, such as Axes that span multiple columns / rows +of the layout or leave some areas of the Figure blank, you can use +`.gridspec.GridSpec` (see :doc:`/tutorials/intermediate/gridspec`) or +manually place your axes. `.Figure.subplot_mosaic` aims to provide an +interface to visually lay out your axes (as either ASCII art or nested +lists) to streamline this process. + +This interface naturally supports naming your axes. +`.Figure.subplot_mosaic` returns a dictionary keyed on the +labels used to lay out the Figure. By returning data structures with +names, it is easier to write plotting code that is independent of the +Figure layout. + + +This is inspired by a `proposed MEP +`__ and the +`patchwork `__ library for R. +While we do not implement the operator overloading style, we do +provide a Pythonic API for specifying (nested) Axes layouts. + +""" +import matplotlib.pyplot as plt +import numpy as np + + +# Helper function used for visualization in the following examples +def identify_axes(ax_dict, fontsize=48): + """ + Helper to identify the Axes in the examples below. + + Draws the label in a large font in the center of the Axes. + + Parameters + ---------- + ax_dict : Dict[str, Axes] + Mapping between the title / label and the Axes. + + fontsize : int, optional + How big the label should be + """ + kw = dict(ha="center", va="center", fontsize=fontsize, color="darkgrey") + for k, ax in ax_dict.items(): + ax.text(0.5, 0.5, k, transform=ax.transAxes, **kw) +############################################################################### +# If we want a 2x2 grid we can use `.Figure.subplots` which returns a 2D array +# of `.axes.Axes` which we can index into to do our plotting. +np.random.seed(19680801) +hist_data = np.random.randn(1_500) + + +fig = plt.figure(constrained_layout=True) +ax_array = fig.subplots(2, 2, squeeze=False) + +ax_array[0, 0].bar(['a', 'b', 'c'], [5, 7, 9]) +ax_array[0, 1].plot([1, 2, 3]) +ax_array[1, 0].hist(hist_data, bins='auto') +ax_array[1, 1].imshow([[1, 2], [2, 1]]) + +identify_axes( + {(j, k): a for j, r in enumerate(ax_array) for k, a in enumerate(r)} +) + +############################################################################### +# Using `.Figure.subplot_mosaic` we can produce the same layout but give the +# axes semantic names + +fig = plt.figure(constrained_layout=True) +ax_dict = fig.subplot_mosaic( + [['bar', 'plot'], + ['hist', 'image']]) +ax_dict['bar'].bar(['a', 'b', 'c'], [5, 7, 9]) +ax_dict['plot'].plot([1, 2, 3]) +ax_dict['hist'].hist(hist_data) +ax_dict['image'].imshow([[1, 2], [2, 1]]) +identify_axes(ax_dict) + +############################################################################### +# A key difference between `.Figure.subplots` and +# `.Figure.subplot_mosaic` is the return value. While the former +# returns an array for index access, the latter returns a dictionary +# mapping the labels to the `.axes.Axes` instances created + +print(ax_dict) + + +############################################################################### +# String short-hand +# ================= +# +# By restricting our axes labels to single characters we can use Using we can +# "draw" the Axes we want as "ASCII art". The following + + +layout = """ + AB + CD + """ + +############################################################################### +# will give us 4 Axes laid out in a 2x2 grid and generates the same +# figure layout as above (but now labeled with ``{"A", "B", "C", +# "D"}`` rather than ``{"bar", "plot", "hist", "image"}``). + +fig = plt.figure(constrained_layout=True) +ax_dict = fig.subplot_mosaic(layout) +identify_axes(ax_dict) + + +############################################################################### +# Something we can do with `.Figure.subplot_mosaic` that you can not +# do with `.Figure.subplots` is specify that an Axes should span +# several rows or columns. +# +# If we want to re-arrange our four Axes to have C be a horizontal +# span on the bottom and D be a vertical span on the right we would do + +axd = plt.figure(constrained_layout=True).subplot_mosaic( + """ + ABD + CCD + """ +) +identify_axes(axd) + +############################################################################### +# If we do not want to fill in all the spaces in the Figure with Axes, +# we can specify some spaces in the grid to be blank + + +axd = plt.figure(constrained_layout=True).subplot_mosaic( + """ + A.C + BBB + .D. + """ +) +identify_axes(axd) + + +############################################################################### +# If we prefer to use another character (rather than a period ``"."``) +# to mark the empty space, we can use *empty_sentinel* to specify the +# character to use. + +axd = plt.figure(constrained_layout=True).subplot_mosaic( + """ + aX + Xb + """, + empty_sentinel="X", +) +identify_axes(axd) + + +############################################################################### +# +# Internally there is no meaning attached to the letters we use, any +# Unicode code point is valid! + +axd = plt.figure(constrained_layout=True).subplot_mosaic( + """αб + ℝ☢""" +) +identify_axes(axd) + +############################################################################### +# It is not recommended to use white space as either a label or an +# empty sentinel with the string shorthand because it may be stripped +# while processing the input. +# +# Controlling layout and subplot creation +# ======================================= +# +# This feature is built on top of `.gridspec` and you can pass the +# keyword arguments through to the underlying `.gridspec.GridSpec` +# (the same as `.Figure.subplots`). +# +# In this case we want to use the input to specify the arrangement, +# but set the relative widths of the rows / columns via *gridspec_kw*. + + +axd = plt.figure(constrained_layout=True).subplot_mosaic( + """ + .a. + bAc + .d. + """, + gridspec_kw={ + # set the height ratios between the rows + "height_ratios": [1, 3.5, 1], + # set the width ratios between the columns + "width_ratios": [1, 3.5, 1], + }, +) +identify_axes(axd) + +############################################################################### +# Or use the {*left*, *right*, *bottom*, *top*} keyword arguments to +# position the overall layout to put multiple versions of the same +# layout in a figure + +layout = """AA + BC""" +fig = plt.figure() +axd = fig.subplot_mosaic( + layout, + gridspec_kw={ + "bottom": 0.25, + "top": 0.95, + "left": 0.1, + "right": 0.5, + "wspace": 0.5, + "hspace": 0.5, + }, +) +identify_axes(axd) + +axd = fig.subplot_mosaic( + layout, + gridspec_kw={ + "bottom": 0.05, + "top": 0.75, + "left": 0.6, + "right": 0.95, + "wspace": 0.5, + "hspace": 0.5, + }, +) +identify_axes(axd) + + +############################################################################### +# We can also pass through arguments used to create the subplots +# (again, the same as `.Figure.subplots`). + + +axd = plt.figure(constrained_layout=True).subplot_mosaic( + "AB", subplot_kw={"projection": "polar"} +) +identify_axes(axd) + + +############################################################################### +# Nested List input +# ================= +# +# Everything we can do with the string short-hand we can also do when +# passing in a list (internally we convert the string shorthand to a nested +# list), for example using spans, blanks, and *gridspec_kw*: + +axd = plt.figure(constrained_layout=True).subplot_mosaic( + [["main", "zoom"], + ["main", "BLANK"] + ], + empty_sentinel="BLANK", + gridspec_kw={"width_ratios": [2, 1]} +) +identify_axes(axd) + + +############################################################################### +# In addition, using the list input we can specify nested layouts. Any element +# of the inner list can be another set of nested lists: + +inner = [ + ["inner A"], + ["inner B"], +] + +outer_nested_layout = [ + ["main", inner], + ["bottom", "bottom"], +] +axd = plt.figure(constrained_layout=True).subplot_mosaic( + outer_nested_layout, empty_sentinel=None +) +identify_axes(axd, fontsize=36) + + +############################################################################### +# We can also pass in a 2D NumPy array to do things like +layout = np.zeros((4, 4), dtype=int) +for j in range(4): + layout[j, j] = j + 1 +axd = plt.figure(constrained_layout=True).subplot_mosaic( + layout, empty_sentinel=0 +) +identify_axes(axd)