Basic Swarm Plot — MUI X Charts

A swarm plot (beeswarm plot) displays individual data points for categorical comparisons, with points spread horizontally to avoid overlap. This reveals the full distribution shape and density while preserving exact values - combining the benefits of strip plots (individual points) and violin plots (density visualization). Ideal when you need to see every observation rather than just summary statistics.

Basic Swarm Plot rendered with MUI X Charts

JavaScript source (MUI X Charts)

// anyplot.ai
// swarm-basic: Basic Swarm Plot
// Library: muix 7.29.1 | JavaScript 22.23.1
// Quality: 90/100 | Created: 2026-07-26
//# anyplot-orientation: landscape
// anyplot.ai
// swarm-basic: Basic Swarm Plot
// Library: MUI X Charts | React | Node 22
// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.
// Quality: pending | Created: 2026-07-26

import { ChartContainer } from "@mui/x-charts/ChartContainer";
import { ChartsXAxis } from "@mui/x-charts/ChartsXAxis";
import { ChartsYAxis } from "@mui/x-charts/ChartsYAxis";
import { ChartsGrid } from "@mui/x-charts/ChartsGrid";
import { ScatterPlot } from "@mui/x-charts/ScatterChart";
import { useXScale, useYScale } from "@mui/x-charts/hooks";

const t = window.ANYPLOT_TOKENS;

// --- Deterministic PRNG (LCG) + Box-Muller for approx-normal samples --------
let seed = 42;
function nextUniform() {
  seed = (seed * 1664525 + 1013904223) % 4294967296;
  return seed / 4294967296;
}
function nextNormal(mean, stdDev) {
  const u1 = Math.max(nextUniform(), 1e-9);
  const u2 = nextUniform();
  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
  return mean + z * stdDev;
}

// --- Data: quarterly performance review scores by department ---------------
const DEPARTMENTS = ["Engineering", "Sales", "Marketing", "Support"];
const MEANS = [78, 70, 74, 83];
const STD_DEVS = [8, 12, 9, 6];
const POINTS_PER_DEPT = 40;

const rawPoints = DEPARTMENTS.flatMap((department, categoryIndex) =>
  Array.from({ length: POINTS_PER_DEPT }, () => ({
    department,
    categoryIndex,
    score: Math.min(99, Math.max(45, nextNormal(MEANS[categoryIndex], STD_DEVS[categoryIndex]))),
  })),
);

const median = (values) => {
  const sorted = [...values].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
};

const medianByDept = DEPARTMENTS.map((_, i) =>
  median(rawPoints.filter((p) => p.categoryIndex === i).map((p) => p.score)),
);

// --- Layout geometry — must stay in sync with the ChartContainer margin ----
const MARGIN = { left: 110, right: 60, top: 80, bottom: 100 };
const SCORE_MIN = 40;
const SCORE_MAX = 100;
const X_MIN = -0.6;
const X_MAX = DEPARTMENTS.length - 1 + 0.6;
const MARKER_SIZE = 6; // circle radius, px
const MARKER_DIAMETER_PX = MARKER_SIZE * 2 + 1;

// --- Beeswarm packing: spread points horizontally within each department so
// none overlap. Collisions are resolved in on-screen pixels (rather than data
// units) so the spread looks even regardless of the score axis' range. Each
// point keeps its true score on the y-axis; only the x-offset is adjusted.
function layoutSwarm(plotWidthPx, plotHeightPx) {
  const pxPerScore = plotHeightPx / (SCORE_MAX - SCORE_MIN);
  const pxPerX = plotWidthPx / (X_MAX - X_MIN);

  return DEPARTMENTS.flatMap((department, categoryIndex) => {
    const points = rawPoints
      .filter((p) => p.categoryIndex === categoryIndex)
      .sort((a, b) => a.score - b.score);

    const placed = [];
    points.forEach((point) => {
      const nearby = placed.filter(
        (p) => Math.abs((point.score - p.score) * pxPerScore) < MARKER_DIAMETER_PX,
      );
      let offsetPx = 0;
      if (nearby.length > 0) {
        const step = MARKER_DIAMETER_PX * 0.92;
        let k = 0;
        let resolved = false;
        while (!resolved && k < 200) {
          const candidate = k === 0 ? 0 : (k % 2 === 1 ? Math.ceil(k / 2) : -Math.ceil(k / 2)) * step;
          if (
            nearby.every(
              (p) => Math.hypot(candidate - p.offsetPx, (point.score - p.score) * pxPerScore) >= MARKER_DIAMETER_PX * 0.95,
            )
          ) {
            offsetPx = candidate;
            resolved = true;
          }
          k += 1;
        }
      }
      placed.push({ ...point, offsetPx });
    });

    return placed.map((p) => ({
      id: `${department}-${p.score.toFixed(3)}-${p.offsetPx.toFixed(2)}`,
      x: categoryIndex + p.offsetPx / pxPerX,
      y: p.score,
    }));
  });
}

// Short reference ticks at each department's median score. Rendered inside
// ChartContainer so it can read the live D3 scales.
function MedianTicks() {
  const xScale = useXScale();
  const yScale = useYScale();
  const halfWidth = 0.34;

  return (
    <g>
      {DEPARTMENTS.map((department, i) => {
        const x1 = xScale(i - halfWidth) ?? 0;
        const x2 = xScale(i + halfWidth) ?? 0;
        const y = yScale(medianByDept[i]) ?? 0;
        return (
          <line
            key={department}
            x1={x1}
            y1={y}
            x2={x2}
            y2={y}
            stroke={t.ink}
            strokeWidth={2.5}
            strokeOpacity={0.75}
          />
        );
      })}
    </g>
  );
}

const TITLE = "swarm-basic · javascript · muix · anyplot.ai";

export default function Chart() {
  const W = window.ANYPLOT_SIZE.width;
  const H = window.ANYPLOT_SIZE.height;
  const plotWidthPx = W - MARGIN.left - MARGIN.right;
  const plotHeightPx = H - MARGIN.top - MARGIN.bottom;
  const scoreData = layoutSwarm(plotWidthPx, plotHeightPx);

  return (
    <ChartContainer
      width={W}
      height={H}
      skipAnimation
      series={[
        {
          type: "scatter",
          id: "scores",
          label: "Performance Score",
          data: scoreData,
          color: t.palette[0],
          markerSize: MARKER_SIZE,
        },
      ]}
      xAxis={[
        {
          id: "xAxis",
          min: X_MIN,
          max: X_MAX,
          tickMinStep: 1,
          valueFormatter: (v) => DEPARTMENTS[Math.round(v)] ?? "",
          tickLabelStyle: { fontSize: 15, fill: t.inkSoft },
          label: "Department",
          labelStyle: { fontSize: 16, fill: t.ink },
        },
      ]}
      yAxis={[
        {
          id: "yAxis",
          min: SCORE_MIN,
          max: SCORE_MAX,
          label: "Performance Score",
          tickLabelStyle: { fontSize: 15, fill: t.inkSoft },
          labelStyle: { fontSize: 16, fill: t.ink },
        },
      ]}
      margin={MARGIN}
    >
      <ChartsGrid horizontal />
      <ScatterPlot />
      <MedianTicks />
      <ChartsXAxis axisId="xAxis" />
      <ChartsYAxis axisId="yAxis" />
      <text x={W / 2} y={42} textAnchor="middle" fontSize={22} fontFamily="sans-serif" fontWeight="500" fill={t.ink}>
        {TITLE}
      </text>
    </ChartContainer>
  );
}

Part of Basic Swarm Plot on anyplot.ai.

Other implementations