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)

图形对象

在前面一节中,我们主要介绍了如何使用 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 保留了一个显示列表,用于记录当前画布中的所有的 viewportgrobs。因此,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.oddcircle.even

然后,我们可以使用 grid.edit() 函数,修改所有名为 circle.oddgrobs 的颜色

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:包含多个 grobslist
  • gTreegrobs 的树形结构,即一个 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

其中 majortickslines groblabelstext 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 结构对象,也包含 gpvp 参数。

在父节点上设置对应的 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)
  )

我们构建一个名为 boxedTextgTree 对象,包含其子对象包括一个文本和一个矩形

我们直接可以绘制组合对象

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")
  )
)

我们可以使用上一章节提到的布局方法,将该图像设计为 12 列的布局

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)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

名本无名

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值