set.seed(42)
groups <- c("Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta")
per_group <- 20000L
make_group <- function(gi) {
nb <- 7L
bx <- runif(nb, -9, 9)
by <- runif(nb, -9, 9)
b <- sample(nb, per_group, replace = TRUE)
data.frame(
x = bx[b] + rnorm(per_group, 0, 0.55),
y = by[b] + rnorm(per_group, 0, 0.55),
group = groups[gi]
)
}
df <- do.call(rbind, lapply(seq_along(groups), make_group))
df$group <- factor(df$group, levels = groups)
df$id <- seq_len(nrow(df))
p <- vplot(df) |>
mark_point(
x = x, y = y, color = group, data_id = id,
tooltip = paste0(group, " · point #", id), size = 0.6
) |>
labs(title = "120,000 points, still interactive")A big scatter that stays interactive
mark_datashade() handles a million points by binning them into a grid and shading each cell by how many points land in it. You get the shape of the cloud, but the individual point is gone, so there is nothing to hover and no way to brush a few of them out.
as_widget() takes a different route that keeps every point addressable. Once a plot passes 20,000 keyed elements it stops writing one SVG node per point. A 150,000-point scatter used to ship a 75 MB file with 150,000 DOM nodes and stutter on every hover. Now the scene is drawn once as a single embedded image, and hover, brush, and pan/zoom run off a compact index of bounding boxes and keys. The page holds a picture and a lookup table in place of the nodes.
Here are 120,000 points in six colour groups, each spread across a few tight sub-blobs. Hover a point for its label, drag to brush a region, scroll to zoom, and click a legend swatch to isolate one group. It is a static HTML file with no server behind it.
as_widget(p, height = 820)