Skip to contents

Overview

In this tutorial, we will illustrate how to perform de novo discovery of spatial ecotypes from a single-cell spatial transcriptomics dataset using SpatialEcoTyper.

We will be analyzing single-cell spatial transcriptomics data from a melanoma sample (raw data available in Vizgen’s MERSCOPE FFPE Human Immuno-oncology). This demo data comprises the spatial expression of 500 genes across 27,907 cells, which are categorized into ten distinct cell types: B cells, CD4+ T cells, CD8+ T cells, NK cells, plasma cells, macrophages, dendritic cells (DC), fibroblasts, endothelial cells, and melanoma cells. For this tutorial, melanoma cells are excluded to reduce computational time; in practice, any cell types can be included.

All cells in this demo data are grouped into four spatial regions: tumor, inner margin, outer margin, and stroma. The tumor (tumor and inner margin) and stroma regions (stroma. and outer margin) are defined based on the density of cancer cells, as described in the CytoSPACE paper. The inner and outer margins are defined as regions extending 250 μm inside and outside the tumor boundaries, respectively. Furthermore, we quantified each cell’s distance to the tumor–stroma interface by calculating the shortest Euclidean distance to the nearest tumor region (for stromal cells) or stromal region (for tumor cells). A positive distance indicates cells located within the tumor region, while a negative distance indicates cells located within the stroma.

SpatialEcoTyper requires two input data objects:

  • Gene expression matrix: a numeric matrix with genes as rows and cells as columns.
  • Metadata: a data frame containing at least three columns: “X” (x-coordinate), “Y” (y-coordinate), and “CellType” (cell type annotation). Row names must match the column names (cell IDs) in the expression matrix.

First load required packages for this vignette

Quick start

## Load single-cell gene expression matrix (genes × cells)
url <- "https://spatialecotyper.stanford.edu/inc/inc.public.vignettes.php?file=Melanoma1_subset_counts.tsv.gz"
scdata <- fread(url, sep = "\t",header = TRUE, data.table = FALSE)
rownames(scdata) <- scdata[, 1]
scdata <- as.matrix(scdata[, -1])

## Normalize the gene expression data
normdata <- NormalizeData(scdata)

## Load single-cell metadata
## Required columns: "X", "Y", and "CellType"
## Row names must match the cell IDs in the expression matrix
url <- "https://spatialecotyper.stanford.edu/inc/inc.public.vignettes.php?file=Melanoma1_subset_scmeta.tsv"
scmeta <- read.table(url, sep = "\t", header = TRUE, row.names = 1)
scmeta <- scmeta[match(colnames(scdata), rownames(scmeta)), ]
head(scmeta[, c("X", "Y", "CellType")])

## Discover spatial ecotypes using the SpatialEcoTyper
se_results <- SpatialEcoTyper(normdata, scmeta, 
                              outprefix = "Melanoma1_subset",
                              radius = 50, ncores = 2)

Loading data

Text files as input

Large text files can be loaded into R using the fread function from the data.table package.

## Load single-cell gene expression matrix (genes × cells)
url <- "https://spatialecotyper.stanford.edu/inc/inc.public.vignettes.php?file=Melanoma1_subset_counts.tsv.gz"
scdata <- fread(url, sep = "\t",header = TRUE, data.table = FALSE)
rownames(scdata) <- scdata[, 1]
scdata <- as.matrix(scdata[, -1])
head(scdata[, 1:5])
##          HumanMelanomaPatient1__cell_3655 HumanMelanomaPatient1__cell_3657
## PDK4                                    0                                1
## TNFRSF17                                0                                0
## ICAM3                                   0                                0
## FAP                                     1                                0
## GZMB                                    0                                0
## TSC2                                    0                                0
##          HumanMelanomaPatient1__cell_3658 HumanMelanomaPatient1__cell_3660
## PDK4                                    1                                0
## TNFRSF17                                0                                0
## ICAM3                                   0                                0
## FAP                                     0                                0
## GZMB                                    0                                0
## TSC2                                    0                                0
##          HumanMelanomaPatient1__cell_3661
## PDK4                                    0
## TNFRSF17                                0
## ICAM3                                   0
## FAP                                     0
## GZMB                                    0
## TSC2                                    0
## Load single-cell metadata
## Required columns: "X", "Y", and "CellType"
## Row names must match the cell IDs in the expression matrix
url <- "https://spatialecotyper.stanford.edu/inc/inc.public.vignettes.php?file=Melanoma1_subset_scmeta.tsv"
scmeta <- read.table(url, sep = "\t", header = TRUE, row.names = 1)
scmeta <- scmeta[match(colnames(scdata), rownames(scmeta)), ]
head(scmeta[, c("X", "Y", "CellType")])
##                                         X         Y   CellType
## HumanMelanomaPatient1__cell_3655 1894.706 -6367.766 Fibroblast
## HumanMelanomaPatient1__cell_3657 1942.480 -6369.602 Fibroblast
## HumanMelanomaPatient1__cell_3658 1963.007 -6374.026 Fibroblast
## HumanMelanomaPatient1__cell_3660 1981.600 -6372.266 Fibroblast
## HumanMelanomaPatient1__cell_3661 1742.939 -6374.851 Fibroblast
## HumanMelanomaPatient1__cell_3663 1921.683 -6383.309 Fibroblast
Sparse matrix as input

SpatialEcoTyper supports sparse matrix as input. Mtx files can be loaded into R using the ReadMtx function from the Seurat package.

scdata <- ReadMtx(mtx = "https://spatialecotyper.stanford.edu/inc/inc.public.vignettes.php?file=Melanoma1_subset_counts.mtx.gz", 
                  cells = "https://spatialecotyper.stanford.edu/inc/inc.public.vignettes.php?file=Melanoma1_subset_cells.tsv.gz", 
                  features = "https://spatialecotyper.stanford.edu/inc/inc.public.vignettes.php?file=Melanoma1_subset_genes.tsv.gz",
                  feature.column = 1, cell.column = 1)

Data normalization

Gene expression data should be normalized prior to SpatialEcoTyper analysis. The data can be performed using either NormalizeData or SCTransform.

Here, we are normalizing using NormalizeData.

normdata <- NormalizeData(scdata)
Using SCTransform for the normalization:

For SCTransform normalization, we recommend installing the glmGamPoi package to accelerate computation.

if(!"glmGamPoi" %in% rownames(installed.packages())){
  BiocManager::install("glmGamPoi")
}
tmpobj <- CreateSeuratObject(scdata) %>%
      SCTransform(clip.range = c(-10, 10), verbose = FALSE)

seurat_version = as.integer(gsub("\\..*", "", as.character(packageVersion("SeuratObject"))))
if(seurat_version<5){
  normdata <- GetAssayData(tmpobj, "data")
}else{
  normdata <- tmpobj[["SCT"]]$data
}

Preview of the sample

The SpatialView function can be used to visualize single cells within the tissue. You can color the cells by cell type or predefined spatial regions.

# Visualize the cell type annotations in the tissue
SpatialView(scmeta, by = "CellType") + scale_color_manual(values = pals::cols25())

# Visualize the regions in the tissue
SpatialView(scmeta, by = "Region") + scale_color_brewer(type = "qual", palette = "Set1")

The SpatialView function can also be used to visualize continuous characteristics, such as the minimum distance of each single cell to tumor/stroma margin. Here, positive distances indicate cells located within the tumor region, while negative distances denote cells within the stroma.

# Visualize the distance to tumor margin
SpatialView(scmeta, by = "Dist2Interface") + 
  scale_colour_gradient2(low = "#5e3c99", high = "#e66101", 
                         mid = "#d9d9d9", midpoint = 0) + 
  labs(color = "Distance to\ntumor margin")

SE discovery using SpatialEcoTyper

The SpatialEcoTyper function is designed to identify spatial ecotypes (SEs) from single-cell spatial transcriptomics data. The workflow begins by defining spatial neighborhoods (SNs) on a regular grid and constructing cell type–specific gene expression profiles (GEPs) for each SN. For each cell type, a similarity network is constructed, where nodes represent SNs and edges reflect transcriptional similarity. These cell type–specific networks are then integrated using Similarity Network Fusion (SNF), originally developed for multi-omics data integration (Wang et al., 2014). This yields a fused similarity network that captures shared spatial transcriptomic variation across cell types, enabling the identification of SEs through clustering. Before start, please check key arguments here.

Selection of Spatial EcoTyper parameters

Choosing the radius parameter: The optimal spatial neighborhood radius depends on the biological question. Larger radii capture broader regional patterns, whereas smaller radii resolve more localized cellular niches. In our analysis, we used a radius of 50 µm, which typically includes ~10–80 neighboring cells and provides a practical balance between granularity and robustness.

Choosing the number of variable features: The nfeatures argument should be selected according to the underlying data platform. For Vizgen MERSCOPE V1 (500-gene panel), we used 200 highly variable features, whereas for Xenium Prime data, we used 300 variable features. As single-cell spatial transcriptomics technologies continue to evolve, the number of detectable genes per cell has steadily increased. For example, the latest 10x Aera platform captures over 1,000 genes per cell on average. For such high-complexity datasets, we recommend using up to 1,000 highly variable genes for downstream analysis to better capture biological variability.

Limiting the number of cell types per spatial neighborhood: The min.cts.per.region argument allows users to filter spatial regions based on cellular diversity by requiring a minimum number of distinct cell types per region (default: ≥ 2). This helps ensure that downstream analyses focus on biologically informative neighborhoods with sufficient cellular heterogeneity.

Limiting analysis to spatial neighborhoods containing specific cell types: The filter.region.by.celltypes argument allows users to define a set of cell types of interest. When specified, only spatial regions that contain all of the selected cell types are retained for downstream analysis. This enables focused interrogation of neighborhoods enriched for predefined cellular compositions, facilitating the identification of multicellular ecosystems associated with the target cell types.
se_results <- SpatialEcoTyper(normdata, scmeta,
                              outprefix = "Melanoma1_subset",
                              radius = 50, ncores = 2)
Optimizing memory usage

The similarity network fusion (Step 2 of SpatialEcoTyper) is the most computationally demanding step and its runtime increases with the number of spatial neighborhoods (SNs) per sample. Performance depends on dataset size and structure rather than tissue technology.

For example, an analysis involving 10 cell types and 18,564 SNs required 436 minutes of runtime and reached a peak memory usage of 169 GB. This benchmark was obtained on a computing cluster equipped with an AMD EPYC 7543 processor (2.75 GHz) and 256 GB RAM, using a single CPU core and excluding data loading time.

To reduce runtime, users can increase the number of cores via the ncores parameter. However, this parallelization comes at the cost of increased memory consumption.

When memory resources are limited, users can increase the grid.size parameter, which will reduce the number of spatial neighborhoods, thereby substantially lowering memory usage and improving computational efficiency.

SpatialEcoTyper result

When the outprefix is specified, the SpatialEcoTyper result will be saved as a RDS file named outprefix_SpatialEcoTyper_results.rds. The result can be loaded into R using readRDS.

se_results <- readRDS("Melanoma1_subset_SpatialEcoTyper_results.rds")

The SpatialEcoTyper result is a list containing two key components:

  • Seurat object constructed from the fused network embedding of spatial neighborhoods, enabling clustering and downstream visualization of SEs.
  • Metadata, single-cell metadata with SE annotations appended.
# Extract the Seurat object and updated single-cell metadata
obj <- se_results$obj # A Seurat object
scmeta <- se_results$metadata # Single-cell meta data, with SE annotation added
head(scmeta)
##                                         X         Y   CellType CellTypeName
## HumanMelanomaPatient1__cell_3655 1894.706 -6367.766 Fibroblast  Fibroblasts
## HumanMelanomaPatient1__cell_3657 1942.480 -6369.602 Fibroblast  Fibroblasts
## HumanMelanomaPatient1__cell_3658 1963.007 -6374.026 Fibroblast  Fibroblasts
## HumanMelanomaPatient1__cell_3660 1981.600 -6372.266 Fibroblast  Fibroblasts
## HumanMelanomaPatient1__cell_3661 1742.939 -6374.851 Fibroblast  Fibroblasts
## HumanMelanomaPatient1__cell_3663 1921.683 -6383.309 Fibroblast  Fibroblasts
##                                  Region Dist2Interface  SE
## HumanMelanomaPatient1__cell_3655 Stroma      -883.1752 SE3
## HumanMelanomaPatient1__cell_3657 Stroma      -894.8463 SE6
## HumanMelanomaPatient1__cell_3658 Stroma      -904.1115 SE6
## HumanMelanomaPatient1__cell_3660 Stroma      -907.8909 SE6
## HumanMelanomaPatient1__cell_3661 Stroma      -874.2712 SE1
## HumanMelanomaPatient1__cell_3663 Stroma      -903.6559 SE3
table(scmeta$SE) ## The number of cells in each SE
## 
##  SE0  SE1  SE2  SE3  SE4  SE5  SE6  SE7 
## 4067 2480 6907 4297 3952 1232 1801 2244

Note: SpatialEcoTyper applies stringent quality control to exclude low-quality spatial neighborhoods, including those with insufficient detected genes (min.features), too few cells (min.cells), or insufficient cell type diversity (min.cts.per.region). These regions are labeled as NA. Regions excluded at one resolution (radius, grid.size) may be retained at a lower resolution if they pass QC. In practice, different resolutions have minimal impact on overall results. By default, NA regions are removed. To retain all cells, set dropcell = FALSE.

Embedding of spatial architecture

The embedding of spatial neighborhoods can be visualized using standard Seurat functions such as DimPlot and FeaturePlot. These visualizations help to explore the spatial organization and heterogeneity within the tissue.

Note: The embedding and clustering results differ slightly between Seurat v4 and v5. However, the overall clustering patterns remain largely consistent, with an Adjusted Rand Index (ARI) of 0.7 for the demonstration dataset. This consistency ensures that, despite minor variations, the key biological insights are preserved across versions.

Visualizing tumor/stroma regions in the embedding

DimPlot(obj, group.by = "Region") + scale_color_brewer(type = "qual", palette = "Set1")

Visualizing the distance of SNs to tumor/stroma interface

This plot shows the distance of each SN to the tumor/stroma interface. Here, positive distances indicate SNs located within the tumor region, while negative distances denote SNs within the stroma.

FeaturePlot(obj, "Dist2Interface", min.cutoff = -600, max.cutoff = 600) + 
  scale_colour_gradient2(low = "#5e3c99", high = "#e66101", mid = "#d9d9d9", midpoint = 0)

Visualizing spatial ecotypes in the embedding

This plot visualizes the SEs within the spatial embedding. Each SE represents a distinct spatial ecosystem with unique molecular and spatial characteristics, and may also differ in cell type composition.

DimPlot(obj, group.by = "SE") + scale_color_manual(values = pals::kelly()[-1])

SE characteristics

Visualizing SEs in the tissue

The spatial distribution of SEs within the tissue can be visualized using the SpatialView function.

SpatialView(scmeta, by = "SE")

Visualizing the cell type composition of SEs

The bar plot below illustrates the cell type composition within each SE.

gg <- scmeta %>% filter(!is.na(SE)) %>% count(SE, CellType)
ggplot(gg, aes(SE, n, fill = CellType)) + 
  geom_bar(stat = "identity", position = "fill") +
  scale_fill_manual(values = pals::cols25()) +
  theme_bw(base_size = 14) + coord_flip() + 
  labs(y = "Cell type abundance")

Visualizing the association between SEs and pre-annotated regions

This bar plot shows the enrichment of SEs in pre-defined regions (e.g., tumor and stroma).

gg <- scmeta %>% filter(!is.na(SE)) %>% count(SE, Region)
ggplot(gg, aes(SE, n, fill = Region)) + 
  geom_bar(stat = "identity", position = "fill") + 
  scale_fill_brewer(type = "qual", palette = "Set1") +
  theme_bw(base_size = 14) + coord_flip() + 
  labs(y = "Fraction")

Visualizing the distance of SEs to tumor/stroma interface

This box plot visualizes the distribution of distances of SEs to the tumor/stroma interface. Positive distances indicate cells located within the tumor region, while negative distances denote cells within the stroma. The SEs are ordered by their median distance, highlighting their spatial localization relative to the tumor/stroma interface.

gg <- scmeta %>% filter(!is.na(SE))
## Order SEs by their distance to tumor/stroma interface
tmp <-  gg %>% group_by(SE) %>% summarise(Mid = median(Dist2Interface)) %>% arrange(Mid) %>% pull(SE)
gg$SE = factor(gg$SE, levels = tmp)
ggplot(gg, aes(SE, Dist2Interface)) + 
  geom_boxplot() + theme_bw() + labs(y = "Distance to tumor/stroma interface (μm)")

Identification of cell-type-specific SE markers

Differential expression analysis

To identify cell-type-specific SE markers, differential expression analysis can be performed using the presto package package. The script below demonstrates how to identify SE-specific markers within each cell type.

if(!"presto" %in% installed.packages()){ 
  BiocManager::install("devtools")
  devtools::install_github("immunogenomics/presto")
}
library("presto")

# Ensure normalized data is aligned with metadata
normdata = normdata[, rownames(scmeta)]

# Perform differential expression analysis
degs = lapply(unique(scmeta$CellType), function(ct){
  idx = which(scmeta$CellType==ct & !is.na(scmeta$SE))
  degs = wilcoxauc(normdata[, idx], scmeta$SE[idx])
  degs$CellType = ct
  degs
})
degs = do.call(rbind, degs)

# Example: top markers for CD4 T cells
degs %>% filter(CellType=="CD4T") %>% 
  filter(logFC>0 & padj<0.05) %>% 
  arrange(desc(logFC)) %>% head()
##   feature group  avgExpr     logFC statistic       auc         pval
## 1     FN1   SE0 1.806530 0.6629562 1183830.0 0.6591026 2.224246e-31
## 2     FOS   SE7 1.925442 0.6490756 1075864.0 0.7009114 5.421664e-41
## 3   DUSP1   SE1 1.395032 0.6396178   72771.5 0.7583841 9.937244e-06
## 4  CTNNB1   SE5 1.354324 0.6391400  214553.5 0.7757488 1.038918e-15
## 5     PKM   SE5 2.373492 0.5948805  214342.5 0.7749859 8.231702e-15
## 6     FOS   SE3 1.803486 0.4922663  608225.5 0.6483962 9.791820e-15
##           padj    pct_in  pct_out CellType
## 1 4.581948e-29  83.26446 63.97198     CD4T
## 2 2.233725e-38  96.04938 79.36675     CD4T
## 3 4.094145e-03  86.95652 64.76510     CD4T
## 4 4.280343e-13  94.02985 68.19283     CD4T
## 5 1.695731e-12 100.00000 97.31105     CD4T
## 6 8.068460e-13  93.67089 80.21728     CD4T

Visualizing the expression of cell state markers

Once candidate markers are identified, their expression can be visualized across SEs.

Visualizing SE-specific markers using heatmap

The example below identifies SE-specific markers in CD4 T cells and visualizes their relative expression across SEs using a heatmap.

library(tidyr)
# Identify significant DEGs
sigDegs = degs %>% filter(CellType=="CD4T") %>% filter(logFC>0 & padj<0.05)
# Identify genes with highest specificity per SE
top_markers <- sigDegs %>% group_by(feature) %>% arrange(desc(logFC)) %>%
  summarize(Highest_SE = first(group), Highest_logFC = first(logFC), 
            Second_logFC = nth(logFC, 2, default = 0),
            Delta = Highest_logFC - Second_logFC) %>% ungroup() %>%
  # Keep top 5 markers per SE based on Delta
  group_by(Highest_SE) %>% arrange(desc(Delta)) %>%
  slice_head(n = 5) %>% ungroup()

# Prepare logFC matrix for heatmap
lfcs = degs %>% filter(CellType=="CD4T") %>% 
        pivot_wider(id_cols = feature, names_from = group,
                    values_from = logFC) %>% as.data.frame
rownames(lfcs) = lfcs$feature
lfcs = lfcs[, -1]
gg <- lfcs[top_markers$feature, unique(top_markers$Highest_SE), drop = FALSE]

# Scale for visualization
gg = scale(t(scale(t(gg), center = FALSE)))
# Plot heatmap
HeatmapView(gg, breaks = c(0, 1, 1.5))

Visualizing an individual marker using box plot

You can also visualize the expression of an individual gene across SEs.

# Subset CD4 T cells with SE annotation
idx = which(scmeta$CellType=="CD4T" & !is.na(scmeta$SE))
gg = scmeta[idx, ]
gg$Expression = normdata["FOXP3", idx]
ggplot(gg, aes(SE, Expression)) + geom_boxplot(outlier.shape = NA) + 
  theme_classic(base_size = 14) + ylab("FOXP3 expression")

Visualizing expression of SE marker in the tissue

You can also visualize the expression of an individual marker (e.g., FOXP3) or average expression of SE-specific markers in the tissue.

# Visualize gene expression in tissue
gg <- scmeta
gg$Expression = normdata["FOXP3", ]

SpatialView(gg, by = "Expression") +
  scale_color_viridis_c() +
  labs(color = "FOXP3\nexpression")

Session info

The session info allows users to replicate the exact environment and identify potential discrepancies in package versions or configurations that might be causing problems.

## R version 4.4.1 (2024-06-14)
## Platform: aarch64-apple-darwin20
## Running under: macOS 26.4.1
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## time zone: America/Los_Angeles
## tzcode source: internal
## 
## attached base packages:
## [1] parallel  stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
##  [1] tidyr_1.3.1           presto_1.0.0          Rcpp_1.0.13          
##  [4] SpatialEcoTyper_1.0.2 NMF_0.28              Biobase_2.64.0       
##  [7] BiocGenerics_0.50.0   cluster_2.1.6         rngtools_1.5.2       
## [10] registry_0.5-1        RANN_2.6.2            Matrix_1.7-0         
## [13] R.utils_2.12.3        R.oo_1.26.0           R.methodsS3_1.8.2    
## [16] data.table_1.16.0     Seurat_5.1.0          SeuratObject_5.0.2   
## [19] sp_2.1-4              ggplot2_3.5.1         dplyr_1.1.4          
## 
## loaded via a namespace (and not attached):
##   [1] RcppAnnoy_0.0.22            splines_4.4.1              
##   [3] later_1.3.2                 tibble_3.2.1               
##   [5] polyclip_1.10-7             fastDummies_1.7.4          
##   [7] lifecycle_1.0.4             sf_1.1-0                   
##   [9] doParallel_1.0.17           pals_1.9                   
##  [11] globals_0.16.3              lattice_0.22-6             
##  [13] MASS_7.3-60.2               magrittr_2.0.3             
##  [15] plotly_4.10.4               sass_0.4.9                 
##  [17] rmarkdown_2.28              jquerylib_0.1.4            
##  [19] yaml_2.3.10                 httpuv_1.6.15              
##  [21] glmGamPoi_1.16.0            sctransform_0.4.1          
##  [23] spam_2.10-0                 spatstat.sparse_3.1-0      
##  [25] reticulate_1.39.0           mapproj_1.2.11             
##  [27] cowplot_1.1.3               pbapply_1.7-2              
##  [29] DBI_1.3.0                   RColorBrewer_1.1-3         
##  [31] maps_3.4.2                  zlibbioc_1.50.0            
##  [33] abind_1.4-5                 GenomicRanges_1.56.1       
##  [35] Rtsne_0.17                  purrr_1.0.2                
##  [37] GenomeInfoDbData_1.2.12     circlize_0.4.16            
##  [39] IRanges_2.38.1              S4Vectors_0.42.1           
##  [41] ggrepel_0.9.6               irlba_2.3.5.1              
##  [43] listenv_0.9.1               spatstat.utils_3.1-0       
##  [45] units_1.0-1                 goftest_1.2-3              
##  [47] RSpectra_0.16-2             spatstat.random_3.3-1      
##  [49] fitdistrplus_1.2-1          parallelly_1.38.0          
##  [51] DelayedMatrixStats_1.26.0   pkgdown_2.1.0              
##  [53] DelayedArray_0.30.1         leiden_0.4.3.1             
##  [55] codetools_0.2-20            tidyselect_1.2.1           
##  [57] shape_1.4.6.1               farver_2.1.2               
##  [59] UCSC.utils_1.0.0            matrixStats_1.4.1          
##  [61] stats4_4.4.1                spatstat.explore_3.3-2     
##  [63] jsonlite_1.8.8              GetoptLong_1.0.5           
##  [65] e1071_1.7-16                progressr_0.14.0           
##  [67] ggridges_0.5.6              survival_3.6-4             
##  [69] iterators_1.0.14            systemfonts_1.1.0          
##  [71] foreach_1.5.2               tools_4.4.1                
##  [73] ragg_1.3.2                  ica_1.0-3                  
##  [75] glue_1.7.0                  SparseArray_1.4.8          
##  [77] gridExtra_2.3               xfun_0.52                  
##  [79] MatrixGenerics_1.16.0       GenomeInfoDb_1.40.1        
##  [81] withr_3.0.1                 BiocManager_1.30.25        
##  [83] fastmap_1.2.0               boot_1.3-30                
##  [85] fansi_1.0.6                 spData_2.3.4               
##  [87] digest_0.6.37               R6_2.5.1                   
##  [89] mime_0.12                   wk_0.9.5                   
##  [91] textshaping_0.4.0           colorspace_2.1-1           
##  [93] scattermore_1.2             tensor_1.5                 
##  [95] dichromat_2.0-0.1           spatstat.data_3.1-2        
##  [97] utf8_1.2.4                  generics_0.1.3             
##  [99] class_7.3-22                S4Arrays_1.4.1             
## [101] httr_1.4.7                  htmlwidgets_1.6.4          
## [103] spdep_1.4-2                 uwot_0.2.2                 
## [105] pkgconfig_2.0.3             gtable_0.3.5               
## [107] ComplexHeatmap_2.20.0       lmtest_0.9-40              
## [109] XVector_0.44.0              htmltools_0.5.8.1          
## [111] dotCall64_1.1-1             clue_0.3-65                
## [113] scales_1.3.0                png_0.1-8                  
## [115] spatstat.univar_3.0-1       knitr_1.48                 
## [117] rstudioapi_0.16.0           reshape2_1.4.4             
## [119] rjson_0.2.22                nlme_3.1-164               
## [121] proxy_0.4-27                cachem_1.1.0               
## [123] zoo_1.8-12                  GlobalOptions_0.1.2        
## [125] stringr_1.5.1               KernSmooth_2.23-24         
## [127] miniUI_0.1.1.1              s2_1.1.9                   
## [129] desc_1.4.3                  pillar_1.9.0               
## [131] grid_4.4.1                  vctrs_0.6.5                
## [133] promises_1.3.0              xtable_1.8-4               
## [135] evaluate_0.24.0             magick_2.8.5               
## [137] cli_3.6.3                   compiler_4.4.1             
## [139] rlang_1.1.4                 crayon_1.5.3               
## [141] future.apply_1.11.2         labeling_0.4.3             
## [143] classInt_0.4-11             plyr_1.8.9                 
## [145] fs_1.6.4                    stringi_1.8.4              
## [147] viridisLite_0.4.2           deldir_2.0-4               
## [149] gridBase_0.4-7              munsell_0.5.1              
## [151] lazyeval_0.2.2              spatstat.geom_3.3-2        
## [153] RcppHNSW_0.6.0              patchwork_1.2.0            
## [155] sparseMatrixStats_1.16.0    future_1.34.0              
## [157] shiny_1.9.1                 highr_0.11                 
## [159] SummarizedExperiment_1.34.0 ROCR_1.0-11                
## [161] igraph_2.0.3                bslib_0.8.0