R语言 未使用DT和下拉选择更新shiny中的值

bfhwhh0e  于 2023-04-09  发布在  其他
关注(0)|答案(2)|浏览(137)

我尝试使用Shiny App动态编辑data.frame。该表描述了项目的实验结构,因此将包括可变的实验因子,每个因子具有可变水平。对于多水平因子,我希望允许从下拉菜单中选择值。出于实际原因,我希望用户也能够在Excel/任何其他表格编辑器中编辑该表,所以将保存为csv太.所以应用程序应该:

  • 重新加载本地csv表
  • 在相应的单元格中使用允许值(因子水平)为因子创建下拉选项,
  • 还允许手动编辑其他列中的值,
  • 当点击“保存”按钮时,应用程序应保存更新的csv表,然后关闭。然而,当手动编辑按预期保存时,如下例所示,下拉选择不起作用。
require(shiny)
require(DT)

# Prepare mock data
wd <- getwd()
Factors <- c("Experiment", "Condition", "Replicate", "Genotype")
ExpMapNm <- "Experiment map"
ExpMapPath <- paste0(wd, "/", ExpMapNm, ".csv")
ExpMap <- data.frame(Experiment = "Exp1",
                     Sample = paste0("Sample", 1:20),
                     Condition = as.character(sapply(c("Treated", "Mock"), function(x) { rep(x, 10) })),
                     Genotype = as.character(sapply(c("KO", "WT"), function(x) { rep(x, 5) })),
                     Replicate = 1:5)
FactorsList <- setNames(lapply(Factors, function(x) { unique(ExpMap[[x]]) }), Factors)
#
# Users should be able to edit this table in two ways:
# - in Excel manually, hence why a local version is saved below and in server when closing the app
# - within the Shiny app
# In either case, the edited table will then be reloaded into r as a data.frame which will guide further data processing
if (!file.exists(ExpMapPath)) { write.csv(ExpMap, ExpMapPath, row.names = FALSE) }

ui <- shiny::fluidPage(shiny::titlePanel(ExpMapNm), # This is the name of the table
                       shiny::mainPanel(shiny::br(),
                                        shiny::actionButton("saveBtn", "Save"),
                                        DT::dataTableOutput("Data"),
                                        verbatimTextOutput(Factors)
                       ))
server <- function(input, output) {
  Data <- read.csv(ExpMapPath)
  for (Fact in Factors) {
    if (length(FactorsList[[Fact]]) > 1) { # We only want to have a drop-down selection if a factor has more than one level.
      # Thus, in this example we do not create a drop-down selection for factor Experiment.
      lvls <- FactorsList[[Fact]] # Allowed values
      for (i in 1:nrow(Data)) {
        val <- Data[[Fact]][i]
        dflt <- c(lvls[1], val)[(val %in% lvls)+1] # If the current value is not a valid level, revert to default (1st) level
        # Otherwise keep current value, but still apply drop-down selection so choices can be changed
        Data[[Fact]][i] <- as.character(selectInput(paste0(Fact, "_", i), NULL, lvls, dflt, width = "100px"))
      }
    } else { Data[[Fact]] <- FactorsList[[Fact]] }
  }
  output$Data <- DT::renderDataTable(
    Data, escape = FALSE, selection = "none", server = FALSE,
    editable = TRUE, # Non-drop-down fields can still be edited: this works
    options = list(paging = TRUE, searching = TRUE, fixedColumns = TRUE, autoWidth = TRUE,
                   ordering = TRUE, dom = "Bfrtip"),
    callback = JS("table.rows().every(function(i, tab, row) {
        var $this = $(this.node());
        $this.attr('id', this.data()[0]);
        $this.addClass('shiny-input-container');
      });
      Shiny.unbindAll(table.table().node());
      Shiny.bindAll(table.table().node());"),
    class = "display")
  shiny::observeEvent(input$Data_cell_edit, {
    Data[input$Data_cell_edit$row, input$Data_cell_edit$col] <<- input$Data_cell_edit$value
  })
  shiny::observeEvent(input$saveBtn, {
    # Hacky HTML cleanup
    # I am not interested in getting cells with html formatting in my table, I just want the value!
    # Moreover, this would clash with the code above when initiliazing the table and comparing values.
    #print(Data$Condition[[1]])
    for (Fact in Factors) {
      if (length(FactorsList[[Fact]]) > 1) {
        for (i in 1:nrow(Data)) {
          val <- Data[[Fact]][i]
          val <- gsub("^option value=\"[^\"]+\" selected>", "",
                      grep("^option value=\"[^\"]+\" selected>", unlist(strsplit(a, " *<|>[\n ]*<|> *$")), value = TRUE))
          Data[[Fact]][i] <- val
        }
      }
    }
    # Check if I have managed to change from the default value:
    print(Data$Condition[[1]])
    # When testing, I change from "Treated" to "Mock" in the table, hit save, but it prints "Treated", and sure enough the table saved contains "Treated".
    #
    write.csv(Data, ExpMapPath, row.names = FALSE)
    tstExpMap <<- Data # Another way to check table values
    stopApp()
  })
}
print(shiny::shinyApp(ui, server, options = list(launch.browser = TRUE)))
> sessionInfo()
R version 4.2.3 (2023-03-15 ucrt)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows Server x64 (build 20348)

Matrix products: default

locale:
[1] LC_COLLATE=English_United Kingdom.utf8  LC_CTYPE=English_United Kingdom.utf8    LC_MONETARY=English_United Kingdom.utf8
[4] LC_NUMERIC=C                            LC_TIME=English_United Kingdom.utf8    

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] DT_0.26     shiny_1.7.4

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.9        rstudioapi_0.14   magrittr_2.0.3    xtable_1.8-4      R6_2.5.1          rlang_1.0.6       fastmap_1.1.0     tools_4.2.3      
 [9] aRmel_4.0.0.13    cli_3.5.0         jquerylib_0.1.4   htmltools_0.5.4   crosstalk_1.2.0   ellipsis_0.3.2    yaml_2.3.6        digest_0.6.31    
[17] lifecycle_1.0.3   crayon_1.5.2      later_1.3.0       sass_0.4.4        htmlwidgets_1.6.0 promises_1.2.0.1  memoise_2.0.1     cachem_1.0.6     
[25] mime_0.12         compiler_4.2.3    bslib_0.4.2       jsonlite_1.8.4    httpuv_1.6.7

我在StackOverflow上看过很多类似的帖子,比如https://github.com/ejbeaty/CellEdit/tree/master/js,DT:基于R shiny app和Edit datatable in shiny with dropdown selection (for DT v0.19)中另一列的selectinput动态更改列值,我实际上是基于这个应用程序的,但我一定是错过了什么,因为它对我不起作用。不幸的是,我对JavaScript一无所知,所以我很难弄清楚缺少了什么。

juud5qan

juud5qan1#

下面是我的答案的修改(一般化)版本,它读入csv文件并允许将其保存回磁盘。

library(DT)
library(shiny)

# Prepare mock data
wd <- getwd()
Factors <- c("Experiment", "Condition", "Replicate", "Genotype")
ExpMapNm <- "dummy_data"
ExpMapPath <- paste0(wd, "/", ExpMapNm, ".csv")
ExpMap <- data.frame(Experiment = "Exp1",
                     Sample = paste0("Sample", 1:20),
                     Condition = as.character(sapply(c("Treated", "Mock"), function(x) { rep(x, 10) })),
                     Genotype = as.character(sapply(c("KO", "WT"), function(x) { rep(x, 5) })),
                     Replicate = 1:5)
FactorsList <- setNames(lapply(Factors, function(x) { unique(ExpMap[[x]]) }), Factors)

if (!file.exists(ExpMapPath)) { write.csv(ExpMap, ExpMapPath, row.names = FALSE) }

ui <- fluidPage(
  shiny::actionButton("saveBtn", "Save"),
  DT::dataTableOutput(outputId = 'my_table'),
)

server <- function(input, output, session) {
  
  resultDF <- displayHTMLDF <- initHTMLDF <- initData <- read.csv(ExpMapPath)
  
  dropdownCols <- names(initData)[3:5]
  dropdownIDs <- setNames(lapply(dropdownCols, function(x){paste0(x, seq_len(nrow(initData)))}), dropdownCols)
  
  for(dropdownCol in dropdownCols){
    colDropdownIDs <- dropdownIDs[[dropdownCol]]
    initHTMLDF[[dropdownCol]] <- sapply(seq_along(colDropdownIDs), function(i){as.character(selectInput(inputId = colDropdownIDs[i], label = "", choices = unique(initData[[dropdownCol]]), selected = initData[[dropdownCol]][i]))})
  }
  
  reactiveHTMLDF <- reactive({
    for(dropdownCol in dropdownCols){
      colDropdownIDs <- dropdownIDs[[dropdownCol]]
      displayHTMLDF[[dropdownCol]] <- sapply(seq_along(colDropdownIDs), function(i){as.character(selectInput(inputId = colDropdownIDs[i], label = "", choices = unique(initData[[dropdownCol]]), selected = input[[colDropdownIDs[i]]]))})
    }
    return(displayHTMLDF)
  })
  
  reactiveResultDF <- reactive({
    for(dropdownCol in dropdownCols){
      colDropdownIDs <- dropdownIDs[[dropdownCol]]
      resultDF[[dropdownCol]] <- sapply(seq_along(colDropdownIDs), function(i){input[[colDropdownIDs[i]]]})
    }
    return(resultDF)
  })
  
  output$my_table = DT::renderDataTable({
    DT::datatable(
      initHTMLDF, escape = FALSE, selection = 'none', rownames = FALSE,
      options = list(paging = FALSE, ordering = FALSE, scrollx = TRUE, dom = "t",
                     preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
                     drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')
      )
    )
  }, server = TRUE)
  
  my_table_proxy <- dataTableProxy(outputId = "my_table", session = session)
  
  observeEvent({sapply(unlist(dropdownIDs), function(x){input[[x]]})}, {
    replaceData(proxy = my_table_proxy, data = reactiveHTMLDF(), rownames = FALSE) # must repeat rownames = FALSE see ?replaceData and ?dataTableAjax
  }, ignoreInit = TRUE)
  
  shiny::observeEvent(input$saveBtn, {
    write.csv(reactiveResultDF(), ExpMapPath, row.names = FALSE)
    stopApp()
  })
}

shinyApp(ui = ui, server = server)

您可能需要调整一些东西以满足您的需要,但是,它们并不需要显示原理。

nr7wwzry

nr7wwzry2#

我想我现在解决了,非常感谢ismirsehregal!
澄清我的原始问题:

  • 多水平因子(Fact 2)列应为下拉选择。
  • 非因子(Others)列应该允许自由的单元格编辑。(下面的代码有点复杂,因为我也很难覆盖单元格中单个水平因子或Fact 1的最终手动编辑。)

所提出的解决方案的问题是,从多水平因子列中进行的任何选择都会将之前的任何自由编辑还原为“其他”。为了解决此问题,我重写了代码,以便在定义了每列中允许的编辑类型后,由同一行代码处理这两个编辑。

library(DT)
library(shiny)

# Prepare mock data
wd <- getwd()
Factors <- c("Experiment", "Condition", "Replicate", "Genotype")
ExpMapNm <- "dummy_data"
ExpMapPath <- paste0(wd, "/", ExpMapNm, ".csv")
ExpMap <- data.frame(Experiment = "Exp1",
                     Sample = paste0("Sample", 1:20),
                     Condition = as.character(sapply(c("Treated", "Mock"), function(x) { rep(x, 10) })),
                     Genotype = as.character(sapply(c("KO", "WT"), function(x) { rep(x, 5) })),
                     Replicate = 1:5)
FactorsList <- setNames(lapply(Factors, function(x) { unique(ExpMap[[x]]) }), Factors)

if (!file.exists(ExpMapPath)) { write.csv(ExpMap, ExpMapPath, row.names = FALSE) }

ui <- fluidPage(
  shiny::titlePanel(ExpMapNm), # This is the name of the table
  shiny::mainPanel(shiny::br(),
                   shiny::actionButton("saveBtn", "Save"),
                   DT::dataTableOutput('my_table'))
)
server <- function(input, output, session) {
  initData <- read.csv(ExpMapPath)
  
  tst <- sapply(FactorsList, length)
  Fact1 <- Factors[which(tst == 1)]
  Fact2 <- Factors[which(tst > 1)]
  Others <- colnames(initData)[which(!colnames(initData) %in% Factors)]
  Editables <- c(Others, Fact2)
  nr <- nrow(initData)
  OtherIDs <- setNames(lapply(Others, function(x) { paste0(x, seq_len(nr))} ), Others)
  Fact2IDs <- setNames(lapply(Fact2, function(x) { paste0(x, seq_len(nr))} ), Fact2)
  AllIDs <- append(OtherIDs, Fact2IDs)
  for (Fact in Fact1) {
    # (Since the table can be manually edited too, we want to make sure to correct any typos in our single level factor columns)
    initData[[Fact]] <- FactorsList[[Fact]]
  }
  # ... before creating 3 copies of that datatable:
  resultDF <- displayHTMLDF <- initHTMLDF <- initData
  # initHTMLDF
  for (Oth in Others) {
    IDs <- OtherIDs[[Oth]]
    initHTMLDF[[Oth]] <- sapply(seq_along(IDs), function(i) {
      val <- initData[[Oth]][i]
      return(as.character(textInput(IDs[i], "", val)))
    })
  }
  for (Fact in Fact2) {
    IDs <- Fact2IDs[[Fact]]
    lvls <- FactorsList[[Fact]]
    initHTMLDF[[Fact]] <- sapply(seq_along(IDs), function(i) {
      val <- initData[[Fact]][i]
      dflt <- c(NA, val)[(val %in% lvls)+1] # If the current value is not a valid level, revert to NA
      return(as.character(selectInput(IDs[i], "", c(lvls, NA), dflt)))
    })
  }
  # displayHTMLDF
  reactiveHTMLDF <- reactive({
    for (Col in Editables) {
      IDs <- AllIDs[[Col]]
      if (Col %in% Others) {
        displayHTMLDF[[Col]] <- sapply(seq_along(IDs), function(i) {
          val <- initData[[Col]][i]
          return(as.character(textInput(IDs[i], "", input[[IDs[i]]])))
        })  
      } else {
        lvls <- FactorsList[[Col]]
        displayHTMLDF[[Col]] <- sapply(seq_along(IDs), function(i) { as.character(selectInput(IDs[i], "", lvls, input[[IDs[i]]])) })
      }
    }
    return(displayHTMLDF)
  })
  # resultDF
  reactiveResultDF <- reactive({
    for (Col in Editables) {
      IDs <- AllIDs[[Col]]
      resultDF[[Col]] <- sapply(seq_along(IDs), function(i) { input[[IDs[i]]] })
    }
    return(resultDF)
  })
  # Let's make sure we explicitly link row.names in multiple calls below 
  rwNms <- FALSE
  #
  output$my_table = DT::renderDataTable({
    DT::datatable(
      initHTMLDF, escape = FALSE, selection = "none", rownames = rwNms,
      editable = list(target = "cell", disable = list(columns = match(Fact1, colnames(initData))-1)), # Factors with 1 level should not be editable; indices appear to start at 0 (JS convention?)
      options = list(paging = FALSE, ordering = FALSE, scrollx = TRUE, dom = "t",
                     preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
                     drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')
      )
    )
  }, server = TRUE)
  my_table_proxy <- dataTableProxy("my_table", session)
  observeEvent({ sapply(unlist(AllIDs), function(x) { input[[x]] }) }, {
    replaceData(proxy = my_table_proxy, data = reactiveHTMLDF(), rownames = rwNms) # must repeat rownames = FALSE see ?replaceData and ?dataTableAjax
  }, ignoreInit = TRUE)
  shiny::observeEvent(input$saveBtn, {
    write.csv(reactiveResultDF(), ExpMapPath, row.names = FALSE)
    stopApp()
  })
}
print(shiny::shinyApp(ui, server, options = list(launch.browser = TRUE)))

相关问题