Win Probability Chart — Matplotlib

A win probability chart shows how each team's likelihood of winning evolves over the course of a game. The line starts near 50% and fluctuates based on scoring events, ultimately reaching 100% or 0% at game end. The area above and below the 50% baseline is filled with team colors to convey momentum at a glance. This visualization is widely used across major sports for post-game analysis and live broadcasting.

Win Probability Chart rendered with Matplotlib

Python source (Matplotlib)

""" anyplot.ai
line-win-probability: Win Probability Chart
Library: matplotlib 3.11.0 | Python 3.13.14
Quality: 88/100 | Updated: 2026-06-21
"""

import os as _os
import sys


# Remove this script's directory from sys.path so this file (matplotlib.py) does
# not shadow the matplotlib package when invoked from inside this directory.
_this_dir = _os.path.dirname(_os.path.abspath(__file__))
sys.path = [p for p in sys.path if _os.path.abspath(p) != _this_dir]
del _this_dir

import os

import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np
from matplotlib.collections import LineCollection


# Theme tokens — Imprint palette, theme-adaptive chrome
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint palette — semantic mapping: Eagles → brand green, Cowboys → blue
EAGLES_COLOR = "#009E73"  # Imprint position 1 — home team
COWBOYS_COLOR = "#4467A3"  # Imprint position 3 — away team

# Data — simulated NFL game: Eagles vs Cowboys
np.random.seed(42)

n_plays = 120
plays = np.arange(n_plays + 1)
win_prob = np.full(n_plays + 1, 0.50)

scoring_events = [
    (8, 0.12, "PHI Field Goal (3-0)"),
    (22, -0.10, "DAL Touchdown (7-3)"),
    (35, 0.15, "PHI Touchdown (10-7)"),
    (48, 0.08, "PHI Field Goal (13-7)"),
    (58, -0.18, "DAL Touchdown (14-13)"),
    (72, 0.14, "PHI Touchdown (20-14)"),
    (85, -0.06, "DAL Field Goal (20-17)"),
    (95, 0.12, "PHI Touchdown (27-17)"),
    (110, -0.05, "DAL Field Goal (27-20)"),
]

prob = 0.50
noise = np.random.normal(0, 0.012, n_plays + 1)
event_indices = {e[0]: (e[1], e[2]) for e in scoring_events}

for i in range(1, n_plays + 1):
    if i in event_indices:
        prob += event_indices[i][0]
    prob += noise[i]
    prob = np.clip(prob, 0.02, 0.98)
    win_prob[i] = prob

# Force convergence to Eagles win
for i in range(105, n_plays + 1):
    t = (i - 105) / (n_plays - 105)
    win_prob[i] = win_prob[105] * (1 - t**2) + 1.0 * t**2

quarter_boundaries = [0, 30, 60, 90, n_plays]
quarter_labels = ["Q1", "Q2", "Q3", "Q4"]

# Plot
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

# Fill above/below 50% — balanced alphas so neither team's fill dominates visually
ax.fill_between(plays, win_prob, 0.5, where=(win_prob >= 0.5), color=EAGLES_COLOR, alpha=0.30, interpolate=True)
ax.fill_between(plays, win_prob, 0.5, where=(win_prob < 0.5), color=COWBOYS_COLOR, alpha=0.30, interpolate=True)

# Win probability line via LineCollection — idiomatic and efficient
points = np.array([plays, win_prob]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
midpoints = (win_prob[:-1] + win_prob[1:]) / 2
seg_colors = [EAGLES_COLOR if m >= 0.5 else COWBOYS_COLOR for m in midpoints]
lc = LineCollection(segments, colors=seg_colors, linewidths=2.5, zorder=3, capstyle="round")
ax.add_collection(lc)

# 50% baseline
ax.axhline(y=0.5, color=INK_MUTED, linewidth=1.2, linestyle="--", alpha=0.6, zorder=2)

# Quarter dividers and labels
for qb in quarter_boundaries[1:-1]:
    ax.axvline(x=qb, color=INK_MUTED, linewidth=0.8, linestyle=":", alpha=0.4)

for i, label in enumerate(quarter_labels):
    mid = (quarter_boundaries[i] + quarter_boundaries[i + 1]) / 2
    ax.text(mid, 0.13, label, ha="center", va="center", fontsize=8, color=INK_MUTED, fontweight="medium")

# Annotate key scoring events
annotation_events = [
    (8, "FG 3-0"),
    (22, "TD 7-3"),
    (35, "TD 10-7"),
    (58, "TD 14-13"),
    (72, "TD 20-14"),
    (95, "TD 27-17"),
]

for play_idx, label in annotation_events:
    wp = win_prob[play_idx]
    offset_y = 0.07 if wp >= 0.5 else -0.07
    txt = ax.annotate(
        label,
        xy=(play_idx, wp),
        xytext=(play_idx, wp + offset_y),
        fontsize=8,
        fontweight="bold",
        ha="center",
        va="center",
        color=INK,
        arrowprops={"arrowstyle": "-", "color": INK_MUTED, "linewidth": 0.8},
        zorder=4,
    )
    txt.set_path_effects([pe.withStroke(linewidth=2.5, foreground=PAGE_BG)])

# Scoring event dots on the curve
for play_idx, _ in annotation_events:
    color = EAGLES_COLOR if win_prob[play_idx] >= 0.5 else COWBOYS_COLOR
    ax.plot(
        play_idx,
        win_prob[play_idx],
        "o",
        color=color,
        markersize=5,
        zorder=5,
        markeredgecolor=PAGE_BG,
        markeredgewidth=0.8,
    )

# Axes
ax.set_xlim(0, n_plays)
ax.set_ylim(0.10, 1.02)
ax.set_yticks([0.25, 0.5, 0.75, 1.0])
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"{x:.0%}"))

# Grid — subtle y-axis only
ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)
ax.set_axisbelow(True)

# Labels and scaled title
title = "Eagles 27 – Cowboys 20 · line-win-probability · python · matplotlib · anyplot.ai"
title_fontsize = max(8, round(12 * 67 / len(title)))
ax.set_xlabel("Play Number", fontsize=10, color=INK)
ax.set_ylabel("Win Probability", fontsize=10, color=INK)
ax.set_title(title, fontsize=title_fontsize, fontweight="medium", color=INK)
ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)

# Spines
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
for s in ("left", "bottom"):
    ax.spines[s].set_color(INK_SOFT)

# Legend
eagles_patch = mpatches.Patch(color=EAGLES_COLOR, alpha=0.5, label="Eagles")
cowboys_patch = mpatches.Patch(color=COWBOYS_COLOR, alpha=0.5, label="Cowboys")
leg = ax.legend(handles=[eagles_patch, cowboys_patch], fontsize=8, loc="upper left", framealpha=0.9, edgecolor=INK_SOFT)
if leg:
    leg.get_frame().set_facecolor(ELEVATED_BG)
    leg.get_frame().set_edgecolor(INK_SOFT)
    plt.setp(leg.get_texts(), color=INK_SOFT)

fig.subplots_adjust(left=0.09, right=0.97, top=0.91, bottom=0.12)

# Save — no bbox_inches="tight" (would trim canvas from 3200×1800)
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

Part of Win Probability Chart on anyplot.ai.

Other implementations