{"spec_id":"scatter-embedding","library":"echarts","language":"javascript","code":"// anyplot.ai\n// scatter-embedding: t-SNE and UMAP Embedding Visualization\n// Library: echarts 6.1.0 | JavaScript 22.23.1\n// Quality: 92/100 | Created: 2026-08-11\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) + Box-Muller for gaussian jitter --------------\nlet seed = 20260811;\nfunction rnd() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction gauss() {\n  const u1 = Math.max(rnd(), 1e-9);\n  const u2 = rnd();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- Synthetic single-cell RNA-seq UMAP embedding ---------------------------\n// 8 cell-type clusters, each with its own center, elongation and rotation —\n// mimics a real UMAP projection where cluster shape/density varies by type,\n// rather than uniform circular blobs.\nconst CLUSTERS = [\n  { label: \"T cells\", cx: -6.5, cy: 3.0, sx: 1.3, sy: 1.0, rot: 0.4, n: 95 },\n  { label: \"B cells\", cx: -3.0, cy: 6.0, sx: 1.0, sy: 0.9, rot: -0.3, n: 70 },\n  { label: \"NK cells\", cx: -8.0, cy: -1.0, sx: 0.9, sy: 1.1, rot: 0.8, n: 55 },\n  { label: \"Monocytes\", cx: 4.0, cy: 4.5, sx: 1.6, sy: 1.3, rot: 0.2, n: 90 },\n  { label: \"Dendritic cells\", cx: 6.5, cy: 1.0, sx: 0.8, sy: 0.8, rot: 0.0, n: 40 },\n  { label: \"Neutrophils\", cx: 2.0, cy: -4.5, sx: 1.4, sy: 1.0, rot: -0.5, n: 65 },\n  { label: \"Erythrocytes\", cx: -1.5, cy: -7.0, sx: 1.1, sy: 1.4, rot: 0.6, n: 80 },\n  { label: \"Platelets\", cx: 7.5, cy: -6.0, sx: 0.7, sy: 0.7, rot: 0.0, n: 35 },\n];\nconst nMin = Math.min(...CLUSTERS.map((c) => c.n));\nconst nMax = Math.max(...CLUSTERS.map((c) => c.n));\n\n// Denser clusters get slightly lower opacity so overplotting stays legible;\n// sparser clusters render a touch bolder — a deliberate, density-aware\n// marker treatment rather than one flat alpha for every series.\nfunction densityOpacity(n) {\n  const frac = (n - nMin) / (nMax - nMin);\n  return 0.78 - frac * 0.24;\n}\n\nconst clusterSeries = CLUSTERS.map((c, idx) => {\n  const points = [];\n  for (let i = 0; i < c.n; i++) {\n    const gx = gauss() * c.sx;\n    const gy = gauss() * c.sy;\n    const rx = gx * Math.cos(c.rot) - gy * Math.sin(c.rot);\n    const ry = gx * Math.sin(c.rot) + gy * Math.cos(c.rot);\n    points.push([c.cx + rx, c.cy + ry]);\n  }\n  return {\n    name: c.label,\n    type: \"scatter\",\n    data: points,\n    symbolSize: 11,\n    // Color pinned to the canonical Imprint index (not render order) so the\n    // legend/z-order reshuffle below never changes which hue a cell type gets.\n    itemStyle: {\n      color: t.palette[idx],\n      opacity: densityOpacity(c.n),\n      borderColor: t.pageBg,\n      borderWidth: 0.5,\n    },\n    emphasis: { itemStyle: { opacity: 1, borderWidth: 1.5 } },\n    n: c.n,\n  };\n})\n  // Deliberate z-ordering: draw the densest clusters first (bottom layer) so\n  // smaller, sparser clusters always render on top and stay fully visible\n  // instead of being buried under a larger neighbor.\n  .sort((a, b) => b.n - a.n);\n\n// A thin scatter of unassigned/background cells between clusters — real\n// UMAP/t-SNE projections rarely produce perfectly clean, noise-free blobs.\nconst NOISE_N = 26;\nconst noisePoints = [];\nfor (let i = 0; i < NOISE_N; i++) {\n  const a = CLUSTERS[Math.floor(rnd() * CLUSTERS.length)];\n  const b = CLUSTERS[Math.floor(rnd() * CLUSTERS.length)];\n  const frac = 0.25 + rnd() * 0.5;\n  const jx = gauss() * 0.5;\n  const jy = gauss() * 0.5;\n  noisePoints.push([a.cx + (b.cx - a.cx) * frac + jx, a.cy + (b.cy - a.cy) * frac + jy]);\n}\nconst noiseSeries = {\n  name: \"noise\",\n  type: \"scatter\",\n  silent: true,\n  data: noisePoints,\n  symbolSize: 7,\n  itemStyle: { color: t.inkSoft, opacity: 0.28 },\n};\n\nconst centroidLabels = {\n  name: \"centroids\",\n  type: \"scatter\",\n  silent: true,\n  symbolSize: 0,\n  // Nudge the label above each cluster's centroid rather than dead center,\n  // so it sits over sparser edge points instead of the densest core.\n  data: CLUSTERS.map((c) => ({ value: [c.cx, c.cy + c.sy * 0.95], name: c.label })),\n  label: {\n    show: true,\n    formatter: \"{b}\",\n    color: t.ink,\n    fontSize: 14,\n    fontWeight: 600,\n    padding: [3, 7],\n    borderRadius: 4,\n    backgroundColor: t.elevatedBg,\n  },\n};\n\n// --- Render -------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"scatter-embedding · javascript · echarts · anyplot.ai\",\n    subtext: \"UMAP (n_neighbors=15, min_dist=0.1) · colored by cell type\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 600 },\n    subtextStyle: { color: t.inkSoft, fontSize: 15 },\n  },\n  legend: {\n    data: CLUSTERS.map((c) => c.label),\n    icon: \"circle\",\n    orient: \"vertical\",\n    right: 24,\n    top: \"middle\",\n    itemWidth: 12,\n    itemHeight: 12,\n    itemGap: 16,\n    textStyle: { color: t.ink, fontSize: 15 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    formatter: (p) => p.seriesName,\n  },\n  grid: { left: 60, right: 240, top: 110, bottom: 70 },\n  // Minimal frame (style-guide \"remove all spines\" alternative for clean\n  // scatter plots): no axis line, no split lines — just the descriptive\n  // dimension names, since embedding coordinates carry no interpretable\n  // ticks and a full box border reads as unnecessary chartjunk here.\n  xAxis: {\n    type: \"value\",\n    name: \"UMAP dimension 1\",\n    nameLocation: \"middle\",\n    nameGap: 28,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    axisLabel: { show: false },\n    axisTick: { show: false },\n    axisLine: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"UMAP dimension 2\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    axisLabel: { show: false },\n    axisTick: { show: false },\n    axisLine: { show: false },\n    splitLine: { show: false },\n  },\n  series: [noiseSeries, ...clusterSeries, centroidLabels],\n});\n\nchart.on(\"finished\", () => {\n  window.__anyplotReady = true;\n});\n"}