R 数据可视化 —— grid 绘图系统
Rbase R传统图像系统grid图像系统传统的图像系统是由graphics包所提供的一系列函数组成,grid系统是grid包提供的grid包是一个底层的绘图系统,提供的都是底层的绘图函数,没有用于绘制复杂图形的高级函数。像ggplot2和lattice两个顶层的绘图包都是基于grid系统的,所以,了解grid包对于理解ggplot2的顶层函数的工作方式是很有帮助的同时,也可以使用grid包来灵活
前言
R 中主要存在两种绘图系统:
base R传统图像系统grid图像系统
传统的图像系统是由 graphics 包所提供的一系列函数组成,grid 系统是 grid 包提供的
grid 包是一个底层的绘图系统,提供的都是底层的绘图函数,没有用于绘制复杂图形的高级函数。
像 ggplot2 和 lattice 两个顶层的绘图包都是基于 grid 系统的,所以,了解 grid 包对于理解 ggplot2 的顶层函数的工作方式是很有帮助的
同时,也可以使用 grid 包来灵活地控制图形的外观和布局
安装导入
install.packages("grid")
library(grid)
grid 图像模型
1. 图形原语
grid 提供了一些函数用于绘制简单的图形,例如
这些函数被称为图形原语,使用这些函数可以直接绘制对应的图形,例如
grid.text(label = "Let's us begin!")

grid.circle(
x=seq(0.1, 0.9, length=100),
y=0.5 + 0.4*sin(seq(0, 2*pi, length=100)),
r=abs(0.1*cos(seq(0, 2*pi, length=100)))
)

2. 坐标系统
grid 的坐标系统是用来确定数值的单位,同样的数值在不同的单位中表示不同的大小,看起来叫单位系统应该会更恰当些
坐标系统如下
使用 unit 函数来设置不同的系统
> unit(1, "cm")
[1] 1cm
> unit(1:4, "mm")
[1] 1mm 2mm 3mm 4mm
> unit(1:4, c("npc", "mm", "native", "lines"))
[1] 1npc 2mm 3native 4lines
坐标系统之间的运算将会以表达式的方式返回
> unit(1:4, "mm")[1] - unit(1:4, "mm")[4]
[1] 1mm-4mm
> unit(1, "npc") - unit(1:4, "mm")
[1] 1npc-1mm 1npc-2mm 1npc-3mm 1npc-4mm
> max(unit(1:4, c("npc", "mm", "native", "lines")))
[1] max(1npc, 2mm, 3native, 4lines)
对于字符串及对象长度坐标系统
> unit(1, "strwidth", "some text")
[1] 1strwidth
> unit(1, "grobwidth", textGrob("some text"))
[1] 1grobwidth
有对应的简便函数可以使用
> stringHeight("some text")
[1] 1strheight
> grobHeight(textGrob("some text"))
[1] 1grobheight
可以使用 convertWidth 和 convertHeight 实现单位之间的转换
> convertHeight(unit(1, "cm"), "mm")
[1] 10mm
> convertHeight(unit(1, "dida"), "points")
[1] 1.07000864304235points
> convertHeight(unit(1, "cicero"), "points")
[1] 12.8401037165082points
> convertHeight(unit(1, "cicero"), "dida")
[1] 12dida
> convertHeight(unit(1, "points"), "scaledpts")
[1] 65536scaledpts
> convertWidth(stringWidth("some text"), "lines")
[1] 3.61246744791667lines
> convertWidth(stringWidth("some text"), "inches")
[1] 0.722493489583333inches
对于一个图形对象,如果修改了图形对象属性,则对应的大小也会改变
> grid.text("some text", name="tgrob")
> convertWidth(grobWidth("tgrob"), "inches")
[1] 0.722493489583333inches
# 修改图形对象的 fontsize 属性
> grid.edit("tgrob", gp=gpar(fontsize=18))
> convertWidth(grobWidth("tgrob"), "inches")
[1] 1.083740234375inches
我们可以使用不同的单位系统来绘制一个矩形
grid.rect(
x=unit(0.5, "npc"),
y=unit(1, "inches"),
width=stringWidth("very snug"),
height=unit(1, "lines"),
just=c("left", "bottom")
)
3. gpar
所有的图形原语函数都有一个 gp(graphical parameters) 参数,用来接收一个 gpar 对象,该对象包含一些图形参数用于控制图像的输出
gpar 对象可以使用 gpar() 函数来生成,例如
> gpar(col="red", lty="dashed")
$col
[1] "red"
$lty
[1] "dashed"
这些图形参数包括

使用 get.gpar 可以获取当前图形参数的值,如果未指定要获取的参数,将会返回所有的参数值
> get.gpar(c("lty", "fill"))
$lty
[1] "solid"
$fill
[1] "white"
因此,我们可以在绘制图像时,传递 gp 参数来设置图像参数
grid.rect(
x=0.66,
height=0.7,
width=0.2,
gp=gpar(fill="blue")
)
grid.rect(
x=0.33,
height=0.7,
width=0.2
)

在 grid 中,cex 参数是累积的,也就是说当前的 cex 值等于当前设置的值乘上之前的 cex 值
例如
pushViewport(viewport(gp=gpar(cex=0.5)))
grid.text("How small do you think?", gp=gpar(cex=0.5))
在一个 viewport 中设置了 cex = 0.5,之后的文本又设置了 cex = 0.5,最后文本的大小就是 0.5*0.5 = 0.25
alpha 参数与 cex 类似,也是累积的
注意: 这些图形参数都可以接受一个向量值,比如,你可以将一个颜色向量传递给 col 或 fill 参数,如果向量的长度小于绘制的图形的个数,则参数会进行循环赋值
如,我们绘制 100 个圆形,但是只传递了一个长度为 50 的颜色向量给 col 参数
grid.circle(
x = seq(0.1, 0.9, length=100),
y = 0.5 + 0.4*sin(seq(0, 2*pi, length=100)),
r = abs(0.1*cos(seq(0, 2*pi, length=100))),
gp = gpar(col=rainbow(50))
)

对于多边形 grid.polygon() 函数,有一个 id 参数可以将多边形的点进行分组,如果某一分组点中包含 NA 值,则又会将在 NA 处将点分为两组
# 设置均等分的角度,并删除最后一个角度
angle <- seq(0, 2*pi, length=11)[-11]
grid.polygon(
x = 0.25 + 0.2*cos(angle),
y = 0.5 + 0.3*sin(angle),
id = rep(1:2, c(7, 3)),
gp = gpar(
fill=c("grey", "white")
)
)
# 将其中一个角度设置为 NA
angle[4] <- NA
grid.polygon(
x = 0.75 + 0.2*cos(angle),
y = 0.5 + 0.3*sin(angle),
id = rep(1:2, c(7, 3)),
gp = gpar(
fill=c("grey", "white")
)
)

从图中可以看出,本来根据 id 值分为两组,第一组为灰色填充,第二组为白色填充。
但是在添加 NA 之后,在 NA 处将 id 为 1 的分组又一分为二,但是填充色还是灰色,并不是接续白色
4. viewport
在 grid 中,图像的绘制需要在画布中执行,也就是在绘制图像时需要新建一个画布
grid.newpage()
通常使用 grid.newpage() 函数来新建一个空白画布
在画布中,又可以定义很多个独立的矩形绘图窗口,在每个矩形窗口中都可以绘制任意你想要绘制的内容,这样的窗口就是 viewport
默认情况下,整个画布就是一个 viewport,如果新增一个 viewport,那么默认会继承所有默认的图形参数值
使用 viewport() 函数来新建一个 viewport,并接受位置参数(x 和 y) 和大小参数(width 和 height),以及对齐方式(just)
> viewport(
+ x = unit(0.4, "npc"),
+ y = unit(1, "cm"),
+ width = stringWidth("very very snug indeed"),
+ height = unit(6, "lines"),
+ just = c("left", "bottom")
+ )
viewport[GRID.VP.4]
viewport() 函数返回的是一个 viewport 对象,但其实你会发现,什么东西都没有画出来
因为,创建了一个 viewport 对象区域之后,需要将其 push 到图像设备中
其位置大致应该是这样的
4.1 viewport 的切换
pushViewport() 函数可以将一个 viewport 对象 push 到图像设备中,例如
grid.text(
"top-left corner",
x=unit(1, "mm"),
y=unit(1, "npc") - unit(1, "mm"),
just=c("left", "top")
)
pushViewport(
viewport(
width=0.8,
height=0.5,
angle=10,
name="vp1"
)
)
grid.rect()
grid.text(
"top-left corner",
x = unit(1, "mm"),
y = unit(1, "npc") - unit(1, "mm"),
just = c("left", "top")
)

我们在最外层画布的左上角添加一串文本,然后添加一个 viewport,同时绘制外侧矩形框,并旋转 10 度,也在左上角添加一串文本
在当前 viewport 的基础上,还可以在新建 viewport,新 push 的 viewport 将会相对于当前 viewport 的位置来放置
pushViewport(
viewport(
width=0.8,
height=0.5,
angle=10,
name="vp2"
)
)
grid.rect()
grid.text(
"top-left corner",
x = unit(1, "mm"),
y = unit(1, "npc") - unit(1, "mm"),
just = c("left", "top")
)

每次 push 一个 viewport 之后,都会将该 viewport 作为当前活动的窗口,如果要回滚到之前的 viewport,可以使用 popViewport() 函数,该函数会将当前活动窗口删除
popViewport()
grid.text(
"bottom-right corner",
x=unit(1, "npc") - unit(1, "mm"),
y=unit(1, "mm"),
just=c("right", "bottom")
)

从图片中可以看到,活动窗口已经切换到第二个 viewport,并将文本绘制在其右下角
popViewport() 还可接受一个参数 n,用于指定需要 pop 几个 viewport。默认 n = 1,传递更大的值可以跳转到更上层的 viewport,如果设置为 0 则会返回到最外层图形设备上。
另一个更改活动窗口的方法是,使用 upViewport() 和 downViewport() 函数。
upViewport() 函数与 popViewport() 类似,不同之处在于,upViewport() 函数不会删除当前活动 viewport。
这样,在重新访问之前的 viewport 时,不用再 push 一遍,而且能够提升访问的速度。
重新访问 viewport 使用的是 downViewport() 函数,通过 name 参数来选择指定的 viewport
# 切换到最外层
upViewport()
# 在右下角添加文本
grid.text(
"bottom-right corner",
x=unit(1, "npc") - unit(1, "mm"),
y=unit(1, "mm"),
just=c("right", "bottom")
)
# 返回 vp1
downViewport("vp1")
# 添加外侧框线
grid.rect(
width=unit(1, "npc") + unit(2, "mm"),
height=unit(1, "npc") + unit(2, "mm"),
gp = gpar(fill = NA)
)

如果想要访问 vp2 会报错,不存在该 viewport
> downViewport("vp2")
Error in grid.Call.graphics(C_downviewport, name$name, strict) :
Viewport 'vp2' was not found
还可以直接使用 seekViewport() 函数来切换到指定名称的 viewport
4.2 裁剪 viewport
我们可以将图形限制在当前 viewport 之内,如果绘制的图形大小超过了当前 viewport 则不会显示,我们可以使用 clip 参数
该参数接受三个值:
on:输出的图形必须保持在当前viewport内,超出的部分会被裁剪inherit:继承上一个viewport的clip值off:不会被裁剪
例如
grid.newpage()
# 在画布中心添加一个 viewport,并设置允许剪切
pushViewport(viewport(w=.5, h=.5, clip="on"))
# 添加矩形框和线条很粗的圆形
grid.rect(
gp = gpar(fill = "#8dd3c7")
)
grid.circle(
r = .7,
gp = gpar(
lwd = 20,
col = "#fdb462"
)
)
# 在当前 viewport 中添加一个 viewport,继承方式
pushViewport(viewport(clip="inherit"))
# 添加线条更细一点的圆形
grid.circle(
r = .7,
gp = gpar(
lwd = 10,
col = "#80b1d3",
fill = NA)
)
# 关闭裁剪
pushViewport(viewport(clip="off"))
# 显示整个圆形
grid.circle(
r=.7,
gp = gpar(
fill = NA,
col = "#fb8072"
)
)

只有最后一个圆显示出了全部,前面两个圆形只显示在 viewport 内的部分
4.3 viewport 的排列
viewport 的排布方式有三种:
vpList:viewport列表,以平行的方式排列各viewportvpStack:以堆叠的方式排列,俗称套娃,与使用pushViewport功能相似vpTree:以树的方式排列,一个根节点可以有任意个子节点
例如,我们新建三个 viewport
vp1 <- viewport(name="A")
vp2 <- viewport(name="B")
vp3 <- viewport(name="C")
然后,我们以列表的方式将这些 viewport push 到图形设备中
pushViewport(vpList(vp1, vp2, vp3))
可以使用 current.vpTree 函数来查看当前的 viewport 排列树
> current.vpTree()
viewport[ROOT]->(viewport[A], viewport[B], viewport[C])
可以看到,这三个 viewport 是并列的关系
我们再看看以堆叠的方式放置
> grid.newpage()
> pushViewport(vpStack(vp1, vp2, vp3))
> current.vpTree()
viewport[ROOT]->(viewport[A]->(viewport[B]->(viewport[C])))
可以看到,根节点是整个画布,画布的子节点是 A,A 的子节点是 B,B 的子节点是 C,这就是堆叠的方式,一个套一个
那对于树形排列也就不难理解了
> grid.newpage()
> pushViewport(vpTree(vp1, vpList(vp2, vp3)))
> current.vpTree()
viewport[ROOT]->(viewport[A]->(viewport[B], viewport[C]))
根节点是整个画布,然后是子节点 A,A 的子节点是 B、C
我们知道,画布中的所有 viewport 是以树的方式存储的,那么我们就可以根据 viewport 的父节点来定位某一个 viewport
例如,我们想查找名称 C 的 viewport,其父节点为 B,再上层父节点为 A,则可以使用 vpPath 函数来构造检索路径
> vpPath("A", "B", "C")
A::B::C
同时也可以消除同名 viewport 的干扰
4.4 将 viewport 作为图形原语的参数
每个原语函数都有一个 vp 参数
例如,在一个 viewport 中绘制文本
vp1 <- viewport(width=0.5, height=0.5, name="vp1")
pushViewport(vp1)
grid.text("Text drawn in a viewport")
popViewport()
也可以下面的代码代替,将文本绘制到指定的 viewport 中
grid.text("Text drawn in a viewport", vp=vp1)
4.5 viewport 的图形参数
viewport 也有一个 gp 参数,用来设置图形属性,设置的值将会作为 viewport 中所有的图形对象的默认值
grid.newpage()
pushViewport(
viewport(
gp = gpar(fill="grey")
)
)
grid.rect(
x = 0.33,
height = 0.7,
width = 0.2
)
grid.rect(
x = 0.66,
height = 0.7,
width = 0.2,
gp = gpar(fill="black")
)
popViewport()

4.6 布局
viewport 的 layout 参数可以用来设置布局,将 viewport 区域分割成不同的行和列,行之间可以有不同的高度,列之间可以有不同的宽度。
grid 布局使用 grid.layout() 函数来构造,例如
vplay <- grid.layout(
nrow = 3,
ncol = 3,
respect=rbind(
c(0, 0, 0),
c(0, 1, 0),
c(0, 0, 0))
)
我们构造了一个 3 行 3 列的布局,中间的位置是一个正方形
构造了布局之后,就可以添加到 viewport 中了
pushViewport(viewport(layout=vplay))
我们可以使用 layout.pos.col 和 layout.pos.row 参数来指定 viewport 放置的位置
# 新建一个 viewport 并放置在第二列
pushViewport(
viewport(
layout.pos.col = 2,
name = "col2")
)
grid.rect(
gp = gpar(
lwd = 10,
col = "black",
fill = NA
))
grid.text(
label = "col2",
x = unit(1, "mm"),
y = unit(1, "npc") - unit(1, "mm"),
just = c("left", "top")
)
upViewport()
# 新建一个 viewport 并放置在第二行
pushViewport(
viewport(
layout.pos.row = 2,
name = "row2")
)
grid.rect(
gp = gpar(
lwd = 10,
col = "grey",
fill = NA
))
grid.text(
x = unit(1, "mm"),
y = unit(1, "npc") - unit(1, "mm"),
label = "row2",
just = c("left", "top")
)
也可以使用 unit 来设置行列的高度和宽度,例如
unitlay <- grid.layout(
nrow = 3,
ncol = 3,
widths = unit(
c(1, 1, 2),
c("inches", "null", "null")
),
heights = unit(
c(3, 1, 1),
c("lines", "null", "null"))
)
我们定义了一个 3 行 3 列的布局,列宽通过 widths 分配,即第一列宽度为 1 inches,剩下的两列的宽度的占比为 1:2
行高通过 heights 分配,第一行为 3 个 lines 单位,剩下的两行高度为 1:1
布局应该是下图这样子的
grid 布局也可以嵌套
假设我们有这样一个,1 行 2 列的 viewport
gridfun <- function() {
# 1*2 的布局
pushViewport(viewport(layout=grid.layout(1, 2)))
# 第一行第一列的 viewport
pushViewport(viewport(layout.pos.col=1))
# 绘制矩形和文本
grid.rect(gp = gpar(fill = "#80b1d3"))
grid.text("black")
grid.text("&", x=1)
popViewport()
# 第一行第二列的 viewport
pushViewport(viewport(layout.pos.col=2, clip="on"))
grid.rect(gp=gpar(fill="#fb8072"))
grid.text("white", gp=gpar(col="white"))
grid.text("&", x=0, gp=gpar(col="white"))
popViewport(2)
}

新建一个 5 行 5 列的 viewport
pushViewport(
viewport(
layout = grid.layout(
nrow = 5,
ncol = 5,
widths=unit(
c(5, 1, 5, 2, 5),
c("mm", "null", "mm", "null", "mm")),
heights=unit(
c(5, 1, 5, 2, 5),
c("mm", "null", "mm", "null", "mm"))
)
)
)
然后,分别在 2 行 2 列和 4 行 4 列 中放置一个 viewport
pushViewport(
viewport(
layout.pos.col=2,
layout.pos.row=2)
)
gridfun()
popViewport()
pushViewport(
viewport(
layout.pos.col=4,
layout.pos.row=4)
)
gridfun()
popViewport(2)

图形对象
在前面一节中,我们主要介绍了如何使用 grid 来生成图形输出,以及图形窗口的布局。利用这些知识,可以很容易地为图形添加注释,编写一些简单的绘图函数
这一节,我们将重点介绍如何使用 grid 函数来创建和操作图形对象。利用这些信息,可以交互式地编辑和修改图形输出
1. 控制图像输出
我们可以使用图形原语来绘制图形输出,并返回一个图形对象(grobs),例如
library(RColorBrewer)
grid.circle(
name = "circles",
x = seq(0.1, 0.9, length = 40),
y = 0.5 + 0.4 * sin(seq(0, 2 * pi, length = 40)),
r = abs(0.1 * cos(seq(0, 2 * pi, length = 40))),
gp = gpar(col = brewer.pal(40, "Set2"))
)
这段代码将会绘制一串圆形
同时也会生成一个 circle grob,该对象保存了当前绘制的这些圆形的信息
grid 保留了一个显示列表,用于记录当前画布中的所有的 viewport 和 grobs。因此,grid.circle() 函数构造的对象也会保存在显示列表中,意味着我们可以根据对象的名称 circles 来获取、修改该对象
使用 grid.get() 函数,可以获取该 circle 对象的拷贝
> grid.get("circles")
circle[circles]
使用 grid.edit() 可以用来修改该 circle 对象的图像属性
grid.edit(
"circles",
gp = gpar(
col = brewer.pal(10, "RdBu")
)
)

修改颜色属性之后,会直接显示在图形输出中
还可以使用 grid.remove() 函数,从显示列表中删除图形对象的输出
grid.remove("circles")

一片空白,什么也没有
1.1 标准的函数及参数
控制 grobs 的函数包括:
所有图像输出函数的第一个参数都是图像对象的名称,如果参数 grep = TRUE,可以接受正则表达式对象名称
如果 global = TRUE,则会返回显示列表中所有匹配的对象,例如
suffix <- c("even", "odd")
for (i in 1:8)
grid.circle(
name = paste0("circle.", suffix[i %% 2 + 1]),
r = (9 - i) / 20,
gp = gpar(
col = NA,
fill = grey(i / 10)
)
)

我们绘制了 8 个同心圆,并根据奇偶顺序将 circle grob 命名为 circle.odd 和 circle.even
然后,我们可以使用 grid.edit() 函数,修改所有名为 circle.odd 的 grobs 的颜色
grid.edit(
"circle.odd",
gp = gpar(
fill = brewer.pal(4, "Set3")[4]),
global = TRUE
)

或者,用正则表达式来匹配以 circle 开头的 grob
grid.edit(
"circle",
gp = gpar(
col = "#80b1d3",
fill = "#fdb462"
),
grep=TRUE,
global=TRUE
)

只要我们知道了 grobs 的名称,就可以对其获取、修改或删除
而 getNames() 函数,可以帮助我们获取当前图形中所有 grobs 的名称
2. grob 排布结构
grob 的排布结果包括:
gList:包含多个grobs的listgTree:grobs的树形结构,即一个grob中包含其他的grob
例如,对于 xaxis grob
pushViewport(viewport(y = 0.5, height = 0.5, width = 0.5))
grid.rect()
pushViewport(viewport(y = 0.5, height = 0.5, width = 0.5))
grid.xaxis(name="axis1", at=1:4/5)

会包含有多个子 grob,如线条,文本等
> childNames(grid.get("axis1"))
[1] "major" "ticks" "labels"
如果把 xaxis grob 看作为一棵树的根,那么它包含三个子 grob。
其中 major 和 ticks 是 lines grob,labels 为 text grob。
其中 at 参数设置了轴刻度,我们可以使用 grid.edit 来修改
grid.edit("axis1", at=1:3/4)

那想要修改 labels 的格式,怎么办?即如何访问一个对象的子对象呢?
可以使用 gPath(grob path) 函数,类似于 vpPath,可以使用父节点名称加子节点名称来访问
grid.edit(gPath("axis1", "labels"), rot=45)

或者,也可以使用 axis1::labels 方式来访问
注意:grobs 的搜索是深度优先,也就是说,如果在显示列表中遍历到了一个 gTree grob,且未找到匹配项,则会对该 grob 执行深度优先遍历
这种 gTree 结构对象,也包含 gp 和 vp 参数。
在父节点上设置对应的 gp 参数值,会作为默认值传递给子对象。例如
grid.xaxis(gp=gpar(col="grey"))
也可以将一个 viewport 直接作为参数值传递
grid.xaxis(vp=viewport(y=0.75, height=0.5))
3. 图形对象
在前面的章节中,我们介绍的都是如何使用函数直接生成图形输出并返回图形对象(grob)
在这一节,我们将介绍如果创建 grob,但不绘制图形,通过对 grob 创建及修改,并在最后使用 grid.draw() 函数来绘制出图形。
每个能产生图形输出和图形对象的 grid 函数都有一个对应的只创建图形对象,没有图形输出的函数
例如,grid.circle() 对应于 circleGrob(),grid.edit() 对应于 editGrob(),在前面的函数表中都有列出
例如
grid.newpage()
pushViewport(viewport(width = 0.5, height = 0.5))
# 创建 x 轴对象
ag <- xaxisGrob(at=1:4/5)
# 修改对象,将标签的字体变为斜体
ag <- editGrob(ag, "labels", gp=gpar(fontface="italic"))
# 绘图
grid.draw(ag)

我们可以将不同的 grob 组合在一起,生成一个复杂的图形。比如
grid.newpage()
tg <- textGrob("sample text")
rg <- rectGrob(
width = 1.2*grobWidth(tg),
height = 1.5*grobHeight(tg)
)
boxedText <- gTree(
children = gList(rg, tg)
)
我们构建一个名为 boxedText 的 gTree 对象,包含其子对象包括一个文本和一个矩形
我们直接可以绘制组合对象
grid.draw(boxedText)

而对该对象的图形属性的修改,会反映到具体的子对象中
grid.draw(
editGrob(
boxedText,
gp=gpar(col="skyblue")
)
)

指定 viewport
grid.draw(
editGrob(
boxedText,
vp = viewport(angle=45),
gp = gpar(fontsize=18)
)
)

3.1 捕获输出
在上面的例子中,我们先构建了一个组合对象,然后绘制该对象
还可以反着来,先绘制图形对象,然后对它们进行组合。
使用 grid.grab() 函数,可以获取当前画布中所有输出的图形对象,并以 gTree 的形式返回
例如,我们使用 ggplot2 绘制一个直方图,并获取所有图形对象
ggplot(mpg) + geom_histogram(aes(displ, fill = class), bins = 10, position = "dodge")
histTree <- grid.grab()
然后,你可以尝试运行下面的代码
grid.newpage()
grid.draw(histTree)
你会发现,可以绘制出一张一模一样的图
也可以使用 grid.grabExpr 来获取表达式的输出图形对象
grid.grabExpr(
print(
ggplot(mpg) +
geom_histogram(
aes(displ, fill = class),
bins = 10,
position = "dodge")
)
)
4. 图形对象的放置
假设我们有一个复杂图形
# 文本对象
label <- textGrob(
"A\nPlot\nLabel ",
x = 0,
just = "left"
)
x <- seq(0.1, 0.9, length=50)
y <- runif(50, 0.1, 0.9)
# gTree 结构图形对象,包括矩形、线图、点图
gplot <- gTree(
children = gList(
rectGrob(
gp = gpar(
col = "grey60",
fill = "#cbd5e8",
alpha = 0.3)
),
linesGrob(
x,
y,
gp = gpar(
col = "#33a02c"
)),
pointsGrob(
x, y,
pch = 16,
size = unit(5, "mm"),
gp = gpar(
col = "#fb8072"
))
),
vp = viewport(
width = unit(1, "npc") - unit(5, "mm"),
height = unit(1, "npc") - unit(5, "mm")
)
)
我们可以使用上一章节提到的布局方法,将该图像设计为 1 行 2 列的布局
layout <- grid.layout(
nrow = 1,
ncol = 2,
widths = unit(
c(1, 1),
c("null", "grobwidth"),
list(NULL, label)
)
)
然后将图形绘制到指定位置中
pushViewport(viewport(layout=layout))
pushViewport(viewport(layout.pos.col=2))
grid.draw(label)
popViewport()
pushViewport(viewport(layout.pos.col=1))
grid.draw(gplot)
popViewport(2)

但其实,grid 提供了更简便的函数用于放置 grobs
grid.frame() 函数创建一个没有子对象的 gTree,可以使用 grid.pack() 向其中添加子对象,同时确保为每个子对象保留足够的绘图空间
上面的代码可以改写成
grid.newpage()
# 新建一个空 frame
grid.frame(name="frame1")
# 放置 gplot 对象,在这一阶段,gplot 会占据整个 frame
grid.pack("frame1", gplot)
# 在 frame 的右边放置 label 对象
grid.pack("frame1", label, side="right")
这种动态的方式很简便,但是也带来了时间上的花费,随着需要放置的对象越来越多,速度会越来越慢。
另一种替代的方式是,先定义一个布局,然后再放置对象
grid.frame(name="frame1", layout=layout)
grid.place("frame1", gplot, col=1)
grid.place("frame1", label, col=2)
4.1 安静模式
在上面两个例子中,每次放置一个 grob 都会更新一遍图形输出。所以,一个更好的方式是,在安静模式下创建一个 frame,然后放置 grobs。
安静模式,即使用对象函数 frameGrob() 和 placeGrob()/packGrob 创建 frame、放置 grobs,但是不会输出图形,只有在所有设置完成之后,使用 grid.draw 一次性绘制
# 创建 frame
fg <- frameGrob(layout=layout)
# 添加 grob
fg <- placeGrob(fg, gplot, col=1)
fg <- placeGrob(fg, label, col=2)
# 一次性绘制
grid.draw(fg)
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)