11

I have the following code:

label_rev <- function(labels, multi_line = TRUE, sep = ": ") {
     label_both(rev(labels), multi_line = multi_line, sep = sep)
  }
require(ggplot2)
p <- ggplot(data = mtcars, aes(wt, mpg)) + geom_point()
p + facet_grid(vs + cyl ~ gear, labeller = label_rev)

I get the following figure: enter image description here

Here is my dilemma: I would like the outerstrip of vs:0 to be only one panel encompassing the three facets (cyl:4, 6, 8) and the outstrip of vs:1 to be one panel encompassing the three facets (cyl:4, 6, 8).

Is it possible to do this using ggplot2?

Thanks again in advance for any help!

6
  • Part 1 is a duplicate of order nested facet labels. Part 2 is something I've also wanted for a long time... Commented Sep 30, 2016 at 23:08
  • The dupe example is amazingly similar! Both this question and the dupe make the unconventional choice of aes(mpg, wt) instead of the more common aes(wt, mpg). The dupe uses cyl + am ~ vs for facetting, while you use vs + cyl ~ gear. It's about the closest I've seen to a identical dupe that isn't just a repost. Commented Sep 30, 2016 at 23:12
  • Anyway, since the ordering question is answered, I'd recommend editing to just ask Part 2. I'm pretty sure there's a dupe out there for it as well, but I can't find it right now and it's probably pretty old - possibly cowplot or other recent innovations make it easier. Commented Sep 30, 2016 at 23:14
  • edited to remove the dupe, thanks! I still would like to figure out a way to do the common panel. Commented Sep 30, 2016 at 23:31
  • Spanning facet labels are not built into ggplot (though I wish they were), but they can be created with some extra coding. Take a look at the last example in this SO answer.
    – eipi10
    Commented Oct 1, 2016 at 1:11

3 Answers 3

12

This can now easily be done with facet_nested() from the ggh4x package

library(ggplot2)
library(ggh4x)
p <- ggplot(data = mtcars, aes(wt, mpg)) + geom_point()
p + 
  facet_nested(vs + cyl ~ am + gear, labeller = label_both) +
  theme(panel.spacing = unit(0,"line")) 

Created on 2020-03-25 by the reprex package (v0.3.0)

1
  • This is very nice but I think you pasted in the output from a different piece of code than the one shown. When I run it I don't get shading on the strips or the dark black lines between facets. I do get the nested strips. Commented Jan 8, 2023 at 17:10
6

I took the liberty to edit and generalise the function given here by Sandy Muspratt so that it allows for two-way nested facets, as well as expressions as facet headers if labeller=label_parsed is specified in facet_grid().

library(ggplot2)
library(grid)
library(gtable)
library(plyr)    

## The function to get overlapping strip labels
OverlappingStripLabels = function(plot) {

  # Get the ggplot grob
  pg = ggplotGrob(plot)

  ### Collect some information about the strips from the plot
  # Get a list of strips
  stripr = lapply(grep("strip-r", pg$layout$name), function(x) {pg$grobs[[x]]})

  stript = lapply(grep("strip-t", pg$layout$name), function(x) {pg$grobs[[x]]})

  # Number of strips
  NumberOfStripsr = sum(grepl(pattern = "strip-r", pg$layout$name))
  NumberOfStripst = sum(grepl(pattern = "strip-t", pg$layout$name))

  # Number of columns
  NumberOfCols = length(stripr[[1]])
  NumberOfRows = length(stript[[1]])

  # Panel spacing
  plot_theme <- function(p) {
    plyr::defaults(p$theme, theme_get())
  }
  PanelSpacing = plot_theme(plot)$panel.spacing

  # Map the boundaries of the new strips
  Nlabelr = vector("list", NumberOfCols)
  mapr = vector("list", NumberOfCols)
  for(i in 1:NumberOfCols) {

    for(j in 1:NumberOfStripsr) {
      Nlabelr[[i]][j] = getGrob(grid.force(stripr[[j]]$grobs[[i]]), gPath("GRID.text"), grep = TRUE)$label
    }

    mapr[[i]][1] = TRUE
    for(j in 2:NumberOfStripsr) {
      mapr[[i]][j] = as.character(Nlabelr[[i]][j]) != as.character(Nlabelr[[i]][j-1])#Nlabelr[[i]][j] != Nlabelr[[i]][j-1]
    }
  }

  # Map the boundaries of the new strips
  Nlabelt = vector("list", NumberOfRows)
  mapt = vector("list", NumberOfRows)
  for(i in 1:NumberOfRows) {

    for(j in 1:NumberOfStripst) {
      Nlabelt[[i]][j] = getGrob(grid.force(stript[[j]]$grobs[[i]]), gPath("GRID.text"), grep = TRUE)$label
    }

    mapt[[i]][1] = TRUE
    for(j in 2:NumberOfStripst) {
      mapt[[i]][j] = as.character(Nlabelt[[i]][j]) != as.character(Nlabelt[[i]][j-1])#Nlabelt[[i]][j] != Nlabelt[[i]][j-1]
    }
  }


  ## Construct gtable to contain the new strip
  newStripr  = gtable(heights = unit.c(rep(unit.c(unit(1, "null"), PanelSpacing), NumberOfStripsr-1), unit(1, "null")), 
                     widths = stripr[[1]]$widths)
  ## Populate the gtable  
  seqTop = list()
  for(i in NumberOfCols:1) {  
    Top = which(mapr[[i]] == TRUE)
    seqTop[[i]] = if(i == NumberOfCols) 2*Top - 1 else  sort(unique(c(seqTop[[i+1]], 2*Top - 1)))  
    seqBottom = c(seqTop[[i]][-1] -2, (2*NumberOfStripsr-1))
    newStripr = gtable_add_grob(newStripr, lapply(stripr[(seqTop[[i]]+1)/2], function(x) x[[1]][[i]]), l = i, t = seqTop[[i]], b = seqBottom)
  }

  mapt <- mapt[NumberOfRows:1]
  Nlabelt <- Nlabelt[NumberOfRows:1]
  ## Do the same for top facets
  newStript  = gtable(heights = stript[[1]]$heights,
                      widths = unit.c(rep(unit.c(unit(1, "null"), PanelSpacing), NumberOfStripst-1), unit(1, "null")))
  seqTop = list()
  for(i in NumberOfRows:1) {  
    Top = which(mapt[[i]] == TRUE)
    seqTop[[i]] = if(i == NumberOfRows) 2*Top - 1 else  sort(unique(c(seqTop[[i+1]], 2*Top - 1)))  
    seqBottom = c(seqTop[[i]][-1] -2, (2*NumberOfStripst-1))
    # newStript = gtable_add_grob(newStript, lapply(stript[(seqTop[[i]]+1)/2], function(x) x[[1]][[i]]), l = i, t = seqTop[[i]], b = seqBottom)
    newStript = gtable_add_grob(newStript, lapply(stript[(seqTop[[i]]+1)/2], function(x) x[[1]][[(NumberOfRows:1)[i]]]), t = (NumberOfRows:1)[i], l = seqTop[[i]], r = seqBottom)
  }

  ## Put the strip into the plot
  # Get the locations of the original strips
  posr = subset(pg$layout, grepl("strip-r", pg$layout$name), t:r)
  post = subset(pg$layout, grepl("strip-t", pg$layout$name), t:r)

  ## Use these to position the new strip
  pgNew = gtable_add_grob(pg, newStripr, t = min(posr$t), l = unique(posr$l), b = max(posr$b))
  pgNew = gtable_add_grob(pgNew, newStript, l = min(post$l), r = max(post$r), t=unique(post$t))
  grid.draw(pgNew)

  return(pgNew)
}


# Initial plot
p <- ggplot(data = mtcars, aes(wt, mpg)) + geom_point() +
  facet_grid(vs + cyl ~ am + gear, labeller = label_both) +
  theme_bw() +
  theme(panel.spacing=unit(.2,"lines"),
        strip.background=element_rect(color="grey30", fill="grey90"))

## Draw the plot
grid.newpage()
grid.draw(OverlappingStripLabels(p))

Here is an example: enter image description here

4

Based on this answer, but sufficiently different to warrant an answer of its own. Given a ggplot with multiple facets on the right margin, this answer provides a function, OverlappingStripLabels(), that takes information from the ggplot to reconstruct the strip so that the labels are overlapping. It uses gtable and grid functions to do so.

library(ggplot2)
library(grid)
library(gtable)
library(plyr)

# Initial plot
plot = ggplot(data = mtcars, aes(wt, mpg)) + geom_point() +
   facet_grid(vs + cyl ~ gear, labeller = label_both) + 
   theme_bw() +
   theme(panel.spacing=unit(.2,"lines"),
         strip.background=element_rect(color="grey30", fill="grey90"))


## The function to get overlapping strip labels
OverlappingStripLabels = function(plot) {

# Get the ggplot grob
pg = ggplotGrob(plot)

### Collect some information about the strips from the plot
# Get a list of strips
strip = lapply(grep("strip-r", pg$layout$name), function(x) {pg$grobs[[x]]})

# Number of strips
NumberOfStrips = sum(grepl(pattern = "strip-r", pg$layout$name))

# Number of columns
NumberOfCols = length(strip[[1]])

# Panel spacing
plot_theme <- function(p) {
   plyr::defaults(p$theme, theme_get())
}
PanelSpacing = plot_theme(plot)$panel.spacing

# Map the boundaries of the new strips
Nlabel = vector("list", NumberOfCols)
map = vector("list", NumberOfCols)
for(i in 1:NumberOfCols) {

  for(j in 1:NumberOfStrips) {
   Nlabel[[i]][j] = getGrob(grid.force(strip[[j]]$grobs[[i]]), gPath("GRID.text"), grep = TRUE)$label
  }

map[[i]][1] = TRUE
for(j in 2:NumberOfStrips) {
   map[[i]][j] = Nlabel[[i]][j] != Nlabel[[i]][j-1]
   }
}

## Construct gtable to contain the new strip
newStrip  = gtable(heights = unit.c(rep(unit.c(unit(1, "null"), PanelSpacing), NumberOfStrips-1), unit(1, "null")), 
                   widths = strip[[1]]$widths)

## Populate the gtable  
seqTop = list()
for(i in NumberOfCols:1) {  
   Top = which(map[[i]] == TRUE)
   seqTop[[i]] = if(i == NumberOfCols) 2*Top - 1 else  sort(unique(c(seqTop[[i+1]], 2*Top - 1)))  
   seqBottom = c(seqTop[[i]][-1] -2, (2*NumberOfStrips-1))
   newStrip = gtable_add_grob(newStrip, lapply(strip[(seqTop[[i]]+1)/2], function(x) x[[1]][[i]]), l = i, t = seqTop[[i]], b = seqBottom)
}

## Put the strip into the plot
# Get the locations of the original strips
pos = subset(pg$layout, grepl("strip-r", pg$layout$name), t:r)

## Use these to position the new strip
pgNew = gtable_add_grob(pg, newStrip, t = min(pos$t), l = unique(pos$l), b = max(pos$b))

return(pgNew)
}

## Draw the plot
grid.newpage()
grid.draw(OverlappingStripLabels(plot))

enter image description here

1
  • Great function, thanks ! But would you be able to modify it to allow to things: (1) two way nested facets, and (2) expression as facet labels. i.e. work with something like facet_grid(vs + cyl ~ gear + am, labeller = label_parsed) Commented Jun 7, 2018 at 9:55

Not the answer you're looking for? Browse other questions tagged or ask your own question.