A Duv
A Duv

Reputation: 403

How to use RShiny reactiveFileReader with reactiveUI and non-existent files?

How would I build a reactive UI that responds to a reactiveFileReader with varying data inputs?

I'm interested in integrating a reactiveFileReader into an app that graphs groups within the data and shows selected points group by group.

Challenges:

  1. Not every file I can identify from the prefix and suffix exists.
  2. There are varying number of groups per file.

CRASHES/FAILS WHEN I

  1. Attempt to open a non-existent file.
  2. Update a file (so it does detect that there was a change)

POTENTIAL SOLUTIONS:

  1. Slow down/delay the next steps after reading the data so it can re-load the data. Fixed via a reactive() and req()
  2. isolate() the dependent UI so it only changes the # of groups the first time a file is loaded.

I included mock data (and its generation), a UI, the broken server, and a working server that doesn't have the reactive file reader.

UPDATES

The only thing left is to have the renderUI 'group' not reset the moment the file is reread. Normally it's a good thing, but here I don't want that.

Packages

library(tidyr); library(dplyr); library(ggplot2); library(readr); library(stringr)
library(shiny)
#library(DT)

Mock Data

a1 <- structure(list(Group = c("alpha_1", "alpha_1", "alpha_2", "alpha_2", "alpha_3", "alpha_3"), Sample = c("ps_1", "ps_2", "ps_1", "ps_2", "ps_1", "ps_2"), x = c(1, 1.1, 4, 4.1, 6.8, 7), y = c(2.1, 2, 7.3, 7, 10, 9.7)), .Names = c("Group", "Sample", "x", "y"), row.names = c(NA,-6L), class = c("tbl_df", "tbl", "data.frame"), spec = structure(list(cols = structure(list(Group = structure(list(), class = c("collector_character", "collector")),Sample = structure(list(), class = c("collector_character","collector")), x = structure(list(), class = c("collector_double", "collector")), y = structure(list(), class = c("collector_double", "collector"))), .Names = c("Group", "Sample", "x", "y")), default = structure(list(), class = c("collector_guess", "collector"))), .Names = c("cols", "default"), class = "col_spec"))
a2 <- structure(list(Group = c("alpha_6", "alpha_6", "alpha_7", "alpha_7", "alpha_9", "alpha_9", "alpha_10", "alpha_10"), Sample = c("ps_1", "ps_2", "ps_1", "ps_2", "ps_1", "ps_2", "ps_1", "ps_2"), x = c(3,3.2, 5, 5.1, 1, 1.1, 5, 5.1), y = c(8.1, 7, 3, 4, 14, 15, 4,3)), .Names = c("Group", "Sample", "x", "y"), row.names = c(NA, -8L), class = c("tbl_df", "tbl", "data.frame"), spec = structure(list(cols = structure(list(Group = structure(list(), class = c("collector_character", "collector")), Sample = structure(list(), class = c("collector_character","collector")), x = structure(list(), class = c("collector_double", "collector")), y = structure(list(), class = c("collector_double", "collector"))), .Names = c("Group", "Sample", "x", "y")), default = structure(list(), class = c("collector_guess", "collector"))), .Names = c("cols", "default"), class = "col_spec"))
b2 <- structure(list(Group = c("beta_3", "beta_3", "beta_4", "beta_4", "beta_6", "beta_6"), Sample = c("ps_1", "ps_2", "ps_1", "ps_2", "ps_1", "ps_2"), x = c(3, 3.2, 5, 5.1, 1, 1.1), y = c(8.1, 7, 3, 4, 14, 15)),.Names = c("Group", "Sample", "x", "y"), row.names = c(NA, -8L), class = c("tbl_df", "tbl", "data.frame"), spec = structure(list(cols = structure(list(Group = structure(list(), class = c("collector_character", "collector")), Sample = structure(list(), class = c("collector_character","collector")), x = structure(list(), class = c("collector_double", "collector")), y = structure(list(), class = c("collector_double", "collector"))), .Names = c("Group", "Sample", "x", "y")), default = structure(list(), class = c("collector_guess", "collector"))), .Names = c("cols", "default"), class = "col_spec"))
b3 <- structure(list(Group = c("beta_3", "beta_3", "beta_4", "beta_4", "beta_6", "beta_6"), Sample = c("ps_1", "ps_2", "ps_1", "ps_2", "ps_1", "ps_2"), x = c(3, 3.2, 5, 5.1, 1, 1.1), y = c(8.1, 7, 3, 4, 14, 15)), .Names = c("Group", "Sample", "x", "y"), row.names = c(NA, -8L), class = c("tbl_df", "tbl", "data.frame"), spec = structure(list(cols = structure(list(Group = structure(list(), class = c("collector_character", "collector")), Sample = structure(list(), class = c("collector_character","collector")), x = structure(list(), class = c("collector_double", "collector")), y = structure(list(), class = c("collector_double", "collector"))), .Names = c("Group", "Sample", "x", "y")), default = structure(list(), class = c("collector_guess", "collector"))), .Names = c("cols", "default"), class = "col_spec"))

# Data export to simulate the problem
lz_write <- function(input) {
  write_csv(input, paste0(substitute(input), ".csv"))
}
lz_write(a1); lz_write(a2); lz_write(b2); lz_write(b3) # Messed up function for lapply...
# rm(list = ls()) # Clean the environment

UI

ui <- fluidPage(
  titlePanel("Minimal Example"),
  fluidRow(
    column(width = 2, class = "well", 
           # File selection
           HTML(paste("Which file?")),
           # Prefix:
           selectInput(inputId = "p",
                       label = "Prefix:",
                       choices = c("a", "b", "c"),  
                       selected = "a"), 

           # Suffix:
           numericInput(inputId = "s",
                        label = "Suffix:",
                        min = 1,
                        max = 3,
                        value = 1,
                        step = 1)), 
    column(width = 10,
           plotOutput(outputId = "scatterplot",
                      dblclick = "plot_dblclick",  # Might not be necessary, but it's not more work to include but more work to exclude
                      brush = brushOpts(id =  "plot_brush", resetOnNew = TRUE)))
    ), 
  fluidRow(
    column(width = 3,
           br(),
           uiOutput(outputId = "group_n")), 
    column(width = 9, 
           fixedRow( 
             column(width = 3,
                    HTML(paste0("Arg 1"))),
             column(width = 3,
                    HTML(paste0("Arg 2"))),
             column(width = 3,
                    uiOutput(outputId = "num_2"))
             )
    )
  ),
  fluidRow(
    br(), br(), br(), #Lets add some gaps or spacing
    DT::dataTableOutput(outputId = "Table")) # Summary table
)  # Not sure if actually necessary for this example

Broken Server It's only problem right now is that the UI resets the moment the file is re-read...

server_broken <- function(input, output, session) { # Broken version

  #Larger subset: A Reactive Expression # May be used later...
  args <- reactive({
    list(input$p, input$s)  #which file do we wish to input. This was our tag
  })
  # Reactive File-reader Subset
  path <- reactive({
    paste0(input$p, input$s, ".csv")
  }) # Reactive Filename, kinda like our args... 



  filereader <- function(input) { # The function we pass into a reactive filereader. 
    suppressWarnings(read_csv(input, col_types = cols(
      Group = col_character(),
      Sample = col_character(),
      x = col_double(),
      y = col_double())
    ))
  }

  ##BROKEN REACTIVE FILE READER HERE##
  data_1 <- reactiveValues() # The function we use for livestream data
  observe({
    if(file.exists(path()) == TRUE) {
      fileReaderData <-  reactiveFileReader(500, session, path(), filereader) 
    }  else { 
      message("This file does not exist") 
    ## OR DO I DO SOMETHING ELSE HERE??##
    }
    data_1$df <- reactive({ 
     ## STOPS APP CRASHING, BUT NO LONGER REFRESHES CONSTANTLY ##
      req(fileReaderData()) 
      fileReaderData()
    })   
  }) # Honestly don't understand still

  data <- reactive(data_1$df()) # Pulling things out just so the rest of our code can stay the same. 

  ## END OF BROKEN FILE READER##
  ## Reactive UI HERE##
  data_m <- reactive({
    req(data()) 
    args()
    tmp <- isolate(select(data(), Group))
    tmp %>% distinct()
  }) # number of groups

  output$num_2 <- renderUI({
    req(data())
    numericInput(inputId = "n",
                 label = "Group:",
                 min = 1,
                 max = length(data_m()$Group), 
                 value = 1 
    )
  }) #This is our 'reactive' numeric input for groups. This caps the max of our function based on the number of groups there are per file

  n <- reactive(input$n) #which marker number we are dealing with. 
  ## End of reactive UI##
  data_n <- reactive({
    req(data()); req(data_m())
    dt <- filter(data(), Group == data_m()[[1]][input$n])
  }) 


  # Create scatterplot object the plotOutput function is expecting ----
  ranges <- reactiveValues(x = NULL, y = NULL)


  output$scatterplot <- renderPlot({
    validate(need(data(), "The specified file does not exist. Please try another"))
    p <- as.numeric(input$p)
    plot <- ggplot(data_n(), aes(x, y)) +  
      labs(title = paste0("Group ", data_n()$Group[1])) + 
      labs(x = "X vals", y = "Y vals") + 
      geom_point() + theme_bw() # I already have customized aesthetics. Removed for minimalism  
    plot + coord_cartesian(xlim = ranges$x, ylim = ranges$y, expand = TRUE) # So we see all points more readily. messes up the zoom but oh well
  })

  # When a double-click happens, check if there's a brush on the plot.
  # If so, zoom to the brush bounds; if not, reset the zoom.
  observeEvent(input$plot_dblclick, {
    brush <- input$plot_brush
    if (!is.null(brush)) {
      ranges$x <- c(brush$xmin, brush$xmax)
      ranges$y <- c(brush$ymin, brush$ymax)
    } else {
      ranges$x <- NULL
      ranges$y <- NULL
    }
  })  


  #Creating text ----
  output$group_n <- renderText({
    req(data())
    paste0("There are ", length(data_m()$Group), " groups in this file.",
           tags$br("This is Group: ", data_m()$Group[n()])
    )
  }) 

  #Building a table for you to visibly see points. You may need to update the DT to the github version ----
  output$Table <- DT::renderDataTable({
    req(data())
    brushedPoints(data_n(), brush = input$plot_brush) %>%
      select(Sample) 
  })

}

Functional Server

It has been removed since the broken one at least doesn't crash, and the problem is apparent. See previous edits for the original.

Sources consulted

Session Info

UPDATES

Placing a reactive within an Observe() stopped the app from crashing, AND it does update the files (forgot to delete some stuff). All that's left is saving the dependent UI somewhere...

Upvotes: 3

Views: 1849

Answers (1)

A Duv
A Duv

Reputation: 403

In short, problems were due to not properly understanding the logic of observers, missing a () after a reactive, and not calling req to stop certain parts from re-executing (see HERE).

Specific line-by-line updates can be found by looking for ##CHANGE: below... The most important changes (in no significant order) are:

  1. Using isolate() for the renderUI
  2. Using req() in the renderUI to slow it down and not run until there is an update in the # of groups, but calling args() to make it dependent upon the file selection
  3. pre-calculating the # of groups outside the renderUI

Updated Server

server_fixed <- function(input, output, session) { 

  #Larger subset: A Reactive Expression # May be used later...
  args <- reactive({
    list(input$p, input$s)  #which file do we wish to input. This was our tag
  })
  # Reactive File-reader Subset
  path <- reactive({
    paste0(input$p, input$s, ".csv")
  }) # Reactive Filename, kinda like our args... 



  filereader <- function(input) { # The function we pass into a reactive filereader. 
    suppressWarnings(read_csv(input, col_types = cols(
      Group = col_character(),
      Sample = col_character(),
      x = col_double(),
      y = col_double())
    ))
  }

  data_1 <- reactiveValues() # The function we use for livestream data
  observe({
    if(file.exists(path()) == TRUE) {
      fileReaderData <-  reactiveFileReader(500, session, path(), filereader) 
    }  else { 
      message("This file does not exist")
    }
    data_1$df <- reactive({
      # if(exists(fileReaderData())) {
      #   fileReaderData()
      # } # Crashed from the beginning
      req(fileReaderData()) 
      fileReaderData()
    })   
  }) 

  data <- reactive(data_1$df()) ##CHANGE: FORGOT THE ()##

  # Group setting...
  data_m <- reactive({
    req(data()) 
    args()
    tmp <- isolate(select(data(), Group))
    tmp %>% distinct()
  }) #number of markers, keeping only the marker name

  data_m_length <- reactive({ ##CHANGE: TOOK OUT OF output$num_2## 
  ##CHANGE: ADDED AN ISOLATE to fix the # of groups per file ##

    isolate(length(data_m()$Group))
  })

  output$num_2 <- renderUI({
    req(data_m_length()) ## CHANGE: ONLY EXECUTE ONCE WE HAVE OUR isolated data_m_length##
    args() ## CHANGE: DEPENDENT UPON changing files##
    isolate(
    numericInput(inputId = "n",
                 label = "Group:",
                 min = 1,
                 max = data_m_length(), 
                 value = 1 # THIS SHOULD BE CACHED! 
    )) ##CHANGE: ADDED IT IN ISOLATE when testing. NOT SURE IF STILL NEEDED##
  }) #This is our 'reactive' numeric input for groups. This caps the max of our function based on the number of groups there are per file

  n <- reactive(input$n) #which marker number we are dealing with. 

  data_n <- reactive({
    req(data()); req(data_m())
    dt <- filter(data(), Group == data_m()[[1]][n()])
  }) 


  # Create scatterplot object the plotOutput function is expecting ----
  ranges <- reactiveValues(x = NULL, y = NULL)


  output$scatterplot <- renderPlot({
    validate(need(data(), "The specified file does not exist. Please try another"))
    p <- as.numeric(input$p)
    plot <- ggplot(data_n(), aes(x, y)) +  
      labs(title = paste0("Group ", data_n()$Group[1])) + 
      labs(x = "X vals", y = "Y vals") + 
      geom_point() + theme_bw() # I already have customized aesthetics. Removed for minimalism  
    plot + coord_cartesian(xlim = ranges$x, ylim = ranges$y, expand = TRUE) # So we see all points more readily. messes up the zoom but oh well
  })

  # When a double-click happens, check if there's a brush on the plot.
  # If so, zoom to the brush bounds; if not, reset the zoom.
  observeEvent(input$plot_dblclick, {
    brush <- input$plot_brush
    if (!is.null(brush)) {
      ranges$x <- c(brush$xmin, brush$xmax)
      ranges$y <- c(brush$ymin, brush$ymax)
    } else {
      ranges$x <- NULL
      ranges$y <- NULL
    }
  })  


  #Creating text ----
  output$group_n <- renderText({
    req(data())
    paste0("There are ", length(data_m()$Group), " groups in this file.",
           tags$br("This is Group: ", data_m()$Group[n()])
    )
  }) 

  #Building a table for you to visibly see points. You may need to update the DT to the github version ----
  output$Table <- DT::renderDataTable({
    req(data())
    brushedPoints(data_n(), brush = input$plot_brush) %>%
      select(Sample) 
  })

}

All that's left is to use suppressError and validate appropriately.

Upvotes: 3

Related Questions