{"spec_id":"scatter-shot-chart","library":"echarts","language":"javascript","code":"// anyplot.ai\n// scatter-shot-chart: Basketball Shot Chart\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-21\n//# anyplot-orientation: square\n\n// anyplot.ai\n// scatter-shot-chart: Basketball Shot Chart\n// Library: echarts 5.5.1 | JavaScript 22\n// Quality: pending | Created: 2026-06-21\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic RNG (seeded LCG) ---\nlet _s = 42;\nfunction rnd() {\n  _s = (Math.imul(_s, 1664525) + 1013904223) >>> 0;\n  return _s / 4294967296;\n}\nfunction randn(mean, std) {\n  const u = Math.max(rnd(), 1e-10);\n  return mean + std * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * rnd());\n}\n\n// --- Shot data generation (realistic NBA half-court distribution) ---\n// Made = #009E73 (green, semantic \"good\"), Missed = #AE3030 (red, semantic \"miss\")\nconst shots = []; // each: [x, y, made:bool]\n\n// Zone 1: Paint / close range (< 5 ft), high FG%\nfor (let i = 0; i < 180; i++) {\n  const r = rnd() * 4.5;\n  const th = rnd() * 2 * Math.PI;\n  const x = r * Math.cos(th);\n  const y = r * Math.sin(th);\n  if (y > -4.5 && y < 10 && Math.abs(x) < 24) {\n    shots.push([x, y, rnd() < 0.62]);\n  }\n}\n\n// Zone 2: Mid-range (5–22 ft), moderate FG%\nfor (let i = 0; i < 180; i++) {\n  const x = randn(0, 11);\n  const y = randn(12, 5);\n  const d = Math.sqrt(x * x + y * y);\n  if (d > 5 && d < 22.5 && y > -4.5 && Math.abs(x) < 24) {\n    shots.push([x, y, rnd() < 0.40]);\n  }\n}\n\n// Zone 3: Three-point arc, lower FG%\nfor (let i = 0; i < 220; i++) {\n  const deg = 12 + rnd() * 156;\n  const r = randn(24.1, 1.0);\n  const a = deg * Math.PI / 180;\n  const x = r * Math.cos(a);\n  const y = r * Math.sin(a);\n  if (r > 22 && y > -4.5 && Math.abs(x) < 24.5) {\n    shots.push([x, y, rnd() < 0.36]);\n  }\n}\n\n// Zone 4: Corner threes (straight segments of 3pt line)\nfor (let i = 0; i < 60; i++) {\n  const side = i < 30 ? 1 : -1;\n  const x = randn(side * 22, 1.2);\n  const y = randn(3, 2.5);\n  if (y > -4.5 && y < 9.5 && Math.abs(x) > 20.5 && Math.abs(x) < 24.5) {\n    shots.push([x, y, rnd() < 0.38]);\n  }\n}\n\nconst madeData   = shots.filter(s => s[2]).map(s => [s[0], s[1]]);\nconst missedData = shots.filter(s => !s[2]).map(s => [s[0], s[1]]);\n\n// --- Court geometry helpers (coordinates in feet, basket at origin) ---\nfunction arc(cx, cy, r, a0Deg, a1Deg, n) {\n  const pts = [];\n  for (let i = 0; i <= n; i++) {\n    const a = (a0Deg + (a1Deg - a0Deg) * i / n) * Math.PI / 180;\n    pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);\n  }\n  return pts;\n}\n\n// Key NBA half-court constants\nconst BL   = -5;      // baseline y (basket is 5 ft from baseline)\nconst CT   = 42;      // midcourt y\nconst FT_Y = 10.9;    // free-throw line (15 ft from backboard at y=-4.1)\nconst TP_R = 23.75;   // three-point arc radius\nconst TP_X = 22;      // corner three-point x distance\nconst TP_JY = Math.sqrt(TP_R * TP_R - TP_X * TP_X); // junction y ≈ 8.95\nconst TP_A0 = Math.atan2(TP_JY, TP_X) * 180 / Math.PI;   // ≈ 22.1°\nconst TP_A1 = 180 - TP_A0;                                 // ≈ 157.9°\n\nconst cw = { color: t.inkSoft, width: 2, type: \"solid\" };\nconst cd = { color: t.inkSoft, width: 1.5, type: \"dashed\" };\n\nfunction cline(data, ls) {\n  return { type: \"line\", data, symbol: \"none\", silent: true, animation: false,\n           lineStyle: ls || cw, z: 1, tooltip: { show: false } };\n}\n\nconst courtSeries = [\n  // Court boundary\n  cline([[-25, BL], [25, BL], [25, CT], [-25, CT], [-25, BL]]),\n  // Backboard\n  cline([[-3, -4.1], [3, -4.1]]),\n  // Paint / key box (16 ft wide, from baseline to free-throw line)\n  cline([[-8, BL], [-8, FT_Y], [8, FT_Y], [8, BL]]),\n  // Free-throw circle — upper (solid, toward midcourt)\n  cline(arc(0, FT_Y, 6, 0, 180, 60)),\n  // Free-throw circle — lower (dashed, toward basket)\n  cline(arc(0, FT_Y, 6, 180, 360, 60), cd),\n  // Restricted area arc (4 ft radius, semi-circle toward midcourt)\n  cline(arc(0, 0, 4, 0, 180, 50)),\n  // Three-point corner segments\n  cline([[TP_X, BL], [TP_X, TP_JY]]),\n  cline([[-TP_X, BL], [-TP_X, TP_JY]]),\n  // Three-point arc (from right junction to left junction, over the top)\n  cline(arc(0, 0, TP_R, TP_A0, TP_A1, 100)),\n  // Basket / hoop (0.75 ft radius circle)\n  cline(arc(0, 0, 0.75, 0, 360, 48)),\n];\n\n// --- Chart ---\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// Title length: 54 chars < 67 baseline, no scaling needed\nconst titleText = \"scatter-shot-chart · javascript · echarts · anyplot.ai\";\n\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    textStyle: { color: t.ink, fontSize: 13 },\n    formatter: function (params) {\n      if (params.seriesType !== \"scatter\") return null;\n      return (\n        \"<b>\" + params.seriesName + \"</b><br/>\" +\n        \"x: \" + params.data[0].toFixed(1) + \" ft<br/>\" +\n        \"y: \" + params.data[1].toFixed(1) + \" ft\"\n      );\n    },\n  },\n\n  title: {\n    text: titleText,\n    left: \"center\",\n    top: 12,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: \"bold\" },\n  },\n\n  legend: {\n    data: [\n      { name: \"Made\",   icon: \"circle\" },\n      { name: \"Missed\", icon: \"triangle\" },\n    ],\n    bottom: 12,\n    left: \"center\",\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    itemWidth: 14,\n    itemHeight: 14,\n  },\n\n  // Equal data ranges (50 ft × 50 ft) over an approximately square grid\n  // → preserves the 1:1 court aspect ratio\n  grid: { left: \"8%\", right: \"3%\", top: 72, bottom: 55 },\n\n  xAxis: {\n    type: \"value\",\n    min: -25, max: 25,\n    axisLabel: { show: false },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    min: -5, max: 45,\n    axisLabel: { show: false },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n\n  series: [\n    ...courtSeries,\n    // Made shots — brand green (semantic: good/success)\n    {\n      name: \"Made\",\n      type: \"scatter\",\n      data: madeData,\n      z: 10,\n      symbol: \"circle\",\n      symbolSize: 9,\n      itemStyle: {\n        color: t.palette[0],   // #009E73 — brand green\n        opacity: 0.80,\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n      },\n    },\n    // Missed shots — matte red (semantic: bad/fail), triangle for redundant encoding\n    {\n      name: \"Missed\",\n      type: \"scatter\",\n      data: missedData,\n      z: 10,\n      symbol: \"triangle\",\n      symbolSize: 10,\n      symbolRotate: 180,\n      itemStyle: {\n        color: t.palette[4],   // #AE3030 — matte red\n        opacity: 0.72,\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n      },\n    },\n  ],\n});\n"}