Search code examples
rggplot2cowplot

Shared panel border with cowplot and plot_grid


I'm trying to draw a border around two plots that have been aligned with plot_grid from the cowplot package. Please see the following example (modified from the "Changing the axis positions" vignette):

require(gtable)
require(cowplot)

# top plot
p1 <- ggplot(mtcars, aes(mpg, disp)) + geom_line(colour = 'blue') +
  background_grid(minor = 'none')
g1 <- switch_axis_position(p1, 'xy') # switch both axes
g1 <- gtable_squash_rows(g1, length(g1$height)) # set bottom row to 0 height

# bottom plot
p2 <- ggplot(mtcars, aes(mpg, qsec)) + geom_line(colour = 'green') + ylim(14, 25) +
  background_grid(minor = 'none')
g2 <- ggplotGrob(p2)
g2 <- gtable_add_cols(g2, g1$widths[5:6], 4) # add the two additional columns that g1 has
g2 <- gtable_squash_rows(g2, 1:2) # set top two rows to 0 height

plot_grid(g1, g2, ncol = 1, align = 'v') + 
  annotate("rect", xmin = 0.1, xmax = 0.9, ymin = 0.1, ymax = 0.9, 
           color = "red", fill = NA)

example

Now, instead of the arbitrarily chosen coordinates for the red box, I'd like to have it aligned with the axis lines. I assume these coordinates can be extracted from the plot_grid output, but I have no idea how.


Solution

  • Based on my understanding of grobs, I'd say it's easier to get the coordinates for each plot & add the border segments before combining the plots using plot_grid.

    Example data:

    library(gtable)
    library(cowplot)
    
    # sample plots
    # (note: the cowplot function switch_axis_position has been deprecated, as its
    # creator notes ggplot2 now natively supports axes on either side of the plot.)
    p1 <- ggplot(mtcars, aes(mpg, disp)) + 
      geom_line(colour = 'blue') +
      scale_x_continuous(position = "top") +
      scale_y_continuous(position = "right") +
      background_grid(minor = 'none'); p1
    
    p2 <- ggplot(mtcars, aes(mpg, qsec)) + 
      geom_line(colour = 'green') + 
      ylim(14, 25) +
      background_grid(minor = 'none'); p2
    
    # convert to grob objects
    g1 <- ggplotGrob(p1)
    g2 <- ggplotGrob(p2)
    

    Function to add appropriate border segments for each plot grob, where:

    • grob is the grob object created via ggplotGrob;
    • sides is a character string containing any combination of "t" / "l" / "b" / "r" in any order to indicate the desired sides for border placement;
    • col is the desired border colour (defaults to red);
    • ... is for any other parameters to be passed to gpar() in segmentsGrob()

    .

    library(grid)
    
    add.segments <- function(grob, sides = "tlbr", col = "red", ...){
    
      # get extent of gtable cells to be surrounded by border
      panel.coords <- g1[["layout"]][g1[["layout"]][["name"]] == "panel", ]
      t <- if(grepl("t", sides)) panel.coords[["t"]] else 1
      b <- if(grepl("b", sides)) panel.coords[["b"]] else length(grob[["heights"]])
      l <- if(grepl("l", sides)) panel.coords[["l"]] else 1
      r <- if(grepl("r", sides)) panel.coords[["r"]] else length(grob[["widths"]])
    
      # define border coordinates, & filter for the desired border sides
      coords <- data.frame(direction = c("t", "b", "l", "r"),
                           x0 = c(0, 0, 0, 1), y0 = c(1, 0, 0, 0),
                           x1 = c(1, 1, 0, 1), y1 = c(1, 0, 1, 1),
                           stringsAsFactors = FALSE)
      coords <- coords[sapply(coords$direction, grepl, sides), ]
    
      # add desired border sides as segments to the grob at specific gtable cells
      grob <- gtable_add_grob(x = grob,
                              grobs = segmentsGrob(
                                x0 = coords[["x0"]], y0 = coords[["y0"]],
                                x1 = coords[["x1"]], y1 = coords[["y1"]],
                                gp = gpar(col = col, ...)
                              ),
                              t = t, l = l, b = b, r = r,
                              clip = "off", name = "segments")
    
      return(grob)
    }
    

    Usage:

    plot_grid(add.segments(g1, "tlr"), 
              add.segments(g2, "lbr"), 
              ncol = 1, align = "v")
    

    plot

    Another example, for aligning two plots horizontally (okay, there's no point to align these particular plots side by side, but you get the idea):

    plot_grid(add.segments(g2, "tlb", col = "gold2", lty = 2, lwd = 5), 
              add.segments(g1, "trb", col = "gold2", lty = 2, lwd = 5), 
              nrow = 1, align = "h")
    

    plot 2