import tkinter as tk from tkinter import ttk, messagebox import json import os import uuid import math
DB_FILE = "biology_db.json"
def load_db(): if os.path.exists(DB_FILE): with open(DB_FILE, "r") as f: return json.load(f) return {"students": []}
def save_db(data): with open(DB_FILE, "w") as f: json.dump(data, f, indent=2) messagebox.showinfo("Saved", "Data saved successfully!")
db = load_db()
SUBJECT = "Biology" SEMESTERS = ["Semester 1", "Semester 2", "Semester 3"] SEM_COLORS = ["#5B8CCC", "#CC7B5B", "#3DAA7A"] RANK_COLORS = ["#FFD700", "#B0B0B0", "#CD7F32", "#607080"]
BG_DARK = "#1E1E1E" # main ash-black background BG_PANEL = "#2A2A2A" # slightly lighter panel BG_CARD = "#333333" # card / treeview background FG_WHITE = "#F0F0F0" FG_MUTED = "#AAAAAA" ACCENT_BLUE = "#4C9BE8" ACCENT_GREEN = "#1D9E75" ACCENT_RED = "#E24B4A" ACCENT_PURP = "#9B59B6" BTN_RADIUS = 8 # used in rounded canvas buttons
def get_suffix(n): if 11 <= n <= 13: return f"{n}th" return f"{n}{({1:'st',2:'nd',3:'rd'}).get(n % 10, 'th')}"
def generate_id(): return "STU-" + str(uuid.uuid4())[:8].upper()
def compute_zscore(mark, all_marks): if len(all_marks) < 2: return 0.0 mean = sum(all_marks) / len(all_marks) variance = sum((x - mean) ** 2 for x in all_marks) / len(all_marks) std = math.sqrt(variance) if std == 0: return 0.0 return (mark - mean) / std
def get_all_sem3_marks(): return [s["semesters"][2] for s in db["students"]]
def rounded_button(parent, text, command, color=BG_CARD, fg=FG_WHITE, width=160, height=34, radius=BTN_RADIUS): """Draw a smooth-edge rounded button using Canvas.""" cvs = tk.Canvas(parent, width=width, height=height, bg=BG_PANEL, highlightthickness=0, cursor="hand2")
def draw(fill):
cvs.delete("btn")
r = radius
x0, y0, x1, y1 = 2, 2, width - 2, height - 2
cvs.create_arc(x0, y0, x0+2*r, y0+2*r, start=90, extent=90,
fill=fill, outline=fill, tags="btn")
cvs.create_arc(x1-2*r, y0, x1, y0+2*r, start=0, extent=90,
fill=fill, outline=fill, tags="btn")
cvs.create_arc(x0, y1-2*r, x0+2*r, y1, start=180, extent=90,
fill=fill, outline=fill, tags="btn")
cvs.create_arc(x1-2*r, y1-2*r, x1, y1, start=270, extent=90,
fill=fill, outline=fill, tags="btn")
cvs.create_rectangle(x0+r, y0, x1-r, y1, fill=fill, outline=fill, tags="btn")
cvs.create_rectangle(x0, y0+r, x1, y1-r, fill=fill, outline=fill, tags="btn")
cvs.create_text(width//2, height//2, text=text,
font=("Arial", 9, "bold"), fill=fg, tags="btn")
draw(color)
def on_enter(_):
# lighten on hover
draw("#4A4A4A" if color == BG_CARD else color)
def on_leave(_):
draw(color)
def on_click(_):
command()
cvs.bind("<Enter>", on_enter)
cvs.bind("<Leave>", on_leave)
cvs.bind("<Button-1>", on_click)
return cvs
def draw_bar_chart(canvas, data_sets, labels, colors, title, width=680, height=300, show_rank=False): canvas.delete("all") pad_left, pad_right, pad_top, pad_bottom = 60, 20, 50, 60
chart_w = width - pad_left - pad_right
chart_h = height - pad_top - pad_bottom
all_vals = [v for ds in data_sets for v in ds["values"]]
max_val = max(all_vals + [100]) + 15
canvas.create_text(width // 2, 22, text=title,
font=("Arial", 11, "bold"), fill=FG_WHITE)
for i in range(6):
y_val = int(i * max_val / 5)
y_px = pad_top + chart_h - int((y_val / max_val) * chart_h)
canvas.create_line(pad_left, y_px, pad_left + chart_w, y_px,
fill="#3A3A3A", dash=(4, 3))
canvas.create_text(pad_left - 6, y_px, text=str(y_val),
anchor="e", font=("Arial", 8), fill=FG_MUTED)
canvas.create_line(pad_left, pad_top, pad_left, pad_top + chart_h, fill="#555")
canvas.create_line(pad_left, pad_top + chart_h,
pad_left + chart_w, pad_top + chart_h, fill="#555")
num_groups = len(labels)
num_sets = len(data_sets)
group_w = chart_w / max(num_groups, 1)
bar_w = (group_w * 0.7) / max(num_sets, 1)
gap = group_w * 0.15
for g, label in enumerate(labels):
group_x = pad_left + g * group_w + gap
for s, ds in enumerate(data_sets):
val = ds["values"][g]
color = colors[g] if show_rank else colors[s % len(colors)]
bar_x = group_x + s * bar_w
bar_h = int((val / max_val) * chart_h)
bar_y = pad_top + chart_h - bar_h
canvas.create_rectangle(bar_x, bar_y, bar_x + bar_w - 2,
pad_top + chart_h,
fill=color, outline=BG_DARK, width=1)
canvas.create_text(bar_x + (bar_w - 2) / 2, bar_y - 7,
text=str(int(val)),
font=("Arial", 8, "bold"), fill=FG_WHITE)
if show_rank:
bx = bar_x + (bar_w - 2) / 2
by = bar_y - 22
canvas.create_rectangle(bx-14, by-8, bx+14, by+8,
fill=color, outline=BG_DARK)
canvas.create_text(bx, by, text=get_suffix(g + 1),
font=("Arial", 8, "bold"), fill="white")
lx = pad_left + g * group_w + group_w / 2
canvas.create_text(lx, pad_top + chart_h + 14, text=label,
font=("Arial", 9), fill=FG_MUTED,
width=int(group_w - 4))
for s, ds in enumerate(data_sets):
lx = pad_left + s * 130
color = colors[s % len(colors)]
canvas.create_rectangle(lx, height - 16, lx + 11, height - 5,
fill=color, outline="")
canvas.create_text(lx + 15, height - 11, text=ds["label"],
anchor="w", font=("Arial", 9), fill=FG_MUTED)
def open_add_student(root, refresh_cb, edit_index=None): win = tk.Toplevel(root) win.title("Edit Student" if edit_index is not None else "Add Student") win.configure(bg=BG_DARK) win.resizable(False, False)
editing = edit_index is not None
student = db["students"][edit_index] if editing else None
tk.Label(win,
text="Edit Student" if editing else "Add Student",
font=("Arial", 13, "bold"),
bg=BG_DARK, fg=FG_WHITE).pack(pady=12)
# Student ID (read-only display when editing)
id_frame = tk.Frame(win, bg=BG_DARK)
id_frame.pack(pady=2)
tk.Label(id_frame, text="Student ID:", width=13,
bg=BG_DARK, fg=FG_MUTED, anchor="w").pack(side="left")
id_val = student["student_id"] if editing else "(auto-generated)"
tk.Label(id_frame, text=id_val,
font=("Arial", 9, "bold"), bg=BG_DARK, fg=ACCENT_BLUE).pack(side="left")
# Name
nf = tk.Frame(win, bg=BG_DARK)
nf.pack(pady=4)
tk.Label(nf, text="Name:", width=13, bg=BG_DARK,
fg=FG_MUTED, anchor="w").pack(side="left")
name_entry = tk.Entry(nf, width=22, font=("Arial", 10),
bg=BG_CARD, fg=FG_WHITE,
insertbackground=FG_WHITE, relief="flat")
name_entry.pack(side="left", ipady=4)
if editing:
name_entry.insert(0, student["name"])
# Marks for each semester
entries = {}
for sem_i, sem in enumerate(SEMESTERS):
tk.Label(win, text=sem, font=("Arial", 10, "bold"),
bg=BG_DARK, fg=SEM_COLORS[sem_i]).pack(pady=(10, 2))
row = tk.Frame(win, bg=BG_DARK)
row.pack(pady=2)
tk.Label(row, text=f" {SUBJECT}:", width=14,
bg=BG_DARK, fg=FG_MUTED, anchor="w").pack(side="left")
e = tk.Entry(row, width=8, font=("Arial", 10),
bg=BG_CARD, fg=FG_WHITE,
insertbackground=FG_WHITE, relief="flat")
e.pack(side="left", ipady=4)
if editing and sem_i < len(student["semesters"]):
val = student["semesters"][sem_i]
if val != "":
e.insert(0, str(int(val)))
entries[sem_i] = e
def save_student():
name = name_entry.get().strip()
if not name:
messagebox.showwarning("Warning", "Enter student name.")
return
semester_marks = []
for sem_i in range(3):
val = entries[sem_i].get().strip()
if not val:
messagebox.showwarning("Warning",
f"Enter Biology mark for Semester {sem_i+1}.")
return
try:
m = float(val)
if not 0 <= m <= 100:
raise ValueError
semester_marks.append(m)
except ValueError:
messagebox.showerror("Error",
f"Invalid mark for Semester {sem_i+1}. Enter 0–100.")
return
if editing:
db["students"][edit_index]["name"] = name
db["students"][edit_index]["semesters"] = semester_marks
else:
db["students"].append({
"student_id": generate_id(),
"name": name,
"semesters": semester_marks
})
save_db(db)
refresh_cb()
win.destroy()
btn = rounded_button(win, "Save Student", save_student,
color=ACCENT_GREEN, width=170, height=36)
btn.pack(pady=16)
def open_progress(student): all_marks = get_all_sem3_marks() z = compute_zscore(student["semesters"][2], all_marks)
win = tk.Toplevel()
win.title(f"Progress — {student['name']}")
win.configure(bg=BG_DARK)
tk.Label(win,
text=f"Progress: {student['name']} | ID: {student['student_id']}",
font=("Arial", 12, "bold"), bg=BG_DARK, fg=FG_WHITE).pack(pady=8)
# Z-score banner
z_color = ACCENT_GREEN if z >= 0 else ACCENT_RED
z_text = f"A/L Z-Score (Sem 3): {z:+.3f} — "
z_text += "Above average" if z > 0 else ("Below average" if z < 0 else "At average")
tk.Label(win, text=z_text, font=("Arial", 10, "bold"),
bg=BG_DARK, fg=z_color).pack(pady=2)
tk.Label(win,
text="Z-score = (your mark − class mean) ÷ std deviation",
font=("Arial", 8), bg=BG_DARK, fg=FG_MUTED).pack()
canvas = tk.Canvas(win, width=680, height=300,
bg=BG_PANEL, highlightthickness=0)
canvas.pack(padx=10, pady=10)
draw_bar_chart(canvas,
[{"label": SUBJECT, "values": student["semesters"]}],
SEMESTERS,
[SEM_COLORS[0], SEM_COLORS[1], SEM_COLORS[2]],
f"{SUBJECT} — Semester Progress")
def open_rankings(): students = db["students"] if not students: messagebox.showinfo("No Data", "No students found.") return
win = tk.Toplevel()
win.title("Semester 3 Rankings")
win.configure(bg=BG_DARK)
tk.Label(win, text="Biology — Semester 3 Rankings",
font=("Arial", 13, "bold"), bg=BG_DARK, fg=FG_WHITE).pack(pady=8)
# Rankings table with Z-scores
cols_frame = tk.Frame(win, bg=BG_DARK)
cols_frame.pack(fill="x", padx=10)
style = ttk.Style()
style.theme_use("default")
style.configure("Rank.Treeview",
background=BG_CARD, foreground=FG_WHITE,
fieldbackground=BG_CARD, rowheight=28,
font=("Arial", 9))
style.configure("Rank.Treeview.Heading",
background=BG_PANEL, foreground=FG_MUTED,
font=("Arial", 9, "bold"), relief="flat")
style.map("Rank.Treeview", background=[("selected", "#3A5A7A")])
cols = ("rank", "id", "name", "mark", "zscore")
tree = ttk.Treeview(win, columns=cols, show="headings",
height=min(len(students), 12),
style="Rank.Treeview")
tree.heading("rank", text="Rank")
tree.heading("id", text="Student ID")
tree.heading("name", text="Name")
tree.heading("mark", text="Sem 3 Mark")
tree.heading("zscore", text="A/L Z-Score")
tree.column("rank", width=60, anchor="center")
tree.column("id", width=120, anchor="center")
tree.column("name", width=160)
tree.column("mark", width=110, anchor="center")
tree.column("zscore", width=120, anchor="center")
tree.pack(padx=10, pady=6, fill="x")
all_marks = [s["semesters"][2] for s in students]
ranked = sorted(students, key=lambda x: x["semesters"][2], reverse=True)
for rank, s in enumerate(ranked, 1):
mark = s["semesters"][2]
z = compute_zscore(mark, all_marks)
tree.insert("", "end", values=(
get_suffix(rank),
s["student_id"],
s["name"],
f"{mark:.0f}",
f"{z:+.3f}"
))
# Bar chart
canvas = tk.Canvas(win, width=680, height=300,
bg=BG_PANEL, highlightthickness=0)
canvas.pack(padx=10, pady=6)
names = [s["name"] for s in ranked]
marks = [s["semesters"][2] for s in ranked]
colors = [RANK_COLORS[i] if i < 3 else RANK_COLORS[3]
for i in range(len(ranked))]
draw_bar_chart(canvas,
[{"label": SUBJECT, "values": marks}],
names, colors,
"Biology — Semester 3 Rankings",
show_rank=True)
def open_comparison(): students = db["students"] if not students: messagebox.showinfo("No Data", "No students found.") return
win = tk.Toplevel()
win.title("All Students Comparison")
win.configure(bg=BG_DARK)
tk.Label(win, text="Biology — All Students Comparison",
font=("Arial", 13, "bold"), bg=BG_DARK, fg=FG_WHITE).pack(pady=8)
canvas = tk.Canvas(win, width=680, height=300,
bg=BG_PANEL, highlightthickness=0)
canvas.pack(padx=10, pady=8)
datasets = []
for sem_i, sem in enumerate(SEMESTERS):
vals = [s["semesters"][sem_i] for s in students]
datasets.append({"label": sem, "values": vals})
names = [s["name"] for s in students]
draw_bar_chart(canvas, datasets, names, SEM_COLORS,
"Biology — All Semesters Comparison")
def open_zscore_overview(): students = db["students"] if not students: messagebox.showinfo("No Data", "No students found.") return
win = tk.Toplevel()
win.title("A/L Z-Score Overview")
win.configure(bg=BG_DARK)
tk.Label(win, text="A/L Z-Score Overview — Semester 3",
font=("Arial", 13, "bold"), bg=BG_DARK, fg=FG_WHITE).pack(pady=8)
tk.Label(win,
text="Z-score shows how far each student is from the class average (in standard deviations).",
font=("Arial", 9), bg=BG_DARK, fg=FG_MUTED).pack()
style = ttk.Style()
style.configure("Z.Treeview",
background=BG_CARD, foreground=FG_WHITE,
fieldbackground=BG_CARD, rowheight=26,
font=("Arial", 9))
style.configure("Z.Treeview.Heading",
background=BG_PANEL, foreground=FG_MUTED,
font=("Arial", 9, "bold"), relief="flat")
style.map("Z.Treeview", background=[("selected", "#3A5A7A")])
cols = ("id", "name", "s1", "s2", "s3", "z1", "z2", "z3")
tree = ttk.Treeview(win, columns=cols, show="headings",
height=min(len(students), 14),
style="Z.Treeview")
for col, head, w in [
("id", "Student ID", 120),
("name", "Name", 150),
("s1", "Sem 1", 80),
("s2", "Sem 2", 80),
("s3", "Sem 3", 80),
("z1", "Z (Sem 1)", 100),
("z2", "Z (Sem 2)", 100),
("z3", "Z (Sem 3)", 100),
]:
tree.heading(col, text=head)
tree.column(col, width=w, anchor="center")
tree.pack(padx=10, pady=10, fill="x")
for sem_i in range(3):
all_m = [s["semesters"][sem_i] for s in students]
for s in students:
s[f"_z{sem_i}"] = compute_zscore(s["semesters"][sem_i], all_m)
for s in students:
tree.insert("", "end", values=(
s["student_id"], s["name"],
f"{s['semesters'][0]:.0f}",
f"{s['semesters'][1]:.0f}",
f"{s['semesters'][2]:.0f}",
f"{s['_z0']:+.3f}",
f"{s['_z1']:+.3f}",
f"{s['_z2']:+.3f}",
))
def main(): root = tk.Tk() root.title("Biology Mark Ranking System") root.geometry("780x600") root.configure(bg=BG_DARK)
# Apply dark ttk theme
style = ttk.Style()
style.theme_use("default")
style.configure("Main.Treeview",
background=BG_CARD, foreground=FG_WHITE,
fieldbackground=BG_CARD, rowheight=26,
font=("Arial", 9))
style.configure("Main.Treeview.Heading",
background=BG_PANEL, foreground=FG_MUTED,
font=("Arial", 9, "bold"), relief="flat")
style.map("Main.Treeview", background=[("selected", "#3A5A7A")])
style.configure("TScrollbar", background=BG_CARD, troughcolor=BG_PANEL)
tk.Label(root, text="🔬 Biology Mark Ranking System",
font=("Arial", 15, "bold"), bg=BG_DARK, fg=FG_WHITE).pack(pady=12)
# ── top action buttons ────────────────────────────
top_frame = tk.Frame(root, bg=BG_DARK)
top_frame.pack(pady=6)
def refresh():
render_student_list()
btn_defs = [
("Add Student", lambda: open_add_student(root, refresh), ACCENT_GREEN),
("View Rankings", open_rankings, ACCENT_BLUE),
("Compare All", open_comparison, "#E87B4C"),
("Z-Score Table", open_zscore_overview, ACCENT_PURP),
]
for col_i, (label, cmd, color) in enumerate(btn_defs):
b = rounded_button(top_frame, label, cmd, color=color, width=155, height=34)
b.grid(row=0, column=col_i, padx=5)
# ── student table ──────────────────────────────────
list_frame = tk.Frame(root, bg=BG_DARK)
list_frame.pack(fill="both", expand=True, padx=16, pady=6)
columns = ("id", "name", "sem1", "sem2", "sem3", "zscore", "trend")
tree = ttk.Treeview(list_frame, columns=columns, show="headings",
height=16, style="Main.Treeview")
tree.heading("id", text="Student ID")
tree.heading("name", text="Name")
tree.heading("sem1", text="Sem 1")
tree.heading("sem2", text="Sem 2")