前言
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
列表,以平行的方式排列各viewport
vpStack
:以堆叠的方式排列,俗称套娃,与使用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
的list
gTree
: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)