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

前言

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

名本无名

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

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

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

打赏作者

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

抵扣说明:

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

余额充值