{"spec_id":"box-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// box-basic: Basic Box Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-24\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + Box-Muller normal sampling ------------------\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction randNormal(mean, sd) {\n  const u1 = Math.max(lcg(), 1e-9);\n  const u2 = lcg();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * sd;\n}\nfunction quantile(sorted, q) {\n  const pos = (sorted.length - 1) * q;\n  const base = Math.floor(pos);\n  const rest = pos - base;\n  return sorted[base + 1] !== undefined\n    ? sorted[base] + rest * (sorted[base + 1] - sorted[base])\n    : sorted[base];\n}\n\n// --- Data: exam scores across 5 classes -------------------------------------\nconst classSpecs = [\n  { label: \"Class A\", mean: 78, sd: 8, n: 60 },\n  { label: \"Class B\", mean: 82, sd: 6, n: 75 },\n  { label: \"Class C\", mean: 70, sd: 10, n: 50 },\n  { label: \"Class D\", mean: 88, sd: 5, n: 90 },\n  { label: \"Class E\", mean: 75, sd: 12, n: 65 },\n];\n\nconst categories = classSpecs.map((spec) => {\n  const scores = Array.from({ length: spec.n }, () =>\n    Math.min(100, Math.max(0, randNormal(spec.mean, spec.sd)))\n  ).sort((a, b) => a - b);\n\n  const q1 = quantile(scores, 0.25);\n  const median = quantile(scores, 0.5);\n  const q3 = quantile(scores, 0.75);\n  const iqr = q3 - q1;\n  const lowerFence = q1 - 1.5 * iqr;\n  const upperFence = q3 + 1.5 * iqr;\n\n  const inRange = scores.filter((v) => v >= lowerFence && v <= upperFence);\n  const outliers = scores.filter((v) => v < lowerFence || v > upperFence);\n\n  return {\n    label: spec.label,\n    q1,\n    median,\n    q3,\n    whiskerMin: inRange[0],\n    whiskerMax: inRange[inRange.length - 1],\n    outliers,\n  };\n});\n\nconst allRangeValues = categories.flatMap((c) => [\n  c.whiskerMin,\n  c.whiskerMax,\n  ...c.outliers,\n]);\nconst dataMin = Math.min(...allRangeValues);\nconst dataMax = Math.max(...allRangeValues);\nconst pad = (dataMax - dataMin) * 0.1;\nconst yMin = Math.floor((dataMin - pad) / 5) * 5;\nconst yMax = Math.ceil((dataMax + pad) / 5) * 5;\n\n// --- Story highlights: call out the strongest and most variable class -------\nconst highestMedianCat = categories.reduce((best, c) =>\n  c.median > best.median ? c : best\n);\nconst widestSpreadCat = categories.reduce((widest, c) =>\n  c.q3 - c.q1 > widest.q3 - widest.q1 ? c : widest\n);\n\n// --- Custom plugin: whiskers, caps, median line, outlier points ------------\nconst boxPlotExtras = {\n  id: \"boxPlotExtras\",\n  afterDatasetsDraw(chart) {\n    const { ctx } = chart;\n    const meta = chart.getDatasetMeta(0);\n    const yScale = chart.scales.y;\n    ctx.save();\n    ctx.lineCap = \"round\";\n\n    categories.forEach((cat, i) => {\n      const bar = meta.data[i];\n      if (!bar) return;\n      const centerX = bar.x;\n      const capHalf = bar.width * 0.3;\n      const yWhiskerMin = yScale.getPixelForValue(cat.whiskerMin);\n      const yWhiskerMax = yScale.getPixelForValue(cat.whiskerMax);\n      const yQ1 = yScale.getPixelForValue(cat.q1);\n      const yQ3 = yScale.getPixelForValue(cat.q3);\n      const yMedian = yScale.getPixelForValue(cat.median);\n\n      // Whiskers\n      ctx.strokeStyle = t.ink;\n      ctx.lineWidth = 2;\n      ctx.beginPath();\n      ctx.moveTo(centerX, yWhiskerMin);\n      ctx.lineTo(centerX, yQ1);\n      ctx.moveTo(centerX, yQ3);\n      ctx.lineTo(centerX, yWhiskerMax);\n      ctx.stroke();\n\n      // Whisker caps\n      ctx.beginPath();\n      ctx.moveTo(centerX - capHalf, yWhiskerMin);\n      ctx.lineTo(centerX + capHalf, yWhiskerMin);\n      ctx.moveTo(centerX - capHalf, yWhiskerMax);\n      ctx.lineTo(centerX + capHalf, yWhiskerMax);\n      ctx.stroke();\n\n      // Median line — page-bg stroke for contrast against the saturated fill\n      ctx.strokeStyle = t.pageBg;\n      ctx.lineWidth = 3;\n      ctx.beginPath();\n      ctx.moveTo(centerX - bar.width / 2, yMedian);\n      ctx.lineTo(centerX + bar.width / 2, yMedian);\n      ctx.stroke();\n\n      // Outliers\n      const color = t.palette[i % t.palette.length];\n      cat.outliers.forEach((value) => {\n        const y = yScale.getPixelForValue(value);\n        ctx.beginPath();\n        ctx.arc(centerX, y, 6, 0, Math.PI * 2);\n        ctx.fillStyle = color;\n        ctx.fill();\n        ctx.lineWidth = 1.5;\n        ctx.strokeStyle = t.pageBg;\n        ctx.stroke();\n      });\n\n      // Story highlight — call out the standout class(es) above its whisker top\n      const topValue = Math.max(cat.whiskerMax, ...cat.outliers, -Infinity);\n      const isHighest = cat === highestMedianCat;\n      const isWidest = cat === widestSpreadCat;\n      if (isHighest || isWidest) {\n        const label =\n          isHighest && isWidest\n            ? \"Highest median · widest spread\"\n            : isHighest\n              ? \"Highest median\"\n              : \"Widest spread (IQR)\";\n        const yTop = yScale.getPixelForValue(topValue);\n        const yLabel = Math.max(yScale.top + 14, yTop - 14);\n        ctx.font = \"600 13px sans-serif\";\n        ctx.fillStyle = t.inkSoft;\n        ctx.textAlign = \"center\";\n        ctx.textBaseline = \"bottom\";\n        ctx.fillText(label, centerX, yLabel);\n      }\n    });\n\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart: floating bars for Q1-Q3, custom plugin for the rest ------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: categories.map((c) => c.label),\n    datasets: [\n      {\n        label: \"Interquartile range\",\n        data: categories.map((c) => [c.q1, c.q3]),\n        backgroundColor: categories.map((_, i) => t.palette[i % t.palette.length]),\n        borderColor: t.ink,\n        borderWidth: 2,\n        borderRadius: 4,\n        barPercentage: 0.45,\n        categoryPercentage: 0.75,\n      },\n    ],\n  },\n  plugins: [boxPlotExtras],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"box-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 26 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        title: { display: true, text: \"Class\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n      },\n      y: {\n        min: yMin,\n        max: yMax,\n        title: {\n          display: true,\n          text: \"Exam Score (0-100 pts)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n    },\n  },\n});\n"}