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.

ComponentFigure it producesRendering engineData scale
Volcano plotVolcano: log2 fold change against −log10 p, with threshold guides and top-N gene labels.regl-scatterplot (WebGL point sprites), d3-scale, d3-array200k points in the dev harness
Expression heatmapSample 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 heatmapClustered 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 SVG120 × 60 in the dev harness
Marker gene dot plotMarker gene dot plot. The equivalent of scanpy sc.pl.dotplot or Seurat DotPlot().Canvas 2-D, SVG overlayPanel scale: features × groups
Stacked violinStacked 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 PythonPayload scales with the density grid, not the cell count
EmbeddingUMAP, t-SNE and PCA score plots, coloured categorically or continuously, with lasso selection.regl-scatterplot (WebGL point sprites, one draw call), d3-scale150k in the dev harness, 500k in the README example
Spatial tissue mapVisium-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 fitVisium 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 legend60 genes × 1,200 samples = 72k cells
Protein domain lollipopProtein domain lollipop, also called a mutation needle plot, over Pfam/InterPro domain rectangles.Canvas 2-D, SVG overlayHundreds to a few thousand variants
Kaplan-Meier curveKaplan-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 lifelinesA handful of strata
Categorical profileSBS96 mutational signature profile: 96 trinucleotide contexts under six substitution-class header blocks.Canvas 2-D, SVG overlay96 contexts, generalises to a few thousand bins
UpSet plotUpSet 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 RDozens of sets
Gene treemapTreemap 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 graphForce-directed biological networks: protein interaction, co-expression, regulatory graphs.sigma v3 (WebGL) + graphology + graphology-layout-forceatlas25k nodes in the dev harness
Hi-C contact matrixHi-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 HiGlass1024 × 1024 ≈ 1M bins
Genome viewerTrack-based genome browser: BAM alignments, BigWig coverage, VCF, BED, refGene.igv.js, which streams and tiles internallyWhole genome, streamed from the server
Genomics figuresDeclarative 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:

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

Documentation

Package registries