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")
)
p1
再绘制每种类型的汽车均值
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")
)
p2
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)
p
我们使用 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)
par(
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() {
par(
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 <- ~{
par(
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 <- ~{
par(
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
)
prow
手动添加图例
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(
p1 +
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"),
rel_widths = c(1, 1, .3),
nrow = 1
)
# 组合所有图形
plot_grid(plots[[1]], bottom_row, labels = c("A"), ncol = 1)