将图例移到ggplot2中刻面图的空面上

Z.L*_*Lin 45 r ggplot2

考虑以下情节:

library(ggplot2)

p <- ggplot(diamonds, 
            aes(x = carat, fill = cut)) +
  geom_density(position = "stack") +
  facet_wrap(~ color)
Run Code Online (Sandbox Code Playgroud)

带注释的facet_wrap图

facet_wrap函数将一系列多面板包装nrow成行和ncol列的大致矩形显示.但是,根据数据的不同,实际的面板数量通常只有几个面板nrow * ncol,这会在图中留下一大块浪费的空间.

如果情节包含图例,则情况会恶化,因为现在我们因图例而浪费的空间更多,无论是在右侧(默认图例位置)还是其他三个方向之一.

为了节省空间,我想将图例移到由未填充的构面创建的空间中.

以下作为一种节省空间的措施,但图例固定在绘图区域的一角,可能会在一侧留下大量空间,从而产生不平衡的外观:

p +
  theme(legend.position = c(1, 0),
        legend.justification = c(1, 0))
Run Code Online (Sandbox Code Playgroud)

传说停泊在一个角落

通过手动调整legend.position/ legend.justification值将图例移向空白区域的中心是一个试验和错误的问题,如果有一个多面图可以处理,则难以扩展.

总之,我想要一个方法:

  1. 将切面绘图的图例移动到由于空面创建的空间中.
  2. 结果是一个相当漂亮的情节.
  3. 很容易自动处理许多情节.

对我来说,这是一个反复出现的用例,我决定将其与我的工作解决方案一起发布,以防其他人发现它有用.我还没有看到在Stack Overflow上的其他地方询问/回答这个场景.如果有人,请发表评论,我很乐意回答,或者将其标记为重复,视具体情况而定.

Z.L*_*Lin 43

以下是我为前一个关于利用空面板空间的问题所写的答案的扩展,但我认为它足以保证自己的空间.

本质上,我编写了一个函数,它接受转换的ggplot/grob对象ggplotGrob(),如果它不是一个,则将其转换为grob,并挖掘底层的grob以将图例grob移动到对应于空白空间的单元格中.

功能:

library(gtable)
library(cowplot)

shift_legend <- function(p){

  # check if p is a valid object
  if(!"gtable" %in% class(p)){
    if("ggplot" %in% class(p)){
      gp <- ggplotGrob(p) # convert to grob
    } else {
      message("This is neither a ggplot object nor a grob generated from ggplotGrob. Returning original plot.")
      return(p)
    }
  } else {
    gp <- p
  }

  # check for unfilled facet panels
  facet.panels <- grep("^panel", gp[["layout"]][["name"]])
  empty.facet.panels <- sapply(facet.panels, function(i) "zeroGrob" %in% class(gp[["grobs"]][[i]]))
  empty.facet.panels <- facet.panels[empty.facet.panels]
  if(length(empty.facet.panels) == 0){
    message("There are no unfilled facet panels to shift legend into. Returning original plot.")
    return(p)
  }

  # establish extent of unfilled facet panels (including any axis cells in between)
  empty.facet.panels <- gp[["layout"]][empty.facet.panels, ]
  empty.facet.panels <- list(min(empty.facet.panels[["t"]]), min(empty.facet.panels[["l"]]),
                             max(empty.facet.panels[["b"]]), max(empty.facet.panels[["r"]]))
  names(empty.facet.panels) <- c("t", "l", "b", "r")

  # extract legend & copy over to location of unfilled facet panels
  guide.grob <- which(gp[["layout"]][["name"]] == "guide-box")
  if(length(guide.grob) == 0){
    message("There is no legend present. Returning original plot.")
    return(p)
  }
  gp <- gtable_add_grob(x = gp,
                        grobs = gp[["grobs"]][[guide.grob]],
                        t = empty.facet.panels[["t"]],
                        l = empty.facet.panels[["l"]],
                        b = empty.facet.panels[["b"]],
                        r = empty.facet.panels[["r"]],
                        name = "new-guide-box")

  # squash the original guide box's row / column (whichever applicable)
  # & empty its cell
  guide.grob <- gp[["layout"]][guide.grob, ]
  if(guide.grob[["l"]] == guide.grob[["r"]]){
    gp <- gtable_squash_cols(gp, cols = guide.grob[["l"]])
  }
  if(guide.grob[["t"]] == guide.grob[["b"]]){
    gp <- gtable_squash_rows(gp, rows = guide.grob[["t"]])
  }
  gp <- gtable_remove_grobs(gp, "guide-box")

  return(gp)
}
Run Code Online (Sandbox Code Playgroud)

结果:

library(grid)

grid.draw(shift_legend(p))
Run Code Online (Sandbox Code Playgroud)

p的垂直图例结果

如果我们利用空白区域的方向水平排列图例,则可以更好地查找结果:

p.new <- p +
  guides(fill = guide_legend(title.position = "top",
                             label.position = "bottom",
                             nrow = 1)) +
  theme(legend.direction = "horizontal")
grid.draw(shift_legend(p.new))
Run Code Online (Sandbox Code Playgroud)

p.new的水平图例结果

其他一些例子:

# example 1: 1 empty panel, 1 vertical legend
p1 <- ggplot(economics_long, 
             aes(date, value, color = variable)) +
  geom_line() +
  facet_wrap(~ variable, 
             scales = "free_y", nrow = 2, 
             strip.position = "bottom") +
  theme(strip.background = element_blank(), 
        strip.placement = "outside")
grid.draw(shift_legend(p1))

# example 2: 2 empty panels (vertically aligned) & 2 vertical legends side by side
p2 <- ggplot(mpg,
             aes(x = displ, y = hwy, color = fl, shape = factor(cyl))) +
  geom_point(size = 3) +
  facet_wrap(~ class, dir = "v") +
  theme(legend.box = "horizontal")
grid.draw(shift_legend(p2))

# example 3: facets in polar coordinates
p3 <- ggplot(mtcars, 
             aes(x = factor(1), fill = factor(cyl))) +
  geom_bar(width = 1, position = "fill") + 
  facet_wrap(~ gear, nrow = 2) +
  coord_polar(theta = "y") +
  theme_void()
grid.draw(shift_legend(p3))
Run Code Online (Sandbox Code Playgroud)

更多插图

  • 很好的解决方案!你的继承测试(`!"gtable"%in%class(p)`etc)应该通过`inherits`编写:`if(!inherits(p,'gtable'))`.由于您根据对象类执行不同的操作,因此请考虑使用S3方法而不是`if`语句. (5认同)

RLa*_*ave 23

不错的问答!

我在这个链接上找到了类似的东西.所以,我认为这对你的功能来说是一个很好的补充.

更确切地说功能reposition_legend()lemon似乎是你需要相当的东西,但它看起来并不为空的空间.

我把灵感来自于你的函数来找到传递给空面板的名称reposition_legend()panelARG.

示例数据和库:

library(ggplot2)
library(gtable)
library(lemon)

p <- ggplot(diamonds, 
            aes(x = carat, fill = cut)) +
  geom_density(position = "stack") +
  facet_wrap(~ color) +
  theme(legend.direction = "horizontal")
Run Code Online (Sandbox Code Playgroud)

当然,我删除了所有的检查(if案例,应该是相同的)只是为了集中精力于重要的事情.

shift_legend2 <- function(p) {
  # ...
  # to grob
  gp <- ggplotGrob(p)
  facet.panels <- grep("^panel", gp[["layout"]][["name"]])
  empty.facet.panels <- sapply(facet.panels, function(i) "zeroGrob" %in% class(gp[["grobs"]][[i]]))
  empty.facet.panels <- facet.panels[empty.facet.panels]

  # establish name of empty panels
  empty.facet.panels <- gp[["layout"]][empty.facet.panels, ]
  names <- empty.facet.panels$name
  # example of names:
  #[1] "panel-3-2" "panel-3-3"

# now we just need a simple call to reposition the legend
  reposition_legend(p, 'center', panel=names)
}

shift_legend2(p)
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

请注意,这可能仍需要一些调整,我只是认为这是值得分享的东西.

此刻行为似乎没问题,而且功能更短几行.


其他情况.

第一个例子:

p1 <- ggplot(economics_long, 
             aes(date, value, color = variable)) +
  geom_line() +
  facet_wrap(~ variable, 
             scales = "free_y", nrow = 2, 
             strip.position = "bottom") +
  theme(strip.background = element_blank(), 
        strip.placement = "outside")

shift_legend2(p1)
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

第二个例子:

p2 <- ggplot(mpg,
             aes(x = displ, y = hwy, color = fl, shape = factor(cyl))) +
  geom_point(size = 3) +
  facet_wrap(~ class, dir = "v") +
  theme(legend.box = "horizontal")

#[1] "panel-2-3" "panel-3-3" are the names of empty panels in this case
shift_legend2(p2) 
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

第三个例子:

p3 <- ggplot(mtcars, 
             aes(x = factor(1), fill = factor(cyl))) +
  geom_bar(width = 1, position = "fill") + 
  facet_wrap(~ gear, nrow = 2) +
  coord_polar(theta = "y") +
  theme_void()
shift_legend2(p3)
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述


功能齐全:

shift_legend2 <- function(p) {
  # check if p is a valid object
  if(!(inherits(p, "gtable"))){
    if(inherits(p, "ggplot")){
      gp <- ggplotGrob(p) # convert to grob
    } else {
      message("This is neither a ggplot object nor a grob generated from ggplotGrob. Returning original plot.")
      return(p)
    }
  } else {
    gp <- p
  }

  # check for unfilled facet panels
  facet.panels <- grep("^panel", gp[["layout"]][["name"]])
  empty.facet.panels <- sapply(facet.panels, function(i) "zeroGrob" %in% class(gp[["grobs"]][[i]]), 
                               USE.NAMES = F)
  empty.facet.panels <- facet.panels[empty.facet.panels]

  if(length(empty.facet.panels) == 0){
    message("There are no unfilled facet panels to shift legend into. Returning original plot.")
    return(p)
  }

  # establish name of empty panels
  empty.facet.panels <- gp[["layout"]][empty.facet.panels, ]
  names <- empty.facet.panels$name

  # return repositioned legend
  reposition_legend(p, 'center', panel=names)
}
Run Code Online (Sandbox Code Playgroud)


Art*_*lov 12

我认为lemon::reposition_legend()由@RLave 确定是最优雅的解决方案。但是,它确实取决于了解空构面的名称。我想分享一个简洁的方法来找到这些,因此提出了另一个版本shift_legend()

shift_legend3 <- function(p) {
    pnls <- cowplot::plot_to_gtable(p) %>% gtable::gtable_filter("panel") %>%
      with(setNames(grobs, layout$name)) %>% purrr::keep(~identical(.x,zeroGrob()))

    if( length(pnls) == 0 ) stop( "No empty facets in the plot" )

    lemon::reposition_legend( p, "center", panel=names(pnls) )
}
Run Code Online (Sandbox Code Playgroud)


Oma*_*sow 8

R 包patchwork在组合多个图时提供了一个优雅的解决方案(与单个多面 ggplot 略有不同)。如果有三个 ggplot 对象,p1、p2、p3,那么语法非常简单:

  • 使用+运算符,将图“添加”到一起
  • 使用该guide_area()命令指定哪个方面应包含指南
  • 如果所有三个图都有相同的图例,请通过命令 patchwork “收集”图例来节省空间plot_layout(guides = 'collect')

请参阅下面的代码了解基本语法,并参阅下面的链接了解完全可重现的示例。

library(patchwork)

# guide_area() puts legend in empty fourth facet
p1 + p2 + p3 + guide_area() + 
  plot_layout(guides = 'collect')
Run Code Online (Sandbox Code Playgroud)

https://patchwork.data-imaginist.com/articles/guides/layout.html#controlling-guides