{"spec_id":"scatter-shot-chart","library":"muix","language":"javascript","code":"// anyplot.ai\n// scatter-shot-chart: Basketball Shot Chart\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-21\n//# anyplot-orientation: square\n// anyplot.ai\n// scatter-shot-chart: Basketball Shot Chart\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-21\n\nimport * as React from \"react\";\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\nconst COURT_STROKE =\n  THEME === \"light\" ? \"rgba(26,26,23,0.40)\" : \"rgba(240,239,232,0.50)\";\n\n// --- Deterministic shot data (LCG seed=42) ------------------------------------\nfunction makeLCG(seed) {\n  let s = seed >>> 0;\n  return function () {\n    s = (Math.imul(1664525, s) + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\n\n// NBA half-court: x ∈ [-25, 25] ft, y ∈ [0, 47] ft (y=0 baseline)\nconst BASKET_Y = 5.25;\nconst THREE_PT_R = 23.75;\nconst CORNER_X = 22;\nconst CORNER_TOP_Y = BASKET_Y + Math.sqrt(THREE_PT_R ** 2 - CORNER_X ** 2);\n\nfunction generateShots() {\n  const rng = makeLCG(42);\n  const shots = [];\n\n  for (let i = 0; i < 300; i++) {\n    const zone = rng();\n    let x, y, shot_type;\n\n    if (zone < 0.20) {\n      // At the rim / paint close range\n      const angle = rng() * Math.PI;\n      const dist = rng() * 4 + 0.5;\n      x = Math.cos(angle) * dist;\n      y = BASKET_Y + Math.abs(Math.sin(angle)) * dist;\n      shot_type = \"2-pointer\";\n    } else if (zone < 0.25) {\n      // Left corner three\n      x = -(rng() * 2.5 + 21.5);\n      y = rng() * 10 + 0.5;\n      shot_type = \"3-pointer\";\n    } else if (zone < 0.30) {\n      // Right corner three\n      x = rng() * 2.5 + 21.5;\n      y = rng() * 10 + 0.5;\n      shot_type = \"3-pointer\";\n    } else if (zone < 0.58) {\n      // Mid-range (inside three-point line)\n      const angle = rng() * Math.PI;\n      const dist = rng() * 8 + 9;\n      x = Math.cos(angle) * dist;\n      y = BASKET_Y + Math.sin(angle) * dist;\n      if (y < 0.5) y = 0.5 + rng() * 2;\n      const d = Math.sqrt(x ** 2 + (y - BASKET_Y) ** 2);\n      if (d > 22) {\n        x = (x / d) * 21.5;\n        y = BASKET_Y + ((y - BASKET_Y) / d) * 21.5;\n      }\n      shot_type = \"2-pointer\";\n    } else if (zone < 0.85) {\n      // Arc three-pointers\n      const angle = rng() * Math.PI;\n      const dist = rng() * 4.5 + THREE_PT_R;\n      x = Math.cos(angle) * dist;\n      y = BASKET_Y + Math.sin(angle) * dist;\n      if (y < 0.5) y = 0.5 + rng() * 2;\n      if (y > 44) y = 44;\n      x = Math.max(-24.5, Math.min(24.5, x));\n      shot_type = \"3-pointer\";\n    } else {\n      // Free throw (~15% of attempts, at the free-throw line ~19 ft from baseline)\n      x = rng() * 0.8 - 0.4;\n      y = 19 + rng() * 0.6 - 0.3;\n      shot_type = \"free-throw\";\n    }\n\n    const d = Math.sqrt(x ** 2 + (y - BASKET_Y) ** 2);\n    const makePct =\n      shot_type === \"free-throw\"\n        ? 0.78\n        : shot_type === \"3-pointer\"\n        ? 0.37\n        : d < 7\n        ? 0.60\n        : 0.41;\n    const made = rng() < makePct;\n\n    shots.push({\n      id: i,\n      x: parseFloat(x.toFixed(2)),\n      y: parseFloat(y.toFixed(2)),\n      made,\n      shot_type,\n    });\n  }\n  return shots;\n}\n\nconst allShots = generateShots();\nconst madeShots = allShots.filter((s) => s.made).map((s) => ({ id: s.id, x: s.x, y: s.y }));\nconst missedShots = allShots.filter((s) => !s.made).map((s) => ({ id: s.id, x: s.x, y: s.y }));\n\nconst X_MIN = -25, X_MAX = 25, Y_MIN = 0, Y_MAX = 47;\n\n// Marker radius varies by shot type to encode the third data dimension\nfunction markerRadius(shot_type) {\n  if (shot_type === \"free-throw\") return 4.5;\n  if (shot_type === \"3-pointer\") return 6.5;\n  return 5.5; // 2-pointer\n}\n\n// --- Custom shot markers: ○ circles for made, × marks for missed -------------\n// Shape encoding provides color-blind-safe redundancy for made/missed outcome\nfunction ShotMarkers() {\n  const { left, top, width, height } = useDrawingArea();\n  const scX = (ft) => left + ((ft - X_MIN) / (X_MAX - X_MIN)) * width;\n  const scY = (ft) => top + ((Y_MAX - ft) / (Y_MAX - Y_MIN)) * height;\n\n  return (\n    <g>\n      {/* Missed shots: × marks in red (rendered first, under made shots) */}\n      {allShots\n        .filter((s) => !s.made)\n        .map((shot) => {\n          const sx = scX(shot.x);\n          const sy = scY(shot.y);\n          const r = markerRadius(shot.shot_type);\n          return (\n            <path\n              key={shot.id}\n              d={`M ${sx - r} ${sy - r} L ${sx + r} ${sy + r} M ${sx + r} ${sy - r} L ${sx - r} ${sy + r}`}\n              stroke=\"#AE3030\"\n              strokeWidth={r * 0.55}\n              strokeOpacity={0.7}\n              strokeLinecap=\"round\"\n            />\n          );\n        })}\n      {/* Made shots: filled circles in green */}\n      {allShots\n        .filter((s) => s.made)\n        .map((shot) => {\n          const sx = scX(shot.x);\n          const sy = scY(shot.y);\n          const r = markerRadius(shot.shot_type);\n          return (\n            <circle\n              key={shot.id}\n              cx={sx}\n              cy={sy}\n              r={r}\n              fill=\"#009E73\"\n              fillOpacity={0.7}\n            />\n          );\n        })}\n    </g>\n  );\n}\n\n// --- Basketball court SVG overlay (accurate NBA half-court dimensions) --------\nfunction BasketballCourt() {\n  const { left, top, width, height } = useDrawingArea();\n  const cx = (ft) => left + ((ft - X_MIN) / (X_MAX - X_MIN)) * width;\n  const cy = (ft) => top + ((Y_MAX - ft) / (Y_MAX - Y_MIN)) * height;\n\n  const threeR_x = (THREE_PT_R / (X_MAX - X_MIN)) * width;\n  const threeR_y = (THREE_PT_R / (Y_MAX - Y_MIN)) * height;\n  const ftCircR = (6 / (X_MAX - X_MIN)) * width;\n  const raR_x = (4 / (X_MAX - X_MIN)) * width;\n  const raR_y = (4 / (Y_MAX - Y_MIN)) * height;\n  const basketR = (0.75 / (X_MAX - X_MIN)) * width;\n\n  const ls = {\n    fill: \"none\",\n    stroke: COURT_STROKE,\n    strokeWidth: 2,\n    strokeLinecap: \"round\" as const,\n    strokeLinejoin: \"round\" as const,\n  };\n\n  return (\n    <g>\n      {/* Court boundary (baseline + sidelines + half-court line) */}\n      <rect\n        x={cx(-25)}\n        y={cy(47)}\n        width={cx(25) - cx(-25)}\n        height={cy(0) - cy(47)}\n        style={{ ...ls, strokeWidth: 2.5 }}\n      />\n      {/* Paint / key area */}\n      <rect\n        x={cx(-8)}\n        y={cy(19)}\n        width={cx(8) - cx(-8)}\n        height={cy(0) - cy(19)}\n        style={ls}\n      />\n      {/* Free throw circle */}\n      <ellipse\n        cx={cx(0)}\n        cy={cy(19)}\n        rx={ftCircR}\n        ry={(6 / (Y_MAX - Y_MIN)) * height}\n        style={ls}\n      />\n      {/* Restricted area arc (semicircle above baseline) */}\n      <path\n        d={`M ${cx(-4)} ${cy(BASKET_Y)} A ${raR_x} ${raR_y} 0 0 1 ${cx(4)} ${cy(BASKET_Y)}`}\n        style={ls}\n      />\n      {/* Corner three-point straight lines */}\n      <line\n        x1={cx(-CORNER_X)}\n        y1={cy(0)}\n        x2={cx(-CORNER_X)}\n        y2={cy(CORNER_TOP_Y)}\n        style={ls}\n      />\n      <line\n        x1={cx(CORNER_X)}\n        y1={cy(0)}\n        x2={cx(CORNER_X)}\n        y2={cy(CORNER_TOP_Y)}\n        style={ls}\n      />\n      {/* Three-point arc (large-arc=0, sweep=1 → curves toward half-court) */}\n      <path\n        d={`M ${cx(-CORNER_X)} ${cy(CORNER_TOP_Y)} A ${threeR_x} ${threeR_y} 0 0 1 ${cx(CORNER_X)} ${cy(CORNER_TOP_Y)}`}\n        style={ls}\n      />\n      {/* Backboard */}\n      <line\n        x1={cx(-3)}\n        y1={cy(4)}\n        x2={cx(3)}\n        y2={cy(4)}\n        style={{ ...ls, strokeWidth: 4 }}\n      />\n      {/* Basket rim */}\n      <ellipse\n        cx={cx(0)}\n        cy={cy(BASKET_Y)}\n        rx={basketR}\n        ry={(0.75 / (Y_MAX - Y_MIN)) * height}\n        style={{ ...ls, strokeWidth: 2.5 }}\n      />\n    </g>\n  );\n}\n\n// --- Chart title as SVG text above the drawing area --------------------------\nfunction ChartTitle() {\n  const { left, top, width } = useDrawingArea();\n  return (\n    <text\n      x={left + width / 2}\n      y={top - 24}\n      textAnchor=\"middle\"\n      style={{\n        fontSize: 22,\n        fontWeight: 600,\n        fontFamily: \"Inter, system-ui, -apple-system, sans-serif\",\n        fill: t.ink,\n      }}\n    >\n      scatter-shot-chart · javascript · muix · anyplot.ai\n    </text>\n  );\n}\n\n// --- Main chart component ----------------------------------------------------\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n\n  return (\n    <ChartContainer\n      width={W}\n      height={H}\n      skipAnimation\n      series={[\n        {\n          type: \"scatter\",\n          id: \"missed\",\n          label: \"Missed\",\n          data: missedShots,\n          color: \"#AE3030\",\n        },\n        {\n          type: \"scatter\",\n          id: \"made\",\n          label: \"Made\",\n          data: madeShots,\n          color: \"#009E73\",\n        },\n      ]}\n      xAxis={[{ min: X_MIN, max: X_MAX }]}\n      yAxis={[{ min: Y_MIN, max: Y_MAX }]}\n      margin={{ top: 70, bottom: 80, left: 60, right: 60 }}\n    >\n      <BasketballCourt />\n      <ShotMarkers />\n      <ChartsLegend\n        position={{ vertical: \"bottom\", horizontal: \"middle\" }}\n        slotProps={{\n          legend: {\n            direction: \"row\",\n            itemMarkWidth: 20,\n            itemMarkHeight: 20,\n            padding: 12,\n            labelStyle: {\n              fontSize: 20,\n              fill: t.ink,\n            },\n          },\n        }}\n      />\n      <ChartTitle />\n    </ChartContainer>\n  );\n}\n"}