dragon123
dragon123

Reputation: 303

Python Tkinter, Display Live Data

I want to display live data in a GUI, in tkinter. The data I am getting contains a list of two integers [current, voltage]. I am getting new data every second.

I managed to create a GUI, now I want to know how to display data in GUI Label widgets (python tkinter) and update labels dynamically. Any suggestions please

Here is my code so far:

#data getting is a list eg. [10, 12]
from tkinter import *
import tkinter.font


#main Window using Tk
win = Tk()

win.title("v1.0")
win.geometry('800x480')
win.configure(background='#CD5C5C')

#Labels
voltage = Label(win, text = "voltage")
voltage.place(x=15, y=100)

current = Label(win, text = "current")
current.place(x=15, y=200)

#display measured values
#how to display here !!!
currentValues = Label(win, text = "want to display somewhere like this")
currentValues.place(x=200, y=100)

voltageValues = Label(win, text = "want to display somewhere like this")
voltageValues.place(x=200, y=200)
mainloop()

Upvotes: 3

Views: 15242

Answers (4)

aVral
aVral

Reputation: 133

I figured out a liveplot inspired by a demo, where I used this to plot the realtime current-voltage scan in Keithley2410.

The whole script is below:

import matplotlib.pyplot as plt
import numpy as np
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from tkinter import ttk

x_data, y_data = [], []

class Win(tk.Tk):

    def __init__(self):
    
        super().__init__()
    
        self.title('I-V liveplot')
        self.geometry('500x450')

        # Frame that holds wigets on the left side
        left_frame = ttk.Frame(self)
        left_frame.pack(side= "left", padx =10, pady = 10)  #, fill="y", expand=True
     
        self.fig = plt.figure(figsize=(4, 3.5), dpi=100)
        self.ax = self.fig.add_subplot(1,1,1)
        self.line, = self.ax.plot([0], [0])
        self.ax.set_xlabel('Voltage / V', fontsize = 12)
        self.ax.set_ylabel('Current / A', fontsize = 12)
        self.fig.tight_layout()
        self.canvas = FigureCanvasTkAgg(self.fig, master=self)
        self.toolbar = NavigationToolbar2Tk(self.canvas, self)
        self.canvas.get_tk_widget().pack(side= tk.BOTTOM)
    
    
        voltage_range_label = tk.Label(left_frame, text = "Voltage range")
        voltage_range_label.pack(side = "top", padx =10, pady =2)
        self.voltage_range = tk.IntVar()
        self.voltage_range.set(10)
        voltage_range_spinbox = ttk.Spinbox(left_frame, from_=-3e2, to = 5e2, textvariable = self.voltage_range, width=8)
        voltage_range_spinbox.pack(side="top", padx =10, pady =5)
    
        voltage_step_label = tk.Label(left_frame, text = "Step")
        voltage_step_label.pack(side = "top", padx =10, pady =2)
        self.step = tk.IntVar()
        self.step.set(1)
        step_spinbox = ttk.Spinbox(left_frame, from_=-3e2, to = 5e2, textvariable = self.step, width =9)
        step_spinbox.pack(side="top", padx =10, pady =5)
    
        self.start = tk.BooleanVar(value = False)
        start_butt = ttk.Button(left_frame, text="Start", command= lambda: self.start.set(True))
        start_butt.pack(side='top', padx =10, pady =10)
         
        stop_butt = ttk.Button(left_frame, text="Resume", command=lambda: self.is_paused.set(False))
        stop_butt.pack(side="top", padx =10, pady =10)    
    
        self.is_paused = tk.BooleanVar()  # variable to hold the pause/resume state
        restart_butt = ttk.Button(left_frame, text="Pause", command=lambda: self.is_paused.set(True))
        restart_butt.pack(side="top", padx =10, pady =10)

    def update(self, k=1):

        if self.start.get() and not self.is_paused.get(): 

            # quasi For Loop
            idx = [i for i in range(0, k, self.step.get())][-1]
            x_data.append(idx)
            y_data.append(np.sin(idx/5))
            self.line.set_data(x_data, y_data)
            self.fig.gca().relim()
            self.fig.gca().autoscale_view()
            self.canvas.draw()
            #self.canvas.flush_events()
            k += self.step.get()[![enter image description here][2]][2]
         
        if k <= self.voltage_range.get():
        
            self.after(1000, self.update, k)


if __name__ == "__main__":
    app = Win()
    app.after(1000, app.update)
    app.mainloop()

This code works properly and results in an output shown in Graph. I hope it would be helpful. enter image description here.

Upvotes: 2

Noctis Skytower
Noctis Skytower

Reputation: 21991

If you want to graph your live data and want to avoid using other libraries to do that for you, you might find the following to be an enlightening starting point for creating your own graphs. The sample draws a full circle of values when evaluating the math.sin function that comes in the standard library. The code takes into account automatic sampling, resizing, and updating as needed and should be fairly responsive.

#! /usr/bin/env python3
import math
import threading
import time
import tkinter.ttk
import uuid
from tkinter.constants import EW, NSEW, SE


class Application(tkinter.ttk.Frame):
    FPS = 10  # frames per second used to update the graph
    MARGINS = 10, 10, 10, 10  # internal spacing around the graph

    @classmethod
    def main(cls):
        tkinter.NoDefaultRoot()
        root = tkinter.Tk()
        root.title('Tkinter Graphing')
        # noinspection SpellCheckingInspection
        root.minsize(640, 480)  # VGA (NTSC)
        cls(root).grid(sticky=NSEW)
        root.grid_rowconfigure(0, weight=1)
        root.grid_columnconfigure(0, weight=1)
        root.mainloop()

    def __init__(self, master=None, **kw):
        super().__init__(master, **kw)
        self.display = tkinter.Canvas(self, background='white')
        self.display.bind('<Configure>', self.draw)
        self.start = StatefulButton(self, 'Start Graphing', self.start_graph)
        self.grip = tkinter.ttk.Sizegrip(self)
        self.grid_widgets(padx=5, pady=5)
        self.data_source = DataSource()
        self.after_idle(self.update_graph, round(1000 / self.FPS))
        self.run_graph = None

    def grid_widgets(self, **kw):
        self.display.grid(row=0, column=0, columnspan=2, sticky=NSEW, **kw)
        self.start.grid(row=1, column=0, sticky=EW, **kw)
        self.grip.grid(row=1, column=1, sticky=SE)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

    def start_graph(self):
        self.run_graph = True
        threading.Thread(target=self.__simulate, daemon=True).start()
        return 'Stop Graphing', self.stop_graph

    def stop_graph(self):
        self.run_graph = False
        return 'Clear Graph', self.clear_graph

    def clear_graph(self):
        self.data_source.clear()
        self.reset_display()
        return 'Start Graphing', self.start_graph

    # def __simulate(self):
    #     # simulate changing populations
    #     for population in itertools.count():
    #         if not self.run_graph:
    #             break
    #         self.data_source.append(population, get_max_age(population, 200))

    # def __simulate(self):
    #     # simulate changing ages
    #     for age in itertools.count(1):
    #         if not self.run_graph:
    #             break
    #         self.data_source.append(age, get_max_age(250_000_000, age))

    def __simulate(self):
        # draw a sine curve
        for x in range(800):
            time.sleep(0.01)
            if not self.run_graph:
                break
            self.data_source.append(x, math.sin(x * math.pi / 400))

    def update_graph(self, rate, previous_version=None):
        if previous_version is None:
            self.reset_display()
        current_version = self.data_source.version
        if current_version != previous_version:
            data_source = self.data_source.copy()
            self.draw(data_source)
        self.after(rate, self.update_graph, rate, current_version)

    def reset_display(self):
        self.display.delete('data')
        self.display.create_line((0, 0, 0, 0), tag='data', fill='black')

    def draw(self, data_source):
        if not isinstance(data_source, DataSource):
            data_source = self.data_source.copy()
        if data_source:
            self.display.coords('data', *data_source.frame(
                self.MARGINS,
                self.display.winfo_width(),
                self.display.winfo_height(),
                True
            ))


class StatefulButton(tkinter.ttk.Button):
    def __init__(self, master, text, command, **kw):
        kw.update(text=text, command=self.__do_command)
        super().__init__(master, **kw)
        self.__command = command

    def __do_command(self):
        self['text'], self.__command = self.__command()


def new(obj):
    kind = type(obj)
    return kind.__new__(kind)


def interpolate(x, y, z):
    return x * (1 - z) + y * z


def interpolate_array(array, z):
    if z <= 0:
        return array[0]
    if z >= 1:
        return array[-1]
    share = 1 / (len(array) - 1)
    index = int(z / share)
    x, y = array[index:index + 2]
    return interpolate(x, y, z % share / share)


def sample(array, count):
    scale = count - 1
    return tuple(interpolate_array(array, z / scale) for z in range(count))


class DataSource:
    EMPTY = uuid.uuid4()

    def __init__(self):
        self.__x = []
        self.__y = []
        self.__version = self.EMPTY
        self.__mutex = threading.Lock()

    @property
    def version(self):
        return self.__version

    def copy(self):
        instance = new(self)
        with self.__mutex:
            instance.__x = self.__x.copy()
            instance.__y = self.__y.copy()
            instance.__version = self.__version
        instance.__mutex = threading.Lock()
        return instance

    def __bool__(self):
        return bool(self.__x or self.__y)

    def frame(self, margins, width, height, auto_sample=False, timing=False):
        if timing:
            start = time.perf_counter()
        x1, y1, x2, y2 = margins
        drawing_width = width - x1 - x2
        drawing_height = height - y1 - y2
        with self.__mutex:
            x_tuple = tuple(self.__x)
            y_tuple = tuple(self.__y)
        if auto_sample and len(x_tuple) > drawing_width:
            x_tuple = sample(x_tuple, drawing_width)
            y_tuple = sample(y_tuple, drawing_width)
        max_y = max(y_tuple)
        x_scaling_factor = max(x_tuple) - min(x_tuple)
        y_scaling_factor = max_y - min(y_tuple)
        coords = tuple(
            coord
            for x, y in zip(x_tuple, y_tuple)
            for coord in (
                round(x1 + drawing_width * x / x_scaling_factor),
                round(y1 + drawing_height * (max_y - y) / y_scaling_factor)))
        if timing:
            # noinspection PyUnboundLocalVariable
            print(f'len = {len(coords) >> 1}; '
                  f'sec = {time.perf_counter() - start:.6f}')
        return coords

    def append(self, x, y):
        with self.__mutex:
            self.__x.append(x)
            self.__y.append(y)
            self.__version = uuid.uuid4()

    def clear(self):
        with self.__mutex:
            self.__x.clear()
            self.__y.clear()
            self.__version = self.EMPTY

    def extend(self, iterable):
        with self.__mutex:
            for x, y in iterable:
                self.__x.append(x)
                self.__y.append(y)
            self.__version = uuid.uuid4()


if __name__ == '__main__':
    Application.main()

Upvotes: 4

David Sidarous
David Sidarous

Reputation: 1272

You can change label text dynamically:

This is a way using textvariable option with StringVar and .set() method

str_var = tk.StringVar(value="Default")

currentValues= Label(win, textvariable=my_string_var)
currentValues.place(x=200, y=100)

str_var.set("New value")

Another way using simply .configure() method

currentValues = Label(win, text = "default")
currentValues.configure(text="New value")

Finally, to make the UI update without waiting the rest of the loop do an update

win.update()

Upvotes: 3

MatthewG
MatthewG

Reputation: 815

I want to display some live data in a GUI.

I think what you want to do is use the .after() method. The .after() method queues tkinter to run some code after a set time.

For example:

currentValues = Label(win, text = "want to display somewhere like this")
currentValues.place(x=200, y=100)

voltageValues = Label(win, text = "want to display somewhere like this")
voltageValues.place(x=200, y=200)


def live_update():
    currentValues['text'] = updated_value
    voltageValues['text'] = updated_value
    win.after(1000, live_update) # 1000 is equivalent to 1 second (closest you'll get)

live_update() # to start the update loop

1000 units in the after method is the closest you'll get to 1 second exactly.

Upvotes: 0

Related Questions