Interactive WebGL Scatterplots with reglScatterplot

Introduction

reglScatterplot is a thin R wrapper around the JavaScript regl-scatterplot library, exposing it as an htmlwidgets widget. It renders millions of two-dimensional points in the browser via WebGL, which makes it well suited to:

  • single-cell RNA-seq UMAP/t-SNE embeddings,
  • spatial transcriptomics tissue maps,
  • high-dimensional flow / mass cytometry projections,
  • any large (x, y) scatter that ggplot2 would draw too slowly.

The widget works in:

  • the RStudio Viewer pane when run interactively,
  • standalone HTML files produced by htmlwidgets::saveWidget(),
  • Shiny applications and interactive R Markdown documents,
  • Jupyter notebooks running the IRkernel (e.g. VSCode’s Jupyter extension).

Installation

# (chunk intentionally not run during knit)
# From GitHub
remotes::install_github("george123ya/reglScatterplotR")

# Once accepted into Bioconductor
if (!requireNamespace("BiocManager", quietly = TRUE)) {
    install.packages("BiocManager")
}
BiocManager::install("reglScatterplotR")
library(reglScatterplotR)

A first plot

The simplest call needs only a data.frame and the names of the two coordinate columns.

set.seed(1L)
df <- data.frame(
    x = rnorm(20000),
    y = rnorm(20000),
    cluster = sample(letters[1:6], 20000, replace = TRUE),
    score = runif(20000)
)

reglScatterplot(df, x = "x", y = "y")

Categorical colour mapping

Pass a character or factor column name (or vector) to colorBy. A draggable legend appears in the upper-right corner of the plot; clicking a legend entry filters the data, and shift/ctrl-click extends the selection.

reglScatterplot(df,
    x = "x", y = "y", colorBy = "cluster",
    legendTitle = "Cluster"
)

Continuous colour mapping

Numeric colorBy produces a continuous colour scale. Use vmin/vmax to clip outliers; the strings "p1", "p99" and so on select percentiles.

reglScatterplot(df,
    x = "x", y = "y", colorBy = "score",
    continuousPalette = "magma",
    vmin = "p1", vmax = "p99"
)

For log-fold-change-style data use centerZero = TRUE to force a symmetric diverging scale.

Working with Bioconductor data structures

reglScatterplot() works directly on a SingleCellExperiment. Pass the name of a reducedDim() as x. colorBy and groupBy can name a colData() column or a gene in rownames(). Gene values are read from the logcounts assay by default; use assay to pick another one.

Here we wrap the bundled example data in a small SingleCellExperiment.

library(SingleCellExperiment)
#> Loading required package: SummarizedExperiment
#> Loading required package: MatrixGenerics
#> Loading required package: matrixStats
#> 
#> Attaching package: 'MatrixGenerics'
#> The following objects are masked from 'package:matrixStats':
#> 
#>     colAlls, colAnyNAs, colAnys, colAvgsPerRowSet, colCollapse,
#>     colCounts, colCummaxs, colCummins, colCumprods, colCumsums,
#>     colDiffs, colIQRDiffs, colIQRs, colLogSumExps, colMadDiffs,
#>     colMads, colMaxs, colMeans2, colMedians, colMins, colOrderStats,
#>     colProds, colQuantiles, colRanges, colRanks, colSdDiffs, colSds,
#>     colSums2, colTabulates, colVarDiffs, colVars, colWeightedMads,
#>     colWeightedMeans, colWeightedMedians, colWeightedSds,
#>     colWeightedVars, rowAlls, rowAnyNAs, rowAnys, rowAvgsPerColSet,
#>     rowCollapse, rowCounts, rowCummaxs, rowCummins, rowCumprods,
#>     rowCumsums, rowDiffs, rowIQRDiffs, rowIQRs, rowLogSumExps,
#>     rowMadDiffs, rowMads, rowMaxs, rowMeans2, rowMedians, rowMins,
#>     rowOrderStats, rowProds, rowQuantiles, rowRanges, rowRanks,
#>     rowSdDiffs, rowSds, rowSums2, rowTabulates, rowVarDiffs, rowVars,
#>     rowWeightedMads, rowWeightedMeans, rowWeightedMedians,
#>     rowWeightedSds, rowWeightedVars
#> Loading required package: GenomicRanges
#> Loading required package: stats4
#> Loading required package: BiocGenerics
#> Loading required package: generics
#> 
#> Attaching package: 'generics'
#> The following objects are masked from 'package:base':
#> 
#>     as.difftime, as.factor, as.ordered, intersect, is.element, setdiff,
#>     setequal, union
#> 
#> Attaching package: 'BiocGenerics'
#> The following objects are masked from 'package:stats':
#> 
#>     IQR, mad, sd, var, xtabs
#> The following object is masked from 'package:utils':
#> 
#>     data
#> The following objects are masked from 'package:base':
#> 
#>     anyDuplicated, aperm, append, as.data.frame, basename, cbind,
#>     colnames, dirname, do.call, duplicated, eval, evalq, Filter, Find,
#>     get, grep, grepl, is.unsorted, lapply, Map, mapply, match, mget,
#>     order, paste, pmax, pmax.int, pmin, pmin.int, Position, rank,
#>     rbind, Reduce, rownames, sapply, saveRDS, scale, sequence, table,
#>     tapply, transform, unique, unsplit, which.max, which.min
#> Loading required package: S4Vectors
#> 
#> Attaching package: 'S4Vectors'
#> The following object is masked from 'package:utils':
#> 
#>     findMatches
#> The following objects are masked from 'package:base':
#> 
#>     expand.grid, I, unname
#> Loading required package: IRanges
#> Loading required package: Seqinfo
#> Loading required package: Biobase
#> Welcome to Bioconductor
#> 
#>     Vignettes contain introductory material; view with
#>     'browseVignettes()'. To cite Bioconductor, see
#>     'citation("Biobase")', and for packages 'citation("pkgname")'.
#> 
#> Attaching package: 'Biobase'
#> The following object is masked from 'package:MatrixGenerics':
#> 
#>     rowMedians
#> The following objects are masked from 'package:matrixStats':
#> 
#>     anyMissing, rowMedians
data(reglScatterExample)
ex <- reglScatterExample
sce <- SingleCellExperiment(
    assays = list(logcounts = t(as.matrix(ex["CD3D"]))),
    colData = ex[, c("celltype", "nCount", "percentMito")],
    reducedDims = list(UMAP = as.matrix(ex[, c("UMAP_1", "UMAP_2")]))
)
sce
#> class: SingleCellExperiment 
#> dim: 1 3400 
#> metadata(0):
#> assays(1): logcounts
#> rownames(1): CD3D
#> rowData names(0):
#> colnames: NULL
#> colData names(3): celltype nCount percentMito
#> reducedDimNames(1): UMAP
#> mainExpName: NULL
#> altExpNames(0):
reglScatterplot(sce, x = "UMAP", colorBy = "celltype")

Colouring by a gene reads its row from the assay:

reglScatterplot(sce, x = "UMAP", colorBy = "CD3D", vmax = "p99")

A SpatialExperiment works the same way. Use x = "spatial" to plot the tissue coordinates from spatialCoords():

reglScatterplot(spe, x = "spatial", colorBy = "celltype")

Seurat objects and in-memory AnnData objects (from anndataR) are also accepted. They need the optional SeuratObject and anndataR packages.

Linking plots in Shiny

When several reglScatterplot() widgets share the same plotIds, you can synchronise pan/zoom across them with enableReglScatterplotSync():

library(shiny)
ui <- fluidPage(
    fluidRow(
        column(6, reglScatterplotOutput("plotA")),
        column(6, reglScatterplotOutput("plotB"))
    ),
    actionButton("link", "Link cameras")
)
server <- function(input, output, session) {
    output$plotA <- renderReglScatterplot({
        reglScatterplot(df,
            x = "x", y = "y", colorBy = "cluster",
            plotId = "plotA"
        )
    })
    output$plotB <- renderReglScatterplot({
        reglScatterplot(df,
            x = "x", y = "y", colorBy = "score",
            plotId = "plotB"
        )
    })
    observeEvent(input$link, {
        enableReglScatterplotSync(c("plotA", "plotB"))
    })
}
shinyApp(ui, server)

Network requirements

On first use the JavaScript bundles (d3, regl-scatterplot, pickr, html2canvas, jsPDF) are loaded from a CDN (esm.sh and cdnjs.cloudflare.com). Once loaded they are cached by the browser. A helpful error message is displayed in-place if the network is unavailable.

Session info

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> 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] SingleCellExperiment_1.35.2 SummarizedExperiment_1.43.0
#>  [3] Biobase_2.73.2              GenomicRanges_1.65.4       
#>  [5] Seqinfo_1.3.2               IRanges_2.47.5             
#>  [7] S4Vectors_0.51.10           BiocGenerics_0.59.12       
#>  [9] generics_0.1.4              MatrixGenerics_1.25.0      
#> [11] matrixStats_1.5.0           reglScatterplotR_0.99.6    
#> 
#> loaded via a namespace (and not attached):
#>  [1] sass_0.4.10         SparseArray_1.13.4  lattice_0.23-1     
#>  [4] digest_0.6.39       magrittr_2.0.5      evaluate_1.0.5     
#>  [7] grid_4.6.1          RColorBrewer_1.1-3  fastmap_1.2.0      
#> [10] jsonlite_2.0.0      Matrix_1.7-6        promises_1.5.0     
#> [13] viridisLite_0.4.3   jquerylib_0.1.4     abind_1.4-8        
#> [16] cli_3.6.6           shiny_1.14.0        rlang_1.3.0        
#> [19] XVector_0.53.0      cachem_1.1.0        DelayedArray_0.39.8
#> [22] yaml_2.3.12         otel_0.2.0          S4Arrays_1.13.2    
#> [25] tools_4.6.1         httpuv_1.6.17       buildtools_1.0.0   
#> [28] R6_2.6.1            mime_0.13           lifecycle_1.0.5    
#> [31] htmlwidgets_1.6.4   bslib_0.12.0        later_1.4.8        
#> [34] Rcpp_1.1.2          xfun_0.62           sys_3.4.3          
#> [37] knitr_1.52          xtable_1.8-8        htmltools_0.5.9    
#> [40] rmarkdown_2.32      maketools_1.3.2     compiler_4.6.1