{"spec_id":"violin-box","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// violin-box: Violin Plot with Embedded Box Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Tiny LCG PRNG + Box-Muller — the browser has no seeded RNG.\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (1103515245 * state + 12345) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\nfunction randNormal() {\n  let u1 = 0;\n  while (u1 === 0) u1 = rand();\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\nfunction normalSample(n, mean, std) {\n  const out = [];\n  for (let i = 0; i < n; i++) {\n    out.push(Math.min(100, Math.max(0, mean + std * randNormal())));\n  }\n  return out;\n}\n\n// Exam scores under 3 study methods. Self-Study is a mix of students who\n// never got going and students who thrived alone — a bimodal distribution\n// that a violin plot reveals but a bare box plot would hide.\nconst groups = [\n  {\n    name: \"Self-Study\",\n    samples: [...normalSample(60, 58, 7), ...normalSample(60, 82, 6)],\n  },\n  { name: \"Group Study\", samples: normalSample(120, 72, 9) },\n  { name: \"Tutored\", samples: normalSample(120, 84, 5) },\n];\n\n// --- Shared helpers ----------------------------------------------------------\nfunction quantile(sortedArr, q) {\n  const pos = (sortedArr.length - 1) * q;\n  const base = Math.floor(pos);\n  const rest = pos - base;\n  return sortedArr[base + 1] !== undefined\n    ? sortedArr[base] + rest * (sortedArr[base + 1] - sortedArr[base])\n    : sortedArr[base];\n}\nfunction boxStats(samples) {\n  const sorted = [...samples].sort((a, b) => a - b);\n  const q1 = quantile(sorted, 0.25);\n  const median = quantile(sorted, 0.5);\n  const q3 = quantile(sorted, 0.75);\n  const iqr = q3 - q1;\n  const lowerFence = q1 - 1.5 * iqr;\n  const upperFence = q3 + 1.5 * iqr;\n  const inFence = sorted.filter((v) => v >= lowerFence && v <= upperFence);\n  return {\n    q1,\n    median,\n    q3,\n    whiskerMin: Math.min(...inFence),\n    whiskerMax: Math.max(...inFence),\n    outliers: sorted.filter((v) => v < lowerFence || v > upperFence),\n  };\n}\nfunction gaussianKde(samples, grid) {\n  const n = samples.length;\n  const mean = samples.reduce((a, b) => a + b, 0) / n;\n  const variance = samples.reduce((a, b) => a + (b - mean) ** 2, 0) / (n - 1);\n  const std = Math.sqrt(variance);\n  const sorted = [...samples].sort((a, b) => a - b);\n  const iqr = quantile(sorted, 0.75) - quantile(sorted, 0.25);\n  const bandwidth = 0.9 * Math.min(std, iqr / 1.34) * Math.pow(n, -0.2);\n  return grid.map((x) => {\n    let sum = 0;\n    for (let j = 0; j < n; j++) {\n      const u = (x - samples[j]) / bandwidth;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    return sum / (n * bandwidth * Math.sqrt(2 * Math.PI));\n  });\n}\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// Shared value grid across all groups so every violin sits on the same scale.\nconst allScores = groups.flatMap((g) => g.samples);\nconst gridMin = Math.max(0, Math.min(...allScores) - 10);\nconst gridMax = Math.min(100, Math.max(...allScores) + 10);\nconst gridN = 161;\nconst grid = Array.from(\n  { length: gridN },\n  (_, i) => gridMin + ((gridMax - gridMin) * i) / (gridN - 1),\n);\n\n// --- Build one violin + embedded box plot per group (core series only:  ----\n// --- highcharts-more's boxplot/arearange/polygon are not vendored here) ----\nconst halfWidthMax = 0.38; // violin half-height, in category-row units\nconst halfBoxHeight = 0.14; // box half-height, centered inside the violin\nconst capHalf = 0.09; // whisker end-cap half-height\n\nconst series = groups.flatMap((g, i) => {\n  const baseline = i;\n  const color = t.palette[i];\n\n  // Violin: two mirrored `area` curves filling toward the row's baseline —\n  // together they trace the closed KDE silhouette without a polygon series.\n  const density = gaussianKde(g.samples, grid);\n  const maxDensity = Math.max(...density);\n  const halfWidth = density.map((v) => (v / maxDensity) * halfWidthMax);\n  const upperCurve = grid.map((x, j) => [x, baseline + halfWidth[j]]);\n  const lowerCurve = grid.map((x, j) => [x, baseline - halfWidth[j]]);\n\n  // Box plot: a floating rectangle (2-point area with an offset threshold),\n  // whiskers/caps (line series with a null-gap over the box), and a median\n  // notch — the same primitives `highcharts-more`'s boxplot draws with, built\n  // from the core `area`/`line` series instead.\n  const st = boxStats(g.samples);\n  const gapX = (st.q1 + st.q3) / 2;\n\n  return [\n    {\n      id: `violin-${i}`,\n      name: g.name,\n      type: \"area\",\n      data: upperCurve,\n      threshold: baseline,\n      color,\n      fillColor: hexToRgba(color, 0.32),\n      lineWidth: 2,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n    },\n    {\n      linkedTo: `violin-${i}`,\n      type: \"area\",\n      data: lowerCurve,\n      threshold: baseline,\n      color,\n      fillColor: hexToRgba(color, 0.32),\n      lineWidth: 2,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      type: \"area\",\n      data: [\n        [st.q1, baseline + halfBoxHeight],\n        [st.q3, baseline + halfBoxHeight],\n      ],\n      threshold: baseline - halfBoxHeight,\n      color: t.ink,\n      fillColor: hexToRgba(t.ink, 0.82),\n      lineWidth: 1.5,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      type: \"line\",\n      data: [\n        [st.whiskerMin, baseline],\n        [st.q1, baseline],\n        [gapX, null],\n        [st.q3, baseline],\n        [st.whiskerMax, baseline],\n      ],\n      color: t.inkSoft,\n      lineWidth: 1.5,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      type: \"line\",\n      data: [\n        [st.whiskerMin, baseline - capHalf],\n        [st.whiskerMin, baseline + capHalf],\n        [gapX, null],\n        [st.whiskerMax, baseline - capHalf],\n        [st.whiskerMax, baseline + capHalf],\n      ],\n      color: t.inkSoft,\n      lineWidth: 1.5,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      type: \"line\",\n      data: [\n        [st.median, baseline - halfBoxHeight],\n        [st.median, baseline + halfBoxHeight],\n      ],\n      color: t.pageBg,\n      lineWidth: 3,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    ...(st.outliers.length\n      ? [\n          {\n            type: \"scatter\",\n            data: st.outliers.map((v) => [v, baseline]),\n            color,\n            marker: {\n              symbol: \"circle\",\n              radius: 5,\n              fillColor: color,\n              lineColor: t.pageBg,\n              lineWidth: 1,\n            },\n            showInLegend: false,\n            tooltip: { pointFormat: `${g.name} outlier: <b>{point.x:.1f}</b>` },\n          },\n        ]\n      : []),\n    // Stat chip: a Highcharts dataLabel badge (own background/border, not just\n    // text) surfacing the box's Q1/median/Q3 numerically — the core-bundle\n    // stand-in for highcharts-more's boxplot tooltip, made permanently visible.\n    {\n      type: \"scatter\",\n      data: [\n        {\n          x: st.median,\n          y: baseline,\n          custom: { q1: st.q1, median: st.median, q3: st.q3 },\n        },\n      ],\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n      dataLabels: {\n        enabled: true,\n        align: \"center\",\n        verticalAlign: \"middle\",\n        y: -100,\n        format: \"Q1 {point.custom.q1:.0f} · Med {point.custom.median:.0f} · Q3 {point.custom.q3:.0f}\",\n        backgroundColor: t.elevatedBg,\n        borderColor: t.grid,\n        borderWidth: 1,\n        borderRadius: 4,\n        padding: 4,\n        style: {\n          color: t.inkSoft,\n          fontSize: \"11px\",\n          fontWeight: \"500\",\n          textOutline: \"none\",\n        },\n      },\n    },\n  ];\n});\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"violin-box · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Self-Study is bimodal — disengaged and independently-thriving students form two distinct peaks\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    type: \"linear\",\n    min: gridMin,\n    max: gridMax,\n    title: {\n      text: \"Exam Score (0–100)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    type: \"category\",\n    categories: groups.map((g) => g.name),\n    min: -0.6,\n    max: groups.length - 1 + 0.6,\n    tickPositions: groups.map((_, i) => i),\n    reversed: true,\n    title: { text: null },\n    gridLineWidth: 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: { enabled: false },\n  tooltip: { enabled: true },\n  plotOptions: { series: { animation: false } },\n  series,\n});\n"}