{"spec_id":"box-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// box-basic: Basic Box Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-24\n\n// The core bundle (no highcharts-more) has no \"boxplot\" series type, so the box\n// is built from two stacked \"column\" series (an invisible floor up to Q1, then\n// a visible box from Q1 to Q3, colorByPoint from the Imprint palette). Whiskers\n// and the median are drawn as two extra core \"line\" series (null-separated\n// segments, one per category) rather than custom chart.renderer shapes — this\n// Highcharts build paints renderer.path() additions behind the column bars\n// they overlap, but a normal series defined after the box series always layers\n// on top, which is what a median mark sitting inside the box needs.\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction randNormal(mean, std) {\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 * std;\n}\nfunction quartile(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}\nfunction boxStats(values) {\n  const sorted = [...values].sort((a, b) => a - b);\n  const q1 = quartile(sorted, 0.25);\n  const median = quartile(sorted, 0.5);\n  const q3 = quartile(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: inRange[0],\n    whiskerHigh: inRange[inRange.length - 1],\n    outliers: sorted.filter((v) => v < lowerFence || v > upperFence),\n  };\n}\n\nconst classes = [\n  { name: \"Algebra I\", mean: 78, std: 9, n: 65 },\n  { name: \"Geometry\", mean: 74, std: 11, n: 72 },\n  { name: \"Algebra II\", mean: 81, std: 7, n: 58 },\n  { name: \"Precalculus\", mean: 71, std: 13, n: 80 },\n  { name: \"Calculus\", mean: 85, std: 8, n: 54 },\n];\nconst categories = classes.map((c) => c.name);\nconst stats = {};\nclasses.forEach((c) => {\n  const scores = Array.from({ length: c.n }, () =>\n    Math.min(100, Math.max(0, randNormal(c.mean, c.std)))\n  );\n  stats[c.name] = boxStats(scores);\n});\n\n// Zoom the axis to the data's actual spread instead of forcing a 0 baseline —\n// the stacked-column technique below still stacks from a true 0 threshold\n// internally, so this only crops the (invisible) empty space beneath it.\nconst allBounds = classes.flatMap((c) => [\n  stats[c.name].whiskerLow,\n  stats[c.name].whiskerHigh,\n  ...stats[c.name].outliers,\n]);\nconst yMin = Math.max(0, Math.floor((Math.min(...allBounds) - 5) / 5) * 5);\nconst yMax = Math.min(100, Math.ceil((Math.max(...allBounds) + 5) / 5) * 5);\n\n// Storytelling: call out the widest- and tightest-spread classes via\n// core-Highcharts xAxis.plotBands + label (no annotations module needed) —\n// computed from the data, not hardcoded, so the highlight stays correct if\n// the class parameters above change.\nconst spreads = categories.map((c) => stats[c].whiskerHigh - stats[c].whiskerLow);\nconst widestIdx = spreads.indexOf(Math.max(...spreads));\nconst tightestIdx = spreads.indexOf(Math.min(...spreads));\n\n// Whiskers (box edge → fence) and caps, one null-separated \"line\" series for\n// every category so a single series draws all the disconnected segments.\n// Every entry (including gaps) carries an explicit x so a mixed array of real\n// points and null-y gap markers can't be re-bucketed by an auto pointStart/\n// pointInterval index — only the y:null gaps break the connecting line.\nconst capHalf = 0.13;\nconst whiskerData = categories.flatMap((c, i) => {\n  const s = stats[c];\n  return [\n    { x: i, y: s.q3 }, { x: i, y: s.whiskerHigh }, { x: i, y: null },\n    { x: i - capHalf, y: s.whiskerHigh }, { x: i + capHalf, y: s.whiskerHigh }, { x: i, y: null },\n    { x: i, y: s.q1 }, { x: i, y: s.whiskerLow }, { x: i, y: null },\n    { x: i - capHalf, y: s.whiskerLow }, { x: i + capHalf, y: s.whiskerLow }, { x: i, y: null },\n  ];\n});\n// Median line spans the same half-width as the box itself (matches the\n// pointPadding/groupPadding below), drawn as its own series so it renders on\n// top of the box fill instead of being covered by it.\nconst boxHalf = 0.24;\nconst medianData = categories.flatMap((c, i) => [\n  { x: i - boxHalf, y: stats[c].median },\n  { x: i + boxHalf, y: stats[c].median },\n  { x: i, y: null },\n]);\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"column\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"box-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    categories,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    title: { text: \"Class\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    plotBands: [\n      {\n        from: widestIdx - 0.5,\n        to: widestIdx + 0.5,\n        color: `${t.inkSoft}14`,\n        label: {\n          text: \"Widest spread\",\n          align: \"center\",\n          verticalAlign: \"top\",\n          y: 12,\n          style: { color: t.inkSoft, fontSize: \"12px\", fontStyle: \"italic\" },\n        },\n      },\n      {\n        from: tightestIdx - 0.5,\n        to: tightestIdx + 0.5,\n        color: `${t.inkSoft}14`,\n        label: {\n          text: \"Most consistent\",\n          align: \"center\",\n          verticalAlign: \"top\",\n          y: 12,\n          style: { color: t.inkSoft, fontSize: \"12px\", fontStyle: \"italic\" },\n        },\n      },\n    ],\n  },\n  yAxis: {\n    min: yMin,\n    max: yMax,\n    reversedStacks: false, // keep series[0] (invisible floor) at the bottom of the stack\n    title: { text: \"Final Exam Score (%)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: { enabled: false },\n  tooltip: { outside: false },\n  plotOptions: {\n    column: {\n      stacking: \"normal\",\n      pointPadding: 0.12,\n      groupPadding: 0.18,\n      borderRadius: 0,\n      animation: false,\n    },\n    series: { animation: false },\n  },\n  series: [\n    {\n      name: \"Floor\",\n      data: categories.map((c) => +stats[c].q1.toFixed(2)),\n      color: \"transparent\",\n      borderWidth: 0,\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      name: \"Interquartile range\",\n      data: categories.map((c) => +(stats[c].q3 - stats[c].q1).toFixed(2)),\n      colorByPoint: true,\n      borderColor: t.inkSoft,\n      borderWidth: 1.25,\n      borderRadius: 2,\n      showInLegend: false,\n      tooltip: {\n        pointFormatter: function () {\n          const s = stats[categories[this.index]];\n          return (\n            `<b>${categories[this.index]}</b><br/>` +\n            `Max: ${s.whiskerHigh.toFixed(1)}<br/>Q3: ${s.q3.toFixed(1)}<br/>` +\n            `Median: ${s.median.toFixed(1)}<br/>Q1: ${s.q1.toFixed(1)}<br/>` +\n            `Min: ${s.whiskerLow.toFixed(1)}`\n          );\n        },\n      },\n    },\n    {\n      type: \"line\",\n      name: \"Whiskers\",\n      data: whiskerData,\n      color: t.inkSoft,\n      lineWidth: 1.5,\n      linecap: \"round\",\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      type: \"line\",\n      name: \"Median\",\n      data: medianData,\n      color: t.pageBg,\n      lineWidth: 3,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      type: \"scatter\",\n      name: \"Outliers\",\n      data: classes.flatMap((c, i) =>\n        stats[c.name].outliers.map((value) => ({\n          x: i,\n          y: +value.toFixed(1),\n          color: t.palette[i % t.palette.length],\n        }))\n      ),\n      marker: { radius: 4.5, symbol: \"circle\", lineWidth: 1, lineColor: t.pageBg, fillOpacity: 0.85 },\n      showInLegend: false,\n      tooltip: { pointFormat: \"Outlier: <b>{point.y:.1f}</b>\" },\n    },\n  ],\n});\n"}