osama-n097/match-performance-api
0
1"""2visualizations/player_dashboard.py — Player Analysis Dashboard3واجهة تحليل اللاعب الكاملة مع filter باسم اللاعب4"""5 6import io7import base648import numpy as np9import pandas as pd10import matplotlib11matplotlib.use("Agg")12import matplotlib.pyplot as plt13import matplotlib.gridspec as gridspec14from matplotlib.patches import FancyBboxPatch15import warnings16 17warnings.filterwarnings("ignore")18 19try:20 from mplsoccer import Pitch, VerticalPitch21 HAS_MPLSOCCER = True22except ImportError:23 HAS_MPLSOCCER = False24 25from config import DATA_DIR, VIZ_COLORS26 27# ── Load Data (once) ──────────────────────────────────────────────────────────28_cache = {}29 30def _load():31 if not _cache:32 _cache["scores"] = pd.read_parquet(DATA_DIR / "model_scores.parquet")33 _cache["computed"] = pd.read_parquet(DATA_DIR / "computed_features.parquet")34 _cache["events"] = pd.read_parquet(DATA_DIR / "events_clean.parquet")35 _cache["matches"] = pd.read_parquet(DATA_DIR / "matches.parquet")36 _cache["vaep"] = pd.read_parquet(DATA_DIR / "player_vaep_ratings.parquet")37 return _cache38 39 40def get_player_list() -> list[str]:41 """قائمة بأسماء كل اللاعبين"""42 data = _load()43 names = data["scores"]["player_name"].dropna().unique().tolist()44 return sorted(names)45 46 47def _fuzzy_match(name_series: pd.Series, query: str) -> pd.Series:48 """Match if ALL tokens in query appear as substrings in the stored name."""49 tokens = query.strip().lower().split()50 mask = pd.Series(False, index=name_series.index)51 for i, val in enumerate(name_series):52 if pd.isna(val):53 continue54 lower = str(val).lower()55 if all(tok in lower for tok in tokens):56 mask.iloc[i] = True57 return mask58 59def get_player_data(player_name: str) -> dict:60 """Fetch all data for a given player (fuzzy name matching)."""61 data = _load()62 scores = data["scores"]63 computed= data["computed"]64 events = data["events"]65 matches = data["matches"]66 vaep = data["vaep"]67 68 # Filter by player (fuzzy token matching)69 match_mask = _fuzzy_match(scores["player_name"], player_name)70 p_scores = scores[match_mask]71 p_feats = computed[computed["player_id"].isin(p_scores["player_id"].unique())]72 p_events = events[events["player_id"].isin(p_scores["player_id"].unique())]73 p_vaep = vaep[vaep["player_id"].isin(p_scores["player_id"].unique())]74 75 if len(p_scores) == 0:76 return None77 78 # Merge مع التواريخ79 p_scores_dated = p_scores.merge(80 matches[["match_id","match_date","home_team","away_team"]],81 on="match_id", how="left"82 ).sort_values("match_date")83 84 return {85 "name": str(p_scores.iloc[0]["player_name"]),86 "player_id": int(p_scores.iloc[0]["player_id"]),87 "position": str(p_scores.iloc[0].get("position_group","Unknown")),88 "cluster": str(p_scores.iloc[0].get("player_cluster","Unknown")),89 "trend": str(p_scores.iloc[0].get("performance_trend","Stable")),90 "scores": p_scores_dated,91 "features": p_feats,92 "events": p_events,93 "vaep": p_vaep,94 "avg_overall": round(float(p_scores["overall_score"].mean()), 2),95 "matches_played": int(p_scores["match_id"].nunique()),96 }97 98 99def get_player_chart_data(player_name: str, match_id: int = None) -> dict:100 """101 جيب البيانات الخام للـ Charts (JSON بدل Base64 images)102 للـ Frontend Rendering مع Animation103 """104 player_data = get_player_data(player_name)105 if not player_data:106 return {"error": f"Player '{player_name}' not found"}107 108 scores = player_data["scores"].sort_values("match_date").reset_index(drop=True)109 dims = ["passing_score", "shooting_score", "positioning_score",110 "pressing_score", "movement_score", "physical_score", "behavioral_score"]111 labels = ["Passing", "Shooting", "Positioning", "Pressing", "Movement", "Physical", "Behavioral"]112 113 # 1. Radar Chart Data114 radar_vals = [float(scores[d].mean()) if d in scores.columns else 0.0 for d in dims]115 radar_data = {116 "labels": labels,117 "values": [round(v, 2) for v in radar_vals]118 }119 120 # 2. Trend Chart Data121 trend_data = [122 {123 "idx": int(i),124 "date": str(row.get("match_date", f"M{i+1}")),125 "overall": float(row.get("overall_score", 0)),126 "rolling_avg": float(scores.iloc[:i+1]["overall_score"].tail(3).mean()) if i >= 0 else 0127 }128 for i, (_, row) in enumerate(scores.iterrows())129 ]130 131 # 3. Score Breakdown Data132 breakdown_data = [133 {"name": label, "value": round(float(scores[dim].mean()) if dim in scores.columns else 0.0, 2)}134 for label, dim in zip(labels, dims)135 ]136 137 # 4. VAEP Data138 vaep_merged = scores.copy()139 for col in ["vaep_rating", "offensive_value", "defensive_value"]:140 if col not in vaep_merged.columns:141 vaep_merged[col] = 0.0142 143 vaep_timeline = [144 {145 "idx": int(i),146 "date": str(row.get("match_date", f"M{i+1}")),147 "vaep": float(row.get("vaep_rating", 0))148 }149 for i, (_, row) in enumerate(vaep_merged.iterrows())150 ]151 152 vaep_totals = {153 "offensive": round(float(vaep_merged["offensive_value"].sum()), 2),154 "defensive": round(float(vaep_merged["defensive_value"].sum()), 2)155 }156 157 vaep_data = {158 "timeline": vaep_timeline,159 "totals": vaep_totals160 }161 162 # 5. Position Comparison Data163 data = _load()164 all_scores = data["scores"]165 pos = player_data["position"]166 pos_avg_scores = all_scores[all_scores["position_group"] == pos]167 168 position_data = [169 {170 "name": label,171 "player": round(float(scores[dim].mean()) if dim in scores.columns else 0.0, 2),172 "position_avg": round(float(pos_avg_scores[dim].mean()) if dim in pos_avg_scores.columns else 0.0, 2)173 }174 for label, dim in zip(labels, dims)175 ]176 177 # 6. Percentile Data178 percentile_data = {179 "in_team": round(float(scores["percentile_in_team"].mean()) if "percentile_in_team" in scores.columns else 50, 2),180 "in_league": round(float(scores["percentile_in_league"].mean()) if "percentile_in_league" in scores.columns else 50, 2),181 "in_position": round(float(scores["percentile_in_position"].mean()) if "percentile_in_position" in scores.columns else 50, 2)182 }183 184 return {185 "player_name": player_data["name"],186 "position": player_data["position"],187 "cluster": player_data["cluster"],188 "trend": player_data["trend"],189 "avg_overall": player_data["avg_overall"],190 "matches_played": player_data["matches_played"],191 "charts": {192 "radar": radar_data,193 "trend": trend_data,194 "breakdown": breakdown_data,195 "vaep": vaep_data,196 "position_comparison": position_data,197 "percentiles": percentile_data198 }199 }200 201 202def _fig_to_base64(fig) -> str:203 buf = io.BytesIO()204 fig.savefig(buf, format="png", dpi=150, bbox_inches="tight",205 facecolor=fig.get_facecolor())206 buf.seek(0)207 encoded = base64.b64encode(buf.read()).decode("utf-8")208 plt.close(fig)209 return f"data:image/png;base64,{encoded}"210 211 212# ── Chart 1: Radar Chart ──────────────────────────────────────────────────────213 214def radar_chart(player_data: dict) -> str:215 scores = player_data["scores"]216 dims = ["passing_score","shooting_score","positioning_score",217 "pressing_score","movement_score","physical_score","behavioral_score"]218 labels = ["Passing","Shooting","Positioning","Pressing","Movement","Physical","Behavioral"]219 vals = [round(float(scores[d].mean()), 2) for d in dims if d in scores.columns]220 vals += vals[:1]221 222 angles = [n / float(len(labels)) * 2 * np.pi for n in range(len(labels))]223 angles+= angles[:1]224 225 fig, ax = plt.subplots(figsize=(7,7), subplot_kw=dict(polar=True))226 fig.patch.set_facecolor("#1a1a2e")227 ax.set_facecolor("#1a1a2e")228 229 ax.plot(angles, vals, "o-", linewidth=2.5, color="#00d4ff")230 ax.fill(angles, vals, alpha=0.3, color="#00d4ff")231 232 ax.set_xticks(angles[:-1])233 ax.set_xticklabels(labels, fontsize=11, color="white")234 ax.set_ylim(0, 10)235 ax.set_yticks([2, 4, 6, 8, 10])236 ax.set_yticklabels(["2","4","6","8","10"], fontsize=8, color="#888888")237 ax.tick_params(colors="white")238 ax.spines["polar"].set_color("#444444")239 ax.grid(color="#333333")240 241 ax.set_title(f"{player_data['name']}\nPerformance Radar",242 fontsize=13, fontweight="bold", color="white", pad=20)243 244 return _fig_to_base64(fig)245 246 247# ── Chart 2: Trend Chart ──────────────────────────────────────────────────────248 249def trend_chart(player_data: dict) -> str:250 scores = player_data["scores"].copy()251 scores = scores.sort_values("match_date").reset_index(drop=True)252 253 fig, ax = plt.subplots(figsize=(14, 5))254 fig.patch.set_facecolor("#1a1a2e")255 ax.set_facecolor("#1a1a2e")256 257 x = range(len(scores))258 ax.plot(x, scores["overall_score"], "o-",259 color="#00d4ff", linewidth=2, markersize=6, label="Match Score", zorder=3)260 261 rolling = scores["overall_score"].rolling(3, min_periods=1).mean()262 ax.plot(x, rolling, "--", color="#ff9f43", linewidth=2.5, label="3-Match Avg")263 264 avg = scores["overall_score"].mean()265 ax.axhline(avg, color="#ff6b6b", linestyle=":", alpha=0.8, label=f"Season Avg ({avg:.2f})")266 267 ax.fill_between(x, scores["overall_score"], avg, alpha=0.1, color="#00d4ff")268 269 ax.set_ylim(0, 10)270 ax.set_xlabel("Match Number", color="white")271 ax.set_ylabel("Overall Score (0-10)", color="white")272 ax.set_title(f"{player_data['name']} — Performance Trend",273 fontsize=13, fontweight="bold", color="white")274 ax.tick_params(colors="white")275 ax.spines["bottom"].set_color("#444444")276 ax.spines["left"].set_color("#444444")277 ax.spines["top"].set_visible(False)278 ax.spines["right"].set_visible(False)279 ax.grid(color="#333333", alpha=0.5)280 ax.legend(facecolor="#2a2a3e", edgecolor="none", labelcolor="white")281 282 return _fig_to_base64(fig)283 284 285# ── Chart 3: Heatmap ──────────────────────────────────────────────────────────286 287def heatmap_chart(player_data: dict, match_id: int = None) -> str:288 events = player_data["events"]289 if match_id is not None and "match_id" in events.columns:290 events = events[events["match_id"] == match_id]291 events = events[events["location_x"].notna()]292 293 fig, ax = plt.subplots(figsize=(12, 8))294 fig.patch.set_facecolor("#22312b")295 296 if HAS_MPLSOCCER:297 pitch = Pitch(pitch_type="statsbomb", pitch_color="#22312b", line_color="white")298 pitch.draw(ax=ax)299 if len(events) > 10:300 pitch.kdeplot(x=events["location_x"], y=events["location_y"],301 ax=ax, cmap="hot", fill=True, alpha=0.7,302 shade_lowest=False, cbar=False)303 else:304 ax.set_facecolor("#22312b")305 ax.scatter(events["location_x"], events["location_y"],306 alpha=0.3, s=5, c="orange")307 ax.set_xlim(0, 120)308 ax.set_ylim(0, 80)309 310 match_suffix = f" (Match {match_id})" if match_id is not None else " (Season)"311 ax.set_title(f"{player_data['name']} — Position Heatmap{match_suffix}",312 fontsize=13, fontweight="bold", color="white", pad=15)313 314 return _fig_to_base64(fig)315 316 317# ── Chart 4: Pass Map ─────────────────────────────────────────────────────────318 319def pass_map_chart(player_data: dict, match_id: int = None) -> str:320 events = player_data["events"]321 passes = events[events["event_type"] == "Pass"].copy()322 323 if match_id:324 passes = passes[passes["match_id"] == match_id]325 else:326 # أحسن ماتش من ناحية عدد التمريرات327 best_match = passes.groupby("match_id").size().idxmax()328 passes = passes[passes["match_id"] == best_match]329 330 complete = passes[passes["pass_outcome"].isna() | (passes["pass_outcome"] == "Complete")]331 incomplete = passes[~passes.index.isin(complete.index)]332 333 fig, ax = plt.subplots(figsize=(12, 8))334 fig.patch.set_facecolor("#22312b")335 336 if HAS_MPLSOCCER:337 pitch = Pitch(pitch_type="statsbomb", pitch_color="#22312b", line_color="white")338 pitch.draw(ax=ax)339 if len(complete):340 pitch.arrows(complete["location_x"], complete["location_y"],341 complete["pass_end_x"], complete["pass_end_y"],342 ax=ax, color="#00ff88", width=1.5, headwidth=5, alpha=0.7)343 if len(incomplete):344 pitch.arrows(incomplete["location_x"], incomplete["location_y"],345 incomplete["pass_end_x"], incomplete["pass_end_y"],346 ax=ax, color="#ff4444", width=1.5, headwidth=5, alpha=0.7)347 else:348 ax.set_facecolor("#22312b")349 if len(complete):350 ax.quiver(complete["location_x"], complete["location_y"],351 complete["pass_end_x"] - complete["location_x"],352 complete["pass_end_y"] - complete["location_y"],353 color="#00ff88", alpha=0.6, scale=1, scale_units="xy", angles="xy")354 if len(incomplete):355 ax.quiver(incomplete["location_x"], incomplete["location_y"],356 incomplete["pass_end_x"] - incomplete["location_x"],357 incomplete["pass_end_y"] - incomplete["location_y"],358 color="#ff4444", alpha=0.6, scale=1, scale_units="xy", angles="xy")359 360 ax.set_title(361 f"{player_data['name']} — Pass Map\n"362 f"Complete: {len(complete)} | Incomplete: {len(incomplete)}",363 fontsize=12, fontweight="bold", color="white", pad=15364 )365 return _fig_to_base64(fig)366 367 368# ── Chart 5: Score Breakdown Bar Chart ────────────────────────────────────────369 370def score_breakdown_chart(player_data: dict) -> str:371 scores = player_data["scores"]372 dims = ["passing_score","shooting_score","positioning_score",373 "pressing_score","movement_score","physical_score","behavioral_score"]374 labels = ["Passing","Shooting","Positioning","Pressing","Movement","Physical","Behavioral"]375 vals = [round(float(scores[d].mean()), 2) for d in dims if d in scores.columns]376 colors = ["#00d4ff","#ff6b6b","#ffd32a","#0be881","#ff9f43","#9b59b6","#2ecc71"]377 378 fig, ax = plt.subplots(figsize=(10, 6))379 fig.patch.set_facecolor("#1a1a2e")380 ax.set_facecolor("#1a1a2e")381 382 bars = ax.barh(labels, vals, color=colors, edgecolor="none", height=0.6)383 ax.set_xlim(0, 10)384 385 for bar, val in zip(bars, vals):386 ax.text(val + 0.1, bar.get_y() + bar.get_height()/2,387 f"{val:.1f}", va="center", fontsize=11, color="white", fontweight="bold")388 389 overall = player_data["avg_overall"]390 ax.axvline(overall, color="white", linestyle="--", alpha=0.5, label=f"Overall: {overall:.2f}")391 392 ax.set_xlabel("Score (0-10)", color="white")393 ax.set_title(f"{player_data['name']} — Dimension Scores",394 fontsize=13, fontweight="bold", color="white")395 ax.tick_params(colors="white")396 ax.spines["bottom"].set_color("#444444")397 ax.spines["left"].set_color("#444444")398 ax.spines["top"].set_visible(False)399 ax.spines["right"].set_visible(False)400 ax.legend(facecolor="#2a2a3e", edgecolor="none", labelcolor="white")401 402 return _fig_to_base64(fig)403 404 405# ── Chart 6: VAEP Over Season ─────────────────────────────────────────────────406 407def vaep_chart(player_data: dict) -> str:408 scores = player_data["scores"].sort_values("match_date")409 vaep = player_data["vaep"]410 411 # Start from scores (it already contains VAEP fields in model_scores),412 # then backfill from player_vaep_ratings when needed.413 merged = scores.copy()414 need_cols = ["vaep_rating", "offensive_value", "defensive_value"]415 missing = [c for c in need_cols if c not in merged.columns]416 417 if missing and {"match_id", "player_id"}.issubset(vaep.columns):418 vaep_subset_cols = ["match_id", "player_id"] + [c for c in need_cols if c in vaep.columns]419 merged = merged.merge(420 vaep[vaep_subset_cols],421 on=["match_id", "player_id"],422 how="left",423 suffixes=("", "_vaep")424 )425 for c in need_cols:426 if c not in merged.columns and f"{c}_vaep" in merged.columns:427 merged[c] = merged[f"{c}_vaep"]428 429 for c in need_cols:430 if c not in merged.columns:431 merged[c] = 0.0432 merged[c] = merged[c].fillna(0)433 434 fig, axes = plt.subplots(1, 2, figsize=(14, 5))435 fig.patch.set_facecolor("#1a1a2e")436 437 # VAEP Timeline438 ax = axes[0]439 ax.set_facecolor("#1a1a2e")440 x = range(len(merged))441 vaep_values = merged["vaep_rating"].fillna(0)442 ax.bar(x, vaep_values,443 color=["#00d4ff" if v >= 0 else "#ff6b6b" for v in vaep_values])444 ax.axhline(0, color="white", linewidth=0.8)445 ax.set_title("VAEP per Match", color="white", fontsize=11)446 ax.tick_params(colors="white")447 ax.set_facecolor("#1a1a2e")448 ax.spines["bottom"].set_color("#444444")449 ax.spines["left"].set_color("#444444")450 ax.spines["top"].set_visible(False)451 ax.spines["right"].set_visible(False)452 453 # Offensive vs Defensive454 ax2 = axes[1]455 ax2.set_facecolor("#1a1a2e")456 total_off = merged["offensive_value"].sum()457 total_def = merged["defensive_value"].sum()458 bars = ax2.bar(["Offensive", "Defensive"], [total_off, total_def],459 color=["#00d4ff","#0be881"], width=0.5)460 ax2.set_title("Total Season VAEP Breakdown", color="white", fontsize=11)461 ax2.tick_params(colors="white")462 ax2.spines["bottom"].set_color("#444444")463 ax2.spines["left"].set_color("#444444")464 ax2.spines["top"].set_visible(False)465 ax2.spines["right"].set_visible(False)466 for bar in bars:467 h = bar.get_height()468 ax2.text(bar.get_x() + bar.get_width()/2, h + 0.3,469 f"{h:.2f}", ha="center", color="white", fontweight="bold")470 471 fig.suptitle(f"{player_data['name']} — VAEP Analysis", color="white",472 fontsize=13, fontweight="bold")473 return _fig_to_base64(fig)474 475 476# ── Chart 7: Comparison with Position Average ────────────────────────────────477 478def position_comparison_chart(player_data: dict) -> str:479 scores = player_data["scores"]480 data = _load()481 all_sc = data["scores"]482 pos = player_data["position"]483 484 pos_avg = all_sc[all_sc["position_group"] == pos]485 dims = ["passing_score","shooting_score","positioning_score",486 "pressing_score","movement_score","physical_score","behavioral_score"]487 labels = ["Passing","Shooting","Positioning","Pressing","Movement","Physical","Behavioral"]488 489 player_vals = [float(scores[d].mean()) for d in dims if d in scores.columns]490 pos_vals = [float(pos_avg[d].mean()) for d in dims if d in pos_avg.columns]491 492 x = np.arange(len(labels))493 width= 0.35494 495 fig, ax = plt.subplots(figsize=(12, 6))496 fig.patch.set_facecolor("#1a1a2e")497 ax.set_facecolor("#1a1a2e")498 499 ax.bar(x - width/2, player_vals, width, label=player_data["name"],500 color="#00d4ff", alpha=0.9, edgecolor="none")501 ax.bar(x + width/2, pos_vals, width, label=f"{pos} Average",502 color="#ff9f43", alpha=0.9, edgecolor="none")503 504 ax.set_xticks(x)505 ax.set_xticklabels(labels, rotation=30, ha="right", color="white")506 ax.set_ylim(0, 10)507 ax.set_ylabel("Score (0-10)", color="white")508 ax.set_title(f"{player_data['name']} vs {pos} Position Average",509 fontsize=13, fontweight="bold", color="white")510 ax.tick_params(colors="white")511 ax.spines["bottom"].set_color("#444444")512 ax.spines["left"].set_color("#444444")513 ax.spines["top"].set_visible(False)514 ax.spines["right"].set_visible(False)515 ax.legend(facecolor="#2a2a3e", edgecolor="none", labelcolor="white")516 517 return _fig_to_base64(fig)518 519 520# ── Chart 8: Shooting Map (Shot Locations + xG) ──────────────────────────────521 522def shooting_map_chart(player_data: dict, match_id: int = None) -> str:523 events = player_data["events"]524 shots = events[events["event_type"] == "Shot"].copy()525 if match_id is not None and "match_id" in shots.columns:526 shots = shots[shots["match_id"] == match_id]527 528 fig, ax = plt.subplots(figsize=(8, 6))529 fig.patch.set_facecolor("#22312b")530 531 if HAS_MPLSOCCER:532 pitch = VerticalPitch(pitch_type="statsbomb", pitch_color="#22312b",533 line_color="white", half=True)534 pitch.draw(ax=ax)535 536 if len(shots):537 goals = shots[shots["shot_outcome"] == "Goal"]538 saves = shots[shots["shot_outcome"] != "Goal"]539 540 if len(saves):541 ax.scatter(saves["location_y"], saves["location_x"],542 c="#ff6b6b", s=saves["shot_xg"].fillna(0.1) * 1000 + 50,543 alpha=0.7, zorder=3, label="No Goal")544 if len(goals):545 ax.scatter(goals["location_y"], goals["location_x"],546 c="#ffd32a", s=goals["shot_xg"].fillna(0.1) * 1000 + 50,547 alpha=1.0, zorder=4, marker="*", label="Goal", edgecolors="white")548 else:549 ax.set_facecolor("#22312b")550 if len(shots):551 goals = shots[shots["shot_outcome"] == "Goal"]552 saves = shots[shots["shot_outcome"] != "Goal"]553 if len(saves):554 ax.scatter(saves["location_x"], saves["location_y"],555 c="#ff6b6b", s=50, alpha=0.7, label="No Goal")556 if len(goals):557 ax.scatter(goals["location_x"], goals["location_y"],558 c="#ffd32a", s=100, marker="*", label="Goal")559 560 match_suffix = f" (Match {match_id})" if match_id is not None else " (Season)"561 ax.set_title(f"{player_data['name']} — Shot Map{match_suffix}\n"562 f"(Size = xG value | Star = Goal)",563 color="white", fontsize=11, fontweight="bold")564 ax.legend(facecolor="#2a2a3e", edgecolor="none", labelcolor="white")565 return _fig_to_base64(fig)566 567 568# ── Chart 8b: Saves Map (Goalkeeper — Shots Faced) ──────────────────────────569 570def saves_map_chart(player_data: dict, match_id: int = None,571 full_events: pd.DataFrame = None) -> str:572 """Show shots a goalkeeper faced: saves made (green) and goals conceded (red)."""573 pid = player_data["player_id"]574 575 if full_events is not None:576 events = full_events577 else:578 events = _load()["events"]579 580 # Filter to match581 if match_id is not None and "match_id" in events.columns:582 events = events[events["match_id"] == match_id]583 584 # Determine the GK's team from their own events585 gk_events = events[events["player_id"] == pid]586 gk_team_id = None587 if len(gk_events) and "team_id" in gk_events.columns:588 teams = gk_events["team_id"].dropna().unique()589 if len(teams):590 gk_team_id = int(teams[0])591 592 shots = events[events["event_type"] == "Shot"].copy()593 if not len(shots):594 fig, ax = plt.subplots(figsize=(8, 6))595 fig.patch.set_facecolor("#22312b")596 ax.set_facecolor("#22312b")597 ax.text(0.5, 0.5, "No shot data for this match",598 ha="center", va="center", color="white", fontsize=12)599 return _fig_to_base64(fig)600 601 saves = shots[(shots["shot_outcome"] == "Saved") & (shots["player_id"] == pid)]602 603 goals_conceded = pd.DataFrame()604 if gk_team_id is not None:605 goals_conceded = shots[606 (shots["shot_outcome"] == "Goal")607 & (shots["team_id"] != gk_team_id)608 & (shots["team_id"].notna())609 ]610 611 other_faced = pd.DataFrame()612 if gk_team_id is not None:613 idx = set(saves.index) | set(goals_conceded.index)614 other_faced = shots[615 (shots["team_id"] != gk_team_id)616 & (shots["team_id"].notna())617 & ~shots.index.isin(idx)618 ]619 620 fig, ax = plt.subplots(figsize=(8, 6))621 fig.patch.set_facecolor("#22312b")622 623 if HAS_MPLSOCCER:624 pitch = VerticalPitch(pitch_type="statsbomb", pitch_color="#22312b",625 line_color="white", half=True)626 pitch.draw(ax=ax)627 628 if len(saves):629 ax.scatter(saves["location_y"], saves["location_x"],630 c="#22c55e", s=saves["shot_xg"].fillna(0.1) * 1000 + 50,631 alpha=0.8, zorder=3, label="Saved", edgecolors="white", linewidth=0.5)632 if len(goals_conceded):633 ax.scatter(goals_conceded["location_y"], goals_conceded["location_x"],634 c="#ef4444", s=goals_conceded["shot_xg"].fillna(0.1) * 1000 + 80,635 alpha=1.0, zorder=4, marker="*", label="Goal Conceded", edgecolors="white")636 if len(other_faced):637 ax.scatter(other_faced["location_y"], other_faced["location_x"],638 c="#94a3b8", s=other_faced["shot_xg"].fillna(0.1) * 500 + 30,639 alpha=0.5, zorder=2, label="Other Shot Faced")640 else:641 ax.set_facecolor("#22312b")642 if len(saves):643 ax.scatter(saves["location_x"], saves["location_y"],644 c="#22c55e", s=80, alpha=0.8, label="Saved")645 if len(goals_conceded):646 ax.scatter(goals_conceded["location_x"], goals_conceded["location_y"],647 c="#ef4444", s=120, marker="*", label="Goal Conceded")648 if len(other_faced):649 ax.scatter(other_faced["location_x"], other_faced["location_y"],650 c="#94a3b8", s=40, alpha=0.5, label="Other Shot Faced")651 652 match_suffix = f" (Match {match_id})" if match_id is not None else " (Season)"653 ax.set_title(f"{player_data['name']} — Saves Map{match_suffix}\n"654 f"(Green = Saved | Red = Goal | Grey = Other)",655 color="white", fontsize=11, fontweight="bold")656 ax.legend(facecolor="#2a2a3e", edgecolor="none", labelcolor="white")657 return _fig_to_base64(fig)658 659 660# ── Chart 8c: Defensive Actions Map (Tackles, Interceptions, Blocks, Clearances, Fouls) ──661 662def defensive_actions_map_chart(player_data: dict, match_id: int = None,663 full_events: pd.DataFrame = None) -> str:664 """Show defensive actions: tackles/duels, interceptions, blocks, clearances, fouls committed."""665 pid = player_data["player_id"]666 667 if full_events is not None:668 events = full_events669 else:670 events = _load()["events"]671 672 if match_id is not None and "match_id" in events.columns:673 events = events[events["match_id"] == match_id]674 675 player_events = events[events["player_id"] == pid]676 677 tackles = player_events[player_events["event_type"] == "Duel"].copy()678 interceptions = player_events[player_events["event_type"] == "Interception"].copy()679 blocks = player_events[player_events["event_type"] == "Block"].copy()680 clearances = player_events[player_events["event_type"] == "Clearance"].copy()681 fouls = player_events[player_events["event_type"] == "Foul Committed"].copy()682 recoveries = player_events[player_events["event_type"] == "Ball Recovery"].copy()683 684 fig, ax = plt.subplots(figsize=(8, 6))685 fig.patch.set_facecolor("#22312b")686 687 if HAS_MPLSOCCER:688 pitch = Pitch(pitch_type="statsbomb", pitch_color="#22312b",689 line_color="white")690 pitch.draw(ax=ax)691 692 if len(tackles):693 pitch.scatter(tackles["location_x"], tackles["location_y"], ax=ax,694 c="#f59e0b", s=60, alpha=0.8, zorder=3,695 label=f"Duels ({len(tackles)})", edgecolors="white", linewidth=0.5)696 if len(interceptions):697 pitch.scatter(interceptions["location_x"], interceptions["location_y"], ax=ax,698 c="#0ea5e9", s=60, alpha=0.8, zorder=3,699 label=f"Interceptions ({len(interceptions)})", edgecolors="white", linewidth=0.5)700 if len(blocks):701 pitch.scatter(blocks["location_x"], blocks["location_y"], ax=ax,702 c="#f97316", s=60, alpha=0.8, zorder=3, marker="s",703 label=f"Blocks ({len(blocks)})", edgecolors="white", linewidth=0.5)704 if len(clearances):705 pitch.scatter(clearances["location_x"], clearances["location_y"], ax=ax,706 c="#14b8a6", s=60, alpha=0.8, zorder=3, marker="^",707 label=f"Clearances ({len(clearances)})", edgecolors="white", linewidth=0.5)708 if len(fouls):709 pitch.scatter(fouls["location_x"], fouls["location_y"], ax=ax,710 c="#ef4444", s=60, alpha=0.7, zorder=3, marker="v",711 label=f"Fouls ({len(fouls)})", edgecolors="white", linewidth=0.5)712 if len(recoveries):713 pitch.scatter(recoveries["location_x"], recoveries["location_y"], ax=ax,714 c="#6366f1", s=40, alpha=0.6, zorder=2, marker="D",715 label=f"Recoveries ({len(recoveries)})", edgecolors="white", linewidth=0.5)716 else:717 ax.set_facecolor("#22312b")718 if len(tackles):719 ax.scatter(tackles["location_x"], tackles["location_y"],720 c="#f59e0b", s=60, alpha=0.8, label=f"Duels ({len(tackles)})")721 if len(interceptions):722 ax.scatter(interceptions["location_x"], interceptions["location_y"],723 c="#0ea5e9", s=60, alpha=0.8, label=f"Interceptions ({len(interceptions)})")724 if len(blocks):725 ax.scatter(blocks["location_x"], blocks["location_y"],726 c="#f97316", s=60, alpha=0.8, marker="s", label=f"Blocks ({len(blocks)})")727 if len(clearances):728 ax.scatter(clearances["location_x"], clearances["location_y"],729 c="#14b8a6", s=60, alpha=0.8, marker="^", label=f"Clearances ({len(clearances)})")730 if len(fouls):731 ax.scatter(fouls["location_x"], fouls["location_y"],732 c="#ef4444", s=60, alpha=0.7, marker="v", label=f"Fouls ({len(fouls)})")733 if len(recoveries):734 ax.scatter(recoveries["location_x"], recoveries["location_y"],735 c="#6366f1", s=40, alpha=0.6, marker="D", label=f"Recoveries ({len(recoveries)})")736 737 match_suffix = f" (Match {match_id})" if match_id is not None else " (Season)"738 ax.set_title(f"{player_data['name']} — Defensive Actions{match_suffix}",739 color="white", fontsize=11, fontweight="bold")740 ax.legend(facecolor="#2a2a3e", edgecolor="none", labelcolor="white",741 fontsize=7, loc="lower left")742 return _fig_to_base64(fig)743 744 745# ── Chart 9: Percentile Profile ───────────────────────────────────────────────746 747def percentile_chart(player_data: dict) -> str:748 scores = player_data["scores"]749 percs = {750 "In Team" : float(scores["percentile_in_team"].mean()) if "percentile_in_team" in scores.columns else 50,751 "In League" : float(scores["percentile_in_league"].mean()) if "percentile_in_league" in scores.columns else 50,752 "In Position": float(scores["percentile_in_position"].mean()) if "percentile_in_position" in scores.columns else 50,753 }754 755 fig, ax = plt.subplots(figsize=(8, 4))756 fig.patch.set_facecolor("#1a1a2e")757 ax.set_facecolor("#1a1a2e")758 759 colors = ["#00d4ff","#ff9f43","#0be881"]760 bars = ax.barh(list(percs.keys()), list(percs.values()),761 color=colors, height=0.5, edgecolor="none")762 ax.set_xlim(0, 100)763 ax.axvline(50, color="#666666", linestyle="--", alpha=0.5)764 765 for bar, val in zip(bars, percs.values()):766 ax.text(val + 1, bar.get_y() + bar.get_height()/2,767 f"{val:.1f}%", va="center", color="white", fontweight="bold")768 769 ax.set_xlabel("Percentile", color="white")770 ax.set_title(f"{player_data['name']} — Percentile Rankings",771 color="white", fontsize=12, fontweight="bold")772 ax.tick_params(colors="white")773 ax.spines["bottom"].set_color("#444444")774 ax.spines["left"].set_color("#444444")775 ax.spines["top"].set_visible(False)776 ax.spines["right"].set_visible(False)777 778 return _fig_to_base64(fig)779 780 781# ── MAIN: Generate All Charts ─────────────────────────────────────────────────782 783def generate_all_charts(player_name: str, match_id: int = None) -> dict:784 """785 توليد كل الـ charts للاعب معين786 بيرجع dict فيه base64 images787 """788 player_data = get_player_data(player_name)789 if not player_data:790 return {"error": f"Player '{player_name}' not found"}791 792 scores = player_data["scores"].sort_values("match_date")793 match_meta = scores[["match_id", "match_date", "home_team", "away_team"]].drop_duplicates(subset=["match_id"]).copy()794 795 available_matches = []796 for _, row in match_meta.iterrows():797 mid = int(row["match_id"])798 match_date = ""799 if "match_date" in row and pd.notna(row["match_date"]):800 match_date = str(row["match_date"])801 home_team = str(row.get("home_team", "")) if pd.notna(row.get("home_team", None)) else ""802 away_team = str(row.get("away_team", "")) if pd.notna(row.get("away_team", None)) else ""803 label = f"{match_date} | {home_team} vs {away_team}" if (home_team or away_team) else f"Match {mid}"804 available_matches.append({805 "match_id": mid,806 "match_date": match_date,807 "home_team": home_team,808 "away_team": away_team,809 "label": label,810 })811 812 available_ids = {m["match_id"] for m in available_matches}813 selected_match_id = int(match_id) if match_id is not None and int(match_id) in available_ids else None814 if selected_match_id is None and len(available_matches) > 0:815 selected_match_id = int(available_matches[-1]["match_id"])816 817 print(f"[CHART] Generating charts for: {player_data['name']} | match_id={selected_match_id}")818 819 return {820 "player_info": {821 "name": player_data["name"],822 "position": player_data["position"],823 "cluster": player_data["cluster"],824 "trend": player_data["trend"],825 "avg_overall": player_data["avg_overall"],826 "matches": player_data["matches_played"],827 },828 "selected_match_id": selected_match_id,829 "available_matches": available_matches,830 "charts": {831 "radar": radar_chart(player_data),832 "trend": trend_chart(player_data),833 "heatmap": heatmap_chart(player_data, selected_match_id),834 "pass_map": pass_map_chart(player_data, selected_match_id),835 "score_breakdown": score_breakdown_chart(player_data),836 "vaep": vaep_chart(player_data),837 "position_comparison":position_comparison_chart(player_data),838 "shooting_map": shooting_map_chart(player_data, selected_match_id),839 "percentiles": percentile_chart(player_data),840 }841 }842 