Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 7ae79cc

Browse filesBrowse 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
Copy full SHA for 7ae79cc

File tree

Expand file treeCollapse file tree

7 files changed

+314
-135
lines changed
Filter options
Expand file treeCollapse file tree

7 files changed

+314
-135
lines changed
+65Lines changed: 65 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: doc/users/next_whats_new/axes_kwargs_collision.rst
+14-14Lines 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/axes/_base.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_base.py
+1Lines changed: 1 addition & 0 deletions
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

Copy file name to clipboardExpand all lines: lib/matplotlib/figure.py
+16-10Lines changed: 16 additions & 10 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 = 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

Copy file name to clipboardExpand all lines: lib/matplotlib/pyplot.py
+56-30Lines changed: 56 additions & 30 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,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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.