I did the following example code just to test how to integrate an animated matplotlib plot with pygtk. However, I get some unexpected behaviors when I run it.
First, when I run my program and click on the button (referred to as button1 in the code), there is another external blank window which shows up and the animated plot starts only after closing this window.
Secondly, when I click on the button many times, it seems that there is more animations which are created on top of each other (which gives the impression that the animated plot speeds up). I have tried to call animation.FuncAnimation inside a thread (as you can see in the comment at end of the function on_button1_clicked), but the problem still the same.
Thirdly, is it a good practice to call animation.FuncAnimation in a thread to allow the user to use the other functions of the gui ? Or should I rather create a thread inside the method animate (I guess this will create too many threads quickly) ? I am not sure how to proceed.
Here is my code:
import gtk
from random import random
import numpy as np
from multiprocessing.pool import ThreadPool
import matplotlib.pyplot as plt
import matplotlib.animation as animation
#from matplotlib.backends.backend_gtk import FigureCanvasGTK as FigureCanvas
from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
#from matplotlib.backends.backend_gtkcairo import FigureCanvasGTKCairo as FigureCanvas
class HelloWorld:
def __init__(self):
interface = gtk.Builder()
interface.add_from_file('interface.glade')
self.dialog1 = interface.get_object("dialog1")
self.label1 = interface.get_object("label1")
self.entry1 = interface.get_object("entry1")
self.button1 = interface.get_object("button1")
self.hbox1 = interface.get_object("hbox1")
self.fig, self.ax = plt.subplots()
self.X = [random() for x in range(10)]
self.Y = [random() for x in range(10)]
self.line, = self.ax.plot(self.X, self.Y)
self.canvas = FigureCanvas(self.fig)
# self.hbox1.add(self.canvas)
self.hbox1.pack_start(self.canvas)
interface.connect_signals(self)
self.dialog1.show_all()
def gtk_widget_destroy(self, widget):
gtk.main_quit()
def on_button1_clicked(self, widget):
name = self.entry1.get_text()
self.label1.set_text("Hello " + name)
self.ani = animation.FuncAnimation(self.fig, self.animate, np.arange(1, 200), init_func=self.init, interval=25, blit=True)
'''
pool = ThreadPool(processes=1)
async_result = pool.apply_async(animation.FuncAnimation, args=(self.fig, self.animate, np.arange(1, 200)), kwds={'init_func':self.init, 'interval':25, 'blit':True} )
self.ani = async_result.get()
'''
plt.show()
def animate(self, i):
# Read XX and YY from a file or whateve
XX = [random() for x in range(10)] # just as an example
YY = [random() for x in range(10)] # just as an example
self.line.set_xdata( XX )
self.line.set_ydata( YY )
return self.line,
def init(self):
self.line.set_ydata(np.ma.array(self.X, mask=True))
return self.line,
if __name__ == "__main__":
HelloWorld()
gtk.main()
Related
By using this answer to produce a LiveGraph and this answer to update variables to a thread, I was able to generate a graph that updates itself each second and whose amplitude is determined by a slider (code below). Both answers were incredibly helpful!
%matplotlib notebook
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
from threading import Thread, Lock
import time
import ipywidgets as widgets
from IPython.display import display
import numpy as np
'''#################### Live Graph ####################'''
# Create a new class which is a live graph
class LiveGraph(object):
def __init__(self, baseline):
self.x_data, self.y_data = [], []
self.figure = plt.figure()
self.line, = plt.plot(self.x_data, self.y_data)
self.animation = FuncAnimation(self.figure, self.update, interval=1200)
# define variable to be updated as a list
self.baseline = [baseline]
self.lock = Lock()
self.th = Thread(target=self.thread_f, args = (self.baseline,), daemon=True)
# start thread
self.th.start()
def update_baseline(self,baseline):
# updating a list updates the thread argument
with self.lock:
self.baseline[0] = baseline
# Updates animation
def update(self, frame):
self.line.set_data(self.x_data, self.y_data)
self.figure.gca().relim()
self.figure.gca().autoscale_view()
return self.line,
def show(self):
plt.show()
# Function called by thread that updates variables
def thread_f(self, base):
x = 0
while True:
self.x_data.append(x)
x += 1
self.y_data.append(base[0])
time.sleep(1)
'''#################### Slider ####################'''
# Function that updates baseline to slider value
def update_baseline(v):
global g
new_value = v['new']
g.update_baseline(new_value)
slider = widgets.IntSlider(
value=10,
min=0,
max=200,
step=1,
description='value:',
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=True,
readout_format='d'
)
slider.observe(update_baseline, names = 'value')
'''#################### Display ####################'''
display(slider)
g = LiveGraph(slider.value)
Still, I would like to put the graph inside a bigger interface which has other widgets. It seems that I should put the LiveGraph inside the Output widget, but when I replace the 'Display section' of my code by the code shown below, no figure is displayed.
out = widgets.Output(layout={'border': '1px solid black'})
with out:
g = LiveGraph(slider.value)
vbox = widgets.VBox([slider,out], align_self='stretch',justify_content='center')
vbox
Is there a way to embed this LiveGraph in the output widget or in a box widget?
I found a solution by avoiding using FuncAnimation and the Output widget altogether while keeping my backend as inline.
Also changing from matplotlib to bqplot was essential!
The code is shown below (be careful because, as it is, it keeps increasing a list).
Details of things I tried:
I had no success updating the graph by a thread when using the Output Widget (tried clearing axes with ax.clear, redrawing the whole plot - since it is a static backend - and also using clear_output() command).
Also, ipywidgets does not allow placing a matplotlib figure straight inside a container, but it does if it is a bqplot figure!
I hope this answer helps anyone trying to integrate ipywidgets with a plot that constantly updates itself within an interface full of other widgets.
%matplotlib inline
import bqplot.pyplot as plt
from threading import Thread, Lock
import time
import ipywidgets as widgets
from IPython.display import display
import numpy as np
fig = plt.figure()
t, value = [], []
lines = plt.plot(x=t, y=value)
# Function that updates baseline to slider value
def update_baseline(v):
global base, lock
with lock:
new_value = v['new']
base = new_value
slider = widgets.IntSlider(
value=10,
min=0,
max=200,
step=1,
description='value:',
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=True,
readout_format='d'
)
base = slider.value
slider.observe(update_baseline, names = 'value')
def thread_f():
global t, value, base, lines
x = 0
while True:
t.append(x)
x += 1
value.append(base)
with lines.hold_sync():
lines.x = t
lines.y = value
time.sleep(0.1)
lock = Lock()
th = Thread(target=thread_f, daemon=True)
# start thread
th.start()
vbox = widgets.VBox([slider,fig], align_self='stretch',justify_content='center')
vbox
P.S. I'm new to using threads, so be careful as the thread may not be properly stopped with this code.
bqplot==0.12.29
ipywidgets==7.6.3
numpy==1.20.2
Starting with the working Matplotlib animation code shown below, my goal is to embed this animation (which is just a circle moving across the screen) within a PyQT4 GUI.
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
from matplotlib import animation
fig,ax = plt.subplots()
ax.set_aspect('equal','box')
circle = Circle((0,0), 1.0)
ax.add_artist(circle)
ax.set_xlim([0,10])
ax.set_ylim([-2,2])
def animate(i):
circle.center=(i,0)
return circle,
anim = animation.FuncAnimation(fig,animate,frames=10,interval=100,repeat=False,blit=True)
plt.show()
I am able to accomplish this using the following code, but there is one hitch: I cannot get blitting to work.
import sys
from PyQt4 import QtGui, QtCore
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Circle
from matplotlib import animation
class Window(QtGui.QDialog): #or QtGui.QWidget ???
def __init__(self):
super(Window, self).__init__()
self.fig = Figure(figsize=(5,4),dpi=100)
self.canvas = FigureCanvas(self.fig)
self.ax = self.fig.add_subplot(111) # create an axis
self.ax.hold(False) # discards the old graph
self.ax.set_aspect('equal','box')
self.circle = Circle((0,0), 1.0)
self.ax.add_artist(self.circle)
self.ax.set_xlim([0,10])
self.ax.set_ylim([-2,2])
self.button = QtGui.QPushButton('Animate')
self.button.clicked.connect(self.animate)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.canvas)
layout.addWidget(self.button)
self.setLayout(layout)
def animate(self):
self.anim = animation.FuncAnimation(self.fig,self.animate_loop,frames=10,interval=100,repeat=False,blit=False)
self.canvas.draw()
def animate_loop(self,i):
self.circle.center=(i,0)
return self.circle,
def main():
app = QtGui.QApplication(sys.argv)
ex = Window()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
When I set blit=True, after pressing the Animate button I get the following error:
a.figure.canvas.restore_region(bg_cache[a])
KeyError: matplotlib.axes._subplots.AxesSubplot object at 0x00000000095F1D30
In searching this error, I find many posts about how blitting does not work on Macs, but I am using Windows 7. I have tried replacing self.canvas.draw() with self.canvas.update(), but this does not work.
After looking at the source code of the animation module, I realized that there is an error in the Animation class (the dictionary bg_cache is empty, when it is accessed for the first time with blitting switched on).
This is fixed in the git version of matplotlib; however, in the most recent stable version 1.5.1, the bug is still present. You can either fix the bug in the matplotlib code itself or you can make a subclass to FuncAnimation. I chose that way, because it should still work after updating matplotlib.
from matplotlib import animation
class MyFuncAnimation(animation.FuncAnimation):
"""
Unfortunately, it seems that the _blit_clear method of the Animation
class contains an error in several matplotlib verions
That's why, I fork it here and insert the latest git version of
the function.
"""
def _blit_clear(self, artists, bg_cache):
# Get a list of the axes that need clearing from the artists that
# have been drawn. Grab the appropriate saved background from the
# cache and restore.
axes = set(a.axes for a in artists)
for a in axes:
if a in bg_cache: # this is the previously missing line
a.figure.canvas.restore_region(bg_cache[a])
Then, simpy use MyFuncAnimation instead of animation.FuncAnimation.
Took me a while to figure it out, but I hope it helps anybody.
After some time I managed to recreate the animation by using the underlying functions directly and not using the animation wrapper:
import sys
from PyQt4 import QtGui, QtCore
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Circle
from matplotlib import animation
from time import sleep
class Window(QtGui.QDialog): #or QtGui.QWidget ???
def __init__(self):
super(Window, self).__init__()
self.fig = Figure(figsize=(5, 4), dpi=100)
self.canvas = FigureCanvas(self.fig)
self.ax = self.fig.add_subplot(111) # create an axis
self.ax.hold(False) # discards the old graph
self.ax.set_aspect('equal', 'box')
self.circle = Circle((0,0), 1.0, animated=True)
self.ax.add_artist(self.circle)
self.ax.set_xlim([0, 10])
self.ax.set_ylim([-2, 2])
self.button = QtGui.QPushButton('Animate')
self.button.clicked.connect(self.animate)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.canvas)
layout.addWidget(self.button)
self.setLayout(layout)
self.canvas.draw()
self.ax_background = self.canvas.copy_from_bbox(self.ax.bbox)
def animate(self):
self.animate_loop(0)
def animate_loop(self,begin):
for i in range(begin,10):
self.canvas.restore_region(self.ax_background)
self.circle.center=(i,0)
self.ax.draw_artist(self.circle)
self.canvas.blit(self.ax.bbox)
self.canvas.flush_events()
sleep(0.1)
def main():
app = QtGui.QApplication(sys.argv)
ex = Window()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Maybe this will be of use to you.
I'v got this matplotlib animation running in tkinter, it works fine but it just never stops looping, when i press 'X' the window closes but i have to force shut it with the task manager.
This is the example code of how i tried to set it up:
from matplotlib import pyplot as plt
from matplotlib import animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
from tkinter import *
class Grapher(tk.Tk): # inherit Tk()
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.wm_title(self, "Quarantined-Grapher")
self.fig = plt.figure()
ax = plt.axes(xlim=(0,2), ylim=(0, 100))
N = 4 # amount of lines
self.lines = [plt.plot([], [])[0] for _ in range(N)]
# give the figure and the root(which is self) to the "canvas"
self.canvas = FigureCanvasTkAgg(self.fig, self)
self.canvas.show()
self.canvas.get_tk_widget().pack()
anim = animation.FuncAnimation(self.fig, self.animate, init_func=self.init,
frames=100, interval=1000, blit=True)
def init(self):
for line in self.lines:
line.set_data([], [])
return self.lines
def animate(self, i):
for j,line in enumerate(self.lines):
line.set_data([0, 2], [10 * j,i]) # some trick to animate fake data.
return self.lines
app = Grapher()
app.mainloop()
My guess is it might be the animation loop that never stops running, because only tkinter knows to stop?..
Note: I made a graph work before, but i was using the tkinter after() method clearing and recreating data points, but it used up to much resources that i had to remake it. This way is so that i don't have to delete/create 10-50K data points every second.
Answering the wrong question:
This is behaving as intended (looping indefinitely). If you would like to only run once use the repeat kwarg (some what arcane docs):
anim = animation.FuncAnimation(self.fig, self.animate, init_func=self.init,
frames=100, interval=1000, blit=True,
repeat=False)
I'm running a Tkinter script that updates a plot every 5 seconds. It calls the function that plots it every 5 seconds. After not that long python starts using a lot of memory, I checked in task manager. The memory usage keeps increasing really fast. It starts a new file every 24 hours so there is a limit to the number of lines in the file.
The file starts empty.
I tried increasing the 5s time span but it does the same thing. Maybe a little slower,
also tried tried plotting every 3 rows or so but the same thing happened again.
Any idea what is causing such high memory usage and how to fix?
Thanks!
data = np.genfromtxt(filename)
time_data = data[:,0]
room_temp_data_celsius = data[:,1]
rad_temp_data_celsius = data[:,2]
fan_state_data = data[:,3]
threshold_data = data[:,4]
hysteresis_data = data[:,5]
threshold_up = [] #empty array
threshold_down = []#empty array
for i in range(0,len(threshold_data)):
threshold_up.append(threshold_data[i]+hysteresis_data[i])
threshold_down.append(threshold_data[i]-hysteresis_data[i])
# Time formatting
dts = map(datetime.datetime.fromtimestamp, time_data)
fds = matplotlib.dates.date2num(dts)
hfmt = matplotlib.dates.DateFormatter('%H:%M')
# Temperature conversion
room_temp_data_fahrenheit = map(celsius_to_fahrenheit, room_temp_data_celsius)
rad_temp_data_fahrenheit = map(celsius_to_fahrenheit, rad_temp_data_celsius)
threshold_data_fahrenheit = map(celsius_to_fahrenheit, threshold_data)
threshold_up_fahrenheit = map(celsius_to_fahrenheit, threshold_up)
threshold_down_fahrenheit = map(celsius_to_fahrenheit, threshold_down)
f = plt.figure()
a = f.add_subplot(111)
a.plot(fds,room_temp_data_fahrenheit, fds, rad_temp_data_fahrenheit, 'r')
a.plot(fds,fan_state_data*(max(rad_temp_data_fahrenheit)+4),'g_')
a.plot(fds, threshold_up_fahrenheit, 'y--')
a.plot(fds, threshold_down_fahrenheit, 'y--')
plt.xlabel('Time (min)')
plt.ylabel('Temperature '+unichr(176)+'F')
plt.legend(["Room Temperature","Radiator","Fan State","Threshold Region"], loc="upper center", ncol=2)
plt.ylim([min(room_temp_data_fahrenheit)-5, max(rad_temp_data_fahrenheit)+5])
plt.grid()
a.xaxis.set_major_formatter(hfmt)
data_graph = FigureCanvasTkAgg(f, master=root)
data_graph.show()
data_graph.get_tk_widget().grid(row=6,column=0, columnspan=3)
root.after(WAIT_TIME, control)
It's not clear to me from your code how your plots are changing with time. So I don't have any specific suggestion for your existing code. However, here is a basic example of how to embed an animated matplotlib figure in a Tkinter app. Once you grok how it works, you should be able to adapt it to your situation.
import matplotlib.pyplot as plt
import numpy as np
import Tkinter as tk
import matplotlib.figure as mplfig
import matplotlib.backends.backend_tkagg as tkagg
pi = np.pi
sin = np.sin
class App(object):
def __init__(self, master):
self.master = master
self.fig = mplfig.Figure(figsize = (5, 4), dpi = 100)
self.ax = self.fig.add_subplot(111)
self.canvas = canvas = tkagg.FigureCanvasTkAgg(self.fig, master)
canvas.get_tk_widget().pack(side = tk.TOP, fill = tk.BOTH, expand = 1)
self.toolbar = toolbar = tkagg.NavigationToolbar2TkAgg(canvas, master)
toolbar.update()
self.update = self.animate().next
master.after(10, self.update)
canvas.show()
def animate(self):
x = np.linspace(0, 6*pi, 100)
y = sin(x)
line1, = self.ax.plot(x, y, 'r-')
phase = 0
while True:
phase += 0.1
line1.set_ydata(sin(x + phase))
newx = x+phase
line1.set_xdata(newx)
self.ax.set_xlim(newx.min(), newx.max())
self.ax.relim()
self.ax.autoscale_view(True, True, True)
self.fig.canvas.draw()
self.master.after(10, self.update)
yield
def main():
root = tk.Tk()
app = App(root)
tk.mainloop()
if __name__ == '__main__':
main()
The main idea here is that plt.plot should only be called once. It returns a Line2D object, line1. You can then manipulate the plot by calling line1.set_xdata and/or line1.set_ydata. This "technique" for animation comes from the Matplotlib Cookbook.
Technical note:
The generator function, animate was used here to allow the state of the plot to be saved and updated without having to save state information in instance attributes. Note that it is the generator function's next method (not the generator self.animate) which is being called repeatedly:
self.update = self.animate().next
master.after(10, self.update)
So we are advancing the plot frame-by-frame by calling the generator, self.animate()'s, next method.
I have a matplotlib graph which I want repeated in two separate windows, under PyQt4. I've tried adding the widget to the layout of both, but then the widget vanishes from the first one. Is there any way to do this except creating two identical graphs and keeping them in sync?
The problem is that you can't add the same qt widget to two differents parents widgets because in the process of adding a widget Qt also make a reparent process which does what you see:
... the widget vanishes from the first one[window]...
So the solution is to make two canvas that share the same figure.
Here is an example code, this will show you two main windows each with two canvas and the four plots will be syncronized:
import sys
from PyQt4 import QtGui
import numpy as np
import numpy.random as rd
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
class ApplicationWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.main_widget = QtGui.QWidget(self)
vbl = QtGui.QVBoxLayout(self.main_widget)
self._fig = Figure()
self.ax = self._fig.add_subplot(111)
#note the same fig for two canvas
self.fc1 = FigureCanvas(self._fig) #canvas #1
self.fc2 = FigureCanvas(self._fig) #canvas #1
self.but = QtGui.QPushButton(self.main_widget)
self.but.setText("Update") #for testing the sync
vbl.addWidget(self.fc1)
vbl.addWidget(self.fc2)
vbl.addWidget(self.but)
self.setCentralWidget(self.main_widget)
#property
def fig(self):
return self._fig
#fig.setter
def fig(self, value):
self._fig = value
#keep the same fig in both canvas
self.fc1.figure = value
self.fc2.figure = value
def redraw_plot(self):
self.fc1.draw()
self.fc2.draw()
qApp = QtGui.QApplication(sys.argv)
aw1 = ApplicationWindow() #window #1
aw2 = ApplicationWindow() #window #2
aw1.fig = aw2.fig #THE SAME FIG FOR THE TWO WINDOWS!
def update_plot():
'''Just a random plot for test the sync!'''
#note that the update is only in the first window
ax = aw1.fig.gca()
ax.clear()
ax.plot(range(10),rd.random(10))
#calls to redraw the canvas
aw1.redraw_plot()
aw2.redraw_plot()
#just for testing the update
aw1.but.clicked.connect(update_plot)
aw2.but.clicked.connect(update_plot)
aw1.show()
aw2.show()
sys.exit(qApp.exec_())
While it's not a perfect solution, matplotlib has a built-in way to keep the limits, ticks, etc of two separate plots in sync.
E.g.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 4 * np.pi, 100)
y = np.cos(x)
figures = [plt.figure() for _ in range(3)]
ax1 = figures[0].add_subplot(111)
axes = [ax1] + [fig.add_subplot(111, sharex=ax1, sharey=ax1) for fig in figures[1:]]
for ax in axes:
ax.plot(x, y, 'go-')
ax1.set_xlabel('test')
plt.show()
Notice that all 3 plots will stay in-sync as you zoom, pan, etc.
There's probably a better way of doing it though.