【问题标题】:Shift legend into empty facets of a faceted plot in ggplot2将图例转换为 ggplot2 中多面图的空白面
【发布时间】:2019-06-23 14:38:52
【问题描述】:

考虑以下情节:

library(ggplot2)

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

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

如果情节包括图例,情况会更加严重,因为现在我们由于图例而浪费了更多空间,无论它是在右侧(默认图例位置),还是其他三个方向之一。

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

以下是一种节省空间的措施,但图例固定在情节区域的一角,一侧可能留有大量空间,造成外观不平衡:

p +
  theme(legend.position = c(1, 0),
        legend.justification = c(1, 0))

通过手动调整 legend.position/legend.justification 值将图例移向空白区域的中心是一个反复试验的问题,如果有许多多面图要处理,则很难扩展。

总之,我想要一个方法:

  1. 将多面图的图例移动到由于空面而创建的空间中。
  2. 产生相当漂亮的情节。
  3. 很容易自动化处理许多情节。

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

【问题讨论】:

    标签: r ggplot2


    【解决方案1】:

    以下是我为 previous question 写的关于利用空分面面板空间的答案的扩展,但我认为它有足够的不同来保证自己的空间。

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

    功能

    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)
    }
    

    结果

    library(grid)
    
    grid.draw(shift_legend(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))
    

    其他一些例子:

    # 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))
    

    【讨论】:

    • 非常好的解决方案!您的继承测试(!"gtable" %in% class(p) 等)应通过inherits 编写:if (! inherits(p, 'gtable'))。而且由于您根据对象类执行不同的操作,因此请考虑使用 S3 方法而不是 if 语句。
    • 不错的答案。那么 shift.legend 函数不是在一个包里吗?
    • 有没有办法将输出转换回 ggplot 对象?
    【解决方案2】:

    很好的问答!

    我在this 链接上发现了类似的东西。所以,我认为这对你的功能来说会是一个很好的补充。

    更准确地说,来自lemon 的函数reposition_legend() 似乎正是您所需要的,只是它不查找空格。

    我从您的函数中获得灵感,以查找使用 panel arg 传递给 reposition_legend() 的空面板的名称。

    示例数据和库:

    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")
    

    当然,我删除了所有的检查(if case,应该是一样的)只是为了专注于重要的东西。

    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)
    

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

    目前行为似乎还可以,并且该函数短了几行。


    其他情况。

    第一个例子:

    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)
    

    第二个例子:

    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) 
    

    第三个例子:

    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)
    


    功能齐全:

    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)
    }
    

    【讨论】:

      【解决方案3】:

      我认为@RLave 识别的lemon::reposition_legend() 是最优雅的解决方案。然而,它确实取决于知道空面的名称。我想分享一个简洁的方法来找到这些,因此提出了另一个版本的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) )
      }
      

      【讨论】:

        【解决方案4】:

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

        • 使用+ 运算符,在构面中将图“添加”在一起
        • 使用guide_area() 命令,指定应包含指南的构面
        • 如果所有三个图都具有相同的图例,则通过使用命令 plot_layout(guides = 'collect') 告诉拼凑“收集”图例来节省空间。

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

        library(patchwork)
        
        # guide_area() puts legend in empty fourth facet
        p1 + p2 + p3 + guide_area() + 
          plot_layout(guides = 'collect')
        

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

        【讨论】:

          猜你喜欢
          • 2013-03-05
          • 1970-01-01
          • 2013-08-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-11-04
          • 2016-10-05
          相关资源
          最近更新 更多