5.3 反应式编程

反应式编程,简而言之,就是当输入变化时,所有相关的输出将会实时更新,而其余无关的输出则保持原状。因此,当你运行shiny程序时它并不会立即执行内部的代码,内部代码仅仅声明了处理的逻辑,是否运行以及何时运行都取决于shiny,也就是说,shiny程序是懒惰的。

shiny的编程风格属于声明式风格,并不像常规代码按照前后顺序运行,而是根据内部的逻辑链运行

5.3.1 服务端

回忆shiny应用程序的一般形式

library(shiny)

ui <- fluidPage(
  # front end interface
)

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

shinyApp(ui, server)

ui表示交互界面,呈现给每个用户的内容是相同的。server表示服务器端,由于每个用户输入的信息不尽相同,因此shiny程序在每次创建新会话(session)时都会独立地激活server()

5.3.1.1 输入

对于ui中的输入input,会将其存储为类似列表的对象,每个元素的名称都是ui中相应的ID,在server中可通过input$ID来调用输入值。

注意input不能修改,只能读取

5.3.1.2 输出

inputoutput也是类似列表的对象。对于你想输出的内容可通过output$ID的赋予其唯一标识符,然后在ui中的输出函数中根据ID进行调用。

注意output$ID赋值时一定要搭配渲染函数`render``

5.3.2 反应表达式

反应表达式创建了一种依赖关系,当且仅当输入变化时才会更新信息,并可重复使用,简化代码。它同时具有input与output的特点:作为信息更新后的output,作为渲染函数的input。

如果大于1次使用,都应考虑使用反应表达式

下面对两个正态分布数据进行模拟,比较这两段代码(仅在server部分有差异):

library(shiny)
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]
  )
}

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")),
    column(3, verbatimTextOutput("ttest"))
  )
)

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)
  
  output$ttest <- renderText({
    x1 <- rnorm(input$n1, input$mean1, input$sd1)
    x2 <- rnorm(input$n2, input$mean2, input$sd2)
    
    t_test(x1, x2)
  })
}

shinyApp(ui,server)
library(shiny)
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]
  )
}

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")),
    column(3, verbatimTextOutput("ttest"))
  )
)

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())
  })
}

shinyApp(ui,server)

在第一段代码中,shiny会把输出看成一个整体,即使你只改变第一个分布的参数,那么在运行时shiny也会重新对这两个分布进行抽样。而我们的本意是对第一个分布重新抽样,保持第二个分布的数据不变。

在第二段代码中,抽样过程都被包含在反应表达式reactive()中,这样shiny仅会在对应输入更新时运行此处的代码,否则保持原始数据。也就是说,反应表达式是一个模块化单元,就像平时使用的函数一样。

在使用反应表达式时,除了要有reactive(),在其他地方调用对象时还得有(),正如这里的x1()x2()

5.3.3 控制更新

5.3.3.1 自动更新

我们除了在更改输入时想更新数据外,有时还想在同一输入下进行多次更新(如果可以的话)。例如在同一分布下(意味着输入不变),我们想看看多次抽样的结果(重复抽样操作)。这时,可以利用reactiveTimer(interval),根据时间间隔interval来周期性地触发响应式更新的计时器函数,依赖于它的响应式表达式/输出按固定时间间隔自动重新计算,非常适合实现实时数据刷新、动态仪表盘等功能。

interval以毫秒为单位, 1s=1000ms

server <- function(input, output, session) {
  timer <- reactiveTimer(500)

  x1 <- reactive({
    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)
}

这里创建了500ms的计时器,并在x1x2中添加了计时器,这使得在1s中就会更新两次x1x2,并重新绘图。

5.3.3.2 手动更新

或许我们也想在同一输入下,手动点击按钮来进行一次更新,这时引入actionButton()

library(shiny)
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)
}

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!")
    ),
    column(9, plotOutput("hist"))
  )
)
server <- function(input, output, session) {
  x1 <- reactive({
    input$simulate
    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)
}

shinyApp(ui,server)

此处利用actionButton()创建了一个ID为”simulate”,标签为”Simulate!“的按钮,并在x1x2处的响应表达式中添加了input$simulate,这使得在每次点击时都能重新更新数据。

但这样的编程结果会创造两种依赖关系:x1x2既依赖输入参数,又依赖按钮。当上述两种源头变化时,数据都会更新。倘若我们只想在点击按钮时更新数据(即使参数已经发生了变化),这里引入eventReactive()eventReactive()有两个参数,第一个参数指定与谁创建依赖关系,第二个参数表示需要计算的内容。

需要计算的内容用{}包裹

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仅会在点击按钮时更新一次数据。

5.3.4 信息反馈

现在介绍新的反应表达式——observeEvent()——用于监控特定事件并执行响应操作。

observeEvent()的第一个参数为依赖对象,第二个参数为要执行的代码块。与eventReactive()不同,observeEvent()并没有返回值。

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(paste0("Greeting performed: ", input$name))
  })
}

这里的observeEvent()监控input$name的变化。