{"spec_id":"scatter-shot-chart","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nscatter-shot-chart: Basketball Shot Chart\nLibrary: plotly 6.8.0 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-06-21\n\"\"\"\n\nimport os\nimport sys\n\n\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport numpy as np\nimport plotly.graph_objects as go\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# Court aesthetics — warm hardwood (light) / dark parquet (dark)\nCOURT_FLOOR = \"#FDF3E3\" if THEME == \"light\" else \"#1E1509\"\nPAINT_FILL = \"#F5E5C5\" if THEME == \"light\" else \"#281C0A\"\nCOURT_LINE = \"#7A6248\" if THEME == \"light\" else \"#A08850\"\nSHADOW_CLR = \"rgba(80,60,40,0.08)\" if THEME == \"light\" else \"rgba(0,0,0,0)\"\n\n# Annotation background — theme-adaptive elevated surface with alpha\nANN_BG = \"rgba(255,253,246,0.88)\" if THEME == \"light\" else \"rgba(36,36,32,0.88)\"\n\n# Imprint palette: green = made (success), red = missed (error) — spec + semantic roles\nBRAND = \"#009E73\"  # Imprint pos 1 — made shots\nMISS_CLR = \"#AE3030\"  # Imprint pos 5 — missed shots\n\n# Data\nnp.random.seed(99)\n\nx = np.concatenate(\n    [\n        np.random.normal(0, 4, 110),  # Paint area\n        np.random.normal(0, 8, 130),  # Mid-range\n        np.random.uniform(-22, 22, 70),  # Corner threes and wings\n        np.random.normal(0, 10, 80),  # Top of key / three-point\n        np.zeros(30),  # Free throws\n    ]\n)\ny = np.concatenate(\n    [\n        np.random.uniform(0, 8, 110),\n        np.random.uniform(6, 18, 130),\n        np.random.uniform(0, 10, 70),\n        np.random.uniform(20, 28, 80),\n        np.full(30, 15.0) + np.random.normal(0, 0.3, 30),\n    ]\n)\nx = np.clip(x, -24.5, 24.5)\ny = np.clip(y, 0.5, 46)\n\ndistance = np.sqrt(x**2 + y**2)\nmake_prob = np.clip(0.65 - distance * 0.012, 0.25, 0.70)\nmade = np.random.random(len(x)) < make_prob\n\nthree_pt_threshold = np.where(np.abs(x) >= 22, 22.0, 23.75)\nft_mask = (np.abs(x) < 1) & (y > 14) & (y < 16)\npaint_mask = (np.abs(x) < 8) & (y < 10)\nmid_mask = ~paint_mask & (distance <= three_pt_threshold) & ~ft_mask\nthree_mask = distance > three_pt_threshold\n\npaint_pct = np.mean(made[paint_mask]) * 100\nmid_pct = np.mean(made[mid_mask]) * 100\nthree_pct = np.mean(made[three_mask]) * 100\noverall_pct = np.mean(made) * 100\n\n# Court shapes\ncourt_shapes = [\n    # Shadow for depth (light only)\n    {\n        \"type\": \"rect\",\n        \"x0\": -24.7,\n        \"y0\": -0.5,\n        \"x1\": 25.3,\n        \"y1\": 47.5,\n        \"line\": {\"width\": 0},\n        \"fillcolor\": SHADOW_CLR,\n        \"layer\": \"below\",\n    },\n    # Court floor (warm hardwood tone)\n    {\n        \"type\": \"rect\",\n        \"x0\": -25,\n        \"y0\": 0,\n        \"x1\": 25,\n        \"y1\": 47,\n        \"line\": {\"color\": COURT_LINE, \"width\": 2.5},\n        \"fillcolor\": COURT_FLOOR,\n        \"layer\": \"below\",\n    },\n    # Paint / key area (slightly highlighted)\n    {\n        \"type\": \"rect\",\n        \"x0\": -8,\n        \"y0\": 0,\n        \"x1\": 8,\n        \"y1\": 19,\n        \"line\": {\"color\": COURT_LINE, \"width\": 2},\n        \"fillcolor\": PAINT_FILL,\n        \"layer\": \"below\",\n    },\n    # Backboard\n    {\n        \"type\": \"line\",\n        \"x0\": -3,\n        \"y0\": -0.5,\n        \"x1\": 3,\n        \"y1\": -0.5,\n        \"line\": {\"color\": COURT_LINE, \"width\": 3},\n        \"layer\": \"below\",\n    },\n]\n\n# Curved court geometry\nangle_3pt = np.arccos(22 / 23.75)\ntheta_3pt = np.linspace(angle_3pt, np.pi - angle_3pt, 200)\narc_x = 23.75 * np.cos(theta_3pt)\narc_y = 23.75 * np.sin(theta_3pt)\ncorner_y = 23.75 * np.sin(angle_3pt)\n\ntheta_ft_top = np.linspace(0, np.pi, 100)\nft_top_x = 6 * np.cos(theta_ft_top)\nft_top_y = 19 + 6 * np.sin(theta_ft_top)\n\ntheta_ft_bot = np.linspace(np.pi, 2 * np.pi, 100)\nft_bot_x = 6 * np.cos(theta_ft_bot)\nft_bot_y = 19 + 6 * np.sin(theta_ft_bot)\n\ntheta_ra = np.linspace(0, np.pi, 100)\nra_x = 4 * np.cos(theta_ra)\nra_y = 4 * np.sin(theta_ra)\n\ntheta_rim = np.linspace(0, 2 * np.pi, 50)\nrim_x = 0.75 * np.cos(theta_rim)\nrim_y = 1.25 + 0.75 * np.sin(theta_rim)\n\n# Plot\nfig = go.Figure()\ncline = {\"color\": COURT_LINE, \"width\": 2}\n\n# Three-point arc connected to corner three lines\nfig.add_trace(\n    go.Scatter(\n        x=np.concatenate([[-22], arc_x[::-1], [22]]),\n        y=np.concatenate([[0], arc_y[::-1], [0]]),\n        mode=\"lines\",\n        line={\"color\": COURT_LINE, \"width\": 2.5},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\nfig.add_trace(go.Scatter(x=[-22, -22], y=[0, corner_y], mode=\"lines\", line=cline, showlegend=False, hoverinfo=\"skip\"))\nfig.add_trace(go.Scatter(x=[22, 22], y=[0, corner_y], mode=\"lines\", line=cline, showlegend=False, hoverinfo=\"skip\"))\n\n# Free-throw circle (top solid, bottom dashed)\nfig.add_trace(go.Scatter(x=ft_top_x, y=ft_top_y, mode=\"lines\", line=cline, showlegend=False, hoverinfo=\"skip\"))\nfig.add_trace(\n    go.Scatter(\n        x=ft_bot_x,\n        y=ft_bot_y,\n        mode=\"lines\",\n        line={\"color\": COURT_LINE, \"width\": 2, \"dash\": \"dash\"},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\n# Restricted area arc\nfig.add_trace(go.Scatter(x=ra_x, y=ra_y, mode=\"lines\", line=cline, showlegend=False, hoverinfo=\"skip\"))\n\n# Basket rim (orange — domain-appropriate structural element)\nfig.add_trace(\n    go.Scatter(\n        x=rim_x, y=rim_y, mode=\"lines\", line={\"color\": \"#CC5500\", \"width\": 2.5}, showlegend=False, hoverinfo=\"skip\"\n    )\n)\n\n# Marker sizes — slightly larger near basket for visual depth\nmarker_sizes = np.clip(14 - distance * 0.15, 8, 14)\n\n# Missed shots (x markers, matte red)\nmissed_mask = ~made\nfig.add_trace(\n    go.Scatter(\n        x=x[missed_mask],\n        y=y[missed_mask],\n        mode=\"markers\",\n        marker={\n            \"size\": marker_sizes[missed_mask],\n            \"color\": MISS_CLR,\n            \"symbol\": \"x\",\n            \"line\": {\"width\": 1.5, \"color\": MISS_CLR},\n            \"opacity\": 0.7,\n        },\n        name=\"Missed\",\n        hovertemplate=\"x: %{x:.1f} ft<br>y: %{y:.1f} ft<br>Missed<extra></extra>\",\n    )\n)\n\n# Made shots (circle markers, brand green)\nfig.add_trace(\n    go.Scatter(\n        x=x[made],\n        y=y[made],\n        mode=\"markers\",\n        marker={\n            \"size\": marker_sizes[made],\n            \"color\": BRAND,\n            \"symbol\": \"circle\",\n            \"line\": {\"width\": 1.2, \"color\": PAGE_BG},\n            \"opacity\": 0.8,\n        },\n        name=\"Made\",\n        hovertemplate=\"x: %{x:.1f} ft<br>y: %{y:.1f} ft<br>Made<extra></extra>\",\n    )\n)\n\n# Zone shooting percentage annotations (data storytelling)\nann_font = {\"size\": 18, \"color\": INK, \"family\": \"Arial, sans-serif\"}\nfor xp, yp, txt in [\n    (0, 5, f\"Paint<br><b>{paint_pct:.0f}%</b>\"),\n    (13, 12, f\"Mid<br><b>{mid_pct:.0f}%</b>\"),\n    (0, 34, f\"3-Point<br><b>{three_pct:.0f}%</b>\"),\n]:\n    fig.add_annotation(\n        x=xp,\n        y=yp,\n        text=txt,\n        showarrow=False,\n        font=ann_font,\n        bgcolor=ANN_BG,\n        borderpad=6,\n        bordercolor=INK_SOFT,\n        borderwidth=1,\n    )\n\n# Style\ntitle_text = \"scatter-shot-chart · python · plotly · anyplot.ai\"  # 49 chars < 67 baseline\nsubtitle = (\n    f\"{int(np.sum(made))}/{len(made)} made ({overall_pct:.1f}% FG)\"\n    f\" · Paint {paint_pct:.0f}% · Mid {mid_pct:.0f}% · 3PT {three_pct:.0f}%\"\n)\n\nfig.update_layout(\n    autosize=False,\n    width=600,\n    height=600,\n    margin={\"l\": 40, \"r\": 40, \"t\": 110, \"b\": 40},\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    title={\n        \"text\": f\"{title_text}<br><span style='font-size:12px;color:{INK_SOFT}'>{subtitle}</span>\",\n        \"font\": {\"size\": 16, \"color\": INK, \"family\": \"Arial, sans-serif\"},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    xaxis={\n        \"range\": [-28, 28],\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"showticklabels\": False,\n        \"showline\": False,\n        \"scaleanchor\": \"y\",\n        \"scaleratio\": 1,\n    },\n    yaxis={\"range\": [-3, 50], \"showgrid\": False, \"zeroline\": False, \"showticklabels\": False, \"showline\": False},\n    shapes=court_shapes,\n    legend={\n        \"font\": {\"size\": 14, \"color\": INK_SOFT, \"family\": \"Arial, sans-serif\"},\n        \"x\": 0.85,\n        \"y\": 0.97,\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"itemsizing\": \"constant\",\n    },\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=600, height=600, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}