I have a separate plot thread with matplotlib and multiprocesses. Now if I interactively zoom in to the window, autoscale_view() does not work anymore (fixed with using autoscale()). But the "home" button in the Toolbox is still not working: It seems to call autoscale_view() and does not show the updated view but the old view (at point when zoomed in). Example code:
import matplotlib
matplotlib.use("qt4agg")
from matplotlib import pyplot as plt
import multiprocessing
def reset_view():
plt.get
xdata = []
ydata = []
temp = 0
test_ax = plt.gca()
test_line, = plt.plot(xdata, ydata)
plt.show(block=False)
for i in range(10):
temp = temp+1
xdata.append(temp)
ydata.append(temp)
test_line.set_data(xdata, ydata)
test_ax.relim()
test_ax.autoscale(tight= False)
plt.show(block=False)
plt.pause(2)
plot_thread = multiprocessing.Process(target = reset_view, args = ())
reset_view()
if __name__ == '__main__':
plot_thread.start()
Try zooming in during plotting and pressing the Home Button after. Is there a way to either make the home button use autoscale() instead of autoscale_view() or reset & update the toolbar history, so that it doesn't jump back to old views?
P.s.: "Home"-button = reset original view
I finally was able to figure it out by trial & error. There is a function to update the toolbar. This updates the toolbar's history and sets the home() function to the new view. Solution:
figure = plt.gcf() #Get current figure
toolbar = figure.canvas.toolbar #Get the toolbar handler
toolbar.update() #Update the toolbar memory
plt.show(block = False) #Show changes
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
Can I remove a matplotlib artist such as a patch by using blitting?
"""Some background code:"""
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
self.figure = Figure()
self.axes = self.figure.add_subplot(111)
self.canvas = FigureCanvas(self, -1, self.figure)
To add a patch to a matplotlib plot by using blit you can do the following:
"""square is some matplotlib patch"""
self.axes.add_patch(square)
self.axes.draw_artist(square)
self.canvas.blit(self.axes.bbox)
This works. However, can I use blit to remove this same artist from the plot?
I managed to remove it with square.remove()and I can update the plot with the self.canvas.draw() function. But ofcourse this is slow and I would like to instead make use of blitting.
"""square is some matplotlib patch"""
square.remove()
self.canvas.draw()
The following does not work:
square.remove()
self.canvas.blit(self.axes.bbox)
The idea to remove a blitted object would be to just blit the same region again, just not draw the object beforehands. You may well also remove it, such that it would also not be seen if the canvas is redrawn for any other reason.
It seems in the code from the question you have forgotten to call restore_region. For a complete set of what commands are needed for blitting, see e.g. this question.
An example would be the following, where the rectangle is shown if you click the left mouse button and is removed if you hit the right button.
import matplotlib.pyplot as plt
import numpy as np
class Test:
def __init__(self):
self.fig, self.ax = plt.subplots()
# Axis with large plot
self.ax.imshow(np.random.random((5000,5000)))
# Draw the canvas once
self.fig.canvas.draw()
# Store the background for later
self.background = self.fig.canvas.copy_from_bbox(self.ax.bbox)
# create square
self.square = plt.Rectangle([2000,2000],900,900, zorder=3, color="crimson")
# Create callback to mouse movement
self.cid = self.fig.canvas.callbacks.connect('button_press_event',
self.callback)
plt.show()
def callback(self, event):
if event.inaxes == self.ax:
if event.button == 1:
# Update point's location
self.square.set_xy((event.xdata-450, event.ydata-450))
# Restore the background
self.fig.canvas.restore_region(self.background)
# draw the square on the screen
self.ax.add_patch(self.square)
self.ax.draw_artist(self.square)
# blit the axes
self.fig.canvas.blit(self.ax.bbox)
else:
self.square.remove()
self.fig.canvas.restore_region(self.background)
self.fig.canvas.blit(self.ax.bbox)
tt = Test()
I have a program which shows an image (fig 1). When the image is clicked it shows the colour in the image that was clicked in a separate Matplotlib window (fig 2). Fig 2 has some buttons that call different functions when they are clicked.
My problem is that the functions that are meant to be called in fig 2 are being called when fig 1 is clicked.
The code looks like this:
def show_fig1(img):
# Plot the image
plt.figure(1)
ax = plt.gca()
fig = plt.gcf()
implot = ax.imshow(img)
# Detect a click on the image
cid = fig.canvas.mpl_connect('button_press_event', on_pixel_click)
plt.show(block=True)
# Called when fig1 is clicked
def on_pixel_click(event):
if event.xdata != None and event.ydata != None:
# Do some computation here that gets the image for fig2
img = get_fig2_img()
show_fig2(img, event)
def show_fig2(img, event):
plt.figure(2)
plt.imshow(img)
# Specify coordinates of the button
ax = plt.axes([0.0, 0.0, 0.2, 0.1])
# Add the button
button = Button(ax, 'button')
# Detect a click on the button
button.on_clicked(test())
plt.show(block=True)
def test():
print "Button clicked"
So test() is called instantly when on_pixel_click() is called even though theoretically it should wait until the button is clicked because of the button.on_clicked() command.
Any help?
Thanks in advance :)
On this line:
button.on_clicked(test())
You are telling Python to execute your test function, rather than just passing a reference to it. Remove the brackets and it should sort it:
button.on_clicked(test)
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.