Nebulosa Density Plots
Nebulosa uses weighted kernel density estimation to address the overplotting problem in single-cell embeddings. Instead of coloring each cell by raw expression, it computes a smoothed density surface weighted by gene expression.
The core idea: cells with high expression in dense regions produce bright density peaks, while isolated expressing cells produce dimmer signals.
Preparing data
Create a synthetic AnnData with two clusters and a cluster-specific gene:
import numpy as np
import pandas as pd
import anndata as ad
np.random.seed(42)
n = 500
# Two clusters in UMAP space
coords = np.vstack([
np.random.normal(loc=[-2, -2], scale=0.8, size=(n // 2, 2)),
np.random.normal(loc=[2, 2], scale=0.8, size=(n // 2, 2)),
])
# Gene expressed only in cluster 1
expr = np.zeros((n, 1))
expr[:n // 2, 0] = np.random.exponential(2, size=n // 2)
adata = ad.AnnData(
X=expr,
var=pd.DataFrame(index=['MarkerGene']),
obsm={'X_umap': coords},
)
Computing density values
Use show=False to get per-cell density values (e.g. for downstream use):
from sjanpy.pl import nebulosa_density
densities = nebulosa_density(
adata,
coord_key='X_umap',
gene='MarkerGene',
show=False,
)
print(densities.shape) # (500,)
# Store in obs for other plotting tools
adata.obs['marker_density'] = densities
Plotting
Use show=True to produce a scatter plot colored by density:
nebulosa_density(
adata,
coord_key='X_umap',
gene='MarkerGene',
show=True,
cmap='magma',
)
Adjusting bandwidth
The adjust parameter scales the KDE bandwidth. Smaller values give sharper
peaks; larger values produce smoother density fields:
# Sharper
nebulosa_density(adata, 'X_umap', 'MarkerGene', adjust=0.5, show=True)
# Smoother
nebulosa_density(adata, 'X_umap', 'MarkerGene', adjust=2.0, show=True)
Low-level 2D KDE
Use wkde2d() directly for custom workflows:
from sjanpy.pl.nebulosa import wkde2d
x = coords[:, 0]
y = coords[:, 1]
w = expr[:, 0]
gx, gy, z = wkde2d(x, y, w, adjust=1.0, n=100)
print(z.shape) # (100, 100)
3D Weighted KDE
For 3D embeddings (e.g. from UMAP with n_components=3), use
wkde3d():
from sjanpy.pl.nebulosa import wkde3d
# Synthetic 3D coordinates
coords_3d = np.random.randn(200, 3)
weights = np.random.exponential(1, size=200)
gx, gy, gz, Z = wkde3d(
coords_3d[:, 0],
coords_3d[:, 1],
coords_3d[:, 2],
weights,
adjust=1.0,
n=30,
)
print(Z.shape) # (30, 30, 30)
The returned grid and density array can be visualized with Plotly or matplotlib’s 3D scatter.