diff --git a/plots/line-win-probability/implementations/matplotlib.py b/plots/line-win-probability/implementations/matplotlib.py new file mode 100644 index 0000000000..d43a1e1648 --- /dev/null +++ b/plots/line-win-probability/implementations/matplotlib.py @@ -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") diff --git a/plots/line-win-probability/metadata/matplotlib.yaml b/plots/line-win-probability/metadata/matplotlib.yaml new file mode 100644 index 0000000000..eb4972d022 --- /dev/null +++ b/plots/line-win-probability/metadata/matplotlib.yaml @@ -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