[Tutor] Tkinter and matplotlib
Dennis Lee Bieber
wlfraed at ix.netcom.com
Fri Feb 11 13:50:32 EST 2022
On Fri, 11 Feb 2022 17:35:24 +1100, Phil <phillor9 at gmail.com> declaimed the
following:
>
>There are many examples to be found on the Internet; some are dated and
>use Python2 while some are overly complex. Referring to one that I found
>one that I have greatly simplified (code following) I don't understand
>exactly what the purpose of "lines = ax.plot([],[])[0]" is and where "
>lines.set_xdata(Time) and "lines.set_ydata(data)" comes from. I only
>know that they're needed to plot something. I have checked the
>matplotlib document page and other references.
>
Can't help with the "lines" references but there are a multitude of
other things that jump out at me...
>Would the following code be better worked into a class or not? I suppose
>the canvas goes onto a frame?
>
A class for the functions would be my first choice...
>import time
>from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
>from matplotlib.figure import Figure
>import tkinter as tk
>import pyfirmata
You aren't using pyfirmata in this example...
>
>data = []
>Time = []
>time0 = time.time()
>cnt = 0
>
>def plot_data():
> global data, Time, time0, cnt
Since you never rebind data/Time/time0 they don't need to be declared
global; you do rebind cnt. "Time" and "data" are poorly named -- especially
when you have a module "time" with function ".time()", and time0 (look at
how similar time0 and time() appear). As a class, all these could become
instance variables (self.whatever).
>
> instance = time.time()
> Time.append(instance-time0)
>
> data.append(cnt)
>
I'm not sure what the purpose of "data" is... you are appending a
linearly incrementing counter. Presuming this function is called on a
regular basis (so that the time values are evenly spaced) all you are
plotting is a diagonal line.
> lines.set_xdata(Time) # Is lines.set_xdata() the correct method?
> lines.set_ydata(data)
See prior: you could just pass range(len(Time)) and forget about "data"
>
> canvas.draw()
> cnt += 1
>
> root.after(1000,plot_data)
I would note that using a fixed "1000" (1 second?) doesn't account for
processing overhead above (nor OS overhead/process swapping -- but you
can't do anything about that without using a deterministic real-time OS) so
the collection interval won't be exactly 1 second intervals. If minimizing
jitter is desirable, you can minimize the accumulation of internal
processing overhead by computing the time delta for the next wake-up.
#initialize for first .after() call
last_time = time.time() + 1.0
... .after(... 1000, ...)
#in the loop process before .after()
#find out how much time has elapsed in processing
#NOTE: I'm not checking for overruns!
#current time minus the scheduled start of this pass
delta = time.time() - last_time
#actual wait is 1 second minus overhead time
... .after(..., int((1.0 - delta) * 1000), ...)
#set last_time to "expected" next pass start time
last_time += 1.0
>
>def plot_start():
> global cond
> cond = True
>
>def plot_stop():
> global cond
> cond = False
These are the only places where "cond" is used, so they are currently
meaningless. Was the intention to make the accumulation function skip
adding time points? If so, you need an "if" statement in the data
collection routine to NOT accumulate the time but still reschedule the
collection routine. NOTE that with a 1 second wake-up, the granularity of
data collection will be to the nearest 1 second relative interval. You
would never see any start/stop (or stop/start) button clicks with less than
1 second between them (and maybe not even those that are less than 2
seconds)
>
>
>#-----Main GUI code-----
>root = tk.Tk()
>root.title('Real Time Plot')
>root.geometry("700x500")
>
>#------create Plot object on GUI----------
># add figure canvas
>fig = Figure();
>ax = fig.add_subplot(111) # I know what this means
>
>ax.set_title('Serial Data');
>ax.set_xlabel('Sample')
>ax.set_ylabel('Voltage')
For the code you've provided, these labels are obviously meaningless
("voltage" starts at 0 and goes up by 1 each second; "sample" is the time
in seconds from start of plot).
>ax.set_xlim(0,100)
>ax.set_ylim(0, 100)
What happens when your routine goes over 100 seconds? (and 100 time
points).
>lines = ax.plot([],[])[0] # I don't know this means
What documentation have you looked at...
ax is a (sub)plot of figure, so you would need to follow documented return
values starting at matplotlib.figure to find just which .plot() is being
invoked here.
https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.plot.html#matplotlib.axes.Axes.plot
"""
Axes.plot(*args, scalex=True, scaley=True, data=None, **kwargs)[source]
Plot y versus x as lines and/or markers.
"""
You are passing empty lists for X and Y, so I'd expect nothing is
plotted.
"""
Returns
list of Line2D
A list of lines representing the plotted data.
"""
... which you are subscripting to keep only the first "line" from the list.
https://matplotlib.org/stable/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D
"""
set_data(*args)
Set the x and y data.
Parameters
*args(2, N) array or two 1D arrays
"""
"""
set_xdata(x)
Set the data array for x.
Parameters
x1D array
set_ydata(y)
Set the data array for y.
Parameters
y1D array
"""
Since you are always setting both x and y, using one call to
.set_data() would be more apropos.
FYI: the font name is "calibri" not "calbiri"!
I have a suspicion you've simplified the actual problem to the point
where a solution as I'd write it may be irrelevant. The labels and
inclusion of pyfirmata indicates that you really want to collect data
points from an Arduino or similar. But you have your data collection tied
to your Tk frame update rate which is maybe not the same rate as that at
which the Arduino is generating it. That would mean you need to be able to
separate the accumulation of data from the display update. Assuming you get
the data on-demand, and not asynchronously...
****** COMPLETELY UNTESTED ******
import time
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import tkinter as tk
import pyfirmata
class TVplot():
def __init__(self, tkparent):
self.tkparent = tkparent
self.voltages = []
self.sampletimes = []
self.expectedtime = None
self.running = False
# set up Arduino interface?
fig = Figure()
ax = fig.add_subplot(111)
ax.set_title("Voltage vs Time")
ax.set_xlabel("Time")
ax.set_ylabel("Voltage")
ax.set_xlim(0.0, 100.0)
ax.set_ylim(0.0, 100.0) #uhm... what is the range you really expect
self.line = ax.plot([], [])[0]
self.canvas = FigureCanvaseTkAgg(self.fig,
master=tkparent)
self.canvas.get_tk_widget().place(x=10, y=10,
width=500,
height=400)
self.canvas.draw()
def update(self):
if self.expectedtime is None:
#initialize t0 and jitter correction times
self.expectedtime = time.time()
self.t0 = self.expectedtime
if self.running:
#don't collect if not running
#TBD Arduino access
self.voltages.append(TBD) #something
self.sampletimes.append(time.time() - self.t0)
#something may take time
self.line.set_data(sampletimes, voltages)
self.canvas.draw()
delta = time.time() - self.expectedtime
self.tkparent.after(int((1.0 - delta) * 1000),
self.update)
self.expectedtime += 1.0
def start(self):
self.running = True
def stop(self):
self.running = False
#-----Main GUI code-----
root = tk.Tk()
root.title('Real Time Plot')
root.geometry("700x500")
aPlot = TVplot(root)
#----------create button---------
start = tk.Button(root, text = "Start",
font = ('calibri',12),
command = lambda: aPlot.start())
# I wonder if you need the lambda?
# command = aPlot.start ) #no () on command
start.place(x = 100, y = 450 )
stop = tk.Button(root, text = "Stop",
font = ('calibri',12),
command = lambda: aPlot.stop()) #ditto
stop.place(x = start.winfo_x()+start.winfo_reqwidth() + 20, y = 450)
root.after(1000, aPlot.update)
root.mainloop()
--
Wulfraed Dennis Lee Bieber AF6VN
wlfraed at ix.netcom.com http://wlfraed.microdiversity.freeddns.org/
More information about the Tutor
mailing list