t-SNE and UMAP Embedding Visualization — Apache ECharts

A scatter plot displaying high-dimensional data projected into 2D space using non-linear dimensionality reduction techniques such as t-SNE or UMAP. Points are colored by cluster or class label, revealing groupings and latent structure in the data. This is a standard visualization in machine learning for exploring embeddings, single-cell RNA-seq data, and NLP document clustering, helping practitioners verify that learned representations capture meaningful distinctions.

t-SNE and UMAP Embedding Visualization rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// scatter-embedding: t-SNE and UMAP Embedding Visualization
// Library: echarts 6.1.0 | JavaScript 22.23.1
// Quality: 92/100 | Created: 2026-08-11

const t = window.ANYPLOT_TOKENS;

// --- Deterministic PRNG (LCG) + Box-Muller for gaussian jitter --------------
let seed = 20260811;
function rnd() {
  seed = (seed * 1103515245 + 12345) & 0x7fffffff;
  return seed / 0x7fffffff;
}
function gauss() {
  const u1 = Math.max(rnd(), 1e-9);
  const u2 = rnd();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

// --- Synthetic single-cell RNA-seq UMAP embedding ---------------------------
// 8 cell-type clusters, each with its own center, elongation and rotation —
// mimics a real UMAP projection where cluster shape/density varies by type,
// rather than uniform circular blobs.
const CLUSTERS = [
  { label: "T cells", cx: -6.5, cy: 3.0, sx: 1.3, sy: 1.0, rot: 0.4, n: 95 },
  { label: "B cells", cx: -3.0, cy: 6.0, sx: 1.0, sy: 0.9, rot: -0.3, n: 70 },
  { label: "NK cells", cx: -8.0, cy: -1.0, sx: 0.9, sy: 1.1, rot: 0.8, n: 55 },
  { label: "Monocytes", cx: 4.0, cy: 4.5, sx: 1.6, sy: 1.3, rot: 0.2, n: 90 },
  { label: "Dendritic cells", cx: 6.5, cy: 1.0, sx: 0.8, sy: 0.8, rot: 0.0, n: 40 },
  { label: "Neutrophils", cx: 2.0, cy: -4.5, sx: 1.4, sy: 1.0, rot: -0.5, n: 65 },
  { label: "Erythrocytes", cx: -1.5, cy: -7.0, sx: 1.1, sy: 1.4, rot: 0.6, n: 80 },
  { label: "Platelets", cx: 7.5, cy: -6.0, sx: 0.7, sy: 0.7, rot: 0.0, n: 35 },
];
const nMin = Math.min(...CLUSTERS.map((c) => c.n));
const nMax = Math.max(...CLUSTERS.map((c) => c.n));

// Denser clusters get slightly lower opacity so overplotting stays legible;
// sparser clusters render a touch bolder — a deliberate, density-aware
// marker treatment rather than one flat alpha for every series.
function densityOpacity(n) {
  const frac = (n - nMin) / (nMax - nMin);
  return 0.78 - frac * 0.24;
}

const clusterSeries = CLUSTERS.map((c, idx) => {
  const points = [];
  for (let i = 0; i < c.n; i++) {
    const gx = gauss() * c.sx;
    const gy = gauss() * c.sy;
    const rx = gx * Math.cos(c.rot) - gy * Math.sin(c.rot);
    const ry = gx * Math.sin(c.rot) + gy * Math.cos(c.rot);
    points.push([c.cx + rx, c.cy + ry]);
  }
  return {
    name: c.label,
    type: "scatter",
    data: points,
    symbolSize: 11,
    // Color pinned to the canonical Imprint index (not render order) so the
    // legend/z-order reshuffle below never changes which hue a cell type gets.
    itemStyle: {
      color: t.palette[idx],
      opacity: densityOpacity(c.n),
      borderColor: t.pageBg,
      borderWidth: 0.5,
    },
    emphasis: { itemStyle: { opacity: 1, borderWidth: 1.5 } },
    n: c.n,
  };
})
  // Deliberate z-ordering: draw the densest clusters first (bottom layer) so
  // smaller, sparser clusters always render on top and stay fully visible
  // instead of being buried under a larger neighbor.
  .sort((a, b) => b.n - a.n);

// A thin scatter of unassigned/background cells between clusters — real
// UMAP/t-SNE projections rarely produce perfectly clean, noise-free blobs.
const NOISE_N = 26;
const noisePoints = [];
for (let i = 0; i < NOISE_N; i++) {
  const a = CLUSTERS[Math.floor(rnd() * CLUSTERS.length)];
  const b = CLUSTERS[Math.floor(rnd() * CLUSTERS.length)];
  const frac = 0.25 + rnd() * 0.5;
  const jx = gauss() * 0.5;
  const jy = gauss() * 0.5;
  noisePoints.push([a.cx + (b.cx - a.cx) * frac + jx, a.cy + (b.cy - a.cy) * frac + jy]);
}
const noiseSeries = {
  name: "noise",
  type: "scatter",
  silent: true,
  data: noisePoints,
  symbolSize: 7,
  itemStyle: { color: t.inkSoft, opacity: 0.28 },
};

const centroidLabels = {
  name: "centroids",
  type: "scatter",
  silent: true,
  symbolSize: 0,
  // Nudge the label above each cluster's centroid rather than dead center,
  // so it sits over sparser edge points instead of the densest core.
  data: CLUSTERS.map((c) => ({ value: [c.cx, c.cy + c.sy * 0.95], name: c.label })),
  label: {
    show: true,
    formatter: "{b}",
    color: t.ink,
    fontSize: 14,
    fontWeight: 600,
    padding: [3, 7],
    borderRadius: 4,
    backgroundColor: t.elevatedBg,
  },
};

// --- Render -------------------------------------------------------------
const chart = echarts.init(document.getElementById("container"));

chart.setOption({
  animation: false,
  color: t.palette,
  backgroundColor: "transparent",
  title: {
    text: "scatter-embedding · javascript · echarts · anyplot.ai",
    subtext: "UMAP (n_neighbors=15, min_dist=0.1) · colored by cell type",
    left: "center",
    textStyle: { color: t.ink, fontSize: 22, fontWeight: 600 },
    subtextStyle: { color: t.inkSoft, fontSize: 15 },
  },
  legend: {
    data: CLUSTERS.map((c) => c.label),
    icon: "circle",
    orient: "vertical",
    right: 24,
    top: "middle",
    itemWidth: 12,
    itemHeight: 12,
    itemGap: 16,
    textStyle: { color: t.ink, fontSize: 15 },
  },
  tooltip: {
    trigger: "item",
    formatter: (p) => p.seriesName,
  },
  grid: { left: 60, right: 240, top: 110, bottom: 70 },
  // Minimal frame (style-guide "remove all spines" alternative for clean
  // scatter plots): no axis line, no split lines — just the descriptive
  // dimension names, since embedding coordinates carry no interpretable
  // ticks and a full box border reads as unnecessary chartjunk here.
  xAxis: {
    type: "value",
    name: "UMAP dimension 1",
    nameLocation: "middle",
    nameGap: 28,
    nameTextStyle: { color: t.inkSoft, fontSize: 14 },
    axisLabel: { show: false },
    axisTick: { show: false },
    axisLine: { show: false },
    splitLine: { show: false },
  },
  yAxis: {
    type: "value",
    name: "UMAP dimension 2",
    nameLocation: "middle",
    nameGap: 40,
    nameTextStyle: { color: t.inkSoft, fontSize: 14 },
    axisLabel: { show: false },
    axisTick: { show: false },
    axisLine: { show: false },
    splitLine: { show: false },
  },
  series: [noiseSeries, ...clusterSeries, centroidLabels],
});

chart.on("finished", () => {
  window.__anyplotReady = true;
});

Part of t-SNE and UMAP Embedding Visualization on anyplot.ai.

Other implementations