CanvasXpressBio connects the CanvasXpress interactive
visualization library to core Bioconductor data structures. It does not
replace the CRAN canvasXpress package — it adds methods
that take a Bioconductor object directly and render it as a reproducible
CanvasXpress widget, carrying the object’s annotations along for
interactive grouping, coloring and sorting.
This vignette builds one
SummarizedExperiment and shows it four ways — heatmap,
volcano plot, Kaplan-Meier curve and paired boxplot — so the same
samples and genes run through every figure.
Bioconductor already has excellent visualization packages. Static
heatmaps come from ComplexHeatmap and
pheatmap; EnhancedVolcano draws volcano plots;
survminer draws Kaplan-Meier curves; and
InteractiveComplexHeatmap adds interactivity to a
ComplexHeatmap — but through a Shiny application, which
needs a running R server to explore the figure.
CanvasXpressBio is complementary rather than a
replacement, and is useful when:
InteractiveComplexHeatmap is the better choice inside a
Shiny app; CanvasXpressBio is the better choice for a
standalone, shareable HTML artefact.SummarizedExperiment drives the heatmap, volcano,
survival curve and boxplot below through four small functions, so the
colData / rowData annotations are reused
consistently instead of being reshaped for a different package per
plot.colData becomes sample overlays and
rowData becomes feature overlays, so cohort, time point and
gene direction are available for interactive colouring and sorting
without extra bookkeeping.The rest of this vignette demonstrates these four views on a single object.
The study follows 24 patients in two cohorts, each measured at
baseline and at week 12, over a panel of twelve marker genes. The
colData records the cohort, the time point, the subject, a
follow-up time with an event indicator, and whether that subject’s
biomarker went up or down.
set.seed(1)
up_genes <- c("IL6", "CXCL8", "MMP9", "TNF", "CCL2", "VEGFA")
down_genes <- c("ALB", "CDH1", "FOXO1", "PPARG", "ADIPOQ", "LEP")
genes <- c(up_genes, down_genes)
patients <- c(sprintf("PA%02d", 1:12), sprintf("PB%02d", 1:12))
cohort <- rep(c("Cohort A", "Cohort B"), each = 12)
names(cohort) <- patients
# Sample table: each patient measured at Baseline and Week 12.
coldata <- data.frame(
Patient = rep(patients, each = 2),
Condition = rep(c("Baseline", "Week 12"), times = length(patients)),
stringsAsFactors = FALSE)
coldata$Cohort <- cohort[coldata$Patient]
rownames(coldata) <- paste(coldata$Patient,
ifelse(coldata$Condition == "Baseline", "BL", "W12"),
sep = "_")
# Expression: baseline around 8; at week 12 the up genes rise and the down
# genes fall. Cohort A responds consistently; cohort B is heterogeneous — some
# patients respond, a few reverse — so the biomarker moves both ways there.
baseline <- matrix(rnorm(length(genes) * length(patients), 8, 0.8),
nrow = length(genes), dimnames = list(genes, patients))
effect <- ifelse(genes %in% up_genes, 1.6, -1.6)
strength <- setNames(rep(1.0, length(patients)), patients)
strength[cohort == "Cohort B"] <-
runif(sum(cohort == "Cohort B"), -0.7, 0.8)
expr <- matrix(0, nrow = length(genes), ncol = nrow(coldata),
dimnames = list(genes, rownames(coldata)))
for (j in seq_len(nrow(coldata))) {
p <- coldata$Patient[j]
delta <- if (coldata$Condition[j] == "Week 12")
effect * strength[[p]] + rnorm(length(genes), 0, 0.4) else 0
expr[, j] <- baseline[, p] + delta
}
# Per-patient survival (cohort A does better) and the direction of the ALB
# change from baseline to week 12 (ALB is the biomarker shown in the boxplot).
alb_bl <- expr["ALB", coldata$Condition == "Baseline"]
alb_w12 <- expr["ALB", coldata$Condition == "Week 12"]
updown <- ifelse(alb_w12 >= alb_bl, "up", "down")
surv_time <- round(ifelse(cohort == "Cohort A",
rexp(length(patients), 1 / 28),
rexp(length(patients), 1 / 16)) + 2)
surv_status <- rbinom(length(patients), 1,
ifelse(cohort == "Cohort A", .5, .75))
coldata$Time <- surv_time[match(coldata$Patient, patients)]
coldata$Status <- surv_status[match(coldata$Patient, patients)]
coldata$updown <- updown[match(coldata$Patient, patients)]
se <- SummarizedExperiment(
assays = list(expr = expr),
colData = S4Vectors::DataFrame(coldata),
rowData = S4Vectors::DataFrame(
Direction = ifelse(genes %in% up_genes, "Up", "Down"),
row.names = genes))
se
#> class: SummarizedExperiment
#> dim: 12 48
#> metadata(0):
#> assays(1): expr
#> rownames(12): IL6 CXCL8 ... ADIPOQ LEP
#> rowData names(1): Direction
#> colnames(48): PA01_BL PA01_W12 ... PB12_BL PB12_W12
#> colData names(6): Patient Condition ... Status updowncxplot() turns the assay matrix into an interactive
heatmap. colData becomes sample (column) annotations and
rowData becomes feature (row) annotations, so the cohort,
time point and gene direction travel with the plot.
overlays and var_overlays draw chosen
annotations as coloured tracks beside the map, and
cluster = TRUE clusters the rows and columns with
dendrograms. These are the same twelve genes highlighted in the volcano
plot below.
cxplot(se, title = "Marker-gene expression",
overlays = c("Cohort", "Condition"), var_overlays = "Direction",
cluster = TRUE)Because SingleCellExperiment and
DESeqDataSet both extend SummarizedExperiment,
the same method renders them without any extra code.
cxvolcano() accepts any result table with a
log-fold-change and a p-value column — a DESeq2::results()
object, a limma::topTable() data frame, or a plain data
frame. Here week 12 is contrasted against baseline; the twelve marker
genes from the heatmap are the labelled hits.
# Observed effect for the twelve markers, placed among a genome-wide background.
marker_fc <- rowMeans(expr[, coldata$Condition == "Week 12"]) -
rowMeans(expr[, coldata$Condition == "Baseline"])
marker_p <- 10^(-abs(marker_fc) * runif(length(genes), 2.5, 3.5))
n_bg <- 1988
bg_fc <- rnorm(n_bg, 0, 0.45)
bg_p <- pmin(10^(-(abs(bg_fc) * runif(n_bg, 1.0, 3.0))), 1)
res <- data.frame(
log2FoldChange = c(marker_fc, bg_fc),
padj = c(marker_p, bg_p),
row.names = c(genes, paste0("bg", seq_len(n_bg))))
cxvolcano(res, title = "Week 12 vs baseline",
label_genes = genes)cxsurvival() reads the follow-up time and event status
straight from colData. Using the baseline rows gives one
record per patient, stratified by cohort.
cxboxplot() plots one marker across the two time points,
draws each patient’s points, and joins the repeated measures with a line
coloured by the direction of change — faceted by cohort.
# Paired t-test per cohort, shown as a label in each panel.
pA <- t.test(alb_w12[cohort == "Cohort A"], alb_bl[cohort == "Cohort A"],
paired = TRUE)$p.value
pB <- t.test(alb_w12[cohort == "Cohort B"], alb_bl[cohort == "Cohort B"],
paired = TRUE)$p.value
ptext <- function(p) if (p < 1e-4) "p<0.0001" else sprintf("p=%.3g", p)
ytop <- max(expr["ALB", ]) + 0.3
cxboxplot(se, feature = "ALB", group = "Condition", segregate = "Cohort",
connect = "Patient", color = "updown",
title = "ALB over time, per patient",
colorKey = list(updown = list(
up = "rgb(224,130,20)",
down = "rgb(58,102,160)")),
decorations = list(text = list(
list(value = ytop, label = ptext(pA), panel = 1,
smpCenter = TRUE, preTransformed = TRUE),
list(value = ytop, label = ptext(pB), panel = 2,
smpCenter = TRUE, preTransformed = TRUE))))sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats4 stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] SummarizedExperiment_1.43.0 Biobase_2.73.2
#> [3] GenomicRanges_1.65.4 Seqinfo_1.3.2
#> [5] IRanges_2.47.5 S4Vectors_0.51.10
#> [7] BiocGenerics_0.59.12 generics_0.1.4
#> [9] MatrixGenerics_1.25.0 matrixStats_1.5.0
#> [11] CanvasXpressBio_0.99.4 BiocStyle_2.41.0
#>
#> loaded via a namespace (and not attached):
#> [1] Matrix_1.7-6 jsonlite_2.0.0 compiler_4.6.1
#> [4] BiocManager_1.30.27 jquerylib_0.1.4 yaml_2.3.12
#> [7] fastmap_1.2.0 lattice_0.23-1 XVector_0.53.0
#> [10] R6_2.6.1 S4Arrays_1.13.2 knitr_1.52
#> [13] htmlwidgets_1.6.4 DelayedArray_0.39.8 maketools_1.3.2
#> [16] bslib_0.12.0 rlang_1.3.0 cachem_1.1.0
#> [19] xfun_0.61 canvasXpress_1.70.3 sass_0.4.10
#> [22] sys_3.4.3 otel_0.2.0 SparseArray_1.13.4
#> [25] cli_3.6.6 digest_0.6.39 grid_4.6.1
#> [28] lifecycle_1.0.5 evaluate_1.0.5 buildtools_1.0.0
#> [31] abind_1.4-8 rmarkdown_2.32 tools_4.6.1
#> [34] htmltools_0.5.9