I'm working on creating a program that utilizes Tkinter and matplotlib. I have 2 lists of lists (one for x-axis, one for y-axis) and I'm looking to have a button that can switch between the lists within the list. I took much of the code from the question Interactive plot based on Tkinter and matplotlib, but I can't get the graph to update when the button is pressed. I'm quite new to using classes and having a bit of difficulty understanding them.
tft is the x-data tf1 is the y-data in my code.
Example of data:
x-data = [[1,2,3,4,5],[10,11,13,15,12,19],[20,25,27]]
y-data = [[5.4,6,10,11,6],[4,6,8,34,20,12],[45,25,50]]
My code below will graph one of the lists within a list, but won't switch between the lists within that list when the button is pressed. The correct value for event_num also prints in the command window (the issue of event_num was solved in a previous questions here).
There is no error that appears, the program only prints the number (when the button is pressed), but doesn't update the graph with the new data from the list.
Preliminary Code (no issues--or there shouldn't be any)
#Importing Modules
import glob
from Tkinter import *
from PIL import Image
from Text_File_breakdown import Text_File_breakdown
import re
import matplotlib.pyplot as plt
from datetime import datetime
#Initializing variables
important_imgs=[]
Image_dt=[]
building=[]
quick=[]
num=0
l=0
match=[]
#Getting the names of the image files
image_names=glob.glob("C:\Carbonite\EL_36604.02_231694\*.jpeg")
#image= Image.open(images_names[1])
#image.show()
#Text_File_breakdown(file,voltage limit,pts after lim, pts before lim)
tft,tf1,tf2=Text_File_breakdown('C:\Carbonite\EL_36604.02_231694.txt',3.0,5,5)
#tft= time of voltages tf1=Voltage signal 1 tf2=Voltage signal 2
#Test Settings: 'C:\Carbonite\EL_36604.02_231694.txt',3.0,5,5
#Getting the Dates from the image names
for m in image_names:
Idt=re.search("([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}.[0-9]{2}.[0-9]{2})", m)
Im_dat_tim=Idt.group(1)
Im_dat_tim=datetime.strptime(Im_dat_tim, '%Y-%m-%d %H.%M.%S')
Image_dt.append(Im_dat_tim)
Im_dat_tim=None
#Looking for the smallest difference between the voltage and image dates and associating an index number (index of the image_names variable) with each voltage time
for event in range(len(tft)):
for i in range(len(tft[event])):
diff=[tft[event][i]-Image_dt[0]]
diff.append(tft[event][i]-Image_dt[0])
while abs(diff[l])>=abs(diff[l+1]):
l=l+1
diff.append(tft[event][i]-Image_dt[l])
match.append(l)
l=0
#Arranging the index numbers (for the image_names variable) in a list of lists like tft variable
for count in range(len(tft)):
for new in range(len(tft[count])):
quick.append(match[num])
num=num+1
building.append(quick)
quick=[]
plt.close('all')
fig, ax = plt.subplots(1)
ax.plot(tft[1],tf1[1],'.')
# rotate and align the tick labels so they look better
fig.autofmt_xdate()
# use a more precise date string for the x axis locations in the
# toolbar
import matplotlib.dates as mdates
ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
plt.title('Single Event')
Continuation of code/Portion of code where the issue is:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import numpy as np
class App:
def __init__(self, master):
self.event_num = 1
# Create a container
frame = Frame(master)
# Create 2 buttons
self.button_left = Button(frame,text="< Previous Event",
command=self.decrease)
self.button_left.grid(row=0,column=0)
self.button_right = Button(frame,text="Next Event >",
command=self.increase)
self.button_right.grid(row=0,column=1)
fig = Figure()
ax = fig.add_subplot(111)
fig.autofmt_xdate()
import matplotlib.dates as mdates
ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
self.line, = ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas = FigureCanvasTkAgg(fig,master=master)
self.canvas.show()
self.canvas.get_tk_widget().grid(row=1,column=0)
frame.grid(row=0,column=0)
def decrease(self):
self.event_num -= 1
print self.event_num
self.line, = ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas.draw()
#self.canvas.draw(tft[self.event_num],tf1[self.event_num],'.')
#self.line.set_xdata(tft[event_num])
#self.line.set_ydata(tf1[event_num])
def increase(self):
self.event_num += 1
print self.event_num
self.line, = ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas.draw()
#self.canvas.draw(tft[self.event_num],tf1[self.event_num],'.')
#self.set_xdata(tft[event_num])
#self.set_ydata(tf1[event_num])
root = Tk()
app = App(root)
root.mainloop()
The problem is that you are constantly plotting on a different set of axes than you think. self.line, = ax.plot(tft[self.event_num],tf1[self.event_num],'.') refers to the axes that you created outside your class, not the axes in the figure you created in the App class. The problem can be remedied by creating self.fig and self.ax attributes:
class App:
def __init__(self, master):
self.event_num = 1
# Create a container
frame = Frame(master)
# Create 2 buttons
self.button_left = Button(frame,text="< Previous Event",
command=self.decrease)
self.button_left.grid(row=0,column=0)
self.button_right = Button(frame,text="Next Event >",
command=self.increase)
self.button_right.grid(row=0,column=1)
self.fig = Figure()
self.ax = self.fig.add_subplot(111)
self.fig.autofmt_xdate()
import matplotlib.dates as mdates
self.ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
self.line, = self.ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas = FigureCanvasTkAgg(self.fig,master=master)
self.canvas.show()
self.canvas.get_tk_widget().grid(row=1,column=0)
frame.grid(row=0,column=0)
def decrease(self):
self.event_num -= 1
print self.event_num
self.line, = self.ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas.draw()
def increase(self):
self.event_num += 1
print self.event_num
self.line, = self.ax.plot(tft[self.event_num],tf1[self.event_num],'.')
self.canvas.draw()
Another (possible) problem is that the data gets appended to the plot instead of being replaced. There are two ways to fix this:
Turn hold off: self.ax.hold(False) somewhere in __init__() before you plot anything.
Actually replace the plot data: Replace the line
self.line, = self.ax.plot(tft[self.event_num],tf1[self.event_num],'.')
with
self.line.set_xdata(tft[self.event_num])
self.line.set_ydata(tf1[self.event_num])
You are plotting to an axes in the global namespace but redrawing the canvas on another, unrelated global object. I think your issues are about global vs. local scope and the self object in object-oriented programming.
Python scoping
Variables created in your file without indentation are in a file's global scope. class and def statements create local scopes. References to a variable name are resolved by checking scopes in this order:
Local scope
Enclosing local scope (enclosing defs only, not classes)
Global scope
Builtins
That cascade is only for references. Assignments to a variable name assign to the current global or local scope, period. A variable of that name will be created in the current scope if it doesn't already exist.
The bindings of local variables are deleted when a function returns. Values that are not bound are eventually garbage collected. If an assignment binds a global variable to a local variable's value, the value will persist with the global.
Applying this to your code
There are two sets of figure/axes objects created in your code.
# From your top chunk of code
from Tkinter import *
import matplotlib.pyplot as plt
# This line creates a global variable ax:
fig, ax = plt.subplots(1)
# From your bottom chunk of code
class App(self, master):
def __init__(self, master):
fig = Figure()
# This line creates a local variable ax:
ax = fig.add_subplot(111)
# This line references the local variable ax:
ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
# This line references the local variable ax, and binds its value
# to the attribute line of the self object,
# which is the global variable app:
self.line, = ax.plot(x[self.event_n],y[self.event_n])
def increase(self):
self.event_num += 1
# This line references the global variable ax
self.line, = ax.plot(x[self.event_n],y[self.event_n])
# This line updates the object's canvas but the plot
# was made on the global axes:
self.canvas.draw()
app = App(root)
You can fix this up by always referencing an Axes associated with your Tkinter root window:
self.fig, self.ax = plt.subplots(1, 1) to create enduring Axes.
self.ax.cla() to remove the previous selection's data
self.ax.plot() to add the data at the current self.event_num index
self.ax.set_xlim(x_min*0.9, x_max*0.9) and self.ax.set_ylim(y_min*1.1, y_max*1.1) to keep the axes window consistent so as to visualize movement of the mean of data among inner lists. The x_max, y_max, x_min, and y_min can be determined before the main event loop by flattening the x and y lists and then using builtins max() and min().
Related
I would like to interactively change a matplotlib.animation argument depending on the value provided by a GUI.
Example:
I prepared an example code which I show below, where I am trying to change the interval argument of animation based on a value provided by the user through a spinBox created with tkinter.
Problem:
In order to be able to update its argument, I want to call my animation into the call back function called by the spinbox. But if I do that, I get the following error message " UserWarning: Animation was deleted without rendering anything. This is most likely unintended. To prevent deletion, assign the Animation to a variable that exists for as long as you need the Animation."
If I call my animation into the main code, then I won't be able to interactively change its arguments
Question:
How can I change an animation argument interactively, i.e. based on a value which the user can set in a tkinter widget?
Example code:
import tkinter as tk
from random import randint
import matplotlib as plt
import matplotlib.animation as animation
import matplotlib.backends.backend_tkagg as tkagg
#Creating an instance of the Tk class
win = tk.Tk()
#Creating an instance of the figure class
fig = plt.figure.Figure()
#Create a Canvas containing fig into win
aCanvas =tkagg.FigureCanvasTkAgg(fig, master=win)
#Making the canvas a tkinter widget
aFigureWidget=aCanvas.get_tk_widget()
#Showing the figure into win as if it was a normal tkinter widget
aFigureWidget.grid(row=0, column=0)
#Defining the animation
ax = fig.add_subplot(xlim=(0, 1), ylim=(0, 1))
(line,) = ax.plot([],[], '-')
CumulativeX, CumulativeY = [], []
# Providing the input data for the plot for each animation step
def update(i):
CumulativeX.append(randint(0, 10) / 10)
CumulativeY.append(randint(0, 10) / 10)
return line.set_data(CumulativeX, CumulativeY)
spinBoxValue=1000
#When the button is pushed, get the value
def button():
spinBoxValue=aSpinbox.get()
#Running the animation
ani=animation.FuncAnimation(fig, update, interval=spinBoxValue, repeat=True)
#Creating an instance of the Spinbox class
aSpinbox = tk.Spinbox(master=win,from_=0, to=1000, command=button)
#Placing the button
aSpinbox .grid(row=2, column=0)
#run the GUI
win.mainloop()
We have to redraw the animation using fig.canvas.draw() when the animation is created inside the function button:
def button():
global spinBoxValue, CumulativeX, CumulativeY, ani
spinBoxValue = aSpinbox.get()
CumulativeX, CumulativeY = [], [] # This is optional
# To stop the background animation
ani.event_source.stop()
# Unlink/delete the reference to the previous animation
# del ani
ani=animation.FuncAnimation(fig, update, interval=int(spinBoxValue) * 1000, repeat=False)
fig.canvas.draw()
In the code provided, it was drawing the lines too fast when it was recreating animation using the value from aSpinbox.get(), so I changed the input to integer to draw the animation at a slower rate using interval=int(spinBoxvalue) * 1000 inside the button function.
On deleting the animation
Since we have to stop the background animation and also run the newly generated animation when the button is pressed, and because an animation must be stored in a variable as long as it runs, we will have to refer to the previous and the latest animation by the same variable name.
We can delete the animation stored in the global variable ani, using del ani after ani.event_source.stop(), which would lose the reference to the animation stored in memory before the button was pressed, but we can't really free the memory address where the reference by ani was made (I am guessing this would be true as long as we are using default garbage collection method in Python).
EDIT
Jumping to a new animation will not update/remove any variables created on the axes here - we will have to take care of it explicitly. To update variables only once after pressing the button, first create those variables in the global scope of code, and delete them inside button function and recreate/define them before/after using fig.canvas.draw:
# Defined in global scope
text = ax.text(0.7, 0.5, "text")
def button():
global spinBoxValue, CumulativeX, CumulativeY, ani, text
spinBoxValue = int(aSpinbox.get())
# To stop the background animation
ani.event_source.stop()
CumulativeX, CumulativeY = [], []
# Unlink/delete the reference to the previous animation
# del ani
text.remove()
text = ax.text(0.7 * spinBoxValue/10 , 0.5, "text")
ani=animation.FuncAnimation(fig, update, interval=spinBoxValue*1000, repeat=False)
fig.canvas.draw()
The same logic can be applied to use update function to redraw text after every button press or after every frame while using the function button provided at the very top:
text = ax.text(0.7, 0.5, "text")
# Providing the input data for the plot for each animation step
def update(i):
global text
text.remove()
# Update text after button press
# "text" is drawn at (0.7 * 1000/10, 0.5) when button = 0
text = ax.text(0.7 * spinBoxValue/10 , 0.5, "text")
# Comment previous line and uncomment next line to redraw text at every frame
# text = ax.text(0.7 * i/10 , 0.5, "text")
CumulativeX.append(randint(0, 10) / 10)
CumulativeY.append(randint(0, 10) / 10)
print(CumulativeX)
return line.set_data(CumulativeX, CumulativeY)
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
# when the code works but it's meaningless to include it
### When I can't get this part to work and I'd need your code
How do you hide or show an Axes object (subplot) in matplotlib so you can toggle between different Axes in the same figure?
I'm using matplotlib to display graphics in a Tkinter GUI and I'd like to use radiobuttons to switch between different axes in the same figure.
Basically I'll have some radiobuttons linked to a IntVar():
graphic_version = tk.IntVar()
tk.Radiobutton(root, text='Option1', variable=graphic_version, value=1).pack()
tk.Radiobutton(root, text='Option2', variable=graphic_version, value=2).pack()
Then I'd trace the IntVar() with a custom methods updating my figure with the requested graphic:
choice.trace("w", lambda choice: myGraphic.showGraphic(version))
so that everytime the user clicks a radiobutton the figure is updated with a different version of the plot. Now the problem is I have no idea how to do the showGraphic properly. Lets say I use this class system to get 2 different versions of plotting the same data:
class Version1():
def __init__(self, ax, data):
self.ax = ax #This is a Axes object
self.data = self._formatDataV1(data)
self._draw()
self._setOptions()
self.hide()
def _formatDataV1(self, data):
#Here I manipulate the raw data to extract the info I need for this version
#Just a bunch of math algorithms it works fine
def _setOptions(self):
#Here I can overwrite or change settings specific for this version
def _draw(self):
self.ax.bar(self.data[0], self.data[1], width=1, color='red')
self._setOptions()
def hide(self):
###How do I remove the ax without affecting the figure?
def show(self):
###If I want to see this version again I don't want the cost of redrawing
class Version2():
def __init__(self, ax, data):
self.ax = ax #This is a Axes object
self.data = self._formatDataV1(data)
self._draw()
self._setOptions()
self.hide()
def _formatDataV2(self, data):
#The data is manipulated differently here to extract new information
def _setOptions(self):
#These options are specific to the version2 to get the display right
def _draw(self): #Drawing a different version of the graphic with differently formated data
self.ax.plot(self.data[0], self.data[1])
self._setOptions()
def hide(self):
###How do I remove the ax without affecting the figure?
def show(self):
###If I want to see this version again I don't want the cost of redrawing
class MyGraphic(tk.LabelFrame):
def __init__(self, root, data, **options):
#I use the labelframe only as a container to make things pretty
tk.LabelFrame.__init__(self, root, text="My 1337 graphic : ", **options)
self.data = data
self.fig = mpl.figure.Figure()
self.ax = self.fig.add_subplot(111)
self._drawCanvas() #This is just for Tkinter compatibility
self.my_versions = {}
self.my_versions.update({'v1' : Version1(self.ax, self.data)})
self.my_versions.update({'v2' : Version2(self.ax, self.data)})
def _drawCanvas(self):
self.canvas = FigureCanvasTkAgg(self.figure, master=self)
self.canvas.show()
self.canvas.get_tk_widget().grid()
self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def _setOptions(self, **kwargs):
#Here I can set options common to both versions of the graphic
def showGraphic(self, graphic_version):
for i, item in enumerate(self.my_versions):
item.hide()
if graphic_version == 1:
self.my_versions['v1'].show()
elif graphic_version == 2:
self.my_versions['v2'].show()
self._setOptions()
Sorry for the lengthy post but I rather include too many details and edit out those who are not necessary when it's solved.
Basically I want to be able to hide and show different ax on the same figure depending on the choice made by my user. The missing parts of the puzzle are myGraphic.show() and myGraphic.hide().
I'm also a complete matplotlib newby I tried this design because it seemed clear and easy to implement additional versions when needed but design inputs are also really appreciated.
You can remove axes from a figure with figure.delaxes (and add with add_axes):
http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.delaxes
I managed to solve it with figure.clear() and figure.add_axes(ax)
I'll try to edit a clean question-answer tomorrow when I have time with a minimal example of how to toggle different version of a plot on the same figure in Tkinter.
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.