|
| 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