Skip to content

Instantly share code, notes, and snippets.

@jcheng5
Last active September 29, 2022 13:08
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jcheng5/76cffbbdd9db0b0e971fd34f575fa45b to your computer and use it in GitHub Desktop.
Save jcheng5/76cffbbdd9db0b0e971fd34f575fa45b to your computer and use it in GitHub Desktop.
library(shiny)
columnFilterUI <- function(id) {
ns <- NS(id)
uiOutput(ns("filter_container"))
}
columnFilter <- function(input, output, session, df, col_num, choice_filter) {
# This renders a selectInput and only re-renders when the selected data
# frame changes. (i.e. it doesn't re-render when filters change state.)
output$filter_container <- renderUI({
# Don't render if col_num is > actual number of cols
req(col_num <= ncol(df()))
freezeReactiveValue(input, "filter_value")
selectInput(session$ns("filter_value"), names(df())[[col_num]],
choices = sort(unique(df()[,col_num,drop=TRUE])),
multiple = TRUE)
})
# When the other filters change, update this filter to remove rows that
# are filtered out by the other filters' criteria. (We also add in the
# currently selected values for this filter, so that changing other
# filters does not cause this filter's selected values to be unselected;
# while that behavior might make sense logically, it's a poor user
# experience.)
observeEvent(choice_filter(), {
current_values <- input$filter_value
updateSelectInput(session, "filter_value",
choices = sort(unique(c(current_values, df()[choice_filter(),col_num,drop=TRUE]))),
selected = current_values
)
})
# Return a reactive that is a row index of selected rows, according to
# just this filter. If this filter shouldn't be taken into account
# because its col_num is too high, or if there are no values selected,
# just return TRUE to accept all rows.
reactive({
if (col_num > ncol(df())) {
TRUE
} else if (!isTruthy(input$filter_value)) {
TRUE
} else {
df()[,col_num,drop=TRUE] %in% input$filter_value
}
})
}
columnFilterSetUI <- function(id, maxcol, colwidth) {
ns <- NS(id)
fluidRow(
lapply(1:maxcol, function(i) {
column(colwidth,
columnFilterUI(ns(paste0("col", i)))
)
})
)
}
columnFilterSet <- function(input, output, session, df, maxcol) {
# Each column filter needs to only display the choices that are
# permitted after all the OTHER filters have had their say. But
# each column filter must not take its own filter into account
# (hence we do filter[-col], not filter, in the reactive below).
create_choice_filter <- function(col) {
reactive({
filter_values <- lapply(filters[-col], do.call, args = list())
Reduce(`&`, filter_values, TRUE)
})
}
# filters is a list of reactive expressions, each of which is a
# logical vector of rows to be selected.
filters <- lapply(1:maxcol, function(i) {
callModule(columnFilter, paste0("col", i), df, i, create_choice_filter(i))
})
reactive({
# Unpack the list of reactive expressions to a list of logical vectors
filter_values <- lapply(filters, do.call, args = list())
# Combine all the logical vectors using & operator
selected_rows <- Reduce(`&`, filter_values, TRUE)
# Return the data frame, filtered by the selected rows
df()[selected_rows,]
})
}
ui <- fluidPage(
selectInput("dataset", "Dataset", c("mtcars", "pressure", "cars"), selected = "mtcars"),
columnFilterSetUI("filterset", maxcol = 4, colwidth = 3),
DT::dataTableOutput("table")
)
server <- function(input, output, session) {
selected_data <- reactive({
get(input$dataset, "package:datasets")
})
filtered_data <- callModule(columnFilterSet, "filterset", df = selected_data, maxcol = 4)
output$table <- DT::renderDataTable({ filtered_data() })
}
shinyApp(ui, server)
@berndtlindner
Copy link

Very useful this. Thanks Joe.

@pyite
Copy link

pyite commented Dec 14, 2017

Thank you! This is a great template to use for interactive Shiny programming. A quick HOWTO guide for installing and trying it out on CentOS:

Download and install shiny-server package from rstudio.com (in my case this was shiny-server-1.5.4.841-rh6-x86_64.rpm)
Make a directory in the sample-apps, e.g.: mkdir /opt/shiny-server/samples/sample-apps/filter/
Copy this file into that directory, but rename to "app.R"
Browse to http://yourserver.com:3838/sample-apps/
Click on "filter" and it should launch.

Note: I am a total n00b so let me know if there is a better way to deploy.

@stephLH
Copy link

stephLH commented Dec 19, 2018

Thanks @jcheng5, it was also very useful to me!

Right now, I try to convert this example for an async app, eg with :

selected_data <- reactive({
  future({
    get(input$dataset, "package:datasets")
  })
 })

I'm new to future/promises and I struggled testing things for the past few days.
Maybe you have already have something working ?

Thanks again !!

@mayankkoul
Copy link

I have date columns also in my data. Is there a way to additional type of input like dateinput in addition to select input and maintain the same functionality

@2238154
Copy link

2238154 commented Jun 16, 2022

Thank you @jcheng5, this is exactly what I have been looking for! I've implemented the code and the table filtering works perfectly.

I am also working on implementing a second output to visualize spatial data using renderLeaflet alongside the table on the same shiny app but I have not been successful.

I was wondering if you know how the code could be altered so that it may be applied to polygon geometry data?

# Create server
server <- function(input, output, session) {

selected_data <- reactive({dataframe})
          
filtered_data <- callModule(columnFilterSet, "filterset", df = selected_data, maxcol = 10)

# Create table output         
output$table <- DT::renderDT(DT::datatable(filtered_data(), class = 'cell-border stripe', rownames = FALSE))

# Create colour palette
library(viridis)
pal <- colorBin(palette = "viridis", domain = filtered_data()$Value, bins = c(0, 50, 100))

# Create map output    
output$map <- renderLeaflet({
                         addProviderTiles("CartoDB.Positron", options = providerTileOptions(noWrap = TRUE)) %>%
                         addPolygons(data = filtered_data(),
                                              weight = 1, 
                                              opacity = 0.5,
                                              fillOpacity = 0.5,
                                              smoothFactor = 1,
                                              fillColor = ~pal(filtered_data()$Value))
})
}

@jslul
Copy link

jslul commented Sep 29, 2022

Thank you for this example @jcheng5 !

One question, how could i return the values that are selected in the inputs (mpeg, cyl etc.) on the server side? Not as logical, but the value selected in selectInput ns "filter_value" (for example 13.3 in mpeg and 8 in cyl).

Appreciate it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment