15

I am trying to create 3 plots where all the panels should have the same size, so I thought the solution might be facet_wrap. My problem is, I do don't want the same number of graphs in each

df <- data.frame(group=c(1,1,2,2,2,3,3),
                 name=c('a','b','c','d','e','f','g'),
                 x=c(1,2,3,4,5,6,7),
                 y=c(2,3,4,5,6,7,8))
ggplot(df, aes(x,y)) + geom_point() + facet_wrap(~ name, ncol=3)

The result combines the graphs into a sequential order but I would like to group them by my group column.

So the result I expect is

a b _
c d e 
f g _

But what I get is

a b c
d e f
g _ _

Has anyone a suggestion to create a graph like this, maybe by adding empty graphs? I found a other thread Force facet_wrap to fill bottom row... But I couldn't get it to run in my case.

Community
  • 1
  • 1
drmariod
  • 11,106
  • 16
  • 64
  • 110

2 Answers2

30

facet_wrap simply puts one plot after the other and inserts "line breaks" after the appropriate number of plots. But there is also facet_grid that lets you specify a row and a column index. Your row index is group and I now add a column index as follows:

cols<-c(a=1,c=1,f=1,b=2,d=2,g=2,e=3)
df$col <- as.factor(cols[as.character(df$name)])

Now you can plot the facet grid with

ggplot(df, aes(x,y)) + geom_point() + facet_grid(group~col)

Of course, depending on what your problem is, you will have to think of an appropriate way to set the column indices.

This is the plot:

enter image description here

Edit:

In reaction to your comment and drawing from this answer, I created a second solution. It uses gridExtra and manipulates the ggplot grob directly. I think it gives the desired result, but in the form I present it, it is "manual work". This solution uses facet_wrap instead of facet_grid.

First, I add a "place holder levels" to the variable name, which will ensure that the empty facets are created and then create the plot:

df$name2 <- factor(df$name,levels=c('a','b','','c','d','e','f','g',' '))
p <- ggplot(df, aes(x,y)) + geom_point() + facet_wrap(~name2,ncol=3,drop=FALSE)

drop=FALSE is what causes ggplot to draw the empty facets. This plot differs from my first solution only in how the facets are labelled. Now to the tricky part:

library(gridExtra)
g <- ggplotGrob(p)
## remove empty panels
g$grobs[names(g$grobs) %in% c("panel3", "panel9", "strip_t3", "strip_t9")] <- NULL
## remove them from the layout
g$layout <- g$layout[!(g$layout$name %in% c("panel-3", "panel-9", 
                                           "strip_t-3", "strip_t-9")),]
## move axis closer to panel
g$layout[g$layout$name == "axis_b-9", c("t", "b")] = c(9,9)

This does basically what the comments say. If you work with another set of plots, look at the output of names(g$grobs) and g$layout$name to figure out, which elements have to be removed.

Now you can create the plot with

grid.newpage()
grid.draw(g)

enter image description here

Edit 2:

For newer versions of ggplot2 the above solution does not work. Unfortunately, I don't know with which version this started, but it definitely does not work with version 2.2.1.

The part that must be changed is the modification of the grob:

g <- ggplotGrob(p)
# get the grobs that must be removed
rm_grobs <- g$layout$name %in% c("panel-1-3", "panel-3-3", "strip-t-3-1", "strip-t-3-3")
# remove grobs
g$grobs[rm_grobs] <- NULL
g$layout <- g$layout[!rm_grobs, ]
## move axis closer to panel
g$layout[g$layout$name == "axis-b-3-3", c("t", "b")] = c(14.5, 14.5)
grid.newpage()
grid.draw(g)

The main changes are that g$grobs is no longer a named list and that the names of the grobs have changed. Note the panels are labelled as "panel-row-col" while for grey bars it is "strip-t-col-row".

enter image description here

Stibu
  • 15,166
  • 6
  • 57
  • 71
1

Small addendum to @Stibu's excellent answer:

Unfortunately, panel names in g$grobs are not always consistent with "panel-row-col" when using facet_wrap (while strip names are and everything works fine with facet_grid).

Here is a function based on @Stibu's answer that I use to avoid having to remember any details:

remove_facets <- function(plot, layout) {
  layout <- strsplit(layout, split = '\n')[[1]]
  layout <- lapply(layout, trimws)
  layout <- matrix(unlist(sapply(layout, strsplit, "")),
                   nrow = length(layout), byrow = T)
  layout <- which(layout == "#", arr.ind = TRUE)
  prm <- apply(layout,1,\(x) {
    c(glue::glue("panel-{x[1]}-{x[2]}"),
      glue::glue("strip-t-{x[2]}-{x[1]}"))
  })
  # https://stackoverflow.com/a/30372692/1296582
  g <- ggplot2::ggplotGrob(plot)
  rm_grobs <- g$layout$name %in% prm
  g$grobs[rm_grobs] <- NULL
  g$layout <- g$layout[!rm_grobs, ]
  ggpubr::as_ggplot(g)
}

where plot is a ggplot and layout is a character string representing the faceted plot, where the facets to remove are indicated by #.

Here is a simple example:

(p1 <- ggplot(mtcars, aes(mpg, disp)) +
  geom_point() +
  facet_grid(rows=vars(cyl), cols=vars(gear)))

# any character can indicate a facet, '#' indicates a facet that will be removed
a <- c("a#a
        ##a
        a##")

remove_facets(p1, a)

Example plot. Left the original, right with some facets removed.

As said, the function is not robust:

  • works best with facet_grid and may require manual tweaking with facet_wrap
  • expects strips on top for facet_wrap
  • the return type is ggpubr::as_ggplot but it is not exactly speaking a ggplot; depending on what you do with the plot, returning or directly plotting as grob may be preferable
  • this pseudo-grob is better handled by cowplot than by patchwork when combining with other plots
jkd
  • 1,327
  • 14
  • 29