Movatterモバイル変換


[0]ホーム

URL:


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

Sign up
Appearance settings

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

Merged
QuLogic merged 3 commits intomatplotlib:masterfromtacaswell:tweak_subplots
Feb 19, 2021

Conversation

tacaswell
Copy link
Member

@tacaswelltacaswell commentedFeb 2, 2021
edited
Loading

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 kwargs passed
through.

We then use that state inpyplot.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).

closes#19432
closes#10700

PR Summary

PR Checklist

  • Has pytest style unit tests (andpytest passes).
  • IsFlake 8 compliant (runflake8 on changed files to check).
  • New features are documented, with examples if plot related.
  • Documentation is sphinx and numpydoc compliant (the docs shouldbuild without error).
  • Conforms to Matplotlib style conventions (installflake8-docstrings and runflake8 --docstring-convention=all).
  • New features have an entry indoc/users/next_whats_new/ (follow instructions in README.rst there).
  • API changes documented indoc/api/next_api_changes/ (follow instructions in README.rst there).

@tacaswelltacaswell added the Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. labelFeb 2, 2021
@tacaswelltacaswell added this to thev3.4.0 milestoneFeb 2, 2021
jklymak
jklymak previously approved these changesFeb 2, 2021
@tacaswell
Copy link
MemberAuthor

If people are happy with this (which seems to be the case) I still need to chase through the docs to make sure they are still correct.

fig = plt.figure()
ax = plt.subplot(1, 2, 1)
ax1 = plt.subplot(1, 2, 1)
ax2 = plt.subplot(1, 2, 2)
# This will delete ax / ax1 as they fully overlap
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Isn't the reason for deletion actually that the projection keywords are not the same?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

That is what I thought too, but the logic is

bbox=ax.bbox
axes_to_delete= []
forother_axinfig.axes:
ifother_ax==ax:
continue
ifbbox.fully_overlaps(other_ax.bbox):
axes_to_delete.append(other_ax)
forax_to_delinaxes_to_delete:
delaxes(ax_to_del)
which says "delete any axes which the now axes fully covers" .

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@tacaswell is correct. I wasn't aware of this behavior, but it's well documented, and it's been around a long time, so this is 👍 for me.

Copy link
Member

@dstansbydstansby left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Looks good to me 👍

@anntzer
Copy link
Contributor

anntzer commentedFeb 2, 2021
edited
Loading

I believe this suffers from the same underlying problem that caused the old implementation to be so roundabout: some keys are not hashable because they are mutable. To be concrete, if you write e.g.

lims = [.1, .1, .9, .9]ax1 = fig.add_axes(lims)lims[0] = .2ax2 = fig.add_axes(lims)ax3 = fig.add_axes([.1, .1, .9, .9])ax4 = fig.add_axes([.2, .1, .9, .9])

what are the axes that you think should be the same? (A similar, and probably less artificial, argument applies e.g. foradd_subplot(111, projection=some_mutable_thing).

(Holding onto kwargs can be useful, but I think this more or less requires (in this specific case) enforcing that projections be immutable.)

@jklymak
Copy link
Member

@anntzer, I'm not following - first this PR doesn't do anything for add_axes etc. it only affectsplt.subplot, and the only danger of

plt.subplot(projection=myMutableProjection)myMutableProjection.mutate()plt.subplot(projection=myMutableProjection)

is that they will lose the original plot because the projection has changed, in which case they should just keep the reference:

ax = plt.subplot(projection=myMutableProjection)myMutableProjection.mutate()plt.sca(ax)

That seems pretty esoteric to me. But it is certainly better than breaking the people who expect the axes to clear when doing

plt.subplot(projection=proj1)...plt.subplot(projection=proj2)

@anntzer
Copy link
Contributor

I wrote the example with add_axes for simplicity, but again something similar can happen with projections if they are not immutable. I don't actually know much about cartopy or WCSAxes, but I don't think it would be incredible to have some projections library that supports e.g.proj.set_central_longitude(whatever) (for example), in which case you run into the same issue (proj = Proj(); subplot(proj); <sometime later>; proj.set_central_longitude(whatever); subplot(proj) which doesn't seem so unlikely to happen).

And if the project is actually immutable, then it should normally be hashable too, so that would be one thing to check.

@jklymak
Copy link
Member

proj = Proj(); subplot(proj); <sometime later>; proj.set_central_longitude(whatever); subplot(proj)

right, and with this change, the second subplot will overwrite the first. But a) thats hopefully rare, and b) when that happens, presumably its pretty obvious, and we can just tell people to save the reference to the subplot, rather than fragile grabbing from thesubplot stack

@anntzer
Copy link
Contributor

the second subplot will overwrite the first

I believe that what will happen is that the second call will return the preexisting axes as is?

presumably its pretty obvious, and we can just tell people to save the reference to the subplot

Just to be clear, it is far from obvious to me what people using the implicit machinery may be expecting, because it's been too long since I've last used it for this kind of manipulations, but heh.

@tacaswell
Copy link
MemberAuthor

The key behavior change in#19153 (and what caused the re-write from#18978 is that we moved the re-use or new (and remove) logic into pyplot from theFigure methods. I think everyone is still on board withadd_subplot doing what it says on the tin and always adding a new Axes to the Figure. I think it is fair to ask people who are using the explicit (nee OO) version of the API to explicitly keep track of their Axes(es) and the only behavior still up for discussion is whatpyplot does.

The argument for keeping the "create or query" logic in pyplot is that if the users are going fully down the implicit route (a number of years ago I had an lengthy discussion with a user who was convinced beyond doubt that the implicit API was easier and clearer to use, 30-45 minutes of effort and several drinks had no effect) then this is the only way for them to come back to an Axes via the internalsca call (which I discovered while writing this up was broken so fixed that too).


If we have a well behaved non-mutable input (or the users just never mutate it ;) ) we have:

obj = SomeClass()ax1 = plt.subplot(111, keyword=obj)obj2 = SomeClass()ax2 = plt.subplots(111, keyword=obj2)
  • on 3.3 this returns a new Axes and removes the existing one
  • on master this return the first Axes because we ignore the kwargs when doing the matching (which@astrofrog makes a good case is a major API change, I think we have to back out of one way or the other)
  • with this patch it return a new Axes and remove the existing one.

However@anntzer is correct that this is opening is up to some new odd behavior when the values are mutable. Python is too flexible to reliably tell if something is mutable (and even checking if it is hashable is not technically enough as you can over-ride__hash__ is questionable ways).

obj = MutableClass()ax1 = plt.subplot(111, keyword=obj)obj.mutate()ax2 = plt.subplots(111, keyword=obj)
  • On 3.3, this gives you back the same Axes or fail to create the first axes depending on ifobj is hashable.
  • On master we fully ignore the kwarg the second time through and return the existing axes
  • With this patch we still get back the same Axes because we decide that the kwargs are equal

Reusing the axes may or may not be the right thing because it will end up depending on how the projections work (which is between the objects being based in and the project code we have discovered). If they look at what ever state is inobj and draw time, then getting back the same Axes is correct, if the state is used at Axes creation time then this behavior is wrong.

We could make a shallow copy of all of the values, but that would only push the problem "down a layer" or make things wrong in the opposite case as above. We could make a deep copy which may be expensive and inverts the cases where the behavior is wrong.

There is already going to be an issue the values are numpy arrays ("truth value of an array is ambiguous!"), but they also didn't work in <=3.3 so I am not worried about that

I think on net, the behavior in this patch is the best option (modulo adding some docs). In addition we could:

  • always warn in pyplot with kwargs, but that would be annoying to polar or 3D users (who are giving us string values).
  • explicitly allow some known OK keys / values and warn on anything else
  • try to hash the values as a hokey test

I'm inclined to leave it as-is and not get too deep on the semantics of this behavior with the "fix" for user problems is "please use the explicit API where you have a variable name to keep track of what Axes you want to use rather than a proxy variable name through some shared state".

lpsinger reacted with thumbs up emoji

@jklymak
Copy link
Member

OK, I misunderstood@anntzer. The problem is that the user might mutate the object and think that they should get a new axes when they call subplot the second time. I guess I am OK with that being squishy and I still stand by my approval of this approach...

@tacaswell
Copy link
MemberAuthor

I am now abit worried aboutpyplot.axes as well.

@anntzer
Copy link
Contributor

Per the above I don't really know what's best for plt.subplot() users, so as long as the implications with mutable args have been thought out I'm fine with that (I agree with@tacaswell fig.add_subplot should just always add a new axes).

@lpsinger
Copy link
Contributor

I have confirmed that, if merged, this will fix the issues in Astropy. Seeastropy/astropy#11315.

jklymak and astrofrog reacted with thumbs up emoji

@astrofrog
Copy link
Contributor

This looks good to me, thanks@tacaswell!

@jklymakjklymak dismissed theirstale reviewFebruary 11, 2021 14:40

PR changed....

@jklymakjklymak removed the request for review fromastrofrogFebruary 11, 2021 15:10
@anntzer
Copy link
Contributor

See also#10700.

@tacaswelltacaswell marked this pull request as ready for reviewFebruary 12, 2021 05:41
@tacaswell
Copy link
MemberAuthor

OK,this time I think I got enough of the corner cases. It is a fair bit of code, but I think we have gotten all of the logic layed out in one place now :) Most of the line count is additional tests.

"""
proj_placeholder = object()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

perhaps call thisunset, I guess the conditions below read better?

should be returned, but if the parameters are different the old Axes is removed
from the Figure and a new Axes is created and returned.

Due to an implementation detail, it was previously the case that for all
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Is this implementation detail what was introduced by my changes in#19153? If so, we need not say that it was "previously the case" because those changes have never been in a release yet.

Copy link
MemberAuthor

@tacaswelltacaswellFeb 17, 2021
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

no, this is abug behavior that goes back to 2.2.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

  • atleast 2.2

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

but this text is missing a crucial half-sentence ....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Got it. Thanks!

"""
proj_placeholder = object()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Why is the placeholder object necessary? Why notNone?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Because the user can palusibly pass inNone, the goal here is to detect if the user passed usanyting .@anntzer 's suggest of calling thisunset is better.

@@ -1228,11 +1249,29 @@ def subplot(*args, **kwargs):
(ax for ax in fig.axes
if hasattr(ax, 'get_subplotspec') and ax.get_subplotspec() == key),
None)
# if we found an axes at the right position sort out if we
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

theax = ax below are a bit weird even though I get the logic. Perhaps rewrite starting by removing the call tonext() and then

for ax in fix.axes:    if hasattr(ax, "...") and ax.get_subplotspec() == key:        p_class, pkw = ...        if <reuse>:            break  # found itelse:  # didn't find an ax we can reuse, so make a new one    ax = fig.add_subplot(...)

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Yup, I like that better. I opted to doax=ax rather thanpass in the bodies of the if statements.

p_class, pkw = fig._process_projection_requirements(*args, **kwargs)
# if no projection or kwargs were passed, return the axes we
# found.
if projection is proj_placeholder and pkw == {}:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

shouldn't this case be handled by the more general case below with_projection_init == (Axes, {})?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

No, because we did not used to include the projection class in the key, only the kwargs, for the matching, and then did isintance checking to see if the Axes was a sub-class of the Axes we found. The first condition is for the "just get me that Axes" logic, the second is for if the user passed anything that we need to resolve.

ax2 = plt.subplot(111, polar=True)
ax3 = plt.subplot(111, polar=True, projection='polar')
assert ax1 is ax2
assert ax1 is ax3
Copy link
Member

@jklymakjklymakFeb 17, 2021
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

My opinion is still that a lot of the logic and copious comments in this PR are to satisfy this case. While I freely admit this worked before, I still do not feel preserving this is worth all this extra logic. The point here is to give the user a handle back to their original object. Alreadyax4=plt.subplot(111) will do that for them, as well as calling it with the original kwargs. I'd be shocked if we broke anyone by making the asserts fail, or if we did that they would not have a trivial fix to make it work again.

@tacaswell
Copy link
MemberAuthor

Simplified the implementation and line-wrapped the comments to 79 characters.

tacaswelland others added2 commitsFebruary 18, 2021 17:45
This adds a small amount of additional state to the Axes created viaFigure.add_axes and Figure.add_subplot (which the other Axes creationmethods eventually funnel through) to track the projection class and(processed) kwargs.We then use that state in `pyplot.subplot` to determine if we shouldre-use an Axes found at a given position or create a new one (andimplicitly destroy the existing one).This also changes the behavior of `plt.subplot` when no kwargs arepassed to return the Axes at the location without doing any matchingof 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 allclosesmatplotlib#19432closesmatplotlib#10700Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
@QuLogicQuLogic merged commit7ae79cc intomatplotlib:masterFeb 19, 2021
Copy link
Contributor

@lpsingerlpsinger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Good news, astropy tests are still passing. Just spotted one typo, and suggested one very minor change that is a matter of style choice.

# 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 == {}:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Suggested change
ifkwargs== {}:
ifnotkwargs:

but I know it's just a matter of style.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The 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== version communicates to the reader better the condition they should be thinking when the read it.

lpsinger added a commit to lpsinger/matplotlib that referenced this pull requestFeb 19, 2021
QuLogic added a commit that referenced this pull requestFeb 19, 2021
@tacaswelltacaswell deleted the tweak_subplots branchFebruary 19, 2021 01:40
MihaiAnton pushed a commit to MihaiAnton/matplotlib that referenced this pull requestMar 8, 2021
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@lpsingerlpsingerlpsinger requested changes

@jklymakjklymakjklymak left review comments

@dstansbydstansbydstansby left review comments

@QuLogicQuLogicQuLogic approved these changes

@anntzeranntzeranntzer approved these changes

Assignees
No one assigned
Labels
Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
Projects
None yet
Milestone
v3.4.0
Development

Successfully merging this pull request may close these issues.

Unexpected change in behavior in plt.subplot gca(projection="rectilinear") fails to reset projection on axes
7 participants
@tacaswell@anntzer@jklymak@lpsinger@astrofrog@QuLogic@dstansby

[8]ページ先頭

©2009-2025 Movatter.jp