{"spec_id":"scatter-embedding","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-embedding: t-SNE and UMAP Embedding Visualization\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-08-11\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Simulated UMAP projection of PBMC (peripheral blood mononuclear cell)\n// scRNA-seq profiles: 8 immune cell-type clusters, each an isotropic Gaussian\n// blob around a hand-placed 2D centroid (mimics how related cell types sit\n// closer together in a real embedding, e.g. lymphocytes on one side).\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\nfunction gaussian() {\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 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// Redundant shape encoding (on top of hue) so cluster identity survives\n// colorblind confusion at n=8 — each shape maps 1:1 to a cluster/legend entry.\nconst POINT_STYLES = [\n  \"circle\",\n  \"triangle\",\n  \"rect\",\n  \"rectRot\",\n  \"star\",\n  \"crossRot\",\n  \"rectRounded\",\n  \"cross\",\n];\n\nconst clusters = [\n  { label: \"T cells\", cx: -20, cy: 14, sx: 6, sy: 5 },\n  { label: \"B cells\", cx: -28, cy: -10, sx: 5, sy: 6 },\n  { label: \"NK cells\", cx: -6, cy: 24, sx: 4, sy: 4 },\n  { label: \"Monocytes\", cx: 16, cy: 20, sx: 7, sy: 6 },\n  { label: \"Dendritic cells\", cx: 30, cy: 6, sx: 5, sy: 5 },\n  { label: \"Neutrophils\", cx: 24, cy: -16, sx: 6, sy: 5 },\n  { label: \"Erythrocytes\", cx: -10, cy: -26, sx: 6, sy: 7 },\n  { label: \"Platelets\", cx: 4, cy: -6, sx: 4, sy: 4 },\n];\nconst pointsPerCluster = 80;\n\nconst datasets = clusters.map((cluster, i) => ({\n  label: cluster.label,\n  data: Array.from({ length: pointsPerCluster }, () => ({\n    x: cluster.cx + gaussian() * cluster.sx,\n    y: cluster.cy + gaussian() * cluster.sy,\n  })),\n  backgroundColor: hexToRgba(t.palette[i % t.palette.length], 0.65),\n  borderColor: t.palette[i % t.palette.length],\n  borderWidth: 1,\n  pointStyle: POINT_STYLES[i % POINT_STYLES.length],\n  pointRadius: 4,\n  pointHoverRadius: 4,\n}));\n\n// Cluster centroids (mean of the generated points), used by the custom\n// centroid-label plugin below — an explicit per-cluster anchor beyond the\n// legend, per the spec's \"optionally annotate centroids\" guidance.\nconst centroids = datasets.map((dataset) => {\n  const n = dataset.data.length;\n  const sumX = dataset.data.reduce((acc, p) => acc + p.x, 0);\n  const sumY = dataset.data.reduce((acc, p) => acc + p.y, 0);\n  return { label: dataset.label, x: sumX / n, y: sumY / n };\n});\n\n// Native Chart.js plugin (not a chartjs-chart-* community plugin) that draws\n// a halo-outlined label at each cluster centroid directly on the canvas.\nconst centroidLabelsPlugin = {\n  id: \"centroidLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    ctx.save();\n    ctx.font = \"600 13px sans-serif\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.lineJoin = \"round\";\n    centroids.forEach((c) => {\n      const px = scales.x.getPixelForValue(c.x);\n      const py = scales.y.getPixelForValue(c.y) - 16;\n      ctx.lineWidth = 3;\n      ctx.strokeStyle = t.pageBg;\n      ctx.strokeText(c.label, px, py);\n      ctx.fillStyle = t.ink;\n      ctx.fillText(c.label, px, py);\n    });\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: { datasets },\n  plugins: [centroidLabelsPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 8, right: 8, bottom: 8, left: 8 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"PBMC Cell Types · scatter-embedding · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 21, weight: \"500\" },\n        padding: { bottom: 4 },\n      },\n      subtitle: {\n        display: true,\n        text: \"UMAP projection (n_neighbors=15, min_dist=0.1)\",\n        color: t.inkSoft,\n        font: { size: 16, style: \"italic\" },\n        padding: { bottom: 16 },\n      },\n      legend: {\n        position: \"right\",\n        labels: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          usePointStyle: true,\n          boxWidth: 10,\n          boxHeight: 10,\n          padding: 14,\n        },\n      },\n    },\n    scales: {\n      x: {\n        title: { display: true, text: \"UMAP 1\", color: t.ink, font: { size: 16 } },\n        ticks: { display: false },\n        grid: { color: t.grid },\n        border: { color: t.inkSoft },\n      },\n      y: {\n        title: { display: true, text: \"UMAP 2\", color: t.ink, font: { size: 16 } },\n        ticks: { display: false },\n        grid: { color: t.grid },\n        border: { color: t.inkSoft },\n      },\n    },\n  },\n});\n"}