Skip to content

Commit a3b59c3

Browse files
committed
Move integerness checks to SubplotSpec._from_subplot_args.
This avoids having to later do them same for other places (axes_grid) which rely on the same machinery to convert three-digit specifications. I'll assume that the already existing deprecation note for add_subplot is effectively sufficient.
1 parent 5c13fb4 commit a3b59c3

File tree

4 files changed

+78
-66
lines changed

4 files changed

+78
-66
lines changed

lib/matplotlib/figure.py

+10-35
Original file line numberDiff line numberDiff line change
@@ -1264,13 +1264,16 @@ def add_subplot(self, *args, **kwargs):
12641264
12651265
Parameters
12661266
----------
1267-
*args, int or (int, int, int) or `SubplotSpec`, default: (1, 1, 1)
1267+
*args, int, (int, int, *index*), or `SubplotSpec`, default: (1, 1, 1)
12681268
The position of the subplot described by one of
12691269
12701270
- Three integers (*nrows*, *ncols*, *index*). The subplot will
12711271
take the *index* position on a grid with *nrows* rows and
12721272
*ncols* columns. *index* starts at 1 in the upper left corner
1273-
and increases to the right.
1273+
and increases to the right. *index* can also be a two-tuple
1274+
specifying the (*first*, *last*) indices (1-based, and including
1275+
*last*) of the subplot, e.g., ``fig.add_subplot(3, 1, (1, 2))``
1276+
makes a subplot that spans the upper 2/3 of the figure.
12741277
- A 3-digit integer. The digits are interpreted as if given
12751278
separately as three single-digit integers, i.e.
12761279
``fig.add_subplot(235)`` is the same as
@@ -1362,48 +1365,21 @@ def add_subplot(self, *args, **kwargs):
13621365
raise TypeError(
13631366
"add_subplot() got an unexpected keyword argument 'figure'")
13641367

1365-
nargs = len(args)
1366-
if nargs == 0:
1367-
args = (1, 1, 1)
1368-
elif nargs == 1:
1369-
if isinstance(args[0], Integral):
1370-
if not 100 <= args[0] <= 999:
1371-
raise ValueError(f"Integer subplot specification must be "
1372-
f"a three-digit number, not {args[0]}")
1373-
args = tuple(map(int, str(args[0])))
1374-
elif isinstance(args[0], (SubplotBase, SubplotSpec)):
1375-
pass # no further validation or normalization needed
1376-
else:
1377-
raise TypeError('Positional arguments are not a valid '
1378-
'position specification.')
1379-
elif nargs == 3:
1380-
for arg in args:
1381-
if not isinstance(arg, Integral):
1382-
cbook.warn_deprecated(
1383-
"3.3",
1384-
message="Passing non-integers as three-element "
1385-
"position specification is deprecated since "
1386-
"%(since)s and will be removed %(removal)s.")
1387-
args = tuple(map(int, args))
1388-
else:
1389-
raise TypeError(f'add_subplot() takes 1 or 3 positional arguments '
1390-
f'but {nargs} were given')
1391-
1392-
if isinstance(args[0], SubplotBase):
1368+
if len(args) == 1 and isinstance(args[0], SubplotBase):
13931369
ax = args[0]
13941370
if ax.get_figure() is not self:
13951371
raise ValueError("The Subplot must have been created in "
13961372
"the present figure")
13971373
# make a key for the subplot (which includes the axes object id
13981374
# in the hash)
13991375
key = self._make_key(*args, **kwargs)
1376+
14001377
else:
1378+
if not args:
1379+
args = (1, 1, 1)
14011380
projection_class, kwargs, key = \
14021381
self._process_projection_requirements(*args, **kwargs)
1403-
1404-
# try to find the axes with this key in the stack
1405-
ax = self._axstack.get(key)
1406-
1382+
ax = self._axstack.get(key) # search axes with this key in stack
14071383
if ax is not None:
14081384
if isinstance(ax, projection_class):
14091385
# the axes already existed, so set it as active & return
@@ -1416,7 +1392,6 @@ def add_subplot(self, *args, **kwargs):
14161392
# Without this, add_subplot would be simpler and
14171393
# more similar to add_axes.
14181394
self._axstack.remove(ax)
1419-
14201395
ax = subplot_class_factory(projection_class)(self, *args, **kwargs)
14211396

14221397
return self._add_axes_internal(key, ax)

lib/matplotlib/gridspec.py

+33-16
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def __init__(self, nrows, ncols, height_ratios=None, width_ratios=None):
4444
relative height of ``height_ratios[i] / sum(height_ratios)``.
4545
If not given, all rows will have the same height.
4646
"""
47+
if not isinstance(nrows, Integral) or nrows <= 0:
48+
raise ValueError(f"Number of rows must be > 0, not {nrows}")
49+
if not isinstance(ncols, Integral) or ncols <= 0:
50+
raise ValueError(f"Number of columns must be > 0, not {ncols}")
4751
self._nrows, self._ncols = nrows, ncols
4852
self.set_height_ratios(height_ratios)
4953
self.set_width_ratios(width_ratios)
@@ -643,36 +647,46 @@ def _from_subplot_args(figure, args):
643647
- a `.SubplotSpec` -- returned as is;
644648
- one or three numbers -- a MATLAB-style subplot specifier.
645649
"""
650+
message = ("Passing non-integers as three-element position "
651+
"specification is deprecated since %(since)s and will be "
652+
"removed %(removal)s.")
646653
if len(args) == 1:
647654
arg, = args
648655
if isinstance(arg, SubplotSpec):
649656
return arg
650657
else:
658+
if not isinstance(arg, Integral):
659+
cbook.warn_deprecated("3.3", message=message)
660+
arg = str(arg)
651661
try:
652-
s = str(int(arg))
653-
rows, cols, num = map(int, s)
654-
except ValueError as err:
655-
raise ValueError("Single argument to subplot must be a "
656-
"3-digit integer") from err
662+
rows, cols, num = map(int, str(arg))
663+
except ValueError:
664+
raise ValueError(
665+
f"Single argument to subplot must be a three-digit "
666+
f"integer, not {arg}") from None
657667
# num - 1 for converting from MATLAB to python indexing
658668
return GridSpec(rows, cols, figure=figure)[num - 1]
659669
elif len(args) == 3:
660670
rows, cols, num = args
661-
rows = int(rows)
662-
cols = int(cols)
663-
if rows <= 0:
664-
raise ValueError(f"Number of rows must be > 0, not {rows}")
665-
if cols <= 0:
666-
raise ValueError(f"Number of columns must be > 0, not {cols}")
671+
if not (isinstance(rows, Integral) and isinstance(cols, Integral)):
672+
cbook.warn_deprecated("3.3", message=message)
673+
rows, cols = map(int, [rows, cols])
674+
gs = GridSpec(rows, cols, figure=figure)
667675
if isinstance(num, tuple) and len(num) == 2:
668-
i, j = map(int, num)
669-
return GridSpec(rows, cols, figure=figure)[i-1:j]
676+
if not all(isinstance(n, Integral) for n in num):
677+
cbook.warn_deprecated("3.3", message=message)
678+
i, j = map(int, num)
679+
else:
680+
i, j = num
681+
return gs[i-1:j]
670682
else:
683+
if not isinstance(num, Integral):
684+
cbook.warn_deprecated("3.3", message=message)
685+
num = int(num)
671686
if num < 1 or num > rows*cols:
672687
raise ValueError(
673688
f"num must be 1 <= num <= {rows*cols}, not {num}")
674-
# num - 1 for converting from MATLAB to python indexing
675-
return GridSpec(rows, cols, figure=figure)[int(num) - 1]
689+
return gs[num - 1] # -1 due to MATLAB indexing.
676690
else:
677691
raise TypeError(f"subplot() takes 1 or 3 positional arguments but "
678692
f"{len(args)} were given")
@@ -727,7 +741,10 @@ def rowspan(self):
727741
def colspan(self):
728742
"""The columns spanned by this subplot, as a `range` object."""
729743
ncols = self.get_gridspec().ncols
730-
return range(self.num1 % ncols, self.num2 % ncols + 1)
744+
# We explicitly support num2 refering to a column on num1's *left*, so
745+
# we must sort the column indices here so that the range makes sense.
746+
c1, c2 = sorted([self.num1 % ncols, self.num2 % ncols])
747+
return range(c1, c2 + 1)
731748

732749
def get_position(self, figure, return_all=False):
733750
"""

lib/matplotlib/pyplot.py

+15-13
Original file line numberDiff line numberDiff line change
@@ -935,19 +935,21 @@ def subplot(*args, **kwargs):
935935
936936
Parameters
937937
----------
938-
*args, default: (1, 1, 1)
939-
Either a 3-digit integer or three separate integers
940-
describing the position of the subplot. If the three
941-
integers are *nrows*, *ncols*, and *index* in order, the
942-
subplot will take the *index* position on a grid with *nrows*
943-
rows and *ncols* columns. *index* starts at 1 in the upper left
944-
corner and increases to the right.
945-
946-
*pos* is a three digit integer, where the first digit is the
947-
number of rows, the second the number of columns, and the third
948-
the index of the subplot. i.e. fig.add_subplot(235) is the same as
949-
fig.add_subplot(2, 3, 5). Note that all integers must be less than
950-
10 for this form to work.
938+
*args : int, (int, int, *index*), or `SubplotSpec`, default: (1, 1, 1)
939+
The position of the subplot described by one of
940+
941+
- Three integers (*nrows*, *ncols*, *index*). The subplot will take the
942+
*index* position on a grid with *nrows* rows and *ncols* columns.
943+
*index* starts at 1 in the upper left corner and increases to the
944+
right. *index* can also be a two-tuple specifying the (*first*,
945+
*last*) indices (1-based, and including *last*) of the subplot, e.g.,
946+
``fig.add_subplot(3, 1, (1, 2))`` makes a subplot that spans the
947+
upper 2/3 of the figure.
948+
- A 3-digit integer. The digits are interpreted as if given separately
949+
as three single-digit integers, i.e. ``fig.add_subplot(235)`` is the
950+
same as ``fig.add_subplot(2, 3, 5)``. Note that this can only be used
951+
if there are no more than 9 subplots.
952+
- A `.SubplotSpec`.
951953
952954
projection : {None, 'aitoff', 'hammer', 'lambert', 'mollweide', \
953955
'polar', 'rectilinear', str}, optional

lib/matplotlib/tests/test_figure.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ def test_add_subplot_invalid():
190190
with pytest.raises(ValueError, match='num must be 1 <= num <= 4'):
191191
fig.add_subplot(2, 2, 5)
192192

193-
with pytest.raises(ValueError, match='must be a three-digit number'):
193+
with pytest.raises(ValueError, match='must be a three-digit integer'):
194194
fig.add_subplot(42)
195-
with pytest.raises(ValueError, match='must be a three-digit number'):
195+
with pytest.raises(ValueError, match='must be a three-digit integer'):
196196
fig.add_subplot(1000)
197197

198198
with pytest.raises(TypeError, match='takes 1 or 3 positional arguments '
@@ -548,3 +548,21 @@ def test_picking_does_not_stale():
548548
inaxes=ax, guiEvent=None)
549549
fig.pick(mouse_event)
550550
assert not fig.stale
551+
552+
553+
def test_add_subplot_twotuple():
554+
fig = plt.figure()
555+
ax1 = fig.add_subplot(3, 2, (3, 5))
556+
assert ax1.get_subplotspec().rowspan == range(1, 3)
557+
assert ax1.get_subplotspec().colspan == range(0, 1)
558+
ax2 = fig.add_subplot(3, 2, (4, 6))
559+
assert ax2.get_subplotspec().rowspan == range(1, 3)
560+
assert ax2.get_subplotspec().colspan == range(1, 2)
561+
ax3 = fig.add_subplot(3, 2, (3, 6))
562+
assert ax3.get_subplotspec().rowspan == range(1, 3)
563+
assert ax3.get_subplotspec().colspan == range(0, 2)
564+
ax4 = fig.add_subplot(3, 2, (4, 5))
565+
assert ax4.get_subplotspec().rowspan == range(1, 3)
566+
assert ax4.get_subplotspec().colspan == range(0, 2)
567+
with pytest.raises(IndexError):
568+
fig.add_subplot(3, 2, (6, 3))

0 commit comments

Comments
 (0)