CanvasXpress visualizations for Bioconductor data

Introduction

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.

library(CanvasXpressBio)
library(SummarizedExperiment)

Why CanvasXpressBio?

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:

  • You want interactivity without a server. Each call returns a self-contained CanvasXpress htmlwidget: hover for values, zoom, reorder and toggle groups all work in a plain knitted vignette, an R Markdown report, a Quarto page or an email — no Shiny runtime. InteractiveComplexHeatmap is the better choice inside a Shiny app; CanvasXpressBio is the better choice for a standalone, shareable HTML artefact.
  • You want one grammar across many figure types. The same 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.
  • You want the object’s metadata to travel automatically. 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.

One dataset for every figure

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; the rowData records whether each gene is an up- or down-regulated marker.

To keep the figures below uncluttered, the data construction lives in a single helper, makeDemoSE(), so the vignette builds the whole object with one call. The derived pieces the other figures need — a differential-expression table for the volcano and the per-cohort paired t-test p-values for the boxplot — travel with the object in its metadata(). (See ?makeDemoSE for exactly how the object is assembled.)

se <- makeDemoSE()
se
#> class: SummarizedExperiment 
#> dim: 12 48 
#> metadata(2): deResults albPaired
#> 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 updown

A heatmap from a SummarizedExperiment

cxplot() 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)

Enriching the hover metadata. The annotations are not only drawn as the coloured tracks beside the map — they also travel into every cell’s tooltip. Hovering a cell shows its expression value together with the gene and sample it belongs to and the colData / rowData fields listed in overlays / var_overlays (here the cohort, time point and gene direction). To surface more metadata on hover, a user simply adds more columns to the object’s colData or rowData and names them in overlays / var_overlays — no extra wiring.

Resetting the view. After zooming (mouse wheel) or a two-finger pinch, the toolbar’s reset button — and a double-click anywhere on the plot — restores the original view, the same role Plotly’s home control plays.

Because SingleCellExperiment and DESeqDataSet both extend SummarizedExperiment, the same method renders them without any extra code.

A volcano plot from differential-expression results

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. The result table (the twelve markers among a genome-wide background) was computed in makeDemoSE() and rides along in the object’s metadata().

res <- S4Vectors::metadata(se)$deResults
cxvolcano(res, title = "Week 12 vs baseline",
    label_genes = rownames(se))

A Kaplan-Meier survival curve

cxsurvival() reads the follow-up time and event status straight from colData. Using the baseline rows gives one record per patient, stratified by cohort.

The curve is interactive, and the interactivity carries the per-subject information: tooltip names extra colData columns to attach to each subject, so hovering a step or a censored observation shows that patient’s metadata. Censored observations are drawn as crosshair marks on the curve and, with kmCensoredRug = TRUE, as a rug along the time axis; the risk table (“Subjects at Risk”) and the log-rank p-value are shown as well.

baseline_se <- se[, se$Condition == "Baseline"]
cxsurvival(baseline_se, time = "Time", status = "Status", group = "Cohort",
    tooltip = c("Patient", "updown"),
    kmCensoredRug = TRUE, kmPvalue = TRUE,
    title = "Survival by cohort")

A paired boxplot with per-patient connections

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.

The per-cohort paired t-test p-values were computed in makeDemoSE() and read back from metadata() to label each panel.

paired <- S4Vectors::metadata(se)$albPaired
ptext  <- function(p) if (p < 1e-4) "p<0.0001" else sprintf("p=%.3g", p)
ytop   <- max(assay(se)["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(paired[["Cohort A"]]), panel = 1,
            smpCenter = TRUE, preTransformed = TRUE),
        list(value = ytop, label = ptext(paired[["Cohort B"]]), panel = 2,
            smpCenter = TRUE, preTransformed = TRUE))))

Beeswarm overlays at scale

The paired boxplot above draws every observation because the design is small. When a group holds many points, the points argument switches the overlay to a sina / beeswarm layout that packs the points inside the distribution envelope rather than overplotting. makeBeeswarmDemo() returns a single-feature object with 500 observations in each of five groups (2,500 points); points = "sina" keeps it legible.

bse <- makeBeeswarmDemo(n_per_group = 500)
cxboxplot(bse, feature = "Expression", group = "Group", color = "Group",
    points = "sina", showLegend = FALSE,
    title = "Beeswarm overlay with 2,500 points")

Session info

sessionInfo()
#> R version 4.6.2 (2026-10-29)
#> 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.13        generics_0.1.4             
#>  [9] MatrixGenerics_1.25.0       matrixStats_1.5.0          
#> [11] CanvasXpressBio_0.99.6      BiocStyle_2.41.0           
#> 
#> loaded via a namespace (and not attached):
#>  [1] Matrix_1.7-6        jsonlite_2.0.0      compiler_4.6.2     
#>  [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.62           canvasXpress_1.70.3 sass_0.4.10        
#> [22] sys_3.4.3           otel_0.3.0          SparseArray_1.13.4 
#> [25] cli_3.6.6           withr_3.0.3         digest_0.6.39      
#> [28] grid_4.6.2          lifecycle_1.0.5     evaluate_1.0.5     
#> [31] buildtools_1.0.0    abind_1.4-8         rmarkdown_2.32     
#> [34] tools_4.6.2         htmltools_0.5.9