{"spec_id":"scatter-pitch-events","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nscatter-pitch-events: Soccer Pitch Event Map\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-06-21\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as patches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.lines import Line2D\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Pitch surface and marking colors — theme-adaptive\nPITCH_BG = \"#EEF4E5\" if THEME == \"light\" else \"#1C3828\"\nPITCH_MARK = \"#3A5A40\" if THEME == \"light\" else \"#C8E6C4\"\n\n# Imprint palette — positions 1-4 for four event categories\nIMPRINT_PALETTE = [\n    \"#009E73\",  # brand green\n    \"#C475FD\",  # lavender\n    \"#4467A3\",  # blue\n    \"#BD8233\",  # ochre\n]\n\n# Data\nnp.random.seed(42)\n\nn_events = 200\nevent_labels = [\"Pass\", \"Shot\", \"Tackle\", \"Interception\"]\nevent_types_arr = np.random.choice(event_labels, size=n_events, p=[0.50, 0.15, 0.20, 0.15])\noutcomes = np.random.choice([\"Successful\", \"Unsuccessful\"], size=n_events, p=[0.65, 0.35])\n\nx_coords = np.zeros(n_events)\ny_coords = np.zeros(n_events)\n\nfor i, etype in enumerate(event_types_arr):\n    if etype == \"Pass\":\n        x_coords[i] = np.random.uniform(10, 95)\n        y_coords[i] = np.random.uniform(5, 63)\n    elif etype == \"Shot\":\n        x_coords[i] = np.random.uniform(72, 100)\n        y_coords[i] = np.random.uniform(18, 50)\n    elif etype == \"Tackle\":\n        x_coords[i] = np.random.uniform(5, 70)\n        y_coords[i] = np.random.uniform(5, 63)\n    else:\n        x_coords[i] = np.random.uniform(15, 80)\n        y_coords[i] = np.random.uniform(5, 63)\n\narrow_dx = np.zeros(n_events)\narrow_dy = np.zeros(n_events)\nfor i, etype in enumerate(event_types_arr):\n    if etype == \"Pass\":\n        arrow_dx[i] = np.random.uniform(5, 18) * np.random.choice([-1, 1], p=[0.2, 0.8])\n        arrow_dy[i] = np.random.uniform(-8, 8)\n    elif etype == \"Shot\":\n        arrow_dx[i] = np.random.uniform(3, 9)\n        arrow_dy[i] = np.random.uniform(-4, 4)\n\ndf = pd.DataFrame(\n    {\"x\": x_coords, \"y\": y_coords, \"Event Type\": event_types_arr, \"Outcome\": outcomes, \"dx\": arrow_dx, \"dy\": arrow_dy}\n)\n\n# Imprint palette mapped to event types\npalette = {\n    \"Pass\": IMPRINT_PALETTE[0],\n    \"Shot\": IMPRINT_PALETTE[1],\n    \"Tackle\": IMPRINT_PALETTE[2],\n    \"Interception\": IMPRINT_PALETTE[3],\n}\n\n# Marker shapes diverged from sibling implementations:\n# squares for passes, X-marks for shots, hexagons for interceptions\nmarker_map = {\"Pass\": \"s\", \"Shot\": \"X\", \"Tackle\": \"^\", \"Interception\": \"h\"}\n\n# Plot — canvas: 8×4.5 in @ 400 dpi → 3200×1800 px\nsns.set_theme(\n    style=\"white\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PITCH_BG,\n        \"text.color\": INK,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400)\nfig.set_facecolor(PAGE_BG)\nax.set_facecolor(PITCH_BG)\n\n# Pitch markings\nlw = 1.5\nalp = 0.85\n\nax.plot([0, 105, 105, 0, 0], [0, 0, 68, 68, 0], color=PITCH_MARK, lw=lw + 0.3, alpha=alp)\nax.plot([52.5, 52.5], [0, 68], color=PITCH_MARK, lw=lw, alpha=alp)\nax.add_patch(patches.Circle((52.5, 34), 9.15, fill=False, edgecolor=PITCH_MARK, lw=lw, alpha=alp))\nax.plot(52.5, 34, \"o\", color=PITCH_MARK, markersize=3, alpha=alp)\n\n# Penalty and goal areas\nax.plot([0, 16.5, 16.5, 0], [13.84, 13.84, 54.16, 54.16], color=PITCH_MARK, lw=lw, alpha=alp)\nax.plot([105, 88.5, 88.5, 105], [13.84, 13.84, 54.16, 54.16], color=PITCH_MARK, lw=lw, alpha=alp)\nax.plot([0, 5.5, 5.5, 0], [24.84, 24.84, 43.16, 43.16], color=PITCH_MARK, lw=lw, alpha=alp)\nax.plot([105, 99.5, 99.5, 105], [24.84, 24.84, 43.16, 43.16], color=PITCH_MARK, lw=lw, alpha=alp)\n\n# Penalty spots\nax.plot(11, 34, \"o\", color=PITCH_MARK, markersize=3, alpha=alp)\nax.plot(94, 34, \"o\", color=PITCH_MARK, markersize=3, alpha=alp)\n\n# Penalty arcs\nax.add_patch(patches.Arc((11, 34), 18.3, 18.3, angle=0, theta1=308, theta2=52, edgecolor=PITCH_MARK, lw=lw, alpha=alp))\nax.add_patch(patches.Arc((94, 34), 18.3, 18.3, angle=0, theta1=128, theta2=232, edgecolor=PITCH_MARK, lw=lw, alpha=alp))\n\n# Corner arcs\nfor cx, cy, t1, t2 in [(0, 0, 0, 90), (105, 0, 90, 180), (105, 68, 180, 270), (0, 68, 270, 360)]:\n    ax.add_patch(patches.Arc((cx, cy), 2, 2, angle=0, theta1=t1, theta2=t2, edgecolor=PITCH_MARK, lw=lw, alpha=alp))\n\n# Goal posts\nfor x0, x1 in [(0, -1.5), (105, 106.5)]:\n    ax.plot([x0, x1], [30.34, 30.34], color=PITCH_MARK, lw=lw + 0.5, alpha=alp)\n    ax.plot([x0, x1], [37.66, 37.66], color=PITCH_MARK, lw=lw + 0.5, alpha=alp)\n    ax.plot([x1, x1], [30.34, 37.66], color=PITCH_MARK, lw=lw + 0.5, alpha=alp)\n\n# KDE density contours — 2 levels to limit visual clutter\nfor etype, color in palette.items():\n    subset = df[df[\"Event Type\"] == etype]\n    if len(subset) > 5:\n        sns.kdeplot(\n            data=subset,\n            x=\"x\",\n            y=\"y\",\n            color=color,\n            levels=2,\n            alpha=0.18,\n            linewidths=1.0,\n            ax=ax,\n            zorder=3,\n            warn_singular=False,\n            clip=((0, 105), (0, 68)),\n        )\n\n# Scatter: successful events (opaque, larger)\ndf_success = df[df[\"Outcome\"] == \"Successful\"]\ndf_unsuccess = df[df[\"Outcome\"] == \"Unsuccessful\"]\n\nsns.scatterplot(\n    data=df_success,\n    x=\"x\",\n    y=\"y\",\n    hue=\"Event Type\",\n    style=\"Event Type\",\n    hue_order=event_labels,\n    style_order=event_labels,\n    markers=marker_map,\n    palette=palette,\n    s=110,\n    alpha=0.90,\n    edgecolor=PAGE_BG,\n    linewidth=0.5,\n    legend=False,\n    ax=ax,\n    zorder=5,\n)\n\n# Scatter: unsuccessful events (faded, smaller)\nsns.scatterplot(\n    data=df_unsuccess,\n    x=\"x\",\n    y=\"y\",\n    hue=\"Event Type\",\n    style=\"Event Type\",\n    hue_order=event_labels,\n    style_order=event_labels,\n    markers=marker_map,\n    palette=palette,\n    s=65,\n    alpha=0.38,\n    edgecolor=PAGE_BG,\n    linewidth=0.4,\n    legend=False,\n    ax=ax,\n    zorder=5,\n)\n\n# Directional arrows — sparse sample; reduced density near crowded shot zone (x > 75)\ndf_arr = df_success[df_success[\"Event Type\"].isin([\"Pass\", \"Shot\"])]\ndf_arr_sparse = pd.concat(\n    [\n        df_arr[df_arr[\"x\"] <= 75].sample(frac=0.40, random_state=42),\n        df_arr[df_arr[\"x\"] > 75].sample(frac=0.20, random_state=42),\n    ]\n)\nfor _, row in df_arr_sparse.iterrows():\n    ax.annotate(\n        \"\",\n        xy=(row[\"x\"] + row[\"dx\"], row[\"y\"] + row[\"dy\"]),\n        xytext=(row[\"x\"], row[\"y\"]),\n        arrowprops={\"arrowstyle\": \"->\", \"color\": palette[row[\"Event Type\"]], \"lw\": 0.8, \"alpha\": 0.45},\n        zorder=4,\n    )\n\n# Style\nax.set_xlim(-4, 109)\nax.set_ylim(-3, 71)\nax.set_aspect(\"equal\")\nax.axis(\"off\")\n\ntitle = \"scatter-pitch-events · python · seaborn · anyplot.ai\"\nax.set_title(title, fontsize=12, fontweight=\"medium\", color=INK, pad=10)\n\n# Legend — 4 entries (event types only); title explains opacity encoding\nlegend_elements = [\n    Line2D(\n        [0],\n        [0],\n        marker=marker_map[etype],\n        color=\"none\",\n        markerfacecolor=palette[etype],\n        markeredgecolor=PAGE_BG,\n        markersize=9 if etype == \"Shot\" else 8,\n        markeredgewidth=0.5,\n        label=etype,\n    )\n    for etype in event_labels\n]\nlegend = ax.legend(\n    handles=legend_elements,\n    loc=\"lower center\",\n    bbox_to_anchor=(0.5, -0.06),\n    ncol=4,\n    fontsize=9,\n    frameon=True,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n    labelcolor=INK,\n    handletextpad=0.4,\n    columnspacing=1.0,\n    title=\"Event type  ·  opacity encodes outcome (opaque = successful)\",\n    title_fontsize=8,\n)\nlegend.get_title().set_color(INK_MUTED)\n\nfig.subplots_adjust(left=0.01, right=0.99, top=0.93, bottom=0.08)\n\n# Save — bbox_inches must stay default (None) to preserve 3200×1800 canvas\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\nplt.close()\n"}