rshinyshiny-reactivity

How to alter error message for 'lazy' shiny function?


This is a simplified version of a larger script I have made. Essentially I have 3 graphs, the first uses a user Input percentage of the total number of rows from a dataset and plots it. The second graph uses the rows that were not plotted in the first graph, applies a different percent selector, and plots that. And the third does the same thing but with respect to the second graph.

My issue is the second graph is able to return a custom error message because the dataset is available but because shiny is 'lazy' I can't query the 3rd graphs data because it doesn't exist yet. I want to do this in order to change the error message to a more useful error message for the user.

When the code below is run, the second graph has the custom error but the 3rd one says "Error: argument is of length zero" but I would like it to show the custom error. I know I can just use

validate(
  need(nrow(Graph1_data_inverse()) > 0, 'There is no data available for this graph')
)

validate(
  need(nrow(Graph2_data_inverse()) > 0, 'There is no data available for this graph')
)

But I would like a more elegant solution if possible.

library(shiny)
library(datasets)
library(shinyFeedback)
library(plotly)
data <- iris


ui <- fluidPage(
  shinyFeedback::useShinyFeedback(),
  
  column(4,
         numericInput("Graph1_percent", tags$span(style="color: #000000; font-size: 13px;", "Percent (%)"), value = 100, min = 0, max = 100),
         plotlyOutput("Graph1", height = 400)
  ),
  column(4,
         numericInput("Graph2_percent", tags$span(style="color: #000000; font-size: 13px;", "Percent (%)"), value = 100, min = 0, max = 100),
         plotlyOutput("Graph2", height = 400)
  ),
  column(4,
         numericInput("Graph3_percent", tags$span(style="color: #000000; font-size: 13px;", "Percent (%)"), value = 100, min = 0, max = 100),
         plotlyOutput("Graph3", height = 400)
  )
  
)

server <- function(input, output, session) {
  
  Graph1_data <- reactive({
    n <- nrow(data)
    if(input$Graph1_percent == 0 | n == 0) {
    } else {
      Graph1_data_ <- data[1:round(n*(input$Graph1_percent /100)), ]
    }
  })
  
  Graph1_data_inverse <- reactive({
    n <- nrow(data)
    if(n == 0 | input$Graph1_percent == 100) {
    } else {
      Graph1_data_inv <- data[(round(n*(input$Graph1_percent /100))+1):n, ]
    }
  })
  
  output$Graph1 <- renderPlotly({
    
    ggplotly(Graph1_data() %>%
               ggplot(aes(x = Sepal.Length, fill = Species))+
               geom_bar()
    )
  })
  
  ###Graph 2
  Graph2_data <- reactive({
    validate(
      need(nrow(Graph1_data_inverse()) > 0, 'There is no data available for this graph.')
    )
    
    n <- nrow(Graph1_data_inverse())
    if(input$Graph2_percent == 0 | n == 0) {
    } else {
      Graph2_data_ <- Graph1_data_inverse()[1:round(n*(input$Graph2_percent /100)), ]
    }
    
  })
  
  Graph2_data_inverse <- reactive({
    n <- nrow(Graph1_data_inverse())
    if(n == 0 | input$Graph2_percent == 100) {
    } else {
      Graph2_data_inv <- Graph1_data_inverse()[(round(n*(input$Graph2_percent /100))+1):n, ]
    }
  })
  
  output$Graph2 <- renderPlotly({
    req(Graph2_data())
    
    ggplotly(Graph2_data() %>%
               ggplot(aes(x = Sepal.Length, fill = Species))+
               geom_bar()
    )
  })
  
  ###Graph 3
  Graph3_data <- reactive({
    
    ###This section doesn't work
    exists <- exists("Graph2_data_inverse", where = -1, mode='function')
    if(exists != TRUE){
      paste("There is no data available for this graph")
      }
    req(exists, cancelOutput = TRUE)
    
    validate(
      need(nrow(Graph2_data_inverse()) > 0, 'There is no data available for this graph')
    )
    
    n <- nrow(Graph2_data_inverse())
    if(input$Graph3_percent == 0 | n == 0) {
    } else {
      Graph3_data_ <- Graph2_data_inverse()[1:round(n*(input$Graph3_percent /100)), ]
    }
    
  })
  
  ###this inverse dataset is not used but I'm including it here so the graphs retain the same structure
  Graph3_data_inverse <- reactive({
    n <- nrow(Graph2_data_inverse())
    if(n == 0 | input$Graph3_percent == 100) {
    } else {
      Graph3_data_inv <- Graph2_data_inverse()[(round(n*(input$Graph3_percent /100))+1):n, ]
    }
  })
  
  output$Graph3 <- renderPlotly({
    req(Graph3_data())
    
    ggplotly(Graph3_data() %>%
               ggplot(aes(x = Sepal.Length, fill = Species))+
               geom_bar()
    )
  })
  
}
# Run the application 
shinyApp(ui = ui, server = server)

Solution

  • When you check the trace log, you should notice that the error does not really originate from Graph3_data() but it's the Graph2_data_inverse() that fails to evaluate with Error in if :

    Warning: Error in if: argument is of length zero
      150: <reactive:Graph2_data_inverse> [#43]
      134: Graph2_data_inverse
      126: <reactive:Graph3_data> [#68]
      110: Graph3_data
      103: renderPlotly [#92]
      102: func
       99: shinyRenderWidget
       98: func
       85: renderFunc
       84: output$Graph3
        3: runApp
        2: print.shiny.appobj
        1: <Anonymous>
    

    Which in turn is a result of having potential NULL-values and elementwise OR, |, in if conditions. E.g. if you have a following reactive:

      Graph2_data_inverse <- reactive({
        n <- nrow(Graph1_data_inverse())
        if(n == 0 | input$Graph2_percent == 100) {
        } else {
          Graph2_data_inv <- Graph1_data_inverse()[(round(n*(input$Graph2_percent /100))+1):n, ]
        }
      })
    

    and Graph1_data_inverse() happens to return NULL, you get something like:

    (n <- nrow(NULL))
    #> NULL
    if (n == 0 | FALSE ) {}
    #> Error in if (n == 0 | FALSE) {: argument is of length zero
    

    In if conditions you most likely want to use ||, && instead of |, &. If there's a chance that NULL-values could sneak in and turn whole expression to 0-length / non-TRUE/FALSE, shiny::isTruthy() can be handy, but as it's also true for 0, a minimal update might look like this :

      Graph2_data_inverse <- reactive({
        n <- nrow(Graph1_data_inverse())
        if(!isTruthy(n) || n == 0 || input$Graph2_percent == 100) {
        } else {
          ...
        }
      })
    

    This applies to all conditionals, not just for the one in Graph2_data_inverse().