{"spec_id":"swarm-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// swarm-basic: Basic Swarm Plot\n// Library: muix 7.29.1 | JavaScript 22.23.1\n// Quality: 90/100 | Created: 2026-07-26\n//# anyplot-orientation: landscape\n// anyplot.ai\n// swarm-basic: Basic Swarm Plot\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-07-26\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + Box-Muller for approx-normal samples --------\nlet seed = 42;\nfunction nextUniform() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction nextNormal(mean, stdDev) {\n  const u1 = Math.max(nextUniform(), 1e-9);\n  const u2 = nextUniform();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * stdDev;\n}\n\n// --- Data: quarterly performance review scores by department ---------------\nconst DEPARTMENTS = [\"Engineering\", \"Sales\", \"Marketing\", \"Support\"];\nconst MEANS = [78, 70, 74, 83];\nconst STD_DEVS = [8, 12, 9, 6];\nconst POINTS_PER_DEPT = 40;\n\nconst rawPoints = DEPARTMENTS.flatMap((department, categoryIndex) =>\n  Array.from({ length: POINTS_PER_DEPT }, () => ({\n    department,\n    categoryIndex,\n    score: Math.min(99, Math.max(45, nextNormal(MEANS[categoryIndex], STD_DEVS[categoryIndex]))),\n  })),\n);\n\nconst median = (values) => {\n  const sorted = [...values].sort((a, b) => a - b);\n  const mid = Math.floor(sorted.length / 2);\n  return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];\n};\n\nconst medianByDept = DEPARTMENTS.map((_, i) =>\n  median(rawPoints.filter((p) => p.categoryIndex === i).map((p) => p.score)),\n);\n\n// --- Layout geometry — must stay in sync with the ChartContainer margin ----\nconst MARGIN = { left: 110, right: 60, top: 80, bottom: 100 };\nconst SCORE_MIN = 40;\nconst SCORE_MAX = 100;\nconst X_MIN = -0.6;\nconst X_MAX = DEPARTMENTS.length - 1 + 0.6;\nconst MARKER_SIZE = 6; // circle radius, px\nconst MARKER_DIAMETER_PX = MARKER_SIZE * 2 + 1;\n\n// --- Beeswarm packing: spread points horizontally within each department so\n// none overlap. Collisions are resolved in on-screen pixels (rather than data\n// units) so the spread looks even regardless of the score axis' range. Each\n// point keeps its true score on the y-axis; only the x-offset is adjusted.\nfunction layoutSwarm(plotWidthPx, plotHeightPx) {\n  const pxPerScore = plotHeightPx / (SCORE_MAX - SCORE_MIN);\n  const pxPerX = plotWidthPx / (X_MAX - X_MIN);\n\n  return DEPARTMENTS.flatMap((department, categoryIndex) => {\n    const points = rawPoints\n      .filter((p) => p.categoryIndex === categoryIndex)\n      .sort((a, b) => a.score - b.score);\n\n    const placed = [];\n    points.forEach((point) => {\n      const nearby = placed.filter(\n        (p) => Math.abs((point.score - p.score) * pxPerScore) < MARKER_DIAMETER_PX,\n      );\n      let offsetPx = 0;\n      if (nearby.length > 0) {\n        const step = MARKER_DIAMETER_PX * 0.92;\n        let k = 0;\n        let resolved = false;\n        while (!resolved && k < 200) {\n          const candidate = k === 0 ? 0 : (k % 2 === 1 ? Math.ceil(k / 2) : -Math.ceil(k / 2)) * step;\n          if (\n            nearby.every(\n              (p) => Math.hypot(candidate - p.offsetPx, (point.score - p.score) * pxPerScore) >= MARKER_DIAMETER_PX * 0.95,\n            )\n          ) {\n            offsetPx = candidate;\n            resolved = true;\n          }\n          k += 1;\n        }\n      }\n      placed.push({ ...point, offsetPx });\n    });\n\n    return placed.map((p) => ({\n      id: `${department}-${p.score.toFixed(3)}-${p.offsetPx.toFixed(2)}`,\n      x: categoryIndex + p.offsetPx / pxPerX,\n      y: p.score,\n    }));\n  });\n}\n\n// Short reference ticks at each department's median score. Rendered inside\n// ChartContainer so it can read the live D3 scales.\nfunction MedianTicks() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const halfWidth = 0.34;\n\n  return (\n    <g>\n      {DEPARTMENTS.map((department, i) => {\n        const x1 = xScale(i - halfWidth) ?? 0;\n        const x2 = xScale(i + halfWidth) ?? 0;\n        const y = yScale(medianByDept[i]) ?? 0;\n        return (\n          <line\n            key={department}\n            x1={x1}\n            y1={y}\n            x2={x2}\n            y2={y}\n            stroke={t.ink}\n            strokeWidth={2.5}\n            strokeOpacity={0.75}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\nconst TITLE = \"swarm-basic · javascript · muix · anyplot.ai\";\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const plotWidthPx = W - MARGIN.left - MARGIN.right;\n  const plotHeightPx = H - MARGIN.top - MARGIN.bottom;\n  const scoreData = layoutSwarm(plotWidthPx, plotHeightPx);\n\n  return (\n    <ChartContainer\n      width={W}\n      height={H}\n      skipAnimation\n      series={[\n        {\n          type: \"scatter\",\n          id: \"scores\",\n          label: \"Performance Score\",\n          data: scoreData,\n          color: t.palette[0],\n          markerSize: MARKER_SIZE,\n        },\n      ]}\n      xAxis={[\n        {\n          id: \"xAxis\",\n          min: X_MIN,\n          max: X_MAX,\n          tickMinStep: 1,\n          valueFormatter: (v) => DEPARTMENTS[Math.round(v)] ?? \"\",\n          tickLabelStyle: { fontSize: 15, fill: t.inkSoft },\n          label: \"Department\",\n          labelStyle: { fontSize: 16, fill: t.ink },\n        },\n      ]}\n      yAxis={[\n        {\n          id: \"yAxis\",\n          min: SCORE_MIN,\n          max: SCORE_MAX,\n          label: \"Performance Score\",\n          tickLabelStyle: { fontSize: 15, fill: t.inkSoft },\n          labelStyle: { fontSize: 16, fill: t.ink },\n        },\n      ]}\n      margin={MARGIN}\n    >\n      <ChartsGrid horizontal />\n      <ScatterPlot />\n      <MedianTicks />\n      <ChartsXAxis axisId=\"xAxis\" />\n      <ChartsYAxis axisId=\"yAxis\" />\n      <text x={W / 2} y={42} textAnchor=\"middle\" fontSize={22} fontFamily=\"sans-serif\" fontWeight=\"500\" fill={t.ink}>\n        {TITLE}\n      </text>\n    </ChartContainer>\n  );\n}\n"}