Generating selectInput choices from multiple factor columns in data frame

I have a challenge in the below code. I am trying to reduce the code by incorporating Selectinput under for Loop. I have a data frame. I tried with the below code but it is not getting executed

df <- structure(list(A = structure(c(1L, 4L, 6L, 1L, 8L, 2L, 7L, 3L, 
5L, 5L, 1L, 8L, 2L, 7L, 2L), .Label = c("asd", "dfg", "fgdsgd", 
"fsd", "gdfgd", "gs", "sdfg", "sf"), class = "factor"), B = c(29L, 
24L, 46L, 50L, 43L, 29L, 32L, 24L, 35L, 39L, 33L, 47L, 53L, 26L, 
31L), C = structure(c(8L, 5L, 1L, 6L, 3L, 2L, 9L, 7L, 6L, 3L, 
2L, 9L, 8L, 8L, 4L), .Label = c("asd", "er", "fg", "gf", "gfd", 
"gfg", "qw", "sf", "tr"), class = "factor"), D = c(36L, 56L, 
39L, 26L, 56L, 35L, 27L, 31L, 33L, 45L, 34L, 27L, 43L, 40L, 56L
), E = structure(c(8L, 5L, 1L, 6L, 3L, 2L, 9L, 7L, 6L, 3L, 2L, 
9L, 8L, 8L, 4L), .Label = c("asd", "er", "fg", "gf", "gfd", "gfg", 
"qw", "sf", "tr"), class = "factor"), F = c(44L, 34L, 37L, 23L, 
37L, 51L, 28L, 36L, 33L, 31L, 39L, 43L, 25L, 37L, 43L)), class = 
"data.frame", row.names = c(NA, -15L))

shinyApp(
ui = fluidPage(
selectInput("state", "Choose a state:",for(i in names(Filter(is.factor,df))){
            list(i = c(levels(df[,i])))
),
textOutput("result")
),
server = function(input, output) {
output$result <- renderText({
  paste("You chose", input$state)
})
}
)
 shinyApp(ui, server)

In this dataset , there are 3 factors that are "A" "C" "E". So I need to put them as a SelectInput with its factors as drop down. For example,

Column A has factor (asd,fsd,gs,asd,sf,dfg,sdfg,fgdsgd,gdfgd,gdfgd,asd,sf, dfg,sdfg,dfg). I need these as drop down. I can make this happen. But i need this under for loop so that there is no need to enter for all factors. Hope my points are clear and makes sense

Does this do what you want?

library(shiny)

  df <- structure(list(A = structure(c(1L, 4L, 6L, 1L, 8L, 2L, 7L, 3L, 
                                       5L, 5L, 1L, 8L, 2L, 7L, 2L), 
                                     .Label = c("asd", "dfg", "fgdsgd", "fsd", "gdfgd", 
                                                "gs", "sdfg", "sf"), class = "factor"), 
                       B = c(29L, 24L, 46L, 50L, 43L, 29L, 32L, 24L, 35L, 39L, 33L, 47L, 53L, 26L,31L), 
                       C = structure(c(8L, 5L, 1L, 6L, 3L, 2L, 9L, 7L, 6L, 3L,  2L, 9L, 8L, 8L, 4L), 
                                     .Label = c("asd", "er", "fg", "gf", "gfd",  "gfg", "qw", "sf", "tr"), 
                                     class = "factor"), 
                       D = c(36L, 56L,39L, 26L, 56L, 35L, 27L, 31L, 33L, 45L, 34L, 27L, 43L, 40L, 56L), 
                       E = structure(c(8L, 5L, 1L, 6L, 3L, 2L, 9L, 7L, 6L, 3L, 2L,  9L, 8L, 8L, 4L), 
                                     .Label = c("asd", "er", "fg", "gf", "gfd", "gfg",  "qw", "sf", "tr"), 
                                     class = "factor"), 
                       F = c(44L, 34L, 37L, 23L, 37L, 51L, 28L, 36L, 33L, 31L, 39L, 43L, 25L, 37L, 43L)), 
                  class = "data.frame", row.names = c(NA, -15L))
  
  theNames <- names(Filter(is.factor,df))
  MyList  <- vector(mode = "list")
  
  for(i in theNames){
    MyList[[i]] <- levels(df[,i])
  }
  
  ui = fluidPage(
    selectInput("state", "Choose a state:", MyList),
    textOutput("result")
  )
  server = function(input, output) {
    output$result <- renderText({
      paste("You chose", input$state)
    })
  }

shinyApp(ui, server)
2 Likes

Perfect thanks, But Column C is not coming in the list, Even that is a factor

When I run it I do see the levels of column C in the Select widget.

Is it. But i do not see. Let me know restart and see again. But thanks a lot, This is what I wanted. But when i was trying to build the code for this, I saw some posts where there put a for loop under server it self. Does this make a difference?

Putting the for loop in the server would make sense if you were dynamically building the html widget. That is, if you used other inputs to modify the content of the selectInput. If the selectInput depends only on the initial content of the data, I would do the calculation as I showed it.

Hy Hi, I got the issue. Since E and C values are same, I am not getting Column C. Can this be solved?

Also there is another issues, if you look at A there are 8 factors, but in filter under A there are only 6. But I need 8, I think the factors that are repeating that are not captured and hence column C is not coming. Can we solve this problem?

I think you can break your problem up into two pieces:

  1. Generating a named list from the factor columns in your data frame
  2. Generating the input control that you want using selectInput()

I think you can make the first step a little more clear with a little purrr:

library(purrr)
states <- df %>% 
  keep(is.factor) %>% 
  map(levels)
str(states)
#> List of 3
#>  $ A: chr [1:8] "asd" "fsd" "gs" "sf" ...
#>  $ C: chr [1:9] "sf" "gfd" "asd" "gfg" ...
#>  $ E: chr [1:9] "sf" "gfd" "asd" "gfg" ...

(If you use a for loop, you need to follow @FJCC's structure, and you may want to refresh your knowledge of the basic structure of a for loop in R)

I think you would have found this part of your problem easier to solve if you made this list explicit so that you could see exactly what was in it.

The second part of the problem is that selectInput() appears to only take unique values. This doesn't appear to be mentioned in the documentation, so it does not appear to be something that you can control. That means that the best path forward is to file an issue, describing why this would be helpful for you, and including a small reproducible example that illustrates the current problem.

1 Like

Thanks for the reply. The code is almost good. But in the filters it is showing only 6 levels under A. Also there is C present in the filters. Do not know what is the issue

As I said above, selectInput() appears to only use the unique values of choices.

HI @FJCC , Is it not possible to solve this? I am trying but not getting

I don't know how to change this behavior of the selectInput(). I am running a down level version of shiny, 1.1.0, and it does allow repeated values. I guess if you really need this, you could use version 1.1.0, which is available on CRAN, though probably as source code and it would have to be compiled.

Before you do that, what are you trying to accomplish by having repeated values in the list? The widget will just return the selected value in any case.

I need repeated values. Because, if I filter any values that are repeated the plot i get will be confusing. So in need exactly whatever values are there under the variable

Isn't it more confusing to work with the repeated data? Let's step back and think about it for a moment. So you have repeated values in A, in the example 3*asg and 3*dfg, each of them has different values in B, C, D, ... Similarly you have multiple entries in the other columns.
You want to filter based on the entries in columns A, C and E and are interested in the numbers in B,D and F. Right?
Now you want to select what, a certain set of combinations of A, C and E?

And when you say you need the repetitions, how do you know if it makes more sense to select the first, the second or the third occurence of the same element? Or do you mean you have problems because the same entries occur in multiple columns and dissappear when generating the list for the selectInput?
Why don't you simply use 3 selections, one for A, C and E? To start with. Could solve the problem of elements in C being present in A as well.
Or you can merge the 3 columns into 1 selector, e.g. asd_sf_sd vs. fsd_gfd_gfd.

1 Like

“Why don't you simply use 3 selections, one for A, C and E? To start with. Could solve the problem of elements in C being present in A as well”

Perfect this could be the solution. I also tried this by putting separately, but the code is not working. Any idea why. I just repeated that 3 times that’s it .

You have to give a unique name to each selector. And due to the combinatorics you cannot simply chose all possible entries from the list (well you could) but in the end you wont have a matching condition in your dataset.
Consequently you shoul limit the selections to the availabe conditions left, this can be done with renderUI, e.g. like this:

library(shiny)
library(dplyr)

df <- structure(list(A = structure(c(1L, 4L, 6L, 1L, 8L, 2L, 7L, 3L, 
                                     5L, 5L, 1L, 8L, 2L, 7L, 2L), .
      Label = c("asd", "dfg", "fgdsgd",  "fsd", "gdfgd", "gs", "sdfg", "sf"), 
                   class = "factor"),    
      B = c(29L,  
      ....

 ui = fluidPage(
  selectInput("CondA", "1st choose Cond. A:", 
              choices = df$A, width = "80%"),
  uiOutput("SelectionC"),
  uiOutput("SelectionE"),
  textOutput("selection"),
  tableOutput("result")
)

server = function(input, output) {

  output$selection <- renderText({
    paste("You chose", paste(input$CondA, input$CondC, input$CondE, sep = " & "))
  })
 output$SelectionC = renderUI({
    req(input$CondA != 0)
    selectInput("CondC", "2nd choose Cond. C:", 
                choices = pull(filter(df, A == input$CondA), C), width = "80%") 
    })
 output$SelectionE = renderUI({
   req(input$CondA != 0)
   selectInput("CondE", "3rd choose Cond. E:", 
               choices = pull(filter(df, C == input$CondC), E), width = "80%") 
   })
  
 output$result <- renderTable({
    req(input$CondE)
    filter(df, A == input$CondA & C == input$CondC & E == input$CondE )
  })
}

shinyApp(ui, server)

grafik

Thanks May I know what is UIOutput and renderUI doing here. We can do without that also right?

You can do, you could just use:
selectInput("CondA", "1st choose Cond. A:", choices = df$A) for A and then replacing A by C and E for the other ones.
But then you will select all possible combinations, not neccessarrily found in your data. In more detail the 8 * 9 * 9 choices create 648 possible combinations, the chance of matching this to your data set is (for the 15 rows in the demo data) 2.3 %.
If you want to check if these combinations are in the data, you are fine.

If you want to extract only combinatiions that are in the data you have to filter the possible options to the avialable space before. This is interactive based on the selections you did. Then you have to send the updates from the server to the UI. The only way I see is through the render UI, because only there the selections are actually updated and not evaluated already at the start.

@Matthias I think you're jumping to renderUI() to early. In this case you can create regular selectInput()s and then later update them:

library(shiny)

df <- rev(expand.grid(C = letters, B = letters, A = letters))
df <- df[sort(sample(nrow(df), 1000)), , drop = FALSE]

ui <- fluidPage(
  selectInput("A", "A", choices = df$A),
  selectInput("B", "B", choices = df$B),
  selectInput("C", "C", choices = df$C),
  textOutput("nrow")
)

server = function(input, output, session) {
  selected <- reactive({
    df[df$A == input$A & df$B == input$B & df$C == input$C, , drop = FALSE]
  })
  
  observeEvent(input$A, {
    choices <- df$B[df$A == input$A]
    updateSelectInput(session, "B", choices = choices, selected = input$B)
  })
  observeEvent(input$B, {
    choices <- df$C[df$A == input$A & df$B == input$B]
    updateSelectInput(session, "C", choices = choices, selected = input$C)
  })

 output$nrow <- renderText({
    nrow(selected())
 })
}

shinyApp(ui, server)

I think renderUI() should only be used as a last resort because it makes it much harder to skim the code and understand the basic structure of the app. It also has the potential to make the app less performant (since it has to do more work and it's easy to make the reactive graph more complicated than needed).

2 Likes

Thanks @hadley or the insights and the warning!
I also don't like the renderUI approach very much, just because it moves UI elements in the server file, adding clutter to the code. But I didn't knew the updateSelectInput() function. That's why I said "The only way I see is through the render UI".

I will definitely include this in the future and review my code accordingly! :slightly_smiling_face: