I'm trying to embed an animation graph into my GUI, however, whenever I try what i've seen according to tutorials regarding the use of canvas, I get a blank tkinter screen.
No errors, nothing.
However, when I use plt.show, it works fine but its not confined to the GUI. Which is the problem.
Its probably a quick fix, but this is what I have.
Any help would be appreciated!
class popupWindowOscil(tk.Frame):
def __init__(self,master,ser):
OscilTop= self.OscilTop= Toplevel(master)
tk.Frame.__init__(self)
self.ser = ser
self.fig = plt.figure()
self.ax = self.fig.add_subplot(1, 1, 1)
self.xs = []
self.ys = []
self.xval =0
self.OscilLoop()
def OscilLoop(self):
ani = animation.FuncAnimation(self.fig, self.Oscilliscope, fargs=(self.xs, self.ys))
#self.canvas = FigureCanvasTkAgg(self.fig, self)
#self.canvas.draw()
#self.canvas.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
plt.show()
return
# The function that creates the values for the plot.
def Oscilliscope(self,i,xs,ys):
tryagain =1
while tryagain == 1:
try:
reading = self.ser.readline().decode()
tryagain = 0
except UnicodeDecodeError:
pass
Incominglist = str(reading).split(",")
try:
numbers = [float(x) for x in Incominglist]
except ValueError:
print ('Failure during string decode, Restart and Try again')
return
# Add x and y to lists
self.ys.extend(numbers)
for val in range(len(Incominglist)):
if self.xval == 0 and val ==0:
self.xs.append(self.xval) # or any arbitrary update to your figure's data
else:
self.xval += 0.005
self.xs.append(self.xval)
# Draw x and y lists
self.ax.clear()
self.ax.plot(self.xs, self.ys)
# Format plot
self.ax.yaxis.set_ticks(np.arange(0,5,0.25))
plt.subplots_adjust(bottom=0.30)
Related
How can I go down to one plot (axes) without getting the error "'AxesSubplot' object is not subscriptable" I tried changing this line <self.fig, axes = plt.subplots(2, 1, figsize=(11,7))> to <self.fig, axes = plt.subplots(1, 1, figsize=(11,7))>.....but I got the error.
Any help would be truly appreciated.
Just some background, this is a live EEG (Biosensor) Stream.
#############3 Create matplotlib plots
#self.figure = Figure() ???
self.fig, axes = plt.subplots(2, 1, figsize=(11,7)) #2 or 1?
prop_cycle = plt.rcParams['axes.prop_cycle'] #CHANGED
colors = prop_cycle.by_key()['color']
# Set the Axes Instance
# Add brainflow waveform and FFT
self.wave_ax = axes[0] #Location 1
# Set titles
self.wave_ax.set_title("Cyton Waveform") #Title
# Create line objects whose data we update in the animation
self.lines = [x[0] for x in
[ax.plot(self.data, self.data, color=colors[i]) for i,ax in enumerate(axes)]]
# Start animation
self.fig.tight_layout() #Creates spaces between titles :)
self.ani = matplotlib.animation.FuncAnimation(self.fig, self.updateFig,
interval=5, blit=True) #Animation
# Create list of objects that get animated
self.ani_objects = [self.wave_ax]
################################################# Animation helpers
def updateVars(self): #THIS IS WHERE YOU PICK THE FIRST CHANNEL
### What's the latest?? # Using the first channel
all_data = self.board.get_current_board_data(self.display_window)
self.curData = all_data[self.board_channel, :]
if (len(self.curData) < self.display_window):
self.data = self.curData
return
################################## Animation function
def updateFig(self, *args):
now = time.time()
# Update data from the brainflow
self.updateVars()
if (len(self.data) == self.display_window):
### Update plots with new data
self.lines[0].set_data(self.waveX, self.data)
else: # Set filler waveform data if the buffer hasn't filled yet
self.lines[0].set_data(list(range(len(self.data))), self.data)
## Reset limits of the waveform plot so it looks nice
self.wave_ax.relim()
self.wave_ax.autoscale_view(tight=True)
self.propagateChanges()
return self.ani_objects
def propagateChanges(self):
self.fig.stale = True #'stale' and needs to be re-drawn for the output to match the internal state
self.fig.canvas.draw() # Redraw the current figure. This is used to update a figure that has been altered, but not automatically re-drawn
self.fig.canvas.flush_events() # Flush the GUI events for the figure.
global stream
'''
i = 0
while i < 5:
print("starting loop")
i += 1
'''
try:
stream = CytonStream()
plt.show()
#except KeyboardInterrupt: # Dont understand incentive behind this???
# print('\nKeyboard interrupted, ending program')
# stream.board.release_session()
except Exception as e:
print("other exception,", e)
time.sleep(1)
When you can self.fig, axes = plt.subplots(2, 1, figsize=(11,7)), axes will be a numpy array of subplots that you can index.
When you call self.fig, axes = plt.subplots(1, 1, figsize=(11,7)), axes will be a single subplot that you can not index.
f1, a1 = plt.subplots(1, 1, figsize=(11,7))
f2, a2 = plt.subplots(2, 1, figsize=(11,7))
print(type(a1)) # <class 'matplotlib.axes._subplots.AxesSubplot'>
print(type(a2)) # <class 'numpy.ndarray'>
I try to implement a matplotlib figure that updates during the simulation of my environment.
The following Classes works fine in my test but doesn't update the figure when I use it in my environment. During the simulation of the environment, the graph is shown, but no lines are plotted.
My guess is that .draw() is not working how I think it does.
Can anyone figure out the issue here?
class Visualisation:
def __init__(self, graphs):
self.graphs_dict = {}
for graph in graphs:
fig = plt.figure()
ax = fig.add_subplot(111)
line, = ax.plot(graph.x, graph.y, 'r-')
self.graphs_dict[graph.title] = {"fig": fig, "ax": ax, "line": line, "graph": graph}
self.graphs_dict[graph.title]["fig"].canvas.draw()
plt.ion()
plt.show()
def update(self, graph):
graph = self.graphs_dict[graph.title]["graph"]
self.graphs_dict[graph.title]["line"].set_xdata(graph.x)
self.graphs_dict[graph.title]["line"].set_ydata(graph.y)
self.graphs_dict[graph.title]["fig"].canvas.flush_events()
x_lim, y_lim = self.get_lim(graph)
self.graphs_dict[graph.title]["ax"].set_xlim(x_lim)
self.graphs_dict[graph.title]["ax"].set_ylim(y_lim)
self.graphs_dict[graph.title]["fig"].canvas.draw()
#staticmethod
def get_lim(graph):
if graph.x_lim is None:
x = np.array(graph.x)
y = np.array(graph.y)
x_lim = [x.min(), x.max()]
y_lim = [y.min(), y.max()]
else:
x_lim = graph.x_lim
y_lim = graph.y_lim
return x_lim, y_lim
class Graph:
def __init__(self, title, x, y, x_label="", y_label=""):
"""
Sets up a graph for Matplotlib
Parameters
----------
title : String
Title of the plot
x : float
y : float
x_label : String
x Label
y_label : String
y Label
"""
self.title = title
self.x = x
self.y = y
self.x_label = x_label
self.y_label = y_label
self.x_lim, self.y_lim = None, None
def set_lim(self, x_lim, y_lim):
self.x_lim = x_lim
self.y_lim = y_lim
class Environment:
def __init__(self, [..], verbose=0):
"""verbose : int
0 - No Visualisation
1 - Visualisation
2 - Visualisation and Logging"""
self.vis = None
self.verbose = verbose
[......]
def simulate(self):
for _ in range(self.n_steps):
[...]
self.visualize()
def visualize(self):
if self.verbose == 1 or self.verbose == 2:
if self.vis is None:
graphs = [Graph(title="VariableY", x=[], y=[])]
graphs[0].set_lim(x_lim=[0, 100], y_lim=[0, 300])
self.vis = Visualisation(graphs=graphs)
else:
self.vis.graphs_dict["VariableY"]["graph"].x.append(self.internal_step)
self.vis.graphs_dict["VariableY"]["graph"].y.append(150)
self.vis.update(self.vis.graphs_dict["VariableY"]["graph"])
When I run the code I more or less just write: env.simulate().
The code runs fine here:
class TestSingularVisualisation(unittest.TestCase):
def setUp(self):
self.graph = Graph(title="Test", x=[0], y=[0])
self.vis = Visualisation(graphs=[self.graph])
class TestSingleUpdate(TestSingularVisualisation):
def test_repeated_update(self):
for i in range(5):
self.graph.x.append(i)
self.graph.y.append(np.sin(i))
self.vis.update(self.graph)
time.sleep(1)
Turns out your code works the way it is set up. Here is the sole problem with the code you provided:
self.vis.graphs_dict["VariableY"]["graph"].x.append(self.internal_step)
self.vis.graphs_dict["VariableY"]["graph"].y.append(150)
You are plotting a line and correctly updating the canvas, however, you keep appending the exact same (x, y) coordinate. So the simulation does update the line, but the line simplifies to a point. Your test case does not do this. You can run a dummy example with your code by simply adding a line like this:
self.internal_step += 5
before adding the new x point, and you will produce a horizontal line.
Let me know if this solves your problem.
Probably not the most elegant, but I use plt.pause(0.1) when I want to update plots during execution. It pauses for 0.1s and forces all plots to be actually displayed. (It work in %debug in ipython as a bonus)
So I've created several charts using the matplotlib library in python 3.5, but I want to be able to have the flexibility to utilize a button to alternate between the views I created within a single window. I've been trying to experiment with an example here, but have not succeeded in doing so. I was curious in how to have the flexibility to click through different views that I created.
My code is sort of organized like this:
def plot1(data1, 'name1'):
...
ax.plot(x,y)
plt.draw()
def plot2(data2, 'name2'):
...
ax2.plot(x,y)
plt.draw()
def plot3(data3, 'name3'):
...
ax3.plot(x,y)
plt.draw()
plot1(data1,'name1')
plot2(data2,'name2')
plot3(data3,'name3')
plt.show()
Currently it will show up in three different windows. Now when I try to make this all into one view accessible via buttons, I'm unable to do so because quite frankly I'm unfamiliar with how to pass on the variables in my methods to create my desired subplots with the callback function. Is there a way to sort of structure my code to have them all run under one matplotlib window?
The following would be a class that uses the functions that you create. Those would not actually plot anything, but provide the required data. They should be put in a list called funcs, and when you click next or prev the corresponding graph would pop up. This should get you started.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.2)
x = range(-50,50)
y = range(-50,50)
l, = plt.plot(x, y, lw=2)
ax.title.set_text('y = x')
class Index(object):
ind = 0
global funcs # used so yu can access local list, funcs, here
def next(self, event):
self.ind += 1
i = self.ind %(len(funcs))
x,y,name = funcs[i]() # unpack tuple data
l.set_xdata(x) #set x value data
l.set_ydata(y) #set y value data
ax.title.set_text(name) # set title of graph
plt.draw()
def prev(self, event):
self.ind -= 1
i = self.ind %(len(funcs))
x,y, name = funcs[i]() #unpack tuple data
l.set_xdata(x) #set x value data
l.set_ydata(y) #set y value data
ax.title.set_text(name) #set title of graph
plt.draw()
def plot1():
x = range(-20,20)
y = x
name = "y = x"
return (x,y, name)
def plot2():
x = range(-20,20)
y = np.power(x, 2)
name = "y = x^2"
return (x,y,name)
def plot3():
x = range(-20,20) # sample data
y = np.power(x, 3)
name = "y = x^3"
return (x,y, name)
funcs = [plot1, plot2, plot3] # functions in a list so you can interate over
callback = Index()
axprev = plt.axes([0.7, 0.05, 0.1, 0.075])
axnext = plt.axes([0.81, 0.05, 0.1, 0.075])
bnext = Button(axnext, 'Next')
bnext.on_clicked(callback.next)
bprev = Button(axprev, 'Previous')
bprev.on_clicked(callback.prev)
plt.show()
My problem is to resume an matplotlib ArtistsAnimation
My code is:
def PlotFields(self):
fig = plt.figure()
axess = []
for i in range(0,self.number_fields):
axess.append(fig.add_subplot((self.number_fields+1)//2,2,i+1))
#find min and max in time
mins = {}
maxs = {}
for key,field in self.fields.items():
mins[key] = field[:,:,:,self.z_slice].min()
maxs[key] = field[:,:,:,self.z_slice].max()
if mins[key] == maxs[key]:#this fixes bug in imshow when vmin = vmax
mins[key] = mins[key]-0.1
maxs[key] = maxs[key]+0.1
#set up list of images for animation
movie = []
images = []
nt = self.fields[list(self.fields)[0]].shape[0] #number of time slices
print('time slices = {}'.format(nt))
first = 1
for time in range(0,nt):
images.clear()
i = 0
for key,field in self.fields.items():
if self.cut == 'xy':
images.append(axess[i].pcolor(field[time,:,:,self.z_slice].T, vmin = mins[key], vmax = maxs[key]))
axess[i].set_xlabel('x')
axess[i].set_ylabel('y')
elif self.cut == 'xz':
images.append(axess[i].pcolor(field[time,:,:,self.y_slice].T, vmin = mins[key], vmax = maxs[key]))
axess[i].set_xlabel('x')
axess[i].set_ylabel('z')
else:
print('unknown cut --- exiting !!')
quit()
axess[i].set_title(key)
i = i + 1
if first == 1:
for i in range(0,self.number_fields):
fig.colorbar(images[i], ax = axess[i])
first = 0
# time_title.set_text('t={}'.format(t_array[time]))
time_title = axess[0].annotate('t={}'.format(self.t_array[time]),xy = (0.1,1.2))
collected_list = [*images] #apparently a bug in matplotlib forces this solution
collected_list.append(time_title)
movie.append(collected_list)
#for i in range(0,number_fields):
# fig.colorbar(images[i], ax = axess[i])
#run animation
self.ani = anim.ArtistAnimation(fig, movie, interval=500, blit = False, repeat_delay = 1000)
fig.canvas.mpl_connect('button_press_event', self.onClick)
if self.save_movie == True:
try:
ani.save('xy_film.mp4')
#ani.save('film.mp4',writer = FFwriter, fps=30, extra_args=['-vcodec', 'libx264'])
except Exception:
print("Save failed: Check ffmpeg path")
plt.show()
def onClick(self, event):
self.pause == True
if self.pause == False:
self.ani._stop()
self.pause = True
print('self pause = False')
else:
self.ani._start()
self.pause = False
#pause ^= True
print('self pause = True')
The animation stop onClick but throws the following error on the second click which is supposed to resume the animation (if possible):
File "PlotFieldsFieldAligned.py", line 149, in onClick
self.ani._start()
File "/home/yolen/scicomp/anaconda3/lib/python3.5/site- packages/matplotlib/animation.py", line 665, in _start
self.event_source.add_callback(self._step)
AttributeError: 'NoneType' object has no attribute 'add_callback'
Any help appreciated:-D
Although it seems that in this case a FuncAnimation might be better suited than an ArtistAnimation, both can be
stopped / started the same way. See this question stop / start / pause in python matplotlib animation.
The main point is that the 'private' ArtistAnimation._start() function is not doing what you think it does. Therefore it is sensible to use the ArtistAnimation.event_source.stop() and ArtistAnimation.event_source.start() functions.
Below is a minimal, runnable example showing how to start/stop an ArtistAnimation by clicking with the mouse button.
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation
class SomeClass():
def __init__(self):
self.pause = False
self.fig, ax = plt.subplots()
ax.set_aspect("equal")
self.movie = []
nt = 10
X,Y = np.meshgrid(np.arange(16), np.arange(16))
for t in range(nt):
data = np.sin((X+t*1)/3.)**2+ 1.5*np.cos((Y+t*1)/3.)**2
pc = ax.pcolor(data)
self.movie.append([pc])
self.ani = animation.ArtistAnimation(self.fig, self.movie, interval=100)
self.fig.canvas.mpl_connect('button_press_event', self.onClick)
plt.show()
def onClick(self, event):
if self.pause:
self.ani.event_source.stop()
else:
self.ani.event_source.start()
self.pause ^= True
a = SomeClass()
I am trying to write a small bit of code that interactively deletes selected slices in an image series using matplotlib. I have created a button 'delete' which stores a number of indices to be deleted when the button 'update' is selected. However, I am currently unable to reset the range of my slider widget, i.e. removing the number of deleted slices from valmax. What is the pythonic solution to this problem?
Here is my code:
import dicom
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button
frame = 0
#store indices of slices to be deleted
delete_list = []
def main():
data = np.random.rand(16,256,256)
nframes = data.shape[0]
raw_dicom_stack = []
for x in range (nframes):
raw_dicom_stack.append(data[x,:,:])
#yframe = 0
# Visualize it
viewer = VolumeViewer(raw_dicom_stack, nframes)
viewer.show()
class VolumeViewer(object):
def __init__(self, raw_dicom_stack, nframes):
global delete_list
self.raw_dicom_stack = raw_dicom_stack
self.nframes = nframes
self.delete_list = delete_list
# Setup the axes.
self.fig, self.ax = plt.subplots()
self.slider_ax = self.fig.add_axes([0.2, 0.03, 0.65, 0.03])
self.delete_ax = self.fig.add_axes([0.85,0.84,0.1,0.04])
self.update_ax = self.fig.add_axes([0.85,0.78,0.1,0.04])
self.register_ax = self.fig.add_axes([0.85,0.72,0.1,0.04])
self.add_ax = self.fig.add_axes([0.85,0.66,0.1,0.04])
# Make the slider
self.slider = Slider(self.slider_ax, 'Frame', 1, self.nframes,
valinit=1, valfmt='%1d/{}'.format(self.nframes))
self.slider.on_changed(self.update)
#Make the buttons
self.del_button = Button(self.delete_ax, 'Delete')
self.del_button.on_clicked(self.delete)
self.upd_button = Button(self.update_ax, 'Update')
self.upd_button.on_clicked(self.img_update)
self.reg_button = Button(self.register_ax, 'Register')
self.add_button = Button(self.add_ax, "Add")
# Plot the first slice of the image
self.im = self.ax.imshow(np.array(raw_dicom_stack[0]))
def update(self, value):
global frame
frame = int(np.round(value - 1))
# Update the image data
dat = np.array(self.raw_dicom_stack[frame])
self.im.set_data(dat)
# Reset the image scaling bounds (this may not be necessary for you)
self.im.set_clim([dat.min(), dat.max()])
# Redraw the plot
self.fig.canvas.draw()
def delete(self,event):
global frame
global delete_list
delete_list.append(frame)
print 'Frame %s has been added to list of slices to be deleted' %str(frame+1)
print 'Please click update to delete these slices and show updated image series \n'
#Remove duplicates from delete list
def img_update(self,event):
#function deletes image stacks and updates viewer
global delete_list
#Remove duplicates from list and sort into numerical order
delete_list = list(set(delete_list))
delete_list.sort()
#Make sure delete_list is not empty
if not delete_list:
print "Delete list is empty, no slices to delete"
#Loop through delete list in reverse numerical order and remove slices from series
else:
for i in reversed(delete_list):
self.raw_dicom_stack.pop(i)
print 'Slice %i removed from dicom series \n' %(i+1)
#Can now remove contents from delete_list
del delete_list[:]
#Update slider range
self.nframes = len(self.raw_dicom_stack)
def show(self):
plt.show()
if __name__ == '__main__':
main()
In order to update a slider range you may set the min and max value of it directly,
slider.valmin = 3
slider.valmax = 7
In order to reflect this change in the slider axes you need to set the limits of the axes,
slider.ax.set_xlim(slider.valmin,slider.valmax)
A complete example, where typing in any digit changes the valmin of the slider to that value.
import matplotlib.pyplot as plt
import matplotlib.widgets
fig, (ax,sliderax) = plt.subplots(nrows=2,gridspec_kw=dict(height_ratios=[1,.05]))
ax.plot(range(11))
ax.set_xlim(5,None)
ax.set_title("Type number to set minimum slider value")
def update_range(val):
ax.set_xlim(val,None)
def update_slider(evt):
print(evt.key)
try:
val = int(evt.key)
slider.valmin = val
slider.ax.set_xlim(slider.valmin,None)
if val > slider.val:
slider.val=val
update_range(val)
fig.canvas.draw_idle()
except:
pass
slider=matplotlib.widgets.Slider(sliderax,"xlim",0,10,5)
slider.on_changed(update_range)
fig.canvas.mpl_connect('key_press_event', update_slider)
plt.show()
It looks like the slider does not have a way to update the range (api). I would suggest setting the range of the slider to be [0,1] and doing
frame = int(self.nframes * value)
On a somewhat related note, I would have made frame an instance variable a data attribute instead of a global variable (tutorial).