t-SNE and UMAP Embedding Visualization — Altair

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 Altair

Renders

Python source (Altair)

""" anyplot.ai
scatter-embedding: t-SNE and UMAP Embedding Visualization
Library: altair 6.2.2 | Python 3.13.14
Quality: 93/100 | Updated: 2026-08-11
"""

import os
import sys


sys.path = [p for p in sys.path if not p.endswith("implementations/python")]

import altair as alt
import numpy as np
import pandas as pd
from PIL import Image
from sklearn.datasets import make_blobs
from sklearn.manifold import TSNE


THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Imprint palette (positions 1-7, canonical order)
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477"]

# Single-cell RNA-seq scenario with domain-specific cell type labels
CELL_TYPES = ["T cells", "B cells", "NK cells", "Monocytes", "Dendritic cells", "Macrophages", "Plasma cells"]
CELL_ABBR = {
    "T cells": "T",
    "B cells": "B",
    "NK cells": "NK",
    "Monocytes": "Mono",
    "Dendritic cells": "DC",
    "Macrophages": "Mac",
    "Plasma cells": "PC",
}

# Data — varying cluster_std for realistic compactness differences across cell types
np.random.seed(42)
n_clusters = 7
cluster_stds = [1.2, 2.0, 1.5, 1.8, 1.0, 2.2, 1.6]
X_high, y_labels = make_blobs(
    n_samples=700, n_features=20, centers=n_clusters, cluster_std=cluster_stds, random_state=42
)
X_2d = TSNE(n_components=2, perplexity=30, random_state=42).fit_transform(X_high)

df = pd.DataFrame({"tsne_1": X_2d[:, 0], "tsne_2": X_2d[:, 1], "cluster": [CELL_TYPES[idx] for idx in y_labels]})

centroids = df.groupby("cluster")[["tsne_1", "tsne_2"]].mean()
centroids = centroids.loc[CELL_TYPES].reset_index()
centroids["abbr"] = [CELL_ABBR[c] for c in centroids["cluster"]]

# Tighten the scale domain to the data extent (+6% pad) instead of Altair's
# default "nice" auto-domain — sparse t-SNE clusters otherwise leave large
# unused margins on the canvas.
x_min, x_max = df["tsne_1"].min(), df["tsne_1"].max()
y_min, y_max = df["tsne_2"].min(), df["tsne_2"].max()
x_pad, y_pad = (x_max - x_min) * 0.06, (y_max - y_min) * 0.06
x_domain = [x_min - x_pad, x_max + x_pad]
y_domain = [y_min - y_pad, y_max + y_pad]

# Interactive selection bound to legend — clicking a cell type highlights its cluster
selection = alt.selection_point(fields=["cluster"], bind="legend")

# Marker size/opacity tuned for 700 overlapping points (high-density heuristic)
scatter = (
    alt.Chart(df)
    .mark_circle(size=45, strokeWidth=0.5)
    .encode(
        x=alt.X(
            "tsne_1:Q",
            scale=alt.Scale(domain=x_domain, nice=False),
            axis=alt.Axis(labels=False, ticks=False, domain=False, grid=False, title="t-SNE Dimension 1"),
        ),
        y=alt.Y(
            "tsne_2:Q",
            scale=alt.Scale(domain=y_domain, nice=False),
            axis=alt.Axis(labels=False, ticks=False, domain=False, grid=False, title="t-SNE Dimension 2"),
        ),
        color=alt.Color(
            "cluster:N", scale=alt.Scale(domain=CELL_TYPES, range=IMPRINT), legend=alt.Legend(title="Cell Type")
        ),
        opacity=alt.condition(selection, alt.value(0.55), alt.value(0.12)),
        stroke=alt.value(PAGE_BG),
        tooltip=[
            "cluster:N",
            alt.Tooltip("tsne_1:Q", title="t-SNE 1", format=".2f"),
            alt.Tooltip("tsne_2:Q", title="t-SNE 2", format=".2f"),
        ],
    )
    .add_params(selection)
)

# Direct abbreviated-name labels on centroids (no legend cross-referencing needed).
# A background-colored halo stroke behind the text keeps labels legible against
# every cluster hue in both themes, independent of the underlying data color.
centroid_marks = (
    alt.Chart(centroids)
    .mark_text(fontSize=13, fontWeight="bold", dy=-9, stroke=PAGE_BG, strokeWidth=0.75)
    .encode(
        x=alt.X("tsne_1:Q", scale=alt.Scale(domain=x_domain, nice=False)),
        y=alt.Y("tsne_2:Q", scale=alt.Scale(domain=y_domain, nice=False)),
        text="abbr:N",
        color=alt.value(INK),
    )
)

title_text = "scatter-embedding · python · altair · anyplot.ai"
title_params = alt.TitleParams(
    text=title_text,
    subtitle="t-SNE (perplexity=30) · 20-dimensional synthetic scRNA-seq · 7 cell types, 700 cells",
    fontSize=round(18 * min(1.0, 67 / len(title_text))),
    subtitleFontSize=12,
    color=INK,
    subtitleColor=INK_SOFT,
    anchor="start",
)

chart = (
    alt.layer(scatter, centroid_marks)
    .properties(
        width=620,
        height=320,
        padding={"left": 0, "right": 0, "top": 0, "bottom": 0},
        title=title_params,
        background=PAGE_BG,
    )
    .interactive()
    .configure_view(continuousWidth=620, continuousHeight=320, fill=PAGE_BG, stroke="transparent")
    .configure_axis(titleColor=INK, titleFontSize=12)
    .configure_legend(
        fillColor=ELEVATED_BG,
        strokeColor=INK_SOFT,
        labelColor=INK_SOFT,
        titleColor=INK,
        labelFontSize=10,
        titleFontSize=10,
        cornerRadius=4,
        padding=8,
    )
)

# Save
chart.save(f"plot-{THEME}.png", scale_factor=4.0)

# Pad-only to the canonical canvas — vl-convert's title/legend padding makes the
# saved PNG larger than width*scale_factor. Never crop: cropping clips title/axis
# labels and trips the AR-09 edge-clipping auto-reject.
TW, TH = 3200, 1800
_img = Image.open(f"plot-{THEME}.png").convert("RGB")
_w, _h = _img.size
if _w > TW or _h > TH:
    raise SystemExit(
        f"altair vl-convert produced {_w}x{_h}, exceeds target {TW}x{TH}. "
        f"Shrink chart .properties(width=, height=) values and re-render."
    )
if _w < TW or _h < TH:
    _canvas = Image.new("RGB", (TW, TH), PAGE_BG)
    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))
    _canvas.save(f"plot-{THEME}.png")

chart.save(f"plot-{THEME}.html")

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

Other implementations