Skip to content

Commit 7ae79cc

Browse files
authored
Merge pull request #19438 from tacaswell/tweak_subplots
FIX: restore creating new axes via plt.subplot with different kwargs
2 parents 0f83ee2 + c189677 commit 7ae79cc

File tree

7 files changed

+314
-135
lines changed

7 files changed

+314
-135
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
``plt.subplot`` re-selection without keyword arguments
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
The purpose of `.pyplot.subplot` is to facilitate creating and re-selecting
5+
Axes in a Figure when working strictly in the implicit pyplot API. When
6+
creating new Axes it is possible to select the projection (e.g. polar, 3D, or
7+
various cartographic projections) as well as to pass additional keyword
8+
arguments through to the Axes-subclass that is created.
9+
10+
The first time `.pyplot.subplot` is called for a given position in the Axes
11+
grid it always creates and return a new Axes with the passed arguments and
12+
projection (defaulting to a rectilinear). On subsequent calls to
13+
`.pyplot.subplot` we have to determine if an existing Axes has equivalent
14+
parameters, in which case in should be selected as the current Axes and
15+
returned, or different parameters, in which case a new Axes is created and the
16+
existing Axes is removed. This leaves the question of what is "equivalent
17+
parameters".
18+
19+
Previously it was the case that an existing Axes subclass, except for Axes3D,
20+
would be considered equivalent to a 2D rectilinear Axes, despite having
21+
different projections, if the kwargs (other than *projection*) matched. Thus
22+
::
23+
24+
ax1 = plt.subplot(1, 1, 1, projection='polar')
25+
ax2 = plt.subplots(1, 1, 1)
26+
ax1 is ax2
27+
28+
We are embracing this long standing behavior to ensure that in the case when no
29+
keyword arguments (of any sort) are passed to `.pyplot.subplot` any existing
30+
Axes is returned, without consideration for keywords or projection used to
31+
initially create it. This will cause a change in behavior when additional
32+
keywords were passed to the original axes ::
33+
34+
ax1 = plt.subplot(111, projection='polar', theta_offset=.75)
35+
ax2 = plt.subplots(1, 1, 1)
36+
ax1 is ax2 # new behavior
37+
# ax1 is not ax2 # old behavior, made a new axes
38+
39+
ax1 = plt.subplot(111, label='test')
40+
ax2 = plt.subplots(1, 1, 1)
41+
ax1 is ax2 # new behavior
42+
# ax1 is not ax2 # old behavior, made a new axes
43+
44+
45+
For the same reason, if there was an existing Axes that was not rectilinear,
46+
passing ``projection='rectilinear'`` would reuse the existing Axes ::
47+
48+
ax1 = plt.subplot(projection='polar')
49+
ax2 = plt.subplot(projection='rectilinear')
50+
ax1 is not ax2 # new behavior, makes new axes
51+
# ax1 is ax2 # old behavior
52+
53+
54+
contrary to the users request.
55+
56+
Previously Axes3D could not be re-selected with `.pyplot.subplot` due to an
57+
unrelated bug (also fixed in mpl3.4). While Axes3D are now consistent with all
58+
other projections there is a change in behavior for ::
59+
60+
plt.subplot(projection='3d') # create a 3D Axes
61+
62+
plt.subplot() # now returns existing 3D Axes, but
63+
# previously created new 2D Axes
64+
65+
plt.subplot(projection='rectilinear') # to get a new 2D Axes

doc/users/next_whats_new/axes_kwargs_collision.rst

+14-14
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ Changes to behavior of Axes creation methods (``gca()``, ``add_axes()``, ``add_s
33

44
The behavior of the functions to create new axes (`.pyplot.axes`,
55
`.pyplot.subplot`, `.figure.Figure.add_axes`,
6-
`.figure.Figure.add_subplot`) has changed. In the past, these functions would
7-
detect if you were attempting to create Axes with the same keyword arguments as
8-
already-existing axes in the current figure, and if so, they would return the
9-
existing Axes. Now, these functions will always create new Axes. A special
10-
exception is `.pyplot.subplot`, which will reuse any existing subplot with a
11-
matching subplot spec. However, if there is a subplot with a matching subplot
12-
spec, then that subplot will be returned, even if the keyword arguments with
13-
which it was created differ.
6+
`.figure.Figure.add_subplot`) has changed. In the past, these
7+
functions would detect if you were attempting to create Axes with the
8+
same keyword arguments as already-existing axes in the current figure,
9+
and if so, they would return the existing Axes. Now, `.pyplot.axes`,
10+
`.figure.Figure.add_axes`, and `.figure.Figure.add_subplot` will
11+
always create new Axes. `.pyplot.subplot` will continue to reuse an
12+
existing Axes with a matching subplot spec and equal *kwargs*.
1413

1514
Correspondingly, the behavior of the functions to get the current Axes
16-
(`.pyplot.gca`, `.figure.Figure.gca`) has changed. In the past, these functions
17-
accepted keyword arguments. If the keyword arguments matched an
18-
already-existing Axes, then that Axes would be returned, otherwise new Axes
19-
would be created with those keyword arguments. Now, the keyword arguments are
20-
only considered if there are no axes at all in the current figure. In a future
21-
release, these functions will not accept keyword arguments at all.
15+
(`.pyplot.gca`, `.figure.Figure.gca`) has changed. In the past, these
16+
functions accepted keyword arguments. If the keyword arguments
17+
matched an already-existing Axes, then that Axes would be returned,
18+
otherwise new Axes would be created with those keyword arguments.
19+
Now, the keyword arguments are only considered if there are no axes at
20+
all in the current figure. In a future release, these functions will
21+
not accept keyword arguments at all.

lib/matplotlib/axes/_base.py

+1
Original file line numberDiff line numberDiff line change
@@ -1237,6 +1237,7 @@ def cla(self):
12371237
self._mouseover_set = _OrderedSet()
12381238
self.child_axes = []
12391239
self._current_image = None # strictly for pyplot via _sci, _gci
1240+
self._projection_init = None # strictly for pyplot.subplot
12401241
self.legend_ = None
12411242
self.collections = [] # collection.Collection instances
12421243
self.containers = []

lib/matplotlib/figure.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ def add_axes(self, *args, **kwargs):
567567

568568
if isinstance(args[0], Axes):
569569
a = args[0]
570+
key = a._projection_init
570571
if a.get_figure() is not self:
571572
raise ValueError(
572573
"The Axes must have been created in the present figure")
@@ -575,12 +576,13 @@ def add_axes(self, *args, **kwargs):
575576
if not np.isfinite(rect).all():
576577
raise ValueError('all entries in rect must be finite '
577578
'not {}'.format(rect))
578-
projection_class, kwargs = self._process_projection_requirements(
579+
projection_class, pkw = self._process_projection_requirements(
579580
*args, **kwargs)
580581

581582
# create the new axes using the axes class given
582-
a = projection_class(self, rect, **kwargs)
583-
return self._add_axes_internal(a)
583+
a = projection_class(self, rect, **pkw)
584+
key = (projection_class, pkw)
585+
return self._add_axes_internal(a, key)
584586

585587
@docstring.dedent_interpd
586588
def add_subplot(self, *args, **kwargs):
@@ -693,6 +695,7 @@ def add_subplot(self, *args, **kwargs):
693695

694696
if len(args) == 1 and isinstance(args[0], SubplotBase):
695697
ax = args[0]
698+
key = ax._projection_init
696699
if ax.get_figure() is not self:
697700
raise ValueError("The Subplot must have been created in "
698701
"the present figure")
@@ -705,17 +708,20 @@ def add_subplot(self, *args, **kwargs):
705708
if (len(args) == 1 and isinstance(args[0], Integral)
706709
and 100 <= args[0] <= 999):
707710
args = tuple(map(int, str(args[0])))
708-
projection_class, kwargs = self._process_projection_requirements(
711+
projection_class, pkw = self._process_projection_requirements(
709712
*args, **kwargs)
710-
ax = subplot_class_factory(projection_class)(self, *args, **kwargs)
711-
return self._add_axes_internal(ax)
713+
ax = subplot_class_factory(projection_class)(self, *args, **pkw)
714+
key = (projection_class, pkw)
715+
return self._add_axes_internal(ax, key)
712716

713-
def _add_axes_internal(self, ax):
717+
def _add_axes_internal(self, ax, key):
714718
"""Private helper for `add_axes` and `add_subplot`."""
715719
self._axstack.push(ax)
716720
self._localaxes.push(ax)
717721
self.sca(ax)
718722
ax._remove_method = self.delaxes
723+
# this is to support plt.subplot's re-selection logic
724+
ax._projection_init = key
719725
self.stale = True
720726
ax.stale_callback = _stale_figure_callback
721727
return ax
@@ -1502,9 +1508,9 @@ def _process_projection_requirements(
15021508
if polar:
15031509
if projection is not None and projection != 'polar':
15041510
raise ValueError(
1505-
"polar=True, yet projection=%r. "
1506-
"Only one of these arguments should be supplied." %
1507-
projection)
1511+
f"polar={polar}, yet projection={projection!r}. "
1512+
"Only one of these arguments should be supplied."
1513+
)
15081514
projection = 'polar'
15091515

15101516
if isinstance(projection, str) or projection is None:

lib/matplotlib/pyplot.py

+56-30
Original file line numberDiff line numberDiff line change
@@ -1072,10 +1072,10 @@ def cla():
10721072
@docstring.dedent_interpd
10731073
def subplot(*args, **kwargs):
10741074
"""
1075-
Add a subplot to the current figure.
1075+
Add an Axes to the current figure or retrieve an existing Axes.
10761076
1077-
Wrapper of `.Figure.add_subplot` with a difference in
1078-
behavior explained in the notes section.
1077+
This is a wrapper of `.Figure.add_subplot` which provides additional
1078+
behavior when working with the implicit API (see the notes section).
10791079
10801080
Call signatures::
10811081
@@ -1142,8 +1142,8 @@ def subplot(*args, **kwargs):
11421142
11431143
Notes
11441144
-----
1145-
Creating a subplot will delete any pre-existing subplot that overlaps
1146-
with it beyond sharing a boundary::
1145+
Creating a new Axes will delete any pre-existing Axes that
1146+
overlaps with it beyond sharing a boundary::
11471147
11481148
import matplotlib.pyplot as plt
11491149
# plot a line, implicitly creating a subplot(111)
@@ -1156,18 +1156,19 @@ def subplot(*args, **kwargs):
11561156
If you do not want this behavior, use the `.Figure.add_subplot` method
11571157
or the `.pyplot.axes` function instead.
11581158
1159-
If the figure already has a subplot with key (*args*,
1160-
*kwargs*) then it will simply make that subplot current and
1161-
return it. This behavior is deprecated. Meanwhile, if you do
1162-
not want this behavior (i.e., you want to force the creation of a
1163-
new subplot), you must use a unique set of args and kwargs. The axes
1164-
*label* attribute has been exposed for this purpose: if you want
1165-
two subplots that are otherwise identical to be added to the figure,
1166-
make sure you give them unique labels.
1159+
If no *kwargs* are passed and there exists an Axes in the location
1160+
specified by *args* then that Axes will be returned rather than a new
1161+
Axes being created.
11671162
1168-
In rare circumstances, `.Figure.add_subplot` may be called with a single
1169-
argument, a subplot axes instance already created in the
1170-
present figure but not in the figure's list of axes.
1163+
If *kwargs* are passed and there exists an Axes in the location
1164+
specified by *args*, the projection type is the same, and the
1165+
*kwargs* match with the existing Axes, then the existing Axes is
1166+
returned. Otherwise a new Axes is created with the specified
1167+
parameters. We save a reference to the *kwargs* which we us
1168+
for this comparison. If any of the values in *kwargs* are
1169+
mutable we will not detect the case where they are mutated.
1170+
In these cases we suggest using `.Figure.add_subplot` and the
1171+
explicit Axes API rather than the implicit pyplot API.
11711172
11721173
See Also
11731174
--------
@@ -1183,10 +1184,10 @@ def subplot(*args, **kwargs):
11831184
plt.subplot(221)
11841185
11851186
# equivalent but more general
1186-
ax1=plt.subplot(2, 2, 1)
1187+
ax1 = plt.subplot(2, 2, 1)
11871188
11881189
# add a subplot with no frame
1189-
ax2=plt.subplot(222, frameon=False)
1190+
ax2 = plt.subplot(222, frameon=False)
11901191
11911192
# add a polar subplot
11921193
plt.subplot(223, projection='polar')
@@ -1199,18 +1200,34 @@ def subplot(*args, **kwargs):
11991200
12001201
# add ax2 to the figure again
12011202
plt.subplot(ax2)
1203+
1204+
# make the first axes "current" again
1205+
plt.subplot(221)
1206+
12021207
"""
1208+
# Here we will only normalize `polar=True` vs `projection='polar'` and let
1209+
# downstream code deal with the rest.
1210+
unset = object()
1211+
projection = kwargs.get('projection', unset)
1212+
polar = kwargs.pop('polar', unset)
1213+
if polar is not unset and polar:
1214+
# if we got mixed messages from the user, raise
1215+
if projection is not unset and projection != 'polar':
1216+
raise ValueError(
1217+
f"polar={polar}, yet projection={projection!r}. "
1218+
"Only one of these arguments should be supplied."
1219+
)
1220+
kwargs['projection'] = projection = 'polar'
12031221

12041222
# if subplot called without arguments, create subplot(1, 1, 1)
12051223
if len(args) == 0:
12061224
args = (1, 1, 1)
12071225

1208-
# This check was added because it is very easy to type
1209-
# subplot(1, 2, False) when subplots(1, 2, False) was intended
1210-
# (sharex=False, that is). In most cases, no error will
1211-
# ever occur, but mysterious behavior can result because what was
1212-
# intended to be the sharex argument is instead treated as a
1213-
# subplot index for subplot()
1226+
# This check was added because it is very easy to type subplot(1, 2, False)
1227+
# when subplots(1, 2, False) was intended (sharex=False, that is). In most
1228+
# cases, no error will ever occur, but mysterious behavior can result
1229+
# because what was intended to be the sharex argument is instead treated as
1230+
# a subplot index for subplot()
12141231
if len(args) >= 3 and isinstance(args[2], bool):
12151232
_api.warn_external("The subplot index argument to subplot() appears "
12161233
"to be a boolean. Did you intend to use "
@@ -1224,15 +1241,24 @@ def subplot(*args, **kwargs):
12241241

12251242
# First, search for an existing subplot with a matching spec.
12261243
key = SubplotSpec._from_subplot_args(fig, args)
1227-
ax = next(
1228-
(ax for ax in fig.axes
1229-
if hasattr(ax, 'get_subplotspec') and ax.get_subplotspec() == key),
1230-
None)
12311244

1232-
# If no existing axes match, then create a new one.
1233-
if ax is None:
1245+
for ax in fig.axes:
1246+
# if we found an axes at the position sort out if we can re-use it
1247+
if hasattr(ax, 'get_subplotspec') and ax.get_subplotspec() == key:
1248+
# if the user passed no kwargs, re-use
1249+
if kwargs == {}:
1250+
break
1251+
# if the axes class and kwargs are identical, reuse
1252+
elif ax._projection_init == fig._process_projection_requirements(
1253+
*args, **kwargs
1254+
):
1255+
break
1256+
else:
1257+
# we have exhausted the known Axes and none match, make a new one!
12341258
ax = fig.add_subplot(*args, **kwargs)
12351259

1260+
fig.sca(ax)
1261+
12361262
bbox = ax.bbox
12371263
axes_to_delete = []
12381264
for other_ax in fig.axes:

0 commit comments

Comments
 (0)