{"spec_id":"scatter-embedding","library":"muix","language":"javascript","code":"// anyplot.ai\n// scatter-embedding: t-SNE and UMAP Embedding Visualization\n// Library: muix 7.29.1 | JavaScript 22.23.1\n// Quality: 90/100 | Created: 2026-08-11\n//# anyplot-orientation: landscape\n// anyplot.ai\n// scatter-embedding: t-SNE and UMAP Embedding Visualization\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-08-11\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// Deterministic LCG (seed 42) — no Math.random() in the browser harness\nlet seed = 42;\nfunction rng() {\n  seed = (1664525 * seed + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\nfunction randn() {\n  const u = Math.max(rng(), 1e-9);\n  const v = rng();\n  return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);\n}\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// --- Data: simulated UMAP projection of single-cell RNA-seq data -----------\n// 8 cell-type clusters scattered around distinct centroids in 2D embedding\n// space (the axes themselves carry no interpretable units). Point count and\n// spread vary per cluster — real UMAP/t-SNE embeddings show clusters of\n// differing size and compactness, not uniform blobs.\nconst CLUSTERS = [\n  { label: \"T cells\", cx: -9, cy: 5, spread: 1.9, n: 90 },\n  { label: \"B cells\", cx: 8, cy: 6.5, spread: 1.3, n: 65 },\n  { label: \"Monocytes\", cx: -7, cy: -6.5, spread: 2.1, n: 95 },\n  { label: \"NK cells\", cx: 9.5, cy: -4, spread: 1.4, n: 60 },\n  { label: \"Dendritic cells\", cx: 0.5, cy: 10, spread: 1.6, n: 70 },\n  { label: \"Platelets\", cx: -1, cy: -10, spread: 1.0, n: 55 },\n  { label: \"Erythrocytes\", cx: -12, cy: -1, spread: 1.75, n: 80 },\n  { label: \"Neutrophils\", cx: 13, cy: 1.5, spread: 1.5, n: 65 },\n];\n\nconst clusterData = CLUSTERS.map((cluster) =>\n  Array.from({ length: cluster.n }, (_, i) => ({\n    id: `${cluster.label}-${i}`,\n    x: cluster.cx + randn() * cluster.spread,\n    y: cluster.cy + randn() * cluster.spread,\n  })),\n);\n\n// Denser, tighter clusters get more transparency so overlapping cores stay\n// distinguishable; sparser clusters can afford to render more opaque.\nconst densities = CLUSTERS.map((c) => c.n / (c.spread * c.spread));\nconst MIN_DENSITY = Math.min(...densities);\nconst MAX_DENSITY = Math.max(...densities);\nfunction alphaForCluster(cluster) {\n  const density = cluster.n / (cluster.spread * cluster.spread);\n  const t01 = (density - MIN_DENSITY) / (MAX_DENSITY - MIN_DENSITY || 1);\n  return 0.72 - t01 * 0.17; // 0.72 (sparse) -> 0.55 (dense)\n}\n\nconst allPoints = clusterData.flat();\nconst xs = allPoints.map((p) => p.x);\nconst ys = allPoints.map((p) => p.y);\nconst xPad = (Math.max(...xs) - Math.min(...xs)) * 0.08;\nconst yPad = (Math.max(...ys) - Math.min(...ys)) * 0.08;\nconst X_MIN = Math.min(...xs) - xPad;\nconst X_MAX = Math.max(...xs) + xPad;\nconst Y_MIN = Math.min(...ys) - yPad;\nconst Y_MAX = Math.max(...ys) + yPad;\n\nconst MARGIN = { top: 120, right: 250, bottom: 60, left: 60 };\n\n// Centroid labels rendered at data coordinates via the live D3 scales — the\n// redundant color+text encoding required once a chart uses all 8 categorical\n// series (see default-style-guide.md \"series-count guidance\").\nfunction CentroidLabels() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  if (!xScale || !yScale) return null;\n\n  return (\n    <g>\n      {CLUSTERS.map((cluster, i) => {\n        const cx = xScale(cluster.cx);\n        const cy = yScale(cluster.cy) - 20;\n        const halfWidth = cluster.label.length * 3.9 + 8;\n        return (\n          <g key={cluster.label}>\n            <rect\n              x={cx - halfWidth}\n              y={cy - 14}\n              width={halfWidth * 2}\n              height={20}\n              rx={5}\n              fill={t.elevatedBg}\n              opacity={0.88}\n            />\n            <text\n              x={cx}\n              y={cy}\n              textAnchor=\"middle\"\n              dominantBaseline=\"middle\"\n              fontSize={13}\n              fontWeight={600}\n              fill={t.palette[i]}\n            >\n              {cluster.label}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nconst TITLE = \"scatter-embedding · javascript · muix · anyplot.ai\";\nconst SUBTITLE = \"UMAP (n_neighbors=15) · synthetic single-cell RNA-seq embedding\";\n\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      margin={MARGIN}\n      sx={{ \"& .MuiChartsAxis-line, & .MuiChartsAxis-tick\": { display: \"none\" } }}\n      series={CLUSTERS.map((cluster, i) => ({\n        type: \"scatter\",\n        id: cluster.label,\n        label: cluster.label,\n        data: clusterData[i],\n        color: hexToRgba(t.palette[i], alphaForCluster(cluster)),\n        markerSize: 5,\n      }))}\n      xAxis={[\n        {\n          id: \"umap1\",\n          scaleType: \"linear\",\n          min: X_MIN,\n          max: X_MAX,\n          disableTicks: true,\n          disableLine: true,\n          tickLabelStyle: { display: \"none\" },\n          label: \"UMAP dimension 1\",\n          labelStyle: { fontSize: 15, fill: t.inkSoft },\n        },\n      ]}\n      yAxis={[\n        {\n          id: \"umap2\",\n          scaleType: \"linear\",\n          min: Y_MIN,\n          max: Y_MAX,\n          disableTicks: true,\n          disableLine: true,\n          tickLabelStyle: { display: \"none\" },\n          label: \"UMAP dimension 2\",\n          labelStyle: { fontSize: 15, fill: t.inkSoft },\n        },\n      ]}\n    >\n      <ScatterPlot skipAnimation />\n      <CentroidLabels />\n      <ChartsXAxis axisId=\"umap1\" />\n      <ChartsYAxis axisId=\"umap2\" />\n      <ChartsLegend\n        position={{ vertical: \"middle\", horizontal: \"right\" }}\n        direction=\"column\"\n        slotProps={{\n          legend: {\n            itemMarkWidth: 12,\n            itemMarkHeight: 12,\n            markGap: 8,\n            itemGap: 18,\n            labelStyle: { fontSize: 14, fill: t.ink },\n          },\n        }}\n      />\n      <text x={width / 2} y={46} textAnchor=\"middle\" fontSize={28} fontWeight={600} fill={t.ink}>\n        {TITLE}\n      </text>\n      <text x={width / 2} y={82} textAnchor=\"middle\" fontSize={15} fill={t.inkSoft}>\n        {SUBTITLE}\n      </text>\n    </ChartContainer>\n  );\n}\n"}