7.2 字体

firatheme 包提供基于 fira sans 字体的 ggplot2 主题,类似的字体主题包还有 trekfontfontHindfontquiver 包与 fontBitstreamVera(Bitstream Vera 字体)、 fontLiberation(Liberation 字体)包和 fontDejaVu (DejaVu 字体)包一道提供了一些可允许使用的字体文件,这样,我们可以不依赖系统制作可重复的图形。Thomas Lin Pedersen 开发的 systemfonts 可直接使用系统自带的字体。

7.2.1 系统字体

以 CentOS 系统为例,软件仓库中包含 NotoDejaVuliberation 等字体。可以安装自己喜欢的字体类型,比如:

sudo dnf install -y \
  google-noto-mono-fonts \
  google-noto-sans-fonts \
  google-noto-serif-fonts \
  dejavu-sans-mono-fonts \
  dejavu-sans-fonts \
  dejavu-serif-fonts
# 或者
sudo dnf install -y dejavu-fonts liberation-fonts

liberation 系列的四款字体可以用来替换 Windows 系统上对应的四款字体,对应关系见表 7.1

表 7.1: Windows 系统上四款字体的替代品
CentOS 系统 Windows 系统
衬线体/宋体 liberation-serif-fonts Times New Roman
无衬线体/黑体 liberation-sans-fonts Arial
Arial 的细瘦版 liberation-narrow-fonts Arial Narrow
等宽体/微软雅黑 liberation-mono-fonts Courier New

此外,我们还可以从网上获取各种个样的字体,特别地,Boryslav Larin 收录的 awesome-fonts 列表是一个不错的开始,比如图标字体 Font-Awesome

sudo dnf install -y fontawesome-fonts

再安装宏包 fontawesome 后,即可在 LaTeX 文档中使用,下面这个示例推荐用 XeLaTeX 引擎编译。

\documentclass[border=10pt]{standalone}
\usepackage{fontawesome}
\begin{document}
Hello, \faGithub
\end{document}

而在 R 绘制的图形中,通过指定 par()plot()title() 等函数的 family 参数值,比如 family = "DejaVu Sans" 来调用系统无衬线 DejaVu 字体,效果见图 7.5

plot(data = pressure, pressure ~ temperature, 
     xlab = "Temperature (deg C)", ylab = "Pressure (mm of Hg)",
     col.lab = "red", col.axis = "blue",
     font.lab = 3, font.axis = 2, family = "DejaVu Sans")
title(main = "Vapor Pressure of Mercury as a Function of Temperature", 
      family = "DejaVu Serif", font.main = 3)
title(sub = "Data Source: Weast, R. C", 
      family = "DejaVu Sans Mono", font.sub = 1)
调用系统字体绘图

图 7.5: 调用系统字体绘图

在 ggplot2 绘图中的调用方式是类似的,如图 7.6

p1 <- ggplot(pressure, aes(x = temperature, y = pressure)) +
  geom_point() +
  ggtitle(label = "默认字体设置")

p2 <- p1 + theme(
  axis.title = element_text(family = "sans"),
  axis.text = element_text(family = "serif")
) +
  ggtitle(label = "英文字体设置")

p3 <- p1 + labs(x = "温度", y = "压力") +
  theme(
    axis.title = element_text(family = "source-han-serif-cn"),
    axis.text = element_text(family = "serif")
  ) +
  ggtitle(label = "中文字体设置")

p4 <- p1 + labs(
  x = "温度", y = "压力", title = "散点图",
  subtitle = "Vapor Pressure of Mercury as a Function of Temperature",
  caption = paste("Data on the relation 
                  between temperature in degrees Celsius and",
    "vapor pressure of mercury in millimeters (of mercury).",
    sep = "\n"
  )
) +
  theme(
    axis.title = element_text(family = "source-han-serif-cn"),
    axis.text.x = element_text(family = "serif"),
    axis.text.y = element_text(family = "sans"),
    title = element_text(family = "source-han-serif-cn"),
    plot.subtitle = element_text(family = "sans", size = rel(0.7)),
    plot.caption = element_text(family = "sans", size = rel(0.6))
  ) +
  ggtitle(label = "任意字体设置")

(p1 + p2) / (p3 + p4)
在 ggplot2 绘图系统中设置中英文字体

图 7.6: 在 ggplot2 绘图系统中设置中英文字体

另外值得一提的是 hrbrthemes 包,除了定制了很多 ggplot2 主题,它还打包了很多的字体主题。比如默认主题 theme_ipsum() 使用 Arial Narrow 字体,如果没有该字体就自动寻找系统中的替代品,如图 7.7 实际使用的是 Nimbus Sans Narrow 字体,因为在 GitHub Action 中,我实际使用的测试环境是 Ubuntu 20.04,该系统自带 Nimbus Sans Narrow 字体,Arial Narrow 毕竟是 Windows 上的闭源字体。

# 导入字体
hrbrthemes::import_roboto_condensed()
# showtextdb::font_add_google(name = "Roboto Condensed", family = "Roboto Condensed")
library(hrbrthemes)
ggplot(mtcars, aes(mpg, wt)) +
  geom_point() +
  labs(
    x = "Fuel efficiency (mpg)", y = "Weight (tons)",
    title = "Seminal ggplot2 scatterplot example",
    subtitle = "A plot that is only useful for demonstration purposes",
    caption = "Brought to you by the letter 'g'"
  ) +
  theme_ipsum()
调用 hrbrthemes 包设置字体主题

图 7.7: 调用 hrbrthemes 包设置字体主题

如果系统没有安装 Arial Narrow 字体,可以导入 hrbrthemes 包自带的一些字体,比如 hrbrthemes::import_roboto_condensed(),然后调用字体主题 theme_ipsum_rc() 。如果不想使用这个包自带的字体,可以用系统中安装的字体去修改主题 theme_ipsum()theme_ipsum_rc() 中的字体设置。如图 7.8 使用了 Liberation Sans Narrow ( MacOS 上没有 Liberation Sans Narrow 字体,用 Liberation Sans 替代了) 字体替换 theme_ipsum() 中的 Arial Narrow 字体。

ggplot(mtcars, aes(mpg, wt)) +
  geom_point() +
  labs(
    x = "Fuel efficiency (mpg)", y = "Weight (tons)",
    title = "Seminal ggplot2 scatterplot example",
    subtitle = "A plot that is only useful for demonstration purposes",
    caption = "Brought to you by the letter 'g'"
  ) +
  theme_ipsum(base_family = "Liberation Sans")
用 Liberation Sans Narrow 字体替换默认字体 Arial Narrow

图 7.8: 用 Liberation Sans Narrow 字体替换默认字体 Arial Narrow

hrbrthemes 包提供了一个全局字体加载选项 hrbrthemes.loadfonts ,如果设置为 TRUE,即 options(hrbrthemes.loadfonts = TRUE) 会先调用函数 extrafont::loadfonts() 预加载系统字体,就不用一次次手动加载字体了。后续在第 7.2.3 节还会提及 extrafont 包的其它功能。

7.2.2 思源字体

邱怡轩开发的 showtext 包支持丰富的外部字体,支持 Base R 和 ggplot2 图形,图 7.9 嵌入了 5 号思源宋体,图例和坐标轴文本使用 serif 字体,更多详细的使用文档见 (Qiu 2015)

# 安装 showtext 包
install.packages('showtext')
# 思源宋体
showtextdb::font_install(showtextdb::source_han_serif())
# 思源黑体
showtextdb::font_install(showtextdb::source_han_sans())
ggplot(iris, aes(Sepal.Length, Sepal.Width)) +
  geom_point(aes(colour = Species)) +
  scale_colour_brewer(palette = "Set1") +
  labs(
    title = "鸢尾花数据的散点图",
    x = "萼片长度", y = "萼片宽度", colour = "鸢尾花类别",
    caption = "鸢尾花数据集最早见于 Edgar Anderson (1935) "
  ) +
  theme(
    title = element_text(family = "source-han-sans-cn"),
    axis.title = element_text(family = "source-han-serif-cn"),
    legend.title = element_text(family = "source-han-serif-cn")
  )
showtext 包处理图里的中文

图 7.9: showtext 包处理图里的中文

斐济是太平洋上的一个岛国,受地壳板块运动的影响,地震活动频繁,图 7.10 清晰展示了它的地震带。

library(maps)
library(mapdata)
FijiMap <- map_data("worldHires", region = "Fiji")
ggplot(FijiMap, aes(x = long, y = lat)) +
  geom_map(map = FijiMap, aes(map_id = region), size = .2) +
  geom_point(data = quakes, aes(x = long, y = lat, colour = mag)) +
  xlim(160, 195) +
  scale_colour_distiller(palette = "Spectral") +
  scale_y_continuous(breaks = (-18:18) * 5) +
  coord_map("ortho", orientation = c(-10, 180, 0)) +
  labs(colour = "震级", x = "经度", y = "纬度", title = "斐济地震带") +
  theme_minimal() +
  theme(
    title = element_text(family = "source-han-sans-cn"),
    axis.title = element_text(family = "source-han-serif-cn"),
    legend.title = element_text(family = "source-han-sans-cn"),
    legend.position = c(1, 0), legend.justification = c(1, 0)
  )
斐济地震带

图 7.10: 斐济地震带

7.2.3 数学字体

Winston Chang 将 Paul Murrell 的 Computer Modern 字体文件打包成 fontcm(Chang, Kryukov, and Murrell 2014)fontcm 包可以在 Base R 图形中嵌入数学字体,15图形中嵌入重音字符。16 下面先下载、安装、加载字体,

library(extrafont)
font_addpackage(pkg = "fontcm")

查看可被 pdf() 图形设备使用的字体列表

# 可用的字体
fonts()
##  [1] "Roboto Condensed"       "xkcd"                   "CM Roman"              
##  [4] "CM Roman Asian"         "CM Roman CE"            "CM Roman Cyrillic"     
##  [7] "CM Roman Greek"         "CM Sans"                "CM Sans Asian"         
## [10] "CM Sans CE"             "CM Sans Cyrillic"       "CM Sans Greek"         
## [13] "CM Symbol"              "CM Typewriter"          "CM Typewriter Asian"   
## [16] "CM Typewriter CE"       "CM Typewriter Cyrillic" "CM Typewriter Greek"

fontcm 包提供数学字体,grDevices::embedFonts() 函数调用 Ghostscript 软件将数学字体嵌入 ggplot2 图形中,达到正确显示数学公式的目的,此方法适用于 pdf 设备保存的图形,对 cairo_pdf() 保存的 PDF 格式图形无效。

library(fontcm)
library(ggplot2)
library(extrafont)
library(patchwork)
p <- ggplot(data = data.frame(x = c(1, 5), y = c(1, 5)), aes(x = x, y = y)) +
  geom_point() +
  labs(x = "Made with CM fonts", y = "Made with CM fonts", 
       title = "Made with CM fonts")
# 公式
eq <- "italic(sum(frac(1, n*'!'), n==0, infinity) ==
       lim(bgroup('(', 1 + frac(1, n), ')')^n, n %->% infinity))"
# 默认字体
p1 <- p + annotate("text",
  x = 3, y = 3,
  parse = TRUE, label = eq
)
# 使用 CM Roman 字体
p2 <- p + annotate("text",
  x = 3, y = 3,
  parse = TRUE, label = eq, family = "CM Roman"
) +
  theme(
    text = element_text(size = 16, family = "CM Roman"),
    axis.title.x = element_text(face = "italic"),
    axis.title.y = element_text(face = "bold")
  )
p1 + p2
fontcm 处理数学公式

图 7.11: fontcm 处理数学公式

为实现图 7.11 的最终效果,需要启用一个有超级牛力的 fig.process 选项,主要是传递一个函数给它,对用 R 语言生成的图形再操作。

# embed math fonts to pdf
embed_math_fonts <- function(fig_path) {
  if(knitr::is_latex_output()){
    embedFonts(
      file = fig_path, outfile = fig_path,
      fontpaths = system.file("fonts", package = "fontcm")
    )
  }
  return(fig_path)
}

代码块选项中设置 fig.process=embed_math_fonts 可在绘图后,立即插入字体,此操作仅限于以 pdf 格式保存的图形设备,也适用于 Base R 绘制的图形,见图 7.12

par(mar = c(4.1, 4.1, 1.5, 0.5), family = "CM Roman")
x <- seq(-4, 4, len = 101)
y <- cbind(sin(x), cos(x))
matplot(x, y,
  type = "l", xaxt = "n",
  main = expression(paste(
    plain(sin) * phi, "  and  ",
    plain(cos) * phi
  )),
  ylab = expression("sin" * phi, "cos" * phi),
  xlab = expression(paste("Phase Angle ", phi)),
  col.main = "blue"
)
axis(1,
  at = c(-pi, -pi / 2, 0, pi / 2, pi),
  labels = expression(-pi, -pi / 2, 0, pi / 2, pi)
)
嵌入数学字体

图 7.12: 嵌入数学字体

7.2.4 TikZ 设备

7.2.3 小节不同,Ralf Stubner 维护的 tikzDevice 包提供了另一种嵌入数学字体的方式,其提供的 tikzDevice::tikz() 绘图设备将图形对象转化为 TikZ 代码,调用 LaTeX 引擎编译成 PDF 文档。安装后,先测试一下 LaTeX 编译环境是否正常。

tikzDevice::tikzTest()
## 
## Active compiler:
##  /home/runner/.TinyTeX/bin/x86_64-linux/xelatex
##  XeTeX 3.141592653-2.6-0.999993 (TeX Live 2021)
##  kpathsea version 6.3.3
## [1] 7.90259

确认没有问题后,下面图 7.13 的坐标轴标签,标题,图例等位置都支持数学公式,使用 tikzDevice 打造出版级的效果图。更多功能的介绍见 https://www.daqana.org/tikzDevice/

x <- rnorm(10)
y <- x + rnorm(5, sd = 0.25)
model <- lm(y ~ x)
rsq <- summary(model)$r.squared
rsq <- signif(rsq, 4)
plot(x, y,
  main = "Hello \\LaTeX!", xlab = "$x$", ylab = "$y$",
  sub = "$\\mathcal{N}(x;\\mu,\\Sigma)$"
)
abline(model, col = "red")
mtext(paste("Linear model: $R^{2}=", rsq, "$"), line = 0.5)
legend("bottomright",
  legend = paste0(
    "$y = ",
    round(coef(model)[2], 3),
    "x +",
    round(coef(model)[1], 3),
    "$"
  ),
  bty = "n"
)
线性回归模型

图 7.13: 线性回归模型

推荐的全局 LaTeX 环境配置如下:

options(
  tinytex.engine = "xelatex",
  tikzDefaultEngine = "xetex",
  tikzDocumentDeclaration = "\\documentclass[tikz]{standalone}\n",
  tikzXelatexPackages = c(
    "\\usepackage[fontset=adobe]{ctex}",
    "\\usepackage[default,semibold]{sourcesanspro}",
    "\\usepackage{amsfonts,mathrsfs,amssymb}\n"
  )
)

设置默认的 LaTeX 编译引擎为 XeLaTeX,相比于 PDFLaTeX,它对中文的兼容性更好,支持多平台下的中文环境,中文字体这里采用了 Adobe 的字体,默认加载了 mathrsfs 宏包支持 \mathcal\mathscr 等命令,此外, LaTeX 发行版采用谢益辉自定义的 TinyTeX。绘制独立的 PDF 图形的过程如下:

library(tikzDevice)
tf <- file.path(getwd(), "tikz-regression.tex")
tikz(tf, width = 6, height = 5.5, pointsize = 30, standAlone = TRUE)
# 绘图代码
dev.off()
# 编译成 PDF 图形
tinytex::latexmk(file = "tikz-regression.tex")

7.2.5 漫画字体

下载 XKCD 字体,并刷新系统字体缓存

mkdir -p ~/.fonts
curl -fLo ~/.fonts/xkcd.ttf http://simonsoftware.se/other/xkcd.ttf
fc-cache -fsv

将 XKCD 字体导入到 R 环境,以便后续被 ggplot2 图形设备调用。

R -e 'library(extrafont);font_import(pattern="[X/x]kcd.ttf", prompt = FALSE)'

7.14 是一个使用 xkcd 字体的简单例子,更多高级特性请看 xkcd 包文档 (Torres-Manzanera 2018)

library(extrafont)
library(xkcd)
ggplot(aes(mpg, wt), data = mtcars) +
  geom_point() +
  theme_xkcd()
漫画风格的字体方案

图 7.14: 漫画风格的字体方案

7.2.6 表情字体

余光创开发的 emojifont 包和 Hadley 开发的 emo 包,下面给出一个示例 7.15

# remotes::install_github("hadley/emo")
data.frame(
  category = c("pineapple", "apple", "watermelon", "mango", "pear"),
  value = c(5, 4, 3, 7, 2)
) %>%
  transform(category = sapply(category, emo::ji)) %>%
  ggplot(aes(x = category, y = value)) +
  geom_text(aes(label = category), size = 12, vjust = -0.5) +
  theme_minimal()
表情字体

图 7.15: 表情字体

除了安装 emo 包,系统需要先安装好 emoji 字体,图形才会正确地渲染出来,想调用更多 emoji 图标请参考 Emoji 速查手册,给出 emoji 对应的名字。

sudo dnf install -y google-noto-emoji-color-fonts \
  google-noto-emoji-fonts
# MacOS
brew cask install font-noto-color-emoji font-noto-emoji

参考文献

Chang, Winston, Alexej Kryukov, and Paul Murrell. 2014. Fontcm: Computer Modern Font for Use with Extrafont Package. https://github.com/wch/fontcm.
Qiu, Yixuan. 2015. showtext: Using System Fonts in R Graphics.” The R Journal 7 (1): 99–108. https://doi.org/10.32614/RJ-2015-008.
Torres-Manzanera, Emilio. 2018. Xkcd: Plotting Ggplot2 Graphics in an XKCD Style.