PANORAMIC tutorial

Introduction

PANORAMIC is designed for multi-sample spatial colocalization analysis. It estimates sample-level spatial effects, then pools them with multilevel meta-analysis to test group-level differences.

The published PANORAMIC paper describes the default workflow as edge-corrected neighborhood enrichment combined with spatial bootstrap uncertainty estimation and multilevel random-effects meta-analysis (Chang et al., Bioinformatics 42(8): btag546, https://doi.org/10.1093/bioinformatics/btag546).

Compared with single-sample analyses, PANORAMIC gives you:

This vignette focuses on practical usage:

  1. Build a toy multi-sample dataset.
  2. Run PANORAMIC stepwise (prepare -> spatial stats -> pooling).
  3. Run the one-call workflow helper (panoramic_analyze()).
  4. Interpret outputs and generate publication-style plots.

Installation

if (!requireNamespace("BiocManager", quietly = TRUE)) {
  install.packages("BiocManager")
}
BiocManager::install("panoramic")

Load packages

library(panoramic)
library(SummarizedExperiment)
library(BiocParallel)
library(dplyr)
library(ggplot2)

Workflow At A Glance

Step Function Primary output Why it matters
1 panoramic_prepare() list of prepared SpatialExperiments with cached spatial objects standardizes per-sample inputs
2 panoramic_spatialstats() SummarizedExperiment with sample-level yi / vi quantifies within-sample spatial effects
3 panoramic_meta_mv() pooled multilevel results + differential contrasts tests group-level biology
Convenience panoramic_analyze() list with prep, stats, pooled, tables one-call reproducible pipeline

Simulate Example Data

We simulate two groups of samples with different spatial structure:

set.seed(123)
toy <- panoramic_simulate_dataset(
  n_group1 = 3,
  n_group2 = 3,
  n_cells_group1 = 200,
  n_cells_group2 = 350,
  group_labels = c("group1", "group2"),
  scenario_group1 = "random",
  scenario_group2 = "colocalized",
  seed = 123
)

spe_list <- toy$spe_list
design <- toy$design
design
#>     sample  group
#> 1 sample_1 group1
#> 2 sample_2 group1
#> 3 sample_3 group1
#> 4 sample_4 group2
#> 5 sample_5 group2
#> 6 sample_6 group2

Quick geometric sanity check:

plot_df <- do.call(rbind, lapply(spe_list, function(spe) {
  coords <- SpatialExperiment::spatialCoords(spe)
  data.frame(
    x = coords[, 1],
    y = coords[, 2],
    cell_type = SummarizedExperiment::colData(spe)$cell_type,
    sample_id = SummarizedExperiment::colData(spe)$sample_id,
    stringsAsFactors = FALSE
  )
}))

ggplot(plot_df, aes(x = x, y = y, color = cell_type)) +
  geom_point(size = 0.7, alpha = 0.7) +
  facet_wrap(~ sample_id, nrow = 2) +
  coord_equal() +
  theme_bw() +
  labs(x = "x", y = "y", color = "Cell type")

plot of chunk plot_samples

Step 1: Prepare Samples

prep <- panoramic_prepare(
  spe_list = spe_list,
  design = design,
  cell_type = "cell_type",
  min_cells = 5,
  window = "concave"
)
names(S4Vectors::metadata(prep[[1]])$panoramic)
#> [1] "sample_id"    "group_id"     "ppp"          "ppp_rescaled" "marks_tab"   
#> [6] "results"

Step 2: Compute Sample-Level Spatial Effects

The default PANORAMIC statistic is stat = "local_comp_enrichment", which reports edge-corrected neighborhood enrichment on a percentage-point scale relative to a sample-specific null expectation.

For tissue with strong large-scale density variation, an alternative is stat = "local_comp_global_enrichment". It compares local composition with the sample-wide target proportion, corresponding to a random-label null that conditions on the observed cell locations. This can be useful when the scientific question is label mixing within an observed tissue architecture; it should not be interpreted as correcting for spatially varying abundance or as evidence of a direct cell-cell interaction.

radii_um <- c(25)

se <- panoramic_spatialstats(
  prep = prep,
  radii_um = radii_um,
  stat = "local_comp_enrichment",
  nsim = 30,
  seed = 123,
  BPPARAM = BiocParallel::SerialParam()
)
dim(se)
#> [1] 9 6
head(as.data.frame(rowData(se))[, c("ct1", "ct2", "radius_um", "stat")])
#>        ct1 ct2 radius_um                  stat
#> A|A|25   A   A        25 local_comp_enrichment
#> B|A|25   B   A        25 local_comp_enrichment
#> C|A|25   C   A        25 local_comp_enrichment
#> A|B|25   A   B        25 local_comp_enrichment
#> B|B|25   B   B        25 local_comp_enrichment
#> C|B|25   C   B        25 local_comp_enrichment

Step 3: Pool Across Samples And Test Group Differences

panoramic_meta_mv() pools sample-level effects and computes differential contrasts between groups.

# Here patient_col and sample_col are both "sample" in toy data.
# In real data, patient_col can differ from sample_col (e.g., multiple cores per patient).

se_meta <- panoramic_meta_mv(
  se,
  patient_col = "sample",
  group_col = "group",
  sample_col = "sample",
  tau_structure = "patient",
  method = "REML",
  group_tau2 = "separate"
)

Interpret Pooled And Differential Outputs

Each row in rowData(se_meta) is one feature (ct1, ct2, radius_um) with pooled effects and contrasts.

For differential terms:

res <- panoramic_extract_contrast(se_meta)
head(res[, c("ct1", "ct2", "radius_um", "beta_diff", "p_diff", "fdr_diff")])
#>   ct1 ct2 radius_um beta_diff       p_diff     fdr_diff
#> 1   A   A        25 13.778909 6.386861e-08 1.437044e-07
#> 2   B   A        25 16.573803 1.110021e-79 9.990190e-79
#> 3   C   A        25  6.304412 9.937553e-04 1.277685e-03
#> 4   A   B        25 15.591724 1.828896e-56 8.230034e-56
#> 5   B   B        25 14.802806 2.928143e-11 8.784428e-11
#> 6   C   B        25  2.151487 1.724900e-01 1.724900e-01
res %>% dplyr::filter(fdr_diff < 0.05)
#>   ct1 ct2 radius_um beta_diff   se_diff    z_diff       p_diff     fdr_diff
#> 1   A   A        25 13.778909 2.5480554  5.407618 6.386861e-08 1.437044e-07
#> 2   B   A        25 16.573803 0.8768544 18.901430 1.110021e-79 9.990190e-79
#> 3   C   A        25  6.304412 1.9149024  3.292289 9.937553e-04 1.277685e-03
#> 4   A   B        25 15.591724 0.9847328 15.833457 1.828896e-56 8.230034e-56
#> 5   B   B        25 14.802806 2.2259389  6.650140 2.928143e-11 8.784428e-11
#> 6   A   C        25  3.881903 0.8770517  4.426082 9.596003e-06 1.727281e-05
#> 7   C   C        25  2.884636 0.7419748  3.887781 1.011648e-04 1.517473e-04
#>   coloc_source coloc_target coloc_direction
#> 1            A            A          A -> A
#> 2            A            B          A -> B
#> 3            A            C          A -> C
#> 4            B            A          B -> A
#> 5            B            B          B -> B
#> 6            C            A          C -> A
#> 7            C            C          C -> C

One-Call Workflow (panoramic_analyze)

Use panoramic_analyze() when you want a single reproducible entry point for end-to-end runs.

out <- panoramic_analyze(
  spe_list = spe_list,
  design = design,
  cell_type = "cell_type",
  radii_um = radii_um,
  nsim = 20,
  min_cells = 5,
  window = "concave",
  BPPARAM = BiocParallel::SerialParam()
)
names(out)
#> [1] "prep"   "stats"  "pooled" "tables"
names(out$tables)
#> [1] "spatialstats" "meta"         "contrast"

Visualization Tools

Forest Plot (Feature-Level)

plot_forest(
  se_meta,
  ct1 = "A",
  ct2 = "B",
  radius_um = 25,
  group_col = "group"
)

plot of chunk forest_plot

Volcano Plot (Global Differential Overview)

plot_volcano(se_meta)

plot of chunk volcano_plot

Network Summary

net <- create_spatial_network(
  se_meta,
  fdr_threshold = 0.2,
  directed = FALSE,
  leiden_resolution = 1.0
)
plot_spatial_network(
  se_meta,
  fdr_threshold = 0.2,
  directed = FALSE,
  layout = "fr",
  node_size_by = "degree"
)

plot of chunk network_plot

Practical Notes

Related Resources

For complementary spatial analysis workflows, see:

For methodological details and biological applications in colorectal cancer and head and neck squamous cell carcinoma tissue microarrays, see the published PANORAMIC paper:

sessionInfo()
#> R version 4.6.1 Patched (2026-06-24 r90190)
#> Platform: x86_64-apple-darwin20
#> Running under: macOS Ventura 13.7.8
#> 
#> Matrix products: default
#> BLAS:   /Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/lib/libRblas.0.dylib 
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
#> 
#> locale:
#> [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#> 
#> time zone: America/New_York
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats4    stats     graphics  grDevices utils     datasets  methods  
#> [8] base     
#> 
#> other attached packages:
#>  [1] ggplot2_4.0.3               dplyr_1.2.1                
#>  [3] BiocParallel_1.47.0         SummarizedExperiment_1.43.0
#>  [5] Biobase_2.73.2              GenomicRanges_1.65.4       
#>  [7] Seqinfo_1.3.2               IRanges_2.47.5             
#>  [9] S4Vectors_0.51.10           BiocGenerics_0.59.12       
#> [11] generics_0.1.4              MatrixGenerics_1.25.0      
#> [13] matrixStats_1.5.0           panoramic_0.99.4           
#> 
#> loaded via a namespace (and not attached):
#>  [1] tidyselect_1.2.1            viridisLite_0.4.3          
#>  [3] concaveman_1.2.0            farver_2.1.2               
#>  [5] viridis_0.6.5               S7_0.2.2                   
#>  [7] ggraph_2.2.2                fastmap_1.2.0              
#>  [9] SingleCellExperiment_1.35.2 spatstat.geom_3.8-3        
#> [11] tweenr_2.0.3                mathjaxr_2.0-0             
#> [13] digest_0.6.39               lifecycle_1.0.5            
#> [15] spatstat.data_3.1-9         magrittr_2.0.5             
#> [17] compiler_4.6.1              rlang_1.3.0                
#> [19] tools_4.6.1                 yaml_2.3.12                
#> [21] igraph_2.3.3                knitr_1.52                 
#> [23] S4Arrays_1.13.1             labeling_0.4.3             
#> [25] graphlayouts_1.2.5          curl_8.0.0                 
#> [27] DelayedArray_0.39.7         RColorBrewer_1.1-3         
#> [29] abind_1.4-8                 withr_3.0.3                
#> [31] purrr_1.2.2                 numDeriv_2016.8-1.1        
#> [33] grid_4.6.1                  polyclip_1.10-7            
#> [35] scales_1.4.0                MASS_7.3-66                
#> [37] spatstat.utils_3.2-5        dichromat_2.0-1            
#> [39] cli_3.6.6                   rmarkdown_2.32             
#> [41] metafor_5.2-1               otel_0.2.0                 
#> [43] rjson_0.2.23                cachem_1.1.0               
#> [45] ggforce_0.5.0               metadat_1.6-0              
#> [47] parallel_4.6.1              BiocManager_1.30.27        
#> [49] XVector_0.53.0              vctrs_0.7.3                
#> [51] V8_8.2.0                    Matrix_1.7-6               
#> [53] jsonlite_2.0.0              ggrepel_0.9.8              
#> [55] magick_2.9.1                spatstat.univar_3.2-0      
#> [57] tidyr_1.3.2                 glue_1.8.1                 
#> [59] codetools_0.2-20            gtable_0.3.6               
#> [61] deldir_2.0-4                tibble_3.3.1               
#> [63] pillar_1.11.1               htmltools_0.5.9            
#> [65] R6_2.6.1                    tidygraph_1.3.1            
#> [67] evaluate_1.0.5              lattice_0.23-1             
#> [69] SpatialExperiment_1.23.0    memoise_2.0.1              
#> [71] BiocStyle_2.41.0            Rcpp_1.1.2                 
#> [73] gridExtra_2.3.1             SparseArray_1.13.3         
#> [75] nlme_3.1-171                xfun_0.61                  
#> [77] pkgconfig_2.0.3