前言
在前面一节中,我们主要介绍了如何使用 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)
网友评论