• R语言
  • 你们在 R 里都是怎么作图和保存方便编辑成发表级别 figure 的?

平时工作我习惯用 RStudio 和 Jupyter lab。前者方便快速处理和探索性分析,后者保存部分中间结果方便后续查看。在日常需要开会汇报或私下和其他人交流,我常常就直接在 RStudio 把图片缩放到合适大小然后截图,或者 Jupyter 中图片 Shift 右键保存到本地,这样的暂存需要或者备用的图片我经常都是统一放到幻灯片。最近经常需要为了发表作图,因为我日常在 Linux 桌面工作,我的工作流程是 R 中统一保存为 PDF,后续在 Inkscape 中导入然后编辑。这个工作流程基本上能满足各种作图要求,Inkscape 排版编辑功能也很不错。但是我经常碰到 R 里保存作图大小的问题,在这里想请教大家有什么日常用的技巧。

用于投稿的 Figure 通常会有特定的尺寸和分辨率要求,我习惯在 Inkcape 里新建 A4 纸张大小的空文档然后导入 R 中保存的 PDF。Inkscape 是一个矢量图片编辑工具,不熟悉的话可以想象成 Windows 下用 Adode AI 之类的工具。这里就存在一个问题,在 R 中保存作图为 PDF 时,选择什么尺寸大小比较合适?以及字号大小?
ggplot2 为例,base_size 应该如何根据保存作图大小相应调整。因为在实际期刊论文发表中,一些 figure 有时候是一行需要排 3 列小图片 (left, middle and right panel),有时候一张图很长可能会纵向占用两行((panel)大小。所以我经常需要在 R 中保存每张单独的小 figure 之后,导入 Inkscape 适当缩放到合适的 panel 大小。这个时候通常字号会因为图片被非整数倍拉伸缩放变成一个奇奇怪怪的非整数字号,经常高度和宽度不同比例缩放会变形,需要在 Inkscape 里重新编辑、调整大小并重新对齐,耗费大量时间。所以我想问,应该如何需求在 R 中作图保存时,最大程度上事先通过简单计算确定作图时应该使用的字号大小,和保存 PDF 对应的尺寸呢?

以防我表述不够清晰,下面以 Nature 一篇文章为例。这是 Nature 最新刊发一篇文章里的 Figure 3,大概判断应该是 R 作图:

image.png

(Ref: Zhao, J., Wan, W., Yu, K. et al. Farmed fur animals harbour viruses with zoonotic spillover potential. Nature (2024). https://doi.org/10.1038/s41586-024-07901-3)

b 中的三个小箱式图为为最小单位,高 x 宽都为 1 x 1 的话,a 的高 x 宽大概为 1.5 x 2,但是 a 和 b 一个四个小图分别作图的话,我的习惯是每张图保存为 PDF,最小宽度为 4inch,高度根据大小以宽度的倍数来设置,字号我经常设置 base_size 在 12-20 号之间。由于每张图都是宽度 4 英寸,导入 Inkscape 后,如果 b 每张小图都不缩放,那么 a 就需要拉宽,拉宽之后为了保持图片整体比例必然还需要拉高,这样,本来 a 和 b 一样的字号现在 b 里字都整体变大,就需要重新编辑。怎么避免这类问题呢?当然 b 里的三张图和 a 可以事先在 R 里用类似于 cowplotpatchwork 之类的工具先拼图再保存,可以达到比例正确。但有时需要 base R 和 ggplot2 作图同时使用就没那么简单。另外,也不是所有的图都在 R 里做,比如一张 Figure 里可能同时有 R 作图和实验数据,示意图,表格等等其他对象相互拼接。大家有什么好的建议呢?谢谢!

纯文本工作流当然需要完全通过编程实现,那就得推荐一下 plotgardener,这个工具摆脱了 patchwork 和 cowplot 使用相对大小的思路,而实现了一个基于坐标的布局系统,让你可以精确控制布局细节,并且支持混排基础图形和 ggplot2。

比较遗憾的是,作者选择了放到 Bioconductor 而不是 CRAN 上,这让它的影响力会受到限制,但是可以理解。

    昨晚想到似乎以前在那里见过类似的代码,今天早上狠狠搜了一下 GitHub star 过的项目终于找到了:

    theme_Publication_blank <- function(base_size=12, base_family="") {
      (theme_foundation(base_size=base_size, base_family=base_family)
       + theme(plot.title = element_text(size = rel(1.2), hjust = 0.5),
               text = element_text(),
               panel.background = element_rect(fill = "transparent",colour = NA),
               plot.background = element_rect(fill = "transparent",colour = NA),
               panel.border = element_rect(colour = NA, fill = "transparent"),
               axis.title = element_text(size = rel(1)),
               axis.title.y = element_text(angle=90,margin=margin(0,10,0,0)),
               axis.title.x = element_text(margin=margin(10,0,0,0)),
               axis.text = element_text(), 
               axis.line = element_line(colour="black"),
               axis.ticks = element_line(size = 0.3),
               axis.line.x = element_line(size = 0.3, linetype = "solid", colour = "black"),
               axis.line.y = element_line(size = 0.3, linetype = "solid", colour = "black"),
               panel.grid.major = element_blank(),
               panel.grid.minor = element_blank(),
               legend.key = element_rect(colour = NA, fill="transparent"),
               legend.position = "bottom",
               legend.margin = margin(t = 10, unit='pt'),
               plot.margin=unit(c(10,5,5,5),"mm"),
               strip.background=element_rect(colour="#d8d8d8",fill="#d8d8d8")
       ))
    } 
    
    # Define plot exporting helper function
    set_panel_size <- function(p=NULL, g=ggplotGrob(p), file=NULL, 
                               margin = unit(1,"mm"),
                               width=unit(7, "inch"), 
                               height=unit(5, "inch")){
      panels <- grep("panel", g$layout$name)
      panel_index_w<- unique(g$layout$l[panels])
      panel_index_h<- unique(g$layout$t[panels])
      nw <- length(panel_index_w)
      nh <- length(panel_index_h)
      
      g$widths[panel_index_w] <-  rep(width,  nw)
      g$heights[panel_index_h] <- rep(height, nh)
      
      if(!is.null(file)) {
        ggplot2::ggsave(file, g,
                        width = convertWidth(sum(g$widths) + margin,
                                             unitTo = "in", valueOnly = TRUE),
                        height = convertHeight(sum(g$heights) + margin,
                                               unitTo = "in", valueOnly = TRUE), useDingbats=F,
                        dpi=300)
        invisible(g)
      }
    }

    代码仓库在 这里

    这里作者首先定义了一个空白主题 theme_Publication_blank,这在出版物里很常见,主题都力求简洁。然后定义了一个 set_panel_size() 函数,这个函数用来保存图片到 PDF。在使用时可以看到作者使用形如:

    # Generate plot
    plot <- ggplot(data_long, aes(x = age, y = expression, color = gene)) +
      theme_Publication_blank() +
      geom_line(aes(linetype=Diagnosis), stat="smooth", size = 2,
                method = "loess", span = 0.75, se = FALSE) +
      scale_color_manual(values = colors) +
      scale_y_continuous(expand = c(0,0)) +
      scale_x_continuous(expand = c(0,0)) +
      guides(size = "none", alpha = "none") +
      ggtitle("CD14+/CD16+/CD68hi\nMonocytes") +
      theme(legend.position = "right",
            text = element_text(size=24),
            legend.key.size =  unit(0.5, "in"))
    
    # Export plot
    set_panel_size(plot, file = paste0(output_dir,
                                       "apoe_apoc1_pltp_loess_cd68hi_mono.pdf"),
                   width = unit(5, "inch"), height = unit(3, "inch"))

    的代码作图和保存。我正在看 set_panel_size() 这个函数代码,我还没有搞懂这个函数具体是在做什么是什么原理。

    JackieMe

    这些都能先统一读成 ggplot2 对象再布局,比如 ggimage, ggtext, svgparser。一个工具专注解决一个问题,组合起来解决整个流程。

    你当然可以选择混合的工作流,但我只能说既要又要是很困难的(同一种方法既要直观可交互,又要能编程可重复精确控制)。每个人都需要决定在某个节点选择对自己来说效率更高的方法。如果 Inkscape 或者 Illustrator 对你的需求还是更高效,那就坚持用它们布局好了。不需要犹豫,因为正如《只狼》中的苇名一心所说:「犹豫,就会败北」。