Skip to content

Commit 0a2ad40

Browse files
feat(matplotlib): implement scatter-pitch-events (#5095)
## Implementation: `scatter-pitch-events` - matplotlib Implements the **matplotlib** version of `scatter-pitch-events`. **File:** `plots/scatter-pitch-events/implementations/matplotlib.py` **Parent Issue:** #4417 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23342848634)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent ad67208 commit 0a2ad40

2 files changed

Lines changed: 439 additions & 0 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
""" pyplots.ai
2+
scatter-pitch-events: Soccer Pitch Event Map
3+
Library: matplotlib 3.10.8 | Python 3.14.3
4+
Quality: 90/100 | Created: 2026-03-20
5+
"""
6+
7+
import matplotlib.patches as patches
8+
import matplotlib.patheffects as pe
9+
import matplotlib.pyplot as plt
10+
import numpy as np
11+
from matplotlib.colors import to_rgba
12+
13+
14+
# Data
15+
np.random.seed(42)
16+
17+
n_passes = 70
18+
n_shots = 25
19+
n_tackles = 40
20+
n_interceptions = 35
21+
22+
pass_x = np.random.uniform(10, 95, n_passes)
23+
pass_y = np.random.uniform(5, 63, n_passes)
24+
pass_dx = np.random.uniform(-15, 25, n_passes)
25+
pass_dy = np.random.uniform(-15, 15, n_passes)
26+
pass_end_x = np.clip(pass_x + pass_dx, 0, 105)
27+
pass_end_y = np.clip(pass_y + pass_dy, 0, 68)
28+
pass_success = np.random.choice([True, False], n_passes, p=[0.78, 0.22])
29+
30+
shot_x = np.random.uniform(70, 104, n_shots)
31+
shot_y = np.random.uniform(15, 53, n_shots)
32+
shot_dx = np.clip(105 - shot_x, 1, 35) * np.random.uniform(0.5, 1.0, n_shots)
33+
shot_dy = (34 - shot_y) * np.random.uniform(-0.3, 0.3, n_shots)
34+
shot_success = np.random.choice([True, False], n_shots, p=[0.35, 0.65])
35+
36+
tackle_x = np.random.uniform(5, 80, n_tackles)
37+
tackle_y = np.random.uniform(5, 63, n_tackles)
38+
tackle_success = np.random.choice([True, False], n_tackles, p=[0.65, 0.35])
39+
40+
intercept_x = np.random.uniform(15, 85, n_interceptions)
41+
intercept_y = np.random.uniform(5, 63, n_interceptions)
42+
intercept_success = np.random.choice([True, False], n_interceptions, p=[0.80, 0.20])
43+
44+
# Plot
45+
fig, ax = plt.subplots(figsize=(16, 9))
46+
fig.set_facecolor("#1a1a2e")
47+
ax.set_facecolor("#2d6a4f")
48+
49+
# Pitch outline and markings
50+
pitch_color = "#e0e0e0"
51+
lw = 2.0
52+
53+
ax.add_patch(patches.Rectangle((0, 0), 105, 68, linewidth=lw, edgecolor=pitch_color, facecolor="none"))
54+
ax.plot([52.5, 52.5], [0, 68], color=pitch_color, linewidth=lw)
55+
ax.add_patch(patches.Circle((52.5, 34), 9.15, linewidth=lw, edgecolor=pitch_color, facecolor="none"))
56+
ax.plot(52.5, 34, "o", color=pitch_color, markersize=4)
57+
58+
# Left penalty area
59+
ax.add_patch(patches.Rectangle((0, 13.84), 16.5, 40.32, linewidth=lw, edgecolor=pitch_color, facecolor="none"))
60+
ax.add_patch(patches.Rectangle((0, 24.84), 5.5, 18.32, linewidth=lw, edgecolor=pitch_color, facecolor="none"))
61+
ax.plot(11, 34, "o", color=pitch_color, markersize=4)
62+
ax.add_patch(patches.Arc((11, 34), 18.3, 18.3, angle=0, theta1=-53, theta2=53, color=pitch_color, linewidth=lw))
63+
64+
# Right penalty area
65+
ax.add_patch(patches.Rectangle((88.5, 13.84), 16.5, 40.32, linewidth=lw, edgecolor=pitch_color, facecolor="none"))
66+
ax.add_patch(patches.Rectangle((99.5, 24.84), 5.5, 18.32, linewidth=lw, edgecolor=pitch_color, facecolor="none"))
67+
ax.plot(94, 34, "o", color=pitch_color, markersize=4)
68+
ax.add_patch(patches.Arc((94, 34), 18.3, 18.3, angle=0, theta1=127, theta2=233, color=pitch_color, linewidth=lw))
69+
70+
# Corner arcs
71+
for cx, cy in [(0, 0), (0, 68), (105, 0), (105, 68)]:
72+
t1 = 0 if cx == 0 and cy == 0 else (270 if cx == 105 and cy == 0 else (90 if cx == 0 and cy == 68 else 180))
73+
ax.add_patch(patches.Arc((cx, cy), 2, 2, angle=0, theta1=t1, theta2=t1 + 90, color=pitch_color, linewidth=lw))
74+
75+
# Goals
76+
ax.plot([0, 0], [30.34, 37.66], color="#ffffff", linewidth=4, solid_capstyle="round")
77+
ax.plot([105, 105], [30.34, 37.66], color="#ffffff", linewidth=4, solid_capstyle="round")
78+
79+
# Attacking zone highlight (right third) — focal point for tactical storytelling
80+
zone_highlight = patches.FancyBboxPatch(
81+
(70, 5), 33, 58, boxstyle="round,pad=2", facecolor="#ffaa00", alpha=0.06, edgecolor="none", zorder=1
82+
)
83+
ax.add_patch(zone_highlight)
84+
ax.text(
85+
86.5,
86+
66,
87+
"Attacking Third",
88+
fontsize=18,
89+
color="#ffcc44",
90+
alpha=0.7,
91+
ha="center",
92+
va="top",
93+
fontweight="bold",
94+
path_effects=[pe.withStroke(linewidth=2, foreground="#1a1a2e")],
95+
)
96+
97+
# Color palette (colorblind-safe: blue, magenta, gold, orange)
98+
c_pass = "#48bfe3"
99+
c_shot = "#f72585"
100+
c_tackle = "#ffd166"
101+
c_intercept = "#7b2d8e"
102+
103+
# Events - passes (arrows with origin markers)
104+
for i in range(n_passes):
105+
alpha = 0.7 if pass_success[i] else 0.35
106+
ax.annotate(
107+
"",
108+
xy=(pass_end_x[i], pass_end_y[i]),
109+
xytext=(pass_x[i], pass_y[i]),
110+
arrowprops={"arrowstyle": "->", "color": c_pass, "lw": 1.2, "alpha": alpha},
111+
)
112+
ax.plot(
113+
pass_x[i], pass_y[i], "o", color=c_pass, markersize=6, alpha=alpha, markeredgecolor="white", markeredgewidth=0.4
114+
)
115+
116+
# Events - shots (arrows with star markers)
117+
for i in range(n_shots):
118+
alpha = 0.9 if shot_success[i] else 0.3
119+
ax.annotate(
120+
"",
121+
xy=(shot_x[i] + shot_dx[i], shot_y[i] + shot_dy[i]),
122+
xytext=(shot_x[i], shot_y[i]),
123+
arrowprops={"arrowstyle": "-|>", "color": c_shot, "lw": 2.0, "alpha": alpha, "mutation_scale": 15},
124+
)
125+
ax.plot(
126+
shot_x[i],
127+
shot_y[i],
128+
"*",
129+
color=c_shot,
130+
markersize=16,
131+
alpha=alpha,
132+
markeredgecolor="white",
133+
markeredgewidth=0.5,
134+
path_effects=[pe.withStroke(linewidth=1, foreground="#1a1a2e")],
135+
)
136+
137+
# Events - tackles (triangles) — RGBA colors for per-point alpha in single call
138+
tackle_rgba = np.array([to_rgba(c_tackle, a) for a in np.where(tackle_success, 0.8, 0.25)])
139+
ax.scatter(tackle_x, tackle_y, marker="^", s=180, c=tackle_rgba, edgecolors="white", linewidth=0.5, zorder=5)
140+
141+
# Events - interceptions (diamonds)
142+
intercept_rgba = np.array([to_rgba(c_intercept, a) for a in np.where(intercept_success, 0.85, 0.35)])
143+
ax.scatter(intercept_x, intercept_y, marker="D", s=140, c=intercept_rgba, edgecolors="white", linewidth=0.5, zorder=5)
144+
145+
# Style
146+
ax.set_xlim(-3, 108)
147+
ax.set_ylim(-5, 73)
148+
ax.set_aspect("equal")
149+
ax.axis("off")
150+
151+
ax.set_title(
152+
"scatter-pitch-events · matplotlib · pyplots.ai",
153+
fontsize=24,
154+
fontweight="medium",
155+
color="#e0e0e0",
156+
pad=15,
157+
path_effects=[pe.withStroke(linewidth=3, foreground="#1a1a2e")],
158+
)
159+
160+
# Legend
161+
legend_elements = [
162+
plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=c_pass, markersize=10, label="Pass", linestyle="None"),
163+
plt.Line2D([0], [0], marker="*", color="w", markerfacecolor=c_shot, markersize=14, label="Shot", linestyle="None"),
164+
plt.Line2D(
165+
[0], [0], marker="^", color="w", markerfacecolor=c_tackle, markersize=10, label="Tackle", linestyle="None"
166+
),
167+
plt.Line2D(
168+
[0],
169+
[0],
170+
marker="D",
171+
color="w",
172+
markerfacecolor=c_intercept,
173+
markersize=10,
174+
label="Interception",
175+
linestyle="None",
176+
),
177+
plt.Line2D(
178+
[0],
179+
[0],
180+
marker="s",
181+
color="w",
182+
markerfacecolor="#aaaaaa",
183+
markersize=10,
184+
label="Successful (bright)",
185+
linestyle="None",
186+
),
187+
plt.Line2D(
188+
[0],
189+
[0],
190+
marker="s",
191+
color="w",
192+
markerfacecolor="#555555",
193+
markersize=10,
194+
label="Unsuccessful (faded)",
195+
linestyle="None",
196+
),
197+
]
198+
ax.legend(
199+
handles=legend_elements,
200+
loc="lower center",
201+
ncol=6,
202+
fontsize=16,
203+
framealpha=0.7,
204+
facecolor="#1a1a2e",
205+
edgecolor="#444444",
206+
labelcolor="#e0e0e0",
207+
bbox_to_anchor=(0.5, -0.04),
208+
)
209+
210+
plt.tight_layout()
211+
plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor=fig.get_facecolor())

0 commit comments

Comments
 (0)