Overview

This 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.

Required Packages

# 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 data

# 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)

Prepare input data for CellChat analysis

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)

Creating CellChat object

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

Creating CellChat database

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

Preprocessing of expression data for cell-cell communication

# 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

Inference of cell-cell comunication

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)

Extract inferred cellular communication to data frame

df.net <- subsetCommunication(cellchat,thresh = 0.05)

Converting CellChat predictions to spot-level communication data

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")

Session information

#> 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

References

Hao, Yuhan, Tim Stuart, Madeline H Kowalski, Saket Choudhary, Paul Hoffman, Austin Hartman, Avi Srivastava, et al. 2023. “Dictionary Learning for Integrative, Multimodal and Scalable Single-Cell Analysis.” Nature Biotechnology. https://doi.org/10.1038/s41587-023-01767-y.
Jin, Suoqin, Christian F. Guerrero-Juarez, Lihua Zhang, Ivan Chang, Raul Ramos, Chen-Hsiang Kuan, Peggy Myung, Maksim V. Plikus, and Qing Nie. 2021. “Inference and Analysis of Cell-Cell Communication Using CellChat.” Nature Communications 12 (1): 1088. https://doi.org/10.1038/s41467-021-21246-9.
Jin, Suoqin, Maksim V. Plikus, and Qing Nie. 2025. CellChat for Systematic Analysis of Cell–Cell Communication from Single-Cell Transcriptomics.” Nature Protocols 20 (1): 180–219. https://doi.org/10.1038/s41596-024-01045-4.