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.
Installation
R
install.packages("plotomics")
Load it with library(plotomics). Widgets also render outside Shiny, in the
RStudio Viewer, R Markdown, and Quarto. R carries 15 of the 17 components. The
genome-browser pair, igv.js and Gosling, is JavaScript and Python only, because their
bundles push the installed R package past CRAN's 5 MB limit.
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.
plotomics ships through CRAN, PyPI, and npm.
Motivation
I built plotomics because I kept hitting the same wall in my own work. A 500,000-cell embedding or a 20,000-gene volcano plot is common today. Most plotting tools were built for a few thousand marks. Past that count, a figure stops being readable before the data stops being interesting.
A second problem had nothing to do with size. My work runs on both R and Python, so the same figure gets built twice, once per language, and the two copies drift apart. I draw a heatmap in R and a different one in a Python notebook, and I cannot say which one is correct without reading both.
So I write each component once and wrap it thinly for both languages, so the notebook and the paper agree. plotomics is not a general plotting library by design, and for a small, static figure, better tools already exist. Read the full 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 components above, it is the better tool, and the motivation doc is explicit about the other cases where an established package should win.
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, how the shared core is built and wrapped
- Examples and gallery
- Contributing
- Changelog