Files
fantabeto/dashboard/viz/components.py
T
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

102 lines
2.7 KiB
Python

"""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)