plotomics
GPU-accelerated bioinformatics visualization for R, Python and the web.
plotomics is a set of visualization widgets built on one TypeScript rendering core, wrapped for R with htmlwidgets and for Python with anywidget. Numeric data is sent to the browser as a binary buffer instead of JSON, so plots stay interactive at hundreds of thousands to millions of points. The same figure works in a Jupyter notebook, a Shiny app, the RStudio Viewer, or a plain web page.
Why
Omics data outgrew the plotting stack. A 500k-cell embedding or a 20k-gene volcano is routine now, but the tools most of these figures are drawn with were designed for a few thousand marks, and past that the figure stops being readable before the data stops being interesting. Separately, labs run on both R and Python, so the same figure gets implemented twice and the two drift apart.
plotomics writes each component once and wraps it thinly for both languages, so the notebook and the paper agree. It is deliberately not a general plotting library, and for a small static figure there are better tools. Read the motivation for the goals, the non-goals, how it compares with ComplexHeatmap, maftools, survminer, Seurat and scanpy, and when to use one of those instead.
Components
Each component is a headless factory that runs the same way in a notebook, a Shiny app, or a web page. The R and Python wrappers stay thin because the rendering logic lives in the shared core.
| Component | Figure it produces | Rendering engine | Data scale |
|---|---|---|---|
| Volcano plot | Volcano: log2 fold change against −log10 p, with threshold guides and top-N gene labels. | regl-scatterplot (WebGL point sprites), d3-scale, d3-array | 200k points in the dev harness |
| Expression heatmap | Sample by gene expression matrix, optional row z-score, colorbar. | regl (WebGL; the matrix is uploaded as a float texture and sampled in a shader) | 1000 × 1000 = 1M cells |
| Clustered heatmap | Clustered heatmap with row and column dendrograms. The seaborn.clustermap / Morpheus layout. | Canvas 2-D ImageData, one pixel per cell then scaled; ml-hclust for the linkage; dendrograms in SVG | 120 × 60 in the dev harness |
| Marker gene dot plot | Marker gene dot plot. The equivalent of scanpy sc.pl.dotplot or Seurat DotPlot(). | Canvas 2-D, SVG overlay | Panel scale: features × groups |
| Stacked violin | Stacked violin panel. The equivalent of scanpy sc.pl.stacked_violin or Seurat VlnPlot(). | Canvas 2-D, SVG overlay. Densities are estimated upstream, in R or Python | Payload scales with the density grid, not the cell count |
| Embedding | UMAP, t-SNE and PCA score plots, coloured categorically or continuously, with lasso selection. | regl-scatterplot (WebGL point sprites, one draw call), d3-scale | 150k in the dev harness, 500k in the README example |
| Spatial tissue map | Visium-style spot map drawn over the H&E section, coloured by cluster or by a gene. | Canvas 2-D, one arc per spot; image and spots share a single computed fit | Visium scale, ~4k spots in the dev harness. Not the path for Xenium-scale single cells, see the note below |
| Oncoplot (OncoPrint) | OncoPrint / oncoplot: gene by sample alteration classes, burden and frequency margins, clinical strips. The maftools / cBioPortal figure. | Canvas 2-D for the whole grid, SVG for labels and legend | 60 genes × 1,200 samples = 72k cells |
| Protein domain lollipop | Protein domain lollipop, also called a mutation needle plot, over Pfam/InterPro domain rectangles. | Canvas 2-D, SVG overlay | Hundreds to a few thousand variants |
| Kaplan-Meier curve | Kaplan-Meier survival curves with censoring ticks, confidence bands and a number-at-risk table. | Canvas 2-D, SVG overlay. The fit comes from survival or lifelines | A handful of strata |
| Categorical profile | SBS96 mutational signature profile: 96 trinucleotide contexts under six substitution-class header blocks. | Canvas 2-D, SVG overlay | 96 contexts, generalises to a few thousand bins |
| UpSet plot | UpSet plot: intersection sizes as bars over a set-membership matrix, for where a Venn diagram is no longer drawable. | Canvas 2-D, SVG overlay. Exclusive intersections are computed in R | Dozens of sets |
| Gene treemap | Treemap of gene sets and pathway composition, coloured by parent or by value. | Canvas 2-D + d3-hierarchy (squarified treemap layout) | 60k leaves in the dev harness |
| Network graph | Force-directed biological networks: protein interaction, co-expression, regulatory graphs. | sigma v3 (WebGL) + graphology + graphology-layout-forceatlas2 | 5k nodes in the dev harness |
| Hi-C contact matrix | Hi-C and Micro-C contact maps, log or linear intensity, symmetric fill. | regl (WebGL) with its own level-of-detail pyramid. No tile server, and no HiGlass | 1024 × 1024 ≈ 1M bins |
| Genome viewer | Track-based genome browser: BAM alignments, BigWig coverage, VCF, BED, refGene. | igv.js, which streams and tiles internally | Whole genome, streamed from the server |
| Genomics figures | Declarative genomics figures from a Gosling spec: circos, ideograms, linked and stacked views. | gosling.js on PIXI.js (WebGL) | Whole genome, per the spec |
On “data scale”. These are the synthetic sizes the dev harness actually drives, not benchmark results. They say what each component is built and exercised for, not where it stops.
Worked example: a million cells
Take a 1M-cell Xenium run. The cell centroids go through the embedding
component, which is regl-scatterplot: points become WebGL sprites drawn from
GPU buffers rather than one node per point, and the coordinates arrive from Python as a
Float32Array buffer rather than JSON. The x and y for a million cells is two
million float32s, so 8 MB of binary the browser hands straight to the GPU, against
roughly 40 MB of JSON text it would otherwise parse first. Colour by cluster, lasso a region, and read the selected indices back in
Shiny.
What you do not get on that path is the H&E underlay. The spatial component is the one that puts measurements on the histology image, and it draws one canvas arc per spot, which is sized for Visium-scale slides of a few thousand spots. Registering a million single cells to the image is not something it does today. That is a real limitation and worth knowing before you plan a figure around it.
Could plotly not do this?
For the scatter, largely yes, and it is worth being straight about that.
plotly.js has a WebGL trace, scattergl, and it will draw a
million points. Anyone who tells you plotly cannot handle large scatter is describing
the default SVG scatter trace, which emits one DOM node per point and dies
in the low tens of thousands. The real differences are narrower and more boring:
- How the data gets there. plotly's R and Python bindings serialise coordinates as JSON. plotomics ships numeric columns as a binary buffer through anywidget and reads them as typed arrays, so nothing is parsed on arrival.
- The domain figure is already built. An oncoprint with burden and frequency margins, a Kaplan-Meier with an aligned number-at-risk table, an UpSet matrix, an SBS96 profile with its six header blocks: each is a substantial custom build in plotly, and one function call here.
- One implementation, two languages. plotly's R and Python APIs are separate surfaces that drift. Here both wrappers pack the same payload for the same renderer, so an R figure and a Python figure cannot disagree.
Against that, plotly is far more general, much more mature, and better documented. If you need a chart that is not one of the seventeen above, it is the better tool, and the motivation doc is explicit about the other cases where an established package should win.
Installation
R
install.packages("plotomics",
repos = "https://samuelbharti.r-universe.dev")
Load it with library(plotomics). Widgets also render outside Shiny, in the
RStudio Viewer, R Markdown, and Quarto.
Python
pip install plotomics
Works in Jupyter, JupyterLab, Colab, VS Code, marimo, Shiny for Python, and Streamlit.
JavaScript
pnpm add plotomics
Headless factories with tree-shakeable subpath imports, for example
plotomics/embedding.
The registries are not populated until the first tagged release. plotomics ships through r-universe, PyPI, and npm. Until then, install from source. See the README.
How it works
- Logic lives once. Every component is written one time, in TypeScript. The R and Python wrappers stay thin, so the two do not drift apart.
- Built for large data. Rendering runs on WebGL or canvas. Numeric columns travel as a binary buffer, so a million floats arrive as about 4 MB instead of about 20 MB of JSON.
- Made for figures. Axes, legends, and labels are drawn as vectors, and components export to SVG and PNG. Two are limited by their upstream engine: the Gosling viewer exports PNG only, and the igv.js viewer SVG only.
- Runs anywhere. Jupyter, Shiny, the RStudio Viewer, Quarto, or a static page. A lasso selection is sent back to your R or Python session.
Documentation
- R reference, generated by pkgdown
- Python reference, generated by pdoc
- Architecture
- Examples and gallery
- Contributing
- Changelog
The R and Python reference sites are generated by the Docs workflow (pkgdown and pdoc). Run that workflow from the Actions tab to publish them to GitHub Pages.