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() accepts any data.frame-like object. To plot a UMAP embedding stored in a SingleCellExperiment, build a small data frame from its reducedDim() and colData() slots:

library(SingleCellExperiment)
umap <- reducedDim(sce, "UMAP")
df <- data.frame(
    UMAP1 = umap[, 1L],
    UMAP2 = umap[, 2L],
    cluster = colData(sce)$cluster,
    nGene = colData(sce)$detected
)
reglScatterplot(df,
    x = "UMAP1", y = "UMAP2",
    colorBy = "cluster",
    pointLabels = colnames(sce)
)

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.0 (2026-04-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 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.26.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] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] reglScatterplotR_0.99.3
#> 
#> loaded via a namespace (and not attached):
#>  [1] digest_0.6.39      RColorBrewer_1.1-3 R6_2.6.1           fastmap_1.2.0     
#>  [5] xfun_0.58          maketools_1.3.2    cachem_1.1.0       knitr_1.51        
#>  [9] htmltools_0.5.9    rmarkdown_2.31     buildtools_1.0.0   lifecycle_1.0.5   
#> [13] cli_3.6.6          viridisLite_0.4.3  sass_0.4.10        jquerylib_0.1.4   
#> [17] compiler_4.6.0     sys_3.4.3          tools_4.6.0        evaluate_1.0.5    
#> [21] bslib_0.11.0       yaml_2.3.12        otel_0.2.0         htmlwidgets_1.6.4 
#> [25] jsonlite_2.0.0     rlang_1.2.0        crosstalk_1.2.2