ROIpad ← Back to Search
stackoverflow › answer

Answer to: Modify the gtable of a ggplot2 and end up with a ggplot2 object without loss of information

Score: 3
Answered: Nov 11, 2025
User Rep: 521
Allan Cameron wrote a great answer that solves all the hard parts of this question. His code has a limitation though: If you add multiple annotations to a plot, only the final one is actually applied -- the previous ones get discarded. Here, I've slightly modified his code to fix this. The basic idea is to redefine Allan's class_ggplot_anno so that its only new property is a list named annotations that can grow to store multiple annotations. So then ggplot_add.anno appends a new annotation to any existing annotations. Lastly, the new ggplot_gtable method iteratively applies all the annotations to the gtable. Aside from the above changes, this code comes from Allan's answer, and thus his explanation of the code still applies. Update (2025-11-12): With a hint from Teun van den Brand, I've revised and majorly simplified the code. The most important change is that the custom ggplot_build method is now just two lines of code instead of 60+ and does not rely on any unexported ggplot2 functions. Basically, instead of copying and modifying the source code for method(ggplot_build, class_ggplot), I just call that method on our class_ggplot_anno object and then convert the output to our custom class_ggplot_built_anno with the new annotations property. This required changing class_ggplot_built_anno so that it inherits from class_ggplot_built rather than S7::S7_object, and the resulting definition of class_ggplot_built_anno is now much simpler than before. One quirk is that I had to use NextMethod (from the base package) to dispatch to the parent class; I couldn't get S7::super to work here. I think that's because the ggplot2 codebase is in a transitional state where it hasn't been fully converted to S7 yet. If ggplot2 does eventually go full S7, I'm not sure if NextMethod will still work and I may need to switch to using S7::super. library(ggplot2) manipulate_gtable <- function(gtable, text, side) { side <- match(side, c("t", "r", "b", "l")) rot <- c(0, -90, 0, 90)[side] sz <- grid::unit(1, "lines") pos <- c(0, -1, -1, 0)[side] fun <- rep(list(gtable::gtable_add_rows, gtable::gtable_add_cols), 2)[[side]] gt <- do.call(fun, list(gtable, sz, pos)) trows <- dim(gt)[1] tcols <- dim(gt)[2] t_pos <- c(1, 1, trows, 1)[side] b_pos <- c(1, trows, trows, trows)[side] l_pos <- c(1, tcols, 1, 1)[side] r_pos <- c(tcols, tcols, tcols, 1)[side] gtable::gtable_add_grob( gt, grobs = grid::textGrob(text, rot = rot), t = t_pos, l = l_pos, b = b_pos, r = r_pos ) } class_ggplot_anno <- S7::new_class( "ggplot_anno", parent = ggplot2::class_ggplot, properties = list(annotations = S7::class_list), constructor = function(plot, annotations) { # The new annotations property should be a list of lists. Each inner list # should have two elements named "text" and "side". for (anno in annotations) { stopifnot("'text' must be a string" = is.character(anno$text) && length(anno$text) == 1) if(length(anno$side) != 1 || !anno$side %in% c("t", "r", "b", "l")) { stop("'side' must be one of 't', 'r', 'b', or 'l'") } } S7::new_object(plot, annotations = annotations) } ) anno_margin <- function(text, side = "t") { structure(list(text = text, side = side), class = "anno") } ggplot_add.anno <- function(object, plot, ...) { # Append new annotation to existing annotations if there are any old_annotations <- tryCatch(plot@annotations, error = \(e) NULL) class_ggplot_anno(plot, annotations = c(old_annotations, list(object))) } class_ggplot_built_anno <- S7::new_class( "ggplot_built_anno", parent = ggplot2::class_ggplot_built, properties = list(annotations = S7::class_list) ) S7::method(ggplot_build, class_ggplot_anno) <- function(plot, ...) { # Use the ggplot_build method from parent class class_ggplot, then convert the # result to our custom class class_ggplot_built_anno and add our new property build <- NextMethod(plot) S7::convert(build, to = class_ggplot_built_anno, annotations = plot@annotations) } S7::method(ggplot_gtable, class_ggplot_built_anno) <- function(plot) { # Use the ggplot_gtable method from parent class class_ggplot_built plot_table <- NextMethod(plot) # Iteratively apply all the annotations to the gtable for (anno in plot@annotations) plot_table <- manipulate_gtable(plot_table, anno$text, anno$side) plot_table } mpg |> ggplot(aes(cty, hwy)) + geom_point() + anno_margin("Hello world!", side = "t") + anno_margin("Goodnight moon!", side = "t") + geom_line() + theme_bw() + anno_margin("¡Hola mundo!", side = "r") + anno_margin("Bonjour le monde!", side = "l") + anno_margin("Hallo Welt!", side = "b")
r ggplot2
View Question ↗
Question