admin管理员组

文章数量:1392098

I am working on a python tkinter GUI, for plotting/modifying plots for device calibration and data analysis. Basically, I want to list various columns of data and then select regions of said data for analysis. I am successfully importing data, creating a dataframe in Pandas, and using Matplotlib/seaborn to graph the data I care about. However, when adjusting the plot based on my selections, I keep getting a pop-up of the graph in its own window, rather than just updating the plot in its current frame. This happens each time I recall the main plot() function. Is there something I'm missing with either calling or closing the graphs? Thanks for any information, I am still newish to python and stackoverflow.

I have tried a few things, like directly using plt.close to attempt to remove the graph before the pop up, isolating the figure and axis creation for matplotlib, and using seaborn for the lineplot instead of mpl in case the axis handling is different; but I still seem to get erroneous pop up plots. Maybe I'm just missing something obvious with how the function does or does not return things... Below is some code to recreate the example.

#My Imports
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.figure import Figure 
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg,  
NavigationToolbar2Tk)

from matplotlib.backend_bases import key_press_handler
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import filedialog
from datetime import datetime
from PIL import ImageTk, Image
import seaborn as sns
from scipy.stats import linregress


#Setting the TKinter window settings
window = tk.Tk()
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
window.title("Data Visualizer - Plotting in TKinter")
window.geometry(str(int(screen_width*.8))+"x"+str(int(screen_height*.8)))

#A function so that selecting the graph doesn't auto plot
def nullFunction(*args):
    print("nullFunction called")

#Create test pd.DataFrame
#Just something simple to visualize; more columns in actual data
df = pd.DataFrame(
    {"time" : [0,1,2,3,4,5,6,7,8,9],
     "linear" : [0,1,2,3,4,5,6,7,8,9],
     "random" : [0,8,4,6,4,7,1,9,2,6],
     "inverse" : [9,8,7,6,5,4,3,2,1,0]}
)

#List the various data columns to be selected later, don't list 'time'
#Called on button press after df is created/uploaded
def generate_List():
    i = 0
    listbox.delete('0','end')
    for column in df.columns:
      if column == 'time':
        pass
      else:
        listbox.insert(i,column)
        if (i%2) == 0:
            listbox.itemconfigure(i, background = '#f0f0f0')
        i = i+1
    plt.ion()

#make figure as its own element
class FigureCreate:
    def __init__(self, figsize=(5, 5)):
        self.fig = plt.figure(figsize=figsize)
    
    def get_figure(self):
        return self.fig

#make axes based on listbox selection
class AxesCreate:
    def __init__(self, figure, position=(1, 1, 1)):
        self.ax = figure.add_subplot(*position)
        self.figure = figure
    
    def get_axes(self):
        return self.ax

    def get_fig(self):
        return self.figure

#plot a preselected group of elements; not currently used
class CanvasDraw:
    def __init__(self, axes):
        self.ax = axes
    
    def plot(self, x, y, label=''):
        self.ax.plot(x, y, label=label)
    
    def show(self):
        self.ax.legend()
        plt.show()

#Called on button press after list is created
def plotSet(*args):
    plt.close

    #Possibly redundant figure and axis creation methods
    fig_create = FigureCreate(figsize=(5,5))
    fig = fig_create.get_figure()
    
    ax_create = AxesCreate(fig, position=(1,1,1))
    ax = ax_create.get_axes()

    #canvas_draw = CanvasDraw(ax)
    
    selection = listbox.curselection()
    
    for trace in selection:
        sns.lineplot(data=df, x='time', y=df.columns[trace],ax=ax)

    #Vlines that will be adjustable after plot issues are solved
    plt.axvline(df['time'][2],color='green',linestyle='--')
    plt.axvline(df['time'][4],color='red',linestyle='--')

    return (ax_create.get_fig())

def plot_MC(fig):
    # Creating the Tkinter canvas containing the figure
    #THIS IS WHERE I THINK THE PROBLEM IS, BUT I'M NOT SURE
    #I thought this would make the canvas in the MC frame, but it seems to want a new plot since plotSet() is returning a figure
    #Is there a better way to plot specifically in the middle frame, and not force plotSet() to return anything?   
    canvas = FigureCanvasTkAgg(fig, master=frame_MC)   
    canvas.draw() 
  
    # Placing the canvas on the Tkinter window 
    canvas.get_tk_widget().grid(row=0, column=0) 
  
    # Creating the Matplotlib toolbar 
    toolbar = NavigationToolbar2Tk(canvas, frame_MC, pack_toolbar=False) 
    toolbar.update() 
    toolbar.grid(row=1, column=0)

def are_you_sure():
    """
    Just a function that opens a message box to confirm window destruction
    """

    if tk.messagebox.askyesno("Quit Dialog","Are you sure you want to quit the app?"):
        window.destroy()

#MC 'Middle-Center'
frame_MC = tk.Frame(window)
#ML 'Middle-Left'
list_frame=tk.Frame(window)
listbox=tk.Listbox(list_frame, selectmode='multiple', height=30, width=30)
listbox.bind('<<ListboxSelect>>', plotSet)
scrollbar=tk.Scrollbar(list_frame, orient="vertical")
listbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command = listbox.yview)
#TL 'Top-Left'
frame_TL = tk.Frame(window)
generate_list_button = tk.Button(frame_TL, text="generate list", command=generate_List, width=30)
generate_list_button.grid(row=0, column=0, columnspan=3)
plot_test_button = tk.Button(frame_TL, text="plot selection", command=plot_MC(plotSet()))
plot_test_button.grid(row=1, column=0, columnspan=3)
#BL 'Bottom-Left'
frame_BL = tk.Frame(window)
button_quit = tk.Button(master = frame_BL, text = "Quit", command = are_you_sure, bg = '#FF2222')
button_quit.grid(row = 0, column = 0)

#Placing elements
frame_TL.grid(row=0, column = 0)
frame_MC.grid(row = 1, column = 1)
frame_BL.grid(row = 2, column = 0)
list_frame.grid(row = 1, column = 0)
listbox.pack(side = "left", fill = "y")
scrollbar.pack(side = "right", fill = "y")

#run the gui
window.mainloop()

Are there any obvious improvements I could make to clean up the plotting so I don't have this problem? Thank you in advance.

I am working on a python tkinter GUI, for plotting/modifying plots for device calibration and data analysis. Basically, I want to list various columns of data and then select regions of said data for analysis. I am successfully importing data, creating a dataframe in Pandas, and using Matplotlib/seaborn to graph the data I care about. However, when adjusting the plot based on my selections, I keep getting a pop-up of the graph in its own window, rather than just updating the plot in its current frame. This happens each time I recall the main plot() function. Is there something I'm missing with either calling or closing the graphs? Thanks for any information, I am still newish to python and stackoverflow.

I have tried a few things, like directly using plt.close to attempt to remove the graph before the pop up, isolating the figure and axis creation for matplotlib, and using seaborn for the lineplot instead of mpl in case the axis handling is different; but I still seem to get erroneous pop up plots. Maybe I'm just missing something obvious with how the function does or does not return things... Below is some code to recreate the example.

#My Imports
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.figure import Figure 
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg,  
NavigationToolbar2Tk)

from matplotlib.backend_bases import key_press_handler
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import filedialog
from datetime import datetime
from PIL import ImageTk, Image
import seaborn as sns
from scipy.stats import linregress


#Setting the TKinter window settings
window = tk.Tk()
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
window.title("Data Visualizer - Plotting in TKinter")
window.geometry(str(int(screen_width*.8))+"x"+str(int(screen_height*.8)))

#A function so that selecting the graph doesn't auto plot
def nullFunction(*args):
    print("nullFunction called")

#Create test pd.DataFrame
#Just something simple to visualize; more columns in actual data
df = pd.DataFrame(
    {"time" : [0,1,2,3,4,5,6,7,8,9],
     "linear" : [0,1,2,3,4,5,6,7,8,9],
     "random" : [0,8,4,6,4,7,1,9,2,6],
     "inverse" : [9,8,7,6,5,4,3,2,1,0]}
)

#List the various data columns to be selected later, don't list 'time'
#Called on button press after df is created/uploaded
def generate_List():
    i = 0
    listbox.delete('0','end')
    for column in df.columns:
      if column == 'time':
        pass
      else:
        listbox.insert(i,column)
        if (i%2) == 0:
            listbox.itemconfigure(i, background = '#f0f0f0')
        i = i+1
    plt.ion()

#make figure as its own element
class FigureCreate:
    def __init__(self, figsize=(5, 5)):
        self.fig = plt.figure(figsize=figsize)
    
    def get_figure(self):
        return self.fig

#make axes based on listbox selection
class AxesCreate:
    def __init__(self, figure, position=(1, 1, 1)):
        self.ax = figure.add_subplot(*position)
        self.figure = figure
    
    def get_axes(self):
        return self.ax

    def get_fig(self):
        return self.figure

#plot a preselected group of elements; not currently used
class CanvasDraw:
    def __init__(self, axes):
        self.ax = axes
    
    def plot(self, x, y, label=''):
        self.ax.plot(x, y, label=label)
    
    def show(self):
        self.ax.legend()
        plt.show()

#Called on button press after list is created
def plotSet(*args):
    plt.close

    #Possibly redundant figure and axis creation methods
    fig_create = FigureCreate(figsize=(5,5))
    fig = fig_create.get_figure()
    
    ax_create = AxesCreate(fig, position=(1,1,1))
    ax = ax_create.get_axes()

    #canvas_draw = CanvasDraw(ax)
    
    selection = listbox.curselection()
    
    for trace in selection:
        sns.lineplot(data=df, x='time', y=df.columns[trace],ax=ax)

    #Vlines that will be adjustable after plot issues are solved
    plt.axvline(df['time'][2],color='green',linestyle='--')
    plt.axvline(df['time'][4],color='red',linestyle='--')

    return (ax_create.get_fig())

def plot_MC(fig):
    # Creating the Tkinter canvas containing the figure
    #THIS IS WHERE I THINK THE PROBLEM IS, BUT I'M NOT SURE
    #I thought this would make the canvas in the MC frame, but it seems to want a new plot since plotSet() is returning a figure
    #Is there a better way to plot specifically in the middle frame, and not force plotSet() to return anything?   
    canvas = FigureCanvasTkAgg(fig, master=frame_MC)   
    canvas.draw() 
  
    # Placing the canvas on the Tkinter window 
    canvas.get_tk_widget().grid(row=0, column=0) 
  
    # Creating the Matplotlib toolbar 
    toolbar = NavigationToolbar2Tk(canvas, frame_MC, pack_toolbar=False) 
    toolbar.update() 
    toolbar.grid(row=1, column=0)

def are_you_sure():
    """
    Just a function that opens a message box to confirm window destruction
    """

    if tk.messagebox.askyesno("Quit Dialog","Are you sure you want to quit the app?"):
        window.destroy()

#MC 'Middle-Center'
frame_MC = tk.Frame(window)
#ML 'Middle-Left'
list_frame=tk.Frame(window)
listbox=tk.Listbox(list_frame, selectmode='multiple', height=30, width=30)
listbox.bind('<<ListboxSelect>>', plotSet)
scrollbar=tk.Scrollbar(list_frame, orient="vertical")
listbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command = listbox.yview)
#TL 'Top-Left'
frame_TL = tk.Frame(window)
generate_list_button = tk.Button(frame_TL, text="generate list", command=generate_List, width=30)
generate_list_button.grid(row=0, column=0, columnspan=3)
plot_test_button = tk.Button(frame_TL, text="plot selection", command=plot_MC(plotSet()))
plot_test_button.grid(row=1, column=0, columnspan=3)
#BL 'Bottom-Left'
frame_BL = tk.Frame(window)
button_quit = tk.Button(master = frame_BL, text = "Quit", command = are_you_sure, bg = '#FF2222')
button_quit.grid(row = 0, column = 0)

#Placing elements
frame_TL.grid(row=0, column = 0)
frame_MC.grid(row = 1, column = 1)
frame_BL.grid(row = 2, column = 0)
list_frame.grid(row = 1, column = 0)
listbox.pack(side = "left", fill = "y")
scrollbar.pack(side = "right", fill = "y")

#run the gui
window.mainloop()

Are there any obvious improvements I could make to clean up the plotting so I don't have this problem? Thank you in advance.

Share Improve this question asked Mar 12 at 7:17 tnazarrotnazarro 114 bronze badges
Add a comment  | 

3 Answers 3

Reset to default 0

You have to remove plt.ion() which run interactive mode and it may display it automatically.

You have also other problem:

command= needs function's name without () and parameters. For parameters you may use lambda

command=lambda:plot_MC(plotSet()))

Now it runs this function when you click Button.
Previous version was running first plot at start and it creates command=None - so it didn't create plot when you click Button.


Other possible problem:

everytime when you click Button it create new canvas and it may put it on top of previous canvas and old canvas is not visible but it is still in memory.

It may need to assign canvas to global variable and destroy() it before creating new one. Because it need to run it even before first canvas so it may need to assign default value at start - None - and always check if it not None. The same can be with toolbar

canvas = None   # create global variable with value at start
toolbar = None

def plot_MC(fig):

    if canvas is not None:
        canvas.destroy()

    canvas = FigureCanvasTkAgg(fig, master=frame_MC)   

    #... code ...

    if toolbar is not None:
        toolbar.destroy()

    toolbar = NavigationToolbar2Tk(canvas, frame_MC, pack_toolbar=False) 

    # ... code ...

As said in other answer, the main issue is calling plt.ion().

Apart from removing the line plt.ion(), I would also suggest that:

  • create the plot frame once globally and update the graph when the "plot selection" button is clicked

  • use matplotlib.figure.Figure class instead of plt.figure() to create the figure object

Below is the simplified code:

import tkinter as tk
import pandas as pd
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import seaborn as sns

def generate_list():
    listbox.delete(0, 'end')
    i = 0
    for column in df.columns:
        if column != 'time':
            listbox.insert(i, column)
            if (i%2) == 0:
                listbox.itemconfigure(i, background='#F0F0F0')
            i += 1
    plot_set()

def plot_set(*args):
    ax.clear()
    for idx in listbox.curselection():
        trace = listbox.get(idx)
        sns.lineplot(data=df, x='time', y=df[trace], ax=ax)
    ax.axvline(df['time'][2], color='green', linestyle='--')
    ax.axvline(df['time'][4], color='red', linestyle='--')
    canvas.draw()

def are_you_sure():
    if tk.messagebox.askyesno('Quit Dialog', 'Are you sure you want to quit the app?'):
        window.destroy()


df = pd.DataFrame({
    'time': [0,1,2,3,4,5,6,7,8,9],
    'linear': [0,1,2,3,4,5,6,7,8,9],
    'random': [0,8,4,6,4,7,1,9,2,6],
    'inverse': [9,8,7,6,5,4,3,2,1,0],
})

window = tk.Tk()
window.title('Data Visualizer - Plotting in Tkinter')

# Top-Left frame
frame_TL = tk.Frame(window)
generate_list_button = tk.Button(frame_TL, text='generate list', command=generate_list, width=30)
generate_list_button.grid(row=0, column=0)
plot_test_button = tk.Button(frame_TL, text='plot selection', command=plot_set)
plot_test_button.grid(row=1, column=0)

# Middle-Left frame
list_frame = tk.Frame(window)
listbox = tk.Listbox(list_frame, selectmode='multiple', width=30, height=30)
scrollbar = tk.Scrollbar(list_frame, orient='vertical', command=listbox.yview)
listbox.config(yscrollcommand=scrollbar.set)
listbox.bind('<<ListboxSelect>>', plot_set)
scrollbar.pack(side='right', fill='y')
listbox.pack(side='left', fill='both', expand=1)

# Bottom-Left frame
frame_BL = tk.Frame(window)
button_quit = tk.Button(frame_BL, text='Quit', command=are_you_sure, bg='#FF2222')
button_quit.grid(row=0, column=0)

# Middle-Center frame
frame_MC = tk.Frame(window)
fig = Figure(figsize=(5, 5))  # use matplotlib.figure.Figure class instead
ax = fig.add_subplot(1, 1, 1)
canvas = FigureCanvasTkAgg(fig, master=frame_MC)
toolbar = NavigationToolbar2Tk(canvas, frame_MC, pack_toolbar=False)
canvas.get_tk_widget().grid(row=0, column=0)
toolbar.grid(row=1, column=0, sticky='w')

frame_TL.grid(row=0, column=0)
list_frame.grid(row=1, column=0)
frame_MC.grid(row=1, column=1)
frame_BL.grid(row=2, column=0)

window.mainloop()

Every time plot_MC() is called, it creates a new FigureCanvasTkAgg and places it on the frame_MC. This is the reason you are getting pop-up windows.

To fix it create the FigureCanvasTkAgg only once and store it as an attribute of your GUI. Then, in plot_MC(), update the existing canvas rather than creating a new one.

def plotSet(*args):
    if not hasattr(window, "fig"):
        window.fig_create = FigureCreate(figsize=(5,5))
        window.fig = window.fig_create.get_figure()
        window.ax_create = AxesCreate(window.fig, position=(1,1,1))
        window.ax = window.ax_create.get_axes()
    else:
        window.ax.clear()

    selection = listbox.curselection()

    for trace in selection: # use trace + 1 to get the correct df column
        sns.lineplot(data=df, x='time', y=df.columns[trace + 1], ax=window.ax)

    # Vlines that will be adjustable after plot issues are solved
    window.ax.axvline(df['time'][2], color='green', linestyle='--')
    window.ax.axvline(df['time'][4], color='red', linestyle='--')


def plot_MC():
    if not hasattr(window, "canvas"):
        # Creating the Tkinter canvas containing the figure
        window.canvas = FigureCanvasTkAgg(window.fig, master=frame_MC)
        window.canvas.draw()
        # Placing the canvas on the Tkinter window
        window.canvas.get_tk_widget().grid(row=0, column=0)
        # Creating the Matplotlib toolbar
        window.toolbar = NavigationToolbar2Tk(window.canvas, frame_MC, pack_toolbar=False)
        window.toolbar.update()
        window.toolbar.grid(row=1, column=0)
    else:
        window.canvas.draw()

The line

plot_test_button = tk.Button(frame_TL, text="plot selection", command=plot_MC(plotSet()))

is passing the result of plot_MC(plotSet()), not the function to be executed when the button is pressed. It should be:

plot_test_button = tk.Button(frame_TL, text="plot selection", command=lambda: [plotSet(None), plot_MC()])

The plt.ion() allows Matplotlib to display plots and update them dynamically as soon as changes are made. And it works with this solution.
If you use plt.ioff() the plots are only displayed when plt.show() is called. You have to do it in every plot display or update.

本文标签: