{"spec_id":"box-grouped","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// box-grouped: Grouped Box Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/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 departments = [\"Engineering\", \"Sales\", \"Support\", \"Operations\"];\nconst levels = [\"Junior\", \"Mid-level\", \"Senior\"];\n\n// Baseline productivity score per department, plus a per-level offset and\n// spread — seniority raises the median and tightens the spread.\nconst deptBase = { Engineering: 58, Sales: 66, Support: 60, Operations: 63 };\nconst levelOffset = { Junior: 0, \"Mid-level\": 12, Senior: 21 };\nconst levelStd = { Junior: 10, \"Mid-level\": 8.5, Senior: 6.5 };\nconst sampleSize = 45;\n\nconst rawSamples = departments.map((dept) =>\n  levels.map((level) => {\n    const mean = deptBase[dept] + levelOffset[level];\n    const std = levelStd[level];\n    const values = [];\n    for (let j = 0; j < sampleSize; j++) {\n      values.push(Math.round(randNormal(mean, std) * 10) / 10);\n    }\n    return values;\n  })\n);\n\n// A few realistic outliers: a struggling senior, a standout junior.\nrawSamples[0][2].push(94.2);\nrawSamples[1][0].push(91.5);\nrawSamples[2][1].push(35.8);\nrawSamples[3][2].push(45.1);\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((deptSamples) =>\n  deptSamples.map((values) => {\n    const sorted = values.slice().sort((a, b) => a - b);\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    return {\n      q1,\n      median,\n      q3,\n      whiskerLow: Math.min(...inFence),\n      whiskerHigh: Math.max(...inFence),\n      outliers,\n    };\n  })\n);\n\nconst allValues = rawSamples.flat(2);\nconst valueRange = Math.max(...allValues) - Math.min(...allValues);\nconst yPad = valueRange * 0.06;\nconst yMin = Math.min(...allValues) - yPad;\nconst yMax = Math.max(...allValues) + yPad;\n\n// --- Chart -------------------------------------------------------------------\nlet customGroup = null;\n\nfunction drawLine(renderer, group, x1, y1, x2, y2, color, width) {\n  renderer\n    .path([\"M\", x1, y1, \"L\", x2, y2])\n    .attr({ \"stroke-width\": width, stroke: color, zIndex: 3 })\n    .add(group);\n}\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(\"grouped-boxes\").add();\n\n        const xAxis = this.xAxis[0];\n        const yAxis = this.yAxis[0];\n        const nSub = levels.length;\n        const slot = Math.abs(xAxis.toPixels(1, false) - xAxis.toPixels(0, false));\n        const groupWidth = slot * 0.72;\n        const boxWidth = (groupWidth / nSub) * 0.78;\n        const capWidth = boxWidth * 0.55;\n\n        boxStats.forEach((deptStats, i) => {\n          const xc0 = xAxis.toPixels(i, false);\n\n          deptStats.forEach((s, k) => {\n            const color = t.palette[k % t.palette.length];\n            const offset = (k - (nSub - 1) / 2) * (groupWidth / nSub);\n            const xc = xc0 + offset;\n            const left = xc - boxWidth / 2;\n\n            // Senior (last level) gets a bolder stroke to call out the\n            // seniority -> productivity trend the data tells.\n            const isSenior = k === nSub - 1;\n            const lineWidth = isSenior ? 3 : 2;\n            const medianWidth = isSenior ? 3.5 : 2.5;\n            const fillOpacity = isSenior ? 0.58 : 0.45;\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 yWhiskerLow = yAxis.toPixels(s.whiskerLow, false);\n            const yWhiskerHigh = yAxis.toPixels(s.whiskerHigh, false);\n\n            // Whiskers + caps\n            drawLine(this.renderer, customGroup, xc, yQ3, xc, yWhiskerHigh, color, lineWidth);\n            drawLine(this.renderer, customGroup, xc, yQ1, xc, yWhiskerLow, color, lineWidth);\n            drawLine(\n              this.renderer, customGroup,\n              xc - capWidth / 2, yWhiskerHigh, xc + capWidth / 2, yWhiskerHigh,\n              color, lineWidth\n            );\n            drawLine(\n              this.renderer, customGroup,\n              xc - capWidth / 2, yWhiskerLow, xc + capWidth / 2, yWhiskerLow,\n              color, lineWidth\n            );\n\n            // Box body (Q1–Q3)\n            const r = Math.min(4, boxWidth * 0.12);\n            this.renderer\n              .rect(left, yQ3, boxWidth, yQ1 - yQ3, r)\n              .attr({\n                fill: Highcharts.color(color).setOpacity(fillOpacity).get(),\n                stroke: color,\n                \"stroke-width\": lineWidth,\n                zIndex: 4,\n              })\n              .add(customGroup);\n\n            // Median line\n            drawLine(this.renderer, customGroup, left, yMed, left + boxWidth, yMed, t.ink, medianWidth);\n\n            // Outliers\n            s.outliers.forEach((v) => {\n              this.renderer\n                .circle(xc, yAxis.toPixels(v, false), 6.5)\n                .attr({ fill: t.pageBg, stroke: color, \"stroke-width\": 2, zIndex: 6 })\n                .add(customGroup);\n            });\n          });\n        });\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"box-grouped · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    categories: departments,\n    title: { text: \"Department\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineWidth: 1,\n    lineColor: t.grid,\n    tickWidth: 0,\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: \"Productivity Score\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    gridLineColor: t.grid,\n    lineWidth: 0,\n    tickWidth: 0,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    title: { text: \"Experience Level\", style: { color: t.inkSoft, fontSize: \"14px\" } },\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: { enabled: false },\n  plotOptions: {\n    series: { animation: false },\n    column: {\n      opacity: 0,\n      borderWidth: 0,\n      enableMouseTracking: false,\n      states: { hover: { enabled: false } },\n    },\n  },\n  series: levels.map((level, k) => ({\n    type: \"column\",\n    name: level,\n    color: t.palette[k % t.palette.length],\n    data: boxStats.map((deptStats) => deptStats[k].median),\n    showInLegend: true,\n  })),\n});\n"}