{"spec_id":"line-win-probability","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nline-win-probability: Win Probability Chart\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 84/100 | Updated: 2026-06-21\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens — Imprint palette\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n# Win prob line: warm near-white in light gives contrast over dark-red Chiefs fill\nLINE_COLOR = \"#FFFDF6\" if THEME == \"light\" else INK\n\n# Semantic team color assignment from Imprint palette\nHOME_COLOR = \"#009E73\"  # CIN Bengals (home) — Imprint position 1, brand green\nAWAY_COLOR = \"#AE3030\"  # KC Chiefs (away) — Imprint matte red, semantic opponent\n\n# Data — 2022 AFC Championship: CIN Bengals vs KC Chiefs (KC wins 23-20 OT)\nnp.random.seed(42)\n\nplays = 140  # regulation (0–120) + OT (120–140)\n\n# Key waypoints: (play_number, bengals_win_probability)\nwaypoints = [\n    (0, 0.50),\n    (18, 0.38),  # KC FG 3-0\n    (33, 0.62),  # CIN TD 7-3\n    (50, 0.41),  # KC TD 13-7\n    (60, 0.45),  # Halftime\n    (72, 0.64),  # CIN TD 14-13\n    (95, 0.26),  # KC TD 20-14\n    (112, 0.72),  # CIN TD 20-20 tie\n    (120, 0.50),  # OT coin flip\n    (140, 0.04),  # KC FG wins 23-20\n]\n\nscoring_events = {\n    18: \"KC FG 3-0\",\n    33: \"CIN TD 7-3\",\n    50: \"KC TD 13-7\",\n    72: \"CIN TD 14-13\",\n    95: \"KC TD 20-14\",\n    112: \"CIN TD 20-20\",\n    140: \"KC FG 23-20\",\n}\n\n# Smooth S-curve interpolation between waypoints\nbengals_wp = np.zeros(plays + 1)\nfor i in range(len(waypoints) - 1):\n    p1, v1 = waypoints[i]\n    p2, v2 = waypoints[i + 1]\n    n_seg = p2 - p1\n    t = np.linspace(0, 1, n_seg, endpoint=False)\n    interp = v1 + (v2 - v1) * (3 * t**2 - 2 * t**3)\n    noise = np.random.normal(0, 0.012, n_seg) * (1 - 0.6 * t)\n    bengals_wp[p1:p2] = np.clip(interp + noise, 0.02, 0.98)\nbengals_wp[plays] = 0.04\n\n# Snap exact values at scoring events\nfor play, _ in scoring_events.items():\n    for p, v in waypoints:\n        if p == play:\n            bengals_wp[play] = v\n\nbengals_wp_pct = [round(float(p) * 100, 1) for p in bengals_wp]\n# Chiefs win probability = complement; fills red from bottom up over green background\nchiefs_wp_pct = [round(100.0 - p, 1) for p in bengals_wp_pct]\n\n# Title length-scaled font size (pygal default 66 for ~67-char baseline)\ntitle = \"Bengals vs Chiefs (20-23 OT) · line-win-probability · python · pygal · anyplot.ai\"\ntitle_font_size = max(44, round(66 * 67 / len(title)))\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    # Series order: Bengals green, Chiefs red, win-prob line (high-contrast neutral), baseline ochre, events cyan\n    colors=(HOME_COLOR, AWAY_COLOR, LINE_COLOR, \"#BD8233\", \"#2ABCCD\", \"#C475FD\"),\n    font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    title_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    title_font_size=title_font_size,\n    label_font_size=46,\n    major_label_font_size=40,\n    value_font_size=36,\n    legend_font_size=44,\n    legend_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    label_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    major_label_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    value_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    opacity=0.88,\n    opacity_hover=0.95,\n    guide_stroke_color=INK_MUTED,\n    guide_stroke_dasharray=\"3,3\",\n    stroke_opacity=1.0,\n    stroke_opacity_hover=1.0,\n    tooltip_font_size=36,\n    tooltip_font_family=\"DejaVu Sans, Helvetica, Arial, sans-serif\",\n    tooltip_border_radius=8,\n)\n\nchart = pygal.Line(\n    width=3200,\n    height=1800,\n    title=title,\n    x_title=\"Game Progression (Play Number)\",\n    y_title=\"Win Probability (%)\",\n    style=custom_style,\n    fill=False,\n    show_dots=False,\n    stroke_style={\"width\": 3},\n    show_y_guides=True,\n    show_x_guides=False,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_box_size=28,\n    value_formatter=lambda x: f\"{x:.0f}%\",\n    range=(0, 100),\n    min_scale=5,\n    max_scale=10,\n    margin_bottom=200,\n    margin_left=80,\n    margin_right=90,\n    margin_top=50,\n    spacing=10,\n    tooltip_border_radius=8,\n    tooltip_fancy_mode=True,\n    x_label_rotation=45,\n    show_minor_x_labels=False,\n)\n\n# Series 1: Bengals home background — fills entire 0-100% in green (#009E73, first Imprint color)\nchart.add(\"CIN Bengals\", [100.0] * (plays + 1), fill=True, show_dots=False, stroke_style={\"width\": 0})\n\n# Series 2: Chiefs away overlay — fills 0-to-chiefs_wp in red, layered over green below\n# Both halves now vary dynamically: more red when Chiefs lead, more green when Bengals lead\nchart.add(\"KC Chiefs\", chiefs_wp_pct, fill=True, show_dots=False, stroke_style={\"width\": 0})\n\n# Series 3: Win probability line — INK (theme-adaptive), contrasts against both fills\n# Hidden from legend (None title); stroke_style width overrides chart default\nchart.add(None, bengals_wp_pct, fill=False, show_dots=False, stroke_style={\"width\": 7})\n\n# Series 4: 50% baseline reference\nchart.add(\"50% Line\", [50.0] * (plays + 1), fill=False, show_dots=False, stroke_style={\"width\": 8, \"dasharray\": \"18,7\"})\n\n# Series 5: Scoring event markers — dots at event plays with tooltip labels\nevent_series = [None] * (plays + 1)\nfor idx, label in scoring_events.items():\n    event_series[idx] = {\"value\": bengals_wp_pct[idx], \"label\": label}\nchart.add(\"Key Events\", event_series, fill=False, show_dots=True, dots_size=20, stroke=False)\n\n# X-axis: period markers combined with scoring event labels for static PNG annotation\n# Drop Q2/Q4 to avoid crowding with nearby scoring events at plays 33 and 95\nlabel_map = {\n    0: \"Kickoff\",\n    18: \"KC FG 3-0\",\n    33: \"CIN TD 7-3\",\n    50: \"KC TD 13-7\",\n    60: \"Halftime\",\n    72: \"CIN TD 14-13\",\n    95: \"KC TD 20-14\",\n    112: \"CIN TD 20-20\",\n    140: \"KC FG 23-20\",\n}\nchart.x_labels = [label_map.get(i, \"\") for i in range(plays + 1)]\nchart.x_labels_major = [label_map[k] for k in sorted(label_map.keys())]\nchart.truncate_label = -1\n\n# Save\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}