Interactive WebGL Scatterplots with reglScatterplot

George Muñoz

2026-10-22

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:

The widget works in:

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")

plot of chunk basic

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"
)

plot of chunk categorical

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"
)

plot of chunk continuous

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':
#> 
#>     Filter, Find, Map, Position, Reduce, anyDuplicated, aperm, append,
#>     as.data.frame, basename, cbind, colnames, dirname, do.call,
#>     duplicated, eval, evalq, get, grep, grepl, is.unsorted, lapply,
#>     mapply, match, mget, order, paste, pmax, pmax.int, pmin, pmin.int,
#>     rank, rbind, 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':
#> 
#>     I, expand.grid, 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")

plot of chunk sce

Colouring by a gene reads its row from the assay:

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

plot of chunk sce-gene

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 24.04.5 LTS
#> 
#> Matrix products: default
#> BLAS:   /home/biocbuild/bbs-3.24-bioc/R/lib/libRblas.so 
#> LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_GB              LC_COLLATE=C              
#>  [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: America/New_York
#> 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] Matrix_1.7-6        jsonlite_2.0.0      compiler_4.6.1     
#>  [4] promises_1.5.0      Rcpp_1.1.2          later_1.4.8        
#>  [7] yaml_2.3.12         fastmap_1.2.0       mime_0.13          
#> [10] lattice_0.23-1      R6_2.6.1            XVector_0.53.0     
#> [13] commonmark_2.0.0    S4Arrays_1.13.2     knitr_1.52         
#> [16] htmlwidgets_1.6.4   DelayedArray_0.39.8 shiny_1.14.0       
#> [19] RColorBrewer_1.1-3  rlang_1.3.0         litedown_0.11      
#> [22] httpuv_1.6.17       xfun_0.62           otel_0.2.0         
#> [25] viridisLite_0.4.3   SparseArray_1.13.4  cli_3.6.6          
#> [28] magrittr_2.0.5      digest_0.6.39       grid_4.6.1         
#> [31] xtable_1.8-8        lifecycle_1.0.5     evaluate_1.0.5     
#> [34] abind_1.4-8         tools_4.6.1         htmltools_0.5.9