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

Skip to content

Commit 337dc38

Browse files
tacaswellQuLogic
andcommitted
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 Co-authored-by: Elliott Sales de Andrade <[email protected]>
1 parent 02c1834 commit 337dc38

File tree

6 files changed

+304
-126
lines changed

6 files changed

+304
-126
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
``plt.subplot`` re-selection without keyword arguments
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
8+
pass additional keyword arguments through to the Axes-subclass that is
9+
created.
10+
11+
The first time `.pyplot.subplot` is called for a given position in the
12+
Axes grid it is clear that the correct behavior is to create and
13+
return an Axes at the given position with the passed arguments
14+
(defaulting to a rectilinear Axes). However, on subsequent calls to
15+
`.pyplot.subplot` the correct behavior is more subtle. If an Axes
16+
exists at the requested location and has equivalent parameters the
17+
existing Axes should be returned, but if the parameters are different
18+
the old Axes is removed from the Figure and a new Axes is created and
19+
returned. This leaves open the question of what is "equivalent
20+
parameters".
21+
22+
Due to an implementation detail, it was previously the case that for
23+
all Axes subclass, except for Axes3D, the Axes would be considered
24+
equivalent to a 2D rectilinear Axes, despite having different
25+
projections, if the kwargs (other than *projection* matched. Thus ::
26+
27+
ax1 = plt.subplot(1, 1, 1, projection='polar')
28+
ax1 is plt.subplots(1, 1, 1)
29+
30+
We are embracing this long standing behavior to ensure that in the
31+
case when no keyword arguments (of any sort) are passed to
32+
`.pyplot.subplot` any existing Axes is returned, without consideration
33+
for key words used to initially create it. This will cause a change
34+
in behavior when keywords were passed to the original axes ::
35+
36+
ax1 = plt.subplot(111, projection='polar', theta_offset=.75)
37+
ax1 is is plt.subplots(1, 1, 1) # new behavior
38+
# ax1 is not plt.subplots(1, 1, 1) # would make
39+
40+
Similarly, if there was an existing Axes that was not rectilinear,
41+
passing ``projection='rectilinear'`` would return (usually) return the
42+
existing Axes: ::
43+
44+
ax1 = plt.subplot(projection='polar')
45+
ax2 = plt.subplot(projection='rectilinear')
46+
ax1 is not ax2 # new behavior
47+
# ax1 is ax2 # old behavior
48+
49+
50+
To get the previous behavior, do not pass *projection* (or any keyword
51+
argument) to `.pyplot.subplot`.
52+
53+
Previously Axes3D could not be re-selected with `.pyplot.subplot` due
54+
to an unrelated bug. While Axes3D are now consistent with all other projections
55+
there is a change in behavior for ::
56+
57+
plt.subplot(projection='3d') # create a 3D Axes
58+
59+
plt.subplot() # now returns existing 3D Axes, but
60+
# previously created new 2D Axes
61+
62+
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 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/figure.py

Lines changed: 13 additions & 7 deletions
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 = getattr(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 = getattr(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

lib/matplotlib/pyplot.py

Lines changed: 63 additions & 24 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+
unset = 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', unset)
1215+
polar = kwargs.pop('polar', unset)
1216+
if polar is not unset and polar:
1217+
# if we got mixed messages from the user, raise
1218+
if projection is not unset 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:
@@ -1224,14 +1245,32 @@ def subplot(*args, **kwargs):
12241245

12251246
# First, search for an existing subplot with a matching spec.
12261247
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)
12311248

1232-
# If no existing axes match, then create a new one.
1233-
if ax is None:
1249+
for ax in fig.axes:
1250+
if hasattr(ax, 'get_subplotspec') and ax.get_subplotspec() == key:
1251+
# if we found an axes at the right position sort out if we
1252+
# should re-use it
1253+
1254+
# sort out what projection class and kwargs would be
1255+
# created so we can tell if we should re-use an existing
1256+
# one
1257+
p_class, pkw = fig._process_projection_requirements(
1258+
*args, **kwargs
1259+
)
1260+
# if no projection or projection kwargs were passed,
1261+
# return the axes we found with out checking anything
1262+
# else.
1263+
if projection is unset and pkw == {}:
1264+
break
1265+
# otherwise, only reuse if the axes class and kwargs are
1266+
# identical
1267+
elif getattr(ax, '_projection_init', ()) == (p_class, pkw):
1268+
break
1269+
else:
1270+
# in the case where we have exhausted the know Axes, make a
1271+
# new one!
12341272
ax = fig.add_subplot(*args, **kwargs)
1273+
12351274
fig.sca(ax)
12361275

12371276
bbox = ax.bbox

0 commit comments

Comments
 (0)