matplotlib animation embedded in tkinter : update function never called - matplotlib

I've been trying to embed a matplotlib animation into tkinter.
The goal of this app is to simulate some differentials equations with rk4 method and show a real time graph as the simulation goes.
In fact the plot is rightly embedded into the tkinter frame.
However, the animation never run, I've noticed that the update function is never called.
I've been searching everywhere but I didn't find anything.
Thanks for the help.
Here is a code sample of the GUI class showing where I execute the animation
# called when I click on a button "start simulation"
def plot_neutrons_flow(self):
# getting parameters from the graphical interface
if not self._started:
I0 = float(self._field_I0.get())
X0 = float(self._field_X0.get())
flow0 = float(self._field_flow0.get())
time_interval = float(self._field_time_interval.get())
stop = int(self._field_stop.get())
FLOW_CI = [I0, X0, flow0] # [I(T_0), X(T_0), PHI[T_0]]
self._simulation = NeutronsFlow(
edo=neutrons_flow_edo,
t0=0,
ci=FLOW_CI,
time_interval=time_interval,
stop=hour_to_seconds(stop)
)
# launch the animation
self._neutrons_flow_plot.animate(self._simulation)
self._started = True
Here is the code for the matplotlib animation :
import matplotlib
import tkinter as tk
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import style
matplotlib.use("TkAgg")
style.use('seaborn-whitegrid')
class PlotAnimation(FigureCanvasTkAgg):
def __init__(self, tk_root):
self._figure = Figure(dpi=100)
# bind plot to tkinter frame
super().__init__(self._figure, tk_root)
x_label = "Temps (h)"
y_label = "Flux / Abondance"
self._axes = self._figure.add_subplot(111, xlabel=x_label, ylabel=y_label, yscale="log")
self.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def update(self, interval):
# this is never called
# get data from rk4 simulation
time_set = self._simulation.get_time_set()
y_set = self._simulation.get_y_set()
self._axes.clear()
self._axes.plot(time_set, y_set, visible=True, linewidth=1)
self._axes.legend(fancybox=True)
# redraw canvas
self.draw_idle()
def animate(self, simulation):
# this is called
self._simulation = simulation
# simulate differential equations with rk4 method
self._simulation.resolve()
# https://github.com/matplotlib/matplotlib/issues/1656
anim = animation.FuncAnimation(
self._figure,
self.update,
interval=1000
)
EDIT :
The solution was to instantiate the FuncAnimation function directly in the init method

As indicated in the documentation of the animation module (emphasis mine)
(...) it is critical to keep a reference to the instance object. The
animation is advanced by a timer (typically from the host GUI
framework) which the Animation object holds the only reference to. If
you do not hold a reference to the Animation object, it (and hence the
timers), will be garbage collected which will stop the animation.
You need to return the anim object from your animate() function, and store it somewhere in your code so that it is not garbage-collected

Related

pyqt5 QLabel Image setScaledContents(True) don't allow Qpainter updates

I want to display an image and put a marker at the current mouse position for every left mouse click.
Below code does the job however, it works only if ("self.imglabel.setScaledContents(True)") is commented. Any reason?
I have to do this job on various images of different resolutions, I read to maintain the proper aspect ratio and display the image appropriately we need to use setScaledContents(True). But why enabling this is not allowing update() (PaintEvent)??
import sys
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel, QSizePolicy, QMessageBox
from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor, QImage, QPalette
class Menu(QMainWindow):
def __init__(self):
super().__init__()
self.central_widget = QWidget() # define central widget
self.setCentralWidget(self.central_widget)
self.vbox = QVBoxLayout(self.central_widget)
self.vbox.addWidget(self.imgWidget())
self.vbox.addWidget(QPushButton("test"))
def imgWidget(self):
self.imglabel = QLabel()
self.imglabel.setScaledContents(True)
self.image = QImage("calib.jpeg")
self.imagepix = QPixmap.fromImage(self.image)
self.imglabel.setPixmap(self.imagepix)
self.imglabel.mousePressEvent = self.imgMousePress
return self.imglabel
def imgMousePress(self, e):
painter = QPainter(self.imglabel.pixmap())
pen = QPen()
pen.setWidth(10)
pen.setColor(QColor('red'))
painter.setPen(pen)
painter.drawPoint(e.x(), e.y())
painter.end()
self.imglabel.update()
if __name__ == '__main__':
app = QApplication(sys.argv)
mainMenu = Menu()
mainMenu.show()
sys.exit(app.exec_())
To avoid unnecessary computation for each paintEvent of the QLabel, whenever the scaledContents property is True the scaled image is cached, and all the painting is automatically discarded.
To avoid that, you should create a new instance of QPixmap using the existing one, and then set the new painted pixmap again. Note that if the image is scaled, the widget coordinates won't reflect the actual position on the pixmap, so you need to use a transformation to get the actual point to paint at.
def imgMousePress(self, e):
pm = QPixmap(self.imglabel.pixmap())
painter = QPainter(pm)
pen = QPen()
pen.setWidth(10)
pen.setColor(QColor('red'))
painter.setPen(pen)
transform = QTransform().scale(
pm.width() / self.imglabel.width(),
pm.height() / self.imglabel.height())
painter.drawPoint(transform.map(e.pos()))
painter.end()
self.imglabel.setPixmap(pm)
Consider that all the "points" will become stretched rectangles if the width/height ratio is not the same of the source image, but this is only a problem of appearance: if you save the pixmap later, they will be squares again, since saving is based on the source pixmap.
If you want to keep their squared shape while displaying instead, you'll need to keep track of the points and overwrite paintEvent to paint them manually on the label.

Can I draw TKinter objects on top of an embedded FigureCanvasTkAgg?

shortly said:
I am creating a quick TKinter API and I firstly generate a tk.Canvas
I am embedding a FigureCanvasTkAgg canvas with master = tk.Canvas above
With this I am able to show an image via Matplotlib
Now I want to draw TKinter objects ON TOP of the FigureCanvasTkAgg canvas (e.g. rectangles or buttons)
Is this possible? Or is there any particular recommendation (i.e. using only one canvas or the other)?
Here some quick code:
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
class MyApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.canvas = tk.Canvas(self, width=500, height=500, cursor="cross")
self.canvas.pack(side="top", fill="both", expand=True)
def draw_image_and_button(self):
self.figure_obj = Figure()
a = self.figure_obj.add_axes([0, 0, 1, 1])
imgplot = a.imshow(some_preloaded_data_array, cmap='gray')
# create tkagg canvas
self.canvas_agg = FigureCanvasTkAgg(self.figure_obj, master=self.canvas)
self.canvas_agg.draw()
self.canvas_agg.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# attempt to draw rectangle
self.rectangle = self.canvas.create_rectangle(0, 0, 100, 100, fill='red')
if __name__ == "__main__":
app = MyApp()
app.draw_image()
app.mainloop()
I mean I see that the rectangle is being drawn before the image. Maybe its my lack of understanding on how FigureCanvasTkAgg is attached to tk.canvas
Thank you!
Ok, this is an app that I recently developed where I have matplotlib widgets and mouse events. You can also have tkinter widgets but I didn't find a way to put them on top of the matplolib canvas. Personally I like matplotlib widgets more than tkinter widgets, so I think it is not too bad.
The only pre-step that you have to take is to modify matplotlib source code because you need pass the canvas to the widget class, while by default the widget takes the figure canvas which will not work when embedding in tk (button would be unresponsive). The modification is actually quite simple, but let's go in order.
Open 'widgets.py' in the matplotlib folder (depending on where you installed it, in my case I have it in "C:\Program Files\Python37\Lib\site-packages\matplotlib").
Go to the class AxesWidget(Widget) (around line 90) and modify the __init__ method with the following code:
def __init__(self, ax, canvas=None):
self.ax = ax
if canvas is None:
self.canvas = ax.figure.canvas
else:
self.canvas = canvas
self.cids = []
As you can see compared to the original code I added a keyword argument canvas=None. In this way the original functionality is mantained, but you can now pass the canvas to the widget.
To have a responsive button on the matplolib canvas that is embedded in tk you now create a widget and you pass the matplolib canvas created with FigureCanvasTkAgg. For example for a Buttonyou would write
from matplotlib.widgets import Button
ok_button = Button(ax_ok_button, 'Ok', canvas=canvas) # canvas created with FigureCanvasTkAgg
Ok now we have all the functionalities required to have matplolib widgets on the matplolib canvas embedded in tk, plus you can also have mouse and key events, which I guess covers 95% of what you expect from a GUI. Note that if you don't want to modify the original source code you can, of course, create your own class copying AxesWidget class.
You find all the available matplolib widgets here https://matplotlib.org/3.1.1/api/widgets_api.html
Here is a modified version of your app where we put everything together:
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
from matplotlib.widgets import Button
import numpy as np
class MyApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.canvas = tk.Canvas(self, width=500, height=500, cursor="cross")
self.canvas.pack(side="top", fill="both", expand=True)
def draw_image_and_button(self):
self.figure_obj = Figure()
self.ax = self.figure_obj.add_subplot()
self.figure_obj.subplots_adjust(bottom=0.25)
some_preloaded_data_array = np.zeros((600,600))
imgplot = self.ax.imshow(some_preloaded_data_array, cmap='gray')
# create tkagg canvas
self.canvas_agg = FigureCanvasTkAgg(self.figure_obj, master=self.canvas)
self.canvas_agg.draw()
self.canvas_agg.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# add matplolib toolbar
toolbar = NavigationToolbar2Tk(self.canvas_agg, self.canvas)
toolbar.update()
self.canvas_agg._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# add matplolib widgets
self.ax_ok_B = self.figure_obj.add_subplot(position=[0.2, 0.2, 0.1, 0.03]) # axes position doesn't really matter here because we have the resize event that adjusts widget position
self.ok_B = Button(self.ax_ok_B, 'Ok', canvas=self.canvas_agg)
# add tkinter widgets (outside of the matplolib canvas)
button = tk.Button(master=self, text="Quit", command=self._quit)
button.pack(side=tk.BOTTOM)
# Connect to Events
self.ok_B.on_clicked(self.ok)
self.canvas_agg.mpl_connect('button_press_event', self.press)
self.canvas_agg.mpl_connect('button_release_event', self.release)
self.canvas_agg.mpl_connect('resize_event', self.resize)
self.canvas_agg.mpl_connect("key_press_event", self.on_key_press)
self.protocol("WM_DELETE_WINDOW", self.abort_exec)
def abort_exec(self):
print('Closing with \'x\' is disabled. Please use quit button')
def _quit(self):
print('Bye bye')
self.quit()
self.destroy()
def ok(self, event):
print('Bye bye')
self.quit()
self.destroy()
def press(self, event):
button = event.button
print('You pressed button {}'.format(button))
if event.inaxes == self.ax and event.button == 3:
self.xp = int(event.xdata)
self.yp = int(event.ydata)
self.cid = (self.canvas_agg).mpl_connect('motion_notify_event',
self.draw_line)
self.pltLine = Line2D([self.xp, self.xp], [self.yp, self.yp])
def draw_line(self, event):
if event.inaxes == self.ax and event.button == 3:
self.yd = int(event.ydata)
self.xd = int(event.xdata)
self.pltLine.set_visible(False)
self.pltLine = Line2D([self.xp, self.xd], [self.yp, self.yd], color='r')
self.ax.add_line(self.pltLine)
(self.canvas_agg).draw_idle()
def release(self, event):
button = event.button
(self.canvas_agg).mpl_disconnect(self.cid)
print('You released button {}'.format(button))
def on_key_press(self, event):
print("you pressed {}".format(event.key))
# Resize event is needed if you want your widget to move together with the plot when you resize the window
def resize(self, event):
ax_ok_left, ax_ok_bottom, ax_ok_right, ax_ok_top = self.ax.get_position().get_points().flatten()
B_h = 0.08 # button width
B_w = 0.2 # button height
B_sp = 0.08 # space between plot and button
self.ax_ok_B.set_position([ax_ok_right-B_w, ax_ok_bottom-B_h-B_sp, B_w, B_h])
print('Window was resized')
if __name__ == "__main__":
app = MyApp()
app.draw_image_and_button()
app.mainloop()
Ok let's see the functionalities of this app:
Press a key on the keyboard → print the pressed key
Press a mouse button → print the pressed button (1 = left, 2 = wheel, 3 = right)
Release a mouse button → print the released button
Press the right button on any point on the plot and draw a line while keeping the mouse button down
Press ok or quit to close the application
Pressing 'x' to close the window is disabled.
Resize the window → Plot and widgets scales accordingly
I also took the liberty to add the classic matplotlib toolbar for other functionalities like zooming.
Note that the image plot is added with add_suplot() method which adds the resizing functionality. In this way when you resize the window the plot scales accordingly.
Most of the things I implemented you also find them on the official tutorial from matplotlib on how to embed in tk (https://matplotlib.org/3.1.3/gallery/user_interfaces/embedding_in_tk_sgskip.html).
Let me know if this answers your question. I wanted to share it because I actually developed something very similar a few days ago.

pyplot example with QT Designer

I have seen many examples of integrating matplotlib with python2.6 and QT4 Designer using mplwidget and they work fine. I now need to integrate a pyplot with QT4 Designer but cannot find any examples. All of my attempts to integrate a pyplot graphic have ended in a seg fault. Can someone please direct me to a working example using Designer and pyplot?
Follow up:
Okay, so I tried your solution but I'm still having issues. Unfortunately the machine I use for this code is not hooked up to the internet, so below is a fat finger of the pertinent parts of the code I am using:
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
import MatplotlibWidget # This is the code snippet you gave in your answer
.
.
.
def drawMap(self) :
fig = plt.figure()
map = Basemap(projection='cyl', llcrnrlat = -90.0, urcrnrlat = 90.0, llcrnrlon = -180.0, urcrnrlon = 180.0, resolution = 'c')
map.drawcoastlines()
map.drawcountries()
plt.show()
def drawMap_Qt(self) :
self.ui.PlotWidget.figure = plt.figure() # This is the Qt widget promoted to MatplotlibWidget
map = Basemap(projection='cyl', llcrnrlat = -90.0, urcrnrlat = 90.0, llcrnrlon = -180.0, urcrnrlon = 180.0, resolution = 'c')
map.drawcoastlines()
map.drawcountries()
self.ui.PlotWidget.show()
The function drawMap() works fine. It creates a separate window and plots the map. The drawMap_Qt() function results in a segmentation fault with no other errors. The end goal is to plot a contour on top of the map. I can do this with the drawMap() function. Of course, I can't even get to the contour part with the drawMap_Qt() function. Any insights as to why it is seg faulting would be greatly appreciated.
If you're referring to the mplwidget from Python(x,y) and WinPython, I think it does use pyplot, but I had trouble putting it in my python install so I just used this class:
from PyQt4 import QtGui
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
class MatplotlibWidget(QtGui.QWidget):
def __init__(self, parent=None, *args, **kwargs):
super(MatplotlibWidget, self).__init__(parent)
self.figure = Figure(*args, **kwargs)
self.canvas = FigureCanvas(self.figure)
self.toolbar = NavigationToolbar(self.canvas, self)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
self.setLayout(layout)
See also How to embed matplotib in pyqt - for Dummies.
Then in Qt Designer you need to create a promoted class, with base class: QWidget, promoted class name: MatplotlibWidget, and header file: the python script containing the MatplotlibWidget class (without the .py). You can add things like ax = self.figure.add_subplot(111), line = ax.plt(...) within the class or by calling methods of the figure attribute of an instance of the class.
Edit:
So I was a bit wrong before, in general with embedded matplotlib widgets you need to use the object oriented methods and not the functions in pyplot. (This sort of explains what the difference is.) Using my snippet above as mymatplotlibwidget.py, try something like this. I don't have basemap installed, so this is somewhat of a guess, but from the examples you need to tell Basemap which axes to use.
import sys
from PyQt4 import QtGui
from mpl_toolkits.basemap import Basemap
from mymatplotlibwidget import MatplotlibWidget
app = QtGui.QApplication(sys.argv)
widget = MatplotlibWidget()
fig = widget.figure
ax = fig.add_subplot(111)
map = Basemap(..., ax=ax)
fig.canvas.draw()
widget.show()
app.exec_()

How to create a figure with show() via the matplotlib API?

I need to create a figure object which has can be show()'ed on the screen (when using some interactive backend) or savefig()'ed, but I need to avoid using the pylab/pyplot API as it sets default backend and messes other things up. I create the figure as
import matplotlib.figure
import matplotlib.backends.backend_qt4agg # or agg for headless backends
figure=matplotlig.figure.Figure()
canvas=matplotlib.backends.backend_qt4agg.FigureCanvasQTAgg(figure)
but I am still missing something. The documentation of Figure.show says
If the figure was not created using figure(), it will lack a FigureManagerBase, and will raise an AttributeError.
How do I do that then?
The 'standard' window you get in interactive sessions is run via the figure_manager family of classes which are tightly to pyplot.
Fortunately, all of the zoom/pan functionality is wrapped up in the NavigationToolbar family of classes.
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
def create_main_frame(self):
# host widget
self.main_frame = QtGui.QWidget()
# set up the mpl side of things
self.fig = Figure((24, 24), tight_layout=True)
self.canvas = FigureCanvas(self.fig)
# attach canvas to host widget
self.canvas.setParent(self.main_frame)
# make axes
self.axes = self.fig.add_subplot(111, adjustable='datalim', aspect='equal')
# make the tool bar
self.mpl_toolbar = NavigationToolbar(self.canvas, self.main_frame)
# set up layout
vbox = QtGui.QVBoxLayout()
vbox.addWidget(self.mpl_toolbar)
vbox.addWidget(self.canvas)
# set the layout to the host widget
self.main_frame.setLayout(vbox)
# make the host widget the central widget in the main frame of the class
# this code is ripped from
self.setCentralWidget(self.main_frame)

Matplotlib plot in Tkinter - every update adds new NavigationToolbar?

I am working on a Tkinter-GUI to interactively generate Matplotlib-plots, depending on user-input. For this end, it needs to re-plot after the user changes the input.
I have gotten it to work in principle, but would like to include the NavigationToolbar. However, I cannot seem to get the updating of the NavigationToolbar to work correctly.
Here is a basic working version of the code (without the user input Entries):
# Import modules
from Tkinter import *
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
# global variable: do we already have a plot displayed?
show_plot = False
# plotting function
def plot(x, y):
fig = plt.figure()
ax1 = fig.add_subplot(1,1,1)
ax1.plot(x,y)
return fig
def main():
x = np.arange(0.0,3.0,0.01)
y = np.sin(2*np.pi*x)
fig = plot(x, y)
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar = NavigationToolbar2TkAgg(canvas, toolbar_frame)
global show_plot
if show_plot:
#remove existing plot and toolbar widgets
canvas.get_tk_widget().grid_forget()
toolbar_frame.grid_forget()
toolbar_frame.grid(row=1,column=1)
canvas.get_tk_widget().grid(row=0,column=1)
show_plot=True
# GUI
root = Tk()
draw_button = Button(root, text="Plot!", command = main)
draw_button.grid(row=0, column=0)
fig = plt.figure()
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().grid(row=0,column=1)
toolbar_frame = Frame(root)
toolbar_frame.grid(row=1,column=1)
root.mainloop()
Pressing "Plot!" once generates the plot and the NavigationToolbar.
Pressing it a second time replots, but generates a second NavigationToolbar (and another every time "Plot!" is pressed). Which sounds like grid_forget() is not working.
However, when I change
if show_plot:
#remove existing plot and toolbar widgets
canvas.get_tk_widget().grid_forget()
toolbar_frame.grid_forget()
toolbar_frame.grid(row=1,column=1)
canvas.get_tk_widget().grid(row=0,column=1)
show_plot=True
to
if show_plot:
#remove existing plot and toolbar widgets
canvas.get_tk_widget().grid_forget()
toolbar_frame.grid_forget()
else:
toolbar_frame.grid(row=1,column=1)
canvas.get_tk_widget().grid(row=0,column=1)
show_plot=True
then the NavigationToolbar does vanish when "Plot!" is pressed a second time (but then there is, as expected, no new NavigationToolbar to replace the old). So grid_forget() is working, just not as expected.
What am I doing wrong? Is there a better way to update the NavigationToolbar?
Any help greatly appreciated!
Lastalda
Edit:
I found that this will work if you destroy the NavigationToolbar instead of forgetting it. But you have to completely re-create the widget again afterwards, of course:
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar_frame = Frame(root)
global show_plot
if show_plot: # if plot already present, remove plot and destroy NavigationToolbar
canvas.get_tk_widget().grid_forget()
toolbar_frame.destroy()
toolbar_frame = Frame(root)
toolbar = NavigationToolbar2TkAgg(canvas, toolbar_frame)
toolbar_frame.grid(row=21,column=4,columnspan=3)
canvas.get_tk_widget().grid(row=1,column=4,columnspan=3,rowspan=20)
show_plot = True
However, the updating approach showed by Hans below is much nicer since you don't have to destroy and recreate anything. I just wanted to highlight that the issue with my approach (apart from the inelegance and performance) was probably that I didn't use destroy().
A slightly different approach might be to reuse the figure for subsequent plots by clearing & redrawing it. That way, you don't have to destroy & regenerate neither the figure nor the toolbar:
from Tkinter import Tk, Button
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
# plotting function: clear current, plot & redraw
def plot(x, y):
plt.clf()
plt.plot(x,y)
# just plt.draw() won't do it here, strangely
plt.gcf().canvas.draw()
# just to see the plot change
plotShift = 0
def main():
global plotShift
x = np.arange(0.0,3.0,0.01)
y = np.sin(2*np.pi*x + plotShift)
plot(x, y)
plotShift += 1
# GUI
root = Tk()
draw_button = Button(root, text="Plot!", command = main)
draw_button.grid(row=0, column=0)
# init figure
fig = plt.figure()
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar = NavigationToolbar2TkAgg(canvas, root)
canvas.get_tk_widget().grid(row=0,column=1)
toolbar.grid(row=1,column=1)
root.mainloop()
When you press the "Plot!" button it calls main. main creates the navigation toolbar. So, each time you press the button you get a toolbar. I don't know much about matplot, but it's pretty obvious why you get multiple toolbars.
By the way, grid_forget doesn't destroy the widget, it simply removes it from view. Even if you don't see it, it's still in memory.
Typically in a GUI, you create all the widgets exactly once rather than recreating the same widgets over and over.