create reports of reactive content by loop

Hi all

I am trying to get my shiny app to download pdf reports for the reactive outputs that appear on screen. The problem I am having is that reactives are not being invalidated within the loop through different data. Similar to this issue https://github.com/rstudio/shiny/issues/532. The app has a file import where you choose what input data to bring in, then you can swap between radio buttons to choose which you view.

Downloading the selected radio button data works but I cant figure out how to iterate through all the listed files to create the plot and then download with the download handler to pdf.

I have tried invalidateLater, lapply and local environments inside a loop as suggested here https://gist.github.com/bborgesr/e1ce7305f914f9ca762c69509dda632e but I cant seem to get the code quite right.

Here is a reprex where single download works but multiple cannot find the contents. (Sorry its a bit big, I tried to keep to how my actual app is working as much as possible). Input data can be found here http://www.filedropper.com/reprexfiles

Thanks in advance

library(shiny)
library(shinydashboard)
library(readxl)
library(ggplot2)
library(plotly)
library(zip)

ui <- dashboardPage(
  dashboardHeader(),
  dashboardSidebar(sidebarMenu(
    # upload file box
    fileInput(
      inputId = "file",
      label = "Select one or more .xlsx spreadsheet(s)",
      multiple = TRUE,
      accept = c(
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        ".xlsx")), # only allows .xlsx format files to be uploaded
      
    # radio buttons (default value of not visible)
    radioButtons("yaxisGrp2", "", c(" " = "1")),
    
    #download button
    downloadButton("report", "Create report(s)"),
    
    radioButtons("report_type", "Number of Files:",
                c("single" = "single", "multiple" = "multiple"),
                selected = "single"
                )
    )
  ),
  dashboardBody(
    plotlyOutput(
    "myplot", inline = TRUE)
  )
)


server <- function(input, output, session) { 
  
  cb_list <- reactiveValues(samples = NULL)
  
  #### Collect data details ####
  # List cases which can be ticked to be replaced with files (.xlsx)
  # selected by the user
  observe({
    data <- input$file
    dsnames2 <- data[, 1]
    cb_options <- list()
    cb_options[dsnames2] <- dsnames2
    updateRadioButtons(session,
                       "yaxisGrp2", # update radio buttons with values
                       label = " ",
                       choices = cb_options,
                       selected = cb_options[1]
    )
    
    cb_list$samples <- cb_options
  })
    

    # record file name into a variable
    input_data <- reactive({
      validate(
        need(input$file != "", "Choose one or more files (.xlsx)")
      )
      
      infile <- input$file
      
      if (is.null(infile)) {
        return(NULL)
      }
      
      infile
    })
    
    # monitor for a change in radio button selection
    chosen_file <- eventReactive(input$yaxisGrp2, {
      input_file <- input_data()
      reactiveValues(
        path = input_file$datapath[input_file$name == input$yaxisGrp2],
        name = input_file$name
      )
    })
    
    # load the selected files (.xlsx) based on selection
    mydata <- reactive({
      
      # check that the uploaded data is in the expected format and stop if it isnt
      temp <- read_excel(chosen_file()$path, sheet = 1)
      
      validate(
        need(
          names(temp)[1] == "Name",
          "I am an error"
        )
      )
      # Read-in each datatable as a different element of a list
      data_list <- list(
        read_excel(chosen_file()$path)
      )  
      return(data_list)
    })
      
  #make the base plot
  do_the_plot <- reactive({
    ggplot(mydata()[[1]], aes(x = Name, y = Size)) +
      geom_col(fill = "blue",
               colour = "black")
      })
  
  #change plot to plotly for interactive env
  plot_for_shiny <- reactive({
    ggplotly(do_the_plot(),
             tooltip = c("Name", "Size"),
             dynamicTicks = "y")
    
  })
  
  #change theme elements for pdf only
  plot_for_report <- reactive({
    do_the_plot() +
      theme_bw()
  })
  
  output$myplot  <- renderPlotly({ 
    plot_for_shiny()
  })
  
  
  #output report
  output$report <-
    downloadHandler(
      filename = function() { # choose filename based on combination of radio buttons chosen
        #select substring of name for file naming
        sample_name <- substr(input$yaxisGrp2, 1, regexpr("([ ])", input$yaxisGrp2) - 1)
        
        if (input$report_type == "single") {
          filename <- paste(sample_name, "Report.pdf", sep = "_")
        } else {
          filename <- "output.zip"
        }
        
        return(filename)
      },
      content = function(file) { # choose content based on combination of radio buttons chosen
        filedir <- dirname(chosen_file()$path[1])
        sample_name <- substr(input$yaxisGrp2, 1, regexpr("([ ])", input$yaxisGrp2) - 1)
        
          # Copy the report file to a known directory before processing it, in
          # case we don't have write permissions to the current working dir (which
          # can happen when deployed).
          tempReport <- file.path(filedir, "example_report.Rmd")
          file.copy("example_report.Rmd", tempReport, overwrite = TRUE)

          # render a single report
          if (input$report_type == "single") {
            
            parameters <- list(
              filename = sample_name,
              myplot = plot_for_report()
            )
              
            # Knit the document, passing in the `params` list, and eval it in a
            # child of the global environment (this isolates the code in the document
            # from the code in this app).
            rmarkdown::render(tempReport,
                              output_format = "pdf_document",
                              output_file = file,
                              params = parameters,
                              envir = new.env(parent = globalenv())
            )
            
            
            
          } else {  # if downloading multiple reports
            fs <- c()
            
            observeEvent(input$report,{
              # invalidate every 1 second
              invalidateLater(1000)
               isolate({
                # Find index of currently selected choice, and then get index of the next one.
                # index %% mod n + 1, so goes back to 1 when index = n
                index = which(cb_list$samples == input$file)
                index = index %% length(cb_list$samples) + 1 
                
                # Update the radioButtons
                updateRadioButtons(session, "yaxisGrp2",selected = cb_list$samples[index])
               })
              
              #select substring of name for file naming
              sample_name <- substr(input$yaxisGrp2, 1, regexpr("([ ])", input$yaxisGrp2) - 1)
              
              
                # Set up parameters to pass to Rmd document for colour report
                parameters <- list(
                  filename = sample_name,
                    myplot = plot_for_report()
                  )
                
              
              
              # set output file name based on colour choice
              fname <- paste0(filedir, "/", sample_name, "_Report.pdf")
              
              # render report
              rmarkdown::render(tempReport,
                                output_format = "pdf_document",
                                output_file = fname,
                                params = parameters,
                                envir = new.env(parent = globalenv())
              )
              
              fs <- c(fs, fname)
              
            }) 

            
            # change to temp directory on exit so zip function can find files
            owd <- setwd(filedir)
            on.exit(setwd(owd))
            
            # zip all created files into one zip file
            zipr(file, fs)
          }
          
        })

A colleague managed to help me come up with a workaround. Effectively making the reactives into functions and calling the functions separately on the incoming files within the reporting loop. Hopefully this helps someone else in need

ibrary(shiny)
library(shinydashboard)
library(readxl)
library(ggplot2)
library(plotly)
library(zip)

# plotting functions ------------------------------------------------------

base_plot <- function(df){
  p <- ggplot(df, aes(x = Name, y = Size)) +
    geom_col(fill = "blue",
             colour = "black") +
    theme_bw()
  p
}

plotly_p <- function(p){
  plotly_p <- ggplotly(p, 
                       tooltip = c("Name", "Size"),
                       dynamicTicks = "y")
  plotly_p
}

# ui ----------------------------------------------------------------------

ui <- dashboardPage(
  dashboardHeader(),
  dashboardSidebar(sidebarMenu(
    # upload file box
    fileInput(
      inputId = "file",
      label = "Select one or more .xlsx spreadsheet(s)",
      multiple = TRUE,
      accept = c(
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        ".xlsx")), # only allows .xlsx format files to be uploaded
    
    # radio buttons (default value of not visible)
    radioButtons("yaxisGrp2", "", c(" " = "1")),
    
    #download button
    downloadButton("report", "Create report(s)"),
    
    radioButtons("report_type", "Number of Files:",
                 c("single" = "single", "multiple" = "multiple"),
                 selected = "single"
    )
  )
  ),
  dashboardBody(
    plotlyOutput(
      "myplot", inline = TRUE)
  )
)


# server ------------------------------------------------------------------

server <- function(input, output, session) { 
  
  cb_list <- reactiveValues(samples = NULL)
  
  #### Collect data details ####
  # List cases which can be ticked to be replaced with files (.xlsx)
  # selected by the user
  observe({
    data <- input$file
    dsnames2 <- data[, 1]
    cb_options <- list()
    cb_options[dsnames2] <- dsnames2
    updateRadioButtons(session,
                       "yaxisGrp2", # update radio buttons with values
                       label = " ",
                       choices = cb_options,
                       selected = cb_options[1]
    )
    
    cb_list$samples <- cb_options
  })
  
  
  # record file name into a variable
  input_data <- reactive({
    validate(
      need(input$file != "", "Choose one or more files (.xlsx)")
    )
    
    infile <- input$file
    
    if (is.null(infile)) {
      return(NULL)
    }
    
    infile
  })
  
  # monitor for a change in radio button selection
  chosen_file <- eventReactive(input$yaxisGrp2, {
    input_file <- input_data()
    reactiveValues(
      path = input_file$datapath[input_file$name == input$yaxisGrp2],
      name = input_file$name
    )
  })
  
  # load the selected files (.xlsx) based on selection
  mydata <- reactive({
    
    # check that the uploaded data is in the expected format and stop if it isnt
    temp <- read_excel(chosen_file()$path, sheet = 1)
    
    validate(
      need(
        names(temp)[1] == "Name",
        "I am an error"
      )
    )
    # Read-in each datatable as a different element of a list
    # data_list <- list(
    #   read_excel(chosen_file()$path)
    # )  
    data_list <- lapply(chosen_file()$path, function(x){
      df <- read_excel(x)
      df
    })
    return(data_list)
  })



  # make the base plot
  # do_the_plot <- reactive({
  #   ggplot(mydata()[[1]], aes(x = Name, y = Size)) +
  #     geom_col(fill = "blue",
  #              colour = "black")
  # })
  
  # use the plotting function to deal with different conditions
  do_the_plot <- reactive({
    base_plot(mydata()[[1]])
  })


  #change plot to plotly for interactive env
  # plot_for_shiny <- reactive({
  #   ggplotly(do_the_plot(),
  #            tooltip = c("Name", "Size"),
  #            dynamicTicks = "y")
  # 
  # })
  plot_for_shiny <- reactive({
    plotly_p(do_the_plot())
  })
  # 
  # #change theme elements for pdf only
  # # plot_for_report <- reactive({
  # #   do_the_plot() +
  # #     theme_bw()
  # # })
  
  output$myplot  <- renderPlotly({ 
    plot_for_shiny()
  })
  
  
  #output report
  output$report <-
    downloadHandler(
      filename = function() { # choose filename based on combination of radio buttons chosen
        #select substring of name for file naming
        sample_name <- substr(input$yaxisGrp2, 1, regexpr("([ ])", input$yaxisGrp2) - 1)
        
        if (input$report_type == "single") {
          filename <- paste(sample_name, "Report.pdf", sep = "_")
        } else {
          filename <- "output.zip"
        }
        
        return(filename)
      },
      content = function(file) { # choose content based on combination of radio buttons chosen
        filedir <- dirname(chosen_file()$path[1])
        tempReport <- normalizePath("example_report.Rmd")
        owd <- setwd(tempdir())
        on.exit(setwd(owd))
        # Copy the report file to a known directory before processing it, in
        # case we don't have write permissions to the current working dir (which
        # can happen when deployed).
        
        # render a single report
        if (input$report_type == "single") {
          
          sample_name <- substr(input$yaxisGrp2, 1, regexpr("([ ])", input$yaxisGrp2) - 1)
      
          file.copy("example_report.Rmd", tempReport, overwrite = TRUE)
          
          
          parameters <- list(
            filename = sample_name,
            myplot = plot_for_report()
          )
          
          # Knit the document, passing in the `params` list, and eval it in a
          # child of the global environment (this isolates the code in the document
          # from the code in this app).
          rmarkdown::render(tempReport,
                            output_format = "pdf_document",
                            output_file = file,
                            params = parameters,
                            envir = new.env(parent = globalenv())
          )
          
          
          
        } else {  # if downloading multiple reports
          # fs <- c()
          # count how many files have been uploaded
          no.of_files <- nrow(input$file)
          # Allocate spaces for a character vector same length as the number of 
          # files uploaded. 
          fs <- vector(mode = "character", length = no.of_files)
          
          # Iterate through each file 
          for( i in seq_len(no.of_files)){
            # Extract output file name from input excel file name
            sample_name <- substr(input$file$name[i], 1, regexpr("([ ])", input$file$name[i]) - 1)
            # Assemble the pdf name
            fname <- paste0(sample_name, "_Report.pdf")
            # Assemble the rmd name for rendering 
            templateRmd <- paste0(sample_name, "_Report.Rmd")
            # Copy the example.Rmd to a new name
            file.copy(tempReport, templateRmd, overwrite = TRUE)
            
            # Read data from cache
            df <- read_excel(input$file$datapath[i])
   
            parameters <- list(
              filename = sample_name,
              myplot = base_plot(df)
            )
            
            rmarkdown::render(templateRmd,
                              output_format = "pdf_document",
                              output_file = fname,
                              params = parameters,
                              envir = new.env(parent = globalenv()))
            # Store the filename into the vector 
            
            fs[i] <-  fname
          }
          print(fs)
          zip(file, fs)
          
        }
        
      })
}


shiny::shinyApp(ui = ui, server = server)

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.