Files
lottosight/ui/dashboard.py
T
2026-05-23 12:11:14 -04:00

176 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ui/dashboard.py
---------------
Dashboard — the home screen shown on app launch.
Sections:
• Last Draw Results — most-recent draw card per active game
• Database Summary — draw counts + prediction total
• Hot Numbers — top-5 most frequent numbers per game (last 100 draws)
"""
import tkinter as tk
from tkinter import ttk
from db.database import get_db_stats
from db.models import get_all_games, get_last_draw, get_draw_count, get_predictions
from core.analyzer import frequency_analysis
_HEADER_FONT = ("TkDefaultFont", 10, "bold")
_CARD_FONT = ("TkDefaultFont", 9)
_NUMBER_FONT = ("TkDefaultFont", 12, "bold")
_SECTION_PAD = (0, 20) # top, bottom
def _fmt_numbers(raw: str) -> str:
"""'1,13,36,61,69' → '01 13 36 61 69'"""
if not raw:
return ""
return " ".join(f"{int(n):02d}" for n in raw.split(","))
class DashboardScreen(ttk.Frame):
def __init__(self, parent, on_fetch=None, **kwargs):
super().__init__(parent, **kwargs)
self._on_fetch = on_fetch
self._build_ui()
# ── Layout ────────────────────────────────────────────────────────────────
def _build_ui(self):
# Scrollable canvas so the screen stays clean at small sizes
canvas = tk.Canvas(self, highlightthickness=0)
vsb = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
inner = ttk.Frame(canvas, padding=(28, 20, 28, 20))
win = canvas.create_window((0, 0), window=inner, anchor="nw")
def _resize(event=None):
canvas.configure(scrollregion=canvas.bbox("all"))
canvas.itemconfig(win, width=canvas.winfo_width())
inner.bind("<Configure>", lambda e: _resize())
canvas.bind("<Configure>", lambda e: _resize())
canvas.bind("<Enter>",
lambda e: canvas.bind_all("<MouseWheel>",
lambda ev: canvas.yview_scroll(-1 * (ev.delta // 120), "units")))
canvas.bind("<Leave>",
lambda e: canvas.unbind_all("<MouseWheel>"))
self._inner = inner
self._draw_sections()
def _section_header(self, title: str):
f = ttk.Frame(self._inner)
f.pack(fill="x", pady=(0, 8))
ttk.Label(f, text=title, font=_HEADER_FONT).pack(anchor="w")
ttk.Separator(f, orient="horizontal").pack(fill="x", pady=(3, 0))
body = ttk.Frame(self._inner)
body.pack(fill="x", pady=_SECTION_PAD)
return body
def _draw_sections(self):
self._last_draw_body = self._section_header("Last Draw Results")
self._db_body = self._section_header("Database Summary")
self._hot_body = self._section_header("Hot Numbers (last 100 draws)")
# ── Refresh ───────────────────────────────────────────────────────────────
def refresh(self):
self._refresh_last_draws()
self._refresh_db_summary()
self._refresh_hot_numbers()
def _refresh_last_draws(self):
for w in self._last_draw_body.winfo_children():
w.destroy()
games = get_all_games(active_only=True)
if not games:
ttk.Label(self._last_draw_body, text="No active games.",
foreground="#aaaaaa").pack(anchor="w")
return
# Lay cards out in a horizontal row that wraps via grid
row_frame = ttk.Frame(self._last_draw_body)
row_frame.pack(fill="x")
for col_idx, game in enumerate(games):
draw = get_last_draw(game["id"])
card = ttk.LabelFrame(
row_frame, text=game["name"], padding=(14, 10, 14, 12)
)
card.grid(row=0, column=col_idx, padx=(0, 16), sticky="nw")
if draw is None:
ttk.Label(card, text="No draws yet.", foreground="#aaaaaa",
font=_CARD_FONT).pack(anchor="w")
else:
ttk.Label(card, text=draw["draw_date"],
foreground="#555555", font=_CARD_FONT).pack(anchor="w")
ttk.Label(card,
text=_fmt_numbers(draw["numbers"]),
font=_NUMBER_FONT, foreground="#1a1a2e").pack(anchor="w", pady=(6, 2))
if game["bonus_count"] > 0 and draw["bonus"]:
bonus_label = "Bonus" if game["name"] != "Powerball" else "Powerball"
ttk.Label(card, text=f"{bonus_label}: {draw['bonus']}",
foreground="#2471a3", font=_CARD_FONT).pack(anchor="w")
if draw["multiplier"]:
ttk.Label(card, text=f"Multiplier: {draw['multiplier']}",
foreground="#777777", font=_CARD_FONT).pack(anchor="w")
def _refresh_db_summary(self):
for w in self._db_body.winfo_children():
w.destroy()
stats = get_db_stats() # {game_name: count}
for name, count in sorted(stats.items()):
row = ttk.Frame(self._db_body)
row.pack(fill="x", pady=1)
ttk.Label(row, text=f"{name}:", width=18, anchor="w",
font=_CARD_FONT).pack(side="left")
ttk.Label(row, text=f"{count:,} draws",
foreground="#555555", font=_CARD_FONT).pack(side="left")
pred_total = len(get_predictions(limit=100_000))
row = ttk.Frame(self._db_body)
row.pack(fill="x", pady=(4, 0))
ttk.Label(row, text="Saved predictions:", width=18, anchor="w",
font=_CARD_FONT).pack(side="left")
ttk.Label(row, text=str(pred_total),
foreground="#555555", font=_CARD_FONT).pack(side="left")
def _refresh_hot_numbers(self):
for w in self._hot_body.winfo_children():
w.destroy()
games = get_all_games(active_only=True)
if not games:
ttk.Label(self._hot_body, text="No active games.",
foreground="#aaaaaa").pack(anchor="w")
return
for game in games:
freq = frequency_analysis(game["id"], last_n=100)
row = ttk.Frame(self._hot_body)
row.pack(fill="x", pady=3)
ttk.Label(row, text=f"{game['name']}:", width=18, anchor="w",
font=_CARD_FONT).pack(side="left")
if not freq:
ttk.Label(row, text="No data yet.", foreground="#aaaaaa",
font=_CARD_FONT).pack(side="left")
continue
top5 = sorted(freq, key=freq.get, reverse=True)[:5]
nums_str = " ".join(f"{n:02d}" for n in sorted(top5))
ttk.Label(row, text=nums_str, font=_CARD_FONT,
foreground="#1a5276").pack(side="left")
counts_str = " ".join(f"({freq[n]}×)" for n in sorted(top5))
ttk.Label(row, text=f" {counts_str}", foreground="#aaaaaa",
font=_CARD_FONT).pack(side="left")