05/23 Phase 10
This commit is contained in:
@@ -11,7 +11,9 @@
|
|||||||
"Bash(python -m pytest tests/ -v --tb=short)",
|
"Bash(python -m pytest tests/ -v --tb=short)",
|
||||||
"Bash(python -m pytest tests/ -q --tb=short)",
|
"Bash(python -m pytest tests/ -q --tb=short)",
|
||||||
"Bash(python -m PyInstaller lottosight.spec --clean)",
|
"Bash(python -m PyInstaller lottosight.spec --clean)",
|
||||||
"Bash(python -c \"from ui.dashboard import DashboardScreen\")"
|
"Bash(python -c \"from ui.dashboard import DashboardScreen\")",
|
||||||
|
"Bash(python -m pytest tests/test_analysis_charts.py -v --tb=short)",
|
||||||
|
"Bash(python -m pytest tests/ -q)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,6 +325,18 @@ All actions are logged to console and optionally to a log file:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### ✅ Phase 10 — Complete Analysis Screen (7 Charts)
|
||||||
|
- [x] Extend `ui/analysis.py` with 4 new chart tabs (was 3, now 7)
|
||||||
|
- [x] Pairs — horizontal bar chart, top-20 most common number pairs
|
||||||
|
- [x] Odd/Even — bar chart of draw-split distribution (e.g. 3O/2E = 38%)
|
||||||
|
- [x] Sum Range — histogram of total draw-sum distribution
|
||||||
|
- [x] Deltas — bar chart of gaps between consecutive numbers within draws
|
||||||
|
- [x] Add `_draw_pairs`, `_draw_odd_even`, `_draw_sum_range`, `_draw_deltas` pure functions
|
||||||
|
- [x] `tests/test_analysis_charts.py` — 23 tests using Agg backend (no display needed)
|
||||||
|
- [x] 200/200 total tests passing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### ✅ Phase 9 — Dashboard Screen
|
### ✅ Phase 9 — Dashboard Screen
|
||||||
- [x] Write `ui/dashboard.py`
|
- [x] Write `ui/dashboard.py`
|
||||||
- [x] Last Draw Results — card per active game (date, numbers, bonus, multiplier)
|
- [x] Last Draw Results — card per active game (date, numbers, bonus, multiplier)
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
"""
|
||||||
|
tests/test_analysis_charts.py
|
||||||
|
------------------------------
|
||||||
|
Tests for the pure chart-drawing functions in ui/analysis.py.
|
||||||
|
Uses matplotlib's Agg (non-interactive) backend so no display is required.
|
||||||
|
Each test verifies that the function populates the axes correctly
|
||||||
|
and doesn't raise on empty or populated data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from db.models import get_game_by_name, insert_draw
|
||||||
|
from ui.analysis import (
|
||||||
|
_empty,
|
||||||
|
_draw_frequency,
|
||||||
|
_draw_heatmap,
|
||||||
|
_draw_gap,
|
||||||
|
_draw_pairs,
|
||||||
|
_draw_odd_even,
|
||||||
|
_draw_sum_range,
|
||||||
|
_draw_deltas,
|
||||||
|
)
|
||||||
|
|
||||||
|
DRAWS = [
|
||||||
|
("2024-01-01", [1, 13, 36, 61, 69]),
|
||||||
|
("2024-01-03", [1, 2, 13, 45, 69]),
|
||||||
|
("2024-01-05", [2, 13, 22, 36, 55]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _ax():
|
||||||
|
"""Return a fresh Axes on a headless figure."""
|
||||||
|
fig = Figure()
|
||||||
|
return fig.add_subplot(111)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pb(tmp_db):
|
||||||
|
game = get_game_by_name("Powerball")
|
||||||
|
for date, nums in DRAWS:
|
||||||
|
insert_draw(game["id"], date, nums, bonus=7, source="test")
|
||||||
|
return game
|
||||||
|
|
||||||
|
|
||||||
|
# ── _empty ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_empty_disables_axis(tmp_db):
|
||||||
|
ax = _ax()
|
||||||
|
_empty(ax, "test message")
|
||||||
|
assert not ax.get_visible() or not ax.axison
|
||||||
|
|
||||||
|
|
||||||
|
# ── _draw_frequency ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_draw_frequency_creates_bars(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_frequency(ax, pb["id"])
|
||||||
|
assert len(ax.patches) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_frequency_empty_db_no_error(tmp_db):
|
||||||
|
pb = get_game_by_name("Powerball")
|
||||||
|
ax = _ax()
|
||||||
|
_draw_frequency(ax, pb["id"]) # should show _empty placeholder, not raise
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_frequency_last_n(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_frequency(ax, pb["id"], last_n=1)
|
||||||
|
assert len(ax.patches) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_frequency_title_includes_last_n(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_frequency(ax, pb["id"], last_n=50)
|
||||||
|
assert "50" in ax.get_title()
|
||||||
|
|
||||||
|
|
||||||
|
# ── _draw_heatmap ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_draw_heatmap_creates_image(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_heatmap(ax, pb["id"])
|
||||||
|
assert len(ax.images) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_heatmap_empty_db_no_error(tmp_db):
|
||||||
|
pb = get_game_by_name("Powerball")
|
||||||
|
ax = _ax()
|
||||||
|
_draw_heatmap(ax, pb["id"])
|
||||||
|
|
||||||
|
|
||||||
|
# ── _draw_gap ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_draw_gap_creates_bars(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_gap(ax, pb["id"])
|
||||||
|
assert len(ax.patches) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_gap_empty_db_no_error(tmp_db):
|
||||||
|
pb = get_game_by_name("Powerball")
|
||||||
|
ax = _ax()
|
||||||
|
_draw_gap(ax, pb["id"])
|
||||||
|
|
||||||
|
|
||||||
|
# ── _draw_pairs ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_draw_pairs_creates_bars(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_pairs(ax, pb["id"])
|
||||||
|
assert len(ax.patches) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_pairs_respects_top_n(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_pairs(ax, pb["id"], top_n=5)
|
||||||
|
assert len(ax.patches) <= 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_pairs_empty_db_no_error(tmp_db):
|
||||||
|
pb = get_game_by_name("Powerball")
|
||||||
|
ax = _ax()
|
||||||
|
_draw_pairs(ax, pb["id"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_pairs_title_set(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_pairs(ax, pb["id"], top_n=10)
|
||||||
|
assert "Pair" in ax.get_title() or "pair" in ax.get_title().lower()
|
||||||
|
|
||||||
|
|
||||||
|
# ── _draw_odd_even ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_draw_odd_even_creates_bars(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_odd_even(ax, pb["id"])
|
||||||
|
assert len(ax.patches) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_odd_even_empty_db_no_error(tmp_db):
|
||||||
|
pb = get_game_by_name("Powerball")
|
||||||
|
ax = _ax()
|
||||||
|
_draw_odd_even(ax, pb["id"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_odd_even_title_set(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_odd_even(ax, pb["id"])
|
||||||
|
assert "Odd" in ax.get_title() or "Even" in ax.get_title()
|
||||||
|
|
||||||
|
|
||||||
|
# ── _draw_sum_range ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_draw_sum_range_creates_patches(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_sum_range(ax, pb["id"])
|
||||||
|
assert len(ax.patches) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_sum_range_empty_db_no_error(tmp_db):
|
||||||
|
pb = get_game_by_name("Powerball")
|
||||||
|
ax = _ax()
|
||||||
|
_draw_sum_range(ax, pb["id"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_sum_range_title_set(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_sum_range(ax, pb["id"])
|
||||||
|
assert "Sum" in ax.get_title()
|
||||||
|
|
||||||
|
|
||||||
|
# ── _draw_deltas ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_draw_deltas_creates_bars(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_deltas(ax, pb["id"])
|
||||||
|
assert len(ax.patches) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_deltas_empty_db_no_error(tmp_db):
|
||||||
|
pb = get_game_by_name("Powerball")
|
||||||
|
ax = _ax()
|
||||||
|
_draw_deltas(ax, pb["id"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_deltas_title_set(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_deltas(ax, pb["id"])
|
||||||
|
assert "Delta" in ax.get_title()
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_deltas_x_axis_are_positive(pb):
|
||||||
|
ax = _ax()
|
||||||
|
_draw_deltas(ax, pb["id"])
|
||||||
|
# All delta values between consecutive sorted numbers are positive
|
||||||
|
x_vals = [p.get_x() for p in ax.patches]
|
||||||
|
assert all(x >= 0 for x in x_vals)
|
||||||
+158
-6
@@ -1,15 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
ui/analysis.py
|
ui/analysis.py
|
||||||
--------------
|
--------------
|
||||||
Analysis screen — three Matplotlib charts embedded in a ttk.Notebook.
|
Analysis screen — seven Matplotlib charts embedded in a ttk.Notebook.
|
||||||
• Frequency — bar chart of how often each number appears
|
• Frequency — bar chart of how often each number appears
|
||||||
• Heatmap — positional frequency matrix
|
• Heatmap — positional frequency matrix
|
||||||
• Gap — draws since each number last appeared
|
• Gap — draws since each number last appeared
|
||||||
|
• Pairs — top-20 most common number pairs
|
||||||
|
• Odd/Even — distribution of odd vs even count per draw
|
||||||
|
• Sum Range — histogram of draw-sum distribution
|
||||||
|
• Deltas — distribution of gaps between consecutive numbers in a draw
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, filedialog, messagebox
|
from tkinter import ttk, filedialog, messagebox
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import matplotlib
|
import matplotlib
|
||||||
@@ -18,7 +23,10 @@ from matplotlib.figure import Figure
|
|||||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
||||||
|
|
||||||
from db.models import get_all_games, get_game_by_id, get_game_by_name
|
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
|
from core.analyzer import (
|
||||||
|
frequency_analysis, gap_analysis, positional_frequency,
|
||||||
|
pair_analysis, odd_even_ratio, sum_range_analysis, delta_analysis,
|
||||||
|
)
|
||||||
from core.exporter import export_frequency_excel, ensure_exports_dir
|
from core.exporter import export_frequency_excel, ensure_exports_dir
|
||||||
|
|
||||||
_LAST_N_OPTIONS = {
|
_LAST_N_OPTIONS = {
|
||||||
@@ -64,13 +72,17 @@ class AnalysisScreen(ttk.Frame):
|
|||||||
ttk.Button(bar, text="↻ Refresh", command=self.refresh).pack(side="right")
|
ttk.Button(bar, text="↻ Refresh", command=self.refresh).pack(side="right")
|
||||||
ttk.Button(bar, text="Export Frequency", command=self._export_frequency).pack(side="right", padx=(0, 4))
|
ttk.Button(bar, text="Export Frequency", command=self._export_frequency).pack(side="right", padx=(0, 4))
|
||||||
|
|
||||||
# Notebook with three chart tabs
|
# Notebook — all seven chart tabs
|
||||||
nb = ttk.Notebook(self)
|
nb = ttk.Notebook(self)
|
||||||
nb.pack(fill="both", expand=True, padx=6, pady=(0, 6))
|
nb.pack(fill="both", expand=True, padx=6, pady=(0, 6))
|
||||||
|
|
||||||
self._freq_fig, self._freq_canvas = self._make_tab(nb, "Frequency")
|
self._freq_fig, self._freq_canvas = self._make_tab(nb, "Frequency")
|
||||||
self._heat_fig, self._heat_canvas = self._make_tab(nb, "Heatmap")
|
self._heat_fig, self._heat_canvas = self._make_tab(nb, "Heatmap")
|
||||||
self._gap_fig, self._gap_canvas = self._make_tab(nb, "Gap")
|
self._gap_fig, self._gap_canvas = self._make_tab(nb, "Gap")
|
||||||
|
self._pair_fig, self._pair_canvas = self._make_tab(nb, "Pairs")
|
||||||
|
self._oe_fig, self._oe_canvas = self._make_tab(nb, "Odd/Even")
|
||||||
|
self._sum_fig, self._sum_canvas = self._make_tab(nb, "Sum Range")
|
||||||
|
self._delta_fig, self._delta_canvas = self._make_tab(nb, "Deltas")
|
||||||
|
|
||||||
def _make_tab(self, notebook, title):
|
def _make_tab(self, notebook, title):
|
||||||
frame = ttk.Frame(notebook)
|
frame = ttk.Frame(notebook)
|
||||||
@@ -116,6 +128,10 @@ class AnalysisScreen(ttk.Frame):
|
|||||||
self._redraw_frequency()
|
self._redraw_frequency()
|
||||||
self._redraw_heatmap()
|
self._redraw_heatmap()
|
||||||
self._redraw_gap()
|
self._redraw_gap()
|
||||||
|
self._redraw_pairs()
|
||||||
|
self._redraw_odd_even()
|
||||||
|
self._redraw_sum_range()
|
||||||
|
self._redraw_deltas()
|
||||||
|
|
||||||
def _redraw_frequency(self):
|
def _redraw_frequency(self):
|
||||||
self._freq_fig.clear()
|
self._freq_fig.clear()
|
||||||
@@ -144,6 +160,42 @@ class AnalysisScreen(ttk.Frame):
|
|||||||
_draw_gap(ax, self._game_id)
|
_draw_gap(ax, self._game_id)
|
||||||
self._gap_canvas.draw()
|
self._gap_canvas.draw()
|
||||||
|
|
||||||
|
def _redraw_pairs(self):
|
||||||
|
self._pair_fig.clear()
|
||||||
|
ax = self._pair_fig.add_subplot(111)
|
||||||
|
if self._game_id is None:
|
||||||
|
_empty(ax, "Select a game above")
|
||||||
|
else:
|
||||||
|
_draw_pairs(ax, self._game_id)
|
||||||
|
self._pair_canvas.draw()
|
||||||
|
|
||||||
|
def _redraw_odd_even(self):
|
||||||
|
self._oe_fig.clear()
|
||||||
|
ax = self._oe_fig.add_subplot(111)
|
||||||
|
if self._game_id is None:
|
||||||
|
_empty(ax, "Select a game above")
|
||||||
|
else:
|
||||||
|
_draw_odd_even(ax, self._game_id)
|
||||||
|
self._oe_canvas.draw()
|
||||||
|
|
||||||
|
def _redraw_sum_range(self):
|
||||||
|
self._sum_fig.clear()
|
||||||
|
ax = self._sum_fig.add_subplot(111)
|
||||||
|
if self._game_id is None:
|
||||||
|
_empty(ax, "Select a game above")
|
||||||
|
else:
|
||||||
|
_draw_sum_range(ax, self._game_id)
|
||||||
|
self._sum_canvas.draw()
|
||||||
|
|
||||||
|
def _redraw_deltas(self):
|
||||||
|
self._delta_fig.clear()
|
||||||
|
ax = self._delta_fig.add_subplot(111)
|
||||||
|
if self._game_id is None:
|
||||||
|
_empty(ax, "Select a game above")
|
||||||
|
else:
|
||||||
|
_draw_deltas(ax, self._game_id)
|
||||||
|
self._delta_canvas.draw()
|
||||||
|
|
||||||
def _export_frequency(self):
|
def _export_frequency(self):
|
||||||
if self._game_id is None:
|
if self._game_id is None:
|
||||||
messagebox.showinfo("Export", "Select a game first.")
|
messagebox.showinfo("Export", "Select a game first.")
|
||||||
@@ -183,7 +235,6 @@ def _draw_frequency(ax, game_id, last_n=None):
|
|||||||
avg = sum(counts) / len(counts)
|
avg = sum(counts) / len(counts)
|
||||||
max_c = max(counts) or 1
|
max_c = max(counts) or 1
|
||||||
|
|
||||||
# Gradient: cold (blue) → hot (red) based on frequency
|
|
||||||
colors = [
|
colors = [
|
||||||
(0.15 + 0.7 * (c / max_c), 0.25, 1.0 - 0.75 * (c / max_c))
|
(0.15 + 0.7 * (c / max_c), 0.25, 1.0 - 0.75 * (c / max_c))
|
||||||
for c in counts
|
for c in counts
|
||||||
@@ -215,7 +266,6 @@ def _draw_heatmap(ax, game_id):
|
|||||||
main_count = game["main_count"]
|
main_count = game["main_count"]
|
||||||
main_max = game["main_max"]
|
main_max = game["main_max"]
|
||||||
|
|
||||||
# Build rows × cols matrix (positions × numbers)
|
|
||||||
matrix = np.zeros((main_count, main_max))
|
matrix = np.zeros((main_count, main_max))
|
||||||
for pos in range(1, main_count + 1):
|
for pos in range(1, main_count + 1):
|
||||||
for num, cnt in pos_freq.get(pos, {}).items():
|
for num, cnt in pos_freq.get(pos, {}).items():
|
||||||
@@ -248,7 +298,6 @@ def _draw_gap(ax, game_id):
|
|||||||
avg = sum(gap_vals) / len(gap_vals)
|
avg = sum(gap_vals) / len(gap_vals)
|
||||||
max_g = max(gap_vals) or 1
|
max_g = max(gap_vals) or 1
|
||||||
|
|
||||||
# Gradient: low gap = blue (hot), high gap = red (due)
|
|
||||||
colors = [
|
colors = [
|
||||||
(0.8 * (g / max_g), 0.15, 1.0 - 0.8 * (g / max_g))
|
(0.8 * (g / max_g), 0.15, 1.0 - 0.8 * (g / max_g))
|
||||||
for g in gap_vals
|
for g in gap_vals
|
||||||
@@ -264,3 +313,106 @@ def _draw_gap(ax, game_id):
|
|||||||
ax.legend(fontsize=9)
|
ax.legend(fontsize=9)
|
||||||
ax.tick_params(axis="x", labelsize=7)
|
ax.tick_params(axis="x", labelsize=7)
|
||||||
ax.grid(axis="y", alpha=0.35)
|
ax.grid(axis="y", alpha=0.35)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_pairs(ax, game_id, top_n=20):
|
||||||
|
pairs = pair_analysis(game_id, top_n=top_n)
|
||||||
|
if not pairs:
|
||||||
|
_empty(ax)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sort by count descending, take top_n
|
||||||
|
sorted_pairs = sorted(pairs.items(), key=lambda x: x[1], reverse=True)[:top_n]
|
||||||
|
labels = [f"{a}-{b}" for (a, b), _ in sorted_pairs]
|
||||||
|
counts = [c for _, c in sorted_pairs]
|
||||||
|
|
||||||
|
y_pos = range(len(labels))
|
||||||
|
bars = ax.barh(list(y_pos), counts, color="#2471a3", edgecolor="none", height=0.7)
|
||||||
|
ax.set_yticks(list(y_pos))
|
||||||
|
ax.set_yticklabels(labels, fontsize=8)
|
||||||
|
ax.invert_yaxis()
|
||||||
|
|
||||||
|
# Annotate count on each bar
|
||||||
|
for bar, count in zip(bars, counts):
|
||||||
|
ax.text(bar.get_width() + 0.1, bar.get_y() + bar.get_height() / 2,
|
||||||
|
str(count), va="center", fontsize=7, color="#333333")
|
||||||
|
|
||||||
|
ax.set_title(f"Top {len(sorted_pairs)} Most Common Pairs", fontsize=11)
|
||||||
|
ax.set_xlabel("Times appeared together", fontsize=9)
|
||||||
|
ax.grid(axis="x", alpha=0.35)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_odd_even(ax, game_id):
|
||||||
|
data = odd_even_ratio(game_id)
|
||||||
|
if not data:
|
||||||
|
_empty(ax)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Distribution: how often each (odd, even) split occurs
|
||||||
|
split_counts: Counter = Counter()
|
||||||
|
main_count = data[0]["odd"] + data[0]["even"]
|
||||||
|
for row in data:
|
||||||
|
split_counts[(row["odd"], row["even"])] += 1
|
||||||
|
|
||||||
|
# Sort by odd count for a natural x-axis: 0 odd … main_count odd
|
||||||
|
splits = sorted(split_counts.keys(), key=lambda x: x[0])
|
||||||
|
labels = [f"{o}O/{e}E" for o, e in splits]
|
||||||
|
counts = [split_counts[s] for s in splits]
|
||||||
|
pcts = [100 * c / len(data) for c in counts]
|
||||||
|
|
||||||
|
bars = ax.bar(labels, pcts, color="#1e8449", edgecolor="none", width=0.6)
|
||||||
|
for bar, pct in zip(bars, pcts):
|
||||||
|
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5,
|
||||||
|
f"{pct:.1f}%", ha="center", va="bottom", fontsize=8)
|
||||||
|
|
||||||
|
ax.set_title("Odd / Even Split Distribution", fontsize=11)
|
||||||
|
ax.set_xlabel("Odd / Even balance", fontsize=9)
|
||||||
|
ax.set_ylabel("% of draws", fontsize=9)
|
||||||
|
ax.grid(axis="y", alpha=0.35)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_sum_range(ax, game_id):
|
||||||
|
data = sum_range_analysis(game_id)
|
||||||
|
if not data:
|
||||||
|
_empty(ax)
|
||||||
|
return
|
||||||
|
|
||||||
|
sums = [row["sum"] for row in data]
|
||||||
|
avg = sum(sums) / len(sums)
|
||||||
|
|
||||||
|
ax.hist(sums, bins=30, color="#8e44ad", edgecolor="white", linewidth=0.4)
|
||||||
|
ax.axvline(avg, color="#e74c3c", linewidth=1.5, linestyle="--",
|
||||||
|
label=f"Avg {avg:.1f}")
|
||||||
|
|
||||||
|
ax.set_title("Draw Sum Distribution", fontsize=11)
|
||||||
|
ax.set_xlabel("Sum of main numbers", fontsize=9)
|
||||||
|
ax.set_ylabel("Number of draws", fontsize=9)
|
||||||
|
ax.legend(fontsize=9)
|
||||||
|
ax.grid(axis="y", alpha=0.35)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_deltas(ax, game_id):
|
||||||
|
data = delta_analysis(game_id)
|
||||||
|
if not data:
|
||||||
|
_empty(ax)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Flatten all deltas across all draws into one distribution
|
||||||
|
all_deltas: Counter = Counter()
|
||||||
|
for row in data:
|
||||||
|
for d in row["deltas"]:
|
||||||
|
all_deltas[d] += 1
|
||||||
|
|
||||||
|
vals = sorted(all_deltas.keys())
|
||||||
|
counts = [all_deltas[v] for v in vals]
|
||||||
|
avg = sum(v * c for v, c in all_deltas.items()) / sum(all_deltas.values())
|
||||||
|
|
||||||
|
ax.bar(vals, counts, color="#d35400", edgecolor="none", width=0.8)
|
||||||
|
ax.axvline(avg, color="#2c3e50", linewidth=1.5, linestyle="--",
|
||||||
|
label=f"Avg {avg:.1f}")
|
||||||
|
|
||||||
|
ax.set_title("Delta Pattern Distribution", fontsize=11)
|
||||||
|
ax.set_xlabel("Gap between consecutive numbers in a draw", fontsize=9)
|
||||||
|
ax.set_ylabel("Frequency", fontsize=9)
|
||||||
|
ax.legend(fontsize=9)
|
||||||
|
ax.grid(axis="y", alpha=0.35)
|
||||||
|
|||||||
Reference in New Issue
Block a user