R ggplotR plot

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

2021-05-09  本文已影响0人  名本无名

前言

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

传统的图像系统是由 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 参数

该参数接受三个值:

例如

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 的排布方式有三种:

例如,我们新建三个 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)
上一篇 下一篇

猜你喜欢

热点阅读