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

Skip to content

Native support for showing OOP-created figures #19956

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

Open
edmundsj opened this issue Apr 14, 2021 · 12 comments
Open

Native support for showing OOP-created figures #19956

edmundsj opened this issue Apr 14, 2021 · 12 comments

Comments

@edmundsj
Copy link

edmundsj commented Apr 14, 2021

Problem

Often, I will create figures using matplotlib using the Figure class explicitly, because either I want to make a figure and repeatedly tweak it to see how it looks, or I am creating a lot of figures and don't need / want to deal with the pyplot interface. However, as far as I can tell, there is no native matplotlib way to show those figures since they weren't created with the pyplot interface.

An example of what I would like to do:

fig =. Figure()
ax = fig.subplots()
ax.plot([1, 2, 3, 4])
fig.show() # This gives an error (below)

Error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/jordan.e/opt/miniconda3/lib/python3.7/site-packages/matplotlib/figure.py", line 408, in show
    "Figure.show works only for figures managed by pyplot, "
AttributeError: Figure.show works only for figures managed by pyplot, normally created by pyplot.figure()

The reason I don't want to use plt.subplots() followed by plt.show(), is, as mentioned above, because I often want to close and re-show the same figure. It seems really odd to me that matplotlib has a way of creating figures via a pure-OOP interface, but no easy way to display them. This is very frustrating and basically leads me to either repeatedly re-create the same plot with the pyplot interface instead of just tweaking the one I already have. I wrote a very hacky method for showing created figures based on a StackOverflow answer to this question which is below, and does not work reliably (the figure doubles in size every time I show it):

def show_figure(fig):
    """
    create a dummy figure and use its manager to display "fig"
    """

    dummy = plt.figure()
    new_manager = dummy.canvas.manager
    new_manager.canvas.figure = fig
    fig.set_canvas(new_manager.canvas)
    plt.show()

Proposed Solution

Either support fig.show() directly for figures created using the OOP-interface, or provide a method like pyplot.show_figure() which takes as an argument an existing figure and prints it to the screen (doing the exact same thing that a plt.plot() followed by plt.show() would do)

@anntzer
Copy link
Contributor

anntzer commented Apr 14, 2021

See #14024, perhaps.
As a side point, note that while I wholeheartedly agree with the idea of miniziming interaction with the pyplot global state, once you require a notion of showing a figure, there's necessarily going to be an GUI event loop running underneath, and that's necessarily a singleton (due to how GUI toolkits are written), which on Matplotlib's side is managed in pyplot (unless you want to create a QApplication/wx.App/etc. yourself).

@edmundsj
Copy link
Author

OK, then I'd be totally fine with something like pyplot.show_figure(fig)

@tacaswell
Copy link
Member

I have some worries about making

from matplotlib.figure import Figure
import matplotlib.pyplot as plt
fig = Figure()
plt.show(fig)

work. The first is that if you create an empty figure it has an Agg canvas (not the canvas that would be picked by pyplot via the backend selection), so plt.show(fig) would have to "promote" the canvas to be the correct GUI-flavored backend. A related issue is that as part of plt.figure we create the Manager which wraps the Canvas widget is a full GUI main window + adds a toolbar and a bunch of key bindings. Presumably we would also want to do that wrapping an promotion? However, if we did that once, what should we do on subsequent calls? What should we do if the user does plt.show(plt.figure())? Using this method you can get a figure with a canvas that is from a different GUI toolkit than pyplot is set up to us, what should we do then?

If you want to do the "promation" your self you can do

m = plt.new_figure_manager_given_figure(0, fig)

and then you will be able to fig.show() and if you are in plt.ion() mode then you can pan/zoom. However, because you will either not have registered it with pyplot and/or not set up the stale callbacks you will not get auto-redraws (and will have to call fig.canvas.draw_idle() when ever you change the figure).

I'm more inclined to go with something @anntzer suggested a while ago which is to add a pyplot.adopt(fig) function or that does all of the wrapping / registering / book keeping to let pyplot know about your figure and then takes responsibility for it.

At the end of the day, once you show a figure on screen you have converted your innocent Python script into a full-blown GUI application! The pyplot machinery does a remarkably good job at hiding this from most users on one hand and on the other we give users the tools to embed Matplotlib in a full-blown GUI of their design. I am not fully sold that there is much space in the middle where we can continue to hide the details of there being a GUI application from the user and let the users out from the oversight of pyplot.

@jklymak
Copy link
Member

jklymak commented Apr 15, 2021

Thanks @tacaswell. We should pin this answer.

@anntzer
Copy link
Contributor

anntzer commented Apr 15, 2021

The first is that if you create an empty figure it has an Agg canvas (not the canvas that would be picked by pyplot via the backend selection)

This is not correct, Figure().canvas is initially of type FigureCanvasBase.

so plt.show(fig) would have to "promote" the canvas to be the correct GUI-flavored backend. A related issue is that as part of plt.figure we create the Manager which wraps the Canvas widget is a full GUI main window + adds a toolbar and a bunch of key bindings. Presumably we would also want to do that wrapping an promotion? However, if we did that once, what should we do on subsequent calls?

It would seem reasonable to promote FigureCanvasBase to the canvas type specified by rcParams["backend"]; if there's already a concrete canvas subclass installed we can check its required_interactive_framework and accept it as is if it is compatible and throw if it isn't. What to do fig.canvas.required_interactive_framework is None (i.e. a non-interactive canvas (but not FigureCanvasBase) is installed) is for us to decide, I guess throwing is cleaner but just swapping it for rcParams["backend"] seems ok too.

Also, while users can do whatever they want (...), I think people passing Qt canvases to a Gtk event loop is quite rare... and again we can just explode in that case.

What should we do if the user does plt.show(plt.figure())?

Ditto.

Using this method you can get a figure with a canvas that is from a different GUI toolkit than pyplot is set up to us, what should we do then?

Again, we can check based on required_interactive_framework.

add a pyplot.adopt(fig) function or that does all of the wrapping / registering / book keeping to let pyplot know about your figure and then takes responsibility for it.

That's fine too, but note that here again you have to decide what to do if a noncompatible (or noninteractive) canvas is already installed.

@tacaswell
Copy link
Member

This is not correct, Figure().canvas is initially of type FigureCanvasBase.

This is like the third time I have gotten that wrong 😞 🐑 . I'll remember it correctly one of these days...

That's fine too, but note that here again you have to decide what to do if a noncompatible (or noninteractive) canvas is already installed.

fair, but feels less odd to me to do that promotion / error checking / ... as a one-off "please take ownership of this figure" rather than "please show and maybe adopt this figure".

There is also the issue that if you are mixing plt.figure and Figure. When you do plt.show(fig) then do we expect any existing pyplot-managed figures to be shown? If you make two figures via Figure, do we need to support plt.show([fig1, fig2]) or plt.show(fig1, fig2, ...)?

We also recently made block=True keyword only because we did have users who were doing plt.show(fig) which because figures are True-ish bool(fig) == True it was blocking (but led to weird behavior). I'm not super stoked about bringing that back.


@edmundsj I want to stress that despite my verbose and relatively skeptical/negative responses this is a very reasonable request.

@anntzer
Copy link
Contributor

anntzer commented Apr 15, 2021

Actually I made block kwonly specifically to later support plt.show([fig1, ...]). We clearly need to be able to take a list of figures (I'd prefer that over varargs figures); whether we want to support a single figure (rather than a list of 1 figure) is not particularly important (I guess it'd be nice if you have a bunch of plt.show(fig, block=False) calls spread through many places and a final plt.show([], block=True) to prevent immediate exiting).
I'd say that if you explicitly pass figures to be shown, then pyplot figures don't get shown, but again that's something that just needs a decision.

@jklymak
Copy link
Member

jklymak commented Apr 15, 2021

Maybe we should discuss this on the call. We seem to have a tension between keep-it-simple and adding more flexibility. My naive take is that pyplot is our very simple GUI manager, and if a user wants something fancier they should spin up their own simple GUI manager.

@tacaswell
Copy link
Member

I am slowly convincing myself that @anntzer is right, but still have doubts.

@nschloe
Copy link
Contributor

nschloe commented Jul 2, 2021

For the sake of comparison, pyvista does it like this:

import pyvista as pv

p = pv.Plotter()
cyl = pv.Cylinder()
p.add_mesh(cyl)
p.show()

As a library author, this is very convenient for me: I create a Plotter object that I return, and give the user full flexibility what she wants to do with it. p.show() is easy enough:

def fun_from_my_library():
    import pyvista as pv
    p = pv.Plotter()
    # generate a fancy plot
    return p

fun_from_my_library().show()
# or
fun_from_my_library().save("out.png")
# or ...

The same works with mpl figures for fig.savefig(), not for show().

I gather from the above discussion that some might rather want to see
plt.show(fig) instead of fig.show(), but perhaps that's something to consider.

Copy link

This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Dec 11, 2023
@tacaswell
Copy link
Member

This turned into https://github.com/matplotlib/mpl-gui

@github-actions github-actions bot removed the status: inactive Marked by the “Stale” Github Action label Dec 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants