{"spec_id":"scatter-shot-chart","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-shot-chart: Basketball Shot Chart\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 87/100 | Created: 2026-06-21\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Seeded LCG for deterministic data ------------------------------------\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) >>> 0;\n  return seed / 0x100000000;\n}\nfunction randn(mean, std) {\n  const u1 = Math.max(lcg(), 1e-10);\n  const u2 = lcg();\n  return mean + std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- NBA Half-Court Geometry (feet) ---------------------------------------\n// Basket at (BX, BY); court: x in [-25, 25], y in [0, 47]\nconst BX = 0, BY = 4;\nconst C3X = 22;\nconst TP_R = 23.75;\nconst FT_Y = BY + 15;\nconst C3Y = BY + Math.sqrt(TP_R * TP_R - C3X * C3X); // ~12.95\n\nfunction arcPts(cx, cy, r, a0, a1, n) {\n  const steps = n || 80;\n  const pts = [];\n  for (let i = 0; i <= steps; i++) {\n    const a = a0 + (a1 - a0) * (i / steps);\n    pts.push({ x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) });\n  }\n  return pts;\n}\n\n// Three-point arc: left corner → top → right corner (decreasing angle)\nconst tpA0 = Math.atan2(C3Y - BY, -C3X);\nconst tpA1 = Math.atan2(C3Y - BY, C3X);\n\nfunction courtLine(data, dash) {\n  const ds = {\n    type: 'line',\n    data: data,\n    borderColor: t.inkSoft,\n    borderWidth: 2,\n    backgroundColor: 'transparent',\n    fill: false,\n    pointRadius: 0,\n    pointHoverRadius: 0,\n    tension: 0,\n    showLine: true,\n    order: 0,\n  };\n  if (dash) ds.borderDash = dash;\n  return ds;\n}\n\nconst courtDatasets = [\n  // Outer boundary\n  courtLine([{x:-25,y:0},{x:25,y:0},{x:25,y:47},{x:-25,y:47},{x:-25,y:0}]),\n  // Paint left/right\n  courtLine([{x:-8,y:0},{x:-8,y:FT_Y}]),\n  courtLine([{x:8,y:0},{x:8,y:FT_Y}]),\n  // Free-throw line\n  courtLine([{x:-8,y:FT_Y},{x:8,y:FT_Y}]),\n  // FT circle top — solid, faces court\n  courtLine(arcPts(BX, FT_Y, 6, 0, Math.PI)),\n  // FT circle bottom — dashed, inside key\n  courtLine(arcPts(BX, FT_Y, 6, Math.PI, 2 * Math.PI), [5, 5]),\n  // Three-point corner straights\n  courtLine([{x:-C3X,y:0},{x:-C3X,y:C3Y}]),\n  courtLine([{x:C3X,y:0},{x:C3X,y:C3Y}]),\n  // Three-point arc\n  courtLine(arcPts(BX, BY, TP_R, tpA0, tpA1)),\n  // Restricted area arc\n  courtLine(arcPts(BX, BY, 4, 0, Math.PI)),\n  // Basket ring\n  courtLine(arcPts(BX, BY, 0.75, 0, 2 * Math.PI, 40)),\n  // Backboard\n  courtLine([{x:-3,y:0.5},{x:3,y:0.5}]),\n];\n\n// --- Shot Data (350 attempts) ---------------------------------------------\n// Marker radius encodes shot type: free-throw=4, 2-pointer=5.5, 3-pointer=7\nconst TYPE_RADIUS = { 'free-throw': 4, '2-pointer': 5.5, '3-pointer': 7 };\n\nfunction genShots(n) {\n  const shots = [];\n  for (let i = 0; i < n; i++) {\n    const z = lcg();\n    let x, y, made, shot_type;\n    if (z < 0.07) {\n      // Free throw — clustered at FT line\n      x = BX + randn(0, 0.25);\n      y = FT_Y + randn(0, 0.25);\n      made = lcg() < 0.78;\n      shot_type = 'free-throw';\n    } else if (z < 0.30) {\n      // At rim / paint → 2-pointer\n      const d = lcg() * 7, a = lcg() * 2 * Math.PI;\n      x = BX + d * Math.cos(a);\n      y = Math.max(0.3, BY + d * Math.sin(a));\n      made = lcg() < 0.63;\n      shot_type = '2-pointer';\n    } else if (z < 0.40) {\n      // Left corner 3\n      x = -(C3X + lcg() * 2.5);\n      y = 0.5 + lcg() * 10;\n      made = lcg() < 0.39;\n      shot_type = '3-pointer';\n    } else if (z < 0.50) {\n      // Right corner 3\n      x = C3X + lcg() * 2.5;\n      y = 0.5 + lcg() * 10;\n      made = lcg() < 0.39;\n      shot_type = '3-pointer';\n    } else if (z < 0.63) {\n      // Above-the-break 3\n      const a = Math.PI * 0.10 + lcg() * Math.PI * 0.80;\n      const d = TP_R + randn(0, 0.8);\n      x = Math.max(-24.5, Math.min(24.5, BX + d * Math.cos(a)));\n      y = Math.max(0.3, Math.min(32, BY + d * Math.sin(a)));\n      made = lcg() < 0.35;\n      shot_type = '3-pointer';\n    } else {\n      // Mid-range → 2-pointer\n      let tries = 0, x2, y2;\n      do {\n        const a = Math.PI * 0.08 + lcg() * Math.PI * 0.84;\n        const d = 9 + lcg() * 12;\n        x2 = BX + d * Math.cos(a);\n        y2 = BY + d * Math.sin(a);\n        tries++;\n      } while (tries < 20 && (Math.abs(x2) < 8.5 && y2 < FT_Y + 1));\n      x = Math.max(-24.5, Math.min(24.5, x2));\n      y = Math.max(0.3, Math.min(32, y2));\n      made = lcg() < 0.43;\n      shot_type = '2-pointer';\n    }\n    shots.push({ x: x, y: y, made: made, shot_type: shot_type });\n  }\n  return shots;\n}\n\nconst shots = genShots(350);\nconst madeShots    = shots.filter(function(s) { return  s.made; });\nconst missedShots  = shots.filter(function(s) { return !s.made; });\nconst madeData     = madeShots.map(function(s) { return { x: s.x, y: s.y }; });\nconst missedData   = missedShots.map(function(s) { return { x: s.x, y: s.y }; });\nconst madeRadius   = madeShots.map(function(s) { return TYPE_RADIUS[s.shot_type]; });\nconst missedRadius = missedShots.map(function(s) { return TYPE_RADIUS[s.shot_type]; });\n\n// --- Mount ----------------------------------------------------------------\nconst canvas = document.createElement('canvas');\ndocument.getElementById('container').appendChild(canvas);\n\n// --- Chart ----------------------------------------------------------------\nnew Chart(canvas, {\n  type: 'scatter',\n  data: {\n    datasets: courtDatasets.concat([\n      {\n        label: 'Made',\n        data: madeData,\n        backgroundColor: t.palette[0] + 'cc',\n        borderColor: t.palette[0],\n        borderWidth: 1,\n        pointRadius: madeRadius,\n        pointHoverRadius: madeRadius,\n        pointStyle: 'circle',\n        order: 1,\n      },\n      {\n        label: 'Missed',\n        data: missedData,\n        backgroundColor: 'transparent',\n        borderColor: t.palette[4],\n        borderWidth: 2,\n        pointRadius: missedRadius,\n        pointHoverRadius: missedRadius,\n        pointStyle: 'crossRot',\n        order: 2,\n      },\n    ]),\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: 'scatter-shot-chart · javascript · chartjs · anyplot.ai',\n        color: t.ink,\n        font: { size: 22, weight: '500' },\n        padding: { top: 12, bottom: 8 },\n      },\n      legend: {\n        display: true,\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          usePointStyle: true,\n          filter: function(item) { return item.datasetIndex >= courtDatasets.length; },\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: 'linear',\n        min: -27,\n        max: 27,\n        display: false,\n        grid: { display: false },\n      },\n      y: {\n        type: 'linear',\n        min: -2,\n        max: 34,\n        display: false,\n        grid: { display: false },\n      },\n    },\n    layout: {\n      padding: { top: 10, bottom: 20, left: 20, right: 20 },\n    },\n  },\n  plugins: [{\n    id: 'background',\n    beforeDraw: function(chart) {\n      const ctx = chart.ctx;\n      ctx.save();\n      ctx.fillStyle = t.pageBg;\n      ctx.fillRect(0, 0, chart.width, chart.height);\n      ctx.restore();\n    },\n  }],\n});\n"}