{"spec_id":"scatter-shot-chart","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nscatter-shot-chart: Basketball Shot Chart\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 89/100 | Created: 2026-06-21\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.patches import Arc\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\"\n\n# Imprint palette — made shots: brand green (position 1); missed: semantic red anchor\nCOLOR_MADE = \"#009E73\"\nCOLOR_MISSED = \"#AE3030\"\n\nCOURT_COLOR = INK_SOFT\n\n# NBA half-court geometry (feet, origin at basket center, y-positive toward half-court)\nCOURT_LEFT = -25.0\nCOURT_RIGHT = 25.0\nCOURT_BOTTOM = -5.25\nCOURT_TOP = 41.75\n\nFT_LINE_Y = 13.75\nFT_RADIUS = 6.0\nRA_RADIUS = 4.0\nTHREE_RADIUS = 23.75\nTHREE_CORNER_X = 22.0\nTHREE_CORNER_Y = np.sqrt(THREE_RADIUS**2 - THREE_CORNER_X**2)\n\n# Data — synthetic shot chart for a versatile NBA wing player\nnp.random.seed(42)\n\n# At-rim attempts (0–3.5 ft): high make rate\nn_rim = 80\nrim_angles = np.random.uniform(0, np.pi, n_rim)\nrim_dists = np.random.triangular(0.3, 1.5, 3.5, n_rim)\nx_rim = rim_dists * np.cos(rim_angles)\ny_rim = rim_dists * np.sin(rim_angles)\nmade_rim = np.random.random(n_rim) < 0.68\n\n# Mid-range two-pointers (8–18 ft)\nn_mid = 80\nmid_angles = np.random.uniform(np.radians(8), np.radians(172), n_mid)\nmid_dists = np.random.uniform(8, 18, n_mid)\nx_mid = mid_dists * np.cos(mid_angles)\ny_mid = mid_dists * np.sin(mid_angles)\nmade_mid = np.random.random(n_mid) < 0.42\n\n# Three-point arc attempts (not corners)\nn_arc3 = 120\narc3_angles = np.random.uniform(np.radians(28), np.radians(152), n_arc3)\narc3_dists = np.random.uniform(THREE_RADIUS, THREE_RADIUS + 3.5, n_arc3)\nx_arc3 = np.clip(arc3_dists * np.cos(arc3_angles), COURT_LEFT + 0.5, COURT_RIGHT - 0.5)\ny_arc3 = arc3_dists * np.sin(arc3_angles)\nmade_arc3 = np.random.random(n_arc3) < 0.36\n\n# Corner three-pointers (both sides)\nn_corner = 50\nx_corner = np.concatenate(\n    [np.random.uniform(22.5, 24.5, n_corner // 2), np.random.uniform(-24.5, -22.5, n_corner // 2)]\n)\ny_corner = np.random.uniform(-3.5, 6.0, n_corner)\nmade_corner = np.random.random(n_corner) < 0.40\n\nx_all = np.concatenate([x_rim, x_mid, x_arc3, x_corner])\ny_all = np.concatenate([y_rim, y_mid, y_arc3, y_corner])\nmade_all = np.concatenate([made_rim, made_mid, made_arc3, made_corner])\n\ndf = pd.DataFrame(\n    {\n        \"x\": x_all,\n        \"y\": y_all,\n        \"Outcome\": pd.Categorical(np.where(made_all, \"Made\", \"Missed\"), categories=[\"Made\", \"Missed\"]),\n    }\n)\n\nn_made = int(made_all.sum())\nn_total = len(made_all)\nfg_pct = n_made / n_total\n\n# Plot — square canvas (2400 × 2400 px) matches the near-square half-court footprint\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400)\nax.set_facecolor(PAGE_BG)\n\n# Draw NBA half-court (standard 50 ft × 47 ft)\nlw = 1.6\n\nax.add_patch(\n    mpatches.Rectangle(\n        (COURT_LEFT, COURT_BOTTOM), 50, 47, linewidth=lw, edgecolor=COURT_COLOR, facecolor=\"none\", zorder=3\n    )\n)\nax.add_patch(\n    mpatches.Rectangle((-8, COURT_BOTTOM), 16, 19, linewidth=lw, edgecolor=COURT_COLOR, facecolor=\"none\", zorder=3)\n)\n\n# Free throw circle — upper arc solid, lower arc dashed\nax.add_patch(\n    Arc(\n        (0, FT_LINE_Y),\n        2 * FT_RADIUS,\n        2 * FT_RADIUS,\n        angle=0,\n        theta1=0,\n        theta2=180,\n        color=COURT_COLOR,\n        linewidth=lw,\n        zorder=3,\n    )\n)\nax.add_patch(\n    Arc(\n        (0, FT_LINE_Y),\n        2 * FT_RADIUS,\n        2 * FT_RADIUS,\n        angle=0,\n        theta1=180,\n        theta2=360,\n        color=COURT_COLOR,\n        linewidth=lw,\n        linestyle=\"--\",\n        zorder=3,\n    )\n)\n\n# Restricted area arc\nax.add_patch(\n    Arc((0, 0), 2 * RA_RADIUS, 2 * RA_RADIUS, angle=0, theta1=0, theta2=180, color=COURT_COLOR, linewidth=lw, zorder=3)\n)\n\n# Three-point line: arc + corner straight sections\ntheta1_three = np.degrees(np.arctan2(THREE_CORNER_Y, THREE_CORNER_X))\nax.add_patch(\n    Arc(\n        (0, 0),\n        2 * THREE_RADIUS,\n        2 * THREE_RADIUS,\n        angle=0,\n        theta1=theta1_three,\n        theta2=180 - theta1_three,\n        color=COURT_COLOR,\n        linewidth=lw,\n        zorder=3,\n    )\n)\nax.plot([THREE_CORNER_X, THREE_CORNER_X], [COURT_BOTTOM, THREE_CORNER_Y], color=COURT_COLOR, linewidth=lw, zorder=3)\nax.plot([-THREE_CORNER_X, -THREE_CORNER_X], [COURT_BOTTOM, THREE_CORNER_Y], color=COURT_COLOR, linewidth=lw, zorder=3)\n\n# Backboard and basket\nax.plot([-3, 3], [-1.25, -1.25], color=COURT_COLOR, linewidth=lw * 1.5, zorder=3)\nax.add_patch(mpatches.Circle((0, 0), radius=0.75, linewidth=lw, edgecolor=COURT_COLOR, facecolor=\"none\", zorder=3))\n\n# KDE density fill — highlights made-shot hot zones (seaborn's signature statistical layer)\nmade_df = df[df[\"Outcome\"] == \"Made\"]\nsns.kdeplot(\n    data=made_df, x=\"x\", y=\"y\", fill=True, levels=5, color=COLOR_MADE, alpha=0.09, bw_adjust=0.8, ax=ax, zorder=1\n)\n# KDE contour lines over the fill\nsns.kdeplot(\n    data=made_df,\n    x=\"x\",\n    y=\"y\",\n    fill=False,\n    levels=5,\n    color=COLOR_MADE,\n    alpha=0.38,\n    linewidths=0.9,\n    bw_adjust=0.8,\n    ax=ax,\n    zorder=2,\n)\n\n# Shot markers with dual encoding: hue (color) + style (shape) for CVD accessibility\npalette = {\"Made\": COLOR_MADE, \"Missed\": COLOR_MISSED}\nmarkers = {\"Made\": \"o\", \"Missed\": \"X\"}\nsns.scatterplot(\n    data=df,\n    x=\"x\",\n    y=\"y\",\n    hue=\"Outcome\",\n    style=\"Outcome\",\n    markers=markers,\n    palette=palette,\n    s=40,\n    alpha=0.55,\n    edgecolors=PAGE_BG,\n    linewidth=0.25,\n    zorder=4,\n    ax=ax,\n)\n\n# Style\nax.set_aspect(\"equal\")\nax.set_xlim(COURT_LEFT - 0.8, COURT_RIGHT + 0.8)\nax.set_ylim(COURT_BOTTOM - 0.8, COURT_TOP + 0.8)\nax.set_xlabel(\"\")\nax.set_ylabel(\"\")\nax.tick_params(axis=\"both\", which=\"both\", bottom=False, left=False, labelbottom=False, labelleft=False)\n\nfor spine in ax.spines.values():\n    spine.set_visible(False)\n\n# Legend\nlegend = ax.legend(\n    fontsize=9,\n    title=\"Shot Outcome\",\n    title_fontsize=9,\n    loc=\"upper right\",\n    markerscale=1.6,\n    framealpha=0.9,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n)\nlegend.get_title().set_color(INK)\nfor text in legend.get_texts():\n    text.set_color(INK_SOFT)\n\n# Shooting stats — placed in the empty half-court area above the three-point arc\nstats_text = f\"{n_made}/{n_total} FGM\\n{fg_pct:.1%} FG%\"\nax.text(\n    COURT_RIGHT - 2,\n    COURT_TOP - 7,\n    stats_text,\n    ha=\"right\",\n    va=\"top\",\n    fontsize=8,\n    color=INK_SOFT,\n    zorder=5,\n    bbox={\"boxstyle\": \"round,pad=0.4\", \"facecolor\": ELEVATED_BG, \"edgecolor\": INK_SOFT, \"alpha\": 0.85},\n)\n\ntitle = \"scatter-shot-chart · python · seaborn · anyplot.ai\"\nax.set_title(title, fontsize=11, fontweight=\"medium\", color=INK, pad=10)\n\nfig.subplots_adjust(top=0.94, bottom=0.02, left=0.02, right=0.98)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\nplt.close()\n"}