前言
R
中主要存在兩種繪圖系統(tǒng):
-
base R
傳統(tǒng)圖像系統(tǒng) -
grid
圖像系統(tǒng)
傳統(tǒng)的圖像系統(tǒng)是由 graphics
包所提供的一系列函數(shù)組成黍匾,grid
系統(tǒng)是 grid
包提供的
grid
包是一個(gè)底層的繪圖系統(tǒng),提供的都是底層的繪圖函數(shù)踢步,沒有用于繪制復(fù)雜圖形的高級函數(shù)。
像 ggplot2
和 lattice
兩個(gè)頂層的繪圖包都是基于 grid
系統(tǒng)的鲁沥,所以酒唉,了解 grid
包對于理解 ggplot2
的頂層函數(shù)的工作方式是很有幫助的
同時(shí),也可以使用 grid
包來靈活地控制圖形的外觀和布局
安裝導(dǎo)入
install.packages("grid")
library(grid)
grid 圖像模型
1. 圖形原語
grid
提供了一些函數(shù)用于繪制簡單的圖形软族,例如
這些函數(shù)被稱為圖形原語,使用這些函數(shù)可以直接繪制對應(yīng)的圖形残制,例如
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. 坐標(biāo)系統(tǒng)
grid
的坐標(biāo)系統(tǒng)是用來確定數(shù)值的單位互订,同樣的數(shù)值在不同的單位中表示不同的大小,看起來叫單位系統(tǒng)應(yīng)該會(huì)更恰當(dāng)些
坐標(biāo)系統(tǒng)如下
使用 unit
函數(shù)來設(shè)置不同的系統(tǒng)
> 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
坐標(biāo)系統(tǒng)之間的運(yùn)算將會(huì)以表達(dá)式的方式返回
> 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)
對于字符串及對象長度坐標(biāo)系統(tǒng)
> unit(1, "strwidth", "some text")
[1] 1strwidth
> unit(1, "grobwidth", textGrob("some text"))
[1] 1grobwidth
有對應(yīng)的簡便函數(shù)可以使用
> stringHeight("some text")
[1] 1strheight
> grobHeight(textGrob("some text"))
[1] 1grobheight
可以使用 convertWidth
和 convertHeight
實(shí)現(xiàn)單位之間的轉(zhuǎn)換
> 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
對于一個(gè)圖形對象痘拆,如果修改了圖形對象屬性,則對應(yīng)的大小也會(huì)改變
> 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
我們可以使用不同的單位系統(tǒng)來繪制一個(gè)矩形
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
所有的圖形原語函數(shù)都有一個(gè) gp(graphical parameters)
參數(shù)氮墨,用來接收一個(gè) gpar
對象纺蛆,該對象包含一些圖形參數(shù)用于控制圖像的輸出
gpar
對象可以使用 gpar()
函數(shù)來生成,例如
> gpar(col="red", lty="dashed")
$col
[1] "red"
$lty
[1] "dashed"
這些圖形參數(shù)包括
使用 get.gpar
可以獲取當(dāng)前圖形參數(shù)的值规揪,如果未指定要獲取的參數(shù)桥氏,將會(huì)返回所有的參數(shù)值
> get.gpar(c("lty", "fill"))
$lty
[1] "solid"
$fill
[1] "white"
因此,我們可以在繪制圖像時(shí)猛铅,傳遞 gp
參數(shù)來設(shè)置圖像參數(shù)
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
參數(shù)是累積的,也就是說當(dāng)前的 cex
值等于當(dāng)前設(shè)置的值乘上之前的 cex
值
例如
pushViewport(viewport(gp=gpar(cex=0.5)))
grid.text("How small do you think?", gp=gpar(cex=0.5))
在一個(gè) viewport
中設(shè)置了 cex = 0.5
,之后的文本又設(shè)置了 cex = 0.5
堕伪,最后文本的大小就是 0.5*0.5 = 0.25
alpha
參數(shù)與 cex
類似揖庄,也是累積的
注意: 這些圖形參數(shù)都可以接受一個(gè)向量值,比如欠雌,你可以將一個(gè)顏色向量傳遞給 col
或 fill
參數(shù)蹄梢,如果向量的長度小于繪制的圖形的個(gè)數(shù),則參數(shù)會(huì)進(jìn)行循環(huán)賦值
如富俄,我們繪制 100
個(gè)圓形禁炒,但是只傳遞了一個(gè)長度為 50
的顏色向量給 col
參數(shù)
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()
函數(shù),有一個(gè) id
參數(shù)可以將多邊形的點(diǎn)進(jìn)行分組霍比,如果某一分組點(diǎn)中包含 NA
值幕袱,則又會(huì)將在 NA
處將點(diǎn)分為兩組
# 設(shè)置均等分的角度,并刪除最后一個(gè)角度
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")
)
)
# 將其中一個(gè)角度設(shè)置為 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")
)
)
從圖中可以看出悠瞬,本來根據(jù) id
值分為兩組们豌,第一組為灰色填充,第二組為白色填充阁危。
但是在添加 NA
之后玛痊,在 NA
處將 id
為 1
的分組又一分為二,但是填充色還是灰色狂打,并不是接續(xù)白色
4. viewport
在 grid
中擂煞,圖像的繪制需要在畫布中執(zhí)行,也就是在繪制圖像時(shí)需要新建一個(gè)畫布
grid.newpage()
通常使用 grid.newpage()
函數(shù)來新建一個(gè)空白畫布
在畫布中趴乡,又可以定義很多個(gè)獨(dú)立的矩形繪圖窗口对省,在每個(gè)矩形窗口中都可以繪制任意你想要繪制的內(nèi)容,這樣的窗口就是 viewport
默認(rèn)情況下晾捏,整個(gè)畫布就是一個(gè) viewport
蒿涎,如果新增一個(gè) viewport
,那么默認(rèn)會(huì)繼承所有默認(rèn)的圖形參數(shù)值
使用 viewport()
函數(shù)來新建一個(gè) viewport
惦辛,并接受位置參數(shù)(x
和 y
) 和大小參數(shù)(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()
函數(shù)返回的是一個(gè) viewport
對象,但其實(shí)你會(huì)發(fā)現(xiàn)胖齐,什么東西都沒有畫出來
因?yàn)椴J纾瑒?chuàng)建了一個(gè) viewport
對象區(qū)域之后,需要將其 push
到圖像設(shè)備中
其位置大致應(yīng)該是這樣的
4.1 viewport 的切換
pushViewport()
函數(shù)可以將一個(gè) viewport
對象 push
到圖像設(shè)備中呀伙,例如
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")
)
我們在最外層畫布的左上角添加一串文本补履,然后添加一個(gè) viewport
,同時(shí)繪制外側(cè)矩形框剿另,并旋轉(zhuǎn) 10
度箫锤,也在左上角添加一串文本
在當(dāng)前 viewport
的基礎(chǔ)上贬蛙,還可以在新建 viewport
,新 push
的 viewport
將會(huì)相對于當(dāng)前 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
一個(gè) viewport
之后谚攒,都會(huì)將該 viewport
作為當(dāng)前活動(dòng)的窗口阳准,如果要回滾到之前的 viewport
,可以使用 popViewport()
函數(shù)五鲫,該函數(shù)會(huì)將當(dāng)前活動(dòng)窗口刪除
popViewport()
grid.text(
"bottom-right corner",
x=unit(1, "npc") - unit(1, "mm"),
y=unit(1, "mm"),
just=c("right", "bottom")
)
從圖片中可以看到溺职,活動(dòng)窗口已經(jīng)切換到第二個(gè) viewport
,并將文本繪制在其右下角
popViewport()
還可接受一個(gè)參數(shù) n
位喂,用于指定需要 pop
幾個(gè) viewport
浪耘。默認(rèn) n = 1
,傳遞更大的值可以跳轉(zhuǎn)到更上層的 viewport
塑崖,如果設(shè)置為 0
則會(huì)返回到最外層圖形設(shè)備上七冲。
另一個(gè)更改活動(dòng)窗口的方法是,使用 upViewport()
和 downViewport()
函數(shù)规婆。
upViewport()
函數(shù)與 popViewport()
類似澜躺,不同之處在于,upViewport()
函數(shù)不會(huì)刪除當(dāng)前活動(dòng) viewport
抒蚜。
這樣掘鄙,在重新訪問之前的 viewport
時(shí),不用再 push
一遍嗡髓,而且能夠提升訪問的速度操漠。
重新訪問 viewport
使用的是 downViewport()
函數(shù),通過 name
參數(shù)來選擇指定的 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")
# 添加外側(cè)框線
grid.rect(
width=unit(1, "npc") + unit(2, "mm"),
height=unit(1, "npc") + unit(2, "mm"),
gp = gpar(fill = NA)
)
如果想要訪問 vp2
會(huì)報(bào)錯(cuò)饿这,不存在該 viewport
> downViewport("vp2")
Error in grid.Call.graphics(C_downviewport, name$name, strict) :
Viewport 'vp2' was not found
還可以直接使用 seekViewport()
函數(shù)來切換到指定名稱的 viewport
4.2 裁剪 viewport
我們可以將圖形限制在當(dāng)前 viewport
之內(nèi)浊伙,如果繪制的圖形大小超過了當(dāng)前 viewport
則不會(huì)顯示,我們可以使用 clip
參數(shù)
該參數(shù)接受三個(gè)值:
-
on
:輸出的圖形必須保持在當(dāng)前viewport
內(nèi)长捧,超出的部分會(huì)被裁剪 -
inherit
:繼承上一個(gè)viewport
的clip
值 -
off
:不會(huì)被裁剪
例如
grid.newpage()
# 在畫布中心添加一個(gè) viewport嚣鄙,并設(shè)置允許剪切
pushViewport(viewport(w=.5, h=.5, clip="on"))
# 添加矩形框和線條很粗的圓形
grid.rect(
gp = gpar(fill = "#8dd3c7")
)
grid.circle(
r = .7,
gp = gpar(
lwd = 20,
col = "#fdb462"
)
)
# 在當(dāng)前 viewport 中添加一個(gè) viewport,繼承方式
pushViewport(viewport(clip="inherit"))
# 添加線條更細(xì)一點(diǎn)的圓形
grid.circle(
r = .7,
gp = gpar(
lwd = 10,
col = "#80b1d3",
fill = NA)
)
# 關(guān)閉裁剪
pushViewport(viewport(clip="off"))
# 顯示整個(gè)圓形
grid.circle(
r=.7,
gp = gpar(
fill = NA,
col = "#fb8072"
)
)
只有最后一個(gè)圓顯示出了全部串结,前面兩個(gè)圓形只顯示在 viewport
內(nèi)的部分
4.3 viewport 的排列
viewport
的排布方式有三種:
-
vpList
:viewport
列表哑子,以平行的方式排列各viewport
-
vpStack
:以堆疊的方式排列,俗稱套娃肌割,與使用pushViewport
功能相似 -
vpTree
:以樹的方式排列卧蜓,一個(gè)根節(jié)點(diǎn)可以有任意個(gè)子節(jié)點(diǎn)
例如,我們新建三個(gè) viewport
vp1 <- viewport(name="A")
vp2 <- viewport(name="B")
vp3 <- viewport(name="C")
然后声功,我們以列表的方式將這些 viewport
push
到圖形設(shè)備中
pushViewport(vpList(vp1, vp2, vp3))
可以使用 current.vpTree
函數(shù)來查看當(dāng)前的 viewport
排列樹
> current.vpTree()
viewport[ROOT]->(viewport[A], viewport[B], viewport[C])
可以看到,這三個(gè) viewport
是并列的關(guān)系
我們再看看以堆疊的方式放置
> grid.newpage()
> pushViewport(vpStack(vp1, vp2, vp3))
> current.vpTree()
viewport[ROOT]->(viewport[A]->(viewport[B]->(viewport[C])))
可以看到宠叼,根節(jié)點(diǎn)是整個(gè)畫布先巴,畫布的子節(jié)點(diǎn)是 A
其爵,A
的子節(jié)點(diǎn)是 B
,B
的子節(jié)點(diǎn)是 C
伸蚯,這就是堆疊的方式摩渺,一個(gè)套一個(gè)
那對于樹形排列也就不難理解了
> grid.newpage()
> pushViewport(vpTree(vp1, vpList(vp2, vp3)))
> current.vpTree()
viewport[ROOT]->(viewport[A]->(viewport[B], viewport[C]))
根節(jié)點(diǎn)是整個(gè)畫布,然后是子節(jié)點(diǎn) A
界轩,A
的子節(jié)點(diǎn)是 B
炮温、C
我們知道殖熟,畫布中的所有 viewport
是以樹的方式存儲(chǔ)的,那么我們就可以根據(jù) viewport
的父節(jié)點(diǎn)來定位某一個(gè) viewport
例如绰姻,我們想查找名稱 C
的 viewport
,其父節(jié)點(diǎn)為 B
引瀑,再上層父節(jié)點(diǎn)為 A
狂芋,則可以使用 vpPath
函數(shù)來構(gòu)造檢索路徑
> vpPath("A", "B", "C")
A::B::C
同時(shí)也可以消除同名 viewport
的干擾
4.4 將 viewport 作為圖形原語的參數(shù)
每個(gè)原語函數(shù)都有一個(gè) vp
參數(shù)
例如,在一個(gè) 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 的圖形參數(shù)
viewport
也有一個(gè) gp
參數(shù)帜矾,用來設(shè)置圖形屬性,設(shè)置的值將會(huì)作為 viewport
中所有的圖形對象的默認(rèn)值
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
參數(shù)可以用來設(shè)置布局屑柔,將 viewport
區(qū)域分割成不同的行和列屡萤,行之間可以有不同的高度,列之間可以有不同的寬度掸宛。
grid
布局使用 grid.layout()
函數(shù)來構(gòu)造死陆,例如
vplay <- grid.layout(
nrow = 3,
ncol = 3,
respect=rbind(
c(0, 0, 0),
c(0, 1, 0),
c(0, 0, 0))
)
我們構(gòu)造了一個(gè) 3
行 3
列的布局,中間的位置是一個(gè)正方形
構(gòu)造了布局之后旁涤,就可以添加到 viewport
中了
pushViewport(viewport(layout=vplay))
我們可以使用 layout.pos.col
和 layout.pos.row
參數(shù)來指定 viewport
放置的位置
# 新建一個(gè) 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()
# 新建一個(gè) 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
來設(shè)置行列的高度和寬度翔曲,例如
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"))
)
我們定義了一個(gè) 3
行 3
列的布局,列寬通過 widths
分配劈愚,即第一列寬度為 1 inches
瞳遍,剩下的兩列的寬度的占比為 1:2
行高通過 heights
分配,第一行為 3
個(gè) lines
單位菌羽,剩下的兩行高度為 1:1
布局應(yīng)該是下圖這樣子的
grid
布局也可以嵌套
假設(shè)我們有這樣一個(gè)掠械,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)
}
新建一個(gè) 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
列 中放置一個(gè) 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)