from __future__ import annotations import base64 from pathlib import Path import numpy as np import pandas as pd from racing_reports.utils import slugify DARK_BG = "#0b1721" CARD_BG = "#111821" TEXT = "#f5f5f5" MUTED = "#9ca3af" GREEN = "#006b3f" LIGHT_GREEN = "#00a86b" AMBER = "#d69a2d" RED = "#d94b4b" BLUE = "#5B8DB8" SHOT_EVENTS = {"Goal", "MissedShots", "SavedShot", "ShotOnPost", "ChanceMissed"} RECOVERY_EVENTS = {"BallRecovery", "Interception", "Tackle", "BlockedPass", "Aerial"} ZONE_RECTS = { "ext izq 3/4": (68, 83, 79, 100), "int izq 3/4": (68, 83, 63, 79), "centro 3/4": (68, 83, 37, 63), "int der 3/4": (68, 83, 21, 37), "ext der 3/4": (68, 83, 0, 21), "ext izq area": (83, 100, 79, 100), "int izq area": (83, 100, 63, 79), "centro area": (83, 100, 37, 63), "int der area": (83, 100, 21, 37), "ext der area": (83, 100, 0, 21), } ZONE_ORDER = list(ZONE_RECTS) def output_match_dir(output_root: Path, league: str, season: str, home: str, away: str, match_id: str) -> Path: return output_root / slugify(league) / str(season) / f"{slugify(home)}_vs_{slugify(away)}_{match_id}" def read_preprocessed(path: Path) -> pd.DataFrame: return pd.read_csv(path, low_memory=False, dtype={"matchId": str, "teamId": str}) def match_events(df: pd.DataFrame, match_id: str) -> pd.DataFrame: out = df[df["matchId"].astype(str) == str(match_id)].copy() if out.empty: raise ValueError(f"No hay eventos para matchId={match_id}") return out def html_page(title: str, subtitle: str, body: str) -> str: return f""" {title}
Racing Reports

{title}

{subtitle}

{body}
""" def add_attack_zone(df: pd.DataFrame) -> pd.DataFrame: out = df.copy() out["attack_zone"] = pd.Series(index=out.index, dtype="object") x = pd.to_numeric(out.get("x"), errors="coerce") y = pd.to_numeric(out.get("y"), errors="coerce") for zone, (x0, x1, y0, y1) in ZONE_RECTS.items(): mask = x.ge(x0) & x.lt(x1) & y.ge(y0) & y.lt(y1) out.loc[mask, "attack_zone"] = zone return out def team_kpis(events: pd.DataFrame, team: str) -> dict[str, float]: t = events[events["TeamName"].astype(str) == team].copy() if t.empty: return {"events": 0, "shots": 0, "xg": 0.0, "pv": 0.0, "goals": 0.0} xg_source = t["xG"] if "xG" in t.columns else pd.Series(0, index=t.index) pv_source = t["pvAdded"] if "pvAdded" in t.columns else pd.Series(0, index=t.index) goal_source = t["goal_int"] if "goal_int" in t.columns else pd.Series(0, index=t.index) return { "events": float(len(t)), "shots": float(t["event_name"].isin(SHOT_EVENTS).sum()) if "event_name" in t.columns else 0.0, "xg": float(pd.to_numeric(xg_source, errors="coerce").fillna(0).sum()), "pv": float(pd.to_numeric(pv_source, errors="coerce").fillna(0).sum()), "goals": float(pd.to_numeric(goal_source, errors="coerce").fillna(0).sum()), } def season_team_events(df: pd.DataFrame, team: str, before_date: str | None = None) -> pd.DataFrame: out = df[df["TeamName"].astype(str) == team].copy() if before_date and "fecha" in out.columns: dates = parse_event_dates(out["fecha"]) out = out[dates < pd.to_datetime(before_date, errors="coerce", utc=True)] return out def parse_event_dates(series: pd.Series) -> pd.Series: cleaned = series.astype(str).str.replace("Z", "", regex=False).str[:10] return pd.to_datetime(cleaned, errors="coerce", utc=True, format="%Y-%m-%d") def save_figure(fig, out_dir: Path, name: str, *, svg: bool = True, dpi: int = 160) -> tuple[Path, Path | None]: """Persiste una figura matplotlib como PNG (y opcionalmente SVG). Devuelve los paths absolutos. NO cierra la figura: el caller decide (algunos flujos también la serializan a base64 después). """ figs_dir = out_dir / "figures" figs_dir.mkdir(parents=True, exist_ok=True) png_path = figs_dir / f"{name}.png" fig.savefig(png_path, dpi=dpi, bbox_inches="tight", facecolor=fig.get_facecolor()) svg_path: Path | None = None if svg: svg_path = figs_dir / f"{name}.svg" fig.savefig(svg_path, format="svg", bbox_inches="tight", facecolor=fig.get_facecolor()) return png_path, svg_path def save_table(df: pd.DataFrame, out_dir: Path, name: str) -> Path: tables_dir = out_dir / "tables" tables_dir.mkdir(parents=True, exist_ok=True) csv_path = tables_dir / f"{name}.csv" df.to_csv(csv_path, index=False) return csv_path def png_to_data_uri(path: Path) -> str: return "data:image/png;base64," + base64.b64encode(path.read_bytes()).decode("ascii")