Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork7.9k
FIX: restore creating new axes via plt.subplot with different kwargs#19438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
``plt.subplot`` re-selection without keyword arguments | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
The purpose of `.pyplot.subplot` is to facilitate creating and re-selecting | ||
Axes in a Figure when working strictly in the implicit pyplot API. When | ||
creating new Axes it is possible to select the projection (e.g. polar, 3D, or | ||
various cartographic projections) as well as to pass additional keyword | ||
arguments through to the Axes-subclass that is created. | ||
The first time `.pyplot.subplot` is called for a given position in the Axes | ||
grid it always creates and return a new Axes with the passed arguments and | ||
projection (defaulting to a rectilinear). On subsequent calls to | ||
`.pyplot.subplot` we have to determine if an existing Axes has equivalent | ||
parameters, in which case in should be selected as the current Axes and | ||
returned, or different parameters, in which case a new Axes is created and the | ||
existing Axes is removed. This leaves the question of what is "equivalent | ||
parameters". | ||
Previously it was the case that an existing Axes subclass, except for Axes3D, | ||
would be considered equivalent to a 2D rectilinear Axes, despite having | ||
different projections, if the kwargs (other than *projection*) matched. Thus | ||
:: | ||
ax1 = plt.subplot(1, 1, 1, projection='polar') | ||
ax2 = plt.subplots(1, 1, 1) | ||
ax1 is ax2 | ||
We are embracing this long standing behavior to ensure that in the case when no | ||
keyword arguments (of any sort) are passed to `.pyplot.subplot` any existing | ||
Axes is returned, without consideration for keywords or projection used to | ||
initially create it. This will cause a change in behavior when additional | ||
keywords were passed to the original axes :: | ||
ax1 = plt.subplot(111, projection='polar', theta_offset=.75) | ||
ax2 = plt.subplots(1, 1, 1) | ||
ax1 is ax2 # new behavior | ||
# ax1 is not ax2 # old behavior, made a new axes | ||
ax1 = plt.subplot(111, label='test') | ||
ax2 = plt.subplots(1, 1, 1) | ||
ax1 is ax2 # new behavior | ||
# ax1 is not ax2 # old behavior, made a new axes | ||
For the same reason, if there was an existing Axes that was not rectilinear, | ||
passing ``projection='rectilinear'`` would reuse the existing Axes :: | ||
ax1 = plt.subplot(projection='polar') | ||
ax2 = plt.subplot(projection='rectilinear') | ||
ax1 is not ax2 # new behavior, makes new axes | ||
# ax1 is ax2 # old behavior | ||
contrary to the users request. | ||
Previously Axes3D could not be re-selected with `.pyplot.subplot` due to an | ||
unrelated bug (also fixed in mpl3.4). While Axes3D are now consistent with all | ||
other projections there is a change in behavior for :: | ||
plt.subplot(projection='3d') # create a 3D Axes | ||
plt.subplot() # now returns existing 3D Axes, but | ||
# previously created new 2D Axes | ||
plt.subplot(projection='rectilinear') # to get a new 2D Axes |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -567,6 +567,7 @@ def add_axes(self, *args, **kwargs): | ||
if isinstance(args[0], Axes): | ||
a = args[0] | ||
key = a._projection_init | ||
if a.get_figure() is not self: | ||
raise ValueError( | ||
"The Axes must have been created in the present figure") | ||
@@ -575,12 +576,13 @@ def add_axes(self, *args, **kwargs): | ||
if not np.isfinite(rect).all(): | ||
raise ValueError('all entries in rect must be finite ' | ||
'not {}'.format(rect)) | ||
QuLogic marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
projection_class,pkw = self._process_projection_requirements( | ||
*args, **kwargs) | ||
# create the new axes using the axes class given | ||
a = projection_class(self, rect, **pkw) | ||
key = (projection_class, pkw) | ||
return self._add_axes_internal(a, key) | ||
@docstring.dedent_interpd | ||
def add_subplot(self, *args, **kwargs): | ||
@@ -693,6 +695,7 @@ def add_subplot(self, *args, **kwargs): | ||
if len(args) == 1 and isinstance(args[0], SubplotBase): | ||
ax = args[0] | ||
key = ax._projection_init | ||
if ax.get_figure() is not self: | ||
raise ValueError("The Subplot must have been created in " | ||
"the present figure") | ||
@@ -705,17 +708,20 @@ def add_subplot(self, *args, **kwargs): | ||
if (len(args) == 1 and isinstance(args[0], Integral) | ||
and 100 <= args[0] <= 999): | ||
args = tuple(map(int, str(args[0]))) | ||
projection_class,pkw = self._process_projection_requirements( | ||
*args, **kwargs) | ||
ax = subplot_class_factory(projection_class)(self, *args, **pkw) | ||
key = (projection_class, pkw) | ||
return self._add_axes_internal(ax, key) | ||
def _add_axes_internal(self, ax, key): | ||
"""Private helper for `add_axes` and `add_subplot`.""" | ||
self._axstack.push(ax) | ||
self._localaxes.push(ax) | ||
self.sca(ax) | ||
ax._remove_method = self.delaxes | ||
# this is to support plt.subplot's re-selection logic | ||
ax._projection_init = key | ||
self.stale = True | ||
ax.stale_callback = _stale_figure_callback | ||
return ax | ||
@@ -1502,9 +1508,9 @@ def _process_projection_requirements( | ||
if polar: | ||
if projection is not None and projection != 'polar': | ||
raise ValueError( | ||
f"polar={polar}, yet projection={projection!r}. " | ||
"Only one of these arguments should be supplied." | ||
) | ||
projection = 'polar' | ||
if isinstance(projection, str) or projection is None: | ||
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1072,10 +1072,10 @@ def cla(): | ||||||
@docstring.dedent_interpd | ||||||
def subplot(*args, **kwargs): | ||||||
""" | ||||||
Addan Axes to the current figure or retrieve an existing Axes. | ||||||
This is a wrapperof `.Figure.add_subplot`which provides additional | ||||||
behaviorwhen working withtheimplicit API (see thenotes section). | ||||||
Call signatures:: | ||||||
@@ -1142,8 +1142,8 @@ def subplot(*args, **kwargs): | ||||||
Notes | ||||||
----- | ||||||
Creating anew Axeswill delete any pre-existingAxes that | ||||||
overlapswith it beyond sharing a boundary:: | ||||||
import matplotlib.pyplot as plt | ||||||
# plot a line, implicitly creating a subplot(111) | ||||||
@@ -1156,18 +1156,19 @@ def subplot(*args, **kwargs): | ||||||
If you do not want this behavior, use the `.Figure.add_subplot` method | ||||||
or the `.pyplot.axes` function instead. | ||||||
If no *kwargs* are passed and there exists an Axes in the location | ||||||
specified by *args* then that Axes will be returned rather than a new | ||||||
Axes being created. | ||||||
If *kwargs* are passed and there exists an Axes in the location | ||||||
specified by *args*, the projection type is the same, and the | ||||||
*kwargs* match with the existing Axes, then the existing Axes is | ||||||
returned. Otherwise a new Axes is created with the specified | ||||||
parameters. We save a reference to the *kwargs* which we us | ||||||
QuLogic marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||||||
for this comparison. If any of the values in *kwargs* are | ||||||
mutable we will not detect the case where they are mutated. | ||||||
In these cases we suggest using `.Figure.add_subplot` and the | ||||||
explicit Axes API rather than the implicit pyplot API. | ||||||
See Also | ||||||
-------- | ||||||
@@ -1183,10 +1184,10 @@ def subplot(*args, **kwargs): | ||||||
plt.subplot(221) | ||||||
# equivalent but more general | ||||||
ax1 =plt.subplot(2, 2, 1) | ||||||
# add a subplot with no frame | ||||||
ax2 =plt.subplot(222, frameon=False) | ||||||
# add a polar subplot | ||||||
plt.subplot(223, projection='polar') | ||||||
@@ -1199,18 +1200,34 @@ def subplot(*args, **kwargs): | ||||||
# add ax2 to the figure again | ||||||
plt.subplot(ax2) | ||||||
# make the first axes "current" again | ||||||
plt.subplot(221) | ||||||
""" | ||||||
# Here we will only normalize `polar=True` vs `projection='polar'` and let | ||||||
# downstream code deal with the rest. | ||||||
unset = object() | ||||||
projection = kwargs.get('projection', unset) | ||||||
polar = kwargs.pop('polar', unset) | ||||||
if polar is not unset and polar: | ||||||
# if we got mixed messages from the user, raise | ||||||
if projection is not unset and projection != 'polar': | ||||||
raise ValueError( | ||||||
f"polar={polar}, yet projection={projection!r}. " | ||||||
"Only one of these arguments should be supplied." | ||||||
) | ||||||
kwargs['projection'] = projection = 'polar' | ||||||
# if subplot called without arguments, create subplot(1, 1, 1) | ||||||
if len(args) == 0: | ||||||
args = (1, 1, 1) | ||||||
# This check was added because it is very easy to type subplot(1, 2, False) | ||||||
# when subplots(1, 2, False) was intended (sharex=False, that is). In most | ||||||
# cases, no error will ever occur, but mysterious behavior can result | ||||||
# because what was intended to be the sharex argument is instead treated as | ||||||
# a subplot index for subplot() | ||||||
if len(args) >= 3 and isinstance(args[2], bool): | ||||||
_api.warn_external("The subplot index argument to subplot() appears " | ||||||
"to be a boolean. Did you intend to use " | ||||||
@@ -1224,15 +1241,24 @@ def subplot(*args, **kwargs): | ||||||
# First, search for an existing subplot with a matching spec. | ||||||
key = SubplotSpec._from_subplot_args(fig, args) | ||||||
for ax in fig.axes: | ||||||
# if we found an axes at the position sort out if we can re-use it | ||||||
if hasattr(ax, 'get_subplotspec') and ax.get_subplotspec() == key: | ||||||
# if the user passed no kwargs, re-use | ||||||
if kwargs == {}: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Suggested change
but I know it's just a matter of style. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I can see arguments either way here. I want to say "If kwargs is equal to the empty dictionary" which is equivalent to "kwargs is falsy", but I think the | ||||||
break | ||||||
# if the axes class and kwargs are identical, reuse | ||||||
elif ax._projection_init == fig._process_projection_requirements( | ||||||
*args, **kwargs | ||||||
): | ||||||
break | ||||||
else: | ||||||
# we have exhausted the known Axes and none match, make a new one! | ||||||
ax = fig.add_subplot(*args, **kwargs) | ||||||
fig.sca(ax) | ||||||
tacaswell marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||||||
bbox = ax.bbox | ||||||
axes_to_delete = [] | ||||||
for other_ax in fig.axes: | ||||||
Uh oh!
There was an error while loading.Please reload this page.