{"spec_id":"violin-grouped-swarm","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// violin-grouped-swarm: Grouped Violin Plot with Swarm Overlay\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-02\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Reaction-time trial: 3 cognitive tasks x 2 expertise levels, 40 trials each.\n// Only the core `highcharts` bundle is loaded (no highcharts-more arearange /\n// boxplot), so the violin silhouette is built from two mirrored `area` series\n// per cell — each filled from its group's center line (`threshold`) out to a\n// Gaussian KDE curve — with the whole chart inverted so the value axis (the\n// series' x) renders vertically and the category/group axis (the series' y)\n// renders horizontally.\nconst categories = [\"Visual Search\", \"Pattern Matching\", \"Sequence Recall\"];\nconst groups = [\"Novice\", \"Expert\"];\nconst difficultyFactor = [1, 1.18, 1.35];\nconst categorySpacing = 3.4;\nconst groupOffset = 0.85;\nconst maxViolinHalfWidth = 0.62;\nconst maxSwarmHalfWidth = 0.55;\nconst trialsPerCell = 40;\nconst kdeGridSize = 46;\n\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction randNormal(mean, std) {\n  const u1 = Math.max(lcg(), 1e-9);\n  const u2 = lcg();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\nconst categoryCenters = categories.map((_, i) => i * categorySpacing);\nconst centerToCategory = {};\ncategoryCenters.forEach((center, i) => {\n  centerToCategory[center] = categories[i];\n});\n\nconst cells = [];\ncategories.forEach((category, ci) => {\n  groups.forEach((group, gi) => {\n    const factor = difficultyFactor[ci];\n    const mean = group === \"Novice\" ? 680 * factor : 410 * factor;\n    const std = group === \"Novice\" ? 140 * factor : 75 * factor;\n    const values = Array.from({ length: trialsPerCell }, () =>\n      randNormal(mean, std),\n    );\n    const center =\n      categoryCenters[ci] + (gi === 0 ? -groupOffset : groupOffset);\n    cells.push({ category, group, ci, values, center });\n  });\n});\n\n// --- Kernel density estimate (Gaussian, Silverman bandwidth) ---------------\nfunction kde(values, gridSize) {\n  const n = values.length;\n  const mean = values.reduce((a, b) => a + b, 0) / n;\n  const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / n;\n  const std = Math.sqrt(variance);\n  const bandwidth = 1.06 * std * Math.pow(n, -0.2);\n  const min = Math.min(...values) - 1.4 * bandwidth;\n  const max = Math.max(...values) + 1.4 * bandwidth;\n  const grid = Array.from(\n    { length: gridSize },\n    (_, i) => min + ((max - min) * i) / (gridSize - 1),\n  );\n  const rawDensity = grid.map((v) =>\n    values.reduce(\n      (acc, xi) => acc + Math.exp(-0.5 * ((v - xi) / bandwidth) ** 2),\n      0,\n    ),\n  );\n  const peak = Math.max(...rawDensity);\n  return { grid, density: rawDensity.map((d) => d / peak) };\n}\n\n// --- Beeswarm-style dodge: spread same-bin trials sideways -----------------\nfunction beeswarmOffsets(values, maxHalfWidth, binCount) {\n  const min = Math.min(...values);\n  const max = Math.max(...values);\n  const binWidth = (max - min) / binCount || 1;\n  const bins = Array.from({ length: binCount }, () => []);\n  values.forEach((v, i) => {\n    const b = Math.min(binCount - 1, Math.floor((v - min) / binWidth));\n    bins[b].push(i);\n  });\n  const offsets = new Array(values.length).fill(0);\n  bins.forEach((idxs) => {\n    const n = idxs.length;\n    if (n <= 1) return;\n    const spacing = Math.min(0.11, (2 * maxHalfWidth) / n);\n    idxs.forEach((i, k) => {\n      offsets[i] = (k - (n - 1) / 2) * spacing;\n    });\n  });\n  return offsets;\n}\n\n// --- Build series ------------------------------------------------------------\nconst groupColors = { Novice: t.palette[0], Expert: t.palette[1] };\nconst series = [];\n\ncells.forEach(({ category, group, ci, values, center }) => {\n  const { grid, density } = kde(values, kdeGridSize);\n  const color = groupColors[group];\n  const rightEdge = grid.map((v, i) => [\n    v,\n    center + density[i] * maxViolinHalfWidth,\n  ]);\n  const leftEdge = grid.map((v, i) => [\n    v,\n    center - density[i] * maxViolinHalfWidth,\n  ]);\n\n  series.push({\n    type: \"area\",\n    name: group,\n    data: rightEdge,\n    threshold: center,\n    color,\n    fillOpacity: 0.45,\n    lineWidth: 1.5,\n    lineColor: color,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: ci === 0,\n  });\n  series.push({\n    type: \"area\",\n    name: group,\n    data: leftEdge,\n    threshold: center,\n    color,\n    fillOpacity: 0.45,\n    lineWidth: 1.5,\n    lineColor: color,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n  });\n\n  const offsets = beeswarmOffsets(values, maxSwarmHalfWidth, 14);\n  series.push({\n    type: \"scatter\",\n    name: `${category} · ${group} trials`,\n    data: values.map((v, i) => [v, center + offsets[i]]),\n    marker: {\n      symbol: \"circle\",\n      radius: 3,\n      fillColor: Highcharts.color(color).setOpacity(0.8).get(),\n      lineWidth: 0,\n    },\n    showInLegend: false,\n    tooltip: {\n      pointFormat: `${category} · ${group}<br/>Reaction time: {point.x:.0f} ms`,\n    },\n  });\n});\n\n// --- Chart -------------------------------------------------------------------\nconst title = \"violin-grouped-swarm · javascript · highcharts · anyplot.ai\";\n\nHighcharts.chart(\"container\", {\n  chart: {\n    inverted: true,\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    text: title,\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    reversed: false,\n    title: {\n      text: \"Reaction Time (ms)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: \"Task Type\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: -2.1,\n    max: categoryCenters[categoryCenters.length - 1] + 2.2,\n    startOnTick: false,\n    endOnTick: false,\n    tickPositions: categoryCenters,\n    gridLineWidth: 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: {\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n      formatter() {\n        return centerToCategory[this.value] || \"\";\n      },\n    },\n  },\n  legend: {\n    title: {\n      text: \"Expertise Level\",\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n    },\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n    symbolRadius: 2,\n  },\n  tooltip: { backgroundColor: t.elevatedBg, style: { color: t.ink } },\n  plotOptions: { series: { animation: false } },\n  series,\n});\n"}