{"spec_id":"scatter-shot-chart","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nscatter-shot-chart: Basketball Shot Chart\nLibrary: matplotlib 3.11.0 | Python 3.13.14\nQuality: 92/100 | Updated: 2026-06-21\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this script's filename from shadowing the installed matplotlib package\nsys.path = [p for p in sys.path if os.path.abspath(p) != os.path.dirname(os.path.abspath(__file__))]\n\nimport matplotlib.patches as patches\nimport matplotlib.patheffects as pe\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.colors import LinearSegmentedColormap\nfrom matplotlib.path import Path\n\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\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# Imprint palette — semantic mapping: made=green (good), missed=red (bad), FT=blue\nC_MADE = \"#009E73\"  # brand green — made shots (position 1)\nC_MISSED = \"#AE3030\"  # matte red — missed shots (semantic: bad/error, position 5)\nC_FT = \"#4467A3\"  # blue — free-throw distinct shot type (position 3)\n\n# Data\nnp.random.seed(42)\n\nn_ft = 25\nn_field = 325\nn_shots = n_field + n_ft\n\n# Field goal shot locations in feet relative to basket center at (0, 0)\nx_field = np.concatenate(\n    [\n        np.random.normal(0, 3, 55),  # paint area shots\n        np.random.normal(0, 1.5, 25),  # close to basket\n        np.random.uniform(-8, 8, 50),  # mid-range middle\n        np.random.normal(-15, 3, 35),  # left wing mid-range\n        np.random.normal(15, 3, 35),  # right wing mid-range\n        np.random.normal(-22, 1.5, 25),  # left corner three\n        np.random.normal(22, 1.5, 25),  # right corner three\n        np.random.normal(0, 8, 40),  # top of arc three\n        np.random.normal(-12, 4, 18),  # left wing three\n        np.random.normal(12, 4, 17),  # right wing three\n    ]\n)\n\ny_field = np.concatenate(\n    [\n        np.random.uniform(0, 12, 55),  # paint\n        np.random.uniform(0, 4, 25),  # close\n        np.random.uniform(10, 18, 50),  # mid-range\n        np.random.uniform(5, 15, 35),  # left wing mid\n        np.random.uniform(5, 15, 35),  # right wing mid\n        np.random.uniform(0, 8, 25),  # left corner\n        np.random.uniform(0, 8, 25),  # right corner\n        np.random.uniform(22, 30, 40),  # top of arc\n        np.random.uniform(15, 25, 18),  # left wing three\n        np.random.uniform(15, 25, 17),  # right wing three\n    ]\n)\n\n# Free-throw shots clustered at the free-throw line (15 ft from backboard)\nx_ft = np.random.normal(0, 0.8, n_ft)\ny_ft = np.random.normal(14.0, 0.6, n_ft)\n\nx = np.clip(np.concatenate([x_field, x_ft]), -24.5, 24.5)\ny = np.clip(np.concatenate([y_field, y_ft]), 0, 40)\n\n# Shot outcome — closer shots have higher make rate; free throws ~75%\ndistance = np.sqrt(x**2 + y**2)\nmake_prob = np.clip(0.65 - distance * 0.012, 0.25, 0.70)\nmake_prob[-n_ft:] = 0.75\nmade = np.random.random(n_shots) < make_prob\n\n# Shot type based on distance from basket\nthree_pt_dist = np.where(np.abs(x) >= 22, 22.0, 23.75)\nshot_type = np.array(\n    [\"3-pointer\" if d >= t else \"2-pointer\" for d, t in zip(distance, three_pt_dist, strict=False)], dtype=object\n)\nshot_type[-n_ft:] = \"free-throw\"\n\n# Plot — square canvas (1:1 aspect for undistorted court)\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nlw = 2.0\n\n# Court geometry — theme-adaptive line color\ncourt_patches = [\n    patches.Rectangle((-25, -5.25), 50, 47, linewidth=lw, edgecolor=INK_SOFT, facecolor=\"none\"),\n    patches.Circle((0, 0), 0.75, linewidth=lw, edgecolor=\"#BD8233\", facecolor=\"none\"),\n    patches.Rectangle((-8, -5.25), 16, 19.25, linewidth=lw, edgecolor=INK_SOFT, facecolor=\"none\"),\n    patches.Arc((0, 14.0), 12, 12, angle=0, theta1=0, theta2=180, linewidth=lw, edgecolor=INK_SOFT),\n    patches.Arc((0, 14.0), 12, 12, angle=0, theta1=180, theta2=360, linewidth=lw, edgecolor=INK_SOFT, linestyle=\"--\"),\n    patches.Arc((0, 0), 8, 8, angle=0, theta1=0, theta2=180, linewidth=lw, edgecolor=INK_SOFT),\n    patches.Arc((0, 41.75), 12, 12, angle=0, theta1=180, theta2=360, linewidth=lw, edgecolor=INK_SOFT),\n]\nfor p in court_patches:\n    ax.add_patch(p)\n\n# Backboard\nax.plot([-3, 3], [-1.0, -1.0], color=INK_SOFT, linewidth=3)\n\n# Three-point line corners and arc\nax.plot([-22, -22], [-5.25, 8.75], color=INK_SOFT, linewidth=lw)\nax.plot([22, 22], [-5.25, 8.75], color=INK_SOFT, linewidth=lw)\nthree_arc_angle = np.degrees(np.arccos(22.0 / 23.75))\nax.add_patch(\n    patches.Arc(\n        (0, 0),\n        47.5,\n        47.5,\n        angle=90,\n        theta1=-90 + three_arc_angle,\n        theta2=90 - three_arc_angle,\n        linewidth=lw,\n        edgecolor=INK_SOFT,\n    )\n)\n\n# Half-court line\nax.plot([-25, 25], [41.75, 41.75], color=INK_SOFT, linewidth=lw)\n\n# Imprint diverging colormap for efficiency underlay (centered near 50% FG)\nmidpoint = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nimprint_div = LinearSegmentedColormap.from_list(\"imprint_div\", [C_MISSED, midpoint, \"#4467A3\"])\nax.hexbin(\n    x,\n    y,\n    C=made.astype(float),\n    gridsize=15,\n    cmap=imprint_div,\n    reduce_C_function=np.mean,\n    alpha=0.14,\n    extent=[-25, 25, -5, 42],\n    mincnt=2,\n    zorder=2,\n    linewidths=0,\n)\n\n# Custom diamond marker for made field goals\ndiamond_verts = [(-0.5, 0), (0, 0.7), (0.5, 0), (0, -0.7), (-0.5, 0)]\ndiamond_codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]\ndiamond_marker = Path(diamond_verts, diamond_codes)\n\n# Shot masks\nfield_made = made & (shot_type != \"free-throw\")\nfield_missed = ~made & (shot_type != \"free-throw\")\nft_made = made & (shot_type == \"free-throw\")\nft_missed = ~made & (shot_type == \"free-throw\")\n\n# Missed field goals — red X\nax.scatter(\n    x[field_missed],\n    y[field_missed],\n    s=55,\n    marker=\"x\",\n    c=C_MISSED,\n    alpha=0.55,\n    linewidths=1.8,\n    zorder=4,\n    label=\"Missed FG\",\n)\n# Made field goals — green diamond\nax.scatter(\n    x[field_made],\n    y[field_made],\n    s=60,\n    marker=diamond_marker,\n    c=C_MADE,\n    alpha=0.65,\n    edgecolors=PAGE_BG,\n    linewidth=0.5,\n    zorder=5,\n    label=\"Made FG\",\n)\n# Made free throws — blue filled circle\nax.scatter(\n    x[ft_made],\n    y[ft_made],\n    s=60,\n    marker=\"o\",\n    c=C_FT,\n    alpha=0.75,\n    edgecolors=PAGE_BG,\n    linewidth=0.5,\n    zorder=5,\n    label=\"Made FT\",\n)\n# Missed free throws — blue open circle, clearly distinct from missed field goals\nax.scatter(\n    x[ft_missed],\n    y[ft_missed],\n    s=60,\n    marker=\"o\",\n    c=\"none\",\n    edgecolors=C_FT,\n    linewidths=1.8,\n    alpha=0.75,\n    zorder=4,\n    label=\"Missed FT\",\n)\n\n# Style\nax.set_xlim(-27, 27)\nax.set_ylim(-7, 44)\nax.set_aspect(\"equal\")\nax.axis(\"off\")\n\ntitle = \"scatter-shot-chart · python · matplotlib · anyplot.ai\"\nax.set_title(\n    title,\n    fontsize=12,\n    fontweight=\"medium\",\n    color=INK,\n    pad=12,\n    path_effects=[pe.withStroke(linewidth=3, foreground=PAGE_BG)],\n)\n\n# Legend — 4 entries to distinguish all shot outcome × type combinations\nlegend = ax.legend(\n    loc=\"lower center\",\n    ncol=4,\n    fontsize=8,\n    framealpha=0.85,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n    bbox_to_anchor=(0.5, -0.03),\n    markerscale=1.6,\n    handletextpad=0.6,\n)\nplt.setp(legend.get_texts(), color=INK_SOFT)\n\n# Shooting summary\ntotal = n_shots\nmakes = int(made.sum())\nfg_pct = makes / total * 100\ntwos = shot_type == \"2-pointer\"\nthrees = shot_type == \"3-pointer\"\nfts = shot_type == \"free-throw\"\nfg2 = made[twos].sum() / twos.sum() * 100 if twos.sum() > 0 else 0\nfg3 = made[threes].sum() / threes.sum() * 100 if threes.sum() > 0 else 0\nft_pct = made[fts].sum() / fts.sum() * 100 if fts.sum() > 0 else 0\nsummary = f\"FG: {makes}/{total} ({fg_pct:.1f}%)  |  2PT: {fg2:.0f}%  |  3PT: {fg3:.0f}%  |  FT: {ft_pct:.0f}%\"\nax.text(\n    0,\n    43.0,\n    summary,\n    fontsize=8,\n    color=INK_MUTED,\n    ha=\"center\",\n    va=\"top\",\n    path_effects=[pe.withStroke(linewidth=2, foreground=PAGE_BG)],\n)\n\nfig.subplots_adjust(left=0.02, right=0.98, top=0.94, bottom=0.09)\n\n# Save — no bbox_inches='tight' to preserve exact 2400×2400 canvas\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}