首发于 R

R 数据可视化 —— 图形排列之 cowplot

前言

cowplot 包是 ggplot 的一个简单插件,可以对多个图形进行排列和对齐,来生成复杂的出版级别的图片,还提供了一些主题和帮助函数。

安装

install.packages("cowplot")
# 安装最新的开发版本
remotes::install_github("wilkelab/cowplot")

导入相关包

library(tidyverse)
library(cowplot)

示例

1. 主题

ggplot2 的默认主题是这样的

ggplot(iris, aes(Sepal.Length, Sepal.Width, color = Species)) + 
  geom_point()

我们更换上不同的 cowplot 主题,来看看效果。例如,经典的 cowplot 主题

ggplot(iris, aes(Sepal.Length, Sepal.Width, color = Species)) + 
  geom_point() +
  theme_cowplot(font_size = 12)

网格最小化主题

ggplot(iris, aes(Sepal.Length, Sepal.Width, color = Species)) + 
  geom_point() +
  theme_minimal_grid(font_size = 12)

水平网格线最小化主题

ggplot(iris, aes(Sepal.Length, fill = Species)) + 
  geom_density(alpha = 0.5) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.05))) +
  theme_minimal_hgrid(font_size = 12)

2. 图形对齐

cowplot 的图形对齐与图形排列是相互独立的,例如,对于如下两个图形

p1 <- ggplot(mtcars, aes(disp, mpg)) + 
  geom_point(colour = 'red')
p2 <- ggplot(mtcars, aes(disp, hp)) + 
  geom_point(colour = 'blue')



虽然它们的横坐标是一样的,但是两个图形的宽度还是有细微的差别,第一幅图更宽。这是由于第二幅图的 y 轴标签更大,占用了更宽的空间,我们可以使用 align_plots 来对齐两幅图

aligned <- align_plots(p1, p2, align = "v")
ggdraw(aligned[[1]])
ggdraw(aligned[[2]])



align = "v" 表示竖直方向对齐, align = "h" 表示水平方向对齐

我们可以使用 plot_grid() 函数来同时完成对齐和重排两个步骤,上面的代码可以写成

plot_grid(p1, p2, ncol = 1, align = "v")

ncol 参数用于指定列数

这是在我们对齐的轴完全一样的情况下,如果我们图形的轴不一样,会怎么样呢

p1 <- ggplot(mtcars, aes(disp, mpg)) + 
  geom_point() +
  theme_minimal_grid(14) + 
  panel_border(color = "black")
p2 <- ggplot(mtcars, aes(factor(vs), colour = factor(vs))) + 
  geom_bar() + 
  facet_wrap(~am) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.1))) +
  theme_minimal_hgrid(12) +
  panel_border(color = "black") +
  theme(strip.background = element_rect(fill = "gray80"))
plot_grid(p1, p2, align = "h", rel_widths = c(1, 1.3))

同时抛出了警告信息

Warning message:
Graphs cannot be horizontally aligned unless the axis parameter is set. Placing graphs unaligned.

告诉我们,图形并没有对齐,需要设置 axis 参数才能生效。

注: rel_widths 参数用于设置两张图片的宽度比例

例如,我们可以让其按照底部的轴线对齐

plot_grid(p1, p2, align = "h", axis = "b", rel_widths = c(1, 1.3))

或者按照底部和顶部轴线对齐

plot_grid(p1, p2, align = "h", axis = "bt", rel_widths = c(1, 1.3))

注: axis 参数可以是 t(top) r(right) b(bottom) l(left) 的任意组合

另一个例子,在用于相同数量的元素的图中,我们也可以设置按 axis 对齐

city_mpg <- mpg %>%
  mutate(class = fct_lump(class, 4, other_level = "other")) %>%
  group_by(class) %>%
  summarize(
    mean_mpg = mean(cty),
    count = n()
  ) %>% mutate(
    class = fct_reorder(class, count)
p1 <- ggplot(city_mpg, aes(class, count, fill = class)) + 
  geom_col(show.legend = FALSE) + 
  ylim(0, 65) + 
  coord_flip()
p2 <- ggplot(city_mpg, aes(mean_mpg, count)) + 
  geom_point()
plot_grid(p1, p2, ncol = 1, align = 'v')

由于两幅图的 y 轴标签大小不一致,导致第二幅图的 y 轴标题与标签之间间距较大,我们可以设置 axis = "l" 按左侧轴对齐

plot_grid(p1, p2, ncol = 1, align = 'v', axis = 'l')

最后,我们来介绍何如将两幅图重叠。我们先绘制一张条形图,用于展示每种类型汽车的数量

city_mpg <- city_mpg %>%
  mutate(class = fct_reorder(class, -mean_mpg))
p1 <- ggplot(city_mpg, aes(class, count)) +
  geom_col(fill = "#6297E770") + 
  scale_y_continuous(
    expand = expansion(mult = c(0, 0.05)),
    position = "right"
  theme_minimal_hgrid(11, rel_small = 1) +
  theme(
    panel.grid.major = element_line(color = "#6297E770"),
    axis.line.x = element_blank(),
    axis.text.x = element_blank(),
    axis.title.x = element_blank(),
    axis.ticks = element_blank(),
    axis.ticks.length = grid::unit(0, "pt"),
    axis.text.y = element_text(color = "#6297E7"),
    axis.title.y = element_text(color = "#6297E7")

再绘制每种类型的汽车均值

p2 <- ggplot(city_mpg, aes(class, mean_mpg)) + 
  geom_point(size = 3, color = "#D5442D") + 
  scale_y_continuous(limits = c(10, 21)) +
  theme_half_open(11, rel_small = 1) +
  theme(
    axis.ticks.y = element_line(color = "#BB2D05"),
    axis.text.y = element_text(color = "#BB2D05"),
    axis.title.y = element_text(color = "#BB2D05"),
    axis.line.y = element_line(color = "#BB2D05")
aligned_plots <- align_plots(p1, p2, align="hv", axis="tblr")
ggdraw(aligned_plots[[1]]) + draw_plot(aligned_plots[[2]])

3. 图上绘制图形

cowplot 包提供了用于在图上绘制的函数,这些函数可以添加任意的图形、注释或背景,可以在其他图上放置图形,或者组合不同的图形系统,如 ggplot2 grid lattice base ,并返回一个 ggplot2 对象。

3.1 添加注释

例如,对于下面这张图

p <- ggplot(mpg, aes(displ, cty)) +
  geom_point() +
  theme_minimal_grid(12)

我们使用 ggdraw() 将图封装成一个绘图环境,然后使用 draw*() 函数添加注释,例如,添加一个水印

ggdraw(p) + 
  draw_label("Watermark", color = "#C0A0A0", size = 50, angle = 45)

ggdraw(p) 用于捕获绘图快照,并将该绘图转换为图片,然后将该图片绘制在一个新的 ggplot2 画布中。

draw_* 函数是常用的几何对象的封装,上面的代码也可以用 geom_text() 来替换 draw_label()

ggdraw(p) + 
  geom_text(
    data = data.frame(x = 0.5, y = 0.5, label = "Watermark"),
    aes(x, y, label = label),
    hjust = 0.5, vjust = 0.5, angle = 45, size = 50/.pt,
    color = "#C0A0A0",
    inherit.aes = FALSE

由于 ggplot2 的字体大小是 mm ,需要除以 .pt 进行转换,而 draw_label 会在内部进行转换。

我们可以将 ggdraw() 当做 ggplot2 一个绘图对象,可以在其后修改主题

ggdraw(p) + 
  draw_label("Watermark", color = "#C0A0A0", size = 50, angle = 45) +
  theme(
    plot.background = element_rect(fill = "cornsilk", color = NA)

如果要保存图片,可以使用 ggsave() 函数,也可以使用 cowplot 提供的 save_plot() 函数,可以自动调整图形为合适的大小

如果要让水印在画布的底层,只需先使用 ggdraw 绘制一个空图层,然后调整绘制的顺序即可

ggdraw() + 
  draw_label("Watermark", color = "#C0A0A0", size = 50, angle = 45) +
  draw_plot(p)

上面的例子需要保证图片的背景是完全透明的,例如,我们将散点图的背景设置为 theme_classic() 水印将会被遮盖

ggdraw() + 
  draw_label("Watermark", color = "#C0A0A0", size = 50, angle = 45) +
  draw_plot(
    p + theme_classic()

可以将主题设置为 theme_half_open()

ggdraw() + 
  draw_label("Watermark", color = "#C0A0A0", size = 50, angle = 45) +
  draw_plot(
    p + theme_half_open()

3.2 插入图形

draw_plot() 函数允许我们将任意大小的图形放置在画布的任意位置,可以很容易的将一个图形嵌入到另一个图形中,构建组合图形

例如,在一个大图中,添加一个小图

inset <- ggplot(mpg, aes(drv)) + 
  geom_bar(fill = "#fb8072", alpha = 0.7) + 
  scale_y_continuous(expand = expansion(mult = c(0, 0.05))) +
  theme_minimal_hgrid(11)
ggdraw(p + theme_half_open(12)) +
  draw_plot(inset, .45, .45, .5, .5) +
  draw_plot_label(
    c("A", "B"),
    c(0, 0.45),
    c(1, 0.95),
    size = 12

组合 ggplot2 base 图形

# 需要 gridGraphics 包
# install.packages("gridGraphics")
# 设置为表达式的形式
inset <- ~{
  counts <- table(mpg$drv)
    cex = 0.8,
    mar = c(3, 3, 1, 1),
    mgp = c(2, 1, 0)
  barplot(counts, xlab = "drv", ylab = "count", col = "#80b1d3")
ggdraw(p + theme_half_open(12)) +
  draw_plot(inset, .45, .45, .5, .5) +
  draw_plot_label(
    c("A", "B"),
    c(0, 0.45),
    c(1, 0.95),
    size = 12

3.3 绝对位置

默认情况下, ggdraw() 函数使用的是相对位置, x y 轴都是在 0-1 之间。

如果要通过绝对位置进行定位,可以使用 grid 图形系统,并搭配 draw_grob() 绘制函数

例如,我们在左上角 1 英寸处添加一个 1 英寸的正方形

library(grid)
rect <- rectGrob(
  x = unit(1, "in"),
  y = unit(1, "npc") - unit(1, "in"),
  width = unit(1, "in"),
  height = unit(1, "in"),
  hjust = 0, vjust = 1,
  gp = gpar(fill = "skyblue2", alpha = 0.5)
ggdraw(p) +
  draw_grob(rect)

我们改变图形的大小,但是正方形的大小和位置不会变


3.4 组合静态图片

draw_image() 可以在绘图中添加静态图片,需要先安装 magick 包。

例如,添加背景图片

library(magick)
library(magick)
img <- system.file("extdata", "cow.jpg", package = "cowplot") %>%
  image_read() %>%
  image_resize("570x380") %>%
  image_colorize(35, "white")
p <- ggplot(mpg, aes(displ, fill = class)) +
  geom_density(alpha = 0.7) +
  scale_fill_grey() +
  coord_cartesian(expand = FALSE) +
  theme_minimal_hgrid(11, color = "grey30")
ggdraw() + 
  draw_image(img) + 
  draw_plot(p)

或者,添加 logo

logo_file <- system.file("extdata", "logo.png", package = "cowplot")
ggdraw() + 
  draw_plot(p) +
  draw_image(
    logo_file, x = 1, y = 1, hjust = 1, vjust = 1, halign = 1, valign = 1,
    width = 0.15

4. 混合不同绘图框架

cowplot 绘图函数( ggdraw() draw_plot() plot_grid() )不仅支持 ggplot2 绘图系统,还支持 base 绘图系统,但是要先安装 gridGraphics

例如,我们用 ggdraw() 绘制基础图形,然后使用 ggplot2 主题

p1 <- function() {
    mar = c(3, 3, 1, 1),
    mgp = c(2, 1, 0)
  boxplot(mpg ~ cyl, xlab = "cyl", 
    ylab = "mpg", data = mtcars, 
    col = c("red", "blue", "green")
ggdraw(p1) +
  theme(plot.background = element_rect(fill = "cornsilk"))

添加 logo

logo_file <- system.file("extdata", "logo.png", package = "cowplot")
ggdraw() + 
  draw_image(
    logo_file,
    x = 1, width = 0.1,
    hjust = 1, halign = 1, valign = 0
  draw_plot(p1)

使用 plot_grid() 函数可以将 base ggplot2 图形以网格的布局绘制在一起

p2 <- ggplot(data = mtcars, aes(factor(cyl), mpg)) + 
  geom_boxplot()
plot_grid(p1, p2)

base 绘图可以封装为函数的形式,并将相应的图形返回,作为一种被记录的绘图。或者封装为表达式的形式,如 3.2 例子。

例如,我们绘制一个基础图形

par(mar = c(3, 3, 1, 1), mgp = c(2, 1, 0))
boxplot(mpg ~ cyl, xlab = "cyl", ylab = "mpg", data = mtcars)

这个图形会被 recordPlot() 函数记录,然后,可以使用 ggdraw() 函数对被记录的图形进行绘制

p1_recorded <- recordPlot()
ggdraw(p1_recorded)

我们也可以将绘图代码放在大括号中,并包裹成表达式

p1_formula <- ~{
    mar = c(3, 3, 1, 1), 
    mgp = c(2, 1, 0)
  boxplot(mpg ~ cyl, xlab = "cyl", ylab = "mpg", data = mtcars)
ggdraw(p1_formula)

还支持 lattice 图形和 grid 图形对象

# base R
p1 <- ~{
    mar = c(3, 3, 1, 1), 
    mgp = c(2, 1, 0)
  boxplot(mpg ~ cyl, xlab = "cyl", ylab = "mpg", data = mtcars)
# ggplot2
p2 <- ggplot(data = mtcars, aes(factor(cyl), mpg)) + geom_boxplot()
# lattice
library(lattice)
p3 <- bwplot(~mpg | cyl, data = mtcars)
# 圆形对象
library(grid)
p4 <- circleGrob(r = 0.3, gp = gpar(fill = "skyblue"))
plot_grid(p1, p2, p3, p4, rel_heights = c(.6, 1), labels = "auto")

只要能返回 grid 对象的其他绘图包,都可以

library(VennDiagram)
p_venn <- draw.pairwise.venn(
  100, 70, 30,
  c("First", "Second"),
  fill = c("light blue", "pink"),
  alpha = c(0.7, 0.7),
  ind = FALSE
# 绘图韦恩图并添加矩形框和边距
ggdraw(p_venn) +
  theme(
    plot.background = element_rect(fill = NA),
    plot.margin = margin(12, 12, 12, 12)

5. 网格布局

5.1 基本使用

plot_grid 用于将图形按照网格布局进行排列,它是基于 ggdraw() draw_*() 函数来绘制图形,图形对齐是通过 align_plots() 函数。

plot_grid 可以很容易地对多个图形就行排列并添加标签

p1 <- ggplot(mtcars, aes(disp, mpg)) + 
  geom_point(colour = "red")
p2 <- ggplot(mtcars, aes(qsec, mpg)) +
  geom_point(colour = "blue")
plot_grid(p1, p2, labels = c('A', 'B'))

如果将 labels 参数设置为 AUTO 或者 auto ,会自动添加大写或小写的标签

plot_grid(p1, p2, labels = "AUTO")
plot_grid(p1, p2, labels = "auto")

plot_grid 默认不会将图形对齐

p3 <- p1 + 
  # 使用更大刻度标签
  theme(axis.text.x = element_text(size = 14, angle = 90, vjust = 0.5))
# 没有对齐
plot_grid(p3, p2, labels = "AUTO")

水平对齐

plot_grid(p3, p2, labels = "AUTO", align = "h")

5.2 图形微调

label_size 用于控制标签的大小,默认为 14

plot_grid(p1, p2, labels = "AUTO", label_size = 12)

对字体进行调整

plot_grid(
  p1, p2,
  labels = "AUTO", 
  label_fontfamily = "serif",
  label_fontface = "plain",
  label_colour = "blue"

可以使用 label_x label_y 参数控制标签的位置, hjust vjust 参数用于控制对齐方式

plot_grid(
  p1, p2,
  labels = "AUTO",
  label_size = 12,
  label_x = 0, label_y = 0,
  hjust = -0.5, vjust = -0.5

当然,对于多个图形,可以传递向量值的方式来分别控制每个图形

行数和列数可以使用 nrow ncol 参数来控制

plot_grid(
  p1, p2,
  labels = "AUTO", ncol = 1

可以使用 NULL 来表示网格中该位置为空白,如果设置了自动设置标签,也会为空白图添加上标签

plot_grid(
  p1, NULL, NULL, p2,
  labels = "AUTO", ncol = 2

rel_widths rel_heights 参数用于设置图形的相对宽度和高度

plot_grid(p1, p2, labels = "AUTO", rel_widths = c(1, 2))

5.3 嵌套绘图

plot_grid() 可以嵌套使用,生成更加复杂的图形布局,例如

bottom_row <- plot_grid(p1, p2, labels = c('B', 'C'), label_size = 12)
p3 <- ggplot(mtcars, aes(x = qsec, y = disp, colour = factor(gear))) + 
  geom_point(show.legend = FALSE) + facet_wrap(~gear)
plot_grid(p3, bottom_row, labels = c('A', ''), label_size = 12, ncol = 1)

这种情况下,图形的对齐将会变得比较困难,我们可以使用 align_plots() 函数来显式的对齐图形

# 首先,将顶行(p3)与底行第一个图形(p1)按左对齐
plots <- align_plots(p3, p1, align = 'v', axis = 'l')
bottom_row <- plot_grid(plots[[2]], p2, labels = c('B', 'C'), label_size = 12)
# 将对齐后的 p3 与底行进行组合
plot_grid(plots[[1]], bottom_row, labels = c('A', ''), label_size = 12, ncol = 1)

5.4 组合标题

如果我们要为组合图形添加一个跨越整个图形的标题,需要将标题也作为一个图形对象,并使用 plot_grid 来组合标题图形和绘图区域图形

p1 <- ggplot(mtcars, aes(x = disp, y = mpg)) + 
  geom_point(colour = "blue") + 
  theme_half_open(12) + 
  background_grid(minor = 'none')
p2 <- ggplot(mtcars, aes(x = hp, y = mpg)) + 
  geom_point(colour = "green") + 
  theme_half_open(12) + 
  background_grid(minor = 'none')
plot_row <- plot_grid(p1, p2)
# 添加标题
title <- ggdraw() + 
  draw_label(
    "Miles per gallon decline with displacement and horsepower",
    fontface = 'bold',
    x = 0,
    hjust = 0
  theme(
    # 添加边距,使标题左对齐边缘
    plot.margin = margin(0, 0, 0, 7)
plot_grid(
  title, plot_row,
  ncol = 1,
  # 控制标题与图形的高度比例
  rel_heights = c(0.1, 1)

6. 共享图例

下面,我们介绍如何为组合图形设置图例,假设我们有如下三个图形

dsamp <- diamonds[sample(nrow(diamonds), 1000), ]
# 绘图函数
plot_diamonds <- function(xaes) {
  xaes <- enquo(xaes)
  ggplot(dsamp, aes(!!xaes, price, color = clarity)) +
    geom_point() +
    theme_half_open(12) +
    # 设置左右边距为 0
    theme(plot.margin = margin(6, 0, 6, 0))
# 添加图形
p1 <- plot_diamonds(carat)
p2 <- plot_diamonds(depth) + ylab(NULL)
p3 <- plot_diamonds(color) + ylab(NULL)
# 将图形排列成一行
prow <- plot_grid(
  p1 + theme(legend.position="none"),
  p2 + theme(legend.position="none"),
  p3 + theme(legend.position="none"),
  align = 'vh',
  labels = c("A", "B", "C"),
  hjust = -1,
  nrow = 1

<br> (二维码自动识别)

手动添加图例

legend <- get_legend(
  # 为图例留出足够空间
  p1 + theme(legend.box.margin = margin(0, 0, 0, 12))
# 将图例和之前的图形进行组合,并设置宽度比例
plot_grid(prow, legend, rel_widths = c(3, .4))

添加水平图例

# 获取水平布局的图例
legend_b <- get_legend(
    guides(color = guide_legend(nrow = 1)) +
    theme(legend.position = "bottom")
# 组合图例之前的图形,并设置高度比例
plot_grid(prow, legend_b, ncol = 1, rel_heights = c(1, .1))

在图形中间添加图例

# 将所有图形放置在同一行,并在 B 和 C 之间留出空间
prow <- plot_grid(
  p1 + theme(legend.position="none"),
  p2 + theme(legend.position="none"),
  NULL,
  p3 + theme(legend.position="none"),
  align = 'vh',
  labels = c("A", "B", "", "C"),
  hjust = -1,
  nrow = 1,
  rel_widths = c(1, 1, .3, 1)
# 添加图例
prow + draw_grob(legend, 2/3.3, 0, .3/3.3, 1)

下面再来一个更复杂的例子

# plot 1
p1 <- ggplot(iris, aes(Sepal.Length, Sepal.Width, color = Species)) + 
  geom_point() + 
  stat_smooth(method = "lm") +
  facet_grid(. ~ Species) +
  theme_half_open(12) +
  background_grid(major = 'y', minor = "none") + 
  panel_border() + 
  theme(legend.position = "none")
# plot 2
p2 <- ggplot(iris, aes(Sepal.Length, fill = Species)) +
  geom_density(alpha = .7) + 
  scale_y_continuous(expand = expansion(mult = c(0, 0.05))) +
  theme_half_open(12) +
  theme(legend.justification = "top")
p2a <- p2 + theme(legend.position = "none")
# plot 3
p3 <- ggplot(iris, aes(Sepal.Width, fill = Species)) +
  geom_density(alpha = .7) + 
  scale_y_continuous(expand = c(0, 0)) +
  theme_half_open(12) +
  theme(legend.position = "none")
# legend
legend <- get_legend(p2)
# 所有图形竖直对齐
plots <- align_plots(p1, p2a, p3, align = 'v', axis = 'l')
# 将后面两张图以及图例组合在一起,并放置在第二行
bottom_row <- plot_grid(
  plots[[2]], plots[[3]], legend,
  labels = c("B", "C"),