How to plot a variable from Tkinter program in real-time automatically - matplotlib

I have a variable, current_value in my Tkinter program. The value of the variable can be increased or decreased using 2 corresponding buttons. I want to plot this variable every 1 second using Matplotlib. So far I've managed to plot the value whenever I press a button, which means that nothing is plotted if I don't press any button. How can I plot the value automatically? (or how could I update the list of values automatically?)
Here is my code so far:
from tkinter import *
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import *
from matplotlib.figure import *
from matplotlib import style
import matplotlib.animation as animation
style.use("ggplot")
# Graph
f = Figure(figsize=(5,2), dpi=70)
# Add subplot
a = f.add_subplot(111)
# List that stores value of variable everytime a button is pressed
val_list = []
def animate(i):
a.clear()
a.plot(val_list) # Plots the values according to list
class Root(Tk):
def __init__(self):
super(Root, self).__init__()
# Increase value button
self.val_incr_btn = Button(self, text="Temp + 1°C", command=self.incr_val)
self.val_incr_btn.place(x=250, y=35)
# Decrease value button
self.val_decr_btn = Button(self, text="Temp - 1°C", command=self.decr_val)
self.val_decr_btn.place(x=330, y=35)
# Displaying the value of the variable as a number
self.current_value = 23 # Actual value of variable
self.value = Label(self, text=str(self.current_value)) # Label that displays the current value
self.value.place(x=150, y=40)
# Bring up canvas
canvas = FigureCanvasTkAgg(f, self)
canvas.draw()
canvas.get_tk_widget().place(x=450, y=0)
def incr_val(self):
value = float(self.current_value) # Convert temp into float
self.value["text"] = f"{value + 1}"
self.current_value += 1
val_list.append(self.current_value)
return self.current_value
def decr_val(self):
value = float(self.current_value) # Convert temp into float
self.value["text"] = f"{value - 1}"
self.current_value -= 1
val_list.append(self.current_value)
return self.current_value
root = Root()
ani = animation.FuncAnimation(f, animate, interval=100)
root.mainloop()

Just move the val_list.append(self.current_value) line into the animate function.
# List that stores value of variable everytime a button is pressed
val_list = []
def animate(i):
a.clear()
val_list.append(root.current_value)
a.plot(val_list) # Plots the values according to list
You may want to use a deque instead of a list, for a "rolling" effect.
from collections import deque
val_list = deque([], maxlen=200) # start scrolling after 200 datapoints

Related

Matplotlib transparent background without save() function

I have this kind of an animation and I want to integrate it to my GUI.
here is the plot
But, the background color is set to black right now. Here is the code. I am using Windows 10 and for GUI I am mostly using PyQt6 but for the matplotlib I used mlp.use("TkAgg") because it didn't create output if I dont use TkAgg.
I want to make it transparent. I only want the curves. I searched on the internet but everything is about save() function. Isn't there another solution for this? I don't want to save it, I am using animation, therefore it should be transparent everytime, not in a image.
import queue
import sys
from matplotlib.animation import FuncAnimation
import PyQt6.QtCore
import matplotlib as mlp
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
as FigureCanvas
mlp.use("TkAgg")
import matplotlib.pyplot as plt
import numpy as np
import sounddevice as sd
plt.rcParams['toolbar'] = 'None'
plt.rcParams.update({
"figure.facecolor": "black", # red with alpha = 30%
})
# Lets define audio variables
# We will use the default PC or Laptop mic to input the sound
device = 0 # id of the audio device by default
window = 1000 # window for the data
downsample = 1 # how much samples to drop
channels = [1] # a list of audio channels
interval = 40 # this is update interval in miliseconds for plot
# lets make a queue
q = queue.Queue()
# Please note that this sd.query_devices has an s in the end.
device_info = sd.query_devices(device, 'input')
samplerate = device_info['default_samplerate']
length = int(window*samplerate/(1000*downsample))
plotdata = np.zeros((length,len(channels)))
# next is to make fig and axis of matplotlib plt
fig,ax = plt.subplots(figsize=(2,1))
fig.subplots_adjust(0,0,1,1)
ax.axis("off")
fig.canvas.manager.window.overrideredirect(1)
# lets set the title
ax.set_title("On Action")
# Make a matplotlib.lines.Line2D plot item of color green
# R,G,B = 0,1,0.29
lines = ax.plot(plotdata,color = "purple")
# We will use an audio call back function to put the data in
queue
def audio_callback(indata,frames,time,status):
q.put(indata[::downsample,[0]])
# now we will use an another function
# It will take frame of audio samples from the queue and update
# to the lines
def update_plot(frame):
global plotdata
while True:
try:
data = q.get_nowait()
except queue.Empty:
break
shift = len(data)
plotdata = np.roll(plotdata, -shift,axis = 0)
# Elements that roll beyond the last position are
# re-introduced
plotdata[-shift:,:] = data
for column, line in enumerate(lines):
line.set_ydata(plotdata[:,column])
return lines
# Lets add the grid
ax.set_yticks([0])
# ax.yaxis.grid(True)
""" INPUT FROM MIC """
stream = sd.InputStream(device = device, channels = max(channels),
samplerate = samplerate, callback = audio_callback)
""" OUTPUT """
ani = FuncAnimation(fig,update_plot,interval=interval,blit=True, )
plt.get_current_fig_manager().window.wm_geometry("200x100+850+450")
with stream:
plt.show()

Python MATPLOTLIB ANIMATION without the use of Global Variables?

QUESTION: Whats the cleanest and simplest way to use Python's MATPLOTLIB animation function without the use of global array's or constantly appending a global "list of data points" to a plot?
Here is an example of a animated graph that plots the bid and ask sizes of a stock ticker. In this example the variables time[], ask[], and bid[] are used as global variables.
How do we modify the matplotlib animate() function to not use global variables?
so I'm trying to remove "all" global variables and just run one function call...
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
from random import randint
stock = {'ask': 12.82, 'askSize': 21900, 'bid': 12.81, 'bidSize': 17800}
def get_askSize():
return stock["askSize"] + randint(1,9000) # grab a random integer to be the next y-value in the animation
def get_bidSize():
return stock["bidSize"] + randint(1,9000) # grab a random integer to be the next y-value in the animation
def animate(i):
pt_ask = get_askSize()
pt_bid = get_bidSize()
time.append(i) #x
ask.append(pt_ask) #y
bid.append(pt_bid) #y
ax.clear()
ax.plot(time, ask)
ax.plot(time, bid)
ax.set_xlabel('Time')
ax.set_ylabel('Volume')
ax.set_title('ask and bid size')
ax.set_xlim([0,40])
#axis = axis_size(get_bidSize, get_askSize)
ylim_min = (get_askSize() + get_bidSize())/6
ylim_max = (get_askSize() + get_bidSize())
ax.set_ylim([ylim_min,ylim_max])
# create empty lists for the x and y data
time = []
ask = []
bid = []
# create the figure and axes objects
fig, ax = plt.subplots()
# run the animation
ani = FuncAnimation(fig, animate, frames=40, interval=500, repeat=False)
plt.show()
As #Warren mentioned, you can use the fargs parameter to pass in shared variables to be used in your animation function.
You should also precompute all of your points, and then use your frames to merely act as an expanding window on those frames. This will be a much more performant solution and prevents you from needing to convert between numpy arrays and lists on every tick of your animation in order to update the underlying data for your lines.
This also enables you to precompute your y-limits to prevent your resultant plot from jumping all over the place.
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
rng = np.random.default_rng(0)
def animate(i, ask_line, bid_line, data):
i += 1
x = data['x'][:i]
ask_line.set_data(x, data['ask'][:i])
bid_line.set_data(x, data['bid'][:i])
stock = {'ask': 12.82, 'askSize': 21900, 'bid': 12.81, 'bidSize': 17800}
frames = 40
data = {
'x': np.arange(0, frames),
'ask': stock['askSize'] + rng.integers(0, 9000, size=frames),
'bid': stock['bidSize'] + rng.integers(0, 9000, size=frames),
}
fig, ax = plt.subplots()
ask_line, = ax.plot([], [])
bid_line, = ax.plot([], [])
ax.set(xlabel='Time', ylabel='Volume', title='ask and bid size', xlim=(0, 40))
ax.set_ylim(
min(data['ask'].min(), data['bid'].min()),
max(data['ask'].max(), data['bid'].max()),
)
# run the animation
ani = FuncAnimation(
fig, animate, fargs=(ask_line, bid_line, data),
frames=40, interval=500, repeat=False
)
plt.show()
You can use the fargs parameter of FuncAnimation to provide additional arguments to your animate callback function. So animate might start like
def animate(i, askSize, bidSize):
...
and in the call of FuncAnimation, you would add the parameter fargs=(askSize, bidSize). Add whatever variables (in whatever form) that you need to make available within the animate function.
I use this in my example of the use of FuncAnimation with AnimatedPNGWriter in the package numpngw; see Example 8. In that example, my callback function is
def update_line(num, x, data, line):
"""
Animation "call back" function for each frame.
"""
line.set_data(x, data[num, :])
return line,
and FuncAnimation is created with
ani = animation.FuncAnimation(fig, update_line, frames=len(t),
init_func=lambda : None,
fargs=(x, sol, lineplot))
You are using animation wrong, as you are adding and removing lines at each iteration, which makes the animation a lot slower. For line plots, the best way to proceed is:
initialize the figure and axes
initialize empty lines
inside the animate function, update the data of each line.
Something like this:
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
from random import randint
stock = {'ask': 12.82, 'askSize': 21900, 'bid': 12.81, 'bidSize': 17800}
def get_askSize():
return stock["askSize"] + randint(1,9000) # grab a random integer to be the next y-value in the animation
def get_bidSize():
return stock["bidSize"] + randint(1,9000) # grab a random integer to be the next y-value in the animation
def add_point_to_line(x, y, line):
# retrieve the previous data in the line
xd, yd = [list(t) for t in line.get_data()]
# append the new point
xd.append(x)
yd.append(y)
# set the new data
line.set_data(xd, yd)
def animate(i):
pt_ask = get_askSize()
pt_bid = get_bidSize()
# append a new value to the lines
add_point_to_line(i, pt_ask, ax.lines[0])
add_point_to_line(i, pt_bid, ax.lines[1])
# update axis limits if necessary
ylim_min = (get_askSize() + get_bidSize())/6
ylim_max = (get_askSize() + get_bidSize())
ax.set_ylim([ylim_min,ylim_max])
# create the figure and axes objects
fig, ax = plt.subplots()
# create empty lines that will be populated on the animate function
ax.plot([], [])
ax.plot([], [])
ax.set_xlabel('Time')
ax.set_ylabel('Volume')
ax.set_title('ask and bid size')
ax.set_xlim([0,40])
# run the animation
ani = FuncAnimation(fig, animate, frames=40, interval=500, repeat=False)
plt.show()

How to embed an interactive matplotlib widget in a Class using %matplotlib notebook

I've written an interactive GUI with the Matplotlib module widgets that runs directly inside my Jupyter Notebook using %matplotlib notebook (i.e. it does not open a separate window for the GUI as with qt or tk, but runs embedded within the notebook, similar to static plots with %matplotlib inline). The purpose of the GUI is to accept a list of images (2D numpy arrays, displayed using plt.imshow()), display them, and use a button to flip back and forth between images in the list.
I have written a block of code that works just fine based on the canonical example, but it fails when I try to wrap the whole thing within a Class.
My end goal is to execute the GUI with a command like:
GUI_object = interactive_plot_class(img_list), where img_list is the list of images.
Here I've given an example of the codeblock below, which works fine:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
from functools import partial
###############################
# CLASSES
# define a class to keep count of the index being displayed
class Counter:
def __init__(self, initial=0):
self.value = initial
def increment_back(self, amount=1):
self.value -= amount
return self.value
def increment_fwd(self, amount=1):
self.value += amount
return self.value
###############################
# GUI FUNCTIONS
# define function to update plot
def update_plot(counter,img):
ax1.imshow(img, cmap=plot_dict['cmap'], vmin=plot_dict['vmin'], vmax=plot_dict['vmax'], origin='lower', interpolation='nearest')
ax1.set_title('Index: {}'.format(counter.value))
fig.canvas.draw()
# define function for iterating backward through image list
def on_click_prev(button,counter=0):
if counter.value == 0: #minor error handling
pass
else:
counter.increment_back()
img_temp = img_list[counter.value]
update_plot(counter,img_temp)
# define function for iterating forward through image list
def on_click_next(button,counter=0):
if counter.value == (n_imgs-1): #minor error handling
pass
else:
counter.increment_fwd()
img_temp = img_list[counter.value]
update_plot(counter,img_temp)
###############################
# create random images stored in numpy arrays
img_size = 55
n_imgs = 20
img_list = [np.random.random((img_size,img_size)) for x in range(n_imgs)]
# define a dict to store parameters for plotting
plot_dict = {'cmap': 'gray_r', 'vmin': 0., 'vmax':1}
# define counter to track index
counter = Counter()
# Plotting
fig = plt.figure(figsize=(10,8))
ax1 = fig.subplots(1,1)
plt.subplots_adjust(left = 0.4, bottom = 0.3)
#-------BUTTONS--------
# create axes: [xposition, yposition, width, height]
ax_button_next = plt.axes([0.12, 0.05, 0.1, 0.10])
ax_button_prev = plt.axes([0.02, 0.05, 0.1, 0.10])
# properties of the button
next_button = Button(ax_button_next, 'Next', color='white', hovercolor='gainsboro')
prev_button = Button(ax_button_prev, 'Prev', color='white', hovercolor='gainsboro')
# triggering event is the clicking
next_button.on_clicked(partial(on_click_next,counter=counter))
prev_button.on_clicked(partial(on_click_prev,counter=counter))
# define an initial image to display
img_temp = img_list[counter.value].copy()
# display initial image
ax1.imshow(img_temp, cmap=plot_dict['cmap'], vmin=plot_dict['vmin'], vmax=plot_dict['vmax'], origin='lower', interpolation='nearest')
ax1.set_title('Index: {}'.format(counter.value))
plt.show(fig)
This little snippet of code works and does exactly what I want it to do. My problem is that when I try to re-write it inside of a class, nothing happens (i.e. after the initial image is displayed, the buttons are non-responsive and do not call the callback function. In fact, they don't do anything).
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
from functools import partial
class interactive_plot_class():
### SUB-CLASSES
# define a class to keep count of the index being displayed
class Counter:
def __init__(self, initial=0):
self.value = initial
def increment_back(self, amount=1):
self.value -= amount
return self.value
def increment_fwd(self, amount=1):
self.value += amount
return self.value
### FUNCTIONS
# define initial state of class
def __init__(self, filter_dict):
self.img_list = filter_dict['images']
self.Index = self.Counter()
self.cmap = 'gray'
self.vmin = 0.
self.vmax = 1
#-------FIGURE--------
# create figure to display image
self.fig = plt.figure(figsize=(10,8))
# create axis for image display
self.ax1 = plt.axes([0.4, 0.3, 0.55, 0.55])
self.disp_img = self.img_list[self.Index.value]
self.ax1.imshow(self.disp_img,cmap=self.cmap, vmin=self.vmin, vmax=self.vmax, origin='lower', interpolation='nearest')
self.ax1.set_title('Index: {}'.format(self.Index.value))
#-------BUTTONS--------
# create axes for buttons: [xposition, yposition, width, height]
self.ax_button_next = plt.axes([0.12, 0.05, 0.1, 0.10])
self.ax_button_prev = plt.axes([0.02, 0.05, 0.1, 0.10])
# properties of the button
self.next_button = Button(self.ax_button_next, 'Next', color='white', hovercolor='gainsboro')
self.prev_button = Button(self.ax_button_prev, 'Prev', color='white', hovercolor='gainsboro')
# assign callback function, triggering event is the clicking
self.prev_button.on_clicked(self.on_click_prev)
self.next_button.on_clicked(self.on_click_next)
# display the figure interactively
self.fig.canvas.draw()
# GUI FUNCTIONS
# define function to update plot
def update_plot(img):
self.ax1.imshow(img, cmap=self.cmap, vmin=selfvmin, vmax=self.vmax, origin='lower', interpolation='nearest')
self.ax1.set_title('Index: {}'.format(self.Index.value))
self.fig.canvas.draw()
# define function for iterating forward through image list
def on_click_next(button_event):
if self.Index.value == (len(self.img_list)-1): #minor error handling
pass
else:
self.Index.increment_fwd()
self.disp_img = self.img_list[self.Index.value]
self.update_plot(self.disp_temp)
# define function for iterating backward through image list
def on_click_prev(button_event):
if self.Index.value == 0: #minor error handling
pass
else:
self.Index.increment_back()
self.disp_img = self.img_list[self.Index.value]
self.update_plot(self.disp_img)
###############
# END OF CLASS
###############
img_size = 55
n_imgs = 20
imgs = [np.random.random((img_size,img_size)) for x in range(n_imgs)]
# store relevant fields to intialize GUI
filter_dict['images'] = imgs
GUI_object = interactive_plot_class(filter_dict)
When I run this second block of code, the buttons don't do anything, nor do they increment the value of the Index counter.
Any help would be most appreciated.

Is it possible to recreate a pyqtgraph without data

I have a pyqt5 based application where I open a file and plot 2 different plots based on the data from the file. Now I want to open another similar file, but I want to save the status/view of the 2 plots, so that I could come quickly back to the previous plots/views, without having to plot it again by reading the data. Is it possible at all to save the state/view of the plots to recreate it very quickly?
You can guide from this answer .
Basically, you can use the PlotWidget class from pyqtgraph.
Generate a PlotWidget.
import pyqtgraph as pg
plot_widget = pg.PlotWidget()
Use the plot() method and store the PlotDataItem in a variable. The PlotDataItem will contain the information of that specific plot: x-data, y-data, the color of the line, the line width, ...
plot_item = plot_widget.plot(xData, yData)
With this you can add/remove the item from the plot every time you want with the addItem() and removeItem() methods
plot_widget.removeItem(plot_item)
plot_widget.addItem(plot_item)
EDIT:
To get the view state of the plot, you can use the viewRect() method of the PlotWidget class. It will return a QRectF object which contains the information of the view state like this:
PyQt5.QtCore.QRectF(x_0, y_0, w, h)
Where:
x_0 and y_0 are the coordinates where the view starts.
w and h are the width and height of the view area.
Also, you can restore the view using the setRange() method of the PlotWidget class.
Example:
Here is an example of the implementation of this:
import sys
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui
class MyApp(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.central_layout = QtGui.QVBoxLayout()
self.buttons_layout = QtGui.QVBoxLayout()
self.boxes_layout = QtGui.QHBoxLayout()
self.save = QtGui.QPushButton('Save View')
self.set = QtGui.QPushButton('Set View')
self.boxes = [QtGui.QCheckBox(f"Box {i+1}") for i in range(3)]
self.plot_widget = pg.PlotWidget()
self.plot_data = [None for _ in range(3)]
self.state = [False for _ in range(3)]
self.setLayout(self.central_layout)
self.central_layout.addWidget(self.plot_widget)
self.central_layout.addLayout(self.buttons_layout)
self.buttons_layout.addLayout(self.boxes_layout)
self.buttons_layout.addWidget(self.save)
self.buttons_layout.addWidget(self.set)
for i in range(3):
self.boxes_layout.addWidget(self.boxes[i])
self.boxes[i].stateChanged.connect(self.box_changed)
self.create_data()
self.save.clicked.connect(self.save_view)
self.set.clicked.connect(self.set_view)
self.view_state = None
self.save_view()
def create_data(self):
x = np.linspace(0, 3.14, 100)
y = [np.sin(x), np.cos(x), np.sin(x)**2]
for i in range(3):
self.plot_data[i] = pg.PlotDataItem(x, y[i])
def box_changed(self):
for i in range(3):
if self.boxes[i].isChecked() != self.state[i]:
self.state[i] = self.boxes[i].isChecked()
if self.state[i]:
if self.plot_data[i] is not None:
self.plot_widget.addItem(self.plot_data[i])
else:
self.plot_data[i] = self.plot_widget.plot(*self.box_data[i])
else:
self.plot_widget.removeItem(self.plot_data[i])
break
def save_view(self):
self.view_state = self.plot_widget.viewRect()
def set_view(self):
self.plot_widget.setRange(self.view_state)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())

Displaying cursor coordinates in embedded matplotlib + pyqt

I'm using matplotlib as an embedded control in a PyQt 4 application to display and interact with images. I'd like to display the cursor coordinates and underlying value when the user moves it over the image. I've found the following post that addresses my needs but can't seem to get it to work:
matplotlib values under cursor
Here's what I have. First, I derive a class from FigureCanvasQtAgg:
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQtAgg as FigureCanvas
import matplotlib as mpImage
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
class MatPlotLibImage(FigureCanvas):
def __init__(self, parent = None):
self.parent = parent
self.fig = Figure()
super(MatPlotLibImage, self).__init__(self.fig)
self.axes = self.fig.add_subplot(111)
self.axes.get_xaxis().set_visible(False)
self.axes.get_yaxis().set_visible(False)
def displayNumpyArray(self, myNumpyArray):
self.dataArray = myNumpyArray
self.dataRows = self.dataArray.shape[0]
self.dataColumns = self.dataArray.shape[1]
self.axes.clear()
imagePlot = self.axes.imshow(myNumpyArray, interpolation = "nearest")
I'm also creating a new class that uses the above as its base, and this is the one that has to display the coords + value:
from MatPlotLibControl import *
class MainMatPlotLibImage(MatPlotLibControl):
def __init__(self, parent = None):
super(MainMatPlotLibImage, self).__init__(parent)
self.parent = parent
self.axes.format_coord = self.format_coord
def format_coord(self, x, y):
column = int(x + 0.5)
row = int(y + 0.5)
if column >= 0 and column <= self.dataColumns - 1 and row >= 0 and row <= self.dataRows - 1:
value = self.dataArray[row, column\
return 'x=%1.4f, y=%1.4f, z=%1.4f'%(column, row, value)
Everything is working smashingly except that when I move the cursor over the image I don't see the coords + value displayed on the plot. I then came across this post that seems to imply that they are actually displayed on the toolbar, not the plot itself: Disable coordinates from the toolbar of a Matplotlib figure
I'm not using the toolbar and this would explain why I don't see anything. Does anyone know if this is indeed the case (i.e. displayed on the toolbar)? If this is the case I will still need to retrieve the cords + underlying value and display them somewhere else in the application, but I've noticed that my "format_coord()" override is never called. Thanks in advance for any help.
-L