111-R可视化35-结合grid与ggplot输出

2022-04-05 15:29:25 浏览数 (2)

  • 参考:
    • 【R>>>gggrid】ggplot2中实现grid功能 - 简书 (jianshu.com)[1]
    • 《R绘图系统》Paul Murrell

前言

在先前的内容中[[101-R可视化29-底层绘图系统grid学习之使用grid作图]],我们说过,如果可以结合grid 与ggplot 绘图就好了:一方面,通过ggplot 绘图的高级语法,可以省去许多绘图中复杂的代码设置;另一方面,通过grid 底层的调用,我们也可以实现更加灵活的图层设置。

这里有两个思路。

1-通通拆成grob处理

既然ggplot 本质也是grid,那我把ggplot 拆成最底层,再慢慢处理,不也是可以的吗?

比如这里我心血来潮,想要在本来的ggplot 图形侧边放一个的棒棒糖图:

代码语言:javascript复制
candy <- circleGrob(r = 0.1, x = 0.5, y = 0.6,
                    gp = gpar(col = "pink",
                              lty = 3,
                              lwd = 2))
stick <- segmentsGrob(x0 = 0.5, x1 = 0.5, y0 = 0, y1 = 0.5, gp = gpar(lwd = 2))
lollipop <- gTree(children = gList(candy, stick))

其实这里都不要转换ggplot 对象,直接在原有图层上叠加即可:

代码语言:javascript复制
p1 <- ggplot(iris)   geom_point(
  aes(Sepal.Length, Sepal.Width)
)   theme_void()
p1

candy <- circleGrob(r = 0.1, x = 0.5, y = 0.6,
                    gp = gpar(col = "pink",
                              lty = 3,
                              lwd = 2))
stick <- segmentsGrob(x0 = 0.5, x1 = 0.5, y0 = 0, y1 = 0.5, gp = gpar(lwd = 2))
lollipop <- gTree(children = gList(candy, stick))

md_inset <- viewport(x = 0, y = 0, 
                     just = c("left", "bottom"),
                     width = 0.35, height = 0.35)
pushViewport(md_inset)
grid.draw(rectGrob(gp = gpar(alpha = 0.5, col = "white")))
grid.draw(rectGrob(gp = gpar(fill = NA, col = "black")))
grid.draw(lollipop)
# popViewport()

但如果,我们想要在md_inset 视图下,结合ggplot 对象呢?

就需要我们将其拆解成grid 对象了:

代码语言:javascript复制
p1 <- ggplot(iris)   geom_point(
  aes(Sepal.Length, Sepal.Width)
)   theme_void()

p2 <- ggplot(iris)   geom_bar(
  aes(Species)
)

p3 <- ggplotGrob(p1)

p2
md_inset <- viewport(x = 0, y = 0, 
                     just = c("left", "bottom"),
                     width = 0.35, height = 0.35)
pushViewport(md_inset)
grid.draw(rectGrob(gp = gpar(alpha = 0.5, col = "white")))
grid.draw(rectGrob(gp = gpar(fill = NA, col = "black")))
grid.draw(ggplotGrob(p1))

而这个转换后的ggplot 对象,我已经认不得了:

代码语言:javascript复制
> names(p3)
 [1] "grobs"         "layout"        "widths"        "heights"       "respect"      
 [6] "rownames"      "colnames"      "name"          "gp"            "vp"           
[11] "children"      "childrenOrder"
> class(p3)
[1] "gtable" "gTree"  "grob"   "gDesc" 

如果我们想要进入指定的ggplot 相关的图层绘图,那就需要更加暴力的手段,grid.force,将ggplot 的视图可以获取使用。接着通过grid.grep获得名称,以进入对应viewport。

2-打印并不开启新页面

上面的例子中,当我们想要实现两个ggplot 结果的叠加显示时,使用的方法是,将被叠加的ggplot 对象转为grid,从而实现视图上的控制:

我们也可以在打印时声明ggplot 不创建newpage,从而实现类似的效果。

代码语言:javascript复制
md_inset <- viewport(x = 0, y = 0, 
                     just = c("left", "bottom"),
                     width = 0.35, height = 0.35)
pushViewport(md_inset)
grid.draw(rectGrob(gp = gpar(alpha = 0.5, col = "white")))
grid.draw(rectGrob(gp = gpar(fill = NA, col = "black")))
print(p1, newpage = F)

3-ggplot原生方法annotation_custom

其实哈德雷也非常有远见的提供了这个方法annotation_custom

★This is a special geom intended for use as static annotations that are the same in every panel. These annotations will not affect scales (i.e. the x and y axes will not grow to cover the range of the grob, and the grob will not be modified by any ggplot settings or mappings). ”

参数如下:

  • grob:grob to display
  • xmin, xmax:x location (in data coordinates) giving horizontal location of raster
  • ymin, ymax:y location (in data coordinates) giving vertical location of raster

使用起来也非常简单,以上面的为例:

代码语言:javascript复制
candy <- circleGrob(r = 0.1, x = 0.5, y = 0.6,
                    gp = gpar(col = "pink",
                              lty = 3,
                              lwd = 2))
stick <- segmentsGrob(x0 = 0.5, x1 = 0.5, y0 = 0, y1 = 0.5, gp = gpar(lwd = 2))
lollipop <- gTree(children = gList(candy, stick))
rect1 <- rectGrob(gp = gpar(alpha = 0.5, col = "white"))
rect2 <- rectGrob(gp = gpar(fill = NA, col = "black"))
rect_tree <- gTree(children = gList(rect1, rect2))

p2   annotation_custom(rect_tree, 
                       xmin=0,xmax=1.5,ymin=0,ymax=10)   
  annotation_custom(lollipop, 
                    xmin=0,xmax=1.5,ymin=0,ymax=10)

也不难发现这个方法的缺点,一个是对于分类数据的位置设定,即使不是分类数据,其位置也是按照坐标轴来确定,而非一个grid 舒舒服服的units 系统。

即使我们直接对grob 对象设置:

代码语言:javascript复制
rect1 <- rectGrob(gp = gpar(alpha = 0.5, col = "white"),
                  x = 0, y = 0, 
                  just = c("left", "bottom"),
                  width = 0.35, height = 0.35)
rect2 <- rectGrob(gp = gpar(fill = NA, col = "black"),
                  x = 0, y = 0, 
                  just = c("left", "bottom"),
                  width = 0.35, height = 0.35)
rect_tree <- gTree(children = gList(rect1, rect2))

p2   annotation_custom(rect_tree)

反正我挺不喜欢的。

4-使用包gggrid

gggrid 也就是R 绘图系统作者Paul Murrel 写的用于grid 融入ggplot 体系的R 包,其一共只有两个函数:

  • grid_panel()
  • grid_group()
代码语言:javascript复制
p2   gggrid::grid_panel(rect_tree)

这不是跟annotation_custom 差不多吗?

虽然grid_panel 也限定在了坐标轴的范围内,但其厉害之处在于可以接受函数作为grob 输入。

代码语言:javascript复制
rectFun <- function(data,coords){
  left=min(coords$x)
  bottom=min(coords$y)
  width=diff(range(coords$x))
  height=diff(range(coords$y))
  rectGrob(left,bottom,width,height,
           just=c("left","bottom"),
           gp=gpar(fill=NA,lwd=1))
}
ggplot(mtcars,aes(disp,mpg)) 
  geom_point() 
  grid_panel(rectFun)

比如我们可以根据对应坐标的结果进行绘图:

有意思的是,在[[106-R可视化30-底层绘图系统grid学习之重头创建ggplot对象1]]中,coords 并不是直接获取的:

代码语言:javascript复制
coords <- coord$transform(data, panel_scales)

原来文档早已说明:

代码语言:javascript复制
`grob`:Either a grid grob or a function. The function must accept two arguments (`data` and `coords`) and must return a grid grob.

我们还可以通过函数的调试,以了解ggplot 的值:

代码语言:javascript复制
debugHead <- function(data,coords){
  print(head(data))
  print(head(coords))
}
ggplot(mtcars,aes(disp,mpg)) 
  geom_point() 
  grid_panel(debug=debugHead)
  
    x    y PANEL group xmin xmax
1 160 21.0     1    -1 -Inf -Inf
2 160 21.0     1    -1 -Inf -Inf
3 108 22.8     1    -1 -Inf -Inf
4 258 21.4     1    -1 -Inf -Inf
5 360 18.7     1    -1 -Inf -Inf
6 225 18.1     1    -1 -Inf -Inf
          x         y PANEL group xmin xmax
1 0.2470464 0.4555126     1    -1    0    0
2 0.2470464 0.4555126     1    -1    0    0
3 0.1291299 0.5251451     1    -1    0    0
4 0.4692737 0.4709865     1    -1    0    0
5 0.7005714 0.3665377     1    -1    0    0
6 0.3944421 0.3433269     1    -1    0    0

总结

其实这个总结真不好写。

如果是更加自由地使用,还是选择拆成grob 再各自处理的方案。

但如果你的grid 使用并不熟练,且需要的功能并不复杂,那么直接打印,也不失为一个对策。

而如果对于ggplot 的元素有所互动,比如通过其坐标判断以增加额外元素,那么gggrid 包,必然是不二选择了。

比如:

代码语言:javascript复制
rectFun <- function(data,coords){
 left=min(coords$x)
 bottom=min(coords$y)
 width=diff(range(coords$x))
 height=diff(range(coords$y))
 rectGrob(left,bottom,width,height,
          just=c("left","bottom"),
          gp=gpar(fill=NA,lwd=1))
}
ggplot(mtcars,aes(disp,mpg)) 
 geom_point() 
 grid_panel(rectFun)

就有个更成熟的R包 ggforce 可以替代。

它的底层代码,也是依靠gggrid吗?还是自己从[[106-R可视化30-底层绘图系统grid学习之重头创建ggplot对象1]] 这样更加底层的方式实现的吗?

其实无非就是获得coords 的结果,再结合[[110-R可视化34-通过seurat包中的LabelClusters给散点图中心添加文本]] 的一些思路实现的吧。

此外,如果是简单的绘图,其实利用图层的叠加关系,也可以实现,并不需要你有任何的grid 基础。

比如[[87-R可视化19-利用其他图层映射自由的控制背景的颜色]]:

随着R 包越来越多,我们还需不需要底层呢?甚至如此之多的shiny 可视化工具,我们还需不需要编程呢?

参考资料

[1]

【R>>>gggrid】ggplot2中实现grid功能 - 简书 (jianshu.com): https://www.jianshu.com/p/eb5a2f7299ff

0 人点赞