Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions plots/line-win-probability/implementations/matplotlib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
""" pyplots.ai
line-win-probability: Win Probability Chart
Library: matplotlib 3.10.8 | Python 3.14.3
Quality: 92/100 | Created: 2026-03-20
"""

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


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

# Game plays (0 to ~120 plays)
n_plays = 120
plays = np.arange(n_plays + 1)

# Build win probability with realistic scoring events
win_prob = np.full(n_plays + 1, 0.50)

# Scoring events: (play_number, probability_shift, label)
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)"),
]

# Generate smooth probability curve with scoring jumps
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]
# Mean reversion toward current level
prob = np.clip(prob, 0.02, 0.98)
win_prob[i] = prob

# Force convergence to final outcome: 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 (roughly 30 plays each)
quarter_boundaries = [0, 30, 60, 90, n_plays]
quarter_labels = ["Q1", "Q2", "Q3", "Q4"]

# Colors
eagles_green = "#004C54"
cowboys_navy = "#003594"
baseline_color = "#444444"

# Plot
fig, ax = plt.subplots(figsize=(16, 9))

# Fill above/below 50%
ax.fill_between(plays, win_prob, 0.5, where=(win_prob >= 0.5), color=eagles_green, alpha=0.3, interpolate=True)
ax.fill_between(plays, win_prob, 0.5, where=(win_prob < 0.5), color=cowboys_navy, alpha=0.45, interpolate=True)

# Win probability line - color changes based on which team leads
for i in range(len(plays) - 1):
color = eagles_green if win_prob[i] >= 0.5 else cowboys_navy
ax.plot(plays[i : i + 2], win_prob[i : i + 2], color=color, linewidth=3, zorder=3, solid_capstyle="round")

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

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

# Quarter labels
for i, label in enumerate(quarter_labels):
mid = (quarter_boundaries[i] + quarter_boundaries[i + 1]) / 2
ax.text(mid, 0.03, label, ha="center", va="center", fontsize=16, color="#888888", 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.06 if wp >= 0.5 else -0.06
txt = ax.annotate(
label,
xy=(play_idx, wp),
xytext=(play_idx, wp + offset_y),
fontsize=12,
fontweight="bold",
ha="center",
va="center",
color="#222222",
arrowprops={"arrowstyle": "-", "color": "#999999", "linewidth": 0.8},
zorder=4,
)
txt.set_path_effects([pe.withStroke(linewidth=3, foreground="white")])

# Scatter dots on scoring events for visibility
for play_idx, _ in annotation_events:
ax.plot(
play_idx,
win_prob[play_idx],
"o",
color=eagles_green if win_prob[play_idx] >= 0.5 else cowboys_navy,
markersize=7,
zorder=5,
markeredgecolor="white",
markeredgewidth=1,
)

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

# Subtle y-axis gridlines for easier probability reading
ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color="#888888")
ax.set_axisbelow(True)
ax.set_xlabel("Play Number", fontsize=20)
ax.set_ylabel("Win Probability", fontsize=20)
ax.set_title(
"Eagles 27 – Cowboys 20 · line-win-probability · matplotlib · pyplots.ai", fontsize=24, fontweight="medium"
)
ax.tick_params(axis="both", labelsize=16)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)

# Legend
eagles_patch = mpatches.Patch(color=eagles_green, alpha=0.4, label="Eagles")
cowboys_patch = mpatches.Patch(color=cowboys_navy, alpha=0.5, label="Cowboys")
ax.legend(handles=[eagles_patch, cowboys_patch], fontsize=16, loc="upper left", framealpha=0.8, edgecolor="none")

plt.tight_layout()
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
223 changes: 223 additions & 0 deletions plots/line-win-probability/metadata/matplotlib.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
library: matplotlib
specification_id: line-win-probability
created: '2026-03-20T12:30:49Z'
updated: '2026-03-20T12:44:26Z'
generated_by: claude-opus-4-5-20251101
workflow_run: 23342814407
issue: 4418
python_version: 3.14.3
library_version: 3.10.8
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-win-probability/matplotlib/plot.png
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-win-probability/matplotlib/plot_thumb.png
preview_html: null
quality_score: 92
review:
strengths:
- Excellent spec compliance — all required features (50% baseline, team color fills,
scoring annotations, quarter markers, final score) present and well-implemented
- Strong data storytelling through color fills, annotated scoring events, and natural
probability convergence
- Professional visual polish with path effects, custom team colors, and refined
typography
- Clean, reproducible code with appropriate complexity
weaknesses:
- Segment-by-segment line plotting loop is less efficient than using LineCollection
- Fill area visibility slightly reduced in narrow Cowboys-lead regions
image_description: The plot displays a win probability chart for an NFL game between
the Eagles and Cowboys. The x-axis shows "Play Number" (0–120) and the y-axis
shows "Win Probability" (0%–100%). A dashed horizontal line marks the 50% baseline.
The area above 50% is filled with a muted teal-green (Eagles) and below 50% with
navy blue (Cowboys). The main probability line changes color based on which team
leads. Six key scoring events are annotated with bold labels (FG 3-0, TD 7-3,
TD 10-7, TD 14-13, TD 20-14, TD 27-17) connected by thin lines to small dots on
the curve, each with a white stroke for readability. Quarter boundaries (Q1–Q4)
are shown as dotted vertical lines with labels at the bottom. The title reads
"Eagles 27 – Cowboys 20 · line-win-probability · matplotlib · pyplots.ai". A legend
in the upper left identifies the team color fills. The probability converges sharply
to 100% at game end. Top and right spines are removed; a subtle y-axis grid aids
reading.
criteria_checklist:
visual_quality:
score: 29
max: 30
items:
- id: VQ-01
name: Text Legibility
score: 8
max: 8
passed: true
comment: 'All font sizes explicitly set: title 24pt, labels 20pt, ticks 16pt,
annotations 12pt, quarter labels 16pt'
- id: VQ-02
name: No Overlap
score: 6
max: 6
passed: true
comment: All annotations well-spaced with white path effects ensuring readability
- id: VQ-03
name: Element Visibility
score: 5
max: 6
passed: true
comment: Line and markers clear; fill areas thin in narrow Cowboys-lead regions
- id: VQ-04
name: Color Accessibility
score: 4
max: 4
passed: true
comment: Teal green vs navy blue fully distinguishable for all color vision
types
- id: VQ-05
name: Layout & Canvas
score: 4
max: 4
passed: true
comment: 16:9 figure with tight_layout, plot fills canvas well
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: Descriptive labels with percentage formatting on y-axis
design_excellence:
score: 16
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 6
max: 8
passed: true
comment: Team-specific palette, color-changing line, path effects, approaching
broadcast quality
- id: DE-02
name: Visual Refinement
score: 5
max: 6
passed: true
comment: Spines removed, subtle grid, dotted quarter dividers, white stroke
on annotations
- id: DE-03
name: Data Storytelling
score: 5
max: 6
passed: true
comment: Clear game narrative through fills, annotations, and convergence
spec_compliance:
score: 15
max: 15
items:
- id: SC-01
name: Plot Type
score: 5
max: 5
passed: true
comment: Correct win probability line chart
- id: SC-02
name: Required Features
score: 4
max: 4
passed: true
comment: 'All spec features present: baseline, fills, annotations, score,
quarters'
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: X=play number, Y=win probability, full range shown
- id: SC-04
name: Title & Legend
score: 3
max: 3
passed: true
comment: Title includes score and follows spec-id format; legend correct
data_quality:
score: 15
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 6
max: 6
passed: true
comment: Shows FG/TD events, lead changes, momentum swings, convergence
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: Eagles vs Cowboys NFL game with realistic 27-20 final score
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: Realistic probability shifts and natural convergence
code_quality:
score: 10
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: Clean imports-data-plot-save flow, no functions or classes
- id: CQ-02
name: Reproducibility
score: 2
max: 2
passed: true
comment: np.random.seed(42) set
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
comment: All imports used
- id: CQ-04
name: Code Elegance
score: 2
max: 2
passed: true
comment: Appropriate complexity, no fake UI
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: Saves as plot.png with dpi=300
library_mastery:
score: 7
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 4
max: 5
passed: true
comment: Good ax-based usage; segment loop less idiomatic than LineCollection
- id: LM-02
name: Distinctive Features
score: 3
max: 5
passed: true
comment: fill_between with interpolate, patheffects, FuncFormatter, mpatches
legend
verdict: APPROVED
impl_tags:
dependencies: []
techniques:
- annotations
- custom-legend
- manual-ticks
patterns:
- data-generation
- iteration-over-groups
dataprep:
- cumulative-sum
styling:
- alpha-blending
- grid-styling
- edge-highlighting
Loading