from __future__ import annotations from typing import Any import streamlit.components.v1 as components from visualization.logo_utils import get_team_flag_url, get_team_logo_url def _safe_text(value: Any, fallback: str = "") -> str: if value is None: return fallback text = str(value).strip() if text == "" or text.lower() in {"nan", "none"}: return fallback return text def _safe_score(value: Any) -> str: if value is None: return "" text = str(value).strip() if text == "" or text.lower() in {"nan", "none"}: return "" try: return str(int(float(text))) except Exception: return text def _safe_num(value: Any, digits: int = 1) -> str: if value is None: return "" text = str(value).strip().lower() if text in {"", "nan", "none"}: return "" try: return f"{float(value):.{digits}f}" except Exception: return str(value) def _status_color(status: str) -> tuple[str, str]: s = (status or "").lower() if any(token in s for token in ["live", "top", "bot", "bottom", "mid", "middle"]): return "#22c55e", "●" if any(token in s for token in ["final", "game over", "completed", "ended"]): return "#fbbf24", "●" return "#94a3b8", "●" def _outs_html(outs: int | None) -> str: try: if outs is None: outs_value = 0 else: text = str(outs).strip().lower() if text in {"", "nan", "none"}: outs_value = 0 else: outs_value = int(float(text)) except Exception: outs_value = 0 dots = [] for i in range(3): color = "#f97316" if i < outs_value else "rgba(255,255,255,0.12)" dots.append( f'' ) return "".join(dots) def _bases_html(first_on: bool, second_on: bool, third_on: bool) -> str: def base(on: bool) -> str: color = "#fbbf24" if on else "rgba(255,255,255,0.10)" return f'background:{color};transition:background 0.3s ease;' return f"""
""" def render_game_card(game: dict[str, Any]) -> None: away_team = _safe_text(game.get("away_team"), "Away") home_team = _safe_text(game.get("home_team"), "Home") competition_bucket = _safe_text(game.get("competition_bucket"), "OTHER").upper() # Be robust to missing / incorrect competition_bucket on some score rows: # always try MLB logo lookup first, then fall back to country flag lookup. away_logo_url = get_team_logo_url(away_team) home_logo_url = get_team_logo_url(home_team) away_flag_url = away_logo_url or get_team_flag_url(away_team) home_flag_url = home_logo_url or get_team_flag_url(home_team) away_score = _safe_score(game.get("away_score")) home_score = _safe_score(game.get("home_score")) away_hits = _safe_score(game.get("away_hits")) home_hits = _safe_score(game.get("home_hits")) away_errors = _safe_score(game.get("away_errors")) home_errors = _safe_score(game.get("home_errors")) status = _safe_text(game.get("status"), "Scheduled") start_time_et = _safe_text(game.get("start_time_et")) tv = _safe_text(game.get("tv")) away_record = _safe_text(game.get("away_record")) home_record = _safe_text(game.get("home_record")) batter_name = _safe_text(game.get("batter_name")) pitcher_name = _safe_text(game.get("pitcher_name")) balls = _safe_text(game.get("balls")) strikes = _safe_text(game.get("strikes")) outs = game.get("outs", 0) first_on = bool(game.get("runner_on_1b", False)) second_on = bool(game.get("runner_on_2b", False)) third_on = bool(game.get("runner_on_3b", False)) away_wp = game.get("away_win_prob") home_wp = game.get("home_win_prob") last_play = _safe_text(game.get("last_play")) last_pitch = _safe_text(game.get("last_pitch")) pitch_type = _safe_text(game.get("pitch_type")) pitch_velocity = _safe_num(game.get("pitch_velocity"), 1) pitch_spin_rate = _safe_num(game.get("pitch_spin_rate"), 0) pitch_extension = _safe_num(game.get("pitch_extension"), 1) pitch_label = last_pitch if last_pitch else (pitch_type if pitch_type else "Not available") status_color, status_dot = _status_color(status) has_score_context = any(value not in {"", None} for value in [away_score, home_score]) has_live_context = any( value not in {"", None} for value in [batter_name, pitcher_name, balls, strikes, last_pitch, pitch_velocity] ) status_lower = status.lower() # Some upstream feeds label upcoming games as plain "Live" before real game-state data exists. # If there is no score context and no live game context, treat the card as upcoming. if status_lower == "live" and not has_score_context and not has_live_context: effective_status = "Scheduled" else: effective_status = status display_status = effective_status if start_time_et and ("scheduled" in effective_status.lower() or effective_status.strip() == ""): display_status = start_time_et effective_status_lower = effective_status.lower() is_live_game = any( token in effective_status_lower for token in ["live", "top", "bot", "bottom", "mid", "middle"] ) is_final_game = any( token in effective_status_lower for token in ["final", "game over", "completed", "ended"] ) velo_text = f"{pitch_velocity} mph" if pitch_velocity else "—" spin_text = f"{pitch_spin_rate} rpm" if pitch_spin_rate else "—" ext_text = f"{pitch_extension} ft" if pitch_extension else "—" count_value = f"{balls}-{strikes}" if balls != "" and strikes != "" else "" matchup_html = "" if batter_name or pitcher_name or count_value or is_live_game: matchup_html = f"""
BATTER
{batter_name or "—"}
PITCHER
{pitcher_name or "—"}
COUNT
{count_value or "—"}
OUTS
{_outs_html(outs)}
BASES
{_bases_html(first_on, second_on, third_on)}
""" else: matchup_html = f"""
OUTS
{_outs_html(outs)}
BASES
{_bases_html(first_on, second_on, third_on)}
""" pitch_html = "" if is_live_game or is_final_game or last_pitch or pitch_type or pitch_velocity: pitch_html = f"""
LAST PITCH
{pitch_label}
VELO
{velo_text}
SPIN
{spin_text}
EXT
{ext_text}
""" wp_html = "" if away_wp is not None and home_wp is not None: try: away_pct = max(0, min(100, float(away_wp) * 100)) home_pct = max(0, min(100, float(home_wp) * 100)) wp_html = f"""
{away_team} {away_pct:.0f}% {home_team} {home_pct:.0f}%
""" except Exception: wp_html = "" rhe_html = "" if any([away_score, home_score, away_hits, home_hits, away_errors, home_errors]): rhe_html = f"""
R
H
E
{away_team}
{away_score or "-"}
{away_hits or "-"}
{away_errors or "-"}
{home_team}
{home_score or "-"}
{home_hits or "-"}
{home_errors or "-"}
""" last_play_html = "" if last_play or is_live_game: last_play_html = f"""
LAST PLAY
{last_play or "—"}
""" html = f"""
{status_dot} {display_status}
{"" if away_flag_url else ""} {away_team}
{away_record}
{away_score}
{"" if home_flag_url else ""} {home_team}
{home_record}
{home_score}
{matchup_html} {pitch_html} {wp_html} {rhe_html} {last_play_html}
{tv}
""" components.html(html, height=620 if (last_play_html or rhe_html or pitch_html or matchup_html) else 460, scrolling=False)