rDeckgl provides R bindings for deck.gl 9.2.2, a WebGL-powered framework for visualizing large datasets. This vignette demonstrates the basic usage of rDeckgl with working examples.
This example demonstrates deck.gl rendering performance with ~10,000 points using a viridis color palette:
library(rDeckgl)
library(scales)
# Generate a spaced-out grid of ~10K points around San Francisco
set.seed(42)
grid_size <- 100L
lon_seq <- seq(-122.515, -122.355, length.out = grid_size)
lat_seq <- seq(37.70, 37.82, length.out = grid_size)
grid <- expand.grid(lon = lon_seq, lat = lat_seq)
grid$value <- rnorm(nrow(grid), mean = 0, sd = 1)
# Add light jitter
jitter_strength <- 0.0007
grid$lon <- grid$lon + runif(nrow(grid), -jitter_strength, jitter_strength)
grid$lat <- grid$lat + runif(nrow(grid), -jitter_strength, jitter_strength)
grid$radius <- runif(nrow(grid), 25, 80)
points_data <- grid
# Map 'value' to viridis color palette
domain_range <- range(points_data$value)
palette_fun <- col_numeric(viridis_pal(option = "B")(256), domain_range)
rgba <- col2rgb(palette_fun(points_data$value))
points_data$color_r <- rgba[1, ]
points_data$color_g <- rgba[2, ]
points_data$color_b <- rgba[3, ]
spec <- list(
`@@type` = "DeckGL",
initialViewState = list(
longitude = mean(range(points_data$lon)),
latitude = mean(range(points_data$lat)),
zoom = 11.5,
pitch = 20,
bearing = 0
),
tooltip = list(
html = "<div><strong>Value:</strong> {value}<br/><strong>Radius:</strong> {radius} m</div>",
style = list(
backgroundColor = "#0e1119",
color = "#FFFFFF",
fontSize = "12px"
)
),
views = list(
list(
`@@type` = "MapView",
controller = TRUE,
mapStyle = "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
)
),
layers = list(
list(
`@@type` = "ScatterplotLayer",
id = "scatterplot",
data = list(
type = "duckdb",
query = "SELECT lon, lat, radius, value, color_r, color_g, color_b FROM points"
),
getPosition = "@@=[lon, lat]",
getRadius = "@@=radius",
getFillColor = "@@=[color_r, color_g, color_b, 200]",
pickable = TRUE,
autoHighlight = TRUE,
radiusUnits = "meters"
)
)
)
deckgl(
spec = spec,
data = list(points = points_data),
width = "100%",
height = "600px"
)This example uses remote data from the deck.gl website to create a 3D hexagon heatmap:
library(rDeckgl)
hexagon_spec <- list(
initialViewState = list(
longitude = -1.4157267858730052,
latitude = 52.232395363869415,
zoom = 6.6,
pitch = 40.5,
bearing = -27.396674584323023
),
views = list(
list(
`@@type` = "MapView",
controller = TRUE,
mapStyle = "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json"
)
),
layers = list(
list(
`@@type` = "HexagonLayer",
id = "heatmap",
data = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv",
coverage = 1,
pickable = TRUE,
elevationRange = c(0, 3000),
elevationScale = 50,
extruded = TRUE,
getPosition = "@@=[lng,lat]",
radius = 1000,
colorRange = list(
c(1, 152, 189),
c(73, 227, 206),
c(216, 254, 181),
c(254, 237, 177),
c(254, 173, 84),
c(209, 55, 78)
)
)
)
)
deckgl(
spec = hexagon_spec,
width = "100%",
height = "600px"
)This example reproduces the kind of visualization used in spatial omics analysis — cell-boundary polygons rendered via the fast GeoArrow path, with a centroid scatter layer coloured by simulated expression level. All data is generated in-memory; no external files are required.
library(rDeckgl)
library(DBI)
library(duckdb)
# ── 1. Generate mock spatial omics data ────────────────────────────────────────
set.seed(42)
n_cells <- 300
field_w <- 8000 # microns
field_h <- 6000
# Random centroids
cx <- runif(n_cells, 200, field_w - 200)
cy <- runif(n_cells, 200, field_h - 200)
# Simulated per-cell metrics
total_expr <- pmax(0, round(rnorm(n_cells, 200, 80)))
cluster <- paste0("c", sample.int(6L, n_cells, replace = TRUE))
# Colour centroids by cluster
pal <- grDevices::hcl.colors(6, "Dark 3")
rgb_ <- grDevices::col2rgb(pal[as.integer(factor(cluster))])
cr <- as.integer(rgb_[1, ])
cg <- as.integer(rgb_[2, ])
cb <- as.integer(rgb_[3, ])
centroids <- data.frame(
cell_ID = sprintf("CELL_%04d", seq_len(n_cells)),
x = cx, y = cy,
total_expr, cluster, r = cr, g = cg, b = cb
)
# Build irregular hexagonal WKT polygons
angles <- seq(0, 2 * pi, length.out = 7)[-7] # 6 vertices
wkt_polygons <- vapply(seq_len(n_cells), function(i) {
radius <- runif(1, 80, 200)
jitter <- runif(6, 0.80, 1.20)
vx <- cx[i] + radius * jitter * cos(angles)
vy <- cy[i] + radius * jitter * sin(angles)
pts <- paste(sprintf("%f %f", c(vx, vx[1]), c(vy, vy[1])), collapse = ", ")
sprintf("POLYGON((%s))", pts)
}, character(1))
polygon_df <- data.frame(
cell_ID = centroids$cell_ID,
wkt = wkt_polygons
)
# ── 2. Load data into in-memory DuckDB with spatial extension ──────────────────
con <- dbConnect(duckdb(), dbdir = ":memory:")
for (sql in c("INSTALL spatial", "LOAD spatial",
"INSTALL nanoarrow FROM community", "LOAD nanoarrow",
"CALL register_geoarrow_extensions()")) {
try(dbExecute(con, sql), silent = TRUE)
}
dbWriteTable(con, "poly_raw", polygon_df, overwrite = TRUE)
dbExecute(con, "
CREATE TABLE cells AS
SELECT cell_ID,
ST_GeomFromText(wkt) AS geometry
FROM poly_raw
WHERE wkt IS NOT NULL
")
# ── 3. Build the deck.gl spec ─────────────────────────────────────────────────
# Orthographic (Cartesian) view — same as spatial omics viewers
cx_mid <- field_w / 2
cy_mid <- field_h / 2
zoom <- log2(600 / field_w) # fit ~600 px wide
polygon_query <- "SELECT cell_ID, geometry FROM cells WHERE geometry IS NOT NULL"
spec <- list(
views = list(
list(
`@@type` = "OrthographicView",
controller = TRUE
)
),
initialViewState = list(
target = list(cx_mid, cy_mid, 0),
zoom = zoom,
minZoom = -5,
maxZoom = 8
),
layers = list(
list(
`@@type` = "GeoArrowSolidPolygonLayer",
id = "cell-polygons",
data = list(
type = "duckdb",
query = polygon_query,
format = "geoarrow"
),
geometryColumn = "geometry",
getFillColor = c(100L, 80L, 220L, 100L),
stroked = FALSE,
coordinateSystem = "@@#COORDINATE_SYSTEM.CARTESIAN",
positionFormat = "XY",
pickable = TRUE
),
list(
`@@type` = "ScatterplotLayer",
id = "cell-centroids",
data = list(
type = "duckdb",
query = "SELECT cell_ID, x, y, r, g, b FROM centroids"
),
getPosition = "@@=[x, y]",
getFillColor = "@@=[r, g, b]",
getRadius = 2,
radiusUnits = "pixels",
coordinateSystem = "@@#COORDINATE_SYSTEM.CARTESIAN",
positionFormat = "XY",
opacity = 0.85,
pickable = TRUE
)
)
)
widget <- deckgl(
spec = spec,
con = con,
data = list(centroids = centroids),
width = "100%",
height = "600px"
)
DBI::dbDisconnect(con)
widgetrDeckgl automatically creates an in-memory DuckDB database for efficient data handling:
data
argumenttype = "duckdb" and SQL queries
in your specUse rDeckgl in Shiny applications with reactive bindings. Here’s a complete working example:
library(shiny)
library(rDeckgl)
library(scales)
ui <- fluidPage(
titlePanel("Deck.gl in Shiny"),
deckglOutput("myDeckgl", width = "100%", height = "600px")
)
server <- function(input, output, session) {
output$myDeckgl <- renderDeckgl({
# Generate sample data
set.seed(42)
n_points <- 500
points_data <- data.frame(
lon = runif(n_points, -122.5, -122.3),
lat = runif(n_points, 37.7, 37.85),
value = rnorm(n_points),
radius = runif(n_points, 50, 150)
)
# Add colors
palette_fun <- col_numeric(viridis_pal(option = "B")(256), range(points_data$value))
rgba <- col2rgb(palette_fun(points_data$value))
points_data$color_r <- rgba[1, ]
points_data$color_g <- rgba[2, ]
points_data$color_b <- rgba[3, ]
# Create spec
spec <- list(
`@@type` = "DeckGL",
initialViewState = list(
longitude = -122.4,
latitude = 37.78,
zoom = 11,
pitch = 0,
bearing = 0
),
views = list(
list(
`@@type` = "MapView",
controller = TRUE,
mapStyle = "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
)
),
layers = list(
list(
`@@type` = "ScatterplotLayer",
id = "points",
data = list(
type = "duckdb",
query = "SELECT lon, lat, radius, color_r, color_g, color_b FROM points"
),
getPosition = "@@=[lon, lat]",
getRadius = "@@=radius",
getFillColor = "@@=[color_r, color_g, color_b, 180]",
pickable = TRUE,
radiusUnits = "meters"
)
)
)
deckgl(spec = spec, data = list(points = points_data))
})
}
shinyApp(ui, server)Note: This example generates data within the server
function. For reactive/dynamic visualizations, wrap your data generation
in reactive() and use observe() or
observeEvent() to update the visualization based on user
input.
The following self-contained scripts mirror our internal GeoArrow
tests. Copy them into your own session—no local Examples/
folder required.
library(rDeckgl)
set.seed(42)
n_points <- 1000
points_data <- data.frame(
id = 1:n_points,
lon = runif(n_points, -122.5, -122.3),
lat = runif(n_points, 37.7, 37.85),
value = rnorm(n_points, mean = 0.5, sd = 0.3),
radius = runif(n_points, 50, 200)
)
points_data$value <- pmax(0, pmin(1, points_data$value))
spec <- list(
`@@type` = "DeckGL",
initialViewState = list(
longitude = -122.4,
latitude = 37.78,
zoom = 12,
pitch = 0,
bearing = 0
),
views = list(
list(
`@@type` = "MapView",
controller = TRUE,
mapStyle = "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
)
),
layers = list(
list(
`@@type` = "ScatterplotLayer",
id = "points",
data = list(
type = "duckdb",
query = "SELECT id, lon, lat, value, radius FROM points_data"
),
getPosition = "@@=[lon, lat]",
getRadius = "@@=radius",
getFillColor = "@@=[value * 255, (1 - value) * 255, 100, 200]",
radiusUnits = "meters",
pickable = TRUE,
autoHighlight = TRUE
)
)
)
deckgl(
spec = spec,
data = list(points_data = points_data),
width = "100%",
height = "600px"
)This script renders the Utah portion of the Microsoft USBuildingFootprints dataset as GeoArrow polygons. Create a GeoParquet with GeoArrow-encoded geometries from the upstream ZIP:
ogr2ogr \
Utah.parquet \
/vsizip/Utah.geojson.zip \
-dialect SQLite \
-sql "SELECT geometry FROM 'Utah.geojson'" \
-lco COMPRESSION=BROTLI \
-lco GEOMETRY_ENCODING=GEOARROW \
-lco POLYGON_ORIENTATION=COUNTERCLOCKWISE \
-lco ROW_GROUP_SIZE=9999999library(rDeckgl)
library(arrow)
parquet_path <- "Utah.parquet"
if (!file.exists(parquet_path)) {
stop("Parquet file not found: ", parquet_path)
}
parquet_data <- arrow::read_parquet(parquet_path)
max_rows <- 1000000
sample_data <- if (nrow(parquet_data) > max_rows) {
head(parquet_data, max_rows)
} else {
parquet_data
}
# Locate the densest cluster of buildings to centre the initial view
bbox_data <- sample_data$GEOMETRY_bbox
if (!is.null(bbox_data)) {
cx <- (as.numeric(bbox_data$xmin) + as.numeric(bbox_data$xmax)) / 2
cy <- (as.numeric(bbox_data$ymin) + as.numeric(bbox_data$ymax)) / 2
grid_key <- paste(floor(cx / 0.1), floor(cy / 0.1), sep = ",")
densest_cell <- names(which.max(table(grid_key)))
idx <- which(grid_key == densest_cell)
center_lon <- mean(cx[idx], na.rm = TRUE)
center_lat <- mean(cy[idx], na.rm = TRUE)
} else {
center_lon <- -110.4144
center_lat <- 39.4991
}
# Encode the Arrow table as base64 IPC for inline delivery
arrow_table <- arrow::arrow_table(sample_data)
temp_file <- tempfile(fileext = ".arrows")
arrow::write_ipc_stream(arrow_table, temp_file)
arrow_bytes <- readBin(temp_file, "raw", n = file.info(temp_file)$size)
unlink(temp_file)
arrow_b64 <- base64enc::base64encode(arrow_bytes)
if (length(arrow_bytes) / 1024 / 1024 > 50) {
warning("Encoded payload exceeds 50 MB and may cause browser issues.")
}
spec <- list(
`@@type` = "DeckGL",
width = 1024,
height = 768,
initialViewState = list(
longitude = center_lon,
latitude = center_lat,
zoom = 12,
pitch = 0,
bearing = 0
),
controller = TRUE,
layers = list(
list(
`@@type` = "GeoArrowSolidPolygonLayer",
id = "utah-buildings",
data = list(`__arrow` = arrow_b64),
geometryColumn = "GEOMETRY",
getFillColor = c(255, 100, 0, 200),
extruded = FALSE,
pickable = TRUE,
`_normalize` = FALSE,
`_windingOrder` = "CCW"
)
)
)
deckgl(spec = spec, width = 1024, height = 768)This example demonstrates rendering GeoArrow polygons in Shiny using
the native GeoArrowPolygonLayer, which passes Arrow tables
directly to the layer without converting to binary format:
library(shiny)
library(rDeckgl)
library(arrow)
# Prepare the data file
parquet_path <- "Utah.parquet"
if (!file.exists(parquet_path)) {
stop("Parquet file not found: ", parquet_path)
}
# Read Parquet and create Arrow IPC file
parquet_data <- arrow::read_parquet(parquet_path)
# Sample for reasonable performance
max_rows <- 1000000
if (nrow(parquet_data) > max_rows) {
sample_data <- head(parquet_data, max_rows)
} else {
sample_data <- parquet_data
}
# Compute density-based center
bbox_data <- sample_data$GEOMETRY_bbox
if (!is.null(bbox_data)) {
xmin <- as.numeric(bbox_data$xmin)
xmax <- as.numeric(bbox_data$xmax)
ymin <- as.numeric(bbox_data$ymin)
ymax <- as.numeric(bbox_data$ymax)
cx <- (xmin + xmax) / 2
cy <- (ymin + ymax) / 2
grid_size <- 0.1
grid_x <- floor(cx / grid_size)
grid_y <- floor(cy / grid_size)
grid_key <- paste(grid_x, grid_y, sep = ",")
grid_counts <- table(grid_key)
densest_cell <- names(which.max(grid_counts))
densest_indices <- which(grid_key == densest_cell)
center_lon <- mean(cx[densest_indices], na.rm = TRUE)
center_lat <- mean(cy[densest_indices], na.rm = TRUE)
} else {
center_lon <- -110.4144
center_lat <- 39.4991
}
# Write Arrow IPC file to temporary location
temp_arrow <- tempfile(fileext = ".arrows")
arrow_table <- arrow::arrow_table(sample_data)
arrow::write_ipc_stream(arrow_table, temp_arrow)
# Set up static file serving
static_root <- dirname(temp_arrow)
static_name <- "utahdata"
addResourcePath(static_name, static_root)
arrow_url <- paste0(static_name, "/", basename(temp_arrow))
# UI
ui <- fluidPage(
titlePanel("Utah Buildings - Native GeoArrow Rendering"),
deckglOutput("map", width = "100%", height = "600px")
)
# Server
server <- function(input, output, session) {
output$map <- renderDeckgl({
spec <- list(
`@@type` = "DeckGL",
width = 1024,
height = 600,
initialViewState = list(
longitude = center_lon,
latitude = center_lat,
zoom = 12,
pitch = 0,
bearing = 0
),
controller = TRUE,
layers = list(
list(
# Use GeoArrowPolygonLayer for native rendering (no binary conversion)
`@@type` = "GeoArrowPolygonLayer",
id = "utah-buildings-native",
data = list(`__arrow_url` = arrow_url),
geometryColumn = "GEOMETRY",
getFillColor = c(255, 100, 0, 200),
getLineColor = c(255, 255, 255, 100),
filled = TRUE,
stroked = TRUE,
extruded = FALSE,
pickable = TRUE,
autoHighlight = TRUE
)
)
)
deckgl(spec = spec, width = "100%", height = "600px")
})
# Cleanup on session end
onSessionEnded(function() {
if (file.exists(temp_arrow)) {
unlink(temp_arrow)
}
})
}
# Run app
shinyApp(ui, server)Key differences from binary conversion:
GeoArrowPolygonLayer instead of
GeoArrowSolidPolygonLayer_normalize or _windingOrder flags
needed