Part 1 of 2 — Global CellChat analysis of the B-ALL microenvironment.
CellChat v2 run separately on BM and CNS then compared. CellChat objects are cached — if pre-computed objects exist they are loaded directly.
This script covers:
See 12b for CNS-focused immunosuppressive deep dive (Galectin-9/TIM-3, TREM2/APP myeloid programming, niche retention circuits, cell-type analysis).
library(CellChat)
library(Seurat)
library(qs2)
library(dplyr)
library(patchwork)
library(ggplot2)
library(ComplexHeatmap)
library(knitr)
if (!requireNamespace("ggalluvial", quietly = TRUE)) {
cat("NOTE: ggalluvial not installed — river plots will be skipped\n")
}
base_path <- "/exports/eddie/scratch/aduguid3"
cache_dir <- file.path(base_path, "harmony_clustering")
tissue_cols <- c("BM" = "steelblue", "CNS" = "firebrick")
# ── Cache file paths ──
bm_cache <- file.path(cache_dir, "12_cellchat_bm_v1.qs")
cns_cache <- file.path(cache_dir, "12_cellchat_cns_v1.qs")
merged_cache <- file.path(cache_dir, "12_cellchat_merged_v1.qs")
combined_cache <- file.path(cache_dir, "12_combined_seurat.qs")
if (file.exists(combined_cache)) {
cat("Loading cached combined Seurat object...\n")
combined <- qs_read(combined_cache)
cat("Combined cells:", ncol(combined), "\n")
} else {
cat("Building combined object from scratch...\n")
immune_obj <- qs_read(file.path(cache_dir, "11_immune_annotated_v3.qs"))
cat("Immune cells:", ncol(immune_obj), "\n")
leuk_path <- file.path(cache_dir, "05_leuk_all_scored.qs")
leuk_obj <- qs_read(leuk_path)
cat("Leukaemia cells:", ncol(leuk_obj), "\n")
leuk_obj$cell_annotation <- "B-ALL"
shared_features <- intersect(rownames(immune_obj), rownames(leuk_obj))
cat("Shared features:", length(shared_features), "\n")
combined <- merge(
immune_obj[shared_features, ],
leuk_obj[shared_features, ],
add.cell.ids = c("immune", "leuk")
)
# JoinLayers only needed for Seurat v5 Assay5 objects
tryCatch(combined <- JoinLayers(combined),
error = function(e) cat("JoinLayers not needed (v3 Assay)\n"))
# ── Annotation mapping ──
annotation_map <- c(
"Vγ6Vδ4" = "Vg6Vd4", "Vγ4" = "Vg4", "Other γδ" = "Other_gd",
"CD8_Tex" = "CD8_Tex", "CD8_Tpex" = "CD8_Tpex",
"T_eff" = "CD8_Teff", "CD8_Teff (unclassified)" = "CD8_Teff",
"CD8_Naive" = "CD8_Naive",
"Effector CD4+ T cells" = "CD4_T",
"NKT-like" = "NKT-like", "CD8+ NKT-like cells" = "NKT-like",
"Natural killer cells" = "NK",
"Macrophages" = "Macrophages",
"Microglia-like macrophages" = "Microglia-like",
"Non-classical monocytes" = "Monocytes",
"Neutrophils" = "Neutrophils", "Myeloid Dendritic cells" = "DCs",
"Pre-B cells" = "Pre-B", "Naive B cells" = "Pre-B",
"Plasma B cells" = "Plasma_B",
"Stroma" = "Stroma", "Progenitor cells" = "Progenitors",
"Basophils" = "Basophils",
"B-ALL" = "B-ALL"
)
combined$cellchat_group <- unname(annotation_map[combined$cell_annotation])
combined$cellchat_group[is.na(combined$cellchat_group)] <- "Other"
qs_save(combined, combined_cache)
cat("Saved combined object to cache\n")
rm(immune_obj, leuk_obj); gc()
}
## Loading cached combined Seurat object...
## Combined cells: 109322
cat("\nCellChat group distribution:\n")
##
## CellChat group distribution:
print(sort(table(combined$cellchat_group), decreasing = TRUE))
##
## B-ALL Pre-B Macrophages CD8_Naive Microglia-like
## 91623 3195 2031 1768 1540
## Stroma Neutrophils Other Plasma_B CD4_T
## 1442 1145 979 844 814
## Progenitors NKT-like Monocytes Basophils Other_gd
## 714 676 606 555 505
## CD8_Tex CD8_Teff CD8_Tpex Vg6Vd4 DCs
## 284 146 143 123 117
## Vg4
## 72
cat("\nTissue split:\n")
##
## Tissue split:
print(table(combined$cellchat_group, combined$Tissue))
##
## BM CNS
## B-ALL 44606 47017
## Basophils 523 32
## CD4_T 631 183
## CD8_Naive 1639 129
## CD8_Teff 81 65
## CD8_Tex 183 101
## CD8_Tpex 113 30
## DCs 76 41
## Macrophages 1127 904
## Microglia-like 64 1476
## Monocytes 356 250
## Neutrophils 1103 42
## NKT-like 392 284
## Other 887 92
## Other_gd 253 252
## Plasma_B 754 90
## Pre-B 2987 208
## Progenitors 709 5
## Stroma 719 723
## Vg4 40 32
## Vg6Vd4 2 121
if (file.exists(bm_cache)) {
cat("Loading cached BM CellChat object...\n")
cellchat_bm <- qs_read(bm_cache)
cat("BM pathways:", length(cellchat_bm@netP$pathways), "\n")
} else {
cat("Building BM CellChat object (this takes ~30 min)...\n")
bm_obj <- subset(combined, subset = Tissue == "BM")
bm_obj <- NormalizeData(bm_obj, verbose = FALSE)
cellchat_bm <- createCellChat(
object = bm_obj, group.by = "cellchat_group",
assay = DefaultAssay(bm_obj)
)
cellchat_bm@DB <- CellChatDB.mouse
cellchat_bm <- subsetData(cellchat_bm)
cellchat_bm <- identifyOverExpressedGenes(cellchat_bm)
cellchat_bm <- identifyOverExpressedInteractions(cellchat_bm)
cellchat_bm <- computeCommunProb(cellchat_bm, type = "triMean")
cellchat_bm <- filterCommunication(cellchat_bm, min.cells = 10)
cellchat_bm <- computeCommunProbPathway(cellchat_bm)
cellchat_bm <- aggregateNet(cellchat_bm)
qs_save(cellchat_bm, bm_cache)
cat("Saved BM CellChat to cache\n")
rm(bm_obj); gc()
}
## Loading cached BM CellChat object...
## BM pathways: 86
if (file.exists(cns_cache)) {
cat("Loading cached CNS CellChat object...\n")
cellchat_cns <- qs_read(cns_cache)
cat("CNS pathways:", length(cellchat_cns@netP$pathways), "\n")
} else {
cat("Building CNS CellChat object (this takes ~30 min)...\n")
cns_obj <- subset(combined, subset = Tissue == "CNS")
cns_obj <- NormalizeData(cns_obj, verbose = FALSE)
cellchat_cns <- createCellChat(
object = cns_obj, group.by = "cellchat_group",
assay = DefaultAssay(cns_obj)
)
cellchat_cns@DB <- CellChatDB.mouse
cellchat_cns <- subsetData(cellchat_cns)
cellchat_cns <- identifyOverExpressedGenes(cellchat_cns)
cellchat_cns <- identifyOverExpressedInteractions(cellchat_cns)
cellchat_cns <- computeCommunProb(cellchat_cns, type = "triMean")
cellchat_cns <- filterCommunication(cellchat_cns, min.cells = 10)
cellchat_cns <- computeCommunProbPathway(cellchat_cns)
cellchat_cns <- aggregateNet(cellchat_cns)
qs_save(cellchat_cns, cns_cache)
cat("Saved CNS CellChat to cache\n")
rm(cns_obj); gc()
}
## Loading cached CNS CellChat object...
## CNS pathways: 122
if (file.exists(merged_cache)) {
cat("Loading cached merged CellChat object...\n")
cellchat_merged <- qs_read(merged_cache)
} else {
cat("Building merged CellChat object...\n")
# Compute centrality (needed for downstream)
cellchat_bm <- netAnalysis_computeCentrality(cellchat_bm)
cellchat_cns <- netAnalysis_computeCentrality(cellchat_cns)
# Lift to shared groups
all_groups <- union(
names(table(cellchat_bm@idents)),
names(table(cellchat_cns@idents))
)
cellchat_bm_l <- liftCellChat(cellchat_bm, all_groups)
cellchat_cns_l <- liftCellChat(cellchat_cns, all_groups)
cellchat_merged <- mergeCellChat(
list(BM = cellchat_bm_l, CNS = cellchat_cns_l),
add.names = c("BM", "CNS")
)
qs_save(cellchat_merged, merged_cache)
cat("Saved merged CellChat to cache\n")
}
## Loading cached merged CellChat object...
CellChat did not detect IL-17 signalling as significant (sparse Il17a transcript). Before moving to the broader landscape, we characterise the IL-17 receptor expression to clarify which cell types are wired to receive IL-17 family signals.
Critical finding from CellChatDB: The IL17RA/IL17RB heterodimer is the receptor for IL-25 (Il25), not IL-17A. IL-17A signals through IL17RA/IL17RC. This changes the mechanistic interpretation for leukaemia cells.
il17_genes <- c("Il17a", "Il17f", "Il17ra", "Il17rb", "Il17rc", "Il25",
"Il17rd", "Il17re", "Rorc", "Il23r", "Ccr6")
il17_present <- il17_genes[il17_genes %in% rownames(combined)]
# Detection rates per group per tissue
results <- list()
for (tissue in c("BM", "CNS")) {
meta <- combined@meta.data[combined$Tissue == tissue, ]
counts <- GetAssayData(combined, layer = "counts")[, rownames(meta)]
for (gene in il17_present) {
det <- tapply(counts[gene, ], meta$cellchat_group,
function(x) round(mean(x > 0) * 100, 1))
for (grp in names(det)) {
results[[length(results) + 1]] <- data.frame(
Gene = gene, Group = grp, Tissue = tissue,
Pct_Detected = det[grp], stringsAsFactors = FALSE
)
}
}
}
il17_det <- bind_rows(results) %>%
filter(Pct_Detected > 0)
# Heatmap of detection rates — CNS
il17_cns <- il17_det %>%
filter(Tissue == "CNS") %>%
tidyr::pivot_wider(id_cols = Group, names_from = Gene,
values_from = Pct_Detected, values_fill = 0)
il17_mat <- as.matrix(il17_cns[, -1])
rownames(il17_mat) <- il17_cns$Group
# Only show rows with any detection
il17_mat <- il17_mat[rowSums(il17_mat) > 0, , drop = FALSE]
ht <- Heatmap(il17_mat,
name = "% Detected",
column_title = "IL-17 family gene detection — CNS",
col = circlize::colorRamp2(c(0, 5, 20, 50),
c("white", "lightyellow", "orange", "red")),
cluster_rows = TRUE, cluster_columns = FALSE,
row_names_gp = gpar(fontsize = 10),
column_names_gp = gpar(fontsize = 10),
cell_fun = function(j, i, x, y, width, height, fill) {
val <- il17_mat[i, j]
if (val > 0) {
grid.text(sprintf("%.1f", val), x, y,
gp = gpar(fontsize = 7))
}
})
ComplexHeatmap::draw(ht)
cat("=== IL-17 receptor expression on B-ALL cells ===\n\n")
## === IL-17 receptor expression on B-ALL cells ===
for (tissue in c("BM", "CNS")) {
meta <- combined@meta.data[combined$Tissue == tissue &
combined$cellchat_group == "B-ALL", ]
if (nrow(meta) == 0) next
counts <- GetAssayData(combined, layer = "counts")[, rownames(meta)]
cat(tissue, "B-ALL (n =", nrow(meta), "):\n")
for (g in c("Il17ra", "Il17rb", "Il17rc", "Il25")) {
if (g %in% rownames(counts)) {
pct <- round(mean(counts[g, ] > 0) * 100, 1)
cat(" ", g, ":", pct, "%\n")
}
}
cat("\n")
}
## BM B-ALL (n = 44606 ):
## Il17ra : 47.8 %
## Il17rb : 47.3 %
## Il17rc : 0.1 %
## Il25 : 0 %
##
## CNS B-ALL (n = 47017 ):
## Il17ra : 44.8 %
## Il17rb : 47.3 %
## Il17rc : 0.1 %
## Il25 : 0 %
cat("Interpretation:\n")
## Interpretation:
cat("- IL17RA + IL17RC = canonical IL-17A/F receptor\n")
## - IL17RA + IL17RC = canonical IL-17A/F receptor
cat("- IL17RA + IL17RB = IL-25 receptor (NOT IL-17A)\n")
## - IL17RA + IL17RB = IL-25 receptor (NOT IL-17A)
cat("- Check which complex the leukaemia cells express to clarify signalling\n")
## - Check which complex the leukaemia cells express to clarify signalling
netVisual_circle(cellchat_bm@net$count,
weight.scale = TRUE, label.edge = FALSE,
title.name = "Number of interactions — BM")
netVisual_circle(cellchat_cns@net$count,
weight.scale = TRUE, label.edge = FALSE,
title.name = "Number of interactions — CNS")
netVisual_circle(cellchat_bm@net$weight,
weight.scale = TRUE, label.edge = FALSE,
title.name = "Interaction strength — BM")
netVisual_circle(cellchat_cns@net$weight,
weight.scale = TRUE, label.edge = FALSE,
title.name = "Interaction strength — CNS")
cellchat_cns <- netAnalysis_computeCentrality(cellchat_cns)
ht <- netAnalysis_signalingRole_heatmap(cellchat_cns, pattern = "outgoing",
title = "Outgoing signals — CNS", height = 14)
if (inherits(ht, c("HeatmapList", "Heatmap"))) ComplexHeatmap::draw(ht)
ht <- netAnalysis_signalingRole_heatmap(cellchat_cns, pattern = "incoming",
title = "Incoming signals — CNS", height = 14)
if (inherits(ht, c("HeatmapList", "Heatmap"))) ComplexHeatmap::draw(ht)
cellchat_bm <- netAnalysis_computeCentrality(cellchat_bm)
ht <- netAnalysis_signalingRole_heatmap(cellchat_bm, pattern = "outgoing",
title = "Outgoing signals — BM", height = 14)
if (inherits(ht, c("HeatmapList", "Heatmap"))) ComplexHeatmap::draw(ht)
ht <- netAnalysis_signalingRole_heatmap(cellchat_bm, pattern = "incoming",
title = "Incoming signals — BM", height = 14)
if (inherits(ht, c("HeatmapList", "Heatmap"))) ComplexHeatmap::draw(ht)
print(compareInteractions(cellchat_merged, show.legend = TRUE))
netVisual_diffInteraction(cellchat_merged, weight.scale = TRUE,
title.name = "Differential interaction count (CNS - BM)")
netVisual_diffInteraction(cellchat_merged, weight.scale = TRUE,
measure = "weight",
title.name = "Differential interaction strength (CNS - BM)")
print(rankNet(cellchat_merged, mode = "comparison",
stacked = TRUE, do.stat = TRUE) +
ggtitle("Information flow — BM vs CNS"))
print(rankNet(cellchat_merged, mode = "comparison",
stacked = FALSE, do.stat = TRUE) +
ggtitle("Information flow (unstacked) — BM vs CNS"))
tryCatch({
print(netAnalysis_signalingChanges_scatter(cellchat_merged,
idents.use = "B-ALL") +
ggtitle("B-ALL — signalling change (BM vs CNS)"))
}, error = function(e) cat("B-ALL scatter failed:", e$message, "\n"))
tryCatch({
print(netAnalysis_signalingChanges_scatter(cellchat_merged,
idents.use = "Macrophages") +
ggtitle("Macrophages — signalling change (BM vs CNS)"))
}, error = function(e) cat("Macrophages scatter failed:", e$message, "\n"))
tryCatch({
print(netAnalysis_signalingChanges_scatter(cellchat_merged,
idents.use = "Stroma") +
ggtitle("Stroma — signalling change (BM vs CNS)"))
}, error = function(e) cat("Stroma scatter failed:", e$message, "\n"))
tryCatch({
print(netAnalysis_signalingChanges_scatter(cellchat_merged,
idents.use = "Microglia-like") +
ggtitle("Microglia-like — signalling change (BM vs CNS)"))
}, error = function(e) cat("Microglia-like scatter failed:", e$message, "\n"))
cxcl_paths <- grep("CXCL", cellchat_cns@netP$pathways, value = TRUE, ignore.case = TRUE)
cat("CXCL pathways in CNS:", paste(cxcl_paths, collapse = ", "), "\n")
## CXCL pathways in CNS: CXCL
for (p in cxcl_paths) {
netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}
for (p in cxcl_paths) {
print(netAnalysis_contribution(cellchat_cns, signaling = p) +
ggtitle(paste0(p, " L-R contribution — CNS")))
}
cxcl_bm <- grep("CXCL", cellchat_bm@netP$pathways, value = TRUE, ignore.case = TRUE)
for (p in cxcl_bm) {
netVisual_aggregate(cellchat_bm, signaling = p, layout = "circle")
}
kit_paths <- grep("KIT|SCF", cellchat_cns@netP$pathways, value = TRUE, ignore.case = TRUE)
cat("KIT/SCF pathways in CNS:", paste(kit_paths, collapse = ", "), "\n")
## KIT/SCF pathways in CNS: KIT
for (p in kit_paths) {
netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}
egf_paths <- grep("EGF|AREG", cellchat_cns@netP$pathways, value = TRUE, ignore.case = TRUE)
if (length(egf_paths) > 0) {
cat("EGF/AREG pathways:", paste(egf_paths, collapse = ", "), "\n")
for (p in egf_paths) {
netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}
} else {
cat("No EGF/AREG pathways detected in CNS\n")
}
## EGF/AREG pathways: EGF
supp_paths <- grep("TGFb|TGFB|IL10", cellchat_cns@netP$pathways,
value = TRUE, ignore.case = TRUE)
cat("Immunosuppressive pathways in CNS:", paste(supp_paths, collapse = ", "), "\n")
## Immunosuppressive pathways in CNS: TGFb
for (p in supp_paths) {
netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}
ckpt_paths <- grep("PD-L|PDL|TIGIT|CTLA|GALECTIN|CD226|PVR|CD80|CD86",
cellchat_cns@netP$pathways, value = TRUE, ignore.case = TRUE)
cat("Checkpoint pathways in CNS:", paste(ckpt_paths, collapse = ", "), "\n")
## Checkpoint pathways in CNS: GALECTIN, CD86, PD-L1, CD80, PDL2
for (p in ckpt_paths) {
netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}
mhc_paths <- grep("MHC|MHCI|MHCII", cellchat_cns@netP$pathways,
value = TRUE, ignore.case = TRUE)
cat("MHC pathways in CNS:", paste(mhc_paths, collapse = ", "), "\n")
## MHC pathways in CNS: MHC-I, MHC-II
for (p in mhc_paths) {
netVisual_aggregate(cellchat_cns, signaling = p, layout = "circle")
}
sessionInfo()
## R version 4.4.1 (2024-06-14)
## Platform: x86_64-pc-linux-gnu
## Running under: Rocky Linux 9.5 (Blue Onyx)
##
## Matrix products: default
## BLAS/LAPACK: /opt/intel/oneapi/mkl/2024.0/lib/libmkl_gf_lp64.so.2; LAPACK version 3.10.1
##
## locale:
## [1] LC_CTYPE=en_GB.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_GB.UTF-8 LC_COLLATE=en_GB.UTF-8
## [5] LC_MONETARY=en_GB.UTF-8 LC_MESSAGES=en_GB.UTF-8
## [7] LC_PAPER=en_GB.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=en_GB.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Europe/London
## tzcode source: system (glibc)
##
## attached base packages:
## [1] grid stats graphics grDevices utils datasets methods
## [8] base
##
## other attached packages:
## [1] knitr_1.51 ComplexHeatmap_2.22.0 patchwork_1.3.2
## [4] qs2_0.1.7 Seurat_5.4.0 SeuratObject_5.3.0
## [7] sp_2.1-4 CellChat_2.2.0.9001 Biobase_2.66.0
## [10] BiocGenerics_0.52.0 ggplot2_4.0.2 igraph_2.2.2
## [13] dplyr_1.1.4
##
## loaded via a namespace (and not attached):
## [1] RcppAnnoy_0.0.23 splines_4.4.1 later_1.4.6
## [4] tibble_3.3.1 polyclip_1.10-7 ggnetwork_0.5.14
## [7] fastDummies_1.7.5 lifecycle_1.0.5 rstatix_0.7.3
## [10] doParallel_1.0.17 globals_0.16.3 lattice_0.22-6
## [13] MASS_7.3-60.2 backports_1.4.1 magrittr_2.0.3
## [16] plotly_4.12.0 sass_0.4.9 rmarkdown_2.30
## [19] jquerylib_0.1.4 yaml_2.3.12 httpuv_1.6.16
## [22] otel_0.2.0 collapse_2.1.6 NMF_0.28
## [25] sctransform_0.4.3 spam_2.11-3 spatstat.sparse_3.1-0
## [28] reticulate_1.45.0 cowplot_1.2.0 pbapply_1.7-4
## [31] RColorBrewer_1.1-3 abind_1.4-5 Rtsne_0.17
## [34] purrr_1.0.2 circlize_0.4.17 IRanges_2.40.1
## [37] S4Vectors_0.44.0 ggrepel_0.9.6 irlba_2.3.7
## [40] listenv_0.9.1 spatstat.utils_3.2-1 goftest_1.2-3
## [43] RSpectra_0.16-2 spatstat.random_3.4-4 fitdistrplus_1.2-6
## [46] parallelly_1.37.1 svglite_2.2.2 codetools_0.2-20
## [49] tidyselect_1.2.1 shape_1.4.6.1 farver_2.1.2
## [52] matrixStats_1.5.0 stats4_4.4.1 spatstat.explore_3.7-0
## [55] jsonlite_2.0.0 GetoptLong_1.1.0 BiocNeighbors_2.0.1
## [58] progressr_0.18.0 Formula_1.2-5 ggridges_0.5.6
## [61] ggalluvial_0.12.6 survival_3.6-4 iterators_1.0.14
## [64] systemfonts_1.3.1 foreach_1.5.2 tools_4.4.1
## [67] sna_2.8 ica_1.0-3 Rcpp_1.0.12
## [70] glue_1.8.0 gridExtra_2.3 xfun_0.56
## [73] withr_3.0.2 BiocManager_1.30.27 fastmap_1.2.0
## [76] fansi_1.0.6 digest_0.6.35 R6_2.6.1
## [79] mime_0.12 textshaping_0.3.7 colorspace_2.1-0
## [82] Cairo_1.7-0 scattermore_1.2 tensor_1.5.1
## [85] dichromat_2.0-0.1 spatstat.data_3.1-9 utf8_1.2.4
## [88] tidyr_1.3.1 generics_0.1.3 data.table_1.15.4
## [91] FNN_1.1.4.1 httr_1.4.7 htmlwidgets_1.6.4
## [94] uwot_0.2.4 pkgconfig_2.0.3 gtable_0.3.6
## [97] registry_0.5-1 lmtest_0.9-40 S7_0.2.1
## [100] htmltools_0.5.8.1 carData_3.0-6 dotCall64_1.2
## [103] clue_0.3-67 scales_1.4.0 png_0.1-8
## [106] spatstat.univar_3.1-6 rstudioapi_0.16.0 reshape2_1.4.4
## [109] rjson_0.2.23 nlme_3.1-164 coda_0.19-4.1
## [112] statnet.common_4.13.0 cachem_1.1.0 zoo_1.8-15
## [115] GlobalOptions_0.1.3 stringr_1.5.1 KernSmooth_2.23-24
## [118] parallel_4.4.1 miniUI_0.1.2 pillar_1.9.0
## [121] vctrs_0.6.5 RANN_2.6.2 promises_1.5.0
## [124] ggpubr_0.6.2 stringfish_0.18.0 car_3.1-5
## [127] xtable_1.8-4 cluster_2.1.6 evaluate_1.0.5
## [130] cli_3.6.5 compiler_4.4.1 rlang_1.1.7
## [133] crayon_1.5.2 rngtools_1.5.2 future.apply_1.11.2
## [136] ggsignif_0.6.4 labeling_0.4.3 plyr_1.8.9
## [139] stringi_1.8.4 deldir_2.0-4 viridisLite_0.4.2
## [142] network_1.20.0 gridBase_0.4-7 lazyeval_0.2.2
## [145] spatstat.geom_3.7-0 Matrix_1.7-0 RcppHNSW_0.6.0
## [148] future_1.33.2 shiny_1.12.1 ROCR_1.0-12
## [151] broom_1.0.5 RcppParallel_5.1.8 bslib_0.7.0