I am using python's matplotlib to draw figures.
I want to draw a figure with a timeout, say 3 seconds, and the window will close to move on the code.
I have known that pyplot.show() will create a blocking window with unlimited timeout; pyplot.show(block=False) or pyplot.draw() will make the window non-blocking. But what I want is let the code block for some seconds.
I have come up an idea that I might use an event handler or something, but still not really clear how to solve this. Is there any simple and elegant solution?
Suppose my code is like the following:
Draw.py:
import matplotlib.pyplot as plt
#Draw something
plt.show() #Block or not?
Here's a simple example where I have created a timer to set your timeout and performed closing of window plot.close() in the callback function of the timer. Start the timer before plot.show() and after three seconds timer invokes close_event() and then continues with the rest of the code.
import matplotlib.pyplot as plt
def close_event():
plt.close() #timer calls this function after 3 seconds and closes the window
fig = plt.figure()
timer = fig.canvas.new_timer(interval = 3000) #creating a timer object and setting an interval of 3000 milliseconds
timer.add_callback(close_event)
plt.plot([1,2,3,4])
plt.ylabel('some numbers')
timer.start()
plt.show()
print("I am doing something else")
Hope that was helpful.
This did not work for me on Mac OSX.
Looks like there are two issues:
Calling plt.close() is not enough to exit the
program sys.exit() works.
The scheduled function seems to be called at startup.
This results in a display time of zero. The window
pops and disappears at the same time. Using state in
the callback that treats the first call differently
solves this problem. An instance of a class with the
special method __call__() is a good approach for such
a stateful callable.
This works for me:
from __future__ import print_function
import sys
import matplotlib.pyplot as plt
class CloseEvent(object):
def __init__(self):
self.first = True
def __call__(self):
if self.first:
self.first = False
return
sys.exit(0)
fig = plt.figure()
timer = fig.canvas.new_timer(interval=3000)
timer.add_callback(CloseEvent())
plt.plot([1,2,3,4])
plt.ylabel('some numbers')
timer.start()
plt.show()
print("Am doing something else")
Related
I have a class object with an attribute display(self):
import matplotlib.pyplot as plt
class Obj:
def display(self) -> None:
fig = plt.figure()
sub = fig.add_subplot()
sub.plot(...)
plt.show()
def dostuff(self) -> 'stuff':
...
self.display()
...
self.display()
...
self.display()
return
I use this function to have a better visual reference of how my dostuff(self) attribute is handling its task. It all works as intended, when the self.display() command is registered the scrypt pauses execution and plots stuff. However, to resume the only way is closing the matplotlib window manually and then the program reopens another one with the next changes.
Is there a way to implement a button or a better way to view the next changes without having to close and reopen a new window every single time?
plt.show has parameter block. If you set it to False, then the execution is not blocked. Hope this helps.
My code is as below.
....(omission)...
sympy.plot(func, (x,-2,20))
Then the plot window successfully pops up but it doesn't close(doesn't terminate).
Is there a function similar to plt.close() in sympy plot methods?
Thank you.
Sadly, it's not fully implemented. Assuming that you are running your code from a Python or IPython console, to achieve your objective we need to modify a few methods.
from sympy import *
var("x")
from sympy.plotting.plot import Plot, MatplotlibBackend
def show(self):
self.process_series()
self.fig.tight_layout()
self.plt.show(block=False)
MatplotlibBackend.show = show
def close(self):
self._backend.close()
Plot.close = close
p = plot(sin(x))
# at this point the matplotlib window is "detached" from the console.
# you can execute other commands. Finally, when you are ready to close
# the figure, run this:
p.close()
The following is a simplified example of my code. The idea behind this class is to show the figure only when the show method is executed.
# my_module.py
import matplotlib.pyplot as plt
import numpy as np
class Test:
def __init__(self):
self._fig = plt.figure()
self.ax = self._fig.add_subplot(1, 1, 1)
def show(self):
x = np.linspace(0, 10, 100)
y = np.sin(x)
self.ax.plot(x, y)
self._fig.tight_layout()
self._fig.show()
The code works as expected when it is executed from a Python shell or ipython. However, if I run this inside a Jypter Notebook:
from my_module import Test
t = Test()
At this point, an empty figure is visualized on the screen. I don't want that! Now, I tried to insert plt.close(self._fig) inside __init__, but then when I run t.show() I get UserWarning: Matplotlib is currently using module://matplotlib_inline.backend_inline, which is a non-GUI backend, so cannot show the figure.
I also tried to load %matplotlib widget with the previous edit plt.close(self._fig). The pictures is only shown when show is called, but it is just a picture with no interactive frame.
Another option would be to rewrite the class in such a way that the figure is created inside the show method. This is far from optimal as I would need to re-adjust my tests.
Are there any other ways to get it working correctly on all shells?
In the original post I've done two mistakes.
First, the figure was instantiated into the __init__ method, then the show method was called. In an interactive environment, once the figure is created it will be shown on the screen. We could turn off that behaviour with plt.ioff(), but then two things can happen:
If %matplotlib widget was executed, the figure will show up only once when calling t.show().
Otherwise, no plot will be shown on the screen when calling t.show().
Hence, plt.ioff() is not a valid solution. Instead, the figure must be instantiated when t.show() is executed.
The second mistake I did was to use self._fig.show(). Remember, in a interactive environment the figure is shown as soon as it is instantiated. Then, the previous command shows the figure a second time! Instead, I have to use plt.show(), which only display the figure once.
Here is the correct code example:
import matplotlib.pyplot as plt
import numpy as np
class Test:
def __init__(self):
# init some attributes
pass
def show(self):
self._fig = plt.figure()
self.ax = self._fig.add_subplot(1, 1, 1)
x = np.linspace(0, 10, 100)
y = np.sin(x)
self.ax.plot(x, y)
self._fig.tight_layout()
plt.show()
t = Test() # no figure is shown
t.show() # figure is shown
I tried to implement the workaround mentioned here to the problem of the figure not updating properly when a draw_event is triggered once a user zooms into a plot. This was meant to improve this answer. In the solution, a Timer is added to the Canvas of the Figure, which delays the draw function for a short while.
In the linked answer this is done for a single figure and a reference to the timer is stored as self.timer to the class that contains the update function. I would like to have an a bit more general behaviour and allow this for many Figure instances. Therefore I tried not to save the Timer reference (I don't need it anymore). But when I do so, the script crashes with a Segmentation Fault (no traceback). Here is my code:
from matplotlib import pyplot as plt
import numpy as np
##plt.switch_backend('TkAgg')
class DrawEventHandler:
def __init__(self):
x = np.linspace(0,1,10)
y = np.sin(x)
self.fig, self.ax = plt.subplots()
self.ax.plot(x,y)
self.ax.figure.canvas.mpl_connect('draw_event', self.update)
def update(self, event = None):
self._redraw_later_ok()
##self._redraw_later_not_ok(self.fig)
def _redraw_later_ok(self):
self.timer = self.fig.canvas.new_timer(interval=10)
self.timer.single_shot = True
self.timer.add_callback(lambda : self.fig.canvas.draw_idle())
self.timer.start()
def _redraw_later_not_ok(self, fig):
print('start')
timer = fig.canvas.new_timer(interval=10)
timer.single_shot = True
timer.add_callback(lambda : fig.canvas.draw_idle())
timer.start()
print('stop')
if __name__ == '__main__':
my_handler = DrawEventHandler()
plt.show()
The original solution is implemented as _redraw_later_ok(), which works fine for me. The problematic solution is called `_redraw_later_not_ok()', which produces this output:
start
stop
Segmentation fault: 11
I use a Mac with High Sierra, Python 3.6.4 or Python 2.7.14 and Matplotlib 2.1.1 with the MacOSX backend. When I switch to the TkAgg backend (which is terribly slow), the code works fine. Can anybody explain what is going on?
As usual with interactive features which use the event loop of a GUI you need to keep a reference to the object that handles the callback.
The problem with the approach of not storing the timer in this code
def _redraw_later_not_ok(self, fig):
print('start')
timer = fig.canvas.new_timer(interval=10)
timer.single_shot = True
timer.add_callback(lambda : fig.canvas.draw_idle())
timer.start()
print('stop')
is that timer gets garbage collected once the _redraw_later_not_ok function terminates (i.e. directly after stop is printed). At this point there would be a pointer to a place in memory, which may or may not store the callback (any more). An attempt by the python interpreter to call the callback would hence (most probably) fail.
The solution is indeed to always keep a reference to the object that steers the callback. This is the solution shown in _redraw_later_ok, where the timer is made a class attribute, self.timer. Such that it can later be used at the time the callback is called.
I do not understand in how far using this working approach would prevent the use of several figures, so it may make sense to create a MWE with two figures that shows the problem clearly.
I am new to Python, and somewhat new to object oriented programming. Can anyone explain what is going on and how things are typically done with a matplotlib GUI callback? I've taken the "event_handling example code" from the Matplotlib website and stripped it down for clarity. When you run this code it makes a plot, and if you press a key on the keyboard the press function is called. The press function is passed only event, but somehow every other variable from main program level appears inside the call to press but as a global variable, is this normal for functions? I can print the value of x, but if I try to change it then it makes a local variable version, worse yet now I have seemingly no way to access the global version anymore?
#!/usr/bin/env python
import numpy as np
import matplotlib.pyplot as plt
x=np.random.rand(3)
y=np.random.rand(3)
def press(event):
print(x)
print('Local Var:', locals().keys())
print('Global Var:', globals().keys())
fig, ax = plt.subplots()
fig.canvas.mpl_connect('key_press_event', press)
ax.plot(x,y)
plt.show()
I have searched and had quite a hard time finding any reference that explains how to access or properly pass useful data in and out of the callback function so that a GUI event can do something useful, like update some data or feature of a plot?
So lets say I wanted to have the callback function modify y and re-plot the data. How is that typically done?
you have global access to x inside your callback, but can't modify it unless you specify it global.
def press(event):
global x
...
locals().keys() and globals().keys() are printing namespaces; I am unsure why you need to do that.
Your callback receives an event that you can use and manipulate inside the function.
Here is an example:
#!/usr/bin/env python
import numpy as np
import matplotlib.pyplot as plt
x=np.random.rand(3)
y=np.random.rand(3)
def press(event):
print(event, event.key)
fig, ax = plt.subplots()
fig.canvas.mpl_connect('key_press_event', press)
ax.plot(x,y)
plt.show()
click on the plot window to set the focus.
pressing f should print the event object and set the plot full screen
pressing f again, will print f and restore the size of the window
pressing s will print s and will offer to save your figure
etc...
To learn more about how you can manipulate events, look up backend_bases in the very rich matplotlib web site. For example, you can set mouse_clicks events that allow you to capture canvas coordinates to add points or modify figures...