19.3 The gtable step

Again, still working with our plot p

p <- ggplot(mpg, aes(displ, hwy, color = drv)) + 
  geom_point(position = position_jitter(seed = 2022)) +
  geom_smooth(method = "lm", formula = y ~ x) + 
  facet_wrap(vars(year)) + 
  ggtitle("A plot for expository purposes")
p

# print(p)
# plot(p)

The return value of ggplot_build() contains the computed data associated with each layer and a Layout ggproto object which holds information about data other than the layers, including the scales, coordinate system, facets, etc.

names(ggplot_build(p))
[1] "data"   "layout" "plot"  
ggplot_build(p)$data %>% map(head, 3)
class(ggplot_build(p)$layout)
[1] "Layout"  "ggproto" "gg"     

The output of ggplot_build() is then passed to ggplot_gtable() to be converted into graphical elements before being drawn:

ggplot2:::plot.ggplot
function (x, newpage = is.null(vp), vp = NULL, ...) 
{
    set_last_plot(x)
    if (newpage) 
        grid.newpage()
    grDevices::recordGraphics(requireNamespace("ggplot2", quietly = TRUE), 
        list(), getNamespace("ggplot2"))
    data <- ggplot_build(x)
    gtable <- ggplot_gtable(data)
    if (is.null(vp)) {
        grid.draw(gtable)
    }
    else {
        if (is.character(vp)) 
            seekViewport(vp)
        else pushViewport(vp)
        grid.draw(gtable)
        upViewport()
    }
    if (isTRUE(getOption("BrailleR.VI")) && rlang::is_installed("BrailleR")) {
        print(asNamespace("BrailleR")$VI(x))
    }
    invisible(x)
}
<bytecode: 0x5626733081f8>
<environment: namespace:ggplot2>

19.3.1 Rendering the panels

First, each layer is converted into a list of graphical objects (grobs) …

body(ggplot2:::ggplot_gtable.ggplot_built)[[6]]
geom_grobs <- by_layer(function(l, d) l$draw_geom(d, layout), 
    plot$layers, data, "converting geom to grob")

This step draws loops through each layer, taking the layer object l and the data associated with that layer d and using the Geom from the layer to draw the data.

geom_grobs <- ggtrace_inspect_vars(
  x = p, method = ggplot2:::ggplot_gtable.ggplot_built,
  at = 7, vars = "geom_grobs"
)
geom_grobs
[[1]]
[[1]]$`1`
points[geom_point.points.31105] 

[[1]]$`2`
points[geom_point.points.31107] 


[[2]]
[[2]]$`1`
gTree[geom_smooth.gTree.31124] 

[[2]]$`2`
gTree[geom_smooth.gTree.31141] 

The geom_grobs calculated at this step can also be accessed using the layer_grob() function on the ggplot object, which is similar to the layer_data() function:

list(
  layer_grob(p, i = 1),
  layer_grob(p, i = 2)
)
[[1]]
[[1]]$`1`
points[geom_point.points.31265] 

[[1]]$`2`
points[geom_point.points.31267] 


[[2]]
[[2]]$`1`
gTree[geom_smooth.gTree.31284] 

[[2]]$`2`
gTree[geom_smooth.gTree.31301] 

Each element of geom_grobs is a list of graphical objects representing a layer’s data in a facet. For example, this draws the data plotted by the first layer in the first facet

grid.newpage()
pushViewport(viewport())
grid.draw(geom_grobs[[1]][[1]])

After this, the facet takes over and assembles the panels…

The graphical representation of each layer in each facet are combined with other “non-data” elements of the plot at this step, where the plot_table variable is defined.

body(ggplot2:::ggplot_gtable.ggplot_built)[[8]]
legend_box <- plot$guides$assemble(theme)
plot_table <- ggtrace_inspect_vars(
  x = p, method = ggplot2:::ggplot_gtable.ggplot_built,
  at = 9, vars = "plot_table"
)

plot_table is a special grob called a gtable, which is the same structure as the final form of the ggplot figure before it’s sent off to the rendering system to get drawn:

plot_table
TableGrob (6 x 9) "layout": 16 grobs
   z     cells        name                                            grob
1  1 (4-4,3-3)   panel-1-1                      gTree[panel-1.gTree.31353]
2  1 (4-4,7-7)   panel-2-1                      gTree[panel-2.gTree.31367]
3  3 (2-2,3-3)  axis-t-1-1                                  zeroGrob[NULL]
4  3 (2-2,7-7)  axis-t-2-1                                  zeroGrob[NULL]
5  3 (5-5,3-3)  axis-b-1-1           absoluteGrob[GRID.absoluteGrob.31370]
6  3 (5-5,7-7)  axis-b-2-1           absoluteGrob[GRID.absoluteGrob.31370]
7  3 (4-4,6-6)  axis-l-1-2                                  zeroGrob[NULL]
8  3 (4-4,2-2)  axis-l-1-1           absoluteGrob[GRID.absoluteGrob.31376]
9  3 (4-4,8-8)  axis-r-1-2                                  zeroGrob[NULL]
10 3 (4-4,4-4)  axis-r-1-1                                  zeroGrob[NULL]
11 2 (3-3,3-3) strip-t-1-1                                   gtable[strip]
12 2 (3-3,7-7) strip-t-2-1                                   gtable[strip]
13 4 (1-1,3-7)      xlab-t                                  zeroGrob[NULL]
14 5 (6-6,3-7)      xlab-b titleGrob[axis.title.x.bottom..titleGrob.31426]
15 6 (4-4,1-1)      ylab-l   titleGrob[axis.title.y.left..titleGrob.31429]
16 7 (4-4,9-9)      ylab-r                                  zeroGrob[NULL]

When it is first defined, it’s only a partially complete representation of the plot - title, legend, margins, etc. are missing:

grid.newpage()
grid.draw(plot_table)

Recall that plot_table is the output of layout$render:

body(ggplot2:::ggplot_gtable.ggplot_built)[[8]]
legend_box <- plot$guides$assemble(theme)

This is the load-bearing step that computes/defines a bunch of smaller components internally:

ggplot_build(p)$layout$render
<ggproto method>
  <Wrapper function>
    function (...) 
render(..., self = self)

  <Inner function (f)>
    function (self, panels, data, theme, labels) 
{
    facet_bg <- self$facet$draw_back(data, self$layout, self$panel_scales_x, 
        self$panel_scales_y, theme, self$facet_params)
    facet_fg <- self$facet$draw_front(data, self$layout, self$panel_scales_x, 
        self$panel_scales_y, theme, self$facet_params)
    panels <- lapply(seq_along(panels[[1]]), function(i) {
        panel <- lapply(panels, `[[`, i)
        panel <- c(facet_bg[i], panel, facet_fg[i])
        coord_fg <- self$coord$render_fg(self$panel_params[[i]], 
            theme)
        coord_bg <- self$coord$render_bg(self$panel_params[[i]], 
            theme)
        if (isTRUE(theme$panel.ontop)) {
            panel <- c(panel, list(coord_bg), list(coord_fg))
        }
        else {
            panel <- c(list(coord_bg), panel, list(coord_fg))
        }
        ggname(paste("panel", i, sep = "-"), gTree(children = inject(gList(!!!panel))))
    })
    plot_table <- self$facet$draw_panels(panels, self$layout, 
        self$panel_scales_x, self$panel_scales_y, self$panel_params, 
        self$coord, data, theme, self$facet_params)
    labels <- self$coord$labels(list(x = self$resolve_label(self$panel_scales_x[[1]], 
        labels), y = self$resolve_label(self$panel_scales_y[[1]], 
        labels)), self$panel_params[[1]])
    labels <- self$render_labels(labels, theme)
    self$facet$draw_labels(plot_table, self$layout, self$panel_scales_x, 
        self$panel_scales_y, self$panel_params, self$coord, data, 
        theme, labels, self$params)
}

We can inspect these individual components:

layout_render_env <- ggtrace_capture_env(p, ggplot2:::Layout$render)
# grob in between the Coord's background and the layer for each panel
layout_render_env$facet_bg
[[1]]
zeroGrob[NULL] 

[[2]]
zeroGrob[NULL] 
# grob in between the Coord's foreground and the layer for each panel
layout_render_env$facet_fg
[[1]]
zeroGrob[NULL] 

[[2]]
zeroGrob[NULL] 
# individual panels (integrating the bg/fg)
layout_render_env$panels
[[1]]
gTree[panel-1.gTree.31513] 

[[2]]
gTree[panel-2.gTree.31527] 
# panels assembled into a gtable
layout_render_env$plot_table
TableGrob (4 x 7) "layout": 12 grobs
   z     cells        name                                  grob
1  1 (3-3,2-2)   panel-1-1            gTree[panel-1.gTree.31513]
2  1 (3-3,6-6)   panel-2-1            gTree[panel-2.gTree.31527]
3  3 (1-1,2-2)  axis-t-1-1                        zeroGrob[NULL]
4  3 (1-1,6-6)  axis-t-2-1                        zeroGrob[NULL]
5  3 (4-4,2-2)  axis-b-1-1 absoluteGrob[GRID.absoluteGrob.31530]
6  3 (4-4,6-6)  axis-b-2-1 absoluteGrob[GRID.absoluteGrob.31530]
7  3 (3-3,5-5)  axis-l-1-2                        zeroGrob[NULL]
8  3 (3-3,1-1)  axis-l-1-1 absoluteGrob[GRID.absoluteGrob.31536]
9  3 (3-3,7-7)  axis-r-1-2                        zeroGrob[NULL]
10 3 (3-3,3-3)  axis-r-1-1                        zeroGrob[NULL]
11 2 (2-2,2-2) strip-t-1-1                         gtable[strip]
12 2 (2-2,6-6) strip-t-2-1                         gtable[strip]
# individual labels drawn before being added to gtable and returned
layout_render_env$labels
$x
$x[[1]]
zeroGrob[NULL] 

$x[[2]]
titleGrob[axis.title.x.bottom..titleGrob.31586] 


$y
$y[[1]]
titleGrob[axis.title.y.left..titleGrob.31589] 

$y[[2]]
zeroGrob[NULL] 

19.3.1.1 Sneak peak:

The rest of the gtable step is just updating this plot_table object.

all_plot_table_versions <- ggtrace_inspect_vars(
  x = p, method = ggplot2:::ggplot_gtable.ggplot_built,
  at = "all", vars = "plot_table"
)
names(all_plot_table_versions)
 [1] "Step8"  "Step10" "Step22" "Step23" "Step24" "Step25" "Step26" "Step27"
 [9] "Step28" "Step29" "Step30" "Step31" "Step32" "Step33" "Step34"
lapply(seq_along(all_plot_table_versions), function(i) {
  ggsave(tempfile(sprintf("plot_table_%02d_", i), fileext = ".png"), all_plot_table_versions[[i]])
})
dir(tempdir(), "plot_table_.*png", full.names = TRUE) %>%
  magick::image_read() %>%
  magick::image_annotate(names(all_plot_table_versions), location = "+1050+0", size = 100) %>% 
  magick::image_write_gif("images/plot_table_animation1.gif", delay = .5)
plot_table_animation1
plot_table_animation1
all_plot_table_versions2 <- ggtrace_inspect_vars(
  x = p +
    labs(
      subtitle = "This is a subtitle",
      caption = "@yjunechoe",
      tag = "A"
    )
  ,
  method = ggplot2:::ggplot_gtable.ggplot_built,
  at = "all", vars = "plot_table"
)
identical(names(all_plot_table_versions), names(all_plot_table_versions2))
lapply(seq_along(all_plot_table_versions2), function(i) {
  ggsave(tempfile(sprintf("plot_table2_%02d_", i), fileext = ".png"), all_plot_table_versions2[[i]])
})
dir(tempdir(), "plot_table2_.*png", full.names = TRUE) %>%
  magick::image_read() %>%
  magick::image_annotate(names(all_plot_table_versions), location = "+1050+0", size = 100) %>% 
  magick::image_write_gif("images/plot_table_animation2.gif", delay = .5)
plot_table_animation2
plot_table_animation2

19.3.2 Adding guides

The legend (legend_box) is first defined in Step 11:

body(ggplot2:::ggplot_gtable.ggplot_built)[[11]]
title_height <- grobHeight(title)
legend_box <- ggtrace_inspect_vars(
  x = p, method = ggplot2:::ggplot_gtable.ggplot_built,
  at = 12, vars = "legend_box"
)
grid.newpage()
grid.draw(legend_box)

It then undergoes some edits/tweaks, including resolving the legend.position theme setting, and then finally gets added to the plot in Step 15:

body(ggplot2:::ggplot_gtable.ggplot_built)[[15]]
caption_height <- grobHeight(caption)
p_with_legend <- ggtrace_inspect_vars(
  x = p, method = ggplot2:::ggplot_gtable.ggplot_built,
  at = 16, vars = "plot_table"
)
grid.newpage()
grid.draw(p_with_legend)

The bulk of the work was done in Step 11, with the build_guides() function. That in turn calls guides_train() and guides_gengrob() which in turn calls guide_train() and guide_gengrob for each scale (including positional aesthetics like x and y).

Why scale? The scale is actually what holds information about guide. They’re two sides of the same coin - the scale translates the underlying data to some defined space, and the guide reverses that (translates a space to data). One’s for drawing, the other is for reading.

This is also why all scale_*() functions take a guide argument. Positional scales use guide_axis() as default, and non-positional scales use guide_legend() as default.

class(guide_legend())
[1] "GuideLegend" "Guide"       "ggproto"     "gg"         
# This explicitly spells out the default
p +
  scale_color_discrete(guide = guide_legend())

This is the output of the guide_train() method defined for guide_legend(). The most important piece of it is key, which is the data associated with the legend.

# TODO: The unexported function no longer exists, so we had to turn off eval.
names( ggtrace_inspect_return(p, ggplot2:::guide_train.legend) )
ggtrace_inspect_return(p, ggplot2:::guide_train.legend)$key

The output of guide_train() is passed to guide_gengrob(). This is the output of the guide_gebgrob() method defined for guide_legend():

# TODO: The unexported function no longer exists, so we had to turn off eval.
legend_gengrob <- ggtrace_inspect_return(p, ggplot2:::guide_gengrob.legend)
grid.newpage()
grid.draw(legend_gengrob)

19.3.3 Adding adornment

It’s everything else after the legend step that we saw in the gifs above. It looks trivial but this step we’re glossing over is ~150 lines of code. But it’s not super complicated - just a lot of if-else statements and a handful of low-level {grid} and {gtable} functions.

19.3.4 Output

To put it all together:

p_built <- ggplot_build(p)
p_gtable <- ggplot_gtable(p_built)
grid.newpage()
grid.draw(p_gtable)