--- title: "CanvasXpress visualizations for Bioconductor data" author: "Isaac Neuhaus" package: CanvasXpressBio output: BiocStyle::html_document: toc: true vignette: > %\VignetteIndexEntry{CanvasXpress visualizations for Bioconductor data} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") ``` # Introduction `CanvasXpressBio` connects the [CanvasXpress](https://canvasxpress.org) 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. ```{r load, message = FALSE} 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. ```{r dataset} 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 ``` # 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. ```{r heatmap} 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. # 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. ```{r volcano} # 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) ``` # 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. ```{r km} baseline_se <- se[, se$Condition == "Baseline"] cxsurvival(baseline_se, time = "Time", status = "Status", group = "Cohort", 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. ```{r boxplot} # 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)))) ``` # Session info ```{r sessioninfo} sessionInfo() ```