{"spec_id":"violin-box","library":"d3","language":"javascript","code":"// anyplot.ai\n// violin-box: Violin Plot with Embedded Box Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 60, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Test scores across three teaching methods. \"Blended Learning\" is generated\n// as a two-component mixture so its KDE shows two humps that the embedded box\n// plot's single median cannot reveal on its own — motivating the combined view.\nfunction makeRng(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nfunction randomNormal(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}\nconst rng = makeRng(42);\nconst pointsPerGroup = 150;\nconst groupSpecs = [\n  { label: \"Traditional Lecture\", sample: () => randomNormal(rng, 68, 11) },\n  { label: \"Flipped Classroom\", sample: () => randomNormal(rng, 76, 9) },\n  {\n    label: \"Blended Learning\",\n    sample: () => (rng() < 0.6 ? randomNormal(rng, 85, 6) : randomNormal(rng, 65, 9)),\n  },\n];\n\nconst data = [];\nfor (const spec of groupSpecs) {\n  for (let i = 0; i < pointsPerGroup; i++) {\n    data.push({ group: spec.label, value: Math.min(100, Math.max(0, spec.sample())) });\n  }\n}\nconst groups = groupSpecs.map((g) => g.label);\n\n// --- Kernel density estimation ----------------------------------------------\nfunction kernelEpanechnikov(bandwidth) {\n  return (v) => (Math.abs((v /= bandwidth)) <= 1 ? (0.75 * (1 - v * v)) / bandwidth : 0);\n}\nfunction kernelDensityEstimator(kernel, thresholds) {\n  return (values) => thresholds.map((x) => [x, d3.mean(values, (v) => kernel(x - v))]);\n}\nconst bandwidth = 6;\n\n// Each group's KDE is evaluated only across its own data extent (padded by the\n// kernel bandwidth so the curve tapers to zero smoothly) rather than the full\n// [0, 100] axis — otherwise the Epanechnikov kernel's hard cutoff produces an\n// exact-zero-width (but still stroked) sliver reaching all the way to the axis\n// ends wherever a group's data doesn't span the full score range.\nconst valuesByGroup = d3.group(data, (d) => d.group);\nconst densityByGroup = new Map();\nlet maxDensity = 0;\nfor (const group of groups) {\n  const values = valuesByGroup.get(group).map((d) => d.value);\n  const lo = Math.max(0, Math.floor(d3.min(values) - bandwidth));\n  const hi = Math.min(100, Math.ceil(d3.max(values) + bandwidth));\n  const thresholds = d3.range(lo, hi + 1, 1);\n  const density = kernelDensityEstimator(kernelEpanechnikov(bandwidth), thresholds)(values);\n  densityByGroup.set(group, density);\n  maxDensity = Math.max(maxDensity, d3.max(density, (d) => d[1]));\n}\n\n// --- Box-plot summary stats (Tukey whiskers, 1.5×IQR) -----------------------\nfunction computeBoxStats(values) {\n  const sorted = values.slice().sort(d3.ascending);\n  const q1 = d3.quantileSorted(sorted, 0.25);\n  const median = d3.quantileSorted(sorted, 0.5);\n  const q3 = d3.quantileSorted(sorted, 0.75);\n  const iqr = q3 - q1;\n  const lowerFence = q1 - 1.5 * iqr;\n  const upperFence = q3 + 1.5 * iqr;\n  const inRange = sorted.filter((v) => v >= lowerFence && v <= upperFence);\n  return {\n    q1,\n    median,\n    q3,\n    whiskerLow: d3.min(inRange),\n    whiskerHigh: d3.max(inRange),\n    outliers: sorted.filter((v) => v < lowerFence || v > upperFence),\n  };\n}\nconst statsByGroup = new Map(groups.map((g) => [g, computeBoxStats(valuesByGroup.get(g).map((d) => d.value))]));\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleBand().domain(groups).range([0, iw]).padding(0.38);\nconst y = d3.scaleLinear().domain([0, 100]).nice().range([ih, 0]);\nconst xNum = d3.scaleLinear().domain([0, maxDensity]).range([0, (x.bandwidth() / 2) * 0.92]);\n\n// --- Y gridlines (drawn first, sit behind the data) ---------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Violin + box per group ----------------------------------------------------\nconst violinArea = d3\n  .area()\n  .curve(d3.curveCatmullRom.alpha(0.5))\n  .y((d) => y(d[0]))\n  .x0((d) => -xNum(d[1]))\n  .x1((d) => xNum(d[1]));\n\nconst groupLayers = g\n  .selectAll(\".group-layer\")\n  .data(groups)\n  .join(\"g\")\n  .attr(\"class\", \"group-layer\")\n  .attr(\"transform\", (group) => `translate(${x(group) + x.bandwidth() / 2},0)`);\n\ngroupLayers\n  .append(\"path\")\n  .attr(\"d\", (group) => violinArea(densityByGroup.get(group)))\n  .attr(\"fill\", (group, i) => t.palette[i])\n  .attr(\"fill-opacity\", 0.42)\n  .attr(\"stroke\", (group, i) => t.palette[i])\n  .attr(\"stroke-width\", 2);\n\nconst boxWidth = x.bandwidth() * 0.16;\n\n// Whiskers (drawn under the box so the box's fill covers the stem cleanly).\ngroupLayers.each(function (group, i) {\n  const layer = d3.select(this);\n  const stats = statsByGroup.get(group);\n  const color = t.palette[i];\n\n  layer\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", 0)\n    .attr(\"y1\", y(stats.q3))\n    .attr(\"y2\", y(stats.whiskerHigh))\n    .attr(\"stroke\", color)\n    .attr(\"stroke-width\", 2);\n  layer\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", 0)\n    .attr(\"y1\", y(stats.q1))\n    .attr(\"y2\", y(stats.whiskerLow))\n    .attr(\"stroke\", color)\n    .attr(\"stroke-width\", 2);\n  for (const whiskerValue of [stats.whiskerHigh, stats.whiskerLow]) {\n    layer\n      .append(\"line\")\n      .attr(\"x1\", -boxWidth * 0.4)\n      .attr(\"x2\", boxWidth * 0.4)\n      .attr(\"y1\", y(whiskerValue))\n      .attr(\"y2\", y(whiskerValue))\n      .attr(\"stroke\", color)\n      .attr(\"stroke-width\", 2);\n  }\n\n  layer\n    .append(\"rect\")\n    .attr(\"x\", -boxWidth / 2)\n    .attr(\"y\", y(stats.q3))\n    .attr(\"width\", boxWidth)\n    .attr(\"height\", y(stats.q1) - y(stats.q3))\n    .attr(\"fill\", t.pageBg)\n    .attr(\"stroke\", color)\n    .attr(\"stroke-width\", 2.5);\n  layer\n    .append(\"line\")\n    .attr(\"x1\", -boxWidth / 2)\n    .attr(\"x2\", boxWidth / 2)\n    .attr(\"y1\", y(stats.median))\n    .attr(\"y2\", y(stats.median))\n    .attr(\"stroke\", t.ink)\n    .attr(\"stroke-width\", 3);\n\n  layer\n    .selectAll(\".outlier\")\n    .data(stats.outliers)\n    .join(\"circle\")\n    .attr(\"class\", \"outlier\")\n    .attr(\"cx\", 0)\n    .attr(\"cy\", (v) => y(v))\n    .attr(\"r\", 4.5)\n    .attr(\"fill\", color)\n    .attr(\"fill-opacity\", 0.75)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 1);\n});\n\n// --- Axes ----------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickFormat((v) => `${v}`));\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"18px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels -----------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"20px\")\n  .text(\"Teaching Method\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"20px\")\n  .text(\"Test Score (%)\");\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"violin-box · javascript · d3 · anyplot.ai\");\n"}