-
Notifications
You must be signed in to change notification settings - Fork 0
/
interface.py
224 lines (175 loc) · 6.98 KB
/
interface.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# Author: Sam Lehmann
# Network with him at: https://www.linkedin.com/in/samuellehmann/
# Date: 2023-01-19
import sys
import tkinter
import tkinter as tk
from tkinter import ttk
from tkinter.ttk import Frame, Button, Label, Radiobutton, Checkbutton
import census
import main
import map_plot
TITLE = "Canadian Census Analyzer"
_PROCESSING_METHODS = ("Mean Difference", "Mean Percent Change", "Mean Percent Difference")
_GEOGRAPHY = ("Census Subdivisions", "Census Divisions", "Provinces")
_DATA_CLIP = ("Yes", "No")
_year_checkbuttons = []
_pm_radio_var = None
_data_clip_var = None
_geo_var = None
_year_selectors = []
_stackcombos = []
_root = None
def generate_interface():
"""
Create a UI to allow creation of a map
:return: None
"""
global _pm_radio_var, _data_clip_var, _geo_var
root = tk.Tk()
root.title(TITLE)
root.geometry('1400x800')
# Set the theme.
# Credits to: https://github.com/rdbende/Sun-Valley-ttk-theme & https://github.com/quantumblacklabs/qbstyles
# TKinter theme:
root.tk.call("source", "sun-valley.tcl")
root.tk.call("set_theme", "dark")
# Year selection
tk.Label(root, text="Select the censuses to analyze data for:").pack(fill="x", pady=10)
years_frame = Frame(root)
years_frame.pack(fill="x", pady=10)
for i, cen in enumerate(census.censuses):
years_frame.grid_columnconfigure(i, weight=1)
_year_checkbuttons.append(tk.IntVar())
c = Checkbutton(years_frame, text=str(cen.year), var=_year_checkbuttons[i], command=year_check_change)
c.grid(row=0, column=i)
# Geography Selection
tk.Label(root, text="What level of geography should be displayed?").pack(fill="x", pady=10)
geo_frame = Frame(root)
geo_frame.pack(fill="x", pady=10)
_geo_var = tkinter.StringVar(value=_GEOGRAPHY[0])
for i in range(0, len(_GEOGRAPHY)):
geo_frame.grid_columnconfigure(i, weight=1)
r = Radiobutton(geo_frame, text=_GEOGRAPHY[i], value=_GEOGRAPHY[i], var=_geo_var)
r.grid(row=0, column=i)
# Processing Method
tk.Label(root, text="Select the way to process data from multiple years:").pack(fill="x", pady=10)
processing_frame = Frame(root)
processing_frame.pack(fill="x", pady=10)
_pm_radio_var = tkinter.StringVar(value=_PROCESSING_METHODS[0])
for i in range(0, len(_PROCESSING_METHODS)):
processing_frame.grid_columnconfigure(i, weight=1)
r = Radiobutton(processing_frame, text=_PROCESSING_METHODS[i], value=_PROCESSING_METHODS[i], var=_pm_radio_var)
r.grid(row=0, column=i)
# Data Clipping
tk.Label(root, text="Should outliers be removed from the data?").pack(fill="x", pady=10)
outlier_frame = Frame(root)
outlier_frame.pack(fill="x", pady=10)
_data_clip_var = tkinter.StringVar(value=_DATA_CLIP[0])
for i in range(0, len(_DATA_CLIP)):
outlier_frame.grid_columnconfigure(i, weight=1)
r = Radiobutton(outlier_frame, text=_DATA_CLIP[i], value=_DATA_CLIP[i], var=_data_clip_var)
r.grid(row=0, column=i)
# Value Selection
for i, cen in enumerate(census.censuses):
_year_selectors.append(Frame(root))
tk.Label(_year_selectors[i], text=f"Select a value of interest from year {cen.year}:").pack(fill="x", pady=10)
_stackcombos.append(StackCombo(_year_selectors[i], cen.char_tree, None, width=150))
_stackcombos[-1].pack(fill="x", pady=10)
Button(root, text="Create Plot", command=create_plot).pack(fill="x", side="bottom")
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
def year_check_change():
"""
Event that updates the UI when a year checkbox changes state
:return: None
"""
for i, check_val in enumerate(_year_checkbuttons):
if check_val.get():
_year_selectors[i].pack(fill="x", pady=10)
else:
_year_selectors[i].pack_forget()
def create_plot():
"""
Creates a map plot based on the options that are selected within the UI
:return: None
"""
cen = []
strings = []
func = None
for i, check_val in enumerate(_year_checkbuttons):
if check_val.get():
cen.append(census.censuses[i])
strings.append(_stackcombos[i].get_final_val())
func_name = _pm_radio_var.get()
for i, proc_method in enumerate(_PROCESSING_METHODS):
if func_name == proc_method:
func = map_plot.FUNC_LIST[i]
break
map_plot.plot_map(func_name, strings, cen, func, clipped=_data_clip_var.get() == _DATA_CLIP[0], type=_geo_var.get())
def on_closing():
"""
Handles the exit button being pressed
:return:
"""
print("End of program")
sys.exit()
class StackCombo(tk.Frame):
"""
An extension of a tkinter ComboBox, but supports nesting, thereby created stacked combo boxes.
"""
# The number of spaces that each child is indented by
_NUM_SPACES = 5
def __init__(self, master, node, master_combo=None, **kwargs):
tk.Frame.__init__(self, master, **kwargs)
self.master = master
self.master_combo = master_combo
self.node = node
self.child = None
if master_combo is not None:
self.indent_level = master_combo.get_indent_level() + 1
text = " " * (self.indent_level - 1) * StackCombo._NUM_SPACES + "↳"
Label(self, text=text).pack(side="left", padx=10)
else:
self.indent_level = 0
values = []
# Remove the separator
for i, ch in enumerate(node.children):
values.append(ch.name.split(main.TREE_SEPARATOR)[-1])
self.combo = ttk.Combobox(self, values=values, **kwargs)
self.combo.bind("<<ComboboxSelected>>", self.field_change)
self.combo.pack(side="left", fill="x", padx=10)
@staticmethod
def _get_child_by_name(node, name):
for child in node.children:
if name in child.name:
return child
raise Exception("No child by that name exists")
def get_final_val(self):
"""
Gets the value of the stack combo that is lowest down in the hierarchy, be it this stack combo or a child
:return: A string of the stack combo text
"""
if self.child is not None:
return self.child.get_final_val()
return self.combo.get()
def field_change(self, _):
"""
Event that is triggered when the value of the combobox changes
:param _:
:return:
"""
# Destroy children
if self.child is not None:
self.child.destroy()
# Add new children if there are any child nodes
child_node = StackCombo._get_child_by_name(self.node, self.combo.get())
if len(child_node.children) > 0:
self.child = StackCombo(self.master, child_node, self, width=150)
self.child.pack(fill="x", pady=10)
def destroy(self):
if self.child is not None:
self.child.destroy()
super().destroy()
def get_indent_level(self):
return self.indent_level