Skip to content

Simpler "pyplotless" use pattern. #14024

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed

Conversation

anntzer
Copy link
Contributor

@anntzer anntzer commented Apr 23, 2019

cf. mailing list discussion on garbage collection.

Right now, if one does not want to use pyplot (e.g., batch use), one can
do

from matplotlib.figure import Figure
fig = Figure(); ... <plot> ...; fig.savefig(...)

but it is impossible to show() the figure interactively (i.e. pyplot
cannot "adopt" the figure) e.g. for debugging.

This patch makes it possible: with it, one can do

fig = plt.new_figure_manager(num).canvas.figure
... <plot> ...
fig.savefig()
plt.show(figures=[fig])

Note that this does not register the figure with pyplot; in particular
fig is garbage-collected when it goes out of scope, does not
participate in gcf(); etc. So this is "pyplotless" in the sense that
there is no global figure registry anymore, but pyplot still stays in
charge of the GUI integration.

Obviously the plt.new_figure_manager(num).canvas.figure expression
could / should be encapsulated in a helper function, up to bikeshedding.

The num parameter is needed as matplotlib currently uses it to set the
figure title, but that can easily be made optional too.

Finally note that plt.show([fig]) would be a nicer API, but right now
the first parameter to plt.show is block, which is undergoing
transition to kwonly.

IOW the end-goal would be e.g.

# intentionally terrible name as placeholder :)
fig = plt.new_figure_not_globally_registered("some title")
... <plot> ...
fig.savefig()
plt.show([fig])

some more thoughts:
While this "logically" belongs to pyplot in the sense that pyplot is in charge of the GUI integration, this doesn't actually interact with any of the pyplot plotting functions that operate on gcf()/gca(), so perhaps it may be better (to avoid user confusion) to have a separate submodule just hosting new_figure_not_globally_registered and show (in which case the latter could have the "better" API show([fig], *, block=False) immediately).


Currently the num argument to new_figure_manager needs to be an int, but note that plt.figure() supports both int and string and the logic to support both can easily be pushed down to new_figure_manager... but preferably after merging #13569 and #13581.

PR Summary

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@anntzer anntzer added this to the v3.2.0 milestone Apr 23, 2019
@jklymak
Copy link
Member

jklymak commented Apr 24, 2019

Huh, as a somewhat knowledgeable user, I had zero idea that pyplot was in charge of GUI integration. When I first read the above I wasn't clear why we don't just have or add fig.show(), which I guess would mean we would need a way for figures to get their own manager.

So, I think that we can go ahead and try to disassociate the GUI integration from pyplot, or we can just accept that some basic things get done in pyplot that have nothing to do w/ the gcf integration.

@anntzer
Copy link
Contributor Author

anntzer commented Apr 24, 2019

The problem of fig.show() is, how would that work if you want to show two figures? (basically you'd want the first show() to not block (so that you can reach the second show(), but the second one to block (so that the process doesn't terminate immediately). As far as I can see, having a single function that takes as parameter a list of figures to be shown is necessary to handle this (not so rare) case.

@jklymak
Copy link
Member

jklymak commented Apr 24, 2019

Forgetting about our current concept of "block", I'd expect you could show as many figures as you want and the process wouldn't end until you manually close all the figures.

@anntzer
Copy link
Contributor Author

anntzer commented Apr 24, 2019

So effectively figure.show() would behave like plt.show(block=False), except that the process does not end at the end of the program? I guess that can work, too.

@jklymak
Copy link
Member

jklymak commented Apr 24, 2019

So effectively figure.show() would behave like plt.show(block=False), except that the process does not end at the end of the program? I guess that can work, too.

Thats how I'd expect a GUI to behave, but no doubt I'm missing a subtlety somewhere...

@ImportanceOfBeingErnest
Copy link
Member

figure.show() doesn't start or run an event loop, while plt.show() does (or in case of ion emulates one), right? So would plt.show([fig1]) start an event loop or not and would it be blocking in case of ioff?

Would

fig1 = plt.figure()
fig2 = mpl.figure.Figure()
plt.show([fig2])

be showing fig1 as well?

@anntzer
Copy link
Contributor Author

anntzer commented Apr 24, 2019

fig.show() can take care of spinning up an event loop if none is running yet, that's not really a problem.

Whether a call to show() would implicitly also include all pyplot-generated figures is just a design choice that needs to be made, I would prefer not but either way is fine.

@ImportanceOfBeingErnest
Copy link
Member

I think we should in general prepare for a way to reshow closed pyplot figures. This is currently one of the most annoying restrictions users face, i.e.

fig, ax = plt.subplots()
ax.plot(...)
plt.show() # Figure is shown, User closes it.
# At this point there is no way[*] to show the figure again.
# Suggestion would be to
plt.show(figures=[fig])
[*] "No way" in the sense of... ...you don't want to do this:
import matplotlib.pyplot as plt

def reshow(fig):
    import importlib
    import matplotlib.backends
    import matplotlib.backend_bases
    backend_mod = importlib.import_module(f"matplotlib.backends.backend_{plt.get_backend().lower()}")
    Backend = type("Backend", (matplotlib.backends._Backend,), vars(backend_mod))
    fm = Backend.new_figure_manager_given_figure(1, fig)
    matplotlib.backend_bases.Gcf.set_active(fm)
    plt.show()
    
fig1, ax1 = plt.subplots()
ax1.plot([1,2], label="ABC")
plt.show() 

#Now reshow the figure
reshow(fig1)

@jklymak
Copy link
Member

jklymak commented Sep 18, 2019

I’m not understanding the situation where the user expects a figure to be able to be reopened. If I click the little red x I expect whatever was there to disappear. You could argue that it could prompt to save but otherwise?

@ImportanceOfBeingErnest
Copy link
Member

Sure, you could register a callback to a close_event that pickles the figure before destroying the manager, such that if unpickled it can be shown again. That is equally cumbersome.

@jklymak
Copy link
Member

jklymak commented Sep 18, 2019

By "save", I meant saving as an image.

I'm 👎 on saving figures so they can be resurrected as an interactive thing later. At the most practical, such figures won't survive changes of matplotlib versions if any internal organization of the objects has changed. Pickle, itself, became incompatible between py2.x and py3.x (I'll never willingly use pickle again). Overall it just seems like a lot of hassle. And why? Users should be able to re-create the figure from their script and not rely on manual fiddling with the GUI or a bunch of manual typing in the REPL.

I admit this is not a universal opinion, but I think having figures that robustly resurrect themselves is really hard from a developers perspective, and not a good idea from the user's perspective.

@ImportanceOfBeingErnest
Copy link
Member

Matplotlib supports pickling of figures just fine (of course in- and output versions must be the same!) and there is a bunch of tests for that, so this is an existing feature, not for debate here. I only mentionned it as workaround for reshowing a figure. And I mentionned the desire for reshowing a figure here, because I think it needs to be taken into account when designing plt.show(figures).

@anntzer
Copy link
Contributor Author

anntzer commented Sep 19, 2019

@ImportanceOfBeingErnest re-show()ing a figure basically just works with this PR.
Actually there's a bug when closing a qt figure for the second time with a key shortcut ("q") because the private _destroying attribute is not cleared out, and I guess other managers may have similar issues, but on pyplot's side I think this works fine.

@ImportanceOfBeingErnest
Copy link
Member

I see. I was missing the fact that apparently closed figures keep their manager. But that wouldn't be the case for a pickled figure, right?

This is what happens
import pickle
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([12, 13], label="ABCDE")
plt.close(fig)

with open('fig.pickle', 'wb') as f:
    pickle.dump(fig, f)

with open('fig.pickle', 'rb') as f:
    fig1 = pickle.load(f)

plt.show(figures=[fig1])

gives

Traceback (most recent call last):
  File "untitled3.py", line 20, in <module>
    plt.show(figures=[fig1])
  File "d:\***\matplotlib\lib\matplotlib\pyplot.py", line 262, in show
    return _show(*args, **kw)
  File "d:\***\matplotlib\lib\matplotlib\cbook\deprecation.py", line 409, in wrapper
    return func(*args, **kwargs)
  File "d:\***\matplotlib\lib\matplotlib\backend_bases.py", line 3278, in show
    if figures is not None else Gcf.get_all_fig_managers())
  File "d:\***\matplotlib\lib\matplotlib\backend_bases.py", line 3277, in <listcomp>
    managers = ([figure.canvas.manager for figure in figures]
AttributeError: 'NoneType' object has no attribute 'manager'

Not sure if it should work though.

@anntzer
Copy link
Contributor Author

anntzer commented Sep 19, 2019

A manager is (holding) a GUI widget, so it's going to be tricky to pickle :p
The relevant piece of code is basically at

if getattr(self.canvas, 'manager', None) \
and
if restore_to_pylab:
-- note that the unpickler explicitly recreates a fresh manager if restore_to_pylab is set, and the pickler explicitly doesn't set it if the figure has been evicted from Gcf (as happens with close()).
Probably just a matter of replacing restore_to_pylab by a tristate for the behavior on unpickling:

  • don't create a manager
  • create a manager, but don't register with pyplot
  • create a manager, and register to pyplot and show the figure

but this can clearly be done separately from (after?) this PR.

@ImportanceOfBeingErnest
Copy link
Member

Yes pickling is again a bad example, I suppose. Let's just take

plt.show(figures=[plt.Figure()])

which will also not have a manager. If that is expected, maybe a better error message should tell people exactly which figures they can show this way.

@anntzer
Copy link
Contributor Author

anntzer commented Sep 19, 2019

That should just be a matter of calling new_figure_manager_given_figure(fig), which can be done automatically within show() for managerless figures. Well, that adopts the figure into Gcf so we need to take it out of Gcf (so that again it can get properly gc'd), but that's the idea. (But note that the original intent was for people to use plt.new_figure_not_globally_registered("title") (which would create a manager) instead of the Figure constructor directly -- but I guess we may as well avoid adding a new API).

"""
Show all figures.

`show` blocks by calling `mainloop` if *block* is ``True``, or if it
is ``None`` and we are neither in IPython's ``%pylab`` mode, nor in
`interactive` mode.
"""
managers = Gcf.get_all_fig_managers()
managers = ([figure.canvas.manager for figure in figures]
Copy link
Member

Choose a reason for hiding this comment

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

This should be more forgiving to canvases without managers?

Copy link
Contributor Author

@anntzer anntzer Nov 1, 2020

Choose a reason for hiding this comment

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

I don't think ignoring (silently or not) figures we can't show because they have no manager is really helpful?

One idea would be to auto-setup a manager for them, though.

@tacaswell
Copy link
Member

I am 👍 on this.

@timhoffm
Copy link
Member

timhoffm commented Nov 1, 2020

Semi OT: While I see the reason behind this approach, plt.show([fig]) is still a crutch, and fig.show() would be much more intuitive from a user perspective.

@anntzer
Copy link
Contributor Author

anntzer commented Nov 1, 2020

The problem of fig.show() is that (per the above) it's not really clear whether we want that to default to blocking or not.

Right now, if one does not want to use pyplot (e.g., batch use), one can
do

    from matplotlib.figure import Figure
    fig = Figure(); ... <plot> ...; fig.savefig(...)

but it is impossible to show() the figure interactively (i.e. pyplot
cannot "adopt" the figure) e.g. for debugging.

This patch makes it possible: with it, one can do

    fig = plt.new_figure_manager(num).canvas.figure
    ... <plot> ...
    fig.savefig()
    plt.show(figures=[fig])

Note that this does *not* register the figure with pyplot; in particular
*fig* is garbage-collected when it goes out of scope, does not
participate in gcf(); etc.  So this is "pyplotless" in the sense that
there is no global figure registry anymore, but pyplot still stays in
charge of the GUI integration.

Obviously the `plt.new_figure_manager(num).canvas.figure` expression
could / should be encapsulated in a helper function, up to bikeshedding.

The `num` parameter is needed as matplotlib currently uses it to set the
figure title, but that can easily be made optional too.

Finally note that `plt.show([fig])` would be a nicer API, but right now
the first parameter to plt.show is `block`, which is undergoing
transition to kwonly.

IOW the end-goal would be e.g.

    # intentionally terrible name as placeholder :)
    fig = plt.new_figure_not_globally_registered("some title")
    ... <plot> ...
    fig.savefig()
    plt.show([fig])
@timhoffm
Copy link
Member

timhoffm commented Nov 1, 2020

I know it's not that simple :sad:.

@QuLogic QuLogic modified the milestones: v3.4.0, v3.5.0 Jan 21, 2021
@jklymak jklymak marked this pull request as draft April 23, 2021 16:33
@QuLogic QuLogic modified the milestones: v3.5.0, v3.6.0 Aug 23, 2021
@timhoffm timhoffm modified the milestones: v3.6.0, unassigned Apr 30, 2022
@story645 story645 modified the milestones: unassigned, needs sorting Oct 6, 2022
@github-actions
Copy link

Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it.

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Jun 14, 2023
@jklymak
Copy link
Member

jklymak commented Jun 14, 2023

Mplgui basically solves this issue. Now we just need a path to merging it!

@github-actions github-actions bot removed the status: inactive Marked by the “Stale” Github Action label Jun 16, 2023
@github-actions
Copy link

Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it.

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Aug 16, 2023
@anntzer
Copy link
Contributor Author

anntzer commented Aug 16, 2023

This is basically tracked by the mplgui work.

@anntzer anntzer closed this Aug 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: inactive Marked by the “Stale” Github Action status: needs comment/discussion needs consensus on next step topic: pyplot API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants