本文引用的所有信息均为公开信息,仅代表作者本人观点,与就职单位无关。
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 官网。
DT 包渲染的交互表格里实现超链接跳转,见 DT 包帮助文档的 2.10 Escaping Table Content 。
动态 UI 实现级联筛选器,见书籍《Mastering Shiny》的第10章 Dynamic UI 。
使用函数
actionButton
实现submitButton
提交(查询)效果,见帮助文档。shiny 函数
isolate
赋能observeEvent
和eventReactive
实现多个查询按钮之间的交互,见书籍《Mastering Shiny》的第 15 章 Reactive building blocks 。用户输入反馈见书籍《Mastering Shiny》的第8章 User feedback 。
flexdashboard 包定制页面布局参考 Bootstrap 布局样式 Columns 。
R 语言中的拟引用 quasiquotation 。
将字符串转变量的函数
get()
和mget()
,见其帮助文档 。从前端 JavaScript 转移到后端 R 服务渲染选择筛选器 Using selectize input 。
在 Shiny 中实现异步查询数据库 Shiny Asynchronous Database Queries 。
利用用户访问 Shiny 页面的会话 session 数据做路径传参。见帮助文档 Session 对象 和客户端数据和查询字符串 Client data and query string。
Shiny 框架的 Github 示例仓库。
John Coene. Javascript for R. 2022. https://book.javascript-for-r.com/
Hadley Wickham. Mastering Shiny. 2021. https://mastering-shiny.org/
Colin Fay, Sébastien Rochette, Vincent Guyader, Cervan Girard. Engineering Production-Grade Shiny Apps. 2022. https://engineering-shiny.org/