Skip to content

Commit 6aa4cec

Browse files
committed
vignettes/articles: add CELLxGENE Census real-data article
- vignettes/articles/soma-census-real.Rmd: streams CD4 T cells from Census public S3 via readSOMA(), runs Seurat pipeline, exports h5ad - .Rbuildignore: exclude vignettes/articles/ from package tarball - _pkgdown.yml: list article under Applications nav - pkgdown.yaml: add restore step to copy pre-built HTML into docs/articles/ after build_site(); CI skips chunk eval via .eval = !nzchar(Sys.getenv("CI"))
1 parent 964bcbc commit 6aa4cec

4 files changed

Lines changed: 219 additions & 3 deletions

File tree

‎.Rbuildignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
^README\.html$
1212
^cran-comments\.md$
1313

14+
# Pre-built articles (knitted locally; not built by R CMD check or CI)
15+
^vignettes/articles$
16+
1417
# Vignette data artifacts
1518
^vignettes/.*\.loom$
1619
^vignettes/.*\.h5ad$

‎.github/workflows/pkgdown.yaml‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,23 @@ jobs:
5151

5252
- name: Build site
5353
shell: Rscript {0}
54-
# clean_site() wipes the checked-in docs/ directory (which holds a
55-
# stale pre-built site). Without it, pkgdown::build_site() aborts
56-
# because it refuses to overwrite a directory it did not create.
54+
# clean_site() wipes docs/ first; build_site() then rebuilds all
55+
# articles. Pre-built articles in vignettes/articles/ are skipped at
56+
# evaluation time (CI env var disables chunk eval) and restored by the
57+
# next step, which overwrites the empty CI-built HTML with the
58+
# committed pre-built version.
5759
run: |
5860
pkgdown::clean_site(force = TRUE)
5961
pkgdown::build_site(new_process = FALSE)
6062
63+
- name: Restore pre-built articles
64+
shell: bash
65+
run: |
66+
mkdir -p docs/articles
67+
for f in vignettes/articles/*.html; do
68+
[ -f "$f" ] && cp "$f" docs/articles/ && echo "Restored: $(basename $f)"
69+
done
70+
6171
- name: Deploy to GitHub pages
6272
uses: JamesIves/github-pages-deploy-action@v4.6.8
6373
with:

‎_pkgdown.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,5 @@ articles:
224224
- convert-spatialdata
225225
- immune-repertoire
226226
- atlas-bpcells
227+
- articles/soma-census-real
227228

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
---
2+
title: "CELLxGENE Census: Real-world Atlas Query"
3+
description: >
4+
Stream CD4 T cells from the 60M-cell CELLxGENE Census directly into
5+
Seurat, run a standard analysis pipeline, and export to h5ad — no
6+
intermediate download required.
7+
output: rmarkdown::html_vignette
8+
vignette: >
9+
%\VignetteIndexEntry{CELLxGENE Census: Real-world Atlas Query}
10+
%\VignetteEngine{knitr::rmarkdown}
11+
%\VignetteEncoding{UTF-8}
12+
---
13+
14+
```{r setup, include = FALSE}
15+
.eval <- !nzchar(Sys.getenv("CI"))
16+
knitr::opts_chunk$set(
17+
collapse = TRUE,
18+
comment = "#>",
19+
eval = .eval,
20+
fig.width = 7,
21+
fig.height = 5,
22+
message = FALSE,
23+
warning = FALSE
24+
)
25+
suppressPackageStartupMessages({
26+
library(scConvert)
27+
library(Seurat)
28+
library(ggplot2)
29+
})
30+
set.seed(42L)
31+
```
32+
33+
## Overview
34+
35+
[CELLxGENE Census](https://chanzuckerberg.github.io/cellxgene-census/)
36+
hosts 60M+ cells across 900+ datasets as a single queryable
37+
[TileDB-SOMA](https://github.com/single-cell-data/TileDB-SOMA) experiment
38+
on public S3. This article demonstrates the two-step pattern that
39+
scConvert enables:
40+
41+
1. Issue a Census slice query via `cellxgene.census`
42+
2. Pass the URI directly to `readSOMA()` — Seurat object materialised on
43+
the fly, no intermediate file written
44+
45+
We then run a standard Seurat analysis pipeline and export to h5ad for
46+
Python interoperability.
47+
48+
### Prerequisites
49+
50+
```r
51+
install.packages("cellxgene.census",
52+
repos = c(
53+
"https://chanzuckerberg.r-universe.dev",
54+
"https://cloud.r-project.org"
55+
)
56+
)
57+
install.packages("tiledbsoma",
58+
repos = c(
59+
"https://tiledb-inc.r-universe.dev",
60+
"https://cloud.r-project.org"
61+
)
62+
)
63+
```
64+
65+
```{r pkg-check, include = FALSE}
66+
has_census <- requireNamespace("cellxgene.census", quietly = TRUE) &&
67+
requireNamespace("tiledbsoma", quietly = TRUE)
68+
```
69+
70+
## Step 1 — Open Census and obtain the slice URI
71+
72+
```{r open-census}
73+
library(cellxgene.census)
74+
library(tiledbsoma)
75+
76+
census <- open_soma(census_version = "stable")
77+
human_uri <- census$get("census_data")$get("homo_sapiens")$uri
78+
cat("Human Census URI:", human_uri, "\n")
79+
```
80+
81+
## Step 2 — Stream CD4 T cells into Seurat
82+
83+
`readSOMA()` opens the URI with tiledbsoma, applies the value filter
84+
server-side (only matching chunks are fetched over the wire), and
85+
materialises a Seurat object.
86+
87+
```{r read-soma}
88+
obs_filter <- paste(
89+
"cell_type == 'CD4-positive, alpha-beta T cell'",
90+
"& tissue_general == 'blood'",
91+
"& is_primary_data == True"
92+
)
93+
94+
cd4 <- readSOMA(
95+
uri = human_uri,
96+
measurement = "RNA",
97+
obs_query = obs_filter
98+
)
99+
100+
census$close()
101+
cat("Loaded:", ncol(cd4), "cells x", nrow(cd4), "genes\n")
102+
cd4
103+
```
104+
105+
```{r donor-summary}
106+
donors <- table(cd4$donor_id)
107+
cat("Donors:", length(donors), "\n")
108+
cat("Datasets:", length(unique(cd4$dataset_id)), "\n")
109+
```
110+
111+
## Step 3 — Standard Seurat pipeline
112+
113+
```{r seurat-pipeline}
114+
cd4 <- NormalizeData(cd4, verbose = FALSE)
115+
cd4 <- FindVariableFeatures(cd4, nfeatures = 2000L, verbose = FALSE)
116+
cd4 <- ScaleData(cd4, verbose = FALSE)
117+
cd4 <- RunPCA(cd4, npcs = 30L, verbose = FALSE)
118+
cd4 <- RunUMAP(cd4, dims = 1:20, verbose = FALSE)
119+
cd4 <- FindNeighbors(cd4, dims = 1:20, verbose = FALSE)
120+
cd4 <- FindClusters(cd4, resolution = 0.4, verbose = FALSE)
121+
```
122+
123+
## Step 4 — Visualisation
124+
125+
### UMAP coloured by cluster
126+
127+
```{r umap-cluster, fig.height = 5}
128+
DimPlot(cd4, reduction = "umap", label = TRUE, pt.size = 0.4) +
129+
ggtitle("CD4 T cells (CELLxGENE Census, blood)") +
130+
NoLegend()
131+
```
132+
133+
### Key marker expression
134+
135+
CD4 itself, the IL7R memory marker, and CCR7 (naive/central-memory).
136+
137+
```{r feature-plot, fig.height = 4}
138+
FeaturePlot(
139+
cd4,
140+
features = c("CD4", "IL7R", "CCR7", "FOXP3"),
141+
ncol = 2L,
142+
pt.size = 0.3,
143+
order = TRUE
144+
)
145+
```
146+
147+
### Donor composition per cluster
148+
149+
```{r donor-heatmap, fig.height = 5}
150+
donor_mat <- table(
151+
cluster = Idents(cd4),
152+
donor = cd4$donor_id
153+
)
154+
donor_frac <- sweep(donor_mat, 1L, rowSums(donor_mat), "/")
155+
pheatmap::pheatmap(
156+
donor_frac,
157+
cluster_rows = TRUE,
158+
cluster_cols = TRUE,
159+
main = "Donor fraction per cluster",
160+
fontsize = 8L
161+
)
162+
```
163+
164+
## Step 5 — Export to h5ad
165+
166+
```{r write-h5ad}
167+
h5ad_path <- file.path(tempdir(), "census_cd4.h5ad")
168+
writeH5AD(cd4, h5ad_path)
169+
cat("Written:", h5ad_path, "\n")
170+
cat("File size:", round(file.info(h5ad_path)$size / 1e6, 1), "MB\n")
171+
```
172+
173+
### Verify with readH5AD
174+
175+
```{r roundtrip}
176+
cd4_rt <- readH5AD(h5ad_path)
177+
stopifnot(
178+
ncol(cd4_rt) == ncol(cd4),
179+
nrow(cd4_rt) == nrow(cd4)
180+
)
181+
cat("Round-trip OK:", ncol(cd4_rt), "cells x", nrow(cd4_rt), "genes\n")
182+
```
183+
184+
```{r cleanup, include = FALSE}
185+
unlink(h5ad_path)
186+
```
187+
188+
## Key takeaways
189+
190+
- **No download required**: tiledbsoma streams only the requested cells
191+
via S3 byte-range requests; the full 60M-cell Census never touches disk.
192+
- **Two-step pattern**: `open_soma()` + `readSOMA()` is the recommended
193+
route for Census queries; scConvert handles the SOMA-to-Seurat conversion.
194+
- **Format portability**: `writeH5AD()` produces a scanpy-compatible h5ad
195+
from any Seurat object in one call, enabling downstream Python analysis
196+
without reloading from Census.
197+
198+
## Session Info
199+
200+
```{r session-info}
201+
sessionInfo()
202+
```

0 commit comments

Comments
 (0)