{"spec_id":"violin-swarm","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// violin-swarm: Violin Plot with Overlaid Swarm Points\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Reaction times (ms) across 4 experimental conditions, 45 trials each.\nfunction mulberry32(seed) {\n  let a = seed;\n  return function () {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let z = Math.imul(a ^ (a >>> 15), 1 | a);\n    z = (z + Math.imul(z ^ (z >>> 7), 61 | z)) ^ z;\n    return ((z ^ (z >>> 14)) >>> 0) / 4294967296;\n  };\n}\nconst rand = mulberry32(42);\n\nfunction randNormal(mean, std) {\n  const u1 = Math.max(rand(), 1e-12);\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\nconst CATEGORY_NAMES = [\"Placebo\", \"Low-Dose Caffeine\", \"High-Dose Caffeine\", \"Sleep-Deprived\"];\nconst MEANS = [420, 380, 350, 480];\nconst STDS = [55, 45, 40, 65];\nconst N_TRIALS = 45;\n\nconst rawValues = CATEGORY_NAMES.map((_, i) =>\n  Array.from({ length: N_TRIALS }, () => Math.max(150, randNormal(MEANS[i], STDS[i]))),\n);\n\n// --- Geometry: kernel density estimate -> violin outline + swarm jitter ----\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\nfunction stdDev(values) {\n  const mean = values.reduce((a, b) => a + b, 0) / values.length;\n  const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;\n  return Math.sqrt(variance);\n}\n\nfunction silvermanBandwidth(values) {\n  return 1.06 * stdDev(values) * Math.pow(values.length, -0.2);\n}\n\nfunction kdeAt(values, bandwidth, y) {\n  let sum = 0;\n  for (const v of values) {\n    const u = (y - v) / bandwidth;\n    sum += Math.exp(-0.5 * u * u);\n  }\n  return sum / (values.length * bandwidth * Math.sqrt(2 * Math.PI));\n}\n\nconst allValues = rawValues.flat();\nconst dataMin = Math.min(...allValues);\nconst dataMax = Math.max(...allValues);\nconst pad = (dataMax - dataMin) * 0.08;\nconst domainMin = dataMin - pad;\nconst domainMax = dataMax + pad;\n\nconst MAX_HALF_WIDTH = 0.4; // violin half-width in x-axis units (category spacing = 1)\nconst GRID_POINTS = 60;\nconst KDE_RANGE_SIGMAS = 3; // how far past the min/max observation the outline extends\n\n// Each category gets its own local y-grid (clamped to the shared axis domain) so\n// the outline tapers to a point near its own data instead of trailing a thin\n// constant-width spike across the full shared axis range.\nconst categories = CATEGORY_NAMES.map((name, i) => {\n  const values = rawValues[i];\n  const bandwidth = silvermanBandwidth(values);\n  const localMin = Math.max(domainMin, Math.min(...values) - KDE_RANGE_SIGMAS * bandwidth);\n  const localMax = Math.min(domainMax, Math.max(...values) + KDE_RANGE_SIGMAS * bandwidth);\n  const yGrid = Array.from(\n    { length: GRID_POINTS },\n    (_, gi) => localMin + ((localMax - localMin) * gi) / (GRID_POINTS - 1),\n  );\n  const densities = yGrid.map((y) => kdeAt(values, bandwidth, y));\n  const maxDensity = Math.max(...densities);\n  const halfWidths = densities.map((d) => (MAX_HALF_WIDTH * d) / maxDensity);\n  halfWidths[0] = 0;\n  halfWidths[halfWidths.length - 1] = 0;\n  return { name, values, bandwidth, maxDensity, yGrid, halfWidths };\n});\n\nfunction halfWidthAt(category, y) {\n  const d = kdeAt(category.values, category.bandwidth, y);\n  return Math.max(0.015, (MAX_HALF_WIDTH * d) / category.maxDensity);\n}\n\n// Beeswarm-style jitter: bin observations along y, spread each bin outward\n// from the center, clipped so points never leave the violin boundary.\nfunction computeSwarm(category, center) {\n  const binCount = 32;\n  const binWidth = (domainMax - domainMin) / binCount;\n  const bins = Array.from({ length: binCount }, () => []);\n  category.values.forEach((v) => {\n    const b = Math.min(binCount - 1, Math.max(0, Math.floor((v - domainMin) / binWidth)));\n    bins[b].push(v);\n  });\n\n  const spacing = 0.06;\n  const points = [];\n  bins.forEach((bin) => {\n    bin.sort((a, b) => a - b);\n    bin.forEach((v, j) => {\n      const step = Math.ceil(j / 2);\n      const sign = j % 2 === 0 ? 1 : -1;\n      let offset = j === 0 ? 0 : sign * step * spacing;\n      const maxOffset = halfWidthAt(category, v) * 0.9;\n      offset = Math.max(-maxOffset, Math.min(maxOffset, offset));\n      points.push({ x: center + offset, y: v });\n    });\n  });\n  return points;\n}\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\n// Each category contributes 3 datasets: an invisible-fill \"left\" boundary line,\n// a \"right\" boundary line that fills back to it (drawing the violin), and a\n// scatter dataset of jittered raw observations on top.\nconst datasets = [];\ncategories.forEach((category, i) => {\n  const center = i + 1;\n  const color = t.palette[i % t.palette.length];\n  const fillColor = `${color}66`; // ~40% alpha — keeps swarm points visible\n\n  const leftPoints = category.halfWidths.map((hw, gi) => ({ x: center - hw, y: category.yGrid[gi] }));\n  const rightPoints = category.halfWidths.map((hw, gi) => ({ x: center + hw, y: category.yGrid[gi] }));\n\n  datasets.push({\n    type: \"line\",\n    label: `${category.name} (left edge)`,\n    data: leftPoints,\n    borderColor: color,\n    borderWidth: 1.5,\n    pointRadius: 0,\n    fill: false,\n    tension: 0.2,\n  });\n  datasets.push({\n    type: \"line\",\n    label: category.name,\n    data: rightPoints,\n    borderColor: color,\n    borderWidth: 1.5,\n    backgroundColor: fillColor,\n    pointRadius: 0,\n    fill: \"-1\",\n    tension: 0.2,\n  });\n  datasets.push({\n    type: \"scatter\",\n    label: `${category.name} (observations)`,\n    data: computeSwarm(category, center),\n    backgroundColor: color,\n    borderColor: t.pageBg,\n    borderWidth: 1,\n    pointRadius: 4.5,\n    pointHoverRadius: 6.5,\n  });\n\n  // Secondary emphasis device: a short bold tick marking the median, so the\n  // central tendency of each condition reads at a glance alongside the raw spread.\n  const medianTickWidth = MAX_HALF_WIDTH * 0.55;\n  datasets.push({\n    type: \"line\",\n    label: `${category.name} (median)`,\n    data: [\n      { x: center - medianTickWidth, y: median(category.values) },\n      { x: center + medianTickWidth, y: median(category.values) },\n    ],\n    borderColor: t.ink,\n    borderWidth: 3,\n    pointRadius: 0,\n    fill: false,\n    tension: 0,\n  });\n});\n\nnew Chart(canvas, {\n  type: \"line\",\n  data: { datasets },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"violin-swarm · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: { display: false },\n      tooltip: {\n        filter: (item) => item.dataset.type === \"scatter\",\n        callbacks: {\n          title: (items) => CATEGORY_NAMES[Math.round(items[0].parsed.x) - 1] ?? \"\",\n          label: (item) => `${Math.round(item.parsed.y)} ms`,\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0.5,\n        max: CATEGORY_NAMES.length + 0.5,\n        ticks: {\n          stepSize: 1,\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => CATEGORY_NAMES[Math.round(value) - 1] ?? \"\",\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"}