挑战动态生成的交互式 R Shiny 图(主要是功能代码)
Challenge with dynamically generated, interactive R Shiny plots (mostly functioning code)
以下是最低代码。它有效,但有一个奇怪的问题。这是有效的方法:
- 用户可以 select 多个地块(默认为 3)。
- 用户可以点击绘图并显示该值(部分有效)。
重现 "partly works" 的步骤:
- 启动时,点击 plot #3,没问题。
- 点击情节#2,没有任何反应。
- 将地块数量从 3 个减少到 2 个,然后再减少到 3 个。
- 点击情节#2,现在可以了。
- 点击 plot #1,没有任何反应。
- 将地块数量从 3 减少到 1,然后再减少到 3。
- 点击 plot #1,现在可以了。
如果您重新加载应用程序,并从上面的第 6 步开始,所有绘图都会按预期进行交互。
rm(list=ls())
library(shiny)
#
# Dynamic number of plots:
# That can invalidate each other:
#
ui <- (fluidPage(sidebarLayout(
sidebarPanel(
numericInput("np", "Plots:", min=0, max=10, value=3, step=1)
)
,mainPanel(
fluidRow(uiOutput("plots"))
)
)))
server <- function(input, output, session) {
val <- reactiveValues()
dum <- reactiveValues(v=0)
obs <- list()
### This is the function to break the whole data into different blocks for each page
plotInput <- reactive({
print("Reactive")
np <- input$np
for(i in 1:np) {
cx <- paste0("clk_p",i); dx <- paste0("dbl_p",i); px <- paste0("p",i)
obs[[cx]] <- observeEvent(input[[cx]], {
req(input[[cx]]); val[[px]] <- input[[cx]]$x; dum$v <- dum$v+1; print(paste("Dum",dum$v))
})
obs[[dx]] <- observeEvent(input[[dx]], {
req(input[[dx]]); val[[px]] <- NULL
})
}
return (list(np=np))
})
##### Create divs######
output$plots <- renderUI({
print("Tag plots")
pls <- list()
for(i in 1:plotInput()$np) {
pls[[i]] <- column(4,
plotOutput(paste0("p",i), height=200, width=200
,click=paste0("clk_p",i)
,dblclick=paste0("dbl_p",i))
)
}
tagList(pls)
})
observe({
print("Observe")
lapply(1:plotInput()$np, function(i){
output[[paste("p", i, sep="") ]] <- renderPlot({
print(paste("Plot",dum$v))
x <- val[[paste0("p",i)]]
x <- ifelse(is.null(x),"NA",round(x,2))
par(mar=c(2,2,2,2))
plot(x=runif(20), y=runif(20), main=i, xlim=c(0,1), ylim=c(0,1), pch=21, bg="gray", cex=1.5)
if(is.numeric(x)) abline(v=x, col="blue")
rm(x)
})
})
})
}
shinyApp(ui, server)
这是您要执行的操作的工作版本:
library(shiny)
ui <- fluidPage(
sidebarPanel(
numericInput("num", "Plots:", 3)
),
mainPanel(
uiOutput("plots")
)
)
server <- function(input, output, session) {
obs <- list()
val <- reactiveValues()
observe({
lapply(seq(input$num), function(i){
output[[paste0("plot", i) ]] <- renderPlot({
click_id <- paste0("clk_p",i);
plot(x = runif(20), y = runif(20), main=i)
if (!is.null(val[[click_id]])) {
abline(v = val[[click_id]], col = "blue")
}
})
})
})
observe({
lapply(seq(input$num), function(i){
id <- paste0("clk_p",i);
if (!is.null(obs[[id]])) {
obs[[id]]$destroy()
}
val[[id]] <- NULL
obs[[id]] <<- observeEvent(input[[id]], {
cat('clicked ', id, ' ', input[[id]]$x, '\n')
val[[id]] <- input[[id]]$x
}, ignoreInit = TRUE)
})
})
output$plots <- renderUI({
lapply(seq(input$num), function(i) {
id <- paste0("plot", i)
plotOutput(id, height=200, width=200, click=paste0("clk_p",i))
})
})
}
shinyApp(ui,server)
给以后看到这个的人的一些主要指示:
- 原始代码的主要问题是所有观察者都只注册最后一个 ID。这是一个有点高级的概念,与 R 中环境的工作方式有关,因为它们是在 for 循环中创建的。解决方法是使用
lapply()
而不是 for 循环来创建观察者
- 另一个问题是
obs
覆盖了列表中的观察者,但是之前的观察者仍然存在并且仍然可以触发,所以我向 destroy()
现有观察者添加了逻辑。
- shiny 中最重要的规则之一是不要在反应式中放置副作用(
plotInput
有副作用)所以我以一种避免这种情况的方式重写了代码
以下是最低代码。它有效,但有一个奇怪的问题。这是有效的方法:
- 用户可以 select 多个地块(默认为 3)。
- 用户可以点击绘图并显示该值(部分有效)。
重现 "partly works" 的步骤:
- 启动时,点击 plot #3,没问题。
- 点击情节#2,没有任何反应。
- 将地块数量从 3 个减少到 2 个,然后再减少到 3 个。
- 点击情节#2,现在可以了。
- 点击 plot #1,没有任何反应。
- 将地块数量从 3 减少到 1,然后再减少到 3。
- 点击 plot #1,现在可以了。
如果您重新加载应用程序,并从上面的第 6 步开始,所有绘图都会按预期进行交互。
rm(list=ls())
library(shiny)
#
# Dynamic number of plots:
# That can invalidate each other:
#
ui <- (fluidPage(sidebarLayout(
sidebarPanel(
numericInput("np", "Plots:", min=0, max=10, value=3, step=1)
)
,mainPanel(
fluidRow(uiOutput("plots"))
)
)))
server <- function(input, output, session) {
val <- reactiveValues()
dum <- reactiveValues(v=0)
obs <- list()
### This is the function to break the whole data into different blocks for each page
plotInput <- reactive({
print("Reactive")
np <- input$np
for(i in 1:np) {
cx <- paste0("clk_p",i); dx <- paste0("dbl_p",i); px <- paste0("p",i)
obs[[cx]] <- observeEvent(input[[cx]], {
req(input[[cx]]); val[[px]] <- input[[cx]]$x; dum$v <- dum$v+1; print(paste("Dum",dum$v))
})
obs[[dx]] <- observeEvent(input[[dx]], {
req(input[[dx]]); val[[px]] <- NULL
})
}
return (list(np=np))
})
##### Create divs######
output$plots <- renderUI({
print("Tag plots")
pls <- list()
for(i in 1:plotInput()$np) {
pls[[i]] <- column(4,
plotOutput(paste0("p",i), height=200, width=200
,click=paste0("clk_p",i)
,dblclick=paste0("dbl_p",i))
)
}
tagList(pls)
})
observe({
print("Observe")
lapply(1:plotInput()$np, function(i){
output[[paste("p", i, sep="") ]] <- renderPlot({
print(paste("Plot",dum$v))
x <- val[[paste0("p",i)]]
x <- ifelse(is.null(x),"NA",round(x,2))
par(mar=c(2,2,2,2))
plot(x=runif(20), y=runif(20), main=i, xlim=c(0,1), ylim=c(0,1), pch=21, bg="gray", cex=1.5)
if(is.numeric(x)) abline(v=x, col="blue")
rm(x)
})
})
})
}
shinyApp(ui, server)
这是您要执行的操作的工作版本:
library(shiny)
ui <- fluidPage(
sidebarPanel(
numericInput("num", "Plots:", 3)
),
mainPanel(
uiOutput("plots")
)
)
server <- function(input, output, session) {
obs <- list()
val <- reactiveValues()
observe({
lapply(seq(input$num), function(i){
output[[paste0("plot", i) ]] <- renderPlot({
click_id <- paste0("clk_p",i);
plot(x = runif(20), y = runif(20), main=i)
if (!is.null(val[[click_id]])) {
abline(v = val[[click_id]], col = "blue")
}
})
})
})
observe({
lapply(seq(input$num), function(i){
id <- paste0("clk_p",i);
if (!is.null(obs[[id]])) {
obs[[id]]$destroy()
}
val[[id]] <- NULL
obs[[id]] <<- observeEvent(input[[id]], {
cat('clicked ', id, ' ', input[[id]]$x, '\n')
val[[id]] <- input[[id]]$x
}, ignoreInit = TRUE)
})
})
output$plots <- renderUI({
lapply(seq(input$num), function(i) {
id <- paste0("plot", i)
plotOutput(id, height=200, width=200, click=paste0("clk_p",i))
})
})
}
shinyApp(ui,server)
给以后看到这个的人的一些主要指示:
- 原始代码的主要问题是所有观察者都只注册最后一个 ID。这是一个有点高级的概念,与 R 中环境的工作方式有关,因为它们是在 for 循环中创建的。解决方法是使用
lapply()
而不是 for 循环来创建观察者 - 另一个问题是
obs
覆盖了列表中的观察者,但是之前的观察者仍然存在并且仍然可以触发,所以我向destroy()
现有观察者添加了逻辑。 - shiny 中最重要的规则之一是不要在反应式中放置副作用(
plotInput
有副作用)所以我以一种避免这种情况的方式重写了代码