Files
ramseshk 65f5b66b05 Dashboard: Stadium Night design system, 5-page Streamlit app, warehouse exporter
- src/export/warehouse.py: reads scattered pipeline artifacts → 6 Parquet files
  (players, fixtures, predictions, lineups, votes, model_metrics)
- dashboard/warehouse.py: read-only cached Parquet loader
- .streamlit/config.toml: dark theme base, server config
- dashboard/viz/template.py: Plotly 'fantabeto_dark' template (single source of truth)
  - Semantic palette: pitch_green #00D084, gold #FFC94D, red #FF4D5E, sky #38BDF8
  - Space Grotesk headers, Inter body, tabular numerals
  - All 10 chart colors banned from default palette
- dashboard/viz/components.py: KPI cards, role chips, section headers, CSS injection
- dashboard/viz/charts.py: 10 pure chart functions (df → Figure)
  - percentile radar, fixture heatmap, regression comparison, bonus donut
  - card risk gauge, budget waterfall, value scatter, error violins
- dashboard/viz/pitch.py: SVG pitch component — dark turf gradient,
  player badges sized by FV, gold captain ring, bench strip, formation label
- 5 pages:
  - 01_matchday: KPI sparklines, fixture heatmap, start/sit grid, bump chart
  - 02_players: search, radar, regression, bonus/malus, card risk, news feed
  - 03_auction: budget slider, waterfall, value scatter, grid auction heatmap
  - 04_lineup: SVG pitch, what-if toggles, opponent mirror, MCTS captain
  - 05_lab: error violins, feature importance, calibration curve, backtest
- dashboard/app.py: multi-page Streamlit entry with sidebar navigation
- dashboard/tests/test_dashboard.py: 18 unit tests (warehouse, template, charts, pitch)
- DASHBOARD.md: full architecture docs, design system reference
- 49 total tests passing (31 existing + 18 dashboard)
2026-08-11 14:53:51 +08:00

148 lines
4.9 KiB
Python

"""Tests for dashboard viz components and warehouse layer."""
import numpy as np
import pandas as pd
import pytest
from dashboard.warehouse import load_players, load_fixtures, load_predictions, load_lineups, load_votes
from dashboard.viz.template import register_template, FANTABETO_TEMPLATE, PITCH_GREEN, GOLD, RED, SKY
from dashboard.viz.components import kpi_card, role_chip
from dashboard.viz.charts import (
fixture_heatmap, percentile_radar, regression_chart,
bonus_donut, card_gauge, budget_waterfall, value_scatter, error_violins,
)
class TestWarehouse:
def test_load_players(self):
df = load_players()
assert len(df) > 0
assert "player" in df.columns
assert "role" in df.columns
assert "fv_avg" in df.columns
def test_load_fixtures(self):
df = load_fixtures()
assert len(df) > 0
assert "home" in df.columns
assert "away" in df.columns
assert "matchday" in df.columns
def test_load_predictions(self):
df = load_predictions()
assert len(df) > 0
assert "fv_mean" in df.columns
assert "starter_prob" in df.columns
def test_load_lineups(self):
df = load_lineups()
assert len(df) > 0
assert "player" in df.columns
assert "starter_pct" in df.columns
def test_load_votes(self):
df = load_votes()
assert len(df) > 10000
assert "fantavote" in df.columns
assert "matchday" in df.columns
class TestTemplate:
def test_registration(self):
register_template()
import plotly.io as pio
assert FANTABETO_TEMPLATE in pio.templates
def test_colors(self):
assert PITCH_GREEN == "#00D084"
assert GOLD == "#FFC94D"
assert RED == "#FF4D5E"
assert SKY == "#38BDF8"
class TestComponents:
def test_kpi_card(self):
html = kpi_card("TEST", "42.0", "subtitle", SKY, delta=2.5)
assert "TEST" in html
assert "42.0" in html
assert "subtitle" in html
def test_role_chip(self):
for role in ["P", "D", "C", "A"]:
html = role_chip(role)
assert role in html
class TestCharts:
def test_fixture_heatmap(self):
df = pd.DataFrame([
{"team": "Inter", "matchday": 1, "opp_strength": 7.5},
{"team": "Inter", "matchday": 2, "opp_strength": 6.0},
{"team": "Milan", "matchday": 1, "opp_strength": 6.5},
])
fig = fixture_heatmap(df)
assert fig is not None
assert len(fig.data) > 0
def test_percentile_radar(self):
row = pd.Series({
"player": "Test", "goals_p90": 0.5, "assists_p90": 0.2,
"xg_p90": 0.4, "progressive_passes_p90": 2.0,
"tackles_p90": 1.5, "interceptions_p90": 1.0,
"shots_on_target_pct": 0.4,
})
metrics = ["goals_p90", "assists_p90", "xg_p90", "progressive_passes_p90",
"tackles_p90", "interceptions_p90", "shots_on_target_pct"]
labels = ["G", "A", "xG", "PP", "TK", "Int", "SoT"]
fig = percentile_radar(row, metrics, labels)
assert fig is not None
def test_regression_chart(self):
df = pd.DataFrame([{"goals_season": 10, "goals_p90": 0.3, "games_season": 30}])
fig = regression_chart(df, "Test", xg_col="goals_p90", goal_col="goals_season")
assert fig is not None
def test_bonus_donut(self):
fig = bonus_donut(5, 3, 1.5)
assert fig is not None
def test_card_gauge(self):
fig = card_gauge(0.15, 0.01)
assert fig is not None
def test_budget_waterfall(self):
fig = budget_waterfall({"P": 30, "D": 170, "C": 160, "A": 140})
assert fig is not None
def test_value_scatter(self):
df = pd.DataFrame({
"player": ["A", "B", "C"], "role": ["P", "D", "C"],
"fv_avg": [5.5, 6.8, 7.2], "qi": [10, 25, 30],
"games_season": [35, 30, 28],
})
fig = value_scatter(df)
assert fig is not None
def test_error_violins(self):
df = pd.DataFrame({
"role": ["P"] * 10 + ["D"] * 20 + ["C"] * 30 + ["A"] * 15,
"error": np.random.randn(75) * 0.5,
})
fig = error_violins(df)
assert fig is not None
class TestPitch:
def test_render_pitch(self):
from dashboard.viz.pitch import render_pitch
lineup = [
{"name": "GK Test", "role": "P", "fv": 6.0, "starter_pct": 95},
*[{"name": f"DEF {i}", "role": "D", "fv": 6.5, "starter_pct": 90} for i in range(4)],
*[{"name": f"MID {i}", "role": "C", "fv": 7.0, "starter_pct": 85} for i in range(4)],
*[{"name": f"FWD {i}", "role": "A", "fv": 8.0, "starter_pct": 90} for i in range(2)],
]
svg = render_pitch(lineup, formation="4-4-2", captain="FWD 0")
assert "<svg" in svg
assert "FWD 0" in svg
assert "GK Test" in svg