{"spec_id":"violin-swarm","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// violin-swarm: Violin Plot with Overlaid Swarm Points\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 84/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + Box-Muller normal sampler -------------------\nfunction makeLcg(seed) {\n  let state = seed;\n  return function next() {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\n\nfunction makeNormalSampler(seed) {\n  const rand = makeLcg(seed);\n  return function normal() {\n    const u1 = Math.max(rand(), 1e-9);\n    const u2 = rand();\n    return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  };\n}\n\n// --- Data: IL-6 biomarker concentration (pg/mL) across a dose-response trial\n// Log-normal draws give the right-skewed shape typical of biomarker assays.\nconst groups = [\n  { name: \"Placebo\", n: 45, mu: 2.3, sigma: 0.35 },\n  { name: \"Low Dose\", n: 55, mu: 2.0, sigma: 0.3 },\n  { name: \"Medium Dose\", n: 60, mu: 1.7, sigma: 0.28 },\n  { name: \"High Dose\", n: 50, mu: 1.4, sigma: 0.25 },\n];\n\nconst normal = makeNormalSampler(42);\ngroups.forEach((group) => {\n  group.values = Array.from({ length: group.n }, () =>\n    Math.exp(group.mu + group.sigma * normal())\n  );\n});\n\n// --- Gaussian KDE ------------------------------------------------------------\nfunction stdDev(values) {\n  const mean = values.reduce((a, b) => a + b, 0) / values.length;\n  const variance =\n    values.reduce((a, v) => a + (v - mean) ** 2, 0) / (values.length - 1);\n  return Math.sqrt(variance);\n}\n\nfunction kernelDensity(values, gridPoints, bandwidth) {\n  const norm = 1 / (values.length * bandwidth * Math.sqrt(2 * Math.PI));\n  return gridPoints.map((g) => {\n    let sum = 0;\n    for (const v of values) {\n      const u = (g - v) / bandwidth;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    return sum * norm;\n  });\n}\n\nconst VIOLIN_HALF_WIDTH = 0.42;\nconst GRID_SIZE = 60;\n\ngroups.forEach((group, index) => {\n  const values = group.values;\n  const bandwidth = 1.4 * stdDev(values) * Math.pow(values.length, -0.2);\n  const pad = 1.5 * bandwidth;\n  const lo = Math.min(...values) - pad;\n  const hi = Math.max(...values) + pad;\n  const step = (hi - lo) / (GRID_SIZE - 1);\n  const grid = Array.from({ length: GRID_SIZE }, (_, i) => lo + i * step);\n  const density = kernelDensity(values, grid, bandwidth);\n  const maxDensity = Math.max(...density);\n  const halfWidths = density.map((d) => (d / maxDensity) * VIOLIN_HALF_WIDTH);\n\n  group.index = index;\n  group.grid = grid;\n  group.halfWidths = halfWidths;\n  group.rightSide = grid.map((v, i) => [v, index + halfWidths[i]]);\n  group.leftSide = grid.map((v, i) => [v, index - halfWidths[i]]);\n});\n\n// Local half-width at an arbitrary value, via linear interpolation on the grid\nfunction localHalfWidth(group, value) {\n  const grid = group.grid;\n  if (value <= grid[0]) return group.halfWidths[0];\n  if (value >= grid[grid.length - 1]) return group.halfWidths[grid.length - 1];\n  for (let i = 0; i < grid.length - 1; i++) {\n    if (value >= grid[i] && value <= grid[i + 1]) {\n      const frac = (value - grid[i]) / (grid[i + 1] - grid[i]);\n      return (\n        group.halfWidths[i] + frac * (group.halfWidths[i + 1] - group.halfWidths[i])\n      );\n    }\n  }\n  return 0;\n}\n\n// --- Swarm layout: bin points by value, spread symmetrically within the\n// locally available violin half-width so points stay inside the silhouette.\nconst SWARM_STEP = 0.026;\nconst MARGIN = 0.04;\n\nfunction swarmOffsets(group) {\n  const sorted = [...group.values].sort((a, b) => a - b);\n  const binWidth = (group.grid[group.grid.length - 1] - group.grid[0]) / 24;\n  const bins = [];\n  let current = [];\n  let binStart = sorted[0];\n  sorted.forEach((v) => {\n    if (v - binStart > binWidth && current.length > 0) {\n      bins.push(current);\n      current = [];\n      binStart = v;\n    }\n    current.push(v);\n  });\n  if (current.length) bins.push(current);\n\n  const points = [];\n  bins.forEach((bin) => {\n    const center = bin.reduce((a, b) => a + b, 0) / bin.length;\n    const available = Math.max(localHalfWidth(group, center) - MARGIN, SWARM_STEP / 2);\n    const rawOffsets = bin.map((_, i) => {\n      const rank = Math.ceil(i / 2);\n      const sign = i % 2 === 0 ? 1 : -1;\n      return rank * SWARM_STEP * sign;\n    });\n    const maxAbs = Math.max(...rawOffsets.map(Math.abs), SWARM_STEP / 2);\n    const scale = maxAbs > available ? available / maxAbs : 1;\n    bin.forEach((v, i) => {\n      points.push([v, group.index + rawOffsets[i] * scale]);\n    });\n  });\n  return points;\n}\n\nconst swarmData = groups.flatMap((group) => swarmOffsets(group));\n\n// --- Chart -------------------------------------------------------------------\nconst violinColor = t.palette[0];\nconst swarmColor = t.palette[2];\nconst categoryNames = groups.map((g) => g.name);\n\nconst violinSeries = groups.flatMap((group, index) => [\n  {\n    type: \"area\",\n    name: \"Density estimate\",\n    data: group.rightSide,\n    threshold: index,\n    color: violinColor,\n    fillOpacity: 0.4,\n    lineWidth: 1.5,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: index === 0,\n  },\n  {\n    type: \"area\",\n    name: \"Density estimate\",\n    data: group.leftSide,\n    threshold: index,\n    color: violinColor,\n    fillOpacity: 0.4,\n    lineWidth: 1.5,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n  },\n]);\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: \"violin-swarm · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    reversed: false,\n    title: {\n      text: \"IL-6 Concentration (pg/mL)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: null },\n    min: -0.5,\n    max: groups.length - 0.5,\n    startOnTick: false,\n    endOnTick: false,\n    tickPositions: groups.map((_, i) => i),\n    gridLineWidth: 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: {\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n      formatter() {\n        return categoryNames[this.value] || \"\";\n      },\n    },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    formatter() {\n      if (this.series.name === \"Individual trial\") {\n        return `${categoryNames[Math.round(this.y)]}<br/>${this.x.toFixed(1)} pg/mL`;\n      }\n      return false;\n    },\n  },\n  plotOptions: {\n    series: { animation: false },\n  },\n  series: [\n    ...violinSeries,\n    {\n      type: \"scatter\",\n      name: \"Individual trial\",\n      data: swarmData,\n      color: swarmColor,\n      marker: {\n        symbol: \"circle\",\n        radius: 3.5,\n        fillColor: swarmColor,\n        lineColor: t.pageBg,\n        lineWidth: 0.5,\n      },\n      opacity: 0.85,\n    },\n  ],\n});\n"}