Win Probability Chart — Makie.jl

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 Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# line-win-probability: Win Probability Chart
# Library: makie 0.22.10 | Julia 1.11.9
# Quality: 88/100 | Created: 2026-06-21

using CairoMakie
using Colors
using Random

Random.seed!(42)

# Theme tokens (Imprint palette, theme-adaptive chrome)
const THEME       = get(ENV, "ANYPLOT_THEME", "light")
const PAGE_BG     = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
const ELEVATED_BG = THEME == "light" ? colorant"#FFFDF6" : colorant"#242420"
const INK         = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
const INK_SOFT    = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
const INK_MUTED   = THEME == "light" ? colorant"#6B6A63" : colorant"#A8A79F"

# Imprint palette — home uses position 1 (brand green, first-series rule), away uses position 3
const HOME_COLOR = colorant"#009E73"  # Imprint position 1 — Lakeside Wolves (home)
const AWAY_COLOR = colorant"#4467A3"  # Imprint position 3 — Harbor Eagles (away); skip pos 2 (lavender) — blue reads as adversarial contrast to home green

# Data: NBA playoff game, 200 play-by-play win-probability snapshots over 48 minutes
n_points   = 200
game_time  = collect(range(0.0, 48.0, length=n_points))
win_prob   = zeros(n_points)
win_prob[1] = 0.52  # slight home-court advantage at tip-off

for i in 2:n_points
    t = (i - 1) / (n_points - 1)
    # Q1: home builds early lead; Q2: home extends; Q3: Eagles surge; Q4: Wolves hold
    if t < 0.25
        drift = 0.0035
    elseif t < 0.50
        drift = 0.0010
    elseif t < 0.75
        drift = -0.0028
    else
        drift = 0.0018
    end
    win_prob[i] = clamp(win_prob[i-1] + drift + randn() * 0.020, 0.04, 0.96)
end

# Quarter-boundary times (minute 12 = end of Q1, 24 = end of Q2, 36 = end of Q3)
quarter_times  = [12.0, 24.0, 36.0]
quarter_labels = ["Q2", "Q3", "Q4"]

# Key scoring events to annotate
event_times  = [9.0, 26.5, 42.5]
event_descs  = ["Wolves 10–0 run", "Eagles 14–2 surge", "Eagles complete comeback"]
event_idx    = [round(Int, t / 48.0 * (n_points - 1)) + 1 for t in event_times]
event_probs  = win_prob[event_idx]

# Band fill split at the 50% line
home_upper = max.(win_prob, 0.5)  # home region: wp → 0.5 floor when below
away_lower = min.(win_prob, 0.5)  # away region: wp → 0.5 ceiling when above

# Figure
fig = Figure(
    size            = (1600, 900),
    fontsize        = 14,
    backgroundcolor = PAGE_BG,
)

ax = Axis(
    fig[1, 1];
    title             = "line-win-probability · julia · makie · anyplot.ai",
    titlesize         = 20,
    titlecolor        = INK,
    titlefont         = :bold,
    xlabel            = "Game Time (minutes)",
    ylabel            = "Win Probability",
    xlabelsize        = 14,
    ylabelsize        = 14,
    xlabelcolor       = INK,
    ylabelcolor       = INK,
    xticklabelsize    = 12,
    yticklabelsize    = 12,
    xticklabelcolor   = INK_SOFT,
    yticklabelcolor   = INK_SOFT,
    xtickcolor        = INK_SOFT,
    ytickcolor        = INK_SOFT,
    backgroundcolor   = PAGE_BG,
    topspinevisible   = false,
    rightspinevisible = false,
    leftspinecolor    = INK_SOFT,
    bottomspinecolor  = INK_SOFT,
    xgridvisible      = false,
    ygridvisible      = true,
    ygridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.12),
    xticks            = ([0, 12, 24, 36, 48], ["0", "12", "24", "36", "48"]),
    yticks            = ([0.0, 0.25, 0.50, 0.75, 1.0], ["0%", "25%", "50%", "75%", "100%"]),
)

xlims!(ax, 0.0, 48.0)
ylims!(ax, 0.0, 1.0)

# Fill: home region above 50% (brand green)
band!(ax, game_time, fill(0.5, n_points), home_upper;
    color = (HOME_COLOR, 0.22))

# Fill: away region below 50% (blue)
band!(ax, game_time, away_lower, fill(0.5, n_points);
    color = (AWAY_COLOR, 0.22))

# 50% reference line (prominent separator, per spec)
hlines!(ax, [0.5]; color = INK_SOFT, linewidth = 1.8)

# Win probability line
lines!(ax, game_time, win_prob; color = INK, linewidth = 2.0)

# Q1 label at far left
text!(ax, 0.5, 0.975;
    text     = "Q1",
    fontsize = 12,
    color    = INK_MUTED,
    align    = (:left, :top))

# Quarter dividers and labels (Q2, Q3, Q4 at minutes 12, 24, 36)
for (t, lbl) in zip(quarter_times, quarter_labels)
    vlines!(ax, [t]; color = (INK_SOFT, 0.45), linewidth = 1.0, linestyle = :dash)
    text!(ax, t + 0.4, 0.975;
        text     = lbl,
        fontsize = 12,
        color    = INK_MUTED,
        align    = (:left, :top))
end

# Key event markers: dot on the line + annotation
for (t, lbl, p) in zip(event_times, event_descs, event_probs)
    scatter!(ax, [t], [p];
        color       = INK,
        markersize  = 10,
        strokecolor = PAGE_BG,
        strokewidth = 1.5)
    y_pos = p > 0.5 ? p + 0.07 : p - 0.07
    y_pos = clamp(y_pos, 0.06, 0.90)
    va    = p > 0.5 ? :bottom : :top
    text!(ax, t, y_pos;
        text     = lbl,
        fontsize = 12,
        color    = INK_MUTED,
        align    = (:center, va))
end

# Team labels in their respective probability regions
text!(ax, 1.0, 0.88;
    text     = "Lakeside Wolves (home)",
    fontsize = 13,
    color    = HOME_COLOR,
    align    = (:left, :center))

text!(ax, 1.0, 0.12;
    text     = "Harbor Eagles (away)",
    fontsize = 13,
    color    = AWAY_COLOR,
    align    = (:left, :center))

# Final score annotation
text!(ax, 47.5, 0.975;
    text     = "Final: Eagles 104  ·  Wolves 98",
    fontsize = 12,
    color    = INK_MUTED,
    align    = (:right, :top))

# Save
save("plot-$(THEME).png", fig; px_per_unit = 2)

Part of Win Probability Chart on anyplot.ai.

Other implementations