{"spec_id":"scatter-shot-chart","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nscatter-shot-chart: Basketball Shot Chart\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 84/100 | Created: 2026-06-21\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-import: when Python runs plotnine.py its own directory lands on\n# sys.path[0], shadowing the real plotnine library.  Remove it before the import.\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _this_dir]\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_label,\n    geom_path,\n    geom_point,\n    geom_rect,\n    ggplot,\n    labs,\n    scale_color_manual,\n    scale_shape_manual,\n    theme,\n)\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# Imprint palette — semantic assignment\nMADE_COLOR = \"#009E73\"  # brand green (made = success)\nMISSED_COLOR = \"#AE3030\"  # matte red (miss = failure)\nCOURT_COLOR = INK_MUTED\nPAINT_FILL = \"#DDCC77\"  # Imprint amber — subtle hardwood zone tint\n\n# NBA half-court dimensions (feet, basket center at origin)\nBASELINE_Y = -5.25  # baseline is 5.25 ft behind basket rim\nHALFCOURT_Y = 41.75  # half-court line from basket\nTHREE_R = 23.75  # three-point arc radius\nCORNER_X = 22.0  # x where corner three meets the arc\nCORNER_BREAK_Y = float(np.sqrt(THREE_R**2 - CORNER_X**2))  # ~8.95 ft\nFT_Y = 13.75  # free-throw line distance from basket\nFT_R = 6.0  # free-throw circle radius\nPAINT_W = 8.0  # half-width of the 16 ft key/paint\nBACKBOARD_Y = -1.25  # backboard (4 ft from baseline)\n\n# Court geometry — one DataFrame per segment, combined for geom_path\n_segs = []\n\n# Outer boundary\n_segs.append(pd.DataFrame({\"x\": [-25, 25], \"y\": [BASELINE_Y] * 2, \"seg\": \"baseline\"}))\n_segs.append(pd.DataFrame({\"x\": [25] * 2, \"y\": [BASELINE_Y, HALFCOURT_Y], \"seg\": \"side_r\"}))\n_segs.append(pd.DataFrame({\"x\": [-25] * 2, \"y\": [BASELINE_Y, HALFCOURT_Y], \"seg\": \"side_l\"}))\n_segs.append(pd.DataFrame({\"x\": [-25, 25], \"y\": [HALFCOURT_Y] * 2, \"seg\": \"halfcourt\"}))\n\n# Paint / key rectangle\n_segs.append(pd.DataFrame({\"x\": [-PAINT_W] * 2, \"y\": [BASELINE_Y, FT_Y], \"seg\": \"paint_l\"}))\n_segs.append(pd.DataFrame({\"x\": [PAINT_W] * 2, \"y\": [BASELINE_Y, FT_Y], \"seg\": \"paint_r\"}))\n_segs.append(pd.DataFrame({\"x\": [-PAINT_W, PAINT_W], \"y\": [FT_Y] * 2, \"seg\": \"ft_line\"}))\n\n# Free-throw circle upper half\n_a = np.linspace(0, np.pi, 60)\n_segs.append(pd.DataFrame({\"x\": FT_R * np.cos(_a), \"y\": FT_Y + FT_R * np.sin(_a), \"seg\": \"ft_upper\"}))\n\n# Free-throw circle lower half (dashed in real life — drawn here for context)\n_a = np.linspace(np.pi, 2 * np.pi, 60)\n_segs.append(pd.DataFrame({\"x\": FT_R * np.cos(_a), \"y\": FT_Y + FT_R * np.sin(_a), \"seg\": \"ft_lower\"}))\n\n# Three-point arc (right corner break to left, sweeping through top)\n_theta1 = float(np.arctan2(CORNER_BREAK_Y, CORNER_X))\n_theta2 = np.pi - _theta1\n_a = np.linspace(_theta1, _theta2, 120)\n_segs.append(pd.DataFrame({\"x\": THREE_R * np.cos(_a), \"y\": THREE_R * np.sin(_a), \"seg\": \"three_arc\"}))\n\n# Corner three straight sections\n_segs.append(pd.DataFrame({\"x\": [CORNER_X] * 2, \"y\": [BASELINE_Y, CORNER_BREAK_Y], \"seg\": \"corner_r\"}))\n_segs.append(pd.DataFrame({\"x\": [-CORNER_X] * 2, \"y\": [BASELINE_Y, CORNER_BREAK_Y], \"seg\": \"corner_l\"}))\n\n# Restricted area arc (4 ft radius)\n_a = np.linspace(0, np.pi, 60)\n_segs.append(pd.DataFrame({\"x\": 4.0 * np.cos(_a), \"y\": 4.0 * np.sin(_a), \"seg\": \"restricted\"}))\n\n# Basket rim circle (0.75 ft radius)\n_a = np.linspace(0, 2 * np.pi, 60)\n_segs.append(pd.DataFrame({\"x\": 0.75 * np.cos(_a), \"y\": 0.75 * np.sin(_a), \"seg\": \"basket\"}))\n\n# Backboard\n_segs.append(pd.DataFrame({\"x\": [-3.0, 3.0], \"y\": [BACKBOARD_Y] * 2, \"seg\": \"backboard\"}))\n\n# Half-court center circle (6 ft radius — partially visible at top)\n_a = np.linspace(0, 2 * np.pi, 80)\n_segs.append(pd.DataFrame({\"x\": 6.0 * np.cos(_a), \"y\": HALFCOURT_Y + 6.0 * np.sin(_a), \"seg\": \"hc_circle\"}))\n\ncourt_df = pd.concat(_segs, ignore_index=True)\n\n# Paint area tint for zone context\npaint_rect = pd.DataFrame({\"xmin\": [-PAINT_W], \"xmax\": [PAINT_W], \"ymin\": [BASELINE_Y], \"ymax\": [FT_Y]})\n\n# Shot data — simulated NBA player shot chart with shot_type field\nnp.random.seed(42)\n\n_zone_specs = [\n    # (x_center, y_center, x_std, y_std, n, fg_pct, shot_type, zone_key)\n    (0, 4.0, 4.0, 3.0, 90, 0.62, \"2-pointer\", \"paint\"),\n    (-11, FT_Y, 2.5, 3.0, 40, 0.41, \"2-pointer\", \"l_elbow\"),\n    (11, FT_Y, 2.5, 3.0, 40, 0.41, \"2-pointer\", \"r_elbow\"),\n    (0, 26.0, 5.5, 1.5, 65, 0.37, \"3-pointer\", \"top3\"),\n    (-22.5, 2.0, 1.0, 1.8, 25, 0.42, \"3-pointer\", \"l_corner\"),\n    (22.5, 2.0, 1.0, 1.8, 25, 0.42, \"3-pointer\", \"r_corner\"),\n    (-20, 19, 2.0, 2.0, 30, 0.34, \"3-pointer\", \"l_wing\"),\n    (20, 19, 2.0, 2.0, 30, 0.34, \"3-pointer\", \"r_wing\"),\n    (0.0, FT_Y, 0.2, 0.2, 20, 0.78, \"free-throw\", \"ft\"),\n]\n\n_all_x, _all_y, _all_m, _all_type, _all_zone = [], [], [], [], []\nfor _xc, _yc, _xs, _ys, _n, _fg, _stype, _zkey in _zone_specs:\n    _x = np.random.normal(_xc, _xs, _n)\n    _y = np.random.normal(_yc, _ys, _n)\n    _m = np.random.random(_n) < _fg\n    _all_x.extend(_x)\n    _all_y.extend(_y)\n    _all_m.extend(_m)\n    _all_type.extend([_stype] * _n)\n    _all_zone.extend([_zkey] * _n)\n\nshots_df = pd.DataFrame({\"x\": _all_x, \"y\": _all_y, \"made\": _all_m, \"shot_type\": _all_type, \"zone\": _all_zone})\nshots_df[\"outcome\"] = shots_df[\"made\"].map({True: \"Made\", False: \"Missed\"})\nshots_df = shots_df[shots_df[\"x\"].between(-25, 25) & shots_df[\"y\"].between(BASELINE_Y, HALFCOURT_Y)].reset_index(\n    drop=True\n)\n\n# Per-zone FG% annotations — computed from actual data\n_zone_fg = shots_df.groupby(\"zone\")[\"made\"].mean()\n\n_zone_annot_pos = {\n    \"paint\": (0.0, -5.5),\n    \"l_elbow\": (-15.0, 18.5),\n    \"r_elbow\": (15.0, 18.5),\n    \"top3\": (0.0, 32.0),\n    \"l_corner\": (-22.5, -5.5),\n    \"r_corner\": (22.5, -5.5),\n    \"l_wing\": (-20.0, 25.5),\n    \"r_wing\": (20.0, 25.5),\n    \"ft\": (5.5, FT_Y),\n}\n\nannot_df = pd.DataFrame(\n    [\n        {\"x\": ax, \"y\": ay, \"label\": f\"{_zone_fg[zkey]:.0%}\"}\n        for zkey, (ax, ay) in _zone_annot_pos.items()\n        if zkey in _zone_fg.index\n    ]\n)\n\n# Plot — square canvas for 1:1 court geometry (2400×2400 at dpi=400)\n_title = \"scatter-shot-chart · python · plotnine · anyplot.ai\"\n\nplot = (\n    ggplot()\n    # Paint zone tint — subtle amber for hardwood spatial context\n    + geom_rect(\n        data=paint_rect,\n        mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        fill=PAINT_FILL,\n        color=\"none\",\n        alpha=0.12,\n    )\n    # Court lines\n    + geom_path(data=court_df, mapping=aes(x=\"x\", y=\"y\", group=\"seg\"), color=COURT_COLOR, size=0.6)\n    # Shot markers — color + shape redundant encoding for CVD accessibility\n    + geom_point(data=shots_df, mapping=aes(x=\"x\", y=\"y\", color=\"outcome\", shape=\"outcome\"), size=2.0, alpha=0.70)\n    + scale_color_manual(values={\"Made\": MADE_COLOR, \"Missed\": MISSED_COLOR}, name=\"\")\n    + scale_shape_manual(values={\"Made\": \"o\", \"Missed\": \"x\"}, name=\"\")\n    # Per-zone FG% callouts via geom_label (plotnine-native background fill)\n    + geom_label(\n        data=annot_df,\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"),\n        color=INK,\n        fill=ELEVATED_BG,\n        size=3.0,\n        ha=\"center\",\n        va=\"center\",\n        alpha=0.90,\n    )\n    + coord_fixed(ratio=1, xlim=(-26.5, 26.5), ylim=(-7.5, 45.5))\n    + labs(title=_title, x=\"\", y=\"\")\n    + theme(\n        figure_size=(6, 6),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_border=element_blank(),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        plot_title=element_text(color=INK, size=13),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(color=INK_SOFT, size=8),\n        legend_title=element_blank(),\n        legend_key=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        legend_position=\"bottom\",\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6, units=\"in\")\n"}