R 数据可视化 —— grid 系统(一)

前言

R 中主要存在两种绘图系统:

  • base R 传统图像系统
  • grid 图像系统

传统的图像系统是由 graphics 包所提供的一系列函数组成,grid 系统是 grid 包提供的

grid 包是一个底层的绘图系统,提供的都是底层的绘图函数,没有用于绘制复杂图形的高级函数。

ggplot2lattice 两个顶层的绘图包都是基于 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

可以使用 convertWidthconvertHeight 实现单位之间的转换

> 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 类似,也是累积的

注意: 这些图形参数都可以接受一个向量值,比如,你可以将一个颜色向量传递给 colfill 参数,如果向量的长度小于绘制的图形的个数,则参数会进行循环赋值

如,我们绘制 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 处将 id1 的分组又一分为二,但是填充色还是灰色,并不是接续白色

4. viewport

grid 中,图像的绘制需要在画布中执行,也就是在绘制图像时需要新建一个画布

grid.newpage()

通常使用 grid.newpage() 函数来新建一个空白画布

在画布中,又可以定义很多个独立的矩形绘图窗口,在每个矩形窗口中都可以绘制任意你想要绘制的内容,这样的窗口就是 viewport

默认情况下,整个画布就是一个 viewport,如果新增一个 viewport,那么默认会继承所有默认的图形参数值

使用 viewport() 函数来新建一个 viewport,并接受位置参数(xy) 和大小参数(widthheight),以及对齐方式(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,新 pushviewport 将会相对于当前 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:继承上一个 viewportclip
  • 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 的排布方式有三种:

  • vpListviewport 列表,以平行的方式排列各 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]))) 

可以看到,根节点是整个画布,画布的子节点是 AA 的子节点是 BB 的子节点是 C,这就是堆叠的方式,一个套一个

那对于树形排列也就不难理解了

> grid.newpage()
> pushViewport(vpTree(vp1, vpList(vp2, vp3)))
> current.vpTree()
viewport[ROOT]->(viewport[A]->(viewport[B], viewport[C]))

根节点是整个画布,然后是子节点 AA 的子节点是 BC

我们知道,画布中的所有 viewport 是以树的方式存储的,那么我们就可以根据 viewport 的父节点来定位某一个 viewport

例如,我们想查找名称 Cviewport,其父节点为 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 布局

viewportlayout 参数可以用来设置布局,将 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))
  )

我们构造了一个 33 列的布局,中间的位置是一个正方形

构造了布局之后,就可以添加到 viewport 中了

pushViewport(viewport(layout=vplay))

我们可以使用 layout.pos.collayout.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"))
  )

我们定义了一个 33 列的布局,列宽通过 widths 分配,即第一列宽度为 1 inches,剩下的两列的宽度的占比为 1:2

行高通过 heights 分配,第一行为 3lines 单位,剩下的两行高度为 1:1

布局应该是下图这样子的


grid 布局也可以嵌套

假设我们有这样一个,12 列的 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)
}

新建一个 55 列的 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"))
      )
    )
  )

然后,分别在 22 列和 44 列 中放置一个 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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容