Linked visualization examples
Source:vignettes/visualization-examples.Rmd
visualization-examples.Rmdmosaic_gl() puts Mosaic views and GPU deck.gl layers in
one browser scope over a single DuckDB-WASM coordinator and a single
shared selection, so a brush in any panel filters every other panel.
Each example below is self-contained: it builds its own data, so you can paste it straight into the console. The last example keeps its data in a DuckDB database and draws polygons.
The three pieces
A spec is an ordinary R list shaped like a Mosaic spec, with two additions:
-
mgl_data()declares a data source — either an R data frame shipped as Arrow IPC (type = "table"), or a query left in DuckDB (type = "duckdb"). -
mgl_deckgl()declares a deck.gl panel as a Mosaic input, so it participates in the same selection as the Mosaic marks around it.
params = list(sel = list(select = "crossfilter"))
creates the shared selection; panels write to it with a
select clause and read from it with
filterBy.
If you show several widgets on one page, give their tables different names: they share one DuckDB-WASM instance, so a reused name replaces the earlier table.
1. Cross-filtered scatter with a marginal histogram
A deck.gl scatterplot beside a Mosaic histogram. Brushing the
histogram filters the points; the interval brush writes to
$sel and the deck panel reads it.
cells <- data.frame(
cell_ID = sprintf("cell_%04d", 1:4000),
x = rnorm(4000, 0, 40),
y = rnorm(4000, 0, 40),
total_expr = rpois(4000, 60)
)
spec1 <- mgl_spec(
meta = list(title = "Points cross-filtered by expression"),
data = list(cells1 = mgl_data(type = "table", df = cells)),
params = list(sel = list(select = "crossfilter")),
vconcat = list(
list(
plot = list(
list(
mark = "rectY",
data = list(from = "cells1", filterBy = "$sel"),
x = list(bin = "total_expr"),
y = list(count = NULL),
fill = "#60a5fa"
),
list(
select = "intervalX", as = "$sel",
brush = list(stroke = "currentColor", fill = "transparent")
)
),
width = 640, height = 110, marginLeft = 40
),
mgl_deckgl(
id = "spatial",
filter_by = "$sel",
width = 640, height = 420,
layers = list(list(
`@@type` = "ScatterplotLayer",
id = "pts",
data = "cells1",
getPosition = "@@=[x, y]",
getFillColor = c(100L, 180L, 255L, 220L),
getRadius = 2,
radiusUnits = "pixels",
coordinateSystem = "@@#COORDINATE_SYSTEM.CARTESIAN",
positionFormat = "XY",
pickable = TRUE
))
)
)
)
mosaic_gl(spec1, width = "100%", height = 560)getPosition = "@@=[x, y]" and
coordinateSystem = "@@#COORDINATE_SYSTEM.CARTESIAN" are the
two conventions worth remembering: @@= marks a JavaScript
accessor expression and @@# marks a deck.gl constant.
Cartesian is what you want for tissue coordinates — the default is
geographic.
2. Dimension-reduction pairs linked to a categorical bar
mgl_deckgl(concat = ...) lays out several panes that
share one layer definition, each pane binding different columns to
x and y. Toggling a bar in the Mosaic bar
chart filters every pane at once.
dimred <- data.frame(
cell_ID = sprintf("cell_%04d", 1:2000),
leiden_clus = sample(sprintf("c%02d", 1:8), 2000, replace = TRUE),
umap_1 = rnorm(2000), umap_2 = rnorm(2000), umap_3 = rnorm(2000),
pca_1 = rnorm(2000), pca_2 = rnorm(2000), pca_3 = rnorm(2000)
)
spec3 <- mgl_spec(
meta = list(title = "UMAP / PCA pairs linked to leiden_clus"),
data = list(dimred = mgl_data(type = "table", df = dimred)),
params = list(sel = list(select = "crossfilter")),
vconcat = list(
list(
plot = list(
list(mark = "barY", data = list(from = "dimred", filterBy = "$sel"),
x = "leiden_clus", y = list(count = NULL), fill = "#60a5fa"),
list(select = "toggleX", as = "$sel")
),
width = 700, height = 140, marginLeft = 40
),
mgl_deckgl(
id = "umap", filter_by = "$sel", width = 700, height = 210,
concat = list(orient = "hconcat", panes = list(
list(x = "umap_1", y = "umap_2", title = "UMAP 1 vs 2"),
list(x = "umap_1", y = "umap_3", title = "UMAP 1 vs 3"),
list(x = "umap_2", y = "umap_3", title = "UMAP 2 vs 3")
)),
layers = list(list(
`@@type` = "ScatterplotLayer", id = "p", data = "dimred",
getFillColor = c(100L, 180L, 255L, 220L),
getRadius = 3, radiusUnits = "pixels", opacity = 0.85
))
),
mgl_deckgl(
id = "pca", filter_by = "$sel", width = 700, height = 210,
concat = list(orient = "hconcat", panes = list(
list(x = "pca_1", y = "pca_2", title = "PC 1 vs 2"),
list(x = "pca_1", y = "pca_3", title = "PC 1 vs 3"),
list(x = "pca_2", y = "pca_3", title = "PC 2 vs 3")
)),
layers = list(list(
`@@type` = "ScatterplotLayer", id = "p", data = "dimred",
getFillColor = c(245L, 158L, 11L, 220L),
getRadius = 3, radiusUnits = "pixels", opacity = 0.85
))
)
)
)
mosaic_gl(spec3, width = "100%", height = 620)3. Keeping data in DuckDB, and drawing polygons
The examples above ship their data frames to the browser. For
anything large, leave the data in DuckDB and send only the query:
mgl_data(type = "duckdb") with a con. Polygons
travel as GeoArrow rather than JSON, and are drawn with
GeoArrowPolygonLayer.
# A small spatial database, so this example runs anywhere. Substitute your own
# file and table names for real work.
db <- file.path(tempdir(), "cells.duckdb")
unlink(db)
con <- DBI::dbConnect(duckdb::duckdb(), db)
DBI::dbExecute(con, "INSTALL spatial")
DBI::dbExecute(con, "LOAD spatial")
DBI::dbExecute(con, "
CREATE TABLE cell_polygons AS
SELECT i AS cell_ID,
((i - 1) % 25) * 12.0 + 6 * random() AS x,
((i - 1) // 25) * 12.0 + 6 * random() AS y,
CAST(20 + 60 * random() AS INTEGER) AS total_expr
FROM range(1, 501) t(i)")
DBI::dbExecute(con, "ALTER TABLE cell_polygons ADD COLUMN geom GEOMETRY")
DBI::dbExecute(con, "UPDATE cell_polygons SET geom = ST_Buffer(ST_Point(x, y), 4.5)")
spec4 <- mgl_spec(
meta = list(title = "Polygons and centroids from DuckDB"),
data = list(
cells4 = mgl_data(
type = "duckdb",
query = "SELECT cell_ID, x, y, total_expr FROM cell_polygons"
),
polys4 = mgl_data(
type = "duckdb",
# The key column matters: the WKB encoder needs a key (cell_ID /
# poly_ID / id) alongside the geometry, or it falls back to plain
# Arrow and the polygon layer is silently skipped.
query = "SELECT cell_ID, ST_AsWKB(geom) AS geometry FROM cell_polygons",
format = "geoarrow"
)
),
params = list(sel = list(select = "crossfilter")),
vconcat = list(
list(
plot = list(
list(mark = "rectY", data = list(from = "cells4", filterBy = "$sel"),
x = list(bin = "total_expr"), y = list(count = NULL), fill = "#a78bfa"),
list(select = "intervalX", as = "$sel",
brush = list(stroke = "currentColor", fill = "transparent"))
),
width = 640, height = 110, marginLeft = 40
),
mgl_deckgl(
id = "insitu", filter_by = "$sel", width = 640, height = 440,
layers = list(
list(
`@@type` = "GeoArrowPolygonLayer",
id = "polys",
data = "polys4",
geometryColumn = "geometry",
getFillColor = c(157L, 78L, 221L, 90L),
stroked = FALSE,
lineWidthMinPixels = 0.4,
coordinateSystem = "@@#COORDINATE_SYSTEM.CARTESIAN",
positionFormat = "XY"
),
list(
`@@type` = "ScatterplotLayer",
id = "centroids",
data = "cells4",
getPosition = "@@=[x, y]",
getFillColor = c(30L, 41L, 59L, 200L),
getRadius = 2,
radiusUnits = "pixels",
coordinateSystem = "@@#COORDINATE_SYSTEM.CARTESIAN",
positionFormat = "XY"
)
)
)
)
)
mosaic_gl(spec4, con = con, width = "100%", height = 600)Passing con also lets the widget issue follow-up queries
against your R-side DuckDB as you interact, instead of materializing
everything up front. Any spatial / nanoarrow / geoarrow extensions
already loaded on that connection are reused.
In Shiny
ui <- shiny::fluidPage(mosaic_glOutput("viewer", height = "780px"))
server <- function(input, output, session) {
output$viewer <- renderMosaic_gl(mosaic_gl(spec1))
}
shiny::shinyApp(ui, server)See also
vignette("getting-started", package = "dbVisuals") for
the browser workspace, and
vignette("reproducing-paper-figures", package = "dbVisuals")
for the published figures.