fig, axes = plt.subplots(2, 2, figsize=(6.5, 4.75))
for ax in axes.flat:
blank(ax)
def ppy_of(ax, span):
fig.canvas.draw_idle()
return ax.get_window_extent().width / span * 72 / fig.dpi
# ---- A. stance (side view, facing right toward the line of scrimmage)
ax = axes[0, 0]
ax.set_xlim(0, 30); ax.set_ylim(-4.8, 11.6); ax.set_aspect("equal")
ax.set_title("A Stance", fontsize=9, loc="left", color=style.INK, fontweight="bold")
ax.plot([0.5, 29.5], [2, 2], color=style.BASELINE, lw=1.2)
ax.plot([29.0, 29.0], [2.0, 9.6], color=style.OFFENSE_DARK, lw=1.0, ls=(0, (3, 2)))
ax.text(28.6, 10.6, "line forward ►", ha="right", va="center", fontsize=6.2, color=style.INK_2)
LW = 2.6
WEIGHT = style.DEFENSE_DARK
def stick(x0, pts, head, col=style.OFFENSE):
(fb, ff, kb, kf, hip, sh, hand) = pts
for a, b in ((fb, kb), (kb, hip), (ff, kf), (kf, hip), (hip, sh), (sh, hand)):
ax.plot([x0 + a[0], x0 + b[0]], [a[1], b[1]], color=col, lw=LW, solid_capstyle="round")
ax.add_patch(Circle((x0 + head[0], head[1]), 0.85, fc=col, ec="none"))
def weight(x, y):
"""A dot where the weight rests, with a short arrow pressing down on it."""
ax.add_patch(Circle((x, y), 0.42, fc=WEIGHT, ec="white", lw=0.5, zorder=5))
ax.annotate("", xy=(x, y + 0.5), xytext=(x, y + 2.3),
arrowprops=dict(arrowstyle="-|>", color=WEIGHT, lw=1.3), zorder=5)
# two-point: a crouch, feet wide, knees bent, hips near knee height, hands resting on the thighs
stick(4.0, [(-1.2, 2), (1.2, 2), (-0.4, 3.8), (1.8, 3.9), (-0.8, 5.0), (1.0, 6.9), (1.7, 4.3)], (1.8, 7.8))
weight(4.0, 1.75)
# light three-point: hips up and back, the hand barely touching; weight on the back foot
stick(13.5, [(-2.4, 2), (0.0, 2), (-1.6, 4.3), (0.8, 4.2), (-1.9, 6.7), (1.8, 7.5), (2.0, 2.15)], (2.9, 8.3))
weight(13.5 - 2.4, 1.75)
# heavy three-point: shoulders out over the hand, weight forward onto it
stick(23.0, [(-2.3, 2), (0.1, 2), (-1.5, 4.0), (0.8, 3.8), (-1.5, 5.9), (2.6, 5.4), (1.9, 2.15)], (3.5, 5.9))
weight(23.0 + 1.9, 1.75)
for x, t in ((4.5, "two-point:\nno hand down,\nweight even"), (13.3, "three-point,\nlight hand:\nweight back\n→ pass set?"),
(23.6, "three-point,\nheavy hand:\nweight forward\n→ run block?")):
ax.text(x, 0.7, t, ha="center", va="top", fontsize=6.3, color=style.INK_2, linespacing=1.1)
# ---- B. where the back lines up
ax = axes[0, 1]
ax.set_xlim(-2.6, 30); ax.set_ylim(-15, 3.5); ax.set_aspect("equal")
ax.set_title("B Where the back lines up", fontsize=9, loc="left", color=style.INK, fontweight="bold")
ppb = ppy_of(ax, 32.6)
for x0, qb_d, rb, name, rate in ((5, -5.0, (1.5, -5.0), "shotgun", "79% pass"),
(15, -4.0, (0.0, -7.0), "pistol", "29% pass"),
(25, -1.6, (0.0, -7.0), "under center", "31% pass")):
ax.plot([x0 - 4.3, x0 + 4.3], [0.4, 0.4], color=style.OFFENSE_DARK, lw=1.0, alpha=0.7)
for dx in (-2.6, -1.3, 0.0, 1.3, 2.6):
draw_marker(ax, x0 + dx, -0.3, "off", "", ppb, r=0.55)
draw_marker(ax, x0, qb_d, "off", "Q", ppb, r=0.6)
draw_marker(ax, x0 + rb[0], rb[1], "off", "R", ppb, r=0.6)
ax.text(x0, -9.4, name, ha="center", va="center", fontsize=7.4, color=style.INK, fontweight="bold")
ax.text(x0, -10.9, rate, ha="center", va="center", fontsize=7.0, color=style.INK_2)
# a depth ruler: yards behind the line
ax.plot([-1.2, -1.2], [0.4, -7.0], color=style.INK_2, lw=0.8)
for dd in (0, 5, 7):
ax.plot([-1.6, -0.8], [0.4 - dd if dd == 0 else -dd, 0.4 - dd if dd == 0 else -dd], color=style.INK_2, lw=0.8)
ax.text(-1.9, -5.0, "5", ha="right", va="center", fontsize=5.8, color=style.INK_2)
ax.text(-1.9, -7.0, "7", ha="right", va="center", fontsize=5.8, color=style.INK_2)
ax.text(-1.2, -8.0, "yd", ha="center", va="center", fontsize=5.6, color=style.INK_2)
ax.text(14.0, -13.4, "2025 regular season, all downs. Shotgun runs often send the\nback across the QB's face: "
"offset right, run usually goes left.", ha="center", va="center", fontsize=6.0, color=style.INK_2,
linespacing=1.15)
# ---- C. receiver splits (offense's right side, ball in the middle of the field)
ax = axes[1, 0]
ax.set_xlim(-3.4, 28.5); ax.set_ylim(-9.6, 6); ax.set_aspect("equal")
ax.set_title("C Receiver splits", fontsize=9, loc="left", color=style.INK, fontweight="bold")
ppc = ppy_of(ax, 31.9)
ax.add_patch(Rectangle((-3.4, -9.6), 31.07, 15.6, fc=style.TURF, ec="none", zorder=0))
ax.plot([26.67, 26.67], [-9.6, 6], color=style.INK_2, lw=1.4) # sideline
ax.plot([-3.4, 26.67], [0.2, 0.2], color=style.OFFENSE_DARK, lw=1.0, alpha=0.7)
ax.text(14.7, 3.8, "30", ha="center", va="center", fontsize=11, color=style.MUTED, alpha=0.6, fontweight="bold")
for yy in (-6, -3, 3):
ax.plot([2.8, 3.4], [yy, yy], color=style.BASELINE, lw=0.8)
ax.text(3.1, 4.9, "hash", ha="center", va="center", fontsize=6.0, color=style.INK_2, style="italic")
ax.text(14.7, 5.3, "numbers", ha="center", va="center", fontsize=6.0, color=style.INK_2, style="italic")
ax.text(25.9, 3.4, "sideline", ha="right", va="center", fontsize=6.0, color=style.INK_2, style="italic")
for dx in (-1.6, 0.0, 1.6):
draw_marker(ax, dx, -0.5, "off", "", ppc, r=0.55)
ax.text(0.0, -2.2, "line", ha="center", va="center", fontsize=6.0, color=style.INK_2, style="italic")
for x, lab in ((7.6, "reduced"), (14.7, "normal"), (21.3, "wide")):
draw_marker(ax, x, -0.5, "off", "Z", ppc, r=0.62)
ax.text(x, -2.2, lab, ha="center", va="center", fontsize=7.0, color=style.INK, fontweight="bold")
ax.text(7.6, -4.6, "crack block,\ncrosser or dig,\nor room to\nbreak outside", ha="center", va="center", fontsize=5.9,
color=style.INK_2, linespacing=1.1)
ax.text(14.7, -4.3, "the default:\ntells little", ha="center", va="center", fontsize=5.9, color=style.INK_2,
linespacing=1.1)
ax.text(21.3, -4.6, "alone: deep,\nbreaking inside,\nor clearing\nspace", ha="center", va="center", fontsize=5.9,
color=style.INK_2, linespacing=1.1)
ax.text(12.0, -8.4, "one receiver, three possible spots", ha="center", va="center", fontsize=6.0,
color=style.INK, style="italic")
# ---- D. safety depth
ax = axes[1, 1]
ax.set_xlim(-1, 41); ax.set_ylim(-3.5, 18.5); ax.set_aspect("equal")
ax.set_title("D Safety depth", fontsize=9, loc="left", color=style.INK, fontweight="bold")
ppd = ppy_of(ax, 42)
for x0, saf, name in ((10, [(-6.5, 13.0), (6.5, 13.0)], "two deep\n(two-high)"),
(30, [(0.0, 13.0), (5.0, 7.5)], "one deep, one down\n(one-high)")):
ax.plot([x0 - 9.5, x0 + 9.5], [0, 0], color=style.OFFENSE_DARK, lw=1.0, alpha=0.7)
for dd in (5, 10, 15):
ax.plot([x0 - 9.5, x0 + 9.5], [dd, dd], color=style.BASELINE, lw=0.5)
for dx in (-3.6, -1.2, 1.2, 3.6):
draw_marker(ax, x0 + dx, 1.0, "def", "", ppd, r=0.6)
for dx in (-2.0, 2.0):
draw_marker(ax, x0 + dx, 4.5, "def", "", ppd, r=0.6)
for sx, sd in saf:
draw_marker(ax, x0 + sx, sd, "def", "S", ppd, r=0.75)
ax.text(x0, -2.2, name, ha="center", va="center", fontsize=6.6, color=style.INK, fontweight="bold",
linespacing=1.05)
for dd in (5, 10, 15):
ax.text(-0.6, dd, f"{dd}", ha="left", va="center", fontsize=5.8, color=style.INK_2)
ax.text(-0.6, 17.3, "yards off the ball", ha="left", va="center", fontsize=5.8, color=style.INK_2)
ax.annotate("walked down:\nan extra man\nnear the line", xy=(35.6, 7.9), xytext=(37.6, 13.6), fontsize=5.8,
color=style.INK_2, ha="center", va="center", linespacing=1.1,
arrowprops=dict(arrowstyle="-|>", color=style.INK_2, lw=0.8, shrinkB=5))
fig.tight_layout(h_pad=0.6, w_pad=0.8)
plt.show()