Skip to contents

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_atom wraps 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 and mm() for absolute device millimetres.
  • Clean scene graph: The scene is a tree where grab_bag nodes carry a viewport and inherited aesthetics, and grab_atom nodes 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.

Installation

# Not yet on CRAN; install from source
# devtools::install_local("/path/to/grrr")

One-Step README Update

bash scripts/update-readme.sh

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:

Basic grrr scene

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:

Nested viewport scene

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:

Grid panel scene

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:

ggplot2 bridge example

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()

License

Mozilla Public License 2.0 (MPL-2.0). See LICENSE file.