{"spec_id":"violin-split","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// violin-split: Split Violin Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-09\n\n// Only the core Highcharts bundle is loaded (no highcharts-more), so there is\n// no native arearange/violin series. Instead each half-violin is a plain\n// \"area\" series: x = recovery score, y = category index +/- normalized KDE\n// density, filled to a per-series threshold at the category's baseline. That\n// reads as a horizontal split violin (score on the shared x-axis, one row per\n// clinic) rather than the classic vertical orientation — a valid orientation\n// variant that stays inside the core bundle.\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic RNG (LCG) + Box-Muller normal samples -------------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction normal(mean, std) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return mean + std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\nfunction clamp01to100(v) {\n  return Math.min(100, Math.max(0, v));\n}\nfunction std(arr) {\n  const m = arr.reduce((a, b) => a + b, 0) / arr.length;\n  const v = arr.reduce((a, b) => a + (b - m) * (b - m), 0) / arr.length;\n  return Math.sqrt(v);\n}\nfunction median(arr) {\n  const s = [...arr].sort((a, b) => a - b);\n  const mid = Math.floor(s.length / 2);\n  return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;\n}\nfunction kde(samples, grid, bandwidth) {\n  return grid.map((x) => {\n    let sum = 0;\n    for (const v of samples) {\n      const u = (x - v) / bandwidth;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    return sum / (samples.length * bandwidth * Math.sqrt(2 * Math.PI));\n  });\n}\n\n// --- Data: patient recovery scores before/after treatment, per clinic ------\nconst clinics = [\n  { name: \"Clinic A\", before: { mean: 52, std: 14 }, after: { mean: 68, std: 10 } },\n  { name: \"Clinic B\", before: { mean: 48, std: 16 }, after: { mean: 64, std: 12 } },\n  { name: \"Clinic C\", before: { mean: 58, std: 12 }, after: { mean: 70, std: 9 } },\n  { name: \"Clinic D\", before: { mean: 45, std: 18 }, after: { mean: 60, std: 14 } },\n];\nconst SAMPLES_PER_GROUP = 180;\nconst HALF_WIDTH = 0.42;\nconst grid = [];\nfor (let x = 0; x <= 100; x += 2) grid.push(x);\n\nconst areaSeries = [];\nconst beforeMedians = [];\nconst afterMedians = [];\n\nclinics.forEach((clinic, i) => {\n  const beforeSamples = Array.from({ length: SAMPLES_PER_GROUP }, () =>\n    clamp01to100(normal(clinic.before.mean, clinic.before.std)),\n  );\n  const afterSamples = Array.from({ length: SAMPLES_PER_GROUP }, () =>\n    clamp01to100(normal(clinic.after.mean, clinic.after.std)),\n  );\n\n  const beforeBw = 1.06 * std(beforeSamples) * Math.pow(beforeSamples.length, -0.2);\n  const afterBw = 1.06 * std(afterSamples) * Math.pow(afterSamples.length, -0.2);\n  const beforeDensity = kde(beforeSamples, grid, beforeBw);\n  const afterDensity = kde(afterSamples, grid, afterBw);\n  const beforeMax = Math.max(...beforeDensity);\n  const afterMax = Math.max(...afterDensity);\n\n  areaSeries.push({\n    name: \"Before treatment\",\n    type: \"area\",\n    color: t.palette[0],\n    threshold: i,\n    showInLegend: i === 0,\n    data: grid.map((x, idx) => ({ x, y: i - (beforeDensity[idx] / beforeMax) * HALF_WIDTH })),\n    tooltip: { pointFormatter: function () { return `Before · score ${this.x}`; } },\n  });\n  areaSeries.push({\n    name: \"After treatment\",\n    type: \"area\",\n    color: t.palette[1],\n    threshold: i,\n    showInLegend: i === 0,\n    data: grid.map((x, idx) => ({ x, y: i + (afterDensity[idx] / afterMax) * HALF_WIDTH })),\n    tooltip: { pointFormatter: function () { return `After · score ${this.x}`; } },\n  });\n\n  beforeMedians.push({ x: median(beforeSamples), y: i });\n  afterMedians.push({ x: median(afterSamples), y: i });\n});\n\n// --- Chart -------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"area\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"violin-split · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Patient recovery scores before vs. after treatment, by clinic\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    min: 0,\n    max: 100,\n    title: { text: \"Recovery Score\", style: { color: t.inkSoft, fontSize: \"16px\" } },\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    min: -0.6,\n    max: clinics.length - 1 + 0.6,\n    startOnTick: false,\n    endOnTick: false,\n    tickPositions: clinics.map((_, i) => i),\n    gridLineWidth: 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    title: { text: null },\n    labels: {\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n      formatter: function () {\n        return clinics[this.value] ? clinics[this.value].name : \"\";\n      },\n    },\n    plotLines: clinics.map((_, i) => ({ value: i, color: t.grid, width: 1, zIndex: 2 })),\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  plotOptions: {\n    series: { animation: false },\n    area: {\n      lineWidth: 2,\n      fillOpacity: 0.75,\n      marker: { enabled: false },\n      states: { hover: { enabled: false } },\n    },\n  },\n  series: [\n    ...areaSeries,\n    {\n      name: \"Median (before)\",\n      type: \"scatter\",\n      color: t.ink,\n      data: beforeMedians,\n      marker: { symbol: \"diamond\", radius: 4, lineWidth: 0 },\n      showInLegend: false,\n      tooltip: { pointFormatter: function () { return `Median before: ${this.x}`; } },\n    },\n    {\n      name: \"Median (after)\",\n      type: \"scatter\",\n      color: t.ink,\n      data: afterMedians,\n      marker: { symbol: \"diamond\", radius: 4, lineWidth: 0 },\n      showInLegend: false,\n      tooltip: { pointFormatter: function () { return `Median after: ${this.x}`; } },\n    },\n  ],\n});\n"}