CySA provides an interactive Shiny application for selecting and visualizing clusters from flow-cytometry data stored in SingleCellExperiment objects. It is designed to work with SOM-based clustering outputs such as those produced by FlowSOM and curated by the CATALYST workflow.
The main functions are:
prepClusterSelectorData() – subsample a SingleCellExperiment and build the inputs required by the app.clusterSelector() – return a Shiny app object that can be launched with shiny::runApp().plotSOMScatter() and plotCytoScatter() – static ggplot2 helpers for SOM and scatter visualizations.Install the package from Bioconductor with:
CySA expects a SingleCellExperiment object that contains at least the following items in metadata(sce):
SOM_codes – a matrix of SOM node codes (one row per SOM node, one column per marker).SOM_stats – a data frame of per-node statistics with an id column.map$colsUsed – an optional character vector of markers used for SOM mapping.The object should also contain a sample_id column and a cluster_id column in colData(sce).
This vignette uses a small example data set shipped with the package:
library(CySA)
sce <- CySA_example_sce()
head(S4Vectors::metadata(sce)$SOM_codes)
#> marker1 marker2 marker3 marker4 marker5 marker6 marker7
#> 1 0.001666667 0.08500000 0.1683333 0.2516667 0.3350000 0.4183333 0.5016667
#> 2 0.003333333 0.08666667 0.1700000 0.2533333 0.3366667 0.4200000 0.5033333
#> 3 0.005000000 0.08833333 0.1716667 0.2550000 0.3383333 0.4216667 0.5050000
#> 4 0.006666667 0.09000000 0.1733333 0.2566667 0.3400000 0.4233333 0.5066667
#> 5 0.008333333 0.09166667 0.1750000 0.2583333 0.3416667 0.4250000 0.5083333
#> 6 0.010000000 0.09333333 0.1766667 0.2600000 0.3433333 0.4266667 0.5100000
#> marker8 marker9 marker10 marker11 marker12
#> 1 0.5850000 0.6683333 0.7516667 0.8350000 0.9183333
#> 2 0.5866667 0.6700000 0.7533333 0.8366667 0.9200000
#> 3 0.5883333 0.6716667 0.7550000 0.8383333 0.9216667
#> 4 0.5900000 0.6733333 0.7566667 0.8400000 0.9233333
#> 5 0.5916667 0.6750000 0.7583333 0.8416667 0.9250000
#> 6 0.5933333 0.6766667 0.7600000 0.8433333 0.9266667Use prepClusterSelectorData() to subsample the data and generate a default list of marker pairs:
prepped <- prepClusterSelectorData(
sce,
total_cells_to_sample = 200,
somCodesName = "SOM_codes"
)
names(prepped)
#> [1] "sce" "sce_subsampled" "dList"clusterSelector() needs a few additional inputs. Here we build minimal versions from the example data:
som_codes <- S4Vectors::metadata(sce)$SOM_codes
markers <- S4Vectors::metadata(sce)$map$colsUsed
dend <- stats::as.dendrogram(stats::hclust(stats::dist(som_codes)))
dendTable <- data.frame(
id = seq_len(nrow(som_codes)),
label = rownames(som_codes),
stringsAsFactors = FALSE
)
clusterPatientTable <- table(
sample_id = sce$sample_id,
cluster_id = sce$cluster_id
)
somRasterData <- data.frame(
x = rep(seq_len(5), length.out = nrow(som_codes)),
y = rep(seq_len(2), each = ceiling(nrow(som_codes) / 2)),
id = seq_len(nrow(som_codes))
)
for (m in markers) {
somRasterData[[m]] <- seq_len(nrow(som_codes)) / nrow(som_codes)
}
arr <- array(
data = seq_len(10 * 10 * length(markers)),
dim = c(10, 10, length(markers))
)
somRasterObj <- raster::brick(arr)
names(somRasterObj) <- markersCreate the reusable Shiny app object:
app <- clusterSelector(
sce = prepped$sce,
sce_subsampled = prepped$sce_subsampled,
dList = prepped$dList,
dend = dend,
dendTable = dendTable,
clusterPatientTable = clusterPatientTable,
somRasterData = somRasterData,
somRasterObj = somRasterObj
)Launch the app interactively:
Once launched, the app lets you select cells or SOM clusters from a 2D scatter plot or an interactive dendrogram:
SOM nodes can also be explored directly, or via t-SNE/UMAP/PCA projections of the SOM code vectors, with the current selection highlighted consistently across all views:
The interactive session writes the selected cluster groupings back to the outputList object that was passed in.
CySA is designed to work with SOM-based clustering outputs, such as those produced by the FlowSOM package. This section walks through a complete preprocessing workflow: from raw FCS data to a SingleCellExperiment object ready for use with CySA.
FlowSOM handles different inputs, such as a flowFrame, a flowSet, or an array of file paths. For this example we use a flowFrame, which allows easier preprocessing. We start by compensating the data and then transforming it with the logicle function. For CyTOF data, an arcsinh transformation is preferred, which is also available in the flowCore package. Besides compensation and transformation, we also recommend cleaning the data by removing margin events and by using cleaning algorithms.
library(flowCore)
library(flowWorkspace)
library(FlowSOM)
# Load example FCS file (shipped with FlowSOM)
fileName <- system.file("extdata", "68983.fcs", package = "FlowSOM")
ff <- read.FCS(fileName)
# Compensation (for flow cytometry data)
comp <- keyword(ff)[["SPILL"]]
ff <- compensate(ff, comp)
# Transformation
# For flow cytometry: logicle transformation
transformList <- estimateLogicle(ff, channels = colnames(comp))
ff <- transform(ff, transformList)
# For CyTOF data, use arcsinh transformation instead:
# ff <- transform(ff, arcsinhTransform())The easiest way to use FlowSOM is via the wrapper function FlowSOM(). It has fewer options than using the separate functions, but is generally powerful enough for most use cases. It returns a list where the first item is the FlowSOM object (as required by many functions in this package) and the second item is the result of the metaclustering.
set.seed(42)
# Run FlowSOM on the preprocessed flowFrame
fSOM <- FlowSOM(ff,
# Input options:
compensate = FALSE, # already compensated above
transform = FALSE, # already transformed above
scale = FALSE,
# SOM options:
colsToUse = c(9, 12, 14:18), # select relevant channels
xdim = 7, ydim = 7, # 7x7 SOM grid = 49 nodes
# Metaclustering options:
nClus = 10 # number of metaclusters
)The resulting object provides cluster and metacluster labels for every individual cell:
To use FlowSOM results with CySA, you need to convert them into a SingleCellExperiment object with the required metadata structure.
Important: The following code includes several critical steps that are easy to get wrong. Common pitfalls and their solutions are noted in comments.
library(SingleCellExperiment)
library(S4Vectors)
# Extract SOM codes (one row per SOM node, one column per marker)
som_codes <- fSOM$map$codes
markers <- fSOM$map$colsUsed # Get the marker names used in the SOM
# Extract cell-level assignments
cell_clusters <- GetClusters(fSOM)
cell_metaclusters <- GetMetaclusters(fSOM)
# Build colData from the flowFrame
# CRITICAL: cluster_id must be a factor with levels for ALL SOM nodes (1 to nrow(som_codes)),
# even if some nodes are empty. Otherwise, prepClusterSelectorData() will fail when it
# tries to set levels based on nrow(SOM_codes), and plot interactions will break because
# node IDs won't map correctly.
coldata <- data.frame(
sample_id = rep("sample1", nrow(ff@exprs)),
cluster_id = factor(cell_clusters, levels = seq_len(nrow(som_codes))),
metacluster_id = factor(cell_metaclusters)
)
# Create the expression matrix with ONLY the markers used in the SOM
# CRITICAL: The rownames of the expression matrix MUST match the column names of som_codes.
# A common mistake is to use all flowFrame channels (colnames(ff)), which includes FSC, SSC,
# Time, and other parameters not used in the SOM. When CySA's plotSOMScatter() tries to
# index som_codes with marker names that don't exist, you get errors like:
# "subscript out of bounds" or "dim(X) must have a positive length"
# The fix: subset to only the markers that were actually used in the FlowSOM analysis.
exprs_mat <- t(ff@exprs[, markers, drop = FALSE])
rownames(exprs_mat) <- markers
# Compute SOM_stats (required by CySA)
# CRITICAL: SOM_stats must have one row per SOM node, with an 'id' column.
# A common mistake is to compute statistics per marker instead of per node.
# The 'id' values must be 1:nrow(som_codes) to match the SOM node indices.
som_stats <- data.frame(
id = seq_len(nrow(som_codes)),
n = tabulate(cell_clusters, nbins = nrow(som_codes)),
mean = som_codes[, 1], # Use first marker code as proxy for visualization
median = som_codes[, 1],
rdQu = som_codes[, 1],
max = som_codes[, 1],
stringsAsFactors = FALSE
)
rownames(som_stats) <- rownames(som_codes)
# Create the SingleCellExperiment object
# CRITICAL: experiment_info must contain at least one NUMERIC column (besides sample_id).
# The Stats panel tries to select numeric columns from experiment_info to offer as
# normalization options. If there are no numeric columns, apply() fails with:
# "dim(X) must have a positive length"
# The fix: include at least one numeric column like total_cells or sample_nr.
experiment_info <- data.frame(
sample_id = unique(coldata$sample_id),
total_cells = nrow(ff@exprs), # numeric column required by stats panel
stringsAsFactors = FALSE
)
sce <- SingleCellExperiment(
assays = list(exprs = exprs_mat),
colData = DataFrame(coldata),
metadata = list(
SOM_codes = som_codes,
SOM_stats = som_stats,
map = list(colsUsed = markers),
experiment_info = experiment_info
)
)
# Now pass to CySA
# CRITICAL: prepClusterSelectorData() requires at least 12 markers to build the default
# dList (marker pairs for 2D plots). FlowSOM often uses fewer markers (e.g., 7 in this
# example). If you get the error:
# "sce must have at least 12 row names to build default dList"
# The fix: provide a custom dList with explicit marker pairs.
dList <- list(
d1 = c(markers[1], markers[2]),
d2 = c(markers[3], markers[4]),
d3 = c(markers[5], markers[6]),
d4 = c(markers[1], markers[3]),
d5 = c(markers[2], markers[4]),
d6 = c(markers[5], markers[7])
)
prepped <- prepClusterSelectorData(
sce,
total_cells_to_sample = 500,
dList = dList
)To launch the CySA app with FlowSOM data, you need to build several additional inputs from the FlowSOM object. These are required arguments for clusterSelector().
Important: Each of these inputs serves a specific purpose in the CySA app. Common errors and their solutions are noted below.
# Extract SOM codes and build dendrogram
som_codes <- fSOM$map$codes
# CRITICAL: FlowSOM may not set rownames on the codes matrix.
# Without rownames, the dendTable will have mismatched row counts, causing:
# "arguments imply differing number of rows"
# The fix: assign node IDs as rownames if they're missing.
if (is.null(rownames(som_codes))) {
rownames(som_codes) <- seq_len(nrow(som_codes))
}
# Build hierarchical clustering dendrogram from SOM codes
# This is used for the dendrogram view in CySA
dend <- stats::as.dendrogram(stats::hclust(stats::dist(som_codes)))
# Build the dendrogram navigation table
# CRITICAL: The 'id' column must match the SOM node indices (1:nrow(som_codes))
# and 'label' must match the rownames of som_codes.
dendTable <- data.frame(
id = seq_len(nrow(som_codes)),
label = rownames(som_codes),
stringsAsFactors = FALSE
)
# Build cluster-by-sample table (SOM nodes x samples)
# This is used for abundance comparisons across samples
clusterPatientTable <- table(
sample_id = sce$sample_id,
cluster_id = sce$cluster_id
)
# Build SOM raster data for heatmap visualization
# CRITICAL: The SOM grid layout must match the FlowSOM dimensions.
# FlowSOM uses a rectangular grid (xdim × ydim). The raster data frame
# must have columns: x, y, id, and one column per marker with the SOM codes.
# A common mistake is to use the wrong grid dimensions, which causes:
# "subscript out of bounds" or misaligned heatmaps
xdim <- fSOM$map$xdim
ydim <- fSOM$map$ydim
# Create grid coordinates for each SOM node
# Nodes are arranged row-by-row in the FlowSOM grid
somRasterData <- data.frame(
x = rep(seq_len(xdim), length.out = nrow(som_codes)),
y = rep(seq_len(ydim), each = ceiling(nrow(som_codes) / ydim))[seq_len(nrow(som_codes))],
id = seq_len(nrow(som_codes))
)
# Add marker expression values for each SOM node
# These are used to color the SOM heatmap tiles
for (m in colnames(som_codes)) {
somRasterData[[m]] <- som_codes[, m]
}
# Create the CySA app
# CRITICAL: All arguments must be provided (no NULLs allowed for required inputs).
# The most common errors at this stage are:
# - Missing experiment_info numeric column → "dim(X) must have a positive length"
# - Mismatched marker names between exprs and som_codes → "subscript out of bounds"
# - cluster_id factor missing levels → "undefined factor levels"
app <- clusterSelector(
sce = prepped$sce,
sce_subsampled = prepped$sce_subsampled,
dList = prepped$dList,
dend = dend,
dendTable = dendTable,
clusterPatientTable = clusterPatientTable,
somRasterData = somRasterData,
somRasterObj = NULL # not needed when somRasterData is provided
)
# Launch interactively
if (interactive()) {
shiny::runApp(app)
}If you encounter errors when running the FlowSOM → CySA workflow, check:
| Error | Cause | Solution |
|---|---|---|
dim(X) must have a positive length |
experiment_info has no numeric columns |
Add a numeric column like total_cells |
sce must have at least 12 row names |
Fewer than 12 markers for default dList |
Provide custom dList with marker pairs |
subscript out of bounds |
Marker names in exprs_mat don’t match som_codes columns |
Subset exprs_mat to only SOM markers |
arguments imply differing number of rows |
dendTable rownames don’t match som_codes |
Ensure rownames(som_codes) is set |
undefined factor levels |
cluster_id factor missing node levels |
Set levels = seq_len(nrow(som_codes)) |
CySA can compare the relative abundance of selected SOM nodes between two sample groups. To use this feature, metadata(sce)$experiment_info must contain a grouping column (for example condition) and a sample_id column that matches colData(sce)$sample_id.
In the app, select:
experiment_info that defines the groups.experiment_info, or normalize by another cluster group.The Stats panel shows per-sample counts and percentages for the current selection, along with the t-test result:
For each selected SOM node, CySA performs a two-sample t-test on the relative cell counts between the two groups and displays the result in the Stats panel.
library(CySA)
library(SingleCellExperiment)
library(S4Vectors)
set.seed(42)
# ── dimensions ────────────────────────────────────────────────────────────────
n_markers <- 12
n_som_nodes <- 50
n_samples <- 6 # 3 control + 3 treated
n_cells <- 300 # per sample
marker_names <- paste0("marker", seq_len(n_markers))
sample_ids <- paste0("S", seq_len(n_samples))
conditions <- factor(
c(rep("control", 3), rep("treated", 3)),
levels = c("control", "treated")
)
# ── experiment_info ───────────────────────────────────────────────────────────
# total_cells: cells acquired by the cytometer (used as relativeTo denominator)
experiment_info <- data.frame(
sample_id = sample_ids,
condition = conditions,
total_cells = c(8000L, 9200L, 7800L, 10500L, 11000L, 9800L),
stringsAsFactors = FALSE
)
# ── SOM codes ─────────────────────────────────────────────────────────────────
# Nodes 1-20 : low expression → "resting" phenotype
# Nodes 21-30 : intermediate
# Nodes 31-50 : high expression → "activated" phenotype
som_codes <- matrix(0,
nrow = n_som_nodes,
ncol = n_markers,
dimnames = list(
paste0("node", seq_len(n_som_nodes)),
marker_names
)
)
for (node in seq_len(n_som_nodes)) {
base <- node / n_som_nodes # 0.02 … 1.00
som_codes[node, ] <- pmax(0, base + rnorm(n_markers, sd = 0.02))
}
# ── cell assignment ───────────────────────────────────────────────────────────
# Control: 70 % in nodes 1-20 (resting)
# Treated: 70 % in nodes 31-50 (activated)
assign_clusters <- function(condition, n) {
if (condition == "control") {
c(
sample(1:20, round(n * 0.70), replace = TRUE),
sample(seq_len(n_som_nodes), n - round(n * 0.70), replace = TRUE)
)
} else {
c(
sample(31:50, round(n * 0.70), replace = TRUE),
sample(seq_len(n_som_nodes), n - round(n * 0.70), replace = TRUE)
)
}
}
coldata_list <- mapply(function(sid, cond) {
data.frame(
sample_id = sid,
cluster_id = assign_clusters(as.character(cond), n_cells),
stringsAsFactors = FALSE
)
}, sample_ids, as.character(conditions), SIMPLIFY = FALSE)
coldata_df <- do.call(rbind, coldata_list)
n_total <- nrow(coldata_df) # 1800
# ── assay matrix — each cell ≈ its node's SOM code + noise ───────────────────
exprs_mat <- vapply(seq_len(n_total), function(i) {
node <- coldata_df$cluster_id[i]
pmax(0, som_codes[node, ] + rnorm(n_markers, sd = 0.05))
}, numeric(n_markers))
dimnames(exprs_mat) <- list(marker_names, paste0("cell", seq_len(n_total)))
# ── SOM_stats ─────────────────────────────────────────────────────────────────
node_counts <- tabulate(coldata_df$cluster_id, nbins = n_som_nodes)
# summarise marker1 expression per node for hover-text columns
m1 <- exprs_mat["marker1", ]
node_factor <- factor(coldata_df$cluster_id, levels = seq_len(n_som_nodes))
som_stats <- data.frame(
id = seq_len(n_som_nodes),
n = node_counts,
mean = as.numeric(tapply(m1, node_factor, mean)),
median = as.numeric(tapply(m1, node_factor, median)),
rdQu = as.numeric(tapply(m1, node_factor, quantile, probs = 0.75)),
max = as.numeric(tapply(m1, node_factor, max)),
stringsAsFactors = FALSE
)
# nodes with zero cells get NA from tapply; replace with 0
som_stats[is.na(som_stats)] <- 0
# ── assemble SCE ──────────────────────────────────────────────────────────────
sce_stats <- SingleCellExperiment(
assays = list(exprs = exprs_mat),
colData = DataFrame(
sample_id = coldata_df$sample_id,
cluster_id = factor(coldata_df$cluster_id, levels = seq_len(n_som_nodes))
),
metadata = list(
SOM_codes = som_codes,
SOM_stats = som_stats,
map = list(colsUsed = marker_names),
experiment_info = experiment_info
)
)
rownames(sce_stats) <- marker_names
# ── clusterPatientTable ───────────────────────────────────────────────────────
clusterPatientTable <- table(
sample_id = sce_stats$sample_id,
cluster_id = sce_stats$cluster_id
)
# ── app inputs ────────────────────────────────────────────────────────────────
prepped <- prepClusterSelectorData(
sce_stats,
total_cells_to_sample = 600,
somCodesName = "SOM_codes"
)
som_codes_mat <- metadata(sce_stats)$SOM_codes
dend <- stats::as.dendrogram(stats::hclust(stats::dist(som_codes_mat)))
dendTable <- data.frame(
id = seq_len(n_som_nodes),
label = rownames(som_codes_mat),
stringsAsFactors = FALSE
)
# SOM raster grid: 10 × 5 layout for 50 nodes
somRasterData <- data.frame(
x = rep(seq_len(10), times = 5),
y = rep(seq_len(5), each = 10),
id = seq_len(n_som_nodes)
)
for (m in marker_names) {
somRasterData[[m]] <- som_codes_mat[, m]
}
app <- clusterSelector(
sce = prepped$sce,
sce_subsampled = prepped$sce_subsampled,
dList = prepped$dList,
dend = dend,
dendTable = dendTable,
clusterPatientTable = clusterPatientTable,
somRasterData = somRasterData,
somRasterObj = NULL
)Launch the app interactively:
How to exercise each statistical feature in the running app Feature What to do Cell counts Select nodes 1–20 in a SOM plot; Stats tab shows counts per sample T-test Set groupsVar = condition, group1 = control, group2 = treated, relativeTo = none; select nodes 1–20; p-value should be large (control-enriched nodes) T-test (significant) Same setup but select nodes 31–50 (treated-enriched); expect small p-value Normalize by total cells Set relativeTo = total_cells; counts divided by cytometer total Normalize by another group Name nodes 31–50 “activated”, then set relativeTo = activated for a different selection UpSet / violin Name both “resting” (1–20) and “activated” (31–50); UpSet shows overlap, violin shows marker separation
For a closer look at the phenotype of a selection, the app also provides per-node marker pie charts, all marker-pair views, and SOM heatmaps:
For non-interactive use, plotSOMScatter() produces a ggplot2 scatter plot of two SOM channels:
plotCytoScatter() provides an alternative scatter visualization:
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] SingleCellExperiment_1.35.2 SummarizedExperiment_1.43.0
#> [3] Biobase_2.73.2 GenomicRanges_1.65.3
#> [5] Seqinfo_1.3.2 IRanges_2.47.5
#> [7] S4Vectors_0.51.9 BiocGenerics_0.59.12
#> [9] generics_0.1.4 MatrixGenerics_1.25.0
#> [11] matrixStats_1.5.0 CySA_0.99.28
#>
#> loaded via a namespace (and not attached):
#> [1] splines_4.6.1 later_1.4.8
#> [3] tibble_3.3.1 polyclip_1.10-7
#> [5] XML_3.99-0.24 lifecycle_1.0.5
#> [7] shinyjqui_0.4.1 rstatix_1.1.0
#> [9] doParallel_1.0.17 lattice_0.23-1
#> [11] MASS_7.3-66 crosstalk_1.2.2
#> [13] backports_1.5.1 magrittr_2.0.5
#> [15] plotly_4.12.1 sass_0.4.10
#> [17] rmarkdown_2.31 jquerylib_0.1.4
#> [19] yaml_2.3.12 plotrix_3.8-14
#> [21] httpuv_1.6.17 otel_0.2.0
#> [23] askpass_1.2.1 sp_2.2-3
#> [25] reticulate_1.46.0 cowplot_1.2.0
#> [27] RColorBrewer_1.1-3 ConsensusClusterPlus_1.77.0
#> [29] multcomp_1.4-32 abind_1.4-8
#> [31] Rtsne_0.17 purrr_1.2.2
#> [33] TH.data_1.1-5 tweenr_2.0.3
#> [35] sandwich_3.1-3 circlize_0.4.18
#> [37] data.tree_1.2.0 ggrepel_0.9.8
#> [39] irlba_2.3.7 CATALYST_1.37.0
#> [41] terra_1.9-46 umap_0.2.10.0
#> [43] RSpectra_0.16-2 codetools_0.2-20
#> [45] DelayedArray_0.39.6 DT_0.34.0
#> [47] scuttle_1.23.2 ggforce_0.5.0
#> [49] tidyselect_1.2.1 shape_1.4.6.1
#> [51] raster_3.6-32 farver_2.1.2
#> [53] ScaledMatrix_1.21.0 viridis_0.6.5
#> [55] jsonlite_2.0.0 GetoptLong_1.1.1
#> [57] BiocNeighbors_2.7.3 Formula_1.2-6
#> [59] ggridges_0.5.7 survival_3.8-11
#> [61] scater_1.41.2 iterators_1.0.14
#> [63] foreach_1.5.2 tools_4.6.1
#> [65] ggnewscale_0.5.2 Rcpp_1.1.2
#> [67] glue_1.8.1 gridExtra_2.3.1
#> [69] SparseArray_1.13.2 xfun_0.60
#> [71] dplyr_1.2.1 shinydashboard_0.7.3
#> [73] withr_3.0.3 fastmap_1.2.0
#> [75] shinyjs_2.1.1 openssl_2.4.2
#> [77] digest_0.6.39 rsvd_1.0.5
#> [79] R6_2.6.1 mime_0.13
#> [81] colorspace_2.1-3 gtools_3.9.5
#> [83] dichromat_2.0-1 tidyr_1.3.2
#> [85] data.table_1.18.6.1 httr_1.4.8
#> [87] htmlwidgets_1.6.4 S4Arrays_1.13.0
#> [89] pkgconfig_2.0.3 gtable_0.3.6
#> [91] ComplexHeatmap_2.29.0 RProtoBufLib_2.25.0
#> [93] S7_0.2.2 XVector_0.53.0
#> [95] htmltools_0.5.9 carData_3.0-6
#> [97] clue_0.3-68 scales_1.4.0
#> [99] png_0.1-9 colorRamps_2.3.4
#> [101] knitr_1.51 reshape2_1.4.5
#> [103] rjson_0.2.23 shinydashboardPlus_2.0.6
#> [105] cachem_1.1.0 zoo_1.9-0
#> [107] GlobalOptions_0.1.4 stringr_1.6.0
#> [109] KernSmooth_2.23-27 parallel_4.6.1
#> [111] vipor_0.4.7 pillar_1.11.1
#> [113] grid_4.6.1 vctrs_0.7.3
#> [115] promises_1.5.0 ggpubr_1.0.0
#> [117] car_3.1-5 BiocSingular_1.29.1
#> [119] cytolib_2.25.0 beachmat_2.29.2
#> [121] xtable_1.8-8 cluster_2.1.8.3
#> [123] beeswarm_0.4.0 evaluate_1.0.5
#> [125] mvtnorm_1.4-2 cli_3.6.6
#> [127] compiler_4.6.1 rlang_1.3.0
#> [129] crayon_1.5.3 ggsignif_0.6.4
#> [131] labeling_0.4.3 FlowSOM_2.21.0
#> [133] flowCore_2.25.1 plyr_1.8.9
#> [135] ggbeeswarm_0.7.3 stringi_1.8.9
#> [137] viridisLite_0.4.3 BiocParallel_1.47.0
#> [139] nnls_1.6 Matrix_1.7-6
#> [141] ggplot2_4.0.3 shiny_1.14.0
#> [143] drc_3.0-1 fontawesome_0.5.3
#> [145] igraph_2.3.3 broom_1.0.13
#> [147] memoise_2.0.1 bslib_0.12.0
#> [149] collapsibleTree_0.1.8