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 badges3 Answers
Reset to default 0You 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 ofplt.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.
本文标签:
版权声明:本文标题:python - Ways to prevent tkinter from opening a graph on each plot drawndraw in a selected frame? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744766788a2624090.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论