Cowplot sizing issue with a 3 x 3 grid plot of rasters with differing parameters for each cell

55 views Asked by At

I have been trying to solve a sizing issue with 3x3 grid of plots with ggplot, cowplot and tidyterra. The plots are spatial rasters, that need a shared legend for each row, a title in the first row, and a y-label in the first column.

I think there is a partial solution to my issue here, referring to the legend: R: Aligning/Sizing for plot_grid in cowplot?

A comment in the link above by the developer of cowplot, Claus Wilke, addresses the legend incorporation (but currently has a broken link), so following this should help with the legend https://wilkelab.org/cowplot/articles/shared_legends.html

But the added difficulty of the y-label and titles still gives me trouble with the sizing.

So in my reproducible example below 2 levels of plot_grid are needed. But note that I need the plots to remain very close to each other (for space limitations and publication purposes), so I reduce the margins to zero.

I started with including the legend within the last column of plots, but looking at Chris's comment, I now have tried to use his approach on this (adding it as object in the plot_grid rather than keeping the legend in the plots for the last column) but I think I get the same issue with random sizing when trying to keep the plots tightly close to each other.

Here is my reproducible example of only a 2 x 3 grid (the last row for my 3x3 is just another addition like the 2nd row, which is not a problem in itself). Not very elegant, but each of the three plots per row need slightly different parameters.

library(terra)
library(ggplot2)
library(cowplot)
library(tidyterra)

#get a sample raster
f <- system.file("ex/elev.tif", package="terra")
r <- terra::rast(f)


#plots for first row, will need titles for all plots, and y-label on first column plot, aand extract legend for last column plot

p <- ggplot2::ggplot()+
  tidyterra::geom_spatraster(data=r)+
  ggplot2::ggtitle("title")+
  ggplot2::ylab("Label")+
  ggplot2::theme(
    plot.margin = ggplot2::margin(0,0,0,0, "cm"),
    
  axis.text= ggplot2::element_blank(), axis.ticks= ggplot2::element_blank(),
  text = ggplot2::element_text(size=20))

#extracting legend
legend <- cowplot::get_legend(
  # create some space to the left of the legend
  p + ggplot2::theme(legend.box.margin = ggplot2::margin(0, 0, 0, 12))
)


p2 <- ggplot2::ggplot()+
  tidyterra::geom_spatraster(data=r)+
  ggplot2::ggtitle("title")+
  ggplot2::theme(legend.position = "none")+
  ggplot2::theme(
    plot.margin = ggplot2::margin(0,0,0,0, "cm"),
    
  axis.text= ggplot2::element_blank(), axis.ticks= ggplot2::element_blank(),
  text = ggplot2::element_text(size=20))

p3 <-ggplot2::ggplot()+
  tidyterra::geom_spatraster(data=r)+
  ggplot2::ggtitle("title")+
  ggplot2::theme(legend.position = "none")+
  ggplot2::theme(
    plot.margin = ggplot2::margin(0,0,0,0, "cm"),
    
  axis.text= ggplot2::element_blank(), axis.ticks= ggplot2::element_blank(),
  text = ggplot2::element_text(size=20))


# add the legend to the row we made earlier. Give it one-third of 
# the width of one plot (via rel_widths).
set1<-cowplot::plot_grid(p+ggplot2::theme(legend.position = "none"), p2, p3, legend,  axis = "bt", align = 'vh',
    rel_widths = c(1,1,1,0.35),#align = 'vh',
                        nrow = 1)

#plots for Second row, no titles, y-label on first column plot, and extract legend for last column plot
p4 <- ggplot2::ggplot()+
  tidyterra::geom_spatraster(data=r)+
  ggplot2::ylab("Label")+
  ggplot2::theme(
    plot.margin = ggplot2::margin(0,0,0,0, "cm"),
    
  axis.text= ggplot2::element_blank(), axis.ticks= ggplot2::element_blank(),
  text = ggplot2::element_text(size=20))

legend2 <- cowplot::get_legend(
  # create some space to the left of the legend
  p4 + ggplot2::theme(legend.box.margin = ggplot2::margin(0, 0, 0, 12))
)


p5 <- ggplot2::ggplot()+
  tidyterra::geom_spatraster(data=r)+
  ggplot2::theme(legend.position = "none")+
  ggplot2::theme(
    plot.margin = ggplot2::margin(0,0,0,0, "cm"),
  axis.text= ggplot2::element_blank(), axis.ticks= ggplot2::element_blank())

p6 <-ggplot2::ggplot()+
  tidyterra::geom_spatraster(data=r)+
  ggplot2::theme(legend.position = "none")+
  ggplot2::theme(
    plot.margin = ggplot2::margin(0,0,0,0, "cm"),
    
  axis.text= ggplot2::element_blank(), axis.ticks= ggplot2::element_blank())

set2<-cowplot::plot_grid( p4+ggplot2::theme(legend.position = "none"), p5, p6, legend2, axis = "bt", align = 'vh',
nrow = 1,  rel_widths = c(1,1,1,0.35))

# add the legend to the row we made earlier. Give it one-third of 
# the width of one plot (via rel_widths).
#set2<-cowplot::plot_grid(p4, p5, p6, legend2, nrow = 1)

plotset<-cowplot::plot_grid(set1, set2, nrow = 2)
ggplot2::ggsave(plotset, 
                filename = "testplot.png", 
                path = "plots/", 
                units = "in", width = 21, height = 18, dpi = 300, bg = "white")

I have attempted modifying the width and height in the ggsave and the closer I get to one nice grid, some other plot is off. If I increase the width or heigth too much, the plots start to spread out too much, and there is too much white space in between.... or even the legend is very spread out too.

I have previously attempted to play with rel_with and rel_height but never get them right either, same as with width and height in the ggsave. I have tried this with all the plots (eg. increase rel_width like c(1.15, 1, 1.25) but again for hours, a not getting anywhere, and feels like eyeballing it too much, rather than a consistent solution).

If I try align = 'vh' as suggested here, and added axis = "bt to avoid warnings, but nothing changes: How to resize plots to same size in a grid of multiple plots

I honestly have spent hours (if not days) trying to size them, but nothing gets me to the perfect consistent sizing (unless I remove the y-label and titles, as per the example below). Many question/solutions do not make reference to my specific case of y-label in 1 column and titles in 1 row only.

What I get so far with the above code: enter image description here

Note that the first column plots are off in both rows. And the space between plots in the first row is also off and even different (not really sure why the space between col1 and col2 differs with the space between col2 and col3).

this is an example of what I get without the y-labels and titles, and what I want in matters of sizing (still with a tad of blank space between rows, but negligible at this point of my struggle): enter image description here

I don't think saving the png with no titles and y-labels and then adding them with an image editor is the way to go. I hope there is way to do this in R programmatically, but I just haven't figured it out or found it. Any help or guidance would be greatly appreciated.

Cheers!

1

There are 1 answers

2
stefan On BEST ANSWER

As always when it comes to combining multiple plots one might consider patchwork as an option.

library(terra)
library(ggplot2)
library(patchwork)
library(tidyterra)

f <- system.file("ex/elev.tif", package = "terra")
r <- terra::rast(f)

p <- ggplot2::ggplot() +
  tidyterra::geom_spatraster(data = r) +
  ggplot2::ggtitle("title") +
  ggplot2::ylab("Label") +
  ggplot2::theme(
    plot.margin = ggplot2::margin(0, 0, 0, 0, "cm"),
    axis.text = ggplot2::element_blank(), 
    axis.ticks = ggplot2::element_blank(),
    text = ggplot2::element_text(size = 20)
  )

library(patchwork)

patch1 <- list(p, p, p) |> 
  # Remove the y axis title and the plot title except for the first plot
  purrr::imap(\(x, y) if (!y == 1) x + labs(y = NULL) else x) |> 
  wrap_plots(guides = "collect")

patch2 <- list(p, p, p) |> 
  purrr::imap(\(x, y) if (!y == 1) x + labs(y = NULL) else x) |> 
  wrap_plots(guides = "collect") &
  # Get rif of the titles
  labs(title = NULL)

patch1 / patch2

enter image description here