Home > Software engineering >  Shiny User-Driven Filter Logic
Shiny User-Driven Filter Logic

Time:10-06

I am creating an Rshiny app where I give the user the choice to filter the dataframe using check boxes which correspond to a filter row. The user also has the option via radio buttons to control the filter logic to use 'AND' logic or 'OR' logic. I've created a minimum reproducible example using the letters x-z as filter options where rows have every possible combination of x,y, and z. The checkboxes however will only have the option of x, y, or z. If a user has the OR logic selected and all letters, every single row will be visible. If a user has the OR logic selected and only 'x' and 'y', only rows x,y,xy,xyz,xz,yz will be shown. If a user has the same 'x' and 'y' selected but with the AND logic selected, only rows 'xy' and 'xyz' will be shown.

I am having difficulties creating the filtering logic for this and any help will be appreciated. The actual filters have about 10 unique choices which may increase as the complexity of the dataframe increases, and I'd like to make the filter as flexible as possible rather than hard coding every possible combination.

A reproducible example:

df <- data.frame(value = 1:7,
        filter = c("x","y","z","xy", "xz", "yz", "xyz"))

library(shiny)

ui <- fluidPage(
  title = "Example",
  sidebarLayout(
    sidebarPanel(
      radioButtons("andOR", 
                   label = "Filters Logic:",
                   c("OR", "AND"),
                   inline = TRUE),
        checkboxGroupInput("filter", "Filter Options:",
                           letters[24:26], selected = letters[24:26])
    ),
    mainPanel(
      DT::dataTableOutput("mytable1")
    )
  )
)

server <- function(input, output) {
  
  #filtering logic
  filteredData <- reactive({
    if(input$andOR == 'OR') {
        df %>%
          filter()
    } else {
      df %>%
        filter()
    }
  })

  
  output$mytable1 <- DT::renderDataTable({
    DT::datatable(filteredData())
  })
  
}

shinyApp(ui, server)

CodePudding user response:

This solution is perhaps a bit verbose, but it gets the job done in a very dynamic way. To tackle the AND logic, the helper function multi_filter uses some tidy eval stuff to generate n number of filter expressions. The OR logic is similar to other answers given, using str_detect and "|". Hopefully those who come across this answer will at least find the use of tidy eval code informative.

library(shiny)
library(tidyverse)

df <- tibble(
  value = 1:7,
  filter = c("x","y","z","xy", "xz", "yz", "xyz")
  )

# helper function to create multiple filter expressions
# .filters will be input$filter

multi_filter <- function(.data, .col, .filters) {
  expressions <- map(.filters, ~ quo(str_detect({{ .col }}, !!.x)))
  filter(.data, !!!expressions)
}

ui <- fluidPage(
  title = "Example",
  sidebarLayout(
    sidebarPanel(
      radioButtons("andOR", 
                   label = "Filters Logic:",
                   c("OR", "AND"),
                   inline = TRUE),
      checkboxGroupInput("filter", "Filter Options:",
                         letters[24:26], selected = letters[24:26])
    ),
    mainPanel(
      DT::dataTableOutput("mytable1")
    )
  )
)

server <- function(input, output) {
  
  filter_inputs <- reactive({
    if (length(input$filter) == 0) {
      return('')
    } else if (length(input$filter) == 1) {
      return(input$filter)
    } else {
      return(input$filter %>% str_flatten(collapse = '|'))
    }
  })
  
  filter_negate <- reactive({
    if (length(input$filter) == 0) {
      return(TRUE)
    } else {
      return(FALSE)
    }
  })
  
  # #filtering logic
  filteredData <- reactive({
    if(input$andOR == 'OR') {
      df %>% filter(str_detect(filter, filter_inputs(), negate = filter_negate()))
    } else {
      df %>% multi_filter(.col = filter, .filters = input$filter)
    }
  })

  
  output$mytable1 <- DT::renderDataTable({
    DT::datatable(filteredData())
  })
  
}

shinyApp(ui, server)

CodePudding user response:

Here's a solution - not very elegant but maybe it will give you ideas.

 server <- function(input, output) {

     #filtering logic
     filteredData <- reactive({
         if(is.null(input$filter)){
             df
         }
         else if(input$andOR == 'OR') {
             df %>% filter(stringr::str_detect(filter, input$filter))
         } else {
             for(x in letters[24:26]){
                 print(x)
                 if(x %in% input$filter){
                     df[[x]] <- stringr::str_detect(df$filter, x)
                 } else {
                      df[[x]] <- TRUE
                 }
             }
        
             df %>% filter(x & y & z) %>% select(-x, -y, -z)
         }
     })


     output$mytable1 <- DT::renderDataTable({
         DT::datatable(filteredData())
     })

 }

CodePudding user response:

This solution seems to work best. Thank you to this answer and this answer in the comments for help.

  #filtering logic
  library(data.table)
  filteredData <- reactive({
    if(length(input$filter)==0){
      df[0,]
    } else if(input$andOR == 'OR') {
       df %>%
        filter(filter %like% paste(input$filter,collapse = '|'))
    } else {
      df %>%
        filter(grepl(paste(sprintf("(?=.*%s)", input$filter), collapse=""), df$filter, perl=TRUE))
    }
  })
  • Related