S7 grab structures: subsetting, joins, and recursive updates
Source:vignettes/s7-grab-structures.Rmd
s7-grab-structures.RmdWhy this vignette
grrr grab objects are S7 classes:
-
grab_atom: a leaf node with one shape type and a tibble-like@data -
grab_bag: an interior node with@children(which can be atoms or bags)
A useful mental model is:
- manipulate rows/columns at the
grab_atom@datalevel - rebuild or recursively transform atoms inside
grab_bag
This vignette focuses on that workflow with manually hard-coded examples.
All examples below use plotting-stage columns (for example
x, y, size, fill,
colour, label). A single technical key
(point_key) is kept only to demonstrate join-style
post-processing.
1) Hard-coded grab_atom examples
pts_atom <- grab_atom(
"point",
dfr(
point_key = c("p1", "p2", "p3", "p4"),
x = c(0.15, 0.35, 0.60, 0.85),
y = c(0.80, 0.45, 0.55, 0.25),
size = c(5, 8, 6, 10),
alpha = c(0.9, 0.6, 0.8, 0.7),
fill = c("#4C78A8", "#4C78A8", "#4C78A8", "#4C78A8"),
colour = "#1F2937",
stroke = 0.3
)
)
seg_atom <- grab_atom(
"segment",
dfr(
x = c(0.1, 0.1),
y = c(0.1, 0.1),
xend = c(0.9, 0.1),
yend = c(0.1, 0.9),
colour = c("#9CA3AF", "#9CA3AF"),
linewidth = c(0.35, 0.35),
linetype = c("solid", "solid")
)
)
tibble::as_tibble(pts_atom@data)
#> # A tibble: 4 × 8
#> point_key x y size alpha fill colour stroke
#> <chr> <dbl> <dbl> <dbl> <dbl> <chr> <chr> <dbl>
#> 1 p1 0.15 0.8 5 0.9 #4C78A8 #1F2937 0.3
#> 2 p2 0.35 0.45 8 0.6 #4C78A8 #1F2937 0.3
#> 3 p3 0.6 0.55 6 0.8 #4C78A8 #1F2937 0.3
#> 4 p4 0.85 0.25 10 0.7 #4C78A8 #1F2937 0.3
tibble::as_tibble(seg_atom@data)
#> # A tibble: 2 × 7
#> x y xend yend colour linewidth linetype
#> <dbl> <dbl> <dbl> <dbl> <chr> <dbl> <chr>
#> 1 0.1 0.1 0.9 0.1 #9CA3AF 0.35 solid
#> 2 0.1 0.1 0.1 0.9 #9CA3AF 0.35 solid2) Subsetting rows in grab_atom
A common operation is to subset rows while preserving S7 structure.
# Keep only smaller symbols
light_pts <- grab_filter(pts_atom, size < 9)
nrow(pts_atom@data)
#> [1] 4
nrow(light_pts@data)
#> [1] 3
light_pts@data[, c("point_key", "size", "alpha")]
#> # A tibble: 3 × 3
#> point_key size alpha
#> <chr> <dbl> <dbl>
#> 1 p1 5 0.9
#> 2 p2 8 0.6
#> 3 p3 6 0.8You can chain additional tibble-like column operations naturally.
light_pts2 <- grab_mutate(light_pts, alpha_size = alpha * size)
light_pts2 <- grab_select(light_pts2, point_key, x, y, size, alpha, alpha_size)
light_pts2@data
#> # A tibble: 3 × 6
#> point_key x y size alpha alpha_size
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 p1 0.15 0.8 5 0.9 4.5
#> 2 p2 0.35 0.45 8 0.6 4.8
#> 3 p3 0.6 0.55 6 0.8 4.83) Joining metadata into grab_atom
grab_left_join() brings lookup metadata directly into
atom data.
class_key <- data.frame(
point_key = c("p1", "p2", "p3", "p4"),
class = c("cool", "warm", "cool", "warm"),
fill_new = c("#2A9D8F", "#E76F51", "#2A9D8F", "#E76F51"),
stringsAsFactors = FALSE
)
pts_joined <- grab_left_join(pts_atom, class_key, by = "point_key")
pts_joined <- grab_mutate(pts_joined, fill = fill_new)
pts_joined <- grab_select(pts_joined, -fill_new)
pts_joined@data[, c("point_key", "class", "fill")]
#> # A tibble: 4 × 3
#> point_key class fill
#> <chr> <chr> <chr>
#> 1 p1 cool #2A9D8F
#> 2 p2 warm #E76F51
#> 3 p3 cool #2A9D8F
#> 4 p4 warm #E76F514) Hard-coded grab_bag examples
Now we manually build a nested scene-like bag from atoms.
bg_atom <- grab_atom(
"rect",
dfr(xmin = 0, ymin = 0, xmax = 1, ymax = 1, fill = "#F8FAFC", colour = "#CBD5E1", linewidth = 0.3)
)
label_atom <- grab_atom(
"text",
dfr(
x = pts_atom@data$x,
y = pts_atom@data$y + 0.04,
label = pts_atom@data$point_key,
size = 3.5,
colour = "#334155",
hjust = 0.5,
vjust = 0.5
)
)
annotation_bag <- grab_bag(
name = "annotation",
children = list(
grab_atom("text", dfr(x = 0.5, y = 0.95, label = "Hard-coded grab_bag", size = 4, colour = "#0F172A", hjust = 0.5, vjust = 1))
)
)
plot_bag <- grab_bag(
name = "plot-bag",
children = list(bg_atom, seg_atom, pts_atom, label_atom, annotation_bag)
)
data.frame(
bag_name = plot_bag@name,
n_children = length(plot_bag@children),
stringsAsFactors = FALSE
)
#> bag_name n_children
#> 1 plot-bag 55) Recursive transformations for grab_bag
Utilities also recurse through nested grab_bag
trees.
# Example: keep only points with size < 9, leave other atoms unchanged.
plot_bag_light <- grab_filter(plot_bag, size < 9)
orig_n <- nrow(plot_bag@children[[3]]@data)
new_n <- nrow(plot_bag_light@children[[3]]@data)
c(orig_points = orig_n, light_points = new_n)
#> orig_points light_points
#> 4 3
emit_grab_preview(plot_bag_light, "After recursive filtering: only point rows with size < 9 remain")Recursive join update
plot_bag_colored <- grab_left_join(plot_bag, class_key, by = "point_key")
plot_bag_colored <- grab_map_atoms(plot_bag_colored, function(atom) {
if (!"fill_new" %in% names(atom@data)) return(atom)
atom <- grab_mutate(atom, fill = fill_new)
grab_select(atom, -fill_new)
})
plot_bag_colored@children[[3]]@data[, c("point_key", "class", "fill")]
#> # A tibble: 4 × 3
#> point_key class fill
#> <chr> <chr> <chr>
#> 1 p1 cool #2A9D8F
#> 2 p2 warm #E76F51
#> 3 p3 cool #2A9D8F
#> 4 p4 warm #E76F51
emit_grab_preview(plot_bag_colored, "After recursive join update: point colors mapped from joined class metadata")6) Summary pattern
For data manipulation with grabs:
- Start with manually hard-coded atoms/bags so structure is explicit.
- Use
grab_filter(),grab_mutate(),grab_select(), andgrab_left_join()for atom-level data manipulation. - Apply the same verbs recursively to bags (default behavior) so nested scene trees update consistently.
- Use
grab_map_atoms()when you need custom atom-wise logic beyond the built-in verbs.
That pattern gives you predictable, testable scene edits while keeping all S7 structure intact.