""" ui/analysis.py -------------- Analysis screen — three Matplotlib charts embedded in a ttk.Notebook. • Frequency — bar chart of how often each number appears • Heatmap — positional frequency matrix • Gap — draws since each number last appeared """ import tkinter as tk from tkinter import ttk import numpy as np import matplotlib matplotlib.use("TkAgg") from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk from db.models import get_all_games, get_game_by_id, get_game_by_name from core.analyzer import frequency_analysis, gap_analysis, positional_frequency _LAST_N_OPTIONS = { "All draws": None, "Last 50": 50, "Last 100": 100, "Last 200": 200, "Last 500": 500, } class AnalysisScreen(ttk.Frame): def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) self._game_id: int | None = None self._build_ui() # ── UI construction ─────────────────────────────────────────────────────── def _build_ui(self): # Controls bar bar = ttk.Frame(self, padding=(6, 6, 6, 4)) bar.pack(fill="x") ttk.Label(bar, text="Game:").pack(side="left") self._game_var = tk.StringVar() self._game_cb = ttk.Combobox( bar, textvariable=self._game_var, state="readonly", width=15 ) self._game_cb.pack(side="left", padx=(4, 14)) self._game_cb.bind("<>", lambda _: self._on_game_change()) ttk.Label(bar, text="Frequency window:").pack(side="left") self._last_n_var = tk.StringVar(value="All draws") last_n_cb = ttk.Combobox( bar, textvariable=self._last_n_var, values=list(_LAST_N_OPTIONS.keys()), state="readonly", width=12, ) last_n_cb.pack(side="left", padx=(4, 0)) last_n_cb.bind("<>", lambda _: self._redraw_frequency()) ttk.Button(bar, text="↻ Refresh", command=self.refresh).pack(side="right") # Notebook with three chart tabs nb = ttk.Notebook(self) nb.pack(fill="both", expand=True, padx=6, pady=(0, 6)) self._freq_fig, self._freq_canvas = self._make_tab(nb, "Frequency") self._heat_fig, self._heat_canvas = self._make_tab(nb, "Heatmap") self._gap_fig, self._gap_canvas = self._make_tab(nb, "Gap") def _make_tab(self, notebook, title): frame = ttk.Frame(notebook) notebook.add(frame, text=title) fig = Figure(tight_layout=True) canvas = FigureCanvasTkAgg(fig, master=frame) canvas.get_tk_widget().pack(fill="both", expand=True) tb_frame = ttk.Frame(frame) tb_frame.pack(fill="x") NavigationToolbar2Tk(canvas, tb_frame) return fig, canvas # ── Data loading ────────────────────────────────────────────────────────── def refresh(self): self._load_games() self._redraw_all() def _load_games(self): games = get_all_games(active_only=True) names = [g["name"] for g in games] self._game_cb["values"] = names if not self._game_var.get() or self._game_var.get() not in names: if names: self._game_var.set(names[0]) game = get_game_by_name(self._game_var.get()) self._game_id = game["id"] if game else None def _on_game_change(self): game = get_game_by_name(self._game_var.get()) self._game_id = game["id"] if game else None self._redraw_all() def _get_last_n(self): return _LAST_N_OPTIONS.get(self._last_n_var.get()) # ── Redraw helpers ──────────────────────────────────────────────────────── def _redraw_all(self): self._redraw_frequency() self._redraw_heatmap() self._redraw_gap() def _redraw_frequency(self): self._freq_fig.clear() ax = self._freq_fig.add_subplot(111) if self._game_id is None: _empty(ax, "Select a game above") else: _draw_frequency(ax, self._game_id, self._get_last_n()) self._freq_canvas.draw() def _redraw_heatmap(self): self._heat_fig.clear() ax = self._heat_fig.add_subplot(111) if self._game_id is None: _empty(ax, "Select a game above") else: _draw_heatmap(ax, self._game_id) self._heat_canvas.draw() def _redraw_gap(self): self._gap_fig.clear() ax = self._gap_fig.add_subplot(111) if self._game_id is None: _empty(ax, "Select a game above") else: _draw_gap(ax, self._game_id) self._gap_canvas.draw() # ── Pure chart-drawing functions (no Tkinter, just axes) ───────────────────── def _empty(ax, message="No data available — use Fetch Now to download draws"): ax.set_axis_off() ax.text(0.5, 0.5, message, ha="center", va="center", fontsize=13, color="#888888", transform=ax.transAxes) def _draw_frequency(ax, game_id, last_n=None): data = frequency_analysis(game_id, last_n=last_n) if not data: _empty(ax) return nums = sorted(data.keys()) counts = [data.get(n, 0) for n in nums] avg = sum(counts) / len(counts) max_c = max(counts) or 1 # Gradient: cold (blue) → hot (red) based on frequency colors = [ (0.15 + 0.7 * (c / max_c), 0.25, 1.0 - 0.75 * (c / max_c)) for c in counts ] ax.bar(nums, counts, color=colors, width=0.75, edgecolor="none") ax.axhline(avg, color="#e74c3c", linewidth=1.3, linestyle="--", label=f"Avg {avg:.1f}") title = "Number Frequency" if last_n: title += f" (last {last_n} draws)" ax.set_title(title, fontsize=11) ax.set_xlabel("Number", fontsize=9) ax.set_ylabel("Count", fontsize=9) ax.legend(fontsize=9) ax.tick_params(axis="x", labelsize=7) ax.grid(axis="y", alpha=0.35) def _draw_heatmap(ax, game_id): pos_freq = positional_frequency(game_id) game = get_game_by_id(game_id) if not pos_freq or not any(pos_freq.values()): _empty(ax) return main_count = game["main_count"] main_max = game["main_max"] # Build rows × cols matrix (positions × numbers) matrix = np.zeros((main_count, main_max)) for pos in range(1, main_count + 1): for num, cnt in pos_freq.get(pos, {}).items(): if 1 <= num <= main_max: matrix[pos - 1, num - 1] = cnt im = ax.imshow(matrix, aspect="auto", cmap="YlOrRd", interpolation="nearest") ax.figure.colorbar(im, ax=ax, fraction=0.025, pad=0.02, label="Count") ax.set_yticks(range(main_count)) ax.set_yticklabels([f"Pos {i + 1}" for i in range(main_count)], fontsize=9) step = 5 xticks = range(0, main_max, step) ax.set_xticks(list(xticks)) ax.set_xticklabels([str(i + 1) for i in xticks], fontsize=8) ax.set_title("Positional Frequency Heatmap", fontsize=11) ax.set_xlabel("Number", fontsize=9) def _draw_gap(ax, game_id): gaps = gap_analysis(game_id) if not gaps: _empty(ax) return nums = sorted(gaps.keys()) gap_vals = [gaps[n] for n in nums] avg = sum(gap_vals) / len(gap_vals) max_g = max(gap_vals) or 1 # Gradient: low gap = blue (hot), high gap = red (due) colors = [ (0.8 * (g / max_g), 0.15, 1.0 - 0.8 * (g / max_g)) for g in gap_vals ] ax.bar(nums, gap_vals, color=colors, width=0.75, edgecolor="none") ax.axhline(avg, color="#e74c3c", linewidth=1.3, linestyle="--", label=f"Avg {avg:.1f}") ax.set_title("Gap Analysis — Draws Since Last Appearance", fontsize=11) ax.set_xlabel("Number", fontsize=9) ax.set_ylabel("Draws since last seen", fontsize=9) ax.legend(fontsize=9) ax.tick_params(axis="x", labelsize=7) ax.grid(axis="y", alpha=0.35)