import os
import logging
import textwrap
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, FancyBboxPatch, FancyArrowPatch, Polygon, Circle, Ellipse
from matplotlib.lines import Line2D
from matplotlib.patches import Patch
from IPython.display import Markdown
from gridiron import Play, Player, Field, offense, defense, routes as r, show_animation, style, draw_marker
logging.getLogger("matplotlib.font_manager").setLevel(logging.ERROR)
style.apply()
PDF = os.environ.get("RTG_FORMAT", "").lower() in ("pdf", "typst", "print")
LEGAL = "#0f7a55" # dark green: legal contact
LEGAL_T = "#d7efe4"
FOUL = "#b8321f" # dark red: a foul
FOUL_T = "#fbe1da"
FLAG_Y = "#f2c200" # the yellow flag
FIELD_W = 160 / 3
def P_(fld, p, d, w):
x, y = fld.xy(*p.to_abs(d, w))
return float(x), float(y)
def no_numbers(ax):
"""Remove gridiron's painted yard-line numbers (they sit where receivers stand)."""
for t in list(ax.texts):
if t.get_alpha() == 0.55:
t.remove()
def say(fld, p, d, w, text, color=style.INK, fs=6.8, ha="center", weight="normal", box=True, style_="normal"):
"""A short on-field label at (d, w), on a pale backing so lines under it stay out of the way."""
x, y = P_(fld, p, d, w)
kw = dict(bbox=dict(boxstyle="round,pad=0.2", fc=style.SURFACE, ec="none", alpha=0.93)) if box else {}
fld.ax.text(x, y, text, fontsize=fs, color=color, ha=ha, va="center", zorder=12, fontweight=weight,
linespacing=1.12, style=style_, **kw)
def band(fld, p, d0, d1, color, alpha=1.0, w0=-26.7, w1=26.7, z=0.6):
"""Shade the strip of field between depths d0 and d1."""
(xa, ya), (xb, yb) = P_(fld, p, d0, w0), P_(fld, p, d1, w1)
fld.ax.add_patch(Rectangle((min(xa, xb), min(ya, yb)), abs(xb - xa), abs(yb - ya), fc=color,
alpha=alpha, ec="none", zorder=z))
def line_across(fld, p, d, color, lw=1.4, ls=(0, (4, 2)), w0=-26.7, w1=26.7, z=2.5):
(xa, ya), (xb, yb) = P_(fld, p, d, w0), P_(fld, p, d, w1)
fld.ax.plot([xa, xb], [ya, yb], color=color, lw=lw, ls=ls, zorder=z)
def star(fld, p, d, w, color, ms=12):
"""Where the contact happens."""
x, y = P_(fld, p, d, w)
fld.ax.plot([x], [y], marker="*", ms=ms, color=color, mec="white", mew=0.7, zorder=13)
def flag(fld, p, d, w, s=1.0):
"""A small yellow penalty flag planted at (d, w); drawn in plot yards, so it stands upright in either view."""
x, y = P_(fld, p, d, w)
ax = fld.ax
ax.plot([x, x], [y, y + 1.9 * s], color=style.INK, lw=0.9, zorder=14, solid_capstyle="round")
ax.add_patch(Polygon([(x, y + 1.9 * s), (x + 1.5 * s, y + 1.5 * s), (x, y + 1.05 * s)], closed=True,
fc=FLAG_Y, ec=style.INK, lw=0.5, zorder=14))
def ball(fld, p, d, w, z=13):
x, y = P_(fld, p, d, w)
fld.ax.add_patch(Ellipse((x, y), 1.3, 0.8, fc=style.BALL, ec="white", lw=0.7, zorder=z))
def arrow(fld, p, frm, to, color, lw=1.6, z=5, ls="-", ms=10, rad=0.0):
fld.ax.add_patch(FancyArrowPatch(P_(fld, p, *frm), P_(fld, p, *to), arrowstyle="-|>", mutation_scale=ms,
color=color, lw=lw, ls=ls, zorder=z, shrinkA=0, shrinkB=0,
connectionstyle=f"arc3,rad={rad}"))
def trail(fld, p, pts, color, lw=1.1, ls=(0, (1, 1.6)), alpha=0.9, z=3):
xs, ys = zip(*[P_(fld, p, d, w) for d, w in pts])
fld.ax.plot(xs, ys, color=color, lw=lw, ls=ls, alpha=alpha, zorder=z)
def panel_title(ax, text, fs=8.5):
ax.set_title(text, fontsize=fs, color=style.INK, loc="left", fontweight="bold")
def O_(key, pos, d, w, label=None):
return Player(key, pos, "off", d, w, label=label)
def D_(key, pos, d, w, label=None):
return Player(key, pos, "def", d, w, label=label)
def line5():
"""Five offensive linemen on the ball (gridiron's spacing), labels off."""
return [O_(k, k, -0.7, w, "C" if k == "C" else "") for k, w in
(("LT", -3.2), ("LG", -1.6), ("C", 0.0), ("RG", 1.6), ("RT", 3.2))]
# ------------------------------------------------- the print version of an animation
def snapshot(play, t, ax, lateral, window, notes=(), flags=(), stars=()):
"""Everyone where he is at time t, with faint trails of where the skill players have been."""
pos = {k: play.position(k, t) for k in play.players}
moved = [pl.moved(d=float(pos[k][0]), w=float(pos[k][1])) for k, pl in play.players.items()]
q = Play([m for m in moved if m.side == "off"], [m for m in moved if m.side == "def"],
los=play.los, ball_y=play.ball_y, to_go=play.to_go)
q.highlights = dict(play.highlights)
fld = q.draw(ax=ax, lateral=lateral, window=window, title=False)
no_numbers(fld.ax)
ppy = fld.pts_per_yard()
t0 = play._compile()[1]
for k, pl in play.players.items():
if pl.side == "off" and pl.pos not in ("WR", "TE", "RB"):
continue
if pl.side == "def" and pl.pos in ("DE", "DT", "NT"):
continue
pts = np.array([play.position(k, tt) for tt in np.linspace(t0, t, 40)])
if np.ptp(pts[:, 0]) + np.ptp(pts[:, 1]) < 0.3:
continue
xs, ys = fld.xy(*play.to_abs(pts[:, 0], pts[:, 1]))
ax.plot(xs, ys, color=style.OFFENSE if pl.side == "off" else style.DEFENSE, lw=1.0, alpha=0.5, zorder=3)
bx, by = fld.xy(*play.to_abs(*play.ball_position(t)))
for kind, _, t_rel in play.ball_events:
if kind == "pass" and t > t_rel:
sx, sy = fld.xy(*play.to_abs(*play.ball_position(t_rel)))
ax.add_patch(FancyArrowPatch((float(sx), float(sy)), (float(bx), float(by)), arrowstyle="-|>",
mutation_scale=8, color=style.BALL, lw=1.3, ls=(0, (2, 1.5)), zorder=9))
draw_marker(ax, float(bx), float(by), "ball", "", ppy)
for d, w in flags:
flag(fld, q, d, w)
for d, w, c in stars:
star(fld, q, d, w, c)
for n in notes:
d, w, text = n[:3]
kw = n[3] if len(n) > 3 else {}
say(fld, q, d, w, text, **kw)
for txt in ax.texts: # labels of players outside the window stay out of sight
txt.set_clip_on(True)
return fld
def strip(play, times, labels, lateral, window, per_panel=None, width_in=6.5, ncols=2):
"""Panels at chosen moments; per_panel[i] = dict(notes=..., flags=..., stars=...)."""
n = len(times)
nrows = int(np.ceil(n / ncols))
pw = width_in / ncols
h = nrows * (pw * (window[1] - window[0]) / abs(lateral[1] - lateral[0]) + 0.34)
fig, axes = plt.subplots(nrows, ncols, figsize=(width_in, h), squeeze=False)
for ax in axes.flat[n:]:
ax.axis("off")
for i, (ax, t, lab) in enumerate(zip(axes.flat, times, labels)):
extra = (per_panel or {}).get(i, {})
snapshot(play, t, ax, lateral, window, **extra)
panel_title(ax, lab, fs=8.3)
fig.tight_layout(h_pad=0.7, w_pad=0.5)
return fig