Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8f8cf4e

Browse files
committed
FIX: restore creating new axes via plt.subplot with different kwargs
This adds a small amount of additional state to the Axes created via Figure.add_axes and Figure.add_subplot (which the other Axes creation methods eventually funnel through) to track the projection class and (processed) kwargs. We then use that state in `pyplot.subplot` to determine if we should re-use an Axes found at a given position or create a new one (and implicitly destroy the existing one). This also changes the behavior of `plt.subplot` when no kwargs are passed to return the Axes at the location without doing any matching of the kwargs. As part of this work we also fixed two additional bugs: - you can now force Axes "back to" rectilinear Axes by passing projection='rectilinear' - Axes3D instances can now be re-selected at all closes #19432 closes #10700
1 parent 00e82c4 commit 8f8cf4e

File tree

6 files changed

+285
-121
lines changed

6 files changed

+285
-121
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
``plt.subplot`` re-selection without kwargs
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
The purpose of `pyplot.subplot` is to facilitate creating and
5+
re-selecting Axes in a Figure when working in the implicit pyplot API.
6+
When used to create Axes it is possible to select the projection
7+
(e.g. polar, 3D, or various cartographic projections) as well as to pass
8+
additional keyword arguments through to the Axes-subclass that is created.
9+
10+
The first time `pyplot.subplot` is called for a given position in the
11+
Axes grid it is clear that the correct behavior is to create and
12+
return an Axes at the given position with the passed arguments
13+
(defaulting to a rectilinear Axes). However, on n subsequent calls to
14+
`pyplot.subplot` the correct behavior is more subtle. If an Axes exists
15+
at the requested location and has equivalent parameters the existing axes
16+
should be returned, but if the parameters are different the old Axes is removed
17+
from the Figure and a new Axes is created and returned.
18+
19+
Due to an implementation detail, it was previously the case that for all
20+
Axes subclass, except for Axes3D, the axes would be deemed equivalent even
21+
if the projections were different. Thus ::
22+
23+
ax1 = plt.subplot(1, 1, 1, projection='polar')
24+
ax1 is plt.subplots(1, 1, 1)
25+
26+
We are embracing this long standing behavior and in the case when no keyword
27+
arguments are passed to `pyplot.subplot` and there is an existing Axes
28+
at the grid position, the existing Axes is returned without consideration
29+
of the keywords used to create the Axes.
30+
31+
Due to the previous implementation, if there was an existing Axes what
32+
was not rectilinear, passing ::
33+
34+
plt.subplot(projection='rectilinear')
35+
36+
would return the existing Axes. Now this will replace the existing Axes
37+
with a new rectilinear Axes. To get the previous behavior, do not pass
38+
*projection*.
39+
40+
Previously Axes3D could not be re-selected with `pyplot.subplot` due
41+
to an unrelated bug. While Axes3D are now consistent with all other projections
42+
there is a change in behavior for ::
43+
44+
plt.subplot(projection='3d') # create a 3D Axes
45+
46+
plt.subplot() # now returns existing 3D Axes
47+
# previously created new 2D Axes
48+
49+
plt.subplot(projection='rectilinear') # to get a new 2D Axes

doc/users/next_whats_new/axes_kwargs_collision.rst

Lines changed: 14 additions & 14 deletions
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 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/figure.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ def add_axes(self, *args, **kwargs):
568568

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

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

586588
@docstring.dedent_interpd
587589
def add_subplot(self, *args, **kwargs):
@@ -694,6 +696,7 @@ def add_subplot(self, *args, **kwargs):
694696

695697
if len(args) == 1 and isinstance(args[0], SubplotBase):
696698
ax = args[0]
699+
key = getattr(ax, '_projection_init', ())
697700
if ax.get_figure() is not self:
698701
raise ValueError("The Subplot must have been created in "
699702
"the present figure")
@@ -706,17 +709,20 @@ def add_subplot(self, *args, **kwargs):
706709
if (len(args) == 1 and isinstance(args[0], Integral)
707710
and 100 <= args[0] <= 999):
708711
args = tuple(map(int, str(args[0])))
709-
projection_class, kwargs = self._process_projection_requirements(
712+
projection_class, pkw = self._process_projection_requirements(
710713
*args, **kwargs)
711-
ax = subplot_class_factory(projection_class)(self, *args, **kwargs)
712-
return self._add_axes_internal(ax)
714+
ax = subplot_class_factory(projection_class)(self, *args, **pkw)
715+
key = (projection_class, pkw)
716+
return self._add_axes_internal(ax, key)
713717

714-
def _add_axes_internal(self, ax):
718+
def _add_axes_internal(self, ax, key):
715719
"""Private helper for `add_axes` and `add_subplot`."""
716720
self._axstack.push(ax)
717721
self._localaxes.push(ax)
718722
self.sca(ax)
719723
ax._remove_method = self.delaxes
724+
# this is to support plt.subplot's re-selection logic
725+
ax._projection_init = key
720726
self.stale = True
721727
ax.stale_callback = _stale_figure_callback
722728
return ax

lib/matplotlib/pyplot.py

Lines changed: 57 additions & 19 deletions
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,7 +1200,27 @@ 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+
proj_placeholder = object()
1209+
# sort out if the user passed us either polar or projection
1210+
# in principle we need to handle if *axes_class* is passed, but
1211+
# we will leave that validation to down stream code. Here we will
1212+
# only normalize `polar=True` vs `projection='polar'` so we can decide
1213+
# if we should create a new Axes or return the existing one
1214+
projection = kwargs.get('projection', proj_placeholder)
1215+
polar = kwargs.pop('polar', proj_placeholder)
1216+
if polar is not proj_placeholder and polar:
1217+
# if we got mixed messages from the user, raise
1218+
if projection is not proj_placeholder and projection != 'polar':
1219+
raise ValueError(
1220+
f"polar={polar}, yet projection={projection!r}. "
1221+
"Only one of these arguments should be supplied."
1222+
)
1223+
kwargs['projection'] = projection = 'polar'
12031224

12041225
# if subplot called without arguments, create subplot(1, 1, 1)
12051226
if len(args) == 0:
@@ -1228,10 +1249,27 @@ def subplot(*args, **kwargs):
12281249
(ax for ax in fig.axes
12291250
if hasattr(ax, 'get_subplotspec') and ax.get_subplotspec() == key),
12301251
None)
1252+
# if we found an axes at the right position sort out if we
1253+
# should re-use it
1254+
if ax is not None:
1255+
# sort out what projection and class would be created so we
1256+
# can tell if we should re-use an existing one
1257+
p_class, pkw = fig._process_projection_requirements(*args, **kwargs)
1258+
# if no projection or kwargs were passed, return the axes we
1259+
# found.
1260+
if projection is proj_placeholder and pkw == {}:
1261+
ax = ax
1262+
# if the axes class and kwargs are identical reuse the Axes
1263+
elif getattr(ax, '_projection_init', ()) == (p_class, pkw):
1264+
ax = ax
1265+
# otherwise we need to make a new one!
1266+
else:
1267+
ax = None
12311268

1232-
# If no existing axes match, then create a new one.
1269+
# if we do not want to re-use the existing Axes,
12331270
if ax is None:
12341271
ax = fig.add_subplot(*args, **kwargs)
1272+
12351273
fig.sca(ax)
12361274

12371275
bbox = ax.bbox

lib/matplotlib/tests/test_figure.py

Lines changed: 13 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -939,30 +939,32 @@ def test_subfigure_double():
939939
axsRight = subfigs[1].subplots(2, 2)
940940

941941

942-
def test_axes_kwargs():
943-
# plt.axes() always creates new axes, even if axes kwargs differ.
944-
plt.figure()
945-
ax = plt.axes()
946-
ax1 = plt.axes()
942+
def test_add_subplot_kwargs():
943+
# fig.add_subplot() always creates new axes, even if axes kwargs differ.
944+
fig = plt.figure()
945+
ax = fig.add_subplot(1, 1, 1)
946+
ax1 = fig.add_subplot(1, 1, 1)
947947
assert ax is not None
948948
assert ax1 is not ax
949949
plt.close()
950950

951-
plt.figure()
952-
ax = plt.axes(projection='polar')
953-
ax1 = plt.axes(projection='polar')
951+
fig = plt.figure()
952+
ax = fig.add_subplot(1, 1, 1, projection='polar')
953+
ax1 = fig.add_subplot(1, 1, 1, projection='polar')
954954
assert ax is not None
955955
assert ax1 is not ax
956956
plt.close()
957957

958-
plt.figure()
959-
ax = plt.axes(projection='polar')
960-
ax1 = plt.axes()
958+
fig = plt.figure()
959+
ax = fig.add_subplot(1, 1, 1, projection='polar')
960+
ax1 = fig.add_subplot(1, 1, 1)
961961
assert ax is not None
962962
assert ax1.name == 'rectilinear'
963963
assert ax1 is not ax
964964
plt.close()
965965

966+
967+
def test_add_axes_kwargs():
966968
# fig.add_axes() always creates new axes, even if axes kwargs differ.
967969
fig = plt.figure()
968970
ax = fig.add_axes([0, 0, 1, 1])
@@ -985,73 +987,3 @@ def test_axes_kwargs():
985987
assert ax1.name == 'rectilinear'
986988
assert ax1 is not ax
987989
plt.close()
988-
989-
# fig.add_subplot() always creates new axes, even if axes kwargs differ.
990-
fig = plt.figure()
991-
ax = fig.add_subplot(1, 1, 1)
992-
ax1 = fig.add_subplot(1, 1, 1)
993-
assert ax is not None
994-
assert ax1 is not ax
995-
plt.close()
996-
997-
fig = plt.figure()
998-
ax = fig.add_subplot(1, 1, 1, projection='polar')
999-
ax1 = fig.add_subplot(1, 1, 1, projection='polar')
1000-
assert ax is not None
1001-
assert ax1 is not ax
1002-
plt.close()
1003-
1004-
fig = plt.figure()
1005-
ax = fig.add_subplot(1, 1, 1, projection='polar')
1006-
ax1 = fig.add_subplot(1, 1, 1)
1007-
assert ax is not None
1008-
assert ax1.name == 'rectilinear'
1009-
assert ax1 is not ax
1010-
plt.close()
1011-
1012-
# plt.subplot() searches for axes with the same subplot spec, and if one
1013-
# exists, returns it, regardless of whether the axes kwargs were the same.
1014-
fig = plt.figure()
1015-
ax = plt.subplot(1, 2, 1)
1016-
ax1 = plt.subplot(1, 2, 1)
1017-
ax2 = plt.subplot(1, 2, 2)
1018-
ax3 = plt.subplot(1, 2, 1, projection='polar')
1019-
assert ax is not None
1020-
assert ax1 is ax
1021-
assert ax2 is not ax
1022-
assert ax3 is ax
1023-
assert ax.name == 'rectilinear'
1024-
assert ax3.name == 'rectilinear'
1025-
plt.close()
1026-
1027-
# plt.gca() returns an existing axes, unless there were no axes.
1028-
plt.figure()
1029-
ax = plt.gca()
1030-
ax1 = plt.gca()
1031-
assert ax is not None
1032-
assert ax1 is ax
1033-
plt.close()
1034-
1035-
# plt.gca() raises a DeprecationWarning if called with kwargs.
1036-
plt.figure()
1037-
with pytest.warns(
1038-
MatplotlibDeprecationWarning,
1039-
match=r'Calling gca\(\) with keyword arguments was deprecated'):
1040-
ax = plt.gca(projection='polar')
1041-
ax1 = plt.gca()
1042-
assert ax is not None
1043-
assert ax1 is ax
1044-
assert ax1.name == 'polar'
1045-
plt.close()
1046-
1047-
# plt.gca() ignores keyword arguments if an axes already exists.
1048-
plt.figure()
1049-
ax = plt.gca()
1050-
with pytest.warns(
1051-
MatplotlibDeprecationWarning,
1052-
match=r'Calling gca\(\) with keyword arguments was deprecated'):
1053-
ax1 = plt.gca(projection='polar')
1054-
assert ax is not None
1055-
assert ax1 is ax
1056-
assert ax1.name == 'rectilinear'
1057-
plt.close()

0 commit comments

Comments
 (0)