Find peak height for a random list of potentiostat data files, interactive version
Usage Video: Demonstration of Use
Sample Data files: Voltammograms
—————————————
#!/usr/bin/env python3 """This program finds peak height values (i.e., peak currents) from .txt files and .csv files containing squarewave voltammogram data, using any selected files. """ __author__ = "Andrew J. Bonham" __copyright__ = "Copyright 2010-2019, Andrew J. Bonham" __credits__ = ["Andrew J. Bonham"] __version__ = 1.5 __maintainer__ = "Andrew J. Bonham" __email__ = "bonham@gmail.com" __status__ = "Production" # Setup: import basic modules we need import csv import os import platform import re import sys import numpy from scipy.optimize import leastsq import pylab import tkinter from tkinter import filedialog as tkFileDialog # Setup Type annotation from typing import List, Tuple, Any # Setup: Fix OS specific problems with ttk and tkinter try: import ttk except ImportError: import tkinter as ttk if platform.system() == "Darwin": import matplotlib matplotlib.use("TkAgg") # Define our Classes class PointBrowser: """This is the class that draws the main graph of data for Peak Finder. This class creates a line and point graph with clickable points. When a point is clicked, it calls the normal test Fit display from the main logic.""" def __init__( self, app: "PeakFinderApp", logic: "PeakLogicFiles", xticksRaw: numpy.ndarray, y: List[float], fileTitle: str, ) -> None: """Create the main output graph and make it clickable.""" self.app: "PeakFinderApp" = app self.logic: "PeakLogicFiles" = logic self.x: List[float] = list(range(len(xticksRaw))) self.y: List[float] = y self.xticks: Tuple[float, ...] = tuple(xticksRaw) self.loc: List[float] = [d + 0.5 for d in self.x] # Setup the matplotlib figure self.fig = pylab.figure(1) self.ax = self.fig.add_subplot(111) self.ax.plot(self.x, self.y, "bo-", picker=5) self.fig.canvas.mpl_connect("pick_event", self.onpick) self.fig.subplots_adjust(left=0.17) self.ax.set_xlabel("File") self.ax.get_xaxis().set_ticks([]) self.ax.set_ylabel("Current (A)") self.ax.ticklabel_format(style="sci", scilimits=(0, 0), axis="y") self.ax.set_title("Peak Current for {}".format(str(fileTitle))) # Track which point is selected by the user self.lastind: int = 0 # Add our data self.selected = self.ax.plot( [self.x[0]], [self.y[0]], "o", ms=12, alpha=0.4, color="yellow", visible=True, ) # Display button to fetch data self.axb = pylab.axes([0.75, 0.03, 0.15, 0.05]) self.button = pylab.Button(self.axb, "Results") # self.ax.plot._test = self.button # self.button.on_clicked(self.app.data_popup) # Draw the figure self.fig.canvas.draw() pylab.show() def onpick(self, event: Any) -> None: """Capture the click event, find the corresponding data point, then update accordingly.""" thisone = event.artist x = thisone.get_xdata() y = thisone.get_ydata() dataind = event.ind self.selected[0].set_data(x[dataind], y[dataind]) self.logic.test_fit(dataind) self.fig.canvas.draw() pylab.show() def update(self) -> None: """Update the main graph and call my response function.""" if self.lastind is None: return dataind: int = self.lastind self.selected.set_visible(True) self.selected.set_data(self.x[dataind], self.y[dataind]) self.logic.test_fit(dataind) self.fig.canvas.draw() pylab.show() class PeakFinderApp(tkinter.Tk): """This is the gui for Peak Finder. This application displays a minimal user interface to select a directory and to specify file formats and output filename, then reports on the programs function with a simple progressbar.""" def __init__(self) -> None: """set up the peak finder app GUI.""" self.directory_manager() tkinter.Tk.__init__(self) self.window_title = "Any Peak Finder {}".format(str(__version__)) # invoke PeakLogicFiles to do the actual work logic: "PeakLogicFiles" = PeakLogicFiles(self) # create the frame for the main window self.title(self.window_title) self.geometry("+100+100") self.resizable(tkinter.FALSE, tkinter.FALSE) mainframe = ttk.Frame(self) mainframe.grid( column=0, row=0, sticky=(tkinter.N, tkinter.W, tkinter.E, tkinter.S), padx=5, pady=5, ) mainframe.columnconfigure(0, weight=1) mainframe.rowconfigure(0, weight=1) # define our variables in tkinter-form and give sane defaults self.filename_ = tkinter.StringVar(value="output1") self.output = tkinter.StringVar() self.filenames_: List[str] = [] self.dir_selected = tkinter.IntVar(value=0) self.init_potential_ = tkinter.DoubleVar(value=-0.15) self.final_potential_ = tkinter.DoubleVar(value=-0.35) self.peak_center_ = tkinter.DoubleVar(value=-0.25) self.final_edge_ = tkinter.DoubleVar(value=-1) self.init_edge_ = tkinter.DoubleVar(value=1) # display the entry boxes ttk.Label(mainframe, text=self.window_title, font="helvetica 12 bold").grid( column=1, row=1, sticky=tkinter.W, columnspan=2 ) self.filename_entry = ttk.Entry( mainframe, width=12, textvariable=self.filename_ ).grid(column=2, row=2, sticky=(tkinter.W, tkinter.E)) ttk.Label(mainframe, text="Filename:").grid(column=1, row=2, sticky=tkinter.W) self.init_potential = ttk.Entry( mainframe, width=12, textvariable=self.init_potential_ ).grid(column=2, row=7, sticky=(tkinter.W, tkinter.E)) ttk.Label(mainframe, text="Initial Potential:").grid( column=1, row=7, sticky=tkinter.W ) self.final_potential = ttk.Entry( mainframe, width=12, textvariable=self.final_potential_ ).grid(column=2, row=8, sticky=(tkinter.W, tkinter.E)) ttk.Label(mainframe, text="Final Potential:").grid( column=1, row=8, sticky=tkinter.W ) self.peak_center = ttk.Entry( mainframe, width=12, textvariable=self.peak_center_ ).grid(column=2, row=9, sticky=(tkinter.W, tkinter.E)) ttk.Label(mainframe, text="Peak Center:").grid( column=1, row=9, sticky=tkinter.W ) ttk.Label(mainframe, text="Peak Location Guess", font="helvetica 10 bold").grid( column=1, row=5, sticky=tkinter.W ) ttk.Label(mainframe, text="(only change if necessary)").grid( column=1, row=6, sticky=tkinter.W ) # Display Generate button ttk.Button(mainframe, text="Find Peaks", command=logic.peakfinder).grid( column=1, row=10, sticky=tkinter.W ) # Display test fit button ttk.Button(mainframe, text="Test fit", command=logic.test_fit).grid( column=2, row=10, sticky=tkinter.W ) # Display Directory button ttk.Button(mainframe, text="Select Files", command=self.files_select).grid( column=1, row=4, sticky=tkinter.W ) # Show the output ttk.Label(mainframe, textvariable=self.output).grid( column=1, row=13, sticky=(tkinter.W, tkinter.E), columnspan=5 ) # Set up Outlier correction ttk.Label(mainframe, text="Regions to include (from -> to):").grid( column=1, row=12, sticky=tkinter.W ) self.final_edge = ttk.Entry( mainframe, width=12, textvariable=self.final_edge_ ).grid(column=1, row=13, sticky=(tkinter.E)) self.init_edge = ttk.Entry( mainframe, width=12, textvariable=self.init_edge_ ).grid(column=2, row=13, sticky=(tkinter.W, tkinter.E)) # Pad the windows for prettiness for child in mainframe.winfo_children(): child.grid_configure(padx=5, pady=5) # Display a progressbar self.bar: "ProgressBar" = ProgressBar(mainframe, width=100, height=10) self.bar.update(0) ttk.Label(mainframe, text="").grid( column=1, row=16, sticky=(tkinter.W, tkinter.E) ) # add a system menu self.option_add("*tearOff", tkinter.FALSE) self.menubar = tkinter.Menu(self) self["menu"] = self.menubar self.menu_file = tkinter.Menu(self.menubar) self.menubar.add_cascade(menu=self.menu_file, label="File") self.menu_file.add_command(label="Exit", command=self.destroy) self.menu_help = tkinter.Menu(self.menubar, name="help") self.menubar.add_cascade(menu=self.menu_help, label="Help") self.menu_help.add_command(label="How to Use", command=self.help_popup) self.menu_help.add_command( label="About Peak Finder...", command=self.about_popup ) # Run the program pylab.show() self.mainloop() def directory_manager(self) -> None: """Set the initial directory to the users home directory.""" if platform.system() == "Windows": # import windows file management from pathlib import Path self.mydocs: Any = str(Path.home()) else: # import mac/linux file management self.mydocs: Any = os.getenv("HOME") os.chdir(self.mydocs) def about_popup(self) -> None: """Display a pop-up menu about the program.""" self.aboutDialog = tkinter.Toplevel(self) self.aboutDialog.resizable(tkinter.FALSE, tkinter.FALSE) self.aboutDialog.geometry("+400+100") aboutframe = ttk.Frame(self.aboutDialog, width="200", height="200") aboutframe.grid_propagate(0) aboutframe.grid( column=0, row=0, sticky=(tkinter.N, tkinter.W, tkinter.E, tkinter.S), padx=5, pady=5, ) aboutframe.columnconfigure(0, weight=1) aboutframe.rowconfigure(0, weight=1) ttk.Label( aboutframe, text=self.window_title, font="helvetica 12 bold", anchor="center", ).grid(column=0, row=0, sticky=(tkinter.N, tkinter.W, tkinter.E)) ttk.Label( aboutframe, text=( "Voltammogram data analysis\nsoftware for " "CH Instruments data.\n\nWritten by\n{0}\n" "http://www.andrewjbonham.com\n{1}\n\n\n" ).format(str(__author__), str(__copyright__)), anchor="center", justify="center", ).grid(column=0, row=1, sticky=tkinter.N) ttk.Button(aboutframe, text="Close", command=self.aboutDialog.destroy).grid( column=0, row=4, sticky=(tkinter.S) ) self.aboutDialog.mainloop() def help_popup(self) -> None: """Display a pop-up menu explaining how to use the program.""" self.helpDialog = tkinter.Toplevel(self) self.helpDialog.resizable(tkinter.FALSE, tkinter.FALSE) self.helpDialog.geometry("+400+100") helpframe = ttk.Frame(self.helpDialog, width="200", height="240") helpframe.grid( column=0, row=0, sticky=(tkinter.N, tkinter.W, tkinter.E, tkinter.S), padx=5, pady=5, ) helpframe.columnconfigure(0, weight=1) helpframe.rowconfigure(0, weight=1) ttk.Label( helpframe, text=("{} Help".format(self.window_title)), font="helvetica 12 bold", anchor="center", ).grid(column=0, row=0, sticky=(tkinter.N, tkinter.W, tkinter.E)) helptext = tkinter.Text(helpframe, width=40, height=9) helpmessage = ( "Peak Finder is used to find the peak current for a " "methylene blue reduction peak for CH instruments voltammogram " "data files. The data files must be sequentially numbered " "in the specified directory and be in text format.\n\n" "The initial and final potential should be adjusted to lie " "outside the gaussian peak." ) helptext.insert("1.0", helpmessage) helptext.config( state="disabled", bg="SystemButtonFace", borderwidth=0, font="helvetica 10", wrap="word", ) helptext.grid(column=0, row=1, sticky=tkinter.N, padx=10) ttk.Button(helpframe, text="Close", command=self.helpDialog.destroy).grid( column=0, row=4, sticky=(tkinter.S) ) self.helpDialog.mainloop() def data_popup(self, event: Any) -> None: """Display a pop-up window of data.""" filename: str = "{}.csv".format(str(self.filename_.get())) self.dataDialog = tkinter.Toplevel(self) self.dataDialog.resizable(tkinter.FALSE, tkinter.FALSE) self.dataDialog.geometry("+400+100") dataframe = ttk.Frame(self.dataDialog, width="300", height="600") dataframe.grid_propagate(0) dataframe.grid( column=0, row=0, sticky=(tkinter.N, tkinter.W, tkinter.E, tkinter.S), padx=5, pady=5, ) dataframe.columnconfigure(0, weight=1) dataframe.rowconfigure(0, weight=1) ttk.Label( dataframe, text="Data for {}".format(str(filename)), font="helvetica 12 bold", anchor="center", ).grid(column=0, row=0, sticky=(tkinter.N, tkinter.W, tkinter.E)) # Read the output data self.df = csv.reader(open(filename), delimiter=",") listfile: List[List[str]] = list(self.df) data_formatter: List[str] = [] for item in listfile: data_formatter.append(" ".join(map(str, item))) data_output: str = "\n".join(map(str, data_formatter)) # Display it self.datatext = tkinter.Text(dataframe, width=30, height=33) self.datatext.grid(column=0, row=1, sticky=(tkinter.N, tkinter.W, tkinter.E)) self.datatext.insert("1.0", data_output) ttk.Button(dataframe, text="Close", command=self.dataDialog.destroy).grid( column=0, row=4, sticky=(tkinter.S) ) self.dataDialog.mainloop() def files_select(self) -> None: """Allow user to select a directory where datafiles are located.""" # filenamesRaw = tkinter.filedialog.askopenfilenames( filenamesRaw = tkFileDialog.askopenfilenames( title="Title", filetypes=[("CSV Files", "*.csv"), ("TXT Files", "*.txt")] ) self.filenames_ = list(filenamesRaw) self.dir_selected.set(1) class PeakLogicFiles: """This is the internal logic of Peak Finder. PeaklogicFiles looks at a user-provided list of files, then fits the square wave voltammogram data inside to a non-linear polynomial plus gaussian function, subtracts the polynomial, and reports the peak current. Ultimately, it prints a pretty graph of the data.""" def __init__(self, app: PeakFinderApp) -> None: """Initialize PeakLogic, passing the gui object to this class.""" self.app: PeakFinderApp = app def peakfinder(self) -> List[float]: """PeakLogic.peakfinder is the core function to find and report peak current values.""" # Make sure the user has selected files if int(self.app.dir_selected.get()) == 1: # grab the text variables that we need filename: str = str(self.app.filename_.get()) filenamesList: List[str] = self.app.filenames_ path: str = os.path.dirname(os.path.normpath(filenamesList[0])) self.app.bar.set_maxval(len(filenamesList) + 1) os.chdir(path) # open our self.output file and set it up with open("{}.csv".format(str(filename)), "w") as self.g: self.g.write("{}\n\n".format(str(filename))) self.g.write("Fitting Parameters\n") self.g.write( "Init Potential,{}\n".format(str(self.app.init_potential_.get())) ) self.g.write( "Final Potential,{}\n".format(str(self.app.final_potential_.get())) ) self.g.write( "Peak Center,{}\n".format(str(self.app.peak_center_.get())) ) self.g.write("Left Edge,{}\n".format(str(self.app.init_edge_.get()))) self.g.write( "Right Edge,{}\n\n".format(str(self.app.final_edge_.get())) ) self.g.write("--------,--------\n") self.g.write("time,file,peak current\n") # run the peakfinder printing_list: List iplist: List printing_list, iplist = self.loop_for_peaks_files(filenamesList) # Catch if peakfinder failed if not all([printing_list, iplist]): raise # Show the user what was found self.app.output.set("Wrote output to {}.csv".format(filename)) # mainGraph = PointBrowser(self.app, self, printing_list, iplist, filename) return iplist else: return [] def loop_for_peaks_files( self, filenamesList: List[str] ) -> Tuple[List[int], List[float]]: """PeakLogic.loopForPeaks() will loop through each file, collecting data and sending it to the peak_math function.""" # clear some lists to hold our data full_x_lists: List[List[str]] = [] full_y_lists: List[List[str]] = [] startT: int = -1 timelist: List[int] = [] printing_list: List[str] = [] pylab.close(2) # close test fitting graph if open for each in filenamesList: # loop through each file try: dialect = csv.Sniffer().sniff(open(each).read(1024), delimiters="\t,") open(each).seek(0) self.f = csv.reader(open(each), dialect) listfile: List[List[str]] = list(self.f) t_list: List[int] = [] y_list: List[str] = [] rx_list: List[str] = [] # remove the header rows from the file, leaving just the data start_pattern: int = 3 for index, line in enumerate(listfile): # try: if line[0]: if re.match("Potential*", line[0]): start_pattern = index # except Exception: # pass datalist: List[List[str]] = listfile[start_pattern + 2 :] pointT: int = 1000 # if it's the first data point, set the initial time to zero if startT == -1: startT = pointT # subtract init time to get time since the start of the trial pointTcorr: int = pointT - startT t_list.append(pointTcorr) for row in datalist: if row == []: # skip empty lines pass else: if row[0]: rx_list.append(row[0]) if row[1]: y_list.append(row[1]) else: pass full_x_lists.append(rx_list) full_y_lists.append(y_list) timelist.append(pointTcorr) justName: str = os.path.split(each)[1] printing_list.append(justName) except Exception: pass iplist: List[float] = self.peak_math(full_x_lists, full_y_lists) # write the output csv file for i, v, y in zip(iplist, timelist, printing_list): self.g.write("{0},{1},{2}\n".format(str(v), str(y), str(i))) # return time and peak current for graphing return timelist, iplist def peak_math(self, listsx: numpy.ndarray, listsy: numpy.ndarray) -> List[float]: """PeakLogic.peak_math() passes each data file to .fitting_math, and returns a list of peak currents.""" iplist: List[float] = [] count: int = 1 for xfile, yfile in zip(listsx, listsy): ip: float = self.fitting_math(xfile, yfile, 1) if ip < 0: ip = 0 iplist.append(ip) self.app.bar.update(count) count = count + 1 return iplist def fitting_math( self, xfile: numpy.ndarray, yfile: numpy.ndarray, flag: int = 1 ) -> Any: """PeakLogic.fitting_math() fits the data to a cosh and a gaussian, then subtracts the cosh to find peak current..""" try: init_pot: float = self.app.init_potential_.get() final_pot: float = self.app.final_potential_.get() edgelength: float = numpy.abs(init_pot - final_pot) center: float = self.app.peak_center_.get() x: numpy.ndarray = numpy.array(xfile, dtype=numpy.float64) y: numpy.ndarray = numpy.array(yfile, dtype=numpy.float64) # fp is full portion with the exp / cosh plus gaussian def fp(v: List[float], x: numpy.ndarray) -> numpy.ndarray: return ( (v[0] * (x ** 2)) + (v[0] * x) + v[1] + v[3] * numpy.exp(-(((x - v[4]) ** 2) / (2 * v[5] ** 2))) ) # pp is just the exp / cosh portion def pp(v: List[float], x: numpy.ndarray) -> numpy.ndarray: return (v[0] * (x ** 2)) + (v[0] * x) + v[1] # e is the error of the full fit from the real data def e(v: List[float], x: numpy.ndarray, y: numpy.ndarray) -> numpy.ndarray: return fp(v, x) - y # cut out outliers passingx: numpy.ndarray passingy: numpy.ndarray passingx, passingy = self.trunc_edges(xfile, yfile) # cut out the middle values and return the edges _outx: numpy.ndarray outy: numpy.ndarray _outx, outy = self.trunc_list(passingx, passingy) AA: float = numpy.average(outy) less: numpy.ndarray = passingx < init_pot greater: numpy.ndarray = passingx > final_pot PeakHeight: float try: PeakHeight = numpy.max(passingy[less & greater]) except Exception: # TODO: More specific exception PeakHeight = numpy.max(passingy) # give reasonable starting values for non-linear regression v0: List[float] = [ 0.5, AA, numpy.average(passingx), PeakHeight, center, edgelength / 6, ] # fit the background and baseline to all data v: List[float] _success: Any v, _success = leastsq(e, v0, args=(passingx, passingy)) ip: numpy.ndarray = fp(v, v[4]) - pp(v, v[4]) if flag == 1: return ip if flag == 0: return x, y, fp(v, passingx), pp(v, passingx), ip, passingx except Exception: print("Error Fitting") print(sys.exc_info()) return -1 def trunc_list( self, listx: numpy.ndarray, listy: numpy.ndarray ) -> Tuple[numpy.ndarray, numpy.ndarray]: """Remove the central portions of an x-y data list (where we suspect the gaussian is found).""" newx: List[float] = [] newy: List[float] = [] start_spot: str start_h: str for start_spot, start_h in zip(listx, listy): spot: float = float(start_spot) h: float = float(start_h) low: float = float(self.app.final_potential_.get()) high: float = float(self.app.init_potential_.get()) if spot < low: # add low values newx.append(spot) newy.append(h) else: pass if spot > high: # add high values newx.append(spot) newy.append(h) else: pass px: numpy.ndarray = numpy.array(newx, dtype=numpy.float64) py: numpy.ndarray = numpy.array(newy, dtype=numpy.float64) return px, py def trunc_edges( self, listx: numpy.ndarray, listy: numpy.ndarray ) -> Tuple[numpy.ndarray, numpy.ndarray]: """PeakLogic.trunc_edges() removes outlier regions of known bad signal from an x-y data list and returns the inner edges.""" newx: List[float] = [] newy: List[float] = [] start_spot: str start_h: str for start_spot, start_h in zip(listx, listy): spot: float = float(start_spot) h: float = float(start_h) low: float = float(self.app.final_edge_.get()) high: float = float(self.app.init_edge_.get()) if spot > low: # add low values if spot < high: newx.append(spot) newy.append(h) else: pass else: pass # convert results back to an array px: numpy.ndarray = numpy.array(newx, dtype=numpy.float64) py: numpy.ndarray = numpy.array(newy, dtype=numpy.float64) return px, py # return partial x and partial y def test_fit(self, dataind: int = 0) -> None: """Perform a fit for the first data point and display it for the user.""" # Make sure the user has selected a directory if int(self.app.dir_selected.get()) == 1: try: filenamesList: List[str] = self.app.filenames_ file: str = filenamesList[dataind] dialect: Any = csv.Sniffer().sniff( open(file).read(1024), delimiters="\t," ) open(file).seek(0) # open the first data file self.testfile: Any = csv.reader(open(file), dialect) listfile: List[str] = list(self.testfile) # remove the header rows from the file, leaving just the data start_pattern: int = 3 for index, line in enumerate(listfile): try: if line[0]: if re.match("Potential*", line[0]): start_pattern = index except Exception: pass datalist: List[str] = listfile[start_pattern + 2 :] x_list: List[str] = [] y_list: List[str] = [] for row in datalist: if row == []: # skip empty lines pass else: if row[0]: x_list.append(row[0]) if row[1]: y_list.append(row[1]) else: pass x: numpy.ndarray y: numpy.ndarray y_fp: numpy.ndarray y_pp: numpy.ndarray ip: float px: numpy.ndarray x, y, y_fp, y_pp, ip, px = self.fitting_math(x_list, y_list, flag=0) self.test_grapher(x, y, y_fp, y_pp, file, ip, px) except Exception: pass else: pass def test_grapher( self, x: numpy.ndarray, y: numpy.ndarray, y_fp: numpy.ndarray, y_pp: numpy.ndarray, file: str, ip: float, px: numpy.ndarray, ) -> None: """PeakLogic.test_grapher() displays a graph of the test fitting.""" pylab.close(2) # close previous test if open file_name: str = os.path.basename(file) self.fig2 = pylab.figure(2) self.ax2 = self.fig2.add_subplot(111) self.ax2.plot(x, y, "ro", label="data") self.ax2.plot(px, y_fp, label="fit") self.ax2.plot(px, y_pp, label="baseline") self.ax2.set_xlabel("Potential (V)") self.ax2.set_ylabel("Current (A)") self.ax2.set_title("Fit of {}".format(str(file_name))) self.ax2.ticklabel_format(style="sci", scilimits=(0, 0), axis="y") self.ax2.legend() self.fig2.subplots_adjust(bottom=0.15) self.fig2.subplots_adjust(left=0.15) self.text = self.ax2.text( 0.05, 0.95, "Peak Current:\n%.2e A" % ip, transform=self.ax2.transAxes, va="top", ) self.fig2.canvas.draw() pylab.show() class ProgressBar: """Create a tkinter Progress bar widget.""" def __init__( self, root: Any, width: float = 100, height: float = 10, maxval: float = 100 ) -> None: """Initialize ProgressBar to make the tkinter progressbar.""" self.root = root self.maxval: float = float(maxval) self.canvas = tkinter.Canvas( self.root, width=width, height=height, highlightt=0, relief="ridge", borderwidth=2, ) self.canvas.create_rectangle(0, 0, 0, 0, fill="blue") self.label = ttk.Label(self.root, text="Progress:").grid( column=1, row=14, sticky=tkinter.W ) self.canvas.grid(column=1, row=15, sticky=(tkinter.W, tkinter.E), columnspan=3) def set_maxval(self, maxval: float) -> None: """ProgressBar.set_maxval() sets the max value of the progressbar.""" self.maxval: float = float(maxval) def update(self, value: float = 0) -> None: """ProgressBar.update() updates the progressbar to a specified value.""" if value < 0: value = 0 elif value > self.maxval: value = self.maxval self.canvas.delete(tkinter.ALL) self.canvas.create_rectangle( 0, 0, self.canvas.winfo_width() * value / self.maxval, self.canvas.winfo_reqheight(), fill="blue", ) self.root.update() # Main magic if __name__ == "__main__": app: PeakFinderApp = PeakFinderApp()