{"spec_id":"raincloud-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// raincloud-basic: Basic Raincloud Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-08-26\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif\";\n\n// --- Data: reaction times (ms) across a 4-arm drug trial ---------------------\n// Deterministic LCG so the sampled distributions (and their KDEs) are stable\n// across renders — the browser has no seeded Math.random().\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function next() {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\nfunction gaussianSample() {\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\nconst CATEGORIES = [\"Placebo\", \"10 mg Dose\", \"25 mg Dose\", \"50 mg Dose\"];\nconst N_PER_GROUP = 70;\n\n// The 50 mg arm is deliberately bimodal (fast responders vs. non-responders)\n// — exactly the shape a plain box plot would hide and a raincloud reveals.\nconst valuesByCategory = [\n  Array.from({ length: N_PER_GROUP }, () => 540 + 55 * gaussianSample()),\n  Array.from({ length: N_PER_GROUP }, () => 495 + 50 * gaussianSample()),\n  Array.from({ length: N_PER_GROUP }, () => 445 + 48 * gaussianSample()),\n  Array.from({ length: N_PER_GROUP }, () =>\n    rand() < 0.55 ? 360 + 35 * gaussianSample() : 470 + 40 * gaussianSample()\n  ),\n];\n// Jitter fractions for the \"rain\", drawn from the same deterministic stream\n// right after the data so the whole script's randomness stays reproducible.\nconst jittersByCategory = valuesByCategory.map((values) => values.map(() => rand()));\n\nconst allValues = valuesByCategory.flat();\nconst dataMin = Math.min(...allValues);\nconst dataMax = Math.max(...allValues);\nconst xPad = (dataMax - dataMin) * 0.08;\nconst X_MIN = dataMin - xPad;\nconst X_MAX = dataMax + xPad;\n\n// --- Box-plot summary stats (Tukey whiskers, 1.5×IQR) ------------------------\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 ? sorted[base] + rest * (sorted[base + 1] - sorted[base]) : sorted[base];\n}\nfunction boxStats(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 lowerWhisker = sorted.find((v) => v >= lowerFence) ?? sorted[0];\n  const upperWhisker = [...sorted].reverse().find((v) => v <= upperFence) ?? sorted[sorted.length - 1];\n  return { q1, median, q3, lowerWhisker, upperWhisker };\n}\nconst statsByCategory = valuesByCategory.map(boxStats);\n\n// --- Gaussian KDE per group, Silverman bandwidth, normalized to its own peak\n// so the \"cloud\" shows shape (including the 50 mg arm's two humps), not raw n.\nconst GRID_N = 140;\nconst grid = Array.from({ length: GRID_N }, (_, k) => X_MIN + (k * (X_MAX - X_MIN)) / (GRID_N - 1));\nfunction stdOf(values) {\n  const m = values.reduce((a, b) => a + b, 0) / values.length;\n  const variance = values.reduce((a, b) => a + (b - m) ** 2, 0) / (values.length - 1);\n  return Math.sqrt(variance);\n}\nfunction kde(values) {\n  const n = values.length;\n  const bandwidth = 0.9 * stdOf(values) * Math.pow(n, -0.2);\n  const raw = grid.map((gx) => values.reduce((sum, v) => sum + Math.exp(-0.5 * ((gx - v) / bandwidth) ** 2), 0));\n  const peak = Math.max(...raw);\n  return raw.map((v) => v / peak);\n}\nconst densityByCategory = valuesByCategory.map(kde);\n\n// --- Cloud (half-violin, upward) + rain (jittered points, downward) + box ---\n// drawn with raw SVG positioned by the chart's own scales — MUI X's documented\n// composition API for chart types the community package has no built-in for.\nfunction CloudRainBox() {\n  const xScale = useXScale(\"value\");\n  const yScale = useYScale(\"category\");\n\n  return (\n    <g>\n      {CATEGORIES.map((cat, i) => {\n        const color = t.palette[i % t.palette.length];\n        const bandWidth = yScale.bandwidth();\n        const baselineY = yScale(cat) + bandWidth / 2;\n        const halfBand = bandWidth / 2;\n        const cloudPeakPx = halfBand * 0.82;\n        const rainNear = halfBand * 0.2;\n        const rainFar = halfBand * 0.92;\n        const boxHalfHeight = Math.min(11, halfBand * 0.18);\n\n        const density = densityByCategory[i];\n        const cloudPoints = grid.map((gx, k) => `${xScale(gx)},${baselineY - density[k] * cloudPeakPx}`);\n        const cloudPath = `M${xScale(grid[0])},${baselineY} L${cloudPoints.join(\" L\")} L${xScale(grid[GRID_N - 1])},${baselineY} Z`;\n\n        const { q1, median, q3, lowerWhisker, upperWhisker } = statsByCategory[i];\n        const boxTop = baselineY - boxHalfHeight;\n        const boxBottom = baselineY + boxHalfHeight;\n\n        return (\n          <g key={cat}>\n            <path d={cloudPath} fill={color} fillOpacity={0.5} stroke={color} strokeOpacity={0.7} strokeWidth={1} />\n            {valuesByCategory[i].map((v, j) => (\n              <circle\n                key={j}\n                cx={xScale(v)}\n                cy={baselineY + rainNear + jittersByCategory[i][j] * (rainFar - rainNear)}\n                r={4}\n                fill={color}\n                fillOpacity={0.55}\n                stroke={t.pageBg}\n                strokeWidth={0.75}\n              />\n            ))}\n            <line x1={xScale(lowerWhisker)} x2={xScale(upperWhisker)} y1={baselineY} y2={baselineY} stroke={t.ink} strokeWidth={1.5} />\n            <line\n              x1={xScale(lowerWhisker)}\n              x2={xScale(lowerWhisker)}\n              y1={baselineY - boxHalfHeight * 0.8}\n              y2={baselineY + boxHalfHeight * 0.8}\n              stroke={t.ink}\n              strokeWidth={1.5}\n            />\n            <line\n              x1={xScale(upperWhisker)}\n              x2={xScale(upperWhisker)}\n              y1={baselineY - boxHalfHeight * 0.8}\n              y2={baselineY + boxHalfHeight * 0.8}\n              stroke={t.ink}\n              strokeWidth={1.5}\n            />\n            <rect\n              x={xScale(q1)}\n              y={boxTop}\n              width={xScale(q3) - xScale(q1)}\n              height={boxHalfHeight * 2}\n              fill={t.elevatedBg}\n              stroke={t.ink}\n              strokeWidth={1.25}\n              rx={2}\n            />\n            <line x1={xScale(median)} x2={xScale(median)} y1={boxTop} y2={boxBottom} stroke={t.ink} strokeWidth={2.2} />\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// Subtle vertical gridlines (15%-opacity ink, per style guide) aligned with\n// the x-axis ticks, so exact reaction-time values are easier to compare\n// across the four category rows.\nfunction Gridlines() {\n  const xScale = useXScale(\"value\");\n  const { top, height } = useDrawingArea();\n  const ticks = xScale.ticks ? xScale.ticks(8) : [];\n\n  return (\n    <g>\n      {ticks.map((tick) => (\n        <line key={tick} x1={xScale(tick)} x2={xScale(tick)} y1={top} y2={top + height} stroke={t.grid} strokeWidth={1} />\n      ))}\n    </g>\n  );\n}\n\nfunction ChartTitle({ text, fontSize }) {\n  const { left, top, width: drawW } = useDrawingArea();\n  return (\n    <text x={left + drawW / 2} y={top - 46} textAnchor=\"middle\" fontSize={fontSize} fontWeight={600} fill={t.ink} fontFamily={FONT}>\n      {text}\n    </text>\n  );\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n\n  const title = \"raincloud-basic · javascript · muix · anyplot.ai\";\n  const titleSize = title.length > 67 ? Math.round(22 * (67 / title.length)) : 22;\n\n  return (\n    <ChartContainer\n      width={W}\n      height={H}\n      skipAnimation\n      series={[]}\n      xAxis={[\n        {\n          id: \"value\",\n          scaleType: \"linear\",\n          min: X_MIN,\n          max: X_MAX,\n          label: \"Reaction Time (ms)\",\n          labelStyle: { fontSize: 16 },\n          tickLabelStyle: { fontSize: 14 },\n        },\n      ]}\n      yAxis={[\n        {\n          id: \"category\",\n          scaleType: \"band\",\n          data: CATEGORIES,\n          categoryGapRatio: 0.35,\n          tickLabelStyle: { fontSize: 15 },\n        },\n      ]}\n      margin={{ top: 110, right: 70, bottom: 90, left: 190 }}\n    >\n      <ChartTitle text={title} fontSize={titleSize} />\n      <Gridlines />\n      <CloudRainBox />\n      <ChartsXAxis axisId=\"value\" position=\"bottom\" />\n      <ChartsYAxis axisId=\"category\" position=\"left\" disableTicks disableLine />\n    </ChartContainer>\n  );\n}\n"}