Shiny actionButton点击x次对应observe()触发x次问题排查求助
Shiny应用observeEvent重复触发问题
我对Shiny较为陌生,近期在开发数据分析类Shiny应用时发现observeEvent块存在异常行为:应用需要支持用户先选择数据子集,再指定散点图的x轴、y轴与颜色梯度映射字段。
为避免未完成全部选择时就执行绘图逻辑,我使用actionButton触发子集筛选逻辑。数据包含不同组织的信号,支持用户选择单个或多个组织。
只有点击actionButton后,才会渲染带复选框的DT表格,表格列出所有标记项,用户可选择分别对应x/y/颜色梯度的标记。该功能通过javascript callback实现,但我之前获取Marker字符串时遇到问题,只有将回调逻辑放在observe()中,搭配我自己并不满意的复杂lapply(sapply())组合才能生效,observer内部会在x/y/颜色梯度三个参数都定义完成后才允许执行绘图。
原本这套流程运行符合预期,直到我发现actionButton每被点击x次,后续对应的observer就会被触发x次。我猜测可能有重置actionButton计数器的简单解决方案,或者问题出在JS回调的读取逻辑上,因此寻求相关建议。我已经移除了所有非必要内容(绘图、额外筛选逻辑等)整理出最小可复现示例:
library(shiny) library(DT) ui <- fluidPage( column(5, br(), h4("点击actionButton x次后代码会重复运行x次"), p(style="text-align: justify;", "我需要对大数据框的子集绘图,用户需要选择三项内容:组织子集、散点图x轴、y轴与颜色映射列。为避免提前绘图,我使用actionButton定义组织筛选字符串,通过subset(data, Tissue %in% input)对数据框进行筛选,代码会等待actionButton被点击: "), DT::dataTableOutput('select.tissues.for.gatecheck'), actionButton("confirmTissuesToCheckGates", "确认并锁定组织选择"), br(), verbatimTextOutput('selected.tissue'), br(), br(), br(), p(style="text-align: justify;", "点击按钮后会出现标记列表,你可以选择三个标记信号用于绘制散点图,只有三个选项都选完后才会执行绘图。该功能原本正常,但如果修改组织选择后再次点击确认,代码就会开始多次执行: "), p(style="text-align: justify;", "你可以通过控制台轻松复现该行为:点击确认按钮并选中三个复选框中的两个时,服务端会打印三次`Not gonna do anything`,选中第三个复选框后会触发绘图打印`Plotting now`。如果之后你重新选择组织并取消一个标记的选中状态,会看到observe块已经会执行两次,再次重新选择组织后会执行三次,我不清楚是什么原因导致该问题。 "), DT::dataTableOutput('select.scatterplot.markers.cell.dat') , verbatimTextOutput('selected.scatterplot.marker.cols') ) #end column ) #end ui fluidpage server <- function(input, output, session) { # 所有响应式变量都存在这里: paths <- reactiveValues() # 这是一个包含信号等数据的长数据框,信号按类别排序: categories <- c("脾脏", "胰腺", "肝脏" ) output$select.tissues.for.gatecheck = DT::renderDataTable( as.matrix( categories ) , server = TRUE , options = list(pageLength = -1, info = TRUE, lengthMenu = list( c(-1, 1), c("全部", "1") ), columns = list( list(title = "数据中可用的组织:" ) ) ) ) marker.signals <- c("Marker1_signal", "Marker2_signal", "Marker3_signal", "Marker4_signal") m = matrix( as.character(1:3), nrow = length(marker.signals), ncol = 3, byrow = TRUE, dimnames = list(marker.signals, c("第一个标记(x轴)","第二个标记(y轴)","第三个标记(颜色梯度)") ) ) for (i in seq_len(nrow(m))) { m[i, ] = sprintf( '<input type="checkbox" name="%s" value="%s"/>', # 单选框不符合需求,因为不支持用户取消选择回到初始状态... marker.signals[i], m[i, ] ) } m output$select.scatterplot.markers.cell.dat = DT::renderDataTable( m, escape = FALSE, selection = 'none', server = FALSE, options = list(dom = 't', paging = FALSE, ordering = FALSE#, ), callback = JS("table.rows().every(function(i, tab, row) { var $this = $(this.node()); $this.attr('id', this.data()[0]); $this.addClass('shiny-input-checkboxgroup'); }); Shiny.unbindAll(table.table().node()); Shiny.bindAll(table.table().node());") ) # 给文本框填充预选提示文本: output$selected.tissue = renderPrint({ cat('未选中任何组织:\n\n') cat( "等待确认") #} }) observeEvent(input$confirmTissuesToCheckGates, { # 存储选中的组织,后续需要使用该选择做其他处理 paths$do.something.with.selected.tissue <- categories[ input$select.tissues.for.gatecheck_rows_selected ] #...这里还有其他无关代码... # 用文本框回显选中的组织子集: output$selected.tissue = renderPrint({ cat( "使用以下组织筛选数据:\n\n" ) cat( isolate(paths$do.something.with.selected.tissue) , sep = ', ') })#end render print # 完成部分绘图逻辑后取回变量: scatterplots.of.selected.tissue <- isolate( paths$scatterplots.of.selected.tissue ) observe({ # 从JS中取回标记字符串的方式非常糟糕 # 1 sapply读取复选框输入,生成包含NULL和选中状态字符1、2、3的列表 # 2 lapply遍历该列表,用grep匹配对应值的元素,而非返回布尔值 # 3 从匹配到的元素中取名称 # 4 选中取消标记的过程中可能出现一个轴对应多个标记的情况,会导致ggplot崩溃,因此只取第一个元素 capture.output( cat( names(unlist(lapply(sapply(marker.signals, function(i) input[[i]]), function(ch) grep("1", ch, value = TRUE))))[1] ) ) -> paths$scatter.plot.x.marker capture.output( cat( names(unlist(lapply(sapply(marker.signals, function(i) input[[i]]), function(ch) grep("2", ch, value = TRUE))))[1] ) ) -> paths$scatter.plot.y.marker capture.output( cat( names(unlist(lapply(sapply(marker.signals, function(i) input[[i]]), function(ch) grep("3", ch, value = TRUE))))[1] ) ) -> paths$scatter.plot.color.marker # 在文本框中打印选中的标记,我没有把该输出放在三个参数都选完才执行的保护块中,希望点击时能动态更新 # 该渲染不会报错,因为变量预加载为NULL,paste0打印NULL不会失败 output$selected.scatterplot.marker.cols = renderPrint({ cat( paste0("x轴标记: ", isolate( paths$scatter.plot.x.marker) , "\ny轴标记: ", isolate( paths$scatter.plot.y.marker) , "\n颜色梯度标记: ", isolate( paths$scatter.plot.color.marker) ) ) })#end textoutput of what is currently selected if( length( isolate( paths$scatter.plot.color.marker) )==1 & length( isolate( paths$scatter.plot.y.marker) )==1 & length( isolate( paths$scatter.plot.x.marker) )==1 ){ message("Plotting now") }else{ message("Not gonna do anything") } })#end observe the JS callback }, ignoreInit = TRUE )# end observe "锁定组织"按钮 }#end server shinyApp(ui,server)
临时解决方案
我找到了一个无需使用actionButton的临时解决方案,直接将rows_selected作为observer的依赖,在组织选择未完成时拦截代码执行即可,对应代码如下:
observe({ paths$do.something.with.selected.tissue <- categories[ input$select.tissues.for.gatecheck_rows_selected ] if( length( isolate( paths$do.something.with.selected.tissue )) ){ # 用文本框回显选中的组织子集: output$selected.tissue = renderPrint({ cat( "使用以下组织筛选数据:\n\n" ) cat( isolate(paths$do.something.with.selected.tissue) , sep = ', ') })#end render print # 从JS中取回标记字符串的方式非常糟糕 # 1 sapply读取复选框输入,生成包含NULL和选中状态字符1、2、3的列表 # 2 lapply遍历该列表,用grep匹配对应值的元素,而非返回布尔值 # 3 从匹配到的元素中取名称 # 4 选中取消标记的过程中可能出现一个轴对应多个标记的情况,会导致ggplot崩溃,因此只取第一个元素 capture.output( cat( names(unlist(lapply(sapply(marker.signals, function(i) input[[i]]), function(ch) grep("1", ch, value = TRUE))))[1] ) ) -> paths$scatter.plot.x.marker capture.output( cat( names(unlist(lapply(sapply(marker.signals, function(i) input[[i]]), function(ch) grep("2", ch, value = TRUE))))[1] ) ) -> paths$scatter.plot.y.marker capture.output( cat( names(unlist(lapply(sapply(marker.signals, function(i) input[[i]]), function(ch) grep("3", ch, value = TRUE))))[1] ) ) -> paths$scatter.plot.color.marker if( length( isolate( paths$scatter.plot.color.marker) )==1 & length( isolate( paths$scatter.plot.y.marker) )==1 & length( isolate( paths$scatter.plot.x.marker) )==1 ){ message("Plotting now") }else{ message("Not gonna do anything") } }#and if lenght action button # 在文本框中打印选中的标记,我没有把该输出放在三个参数都选完才执行的保护块中,希望点击时能动态更新 # 该渲染不会报错,因为变量预加载为NULL,paste0打印NULL不会失败 output$selected.scatterplot.marker.cols = renderPrint({ cat( paste0("x轴标记: ", isolate( paths$scatter.plot.x.marker) , "\ny轴标记: ", isolate( paths$scatter.plot.y.marker) , "\n颜色梯度标记: ", isolate( paths$scatter.plot.color.marker) ) ) })#end textoutput of what is currently selected })#end observe the JS callback
内容的提问来源于stack exchange,提问作者MaBar1
相关产品推荐
相关产品推荐

