1

I've created a faceted plot, separately for three different groups in my data, like so:

df <- data.frame(x=rep(seq(0.05,1,by=0.05),times=40),
                 y=sample(c('A','B'),20*40,replace=TRUE),
                 id=rep(1:40,each=20),
                 group=c(rep(1,20*12),rep(2,20*12),rep(3,20*16)))

g1 <- ggplot(df[df$group==1,],aes(x,y,group=id))
g1 <- g1 + geom_line()
g1 <- g1 + facet_wrap(~id,ncol=3)

g2 <- ggplot(df[df$group==2,],aes(x,y,group=id))
g2 <- g2 + geom_line()
g2 <- g2 + facet_wrap(~id,ncol=3)

g3 <- ggplot(df[df$group==3,],aes(x,y,group=id))
g3 <- g3 + geom_line()
g3 <- g3 + facet_wrap(~id,ncol=3)

grid.arrange(g1,g2,g3,nrow=1)

which gives me this:

enter image description here

As you can see, the number of facets differs between the three groups which means that the facets in the three columns have different heights. Is there any way to harmonize this height in a non-fragile way (i.e. without me having to manually determine the heights of columns 2 and 3 that gives me facets that look like they have roughly the same height)?

RoyalTS
  • 9,545
  • 12
  • 60
  • 101

2 Answers2

1

enter image description hereHere's a solution with some guidance from this question.

library(ggplot2)
library(gridExtra)


ncol = 3 
df <- data.frame(x=rep(seq(0.05,1,by=0.05),times=40),
                 y=factor(sample(c('A','B'),20*40,replace=TRUE), levels = c("A", "B")),
                 id=rep(1:40,each=20),
                 group=c(rep(1,20*12),rep(2,20*12),rep(3,20*16)))

max_cases <- max(table(unique(df[,c("id", "group")])$group))

# create phantom plots for everything in the containing rectangle to standardize labels
rect_dim <- ceiling(max_cases / ncol) * ncol

plots <- lapply(X=unique(df$group), FUN= function(i){

  df_case <- subset(df, subset= group == i)
  tot_case <- nrow(unique(df_case[,c("id", "group")]))
  # create fill levels to pad the plots
  fill_levels <- unlist(lapply(X=1:(rect_dim - tot_case), function(y){paste0(rep(x=" ", times=y), collapse="")}))
  df_case$id.label <- ordered(df_case$id, levels = c(unique(df_case$id), fill_levels))

  g_case <- ggplot(df_case,aes(x,y,group=id.label)) + 
    geom_line() +
    facet_wrap(~id.label, ncol = ncol, drop=FALSE)

  # whiteout the inner y axis elements to clean it up a bit
  if(i != 1){
    g_case <- g_case + theme(axis.text.y = element_text(color = "white"), 
                             axis.title.y = element_text(color = "white"),
                             axis.ticks.y = element_line(color = "white"))
  }

  g_case <- ggplotGrob(g_case)
  rm_me <- (tot_case:rect_dim)[-1]
  # remove empty panels and layout
  g_case$grobs[names(g_case$grobs) %in% c(paste0("panel", rm_me), paste0("strip_t.", rm_me))] <- NULL
  g_case$layout <- g_case$layout[!(g_case$layout$name %in% c(paste0("panel-", rm_me), paste0("strip_t-", rm_me))),]
  g_case
})

plots$nrow = 1
do.call("grid.arrange", plots)
Community
  • 1
  • 1
Noah
  • 1,404
  • 8
  • 12
  • I just made this fully reproducible. `drop=FALSE` doesn't work because there aren't any `NA` `id`s there to be dropped. And if I understand `facet_grid` right that only allows me to have a plot with three columns (one for each group) and however many rows I need to fit all `id`s. Is that correct? – RoyalTS Feb 18 '14 at 16:18
  • Does the `group` variable have analytical significance, or is it just included to separate the plots into columns? – Noah Feb 18 '14 at 16:30
  • Not sure what you mean with "analytical significance" and why that would make a difference. These are data from an experiment and the `group`s are really different treatments. But for purposes of the plot the groups are simply used to separate the `id`s into three columns – RoyalTS Feb 18 '14 at 16:43
  • By "analytical significance", I just mean that it's actually a factor in your experiment. Sounds like "yes" if the groups are different treatments. Basically you want to preserve scale in the x and y directions. Set `nrow` instead of `ncol` and add a `widths` argument to `grid.arrange`. Edited my solution above. – Noah Feb 18 '14 at 17:14
  • That'll work but will result in four sub-columns for the third group. I'd much rather have three sub-columns each and get the alignment by adding free space at the bottom. Perhaps I should have been more precise in my original question. – RoyalTS Feb 18 '14 at 17:43
  • The `facet` functions won't do that by themselves, unfortunately. You'd need to add a spacing block below the shorter arrays in `grid`. Doable, but a pain. The alternative would be to implement your faceting and columning solely in `grid`, which also has its downsides. If you're not averse to some color, you could change your approach: `ggplot(df,aes(x,y,group=id, color = factor(group))) + geom_line() + facet_wrap(~id)`? – Noah Feb 18 '14 at 18:14
  • Scratch that. Here's a solution that should scale to most data. It involves editing the `grobs`. – Noah Feb 18 '14 at 21:31
  • Fantastic work. Thanks for going to all this effort! – RoyalTS Feb 18 '14 at 21:57
  • Note: There seems to have been a change to `gridExtra` that means `"strip_t."` in the above should now be `"strip_t"`. – RoyalTS Mar 02 '16 at 13:31
1

It's a bit messy, but you can massage the gtables to have the same number of rows, and align them. Further refinement would locate the rows corresponding to plot panels, rather than assume that all plots have the same row sequence of panel - axes - etc.

library(gtable)

cbind_top = function(...){
  pl <- list(...)
  ## test that only passing plots
  stopifnot(do.call(all, lapply(pl, inherits, "gg")))
  gl <- lapply(pl, ggplotGrob)
  nrows <- sapply(gl, function(x) length(x$heights))
  tallest <- max(nrows)
  add_dummy <- function(x, n){
    if(n == 0) return(x)
    gtable_add_rows(x, rep(unit(0, "mm"), n), nrow(x)-2)
  }
  gl <- mapply(add_dummy, x=gl, n=tallest - nrows)

  compare_unit <- function(u1,u2){
    n <- length(u1)
    stopifnot(length(u2) == n)
    null1 <- sapply(u1, attr, "unit")
    null2 <- sapply(u2, attr, "unit")
    null12 <- null1 == "null" | null2 == "null"
    both <- grid::unit.pmax(u1, u2)
    both[null12] <- rep(list(unit(1,"null")), sum(null12))
    both
  }

  bind2 <- function(x,y){
    y$layout$l <- y$layout$l + ncol(x)
    y$layout$r <- y$layout$r + ncol(x)
    x$layout <- rbind(x$layout, y$layout)
    x$widths <- gtable:::insert.unit(x$widths, y$widths)
    x$colnames <- c(x$colnames, y$colnames)
    x$heights <- compare_unit(x$heights, y$heights)
    x$grobs <- append(x$grobs, y$grobs)
    x
  }
  combined <- Reduce(bind2, gl[-1], gl[[1]])

  grid::grid.newpage()
  grid::grid.draw(combined)
}

cbind_top(g1,g2,g3)
baptiste
  • 75,767
  • 19
  • 198
  • 294
  • This is going in the right direction but the facets in the third column don't come out equally spaced (presumably because of the x-axes for the other two columns take up that space). – RoyalTS Feb 18 '14 at 17:41
  • the axes do need special care with this approach, it would take more time than I can spend to get it right. – baptiste Feb 18 '14 at 17:52