Поиск обходного пути для файла gtable_add_grob, нарушенного ggplot 2.2.0

В графиках с несколькими фасетными переменными ggplot2 повторяет метку фасета для "внешней" переменной, вместо того, чтобы иметь единую полосу фасетирования по всем уровням "внутренней" переменной. У меня есть код, который я использовал, чтобы покрыть повторяющиеся метки внешних фасетов одной полосой фасета, используя gtable_add_grob из пакета gtable.

К сожалению, этот код больше не работает с ggplot2 2.2.0 из-за изменений в структуре grob фасетных полос. В частности, в предыдущих версиях ggplot2 каждая строка меток с метками получила свой собственный набор гномов. Тем не менее, в версии 2.2.0 он выглядит как каждый вертикальный стек меток фасет - это один гун. Это нарушает мой код, и я не уверен, как его исправить.

Вот конкретный пример, взятый из вопроса SO, на который я ответил несколько месяцев назад:

# Data
df = structure(list(location = structure(c(1L, 1L, 1L, 1L, 1L, 1L, 
    1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 
    1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 
    2L, 2L), .Label = c("SF", "SS"), class = "factor"), species = structure(c(1L, 
    1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 
    1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 
    2L, 2L, 2L, 2L, 2L, 2L, 2L), .Label = c("AGR", "LKA"), class = "factor"), 
        position = structure(c(1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 
        2L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 
        1L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 
        2L), .Label = c("top", "bottom"), class = "factor"), density = c(0.41, 
        0.41, 0.43, 0.33, 0.35, 0.43, 0.34, 0.46, 0.32, 0.32, 0.4, 
        0.4, 0.45, 0.34, 0.39, 0.39, 0.31, 0.38, 0.48, 0.3, 0.42, 
        0.34, 0.35, 0.4, 0.38, 0.42, 0.36, 0.34, 0.46, 0.38, 0.36, 
        0.39, 0.38, 0.39, 0.39, 0.39, 0.36, 0.39, 0.51, 0.38)), .Names = c("location", 
    "species", "position", "density"), row.names = c(NA, -40L), class = "data.frame")

# Begin with a regular ggplot with three facet levels
p=ggplot(df, aes("", density)) + 
  geom_boxplot(width=0.7, position=position_dodge(0.7)) + 
  theme_bw() +
  facet_grid(. ~ species + location +  position) +
  theme(panel.margin=unit(0,"lines"),
        strip.background=element_rect(color="grey30", fill="grey90"),
        panel.border=element_rect(color="grey90"),
        axis.ticks.x=element_blank()) +
  labs(x="")

Начнем с графика, имеющего три уровня граней.

введите описание изображения здесь

Теперь мы рассмотрим две верхние полосы фасетов с охватывающими полосами, чтобы у нас не было повторяющихся меток полосы:

pg = ggplotGrob(p)

# Add spanning strip labels for species
pos = c(4,11)    
for (i in 1:2) {
  pg <- gtable_add_grob(pg, 
                        list(rectGrob(gp=gpar(col="grey50", fill="grey90")),
                             textGrob(unique(densityAGRLKA$species)[i], 
                                      gp=gpar(cex=0.8))), t=3,l=pos[i],b=3,r=pos[i]+7,
                        name=c("a","b"))
}

# Add spanning strip labels for location
pos=c(4,7,11,15)
for (i in 1:4) {
    pg = gtable_add_grob(pg, 
                         list(rectGrob(gp = gpar(col="grey50", fill="grey90")),
                              textGrob(rep(unique(densityAGRLKA$location),2)[i], 
                                       gp=gpar(cex=0.8))), t=4,l=pos[i],b=4,r=pos[i]+3, 
                         name = c("c","d"))
}

grid.draw(pg)

Вот как выглядит этот сюжет с ggplot2 2.1.0:

введите описание изображения здесь

Однако, если я попробую тот же код с ggplot2 2.2.0, я получаю исходный график назад, без изменений на метках полосы. Взгляд на структуру гроб первоначального сюжета p подсказывает, почему это происходит. Я вставил в столы гвоздя в нижней части этого вопроса. Чтобы сэкономить место, я включил только строки, связанные с полосами фасета.

Посмотрите на столбец cells, обратите внимание, что в версии 2.1.0 графика первые два числа в каждой строке являются либо 3, 4, либо 5, что указывает на вертикальное положение grob относительно других грызков в сюжете. В приведенном выше коде аргументы t и l для gtable_add_grob устанавливаются в значения 3 или 4, потому что это строки полосы фасетки, которые я хотел бы покрыть с помощью охватывающих полосок.

Теперь посмотрим на столбец cells в версии 2.2.0 сюжета: обратите внимание, что первые два числа всегда равны 6. Также обратите внимание, что полосы фасета состоят из 8 гномов вместо 24 в версии 2.1. 0. В версии 2.2.0 кажется, что каждый стек из трех фасетных меток теперь представляет собой один гроб вместо трех отдельных грызунов. Поэтому, даже если я изменил аргументы t и b в gtable_add_grob до 6, все три полосы фасетов будут покрыты. Вот пример:

pg = ggplotGrob(p)

# Add spanning strip labels for species
pos = c(4,11)    
for (i in 1:2) {
  pg <- gtable_add_grob(pg, 
                        list(rectGrob(gp=gpar(col="grey50", fill="grey90")),
                             textGrob(unique(densityAGRLKA$species)[i], 
                                      gp=gpar(cex=0.8))), t=6,l=pos[i],b=6,r=pos[i]+7,
                        name=c("a","b"))
}

введите описание изображения здесь

Итак, после этого очень затянувшегося введения, вот мой вопрос: как я могу создать остовные полосы гранётов с ggplot2 версии 2.2.0, которые выглядят как те, которые я создал с помощью gtable_add_grob с ggplot2 версии 2.1.0? Я надеюсь, что там будет простая настройка, но если это потребует серьезной операции, хорошо, это тоже хорошо.

ggplot 2.1.0

pg
TableGrob (9 x 19) "layout": 45 grobs
    z         cells       name                                   grob
2   1 ( 3- 3, 4- 4)  strip-top   absoluteGrob[strip.absoluteGrob.147]
3   2 ( 4- 4, 4- 4)  strip-top   absoluteGrob[strip.absoluteGrob.195]
4   3 ( 5- 5, 4- 4)  strip-top   absoluteGrob[strip.absoluteGrob.243]
5   4 ( 3- 3, 6- 6)  strip-top   absoluteGrob[strip.absoluteGrob.153]
6   5 ( 4- 4, 6- 6)  strip-top   absoluteGrob[strip.absoluteGrob.201]
7   6 ( 5- 5, 6- 6)  strip-top   absoluteGrob[strip.absoluteGrob.249]
8   7 ( 3- 3, 8- 8)  strip-top   absoluteGrob[strip.absoluteGrob.159]
9   8 ( 4- 4, 8- 8)  strip-top   absoluteGrob[strip.absoluteGrob.207]
10  9 ( 5- 5, 8- 8)  strip-top   absoluteGrob[strip.absoluteGrob.255]
11 10 ( 3- 3,10-10)  strip-top   absoluteGrob[strip.absoluteGrob.165]
12 11 ( 4- 4,10-10)  strip-top   absoluteGrob[strip.absoluteGrob.213]
13 12 ( 5- 5,10-10)  strip-top   absoluteGrob[strip.absoluteGrob.261]
14 13 ( 3- 3,12-12)  strip-top   absoluteGrob[strip.absoluteGrob.171]
15 14 ( 4- 4,12-12)  strip-top   absoluteGrob[strip.absoluteGrob.219]
16 15 ( 5- 5,12-12)  strip-top   absoluteGrob[strip.absoluteGrob.267]
17 16 ( 3- 3,14-14)  strip-top   absoluteGrob[strip.absoluteGrob.177]
18 17 ( 4- 4,14-14)  strip-top   absoluteGrob[strip.absoluteGrob.225]
19 18 ( 5- 5,14-14)  strip-top   absoluteGrob[strip.absoluteGrob.273]
20 19 ( 3- 3,16-16)  strip-top   absoluteGrob[strip.absoluteGrob.183]
21 20 ( 4- 4,16-16)  strip-top   absoluteGrob[strip.absoluteGrob.231]
22 21 ( 5- 5,16-16)  strip-top   absoluteGrob[strip.absoluteGrob.279]
23 22 ( 3- 3,18-18)  strip-top   absoluteGrob[strip.absoluteGrob.189]
24 23 ( 4- 4,18-18)  strip-top   absoluteGrob[strip.absoluteGrob.237]
25 24 ( 5- 5,18-18)  strip-top   absoluteGrob[strip.absoluteGrob.285]

ggplot2 2.2.0

pg
TableGrob (11 x 21) "layout": 42 grobs
    z         cells       name                                    grob
28  2 ( 6- 6, 4- 4)  strip-t-1                           gtable[strip]
29  2 ( 6- 6, 6- 6)  strip-t-2                           gtable[strip]
30  2 ( 6- 6, 8- 8)  strip-t-3                           gtable[strip]
31  2 ( 6- 6,10-10)  strip-t-4                           gtable[strip]
32  2 ( 6- 6,12-12)  strip-t-5                           gtable[strip]
33  2 ( 6- 6,14-14)  strip-t-6                           gtable[strip]
34  2 ( 6- 6,16-16)  strip-t-7                           gtable[strip]
35  2 ( 6- 6,18-18)  strip-t-8                           gtable[strip]

Ответ 1

В самом деле, ggplot2 v2.2.0 строит комплексный столбец по столбцу, причем каждый столбец представляет собой один гун. Это можно проверить, извлекая одну полоску, а затем исследуя ее структуру. Используя ваш сюжет:

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

# Your data
df = structure(list(location = structure(c(1L, 1L, 1L, 1L, 1L, 1L, 
 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 
 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 
 2L, 2L), .Label = c("SF", "SS"), class = "factor"), species = structure(c(1L, 
 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 
 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 
 2L, 2L, 2L, 2L, 2L, 2L, 2L), .Label = c("AGR", "LKA"), class = "factor"), 
    position = structure(c(1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 
    2L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 
    1L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 
    2L), .Label = c("top", "bottom"), class = "factor"), density = c(0.41, 
    0.41, 0.43, 0.33, 0.35, 0.43, 0.34, 0.46, 0.32, 0.32, 0.4, 
    0.4, 0.45, 0.34, 0.39, 0.39, 0.31, 0.38, 0.48, 0.3, 0.42, 
    0.34, 0.35, 0.4, 0.38, 0.42, 0.36, 0.34, 0.46, 0.38, 0.36, 
    0.39, 0.38, 0.39, 0.39, 0.39, 0.36, 0.39, 0.51, 0.38)), .Names = c("location", 
   "species", "position", "density"), row.names = c(NA, -40L), class = "data.frame")

# Your ggplot with three facet levels
p=ggplot(df, aes("", density)) + 
  geom_boxplot(width=0.7, position=position_dodge(0.7)) + 
  theme_bw() +
  facet_grid(. ~ species + location +  position) +
  theme(panel.spacing=unit(0,"lines"),
     strip.background=element_rect(color="grey30", fill="grey90"),
     panel.border=element_rect(color="grey90"),
     axis.ticks.x=element_blank()) +
  labs(x="")

# Get the ggplot grob
pg = ggplotGrob(p)

# Get the left most strip
index = which(pg$layout$name == "strip-t-1")
strip1 = pg$grobs[[index]]

# Draw the strip
grid.newpage()
grid.draw(strip1)

# Examine its layout
strip1$layout
gtable_show_layout(strip1)

Один из грубых способов получить метки внутренней полосы на внутренней этикетке - это построить полоску с нуля:

# Get the strips, as a list, from the original plot
strip = list()
for(i in 1:8) {
   index = which(pg$layout$name == paste0("strip-t-",i))
   strip[[i]] = pg$grobs[[index]]
}

# Construct gtable to contain the new strip
newStrip  = gtable(widths = unit(rep(1, 8), "null"), heights = strip[[1]]$heights)

## Populate the gtable    
# Top row
for(i in 1:2) {
   newStrip = gtable_add_grob(newStrip, strip[[4*i-3]][1], 
           t = 1, l = 4*i-3, r = 4*i)
}

# Middle row
for(i in 1:4){
   newStrip = gtable_add_grob(newStrip, strip[[2*i-1]][2], 
         t = 2, l = 2*i-1, r = 2*i)
}

# Bottom row
for(i in 1:8) {
   newStrip = gtable_add_grob(newStrip, strip[[i]][3], 
       t = 3, l = i)
}

# Put the strip into the plot 
# (It could be better to remove the original strip. 
# In this case, with a coloured background, it doesn't matter)
pgNew = gtable_add_grob(pg, newStrip, t = 6, l = 4, r = 18)

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

ИЛИ с помощью векторизованного gtable_add_grob (см. комментарии):

pg = ggplotGrob(p)

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

# Construct gtable to contain the new strip
newStrip  = gtable(widths = unit(rep(1, 8), "null"), heights = strip[[1]]$heights)

## Populate the gtable    
# Top row
cols = seq(1, by = 4, length.out = 2)
newStrip = gtable_add_grob(newStrip, lapply(strip[cols], `[`, 1), t = 1, l = cols, r = cols + 3)

# Middle row
cols = seq(1, by = 2, length.out = 4)
newStrip = gtable_add_grob(newStrip, lapply(strip[cols], `[`, 2), t = 2, l = cols, r = cols + 1)

# Bottom row
newStrip = gtable_add_grob(newStrip, lapply(strip, `[`, 3), t = 3, l = 1:8)

# Put the strip into the plot
pgNew = gtable_add_grob(pg, newStrip, t = 6, l = 4, r = 18)

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

введите описание изображения здесь

Ответ 2

Эта попытка берет оригинальный ggplot, извлекает некоторую информацию, затем конструирует новый гроб, содержащий перекрывающиеся полосы. Функция не очень красивая, но она работает... пока. Для этого требуется plyr.

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


df = structure(list(location = structure(c(1L, 1L, 1L, 1L, 1L, 1L, 
    1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 
    1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 
    2L, 2L), .Label = c("SF", "SS"), class = "factor"), species = structure(c(1L, 
    1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 
    1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 
    2L, 2L, 2L, 2L, 2L, 2L, 2L), .Label = c("AGR", "LKA"), class = "factor"), 
        position = structure(c(1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 
        2L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 
        1L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 
        2L), .Label = c("top", "bottom"), class = "factor"), density = c(0.41, 
        0.41, 0.43, 0.33, 0.35, 0.43, 0.34, 0.46, 0.32, 0.32, 0.4, 
        0.4, 0.45, 0.34, 0.39, 0.39, 0.31, 0.38, 0.48, 0.3, 0.42, 
        0.34, 0.35, 0.4, 0.38, 0.42, 0.36, 0.34, 0.46, 0.38, 0.36, 
        0.39, 0.38, 0.39, 0.39, 0.39, 0.36, 0.39, 0.51, 0.38)), .Names = c("location", 
    "species", "position", "density"), row.names = c(NA, -40L), class = "data.frame")

# Begin with a regular ggplot with three facet levels
p=ggplot(df, aes("", density)) + 
  geom_boxplot(width=0.7, position=position_dodge(0.7)) + 
  theme_bw() +
  facet_grid(. ~ species + location + position) +
  theme(panel.spacing=unit(0,"lines"),
        strip.background=element_rect(color="grey30", fill="grey90"),
        panel.border=element_rect(color="grey90"),
        axis.ticks.x=element_blank()) +
  labs(x="")

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

# Get the ggplot grob
g = ggplotGrob(plot)

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

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

# Number of rows
NumberOfRows = 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", NumberOfRows)
map = vector("list", NumberOfRows)
for(i in 1:NumberOfRows) {

  for(j in 1:NumberOfStrips) {
   Nlabel[[i]][j] = getGrob(grid.force(strip[[j]][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(widths = unit.c(rep(unit.c(unit(1, "null"), PanelSpacing), NumberOfStrips-1), unit(1, "null")), 
                   heights = strip[[1]]$heights)

## Populate the gtable  
seqLeft = list()
for(i in 1:NumberOfRows) {  
   Left = which(map[[i]] == TRUE)
   seqLeft[[i]] = if((i-1) < 1) 2*Left - 1 else sort(unique(c(seqLeft[[i-1]], 2*Left - 1))) 
   seqRight = c(seqLeft[[i]][-1] -2, (2*NumberOfStrips-1))
   newStrip = gtable_add_grob(newStrip, lapply(strip[(seqLeft[[i]]+1)/2], `[`, i), t = i, l = seqLeft[[i]], r = seqRight)
}

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

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

return(pgNew)
}

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

введите описание изображения здесь

Вероятно, было бы не слишком сложно сломать функцию, но я попробовал ее на данных, где последовательность строк не так уж и четная.

p1 = ggplot(mtcars, aes("", hp)) + 
  geom_boxplot(width=0.7, position=position_dodge(0.7)) + 
  theme_bw() +
  facet_grid(. ~ vs + am + carb, labeller = label_both) +
  theme(panel.spacing=unit(0.2,"lines"),
        strip.background=element_rect(color="grey30", fill="grey90"),
        panel.border=element_rect(color="grey90"),
        axis.ticks.x=element_blank()) +
  labs(x="")

  grid.draw(OverlappingStripLabels(p1))

p2 = ggplot(mtcars, aes("", hp)) + 
  geom_boxplot(width=0.7, position=position_dodge(0.7)) + 
  theme_bw() +
  facet_grid(. ~ vs + carb +  am, labeller = label_both) +
  theme(panel.spacing=unit(0.2,"lines"),
        strip.background=element_rect(color="grey30", fill="grey90"),
        panel.border=element_rect(color="grey90"),
        axis.ticks.x=element_blank()) +
  labs(x="")

 grid.draw(OverlappingStripLabels(p2))