{"spec_id":"swarm-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// swarm-basic: Basic Swarm Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 92/100 | Created: 2026-07-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Reaction times (ms) across a psychology experiment: 4 conditions, 40 obs each.\nfunction makeRng(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (1664525 * state + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nfunction randNormal(rng, mean, sd) {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * sd;\n}\n\nconst rng = makeRng(42);\nconst categories = [\"Control\", \"Caffeine\", \"Sleep-Deprived\", \"Exercise\"];\nconst conditionStats = [\n  { mean: 340, sd: 35 },\n  { mean: 285, sd: 28 },\n  { mean: 415, sd: 55 },\n  { mean: 305, sd: 38 },\n];\nconst observationsPerGroup = 40;\n\nconst valuesByCategory = conditionStats.map(({ mean, sd }) =>\n  Array.from({ length: observationsPerGroup }, () =>\n    Math.max(150, Math.round(randNormal(rng, mean, sd) * 10) / 10),\n  ),\n);\n\n// --- Swarm layout: bin by value, spread symmetrically within each bin -------\n// binCount adapts to each group's own spread (finer bins for wider-spread groups)\n// so a group with more within-bin crowding still resolves into distinct points\n// instead of a dense clump, without touching marker size (kept uniform per spec).\nfunction computeSwarmOffsets(values, targetPointsPerBin, step) {\n  const min = Math.min(...values);\n  const max = Math.max(...values);\n  const binCount = Math.max(8, Math.round(values.length / targetPointsPerBin));\n  const binSize = (max - min) / binCount || 1;\n  const binCounts = new Array(binCount).fill(0);\n  const sortedIdx = values.map((_, i) => i).sort((a, b) => values[a] - values[b]);\n  const offsets = new Array(values.length).fill(0);\n  for (const i of sortedIdx) {\n    const binIdx = Math.min(binCount - 1, Math.floor((values[i] - min) / binSize));\n    const count = binCounts[binIdx];\n    const side = count % 2 === 0 ? 1 : -1;\n    const rank = Math.ceil(count / 2);\n    offsets[i] = side * rank * step;\n    binCounts[binIdx] = count + 1;\n  }\n  return offsets;\n}\n\nconst HIGHLIGHT_CATEGORY_IDX = categories.indexOf(\"Sleep-Deprived\");\n\nconst swarmDatasets = categories.map((category, catIdx) => {\n  const values = valuesByCategory[catIdx];\n  const offsets = computeSwarmOffsets(values, 2.5, 0.05);\n  return {\n    type: \"scatter\",\n    label: category,\n    data: values.map((v, i) => ({ x: catIdx + offsets[i], y: v })),\n    backgroundColor: t.palette[catIdx % t.palette.length],\n    borderColor: t.pageBg,\n    borderWidth: 1.5,\n    pointRadius: 5,\n    pointHoverRadius: 5,\n    order: 2,\n  };\n});\n\nfunction 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 medianPoints = [];\ncategories.forEach((_, catIdx) => {\n  const med = median(valuesByCategory[catIdx]);\n  medianPoints.push({ x: catIdx - 0.32, y: med });\n  medianPoints.push({ x: catIdx + 0.32, y: med });\n  medianPoints.push({ x: catIdx, y: null });\n});\n\n// Median line is a single dataset, but its per-segment color/width is driven by\n// Chart.js's `segment` styling API so the standout Sleep-Deprived condition\n// (markedly slower and more variable reaction times) reads as the visual focal\n// point, without varying the swarm point size the spec asks to keep consistent.\nconst medianDataset = {\n  type: \"line\",\n  label: \"Median\",\n  data: medianPoints,\n  borderColor: t.ink,\n  borderWidth: 3,\n  pointRadius: 0,\n  spanGaps: false,\n  order: 1,\n  segment: {\n    borderColor: (ctx) =>\n      Math.floor(ctx.p0DataIndex / 3) === HIGHLIGHT_CATEGORY_IDX ? t.amber : t.ink,\n    borderWidth: (ctx) => (Math.floor(ctx.p0DataIndex / 3) === HIGHLIGHT_CATEGORY_IDX ? 5 : 3),\n  },\n};\n\n// Custom plugin (native Chart.js plugin-core API, not a community plugin): draws\n// a subtle backdrop band behind the standout condition so it reads as the focal\n// point at a glance, before any dataset is drawn.\nconst swarmHighlightPlugin = {\n  id: \"swarmHighlight\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    if (!chartArea) return;\n    const left = scales.x.getPixelForValue(HIGHLIGHT_CATEGORY_IDX - 0.46);\n    const right = scales.x.getPixelForValue(HIGHLIGHT_CATEGORY_IDX + 0.46);\n    ctx.save();\n    ctx.fillStyle = `${t.amber}1f`;\n    ctx.fillRect(left, chartArea.top, right - left, chartArea.bottom - chartArea.top);\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: { datasets: [...swarmDatasets, medianDataset] },\n  plugins: [swarmHighlightPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"swarm-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        position: \"top\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          filter: (item) => item.text !== \"Median\",\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: -0.6,\n        max: categories.length - 1 + 0.6,\n        afterBuildTicks: (axis) => {\n          axis.ticks = categories.map((_, i) => ({ value: i }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => categories[value] ?? \"\",\n        },\n        grid: { display: false },\n        title: { display: true, text: \"Experimental Condition\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Reaction Time (ms)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}