前言
多次有人问我,如何使用 R
来绘制多个 Y
轴。那我就研究了下,稍微介绍一下学习所得。
当我们需要展示的数据,包含相同的 X
轴,但是具有两个不同的 Y
轴变量时,我们会选择在右侧添加一条轴,在 ggplot2
中,可以使用 sec_axis
来设置。
那如果 Y
轴变量有多个呢?我们该如何绘制?
一种简单的思路就是,在图像绘图区域的左右两侧添加两条轴,多余的轴可以添加到右侧,图像还是填充到绘制区域。
那如何实现这一功能呢?下面我们将介绍今天的主角 —— gtable
,搭配 grid
包,可以轻松的为 ggplot
添加上多个 Y
轴
gtable
1. 介绍
gtable
是基于 grid
包的布局引擎,可以用来抽象化地创建网格视图,每个网格内都可以放置不同的图形对象
gtable
可以很容易地将图形元素的对齐,组合成复杂的图形。同时,完美兼容 ggplot2
图形
首先,当然是安装和导入了,不必多说
# 从 CRAN 上安装
install.packages("gtable")
# 从 GitHub 上安装
# install.packages("remotes")
remotes::install_github("r-lib/gtable")
导入
library(gtable)
library(ggplot2)
library(grid)
2. 创建布局
创建布局的函数主要有如下几个:
-
gtable
: 创建grob
(grid object
) 的table
布局 -
gtable_matrix
: 矩阵布局 -
gtable_col
: 单列 -
gtable_row
: 单行 -
gtable_row_spacer
/gtable_col_spacer
:
2.1 gtable
table
布局可以将 grob
以表的排列形式放置。它支持跨行、跨列,还提供了一些工具来自动计算出正确的尺寸
gtable(widths = list(), heights = list(), respect = FALSE,
name = "layout", rownames = NULL, colnames = NULL, vp = NULL)
其中前两个参数指定了表格布局,例如
> a <- gtable(unit(1:3, c("cm")), unit(5, "cm"))
> a
TableGrob (1 x 3) "layout": 0 grobs
> class(a)
[1] "gtable" "gTree" "grob" "gDesc"
创建了一个宽度分别为 1cm
、2cm
、3cm
,长度为 5cm
的一行三列的表格布局,,返回了一个 gtable
对象
使用 gtable_show_layout
函数可以绘制布局结构
gtable_show_layout(a)
每个绘图对象(grob
)都放置在自己的视图中,并占满整个视图,因此对齐方式在这里不会发挥作用
布局中包含三个基础组件,表格的规格(单元格高度和宽度)、布局(对每个 grob
来说就是它的位置,名称及其他属性)和全局参数
每个单元格内可以有 0
、1
或多个 grob
。每个 grob
必须至少属于一个单元格,但可以跨越多个单元格。
布局的细节被存储在一个数据框中,每行表示一个 grob
,列包括:
-
t
:grob
的顶部位置 -
r
:grob
的右边位置 -
b
:grob
的底部位置 -
l
:grob
的左边位置 -
z
:grob
的次序 -
clip
:grob
的裁剪方式,可以是:"on"
、"off"
或"inherit"
- name: 每个
grob
和视图的名称
不能直接修改这个数据框,需要使用 gtable_add_grob
等操作函数来修改
2.2 gtable_matrix
gtable_matrix
可以创建矩阵布局,允许为每个单元格设置不同的的高度和宽度
gtable_matrix(name, grobs, widths = NULL, heights = NULL, z = NULL,
respect = FALSE, clip = "on", vp = NULL)
首先,创建图形对象
a <- rectGrob(gp = gpar(fill = "red"))
b <- circleGrob()
c <- linesGrob()
row <- matrix(list(a, b, c), nrow = 1)
col <- matrix(list(a, b, c), ncol = 1)
mat <- matrix(list(a, b, c, nullGrob()), nrow = 2)
设置布局,一行三列
> gtable_matrix("demo", row, unit(c(1, 2, 3), "null"), unit(1, "null"))
TableGrob (1 x 3) "demo": 3 grobs
z cells name grob
1 1 (1-1,1-1) demo rect[GRID.rect.143]
2 2 (1-1,2-2) demo circle[GRID.circle.144]
3 3 (1-1,3-3) demo lines[GRID.lines.145]
三行一列
> gtable_matrix("demo", col, unit(1, "null"), unit(c(1, 2, 3), "null"))
TableGrob (3 x 1) "demo": 3 grobs
z cells name grob
1 1 (1-1,1-1) demo rect[GRID.rect.143]
2 2 (2-2,1-1) demo circle[GRID.circle.144]
3 3 (3-3,1-1) demo lines[GRID.lines.145]
两行两列
> gtable_matrix("demo", mat, unit(c(1, 1), "null"), unit(c(1, 1), "null"))
TableGrob (2 x 2) "demo": 4 grobs
z cells name grob
1 1 (1-1,1-1) demo rect[GRID.rect.143]
2 2 (2-2,1-1) demo circle[GRID.circle.144]
3 3 (1-1,2-2) demo lines[GRID.lines.145]
4 4 (2-2,2-2) demo null[GRID.null.146]
设置绘制顺序
> z <- matrix(c(3, 1, 2, 4), nrow = 2)
> gtable_matrix("demo", mat, unit(c(1, 1), "null"), unit(c(1, 1), "null"), z = z)
TableGrob (2 x 2) "demo": 4 grobs
z cells name grob
1 3 (1-1,1-1) demo rect[GRID.rect.143]
2 1 (2-2,1-1) demo circle[GRID.circle.144]
3 2 (1-1,2-2) demo lines[GRID.lines.145]
4 4 (2-2,2-2) demo null[GRID.null.146]
2.3 gtable_col 和 gtable_row
相较于前两个布局,gtable_col
和 gtable_row
就比较简单,只是把所有 grob
绘制在一列或一行
gtable_col(name, grobs, width = NULL, heights = NULL, z = NULL,
vp = NULL)
gtable_row(name, grobs, height = NULL, widths = NULL, z = NULL,
vp = NULL)
也是通过 width
和 height
控制宽度和高度,例如,绘制成一列
> gt <- gtable_col("demo", list(a, b, c))
> gt
TableGrob (3 x 1) "demo": 3 grobs
z cells name grob
1 1 (1-1,1-1) demo rect[GRID.rect.143]
2 2 (2-2,1-1) demo circle[GRID.circle.144]
3 3 (3-3,1-1) demo lines[GRID.lines.145]
> plot(gt)
gtable_show_layout(gt)
绘制成一行
> gt <- gtable_row("demo", list(a, b, c))
> plot(gt)
gtable_show_layout(gt)
3. 布局操作
-
gtable_add_grob
: 添加一个grob
,可以跨越多行或多列 -
gtable_add_cols
: 在指定位置添加新列 -
gtable_add_rows
: 在指定位置添加新行 -
gtable_add_padding
: 在table
边界添加填充 -
gtable_add_col_space
和gtable_add_row_space
: 添加行/列间距 -
gtable_trim
: 删除空单元格 -
gtable_filter
: 通过名称来筛选单元格
3.1 gtable_add_grob
gtable_add_grob
可以把一个 grob
添加到 table
布局中,但是不会影响 table
布局
在 gtable
中,添加的对象会全部覆盖整个单元格,所以,如果你要设置对齐方式,则需要为 grob
设置绝对大小
使用方式
gtable_add_grob(x, grobs, t, l, b = t, r = l, z = Inf, clip = "on",
name = x$name)
来看下面这个例子,新建一个一行三列的 table
布局,每列的宽度分别为 1
、2
、3
a <- gtable(unit(1:3, c("cm")), unit(5, "cm"))
gtable_show_layout(a)
为第一列添加一个矩形
> rect <- rectGrob(gp = gpar(fill = "black"))
> a <- gtable_add_grob(a, rect, 1, 1)
> a
TableGrob (1 x 3) "layout": 1 grobs
z cells name grob
1 1 (1-1,1-1) layout rect[GRID.rect.236]
> plot(a)
可以使用 t
对布局进行转置
> dim(a)
[1] 1 3
> t(a)
TableGrob (3 x 1) "layout": 1 grobs
z cells name grob
1 1 (1-1,1-1) layout rect[GRID.rect.236]
> dim(t(a))
[1] 3 1
> plot(t(a))
布局索引
> b <- gtable(unit(c(2, 2, 2), "cm"), unit(c(2, 2, 2), "cm"))
> b <- gtable_add_grob(b, rect, 2, 2)
> b[1, ]
TableGrob (1 x 3) "layout": 0 grobs
> b[, 1]
TableGrob (3 x 1) "layout": 0 grobs
> b[2, 2]
TableGrob (1 x 1) "layout": 1 grobs
z cells name grob
1 1 (1-1,1-1) layout rect[GRID.rect.236]
> plot(b)
访问 gtable
对象的名称
> rownames(b) <- 1:3
> rownames(b)[2] <- 200
> colnames(b) <- letters[1:3]
> dimnames(b)
[[1]]
[1] 1 200 3
[[2]]
[1] "a" "b" "c"
3.2 gtable_add_cols
gtable_add_cols
函数可以为 gtable
对象插入新的列,并会调整相应的 grob
的位置。
如果在一个横跨多列的 grob
中间添加列,grob
将继续横跨所有列。如果列被添加到 grob
的左边或右边,grob
将不会跨越新的列。
gtable_add_cols(x, widths, pos = -1)
创建一个 3
行 3
列的 table
布局,并添加三个矩形对象
rect <- rectGrob(gp = gpar(fill = "#00000080"))
tab <- gtable(unit(rep(1, 3), "null"), unit(rep(1, 3), "null"))
# 矩形占据第一行
tab <- gtable_add_grob(tab, rect, t = 1, l = 1, r = 3)
# 矩形占据第一列
tab <- gtable_add_grob(tab, rect, t = 1, b = 3, l = 1)
# 矩形占据第三列
tab <- gtable_add_grob(tab, rect, t = 1, b = 3, l = 3)
plot(tab)
在中间添加一列
> tab2 <- gtable_add_cols(tab, unit(1, "null"), 1)
> dim(tab2)
[1] 3 4
> plot(tab2)
指定 pos
为 0
在左边添加列,-1
(默认)在右边添加列
tab3 <- gtable_add_cols(tab, unit(1, "null"))
tab3 <- gtable_add_cols(tab3, unit(1, "null"), 0)
> dim(tab3)
[1] 3 5
> plot(tab3)
gtable_add_rows
的左右是添加列,这里就不再赘述了
3.3 gtable_add_padding
gtable_add_padding
可以为 gtable
四周添加 padding
,使用方式
gtable_add_padding(x, padding)
padding
为长度为 4
的向量,分别表示上、右、下、左的 padding
,如果长度不足 4
,则会循环使用
例如,对于这样一个只包含一个以矩形对象填充的单元格的布局
gt <- gtable(unit(1, "null"), unit(1, "null"))
gt <- gtable_add_grob(gt, rectGrob(gp = gpar(fill = "black")), 1, 1)
plot(gt)
可以使用 rbind
或 cbind
来连接两个 gtable
对象,绘制的图形根本看不出有两个图形
plot(cbind(gt, gt))
为 gtable
添加 padding
,四周的 padding
都是 1cm
pad <- gtable_add_padding(gt, unit(1, "cm"))
plot(pad)
plot(rbind(pad, pad))
plot(cbind(pad, pad))
3.4 gtable_add_col/row_space
为网格的单元格之间添加行距和列距
gtable_add_col_space(x, width)
gtable_add_row_space(x, height)
创建矩阵布局,每个单元格放置一个矩形对象
rect <- rectGrob()
rect_mat <- matrix(rep(list(rect), 9), nrow = 3)
gt <- gtable_matrix(
"rects", rect_mat, widths = unit(rep(1, 3), "null"),
heights = unit(rep(1, 3), "null")
)
plot(gt)
添加行列间距
# 行距 0.5cm
gt <- gtable_add_row_space(gt, unit(0.5, "cm"))
# 列距分别为 0.5cm、1cm
gt <- gtable_add_col_space(gt, unit(c(0.5, 1), "cm"))
plot(gt)
3.5 gtable_trim
gtable_trim
函数用于删除不包含 grob
的行或列,如果删除的行和列的高度或宽度为 0
,则有可能会改变整个布局
rect <- rectGrob(gp = gpar(fill = "black"))
base <- gtable(unit(c(2, 2, 2), "cm"), unit(c(2, 2, 2), "cm"))
center <- gtable_add_grob(base, rect, 2, 2)
plot(center)
gtable_show_layout(center)
删除空行、空列
plot(gtable_trim(center))
最后只剩下一个单元格
gtable_show_layout(gtable_trim(center))
3.6 gtable_filter
通常来说,gtable
对象在索引时可以将其当做一个矩阵,gtable_filter
可以根据名称对 grob
进行筛选
gtable_filter(x, pattern, fixed = FALSE, trim = TRUE, invert = FALSE)
-
pattern
: 可以是正则表达式,匹配grob
名称 -
fixed
: 如果为TRUE
,关闭正则匹配 -
trim
: 如果为TRUE
,调用gtable_trim
-
invert
: 是否逆向匹配
创建 1
行 3
列的布局,并在第一列绘制矩形,第三列绘制圆形
gt <- gtable(unit(rep(5, 3), c("cm")), unit(5, "cm"))
rect <- rectGrob(gp = gpar(fill = "black"))
circ <- circleGrob(gp = gpar(fill = "red"))
gt <- gtable_add_grob(gt, rect, 1, 1, name = "rect")
gt <- gtable_add_grob(gt, circ, 1, 3, name = "circ")
plot(gt)
只显示矩形
plot(gtable_filter(gt, "rect"))
只显示圆形
plot(gtable_filter(gt, "circ"))
不删除空列
plot(gtable_filter(gt, "circ", trim = FALSE))
介绍完了 gtable
之后,下一节将介绍如何绘制多个 Y
轴