Cannot get tkinter grid to layout my GUI as I want - python

sorry for the simple question. Relatively new to python and especially Object Oriented and Class systems. Basically, I want my GUI to have a couple of rows across the top of the window (across full width). Beneath, I want 4 equal sized columns. I don't want this to impact my plots (on a different page). Any help is much appreciated. I cannot workout how to use tkinter to get this to work.
Thanks
__author__ = "dev"
# https://pythonprogramming.net/object-oriented-programming-crash-course-tkinter/?completed=/tkinter-depth-tutorial-making-actual-program/
import tkinter as tk
from tkinter import ttk
import yfinance as yf
import datetime as dt
import matplotlib
from matplotlib import style
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import pyplot as plt
import matplotlib.dates as mdates
LargeFont = ("Verdana", 16)
style.use("ggplot")
def yfinance_get(index):
tkr = yf.Ticker(index)
today = dt.datetime.today().isoformat()
m_ago = dt.datetime.today() - dt.timedelta(days=30)
h_px = tkr.history(period="1d", start=m_ago, end=today[:10])
return h_px
class DailySumm(tk.Tk):
def __init__(self, *args, **kwargs): # initialise - runs as soon as the class is called
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self) # frame creates the window of our GUI
container.pack(side="top", expand=True) # pack is similar to grid, but less accurate
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
# creates an empty dictionary called named self.frames, this will hold the diff "pages" in the Gui
for f in (StartPage, Equities): # for each page defined, this tuple needs to be updated to include
# this loop will add the pages to the self.frames dict
frame = f(container, self)
self.frames[f] = frame # enters a new entry into the dictionary, the StartPage page
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(StartPage) # this calls the show_frame method with cont = StartPage
def show_frame(self, cont):
frame = self.frames[cont] # cont = dict key to frames (dict)
frame.tkraise() # this raises the "frame" to the front of the window (i.e. shows it to the user)
class StartPage(tk.Frame): # this creates the StartPage page which inherits the tk.Frame functionality (ie imports)
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent) # parent is the parent class, in this case DailySumm
label = tk.Label(self, text="News and Market Data", font=LargeFont).grid(row=0, column=0, columnspan=4)
label2 = tk.Label(self, text="Equities", font=LargeFont).grid(row=1, column=0, padx=10)
label3 = tk.Label(self, text="Credit", font=LargeFont).grid(row=1, column=1, padx=10)
label4 = tk.Label(self, text="Currencies", font=LargeFont).grid(row=1, column=2, padx=10)
label5 = tk.Label(self, text="Commodities", font=LargeFont).grid(row=1, column=3, padx=10)
button_equ = ttk.Button(self, text="Equities",
command=lambda: controller.show_frame(Equities)).grid(row=2, column=0)
# lambda stops the function being called on inception, only when the button is pressed
class Equities(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="Equities", font=LargeFont)
label.pack(pady=10, padx=10)
button_home = ttk.Button(self, text="Back to Home",
command=lambda: controller.show_frame(StartPage))
button_home.pack()
index = [['FTSE 100', '^FTSE'], ['S&P 500', '^GSPC'], ["Nikkei 225", "^N225"], ["MSCI EM", "EEM"]]
fig = Figure(dpi=100)
for i in range(0, 3+1):
ax = fig.add_subplot(2, 2, i+1)
plot_data = yfinance_get(index[i][1])
ax.plot(plot_data['Close'], color='r', label=index[i][0])
ax.grid(which="major", color='k', linestyle='-.', linewidth=0.3)
ax.legend(loc=2)
ax.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MONDAY))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%d-%m"))
canvas = FigureCanvasTkAgg(fig, self)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
app = DailySumm()
app.geometry("2560x1600")
app.mainloop()

One way to do this is to create a frame for the rows at the top, and another frame to hold the four columns below. You can use pack to add these frames to the window, and use grid in the lower frame to manage the columns. To get the columns to be the same size you can use the uniform option for each column.
Here's an example of the technique:
import tkinter as tk
root = tk.Tk()
top_frame = tk.Frame(bd=1, relief="raised", height=50)
bottom_frame = tk.Frame(bd=1, relief="raised")
top_frame.pack(side="top", fill="x")
bottom_frame.pack(side="bottom", fill="both", expand=True)
# add labels to the columns so that we can visualize the columns
labels = (
tk.Label(bottom_frame, bd=1, relief="raised", text="Column 1", height=8, width=10),
tk.Label(bottom_frame, bd=1, relief="raised", text="Column 2", height=8, width=10),
tk.Label(bottom_frame, bd=1, relief="raised", text="Column 3", height=8, width=10),
tk.Label(bottom_frame, bd=1, relief="raised", text="Column 4", height=8, width=10)
)
# add the labels to the columns.
labels[0].grid(row=0, column=0, sticky="nsew", ipadx=4)
labels[1].grid(row=0, column=1, sticky="nsew", ipadx=4)
labels[2].grid(row=0, column=2, sticky="nsew", ipadx=4)
labels[3].grid(row=0, column=3, sticky="nsew", ipadx=4)
# allocate all extra horizontal space to each column evenly
# the critical piece is both the weight and uniform options
bottom_frame.grid_columnconfigure(0, weight=1, uniform="column")
bottom_frame.grid_columnconfigure(1, weight=1, uniform="column")
bottom_frame.grid_columnconfigure(2, weight=1, uniform="column")
bottom_frame.grid_columnconfigure(3, weight=1, uniform="column")
# allocate all extra vertical space to row 0
bottom_frame.grid_rowconfigure(0, weight=1)
root.mainloop()

Related

Sizing the Canvas widget with other widgets on the GUI

I'm creating a GUI that has a spreadsheet-like interface which is wrapped with entry method to allow the person that uses the program to enter it as if it is on a spreadsheet. This allows me much greater control over all the data and manipulate it for data analysis later. However, I can't restrict the size of the canvas to allow the scrolling bar to take effect. I.E. if I change the number of rows, the canvas will resize to that (along with column(s) changes too). I have other widgets within the GUI that isn't shown in the code but I'm just focusing on trying to force a size on the Canvas widget itself.
Is there a way for me to force the Canvas to stay within "width and height" size without having the rows and columns controlling the size of the Canvas?
import tkinter as tk
from tkinter import *
from textwrap import fill
from datetime import date
#Instantiate the GUI
class StartApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
t1 = ExcelTable(self)
t1.grid(row = 0, columnspan=2, sticky = "N")
t2 = ProductWidget()
t2.grid(row=1, column=0,sticky="SW")
t3 = TotalTrucks()
t3.grid(row=1, column=1, sticky="nsew")
#Instantiate the layout for the excel-like table entry for the top
part of the GUI
class ExcelTable(tk.Frame):
def __init__(self, parent, width=500, height=500):
rows=20
columns=10
#label for each column in the table colHeaders = ["#", "Driver", "Tare", "Ticket #", "Location", "Product", "Gross", "Net", "Tons", "Date"]
# use black background so it "peeks through" to
# form grid lines
#tk.Frame.__init__(self, parent, background="black", width=150, height=200)
#attempt to create a scrollbar within a canvas
canvas = tk.Canvas(parent)
scrollbar = tk.Scrollbar(parent, orient="vertical", command=canvas.yview)
#contain the Frame within the canvas
tk.Frame.__init__(self, canvas)
#creates the widgets to behave like excel-like table for data entry
self._widgets = []
for row in range(rows):
current_row = []
for column in range(columns):
if row == 0: #create the headers for the spreadsheet widget, using the colHeaders array
if column == 0:
label = tk.Label(self, text = colHeaders[column], borderwidth=0, width = 4)
else:
label = tk.Label(self, text = colHeaders[column], borderwidth=0, width=10)
else:
if column == 0:
label = tk.Label(self, text = (row))
else:
label = tk.Entry(self, text="")
label.bind
label.grid(row=row, column=column, sticky="nsew", padx=1, pady=1)
current_row.append(label)
self._widgets.append(current_row)
for column in range(columns):
self.grid_columnconfigure(column, weight=1)
canvas.create_window(0,0,anchor='nw', window=self)
canvas.update_idletasks()
canvas.configure(scrollregion=parent.bbox('all'))
canvas.grid(row=0,column=0)
scrollbar.grid(row=0, column=1, sticky="ns")
def set(self, row, column, value):
widget = self._widgets[row][column]
widget.configure(text=value)
def key(event):
print ("key: {}".format(event.char))
#obtain and store values that are entered in the ExcelTable
def find_in_grid(frame, row, column):
for children in frame.children.values():
info = children.grid_info()
#note that rows and column numbers are stored as string
if info['row'] == str(row) and info['column'] == str(column):
return children
return None
class ProductWidget(tk.Frame):
def __init__(self):
tk.Frame.__init__(self, background="white")
self._widgets = []
label1 = tk.Label(self, text="Product")
label1.grid(row=0,column=0, sticky="nsew")
label2 = tk.Label(self, text="Total Tons")
label2.grid(row=0, column=1, sticky="nsew")
class TotalTrucks(tk.Frame):
def __init__(self):
tk.Frame.__init__(self, background="white" )
self._widgets = []
label1 = tk.Label(self, text="Total Trucks")
label1.grid(row=0, rowspan=2, column=0, sticky="nsew")
label2 = tk.Label(self, text="Today: ")
label2.grid(row=1, column=0, stick="nsew")
label3 = tk.Label(self, text="Last Week: ")
label3.grid(row=2, column=0, sticky="nsew")
label4 = tk.Label(self, text="Overall")
label4.grid(row=3, column=0, sticky="nsew")
if __name__ == "__main__":
currYear = date.today().year
startGUI = StartApp()
startGUI.title("Truck Log " + str(currYear))
startGUI.mainloop()
Since ExcelTable is the internal frame inside canvas and is already put inside canvas using canvas.create_window(...), so you should not call t1.grid(...) inside StartApp.__init__().
Also you set the scrollregion of canvas wrong in the following line:
canvas.configure(scrollregion=parent.bbox('all'))
It should be:
canvas.configure(scrollregion=canvas.bbox('all'))
Finally you forgot to configure yscrollcommand option of canvas.
Below is the changes required:
...
class StartApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
t1 = ExcelTable(self)
### should not call t1.grid(...) since t1 has already put in canvas using canvas.create_window(...)
#t1.grid(row = 0, columnspan=2, sticky = "N")
t2 = ProductWidget()
t2.grid(row=1, column=0,sticky="SW")
t3 = TotalTrucks()
t3.grid(row=1, column=1, sticky="nsew")
...
class ExcelTable(tk.Frame):
def __init__(self, parent, width=500, height=500):
...
#canvas.configure(scrollregion=parent.bbox('all'))
canvas.configure(scrollregion=canvas.bbox('all'), width=self.winfo_width(),
yscrollcommand=scrollbar.set)
...

How To Add Multiple Tables In One Tkinter Frame

I am using a library called tkintertable in python which essentially gives me the capability to efficiently add tables to my Tkinter application. I am trying to setup 6 different tables with 6 charts to go with each table in this frame(my home page essentially) but am relitvely new to tkinter so having some trouble organizing it, currently my code looks like the following:
import tkinter as tk
from tkintertable import TableCanvas, TableModel
class MainApplication(tk.Tk):
"""Main application class"""
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self)
self._frame = None
self.switch_frame(TickerInput, None)
def switch_frame(self, frame_class, ticker):
"""Destroys current frame and replaces it with a new one."""
if ticker is not None:
new_frame = frame_class(self, ticker)
else:
new_frame = frame_class(self)
if self._frame is not None:
self._frame.destroy()
self._frame = new_frame
self._frame.grid()
class TickerInput(tk.Frame):
"""Ticker input page that allows input of ticker and redirects to main indicator page"""
def __init__(self, master):
tk.Frame.__init__(self, master, background="#212020")
# Centers the frame
self.master.columnconfigure(0, weight=1)
self.master.rowconfigure(0, weight=1)
ticker_label = tk.Label(self, text="Enter ticker..")
ticker_label.grid(row=0, column=1, columnspan=2)
ticker_input = tk.Entry(self, width=35)
ticker_input.grid(row=0, column=3)
button = tk.Button(self, text="Search", command=lambda: master.switch_frame(Indicator, ticker_input.get()))
button.grid(row=1, column=1, columnspan=3)
class Indicator(tk.Frame):
"""Indicator page that shows the indicators and whether its a buy or sell"""
def __init__(self, master, ticker):
tk.Frame.__init__(self, master)
self.ticker = ticker
self.master.columnconfigure(0, weight=0)
self.master.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
button = tk.Button(self, text="Go Back", command=lambda: master.switch_frame(TickerInput, None))
button.grid(column=0, row=0)
ticker_label = tk.Label(self, text=("Current Ticker: " + self.ticker))
ticker_label.grid(column=1, row=0)
tableOne = Table(self)
tableOne.grid(column=0, row=1)
tableOne.createTable()
# tableTwo = Table(self)
# tableTwo.createTable()
class Table(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
def createTable(self):
data = {'rec1': {'col1': 99.88, 'col2': 108.79, 'label': 'rec1'},
'rec2': {'col1': 99.88, 'col2': 108.79, 'label': 'rec2'}
}
model = TableModel()
table = TableCanvas(self.master, model=model, data=data)
table.show()
return table
if __name__ == "__main__":
app = MainApplication()
app.title("Indicator Trading Confirmation Tool")
app.geometry("1920x1080")
app.config(background="#121212")
app.mainloop()
and here is the GUI
Why is my Go Back Button stuck on the table if I have it on a different row and have my weight set to 1 which should seperate them 50% correct? And also what is the best way to organize this both grid/code related to make sure that the GUI has 6 equal tables/charts, 3 on the left and 3 on the right? Thank you!
Okay, lets go step by step,
having replaced your Table class with just a colored Frame there is nothing overlapping for me, so this is probably just due to the Layout of the table itself - quick fix for that could be adding some padding
the weight option is used for distribution of extra space when resizing the window, for more information you can take a look here, there is also a nice visualization found here
in order to use weight for controlling actual used width or height of your widgets you might want to take a look at the sticky option of grid
see for a very nice overview you can also check this which is very detailed for both
for your general layout you can just go the easy route of combining weight and sticky options like this:
self.l1 = tk.Frame(self, background="red")
self.l2 = tk.Frame(self, background="orange")
self.l3 = tk.Frame(self, background="green")
self.l4 = tk.Frame(self, background="blue")
self.l5 = tk.Frame(self, background="brown")
self.l6 = tk.Frame(self, background="yellow")
self.rowconfigure(0, weight = 1)
self.rowconfigure(1, weight = 1)
self.rowconfigure(2, weight = 1)
self.columnconfigure(0, weight = 1)
self.columnconfigure(1, weight = 1)
self.l1.grid(row=0, column=0, sticky="nsew")
self.l2.grid(row=1, column=0, sticky="nsew")
self.l3.grid(row=2, column=0, sticky="nsew")
self.l4.grid(row=0, column=1, sticky="nsew")
self.l5.grid(row=1, column=1, sticky="nsew")
self.l6.grid(row=2, column=1, sticky="nsew")
for a 50:50 separation in width and 33:33:33 in height.
If you then later on want to add something like a menubar within your frame you might want to bundle your table frames within another Frame in order to have them resize equally

tkinter problem in displaying two frames using tkraise

I'm trying to display two frames in a way such that one frame has a button to display the other frame and vice versa. I'm trying to use tkinter frame function of tkraise(). But they are getting simultaneously displayed over each other. Please you can ignore the import board file because it has some helper functions that I'm using to display some data on the GUI.
Here is my code:
import tkinter as tk
import tkinter.scrolledtext as st
from board import *
FONT = ("Helvetica", 20)
attributes = {'padx': 5, 'pady': 5}
class App(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title('DAS')
self.geometry('500x325')
container = tk.Frame(self)
container.pack(fill=tk.BOTH, expand=True)
self.frames = {}
self.frames[0] = Display(container, self)
self.frames[0].grid(row=0, column=0)
self.frames[1] = DataStored(container, self)
self.frames[1].grid(row=0, column=0)
self.show_frame(1)
def show_frame(self, frame):
frame = self.frames[frame]
frame.tkraise()
class Display(tk.Frame):
def __init__(self, container, controller):
super().__init__(container)
name = tk.Label(self, text='Data Acquistion System', font=FONT)
name.pack(fill='x')
left_wrapper = tk.Frame(self)
left_wrapper.pack(side='left')
right_wrapper = tk.Frame(self)
right_wrapper.pack(side='left')
port_label = tk.Label(left_wrapper, text='Port:', font=FONT)
port_label.grid(row=0, column=0)
port_option = tk.StringVar(left_wrapper, value='None')
ports_availiable = get_arduino_ports()
port_value = tk.OptionMenu(left_wrapper, port_option, *ports_availiable)
port_value.grid(row=0, column=1)
baud_label = tk.Label(left_wrapper, text='Baudrate:', font=FONT)
baud_label.grid(row=1, column=0)
baud_value = tk.Entry(left_wrapper)
baud_value.grid(row=1, column=1)
baud_value.insert(0, 9600)
connect_button = tk.Button(left_wrapper, text='Connect', command=lambda: connect_to_port(port_option.get(), baud_value.get()))
connect_button.grid(row=2, column=1)
playback_button = tk.Button(left_wrapper, text='Playback', command=lambda: controller.show_frame(1))
playback_button.grid(row=3, column=1)
data_display = st.ScrolledText(right_wrapper)
data_display.pack()
class DataStored(tk.Frame):
def __init__(self, container, controller):
super().__init__(container)
tk.Label(self, text='Playback Data').pack(fill=tk.X)
top_frame = tk.Frame(self)
top_frame.pack(fill=tk.X)
bottom_frame = tk.Frame(self)
bottom_frame.pack(fill=tk.X)
canvas = st.ScrolledText(top_frame)
canvas.pack()
update = tk.Button(bottom_frame, text='Update', command=self.update_playback)
update.grid(row=0, column=0)
back = tk.Button(bottom_frame, text='Back', command=lambda: controller.show_frame(0))
back.grid(row=0, column=1)
def update_playback(self):
with open("data.txt", "r") as file:
data = file.read()
self.data.insert(tk.END, data)
self.data.config(state="disabled")
if __name__ == "__main__":
window = App()
window.mainloop()
There is a simple solution by changing the way in which you use your show_frame function:
def show_frame(self, frame_num):
for frame in self.frames.values():
frame.grid_remove()
frame = self.frames[frame_num]
frame.grid()
The reason tkraise does not work is that your two frames are of different sizes, if they were the same rise you would be entirely right with your code. Because they are of different sizes you can still see the bigger frame when looking at the smaller one.
grid_remove removes a frame from being loaded on the window, and then the following grid command places the frame back where it was before, removing any issues with overlap and such.
As the two frames have different sizes, you need to add sticky="nsew" in .grid(...) to make both frames to occupy the available space:
self.frames[0] = Display(container, self)
self.frames[0].grid(row=0, column=0, sticky="nsew")
self.frames[1] = DataStored(container, self)
self.frames[1].grid(row=0, column=0, sticky="nsew")

How can I create this layout with Python3 using Tkinter with the grid geometry manager?

I am trying to create the following layout:
This is my code:
from . import FlowPane,JsonPane,PropertiesPane,ToolbarPane
import tkinter as tk
class ConflixEditor(tk.Tk):
def __init__(self, args):
super().__init__()
self.__dict__.update({k: v for k, v in locals().items() if k != 'self'})
self.minsize(width=1024, height=768)
self.title('Conflix Editor')
# Widget Creation
self.frame = tk.Frame(self)
self.toolbarPane = ToolbarPane.ToolbarPane(self.frame, bg='black')
self.flowPane = FlowPane.FlowPane(self.frame, bg='red')
self.propertiesPane = PropertiesPane.PropertiesPane(self.frame, bg='blue')
self.jsonPane = JsonPane.JsonPane(self.frame, bg='green')
# Widget Layout
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(2, weight=1)
self.frame.grid(row=0, column=0, sticky=tk.N+tk.E+tk.S+tk.W)
self.toolbarPane.grid(row=0, column=0, columnspan=3, rowspan=2, sticky=tk.N+tk.E+tk.W)
self.flowPane.grid(row=2, column=0, columnspan=2, rowspan=5, sticky=tk.N+tk.S+tk.W)
self.propertiesPane.grid(row=2, column=2, columnspan=1, rowspan=5, sticky=tk.N+tk.E+tk.S)
self.jsonPane.grid(row=7, column=0, columnspan=3, rowspan=3, sticky=tk.E+tk.S+tk.W)
The constructors for FlowPane, JsonPane, PropertiesPane, ToolbarPane all take two parameters: the parent widget and the background color.
Instead of getting the desired result above, I am getting the following result:
What am I doing wrong? How can I create the desired layout? Note that the background colors are just temporary to confirm that each widget is using the correct amount of space. This is eventually going to be an application for designing and building Netflix Conductor workflows. I want to have a toolbar with menus and buttons in the black area, a Canvas in the red area for displaying the flow-chart elements that represent tasks in the workflows, a Treeview for viewing properties in the blue area, and a Text Editor in the green area at the bottom for viewing/editing the raw JSON.
You need to:
specify height option for toolbarPane and jsonPane;
specify width option for propertiesPane;
add tk.E to sticky option for flowPane;
use grid_rowconfigure() and grid_columnconfigure() for self and self.frame as well
Below is an updated code:
class ConflixEditor(tk.Tk):
def __init__(self, *args):
super().__init__()
#self.__dict__.update({k: v for k, v in locals().items() if k != 'self'})
self.minsize(width=1024, height=768)
self.title('Conflix Editor')
# make self.frame use all the space of root window
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
# Widget Creation
self.frame = tk.Frame(self)
self.toolbarPane = ToolbarPane.ToolbarPane(self.frame, bg='black', height=100) # added height option
self.flowPane = FlowPane.FlowPane(self.frame, bg='red')
self.propertiesPane = PropertiesPane.PropertiesPane(self.frame, bg='blue', width=250) # added width option
self.jsonPane = JsonPane.JsonPane(self.frame, bg='green', height=200) # added height option
# Widget Layout
self.frame.grid_columnconfigure(0, weight=1) # used self.frame instead of self
self.frame.grid_rowconfigure(2, weight=1) # used self.frame instead of self
self.frame.grid(row=0, column=0, sticky=tk.N+tk.E+tk.S+tk.W)
self.toolbarPane.grid(row=0, column=0, columnspan=3, rowspan=2, sticky=tk.N+tk.E+tk.W)
self.flowPane.grid(row=2, column=0, columnspan=2, rowspan=5, sticky=tk.N+tk.E+tk.S+tk.W) # added tk.E
self.propertiesPane.grid(row=2, column=2, columnspan=1, rowspan=5, sticky=tk.N+tk.E+tk.S)
self.jsonPane.grid(row=7, column=0, columnspan=3, rowspan=3, sticky=tk.E+tk.S+tk.W)

Matplotlib drawing excessive tick labels, drawing data incorrectly

So, I am building a tkinter GUI program which has a matplotlib graph on one of the pages. From various popups, the user can add, delete, and modify the traces on the graph.
What I had been doing up till now is using the draw() method on the FigureCanvasTkAgg object to update the graphic whenever something is changed. I have read in several other posts that this redraws the entire figure, and makes the rendering very slow. Additionally, in my specific case, it draws a lot of tick labels on the axes, seemingly one for every point in the source, so, in addition to taking forever, it also makes the graph absolutely unreadable. It also does not plot the data correctly.
I came across some posts suggesting not to use the draw method, as it redraws the entire figure, but that doesn't really explain why so many tick labels are being drawn, or why the data isn't being plotted as expected. I've looked into using animate, but I'm not sure that that's what I need, because the graph doesn't have to update constantly, only when it's changed by the user.
I'm using Python 3.7.2, and matplotlib 3.1.1. The data I'm plotting is spectral, and only has about 4000 points. I've also tested it with very small datasets (10-15 points) and this doesn't seem to happen. I've also tried setting the tick labels with axis.locator_params(nbins=10), but it doesn't prevent all those labels from being drawn.
Any idea what's happening here, and how I can prevent it? Here's the graphic it produces, and the relevant parts of my code. Sorry if I'm missing anything; I'm new to SO and I'm still a relatively inexperienced programmer.
The offensive graph:
class App(tk.Tk): ###The controller of all pages, & control of operations
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
self.dfs = {} #contains dataframes loaded from csv/made by the user
self.spectra = {} #contains spectrum objects
self.plots = {} #contains Figure objects. Each figure can have exactly one axis
for F in (HomePage, SpectraPage, GraphPage, MakeSpectrumPage):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(HomePage)
def show_frame(self, cont):
self.frames[cont].tkraise()
def load(self, filename):
df = pd.read_csv(filename, header=None)
if any(df.iloc[0].apply(lambda x: isinstance(x, str))):
df = df[1:].reset_index(drop=True).rename(columns=df.iloc[0])
else:
names=[]
for i in range(len(df.columns)):
names.append("w%i" %i)
df.columns = names
self.dfs[filename] = df
self.frames[MakeSpectrumPage].makeFileList()
def get_dfs(self):
return self.dfs
def get_spectra(self):
return self.spectra
def get_plots(self):
return self.plots
def make_spectrum(self, name, df, x, y):
spectrum = Spectrum(name, df, x, y)
self.spectra[name] = spectrum
self.frames[SpectraPage].insertItems()
def make_plot(self, name):
fig = Figure(dpi=100)
fig.suptitle(name)
axis = fig.add_subplot(111, xlim=(4000, 500), ylim=(0,1))
axis.locator_params(nbins=10)
self.plots[name] = fig
def graph(self, axis, spectrum, **kwargs): #plot spectrum obj
axis.plot(spectrum.xdata, spectrum.ydata, label=spectrum.name, **kwargs)
class GraphPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller=controller
self.grid_columnconfigure(0,weight=1)
self.controller.make_plot('Plot 1')
self.showFigure(self.controller.get_plots()['Plot 1'])
self.makeGraphTray()
def showFigure(self, fig):
self.canvas = FigureCanvasTkAgg(fig, self)
self.canvas.draw()
self.canvas._tkcanvas.grid(row=1, column=1, rowspan=2, columnspan=2)
self.makeToolbar()
def makeToolbar(self):
self.toolbar_frame = tk.Frame(self)
self.toolbar_frame.grid(row=3,column=1, columnspan=2)
self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
self.toolbar.update()
def makeGraphTray(self):
#make container for graph/trace modification buttons
self.graphTray = tk.Frame(self, width=50)
self.graphTray.grid(row=1, column=3, sticky='nsew')
addTraceButton = ttk.Button(self.graphTray, text="Add Trace", command=lambda:self.raisePopup(NewTracePopup))
addTraceButton.grid(row=0, column=0, sticky='nsew')
def raisePopup(self, Popup):
popup = Popup(self)
class Spectrum: #Objects of this class are two-column structures.
def __init__(self, name, sourcedf, x, y):
self.xdata = sourcedf[x]
self.ydata = sourcedf[y]
self.df = pd.concat([self.xdata, self.ydata], axis=1)
self.name = name
class ConditionalPopup(tk.Toplevel):
#parent class of OK/Cancel popups where OK is disabled until all fields are filled
def __init__(self, master, title, **kwargs):##param **kwargs the Variables traced by the widgets in the popup
if all(isinstance(kwarg, tk.Variable) for kwarg in kwargs.values()):
super().__init__(master)
self.__dict__.update(kwargs)
self.master = master
self.vars = kwargs #dictionary of kwargs
self.wm_title(title)
self.widgetFrame = tk.Frame(self)
self.widgetFrame.grid(row=0, column=0)
self.traceVars()
self.makeWidgets(self.widgetFrame)
self.placeWidgets()
else: raise TypeError
def traceVars(self):
for var in self.vars.values():
var.trace('w', self.activateOK)
def activateOK(self, *args):
if all(self.vars[key].get() for (key, value) in self.vars.items()):
self.okButton.configure(state='normal')
else:
self.okButton.configure(state='disabled')
def makeWidgets(self, frame):
self.okButton = ttk.Button(self, text="OK", state='disabled', command=self.okPressed)
self.cancelButton = ttk.Button(self, text="Cancel", command=self.destroy)
def placeWidgets(self):
self.okButton.grid(row=1, column=0, padx=2.5, pady=10, sticky='e')
self.cancelButton.grid(row=1, column=1, padx=2.5, pady=10, sticky='w')
def okPressed(self, *args):
self.destroy()
class NewTracePopup(ConditionalPopup):
#a popup that enables adding a trace to a chosen plot
def __init__(self, master):
super().__init__(master, "Add trace to plot",
plotVar=tk.StringVar(),
spectrumVar=tk.StringVar(),
colorVar=tk.StringVar(),
linewidthVar=tk.StringVar())
def activateOK(self, *args):
if self.linewidthVar.get().isdecimal():
super().activateOK(*args)
def makeWidgets(self, frame):
self.plotLabel = tk.Label(frame, text="Plot:")
self.plotCombobox = ttk.Combobox(frame, state='readonly', values=list(self.master.controller.get_plots().keys()), textvariable=self.plotVar)
self.spectrumLabel = tk.Label(frame, text="Spectrum:")
self.spectrumCombobox = ttk.Combobox(frame, state='readonly', values=list(self.master.controller.get_spectra().keys()), textvariable=self.spectrumVar)
self.cLabel = tk.Label(frame, text="Colour:")
self.cCombobox = ttk.Combobox(frame, state='readonly', values=list(mcolors.BASE_COLORS)+list(mcolors.TABLEAU_COLORS), textvariable=self.colorVar)
self.mLabel = tk.Label(frame, text="Line width:")
self.mEntry = ttk.Entry(frame, textvariable=self.linewidthVar)
super().makeWidgets(frame)
def placeWidgets(self):
self.plotLabel.grid(row=0, column=0, padx=10, pady=10, sticky='e')
self.plotCombobox.grid(row=0, column=1, padx=10, pady=10, sticky='w')
self.spectrumLabel.grid(row=1, column=0, padx=10, pady=10, sticky='e')
self.spectrumCombobox.grid(row=1, column=1, padx=10, pady=10, sticky='w')
self.cLabel.grid(row=2, column=0, padx=10, pady=10, sticky='e')
self.cCombobox.grid(row=2, column=1, padx=10, pady=10, sticky='w')
self.mLabel.grid(row=3, column=0, padx=10, pady=10, sticky='e')
self.mEntry.grid(row=3, column=1, padx=10, pady=10, sticky='w')
super().placeWidgets()
def okPressed(self, *args):
self.master.controller.graph(self.master.controller.get_plots()[self.plotVar.get()].axes[0],
self.master.controller.get_spectra()[self.spectrumVar.get()],
color=self.colorVar.get(),
linewidth=float(self.linewidthVar.get()))
self.master.canvas.draw()
super().okPressed()

Categories