{"spec_id":"violin-box","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// violin-box: Violin Plot with Embedded Box Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-09-09\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: adult height (cm) across 4 sports, each a distinct shape --------\nconst rng = makeLcg(2026);\nconst sampleSize = 140;\n\nfunction sampleGroup(generator) {\n  return Array.from({ length: sampleSize }, generator).map((v) => clamp(v, 140, 220));\n}\n\nconst sportGroups = [\n  { name: \"Gymnastics\", values: sampleGroup(() => randNormal(rng, 159, 5)) },\n  {\n    name: \"Swimming\",\n    values: sampleGroup(() => (rng() < 0.5 ? randNormal(rng, 173, 4) : randNormal(rng, 188, 4))),\n  },\n  { name: \"Rowing\", values: sampleGroup(() => 176 + randExponential(rng, 1 / 9)) },\n  { name: \"Basketball\", values: sampleGroup(() => randNormal(rng, 198, 6)) },\n];\n\n// --- Stats helpers -----------------------------------------------------------\nfunction std(values, m) {\n  const variance = values.reduce((sum, v) => sum + (v - m) ** 2, 0) / (values.length - 1);\n  return Math.sqrt(variance);\n}\n\nfunction mean(values) {\n  return values.reduce((sum, v) => sum + v, 0) / values.length;\n}\n\nfunction silvermanBandwidth(values) {\n  return 1.06 * std(values, mean(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 the KDE silhouette + embedded box/whisker stats per group -------\nconst gridSize = 120;\nconst maxHalfWidth = 0.4; // groups are spaced 1 unit apart on the x-axis\nconst boxHalfWidth = 0.12; // fixed, narrower than the violin envelope\n\nconst violins = sportGroups.map((group, i) => {\n  const catX = i + 1;\n  const sorted = [...group.values].sort((a, b) => a - b);\n  const bandwidth = silvermanBandwidth(sorted);\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 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 loBound = q1 - 1.5 * iqr;\n  const hiBound = q3 + 1.5 * iqr;\n  const inRange = sorted.filter((v) => v >= loBound && v <= hiBound);\n  const whiskerLo = inRange.length ? inRange[0] : q1;\n  const whiskerHi = inRange.length ? inRange[inRange.length - 1] : q3;\n  const outliers = sorted.filter((v) => v < whiskerLo || v > whiskerHi);\n\n  return {\n    catX,\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    stats: { q1, median, q3, whiskerLo, whiskerHi, outliers },\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 (the violin silhouette) -----------------\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.3),\n    borderWidth: 2,\n    pointRadius: 0,\n    fill: leftIdx,\n    tension: 0,\n  });\n});\n\n// Round to clean tick bounds based on the raw (clamped) data range so a\n// single skewed group's KDE padding can't dictate the shared axis extent.\nconst rawValues = sportGroups.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// --- Embedded box plot: hand-drawn on top of the violin silhouettes --------\n// Chart.js has no built-in violin or box-plot type; the box/whisker/outlier\n// geometry inside each violin is drawn by hand with the canvas API in a\n// plugin hook, using the same linear x/y scales the violin datasets sit on —\n// no external chart type or plugin package.\nconst embeddedBoxPlugin = {\n  id: \"embeddedBox\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n\n    violins.forEach((violin) => {\n      const s = violin.stats;\n      const cx = scales.x.getPixelForValue(violin.catX);\n      const halfWidthPx =\n        scales.x.getPixelForValue(violin.catX + boxHalfWidth) - scales.x.getPixelForValue(violin.catX);\n\n      const yQ1 = scales.y.getPixelForValue(s.q1);\n      const yQ3 = scales.y.getPixelForValue(s.q3);\n      const yMed = scales.y.getPixelForValue(s.median);\n      const yWhiskerLo = scales.y.getPixelForValue(s.whiskerLo);\n      const yWhiskerHi = scales.y.getPixelForValue(s.whiskerHi);\n\n      // Whiskers\n      ctx.save();\n      ctx.strokeStyle = t.ink;\n      ctx.lineWidth = 2.5;\n      ctx.beginPath();\n      ctx.moveTo(cx, yQ3);\n      ctx.lineTo(cx, yWhiskerHi);\n      ctx.moveTo(cx - halfWidthPx * 0.5, yWhiskerHi);\n      ctx.lineTo(cx + halfWidthPx * 0.5, yWhiskerHi);\n      ctx.moveTo(cx, yQ1);\n      ctx.lineTo(cx, yWhiskerLo);\n      ctx.moveTo(cx - halfWidthPx * 0.5, yWhiskerLo);\n      ctx.lineTo(cx + halfWidthPx * 0.5, yWhiskerLo);\n      ctx.stroke();\n\n      // Quartile box — opaque elevated fill so it reads as a distinct layer\n      // sitting inside the translucent violin, per the spec's \"box plot\n      // centered inside violin\" requirement.\n      ctx.fillStyle = t.elevatedBg;\n      ctx.strokeStyle = t.ink;\n      ctx.lineWidth = 2;\n      ctx.fillRect(cx - halfWidthPx, yQ3, halfWidthPx * 2, yQ1 - yQ3);\n      ctx.strokeRect(cx - halfWidthPx, yQ3, halfWidthPx * 2, yQ1 - yQ3);\n\n      // Median line\n      ctx.strokeStyle = t.ink;\n      ctx.lineWidth = 3;\n      ctx.beginPath();\n      ctx.moveTo(cx - halfWidthPx, yMed);\n      ctx.lineTo(cx + halfWidthPx, yMed);\n      ctx.stroke();\n      ctx.restore();\n\n      // Outliers\n      ctx.save();\n      ctx.fillStyle = t.inkSoft;\n      ctx.strokeStyle = t.pageBg;\n      ctx.lineWidth = 1.5;\n      s.outliers.forEach((v) => {\n        const cy = scales.y.getPixelForValue(v);\n        ctx.beginPath();\n        ctx.arc(cx, cy, 5, 0, 2 * Math.PI);\n        ctx.fill();\n        ctx.stroke();\n      });\n      ctx.restore();\n    });\n  },\n};\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-box · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      subtitle: {\n        display: true,\n        text: \"Shaded silhouette = density (KDE) · Box = IQR, median & 1.5×IQR whiskers\",\n        color: t.inkSoft,\n        font: { size: 14, style: \"italic\" },\n        padding: { bottom: 12 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0.5,\n        max: sportGroups.length + 0.5,\n        afterBuildTicks: (axis) => {\n          axis.ticks = sportGroups.map((_, i) => ({ value: i + 1 }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => sportGroups[Math.round(value) - 1]?.name ?? \"\",\n        },\n        grid: { display: false },\n        title: { display: true, text: \"Sport\", 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: \"Height (cm)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n  plugins: [embeddedBoxPlugin],\n});\n"}