没有特别系统的学习 tidy evaluation 这方面的高级操作,最近有空准备补一补,学习下这方面的知识。
原英文:https://github.com/tidyverse/ggplot2/blob/HEAD/vignettes/ggplot2-in-packages.Rmd
这篇文章是为在包代码中使用ggplot2的包开发人员准备的。在撰写本文时,ggplot2涉及在CRAN上的超过2,000个包和其他地方的更多包!在包中使用ggplot2编程增加了几个约束,特别是如果你想将包提交给CRAN。尤其是在R包中编程改变了从ggplot2引用函数的方式,以及在aes()
和vars()
中使用ggplot2的非标准求值的方式。
与引用其他包类似,你需要在DESCRIPTION
文件下的Imports
条目下列出ggplot2,并使用::
访问ggplot2提供的函数。(例如, ggplot2::function_name
):
mpg_drv_summary <- function() {
ggplot2::ggplot(ggplot2::mpg) +
ggplot2::geom_bar(ggplot2::aes(x = .data$drv)) +
ggplot2::coord_flip()
}
如果你经常使用ggplot2,你可能希望将ggplot2
的多个函数写入NAMESPACE
文件。如果你使用roxygen2[1],那么你可以利用注释代码块 #' @importFrom ggplot2 <one or more object names>
(注意,这对数据集mpg
不起作用)。
#' @importFrom ggplot2 ggplot aes geom_bar coord_flip
mpg_drv_summary <- function() {
ggplot(ggplot2::mpg) +
geom_bar(aes(x = drv)) +
coord_flip()
}
即使你的包里使用了很多的ggplot2函数,将ggplot2列入Depends
条目或者将它整个导入NAMESPACE
(例如,通过#' @import ggplot2
)都是不明智的。将ggplot2列入Depends
会让你的包在被加载/测试的同时加载ggplot2。这会让其他想要使用你包的人通过::
使用你的函数而无需加载它。同样地,导入ggplot2全部450个导出对象到你的命名空间会让分离你的包和ggplot2包的责任变得困难,特别是读者会搞不清这些函数到底来自哪里。
我个人碰到过很多这种情况。有时候在开发R包时为了保证正常运行,不得不将依赖包列入
Depdens
。实际上,如上所说,这一方面会让使用者懵逼,另一方面会造成开发病毒式感染,既不方便调试错误, 想要使用你的包开发的人又不得不将你的包列入Depends
。
aes()
和 vars()
为了使用ggplot2创建图形,你很可能至少要使用一次aes()
函数。如果你的图形使用了分面操作,你可能也会使用vars()
用来指向绘图数据。而这两个函数都使用了非标准计算,如果你在包中直接使用它,后面再CMD check的使用会引入一个note。
所有的Error, warning和note都需要解决才能上传到CRAN。
mpg_drv_summary <- function() {
ggplot(ggplot2::mpg) +
geom_bar(aes(x = drv)) +
coord_flip()
}
N checking R code for possible problems (2.7s)
mpg_drv_summary: no visible binding for global variable ‘drv’
Undefined global functions or variables:
drv
这大体又分为3种情况:
aes()
同样的方式执行非标准计算。如果你已经像上面的例子一样事先知道了列名,你可以使用来自rlang[2]的代词.data
指代你要使用的图层数据。为了避免CMD check抛出note,你需要#' @importFrom rlang .data
注释块引入该符号名。(通常你可以通过usethis::use_package_doc()
生成包的注释块)
mpg_drv_summary <- function() {
ggplot(ggplot2::mpg) +
geom_bar(aes(x = .data$drv)) +
coord_flip()
}
如果你的列名是字符串向量(例如, col = "drv"
),使用 .data[[col]]
这种方式:
col_summary <- function(df, col) {
ggplot(df) +
geom_bar(aes(x = .data[[col]])) +
coord_flip()
}
col_summary(mpg, "drv")
如果列名或者表达式是由用户提供的,你可以以 {{ col }}
的方式将其传入aes()
或vars()
。这种tidy eval计算符号会捕捉用户提供的表达式,并将其传递给使用非标准计算的函数,如aes()
或vars()
。
col_summary <- function(df, col) {
ggplot(df) +
geom_bar(aes(x = {{ col }})) +
coord_flip()
}
col_summary(mpg, drv)
你可能看到了其他的一些方式可以达到相同的目的,但我们(ggplot2的作者)只会保证上述的用法在未来也是有效的。特别的,不要使用aes_()
或aes_string()
,它们已经过时了,未来的版本中将不再支持。
这里有一些删减,没有特别的意义。
ggplot2在包中通常用于可视化对象(例如,在一个plot()
-风格的函数中)。例如,一个包可能定义了 如下一个S3类用于表达式不同离散值的概率:
mpg_drv_dist <- structure(
c(
"4" = 103 / 234,
"f" = 106 / 234,
"r" = 25 / 234
),
class = "discrete_distr"
)
R中需要的类都有plot()
方法,但想要依赖一个单一的plot()
为你的每个用户都提供他们所需要的可视化需求是不现实的。然而,提供一个 plot()
用于一个对象的可视化总结帮助用户理解该对象是有帮助的。为了满足你的所有用户,我们建议写一个函数将这个对象转换为一个数据框(如果更加复杂,可以是包含数据框的列表)。一个很好的例子是ggdendro[3],它创建系统树图但同时计算出数据以方便用户干自己想要做的事情。对于上面的例子,函数可能是这样的:
discrete_distr_data <- function(x) {
tibble::tibble(
value = names(x),
probability = as.numeric(x)
)
}
discrete_distr_data(mpg_drv_dist)
#> # A tibble: 3 x 2
#> value probability
#> <chr> <dbl>
#> 1 4 0.440
#> 2 f 0.453
#> 3 r 0.107
通常,plot()
的使用者调用它是为了它的副作用:它生成一个图形用于展示。这与ggplot()
不同,除非交互使用或者显式地调用print()
,否则是不是展示的。因为这个原因,ggplot2定义了一个自己的泛型函数autoplot()
,调用它会返回一个ggplot()
。
#' @importFrom ggplot2 autoplot
autoplot.discrete_distr <- function(object, ...) {
plot_data <- discrete_distr_data(object)
ggplot(plot_data, aes(.data$value, .data$probability)) +
geom_col() +
coord_flip() +
labs(x = "Value", y = "Probability")
}
一旦定义了 autoplot()
,可以接着创建一个plot()
方法包含(打印)绘图步骤:
#' @importFrom graphics plot
plot.discrete_distr <- function(x, ...) {
print(autoplot(x, ...))
}
如果你不懂S3类,实现像plot()
或者autoplot()
这种泛型是一个不好的实践,因为这限制了包开发者自己控制S3用于实现自己的方法。不应该停止你创建自己的函数可视化对象!
当创建一个新的主题时,从已有主题出发总是好的实践(例如,theme_grey()
),然后使用%+replace%
替换需要该包的元素。这是一种好的策略,哪怕几乎所有的元素都要替换,如果不这样做会让我们通过添加元素优化主题变得困难。ggthemes[4]包中有很多好的主题作为参考。
#' @importFrom ggplot2 %+replace%
theme_custom <- function(...) {
theme_grey(...) %+replace%
theme(
panel.border = element_rect(size = 1, fill = NA),
panel.background = element_blank(),
panel.grid = element_line(colour = "grey80")
)
}
mpg_drv_summary() + theme_custom()
在加载包之后计算主题是很重要的。如果没有,则会将主题对象存储在编译后的包的字节码中,而该字节码可能与安装的ggplot2不一致!如果你的包有一个默认的可视化主题,正确的加载方法是使用一个返回默认主题的函数:
default_theme <- function() {
theme_custom()
}
mpg_drv_summary2 <- function() {
mpg_drv_summary() + default_theme()
}
我们建议使用vdiffr[5]测试ggplot2的输出,这是一个管理可视化测试案例的工具(这是我们测试ggplot2的方式之一)。如果ggplot2或者你代码的改变对可视化输出引入了改变,当你在本地或者Travis运行测试时会失败。为了使用vdiffr,你需要将testthat[6](通过usethis::use_testthat()
初始化)和vdiffr加入DESCRIPTION
的Suggests
条目。然后,使用 vdiffr::expect_doppleganger(<name of plot>, <ggplot object>)
创建一个测试。
test_that("output of ggplot() is stable", {
vdiffr::expect_doppelganger("A blank plot", ggplot())
})
Suggests
如果你在包中使用ggplot2,大概率你会想要将它列入Imports
。如果你想要将它列入Suggests
,那么你不能使用#' @importFrom ggplot2 ...
载入函数,但是如果你仍然想要使用ggplot2的像%+replace%
这样的中缀操作符号,你可以在函数中进行赋值,然后再使用。
theme_custom <- function(...) {
`%+replace%` <- ggplot2::`%+replace%`
ggplot2::theme_grey(...) %+replace%
ggplot2::theme(panel.background = ggplot2::element_blank())
}
通过,如果你为ggplot2的autoplot()
等泛型创建了新的方法,ggplot2应该列入Imports
。如果出于一些原因你想要将其保留在Suggests
,那么可以利用vctrs::s3_register()
仅当ggplot2被安装时才注册你的泛型函数。为了达到这样的目的,你需要拷贝和粘贴vctrs::s3_register()
的源代码,以避免引入vctrs[7]作为依赖。
.onLoad <- function(...) {
if (requireNamespace("ggplot2", quietly = TRUE)) {
vctrs::s3_register("ggplot2::autoplot", "discrete_distr")
}
}
[1]roxygen2: https://cran.r-project.org/package=roxygen2
[2]rlang: https://rlang.r-lib.org/
[3]ggdendro: https://cran.r-project.org/package=ggdendro
[4]ggthemes: https://cran.r-project.org/package=ggthemes
[5]vdiffr: https://cran.r-project.org/package=vdiffr
[6]testthat: https://testthat.r-lib.org/
[7]vctrs: https://vctrs.r-lib.org/
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有