From 2977f37d46bf5db2756279e6691b517714de3632 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 10 Jul 2018 13:52:51 -0700 Subject: [PATCH] ENH: Add gridspec method to figure, and subplotspecs --- doc/users/whats_new.rst | 20 ++- lib/matplotlib/figure.py | 47 ++++++ lib/matplotlib/gridspec.py | 41 +++++ .../tests/test_constrainedlayout.py | 6 +- .../intermediate/constrainedlayout_guide.py | 45 +++--- tutorials/intermediate/gridspec.py | 144 +++++++++++------- 6 files changed, 227 insertions(+), 76 deletions(-) diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index 967b82a7a8c2..dcd0c899b6dd 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -15,7 +15,7 @@ revision, see the :ref:`github-stats`. For a release, add a new section after this, then comment out the include and toctree below by indenting them. Uncomment them after the release. - .. include:: next_whats_new/README.rst + .. include:: next_whats_new/README.rst .. toctree:: :glob: :maxdepth: 1 @@ -190,6 +190,24 @@ specify a number that is close (i.e. ``ax.title.set_position(0.5, 1.01)``) and the title will not be moved via this algorithm. +New convenience methods for GridSpec +------------------------------------ + +There are new convenience methods for `.gridspec.GridSpec` and +`.gridspec.GridSpecFromSubplotSpec`. Instead of the former we can +now call `.Figure.add_gridspec` and for the latter `.SubplotSpec.subgridspec`. + +.. code-block:: python + + import matplotlib.pyplot as plt + + fig = plt.figure() + gs0 = fig.add_gridspec(3, 1) + ax1 = fig.add_subplot(gs0[0]) + ax2 = fig.add_subplot(gs0[1]) + gssub = gs0[2].subgridspec(1, 3) + for i in range(3): + fig.add_subplot(gssub[0, i]) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 481dc9c5e32f..ef400898d802 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -394,6 +394,9 @@ def __init__(self, self._align_xlabel_grp = cbook.Grouper() self._align_ylabel_grp = cbook.Grouper() + # list of child gridspecs for this figure + self._gridspecs = [] + # TODO: I'd like to dynamically add the _repr_html_ method # to the figure in the right context, but then IPython doesn't # use it, for some reason. @@ -1480,6 +1483,7 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, else: # this should turn constrained_layout off if we don't want it gs = GridSpec(nrows, ncols, figure=None, **gridspec_kw) + self._gridspecs.append(gs) # Create array to hold all axes. axarr = np.empty((nrows, ncols), dtype=object) @@ -2474,6 +2478,49 @@ def align_labels(self, axs=None): self.align_xlabels(axs=axs) self.align_ylabels(axs=axs) + def add_gridspec(self, nrows, ncols, **kwargs): + """ + Return a `.GridSpec` that has this figure as a parent. This allows + complex layout of axes in the figure. + + Parameters + ---------- + nrows : int + Number of rows in grid. + + ncols : int + Number or columns in grid. + + Returns + ------- + gridspec : `.GridSpec` + + Other Parameters + ---------------- + *kwargs* are passed to `.GridSpec`. + + See Also + -------- + matplotlib.pyplot.subplots + + Examples + -------- + Adding a subplot that spans two rows:: + + fig = plt.figure() + gs = fig.add_gridspec(2, 2) + ax1 = fig.add_subplot(gs[0, 0]) + ax2 = fig.add_subplot(gs[1, 0]) + # spans two rows: + ax3 = fig.add_subplot(gs[:, 1]) + + """ + + _ = kwargs.pop('figure', None) # pop in case user has added this... + gs = GridSpec(nrows=nrows, ncols=ncols, figure=self, **kwargs) + self._gridspecs.append(gs) + return gs + def figaspect(arg): """ diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index f85f061f0528..a9cfc5e9ffbc 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -498,3 +498,44 @@ def __eq__(self, other): def __hash__(self): return hash((self._gridspec, self.num1, self.num2)) + + def subgridspec(self, nrows, ncols, **kwargs): + """ + Return a `.GridSpecFromSubplotSpec` that has this subplotspec as + a parent. + + Parameters + ---------- + nrows : int + Number of rows in grid. + + ncols : int + Number or columns in grid. + + Returns + ------- + gridspec : `.GridSpec` + + Other Parameters + ---------------- + **kwargs + All other parameters are passed to `.GridSpec`. + + See Also + -------- + matplotlib.pyplot.subplots + + Examples + -------- + Adding three subplots in the space occupied by a single subplot:: + + fig = plt.figure() + gs0 = fig.add_gridspec(3, 1) + ax1 = fig.add_subplot(gs0[0]) + ax2 = fig.add_subplot(gs0[1]) + gssub = gs0[2].subgridspec(1, 3) + for i in range(3): + fig.add_subplot(gssub[0, i]) + """ + + return GridSpecFromSubplotSpec(nrows, ncols, self, **kwargs) diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index c1ad1050c374..0803504cea94 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -94,9 +94,9 @@ def test_constrained_layout5(): def test_constrained_layout6(): 'Test constrained_layout for nested gridspecs' fig = plt.figure(constrained_layout=True) - gs = gridspec.GridSpec(1, 2, figure=fig) - gsl = gridspec.GridSpecFromSubplotSpec(2, 2, gs[0]) - gsr = gridspec.GridSpecFromSubplotSpec(1, 2, gs[1]) + gs = fig.add_gridspec(1, 2, figure=fig) + gsl = gs[0].subgridspec(2, 2) + gsr = gs[1].subgridspec(1, 2) axsl = [] for gs in gsl: ax = fig.add_subplot(gs) diff --git a/tutorials/intermediate/constrainedlayout_guide.py b/tutorials/intermediate/constrainedlayout_guide.py index 144f4685c605..df3bcdf9517d 100644 --- a/tutorials/intermediate/constrainedlayout_guide.py +++ b/tutorials/intermediate/constrainedlayout_guide.py @@ -78,7 +78,7 @@ def example_plot(ax, fontsize=12, nodec=False): ax.set_yticklabels('') -fig, ax = plt.subplots() +fig, ax = plt.subplots(constrained_layout=False) example_plot(ax, fontsize=24) ############################################################################### @@ -334,8 +334,10 @@ def example_plot(ax, fontsize=12, nodec=False): # with :func:`~matplotlib.figure.Figure.subplots` or # :func:`~matplotlib.gridspec.GridSpec` and # :func:`~matplotlib.figure.Figure.add_subplot`. +# +# Note that in what follows ``constrained_layout=True`` -fig = plt.figure(constrained_layout=True) +fig = plt.figure() gs1 = gridspec.GridSpec(2, 1, figure=fig) ax1 = fig.add_subplot(gs1[0]) @@ -345,20 +347,21 @@ def example_plot(ax, fontsize=12, nodec=False): example_plot(ax2) ############################################################################### -# More complicated gridspec layouts are possible. +# More complicated gridspec layouts are possible. Note here we use the +# convenenience functions ``add_gridspec`` and ``subgridspec`` -fig = plt.figure(constrained_layout=True) +fig = plt.figure() -gs0 = gridspec.GridSpec(1, 2, figure=fig) +gs0 = fig.add_gridspec(1, 2) -gs1 = gridspec.GridSpecFromSubplotSpec(2, 1, gs0[0]) +gs1 = gs0[0].subgridspec(2, 1) ax1 = fig.add_subplot(gs1[0]) ax2 = fig.add_subplot(gs1[1]) example_plot(ax1) example_plot(ax2) -gs2 = gridspec.GridSpecFromSubplotSpec(3, 1, gs0[1]) +gs2 = gs0[1].subgridspec(3, 1) for ss in gs2: ax = fig.add_subplot(ss) @@ -373,9 +376,9 @@ def example_plot(ax, fontsize=12, nodec=False): # extent. If we want the top and bottom of the two grids to line up then # they need to be in the same gridspec: -fig = plt.figure(constrained_layout=True) +fig = plt.figure() -gs0 = gridspec.GridSpec(6, 2, figure=fig) +gs0 = fig.add_gridspec(6, 2) ax1 = fig.add_subplot(gs0[:3, 0]) ax2 = fig.add_subplot(gs0[3:, 0]) @@ -398,10 +401,10 @@ def example_plot(ax, fontsize=12, nodec=False): def docomplicated(suptitle=None): - fig = plt.figure(constrained_layout=True) - gs0 = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[1., 2.]) - gsl = gridspec.GridSpecFromSubplotSpec(2, 1, gs0[0]) - gsr = gridspec.GridSpecFromSubplotSpec(2, 2, gs0[1]) + fig = plt.figure() + gs0 = fig.add_gridspec(1, 2, figure=fig, width_ratios=[1., 2.]) + gsl = gs0[0].subgridspec(2, 1) + gsr = gs0[1].subgridspec(2, 2) for gs in gsl: ax = fig.add_subplot(gs) @@ -430,7 +433,7 @@ def docomplicated(suptitle=None): # no effect on it anymore. (Note that constrained_layout still leaves the # space for the axes that is moved). -fig, axs = plt.subplots(1, 2, constrained_layout=True) +fig, axs = plt.subplots(1, 2) example_plot(axs[0], fontsize=12) axs[1].set_position([0.2, 0.2, 0.4, 0.4]) @@ -444,7 +447,7 @@ def docomplicated(suptitle=None): from matplotlib.transforms import Bbox -fig, axs = plt.subplots(1, 2, constrained_layout=True) +fig, axs = plt.subplots(1, 2) example_plot(axs[0], fontsize=12) fig.execute_constrained_layout() # put into data-space: @@ -468,7 +471,7 @@ def docomplicated(suptitle=None): # to yield a nice layout: -fig = plt.figure(constrained_layout=True) +fig = plt.figure() ax1 = plt.subplot(221) ax2 = plt.subplot(223) @@ -481,8 +484,8 @@ def docomplicated(suptitle=None): ############################################################################### # Of course that layout is possible using a gridspec: -fig = plt.figure(constrained_layout=True) -gs = gridspec.GridSpec(2, 2, figure=fig) +fig = plt.figure() +gs = fig.add_gridspec(2, 2) ax1 = fig.add_subplot(gs[0, 0]) ax2 = fig.add_subplot(gs[1, 0]) @@ -497,7 +500,7 @@ def docomplicated(suptitle=None): # :func:`~matplotlib.pyplot.subplot2grid` doesn't work for the same reason: # each call creates a different parent gridspec. -fig = plt.figure(constrained_layout=True) +fig = plt.figure() ax1 = plt.subplot2grid((3, 3), (0, 0)) ax2 = plt.subplot2grid((3, 3), (0, 1), colspan=2) @@ -513,8 +516,8 @@ def docomplicated(suptitle=None): # The way to make this plot compatible with ``constrained_layout`` is again # to use ``gridspec`` directly -fig = plt.figure(constrained_layout=True) -gs = gridspec.GridSpec(3, 3, figure=fig) +fig = plt.figure() +gs = fig.add_gridspec(3, 3) ax1 = fig.add_subplot(gs[0, 0]) ax2 = fig.add_subplot(gs[0, 1:]) diff --git a/tutorials/intermediate/gridspec.py b/tutorials/intermediate/gridspec.py index 64f444e4c73a..301bf8d21b08 100644 --- a/tutorials/intermediate/gridspec.py +++ b/tutorials/intermediate/gridspec.py @@ -7,8 +7,9 @@ :func:`~matplotlib.pyplot.subplots` Perhaps the primary function used to create figures and axes. - It's also similar to :func:`~matplotlib.pyplot.subplot`, - but creates and places all axes on the figure at once. + It's also similar to :func:`.matplotlib.pyplot.subplot`, + but creates and places all axes on the figure at once. See also + `.matplotlib.Figure.subplots`. :class:`~matplotlib.gridspec.GridSpec` Specifies the geometry of the grid that a subplot will be @@ -20,12 +21,14 @@ Specifies the location of the subplot in the given *GridSpec*. :func:`~matplotlib.pyplot.subplot2grid` - A helper function that is similar to :func:`~matplotlib.pyplot.subplot`, + A helper function that is similar to + :func:`~matplotlib.pyplot.subplot`, but uses 0-based indexing and let subplot to occupy multiple cells. This function is not covered in this tutorial. """ +import matplotlib import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec @@ -33,15 +36,14 @@ # Basic Quickstart Guide # ====================== # -# These first two examples show how to create a basic 4-by-4 grid using +# These first two examples show how to create a basic 2-by-2 grid using # both :func:`~matplotlib.pyplot.subplots` and :mod:`~matplotlib.gridspec`. # # Using :func:`~matplotlib.pyplot.subplots` is quite simple. # It returns a :class:`~matplotlib.figure.Figure` instance and an array of # :class:`~matplotlib.axes.Axes` objects. -fig1, f1_axes = plt.subplots(ncols=2, nrows=2) -fig1.tight_layout() +fig1, f1_axes = plt.subplots(ncols=2, nrows=2, constrained_layout=True) ############################################################################ # For a simple use case such as this, :mod:`~matplotlib.gridspec` is @@ -53,36 +55,58 @@ # The elements of the gridspec are accessed in generally the same manner as # numpy arrays. -fig2 = plt.figure() +fig2 = plt.figure(constrained_layout=True) spec2 = gridspec.GridSpec(ncols=2, nrows=2) f2_ax1 = fig2.add_subplot(spec2[0, 0]) f2_ax2 = fig2.add_subplot(spec2[0, 1]) f2_ax3 = fig2.add_subplot(spec2[1, 0]) f2_ax4 = fig2.add_subplot(spec2[1, 1]) -fig2.tight_layout() ############################################################################# -# When you want to have subplots of different sizes, however, -# :mod:`~matplotlib.gridspec` becomes indispensable and provides a couple -# of options. -# The method shown here initializes a uniform grid specification, -# and then uses typical numpy indexing and slices to allocate multiple +# The power of gridspec comes in being able to create subplots that span +# rows and columns. Note the +# `Numpy slice `_ +# syntax for selecing the part of the gridspec each subplot will occupy. +# +# Note that we have also used the convenience method `.Figure.add_grisdpec` +# instead of `.gridspec.GridSpec`, potentially saving the user an import, +# and keeping the namespace cleaner. + +fig = plt.figure(constrained_layout=True) +gs = fig.add_gridspec(3, 3) +ax1 = fig.add_subplot(gs[0, :]) +ax1.set_title('gs[0, :]') +ax2 = fig.add_subplot(gs[1, :-1]) +ax2.set_title('gs[1, :-1]') +ax3 = fig.add_subplot(gs[1:, -1]) +ax3.set_title('gs[1:, -1]') +ax4 = fig.add_subplot(gs[-1, 0]) +ax4.set_title('gs[-1, 0]') +ax5 = fig.add_subplot(gs[-1, -2]) +ax5.set_title('gs[-1, -2]') + +############################################################################# +# :mod:`~matplotlib.gridspec` is also indispensable for creating subplots +# of different widths via a couple of methods. +# +# The method shown here is similar to the one above and initializes a +# uniform grid specification, +# and then uses numpy indexing and slices to allocate multiple # "cells" for a given subplot. -fig3 = plt.figure() -spec3 = gridspec.GridSpec(ncols=3, nrows=3) +fig3 = plt.figure(constrained_layout=True) +spec3 = fig3.add_gridspec(ncols=3, nrows=3) anno_opts = dict(xy=(0.5, 0.5), xycoords='axes fraction', va='center', ha='center') -fig3.add_subplot(spec3[0, 0]).annotate('GridSpec[0, 0]', **anno_opts) +ax1 = fig3.add_subplot(spec3[0, 0]) +ax1.annotate('GridSpec[0, 0]', **anno_opts) fig3.add_subplot(spec3[0, 1:]).annotate('GridSpec[0, 1:]', **anno_opts) fig3.add_subplot(spec3[1:, 0]).annotate('GridSpec[1:, 0]', **anno_opts) fig3.add_subplot(spec3[1:, 1:]).annotate('GridSpec[1:, 1:]', **anno_opts) -fig3.tight_layout() - ############################################################################ -# Other option is to use the ``width_ratios`` and ``height_ratios`` +# Another option is to use the ``width_ratios`` and ``height_ratios`` # parameters. These keyword arguments are lists of numbers. # Note that absolute values are meaningless, only their relative ratios # matter. That means that ``width_ratios=[2, 4, 8]`` is equivalent to @@ -90,10 +114,10 @@ # For the sake of demonstration, we'll blindly create the axes within # ``for`` loops since we won't need them later. -fig4 = plt.figure() +fig4 = plt.figure(constrained_layout=True) widths = [2, 3, 1.5] heights = [1, 3, 2] -spec4 = gridspec.GridSpec(ncols=3, nrows=3, width_ratios=widths, +spec4 = fig4.add_gridspec(ncols=3, nrows=3, width_ratios=widths, height_ratios=heights) for row in range(3): for col in range(3): @@ -101,8 +125,6 @@ label = 'Width: {}\nHeight: {}'.format(widths[col], heights[row]) ax.annotate(label, (0.1, 0.5), xycoords='axes fraction', va='center') -fig4.tight_layout() - ############################################################################ # Learning to use ``width_ratios`` and ``height_ratios`` is particularly # useful since the top-level function :func:`~matplotlib.pyplot.subplots` @@ -114,7 +136,8 @@ # gridspec instance. gs_kw = dict(width_ratios=widths, height_ratios=heights) -fig5, f5_axes = plt.subplots(ncols=3, nrows=3, gridspec_kw=gs_kw) +fig5, f5_axes = plt.subplots(ncols=3, nrows=3, constrained_layout=True, + gridspec_kw=gs_kw) for r, row in enumerate(f5_axes): for c, ax in enumerate(row): label = 'Width: {}\nHeight: {}'.format(widths[c], heights[r]) @@ -144,14 +167,16 @@ # ===================================== # # When a GridSpec is explicitly used, you can adjust the layout -# parameters of subplots that are created from the GridSpec. - -fig = plt.figure() -gs1 = gridspec.GridSpec(nrows=3, ncols=3, left=0.05, right=0.48, wspace=0.05) -ax1 = fig.add_subplot(gs1[:-1, :]) -ax2 = fig.add_subplot(gs1[-1, :-1]) -ax3 = fig.add_subplot(gs1[-1, -1]) +# parameters of subplots that are created from the GridSpec. Note this +# option is not compatible with ``constrained_layout`` or +# `.Figure.tight_layout` which both adjust subplot sizes to fill the +# figure. +fig6 = plt.figure(constrained_layout=False) +gs1 = fig6.add_gridspec(nrows=3, ncols=3, left=0.05, right=0.48, wspace=0.05) +ax1 = fig6.add_subplot(gs1[:-1, :]) +ax2 = fig6.add_subplot(gs1[-1, :-1]) +ax3 = fig6.add_subplot(gs1[-1, -1]) ############################################################################### # This is similar to :func:`~matplotlib.pyplot.subplots_adjust`, but it only @@ -159,19 +184,19 @@ # # For example, compare the left and right sides of this figure: -fig = plt.figure() -gs1 = gridspec.GridSpec(nrows=3, ncols=3, left=0.05, right=0.48, +fig7 = plt.figure(constrained_layout=False) +gs1 = fig7.add_gridspec(nrows=3, ncols=3, left=0.05, right=0.48, wspace=0.05) -ax1 = fig.add_subplot(gs1[:-1, :]) -ax2 = fig.add_subplot(gs1[-1, :-1]) -ax3 = fig.add_subplot(gs1[-1, -1]) +ax1 = fig7.add_subplot(gs1[:-1, :]) +ax2 = fig7.add_subplot(gs1[-1, :-1]) +ax3 = fig7.add_subplot(gs1[-1, -1]) - -gs2 = gridspec.GridSpec(nrows=3, ncols=3, left=0.55, right=0.98, +gs2 = fig7.add_gridspec(nrows=3, ncols=3, left=0.55, right=0.98, hspace=0.05) -ax4 = fig.add_subplot(gs2[:, :-1]) -ax5 = fig.add_subplot(gs2[:-1, -1]) -ax6 = fig.add_subplot(gs2[-1, -1]) +ax4 = fig7.add_subplot(gs2[:, :-1]) +ax5 = fig7.add_subplot(gs2[:-1, -1]) +ax6 = fig7.add_subplot(gs2[-1, -1]) + ############################################################################### # GridSpec using SubplotSpec @@ -180,20 +205,21 @@ # You can create GridSpec from the :class:`~matplotlib.gridspec.SubplotSpec`, # in which case its layout parameters are set to that of the location of # the given SubplotSpec. +# +# Note this is also available from the more verbose +# `.gridspec.GridSpecFromSubplotSpec`. -fig = plt.figure() -gs0 = gridspec.GridSpec(1, 2) +fig = plt.figure(constrained_layout=True) +gs0 = fig.add_gridspec(1, 2) -gs00 = gridspec.GridSpecFromSubplotSpec(2, 3, subplot_spec=gs0[0]) -gs01 = gridspec.GridSpecFromSubplotSpec(3, 2, subplot_spec=gs0[1]) +gs00 = gs0[0].subgridspec(2, 3) +gs01 = gs0[1].subgridspec(3, 2) for a in range(2): for b in range(3): fig.add_subplot(gs00[a, b]) fig.add_subplot(gs01[b, a]) -fig.tight_layout() - ############################################################################### # A Complex Nested GridSpec using SubplotSpec # =========================================== @@ -209,14 +235,14 @@ def squiggle_xy(a, b, c, d, i=np.arange(0.0, 2*np.pi, 0.05)): return np.sin(i*a)*np.cos(i*b), np.sin(i*c)*np.cos(i*d) -fig = plt.figure(figsize=(8, 8)) + +fig = plt.figure(figsize=(8, 8), constrained_layout=False) # gridspec inside gridspec -outer_grid = gridspec.GridSpec(4, 4, wspace=0.0, hspace=0.0) +outer_grid = fig.add_gridspec(4, 4, wspace=0.0, hspace=0.0) for i in range(16): - inner_grid = gridspec.GridSpecFromSubplotSpec( - 3, 3, subplot_spec=outer_grid[i], wspace=0.0, hspace=0.0) + inner_grid = outer_grid[i].subgridspec(3, 3, wspace=0.0, hspace=0.0) a, b = int(i/4)+1, i % 4+1 for j, (c, d) in enumerate(product(range(1, 4), repeat=2)): ax = plt.Subplot(fig, inner_grid[j]) @@ -241,3 +267,19 @@ def squiggle_xy(a, b, c, d, i=np.arange(0.0, 2*np.pi, 0.05)): ax.spines['right'].set_visible(True) plt.show() + +############################################################################# +# +# ------------ +# +# References +# """""""""" +# +# The usage of the following functions and methods is shown in this example: + +matplotlib.pyplot.subplots +matplotlib.figure.Figure.add_gridspec +matplotlib.figure.Figure.add_subplot +matplotlib.gridspec.GridSpec +matplotlib.gridspec.SubplotSpec.subgridspec +matplotlib.gridspec.GridSpecFromSubplotSpec