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
159 changes: 159 additions & 0 deletions plots/line-win-probability/implementations/seaborn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
""" pyplots.ai
line-win-probability: Win Probability Chart
Library: seaborn 0.13.2 | Python 3.14.3
Quality: 91/100 | Created: 2026-03-20
"""

import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns


# Seaborn theme and context for global styling
sns.set_theme(
style="ticks",
rc={
"axes.facecolor": "#F7F9FC",
"figure.facecolor": "#FFFFFF",
"grid.color": "#D0D8E0",
"grid.alpha": 0.3,
"grid.linewidth": 0.6,
"font.family": "sans-serif",
},
)
sns.set_context("talk", font_scale=1.05, rc={"lines.linewidth": 2.8})

# Palette from seaborn
palette = sns.color_palette(["#306998", "#D4583B"])
home_color = palette[0]
away_color = palette[1]

# Data
np.random.seed(42)

plays = np.arange(0, 121)
win_prob = np.full(len(plays), 0.50)

events = {
8: ("FG Home", 0.07),
22: ("TD Away", -0.15),
35: ("TD Home", 0.18),
48: ("INT Home", 0.10),
55: ("FG Away", -0.08),
65: ("TD Home", 0.16),
78: ("TD Away", -0.14),
85: ("FG Home", 0.09),
95: ("TD Away", -0.20),
105: ("TD Home", 0.22),
115: ("FG Home", 0.08),
}

for i in range(1, len(plays)):
noise = np.random.normal(0, 0.015)
if i in events:
shift = events[i][1]
else:
shift = 0
win_prob[i] = np.clip(win_prob[i - 1] + shift + noise, 0.02, 0.98)

win_prob[-1] = 0.95

df = pd.DataFrame({"play": plays, "win_probability": win_prob})

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

sns.lineplot(data=df, x="play", y="win_probability", color=home_color, linewidth=2.8, ax=ax)

ax.fill_between(plays, win_prob, 0.5, where=(win_prob >= 0.5), color=home_color, alpha=0.2, interpolate=True)
ax.fill_between(plays, win_prob, 0.5, where=(win_prob < 0.5), color=away_color, alpha=0.2, interpolate=True)

ax.axhline(y=0.5, color="#888888", linewidth=1.2, linestyle="--", alpha=0.5)

key_events = {
22: ("TD Away\n7-3", away_color),
35: ("TD Home\n10-7", home_color),
65: ("TD Home\n20-14", home_color),
95: ("TD Away\n23-27", away_color),
105: ("TD Home\n30-27", home_color),
}

annotation_offsets = {22: (-8, 0.10), 35: (0, -0.10), 65: (8, 0.10), 95: (-12, 0.10), 105: (0, -0.12)}

for play_num, (label, color) in key_events.items():
y_val = win_prob[play_num]
x_off, y_off = annotation_offsets[play_num]
ax.annotate(
label,
xy=(play_num, y_val),
xytext=(play_num + x_off, y_val + y_off),
fontsize=13,
fontweight="bold",
color=color,
ha="center",
va="center",
arrowprops={"arrowstyle": "->", "color": color, "lw": 1.5, "connectionstyle": "arc3,rad=0.1"},
)

sns.scatterplot(
x=list(key_events),
y=[win_prob[p] for p in key_events],
color=[key_events[p][1] for p in key_events],
s=100,
zorder=5,
edgecolor="white",
linewidth=1.5,
ax=ax,
legend=False,
)

# Quarter markers
for q, label in [(30, "Q1"), (60, "Q2"), (90, "Q3"), (120, "Q4")]:
ax.axvline(x=q, color="#C0C8D0", linewidth=0.8, linestyle=":", alpha=0.6)
ax.text(q - 15, 0.025, label, fontsize=14, color="#8899AA", ha="center", fontweight="medium")

# Style
ax.set_xlabel("Play Number", fontsize=20)
ax.set_ylabel("Home Win Probability", fontsize=20)
ax.set_title("line-win-probability · seaborn · pyplots.ai\n", fontsize=24, fontweight="medium", pad=4)
ax.text(
0.5,
1.02,
"NFL Game — Home vs Away | Lead changes and momentum shifts across 120 plays",
transform=ax.transAxes,
ha="center",
fontsize=14,
color="#667788",
fontstyle="italic",
)
ax.tick_params(axis="both", labelsize=16)

ax.set_ylim(0, 1)
ax.set_xlim(0, 120)
ax.set_yticks([0, 0.25, 0.5, 0.75, 1.0])
ax.set_yticklabels(["0%", "25%", "50%", "75%", "100%"])

sns.despine(ax=ax)
ax.yaxis.grid(True, alpha=0.2, linewidth=0.6)

home_patch = mpatches.Patch(color=home_color, alpha=0.4, label="Home")
away_patch = mpatches.Patch(color=away_color, alpha=0.4, label="Away")
ax.legend(handles=[home_patch, away_patch], fontsize=16, loc="upper left", frameon=False)

ax.text(
118,
0.015,
"Final: Home 30 – Away 27",
fontsize=15,
ha="right",
color="#556677",
fontweight="semibold",
fontstyle="italic",
bbox={"boxstyle": "round,pad=0.3", "facecolor": "#E8EEF4", "edgecolor": "#C0C8D0", "alpha": 0.8},
)

# Save
plt.tight_layout()
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
224 changes: 224 additions & 0 deletions plots/line-win-probability/metadata/seaborn.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
library: seaborn
specification_id: line-win-probability
created: '2026-03-20T12:32:04Z'
updated: '2026-03-20T12:45:51Z'
generated_by: claude-opus-4-5-20251101
workflow_run: 23342814414
issue: 4418
python_version: 3.14.3
library_version: 0.13.2
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-win-probability/seaborn/plot.png
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-win-probability/seaborn/plot_thumb.png
preview_html: null
quality_score: 91
review:
strengths:
- Excellent data storytelling with annotated scoring events showing running score
- Polished design with custom background, cohesive palette, and styled final score
box
- Complete spec compliance — all required features implemented
- Realistic NFL game data with compelling back-and-forth narrative
weaknesses:
- Q4 quarter label is obscured by the Final score box (minor overlap issue)
- Library mastery could be stronger — core visualization relies heavily on matplotlib
rather than seaborn-specific plotting features
image_description: 'The plot displays a win probability line chart for an NFL game
across 120 plays. A dark blue line (#306998) traces the home team''s win probability
from ~50% at kickoff through various scoring events. The area above the 50% dashed
baseline is filled with translucent blue (Home) and below with translucent salmon/red
(#D4583B, Away). Five key events are annotated with curved arrows and bold colored
text showing the scoring play and running score: "TD Away 7-3", "TD Home 10-7",
"TD Home 20-14", "TD Away 23-27", and "TD Home 30-27". Dotted vertical lines mark
Q1, Q2, and Q3 boundaries. The y-axis shows 0%-100% with the 50% reference line.
A styled box at the bottom right displays "Final: Home 30 – Away 27". The title
follows the required format. A legend in the upper left shows Home (blue) and
Away (red) patches. The background is a subtle light blue-gray (#F7F9FC). An italic
subtitle reads "NFL Game — Home vs Away | Lead changes and momentum shifts across
120 plays".'
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 13pt'
- id: VQ-02
name: No Overlap
score: 5
max: 6
passed: true
comment: Q4 quarter label hidden behind Final score box; remaining annotations
well-spaced
- id: VQ-03
name: Element Visibility
score: 6
max: 6
passed: true
comment: Line, fills, and scatter markers all clearly visible with appropriate
sizing
- id: VQ-04
name: Color Accessibility
score: 4
max: 4
passed: true
comment: Blue and salmon provide strong contrast, colorblind-distinguishable
- id: VQ-05
name: Layout & Canvas
score: 4
max: 4
passed: true
comment: Good 16:9 layout, balanced margins, nothing cut off
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: Descriptive labels with percentage units on y-axis
design_excellence:
score: 16
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 6
max: 8
passed: true
comment: Custom background, cohesive palette, italic subtitle, styled score
box — clearly above defaults
- id: DE-02
name: Visual Refinement
score: 5
max: 6
passed: true
comment: Spines removed, subtle grid, custom background, polished quarter
markers
- id: DE-03
name: Data Storytelling
score: 5
max: 6
passed: true
comment: Annotations with running score, fill colors for momentum, final score
box — strong narrative
spec_compliance:
score: 15
max: 15
items:
- id: SC-01
name: Plot Type
score: 5
max: 5
passed: true
comment: Correct line chart showing win probability over game progression
- id: SC-02
name: Required Features
score: 4
max: 4
passed: true
comment: 'All features: 50% reference, fill areas, annotations, final score,
quarter markers'
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: X=Play Number, Y=Win Probability, full range displayed
- id: SC-04
name: Title & Legend
score: 3
max: 3
passed: true
comment: Correct title format and Home/Away legend patches
data_quality:
score: 15
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 6
max: 6
passed: true
comment: Lead changes, momentum swings, multiple event types, both teams scoring
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: NFL game scenario, neutral sports context, plausible data
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: 120 plays realistic for NFL, probability values in valid range
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 functionality
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: Saves as plot.png with dpi=300
library_mastery:
score: 6
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 4
max: 5
passed: true
comment: Good use of seaborn axes-level API, set_theme, set_context, despine
- id: LM-02
name: Distinctive Features
score: 2
max: 5
passed: false
comment: Seaborn features are mainly styling utilities; core viz relies on
matplotlib
verdict: APPROVED
impl_tags:
dependencies: []
techniques:
- annotations
- custom-legend
- manual-ticks
patterns:
- data-generation
- explicit-figure
dataprep:
- cumulative-sum
styling:
- alpha-blending
- grid-styling
- edge-highlighting
Loading