ggplot2 学术绘图案例

ggplot2 学术绘图案例

在这份笔记中,我将尝试用ggplot2复现自己在学术期刊、新闻报刊中看到的漂亮的数据可视化图,或是总结自己在科研过程中使用ggplot2学习到的画图技巧。所有内容都会以案例形式展示,并将持续更新。

注意:

  1. 由于全部笔记都为案例形式,其中的画图技巧可能总结得不全面而且比较零散,因此仅供参考。

  2. 所有案例数据都为公开数据或测试数据(无实际意义),仅供画图学习,本人不保证数据真实正确、所有数据也都不代表本人观点。

  3. 如果对这些绘图案例有修改意见,欢迎交流讨论。

  4. 文中所有代码均为原创,转载请注明出处

环境设定

# 加载包
library(data.table)
## Warning: 程辑包'data.table'是用R版本4.3.3 来建造的
library(ggplot2)
## Warning: 程辑包'ggplot2'是用R版本4.3.3 来建造的
library(magrittr)

# 定义函数
# 1. 建立函数将R environment中的data.frame转为字符串,用于在文档中保存短小的数据(如案例5数据,注意数据量大时会报错)
texttable <- function(data){
  # 表格转文字格式的函数
  col.n <- ncol(data)
  col.name <- names(data) 
  res <- "data <- data.frame("
  for(i in 1:col.n){
    val <- data[[i]]
    if(is.character(val)){
      tres <- paste0(col.name[i]," = c('", paste0(val, collapse = "', '"), "'), ")
    } else{
      tres <- paste0(col.name[i], " = c(", paste0(val, collapse = ", "), "), ")
    }
    res <- paste(res, tres)
  }
  # res <- str_remove(res,", $") %>%  paste0(")")
  res <- gsub(", $", "", res)
  res <- paste0(res, ")")
  return(res)
}

案例1:显示数学符号和公式

ggplot2中,显示数学符号公式的方法有Rbase的expression(...)和html语法的ggtext::element_markdown()

注意:

  1. expression函数中任意字符如不能识别为符号,则以字符串形式(公式中的变量名)显示,例如expression(abscsdfwlapha3)直接会显示abscsdfwlapha3,可以有括号,但是要单独在首尾。

  2. expression或parse函数(将字符串转为expression)中,数学符号和字符串之间必须有连接符,连接方法有*paste(无空格连接)以及~(有空格连接)。例如以下三个结果相同:expression(''^210*Pb[ex])expression(paste(''^210,Pb[ex]))parse(text="''^210*Pb[ex]")。注意有一些符号(如上下标)前必须要有字符,没有要加空字符,例如上面^号前的'';注意^后至*~前所有字符都是上标,如果不确定,可以用^{210}指定上标部分,当然遇到特殊符号格式不对会报错。

  3. geom_text中,使用expression类型的输入会报警告(无论是否有parse=T),但仍可在图像上画出数学符号(ggplot2 v.3.4.2)。解决办法是,直接用字符型,然后设置parse=T(注意仅在geom_text中这么用),例如:建议用geom_text(label="''^210*Pb[ex]",parse=T)取代geom_text(label=expression(''^210*Pb[ex]))

  4. 对于geom_text,如果有向量变量(如a)单独存储数学符号(expression或字符串),则只能在aes外指定label:geom_text(label=a,parse=T);如果在画图数据中有一列(如dt$l)储存字符串格式的数学符号,则只能这样使用:geom_text(aes(label=l), parse = T)

  5. 在数据的一些位置不能使用expression,例如data.table的元素和列名都不支持,而虽然data.frame支持,但是在ggplot2分面标题中,也不能使用expression,此时可借助ggtext::element_markdown(),使用html语法实现分面标题的数学符号(如下面第二个图),注意这个方法,除了分面标题,其他位置不建议用,因为常不起作用。查看或生成html格式的数学公式,可参考以下资料:

    5.1 HTML数学公式

    5.2 公式手写识别与html代码生成

    5.4 Unicode字符代码

  6. 关于expression在绘图中显示的符号,详细帮助见?plotmath;其他可用符号,可参考The Adobe Symbol Encoding

图1,base::expression

point <- data.table(px = c(18,20,22,24,26,28,30,32,34,36,38,40),
                    py = c(4,5,12,35,84,45,47,41,22,25,3,5))
fline <- data.table(lx = seq(15, 43, by = 0.02), 
                    ly = 60.5921*exp(-((seq(15, 43, by = 0.02)-28.0006)/6.0670)^2))

x.labl <- c(expression(gamma),expression(delta),expression(epsilon),
            expression(zeta),expression(eta),expression(kappa),expression(lambda))
y.labl <- c(expression(Alpha), expression(Beta), expression(Gamma), expression(Delta),
            expression(Epsilon), expression(Zeta), expression(Eta), expression(Theta),
            expression(Iota), expression(Kappa))

# 注释掉的代码有助于理解其他方法,但不推荐使用;数据和符号无实际意义,纯粹为了实验能否使用
ggplot()+
  geom_point(data = point, aes(px, py, shape = "sha"), colour = "red", size = 2)+   #给定一个shape的名称为sha
  geom_line(data = fline, aes(lx, ly, color = "col"), linetype = 2)+
  scale_color_manual(values = c("col" = "blue"), labels = expression(beta))+  #线label
  scale_shape_manual(values = c("sha" = 15), labels = expression(alpha))+     #点label        
  guides(color = guide_legend(title = expression(r^2 == 111), order = 1))+    #线title
  guides(shape = guide_legend(title = expression(r[2] == 222), order = 2))+   #点title
  geom_text(data = NULL, aes(36, 70), label = "Fit~line:~y == 61*e^{-(frac(x-28,6))}^2", parse = T)+  #拟合线
  geom_text(data = NULL, aes(36, 60), label = "r^2 * '=' * '0.7610'", parse = T)+           #拟合优度
  # geom_text(data = NULL, aes(36, 60), label = "paste(r^2, '=','0.7610')", parse = T)+     #结果同上,但略复杂
  # geom_text(data = NULL, aes(36, 60), label = "r^2 == 0.7610", parse = T)+                #结果类似,但等式格式最后0不显示
  scale_x_continuous(breaks = seq(15, 45, by = 5), limits = c(15, 45), expand = c(0,0), labels = x.labl)+
  scale_y_continuous(breaks = seq(0, 90, by = 10), limits = c(0, 90),  expand = c(0,0), labels = y.labl)+
  labs(title = expression(sqrt(a,b)), x = expression(bold(prod(plain(P)(X==x), x))~(km^3)), y = expression(intersect(A[i], i==1, n)~italic(H[2]*CO[3]~mol%.%L^-1)))+
  theme_bw()+
  theme(aspect.ratio = 1/1.5,
        panel.border = element_rect(linewidth = 1),
        panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        plot.title = element_text(hjust = 0.5))

图2,ggtext::element_markdown

library(ggtext)
data <- data.table(class = c("a"), 
                   cs137 = c(1),  pb210 = c(2),   xlf = c(3),  
                   xhf = c(4),    L_star = c(5),  a_star = c(6), 
                   b_star = c(7), c_star = c(8),  h_star = c(9))
ename <- c("<sup>137</sup>Cs (Bq/kg)", 
           "<sup>210</sup>Pb<sub>ex</sub> (Bq/kg)",
           "&chi;<sub>lf</sub> (10<sup>-8</sup> m<sup>3</sup>/kg)", 
           "&chi;<sub>hf</sub> (10<sup>-8</sup> m<sup>3</sup>&#8729;kg<sup>-1</sup>)",
           "L<sup>*</sup> (&int;)", 
           "a<sup>*</sup> (&delta;)", 
           "b<sup>*</sup> (&#8710;)",
           "c<sup>*</sup> (&sigma;)", 
           "h<sup>*</sup> (&Omega;)")
dt.plot <- data %>% set_colnames(c("class", ename)) %>%  melt(id = "class", variable = "tracer", value = "value")  

ggplot(dt.plot, aes(class, value)) +
  geom_point()+
  facet_wrap(tracer~., scales = "free") +
  labs(x = "", y = "", title = "")+
  theme_bw() +
  theme(aspect.ratio = 1/1.5,
        plot.title = element_text(hjust = 0.5),
        strip.background = element_rect(fill = "white", color = "black"),
        strip.text = element_markdown())

案例2:ggplot2设置文字字体

注意:

\* 注意使用`sysfonts::font.families()`查看当前可用字体,这几个字体一般可以直接在ggplot中使用,而不需要额外包,windows版默认的字体中,'sans'是无衬线(Arial),'serif'是有衬线(times new roman),'mono'是等宽字体(Courier New),科研绘图最常用是'serif'。
  1. 一个可行的ggplot2配置、显示字体流程:

    1.1. 在R中查看哪些字体可用:sysfonts::font.families()

    1.2. 选择字体或使用sysfonts::font.add()用本地文件添加字体;

    1.3. 使用showtext::showtext_auto()在ggplot2中显示字体;

    1.4. 使用ggplot2画图并定义字体

  2. windows字体保存位置C:\Windows\Fonts

  3. 某些时候Rstudio的plot窗口不能显示字体,可在Tools - Global options - General - Graphics - Graphics Device - Backend修改参数,或者将图片保存本地后查看

  4. 在Rstudio中绘图显示不同字体,与Rmd和Rmd保存为html时,显示结果不一样。例如有时windows系统字体虽然sysfonts::font_families()不显示,但是在Rstudio也能调用,如Times New Roman,但是在Rmd html中则必须要载入、调用,否则提示没有该字体。如果在Rstudio中可以直接用,则不建议showtext::showtext_auto(),因为它会让文字变得非常小,要显著调大size才能正常显示,也因此建议画完图后就用showtext::showtext_auto(FALSE)关闭该功能。

library(showtextdb)
library(sysfonts)
## Warning: 程辑包'sysfonts'是用R版本4.3.3 来建造的
library(showtext)
## Warning: 程辑包'showtext'是用R版本4.3.3 来建造的
sysfonts::font_add("font1", regular = "D:/#R/learn_ggplot2/STXINGKA.TTF")
sysfonts::font_add("font2", regular = "D:/#R/learn_ggplot2/times.ttf")
sysfonts::font_add("font3", regular = "D:/#R/learn_ggplot2/SmileySans-Oblique.TTF")
sysfonts::font_add("font4", regular = "D:/#R/learn_ggplot2/STHUPO.TTF")
# sysfonts::font_families() #查看可行字体
showtext::showtext_auto()   #在ggplot2图中显示字体

p <- ggplot()+
  geom_text(aes(1.5,1),label="(theme改字体geom_text不生效)\nABcde\n12345\n中文字体",size=8)+
  geom_text(aes(4.5,1),label="(必须在geom_text改字体)\nABcde\n12345\n中文字体",size=8,family="font1")+
  geom_text(aes(0,0.86),label="注意:轴label字体会随theme的element_text(family='font1')设置改变",size=7,hjust=0)+
  scale_x_continuous(limits=c(0,6))+
  scale_y_continuous(limits=c(0.85,1.1), expand=c(0,0))+
  labs(x="variable 12345 横坐标轴", y="value 12345 纵坐标轴", title="title 12345 图标题")+
  theme_bw()+
  theme(panel.grid = element_blank(),
        text = element_text(family = "font1"),
        axis.text = element_text(size=20),
        axis.title.x = element_text(family = "font2", size=20),  #Times New Roman不显示中文
        axis.title.y = element_text(family = "font3", size=20),
        title = element_text(family = "font4", size=20))
# ggsave("text.tiff",p,width = 10, height = 10, units = "cm", dpi = 300) #保存图注意p的位置
print(p)

showtext_auto(FALSE) #画完图关闭,打开时画图文字会变得非常小,只能把字号设置的额外大才正常

案例3:指示线

来自网上的一个画图案例,具体来源记不清了,我把代码简化了,画图结果不变。

mydata <- data.table(response = c("Stringent complete response", "Complete response", "Very good partial response", "Partial response"), percentage = c(0.327, 0.067, 0.194, 0.042))
mydata$response <- factor(mydata$response , levels = c("Stringent complete response", "Complete response", "Very good partial response", "Partial response"))
dat.col <- c("darkgreen", "darkolivegreen3", "steelblue4", "lightskyblue3")

dat.tex <- data.table(x=c(1,0.55,1.5), y=c(0.66,0.43,0.33), l=c("63.0 (104/165)", " ≥CR: \n39.4", " ≥VGPR: \n58.8"))
dat.seg.left <- data.table(x=c(0.65,0.69,0.69,0.69), 
                           xe=c(0.69,0.73,0.73,0.69),
                           y=c(0.43,0.63,0.236,0.236),
                           ye=c(0.43,0.63,0.236,0.63))
dat.seg.righ <- data.table(x=c(1.31,1.27,1.27,1.31),
                           xe=c(1.35,1.31,1.31,1.31),
                           y=c(0.33,0.63,0.042,0.042),
                           ye=c(0.33,0.63,0.042,0.63))

ggplot(mydata, aes(x = "", y = percentage, fill = response)) +
  geom_bar(stat = "identity", width = 0.5, alpha = 0.9) +
  geom_text(aes(label = percentage * 100), position = position_stack(vjust = 0.5), size = 2.3, colour = c("white", "white", "white", "black")) +
  geom_text(data=dat.tex, aes(x,y,label=l,fill=NULL), size=2.5)+ #无法继承的aes要为NULL
  geom_segment(data=dat.seg.left, aes(x=x,xend=xe,y=y,yend=ye,fill=NULL))+ # 左侧{
  geom_segment(data=dat.seg.righ, aes(x=x,xend=xe,y=y,yend=ye,fill=NULL))+ # 右侧{
  scale_fill_manual(values = dat.col) +
  scale_y_continuous(breaks = seq(0, 1, 0.1), label = seq(0, 100, 10), limits = c(0, 1), expand = c(0, 0)) +
  labs(x="All Patients", y="Percentage of Patients with Response", fill = "Response:") +
  coord_cartesian(clip = "off") +
  theme_classic() +
  theme(legend.position = "top",
        legend.text = element_text(size = 7),
        legend.title = element_text(size = 7, face = "bold"),
        legend.key.size = unit(3, "mm"),
        axis.title = element_text(size = 8, face = "bold"),
        axis.text = element_text(size = 8),
        axis.ticks.x = element_blank(),
        axis.ticks.length.y = unit(2, "mm"),
        plot.margin = unit(c(0.6, 5, 0.6, 5), "cm")
  )