前言
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)