{"spec_id":"box-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// box-basic: Basic Box Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-24\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Deterministic PRNG (LCG) + Box-Muller normal sampler -------------------\nfunction makeRng(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rng = makeRng(42);\nfunction randNormal(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\n// --- Data: exam scores across 5 classes -------------------------------------\nconst classConfig = [\n  { label: \"Class A\", n: 62, mean: 78, std: 9 },\n  { label: \"Class B\", n: 71, mean: 71, std: 12 },\n  { label: \"Class C\", n: 55, mean: 84, std: 6 },\n  { label: \"Class D\", n: 68, mean: 68, std: 14 },\n  { label: \"Class E\", n: 60, mean: 74, std: 10 },\n];\n\nconst dataset = classConfig.map((c) => ({\n  label: c.label,\n  scores: Array.from({ length: c.n }, () =>\n    Math.min(100, Math.max(20, randNormal(c.mean, c.std)))\n  ),\n}));\n\n// Inject a couple of deterministic low-score outliers beyond Class A's so the\n// \"outliers as individual points\" feature reads clearly across multiple categories.\ndataset[1].scores.push(25); // Class B\ndataset[3].scores.push(20); // Class D\n\n// --- Box-plot statistics (median, quartiles, 1.5*IQR whiskers, outliers) ---\nconst boxStats = dataset.map((d) => {\n  const sorted = [...d.scores].sort((a, b) => a - b);\n  const q1 = d3.quantile(sorted, 0.25);\n  const median = d3.quantile(sorted, 0.5);\n  const q3 = d3.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 inliers = sorted.filter((v) => v >= lowerFence && v <= upperFence);\n  const outliers = sorted.filter((v) => v < lowerFence || v > upperFence);\n  return {\n    label: d.label,\n    q1,\n    median,\n    q3,\n    whiskerMin: d3.min(inliers),\n    whiskerMax: d3.max(inliers),\n    outliers,\n  };\n});\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 140, right: 60, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\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\n  .scaleBand()\n  .domain(boxStats.map((d) => d.label))\n  .range([0, iw])\n  .padding(0.35);\n\nconst allValues = boxStats.flatMap((d) => [d.whiskerMin, d.whiskerMax, ...d.outliers]);\nconst y = d3\n  .scaleLinear()\n  .domain([d3.min(allValues) - 5, d3.max(allValues) + 5])\n  .nice()\n  .range([ih, 0]);\n\nconst color = d3.scaleOrdinal().domain(boxStats.map((d) => d.label)).range(t.palette);\nconst boxWidth = Math.min(x.bandwidth(), 140);\n\n// Insight annotations: draw the eye to the tightest vs. the widest distribution\nconst insightNotes = { \"Class C\": \"Tightest, highest scores\", \"Class D\": \"Widest spread\" };\n\n// --- Y-axis gridlines (subtle, y-only) -------------------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Box groups -------------------------------------------------------------\nconst groups = g\n  .selectAll(\".box-group\")\n  .data(boxStats)\n  .join(\"g\")\n  .attr(\"transform\", (d) => `translate(${x(d.label) + x.bandwidth() / 2 - boxWidth / 2},0)`);\n\n// Whisker stem\ngroups\n  .append(\"line\")\n  .attr(\"x1\", boxWidth / 2)\n  .attr(\"x2\", boxWidth / 2)\n  .attr(\"y1\", (d) => y(d.whiskerMin))\n  .attr(\"y2\", (d) => y(d.whiskerMax))\n  .attr(\"stroke\", (d) => color(d.label))\n  .attr(\"stroke-width\", 2.5);\n\n// Whisker caps\nfor (const key of [\"whiskerMin\", \"whiskerMax\"]) {\n  groups\n    .append(\"line\")\n    .attr(\"x1\", boxWidth * 0.25)\n    .attr(\"x2\", boxWidth * 0.75)\n    .attr(\"y1\", (d) => y(d[key]))\n    .attr(\"y2\", (d) => y(d[key]))\n    .attr(\"stroke\", (d) => color(d.label))\n    .attr(\"stroke-width\", 2.5);\n}\n\n// Box (IQR)\ngroups\n  .append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"width\", boxWidth)\n  .attr(\"y\", (d) => y(d.q3))\n  .attr(\"height\", (d) => y(d.q1) - y(d.q3))\n  .attr(\"fill\", (d) => color(d.label))\n  .attr(\"fill-opacity\", 0.35)\n  .attr(\"stroke\", (d) => color(d.label))\n  .attr(\"stroke-width\", (d) => (insightNotes[d.label] ? 3.5 : 2.5));\n\n// Median line\ngroups\n  .append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", boxWidth)\n  .attr(\"y1\", (d) => y(d.median))\n  .attr(\"y2\", (d) => y(d.median))\n  .attr(\"stroke\", (d) => color(d.label))\n  .attr(\"stroke-width\", 3.5);\n\ngroups\n  .filter((d) => insightNotes[d.label])\n  .append(\"text\")\n  .attr(\"x\", boxWidth / 2)\n  .attr(\"y\", (d) => y(d.whiskerMax) - 14)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-style\", \"italic\")\n  .text((d) => insightNotes[d.label]);\n\n// Outliers\ngroups.each(function (d) {\n  d3.select(this)\n    .selectAll(\".outlier\")\n    .data(d.outliers)\n    .join(\"circle\")\n    .attr(\"class\", \"outlier\")\n    .attr(\"cx\", boxWidth / 2)\n    .attr(\"cy\", (v) => y(v))\n    .attr(\"r\", 5)\n    .attr(\"fill\", t.pageBg)\n    .attr(\"stroke\", color(d.label))\n    .attr(\"stroke-width\", 2);\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).ticks(6));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  ax.selectAll(\"line\").remove();\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -78)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Exam Score (%)\");\n\n// --- Title --------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"box-basic · javascript · d3 · anyplot.ai\");\n"}