{"spec_id":"violin-box","library":"muix","language":"javascript","code":"// anyplot.ai\n// violin-box: Violin Plot with Embedded Box Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-09\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst TITLE = \"violin-box · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 56;\n\n// --- Data (in-memory, deterministic LCG — no seeded RNG in the browser) -----\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\n\nfunction randomNormal(rand, mean, stdDev) {\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 * stdDev;\n}\n\nconst rand = lcg(7);\n\n// Simple-reaction-time experiment across 4 caffeine-dosage conditions. The\n// shapes are deliberately different: placebo carries a slow right-skewed\n// \"attention lapse\" tail, 200mg is the tight, fast optimum, and 300mg turns\n// bimodal (impulsive-fast vs. normal-latency) — the overstimulation pattern\n// the Yerkes-Dodson law predicts. A plain box plot's median alone would hide\n// that split; the violin's KDE makes it visible while the embedded box still\n// reports the quartiles.\nfunction sampleReactionTime(dose) {\n  let v;\n  if (dose === \"300 mg\") {\n    v = rand() < 0.35 ? randomNormal(rand, 350, 22) : randomNormal(rand, 465, 34);\n  } else {\n    const base = { \"0 mg (Placebo)\": 515, \"100 mg\": 468, \"200 mg\": 408 }[dose];\n    const std = { \"0 mg (Placebo)\": 52, \"100 mg\": 40, \"200 mg\": 26 }[dose];\n    v = randomNormal(rand, base, std);\n    const lapseProb = { \"0 mg (Placebo)\": 0.06, \"100 mg\": 0.03, \"200 mg\": 0.01 }[dose];\n    if (rand() < lapseProb) v += 130 + rand() * 110;\n  }\n  return Math.max(220, v);\n}\n\nconst N_PER_GROUP = 240;\nconst categories = [\"0 mg (Placebo)\", \"100 mg\", \"200 mg\", \"300 mg\"];\nconst valuesByCategory = categories.map((dose) => Array.from({ length: N_PER_GROUP }, () => sampleReactionTime(dose)));\n\nconst allValues = valuesByCategory.flat();\nconst dataMin = Math.min(...allValues);\nconst dataMax = Math.max(...allValues);\nconst yPad = (dataMax - dataMin) * 0.08;\nconst Y_MIN = dataMin - yPad;\nconst Y_MAX = dataMax + yPad;\n\n// --- Quartile stats (Tukey whiskers, 1.5×IQR) with explicit outlier points --\nfunction quantile(sorted, q) {\n  const pos = (sorted.length - 1) * q;\n  const base = Math.floor(pos);\n  const rest = pos - base;\n  return base + 1 < sorted.length ? 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 inliers = sorted.filter((v) => v >= lowerFence && v <= upperFence);\n  const outliers = sorted.filter((v) => v < lowerFence || v > upperFence);\n  return {\n    q1,\n    median,\n    q3,\n    whiskerLow: inliers.length ? inliers[0] : q1,\n    whiskerHigh: inliers.length ? inliers[inliers.length - 1] : q3,\n    outliers,\n  };\n}\nconst statsByCategory = valuesByCategory.map(boxStats);\n\n// --- Gaussian KDE per group, Silverman bandwidth, normalized to its own peak\n// so each violin shows shape (including 300mg's bimodal split), not sample n.\nconst GRID_N = 140;\nconst grid = Array.from({ length: GRID_N }, (_, k) => Y_MIN + (k * (Y_MAX - Y_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((gy) => values.reduce((sum, v) => sum + Math.exp(-0.5 * ((gy - 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// --- Mirrored violin (KDE on both sides) + inner quartile box + outliers ---\n// The community package (7.29.1) has no violin/box-plot component. A custom\n// SVG layer positioned via the chart's own band/linear scale hooks reproduces\n// one while staying entirely within the community ChartContainer surface —\n// the documented \"composition\" technique for chart types MUI X doesn't ship.\nfunction ViolinBoxes() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const bandwidth = xScale.bandwidth();\n  const violinHalfWidth = bandwidth * 0.42;\n  const boxHalfWidth = Math.min(15, bandwidth * 0.09);\n\n  return (\n    <g>\n      {categories.map((cat, i) => {\n        const color = t.palette[i % t.palette.length];\n        const center = xScale(cat) + bandwidth / 2;\n        const density = densityByCategory[i];\n\n        const leftSide = grid.map((gy, k) => `${center - density[k] * violinHalfWidth},${yScale(gy)}`);\n        const rightSide = grid.map((gy, k) => `${center + density[k] * violinHalfWidth},${yScale(gy)}`).reverse();\n        const violinPath = `M${leftSide.join(\" L\")} L${rightSide.join(\" L\")} Z`;\n\n        const { q1, median, q3, whiskerLow, whiskerHigh, outliers } = statsByCategory[i];\n\n        return (\n          <g key={cat}>\n            <path d={violinPath} fill={color} fillOpacity={0.42} stroke={color} strokeWidth={2} strokeLinejoin=\"round\" />\n            <line x1={center} x2={center} y1={yScale(whiskerLow)} y2={yScale(whiskerHigh)} stroke={t.ink} strokeWidth={1.5} />\n            <rect\n              x={center - boxHalfWidth}\n              y={yScale(q3)}\n              width={boxHalfWidth * 2}\n              height={Math.max(1, yScale(q1) - yScale(q3))}\n              fill={color}\n              stroke={t.pageBg}\n              strokeWidth={1.5}\n              rx={3}\n            />\n            <line\n              x1={center - boxHalfWidth}\n              x2={center + boxHalfWidth}\n              y1={yScale(median)}\n              y2={yScale(median)}\n              stroke={t.pageBg}\n              strokeWidth={2.5}\n            />\n            {outliers.map((v, j) => (\n              <circle key={j} cx={center} cy={yScale(v)} r={4.5} fill={t.pageBg} stroke={color} strokeWidth={2} />\n            ))}\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const chartHeight = window.ANYPLOT_SIZE.height - TITLE_HEIGHT;\n\n  return (\n    <div style={{ width: window.ANYPLOT_SIZE.width, height: window.ANYPLOT_SIZE.height }}>\n      <div\n        style={{\n          height: TITLE_HEIGHT,\n          lineHeight: `${TITLE_HEIGHT}px`,\n          paddingLeft: 24,\n          fontSize: 22,\n          fontWeight: 500,\n          color: t.ink,\n        }}\n      >\n        {TITLE}\n      </div>\n      <ChartContainer\n        width={window.ANYPLOT_SIZE.width}\n        height={chartHeight}\n        series={[]}\n        skipAnimation\n        margin={{ top: 32, right: 50, bottom: 70, left: 105 }}\n        xAxis={[\n          {\n            id: \"doses\",\n            data: categories,\n            scaleType: \"band\",\n            label: \"Caffeine Dose\",\n            labelStyle: { fontSize: 16 },\n            tickLabelStyle: { fontSize: 14 },\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"reaction\",\n            min: Y_MIN,\n            max: Y_MAX,\n            label: \"Reaction Time (ms)\",\n            labelStyle: { fontSize: 16 },\n            tickLabelStyle: { fontSize: 14 },\n            tickFontSize: 34,\n          },\n        ]}\n      >\n        <ChartsGrid\n          horizontal\n          sx={{\n            \"& .MuiChartsGrid-line\": {\n              stroke: t.grid,\n              opacity: 0.2,\n            },\n          }}\n        />\n        <ViolinBoxes />\n        <ChartsXAxis axisId=\"doses\" disableTicks />\n        <ChartsYAxis axisId=\"reaction\" />\n      </ChartContainer>\n    </div>\n  );\n}\n"}