Skip to content

Commit 2c67da6

Browse files
committed
Add dark-background 3D UMAP rotation for profile
1 parent 6a61183 commit 2c67da6

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

docs/slow_rotation.gif

1.56 MB
Loading

scripts/make_slow_rotation.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Slow-rotating 3D UMAP for profile banner. Clean, minimal, floaty."""
2+
3+
import scanpy as sc
4+
import matplotlib.pyplot as plt
5+
import imageio.v3 as iio
6+
from pathlib import Path
7+
from io import BytesIO
8+
9+
RESULTS_DIR = Path("results")
10+
11+
PALETTE = {
12+
"CD4+ T cells": "#E69F00",
13+
"CD8+ T cells": "#56B4E9",
14+
"NK cells": "#009E73",
15+
"B cells": "#F0E442",
16+
"CD14+ Monocytes": "#0072B2",
17+
"FCGR3A+ Monocytes": "#D55E00",
18+
"Dendritic cells": "#CC79A7",
19+
"Megakaryocytes": "#999999",
20+
}
21+
22+
BG = "#0d1117"
23+
N_FRAMES = 120
24+
FPS = 20
25+
26+
27+
def render_frame(coords_3d, cell_types, azim, elev=20):
28+
fig = plt.figure(figsize=(9, 6), facecolor=BG)
29+
ax = fig.add_subplot(111, projection="3d", facecolor=BG)
30+
31+
for ct in cell_types.cat.categories:
32+
mask = cell_types == ct
33+
ax.scatter(
34+
coords_3d[mask, 0], coords_3d[mask, 1], coords_3d[mask, 2],
35+
c=PALETTE.get(ct, "#AAAAAA"), s=5, alpha=0.8,
36+
label=ct, edgecolors="none",
37+
)
38+
39+
ax.view_init(elev=elev, azim=azim)
40+
41+
# Clean axes
42+
ax.set_xticks([])
43+
ax.set_yticks([])
44+
ax.set_zticks([])
45+
ax.xaxis.pane.fill = False
46+
ax.yaxis.pane.fill = False
47+
ax.zaxis.pane.fill = False
48+
ax.xaxis.pane.set_edgecolor(BG)
49+
ax.yaxis.pane.set_edgecolor(BG)
50+
ax.zaxis.pane.set_edgecolor(BG)
51+
ax.xaxis.line.set_color(BG)
52+
ax.yaxis.line.set_color(BG)
53+
ax.zaxis.line.set_color(BG)
54+
ax.grid(False)
55+
56+
ax.legend(
57+
loc="upper left", fontsize=7, framealpha=0.0,
58+
labelcolor="#c9d1d9", edgecolor="none",
59+
markerscale=3,
60+
)
61+
62+
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
63+
buf = BytesIO()
64+
fig.savefig(buf, format="png", dpi=110, facecolor=BG, edgecolor="none")
65+
plt.close(fig)
66+
buf.seek(0)
67+
return iio.imread(buf)
68+
69+
70+
def main():
71+
adata = sc.read_h5ad(RESULTS_DIR / "05_annotated.h5ad")
72+
print(f"Loaded {adata.n_obs} cells")
73+
74+
# Compute 3D UMAP
75+
if "neighbors" not in adata.uns:
76+
sc.pp.neighbors(adata, n_neighbors=15, n_pcs=40)
77+
sc.tl.umap(adata, n_components=3)
78+
coords_3d = adata.obsm["X_umap"]
79+
cell_types = adata.obs["cell_type"]
80+
81+
print(f"Rendering {N_FRAMES} frames...")
82+
frames = []
83+
for i in range(N_FRAMES):
84+
azim = (i / N_FRAMES) * 360
85+
frame = render_frame(coords_3d, cell_types, azim)
86+
frames.append(frame)
87+
if (i + 1) % 30 == 0:
88+
print(f" {i + 1}/{N_FRAMES}")
89+
90+
out_path = Path("docs") / "slow_rotation.gif"
91+
out_path.parent.mkdir(exist_ok=True)
92+
iio.imwrite(out_path, frames, duration=int(1000 / FPS), loop=0)
93+
print(f"Done! {out_path.stat().st_size / 1024 / 1024:.1f} MB")
94+
95+
96+
if __name__ == "__main__":
97+
main()

0 commit comments

Comments
 (0)