Win Probability Chart — Plotly

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 Plotly

Python source (Plotly)

""" anyplot.ai
line-win-probability: Win Probability Chart
Library: plotly 6.8.0 | Python 3.13.14
Quality: 91/100 | Updated: 2026-06-21
"""

import os

import numpy as np
import plotly.graph_objects as go


# Theme-adaptive chrome (Imprint palette)
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"
GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"
GRID_RULE = "rgba(26,26,23,0.30)" if THEME == "light" else "rgba(240,239,232,0.30)"

# Team colors — semantic exception: football teams carry strong real-world color identity.
# Eagles (PHI) → Imprint brand green; Cowboys (DAL) → Imprint blue.
HOME_COLOR = "#009E73"  # Imprint position 1 — Eagles green
AWAY_COLOR = "#4467A3"  # Imprint position 3 — Cowboys blue
HOME_FILL = "rgba(0,158,115,0.22)"
AWAY_FILL = "rgba(68,103,163,0.22)"

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

play_count = 120
plays = np.arange(play_count)

win_prob = np.zeros(play_count)
win_prob[0] = 0.50

q1_end, q2_end, q3_end = 30, 60, 90

# Key scoring events (play_index, prob_shift, label)
events = [
    (10, 0.15, "Eagles TD\n7-0"),
    (25, -0.10, "Cowboys FG\n7-3"),
    (40, 0.16, "Eagles TD\n14-3"),
    (55, -0.18, "Cowboys TD\n14-10"),
    (68, 0.12, "Eagles FG\n17-10"),
    (80, -0.22, "Cowboys TD\n17-17"),
    (98, 0.20, "Eagles TD\n24-17"),
    (112, 0.10, "Eagles FG\n27-17"),
]

event_plays = {e[0]: e[1] for e in events}

for i in range(1, play_count):
    if i in event_plays:
        drift = event_plays[i]
    else:
        drift = np.random.normal(0, 0.012)
    win_prob[i] = np.clip(win_prob[i - 1] + drift, 0.03, 0.97)

win_prob[-1] = 1.0
win_prob[-2] = 0.96
win_prob[-3] = 0.92

win_pct = win_prob * 100

# Plot
fig = go.Figure()

# Fill above 50% — home team (Eagles)
win_above = np.clip(win_pct, 50, 100)
fig.add_trace(go.Scatter(x=plays, y=win_above, mode="lines", line={"width": 0}, showlegend=False, hoverinfo="skip"))
fig.add_trace(
    go.Scatter(
        x=plays,
        y=np.full(play_count, 50),
        mode="lines",
        line={"width": 0},
        fill="tonexty",
        fillcolor=HOME_FILL,
        showlegend=False,
        hoverinfo="skip",
    )
)

# Fill below 50% — away team (Cowboys)
win_below = np.clip(win_pct, 0, 50)
fig.add_trace(go.Scatter(x=plays, y=win_below, mode="lines", line={"width": 0}, showlegend=False, hoverinfo="skip"))
fig.add_trace(
    go.Scatter(
        x=plays,
        y=np.full(play_count, 50),
        mode="lines",
        line={"width": 0},
        fill="tonexty",
        fillcolor=AWAY_FILL,
        showlegend=False,
        hoverinfo="skip",
    )
)

# Main win probability line
fig.add_trace(
    go.Scatter(
        x=plays,
        y=win_pct,
        mode="lines",
        line={"width": 2.5, "color": INK, "shape": "spline", "smoothing": 0.8},
        name="Win Probability",
        hovertemplate="Play %{x}<br>Win Prob: %{y:.1f}%<extra></extra>",
    )
)

# 50% reference line
fig.add_hline(y=50, line_dash="dash", line_color=INK_SOFT, line_width=1.5, opacity=0.6)

# Quarter dividers and labels
for q_play, q_label in [(q1_end, "Q2"), (q2_end, "Q3"), (q3_end, "Q4")]:
    fig.add_vline(x=q_play, line_dash="dot", line_color=GRID_RULE, line_width=1.5)
    fig.add_annotation(
        x=q_play, y=100, text=f"<b>{q_label}</b>", showarrow=False, font={"size": 10, "color": INK_MUTED}, yshift=14
    )

fig.add_annotation(x=0, y=100, text="<b>Q1</b>", showarrow=False, font={"size": 10, "color": INK_MUTED}, yshift=14)

# Scoring event markers and annotations
for play_idx, _, label in events:
    label_clean = label.replace("\n", "<br>")
    is_home = "Eagles" in label
    marker_color = HOME_COLOR if is_home else AWAY_COLOR
    y_val = win_pct[play_idx]

    fig.add_trace(
        go.Scatter(
            x=[play_idx],
            y=[y_val],
            mode="markers",
            marker={"size": 10, "color": marker_color, "line": {"color": PAGE_BG, "width": 2}},
            showlegend=False,
            hovertemplate=f"{label_clean}<br>Play {play_idx}<br>Win Prob: {y_val:.1f}%<extra></extra>",
        )
    )

    ay_offset = -55 if y_val > 55 else 55
    ax_offset = 0
    if play_idx == 10:
        ax_offset = 60  # steer right to avoid PHI Eagles legend in upper-left
        ay_offset = -60
    elif play_idx == 68:
        ax_offset = 55
        ay_offset = -45
    elif play_idx == 80:
        ax_offset = -55
        ay_offset = 50
    elif play_idx == 98:
        ax_offset = 45
        ay_offset = -55
    elif play_idx == 112:
        ax_offset = 55
        ay_offset = -40

    fig.add_annotation(
        x=play_idx,
        y=y_val,
        text=f"<b>{label_clean}</b>",
        showarrow=True,
        arrowhead=2,
        arrowwidth=1.5,
        arrowcolor=marker_color,
        ax=ax_offset,
        ay=ay_offset,
        font={"size": 10, "color": marker_color},
        bgcolor=ELEVATED_BG,
        bordercolor=marker_color,
        borderwidth=1.5,
        borderpad=5,
    )

# Team legend
fig.add_annotation(
    x=0.01,
    y=0.97,
    xref="paper",
    yref="paper",
    text="<b>▲ PHI Eagles</b>",
    showarrow=False,
    font={"size": 12, "color": HOME_COLOR},
    bgcolor=ELEVATED_BG,
    bordercolor=HOME_COLOR,
    borderwidth=1,
    borderpad=6,
)
fig.add_annotation(
    x=0.01,
    y=0.04,
    xref="paper",
    yref="paper",
    text="<b>▼ DAL Cowboys</b>",
    showarrow=False,
    font={"size": 12, "color": AWAY_COLOR},
    bgcolor=ELEVATED_BG,
    bordercolor=AWAY_COLOR,
    borderwidth=1,
    borderpad=6,
)

# Final score — upper right corner inside the plot
fig.add_annotation(
    x=0.99,
    y=0.97,
    xref="paper",
    yref="paper",
    xanchor="right",
    yanchor="top",
    text="Final Score: Eagles 27 – Cowboys 17",
    showarrow=False,
    font={"size": 12, "color": INK_SOFT},
    bgcolor=ELEVATED_BG,
    borderpad=5,
)

# Layout
fig.update_layout(
    autosize=False,
    title={
        "text": "line-win-probability · python · plotly · anyplot.ai",
        "font": {"size": 16, "color": INK},
        "x": 0.5,
        "xanchor": "center",
        "y": 0.97,
    },
    paper_bgcolor=PAGE_BG,
    plot_bgcolor=PAGE_BG,
    font={"color": INK},
    xaxis={
        "title": {"text": "Play Number", "font": {"size": 12, "color": INK}},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "showline": True,
        "linewidth": 1,
        "linecolor": INK_SOFT,
        "showgrid": False,
        "range": [-2, play_count + 2],
    },
    yaxis={
        "title": {"text": "Win Probability (%)", "font": {"size": 12, "color": INK}},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "tickvals": [0, 25, 50, 75, 100],
        "ticktext": ["0%", "25%", "50%", "75%", "100%"],
        "range": [0, 100],
        "showline": True,
        "linewidth": 1,
        "linecolor": INK_SOFT,
        "showgrid": True,
        "gridwidth": 1,
        "gridcolor": GRID,
    },
    showlegend=False,
    margin={"l": 80, "r": 40, "t": 100, "b": 70},
    hovermode="x unified",
)

fig.update_xaxes(showspikes=True, spikecolor=INK_SOFT, spikethickness=1, spikedash="dot")

# Save — landscape 3200×1800 (width=800, height=450, scale=4)
fig.write_image(f"plot-{THEME}.png", width=800, height=450, scale=4)
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

Part of Win Probability Chart on anyplot.ai.

Other implementations