How to put multiple separate graphs into one Tkinter window? - python

I have been running this script:
from threading import Thread
import serial
import time
import collections
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import struct
import copy
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as Tk
from tkinter.ttk import Frame
class serialPlot:
def __init__(self, serialPort='/dev/ttyACM0', serialBaud=38400, plotLength=100, dataNumBytes=2, numPlots=4):
self.port = serialPort
self.baud = serialBaud
self.plotMaxLength = plotLength
self.dataNumBytes = dataNumBytes
self.numPlots = numPlots
self.rawData = bytearray(numPlots * dataNumBytes)
self.dataType = None
if dataNumBytes == 2:
self.dataType = 'h' # 2 byte integer
elif dataNumBytes == 4:
self.dataType = 'f' # 4 byte float
self.data = []
self.privateData = None
for i in range(numPlots): # give an array for each type of data and store them in a list
self.data.append(collections.deque([0] * plotLength, maxlen=plotLength))
self.isRun = True
self.isReceiving = False
self.thread = None
self.plotTimer = 0
self.previousTimer = 0
# self.csvData = []
print('Trying to connect to: ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
try:
self.serialConnection = serial.Serial(serialPort, serialBaud, timeout=4)
print('Connected to ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
except:
print("Failed to connect with " + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
def readSerialStart(self):
if self.thread == None:
self.thread = Thread(target=self.backgroundThread)
self.thread.start()
# Block till we start receiving values
while self.isReceiving != True:
time.sleep(0.1)
def getSerialData(self, frame, lines, lineValueText, lineLabel, timeText, pltNumber):
if pltNumber == 0: # in order to make all the clocks show the same reading
currentTimer = time.perf_counter()
self.plotTimer = int((currentTimer - self.previousTimer) * 1000) # the first reading will be erroneous
self.previousTimer = currentTimer
self.privateData = copy.deepcopy(self.rawData) # so that the 3 values in our plots will be synchronized to the same sample time
timeText.set_text('' + str(self.plotTimer) + '')
data = self.privateData[(pltNumber*self.dataNumBytes):(self.dataNumBytes + pltNumber*self.dataNumBytes)]
value, = struct.unpack(self.dataType, data)
self.data[pltNumber].append(value) # we get the latest data point and append it to our array
lines.set_data(range(self.plotMaxLength), self.data[pltNumber])
lineValueText.set_text('[' + lineLabel + '] = ' + str(value))
def backgroundThread(self): # retrieve data
time.sleep(1.0) # give some buffer time for retrieving data
self.serialConnection.reset_input_buffer()
while (self.isRun):
self.serialConnection.readinto(self.rawData)
self.isReceiving = True
#print(self.rawData)
def sendSerialData(self, data):
self.serialConnection.write(data.encode('utf-8'))
def close(self):
self.isRun = False
self.thread.join()
self.serialConnection.close()
print('Disconnected...')
# df = pd.DataFrame(self.csvData)
# df.to_csv('/home/rikisenia/Desktop/data.csv')
class Window(Frame):
def __init__(self, figure, master, SerialReference):
Frame.__init__(self, master)
self.entry = []
self.setPoint = None
self.master = master # a reference to the master window
self.serialReference = SerialReference # keep a reference to our serial connection so that we can use it for bi-directional communicate from this class
self.initWindow(figure) # initialize the window with our settings
def initWindow(self, figure):
self.master.title("Haptic Feedback Grasping Controller")
canvas = FigureCanvasTkAgg(figure, master=self.master)
toolbar = NavigationToolbar2Tk(canvas, self.master)
canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
# create out widgets in the master frame
lbl1 = Tk.Label(self.master, text="Distance")
lbl1.pack(padx=5, pady=5)
self.entry = Tk.Entry(self.master)
self.entry.insert(0, '0') # (index, string)
self.entry.pack(padx=5)
SendButton = Tk.Button(self.master, text='Send', command=self.sendFactorToMCU)
SendButton.pack(padx=5)
def sendFactorToMCU(self):
self.serialReference.sendSerialData(self.entry.get() + '%') # '%' is our ending marker
def main():
# portName = 'COM5'
portName = '/dev/ttyACM0'
baudRate = 38400
maxPlotLength = 100 # number of points in x-axis of real time plot
dataNumBytes = 4 # number of bytes of 1 data point
numPlots = 1 # number of plots in 1 graph
s = serialPlot(portName, baudRate, maxPlotLength, dataNumBytes, numPlots) # initializes all required variables
s.readSerialStart() # starts background thread
# plotting starts below
pltInterval = 50 # Period at which the plot animation updates [ms]
xmin = 0
xmax = maxPlotLength
ymin = -(1)
ymax = 200
fig = plt.figure()
ax = plt.axes(xlim=(xmin, xmax), ylim=(float(ymin - (ymax - ymin) / 10), float(ymax + (ymax - ymin) / 10)))
ax.set_title('Strain Gauge/ToF')
ax.set_xlabel("Time")
ax.set_ylabel("Force/Distance")
# put our plot onto Tkinter's GUI
root = Tk.Tk()
app = Window(fig, root, s)
lineLabel = ['W', 'X', 'Y', 'Z']
style = ['y-', 'r-', 'c-', 'b-'] # linestyles for the different plots
timeText = ax.text(0.70, 0.95, '', transform=ax.transAxes)
lines = []
lineValueText = []
for i in range(numPlots):
lines.append(ax.plot([], [], style[i], label=lineLabel[i])[0])
lineValueText.append(ax.text(0.70, 0.90-i*0.05, '', transform=ax.transAxes))
anim = animation.FuncAnimation(fig, s.getSerialData, fargs=(lines, lineValueText, lineLabel, timeText), interval=pltInterval) # fargs has to be a tuple
plt.legend(loc="upper left")
root.mainloop() # use this instead of plt.show() since we are encapsulating everything in Tkinter
s.close()
if __name__ == '__main__':
main()
A window shows up with no data passing through it even though I have 4 sensors that have data coming from an Arduino. The window contains 1 graph with 4 plots in it currently. I want 4 graphs each with one plot all in one window. I have been using https://thepoorengineer.com/en/python-gui/ as a reference to make graphs within python. The code for the data transfer is within the link as well. I tried to combine his 2 different codes and debugging it to make 4 graphs each with one plot to work with one Tkinter GUI window but it doesn't work. I also get an error of TypeError: getSerialData() missing 1 required positional argument: 'pltNumber' . Not sure why I get this error if pltNumber is in the parentheses in the code. I'm a beginner at python. What should I change to make the code work?
Script that can generate 4 separate graphs each with one plot that are not within a Tkinter GUI(works with 4 sensors but I need them within a Tkinter window):
from threading import Thread
import serial
import time
import collections
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import struct
import copy
class serialPlot:
def __init__(self, serialPort='/dev/ttyACM0', serialBaud=38400, plotLength=100, dataNumBytes=2, numPlots=1):
self.port = serialPort
self.baud = serialBaud
self.plotMaxLength = plotLength
self.dataNumBytes = dataNumBytes
self.numPlots = numPlots
self.rawData = bytearray(numPlots * dataNumBytes)
self.dataType = None
if dataNumBytes == 2:
self.dataType = 'h' # 2 byte integer
elif dataNumBytes == 4:
self.dataType = 'f' # 4 byte float
self.data = []
self.privateData = None # for storing a copy of the data so all plots are synchronized
for i in range(numPlots): # give an array for each type of data and store them in a list
self.data.append(collections.deque([0] * plotLength, maxlen=plotLength))
self.isRun = True
self.isReceiving = False
self.thread = None
self.plotTimer = 0
self.previousTimer = 0
print('Trying to connect to: ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
try:
self.serialConnection = serial.Serial(serialPort, serialBaud, timeout=4)
print('Connected to ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
except:
print("Failed to connect with " + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
def readSerialStart(self):
if self.thread == None:
self.thread = Thread(target=self.backgroundThread)
self.thread.start()
# Block till we start receiving values
while self.isReceiving != True:
time.sleep(0.1)
def getSerialData(self, frame, lines, lineValueText, lineLabel, timeText, pltNumber):
if pltNumber == 0: # in order to make all the clocks show the same reading
currentTimer = time.perf_counter()
self.plotTimer = int((currentTimer - self.previousTimer) * 1000) # the first reading will be erroneous
self.previousTimer = currentTimer
self.privateData = copy.deepcopy(self.rawData) # so that the 3 values in our plots will be synchronized to the same sample time
timeText.set_text('' + str(self.plotTimer) + '')
data = self.privateData[(pltNumber*self.dataNumBytes):(self.dataNumBytes + pltNumber*self.dataNumBytes)]
value, = struct.unpack(self.dataType, data)
self.data[pltNumber].append(value) # we get the latest data point and append it to our array
lines.set_data(range(self.plotMaxLength), self.data[pltNumber])
lineValueText.set_text('[' + lineLabel + '] = ' + str(value))
def backgroundThread(self): # retrieve data
time.sleep(1.0) # give some buffer time for retrieving data
self.serialConnection.reset_input_buffer()
while (self.isRun):
self.serialConnection.readinto(self.rawData)
self.isReceiving = True
def close(self):
self.isRun = False
self.thread.join()
self.serialConnection.close()
print('Disconnected...')
def makeFigure(xLimit, yLimit, title):
xmin, xmax = xLimit
ymin, ymax = yLimit
fig = plt.figure()
ax = plt.axes(xlim=(xmin, xmax), ylim=(int(ymin - (ymax - ymin) / 10), int(ymax + (ymax - ymin) / 10)))
ax.set_title(title)
ax.set_xlabel("Time")
ax.set_ylabel("Force/Distance")
return fig, ax
def main():
# portName = 'COM5'
portName = '/dev/ttyACM0'
baudRate = 38400
maxPlotLength = 100 # number of points in x-axis of real time plot
dataNumBytes = 4 # number of bytes of 1 data point
numPlots = 4 # number of plots in 1 graph
s = serialPlot(portName, baudRate, maxPlotLength, dataNumBytes, numPlots) # initializes all required variables
s.readSerialStart() # starts background thread
# plotting starts below
pltInterval = 50 # Period at which the plot animation updates [ms]
lineLabelText = ['W', 'X', 'Y', 'Z']
title = ['Strain Gauge 1 Force', 'Strain Gauge 2 Force', 'ToF 1 Distance', 'ToF 2 Distance']
xLimit = [(0, maxPlotLength), (0, maxPlotLength), (0, maxPlotLength), (0, maxPlotLength)]
yLimit = [(-1, 1), (-1, 1), (-1, 1), (-1, 1)]
style = ['y-', 'r-', 'g-', 'b-'] # linestyles for the different plots
anim = []
for i in range(numPlots):
fig, ax = makeFigure(xLimit[i], yLimit[i], title[i])
lines = ax.plot([], [], style[i], label=lineLabelText[i])[0]
timeText = ax.text(0.50, 0.95, '', transform=ax.transAxes)
lineValueText = ax.text(0.50, 0.90, '', transform=ax.transAxes)
anim.append(animation.FuncAnimation(fig, s.getSerialData, fargs=(lines, lineValueText, lineLabelText[i], timeText, i), interval=pltInterval)) # fargs has to be a tuple
plt.legend(loc="upper left")
plt.show()
s.close()
if __name__ == '__main__':
main()

I think something like this can be useful.
A similar issue was addressed in this post too.
Here we can use the backend class of matplotlib namely FigureCanvasTkAgg.
It works like a tkinter canvas but with the additional ability to be able to plot figures into it.
This means that we can initialize multiple matplotlib figures, plot graphs on them and then plot those figures onto the canvas.
This allows us to plot multiple graphs on the same tkinter window.
To import this class -:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
Then a figure object of matplotlib can be used to plot a graph on the canvas like so -:
from matplotlib.figure import Figure
fig = Figure(...) # Initializing the figure object.
canvas = FigureCanvasTkAgg(fig, master=root) # Initializing the FigureCanvasTkAgg Class Object.
tk_canvas = canvas.get_tk_widget() # Getting the Figure canvas as a tkinter widget.
tk_canvas.pack() # Packing it into it's master window.
canvas.draw() # Drawing the canvas onto the screen.
Similarly multiple canvases can be initialized and packed into the tk window, thus giving multiple plotted graphs.
The plotting of the figure object can be done using matplotlib methods.
The full code for two such figures will become -:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
# FOR FIRST GRAPH
fig = Figure(...) # Initializing the figure object.
canvas = FigureCanvasTkAgg(fig, master=root) # Initializing the FigureCanvasTkAgg Class Object.
tk_canvas = canvas.get_tk_widget() # Getting the Figure canvas as a tkinter widget.
tk_canvas.pack() # Packing it into it's master window.
canvas.draw() # Drawing the canvas onto the screen.
# FOR SECOND GRAPH
fig_2 = Figure(...) # Initializing the second figure object.
canvas_2 = FigureCanvasTkAgg(fig_2, master=root) # Initializing the second FigureCanvasTkAgg Class Object.
tk_canvas_2 = canvas_2.get_tk_widget() # Getting the second Figure canvas as a tkinter widget.
tk_canvas_2.pack() # Packing it into it's master window.
canvas_2.draw() # Drawing the second canvas onto the screen.
# CAN BE REPEATED FOR MULTIPLE SUCH GRAPHS....

EDIT: Misunderstood question, but will still leave this here as it'll be useful for you in terms of manipulating the graphs
Here is some code that I use to generate 5 listboxes, and append them to a dictionary so I can reference them later in the code.
self.listboxes = []
for i in range(5):
self.lb = tk.Listbox(self.modeSelect)
self.lb.configure(background='#2f2a2d', exportselection='false', font='{Arial} 12 {}', foreground='#feffff', height='23')
self.lb.configure(relief='flat', width='9', justify='center', selectbackground='#feffff', selectforeground='#000000')
self.lb.pack(side='left')
self.listboxes.append(self.lb)
self.lb.bind("<Double-1>", self.getIndexLB)
You can then assign functions or call attributes by using
self.listboxes[0].get() #for example
That way you can assign points to each graph, it will also allow you to control all the graphs simultaneously by doing something like:
for child in self.modeSelect.winfo_children():
if isinstance(child, tk.Listbox):
child.delete(index)

Related

Animating a real time breathing curve in python

I want to create an animation that people can use to align their breathing with. I have made a class with PyQt5 that does exactly this, and has the breathing period as parameter. (See code below).
It works well, apart from the timing. When setting a specific delta_t and window size during the FuncAnimation I can get accurate timings. But when I change the window size, it either speeds up or slows down...
Im probably going to model this in another language, but I am still curious if I can get this right in Python. Can anyone here point me in the right direction?
import os
import time
import sys
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import PyQt5.QtGui
import PyQt5.QtCore
import PyQt5.QtWidgets
class BreathingAnimation(PyQt5.QtWidgets.QMainWindow):
# Could inherit from GenericInterface
# Animate three ways:
# o Fixed sinusoid -> moving ball
# o Fixed ball (up/down) -> moving wave
# o Expanding ball
def __init__(self, period=1, delta_t=0.01, animation_type='ball'):
super().__init__()
self._main = PyQt5.QtWidgets.QWidget()
self.setCentralWidget(self._main)
"""
Plot variables
sin(omega * x)
period = 2 * np.pi * omega
omega = period / (2 * np.pi)
"""
self.delta_t = delta_t
self.frequentie = 1/period
self.max_plot_time = 5
self.max_periods = 20
self.t_range = np.arange(0, self.max_periods * period, delta_t)
self.animation_type = animation_type
self.cool_down = 10
self.prev_time = time.time()
self.prev_time_cor_time = time.time()
"""
Graphical definitions
"""
self.canvas, self.fig, self.axes = self.get_figure(self.max_plot_time)
self.axes.set_axis_off()
self.line_obj, self.scatter_obj = self.get_plot_objects(self.axes)
# Get the animation object
self.anim_obj = self.get_animation()
h_layout = PyQt5.QtWidgets.QHBoxLayout(self._main)
# Create buttons
# self.button_box_layout = self.get_button_box_layout()
# write_button = self.get_push_button(name='Write', shortcut='W', connect=self.write_animation)
# self.edit_button = self.get_line_edit(name=f'{period}', connect=self.update_period, max_length=4)
# self.button_box_layout.addWidget(self.edit_button)
# self.button_box_layout.addWidget(write_button)
# Add canvas to the figure
temp_canvas_layout = PyQt5.QtWidgets.QVBoxLayout()
temp_canvas_layout.addWidget(self.canvas)
h_layout.addLayout(temp_canvas_layout, stretch=1)
# h_layout.addLayout(self.button_box_layout, stretch=0.01)
#staticmethod
def get_figure(max_plot_time, debug=False):
if debug:
fig = plt.figure()
else:
fig = Figure(figsize=(5, 5), dpi=100)
canvas = FigureCanvas(fig)
axes = canvas.figure.subplots()
# self.axes.set_axis_off()
axes.set_ylim(-2, 2)
axes.set_xlim(0, max_plot_time)
return canvas, fig, axes
#staticmethod
def get_plot_objects(axes):
# Create a line object
line_obj = axes.plot([], [], zorder=1)[0]
# Create a scatter object
scatter_obj = axes.scatter([], [], s=40, marker='o', c='r')
return line_obj, scatter_obj
def get_y_value(self, i_t):
omega = 2 * np.pi * self.frequentie
y_value = np.sin(omega * i_t)
return y_value
def animate_moving_ball(self, i, line_obj=None, scatter_obj=None):
i = i % len(self.t_range)
if line_obj is None:
line_obj = self.line_obj
if scatter_obj is None:
scatter_obj = self.scatter_obj
line_obj.set_data(self.t_range, self.get_y_value(self.t_range))
sel_time = self.t_range[i]
scatter_obj.set_offsets(np.c_[sel_time, self.get_y_value(sel_time)])
return scatter_obj
def animate_moving_wave(self, i, line_obj=None, scatter_obj=None):
i = i % len(self.t_range)
if line_obj is None:
line_obj = self.line_obj
if scatter_obj is None:
scatter_obj = self.scatter_obj
line_obj.set_data(self.t_range, np.roll(self.get_y_value(self.t_range), -i))
sel_time = self.t_range[i] + self.max_plot_time/2.
# print(f'max plot time {i}', self.max_plot_time/2, self.get_y_value(sel_time))
scatter_obj.set_offsets(np.c_[self.max_plot_time/2., self.get_y_value(sel_time)])
# self.cool_down -= 1
# # print(self.cool_down)
if ((1 - self.get_y_value(sel_time)) < 0.0001):
time_difference = time.time() - self.prev_time
self.prev_time = time.time()
print('Time interval in seconds ', time_difference)
return scatter_obj
def update_period(self):
new_periode = float(self.edit_button.text())
self.frequentie = 1./new_periode
self.t_range = np.arange(0, self.max_periods * new_periode, self.delta_t)
def get_animation(self):
if self.animation_type == 'ball':
# Return a moving ball..
animation_fun = self.animate_moving_ball
elif self.animation_type == 'wave':
# Return a wave..
animation_fun = self.animate_moving_wave
else:
animation_fun = None
self.animation_obj = animation.FuncAnimation(self.canvas.figure, animation_fun,
blit=False, repeat=True,
interval=self.delta_t, # Delay in ms
frames=len(self.t_range))
self.animation_obj.new_frame_seq()
return self.animation_obj
def write_animation(self):
num_frames = len(self.t_range)
max_time = np.max(self.t_range) # in seconds?
print('frames ', num_frames / max_time)
ffmpeg_writer = animation.FFMpegWriter(fps=num_frames / max_time)
self.animation_obj.save(os.path.expanduser('~/breathing_animation.mp4'), writer=ffmpeg_writer)
print('Written')
if __name__ == "__main__":
qapp = PyQt5.QtWidgets.QApplication(sys.argv)
app = BreathingAnimation(period=3, animation_type='wave', delta_t=0.009)
app.show()
qapp.exec_()

graph no actualizing on pysimplegui with matplotlib

I have the following code, I want my GUI to change my two graphs everytime I press the button, but only the one in the left changes. Can someone please help me.
It's very strange because I took care of saving all the canvas and that way I can edit them with first using .forget() but for some reason it really doesn't work for the first graph.
import PySimpleGUI as sg
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# VARS CONSTS:
# New figure and plot variables so we can manipulate them
_VARS = {'window': False,
'fig_agg1': False,
'pltFig1': False,
'fig_agg2': False,
'pltFig2': False}
dataSize = 1000 # For synthetic data
# Helper Functions
def draw_figure(canvas, figure):
figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
figure_canvas_agg.draw()
figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
return figure_canvas_agg
def update_graph(zoom,or1,or2):
updateChart(1,zoom,or1,or2)
updateChart(2,zoom,or1,or2)
# \\ -------- PYSIMPLEGUI -------- //
AppFont = 'Any 16'
sg.theme('DarkTeal12')
#layout = [[sg.Canvas(key='figCanvas')],
# [sg.Button('Update', font=AppFont), sg.Button('Exit', font=AppFont)]]
control_col=sg.Column([
[sg.Frame('Zoom',layout = [[sg.Slider(range = (0,100), orientation = 'h', key = '-ZOOM-')]])],
[sg.Frame('Orientation 1',layout = [[sg.Slider(range = (-180,180), orientation = 'h', key = '-OR1-')]])],
[sg.Frame('Orientation 2',layout = [[sg.Slider(range = (-180,180), orientation = 'h', key = '-OR2-')]])],
[sg.Checkbox('Blackhole 1' , key = '-BH1-')],
[sg.Checkbox('Blackhole 2' , key = '-BH2-')],
[sg.Checkbox('Blackhole 3' , key = '-BH3-')],
[sg.Button('Show', key = '-SHOW-')]
])
graph1_col=sg.Column([[sg.Canvas(key = '-CANVAS1-')]])
graph2_col=sg.Column([[sg.Canvas(key = '-CANVAS2-')]])
layout=[[control_col,graph1_col,graph2_col]]
#_VARS['window'] = sg.Window('Such Window',
# layout,
# finalize=True,
# resizable=True,
# location=(100, 100),
# element_justification="right")
_VARS['window'] = sg.Window('Visualization', layout, finalize = True)
# \\ -------- PYSIMPLEGUI -------- //
# \\ -------- PYPLOT -------- //
def makeSynthData():
xData = np.random.randint(100, size=dataSize)
yData = np.random.randint(100, size=dataSize)
zData = np.random.randint(100, size=dataSize)
return (xData, yData, zData)
def drawChart(number):
_VARS['pltFig'+str(number)] = plt.figure()
dataXYZ = makeSynthData()
#plt.plot(dataXYZ[0], dataXYZ[1],dataXYZ[2] '.k')
ax = plt.axes(projection='3d')
ax.scatter3D(dataXYZ[0], dataXYZ[1], dataXYZ[2], c=dataXYZ[2], cmap='Greens')
_VARS['fig_agg'+str(number)] = draw_figure(
_VARS['window']['-CANVAS'+str(number)+'-'].TKCanvas, _VARS['pltFig'+str(number)])
# Recreate Synthetic data, clear existing figre and redraw plot.
def updateChart(number,zoom,or1,or2):
_VARS['fig_agg'+str(number)].get_tk_widget().forget()
dataXYZ = makeSynthData()
# plt.cla()
plt.clf()
#plt.plot(dataXYZ[0], dataXYZ[1], '.k')
ax = plt.axes(projection='3d')
ax.scatter3D(dataXYZ[0], dataXYZ[1], dataXYZ[2], c=dataXYZ[2], cmap='Greens')
ax.view_init(or1, or2)
_VARS['fig_agg'+str(number)] = draw_figure(
_VARS['window']['-CANVAS'+str(number)+'-'].TKCanvas, _VARS['pltFig'+str(number)])
# \\ -------- PYPLOT -------- //
drawChart(1)
drawChart(2)
# MAIN LOOP
while True:
event, values = _VARS['window'].read(timeout = 50)
if event == sg.WIN_CLOSED:
break
if event == '-SHOW-':
print(values)
update_graph(
values['-ZOOM-'],
values['-OR1-'],
values['-OR2-'])
_VARS['window'].close()
For plt.figure
Option num: A unique identifier for the figure.
If a figure with that identifier already exists, this figure is made active and returned.
Following code set which figure activated to draw
def drawChart(number):
_VARS['pltFig'+str(number)] = plt.figure()
After
drawChart(1)
drawChart(2)
Active figure set to figure 2 and not been changed until script end.
Try to update following statement in your code.
def drawChart(number):
_VARS['pltFig'+str(number)] = plt.figure(num=number)
def updateChart(number,zoom,or1,or2):
_VARS['fig_agg'+str(number)].get_tk_widget().forget()
dataXYZ = makeSynthData()
plt.figure(num=number)

Embedding matplotlib in tkinter GUI

Trying to embed matplotlib in tkinter GUI, however I get the error:
TypeError: init() got multiple values for argument 'master'
Could you tell me please how to handle it?
Here's the code:
interface - tkinter GUI
visualizer - selv updating graph that uses function live_plotter from pylive_mod file
interface:
# -*- coding: utf-8 -*-
"""
Created on Tue Oct 6 10:24:35 2020
#author: Dar0
"""
from tkinter import * #import tkinter module
from visualizer import main #import module 'visualizer' that shows the graph in real time
class Application(Frame):
''' Interface for visualizing graphs, indicators and text-box. '''
def __init__(self, master):
super(Application, self).__init__(master)
self.grid()
self.create_widgets()
def create_widgets(self):
# Label of the 1st graph
Label(self,
text='Hook Load / Elevator Height / Depth vs Time'
).grid(row = 0, column = 0, sticky = W)
# Graph 1 - Hook Load / Elevator Height / Depth vs Time
# button that displays the plot
plot_button = Button(self,
master = root,
command = main,
height = 2,
width = 10,
text = "Plot").grid(row = 1, column = 0, sticky = W)
# place the button
# in main window
# Label of the 2nd graph
Label(self,
text = 'Hook Load / Elevator Height vs Time'
).grid(row = 2, column = 0, sticky = W)
# Graph 2 - Hook Load / Elevator Height vs Time
#Label of the 3rd graph
Label(self,
text = 'Hook Load vs Time'
).grid(row = 4, column = 0, sticky = W)
#Graph 3 - Hook Load vs Time
#Label of the 1st indicator
Label(self,
text = '1st performance indicator'
).grid(row = 0, column = 1, sticky = W)
#1st performance indicator
#Label of 2nd performance indicator
Label(self,
text = '2nd performance indicator'
).grid(row = 2, column = 1, sticky = W)
#2nd performance indicator
#Label of 3rd performance indicator
Label(self,
text = '3rd performance indicator'
).grid(row = 4, column = 1, sticky = W)
#Text-box showing comments based on received data
self.text_box = Text(self, width = 50, height = 10, wrap = WORD)
self.text_box.grid(row = 6, column = 0, columnspan = 1)
self.text_box.delete(0.0, END)
self.text_box.insert(0.0, 'My message will be here.')
#Main part
root = Tk()
root.title('WiTSML Visualizer by Dar0')
app = Application(root)
root.mainloop()
visualizer:
#WiTSML visualizer
#Created by Dariusz Krol
#import matplotlib
#matplotlib.use('TkAgg')
#from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
#from matplotlib.figure import Figure
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random
class Visualizer(object):
""" Includes all the methods needed to show streamed data. """
def __init__(self):
self.file_path = 'C:/Anaconda/_my_files/witsml_reader/modified_witsml.csv' #Defines which file is streamed
self.datetime_mod = []
self.bpos_mod = []
self.woh_mod = []
self.torq_mod = []
self.spp_mod = []
self.depth_mod = []
self.flow_in_mod = []
self.rpm_mod = []
def open_file(self):
self.df = pd.read_csv(self.file_path, low_memory = False, nrows = 300000) #Opens the STREAMED file (already modified so that data convert is not required)
self.df = self.df.drop(0)
self.df = pd.DataFrame(self.df)
return self.df
def convert_dataframe(self):
self.df = self.df.values.T.tolist() #Do transposition of the dataframe and convert to list
#Columns are as following:
# - DATETIME
# - BPOS
# - WOH
# - TORQ
# - SPP
# - DEPTH
# - FLOW_IN
# - RPM
self.datetime_value = self.df[0]
self.bpos_value = self.df[1]
self.woh_value = self.df[2]
self.torq_value = self.df[3]
self.spp_value = self.df[4]
self.depth_value = self.df[5]
self.flow_in_value = self.df[5]
self.rpm_value = self.df[7]
return self.datetime_value, self.bpos_value, self.woh_value, self.torq_value, self.spp_value, self.depth_value, self.flow_in_value, self.rpm_value
#print(self.bpos_value)
def deliver_values(self, no_dp, columns):
''' Method gets no_dp amount of data points from the original file. '''
self.no_dp = no_dp #defines how many data points will be presented in the graph
val_dict = {
'datetime': [self.datetime_value, self.datetime_mod],
'bpos': [self.bpos_value, self.bpos_mod],
'woh': [self.woh_value, self.woh_mod],
'torq': [self.torq_value, self.torq_mod],
'spp': [self.spp_value, self.spp_mod],
'depth': [self.depth_value, self.depth_mod],
'flow_in': [self.flow_in_value, self.flow_in_mod],
'rpm': [self.rpm_value, self.rpm_mod]
}
for item in columns:
if self.no_dp > len(val_dict[item][0]):
dp_range = len(val_dict[item][0])
else:
dp_range = self.no_dp
for i in range(dp_range):
val_dict[item][1].append(val_dict[item][0][i])
return self.datetime_mod, self.bpos_mod, self.woh_mod, self.torq_mod, self.spp_mod, self.depth_mod, self.flow_in_mod, self.rpm_mod
def show_graph2(self):
from pylive_mod import live_plotter
self.open_file()
self.convert_dataframe()
self.deliver_values(no_dp = 100000, columns = ['datetime', 'depth', 'bpos', 'woh'])
fst_p = 0
size = 300 # density of points in the graph (100 by default)
x_vec = self.datetime_mod[fst_p:size]
y_vec = self.depth_mod[fst_p:size]
y2_vec = self.bpos_mod[fst_p:size]
y3_vec = self.woh_mod[fst_p:size]
line1 = []
line2 = []
line3 = []
for i in range(self.no_dp):
#print(self.datetime_mod[i:6+i])
#print('Ostatni element y_vec: ', y_vec[-1])
#print(x_vec)
x_vec[-1] = self.datetime_mod[size+i]
y_vec[-1] = self.depth_mod[size+i]
y2_vec[-1] = self.bpos_mod[size+i]
y3_vec[-1] = self.woh_mod[size+i]
line1, line2, line3 = live_plotter(x_vec, y_vec, y2_vec, y3_vec, line1, line2, line3)
x_vec = np.append(x_vec[1:], 0.0)
y_vec = np.append(y_vec[1:], 0.0)
y2_vec = np.append(y2_vec[1:], 0.0)
y3_vec = np.append(y3_vec[1:], 0.0)
def main():
Graph = Visualizer()
Graph.open_file() #Opens the streamed file
Graph.convert_dataframe() #Converts dataframe to readable format
Graph.show_graph2()
#Show us the graph
#main()
pylive_mod (live_plotter):
def live_plotter(x_data, y1_data, y2_data, y3_data, line1, line2, line3, identifier='',pause_time=1):
if line1 == [] and line2 == [] and line3 == []:
# this is the call to matplotlib that allows dynamic plotting
plt.ion()
#fig = Figure(figsize = (5, 4), dpi = 100)
#host = fig.add_subplot()
fig, host = plt.subplots()
fig.set_figheight(7) #adjust figure's height
fig.set_figwidth(14) #adjust figure's width
fig.subplots_adjust(0.15)
#Line1
#line1 = host
ln1 = host
ln2 = host.twinx()
ln3 = host.twinx()
ln2.spines['right'].set_position(('axes', 1.))
ln3.spines['right'].set_position(('axes', 1.12))
make_patch_spines_invisible(ln2)
make_patch_spines_invisible(ln3)
ln2.spines['right'].set_visible(True)
ln3.spines['right'].set_visible(True)
ln1.set_xlabel('Date & Time') #main x axis
ln1.set_ylabel('Depth') #left y axis
ln2.set_ylabel('Elevator Height')
ln3.set_ylabel('Weight on Hook')
#
x_formatter = FixedFormatter([x_data])
x_locator = FixedLocator([x_data[5]])
ln1.xaxis.set_major_formatter(x_formatter)
ln1.xaxis.set_major_locator(x_locator)
#
ln1.locator_params(nbins = 5, axis = 'y')
ln1.tick_params(axis='x', rotation=90) #rotates x ticks 90 degrees down
ln2.axes.set_ylim(0, 30)
ln3.axes.set_ylim(200, 250)
line1, = ln1.plot(x_data, y1_data, color = 'black', linestyle = 'solid', alpha=0.8, label = 'Depth')
line2, = ln2.plot(x_data, y2_data, color = 'blue', linestyle = 'dashed', alpha=0.8, label = 'Elevator Height')
line3, = ln3.plot(x_data, y3_data, color = 'red', linestyle = 'solid', alpha=0.8, label = 'Weight on Hook')
# ----- embedding -----
canvas = FigureCanvasTkAgg(fig, master = root)
canvas.draw()
# placing the canvas on the Tkinter window
canvas.get_tk_widget().pack()
# creating the Matplotlib toolbar
toolbar = NavigationToolbar2Tk(canvas, root)
toolbar.update()
# placing the toolbar on the Tkinter window
canvas.get_tk_widget().pack()
#----- -----
plt.title('WiTSML Visualizer')
fig.tight_layout() #the graphs is not clipped on sides
#Shows legend
lines = [line1, line2, line3]
host.legend(lines, [l.get_label() for l in lines], loc = 'lower left')
#Shows grid
plt.grid(True)
#Shows the whole graph
plt.show()
# after the figure, axis, and line are created, we only need to update the y-data
mod_x_data = convert_x_data(x_data, 10)
line1.axes.set_xticklabels(mod_x_data)
line1.set_ydata(y1_data)
line2.set_ydata(y2_data)
line3.set_ydata(y3_data)
#Debugging
#rint('plt.lim: ', ln2.axes.get_ylim())
# adjust limits if new data goes beyond bounds
# limit for line 1
if np.min(y1_data)<=line1.axes.get_ylim()[0] or np.max(y1_data)>=line1.axes.get_ylim()[1]:
plt.ylim(0, 10)
line1.axes.set_ylim([np.min(y1_data)-np.std(y1_data),np.max(y1_data)+np.std(y1_data)])
# limit for line 2
if np.min(y2_data)<=line2.axes.get_ylim()[0] or np.max(y2_data)>=line2.axes.get_ylim()[1]:
plt.ylim([np.min(y2_data)-np.std(y2_data),np.max(y2_data)+np.std(y2_data)])
#plt.ylim(0, 25)
# limit for line 3
if np.min(y3_data)<=line3.axes.get_ylim()[0] or np.max(y3_data)>=line3.axes.get_ylim()[1]:
plt.ylim([np.min(y3_data)-np.std(y3_data),np.max(y3_data)+np.std(y3_data)])
#plt.ylim(0, 25)
# Adds lines to the legend
#host.legend(lines, [l.get_label() for l in lines])
# this pauses the data so the figure/axis can catch up - the amount of pause can be altered above
plt.pause(pause_time)
# return line so we can update it again in the next iteration
return line1, line2, line3

Multiple matplotlib instances in tkinter GUI

I have build simple tkinter GUI.
Now, I am trying to visualise 3 different graphs (by calling the same function with different variables) and place them in 3 different rows of the GUI.
When I do that I get 2 problems:
Every time I run the script (interface.py) I get 2 windows - both GUI and external graph's window. How to get rid of the second one?
I am not able to visualize all the 3 graphs. The script stops after showing the first one. I believe this is because of that the first graph works in a loop (iterates through plenty of data points). Is there any work around it?
Interface:
# -*- coding: utf-8 -*-
"""
Created on Tue Oct 6 10:24:35 2020
#author: Dar0
"""
from tkinter import * #import tkinter module
from visualizer import main #import module 'visualizer' that shows the graph in real time
class Application(Frame):
''' Interface for visualizing graphs, indicators and text-box. '''
def __init__(self, master):
super(Application, self).__init__(master)
self.grid()
self.create_widgets()
def create_widgets(self):
# Label of the 1st graph
Label(self,
text='Hook Load / Elevator Height / Depth vs Time'
).grid(row = 0, column = 0, sticky = W)
# Graph 1 - Hook Load / Elevator Height / Depth vs Time
# button that displays the plot
#plot_button = Button(self,2
# command = main,
# height = 2,
# width = 10,
# text = "Plot"
# ).grid(row = 1, column = 0, sticky = W)
self.graph_1 = main(root, 1, 0)
# place the button
# in main window
# Label of the 2nd graph
Label(self,
text = 'Hook Load / Elevator Height vs Time'
).grid(row = 3, column = 0, sticky = W)
# Graph 2 - Hook Load / Elevator Height vs Time
self.graph_2 = main(root, 4, 0)
#Label of the 3rd graph
Label(self,
text = 'Hook Load vs Time'
).grid(row = 6, column = 0, sticky = W)
#Graph 3 - Hook Load vs Time
#Label of the 1st indicator
Label(self,
text = '1st performance indicator'
).grid(row = 0, column = 1, sticky = W)
#1st performance indicator
#Label of 2nd performance indicator
Label(self,
text = '2nd performance indicator'
).grid(row = 3, column = 1, sticky = W)
#2nd performance indicator
#Label of 3rd performance indicator
Label(self,
text = '3rd performance indicator'
).grid(row = 6, column = 1, sticky = W)
#Text-box showing comments based on received data
self.text_box = Text(self, width = 50, height = 10, wrap = WORD)
self.text_box.grid(row = 9, column = 0, columnspan = 1)
self.text_box.delete(0.0, END)
self.text_box.insert(0.0, 'My message will be here.')
#Main part
root = Tk()
root.title('WiTSML Visualizer by Dar0')
app = Application(root)
root.mainloop()
Visualizer:
#WiTSML visualizer
#Created by Dariusz Krol
#import matplotlib
#matplotlib.use('TkAgg')
#from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
#from matplotlib.figure import Figure
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random
class Visualizer(object):
""" Includes all the methods needed to show streamed data. """
def __init__(self):
self.file_path = 'C:/Anaconda/_my_files/witsml_reader/modified_witsml.csv' #Defines which file is streamed
self.datetime_mod = []
self.bpos_mod = []
self.woh_mod = []
self.torq_mod = []
self.spp_mod = []
self.depth_mod = []
self.flow_in_mod = []
self.rpm_mod = []
def open_file(self):
self.df = pd.read_csv(self.file_path, low_memory = False, nrows = 300000) #Opens the STREAMED file (already modified so that data convert is not required)
self.df = self.df.drop(0)
self.df = pd.DataFrame(self.df)
return self.df
def convert_dataframe(self):
self.df = self.df.values.T.tolist() #Do transposition of the dataframe and convert to list
#Columns are as following:
# - DATETIME
# - BPOS
# - WOH
# - TORQ
# - SPP
# - DEPTH
# - FLOW_IN
# - RPM
self.datetime_value = self.df[0]
self.bpos_value = self.df[1]
self.woh_value = self.df[2]
self.torq_value = self.df[3]
self.spp_value = self.df[4]
self.depth_value = self.df[5]
self.flow_in_value = self.df[5]
self.rpm_value = self.df[7]
return self.datetime_value, self.bpos_value, self.woh_value, self.torq_value, self.spp_value, self.depth_value, self.flow_in_value, self.rpm_value
#print(self.bpos_value)
def deliver_values(self, no_dp, columns):
''' Method gets no_dp amount of data points from the original file. '''
self.no_dp = no_dp #defines how many data points will be presented in the graph
val_dict = {
'datetime': [self.datetime_value, self.datetime_mod],
'bpos': [self.bpos_value, self.bpos_mod],
'woh': [self.woh_value, self.woh_mod],
'torq': [self.torq_value, self.torq_mod],
'spp': [self.spp_value, self.spp_mod],
'depth': [self.depth_value, self.depth_mod],
'flow_in': [self.flow_in_value, self.flow_in_mod],
'rpm': [self.rpm_value, self.rpm_mod]
}
for item in columns:
if self.no_dp > len(val_dict[item][0]):
dp_range = len(val_dict[item][0])
else:
dp_range = self.no_dp
for i in range(dp_range):
val_dict[item][1].append(val_dict[item][0][i])
return self.datetime_mod, self.bpos_mod, self.woh_mod, self.torq_mod, self.spp_mod, self.depth_mod, self.flow_in_mod, self.rpm_mod
def show_graph2(self, tr_val, row, column):
from pylive_mod import live_plotter, live_plotter2
self.open_file()
self.convert_dataframe()
self.deliver_values(no_dp = 100000, columns = ['datetime', 'depth', 'bpos', 'woh'])
fst_p = 0
size = 300 # density of points in the graph (100 by default)
x_vec = self.datetime_mod[fst_p:size]
y_vec = self.depth_mod[fst_p:size]
y2_vec = self.bpos_mod[fst_p:size]
y3_vec = self.woh_mod[fst_p:size]
line1 = []
line2 = []
line3 = []
for i in range(self.no_dp):
#print(self.datetime_mod[i:6+i])
#print('Ostatni element y_vec: ', y_vec[-1])
#print(x_vec)
x_vec[-1] = self.datetime_mod[size+i]
y_vec[-1] = self.depth_mod[size+i]
y2_vec[-1] = self.bpos_mod[size+i]
y3_vec[-1] = self.woh_mod[size+i]
line1, line2, line3 = live_plotter2(tr_val, row, column, x_vec, y_vec, y2_vec, y3_vec, line1, line2, line3)
x_vec = np.append(x_vec[1:], 0.0)
y_vec = np.append(y_vec[1:], 0.0)
y2_vec = np.append(y2_vec[1:], 0.0)
y3_vec = np.append(y3_vec[1:], 0.0)
def main(tr_val, row, column):
Graph = Visualizer()
Graph.open_file() #Opens the streamed file
Graph.convert_dataframe() #Converts dataframe to readable format
Graph.show_graph2(tr_val, row, column)
#Show us the graph
#main()
Function that creates the graph:
def live_plotter2(tr_val, row, column, x_data, y1_data, y2_data, y3_data, line1, line2, line3, identifier='',pause_time=1):
if line1 == [] and line2 == [] and line3 == []:
# this is the call to matplotlib that allows dynamic plotting
plt.ion()
fig = plt.figure(figsize = (5, 4), dpi = 100)
fig.subplots_adjust(0.15)
# -------------------- FIRST GRAPH --------------------
host = fig.add_subplot()
ln1 = host
ln2 = host.twinx()
ln3 = host.twinx()
ln2.spines['right'].set_position(('axes', 1.))
ln3.spines['right'].set_position(('axes', 1.12))
make_patch_spines_invisible(ln2)
make_patch_spines_invisible(ln3)
ln2.spines['right'].set_visible(True)
ln3.spines['right'].set_visible(True)
ln1.set_xlabel('Date & Time') #main x axis
ln1.set_ylabel('Depth') #left y axis
ln2.set_ylabel('Elevator Height')
ln3.set_ylabel('Weight on Hook')
#
x_formatter = FixedFormatter([x_data])
x_locator = FixedLocator([x_data[5]])
#ln1.xaxis.set_major_formatter(x_formatter)
ln1.xaxis.set_major_locator(x_locator)
#
ln1.locator_params(nbins = 5, axis = 'y')
ln1.tick_params(axis='x', rotation=90) #rotates x ticks 90 degrees down
ln2.axes.set_ylim(0, 30)
ln3.axes.set_ylim(200, 250)
line1, = ln1.plot(x_data, y1_data, color = 'black', linestyle = 'solid', alpha=0.8, label = 'Depth')
line2, = ln2.plot(x_data, y2_data, color = 'blue', linestyle = 'dashed', alpha=0.8, label = 'Elevator Height')
line3, = ln3.plot(x_data, y3_data, color = 'red', linestyle = 'solid', alpha=0.8, label = 'Weight on Hook')
fig.tight_layout() #the graphs is not clipped on sides
plt.title('WiTSML Visualizer')
plt.grid(True)
#Shows legend
lines = [line1, line2, line3]
host.legend(lines, [l.get_label() for l in lines], loc = 'lower left')
#Shows the whole graph
#plt.show()
#-------------------- Embedding --------------------
canvas = FigureCanvasTkAgg(fig, master=tr_val)
canvas.draw()
canvas.get_tk_widget().grid(row=row, column=column, ipadx=40, ipady=20)
# navigation toolbar
toolbarFrame = tk.Frame(master=tr_val)
toolbarFrame.grid(row=row,column=column)
toolbar = NavigationToolbar2Tk(canvas, toolbarFrame)
# after the figure, axis, and line are created, we only need to update the y-data
mod_x_data = convert_x_data(x_data, 20)
line1.axes.set_xticklabels(mod_x_data)
line1.set_ydata(y1_data)
line2.set_ydata(y2_data)
line3.set_ydata(y3_data)
#Debugging
#rint('plt.lim: ', ln2.axes.get_ylim())
# adjust limits if new data goes beyond bounds
# limit for line 1
if np.min(y1_data)<=line1.axes.get_ylim()[0] or np.max(y1_data)>=line1.axes.get_ylim()[1]:
plt.ylim(0, 10)
line1.axes.set_ylim([np.min(y1_data)-np.std(y1_data),np.max(y1_data)+np.std(y1_data)])
# limit for line 2
if np.min(y2_data)<=line2.axes.get_ylim()[0] or np.max(y2_data)>=line2.axes.get_ylim()[1]:
plt.ylim([np.min(y2_data)-np.std(y2_data),np.max(y2_data)+np.std(y2_data)])
#plt.ylim(0, 25)
# limit for line 3
if np.min(y3_data)<=line3.axes.get_ylim()[0] or np.max(y3_data)>=line3.axes.get_ylim()[1]:
plt.ylim([np.min(y3_data)-np.std(y3_data),np.max(y3_data)+np.std(y3_data)])
#plt.ylim(0, 25)
# Adds lines to the legend
#host.legend(lines, [l.get_label() for l in lines])
# this pauses the data so the figure/axis can catch up - the amount of pause can be altered above
plt.pause(pause_time)
# return line so we can update it again in the next iteration
return line1, line2, line3
The key is to not use pyplot when you want to plot within tkinter as shown in the official example. Use matplotlib.figure.Figure instead (see this for added info).
Below is a minimum sample that plots 3 independent graphs along a Text widget which I see in your code:
import pandas as pd
import numpy as np
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
class Graph(tk.Frame):
def __init__(self, master=None, title="", *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.fig = Figure(figsize=(4, 3))
ax = self.fig.add_subplot(111)
df = pd.DataFrame({"values": np.random.randint(0, 50, 10)}) #dummy data
df.plot(ax=ax)
self.canvas = FigureCanvasTkAgg(self.fig, master=self)
self.canvas.draw()
tk.Label(self, text=f"Graph {title}").grid(row=0)
self.canvas.get_tk_widget().grid(row=1, sticky="nesw")
toolbar_frame = tk.Frame(self)
toolbar_frame.grid(row=2, sticky="ew")
NavigationToolbar2Tk(self.canvas, toolbar_frame)
root = tk.Tk()
for num, i in enumerate(list("ABC")):
Graph(root, title=i, width=200).grid(row=num//2, column=num%2)
text_box = tk.Text(root, width=50, height=10, wrap=tk.WORD)
text_box.grid(row=1, column=1, sticky="nesw")
text_box.delete(0.0, "end")
text_box.insert(0.0, 'My message will be here.')
root.mainloop()
Result:

Tkinter Matplotlib and Thread

In the main loop, a 2 x 2 tkinter grid is cretaed with
one label in each cell of the first line.
In the second line, two Matplotlib figures are crated with a subplot
Two functions are in charge to dynamicly refresh the grid.
They are running each one in a thread.
The first line (two labels) is well refresh by the two functions.
But, in the second line nothing ... no plot !
from tkinter import *
import threading
import time
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib import style
def read_api1():
n = 0
while 1:
n = n + 1
texte1.config(text="fig1 " + str(n))
ax1.plot([1,2], [12,14])
time.sleep(2)
ax1.cla()
def read_api2():
m = 0
while 1:
m = m + 1
texte2.config(text="fig2 " + str(m))
ax2.plot([.1,.2,.3], [2,4,3])
time.sleep(1)
main = Tk()
style.use("ggplot")
texte1 = Label(main, text="fig1")
texte1.grid(row=0,column=0)
fig1 = Figure(figsize=(2, 2), dpi=112)
ax1 = fig1.add_subplot()
fig1.set_tight_layout(True)
graph = FigureCanvasTkAgg(fig1, master=main)
canvas = graph.get_tk_widget()
canvas.grid(row=1, column=0)
texte2 = Label(main, text="fig2")
texte2.grid(row=0,column=1)
fig2 = Figure(figsize=(2, 2), dpi=112)
ax2 = fig2.add_subplot()
fig2.set_tight_layout(True)
graph = FigureCanvasTkAgg(fig2, master=main)
canvas = graph.get_tk_widget()
canvas.grid(row=1, column=1)
t = threading.Thread(target=read_api1)
t.start()
t = threading.Thread(target=read_api2)
t.start()
main.mainloop()
Any help would be appreciated :)
EDIT:
Some more details #furas:
read_api2 is supposed to get data from a WEB API at specific time. So what you recommend (after method) should work.
read_api1 is supposed to acquire data from a serial port (GPIO UART). So the thread will be waiting for data beeing available for reading.
In that case, I don't see how to use the after method
In other words, the question is : how to refresh a matplotlib plot in a tkinter environnement based on asynchronous input ? The asynchronous serial data read cannot be in the mainloop, so I put it in the thread but even with graph.draw(), it does not work. Any suggestion ?
There are two problems:
it needs graph.draw() to update/redraw plot
usually GUIs don't like to run in thread and it seems graph.draw() doesn't work in thread (at least on my Linux).
You may have to use main.after(1000, main_api1) to run the same function after 1000ms (1s) without using thread and without blocking mainloop()
import tkinter as tk # PEP8: `import *` is not preferred
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib import style
import random
# --- functions ---
def read_api1():
global n
n = n + 1
texte1.config(text="fig1 " + str(n))
ax1.cla()
ax1.plot([1,2], [random.randint(0,10),random.randint(0,10)])
graph1.draw()
main.after(1000, read_api1)
def read_api2():
global m
m = m + 1
texte2.config(text="fig2 " + str(m))
ax2.cla()
ax2.plot([.1,.2,.3], [random.randint(0,10),random.randint(0,10),random.randint(0,10)])
graph2.draw()
main.after(1000, read_api2)
# --- main ---
m = 0
n = 0
main = tk.Tk()
style.use("ggplot")
texte1 = tk.Label(main, text="fig1")
texte1.grid(row=0, column=0)
fig1 = Figure(figsize=(2, 2), dpi=112)
ax1 = fig1.add_subplot()
fig1.set_tight_layout(True)
graph1 = FigureCanvasTkAgg(fig1, master=main)
canvas1 = graph1.get_tk_widget()
canvas1.grid(row=1, column=0)
#graph1.draw()
texte2 = tk.Label(main, text="fig2")
texte2.grid(row=0,column=1)
fig2 = Figure(figsize=(2, 2), dpi=112)
ax2 = fig2.add_subplot()
fig2.set_tight_layout(True)
graph2 = FigureCanvasTkAgg(fig2, master=main)
canvas2 = graph2.get_tk_widget()
canvas2.grid(row=1, column=1)
#graph2.draw()
read_api1()
read_api2()
main.mainloop()
EDIT: Example which runs two threads. Every threads generate data in different speed and use two queues to send data to main thread. And main thread use two after() to check two queues and update two plots.
import tkinter as tk # PEP8: `import *` is not preferred
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib import style
import random
import threading
import queue
import time
# --- functions ---
def WEB_API(queue):
# it will run in thread
print('WEB_API: start')
while web_api_running:
value = random.randint(1, 3)
time.sleep(.1)
print('WEB_API:', value)
queue.put(value)
def GPIO_API(queue):
# it will run in thread
print('GPIO_API: start')
while gpio_api_running:
value = random.randint(1, 3)
time.sleep(value)
print('GPIO_API:', value)
queue.put(value)
def read_api1():
global n
global data1
if not queue1.empty():
value = queue1.get()
# remove first item and add new item at the the end
data1 = data1[1:] + [value]
n += 1
texte1.config(text="fig1 " + str(n))
ax1.cla()
ax1.plot(range(10), data1)
graph1.draw()
main.after(100, read_api1)
def read_api2():
global m
global data2
if not queue2.empty():
value = queue2.get()
# remove first item and add new item at the the end
data2 = data2[1:] + [value]
m = m + 1
texte2.config(text="fig2 " + str(m))
ax2.cla()
ax2.plot([.1,.2,.3], data2)
graph2.draw()
main.after(100, read_api2)
# --- before GUI ---
# default data at start (to add new value at the end and remove first value)
data1 = [0,0,0,0,0,0,0,0,0,0]
data2 = [0,0,0]
m = 0
n = 0
# queues to communicate with threads
queue1 = queue.Queue()
queue2 = queue.Queue()
# global variables to control loops in thread
web_api_running = True
gpio_api_running = True
# start threads and send queues as arguments
thread1 = threading.Thread(target=WEB_API, args=(queue1,))
thread1.start()
thread2 = threading.Thread(target=GPIO_API, args=(queue2,))
thread2.start()
# --- GUI ---
main = tk.Tk()
style.use("ggplot")
texte1 = tk.Label(main, text="fig1")
texte1.grid(row=0, column=0)
fig1 = Figure(figsize=(2, 2), dpi=112)
ax1 = fig1.add_subplot()
fig1.set_tight_layout(True)
graph1 = FigureCanvasTkAgg(fig1, master=main)
canvas1 = graph1.get_tk_widget()
canvas1.grid(row=1, column=0)
texte2 = tk.Label(main, text="fig2")
texte2.grid(row=0,column=1)
fig2 = Figure(figsize=(2, 2), dpi=112)
ax2 = fig2.add_subplot()
fig2.set_tight_layout(True)
graph2 = FigureCanvasTkAgg(fig2, master=main)
canvas2 = graph2.get_tk_widget()
canvas2.grid(row=1, column=1)
# draw plots first time
ax1.plot(range(10), data1)
ax2.plot([.1,.2,.3], data2)
# run after which will update data and redraw plots
read_api1()
read_api2()
main.mainloop()
# --- after GUI ---
# stop loops in threads
web_api_running = False
gpio_api_running = False
# wait for end of theads
thread1.join()
thread2.join()

Categories

Resources