2

In my code I use two selectInput() functions: one for the indicating the start period to feed into a custom function, and another for indicating the end period to feed into the same custom function. At the bottom is a simplified MWE extract of the code, which doesn't use this custom function, using instead rbind() to join and output the to/from data for sake of simplicity. In the full code, the end period must always be greater than (>) the start period for the custom function to work.

How would I make the choices in the "To" (selectInput(inputId = "toPeriod"...) reflect only those values > than what was input by the user in the "From" (selectInput(inputId = "fromPeriod") function?

I realize this requires making the to/from input choices reactive, so I started by moving the selectInput() functions into the server section using renderUI, but I stopped when receiving the message "Warning: Error in : Problem with filter() input ..1." even though the output is correct. In any case, both before and after moving the selectInput() functions into the server section, this code seems to run slowly.

This image better explains:

enter image description here

There are other posts getting at the same issue but either the code examples are overly-cumbersome or the questions/answers are poorly written or explained: R Shiny selectInput Reactivity, R shiny passing reactive to selectInput choices, Vary the choices in selectinput based on other conditions in shiny R, etc.

MWE code:

library(dplyr)
library(shiny)
library(tidyverse)

data <- 
  data.frame(
    ID = c(1,1,1,2,2,2,3,3,3),
    Period = c(1, 2, 3, 1, 2, 3, 1, 2, 3),
    Values = c(5, 10, 15, 0, 2, 4, 3, 6, 9)
  )

ui <- fluidPage(
  tableOutput("data"),
  uiOutput("fromPeriod"),
  uiOutput("toPeriod"),
  tableOutput("dataSelect")
)

server <- function(input, output) {
  
  output$fromPeriod <- renderUI({
    selectInput(inputId = "fromPeriod",label = "From period:",choices = unique(data$Period), selected = 1)
  })
  
  output$toPeriod <- renderUI({
    selectInput(inputId = "toPeriod",label = "To period:",choices = unique(data$Period), selected = 2)
  })
  
  output$data <- renderTable({data})
  
  output$dataSelect <- renderTable({
    part1 <- data %>% filter(Period == input$fromPeriod)
    part2 <- data %>% filter(Period == input$toPeriod)
    rbind(part1,part2)
  }, rownames = TRUE)
}

shinyApp(ui, server)

2 Answers2

3

You should avoid renderUI where possible and use update* functions instead - updating is faster than re-rendering:

library(shiny)
library(data.table)

DT <- data.table(
  ID = c(1, 1, 1, 2, 2, 2, 3, 3, 3),
  Period = c(1, 2, 3, 1, 2, 3, 1, 2, 3),
  Values = c(5, 10, 15, 0, 2, 4, 3, 6, 9)
)

all_choices <- unique(DT$Period)

ui <- fluidPage(
  tableOutput("data"),
  selectizeInput(
    inputId = "fromPeriod",
    label = "From period:",
    choices = setdiff(all_choices, last(all_choices)),
    selected = 1
  ),
  selectizeInput(
    inputId = "toPeriod",
    label = "To period:",
    choices = setdiff(all_choices, first(all_choices)),
    selected = 2
  ),
  tableOutput("dataSelect")
)

server <- function(input, output, session) {
  output$data <- renderTable({
    DT
  })
  
  observeEvent(input$fromPeriod, {
    freezeReactiveValue(input, "toPeriod")
    updateSelectizeInput(
      session,
      inputId = "toPeriod",
      choices = all_choices[all_choices > input$fromPeriod],
      selected = max(all_choices[all_choices > input$fromPeriod])
    )
  }, ignoreInit = TRUE)
  
  output$dataSelect <- renderTable({
    # in one line, however you seem to need part1 / part2 for your custom function
    # setorder(DT[Period %in% c(input$fromPeriod, input$toPeriod)], Period)
    part1 <- DT[Period == input$fromPeriod]
    part2 <- DT[Period == input$toPeriod]
    rbindlist(list(part1, part2))
  }, rownames = TRUE)
}

shinyApp(ui, server)

To avoid triggering reactives or outputs unnecessarily you should almost alway use freezeReactiveValue when using a update* function in . Please see this related chapter from Mastering Shiny.

ismirsehregal
  • 30,045
  • 5
  • 31
  • 78
  • 1
    I think it's worth noting that in this case there is no re-rendering either way, since the outputs don't contain any reactive dependencies and as such never get re-executed. That said, this is definitely the clearer way to go about this. – Mikko Marttila Mar 29 '22 at 12:05
  • 1
    @MikkoMarttila that's correct - I assumed that in CuriousJorge-user9788072's actual code the dataset is `reactive`. – ismirsehregal Mar 29 '22 at 12:21
  • Yes Mikko, and the implementation into the full code that this was meant for was much easier. I agree it is better to avoid renderUI when possible. My only change from ismirsehregal's solution is changing the "selected" in the updateSelectizeInput(), from max to min, so that the "To" period is the one that immediately follows the "From" period --- because in the fuller code this is intended for, there are very many unique periods and the last period will likely never be selected by a user. Also, this is my first exposure to freezeReactiveValue(), it seems very useful, I'm studying it now. – Curious Jorge - user9788072 Mar 29 '22 at 12:25
1

Thanks for the clear question and great minimal example.

You receive the warning "Warning: Error in : Problem with filter() input ..1." because on loading the first time, initially the fromPeriod and toPeriod are NULL. They are loaded right thereafter, so you can see the results just fine. You can prevent the warning by adding req(input$fromPeriod) to the renderTable({...}) body.

The SelectInput can be updated using updateSelectInput. We need to wrap this in an observe statement such that it reacts to changes in input$fromPeriod. I created a variable all_choices in the beginning of the server body to make the code a bit more readable.

library(dplyr)
library(shiny)
library(tidyverse)

data <- 
  data.frame(
    ID = c(1,1,1,2,2,2,3,3,3),
    Period = c(1, 2, 3, 1, 2, 3, 1, 2, 3),
    Values = c(5, 10, 15, 0, 2, 4, 3, 6, 9)
  )

ui <- fluidPage(
  tableOutput("data"),
  uiOutput("fromPeriod"),
  uiOutput("toPeriod"),
  tableOutput("dataSelect")
)

server <- function(input, output, session) {
  all_choices <- unique(data$Period)
  
  output$fromPeriod <- renderUI({
    selectInput(inputId = "fromPeriod", label = "From period:", choices = unique(data$Period), selected = 1)
  })
  
  output$toPeriod <- renderUI({
    selectInput(inputId = "toPeriod", label = "To period:",choices = unique(data$Period), selected = 2)
  })
  
  observe({
    req(input$fromPeriod)
    
    new_choices <- all_choices[all_choices > as.numeric(input$fromPeriod)]
    updateSelectInput(session, inputId = "toPeriod", choices = new_choices, selected = min(new_choices))
  })
  
  output$data <- renderTable({data})
  
  output$dataSelect <- renderTable({
    req(input$fromPeriod)
    part1 <- data %>% filter(Period == input$fromPeriod)
    part2 <- data %>% filter(Period == input$toPeriod)
    rbind(part1,part2)
  }, rownames = TRUE)
}

shinyApp(ui, server)
Bas
  • 4,628
  • 1
  • 14
  • 16
  • Beautiful! Nice explanation too. I realized this as I tested your solution, something I should have realized in my OP: the FROM period can never be the last period in the listing of Periods (just as the TO period can't be the first period in the list), so I changed the choices in the output$fromPeriod to choices = head(unique(data$Period),-1) to drop the last period. – Curious Jorge - user9788072 Mar 29 '22 at 10:18