From be1b73041cba9c5e7963276d0520f0fdf132a67e Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 6 May 2020 14:44:16 -0700 Subject: [PATCH 1/2] FIX: allow start-stop subplot --- lib/matplotlib/figure.py | 33 +++++++++++++++++++++-------- lib/matplotlib/tests/test_figure.py | 13 ++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index f7889aa5344e..2f8f9749da9e 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1264,13 +1264,16 @@ def add_subplot(self, *args, **kwargs): Parameters ---------- - *args, int or (int, int, int) or `SubplotSpec`, default: (1, 1, 1) + *args, int, (int, int, *index*), or `SubplotSpec`, default: (1, 1, 1) The position of the subplot described by one of - Three integers (*nrows*, *ncols*, *index*). The subplot will take the *index* position on a grid with *nrows* rows and *ncols* columns. *index* starts at 1 in the upper left corner - and increases to the right. + and increases to the right. *index* can also be a two-tuple + specifying the (*start*, *stop*) indices of the subplot, i.e., + ``fig.add_subplot(3, 1, (1, 2))`` makes a subplot that spans the + upper 2/3 of the figure. - A 3-digit integer. The digits are interpreted as if given separately as three single-digit integers, i.e. ``fig.add_subplot(235)`` is the same as @@ -1377,14 +1380,26 @@ def add_subplot(self, *args, **kwargs): raise TypeError('Positional arguments are not a valid ' 'position specification.') elif nargs == 3: - for arg in args: + newarg = [None] * 3 + message = ("Passing non-integers as three-element " + "position specification is deprecated since " + "%(since)s and will be removed %(removal)s.") + for nn, arg in enumerate(args[:2]): if not isinstance(arg, Integral): - cbook.warn_deprecated( - "3.3", - message="Passing non-integers as three-element " - "position specification is deprecated since " - "%(since)s and will be removed %(removal)s.") - args = tuple(map(int, args)) + cbook.warn_deprecated("3.3", message=message) + newarg[nn] = int(arg) + args2 = args[2] + if isinstance(args2, tuple) and len(args2) == 2: + # start/stop two-tuple is allowed... + for arg in args2: + if not isinstance(arg, Integral): + cbook.warn_deprecated("3.3", message=message) + newarg[2] = (int(args2[0]), int(args2[1])) + else: + if not isinstance(args2, Integral): + cbook.warn_deprecated("3.3", message=message) + newarg[2] = int(args2) + args = tuple(newarg) else: raise TypeError(f'add_subplot() takes 1 or 3 positional arguments ' f'but {nargs} were given') diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 298924bf86a4..6549bcf60a2a 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -533,3 +533,16 @@ def test_removed_axis(): fig, axs = plt.subplots(2, sharex=True) axs[0].remove() fig.canvas.draw() + + +def test_add_subplot_twotuple(): + fig = plt.figure() + ax2 = fig.add_subplot(3, 2, (3, 5)) + assert ax2.get_subplotspec().rowspan == range(1, 3) + assert ax2.get_subplotspec().colspan == range(0, 1) + ax3 = fig.add_subplot(3, 2, (4, 6)) + assert ax3.get_subplotspec().rowspan == range(1, 3) + assert ax3.get_subplotspec().colspan == range(1, 2) + ax4 = fig.add_subplot(3, 2, (3, 6)) + assert ax4.get_subplotspec().rowspan == range(1, 3) + assert ax4.get_subplotspec().colspan == range(0, 2) From fdc90859bb40d7daa61f53c510abbeadd07bcd1c Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Thu, 7 May 2020 09:07:08 -0700 Subject: [PATCH 2/2] FIX: re-factor --- lib/matplotlib/figure.py | 41 +---------------- lib/matplotlib/gridspec.py | 90 ++++++++++++++++++++++++++------------ 2 files changed, 65 insertions(+), 66 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 2f8f9749da9e..7ba64b4b2b40 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1271,7 +1271,7 @@ def add_subplot(self, *args, **kwargs): take the *index* position on a grid with *nrows* rows and *ncols* columns. *index* starts at 1 in the upper left corner and increases to the right. *index* can also be a two-tuple - specifying the (*start*, *stop*) indices of the subplot, i.e., + specifying the (*start*, *stop*) indices of the subplot, e.g., ``fig.add_subplot(3, 1, (1, 2))`` makes a subplot that spans the upper 2/3 of the figure. - A 3-digit integer. The digits are interpreted as if given @@ -1365,44 +1365,7 @@ def add_subplot(self, *args, **kwargs): raise TypeError( "add_subplot() got an unexpected keyword argument 'figure'") - nargs = len(args) - if nargs == 0: - args = (1, 1, 1) - elif nargs == 1: - if isinstance(args[0], Integral): - if not 100 <= args[0] <= 999: - raise ValueError(f"Integer subplot specification must be " - f"a three-digit number, not {args[0]}") - args = tuple(map(int, str(args[0]))) - elif isinstance(args[0], (SubplotBase, SubplotSpec)): - pass # no further validation or normalization needed - else: - raise TypeError('Positional arguments are not a valid ' - 'position specification.') - elif nargs == 3: - newarg = [None] * 3 - message = ("Passing non-integers as three-element " - "position specification is deprecated since " - "%(since)s and will be removed %(removal)s.") - for nn, arg in enumerate(args[:2]): - if not isinstance(arg, Integral): - cbook.warn_deprecated("3.3", message=message) - newarg[nn] = int(arg) - args2 = args[2] - if isinstance(args2, tuple) and len(args2) == 2: - # start/stop two-tuple is allowed... - for arg in args2: - if not isinstance(arg, Integral): - cbook.warn_deprecated("3.3", message=message) - newarg[2] = (int(args2[0]), int(args2[1])) - else: - if not isinstance(args2, Integral): - cbook.warn_deprecated("3.3", message=message) - newarg[2] = int(args2) - args = tuple(newarg) - else: - raise TypeError(f'add_subplot() takes 1 or 3 positional arguments ' - f'but {nargs} were given') + args = SubplotSpec._check_subplots_args(args) if isinstance(args[0], SubplotBase): ax = args[0] diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index b25bfe853534..4b76edf2b26a 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -11,6 +11,7 @@ import copy import logging +from numbers import Integral import numpy as np @@ -516,6 +517,60 @@ def __repr__(self): f"{self.rowspan.start}:{self.rowspan.stop}, " f"{self.colspan.start}:{self.colspan.stop}]") + @staticmethod + def _check_subplots_args(args): + print('args', args, type(args)) + nargs = len(args) + if nargs == 0: + args = (1, 1, 1) + elif nargs == 1: + if isinstance(args[0], Integral): + if not 100 <= args[0] <= 999: + raise ValueError(f"Integer subplot specification must be " + f"a three-digit number, not {args[0]}") + args = tuple(map(int, str(args[0]))) + elif isinstance(args[0], (mpl.axes.SubplotBase, SubplotSpec)): + return args + else: + raise ValueError("Single argument to subplot must be a " + "3-digit integer or a SubplotSpec") + elif nargs == 3: + message = ("Passing non-integers as three-element " + "position specification is deprecated since " + "%(since)s and will be removed %(removal)s.") + rows, cols, nums = args + if not isinstance(rows, Integral): + cbook.warn_deprecated("3.3", message=message) + rows = int(rows) + if rows <= 0: + raise ValueError(f"Number of rows must be > 0, not {rows}") + + if not isinstance(cols, Integral): + cbook.warn_deprecated("3.3", message=message) + cols = int(cols) + if cols <= 0: + raise ValueError(f"Number of columns must be > 0, not {cols}") + + if isinstance(nums, tuple) and len(nums) == 2: + # start/stop two-tuple is allowed... + for arg in nums: + if not isinstance(arg, Integral): + cbook.warn_deprecated("3.3", message=message) + nums = (int(nums[0]), int(nums[1])) + else: + if not isinstance(nums, Integral): + cbook.warn_deprecated("3.3", message=message) + nums = int(nums) + if nums < 1 or nums > rows*cols: + raise ValueError( + f"num must be 1 <= num <= {rows*cols}, not {nums}") + args = (rows, cols, nums) + else: + raise TypeError(f'subplot() takes 1 or 3 positional arguments ' + f'but {nargs} were given') + + return args + @staticmethod def _from_subplot_args(figure, args): """ @@ -524,39 +579,20 @@ def _from_subplot_args(figure, args): - a `.SubplotSpec` -- returned as is; - one or three numbers -- a MATLAB-style subplot specifier. """ + args = SubplotSpec._check_subplots_args(args) if len(args) == 1: - arg, = args - if isinstance(arg, SubplotSpec): - return arg - else: - try: - s = str(int(arg)) - rows, cols, num = map(int, s) - except ValueError as err: - raise ValueError("Single argument to subplot must be a " - "3-digit integer") from err - # num - 1 for converting from MATLAB to python indexing - return GridSpec(rows, cols, figure=figure)[num - 1] + # args is a subplotspec + return args[0] elif len(args) == 3: rows, cols, num = args - rows = int(rows) - cols = int(cols) - if rows <= 0: - raise ValueError(f"Number of rows must be > 0, not {rows}") - if cols <= 0: - raise ValueError(f"Number of columns must be > 0, not {cols}") if isinstance(num, tuple) and len(num) == 2: - i, j = map(int, num) - return GridSpec(rows, cols, figure=figure)[i-1:j] + start, stop = num else: - if num < 1 or num > rows*cols: - raise ValueError( - f"num must be 1 <= num <= {rows*cols}, not {num}") # num - 1 for converting from MATLAB to python indexing - return GridSpec(rows, cols, figure=figure)[int(num) - 1] - else: - raise TypeError(f"subplot() takes 1 or 3 positional arguments but " - f"{len(args)} were given") + start = num + stop = num + # start - 1 for converting from MATLAB to python indexing + return GridSpec(rows, cols, figure=figure)[start - 1:stop] # num2 is a property only to handle the case where it is None and someone # mutates num1.