很多人都在学ggplot2,也有很多「高手」,比如你搜索「精通ggplot2」,你甚至会看到各种「从入门到精通」的课程,然而都是各种照猫画老虎的入门菜谱而已,当然广大群众去上课也可能只是为了求菜谱好在需要的时候,可以copy-paste而已。

ggplot2学习容易,因为无非是图层叠加,而图层的参数一致性非常好,网上也有很全的文档。然而要深入很难,比如你想干点什么是ggplot2自身没有支持的,你需要自己去hack,需要用到更底层的东西的时候,一般情况下,会无从入手,尝试之后才知道什么叫绝望!因为ggplot2套路实在是太深。

正如我在《听说你还不会画热图》吐槽的,有些所谓的「高手」实际上连ggplot2底层是什么都不知道!然后可以把ggplot2吹上天。很多人根本不知道当你打下ggplot这个命令的时候,到底发生了什么?要理解ggplot2并不容易,正如前面所说「套路太深」,没有金钢钻,读不了它的代码,有没有一个简单点的包,能够读懂代码,并且有助于理解ggplot2呢?我想我的meme包就非常好,虽然仿的有点肤浅,然而胜在简单,看过下面这两篇文章的,都知道这个包在做什么。

在非常清楚主要函数的目的之后,代码读起来,仿佛就有了指南一般,又因为包的功能简单,代码简短,容易理顺代码的含义。这个包除去注释和空白行,只有146行,你没有看错,就是这么短。功能简单、目的明确,但能读懂也不是很容易。

1. meme的输出不是图?为什么能画出图?

meme <- function(img, upper="", lower="", size="auto", color="white", font="Impact",
                 vjust = .1, bgcolor="black", r = 0.2) {
    x <- image_read(img)
    info <- image_info(x)

    imageGrob <- rasterGrob(x)

    p <- structure(
        list(img = img, imageGrob = imageGrob,
             width = info$width, height = info$height,
             upper=upper, lower=lower,
             size = size, color = color,
             font = font, vjust = vjust,
             bgcolor = bgcolor, r = r),
        class = c("meme", "recordedplot"))
    p
}

如果你看代码,meme的输出就是一个叫meme的对象,只是个简单的list,而内容基本上就是参数而已。除了图已经读了(避免重复读图)。那么这样一个list,为什么当你打meme命令的时候会出图?

ggplot的输出是一个叫gg的对象,实质上也是一个简单的list,同理你就能理解为什么它能出图?

2. 为什么+可以改变图的内容和状态?

meme包中,你可以用+aes()或者+list()来改变打印的字、字体、颜色等,为什么能改变内容和状态?相对应于ggplot2+geom_XXX图层是改变内容,+theme()是改变状态,这是如何实现的?

如果理解了1,你就能理解2这个问题。

3. 为什么ggsave能识别meme对象?

meme_save <- function(x, file, width = NULL, height = NULL, ...) {
    if (!is(x, "meme")) {
        stop("x should be an instance of 'meme'")
    }

    if (is.null(width) && is.null(height)) {
        width <- px2in(x$width)
        height <- px2in(x$height)
    } else if (is.null(width)) {
        width <- height / asp(x)
    } else {
        height <- width * asp(x)
    }

    ggsave(filename = file, plot = x,
           width = width,
           height = height,
           ...)
}

meme包中提供的meme_save()函数,无非是设置了原图片的长宽比,然后直接应用ggsave来保存,为什么ggsave能识别meme

如果ggsave是原型函数,那么我在meme里定义相应的方法就可以了,但你看ggplot2::ggsave的代码,它只是一个普通的函数,但我们在使用cowplot拼图的时候,输出也不是ggggplot对象了,已经和ggplot2不兼容了,然而plot_grid的输出,你也能用ggsave来保存?ggsave到底干了什么?它是如何识别ggplot以外的对象的?

事实上如果你把base plot套在expression或formula里,我们甚至可以让ggsave来支持base plot,使用诸如ggsave(expression(plot()))ggsave(~plot())这样的语法,当然默认是不支持的,需要我们做点什么东西,如果你真正理解了,你就能够实现出来。


前面三个问题,从输入、中间操作和输出三个角度,拷问我们自己,如果能够回答,那么就算没有理解整个过程,起码是有点感性认识,毕竟ggplot2的实现是复杂的,套路很深,但很多是细节性的,大的方向、思路无非就是支持图形语法来进行「中间操作」,改变最终出图的内容。好好思考一下这三个问题。


前面三个问题是核心的,再提问一点相关的。

4. 为什么使用传统的出图方式来画meme,在循环中需要显示print(object)?而ggsave则不用?到底区别在那里?

这是ggplot2一个非常常见的问题,借用meme来思考一下,可以与问题3联合想一下,ggsave到底干了什么?为什么不循环可以?而循环就见了鬼了?

5. 为什么meme对象能够被ggimagecowplot识别?

这个问题有点跳跃了,ggimagememe是原生支持,因为我是ggimage包作者,我想支持就能支持,但到底我是怎么实现的?而cowplot,不好意思,它是不支持的,而我不是作者,那么我就去看cowplot的实现中,有没有什么可以hack的,我用「欺骗」的手段,穿个「马甲」让它来认,这都挺有意思的,到底怎么实现?