【问题标题】:How to validate date range input in Shiny如何在 Shiny 中验证日期范围输入
【发布时间】:2017-06-17 04:44:03
【问题描述】:

为了测试,请上传一个1+列的csv文件,可以在应用中转换为日期。

我的应用会根据所选日期列动态生成日期范围输入 (input$daterange)。我想验证每个 input$daterange 从 1 到 n(dt$datecols 的长度),以确保用户不会选择早于最旧日期的 start 日期和晚于最新日期的 end 日期相应列中的日期。我在observeEvent 上使用lapply 来做到这一点。

为了便于调试,我将input$daterange(i) 的值传递给反应值dt$daterange(i) 并将dt$daterange1(第一个日期范围的值)打印到渲染的控制台以检查它是否小于或大于@对应日期列的 987654332@ 和 max,就像我在 lapply 函数中所做的那样。假设检查结果为FALSE 时,lappy 函数将显示一条错误消息,警告用户start 或end 日期无效,但不起作用。请在下面找到我的代码,请查看 cmets 以了解问题的解释。

library("shiny")
library("DT") # Datatable 
library("rsconnect") # deploy to shinyapps.io
library("shinyjs") # use toggle button from shinyJS pacakage
library("stats")
library("zoo") # to use as.Date() on numeric value

ui <- fluidPage(

      fluidRow(

            column(4,
                  # file upload div
                  fileInput("file", "Choose a file",

                  accept=c(
                  "text/csv", 
                  "text/comma-separated-values,text/plain", 
                  ".csv"
                  )),

                  # show ui for upload file control
                  uiOutput("ui")
            ),


            column(4,
                  # no choices before a file uploaded
                  uiOutput("columnscontrol")
            )

        ),

        hr(),

        fluidRow(
                  column(4,
                         uiOutput("datecolscontrol")),

                  column(6,
                         uiOutput("daterangescontrol"))
        ),

        hr(),

        dataTableOutput("datatbl"),

        # print console for debugging (delete after completion)
        verbatimTextOutput("print_con")

) #end of fluidPage (ui)



# server
server <- function(input, output, session) {


    #########################################################
    #  upload & datatable output
    #########################################################

    # create dataset reactive objects
    dt <- reactiveValues()

    # reset all uis upon new file upload
    observeEvent(input$file, {

        # reset reactive values
        dt$data = NULL
        dt$df = NULL
        dt$cols = NULL
        dt$rows = NULL
        dt$summary = NULL
        dt$colchoices = NULL
        dt$datecols = NULL

        # remove columns div and datecols div when a new file uploaded
        removeUI(selector = "div#columns_div")
        removeUI(selector = "div#datecols_div")

        # remove all <div> elements indside <div>#daterangescontrol:
        removeUI(selector = "div#daterangescontrol div")

        # generate upload file control ui once file uploaded
        output$ui <- renderUI({
          actionButton("readF", "Update")
        })

    })


    # when read file button pressed:
    observeEvent (input$readF, {

        # store data to dt$data
        file <- input$file
        dt$data <- read.csv(file$datapath, header = TRUE)


        # render columnscontrol
        output$columnscontrol <-  renderUI({

            # get the col names of the dataset and assign them to a list
            dt$colchoices <- mapply(list, names(dt$data))

            # render column group checkbox ui after loading the data
            # tags#div has the advantage that you can give it an id to make it easier to reference or remove it later on
            tags$div(id = "columns_div", 
                     checkboxGroupInput("columns", "", choices = NULL, selected = NULL))
        })

        # render div containing #datecols under datecolscontrol 
        output$datecolscontrol <- renderUI({
            tags$div(id = "datecols_div",
            selectInput("datecols", "Filter data by dates):", choices = NULL, multiple = TRUE, selected = NULL))
        })

    }) 


    # update columns choices when dt$choices is ready
    observeEvent(dt$colchoices, {
        updateCheckboxGroupInput(session, "columns", "Select Columns:", choices = dt$colchoices, selected = dt$colchoices)
    })


    # the other reactivity on dt$cols is input$file (when new file uploaded, dt$data and dt$cols set to NULL)
    # so that the following line set apart the reactivity of input$columns on dt$cols
    observeEvent(input$columns, { 
        dt$cols <- input$columns
        dt$df <- dt$data[dt$cols]
    }, ignoreNULL = FALSE)


    # upon any change of dt$df 
    observeEvent(dt$df, {

          f <- dt$df

          # render output$datatbl 
          output$datatbl <- DT::renderDataTable( 
          f, rownames = FALSE,
          filter = 'top',
          options = list(autoWidth = TRUE)
          )

          # update datecols choices with those columns can be converted to Date only:
          dt$date_ok = sapply(f, function(x) !all(is.na(as.Date(as.character(x), format = "%Y-%m-%d"))))
          dt$datecolchoices = colnames(f[dt$date_ok])
          updateSelectInput(session, "datecols", "Filter data by dates:", choices = dt$datecolchoices, selected = NULL)

    }, ignoreNULL = FALSE)


    # whenver columns convertable to date updated to choices of input$datecols, convert the columns to Date in the dataset
    observeEvent(dt$datecolchoices, {
        dt$df[dt$date_ok] = lapply(dt$df[dt$date_ok], function(x) as.Date(as.character(x)))
    })


    # generate daterange uis per selected input$datecols
    observeEvent(input$datecols, {

        dt$datecols = input$datecols
        dt$datecols_len = length(dt$datecols)

        # render daterange ui(s) per selected datecols
        output$daterangescontrol <- renderUI({

            # when input$datecols is NULL, no daterange ui
            if ( is.null(input$datecols) ) { return(NULL) }

            # otherwise
            else {

                D = dt$df[dt$rows, dt$cols]

                output = tagList()

                for (i in 1:dt$datecols_len) {
                    output[[i]]= tagList()
                    output[[i]][[1]] = tags$div(id = paste("dateranges_div", i, sep = "_"), 
                                                dateRangeInput(paste0("daterange", i),
                                                paste("Date range of", dt$datecols[[i]]),
                                                start = min(D[[dt$datecols[[i]]]]),
                                                end = max(D[[dt$datecols[[i]]]])))
                }

                # return output tagList() with ui elements
                output
            } 
        }) # end of renderUI
    }, ignoreNULL = FALSE)

    # loop observeEvent to check whether each input$daterange is valid:
    #### why I can't just call lapply() without observe() as suggested in this post:
    #### https://stackoverflow.com/questions/40038749/r-shiny-how-to-write-loop-for-observeevent
    observe({
      lapply( X = 1:dt$datecols_len, 

              FUN = function(i) { 

                observeEvent(input[[paste0("daterange", i)]], {

                  # update reactive values to test whether this loop is working
                  dt[[paste0("range",i)]] = input[[paste0("daterange", i)]]

                  range = dt[[paste0("range",i)]] 
                  req(range)
                  #########################################
                  ##     CODE BLOCK WITH PROBLEM!!!
                  #########################################
                  # Why the following doesn't work, when I pick a date earlier than the oldest date
                  # no error message shows!
                  shiny::validate(
                    need( range[[1]] >= min(dt$df[[dt$datecols[[i]]]]), "The start date cannot be earlier than the oldest date!"),
                    need( range[[2]] <= max(dt$df[[dt$datecols[[i]]]]), "The end date cannot be later than the latest date!")
                  )
                }) 
              }
        ) # end of lapply
      })

    # rows displayed in input$datatbl (the rendered data table)
    observeEvent( input$datatbl_rows_all, { 
      dt$rows <- input$datatbl_rows_all
    })



    #########################################################
    # print console
    #########################################################
    output$print_con <- renderPrint({

      req(input$daterange1)
      list(
        # to verify whether the observeEvent loop is working for input validation
        # I used dt$range1 to check the first (input$daterange1) against the date range of the corresponding column of the dataset. 
        # It's supposed that when the check result is FALSE (either by selecting a start date earlier than the oldest date or selecting an end date later than the latest date), 
        # the code block with problem shall prompt an error message to warn the user
        min(dt$range1) >= min(dt$df[[dt$datecols[[1]]]]),
        max(dt$range1) <= max(dt$df[[dt$datecols[[1]]]])
      )

    })

} # end of shiny server function

shinyApp(ui = ui, server = server)

【问题讨论】:

  • 请按照有关创建minimal reproducible example 的提示将您的示例简化为说明问题的基本部分。
  • 为什么不对日期列表进行排序?您只需比较 [1]st 和 [n]th 元素,而不是 [i] 元素。

标签: r shiny


【解决方案1】:

这可能不是您正在寻找的确切答案,但我认为它可能会简化事情。我会简单地订购您的日期列,这将允许您选择最旧和最新的日期。然后将您的开始日期和结束日期设置为这两个值(请参阅 ?dateRangeInput)。 Lubridate 也是处理日期的绝佳软件包

【讨论】:

  • 我确实为dateRangeInput 设置了开始和结束日期,但是,shiny 仍然让用户选择早于开始和晚于结束的日期。
  • 然后你需要设置最小值和最大值。这些限制了可选择的范围。再次,请参阅 ?dateRangeInput
【解决方案2】:

我认为问题可能与您的日期格式有关。

请看这篇文章: R: Shiny dateRangeInput format

你可能需要使用

format(range[[1]])

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-10-06
    • 2018-03-06
    • 1970-01-01
    • 1970-01-01
    • 2021-09-11
    • 1970-01-01
    • 2019-06-09
    • 2016-03-15
    相关资源
    最近更新 更多