{"spec_id":"cat-box-strip","library":"muix","language":"javascript","code":"// anyplot.ai\n// cat-box-strip: Box Plot with Strip Overlay\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-02\n//# anyplot-orientation: landscape\n// anyplot.ai\n// cat-box-strip: Box Plot with Strip Overlay\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-02\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Commute time (minutes) by transport mode — synthetic but realistic: bike\n// and walk are tight low-variance distributions, train is fairly consistent,\n// bus has wider spread from stop-to-stop variability, and car carries a right\n// skew from occasional traffic-jam outliers.\nfunction mulberry32(seed) {\n  return function rand() {\n    seed |= 0;\n    seed = (seed + 0x6d2b79f5) | 0;\n    let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;\n    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;\n  };\n}\nfunction gaussian(rand) {\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}\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 N_PER_CATEGORY = 45;\nconst CATEGORY_PARAMS = [\n  { category: \"Bike\", mean: 17, sd: 4, min: 5 },\n  { category: \"Walk\", mean: 21, sd: 5, min: 6 },\n  { category: \"Train\", mean: 26, sd: 5, min: 10 },\n  { category: \"Bus\", mean: 31, sd: 8, min: 9 },\n  { category: \"Car\", mean: 24, sd: 9, min: 7, jamProb: 0.12, jamExtra: 25 },\n];\nconst CATEGORIES = CATEGORY_PARAMS.map((c) => c.category);\n\nconst dataRand = mulberry32(42);\nconst jitterRand = mulberry32(1337);\n\nconst STATS = CATEGORY_PARAMS.map(\n  ({ category, mean, sd, min, jamProb, jamExtra }) => {\n    const values = [];\n    for (let i = 0; i < N_PER_CATEGORY; i += 1) {\n      let v = mean + gaussian(dataRand) * sd;\n      if (jamProb && dataRand() < jamProb) v += jamExtra + dataRand() * jamExtra;\n      values.push(Math.max(min, Math.round(v * 10) / 10));\n    }\n    const jittered = values.map((value) => ({\n      value,\n      offset: jitterRand() - 0.5,\n    }));\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 lowFence = q1 - 1.5 * iqr;\n    const highFence = q3 + 1.5 * iqr;\n    const inFence = sorted.filter((v) => v >= lowFence && v <= highFence);\n    const whiskerLow = inFence.length ? inFence[0] : sorted[0];\n    const whiskerHigh = inFence.length ? inFence[inFence.length - 1] : sorted[sorted.length - 1];\n    return {\n      category,\n      jittered,\n      q1,\n      median,\n      q3,\n      whiskerLow,\n      whiskerHigh,\n      outliers: sorted.filter((v) => v < whiskerLow || v > whiskerHigh),\n    };\n  },\n);\n\nconst ALL_VALUES = STATS.flatMap((s) => s.jittered.map((p) => p.value));\nconst RAW_MIN = Math.min(...ALL_VALUES);\nconst RAW_MAX = Math.max(...ALL_VALUES);\nconst PAD = (RAW_MAX - RAW_MIN) * 0.1;\nconst Y_MIN = Math.max(0, Math.floor((RAW_MIN - PAD) / 5) * 5);\nconst Y_MAX = Math.ceil((RAW_MAX + PAD) / 5) * 5;\n\n// --- Custom layer: whiskers, box, median, jittered strip points -------------\n// MUI X community has no boxplot series type, so the box + whisker + strip\n// geometry is drawn directly from the chart's own band/linear scales via\n// useXScale/useYScale — the same composition pattern the harness expects for\n// chart types outside the built-in series set.\nfunction BoxStripLayer() {\n  const xScale = useXScale(\"x\");\n  const yScale = useYScale(\"y\");\n  if (!xScale || !yScale) return null;\n\n  const bandwidth = (xScale as any).bandwidth();\n  const boxWidth = bandwidth * 0.34;\n  const capHalf = boxWidth * 0.3;\n  const jitterSpread = bandwidth * 0.3;\n\n  return (\n    <g>\n      {STATS.map((s) => {\n        const cx = (xScale as any)(s.category) + bandwidth / 2;\n        const yLow = (yScale as any)(s.whiskerLow);\n        const yQ1 = (yScale as any)(s.q1);\n        const yQ3 = (yScale as any)(s.q3);\n        const yHigh = (yScale as any)(s.whiskerHigh);\n        const yMed = (yScale as any)(s.median);\n        return (\n          <g key={s.category}>\n            <line x1={cx} y1={yLow} x2={cx} y2={yQ1} stroke={t.palette[0]} strokeWidth={2} strokeOpacity={0.85} />\n            <line x1={cx} y1={yQ3} x2={cx} y2={yHigh} stroke={t.palette[0]} strokeWidth={2} strokeOpacity={0.85} />\n            <line x1={cx - capHalf} y1={yLow} x2={cx + capHalf} y2={yLow} stroke={t.palette[0]} strokeWidth={2} strokeOpacity={0.85} />\n            <line x1={cx - capHalf} y1={yHigh} x2={cx + capHalf} y2={yHigh} stroke={t.palette[0]} strokeWidth={2} strokeOpacity={0.85} />\n            <rect\n              x={cx - boxWidth / 2}\n              y={yQ3}\n              width={boxWidth}\n              height={Math.max(1, yQ1 - yQ3)}\n              fill={t.palette[0]}\n              fillOpacity={0.14}\n              stroke={t.palette[0]}\n              strokeWidth={2.5}\n            />\n            <line x1={cx - boxWidth / 2} y1={yMed} x2={cx + boxWidth / 2} y2={yMed} stroke={t.ink} strokeWidth={3} strokeLinecap=\"round\" />\n          </g>\n        );\n      })}\n      {STATS.map((s) => {\n        const cx = (xScale as any)(s.category) + bandwidth / 2;\n        return s.jittered.map((p, i) => (\n          <circle\n            key={`${s.category}-${i}`}\n            cx={cx + p.offset * jitterSpread}\n            cy={(yScale as any)(p.value)}\n            r={5}\n            fill={t.palette[0]}\n            fillOpacity={0.42}\n            stroke={t.pageBg}\n            strokeWidth={0.75}\n          />\n        ));\n      })}\n      {(() => {\n        // Callout on the category whose outliers reach furthest above its\n        // whisker cap — sharpens the story instead of a fixed, hardcoded label.\n        const withOutliers = STATS.filter((s) => s.outliers.length > 0);\n        if (!withOutliers.length) return null;\n        const target = withOutliers.reduce((a, b) =>\n          Math.max(...b.outliers) > Math.max(...a.outliers) ? b : a,\n        );\n        const topOutlier = Math.max(...target.outliers);\n        const cx = (xScale as any)(target.category) + bandwidth / 2;\n        const cyOutlier = (yScale as any)(topOutlier);\n        const labelY = cyOutlier - 30;\n        return (\n          <g>\n            <line\n              x1={cx}\n              y1={cyOutlier - 8}\n              x2={cx}\n              y2={labelY + 8}\n              stroke={t.inkSoft}\n              strokeWidth={1}\n              strokeDasharray=\"2,3\"\n            />\n            <text\n              x={cx}\n              y={labelY}\n              textAnchor=\"middle\"\n              fontSize={13}\n              fontStyle=\"italic\"\n              fill={t.inkSoft}\n            >\n              traffic-jam outliers\n            </text>\n          </g>\n        );\n      })()}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n\n  const title =\n    \"Commute Time by Transport Mode · cat-box-strip · javascript · muix · anyplot.ai\";\n  const titleSize = title.length > 70 ? Math.round(24 * (70 / title.length)) : 24;\n  const subtitle =\n    \"Box: median, Q1–Q3, whiskers to 1.5×IQR · dots: individual commutes (jittered), n=45 per mode\";\n\n  return (\n    <ChartContainer\n      width={W}\n      height={H}\n      series={[]}\n      skipAnimation\n      xAxis={[\n        {\n          id: \"x\",\n          scaleType: \"band\",\n          data: CATEGORIES,\n          label: \"Transport Mode\",\n          labelStyle: { fontSize: 16, fill: t.inkSoft },\n          tickLabelStyle: { fontSize: 15, fill: t.inkSoft },\n        },\n      ]}\n      yAxis={[\n        {\n          id: \"y\",\n          scaleType: \"linear\",\n          min: Y_MIN,\n          max: Y_MAX,\n          label: \"Commute Time (minutes)\",\n          labelStyle: { fontSize: 16, fill: t.inkSoft },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        },\n      ]}\n      margin={{ top: 130, bottom: 90, left: 110, right: 60 }}\n    >\n      <ChartsGrid horizontal />\n      <BoxStripLayer />\n      <ChartsXAxis axisId=\"x\" />\n      <ChartsYAxis axisId=\"y\" />\n      <text\n        x={W / 2}\n        y={44}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fontSize={titleSize}\n        fontWeight={600}\n        fill={t.ink}\n        fontFamily=\"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif\"\n      >\n        {title}\n      </text>\n      <text\n        x={W / 2}\n        y={78}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fontSize={16}\n        fontStyle=\"italic\"\n        fill={t.inkSoft}\n        fontFamily=\"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif\"\n      >\n        {subtitle}\n      </text>\n    </ChartContainer>\n  );\n}\n"}