预计阅读

R Shiny 食谱





本文引用的所有信息均为公开信息,仅代表作者本人观点,与就职单位无关。

R Shiny 框架

R Shiny 框架

一. 背景介绍

现在大厂都有自研的 BI 工具,只在遇到一些探索性的、定制化的需求时,需要 R Shiny 这套工具。有固定模式的需求通常都可以用工具固定下来,以拖拉拽的方式便捷的完成。与之相反,则需要通过编程实现灵活性、特异性。这样的话,原有工具的部分功能就得在 R Shiny 中实现,比如级联筛选器,表格中的超链接等。R Shiny 实现一种低代码的响应式设计,作为开发者,需要考虑四个方面。其一页面布局设计、开发,其二页面元素设计、开发,其三用户和页面元素交互设计、开发 ,其四页面元素之间交互设计、开发。

二. 踩坑记录

2.1 三层级联筛选器的实现

筛选器之间存在联动,筛选器 A 的选项影响筛选器 B 的选项,筛选器 B 的选项又影响筛选器 C 的选项。从用户的视角看,这是一种数据的下钻,举例来说,从省到市再到区县。以筛选器联动的方式来处理,是因为省-市-区(县)的行政体系存在层级隶属关系,往往维度表也很大。在零售行业广泛存在品类体系,当我们逛超市的时候,会发现这样的现象,在不同的区域进行不同风格的装饰,比如生鲜、家电、水酒等等,每个大的类目下面又有很多小的品类。

2.2 查询按钮和筛选器交互

我改变了筛选器的选项,并不是立马改变输出结果,而是等待用户点击查询按钮才开始执行。

2.3 查询按钮和结果渲染交互

点击查询才开始渲染后续的结果,比如绘制图形和制作表格等。举例如下:

# 数据集 economics 来自 ggplot2 包
index_vec <- c(
  "个人消费支出(单位:10亿美金)" = "pce",
  "总人口(单位:千)" = "pop",
  "个人储蓄率(单位:%)" = "psavert",
  "失业时长(单位:周)" = "uempmed",
  "失业人数(单位:千)" = "unemploy"
)

div(
  class = "row justify-content-start",
  column(
    width = 4,
    dateRangeInput("daterange", "日期范围",
      start = "1967-07-01", end = "2015-04-01",
      min = "1967-07-01", max = "2015-04-01"
    )
  ),
  column(
    width = 5,
    selectInput("index", "经济指标", choices = index_vec, selected = "psavert")
  ),
  column(
    width = 2,
    actionButton("action",
      label = "查询",
      style = "margin-top:25px; color: #fff; background-color: #5b89f7; border-color: #5b89f7"
    )
  )
)

# 点击执行按钮,才渲染后续结果
input_vec2 <- eventReactive(input$action, {
  list(
    "开始日期" = input$daterange[1],
    "结束日期" = input$daterange[2],
    "指标" = input$index
  )
})

对一个时间序列数据集,想选择其中某个指标数据,绘制时序图展示趋势变化。有时候,并不想打开 Shiny 应用的时候就加载数据,而是等用户选择时间范围和数据指标后,开始查询数据,展示结果。这会节省 R Shiny 应用启动的时间,和一部分计算资源。核心的交互逻辑是 shiny 包的两个函数实现的。

# 查询动作的按钮
actionButton
# 对查询动作的再响应
eventReactive

2.4 服务端渲染筛选器

当筛选器的项目非常多时,selectInput()selectizeInput() 运行起来会非常慢,此时,可将有关计算转移到 R 软件。详情见Using selectize input

# in ui
selectizeInput('foo', choices = NULL, ...)

# in server
server <- function(input, output, session) {
  updateSelectizeInput(session, 'foo', choices = data, server = TRUE)
}

2.5 筛选器排列布局

# ?column()
column(width, ..., offset = 0)
  • width: 宽度范围是 1-12
  • … 放筛选器
  • offset 距离左侧筛选器漂移一段距离,负值表示靠近,正值表示远离
library(shiny)
column
## function (width, ..., offset = 0) 
## {
##     if (!is.numeric(width) || (width < 1) || (width > 12)) 
##         stop("column width must be between 1 and 12")
##     colClass <- paste0("col-sm-", width)
##     if (offset > 0) {
##         colClass <- paste0(colClass, " offset-md-", offset, " col-sm-offset-", 
##             offset)
##     }
##     div(class = colClass, ...)
## }
## <bytecode: 0x7f9f8bd12df0>
## <environment: namespace:shiny>

2.6 输入字符串转变量

函数 get()mget() 可以将用户输入的字符串转为 R 语言环境中的变量,用户切换不同的筛选框的选项,可以获得不同的结果。与 data.table 包组合使用非常合适。

INPUT = "Species"
library(data.table)
iris_dt <- as.data.table(iris)
# 筛选数据集 iris 中的 Species 列
iris_dt[, get(INPUT)]
##   [1] setosa     setosa     setosa     setosa     setosa     setosa    
##   [7] setosa     setosa     setosa     setosa     setosa     setosa    
##  [13] setosa     setosa     setosa     setosa     setosa     setosa    
##  [19] setosa     setosa     setosa     setosa     setosa     setosa    
##  [25] setosa     setosa     setosa     setosa     setosa     setosa    
##  [31] setosa     setosa     setosa     setosa     setosa     setosa    
##  [37] setosa     setosa     setosa     setosa     setosa     setosa    
##  [43] setosa     setosa     setosa     setosa     setosa     setosa    
##  [49] setosa     setosa     versicolor versicolor versicolor versicolor
##  [55] versicolor versicolor versicolor versicolor versicolor versicolor
##  [61] versicolor versicolor versicolor versicolor versicolor versicolor
##  [67] versicolor versicolor versicolor versicolor versicolor versicolor
##  [73] versicolor versicolor versicolor versicolor versicolor versicolor
##  [79] versicolor versicolor versicolor versicolor versicolor versicolor
##  [85] versicolor versicolor versicolor versicolor versicolor versicolor
##  [91] versicolor versicolor versicolor versicolor versicolor versicolor
##  [97] versicolor versicolor versicolor versicolor virginica  virginica 
## [103] virginica  virginica  virginica  virginica  virginica  virginica 
## [109] virginica  virginica  virginica  virginica  virginica  virginica 
## [115] virginica  virginica  virginica  virginica  virginica  virginica 
## [121] virginica  virginica  virginica  virginica  virginica  virginica 
## [127] virginica  virginica  virginica  virginica  virginica  virginica 
## [133] virginica  virginica  virginica  virginica  virginica  virginica 
## [139] virginica  virginica  virginica  virginica  virginica  virginica 
## [145] virginica  virginica  virginica  virginica  virginica  virginica 
## Levels: setosa versicolor virginica

2.7 表格中添加超链接

表格中添加超链接是为了和相关的资源联系起来,可以从一个工具跳转到另一个工具,甚至跳转到外部网络。下面给搜索关键词批量的添加跳转链接,跳转链接支持传递文本和日期类型的字段,构造的跳转链接可以用 DT::datatable() 展示出来。

# 生成链接
hyperlink <- function(text = "肯德基", begin_date = Sys.Date() - 1, end_date = Sys.Date()) {
  links <- sprintf('网站访问链接/query/queryquery?filter=[{"name":"请输入关键词","type":31,"value":"%s"},{"name":"开始日期","type":11,"value":"%s"},{"name":"结束日期","type":11,"value":"%s"}]', text, begin_date, end_date)
  as.character(htmltools::a(href = links, text, target = "_blank"))
}
# 批量化生成链接
hyperlink2vec <- function(
    text2vec = c("肯德基", "外卖"),
    begin_date = Sys.Date() - 1,
    end_date = Sys.Date) {
  unlist(lapply(text2vec, hyperlink,
    begin_date = begin_date,
    end_date = end_date
  ))
}

注意:路径中支持传递多个不同类型的筛选器参数,筛选器参数之间不要有空格,即路径中 ?filter 后面跟的 JSON 串不要有空格。

2.8 R 中渲染 SQL 代码

R 中渲染 SQL 代码的常用函数是 sprintf() ,就是字符串格式化打印,用 %s 替换真实的字符串。只是遇到特殊字符的时候,需要一些注意。比如,函数 sprintf() 渲染带百分号 % 的 SQL 语句。当遇到 SQL 语言里日期格式化函数 date_format() 时,函数 sprintf() 使用字面 % 需要用 %%

SELECT date_format(partition_date, '%X-%m') as year_month
  FROM app_xx_metric_d_d
  AND  partition_date BETWEEN '2020-04-01' AND '2020-04-30'
# sprintf 使用字面 % 需要用 %% 
SELECT date_format(partition_date, '%%X-%%m') as year_month
  FROM app_xx_metric_d_d
  AND  partition_date BETWEEN '2020-04-01' AND '2020-04-30'

值得注意,函数 sprintf() 支持的字符串长度有限。当 SQL 语句非常长时,推荐使用 glue 包的函数 glue() 代替函数 sprintf() ,使用方法详见 glue() 函数帮助文档。

2.9 定制页面布局

shiny 包有四个常用的页面布局函数,布局函数 flowLayout() 实现一个从左到右,从上到下的排列,其他三个函数分别是 sidebarLayout(), splitLayout(), verticalLayout() 。在 bslib 包出现之前,定制页面布局用的最多的是 flexdashboard 包,常常在导言区做类似如下的设置。

---
title: "-"
runtime: shiny
output: 
  flexdashboard::flex_dashboard:
    orientation: rows # 横向
    vertical_layout: scroll # 垂直滚动
    theme: bootstrap # 配色
    mathjax: NULL # 不加载数学库
---

2.10 路径传参数

每个用户访问 Shiny 页面,都会自带个人信息,在 Shiny 应用中比对访问人的信息,有权限的页面可以访问,没权限的不可访问,达到一个数据鉴权的效果。函数 parseQueryString 可以获取用户访问 Shiny 页面时的个人信息。

三. 参考文献

参考材料主要来源于 Hadley Wickham 的书籍《Mastering Shiny》,Shiny 框架官网,RStudio 官网。

  1. DT 包渲染的交互表格里实现超链接跳转,见 DT 包帮助文档的 2.10 Escaping Table Content 。

  2. 动态 UI 实现级联筛选器,见书籍《Mastering Shiny》的第10章 Dynamic UI

  3. 使用函数 actionButton 实现 submitButton 提交(查询)效果,见帮助文档

  4. shiny 函数 isolate 赋能 observeEventeventReactive 实现多个查询按钮之间的交互,见书籍《Mastering Shiny》的第 15 章 Reactive building blocks

  5. 用户输入反馈见书籍《Mastering Shiny》的第8章 User feedback

  6. flexdashboard 包定制页面布局参考 Bootstrap 布局样式 Columns

  7. R 语言中的拟引用 quasiquotation

  8. 将字符串转变量的函数 get()mget() ,见其帮助文档

  9. 从前端 JavaScript 转移到后端 R 服务渲染选择筛选器 Using selectize input

  10. 在 Shiny 中实现异步查询数据库 Shiny Asynchronous Database Queries

  11. 利用用户访问 Shiny 页面的会话 session 数据做路径传参。见帮助文档 Session 对象 和客户端数据和查询字符串 Client data and query string

  12. Shiny 框架的 Github 示例仓库

  13. John Coene. Javascript for R. 2022. https://book.javascript-for-r.com/

  14. Hadley Wickham. Mastering Shiny. 2021. https://mastering-shiny.org/

  15. Colin Fay, Sébastien Rochette, Vincent Guyader, Cervan Girard. Engineering Production-Grade Shiny Apps. 2022. https://engineering-shiny.org/