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 "—"}
BASES
{_bases_html(first_on, second_on, third_on)}
"""
else:
matchup_html = f"""
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"""
"""
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"""
{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}
{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)