0

I don't understand how figures created with matplotlib are shown, when they are updated, when they are blocking, etc.

To help my understanding and to help me make matplotlib do what I need specifically, could anyone help me create a matplotlib.figure wrapper/almost-clone, let's call it MyFigure, that behaves exactly as described in the code (and its comments) below?

X=[0,1]
Y=[0,1]
Y2a=[0,2]
Y2b=[0,2.5]
Y3a=[0,3]
Y3b=[0,3.5]
Y4=[0,4]

fig = MyFigure() # Nothing happens
ax=fig.gca() # Nothing happens
ax.plot(X,Y) # Nothing happens
fig.show() # New window (1) appears and shows plot of X,Y; execution continues directly
ax.set_xlabel('X') # Window 1 gets X axis label
ax.set_ylabel('Y') # Window 1 gets Y axis label
fig.hide() # Window 1 disappears
time.sleep(10) # No open windows for 10 seconds 
fig.show() # Window 1 reappears and looks the same as before

fig2 = MyFigure() # Nothing happens
fig2.show() # New window (2) appears and is empty
ax2 = fig2.gca() # Window 2 shows axes
ax2.plot(X,Y2a) # Window 2 shows X,Y2a
fig2.show() # Nothing happens, could be omitted
time.sleep(60) # Long computation. In the meantime, windows 1 and 2 can still be resized and their controls (zoom etc.) can be used
ax2.plot(X,Y2b) # Window 2 shows X,Y2a as well as X,Y2b
fig2.hide() # Window 2 disappears

fig3 = MyFigure() # Nothing happens
ax3 = fig3.gca() # Nothing happens
ax3.plot(X,Y3a) # Nothing happens 
fig3.show() # Window 3 appears and shows plot of X,Y3a; execution continues
fig3.freeze() # Nothing happens, Window 3 is still open and can be manipulated
ax3.set_xlabel('X') # Nothing happens
ax3.set_ylabel('Y') # Nothing happens
fig3.thaw() # Window 3 gets X and Y axis labels 
fig3.clear() # Window 3 is still open but empty
ax3 = fig3.gca() # Window 3 shows empty axes
ax3.plot(X,Y3b) # Window 3 shows plot of X,Y3b

fig4 = MyFigure() # Nothing happens 
ax4 = fig4.gca() # Nothing happens 
ax4.plot(X,Y4) # Nothing happens
fig4.show(block=True) # Window 4 opens and shows X,Y4. Execution pauses 
print('Window 4 was closed by user') # Happens after user closes window 4
fig4.show() # Window 4 reappears and looks as before
fig4.close() # Window 4 disappears and its resources are freed
try:
   fig4.show()
except Exception:
   print('Something went wrong') # Prints 'Something went wrong'

# Windows 1,2,3 still "exist". 
# Window 2 is not visible but could be manipulated and shown again in later code.
# Windows 1 and 3 are still visible and can be manipulated by the user. 
# They disappear as soon as the objects fig and fig3 are garbage collected, 
# or their `hide` or `close` methods are called.

Note that the call to MyFigure().show() can do anything you need it to in order to get things work as I describe, I just used the word show because it's similar to what plt.show already does. I have heard of at least three ways that could help with some of what I want regarding interactivity (plt.ion(), fig.show(block=False), plt.draw(); three and a half if you count ipython's magic commands, which I don't intend to use), yet none of these really work exactly as I'd hope and I don't really understand the logical model behind any of them. See the bottom of this question for my failed attempts.

I do not want to discuss or criticize the way matplotlib figures and their methods work by default. I'm sure there are good reasons, technical and functional, for the way they work, but its simply not compatible with my intuition (fed from no real GUI experience elsewhere) and I get frustrated with it anytime I have to use it before I get to learning anything beyond simple plot commands. I hope to change that.

Basically, I imagine MyFigure instances as representing threads with two important boolean instance variables: visible and frozen. The first one determines whether an open window is associated to the instance and can be set by show() and hide(); the second one controls whether changes (such as new plots) are added to the drawing area immediately or to a queue, and can be set by freeze() and thaw().

The call show(block=False) would only returns once the corresponding window is closed by the user (or by another thread with access to the MyFigure instance). It can be called no matter the current value of visible and will result in visible=False. The call thaw() would set frozen=False and applies the change queue to the figure (this could be applied even when visible=False, though it wouldn't make sense to freeze and unfreeze the figure in that state, unless the figure is manipulated in another thread in the meanwhile).

Most important to me is the behavior of show() and interactive behavior of the instances; I mostly described the freeze behavior in anticipation of questions how that use case would be handled in my setup. I don't like the ax=fig.gca() behavior and would feel better with something like fig.add(matplotlib.Axes()) but that's not relevant to this question (besides, I'm confident I can figure this out myself).


To clarify my struggles, let me record my failed attempts so far:

plt.ion()

I prepend my code with

def MyFigure(): 
    plt.ion()
    return plt.figure()

I comment out block one and continue at fig2 = .... When the execution hits time.sleep(60), there is no open window for 60 seconds.

fig.show(block=False)

I remember this working on the laptop where I first wrote this post, but the block keyword seems to have been removed in the newer version of matplotlib that I am using on my desktop computer

plt.draw()

This affects all figures equally. Trying to call draw as an instance method of fig results in TypeError: draw_wrapper() missing 1 required positional argument: 'renderer'.

Ignoring this problem for the moment, plt.draw() in itself doesn't do anything. Based on some internet research, I found out that a subsequent plt.pause(0.01) does what I want. This is actually the solution that comes closest to what I want. Block two works exactly as desired (ignoring the final fig.hide()) if I replace all fig.show() by plt.draw();plt.pause(0.01), but it seems beyond ugly and also does not seem what the internet recommends.


(To potential close voters: I could formulate a number of specific questions for each of the lines in the code. However, I feel like all the desired behaviour is (a) related enough to be presented together (b) best described in the form of code that is wish to write together with a comment of what I'd wish it didn't.)

Bananach
  • 2,016
  • 26
  • 51
  • After reading this twice, I'm still not sure what's being asked for. Seems at least one other person sees this similarly. The bits that I seem to understand would all direct towards using `plt.ion()`. So maybe you want to rather tell what's wrong with that? Also one thing to note is that a GUI needs to run in the main thread. – ImportanceOfBeingErnest Dec 09 '18 at 19:48
  • @ImportanceOfBeingErnest For example, to verify whether `plt.ion` does what I want, I just defined `def MyFigure(): plt.ion(); return plt.figure()`. Unfortunately, if I then run the first two blocks of my code, I don't get the functionality I described in the comments to my code. I tried fixing this by adding `plt.draw()` calls but failed. Can you get the functionality described in the comments somehow? I'm not saying it's difficult. And https://matplotlib.org/api/_as_gen/matplotlib.pyplot.ion.html certainly doesn't help – Bananach Dec 09 '18 at 20:31
  • @ImportanceOfBeingErnest I don't understand the 'a GUI needs to run in the main thread' part of your comment. I am *thinking* of `MyFigure` instances as behaving as if they represented threads. How the behavior is implemented, I don't care too much. But I'm sure it is somehow possible to achieve all of what I wanted somehow and am happy with any solution, whether it involves, threads, subprocesses, ..., or nothing in that direction at all – Bananach Dec 09 '18 at 20:35
  • Your question is tagged "multithreading", hence my comment on that. And in a way it seems you do want to run the GUI in a different thread than the code, to which `plt.ion()` is kind of a workaround, because it opens a GUI window, which is only partially functional by emulating an event loop in it. By `fig.hide` you mean something like [this](https://stackoverflow.com/questions/12439588/how-to-maximize-a-plt-show-window-using-python)? What else does not work? – ImportanceOfBeingErnest Dec 09 '18 at 21:19
  • @ImportanceOfBeingErnest Okay, so I use `def MyFigure(): plt.ion; return plt.figure()` Then I run my code exactly as above but with `fig.hide()` commented out for the moment. Already during the first `time.sleep(10)`, there is no window open, contrary to the behavior I'd desire. What am I doing wrong? – Bananach Dec 10 '18 at 06:45
  • @ImportanceOfBeingErnest I see, I forgot about that tag. Nonetheless, my focus is really on results, not the underlying technology (I have never actively used threads anyway). I'm confused about the link in your last comment. It seems to discuss maximization of windows only – Bananach Dec 10 '18 at 06:48
  • @ImportanceOfBeingErnest I updated my question with my failed attempts so far – Bananach Dec 10 '18 at 07:12

2 Answers2

4

To be honest I feel the frustration with matplotlib tutorials around the web and how it is used in practice. Personally a lot of matplotlib frustrations vanished when I started to use it in an Object Orientated approach. Instead of doing:

from matplotlib.pyplot import *
fig = figure() # implicitly create axis
ax  = gca() 

Do:

fig, ax = subplots() # explicitly create figure and axis

Similar to draw this changes from

draw()
show()
fig.canvas.draw()
fig.show()

In this way you are explicitly holding a particular figure instead of relying on the figure that is currently in focus by matplotlib. The ion command is kinda a mystery to me as my backends were always interactive (qt5/qt4) and hence updating figure properties always changed it for me anyway.

cvanelteren
  • 1,633
  • 9
  • 16
  • I'm happy someone understands my struggles. They're difficult to formulate sometimes. What's the difference between `fig=figure();ax=gca()` and `fig,ax=subplots()` though? And can you tell me what the conceptual difference between `draw` and `show` commands is? Also, could you maybe sketch how you would solve some of the problems laid out in my question? – Bananach Dec 10 '18 at 13:20
  • There is no physical difference in the sense that they achieve the same. However, they offer more sanity as you are explicitly creating the axis object. I believe that matplotlib automatically creates an axis object for every figure. However, the main difference here is that I am not relying on the focus of matplotlib when referring to the figures. What I mean with this is that gca() looks for the latest axis that is in focus. Additionally the subplots function has some nice properties that you can specify a lot of subplots with in one line, reducing code bloat. – cvanelteren Dec 10 '18 at 13:24
  • To clarify on the last point. The focus may become and issue if you have multiple figures open and writing data to it. gca will default to the last one that is created (or is in focus). Hence I prefer to have it explicitly created to be able to edit the axis. – cvanelteren Dec 10 '18 at 13:28
2

Maybe the following is what you're looking for. Since the question is really broad, asking for a lot of things at the same time, I will restrict this to the first two examples. The key here is the use of plt.ion().

import matplotlib
matplotlib.use("Qt5Agg")
import matplotlib.pyplot as plt


X=[0,1]
Y=[0,1]
Y2a=[0,2]
Y2b=[0,2.5]
Y3a=[0,3]
Y3b=[0,3.5]
Y4=[0,4]

plt.ion()

fig, ax  = plt.subplots() # Nothing happens
ax.plot(X,Y) # Nothing happens
plt.draw()
plt.pause(.1) # New window (1) appears and shows plot of X,Y; execution continues directly
ax.set_xlabel('X') # Window 1 gets X axis label
ax.set_ylabel('Y') # Window 1 gets Y axis label
figManager = plt.get_current_fig_manager()
figManager.window.showMinimized() # Window 1 disappears
plt.pause(10) # No open windows for 10 seconds 
figManager.window.showNormal() # Window 1 reappears and looks the same as before


fig2 = plt.figure() # Nothing happens
fig2.show() # New window (2) appears and is empty
ax2 = fig2.gca() # Window 2 shows axes
ax2.plot(X,Y2a) # Window 2 shows X,Y2a
pass # Nothing happens, could be omitted
for i in range(60*2):
    plt.pause(1/2) # Long computation. In the meantime, windows 1 and 2 can still be resized and their controls (zoom etc.) can be used
ax2.plot(X,Y2b) # Window 2 shows X,Y2a as well as X,Y2b
figManager = plt.get_current_fig_manager()
figManager.window.showMinimized() # Window 2 disappears

  • (1) What does plt.ion do here?
    plt.ion() turns interactive mode on. This means that a figure can be drawn inside a GUI without starting the GUI event loop. The advantage is obviously that the GUI event loop does not block subsequent code; the disadvantage is that you do not have an event loop - hence the GUI may quickly become unresponsive and you are responsible yourself for letting it manage events. This is what plt.pause does in this case. It may happen that whatever you use for running the script already has interactive mode turned on for you. In that case use plt.ioff() to see the difference.

  • (2) Can something like plt.pause be achieved without grabbing focus?
    Probably, but most users would like to actually see their plot when drawing it. So it's not implemented in that way.

  • (3) If plt.draw doesn't have any effect without plt.pause(.1), why does it exist in its current form?
    plt.draw() draws the figure. This is useful in all kinds of cases and completely independent on whether ion is used.

  • (4) Is it correct that fig.show is conceptually the same as (a) open window if it doesn't exist (b) put focus on window (c) plt.draw();plt.pause(0.1)?
    I strongly doubt that. It may however be that it has the same user facing effect in interactive mode.

  • (5) Is it really impossible to have the figures responsive while doing actual computations (as opposed to plt.pause)?
    No. You can run the computation inside the event loop. You can also run it inside a thread. And finally you can call plt.pause() or fig.canvas.flush_events() repeatedly during your calculation.

  • (6) The figManager.window.showMinimized doesn't have any effects for me; is that because this is OS dependent?
    It shouldn't be OS dependent. But it's sure backend-dependent. That's why I set the backend on top of the code.

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Thanks, this helps a lot. (1) What does `plt.ion` do here? For me, I believe everything stays the same if I omit it. (2) Can something like `plt.pause` be achieved without grabbing focus? (3) If `plt.draw` doesn't have any effect without `plt.pause(.1)`, why does it exist in its current form? (4) Is it correct that `fig.show` is conceptually the same as (a) open window if it doesn't exist (b) put focus on window (c) `plt.draw();plt.pause(0.1)`? (5) Is it really impossible to have the figures responsive while doing actual computations (as opposed to `plt.pause`)? – Bananach Dec 12 '18 at 13:41
  • Also, the `figManager.window.showMinimized` doesn't have any effects for me; is that because this is OS dependent? – Bananach Dec 12 '18 at 13:42
  • I tried to answer those questions inside the answer. – ImportanceOfBeingErnest Dec 12 '18 at 19:14
  • Just a final round: (1) So it is expected/true that `plt.ion` does not have a noticeable effect in your code? (6) I ran your code exactly as provided – Bananach Dec 12 '18 at 20:07
  • (1) The default is `ioff`, hence I need to set it on for this to work. (6) Did you get any warning about the backend not being set to Qt? – ImportanceOfBeingErnest Dec 12 '18 at 20:16
  • I wasn't clear about (1), what I mean is that the code runs exactly the same for me whether I comment out plt.ion or not. (6) no warnings, running Python 3.6 (anaconda) on Ubuntu 18. But that's maybe for a new question – Bananach Dec 13 '18 at 06:22
  • Urgh, and more mysterious behavior: (7) it seems the plt.draw() is also not necessary, everything behaves the same without it; again this holds whether `plt.ion()` or `plt.ioff()`. (Btw, I put your code in a file and execute it, I don't use the interactive python shell) – Bananach Dec 13 '18 at 07:46
  • And no, I didn't get a warning about the backend. – Bananach Dec 13 '18 at 09:17