1 Introduction

1.1 下载加载shiny包

#install.packages("shiny")
library(shiny)

1.2 创建shinyapp

  • R studioFile-New Project-New Directory-Shiny Web Application
  • R code: 已有app.R file,输入shinyapp然后Shift+tab
ui <- fluidPage(
  "Hello, world!"
)
server <- function(input, output, session) {
}
shinyApp(ui, server)
The very basic shiny app you’ll see when you run the code above

Figure 1.1: The very basic shiny app you’ll see when you run the code above

1.3 运行app

  • R studio: Run App
  • 快捷键ctrl+shift+enter
  • 关闭shiny app窗口结束运行

1.4 添加UI

ui <- fluidPage(
  selectInput("dataset", label = "Dataset", choices = ls("package:datasets")),
  verbatimTextOutput("summary"),
  tableOutput("table")
)
  • fluidPage()设置页面布局
  • selectInput()控制输入
  • verbatimTextOutput()tableOutput()控制输出
The datasets app with UI

Figure 1.2: The datasets app with UI

1.5 添加server

server <- function(input, output, session) {
  output$summary <- renderPrint({
    dataset <- get(input$dataset, "package:datasets")
    summary(dataset)
  }) #对data的统计结果
  
  output$table <- renderTable({
    dataset <- get(input$dataset, "package:datasets")
    dataset
  })#data数据
}
  • 赋值操作符(<-)的左侧,output$ID,表明正在定义具有该ID的Shiny输出
  • 赋值的右侧,使用特定的render{Type}函数来包装代码,每个render{Type}一般会对应一个{type}Output
Now that we’ve provided a server function that connects outputs and inputs, we have a fully functional app

Figure 1.3: Now that we’ve provided a server function that connects outputs and inputs, we have a fully functional app

1.6 利用reactive expression减少重复代码

  • 在上节代码中(Section 1.5),dataset <- get(input$dataset, "package:datasets")重复出现了两次,每次都需要提取一次数据
  • 为了简化代码,可以在reactive({...})里包装一段代码,然后将值赋给一个变量
  • 代码只会运行一次,之后每次调用该变量都是使用之前运行的缓存记录
server <- function(input, output, session) {
  # 创建reactive expression
  dataset <- reactive({
    get(input$dataset, "package:datasets")
  })

  output$summary <- renderPrint({
    # 像调用函数一样调用reactive expression
    summary(dataset())
  })
  
  output$table <- renderTable({
    dataset()
  })
}

1.7 shiny cheatsheet

更多关于shiny的内容可参见 shiny cheatsheet
Shiny cheatsheet, available from https://www.rstudio.com/resources/cheatsheets/

Figure 1.4: Shiny cheatsheet, available from https://www.rstudio.com/resources/cheatsheets/

2 Basic UI

2.1 输入 Inputs

所有的输入类型包括

  • 文本型 (Section 2.1.2)
  • 数值型 (Section 2.1.3)
  • 日期型 (Section 2.1.4)
  • 选择型 (Section 2.1.5)
  • 文件型 (Section 2.1.6)
  • 操作型 (Section 2.1.7)

2.1.1 基本结构

  • 所有的输入的第一个参数都是要定义一个名称inputId(如name),之后在server段通过input$name调用该输入内容,inputID必须:
    • 只包含数字、字母和下划线
    • 独一无二的
  • 大部分输入的第二个参数是label,在app内提示用户输入信息
  • 第三个参数是value,可以设置默认输入
sliderInput("min", "Limit (minimum)", value = 50, min = 0, max = 100)

2.1.2 自由文本类型输入

  • textInput() 输入文本
  • passwordInput() 输入密码
  • textAreaInput() 输入大段文本
ui <- fluidPage(
  textInput("name", "What's your name?"),
  passwordInput("password", "What's your password?"),
  textAreaInput("story", "Tell me about yourself", rows = 3)
)
自由文本类型输入

Figure 2.1: 自由文本类型输入

  • 除了label,也可以采用placeholder提示输入信息
textInput("name",placeholder = 'Your name')
placeholder提示输入信息

Figure 2.2: placeholder提示输入信息

2.1.3 数值输入

  • numericInput() 数值型文本框
  • sliderInput() 数值型滑块条
ui <- fluidPage(
  numericInput("num", "Number one", value = 0, min = 0, max = 100),
  sliderInput("num2", "Number two", value = 50, min = 0, max = 100),
  #如果sliderInput的value是两个数值的向量,则指定一个范围
  sliderInput("rng", "Range", value = c(10, 20), min = 0, max = 100)
)
数值输入

Figure 2.3: 数值输入

#用sliderInput输入日期
  sliderInput("deliver", "When should we deliver?", 
              value = as.Date('2020-09-17'),timeFormat = '%F',
              min = as.Date('2020-09-16'), max = as.Date('2020-09-23')),
用sliderInput输入日期

Figure 2.4: 用sliderInput输入日期

#animation sliderInput
  sliderInput("animation", "Looping Animation:",
              min = 0, max = 100,
              value = 5, step = 5,
              animate =
                animationOptions(interval = 5, loop = TRUE))

2.1.4 日期输入

  • dateInput() 输入一个日期
  • dateRangeInput() 输入一个日期范围
ui <- fluidPage(
  dateInput("dob", "When were you born?"),
  dateRangeInput("holiday", "When do you want to go on vacation next?")
)
日期输入

Figure 2.5: 日期输入

2.1.5 选择输入

  • selectInput() 在下拉框里选择
  • radioButtons() 在选项按钮里选择
animals <- c("dog", "cat", "mouse", "bird", "other", "I hate animals")

ui <- fluidPage(
  selectInput("state", "What's your favourite state?", state.name),
  radioButtons("animal", "What's your favourite animal?", animals)
)
选择输入

Figure 2.6: 选择输入

  • radioButtons()可以使用非文本的选项,用choiceNames设置在app里的展示,choiceValues设置对应的返回值
ui <- fluidPage(
  radioButtons("rb", "Choose one:",
    choiceNames = list(
      icon("angry"),
      icon("smile"),
      icon("sad-tear")
    ),
    choiceValues = list("angry", "happy", "sad")
  )
)
非文本的按钮选择

Figure 2.7: 非文本的按钮选择

  • selectInput() 设置multiple = TRUE可以同时选择多个选项
ui <- fluidPage(
  selectInput(
    "state", "What's your favourite state?", state.name,
    multiple = TRUE
  )
)
多个选择的选择输入

Figure 2.8: 多个选择的选择输入

  • selectInput() 在选项中设置子集
selectInput("state", "Choose a state:",
              list(`East Coast` = list("NY", "NJ", "CT"),
                   `West Coast` = list("WA", "OR", "CA"),
                   `Midwest` = list("MN", "WI", "IA"))
  )
  • radioButtons()没有办法同时选择多项,但是可以用checkboxGroupInput()进行多项选择
ui <- fluidPage(
  checkboxGroupInput("animal", "What animals do you like?", animals)
)
checkbox多项选择输入

Figure 2.9: checkbox多项选择输入

  • checkboxInput()单项的勾选框
ui <- fluidPage(
  checkboxInput("cleanup", "Clean up?", value = TRUE),
  checkboxInput("shutdown", "Shutdown?")
)
checkbox单项选择输入

Figure 2.10: checkbox单项选择输入

2.1.6 文件输入

  • fileInput() 输入文件
ui <- fluidPage(
  fileInput("upload", NULL)
)
文件输入

Figure 2.11: 文件输入

2.1.7 操作输入

  • actionButton() 操作按钮
  • actionLink() 操作链接
  • 操作链接和按钮常与server函数中的observeEvent()eventReactive()配对。
ui <- fluidPage(
  actionButton("click", "Click me!"),
  actionButton("drink", "Drink me!", icon = icon("cocktail"))
)
操作按钮

Figure 2.12: 操作按钮

  • 可以通过class参数设置按钮的外观
    • 样式设置:"btn-primary", "btn-success", "btn-info", "btn-warning", or "btn-danger"
    • 大小设置:"btn-lg", "btn-sm", "btn-xs"
    • 使用"btn-block"使按钮跨越嵌入元素的整个宽度
ui <- fluidPage(
  fluidRow(
    actionButton("click", "Click me!", class = "btn-danger"),
    actionButton("drink", "Drink me!", class = "btn-lg btn-success")
  ),
  fluidRow(
    actionButton("eat", "Eat me!", class = "btn-block")
  )
)
操作按钮样式

Figure 2.13: 操作按钮样式

2.2 输出 Outputs

所有的输出类型包括

  • 文本型 (Section 2.2.1)
  • 表格型 (Section 2.2.2)
  • 图片型 (Section 2.2.3)
  • 下载型 (Section 2.2.4)

2.2.1 文本输出

  • textOutput() 简单文本输出,和renderText()联用,用于将结果连成简单的字符串
  • verbatimTextOutput() 代码和控制输出,和renderPrint()联用,输出R代码的结果
ui <- fluidPage(
  textOutput("text"),
  verbatimTextOutput("code")
)
server <- function(input, output, session) {
  output$text <- renderText({ 
    "Hello friend!" 
  })#简单文本输出
  output$code <- renderPrint({ 
    summary(1:10) 
  })#代码输出
}
文本输出

Figure 2.14: 文本输出

  • {}用于包含多条代码的render语句,上述代码可以简化成
server <- function(input, output, session) {
  output$text <- renderText("Hello friend!")
  output$code <- renderPrint(summary(1:10))
}
  • renderText()verbatimTextOutput()的区别
ui <- fluidPage(
  textOutput("text"),
  verbatimTextOutput("print")
)
server <- function(input, output, session) {
  output$text <- renderText("hello!")
  output$print <- renderPrint("hello!")
}
两种文本输出的不同

Figure 2.15: 两种文本输出的不同

2.2.2 表格输出

  • tableOutput()renderTable() 输出静态表格
  • dataTableOutput()renderDataTable() 输出动态表格
  • reactable包中的 reactableOutput()renderReactable()提供了更多自定义动态表格的选项
ui <- fluidPage(
  tableOutput("static"),
  dataTableOutput("dynamic")
)
server <- function(input, output, session) {
  output$static <- renderTable(head(mtcars))
  output$dynamic <- renderDataTable(mtcars, options = list(pageLength = 5))
}
表格输出

Figure 2.16: 表格输出

2.2.3 图形输出

  • 使用plotOutput()renderPlot()输出图形
ui <- fluidPage(
  plotOutput("plot", width = "400px")
)
server <- function(input, output, session) {
  output$plot <- renderPlot(plot(1:5), res = 96,
                            width = 700,height = 300,alt="a scatterplot of five random numbers")
)
}#建议始终设置res =96,因为这将使您的Shiny图表尽可能接近您在RStudio中看到的。
图形输出

Figure 2.17: 图形输出

2.2.4 下载输出

  • 可以使用downloadButton()downloadLink()让用户下载文件
  • 参见更多信息

3 Basic reactivity

3.1 serve 函数

  • 基本的shiny app组成
library(shiny)

ui <- fluidPage(
  # front end interface
)

server <- function(input, output, session) {
  # back end logic
}

shinyApp(ui, server)
  • ui对象包含html格式的用户界面,每个用户有相同的ui
  • server函数控制app后台操作,每个用户的server不同。每个新的session开始时,shiny都会唤起一次server函数
  • server函数包括三个参数:input,output,session
    • input$ID 调用UI输入的内容
      • 不能在server里直接调用input$ID,需要在reactive环境中调用如:renderText()reactive()
      • 不能给input$ID赋值,必要时可以采用updateNumericInput()
    • outnput$ID 调用UI定义的输出内容,通常与render{}()系列函数联用设置输出内容代码
      • render{}()函数创建一个reactive环境,可以调用input参数内容
      • render{}()函数将输出结果转化成html格式,便于在web上显示

3.2 reactive 编程

  • reactive programming:输入变化时,输出结果也自动跟着变化
  • imperative programming 命令式编程,提出命令,执行结果 (R script)
  • declarative programmin 声明式编程,表达更高层次的目标或描述重要的限制,并依靠其他人来决定如何和/或何时将其转化为行动 (shiny)
  • 执行顺序不是逐条执行,例如,下述代码不会报错:
server <- function(input, output, session) {
  output$greeting <- renderText(string())
  #注意这里要像调用函数一样调用reactive expression
  string <- reactive(paste0("Hello ", input$name, "!"))
  #string定义在调用后
}
  • 上述代码的执行顺序(reactive graph)
    A reactive expression is drawn with angles on both sides because it connects inputs to outputs

    Figure 3.1: A reactive expression is drawn with angles on both sides because it connects inputs to outputs

3.3 reactive expression

响应表达式使用reative()函数创建。可以简化代码(避免重复代码,参见Section 1.6Section 3.3.3,同时具有inputs和outputs的特性

  • 与input一样,可以在output中使用reactive expression的结果。
  • 与output一样,reactive expression依赖于input,并自动知道何时需要更新。
Inputs and expressions are reactive producers; expressions and outputs are reactive consumers

Figure 3.2: Inputs and expressions are reactive producers; expressions and outputs are reactive consumers

3.3.1 一个简单的EDA(Exploratory Data Analysis)例子

下面的例子分析两组数据的分布,并用T检验判断它们的差异是否显著

  • 创建函数绘图和输出结果
library(ggplot2)

freqpoly <- function(x1, x2, binwidth = 0.1, xlim = c(-3, 3)) {
  df <- data.frame(
    x = c(x1, x2),
    g = c(rep("x1", length(x1)), rep("x2", length(x2)))
  )

  ggplot(df, aes(x, colour = g)) +
    geom_freqpoly(binwidth = binwidth, size = 1) +
    coord_cartesian(xlim = xlim)
}

t_test <- function(x1, x2) {
  test <- t.test(x1, x2)
  
  # use sprintf() to format t.test() results compactly
  sprintf(
    "p value: %0.3f\n[%0.2f, %0.2f]",
    test$p.value, test$conf.int[1], test$conf.int[2]
  )
}
  • 创建两组随机数使用上述函数
x1 <- rnorm(100, mean = 0, sd = 0.5)
x2 <- rnorm(200, mean = 0.15, sd = 0.9)

freqpoly(x1, x2)

cat(t_test(x1, x2))
## p value: 0.053
## [-0.31, 0.00]
#> p value: 0.005
#> [-0.39, -0.07]

3.3.2 将常规代码转化成shiny app

  • 创建UI界面
ui <- fluidPage(
  #输入布局
  fluidRow(
    #第一个变量数据设置
    column(4, 
      "Distribution 1",
      numericInput("n1", label = "n", value = 1000, min = 1),
      numericInput("mean1", label = "µ", value = 0, step = 0.1),
      numericInput("sd1", label = "σ", value = 0.5, min = 0.1, step = 0.1)
    ),
    #第二个变量数据设置
    column(4, 
      "Distribution 2",
      numericInput("n2", label = "n", value = 1000, min = 1),
      numericInput("mean2", label = "µ", value = 0, step = 0.1),
      numericInput("sd2", label = "σ", value = 0.5, min = 0.1, step = 0.1)
    ),
    #输出图形设置
    column(4,
      "Frequency polygon",
      numericInput("binwidth", label = "Bin width", value = 0.1, step = 0.1),
      sliderInput("range", label = "range", value = c(-3, 3), min = -5, max = 5)
    )
  ),
  #输出布局
  fluidRow(
    #输出分布图
    column(9, plotOutput("hist")),
    #输出t-test结果
    column(3, verbatimTextOutput("ttest"))
  )
)
  • 创建server函数
server <- function(input, output, session) {
  #绘图
  output$hist <- renderPlot({
    x1 <- rnorm(input$n1, input$mean1, input$sd1)
    x2 <- rnorm(input$n2, input$mean2, input$sd2)
    
    freqpoly(x1, x2, binwidth = input$binwidth, xlim = input$range)
  }, res = 96)
  
  #t-test检验
  output$ttest <- renderText({
    x1 <- rnorm(input$n1, input$mean1, input$sd1)
    x2 <- rnorm(input$n2, input$mean2, input$sd2)
    
    t_test(x1, x2)
  })
}
A Shiny app that lets you compare two simulated distributions with a t-test and a frequency polygon See live at https://hadley.shinyapps.io/ms-case-study-1

Figure 3.3: A Shiny app that lets you compare two simulated distributions with a t-test and a frequency polygon See live at https://hadley.shinyapps.io/ms-case-study-1

  • 上述代码的reactive graph如下图所示,具有如下缺点
    • 这个应用程序很难理解,因为有太多的连接。
    • 这个应用的效率很低,因为它做了太多不必要的工作。例如,如果您更改了绘图的breaks,则会重新计算数据;如果你改变n1的值,x2也会被更新(在histttest两个地方!)
      The reactive graph shows that every output depends on every input

      Figure 3.4: The reactive graph shows that every output depends on every input

3.3.3 用reative expression简化app

  • 之前的server函数里x1x2都重复计算了两次,可以使用reactive expression中简化代码
server <- function(input, output, session) {
  x1 <- reactive(rnorm(input$n1, input$mean1, input$sd1))
  x2 <- reactive(rnorm(input$n2, input$mean2, input$sd2))

  output$hist <- renderPlot({
    freqpoly(x1(), x2(), binwidth = input$binwidth, xlim = input$range)
  }, res = 96)

  output$ttest <- renderText({
    t_test(x1(), x2())
  })
}
  • 此时的reactive graph如下图所示
    Using reactive expressions considerably simplifies the graph, making it much easier to understand

    Figure 3.5: Using reactive expressions considerably simplifies the graph, making it much easier to understand

在Shiny中,应该考虑一个规则:

每当复制和粘贴一次内容时,应该考虑将重复的代码提取到reactive expression中。

因为reactive expression不仅使人类更容易理解代码,而且还提高了Shiny有效重新运行代码的能力。

3.4 控制app更新

3.4.1 一个app例子

下面是一个简单的app例子,输入两个lambda分布的参数lambda1lambda2n,使用rpoins()生成随机的两组数,输出它们的分布图

ui <- fluidPage(
  fluidRow(
    column(3, 
      numericInput("lambda1", label = "lambda1", value = 3),
      numericInput("lambda2", label = "lambda2", value = 5),
      numericInput("n", label = "n", value = 1e4, min = 0)
    ),
    column(9, plotOutput("hist"))
  )
)
server <- function(input, output, session) {
  x1 <- reactive(rpois(input$n, input$lambda1))
  x2 <- reactive(rpois(input$n, input$lambda2))
  output$hist <- renderPlot({
    freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  }, res = 96)
}
A simpler app that displays a frequency polygon of random numbers drawn from two Poisson distributions. See live at https://hadley.shinyapps.io/ms-simulation-2.

Figure 3.6: A simpler app that displays a frequency polygon of random numbers drawn from two Poisson distributions. See live at https://hadley.shinyapps.io/ms-simulation-2.

The reactive graph

Figure 3.7: The reactive graph

3.4.2 reactiveTimer()定时更新

  • reactiveTimer()是一个响应式表达式,它依赖于一个隐藏的输入:当前时间。
  • 下面的例子通过reactiveTimer()设置每0.5s更新一次
server <- function(input, output, session) {
  timer <- reactiveTimer(500)#引入reactiveTimer
  
  x1 <- reactive({
    #调用创建的reactiveTimer,使得x1和x2响应表达式依赖于timer
    timer()
    rpois(input$n, input$lambda1)
  })
  x2 <- reactive({
    timer()
    rpois(input$n, input$lambda2)
  })
  
  output$hist <- renderPlot({
    freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  }, res = 96)
}
  • 此时的reactive graph为
    reactiveTimer(500) introduces a new reactive input that automatically invalidates every half a second

    Figure 3.8: reactiveTimer(500) introduces a new reactive input that automatically invalidates every half a second

3.4.3 添加操作按钮手动更新

  • 使用actionButton引入操作按钮(Section 2.1.7)
  • 将其引入x1x2的响应表达式中,每次点击时激活一次更新
ui <- fluidPage(
  fluidRow(
    column(3, 
      numericInput("lambda1", label = "lambda1", value = 3),
      numericInput("lambda2", label = "lambda2", value = 5),
      numericInput("n", label = "n", value = 1e4, min = 0),
      actionButton("simulate", "Simulate!")#引入actionbutton
    ),
    column(9, plotOutput("hist"))
  )
)

server <- function(input, output, session) {
  x1 <- reactive({
    input$simulate #actionbutton加入reactive expression
    rpois(input$n, input$lambda1)
  })
  x2 <- reactive({
    input$simulate
    rpois(input$n, input$lambda2)
  })
  output$hist <- renderPlot({
    freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  }, res = 96)
}
App with action button. See live at https://hadley.shinyapps.io/ms-action-button.

Figure 3.9: App with action button. See live at https://hadley.shinyapps.io/ms-action-button.

  • x1x2随着simulatelambda1lambda2n的变化更新,这意味者上述任意一个发生变化都会影响输出结果
    This reactive graph doesn’t accomplish our goal; we’ve added a dependency instead of replacing the existing dependencies.

    Figure 3.10: This reactive graph doesn’t accomplish our goal; we’ve added a dependency instead of replacing the existing dependencies.

3.4.4 eventReactive()事件触发

  • 如果只想当simulate点击时才触发响应表达式,需要采用eventReactive()函数
  • eventReactive()有两个参数:第一个参数指定要依赖什么(这里是silumate()),第二个参数指定要计算什么(这里是rpois()
server <- function(input, output, session) {
  x1 <- eventReactive(input$simulate, {
    rpois(input$n, input$lambda1)
  })
  x2 <- eventReactive(input$simulate, {
    rpois(input$n, input$lambda2)
  })

  output$hist <- renderPlot({
    freqpoly(x1(), x2(), binwidth = 1, xlim = c(0, 40))
  }, res = 96)
}
  • x1x2只依赖于simulate,`这意味者只有点击simulate按钮才会改变输出结果
    eventReactive() makes it possible to separate the dependencies (black arrows) from the values used to compute the result (pale gray arrows).

    Figure 3.11: eventReactive() makes it possible to separate the dependencies (black arrows) from the values used to compute the result (pale gray arrows).

3.4.5 observerEvent()响应输入变化

  • observeEvent()eventReactive()非常相似。它有两个重要参数:eventExprhandlerExpr。第一个参数是要依赖的输入或表达式;第二个参数是将要运行的代码。
  • 下列代码在input$name发生变化时,输出message到控制台
ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting")
)

server <- function(input, output, session) {
  string <- reactive(paste0("Hello ", input$name, "!"))
  
  output$greeting <- renderText(string())
  observeEvent(input$name, {
    message("Greeting performed")
  })
}
 In the reactive graph, an observer looks the same as an output

Figure 3.12: In the reactive graph, an observer looks the same as an output

  • 注意:observeEvent()不能赋给一个变量,也不能在其他的响应表达式中使用它