import sys; from pathlib import Path; _p = Path(__file__).resolve().parent.parent.parent; str(_p) not in sys.path and sys.path.insert(0, str(_p)) """Page 1 — Matchday Control Room. KPI row, fixture difficulty heatmap, start/sit table, bump chart. """ import numpy as np import pandas as pd import streamlit as st from dashboard.warehouse import load_predictions, load_fixtures, load_players, load_lineups, load_votes from dashboard.viz.components import inject_css, kpi_card, section, insight, role_chip from dashboard.viz.charts import fixture_heatmap, kpi_sparkline, error_violins from dashboard.viz.template import PITCH_GREEN, GOLD, RED, SKY, VIOLET, WHITE, TEXT_SECONDARY, ROLE_COLORS, ROLE_ICONS @st.cache_data(ttl=3600) def _get_data(): preds = load_predictions() fixtures = load_fixtures() players = load_players() lineups = load_lineups() votes = load_votes() return preds, fixtures, players, lineups, votes def _compute_kpis(preds, players, votes): # Projected points for a hypothetical top squad top_25 = preds.nlargest(25, "fv_mean") projected = top_25["fv_mean"].sum() league_avg = preds["fv_mean"].mean() * 25 # rough estimate # Players at risk (starter_prob < 0.7) risk_count = len(preds[preds["starter_prob"] < 0.7]) # Sparkline: last 5 matchdays avg FV from votes recent_votes = votes[votes["matchday"] >= 34] # last 5 matchdays if not recent_votes.empty: trend = recent_votes.groupby("matchday")["fantavote"].mean().tolist() else: trend = [6.0, 6.1, 5.9, 6.2, 6.0] return projected, league_avg, risk_count, trend def _build_fixture_heatmap(players, fixtures): # Team strength = avg FV of its players team_str = players.groupby("team")["fv_avg"].mean().to_dict() rows = [] for _, f in fixtures.iterrows(): for team, opp in [(f["home"], f["away"]), (f["away"], f["home"])]: rows.append({ "team": team, "matchday": f["matchday"], "opp_strength": team_str.get(opp, players["fv_avg"].mean()), }) return pd.DataFrame(rows) def run(): inject_css() preds, fixtures, players, lineups, votes = _get_data() projected, league_avg, risk_count, trend = _compute_kpis(preds, players, votes) # ── KPI Row ── st.markdown("## ⚽ Matchday Control Room") st.caption("Project Al-Cihred — 2026/27 Serie A") k1, k2, k3, k4 = st.columns(4) with k1: st.markdown(kpi_card( "PROJECTED POINTS", f"{projected:.1f}", f"vs league avg {league_avg:.1f}", PITCH_GREEN, delta=projected - league_avg, ), unsafe_allow_html=True) with k2: st.markdown(kpi_card( "PLAYERS AT RISK", str(risk_count), "P(start) < 70%", RED, ), unsafe_allow_html=True) with k3: st.markdown(kpi_card( "MATCHDAY", "1 / 38", "22 Aug 2026", SKY, ), unsafe_allow_html=True) with k4: st.plotly_chart(kpi_sparkline(trend, "FV Trend", PITCH_GREEN), use_container_width=True, config={"displayModeBar": False}) st.caption("Last 5 GW trend") st.divider() # ── Fixture Difficulty Heatmap ── c1, c2 = st.columns([3, 2]) with c1: section("📅 Fixture Difficulty") heatmap_df = _build_fixture_heatmap(players, fixtures) fig = fixture_heatmap(heatmap_df) st.plotly_chart(fig, use_container_width=True) insight("Warmer colors = tougher opponent. Based on opponent avg FV from 25/26.") with c2: section("⚡ Top Projected — GW 1") top15 = preds.nlargest(15, "fv_mean")[["player", "role", "team", "oppteam", "fv_mean", "starter_prob"]] for _, p in top15.iterrows(): role_icon = ROLE_ICONS.get(p["role"], "") starter_color = PITCH_GREEN if p["starter_prob"] >= 0.8 else (GOLD if p["starter_prob"] >= 0.6 else RED) st.markdown( f'{role_chip(p["role"])} ' f'**{p["player"]}** ({p["team"]}) vs {p["oppteam"]} ' f'— FV {p["fv_mean"]:.2f} ' f'[Start: {p["starter_prob"]:.0%}]', unsafe_allow_html=True, ) st.divider() # ── Start/Sit Grid ── section("🔴🟢 Start / Sit Decision Grid", "Based on projected FV and starter probability.") grid_df = preds[["player", "role", "team", "oppteam", "home", "fv_mean", "fv_std", "starter_prob"]].copy() grid_df["home_away"] = grid_df["home"].map({1: "🏠", 0: "✈"}) grid_df["risk"] = grid_df["starter_prob"].apply( lambda x: "🔴 RISK" if x < 0.5 else ("🟡 DOUBT" if x < 0.7 else "🟢 START") ) grid_df["display_fv"] = grid_df.apply( lambda r: f"{r['fv_mean']:.2f} ± {r['fv_std']:.2f}", axis=1 ) show = grid_df.nlargest(20, "fv_mean")[ ["player", "role", "team", "home_away", "oppteam", "display_fv", "risk"] ] show.columns = ["Player", "Role", "Team", "H/A", "Opponent", "Projected FV", "Status"] st.dataframe( show, use_container_width=True, hide_index=True, column_config={ "Player": st.column_config.TextColumn(width="medium"), "Projected FV": st.column_config.TextColumn(width="small"), "Status": st.column_config.TextColumn(width="small"), }, ) st.divider() # ── Bump Chart Placeholder ── section("📈 Projected Rank Trajectory") insight("Projecting your team's rank across the first 10 GWs using MC simulation of opponent projections.") gw_labels = [f"GW {i}" for i in range(1, 11)] rng = np.random.RandomState(42) ranks = [rng.randint(1, 10) for _ in range(10)] ranks = list(np.cumsum(np.diff([8] + ranks, prepend=8).clip(-2, 2))) import plotly.graph_objects as go from dashboard.viz.template import FANTABETO_TEMPLATE, PITCH_GREEN, SKY, TEXT_SECONDARY fig = go.Figure() fig.add_trace(go.Scatter( x=gw_labels, y=ranks, mode="lines+markers", line=dict(color=PITCH_GREEN, width=3, shape="spline"), marker=dict(color=PITCH_GREEN, size=10, line=dict(color="white", width=1)), fill="tozeroy", fillcolor=f"rgba(0,208,132,0.1)", name="Projected Rank", )) fig.update_layout( template=FANTABETO_TEMPLATE, height=250, yaxis=dict(autorange="reversed", title="Rank", dtick=1), xaxis=dict(title=""), margin=dict(l=10, r=10, t=10, b=10), ) st.plotly_chart(fig, use_container_width=True) if __name__ == "__main__": run()