{"spec_id":"box-notched","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// box-notched: Notched Box Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-18\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function lcg() {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\n\nfunction randNormal(mean, std) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\nconst categories = [\"Batch A\", \"Batch B\", \"Batch C\", \"Batch D\", \"Batch E\"];\nconst meanLifespans = [1180, 1155, 1240, 1150, 1205];\nconst stdLifespans = [55, 60, 50, 65, 58];\nconst sampleSizes = [50, 32, 60, 28, 45];\n\nconst rawSamples = categories.map((name, i) => {\n  const values = [];\n  for (let j = 0; j < sampleSizes[i]; j++) {\n    values.push(Math.round(randNormal(meanLifespans[i], stdLifespans[i])));\n  }\n  return values;\n});\n\n// A handful of realistic outliers: an early failure and a long-lived unit\nrawSamples[0].push(1430);\nrawSamples[2].push(1015);\nrawSamples[3].push(980, 1395);\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\nconst boxStats = rawSamples.map((values) => {\n  const sorted = values.slice().sort((a, b) => a - b);\n  const n = sorted.length;\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 lowFence = q1 - 1.5 * iqr;\n  const highFence = q3 + 1.5 * iqr;\n  const inFence = sorted.filter((v) => v >= lowFence && v <= highFence);\n  const outliers = sorted.filter((v) => v < lowFence || v > highFence);\n  const notch = (1.57 * iqr) / Math.sqrt(n);\n  return {\n    q1,\n    median,\n    q3,\n    whiskerLow: Math.min(...inFence),\n    whiskerHigh: Math.max(...inFence),\n    outliers,\n    notchTop: Math.min(median + notch, q3),\n    notchBottom: Math.max(median - notch, q1),\n  };\n});\n\nconst allValues = rawSamples.flat();\nconst valueRange = Math.max(...allValues) - Math.min(...allValues);\nconst yPad = valueRange * 0.05;\nconst yMin = Math.min(...allValues) - yPad;\nconst yMax = Math.max(...allValues) + yPad;\n\n// Pick the pair of batches with the widest non-overlapping notch gap - the\n// clearest \"significantly different medians\" claim - to call out visually.\nfunction findMostSignificantPair(stats) {\n  let best = null;\n  for (let i = 0; i < stats.length; i++) {\n    for (let j = i + 1; j < stats.length; j++) {\n      const a = stats[i], b = stats[j];\n      let gap = null;\n      if (a.notchTop < b.notchBottom) gap = b.notchBottom - a.notchTop;\n      else if (b.notchTop < a.notchBottom) gap = a.notchBottom - b.notchTop;\n      if (gap !== null && (!best || gap > best.gap)) best = { i, j, gap };\n    }\n  }\n  return best;\n}\nconst highlightPair = findMostSignificantPair(boxStats);\n\n// --- Chart -------------------------------------------------------------------\nlet customGroup = null;\n\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      render() {\n        if (customGroup) customGroup.destroy();\n        customGroup = this.renderer.g(\"notched-boxes\").add();\n\n        const xAxis = this.xAxis[0];\n        const yAxis = this.yAxis[0];\n        const slot = Math.abs(xAxis.toPixels(1, false) - xAxis.toPixels(0, false));\n        const boxWidth = slot * 0.5;\n        const notchInset = boxWidth * 0.3;\n        const capWidth = boxWidth * 0.4;\n\n        boxStats.forEach((s, i) => {\n          const color = t.palette[i % t.palette.length];\n          const xc = xAxis.toPixels(i, false);\n          const left = xc - boxWidth / 2;\n          const right = xc + boxWidth / 2;\n\n          const yQ1 = yAxis.toPixels(s.q1, false);\n          const yQ3 = yAxis.toPixels(s.q3, false);\n          const yMed = yAxis.toPixels(s.median, false);\n          const yNotchTop = yAxis.toPixels(s.notchTop, false);\n          const yNotchBottom = yAxis.toPixels(s.notchBottom, false);\n          const yWhiskerLow = yAxis.toPixels(s.whiskerLow, false);\n          const yWhiskerHigh = yAxis.toPixels(s.whiskerHigh, false);\n\n          // Whiskers + caps\n          this.renderer\n            .path([\"M\", xc, yQ3, \"L\", xc, yWhiskerHigh])\n            .attr({ \"stroke-width\": 2, stroke: color, zIndex: 3 })\n            .add(customGroup);\n          this.renderer\n            .path([\"M\", xc, yQ1, \"L\", xc, yWhiskerLow])\n            .attr({ \"stroke-width\": 2, stroke: color, zIndex: 3 })\n            .add(customGroup);\n          this.renderer\n            .path([\"M\", xc - capWidth / 2, yWhiskerHigh, \"L\", xc + capWidth / 2, yWhiskerHigh])\n            .attr({ \"stroke-width\": 2, stroke: color, zIndex: 3 })\n            .add(customGroup);\n          this.renderer\n            .path([\"M\", xc - capWidth / 2, yWhiskerLow, \"L\", xc + capWidth / 2, yWhiskerLow])\n            .attr({ \"stroke-width\": 2, stroke: color, zIndex: 3 })\n            .add(customGroup);\n\n          // Notched box body — hourglass waist marks the median CI, with\n          // gently rounded outer corners for a more polished, less \"default\"\n          // silhouette.\n          const r = Math.min(6, boxWidth * 0.15, notchInset * 0.6);\n          this.renderer\n            .path([\n              \"M\", left + r, yQ3,\n              \"L\", right - r, yQ3,\n              \"A\", r, r, 0, 0, 1, right, yQ3 + r,\n              \"L\", right, yNotchTop,\n              \"L\", right - notchInset, yMed,\n              \"L\", right, yNotchBottom,\n              \"L\", right, yQ1 - r,\n              \"A\", r, r, 0, 0, 1, right - r, yQ1,\n              \"L\", left + r, yQ1,\n              \"A\", r, r, 0, 0, 1, left, yQ1 - r,\n              \"L\", left, yNotchBottom,\n              \"L\", left + notchInset, yMed,\n              \"L\", left, yNotchTop,\n              \"L\", left, yQ3 + r,\n              \"A\", r, r, 0, 0, 1, left + r, yQ3,\n              \"Z\",\n            ])\n            .attr({\n              fill: Highcharts.color(color).setOpacity(0.4).get(),\n              stroke: color,\n              \"stroke-width\": 2,\n              zIndex: 4,\n            })\n            .shadow({ color: t.ink, offsetX: 0, offsetY: 2, opacity: 0.15, width: 3 })\n            .add(customGroup);\n\n          // Median line through the notch waist\n          this.renderer\n            .path([\"M\", left + notchInset, yMed, \"L\", right - notchInset, yMed])\n            .attr({ \"stroke-width\": 2.5, stroke: t.ink, zIndex: 5 })\n            .add(customGroup);\n\n          // Outliers\n          s.outliers.forEach((v) => {\n            this.renderer\n              .circle(xc, yAxis.toPixels(v, false), 7.5)\n              .attr({ fill: t.pageBg, stroke: color, \"stroke-width\": 2, zIndex: 6 })\n              .add(customGroup);\n          });\n        });\n\n        // Significance bracket: make the \"non-overlapping notches\" claim\n        // visible directly on the chart for the most clearly separated pair.\n        if (highlightPair) {\n          const { i, j } = highlightPair;\n          const x1 = xAxis.toPixels(i, false);\n          const x2 = xAxis.toPixels(j, false);\n          const topPixel = Math.min(\n            yAxis.toPixels(boxStats[i].whiskerHigh, false),\n            yAxis.toPixels(boxStats[j].whiskerHigh, false)\n          );\n          const barY = topPixel - 34;\n          const tick = 12;\n\n          this.renderer\n            .path([\"M\", x1, barY + tick, \"L\", x1, barY, \"L\", x2, barY, \"L\", x2, barY + tick])\n            .attr({ \"stroke-width\": 1.5, stroke: t.inkSoft, zIndex: 7, fill: \"none\" })\n            .add(customGroup);\n          this.renderer\n            .text(\"significant (95% CI)\", (x1 + x2) / 2, barY - 8)\n            .attr({ align: \"center\", zIndex: 7 })\n            .css({ color: t.ink, fontSize: \"13px\", fontWeight: \"600\" })\n            .add(customGroup);\n        }\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"box-notched · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Non-overlapping notches indicate significantly different medians (95% CI)\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    categories,\n    title: { text: \"Manufacturing Batch\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    min: yMin,\n    max: yMax,\n    startOnTick: false,\n    endOnTick: false,\n    title: { text: \"Bulb Lifespan (hours)\", 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: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  series: [\n    {\n      type: \"scatter\",\n      name: \"Median\",\n      data: boxStats.map((s, i) => ({ x: i, y: s.median })),\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n  ],\n});\n"}