前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >「R」ggplot2在R包开发中的使用

「R」ggplot2在R包开发中的使用

作者头像
王诗翔呀
发布于 2022-03-30 00:47:54
发布于 2022-03-30 00:47:54
7.3K00
代码可运行
举报
文章被收录于专栏:优雅R优雅R
运行总次数:0
代码可运行

没有特别系统的学习 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的非标准求值的方式。

引用ggplot2函数

与引用其他包类似,你需要在DESCRIPTION文件下的Imports条目下列出ggplot2,并使用::访问ggplot2提供的函数。(例如, ggplot2::function_name):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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不起作用)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#' @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。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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()生成包的注释块)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mpg_drv_summary <- function() {
  ggplot(ggplot2::mpg) + 
    geom_bar(aes(x = .data$drv)) + 
    coord_flip()
}

如果你的列名是字符串向量(例如, col = "drv"),使用 .data[[col]] 这种方式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
col_summary <- function(df, col) {
  ggplot(df) + 
    geom_bar(aes(x = {{ col }})) + 
    coord_flip()
}
col_summary(mpg, drv)

你可能看到了其他的一些方式可以达到相同的目的,但我们(ggplot2的作者)只会保证上述的用法在未来也是有效的。特别的,不要使用aes_()aes_string(),它们已经过时了,未来的版本中将不再支持。

这里有一些删减,没有特别的意义。

常规任务最佳实践

使用ggplot2可视化一个对象

ggplot2在包中通常用于可视化对象(例如,在一个plot()-风格的函数中)。例如,一个包可能定义了 如下一个S3类用于表达式不同离散值的概率:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mpg_drv_dist <- structure(
  c(
    "4" = 103 / 234,
    "f" = 106 / 234,
    "r" = 25 / 234
  ),
  class = "discrete_distr"
)

R中需要的类都有plot()方法,但想要依赖一个单一的plot()为你的每个用户都提供他们所需要的可视化需求是不现实的。然而,提供一个 plot()用于一个对象的可视化总结帮助用户理解该对象是有帮助的。为了满足你的所有用户,我们建议写一个函数将这个对象转换为一个数据框(如果更加复杂,可以是包含数据框的列表)。一个很好的例子是ggdendro[3],它创建系统树图但同时计算出数据以方便用户干自己想要做的事情。对于上面的例子,函数可能是这样的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#' @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()方法包含(打印)绘图步骤:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#' @importFrom graphics plot
plot.discrete_distr <- function(x, ...) {
  print(autoplot(x, ...))
}

如果你不懂S3类,实现像plot()或者autoplot()这种泛型是一个不好的实践,因为这限制了包开发者自己控制S3用于实现自己的方法。不应该停止你创建自己的函数可视化对象!

创建一个新的主题

当创建一个新的主题时,从已有主题出发总是好的实践(例如,theme_grey()),然后使用%+replace%替换需要该包的元素。这是一种好的策略,哪怕几乎所有的元素都要替换,如果不这样做会让我们通过添加元素优化主题变得困难。ggthemes[4]包中有很多好的主题作为参考。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#' @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不一致!如果你的包有一个默认的可视化主题,正确的加载方法是使用一个返回默认主题的函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
default_theme <- function() {
  theme_custom()
}
mpg_drv_summary2 <- function() {
  mpg_drv_summary() + default_theme()
}

测试ggplot2输出

我们建议使用vdiffr[5]测试ggplot2的输出,这是一个管理可视化测试案例的工具(这是我们测试ggplot2的方式之一)。如果ggplot2或者你代码的改变对可视化输出引入了改变,当你在本地或者Travis运行测试时会失败。为了使用vdiffr,你需要将testthat[6](通过usethis::use_testthat()初始化)和vdiffr加入DESCRIPTIONSuggests条目。然后,使用 vdiffr::expect_doppleganger(<name of plot>, <ggplot object>)创建一个测试。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
test_that("output of ggplot() is stable", {
  vdiffr::expect_doppelganger("A blank plot", ggplot())
})

ggplot2在Suggests

如果你在包中使用ggplot2,大概率你会想要将它列入Imports。如果你想要将它列入Suggests,那么你不能使用#' @importFrom ggplot2 ...载入函数,但是如果你仍然想要使用ggplot2的像%+replace%这样的中缀操作符号,你可以在函数中进行赋值,然后再使用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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]作为依赖。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
.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/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 优雅R 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
基于单片机的水位检测系统_51单片机温度传感器程序
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
全栈程序员站长
2022/11/10
4000
基于单片机的水位检测系统_51单片机温度传感器程序
设计分享|基于51单片机的按键计数
51单片是一种低功耗、高性能CMOS-8位微控制器,具有8K可编程Flash存储器,使得其为众多嵌入式控制应用系统提供高灵活、超有效的解决方案。
电子工程师成长日记
2025/03/06
870
设计分享|基于51单片机的按键计数
蓝桥杯单片机必备知识—–(9)超声波测距
超声波发射器向某一方向发射超声波,在发射时刻的同时开始计时,超声波在空气中传播,途中碰到障碍物就立即返回来,超声波接收器收到反射波就立即停止计时。超声波在空气中的传播速度为340m/s,根据计时器记录的时间t,就可以计算出发射点距障碍物的距离(s),即:s=340t/2 。这就是所谓的时间差测距法。 超声波测距的原理是利用超声波在空气中的传播速度为已知,测量声波在发射后遇到障碍物反射回来的时间,根据发射和接收的时间差计算出发射点到障碍物的实际距离。由此可见,超声波测距原理与雷达原理是一样的。 测距的公式表示为:L=C×T 式中L为测量的距离长度;C为超声波在空气中的传播速度;T为测量距离传播的时间差(T为发射到接收时间数值的一半)。 超声波测距主要应用于倒车提醒、建筑工地、工业现场等的距离测量,虽然目前的测距量程上能达到百米,但测量的精度往往只能达到厘米数量级。 由于超声波易于定向发射、方向性好、强度易控制、与被测量物体不需要直接接触的优点,是作为液体高度测量的理想手段。在精密的液位测量中需要达到毫米级的测量精度,但是目前国内的超声波测距专用集成电路都是只有厘米级的测量精度。
全栈程序员站长
2021/04/16
8040
蓝桥杯单片机必备知识—–(9)超声波测距
蓝桥杯单片机NE555编程
蓝桥杯单片机中的NE555模块的编程,其实是很简单。简单来说就是计算一秒钟之内有多少个频率响应。中间所谓的频率响应也就是计算定时器作为计数器时候溢出的个数而已。能够考察的点就是这样子了,也要稍微了解,之后进行编程,就能够熟练掌握了。
全栈程序员站长
2021/11/15
6400
单片机入门:定时器/计数器应用
利用单片机内部定时器/计数器中断实现一个数码管的秒记数,重点学习定时器/计数器的工作方式以及其控制寄存器TMOD、TCON的功能,在程序实现过程中掌握定时器/计数器中断的一般步骤。
电子工程师成长日记
2022/07/21
6590
单片机入门:定时器/计数器应用
蓝桥杯单片机必备知识—–(8)NE555测频
已经使用跳线帽将P34和NE555的输出端接在一起了,关乎NE555,有些许数电基础的都明白,NE555就是改变RB3,改变负载电阻,使其输出方波的频率发生改变,其中并不涉及任何对NE555的编程,当然你也没法对其编程。
全栈程序员站长
2021/04/15
1.3K0
4.10 51单片机-使用计数器测量NE555脉冲频率
STC90C51RC/RD+系列单片机内部设置的两个16位定时器/计数器T0和T1都具有计数方式和定时方式两种工作方式。对每个定时器/计数器(T0和T1),在特殊功能寄存器TMOD中都有一控制-C/T来选择T0或者T1为定时器还是计数器。定时器/计数器的核心部件是一个加法计数器,其本质是对脉冲进行计数。只是计数脉冲来源不同:如果计数脉冲来自系统时钟,则为定时方式,此时定时器/计数器每12个时钟或者每6个时钟得到一个计数脉冲,计数值加1;如果计数脉冲来自单片机外部引脚(T0为P3.3,T1为P3.3),则为计数方式,每来一个脉冲加1。
DS小龙哥
2022/01/10
1.7K0
4.10 51单片机-使用计数器测量NE555脉冲频率
单片机实验说明<三>数码管和定时器基本使用
编程实现8段数码管的动态扫描显示,要求4个数码管从左到右分别显示“1”、“2”、“3”、“4”、及“A”、“b”、“C”、“d”。
zhangrelay
2021/03/03
5270
51单片机的中断及其使用方法
比如说我正在厨房用煤气烧一壶水,这样就只能守在厨房里,苦苦等着水开——如果水溢出来浇灭了煤气,有可能就要发生一场灾难了。
緣來
2020/01/02
2.1K0
新概念51单片机C语言教程纠错(3)「建议收藏」
这一次错误在课本P140页例6.6.1中,这个例子在proteus仿真中再一次刷新了我的认知。
全栈程序员站长
2022/06/28
5460
设计分享|51单片机定时器Timer0控制4个LED滚动闪烁
51单片是一种低功耗、高性能CMOS-8位微控制器,具有8K可编程Flash存储器,使得其为众多嵌入式控制应用系统提供高灵活、超有效的解决方案。
电子工程师成长日记
2024/10/16
2190
设计分享|51单片机定时器Timer0控制4个LED滚动闪烁
单片机入门:数码管秒表
采用8位数码管进行秒表设计。 仿真原理图: 程序设计: /* 数码管秒表设计 */ #include<reg52.h> unsigned char LedBuff[]={ 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF }; unsigned char code LedChar[]={ 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e };//共阳 /* unsigned c
电子工程师成长日记
2022/07/21
6330
单片机入门:数码管秒表
实验四:定时器实验[通俗易懂]
一、实验目的 1、掌握单片机定时/计数器的使用方法。 2、掌握定时/计数器编程方法。
全栈程序员站长
2022/09/17
9730
实验四:定时器实验[通俗易懂]
山东大学单片机原理与应用实验 3.8 ADC0808/9信号采集实验
利用LCD1602和AD0808实现简单的交流信号过零检测与频率分析。要求信号幅度变化时(满量程的5%—95%),不影响检测到结果。频率检测的结果通过LCD1602的第一行显示出来,信号过零时,能够通过P2.6输出一个脉冲宽度为5μs的脉冲信号。
timerring
2022/07/20
9400
山东大学单片机原理与应用实验 3.8 ADC0808/9信号采集实验
设计分享|单片机8路抢答器
51单片是一种低功耗、高性能CMOS8位微控制器,具有 8K 在系统可编程Flash 存储器。在单芯片上,拥有灵巧的8 位CPU 和在系统可编程Flash,使得STC89C51为众多嵌入式控制应用系统提供高灵活、超有效的解决方案。具有以下标准功能:8k字节Flash,512字节RAM,32 位I/O 口线,看门狗定时器,内置4KB EEPROM,MAX810复位电路,三个16 位 定时器/计数器,一个6向量2级中断结构,全双工串行口。另外 STC89X51 可降至0Hz 静态逻辑操作,支持2种软件可选择节电模式。空闲模式下,CPU 停止工作,允许RAM、定时器/计数器、串口、中断继续工作。掉电保护方式下,RAM内容被保存,振荡器被冻结,单片机一切工作停止,直到下一个中断或硬件复位为止。最高运作频率35Mhz,6T/12T可选。
电子工程师成长日记
2022/07/27
6240
设计分享|单片机8路抢答器
单片机实验2提示
参考程序,未必准确: /***************************************************************************** * 头文件 * *****************************************************************************/ #includ
zhangrelay
2021/12/02
2530
单片机实验2提示
单片机课堂思考题-2000秒倒计时?
99秒倒计时,改为2000(6666)秒倒计时。 #include<8052.h> #define LSC P1_7 //特殊功能寄存器的位定义, #define LSB P1_6 //3-8译码器的输入端, #define LSA P1_5 //控制三极管Q2~Q6的导通 unsigned char smgduan[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0x0f8,0x80,
zhangrelay
2021/12/02
5110
单片机课堂思考题-2000秒倒计时?
设计分享|51单片机双定时器控制16位流水灯
51单片是一种低功耗、高性能CMOS-8位微控制器,具有8K可编程Flash存储器,使得其为众多嵌入式控制应用系统提供高灵活、超有效的解决方案。
电子工程师成长日记
2025/02/10
980
设计分享|51单片机双定时器控制16位流水灯
8051和8052_基于单片机多用途定时器的设计
大家好,我是架构君,一个会写代码吟诗的架构师。今天说一说8051和8052_基于单片机多用途定时器的设计,希望能够帮助大家进步!!!
Java架构师必看
2022/10/24
5010
【51单片机】配置定时器
​ 那么我们先创建一个子函数 void Timer0_Init() ⇥ 对定时器进行初始化。 那么首先是TMOD的一个配置,定时器1我们先不管(高四位定时器全部给上0),只管低四位的一个定时器0的一个配置。 在前面的定时器工作有介绍过,STC89C52 的 T0 和 T1 一共有四种工作模式:在这里我们选择最常用的工作模式1。 Ⅰ:工作模式①的话,我们只需要把 M1 = 0,M0 = 1 就是定时器模式①的配置了。 Ⅱ:C/T(取反):这个我们只需要给C/T(取反) = 0即可,SYSclk⇢系统时钟。 Ⅲ:GATE(门控端),GATE = 0,这里当中都是数字电路当中的一些基础逻辑门,TR0 = 1。
謓泽
2022/12/12
7340
【51单片机】配置定时器
推荐阅读
相关推荐
基于单片机的水位检测系统_51单片机温度传感器程序
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验