Skip to content

Commit eeaf3e2

Browse files
committed
ENH: reuse gridspec if possible
1 parent 969513e commit eeaf3e2

File tree

4 files changed

+71
-39
lines changed

4 files changed

+71
-39
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Constrained layout now supports subplot and subplot2grid
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
Previously ``constrained_layout`` depended on a single ``GridSpec``
5+
for each logical layout on a figure, whereas ``plt.subplot`` added
6+
a new ``GridSpec`` for each call. Now ``plt.subplot`` attempts to
7+
reuse the ``GridSpec`` if the number of rows and columns is the same
8+
as the top level gridspec already in the figure. i.e. ``plt.subplot(2, 1, 2)``
9+
will use the same gridspec as ``plt.subplot(2, 1, 1)`` and the
10+
``constrained_layout=True`` option to `~.figure.Figure` will work. Note that
11+
mixing ``nrows`` and ``ncols`` will *not* work: ``plt.subplot(2, 2, 1)``
12+
followed by ``plt.subplots(2, 1, 2)`` will produce two gridspecs, and
13+
``constrained_layout=True`` will give bad results. In order to get the
14+
same effect, the second call can specify the cells the second axes is meant
15+
to cover: ``plt.subplots(2, 2, (2, 4))``, or the more pythonic
16+
``plt.subplots2grid((2, 2), (0, 1), rowspan=2)`` can be used.
17+

lib/matplotlib/gridspec.py

+31-3
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,29 @@ def get_grid_positions(self, fig, raw=False):
202202
fig_lefts, fig_rights = (left + cell_ws).reshape((-1, 2)).T
203203
return fig_bottoms, fig_tops, fig_lefts, fig_rights
204204

205+
@staticmethod
206+
def _check_gridspec_exists(figure, nrows, ncols):
207+
"""
208+
Check if the figure already has a gridspec with these dimensions,
209+
or create a new one
210+
"""
211+
for ax in figure.get_axes():
212+
if hasattr(ax, 'get_subplotspec'):
213+
gs = ax.get_subplotspec().get_gridspec()
214+
if hasattr(gs, 'get_topmost_subplotspec'):
215+
# This is needed for colorbar gridspec layouts.
216+
# This is probably OK becase this whole logic tree
217+
# is for when the user is doing simple things with the
218+
# add_subplot command. For complicated layouts
219+
# like subgridspecs the proper gridspec is passed in...
220+
gs = gs.get_topmost_subplotspec().get_gridspec()
221+
(nrow, ncol) = gs.get_geometry()
222+
if nrow == nrows and ncol == ncols:
223+
return gs
224+
# else gridspec not found:
225+
return GridSpec(nrows, ncols, figure=figure)
226+
227+
205228
def __getitem__(self, key):
206229
"""Create and return a `.SubplotSpec` instance."""
207230
nrows, ncols = self.get_geometry()
@@ -665,7 +688,7 @@ def _from_subplot_args(figure, args):
665688
f"Single argument to subplot must be a three-digit "
666689
f"integer, not {arg}") from None
667690
# num - 1 for converting from MATLAB to python indexing
668-
return GridSpec(rows, cols, figure=figure)[num - 1]
691+
i = j = num
669692
elif len(args) == 3:
670693
rows, cols, num = args
671694
if not (isinstance(rows, Integral) and isinstance(cols, Integral)):
@@ -678,19 +701,24 @@ def _from_subplot_args(figure, args):
678701
i, j = map(int, num)
679702
else:
680703
i, j = num
681-
return gs[i-1:j]
682704
else:
683705
if not isinstance(num, Integral):
684706
cbook.warn_deprecated("3.3", message=message)
685707
num = int(num)
686708
if num < 1 or num > rows*cols:
687709
raise ValueError(
688710
f"num must be 1 <= num <= {rows*cols}, not {num}")
689-
return gs[num - 1] # -1 due to MATLAB indexing.
711+
else:
712+
i = j = num
690713
else:
691714
raise TypeError(f"subplot() takes 1 or 3 positional arguments but "
692715
f"{len(args)} were given")
693716

717+
gs = GridSpec._check_gridspec_exists(figure, rows, cols)
718+
if gs is None:
719+
gs = GridSpec(rows, cols, figure=figure)
720+
return gs[i-1:j]
721+
694722
# num2 is a property only to handle the case where it is None and someone
695723
# mutates num1.
696724

lib/matplotlib/pyplot.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1311,10 +1311,10 @@ def subplot2grid(shape, loc, rowspan=1, colspan=1, fig=None, **kwargs):
13111311
if fig is None:
13121312
fig = gcf()
13131313

1314-
s1, s2 = shape
1315-
subplotspec = GridSpec(s1, s2).new_subplotspec(loc,
1316-
rowspan=rowspan,
1317-
colspan=colspan)
1314+
rows, cols = shape
1315+
gs = GridSpec._check_gridspec_exists(fig, rows, cols)
1316+
1317+
subplotspec = gs.new_subplotspec(loc, rowspan=rowspan, colspan=colspan)
13181318
ax = fig.add_subplot(subplotspec, **kwargs)
13191319
bbox = ax.bbox
13201320
axes_to_delete = []

tutorials/intermediate/constrainedlayout_guide.py

+19-32
Original file line numberDiff line numberDiff line change
@@ -492,40 +492,43 @@ def docomplicated(suptitle=None):
492492
# Incompatible functions
493493
# ----------------------
494494
#
495-
# ``constrained_layout`` will not work on subplots created via
496-
# `.pyplot.subplot`. The reason is that each call to `.pyplot.subplot` creates
497-
# a separate `.GridSpec` instance and ``constrained_layout`` uses (nested)
498-
# gridspecs to carry out the layout. So the following fails to yield a nice
499-
# layout:
495+
# ``constrained_layout`` will work with `.pyplot.subplot`, but only if the
496+
# number of rows and columns is the same for each call.
497+
# The reason is that each call to `.pyplot.subplot` will create a new
498+
# `.GridSpec` instance if the geometry is not the same, and
499+
# ``constrained_layout``. So the following works fine:
500+
500501

501502
fig = plt.figure()
502503

503-
ax1 = plt.subplot(221)
504-
ax2 = plt.subplot(223)
505-
ax3 = plt.subplot(122)
504+
ax1 = plt.subplot(2, 2, 1)
505+
ax2 = plt.subplot(2, 2, 3)
506+
# third axes that spans both rows in second column:
507+
ax3 = plt.subplot(2, 2, (2, 4))
506508

507509
example_plot(ax1)
508510
example_plot(ax2)
509511
example_plot(ax3)
512+
plt.suptitle('Homogenous nrows, ncols')
510513

511514
###############################################################################
512-
# Of course that layout is possible using a gridspec:
515+
# but the following leads to a poor layout:
513516

514517
fig = plt.figure()
515-
gs = fig.add_gridspec(2, 2)
516518

517-
ax1 = fig.add_subplot(gs[0, 0])
518-
ax2 = fig.add_subplot(gs[1, 0])
519-
ax3 = fig.add_subplot(gs[:, 1])
519+
ax1 = plt.subplot(2, 2, 1)
520+
ax2 = plt.subplot(2, 2, 3)
521+
ax3 = plt.subplot(1, 2, 2)
520522

521523
example_plot(ax1)
522524
example_plot(ax2)
523525
example_plot(ax3)
526+
plt.suptitle('Mixed nrows, ncols')
524527

525528
###############################################################################
526529
# Similarly,
527-
# :func:`~matplotlib.pyplot.subplot2grid` doesn't work for the same reason:
528-
# each call creates a different parent gridspec.
530+
# :func:`~matplotlib.pyplot.subplot2grid` works with the same limitation
531+
# that nrows and ncols cannot change for the layout to look good.
529532

530533
fig = plt.figure()
531534

@@ -538,23 +541,7 @@ def docomplicated(suptitle=None):
538541
example_plot(ax2)
539542
example_plot(ax3)
540543
example_plot(ax4)
541-
542-
###############################################################################
543-
# The way to make this plot compatible with ``constrained_layout`` is again
544-
# to use ``gridspec`` directly
545-
546-
fig = plt.figure()
547-
gs = fig.add_gridspec(3, 3)
548-
549-
ax1 = fig.add_subplot(gs[0, 0])
550-
ax2 = fig.add_subplot(gs[0, 1:])
551-
ax3 = fig.add_subplot(gs[1:, 0:2])
552-
ax4 = fig.add_subplot(gs[1:, -1])
553-
554-
example_plot(ax1)
555-
example_plot(ax2)
556-
example_plot(ax3)
557-
example_plot(ax4)
544+
fig.suptitle('subplot2grid')
558545

559546
###############################################################################
560547
# Other Caveats

0 commit comments

Comments
 (0)