A minimal, backend-agnostic scene graph for R graphics. grrr rethinks how R renders visualizations by dispensing with the historical baggage of grid and providing a clean, data-driven interface for building and rendering graphics scenes.
Design Philosophy
grrr is built around a few core principles:
- Normalised coordinates: All positional data lives in [0,1] relative to the enclosing viewport. Coordinate transforms (scale training, stat computation) happen upstream, typically in ggplot2’s scale system before a grab is constructed.
-
Data-frame-native shapes: A
grab_atomwraps a tibble where each row represents one shape instance (one point, one segment, one text label). -
Two unit types: A simple unit system with just
rel()for relative fractions andmm()for absolute device millimetres. -
Clean scene graph: The scene is a tree where
grab_bagnodes carry a viewport and inherited aesthetics, andgrab_atomnodes are leaves containing the actual geometry data. - Backend-agnostic: Rendering backends (SVG, Typst, raster) are separate packages implementing a simple protocol.
Implementation direction and architecture notes are tracked in DESIGN.md.
Quick Example
pts <- grab_atom(
"point",
data = dfr(
x = c(0.2, 0.5, 0.8),
y = c(0.3, 0.7, 0.4),
colour = c("red", "blue", "green"),
size = c(2, 6, 12)
)
)
sc <- scene(
children = list(
grab_atom("rect", dfr(xmin = 0, ymin = 0, xmax = 1, ymax = 1, fill = "#f7f7f7")),
pts,
grab_atom("text", dfr(x = 0.5, y = 0.95, label = "grrr scene", size = 5, colour = "#333333"))
),
width_mm = 120,
height_mm = 80
)
dev <- device_svg()
render_scene(sc, dev)
svg_out <- save_readme_svg(dev, "readme-basic-scene.svg")
svg_out
#> [1] "man/figures/readme-basic-scene.svg"Rendered output:
Nested Viewports
nested <- scene(
children = list(
grab_atom("rect", dfr(xmin = 0, ymin = 0, xmax = 1, ymax = 1, fill = "#f2efe8")),
grab_bag(
children = list(
grab_atom("point", dfr(
x = c(0.2, 0.4, 0.6, 0.8),
y = c(0.2, 0.7, 0.3, 0.9),
colour = "#b22222",
size = 3
))
),
viewport = vp(x = rel(0.55), y = rel(0.1), w = rel(0.35), h = rel(0.8), clip = TRUE)
)
),
width_mm = 140,
height_mm = 90
)
dev2 <- device_svg()
render_scene(nested, dev2)
svg_nested <- save_readme_svg(dev2, "readme-nested-viewports.svg")
svg_nested
#> [1] "man/figures/readme-nested-viewports.svg"Rendered output:
Multi-Panel Layout with vp_grid
vps <- vp_grid(2, 2, gap_x = mm(2), gap_y = mm(2))
panels <- lapply(seq_len(4), function(i) {
grab_bag(
children = list(
grab_atom("rect", dfr(xmin = 0, ymin = 0, xmax = 1, ymax = 1, fill = "#d9e8fb")),
grab_atom("text", dfr(x = 0.5, y = 0.5, label = paste("Panel", i), size = 5))
),
viewport = vps[[((i - 1) %/% 2) + 1, ((i - 1) %% 2) + 1]]
)
})
sc_grid <- scene(children = panels, width_mm = 140, height_mm = 100)
dev3 <- device_svg()
render_scene(sc_grid, dev3)
svg_grid <- save_readme_svg(dev3, "readme-grid-panels.svg")
svg_grid
#> [1] "man/figures/readme-grid-panels.svg"Rendered output:
ggplot2 Bridge
library(ggplot2)
#>
#> Attaching package: 'ggplot2'
#> The following object is masked from 'package:grrr':
#>
#> rel
p <- ggplot(mtcars, aes(wt, mpg, colour = factor(cyl))) +
geom_point() +
facet_wrap(~am) +
theme_minimal(base_size = 5) +
theme(
panel.spacing = grid::unit(4, "mm"),
strip.text = element_text(size = 4),
axis.title = element_text(size = 4.5),
axis.text = element_text(size = 4),
legend.title = element_text(size = 4.5),
legend.text = element_text(size = 4)
)
b <- ggplot_build(p)
sc <- ggbuild_to_scene(b, width_mm = 180, height_mm = 120)
dev <- device_svg()
render_scene(sc, dev)
svg_ggplot <- save_readme_svg(dev, "readme-ggplot-bridge.svg")
svg_ggplot
#> [1] "man/figures/readme-ggplot-bridge.svg"Rendered output:
Architecture
Core Objects
-
grab_atom: A leaf node wrapping a tibble of shape instances. -
grab_bag: An interior node containing child grabs, a viewport, and optional inherited aesthetics. -
viewport: Defines a rectangular region within a parent. -
aes_spec: Specifies aesthetic attributes cascading down the scene tree.
Primitive Shapes
| Shape | Required Columns | Optional |
|---|---|---|
point |
x, y | colour, fill, size, shape, alpha, stroke |
segment |
x, y, xend, yend | colour, linewidth, linetype, alpha |
rect |
xmin, ymin, xmax, ymax | fill, colour, linewidth, linetype, alpha |
path |
x, y, group | colour, linewidth, linetype, alpha |
polygon |
x, y, group | fill, colour, linewidth, linetype, alpha |
text |
x, y, label | colour, size, angle, hjust, vjust, alpha |
Testing
devtools::test()