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.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的计时器,并在x1
和x2
中添加了计时器,这使得在1s中就会更新两次x1
和x2
,并重新绘图。
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!“的按钮,并在x1
和x2
处的响应表达式中添加了input$simulate
,这使得在每次点击时都能重新更新数据。
但这样的编程结果会创造两种依赖关系:x1
和x2
既依赖输入参数,又依赖按钮。当上述两种源头变化时,数据都会更新。倘若我们只想在点击按钮时更新数据(即使参数已经发生了变化),这里引入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)
}
这里的x1
和x2
仅会在点击按钮时更新一次数据。
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
的变化。