{"spec_id":"cat-box-strip","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// cat-box-strip: Box Plot with Strip Overlay\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Commute time (minutes) by transportation mode. Sample sizes differ across\n// groups on purpose (comparing groups with sample size awareness).\nfunction lcg(seed) {\n  let state = seed;\n  return function () {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nfunction randNormal() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\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 groups = [\n  { name: \"Car\", n: 80, mean: 28, std: 6 },\n  { name: \"Bus\", n: 65, mean: 42, std: 10 },\n  { name: \"Bike\", n: 50, mean: 22, std: 5 },\n  { name: \"Walk\", n: 35, mean: 48, std: 8 },\n];\nconst categories = groups.map((g) => g.name);\n\nconst samples = groups.map((g) => {\n  const values = [];\n  for (let i = 0; i < g.n; i++) {\n    values.push(Math.max(4, Math.round(g.mean + g.std * randNormal())));\n  }\n  return values;\n});\n// A couple of delayed-bus outliers, kept alongside the full box context.\nsamples[1].push(95, 91);\n\nconst stats = samples.map((values) => {\n  const sorted = [...values].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 lowerFence = q1 - 1.5 * iqr;\n  const upperFence = q3 + 1.5 * iqr;\n  const inFence = sorted.filter((v) => v >= lowerFence && v <= upperFence);\n  return {\n    q1,\n    median,\n    q3,\n    whiskerLow: inFence[0],\n    whiskerHigh: inFence[inFence.length - 1],\n  };\n});\n\n// --- Box + whiskers, drawn with the core SVG renderer -----------------------\n// highcharts-more (boxplot/errorbar series) isn't loaded in this bundle, so\n// the box and whiskers are built from primitive shapes anchored to real axis\n// coordinates, using the actual quartile/whisker statistics computed above.\nfunction drawBoxes(chart) {\n  const xAxis = chart.xAxis[0];\n  const yAxis = chart.yAxis[0];\n  const categoryPx = xAxis.toPixels(1) - xAxis.toPixels(0);\n  const halfWidth = categoryPx * 0.22;\n\n  stats.forEach((s, i) => {\n    const color = t.palette[i % t.palette.length];\n    const cx = xAxis.toPixels(i);\n    const q1Y = yAxis.toPixels(s.q1);\n    const q3Y = yAxis.toPixels(s.q3);\n    const medianY = yAxis.toPixels(s.median);\n    const lowY = yAxis.toPixels(s.whiskerLow);\n    const highY = yAxis.toPixels(s.whiskerHigh);\n\n    chart.renderer\n      .path([\"M\", cx, highY, \"L\", cx, q3Y])\n      .attr({ \"stroke-width\": 2, stroke: t.inkSoft, zIndex: 2 })\n      .add();\n    chart.renderer\n      .path([\"M\", cx, q1Y, \"L\", cx, lowY])\n      .attr({ \"stroke-width\": 2, stroke: t.inkSoft, zIndex: 2 })\n      .add();\n    chart.renderer\n      .path([\"M\", cx - halfWidth, highY, \"L\", cx + halfWidth, highY])\n      .attr({ \"stroke-width\": 2, stroke: t.inkSoft, zIndex: 2 })\n      .add();\n    chart.renderer\n      .path([\"M\", cx - halfWidth, lowY, \"L\", cx + halfWidth, lowY])\n      .attr({ \"stroke-width\": 2, stroke: t.inkSoft, zIndex: 2 })\n      .add();\n    chart.renderer\n      .rect(cx - halfWidth, Math.min(q1Y, q3Y), halfWidth * 2, Math.abs(q1Y - q3Y))\n      .attr({\n        fill: color,\n        \"fill-opacity\": 0.16,\n        stroke: color,\n        \"stroke-width\": 2.5,\n        zIndex: 3,\n      })\n      .add();\n    chart.renderer\n      .path([\"M\", cx - halfWidth, medianY, \"L\", cx + halfWidth, medianY])\n      .attr({ \"stroke-width\": 3, stroke: color, zIndex: 4 })\n      .add();\n  });\n}\n\n// --- Strip overlay (jittered scatter, drawn above the box) ------------------\nconst stripSeries = samples.map((values, i) => {\n  const color = t.palette[i % t.palette.length];\n  return {\n    type: \"scatter\",\n    name: categories[i],\n    data: values.map((v) => [i + (rand() - 0.5) * 0.64, v]),\n    color: color,\n    marker: {\n      radius: 4,\n      lineWidth: 1,\n      lineColor: t.pageBg,\n      fillColor: Highcharts.color(color).setOpacity(0.55).get(\"rgba\"),\n    },\n    zIndex: 5,\n    showInLegend: false,\n    states: { hover: { enabled: false } },\n  };\n});\n\n// --- Chart --------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      load: function () {\n        drawBoxes(this);\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"cat-box-strip · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    categories: categories,\n    min: -0.5,\n    max: categories.length - 0.5,\n    tickPositions: categories.map((_, i) => i),\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    title: {\n      text: \"Transportation Mode\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n  },\n  yAxis: {\n    title: {\n      text: \"Commute Time (minutes)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: { enabled: false },\n  tooltip: {\n    pointFormat: \"Commute: <b>{point.y:.0f} min</b>\",\n  },\n  plotOptions: {\n    series: { animation: false },\n  },\n  series: stripSeries,\n});\n"}