美文网首页R ggplotR plot
R 数据可视化 —— grid 系统(一)

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

作者: 名本无名 | 来源:发表于2021-05-09 16:46 被阅读0次

    前言

    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)
    

    相关文章

      网友评论

        本文标题:R 数据可视化 —— grid 系统(一)

        本文链接:https://www.haomeiwen.com/subject/wiqudltx.html