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,101 @@
|
||||
"""Reusable UI components: KPI cards, role chips, insight row, section headers."""
|
||||
|
||||
import streamlit as st
|
||||
from .template import (
|
||||
PITCH_GREEN, GOLD, RED, SKY, VIOLET, WHITE, TEXT_SECONDARY,
|
||||
CARD_BG, BORDER, ROLE_COLORS, ROLE_ICONS,
|
||||
)
|
||||
|
||||
CSS_CLASS = """
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=Inter:wght@400;500;600&display=swap');
|
||||
|
||||
.kpi-card {
|
||||
background: rgba(17, 24, 39, 0.70);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid #1F2937;
|
||||
border-radius: 14px;
|
||||
padding: 18px 20px;
|
||||
text-align: center;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.kpi-card:hover { border-color: #374151; }
|
||||
.kpi-value {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.kpi-label {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 11px;
|
||||
color: #9CA3AF;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.kpi-sub {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.role-chip {
|
||||
display: inline-block;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.section-header {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #E5E7EB;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.insight {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 12px;
|
||||
color: #9CA3AF;
|
||||
font-style: italic;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
def inject_css():
|
||||
st.markdown(CSS_CLASS, unsafe_allow_html=True)
|
||||
|
||||
|
||||
def kpi_card(label: str, value: str, sub: str = "", color: str = WHITE, delta: float | None = None):
|
||||
delta_html = ""
|
||||
if delta is not None:
|
||||
d_color = PITCH_GREEN if delta >= 0 else RED
|
||||
d_sign = "+" if delta > 0 else ""
|
||||
delta_html = f'<span style="color:{d_color};font-size:13px;">{d_sign}{delta:+.1f}</span>'
|
||||
|
||||
return f"""
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" style="color:{color};">{value}{delta_html}</div>
|
||||
<div class="kpi-label">{label}</div>
|
||||
{f'<div class="kpi-sub">{sub}</div>' if sub else ''}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def role_chip(role: str) -> str:
|
||||
color = ROLE_COLORS.get(role, TEXT_SECONDARY)
|
||||
return f'<span class="role-chip" style="background:{color}22;color:{color};border:1px solid {color}44;">{ROLE_ICONS.get(role, "")} {role}</span>'
|
||||
|
||||
|
||||
def section(title: str, caption: str = ""):
|
||||
st.markdown(f'<p class="section-header">{title}</p>', unsafe_allow_html=True)
|
||||
if caption:
|
||||
st.markdown(f'<p class="insight">{caption}</p>', unsafe_allow_html=True)
|
||||
|
||||
|
||||
def insight(text: str):
|
||||
st.markdown(f'<p class="insight">{text}</p>', unsafe_allow_html=True)
|
||||
Reference in New Issue
Block a user