如何在 Shiny 应用程序中验证 textInput?

Posted

技术标签:

【中文标题】如何在 Shiny 应用程序中验证 textInput?【英文标题】:How do I Validate textInput in a Shiny app? 【发布时间】:2019-12-15 04:35:23 【问题描述】:

我有一个闪亮的应用程序,它会提示用户输入 USGS NWIS 站点编号,然后返回附近站点的地图和该站点周围历史流量的条形图。我很难将站点编号的用户输入验证为 textInput()。当用户不输入数字并点击提交、输入不正确的数字(在 NWIR 数据库中不存在)或输入带有前导的数字时,我需要提示用户重试(并且不接受输入)或尾随空格。我应该在这个应用程序的哪里调用“验证”?

##############################################################################
# Libraries
##############################################################################
rm(list=ls())
list.of.packages <- c("RColorBrewer",
                      "dataRetrieval",
                      "curl",
                      "repr",
                      "maps",
                      "dplyr",
                      "ggplot2",
                      "leaflet",
                      "leafem",
                      "raster",
                      "raster",
                      "shiny",
                      "htmlwidgets",
                      "devtools",
                      "shinycustomloader",
                      "shinydashboard",
                      "shinyjs",
                      "DT",
                      "spData",
                      "sf",
                      "shinythemes",
                      "plotly")
new.packages <- list.of.packages[!(list.of.packages %in% installed.packages()[,"Package"])]
if(length(new.packages)) install.packages(new.packages)
lapply(list.of.packages, require, character.only = TRUE)

##############################################################################
# UI Side
##############################################################################
ui <- fluidPage(
  shinyjs::useShinyjs(),
  #titlePanel("USGS Gages Annual Flow Peak Tool"),
  h1(id="big-heading", "USGS Gages Annual Flow Peak Tool"),
  tags$style(HTML("
      @import url('//fonts.googleapis.com/css?family=Lobster|Cabin:400,700');

      h1 
        font-family: 'Lobster', cursive;
        font-weight: 500;
        line-height: 1.1;
        color: #006F41;
      

    ")),

  # side panel
  sidebarPanel(


    textInput(inputId ="site_no", 
              label = "Site Number", 
              width = '400px',
              #value=01615000,
              placeholder = "Please enter the NWIS Site Number."),
    textInput(inputId ="years_of_records", 
              label = "Years of Records", 
              width = '400px',
              value = 30,
              placeholder = "How many years of Records would you like?"),
    textInput(inputId ="da_epsilon", 
              label = "Drainage Area Epsilon", 
              width = '400px',
              value = 0.25,
              placeholder = "What is the Drainage Area Epsilon?"),
    textInput(inputId ="bbox_delta", 
              label = "Bounding Box Delta - Degrees", 
              width = '400px',
              value = 1,
              placeholder = "What is the Bounding Box delta?"),

    actionButton(
      inputId = "submit_loc",
      label = "Submit"
    ),
    downloadButton('downloadData', 'Download Data'),
    h4(''),
    dataTableOutput('table01'),
    width = 3),

  # main panel
  mainPanel(
    leafletOutput('map01', width = "110%", ),
    br(),
    plotlyOutput('hist01', width = "110%")
      )
)

##############################################################################
# Server Side
##############################################################################
server <- function(input,output, session)
  shinyjs::hide("downloadData")
  observeEvent(input$submit_loc, 

    cat("START\n")

    validate(
      need(input$site_no, 'Enter a Site Number!')
    )

    SITE_NUM=input$site_no
    SITE_URL <- paste0("https://waterdata.usgs.gov/nwis/inventory/?site_no=",SITE_NUM,"&agency_cd=USGS")
    paraCode <- "00060"
    years_of_records <- as.numeric(input$years_of_records)
    da_epsilon <- as.numeric(input$da_epsilon)
    bbox_delta <- as.numeric(input$bbox_delta) # Degrees
    cat("Showing", SITE_NUM, "NWIS id",
        "\nUsing URL:", SITE_URL,
        "\nwith ", years_of_records, "years of records",
        "\n& Drainage Area of: ", da_epsilon,
        "\n& Bounding Box delta of: ", bbox_delta, "\n")
    # CODE TO MAKE DATA FRAME

    # Get site coordinates to build Bbox
    site_data <- whatNWISsites(siteNumber=SITE_NUM, parameterCd=paraCode)
    site_lat <- site_data$dec_lat_va
    site_long <- site_data$dec_long_va
    site_data$site_url <- SITE_URL

    # Get site drainage area
    site_summary <- readNWISsite(siteNumber=SITE_NUM)
    site_da <- site_summary$drain_area_va


    # need to use SIG FIGS --- Otherwise the curl command gets confused.
    bBox <- c(signif(site_long - bbox_delta,7),
              signif(site_lat - bbox_delta,7),
              signif(site_long + bbox_delta,7),
              signif(site_lat + bbox_delta,7))

    bbox_shiny <- c(bBox[1],bBox[3],bBox[2],bBox[4])

    # Get site metadata for the Bbox
    para_sites <- as.data.frame(whatNWISsites(bBox=bBox, parameterCd=paraCode))
    para_sites$gtype = paraCode #gtype: gage type (stage, flow, ...etc)

    # Filter the retrieved USGS gages based on the defined criteria
    sites_meta <- whatNWISdata(siteNumber=para_sites$site_no, parameterCd=paraCode)
    sites_meta_years <- sites_meta[(sites_meta['end_date'] - sites_meta['begin_date']) > (years_of_records * 365.0),]
    sites_summary <- readNWISsite(siteNumber=sites_meta_years$site_no)
    sites_selected <- sites_summary[((1-da_epsilon)* site_da) <= sites_summary['drain_area_va'] & sites_summary['drain_area_va'] <= ((1+da_epsilon)* site_da), ]
    # Separate surrounding sites
    site_surrounding <- sites_selected[sites_selected$site_no != SITE_NUM, ]

    # Append URL 
    for(i in 1:nrow(sites_selected))
      sites_selected_no <- as.character(sites_selected$site_no)
      sites_selected$site_url <- paste0("https://waterdata.usgs.gov/nwis/inventory/?site_no=",sites_selected_no,"&agency_cd=USGS")
    

    # Separate central site
    red_site <- sites_selected[sites_selected$site_no == paste(SITE_NUM),]

    # GET PEAK STREAMFLOW DATA
    peak_ts <- readNWISpeak(input$site_no)
    cols = c("site_no","peak_dt","peak_va","gage_ht")
    peak_ts <- peak_ts[,cols]
    names(peak_ts) <- c("Site Number", "Peak Streamflow: Date", "Peak streamflow (cfs)", "Gage Height (feet)")
    output$table01 <- renderDataTable(
    DT::datatable(peak_ts, 
                  selection = "single",
                  extensions = 'Responsive',
                  rownames=FALSE,
                  options=list(stateSave = FALSE, 
                               autoWidth = TRUE,
                               lengthMenu = c(10, 10)))
    )

    shinyjs::show("downloadData")

    data <- sites_selected
    # Downloadable csv of selected dataset ----
    output$downloadData <- downloadHandler(
      filename = function() 
        paste0(input$site_no, "_data.csv")
      ,
      content = function(file) 
        write.csv(data, file, row.names = FALSE)
      
    )


    output$map01 <- renderLeaflet(

      leaflet(sites_selected) %>% 
        clearShapes() %>%
        addTiles() %>% 
        leafem::addMouseCoordinates() %>% 
        leafem::addHomeButton(extent(us_states),"Zoom to Home")%>%
        fitBounds(~min(dec_long_va), ~min(dec_lat_va), ~max(dec_long_va), ~max(dec_lat_va)) %>% 
        addCircleMarkers(data = red_site,
                         lng= ~dec_long_va,
                         lat = ~dec_lat_va,
                         color='red',
                         popup= paste0( red_site$station_nm,
                                        "<br>", "USGS site: ", red_site$site_no,
                                        "<br>", "<a href='", red_site$site_url,
                                        "' target='_blank'>", "USGS URL</a>"),
                         label = red_site$station_nm) %>% 
        addCircleMarkers(data = site_surrounding,
                         lng= ~dec_long_va,
                         lat = ~dec_lat_va,
                         color='blue',
                         popup= paste0( site_surrounding$station_nm,
                                        "<br>", "USGS site: ", site_surrounding$site_no,
                                        "<br>", "<a href='", site_surrounding$site_url,
                                        "' target='_blank'>", "USGS URL</a>"),
                         label = site_surrounding$station_nm)
    )

    peak_named <- cbind(red_site[,"station_nm"], peak_ts)
    names(peak_named[1]) <- c("Station Name")
    chart_title=paste(peak_named[1,1], peak_named[1,2],': Peak streamflow (cfs)')
    qSub <-  reactive(
      peak_named
    )

    # histogram
    output$hist01 <- renderPlotly(

      ggplot(data=qSub()) +
        geom_bar(aes(x=peak_ts[,"Peak Streamflow: Date"],y=peak_ts[,"Peak streamflow (cfs)"]),
                 stat="identity", 
                 width=125) +
        ylab('Peak streamflow (cfs)') +
        xlab('Date') +
        # xlim(min(qDat$drain_area_va), max(qDat$drain_area_va))+
        ggtitle(chart_title)+
        theme(text = element_text(family = "Arial", color = "grey20", size=12, face="bold"))

    )
  )



  output$map01 <- renderLeaflet(
    leaflet() %>% setView(-93.65, 42.0285, zoom = 4) %>% addTiles()
  )


##############################################################################
shinyApp(ui = ui, server = server)
##############################################################################

【问题讨论】:

为什么不使用selectInput,您可以将选择限制为仅有效站点? 很好的建议 - 不幸的是,有成千上万的有效网站。用户会对编号系统有一定的了解,主要是因为错误而输入了错误的值 但用户实际上可以像textInput 一样使用selectInput,并且下拉选项将根据输入的文本进行过滤。如果键入的文本与任何内容都不匹配,则下拉选项将不会显示任何内容。基本上用户无法输入不良数据。 有趣...我会试一试。谢谢! 【参考方案1】:

使用selectInput 将选择限制为有效站点会更容易。 selectInput 可用作文本搜索,其中将根据用户输入的文本过滤下拉列表。

下图显示了我的意思。请注意,即使您在 selectInput 中有 multiple = FALSE,它也可以工作 -

至于回答你的问题-

您需要创建一个有效sites 的向量,并在上游reative 或下游在任何相关render* 中使用关注-

validate(
  need(input$site_no %in% sites, 'Site does not exist!')
)

更新 -

根据您的评论,这是一种方法 -

test <- reactive(
  some inexpensive function to check if input$site_no exists in data source
  if(site exists) return("Good")
  return("Bad")
)

validate(
  need(test() == "Good", "Site does not exist!")
)

【讨论】:

再次感谢 - 虽然我认为这种方法不适用于我的用例(有太多 NWIS 站点编号(成千上万)加载到闪亮的应用程序中以保存为有效的网站),我确实了解了 selectInput,所以很感激。我将转向 tryCatch 错误处理解决方案。 @JasonMatney 看看我的更新是否有帮助。这避免了一次性提取 100 多个站点,但现在您必须处理一个函数,该函数将在每次站点更改时检查数据源,如果此函数很快,应该没问题。希望这会有所帮助。

以上是关于如何在 Shiny 应用程序中验证 textInput?的主要内容,如果未能解决你的问题,请参考以下文章

如何安全地将 EC2 托管的 Shiny 应用程序集成到 asp.net 项目中

Shiny 中的一系列需要(验证):仅打印第一个失败案例

r 适用于R Shiny应用的Google OAuth2身份验证功能

R使用AWS Cognito进行Shiny身份验证

如何访问在 Shiny 中呈现的 rhandsontable 的 JS 变量名?

如何通过 div 标签要求在 R Shiny 中输入?