{"spec_id":"violin-grouped-swarm","library":"muix","language":"javascript","code":"// anyplot.ai\n// violin-grouped-swarm: Grouped Violin Plot with Swarm Overlay\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 92/100 | 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, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + Box-Muller for approx-normal samples --------\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}\nfunction clamp(v, lo, hi) {\n  return Math.min(hi, Math.max(lo, v));\n}\n\n// --- Data: support-ticket resolution times (s) by task type & agent level --\nconst CATEGORIES = [\"Bug Triage\", \"Data Entry\", \"Code Review\"];\nconst GROUPS = [\"Junior\", \"Senior\"];\nconst N_PER_CELL = 40;\n\n// Junior agents run slower and more variably than Senior agents on most task\n// types — except Code Review, where the gap nearly closes (both levels lean\n// on the same review checklist), a shape only the swarm's raw points make\n// obvious against the violins.\nconst PARAMS = {\n  \"Bug Triage\": { Junior: { mean: 95, std: 24 }, Senior: { mean: 52, std: 12 } },\n  \"Data Entry\": { Junior: { mean: 60, std: 16 }, Senior: { mean: 38, std: 9 } },\n  \"Code Review\": { Junior: { mean: 130, std: 22 }, Senior: { mean: 118, std: 20 } },\n};\n\nconst cells = CATEGORIES.flatMap((category) =>\n  GROUPS.map((group) => {\n    const { mean, std } = PARAMS[category][group];\n    const values = Array.from({ length: N_PER_CELL }, () => clamp(mean + std * gaussianSample(), 10, 220));\n    return { category, group, values };\n  }),\n);\nfunction getCell(category, group) {\n  return cells.find((c) => c.category === category && c.group === group);\n}\n\nconst allValues = cells.flatMap((c) => c.values);\nconst dataMin = Math.min(...allValues);\nconst dataMax = Math.max(...allValues);\nconst yPad = (dataMax - dataMin) * 0.1;\nconst Y_MIN = Math.max(0, dataMin - yPad);\nconst Y_MAX = dataMax + yPad;\n\nfunction median(values) {\n  const sorted = [...values].sort((a, b) => a - b);\n  const mid = Math.floor(sorted.length / 2);\n  return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];\n}\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}\n\n// --- Gaussian KDE per cell, Silverman bandwidth, normalized to its own peak\n// so each violin shows shape, not sample count. Each cell gets its OWN grid,\n// local to its data range ± 3 bandwidths (clipped to the shared axis domain)\n// rather than the full shared Y_MIN..Y_MAX span — otherwise a cell whose\n// values cluster in a narrow band (e.g. Data Entry) draws a near-invisible\n// KDE tail that still strokes a hairline the full height of the axis.\nconst GRID_N = 120;\nfunction kde(values) {\n  const n = values.length;\n  const bandwidth = 0.9 * stdOf(values) * Math.pow(n, -0.2);\n  const localMin = Math.max(Y_MIN, Math.min(...values) - 3 * bandwidth);\n  const localMax = Math.min(Y_MAX, Math.max(...values) + 3 * bandwidth);\n  const grid = Array.from({ length: GRID_N }, (_, k) => localMin + (k * (localMax - localMin)) / (GRID_N - 1));\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 { grid, density: raw.map((v) => v / peak) };\n}\ncells.forEach((cell) => {\n  const { grid, density } = kde(cell.values);\n  cell.grid = grid;\n  cell.density = density;\n  cell.median = median(cell.values);\n});\n\n// --- Beeswarm packing in pixel space: points are sorted by value and pushed\n// sideways from the violin's centerline only when they'd overlap a neighbor\n// already placed, capped at maxHalfWidth so the swarm stays inside its violin.\nfunction layoutSwarmForCell(values, yScale, markerRadius, maxHalfWidth) {\n  const diameter = markerRadius * 2 + 1;\n  const step = diameter * 0.92;\n  const maxK = Math.max(1, Math.floor(maxHalfWidth / step));\n  const sorted = values.map((v) => ({ value: v, y: yScale(v) })).sort((a, b) => a.y - b.y);\n  const placed = [];\n  sorted.forEach((point) => {\n    const nearby = placed.filter((p) => Math.abs(point.y - p.y) < diameter);\n    let offsetPx = 0;\n    if (nearby.length > 0) {\n      let resolved = false;\n      let k = 0;\n      while (!resolved && k <= maxK) {\n        const candidate = k === 0 ? 0 : (k % 2 === 1 ? Math.ceil(k / 2) : -Math.ceil(k / 2)) * step;\n        if (nearby.every((p) => Math.hypot(candidate - p.offsetPx, point.y - p.y) >= diameter * 0.95)) {\n          offsetPx = candidate;\n          resolved = true;\n        }\n        k += 1;\n      }\n      if (!resolved) offsetPx = (k % 2 === 1 ? 1 : -1) * maxHalfWidth;\n    }\n    placed.push({ ...point, offsetPx });\n  });\n  return placed;\n}\n\n// A small labeled bracket that calls out the Code Review group's narrowing\n// Junior/Senior gap — the standout finding in the data — rendered in the\n// padding zone above Y_MAX (below `top`, above where any KDE curve or swarm\n// point can reach) so it never collides with the marks it's annotating.\nfunction GapAnnotation({ x1, x2, y, label }) {\n  return (\n    <g>\n      <line x1={x1} x2={x2} y1={y} y2={y} stroke={t.inkSoft} strokeWidth={1} />\n      <line x1={x1} x2={x1} y1={y} y2={y + 5} stroke={t.inkSoft} strokeWidth={1} />\n      <line x1={x2} x2={x2} y1={y} y2={y + 5} stroke={t.inkSoft} strokeWidth={1} />\n      <text x={(x1 + x2) / 2} y={y - 6} textAnchor=\"middle\" fontSize={11} fontStyle=\"italic\" fill={t.inkSoft}>\n        {label}\n      </text>\n    </g>\n  );\n}\n\n// --- Grouped violins (mirrored KDE, dodged by group within each category's\n// band) with swarm points overlaid, matching each violin's hue. A custom SVG\n// layer positioned via the chart's own band/linear scale hooks — the\n// community package (7.29.1) has no violin/swarm component, so this is the\n// documented \"composition\" technique for chart types MUI X doesn't ship.\nfunction GroupedViolinSwarm() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const { top } = useDrawingArea();\n  const bandwidth = xScale.bandwidth();\n  const groupWidth = bandwidth * 0.82;\n  const slotWidth = groupWidth / GROUPS.length;\n  const violinHalfWidth = slotWidth * 0.42;\n  const swarmMaxHalfWidth = slotWidth * 0.4;\n  const markerRadius = 4;\n\n  return (\n    <g>\n      {CATEGORIES.map((category) => {\n        const bandStart = xScale(category) ?? 0;\n        const groupStart = bandStart + (bandwidth - groupWidth) / 2;\n\n        const groupData = GROUPS.map((group, groupIndex) => ({\n          group,\n          groupIndex,\n          cell: getCell(category, group),\n          color: t.palette[groupIndex % t.palette.length],\n          cx: groupStart + slotWidth * (groupIndex + 0.5),\n        }));\n\n        return (\n          <g key={category}>\n            {groupData.map(({ group, cell, color, cx }) => {\n              const leftSide = cell.grid.map((gy, k) => `${cx - cell.density[k] * violinHalfWidth},${yScale(gy)}`);\n              const rightSide = cell.grid.map((gy, k) => `${cx + cell.density[k] * violinHalfWidth},${yScale(gy)}`).reverse();\n              const violinPath = `M${leftSide.join(\" L\")} L${rightSide.join(\" L\")} Z`;\n\n              const swarmPoints = layoutSwarmForCell(cell.values, yScale, markerRadius, swarmMaxHalfWidth);\n\n              return (\n                <g key={`${category}-${group}`}>\n                  <path d={violinPath} fill={color} fillOpacity={0.45} stroke={color} strokeWidth={1.75} strokeLinejoin=\"round\" />\n                  <line\n                    x1={cx - violinHalfWidth * 0.8}\n                    x2={cx + violinHalfWidth * 0.8}\n                    y1={yScale(cell.median)}\n                    y2={yScale(cell.median)}\n                    stroke={t.ink}\n                    strokeWidth={2.1}\n                    strokeOpacity={0.85}\n                  />\n                  {swarmPoints.map((p, i) => (\n                    <circle\n                      key={i}\n                      cx={cx + p.offsetPx}\n                      cy={p.y}\n                      r={markerRadius}\n                      fill={color}\n                      fillOpacity={0.85}\n                      stroke={t.pageBg}\n                      strokeWidth={0.75}\n                    />\n                  ))}\n                </g>\n              );\n            })}\n            {category === \"Code Review\" && (\n              <GapAnnotation\n                x1={groupData[0].cx}\n                x2={groupData[1].cx}\n                y={top + 20}\n                label=\"Junior/Senior gap narrows\"\n              />\n            )}\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// Custom y-axis title, positioned well clear of the (up to 3-digit) tick\n// labels — MUI X's built-in yAxis `label` sits at a fixed offset from the\n// axis line that doesn't grow with tick-label width, which crowds a 3-digit\n// value axis. Rendering it ourselves via useDrawingArea sidesteps that.\nfunction YAxisLabel({ text }) {\n  const { top, height } = useDrawingArea();\n  const cy = top + height / 2;\n  return (\n    <text x={26} y={cy} textAnchor=\"middle\" transform={`rotate(-90, 26, ${cy})`} fontSize={16} fill={t.ink}>\n      {text}\n    </text>\n  );\n}\n\nconst TITLE = \"violin-grouped-swarm · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 56;\nconst LEGEND_HEIGHT = 34;\nconst GAP = 20;\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const chartHeight = H - TITLE_HEIGHT - LEGEND_HEIGHT - GAP;\n\n  return (\n    <div style={{ width: W, height: H, display: \"flex\", flexDirection: \"column\" }}>\n      <div style={{ height: TITLE_HEIGHT, display: \"flex\", alignItems: \"center\", paddingLeft: 8 }}>\n        <span style={{ fontSize: 36, fontWeight: 500, color: t.ink }}>{TITLE}</span>\n      </div>\n      <div style={{ height: LEGEND_HEIGHT, display: \"flex\", alignItems: \"center\", gap: 24, paddingLeft: 8 }}>\n        {GROUPS.map((group, i) => (\n          <div key={group} style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n            <span style={{ width: 14, height: 14, borderRadius: 7, background: t.palette[i], display: \"inline-block\" }} />\n            <span style={{ fontSize: 14, color: t.inkSoft }}>{group}</span>\n          </div>\n        ))}\n      </div>\n      <div style={{ height: GAP }} />\n      <ChartContainer\n        width={W}\n        height={chartHeight}\n        series={[]}\n        skipAnimation\n        margin={{ top: 34, right: 50, bottom: 84, left: 110 }}\n        xAxis={[\n          {\n            id: \"category\",\n            scaleType: \"band\",\n            data: CATEGORIES,\n            categoryGapRatio: 0.35,\n            disableTicks: true,\n            label: \"Task Type\",\n            labelStyle: { fontSize: 16 },\n            tickLabelStyle: { fontSize: 14 },\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"value\",\n            min: Y_MIN,\n            max: Y_MAX,\n            disableTicks: true,\n            tickLabelStyle: { fontSize: 14 },\n          },\n        ]}\n      >\n        <ChartsGrid\n          horizontal\n          sx={{\n            \"& .MuiChartsGrid-line\": {\n              stroke: t.grid,\n              opacity: 0.2,\n            },\n          }}\n        />\n        <GroupedViolinSwarm />\n        <ChartsXAxis axisId=\"category\" />\n        <ChartsYAxis axisId=\"value\" />\n        <YAxisLabel text=\"Resolution Time (s)\" />\n      </ChartContainer>\n    </div>\n  );\n}\n"}