stxBrain dataset with CellChatThis vignette demonstrates a spatial transcriptomics workflow for
inferring ligand-receptor-mediated cell-cell communication in the
stxBrain dataset from Seurat (Hao et
al. 2023). The analysis follows the main steps described in the
CellChat framework (Jin et al. 2021; Jin, Plikus,
and Nie 2025), with adaptations for referred dataset.
Starting from a Seurat object, the workflow normalizes gene expression, performs dimensionality reduction and clustering, and prepares the main inputs required by CellChat. CellChat is then used to infer communication probabilities between Seurat clusters.
Because CellChat infers communication at the group level rather than directly at the spot-by-spot level, this vignette also includes an additional step to project cluster-level ligand-receptor predictions onto neighboring spatial spots.
Therefore, the final spot-level communication table should be interpreted as a spatial projection of CellChat group-level predictions onto local spot neighborhoods. It does not represent independent de novo CellChat inference for each individual spot pair.
This workflow is based on the official CellChat spatial transcriptomics tutorial: CellChat inference in spatial transcriptomics.
# Install helper packages if needed
if (!requireNamespace("devtools", quietly = TRUE)) {
install.packages("devtools")
}
if (!requireNamespace("BiocManager", quietly = TRUE)) {
install.packages("BiocManager")
}
if (!requireNamespace("SpatialCellChat", quietly = TRUE)) {
devtools::install_github("jinworks/SpatialCellChat")
}
if (!requireNamespace("CellChat", quietly = TRUE)) {
devtools::install_github("jinworks/CellChat")
}
if (!requireNamespace("BiocNeighbors", quietly = TRUE)) {
BiocManager::install("BiocNeighbors")
}
if (!requireNamespace("MERINGUE", quietly = TRUE)) {
devtools::install_github("JEFworks-Lab/MERINGUE")
}
if (!requireNamespace("ALRA", quietly = TRUE)) {
devtools::install_github("KlugerLab/ALRA")
}
if (!requireNamespace("RcppML", quietly = TRUE)) {
devtools::install_github("zdebruine/RcppML")
}
if (!requireNamespace("presto", quietly = TRUE)) {
devtools::install_github("immunogenomics/presto")
}
# Load packages
library(CellChat)
library(patchwork)
library(Seurat)
library(SeuratObject)
library(SeuratData)
library(dplyr)
library(tibble)
options(stringsAsFactors = FALSE)
# Load Seurat data: stxbrain
seurat_obj <- LoadData("stxBrain", type = "anterior1")
#> Validating object structure
#> Updating object slots
#> Ensuring keys are in the proper structure
#> Ensuring keys are in the proper structure
#> Ensuring feature names don't have underscores or pipes
#> Updating slots in Spatial
#> Updating slots in anterior1
#> Warning: Not validating Centroids objects
#> Updated Centroids object 'centroids' in FOV 'anterior1'
#> Updated boundaries in FOV 'anterior1'
#> Validating object structure for Assay5 'Spatial'
#> Validating object structure for VisiumV2 'anterior1'
#> Object representation is consistent with the most current Seurat version
# Normalize, reduce dimensions, and annotate clusters
seurat_obj <- SCTransform(seurat_obj, assay = "Spatial", verbose = FALSE)
seurat_obj <- RunPCA(seurat_obj, assay = "SCT", verbose = FALSE)
seurat_obj <- FindNeighbors(seurat_obj, reduction = "pca", dims = 1:30)
#> Computing nearest neighbor graph
#> Computing SNN
seurat_obj <- FindClusters(seurat_obj, verbose = FALSE)
# Plot Seurat object
Seurat::SpatialDimPlot(seurat_obj, label = T, label.size = 3)
As described by the maintainers, CellChat requires four user set inputs:
data.input (Gene expression data of
spots/cells)
meta (User assigned cell labels and samples
labels)
coordinates (Spatial coordinates of
spots/cells)
spatial.factors (Spatial factors of spatial
distance)
data.input = Seurat::GetAssayData(seurat_obj, layer = "data", assay = "SCT") # normalized data matrix
# define the meta data:
# a column named `samples` should be provided for spatial transcriptomics analysis, which is useful for analyzing cell-cell communication by aggregating multiple samples/replicates. Of note, for comparison analysis across different conditions, users still need to create a CellChat object seperately for each condition.
meta = data.frame(labels = Seurat::Idents(seurat_obj), samples = "sample1",
row.names = names(Seurat::Idents(seurat_obj))) # manually create a dataframe consisting of the cell labels
meta$labels <- paste0("s_",as.character(meta$labels))
# Spatial locations of spots from full (NOT high/low) resolution images are required.
spatial.locs = Seurat::GetTissueCoordinates(seurat_obj, scale = NULL, cols = c("imagerow", "imagecol"))
spatial.locs <- spatial.locs %>% select(-cell)
# Spatial factors of spatial coordinates
d.spatial <- computeCellDistance(coordinates = spatial.locs)
spot.size = 55 # the theoretical spot size (um) in 10X Visium
spatial.factors = data.frame(ratio = 100/min(d.spatial[d.spatial!=0]), tol = spot.size/2)
cellchat <- createCellChat(object = data.input,
meta = meta,
group.by = "labels",
datatype = "spatial",
coordinates = spatial.locs,
spatial.factors = spatial.factors)
#> [1] "Create a CellChat object from a data matrix"
#> Create a CellChat object from spatial transcriptomics data...
#> Warning in createCellChat(object = data.input, meta = meta, group.by = "labels", : The 'meta$samples' is not a factor. We now force it as a factor!
#> Set cell identities for the new CellChat object
#> The cell groups used for CellChat analysis are s_0, s_1, s_10, s_11, s_12, s_13, s_14, s_2, s_3, s_4, s_5, s_6, s_7, s_8, s_9
cellchat
#> An object of class CellChat created from a single dataset
#> 17668 genes.
#> 2696 cells.
#> CellChat analysis of spatial data! The input spatial locations are
#> x_cent y_cent
#> AAACAAGTATCTCCCA-1 8501 7475
#> AAACACCAATAACTGC-1 2788 8553
#> AAACAGAGCGACTCCT-1 7950 3164
#> AAACAGCTTTCAGAAG-1 2099 6637
#> AAACAGGGTCTATATT-1 2375 7116
#> AAACATGGTGAGAGGA-1 1480 8913
CellChatDB <- CellChatDB.mouse
showDatabaseCategory(CellChatDB)
# use a subset of CellChatDB for cell-cell communication analysis
CellChatDB.use <- subsetDB(CellChatDB, search = "Secreted Signaling", key = "annotation") # use Secreted Signaling
# set the used database in the object
cellchat@DB <- CellChatDB.use
# subset the expression data of signaling genes for saving computation cost
cellchat <- subsetData(cellchat) # This step is necessary even if using the whole database
future::plan("multisession", workers = 4)
cellchat <- identifyOverExpressedGenes(cellchat)
cellchat <- identifyOverExpressedInteractions(cellchat, variable.both = F)
#> The number of highly variable ligand-receptor pairs used for signaling inference is 821
Note: this step may take some time.
Considering that we are interested in spot neighborhood communication, the following inference can be restricted by some attributes, such as ligand-receptor distance no greater than 150um. Moreover, set contect.range = 100 will restrict contact-dependent signalling to the spot-spot mean distance, 100um.
cellchat <- computeCommunProb(cellchat,
type = "triMean",
distance.use = TRUE,
interaction.range = 150,
scale.distance = 0.01,
contact.range = 100,
contact.dependent = TRUE,
contact.knn.k = 6, #limit to theorical 6 nearest neighbors
nboot = 100)
#> triMean is used for calculating the average gene expression per cell group.
#> [1] ">>> Run CellChat on spatial transcriptomics data using distances as constraints of the computed communication probability <<< [2026-07-07 00:15:24.514288]"
#> Molecules of the input L-R pairs are diffusible. Run CellChat in a diffusion manner based on the `interaction.range`.
#> [1] ">>> CellChat inference is done. Parameter values are stored in `object@options$parameter` <<< [2026-07-07 00:24:36.27658]"
cellchat <- filterCommunication(cellchat, min.cells = 10)
df.net <- subsetCommunication(cellchat,thresh = 0.05)
CellChat infers communication probabilities at the group level, not directly at the spot-by-spot level. In this analysis, each group corresponds to a Seurat cluster. Therefore, CellChat estimates ligand-receptor communication between clusters, such as cluster 1 to cluster 2, but it does not directly report which individual spatial spots are involved in each interaction.
To approximate spot-level communication, we first identify all pairs
of neighboring spots within the same spatial interaction range used in
computeCommunProb(). Then, each spot pair is annotated with
its corresponding source and target cluster identity. Finally, these
spatially neighboring spot pairs are joined with the CellChat
group-level ligand-receptor predictions. As a result, each inferred
cluster-level interaction is assigned to all neighboring spot pairs that
match the same source-target cluster combination.
# Spot IDs from metadata
spot.ids <- rownames(cellchat@meta)
# Spot-to-group annotation
spot.groups <- tibble(
spot = spot.ids,
group = as.character(cellchat@idents)
)
# Reorder coordinates to match CellChat metadata
coords <- spatial.locs[spot.ids, , drop = FALSE]
# Convert pixel coordinates to micrometers
coords.um <- coords * spatial.factors$ratio
# Pairwise spot distances
dmat <- as.matrix(dist(coords.um))
diag(dmat) <- Inf
# Same interaction range used in computeCommunProb()
interaction.range <- 150
idx <- which(dmat <= interaction.range, arr.ind = TRUE)
spot.pairs <- tibble(
source_spot = rownames(dmat)[idx[, 1]],
target_spot = colnames(dmat)[idx[, 2]],
distance_um = dmat[idx]
)
# Add source and target group identities
spot.pairs <- spot.pairs %>%
left_join(
spot.groups,
by = c("source_spot" = "spot")
) %>%
rename(source = group) %>%
left_join(
spot.groups,
by = c("target_spot" = "spot")
) %>%
rename(target = group)
The resulting spot.pairs object contains all neighboring
spot pairs within the selected distance threshold, together with their
corresponding source and target cluster annotations.
# Join nearby spot pairs with CellChat group-level predictions
spot.to.spot.communication <- spot.pairs %>%
inner_join(df.net, by = c("source", "target")) %>%
arrange(source_spot, target_spot, pathway_name, interaction_name)
#> Warning in inner_join(., df.net, by = c("source", "target")): Detected an unexpected many-to-many relationship between `x` and `y`.
#> ℹ Row 1 of `x` matches multiple rows in `y`.
#> ℹ Row 273 of `y` matches multiple rows in `x`.
#> ℹ If a many-to-many relationship is expected, set `relationship =
#> "many-to-many"` to silence this warning.
head(spot.to.spot.communication)
#> # A tibble: 6 × 14
#> source_spot target_spot distance_um source target ligand receptor prob
#> <chr> <chr> <dbl> <chr> <chr> <chr> <chr> <dbl>
#> 1 AAACAAGTATCTCCC… ACTAGTTGCG… 100 s_1 s_1 Fgf1 Fgfr1 0.0143
#> 2 AAACAAGTATCTCCC… ACTAGTTGCG… 100 s_1 s_1 Fgf1 Fgfr2 0.0159
#> 3 AAACAAGTATCTCCC… ACTAGTTGCG… 100 s_1 s_1 Fgf1 Fgfr3 0.0242
#> 4 AAACAAGTATCTCCC… ACTAGTTGCG… 100 s_1 s_1 Gas6 Axl 0.00518
#> 5 AAACAAGTATCTCCC… ACTAGTTGCG… 100 s_1 s_1 Mdk Sdc4 0.0143
#> 6 AAACAAGTATCTCCC… ACTAGTTGCG… 100 s_1 s_1 Mdk Ptprz1 0.0316
#> # ℹ 6 more variables: pval <dbl>, interaction_name <fct>,
#> # interaction_name_2 <chr>, pathway_name <chr>, annotation <chr>,
#> # evidence <chr>
saveRDS(spot.to.spot.communication, file = "spot-spot_commu_Allbrain.rds")
#> R version 4.5.3 (2026-03-11 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26200)
#>
#> Matrix products: default
#> LAPACK version 3.12.1
#>
#> locale:
#> [1] LC_COLLATE=Portuguese_Brazil.utf8 LC_CTYPE=Portuguese_Brazil.utf8
#> [3] LC_MONETARY=Portuguese_Brazil.utf8 LC_NUMERIC=C
#> [5] LC_TIME=Portuguese_Brazil.utf8
#>
#> time zone: America/Sao_Paulo
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] future_1.70.0 tibble_3.3.1
#> [3] stxBrain.SeuratData_0.1.2 SeuratData_0.2.2.9002
#> [5] Seurat_5.5.1 SeuratObject_5.4.0
#> [7] sp_2.2-1 patchwork_1.3.2
#> [9] CellChat_2.2.0.9001 Biobase_2.70.0
#> [11] BiocGenerics_0.56.0 generics_0.1.4
#> [13] ggplot2_4.0.3 igraph_2.3.3
#> [15] dplyr_1.2.1
#>
#> loaded via a namespace (and not attached):
#> [1] RcppAnnoy_0.0.23 splines_4.5.3
#> [3] later_1.4.8 polyclip_1.10-7
#> [5] ggnetwork_0.5.14 fastDummies_1.7.6
#> [7] lifecycle_1.0.5 rstatix_1.0.0
#> [9] doParallel_1.0.17 globals_0.19.1
#> [11] lattice_0.22-9 MASS_7.3-65
#> [13] backports_1.5.1 magrittr_2.0.5
#> [15] plotly_4.12.0 sass_0.4.10
#> [17] rmarkdown_2.31 jquerylib_0.1.4
#> [19] yaml_2.3.12 httpuv_1.6.17
#> [21] otel_0.2.0 collapse_2.1.7
#> [23] glmGamPoi_1.22.0 NMF_0.28
#> [25] sctransform_0.4.3 spam_2.11-4
#> [27] spatstat.sparse_3.2-0 reticulate_1.46.0
#> [29] cowplot_1.2.0 pbapply_1.7-4
#> [31] RColorBrewer_1.1-3 abind_1.4-8
#> [33] GenomicRanges_1.62.1 Rtsne_0.17
#> [35] presto_1.0.0 purrr_1.2.2
#> [37] rappdirs_0.3.4 circlize_0.4.18
#> [39] IRanges_2.44.0 S4Vectors_0.48.1
#> [41] ggrepel_0.9.8 irlba_2.3.7
#> [43] listenv_1.0.0 spatstat.utils_3.2-3
#> [45] goftest_1.2-3 RSpectra_0.16-2
#> [47] spatstat.random_3.5-0 fitdistrplus_1.2-6
#> [49] parallelly_1.48.0 DelayedMatrixStats_1.32.0
#> [51] svglite_2.2.2 DelayedArray_0.36.1
#> [53] codetools_0.2-20 tidyselect_1.2.1
#> [55] shape_1.4.6.1 farver_2.1.2
#> [57] matrixStats_1.5.0 stats4_4.5.3
#> [59] spatstat.explore_3.8-1 Seqinfo_1.0.0
#> [61] jsonlite_2.0.0 GetoptLong_1.1.1
#> [63] BiocNeighbors_2.4.0 progressr_1.0.0
#> [65] Formula_1.2-5 ggridges_0.5.7
#> [67] ggalluvial_0.12.6 survival_3.8-6
#> [69] iterators_1.0.14 systemfonts_1.3.2
#> [71] foreach_1.5.2 tools_4.5.3
#> [73] sna_2.8 ica_1.0-3
#> [75] Rcpp_1.1.2 glue_1.8.1
#> [77] SparseArray_1.10.10 gridExtra_2.3.1
#> [79] xfun_0.59 MatrixGenerics_1.22.0
#> [81] withr_3.0.3 BiocManager_1.30.27
#> [83] fastmap_1.2.0 digest_0.6.39
#> [85] R6_2.6.1 mime_0.13
#> [87] textshaping_1.0.5 colorspace_2.1-2
#> [89] scattermore_1.2 tensor_1.5.1
#> [91] spatstat.data_3.1-9 utf8_1.2.6
#> [93] tidyr_1.3.2 data.table_1.18.4
#> [95] FNN_1.1.4.1 S4Arrays_1.10.1
#> [97] httr_1.4.8 htmlwidgets_1.6.4
#> [99] uwot_0.2.4 pkgconfig_2.0.3
#> [101] gtable_0.3.6 registry_0.5-1
#> [103] ComplexHeatmap_2.26.1 lmtest_0.9-40
#> [105] S7_0.2.2 XVector_0.50.0
#> [107] htmltools_0.5.9 carData_3.0-6
#> [109] dotCall64_1.2 clue_0.3-68
#> [111] scales_1.4.0 png_0.1-9
#> [113] spatstat.univar_3.2-0 knitr_1.51
#> [115] rstudioapi_0.19.0 reshape2_1.4.5
#> [117] rjson_0.2.23 coda_0.19-4.1
#> [119] statnet.common_4.13.0 nlme_3.1-168
#> [121] cachem_1.1.0 zoo_1.8-15
#> [123] GlobalOptions_0.1.4 stringr_1.6.0
#> [125] KernSmooth_2.23-26 parallel_4.5.3
#> [127] miniUI_0.1.2 pillar_1.11.1
#> [129] grid_4.5.3 vctrs_0.7.3
#> [131] RANN_2.6.2 promises_1.5.0
#> [133] ggpubr_0.6.3 car_3.1-5
#> [135] beachmat_2.26.0 xtable_1.8-8
#> [137] cluster_2.1.8.2 evaluate_1.0.5
#> [139] cli_3.6.6 compiler_4.5.3
#> [141] rlang_1.2.0 crayon_1.5.3
#> [143] rngtools_1.5.2 future.apply_1.20.2
#> [145] ggsignif_0.6.4 labeling_0.4.3
#> [147] plyr_1.8.9 stringi_1.8.7
#> [149] viridisLite_0.4.3 network_1.20.0
#> [151] deldir_2.0-4 gridBase_0.4-7
#> [153] lazyeval_0.2.3 spatstat.geom_3.8-1
#> [155] Matrix_1.7-4 RcppHNSW_0.7.0
#> [157] sparseMatrixStats_1.22.0 shiny_1.14.0
#> [159] SummarizedExperiment_1.40.0 ROCR_1.0-12
#> [161] broom_1.0.13 bslib_0.11.0