{"spec_id":"line-win-probability","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nline-win-probability: Win Probability Chart\nLibrary: letsplot 4.10.1 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-06-21\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom lets_plot import ggsave\n\n\nLetsPlot.setup_html()\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome tokens\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — first series always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Team fills — semantic: home team (Eagles) → Imprint green, away (Cowboys) → Imprint blue\nHOME_COLOR = IMPRINT_PALETTE[0]  # #009E73 — Eagles/home\nAWAY_COLOR = IMPRINT_PALETTE[2]  # #4467A3 — Cowboys/away\n\n# --- Data: simulated NFL game, Eagles vs Cowboys ---\nnp.random.seed(42)\ntotal_plays = 120\nplay_number = np.arange(total_plays)\n\nwin_prob = np.zeros(total_plays)\nwin_prob[0] = 0.50\n\n# Scoring events: (play_index, probability_shift, label)\nevents = [\n    (8, 0.12, \"Eagles FG 3-0\"),\n    (22, -0.15, \"Cowboys TD 3-7\"),\n    (35, 0.18, \"Eagles TD 10-7\"),\n    (52, 0.08, \"Eagles FG 13-7\"),\n    (68, -0.22, \"Cowboys TD 13-14\"),\n    (78, 0.15, \"Eagles TD 20-14\"),\n    (92, -0.10, \"Cowboys FG 20-17\"),\n    (105, 0.20, \"Eagles TD 27-17\"),\n]\n\nevent_plays = {e[0]: e[1] for e in events}\n\nfor i in range(1, total_plays):\n    drift = 0.002 if i > 90 else 0.0\n    noise = np.random.normal(0, 0.02)\n    shift = event_plays.get(i, 0.0)\n    win_prob[i] = np.clip(win_prob[i - 1] + shift + noise + drift, 0.02, 0.98)\n\n# Converge to Eagles win at the end\nwin_prob[-5:] = np.linspace(win_prob[-6], 0.95, 5)\nwin_prob[-1] = 0.97\n\ndf = pd.DataFrame(\n    {\n        \"play\": play_number,\n        \"win_prob\": win_prob,\n        \"baseline\": 0.5,\n        \"above_50\": np.maximum(win_prob, 0.5),\n        \"below_50\": np.minimum(win_prob, 0.5),\n    }\n)\n\n# Key events — alternating label offsets to avoid overlap\nkey_event_indices = [1, 2, 4, 5, 7]\nnudge_directions = [-0.08, 0.07, -0.08, 0.07, -0.08]\nkey_events = pd.DataFrame(\n    {\n        \"play\": [events[i][0] for i in key_event_indices],\n        \"win_prob\": [win_prob[events[i][0]] for i in key_event_indices],\n        \"label\": [events[i][2] for i in key_event_indices],\n        \"label_y\": [win_prob[events[i][0]] + nudge_directions[j] for j, i in enumerate(key_event_indices)],\n    }\n)\n\n# --- Theme-adaptive chrome ---\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major_y=element_line(color=INK_MUTED, size=0.2),\n    panel_grid_major_x=element_blank(),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_line=element_line(color=INK_SOFT),\n    panel_border=element_blank(),\n    plot_title=element_text(color=INK, size=16),\n    plot_subtitle=element_text(color=INK_SOFT, size=10),\n    plot_margin=[40, 60, 20, 20],\n)\n\n# --- Build plot ---\nplot = (\n    ggplot(df, aes(x=\"play\"))\n    # Area fills — Imprint palette, theme-constant data colors\n    + geom_ribbon(\n        aes(ymin=\"baseline\", ymax=\"above_50\"),\n        fill=HOME_COLOR,\n        alpha=0.28,\n    )\n    + geom_ribbon(\n        aes(ymin=\"below_50\", ymax=\"baseline\"),\n        fill=AWAY_COLOR,\n        alpha=0.28,\n    )\n    # Win probability line — theme-adaptive ink\n    + geom_line(\n        aes(y=\"win_prob\"),\n        color=INK,\n        size=1.0,\n        tooltips=layer_tooltips()\n        .line(\"Play @play\")\n        .format(\"win_prob\", \".0%\")\n        .line(\"Win prob: @win_prob\"),\n    )\n    # 50% reference line\n    + geom_hline(yintercept=0.5, color=INK_SOFT, size=0.7, linetype=\"dashed\")\n    # Quarter dividers\n    + geom_vline(xintercept=30, color=INK_MUTED, size=0.5, linetype=\"dotted\")\n    + geom_vline(xintercept=60, color=INK_MUTED, size=0.5, linetype=\"dotted\")\n    + geom_vline(xintercept=90, color=INK_MUTED, size=0.5, linetype=\"dotted\")\n    # Key event markers\n    + geom_point(\n        data=key_events,\n        mapping=aes(x=\"play\", y=\"win_prob\"),\n        size=3.0,\n        color=INK,\n        fill=PAGE_BG,\n        shape=21,\n        stroke=1.5,\n    )\n    # Key event labels with elevated background\n    + geom_label(\n        data=key_events,\n        mapping=aes(x=\"play\", y=\"label_y\", label=\"label\"),\n        size=4,\n        color=INK,\n        fill=ELEVATED_BG,\n        alpha=0.92,\n        label_padding=0.3,\n        label_r=0.15,\n        label_size=0.3,\n    )\n    # Scales\n    + scale_y_continuous(\n        breaks=[0.0, 0.25, 0.5, 0.75, 1.0], labels=[\"0%\", \"25%\", \"50%\", \"75%\", \"100%\"]\n    )\n    + coord_cartesian(ylim=[0.0, 1.05])\n    + scale_x_continuous(\n        breaks=[0, 30, 60, 90, 120], labels=[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"End\"]\n    )\n    + labs(\n        x=\"Game Progress\",\n        y=\"Eagles Win Probability\",\n        title=\"line-win-probability · python · letsplot · anyplot.ai\",\n        subtitle=\"Eagles 27 – Cowboys 17  ·  Eagles recover from Q3 deficit for convincing finish\",\n    )\n    # Canvas: 800×450 × scale=4 → 3200×1800 px (landscape)\n    + ggsize(800, 450)\n    + theme_minimal()\n    + anyplot_theme\n)\n\n# --- Save PNG + HTML ---\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}