{"spec_id":"line-win-probability","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nline-win-probability: Win Probability Chart\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 89/100 | Updated: 2026-06-21\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file from shadowing the plotnine library\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    coord_cartesian,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_hline,\n    geom_line,\n    geom_point,\n    geom_rect,\n    geom_ribbon,\n    geom_segment,\n    geom_text,\n    ggplot,\n    labs,\n    scale_alpha_identity,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\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\"\nANYPLOT_AMBER = \"#DDCC77\"\n\n# Data\nnp.random.seed(42)\n\nn_plays = 130\nplays = np.arange(n_plays)\nwin_prob = np.zeros(n_plays)\nwin_prob[0] = 0.50\n\nscoring_plays = {\n    12: (\"FG Home\", 0.10),\n    28: (\"TD Away\", -0.18),\n    42: (\"TD Home\", 0.22),\n    55: (\"FG Away\", -0.08),\n    68: (\"TD Home\", 0.15),\n    82: (\"TD Away\", -0.20),\n    95: (\"FG Home\", 0.12),\n    110: (\"TD Home\", 0.16),\n    122: (\"FG Away\", -0.05),\n}\n\nevents = {}\nfor i in range(1, n_plays):\n    drift = np.random.normal(0, 0.012)\n    if i in scoring_plays:\n        label, shift = scoring_plays[i]\n        win_prob[i] = win_prob[i - 1] + shift + drift\n        events[i] = label\n    else:\n        win_prob[i] = win_prob[i - 1] + drift\n\nwin_prob = np.clip(win_prob, 0.04, 0.96)\n\nfor i in range(n_plays - 8, n_plays):\n    t = (i - (n_plays - 8)) / 7.0\n    win_prob[i] = win_prob[n_plays - 9] * (1 - t) + 0.78 * t\n\nhome_fill = np.maximum(win_prob, 0.5)\naway_fill = np.minimum(win_prob, 0.5)\n\ndf = pd.DataFrame({\"play\": plays, \"win_prob\": win_prob})\n\ndf_home = pd.DataFrame({\"play\": plays, \"ymin\": 0.5, \"ymax\": home_fill, \"team\": \"Eagles (Home)\"})\ndf_away = pd.DataFrame({\"play\": plays, \"ymin\": away_fill, \"ymax\": 0.5, \"team\": \"Cowboys (Away)\"})\ndf_ribbon = pd.concat([df_home, df_away], ignore_index=True)\n\nevent_df = pd.DataFrame(\n    {\"play\": list(events.keys()), \"win_prob\": [win_prob[p] for p in events.keys()], \"label\": list(events.values())}\n)\n\n# Annotation positioning: alternate offset direction for closely-spaced events\nsorted_plays = sorted(events.keys())\ny_offsets = {}\nfor i, play in enumerate(sorted_plays):\n    prob = win_prob[play]\n    base = 0.12 if prob > 0.5 else -0.12\n    if i > 0:\n        prev_play = sorted_plays[i - 1]\n        if abs(play - prev_play) < 22:\n            prev_y = y_offsets[prev_play]\n            if (prev_y > win_prob[prev_play]) == (base > 0):\n                base = -base\n    y_offsets[play] = float(np.clip(prob + base, 0.07, 0.93))\nevent_df[\"label_y\"] = [y_offsets[p] for p in event_df[\"play\"]]\n\nhighlight_df = pd.DataFrame({\"xmin\": [104], \"xmax\": [116], \"ymin\": [0.50], \"ymax\": [0.96], \"alpha\": [0.06]})\nquarter_df = pd.DataFrame({\"x\": [32, 65, 97], \"ymin\": [0.0] * 3, \"ymax\": [1.0] * 3})\n\n# Plot\nquarter_breaks = [0, 32, 65, 97, 129]\nquarter_labels = [\"Kickoff\", \"Q2\", \"Halftime\", \"Q4\", \"Final\"]\n\ntitle = \"line-win-probability · python · plotnine · anyplot.ai\"\ntitle_fontsize = round(12 * (67 / len(title))) if len(title) > 67 else 12\n\nplot = (\n    ggplot()\n    # Decisive moment highlight zone (golden background)\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", alpha=\"alpha\"),\n        data=highlight_df,\n        fill=ANYPLOT_AMBER,\n        inherit_aes=False,\n    )\n    + scale_alpha_identity()\n    # Team-colored area fills above/below 50%\n    + geom_ribbon(aes(x=\"play\", ymin=\"ymin\", ymax=\"ymax\", fill=\"team\"), data=df_ribbon, alpha=0.35)\n    # 50% reference line\n    + geom_hline(yintercept=0.5, color=INK_MUTED, size=0.6, linetype=\"dashed\")\n    # Quarter boundary markers\n    + geom_segment(\n        aes(x=\"x\", xend=\"x\", y=\"ymin\", yend=\"ymax\"),\n        data=quarter_df,\n        color=INK_MUTED,\n        size=0.4,\n        linetype=\"dotted\",\n        inherit_aes=False,\n    )\n    # Win probability trace\n    + geom_line(aes(x=\"play\", y=\"win_prob\"), data=df, color=INK, size=1.2)\n    # Scoring event markers\n    + geom_point(aes(x=\"play\", y=\"win_prob\"), data=event_df, color=INK, fill=ELEVATED_BG, size=4, stroke=0.8, shape=\"o\")\n    # Event annotation labels (size in mm; 4mm ≈ 11pt — visibly larger than tick labels)\n    + geom_text(aes(x=\"play\", y=\"label_y\", label=\"label\"), data=event_df, size=4, fontweight=\"bold\", color=INK_SOFT)\n    # Scales\n    + scale_fill_manual(values={\"Eagles (Home)\": \"#009E73\", \"Cowboys (Away)\": \"#AE3030\"})\n    + scale_x_continuous(breaks=quarter_breaks, labels=quarter_labels, expand=(0.03, 2))\n    + scale_y_continuous(\n        labels=lambda lst: [f\"{int(v * 100)}%\" for v in lst], limits=(0, 1), breaks=[0, 0.25, 0.5, 0.75, 1.0]\n    )\n    + coord_cartesian(xlim=(-2, 134))\n    + labs(x=\"Game Progression\", y=\"Home Win Probability\", title=title, fill=\"\")\n    # Final score callout box\n    + annotate(\n        \"label\",\n        x=10,\n        y=0.06,\n        label=\"Final: Eagles 24 – Cowboys 17\",\n        size=3.5,\n        fill=ELEVATED_BG,\n        color=INK,\n        fontweight=\"bold\",\n        label_padding=0.5,\n    )\n    # Theme — canvas 8×4.5 in @ 400 dpi → 3200×1800 px\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=title_fontsize, weight=\"bold\", color=INK),\n        axis_title_x=element_text(size=10, color=INK),\n        axis_title_y=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=8, color=INK),\n        legend_position=\"top\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.3),\n        legend_key_size=14,\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.15),\n        axis_line=element_line(color=INK_SOFT, size=0.3),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}