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)
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user