Check the file extension and use it as a condition for UI elements

shiny
extensions

#1

Hello,

I try to make Shiny react to an uploaded file. Depending on the extension of the file (csv/txt or xlsx) specific ui elements should show up or hide. For this I loaded the package "assertthat", but I think I am using it wrong.
I am also new to error handling with validate and I am not sure if the error has something to do with that.

When I use the app, the error message appears on the "Tabelle" register:
"assert_that: length of assertion is not 1".
And while the error message of validate changes correctly, the ui elements I want to show up when a .txt file is uploaded, aren't getting rendered.

I call for the ui elements with a conditional panel:
conditionalPanel(condition = "output.dateiformat == 'csvtxt'",

And output.dateiformat ist generated when the file is put into a reactive data frame:

    if (see_if(has_extension(infile$datapath, 'csv')) == TRUE
        | see_if(has_extension(infile$datapath, 'txt')) == TRUE
        | see_if(has_extension(infile$datapath, 'TXT')) == TRUE
        | see_if(has_extension(infile$datapath, 'CSV')) == TRUE) {
      output$dateiformat <- renderText('csvtxt')
      outputOptions(output, "dateiformat", suspendWhenHidden = FALSE)

(see_if and has_extension are functions of the external package "assertthat" and I am not sure if I need it.)

Ideally my app would work without the help of the package "assertthat", but I could not find a way to return the extension of a file without additional input of the user.

All relevant parts of the code:

library(shiny)
library(xlsx)
library(readxl)
library(assertthat)

`%then%` <- shiny:::`%OR%`

ui <- fluidPage(
  sidebarLayout(sidebarPanel(
    fileInput("userDatei",
                "Datei hochladen:",
                multiple = FALSE,
                accept = c("text/csv",
                           "text/comma-separated-values,text/plain",
                           ".csv",
                           '.xlsx'),
                placeholder = "...",
                buttonLabel = "Durchsuchen"),

    conditionalPanel(condition = "output.dateiformat == 'csvtxt'",
                     radioButtons("dec", "Dezimalzeichen:",
                                  choices = c("Komma" = ",",
                                              "Punkt" = "."),
                                  selected = "."),
                     radioButtons("sep", "Trennzeichhen:",
                                  choices = c("Komma" = ",",
                                              "Semikolon" = ";",
                                              "Tab" = "\t"),
                                  selected = ","),
                     radioButtons("quote", "Anführungszeichen:",
                                  choices = c("Keine" = "",
                                              "Doppelte" = '"',
                                              "Einfache" = "'"),
                                  selected = '"')
    ),

    conditionalPanel(condition = "output.dateiformat == 'xlsx'", "")
    ),

    mainPanel(
      tabsetPanel(

        tabPanel("Diagramm", fluidPage(plotOutput("userBoxplot"))),
        tabPanel("Tabelle", fluidPage(tableOutput("userTabelle"))))
      ))
  )

server <- function(input, output) {

  userDaten <- reactive({

    validate(need(input$userDatei, "Es ist keine Datei ausgewählt."))
    req(input$userDatei)
    infile <- input$userDaten

    if (see_if(has_extension(infile$datapath, 'csv')) == TRUE
        | see_if(has_extension(infile$datapath, 'txt')) == TRUE
        | see_if(has_extension(infile$datapath, 'TXT')) == TRUE
        | see_if(has_extension(infile$datapath, 'CSV')) == TRUE) {
      output$dateiformat <- renderText('csvtxt')
      outputOptions(output, "dateiformat", suspendWhenHidden = FALSE)
      daten <- read.csv(infile$datapath,
                        header = input$header,
                        dec = input$dec,
                        sep = input$sep,
                        quote = input$quote)}

    else {
      output$dateiformat <- renderText('xlsx')
      daten <- read.xlsx(infile$datapath,
                         header = input$header,
                         sheetIndex = 1)}
    })

  output$userBoxplot <- renderPlot({
    validate(
      need(input$userDatei, "Es ist keine Datei ausgewählt.") %then%
        need(try(
          boxplot(x = userDaten()), 
          TRUE), "Dezimalzeichen und Seperator müssen angepasst werden."))
  })

  output$userTabelle <- renderTable({
      return(userDaten())})
}

shinyApp(ui = ui, server = server)

And this is the content of the .txt file I use to test the app:

A   B   C
0.509   0.170   0.042
0.071   0.540   0.093
0.623   0.443   0.277
0.290   0.313   0.436
0.925   0.096   0.716
0.358   0.792   0.373
0.269   0.621   0.445
0.205   0.733   0.787
0.953   0.905   0.534
0.881   0.162   0.918
0.332   0.905   0.901

#2

Hi! Welcome!

You don't need to use the assertthat package just to check the file extensions — if you peek at the has_extension() source, you'll see that it's wrapping the function file_ext() from the tools package (one of the standard packages installed by default with R). Shiny's own validate() and need() take care of providing helpful error messages.

Here's another take on your app code, with several changes:

  • Fixed typo ( infile <- input$userDaten should be infile <- input$userDatei)
  • Added missing input$header that both import functions were referencing
  • If you're using validate(), you don't need to also use req()
  • Validate existence of input$userDatei only in userDaten() reactive; validate compatibility of imported data frame with boxplot() ( = has at least one numeric variable) in renderPlot().
  • Specify MIME type for xlsx (otherwise browser may not allow user to select an xlsx file for upload)
  • Logic in userDaten() checks for xlsx extension, otherwise assumes CSV/txt. I find this easier to read, but I think it's a matter of personal taste. If you want to explicitly check for csv or txt, you can use tolower(tools::file_ext(infile$datapath)) %in% c("csv", "txt").
  • Only package dependency is readxl (again, a matter of taste/choice)

This works, but it's somewhat fragile. You probably want to add more validation or error handling (e.g., via tryCatch) to the file import code, since believing user-supplied file extensions isn't very robust. For example, currently you can upload a CSV where you've changed the file extension to xlsx, but the import will fail with an ugly red error message.

library(shiny)
library(readxl)

ui <- fluidPage(
  
  sidebarLayout(
    
    sidebarPanel(
      
      fileInput("userDatei",
                "Datei hochladen:",
                multiple = FALSE,
                accept = c(
                  "text/csv",
                  "text/comma-separated-values,text/plain",
                  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                  ".csv",
                  ".xlsx"
                ),
                placeholder = "...",
                buttonLabel = "Durchsuchen"
      ),
      
      checkboxInput("header", "Header", TRUE),
      
      conditionalPanel(
        condition = "output.dateiformat == 'csvtxt'",
        
        radioButtons("dec",
                     "Dezimalzeichen:",
                     choices = c("Komma" = ",",
                                 "Punkt" = "."),
                     selected = "."
        ),
        
        radioButtons("sep",
                     "Trennzeichhen:",
                     choices = c(
                       "Komma" = ",",
                       "Semikolon" = ";",
                       "Tab" = "\t"
                     ),
                     selected = ","
        ),
        
        radioButtons("quote",
                     "Anführungszeichen:",
                     choices = c(
                       "Keine" = "",
                       "Doppelte" = '"',
                       "Einfache" = "'"
                     ),
                     selected = '"'
        )
      ),
      
      conditionalPanel(
        condition = "output.dateiformat == 'xlsx'",
        
        p("Import options for xlsx go here")
      )
    ),
    
    mainPanel(
      
      tabsetPanel(
        
        tabPanel("Diagramm",
                 fluidPage(plotOutput("userBoxplot"))),
        
        tabPanel("Tabelle",
                 fluidPage(tableOutput("userTabelle")))
      )
    )
  )
)

server <- function(input, output) {
  
  userDaten <- reactive({
    
    validate(need(input$userDatei, "Es ist keine Datei ausgewählt."))
    infile <- input$userDatei
    
    if (tolower(tools::file_ext(infile$datapath)) == "xlsx") { 
      
      output$dateiformat <- renderText("xlsx")
      outputOptions(output, "dateiformat", suspendWhenHidden = FALSE)
      
      daten <- read_xlsx(infile$datapath,
                         col_names = input$header)
      
    } else {
      
      output$dateiformat <- renderText("csvtxt")
      outputOptions(output, "dateiformat", suspendWhenHidden = FALSE)
      
      daten <- read.csv(
        infile$datapath,
        header = input$header,
        dec = input$dec,
        sep = input$sep,
        quote = input$quote
      )
    }
  })
  
  output$userBoxplot <- renderPlot({
    validate(
      need(any(unlist(lapply(userDaten(), is.numeric))),
           "Dezimalzeichen und Seperator müssen angepasst werden.")
    )
    boxplot(userDaten())
  })
  
  output$userTabelle <- renderTable({
    userDaten()
  })
}

shinyApp(ui = ui, server = server)