{"spec_id":"violin-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// violin-basic: Basic Violin Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + samplers ------------------------------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function next() {\n    state = (Math.imul(1664525, state) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\n\nfunction randNormal(rng, mean, std) {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\nfunction randExponential(rng, rate) {\n  return -Math.log(1 - rng()) / rate;\n}\n\nfunction clamp(value, min, max) {\n  return Math.min(max, Math.max(min, value));\n}\n\n// --- Data: test scores across 4 class groups, each a distinct shape --------\nconst rng = makeLcg(42);\nconst sampleSize = 150;\n\nfunction sampleClass(generator) {\n  return Array.from({ length: sampleSize }, generator).map((v) => clamp(v, 50, 100));\n}\n\nconst classGroups = [\n  { name: \"Class A\", values: sampleClass(() => randNormal(rng, 75, 6)) },\n  {\n    name: \"Class B\",\n    values: sampleClass(() => (rng() < 0.5 ? randNormal(rng, 64, 4) : randNormal(rng, 86, 4))),\n  },\n  { name: \"Class C\", values: sampleClass(() => 58 + randExponential(rng, 1 / 9)) },\n  { name: \"Class D\", values: sampleClass(() => randNormal(rng, 91, 3)) },\n];\n\n// --- Kernel density estimation (Silverman bandwidth, Gaussian kernel) -------\nfunction mean(values) {\n  return values.reduce((sum, v) => sum + v, 0) / values.length;\n}\n\nfunction std(values) {\n  const m = mean(values);\n  const variance = values.reduce((sum, v) => sum + (v - m) ** 2, 0) / (values.length - 1);\n  return Math.sqrt(variance);\n}\n\nfunction silvermanBandwidth(values) {\n  return 1.06 * std(values) * values.length ** (-1 / 5);\n}\n\nfunction gaussianKde(values, evalPoints, bandwidth) {\n  const norm = 1 / (values.length * bandwidth * Math.sqrt(2 * Math.PI));\n  return evalPoints.map((point) => {\n    let sum = 0;\n    for (const v of values) {\n      const u = (point - v) / bandwidth;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    return sum * norm;\n  });\n}\n\nfunction quantile(sortedValues, q) {\n  const idx = q * (sortedValues.length - 1);\n  const lower = Math.floor(idx);\n  const upper = Math.ceil(idx);\n  if (lower === upper) return sortedValues[lower];\n  return sortedValues[lower] + (sortedValues[upper] - sortedValues[lower]) * (idx - lower);\n}\n\n// --- Build a mirrored density silhouette (the \"violin\") per category -------\nconst gridSize = 120;\nconst maxHalfWidth = 0.4; // categories are spaced 1 unit apart on the x-axis\n\nconst violins = classGroups.map((group, i) => {\n  const catX = i + 1;\n  const sorted = [...group.values].sort((a, b) => a - b);\n  const bandwidth = silvermanBandwidth(sorted);\n  // Pad around the 1st/99th percentile (not the raw min/max) so a single\n  // far-outlier tail (e.g. Class C's right-skew) can't stretch the shared\n  // y-axis; the KDE still tapers smoothly toward the trimmed edges.\n  const pad = bandwidth * 1.5;\n  const yMin = quantile(sorted, 0.01) - pad;\n  const yMax = quantile(sorted, 0.99) + pad;\n  const step = (yMax - yMin) / (gridSize - 1);\n  const evalPoints = Array.from({ length: gridSize }, (_, j) => yMin + j * step);\n  const density = gaussianKde(sorted, evalPoints, bandwidth);\n  const scale = maxHalfWidth / Math.max(...density);\n\n  const widthAt = (y) => density[clamp(Math.round((y - yMin) / step), 0, gridSize - 1)] * scale;\n\n  return {\n    catX,\n    yMin,\n    yMax,\n    left: evalPoints.map((y, j) => ({ x: catX - density[j] * scale, y })),\n    right: evalPoints.map((y, j) => ({ x: catX + density[j] * scale, y })),\n    q1: quantile(sorted, 0.25),\n    median: quantile(sorted, 0.5),\n    q3: quantile(sorted, 0.75),\n    widthAt,\n  };\n});\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\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// --- Datasets: mirrored fill areas first, quartile/median markers on top ---\nconst datasets = [];\n\nviolins.forEach((violin, i) => {\n  const color = t.palette[i % t.palette.length];\n  const leftIdx = datasets.length;\n  datasets.push({\n    data: violin.left,\n    borderColor: color,\n    borderWidth: 2,\n    pointRadius: 0,\n    fill: false,\n    tension: 0,\n  });\n  datasets.push({\n    data: violin.right,\n    borderColor: color,\n    backgroundColor: hexToRgba(color, 0.35),\n    borderWidth: 2,\n    pointRadius: 0,\n    fill: leftIdx,\n    tension: 0,\n  });\n});\n\nviolins.forEach((violin) => {\n  const q1Span = violin.widthAt(violin.q1) * 0.7;\n  const q3Span = violin.widthAt(violin.q3) * 0.7;\n  const medianSpan = violin.widthAt(violin.median) * 0.95;\n\n  [\n    { y: violin.q1, span: q1Span, color: t.inkSoft, width: 2, dash: [6, 4] },\n    { y: violin.q3, span: q3Span, color: t.inkSoft, width: 2, dash: [6, 4] },\n    { y: violin.median, span: medianSpan, color: t.ink, width: 3, dash: [] },\n  ].forEach((marker) => {\n    datasets.push({\n      data: [\n        { x: violin.catX - marker.span, y: marker.y },\n        { x: violin.catX + marker.span, y: marker.y },\n      ],\n      borderColor: marker.color,\n      borderWidth: marker.width,\n      borderDash: marker.dash,\n      pointRadius: 0,\n      fill: false,\n      tension: 0,\n    });\n  });\n});\n\n// Round to clean tick bounds based on the actual (clamped) data range, not\n// the padded KDE eval range — keeps a single skewed group's tail from\n// dictating the shared axis extent (see per-group padding above).\nconst rawValues = classGroups.flatMap((group) => group.values);\nconst rawMin = Math.min(...rawValues);\nconst rawMax = Math.max(...rawValues);\nconst axisPad = (rawMax - rawMin) * 0.08;\nconst yAxisMin = Math.floor((rawMin - axisPad) / 5) * 5;\nconst yAxisMax = Math.ceil((rawMax + axisPad) / 5) * 5;\n\n// --- Chart -------------------------------------------------------------------\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-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      subtitle: {\n        display: true,\n        text: \"Solid line = median · Dashed lines = Q1 / Q3\",\n        color: t.inkSoft,\n        font: { size: 14, style: \"italic\" },\n        padding: { bottom: 12 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0.5,\n        max: classGroups.length + 0.5,\n        afterBuildTicks: (axis) => {\n          axis.ticks = classGroups.map((_, i) => ({ value: i + 1 }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => classGroups[Math.round(value) - 1]?.name ?? \"\",\n        },\n        grid: { display: false },\n        title: { display: true, text: \"Class Group\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        min: yAxisMin,\n        max: yAxisMax,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Test Score (%)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}