Files
2026-05-23 17:36:33 -04:00

264 lines
10 KiB
Python
Raw Permalink 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 datetime import date, timedelta
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, gap_analysis
from ui.widgets import BallsBar
# Weekday indices: Monday=0 … Sunday=6
_DRAW_DAYS: dict[str, list[int]] = {
"Powerball": [0, 2, 5], # Mon, Wed, Sat
"Mega Millions": [1, 4], # Tue, Fri
}
def _next_draw_date(game_name: str, from_date: date | None = None) -> str:
"""Return the next scheduled draw date as 'Day, Mon DD' (e.g. 'Sat, May 24')."""
draw_days = _DRAW_DAYS.get(game_name)
if not draw_days:
return ""
today = from_date or date.today()
for delta in range(1, 8):
candidate = today + timedelta(days=delta)
if candidate.weekday() in draw_days:
return f"{candidate.strftime('%a, %b')} {candidate.day}"
return ""
_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._filter_var = tk.StringVar(value="All Games")
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, 16, 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
# Game filter bar
filter_bar = ttk.Frame(inner)
filter_bar.pack(fill="x", pady=(0, 12))
ttk.Label(filter_bar, text="Show game:").pack(side="left", padx=(0, 6))
self._game_filter = ttk.Combobox(
filter_bar, textvariable=self._filter_var,
state="readonly", width=22,
)
self._game_filter.pack(side="left")
self._game_filter.bind("<<ComboboxSelected>>", lambda _e: self.refresh())
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)")
self._overdue_body = self._section_header("Most Overdue Numbers")
# ── Refresh ───────────────────────────────────────────────────────────────
def _update_filter_options(self):
games = get_all_games(active_only=True)
names = ["All Games"] + [g["name"] for g in games]
self._game_filter["values"] = names
if self._filter_var.get() not in names:
self._filter_var.set("All Games")
def _filtered_games(self):
games = get_all_games(active_only=True)
sel = self._filter_var.get()
if not sel or sel == "All Games":
return games
return [g for g in games if g["name"] == sel]
def refresh(self):
self._update_filter_options()
self._refresh_last_draws()
self._refresh_db_summary()
self._refresh_hot_numbers()
self._refresh_overdue()
def _refresh_last_draws(self):
for w in self._last_draw_body.winfo_children():
w.destroy()
games = self._filtered_games()
if not games:
ttk.Label(self._last_draw_body, text="No active games.",
foreground="#aaaaaa").pack(anchor="w")
return
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")
top_prize = game["top_prize"] if "top_prize" in game.keys() else ""
if top_prize:
ttk.Label(card, text=f"Top prize: {top_prize}",
foreground="#8e44ad", font=_CARD_FONT).pack(anchor="w")
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")
nums = [int(n) for n in draw["numbers"].split(",") if n.strip().isdigit()]
bonus = (int(draw["bonus"]) if draw["bonus"] and str(draw["bonus"]).isdigit()
and game["bonus_count"] > 0 else None)
BallsBar(card, numbers=nums, bonus=bonus).pack(anchor="w", pady=(6, 2))
if draw["multiplier"]:
ttk.Label(card, text=f"Multiplier: {draw['multiplier']}",
foreground="#777777", font=_CARD_FONT).pack(anchor="w")
next_draw = _next_draw_date(game["name"])
if next_draw:
ttk.Label(card, text=f"Next draw: {next_draw}",
foreground="#117a65", font=_CARD_FONT).pack(anchor="w", pady=(6, 0))
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 = self._filtered_games()
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(sorted(freq, key=freq.get, reverse=True)[:5])
BallsBar(row, numbers=top5, radius=11).pack(side="left", padx=(0, 6))
counts_str = " ".join(f"({freq[n]}×)" for n in top5)
ttk.Label(row, text=counts_str, foreground="#aaaaaa",
font=_CARD_FONT).pack(side="left")
def _refresh_overdue(self):
for w in self._overdue_body.winfo_children():
w.destroy()
games = self._filtered_games()
if not games:
ttk.Label(self._overdue_body, text="No active games.",
foreground="#aaaaaa").pack(anchor="w")
return
_ORANGE = ("#e67e22", "#ffffff")
for game in games:
gaps = gap_analysis(game["id"]) # {number: gap}
row = ttk.Frame(self._overdue_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 gaps:
ttk.Label(row, text="No data yet.", foreground="#aaaaaa",
font=_CARD_FONT).pack(side="left")
continue
top5 = sorted(sorted(gaps, key=gaps.get, reverse=True)[:5])
highlights = {n: _ORANGE for n in top5}
BallsBar(row, numbers=top5, radius=11,
highlights=highlights).pack(side="left", padx=(0, 6))
gaps_str = " ".join(f"({gaps[n]} ago)" for n in top5)
ttk.Label(row, text=gaps_str, foreground="#aaaaaa",
font=_CARD_FONT).pack(side="left")