From f15a7c73ecb972d4678f2ace1d383632a8ec8baa Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Fri, 15 Jun 2018 11:52:34 -0700 Subject: [PATCH 1/3] ENH: make subplot reuse gridspecs if they fit --- lib/matplotlib/axes/_subplots.py | 89 +++++++++++++++++++++++--------- lib/matplotlib/figure.py | 2 + 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/lib/matplotlib/axes/_subplots.py b/lib/matplotlib/axes/_subplots.py index 257c5711f2de..4db01e0a6c09 100644 --- a/lib/matplotlib/axes/_subplots.py +++ b/lib/matplotlib/axes/_subplots.py @@ -1,4 +1,5 @@ import functools +import numpy as np import warnings from matplotlib import docstring @@ -31,39 +32,40 @@ def __init__(self, fig, *args, **kwargs): """ self.figure = fig - - if len(args) == 1: - if isinstance(args[0], SubplotSpec): - self._subplotspec = args[0] - else: + if len(args) == 1 and isinstance(args[0], SubplotSpec): + self._subplotspec = args[0] + else: + # we need to make the subplotspec either from a new gridspec or + # an existing one: + if len(args) == 1: + # 223-style argument... try: s = str(int(args[0])) rows, cols, num = map(int, s) except ValueError: raise ValueError('Single argument to subplot must be ' 'a 3-digit integer') - self._subplotspec = GridSpec(rows, cols, - figure=self.figure)[num - 1] - # num - 1 for converting from MATLAB to python indexing - elif len(args) == 3: - rows, cols, num = args - rows = int(rows) - cols = int(cols) - if isinstance(num, tuple) and len(num) == 2: - num = [int(n) for n in num] - self._subplotspec = GridSpec( - rows, cols, - figure=self.figure)[(num[0] - 1):num[1]] + num = [num, num] + elif len(args) == 3: + rows, cols, num = args + rows = int(rows) + cols = int(cols) + if isinstance(num, tuple) and len(num) == 2: + num = [int(n) for n in num] + else: + if num < 1 or num > rows*cols: + raise ValueError( + ("num must be 1 <= num <= {maxn}, not {num}" + ).format(maxn=rows*cols, num=num)) + num = [num, num] else: - if num < 1 or num > rows*cols: - raise ValueError( - ("num must be 1 <= num <= {maxn}, not {num}" - ).format(maxn=rows*cols, num=num)) - self._subplotspec = GridSpec( - rows, cols, figure=self.figure)[int(num) - 1] + raise ValueError('Illegal argument(s) to subplot: %s' % + (args,)) + gs, num = self._make_subplotspec(rows, cols, num, + figure=self.figure) + self._subplotspec = gs[(num[0] - 1):num[1]] # num - 1 for converting from MATLAB to python indexing - else: - raise ValueError('Illegal argument(s) to subplot: %s' % (args,)) + self.update_params() @@ -87,6 +89,43 @@ def __init__(self, fig, *args, **kwargs): name=self._layoutbox.name+'.pos', pos=True, subplot=True, artist=self) + def _make_subplotspec(self, rows, cols, num, figure=None): + """ + Return the subplotspec for this subplot, but reuse an old + GridSpec if it exists and if the new gridspec "fits". + """ + axs = figure.get_axes() + for ax in axs: + if hasattr(ax, 'get_subplotspec'): + gs = ax.get_subplotspec().get_gridspec() + (nrow, ncol) = gs.get_geometry() + if (not (nrow % rows) and not (ncol % cols)): + # this gridspec "fits"... + # now we have to see if we need to modify num... + rowfac = int(nrow / rows) + colfac = int(ncol / cols) + if (not isinstance(num, tuple) and + not isinstance(num, list)): + num = [num, num] + # converting between num and rows/cols is a PITA: + newnum = num + row = int(np.floor((num[0]-1) / cols)) + col = (num[0]-1) - row * cols + row *= rowfac + col *= colfac + newnum[0] = row * ncol + col + 1 + row = int(np.floor((num[1]-1) / cols)) + col = (num[1]-1) - row * cols + row *= rowfac + col *= colfac + row = row + (rowfac - 1) + col = col + (colfac - 1) + newnum[1] = row * ncol + col + 1 + return gs, newnum + # no axes fit with the new subplot specification so make a + # new one... + return GridSpec(rows, cols, figure=figure), num + def __reduce__(self): # get the first axes class which does not inherit from a subplotbase axes_class = next( diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index ac2de0e88daa..d07759442b5a 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1197,6 +1197,8 @@ def add_subplot(self, *args, **kwargs): fig.add_subplot(111, projection='polar') # add Subplot instance sub + gs = gridspec.GridSpec(2, 3) + sub = gs[1, 1] fig.add_subplot(sub) See Also From 669eac523d3b8f0292578a67872fd7eeb509b488 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Fri, 15 Jun 2018 14:18:58 -0700 Subject: [PATCH 2/3] FIX: working --- lib/matplotlib/axes/_subplots.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/axes/_subplots.py b/lib/matplotlib/axes/_subplots.py index 4db01e0a6c09..dae1bf498b05 100644 --- a/lib/matplotlib/axes/_subplots.py +++ b/lib/matplotlib/axes/_subplots.py @@ -45,7 +45,7 @@ def __init__(self, fig, *args, **kwargs): except ValueError: raise ValueError('Single argument to subplot must be ' 'a 3-digit integer') - num = [num, num] + num = [int(num), int(num)] elif len(args) == 3: rows, cols, num = args rows = int(rows) @@ -57,7 +57,7 @@ def __init__(self, fig, *args, **kwargs): raise ValueError( ("num must be 1 <= num <= {maxn}, not {num}" ).format(maxn=rows*cols, num=num)) - num = [num, num] + num = [int(num), int(num)] else: raise ValueError('Illegal argument(s) to subplot: %s' % (args,)) @@ -66,7 +66,6 @@ def __init__(self, fig, *args, **kwargs): self._subplotspec = gs[(num[0] - 1):num[1]] # num - 1 for converting from MATLAB to python indexing - self.update_params() # _axes_class is set in the subplot_class_factory @@ -98,6 +97,14 @@ def _make_subplotspec(self, rows, cols, num, figure=None): for ax in axs: if hasattr(ax, 'get_subplotspec'): gs = ax.get_subplotspec().get_gridspec() + if hasattr(gs, 'get_topmost_subplotspec'): + # This is needed for colorbar gridspec layouts. + # This is probably OK becase this whole logic tree + # is for when the user is doing simple things with the + # add_subplot command. Complicated stuff, the proper + # gridspec is passed in... + gs = gs.get_topmost_subplotspec().get_gridspec() + (nrow, ncol) = gs.get_geometry() if (not (nrow % rows) and not (ncol % cols)): # this gridspec "fits"... From a7faa4c9d9f2afae3cc8577f5e16c1bb32010790 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Fri, 15 Jun 2018 14:29:53 -0700 Subject: [PATCH 3/3] FIX: working --- lib/matplotlib/gridspec.py | 2 +- lib/matplotlib/tests/test_figure.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index c4f2bf7f703b..69301f3940af 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -394,7 +394,7 @@ def __init__(self, gridspec, num1, num2=None): self._gridspec = gridspec self.num1 = num1 self.num2 = num2 - if gridspec._layoutbox is not None: + if hasattr(gridspec, '_layoutbox') and gridspec._layoutbox is not None: glb = gridspec._layoutbox # So note that here we don't assign any layout yet, # just make the layoutbox that will conatin all items diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index d0079012a5e6..9e7778a7805d 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -151,14 +151,17 @@ def test_gca(): assert fig.gca() is ax3 # the final request for a polar axes will end up creating one - # with a spec of 111. + # with a spec of 121. The 2 stays in there, because we reuse the + # grid spec of the 12x calls... with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') # Changing the projection will throw a warning assert fig.gca(polar=True) is not ax3 assert len(w) == 1 + assert fig.gca(polar=True) is not ax1 assert fig.gca(polar=True) is not ax2 - assert fig.gca().get_geometry() == (1, 1, 1) + assert fig.gca(polar=True) is not ax3 + # assert fig.gca().get_geometry() == (1, 1, 1) fig.sca(ax1) assert fig.gca(projection='rectilinear') is ax1