Update other menus based on selection from another menu - r

I currently have a Shiny app with 3 menus (more to be added once the bugs are worked out).
I have found examples online of a top down menu filtering approach. Meaning the user must select from the first menu, then the second menu, and so on, but in order. If they select from the 2nd menu first then it does not filter the first menu, only the ones below it and obviously that is a problem.
I want my users to be able to jump around to the menus in any order and have them filter.
In my example there are 3 menus, and what I am trying to do is if observeEvent on any menu (user makes a selection from any menu) then:
Filter the data based on the selection made
updateSelectInput for any menus that have no input selected yet
This will ensure that the menus are up to date with what is actually in the data and ensures that the user doesn't slice down to something that does not actually exist in the data.
Also, note that step #2 is very important - only update menus with no selection made, I have had issues with this because if I just update all other menus then it clears the user selected input, which is still the wrong behavior.
I know what I need to do but I have not been able to pull it off yet, so the help is appreciated.
Update
I updated my code to work with the one answer posted below but it still does not quite work correctly.
Now it does filter down the menus, however, once the subset has been created, it does not allow for it to "filter" back up.
What I mean by this is that If I select the value 3 from the first menu TreeNumber then the last menu filters down to just the value 300 - that is good. BUT if I then go back to the first menu and also select the value 4, I expect that the Circumference menu will now show the values: 300 and 400, however, it still only shows the value 300.
Updated Code:
d <- data.frame("TreeNumber" = c(replicate(7, 1), replicate(7, 2),
replicate(7, 3), replicate(7, 4)),
"TreeAge" = c(1:28),
"Circumference" = c(replicate(7, 100), replicate(7, 200),
replicate(7, 300), replicate(7, 400)))
col_names <- names(d)
# TODO - change these to: "Tree Number", "Tree Age", "Circumference"
user_friendly_names <- c('TreeNumber', 'TreeAge', 'Circumference')
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
h3("Filters:"),
uiOutput("filters"),
# Plot button
fluidRow(column(2, align = "right",
actionButton("plot_graph_button", "Plot")))
),
mainPanel(tableOutput("summary"))
)
)
server <- function(input, output, session) {
#### Create the filter lists for UI ####
output$filters <- renderUI({
if(is.null(col_names)) return(NULL)
lapply(1:length(col_names), function(i) {
col <- paste0(col_names[i])
alias <- user_friendly_names[i]
# Populate input with unique values from column
pickerInput(inputId = alias, label = paste(alias,':'),
choices = unique(d[[col]]), multiple = T)
})
})
# lapply(X = vars, FUN = function(x) {
# vals <- sort(unique(data[[x]]))
# updatePickerInput(session = session, inputId = x, choices = vals)
# })
my_filter <- function(data, var) {
# TODO - Need to convert from user_friendly_names --> col_names in here
if (length(input[[var]]) == 0) return(data)
data %>% subset(data[[var]] %in% input[[var]])
}
subsettedData <- reactive({
d %>% my_filter("TreeNumber") %>% my_filter("TreeAge") %>%
my_filter("Circumference")
# TODO - get into for loop versus hard coding this step:
# for(z in 1:length(col_names)){
# d %>% my_filter(col_names[z])
# }
})
observeEvent(subsettedData(), {
lapply(col_names, function(var) {
selections <- unique(subsettedData()[[var]])
if (length(input[[var]]) == 0)
updatePickerInput(session = session, inputId = var, choices = selections)
})
})
observeEvent(input$plot_graph_button, {
for (j in seq_along(d)) {
updateSelectInput(session = session, inputId = user_friendly_names[j],
choices = c("All", unique(d[[j]])), selected = "All")
}
})
output$summary <- renderTable({
# Do not show a plot when the page first loads
# Wait until the user clicks "Plot" button
if (input$plot_graph_button == 0){
return()
}
# Update code below everytime the "Plot" button is clicked
input$plot_graph_button
isolate({
# Fresh copy of the full data set every time "Plot" button is clicked
d <- copy(Orange)
# Filter data based on UI
for(f in 1:length(col_names)){
if(eval(parse(text = paste0('is.null(input$',user_friendly_names[f],')')))){
# Default to "All" - do not filter
print("All")
}else{
d <- d[d[[col_names[f]]] ==
unlist(eval(parse(text =
paste0('input$',user_friendly_names[f])))), ]
}
}
final_summary_table <<- d
})
})
}
shinyApp(ui = ui, server = server)

Here is an app that applies filtering based on all inputs. I'm not sure how intuitive it is to give a selection called "all" in a selectInput with multiple = TRUE. Maybe It would be better to add a reset button for each selection instead.
I replaced the dataset Orange with tips to get more factor variables. Also, I didn't use data.table in the example since it seems irrelevant for your problem.
library(shiny)
library(dplyr)
data(tips, package = "reshape2")
filter_vars <- c("sex", "smoker", "day", "time")
ui <- fluidPage(
lapply(filter_vars, function(var) {
selectInput(var, var, unique(tips[[var]]), multiple = TRUE)
}),
tableOutput("table")
)
server <- function(input, output, session) {
my_filter <- function(data, var) {
if (length(input[[var]]) == 0) return(data)
data %>% subset(data[[var]] %in% input[[var]])
}
subsettedData <- reactive({
tips %>% my_filter("sex") %>% my_filter("smoker") %>%
my_filter("day") %>% my_filter("time")
})
observeEvent(subsettedData(), {
lapply(filter_vars, function(var) {
selections <- unique(subsettedData()[[var]])
if (length(input[[var]]) == 0)
updateSelectInput(session, var, choices = selections)
})
})
output$table <- renderTable({ subsettedData() })
}
shinyApp(ui, server)

Related

How to integrate Shiny updateSelectInput to update choices for specific cell InputIDs in editable data table

What I am trying to do?
I am building a Shiny app that imports data, runs some analysis, and allows the User to make selections regarding the analysis via drop downs in a data table. The initial choices available are specific to each row in the table based on values found in the data. I want the User to be able to augment the data so new values that weren’t found in the imported data are available as choices, too. It is this last part that is giving me trouble.
I’ve created an example based on mtcars to illustrate. The construct I have for creating an editable data table is based on ID's for each cell in a column as follows (thanks to some earlier help I had on Stack to figure it out). The snippet of code below is contained in an observeEvent when I load new data. [Note the full code is at the bottom]
selectInputIDmodel <<- paste0("sel_model", 1:nrow(v$cars()$cars_meta))
v$model_applied <- reactive({match_cars(v$cars())$model_applied})
v$initTbl <-
dplyr::tibble(
car = v$cars()$cars_meta$car,
make = v$cars()$cars_meta$make,
mpg = v$cars()$cars_meta$mpg,
model = sapply(selectInputIDmodel, function(x){as.character(selectInput(inputId = ns(x), label = "",
choices = v$model_applied()$model[v$model_applied()$car == v$cars()$cars_meta$car[which(selectInputIDmodel == x)]],
selected = v$cars()$cars_meta$model[which(selectInputIDmodel == x)]
))})
)
I've set up another observeEvent for when a new model is added. I expect I need to use updateSelectInput to update the choices under the model variable. I've tried this by recreating v$initTbl under this observeEvent, but haven't figured out how to work in the updateSelectInput instead of SelectInput. The former is calling for a "session" argument, so if I just substitute "updateSelectInput" I get an error saying that I cannot convert an environment to character. If I remove the "as.character" I get a "cannot unclass an environment" error.
Further Context
Below is further context for what I am trying to do followed by the code I have.
When running the app, the Load Data button imports the mtcars data and splits the car name into make and model fields. The model field in the display table is a drop down list and has as choices the various models that are found in the data for the specific make of car. The first one in each list is the default value. The User can select from the drop downs and use the Commit button to register the choices selected. The User can go back to make changes and Commit multiple times.
There are fields to allow the User to add a new model name for a particular make of car. Save Model should apply the new model entry as a drop down choice for the relevant make of car. This is what I haven’t been able to get working.
In order to be able to confirm the updates that were committed, once the User selects Commit the first time, I am showing the resultsTbl as verbatim output at the bottom of the page. The output refreshes every time the Commit button is clicked. It is the resultsTbl that I store and will use for onward processing in another module.
Here is a sequence of steps that should be able to be completed.
Step 1: Load Data
Step 2: Change the Model in the 2nd row from “RX4” to “RX4 Wag”
Step 3: Commit and see updates reflected in the resultsTbl
Step 4: Set Select Make to “Valiant”
Step 5: Set Add Model Name to “V”
Step 6: Save Model
Step 7: “V” should appear under “Valiant” as a selection in the drop down
Step 8: Commit and “V” should appear as the model for row 6 in resultsTbl
Step 9: Change the Model in the last row from “240D” to “280”
Step10: Commit and see update reflected in the resultsTbl
What have I tried?
The Load Data button triggers an observeEvent that does the following:
Sets up the data
Determines which models are available for which makes of car (for the drop downs)
Initiates the data table (initTbl)
I use a reactive (displayTbl) to capture the updates to feed the proxy table.
I then use a reactive (resultTbl) to store the captured values.
This all works fine.
I use Save Model as another observeEvent to update which models are available for which makes of car, to add new values to the drop downs where relevant.
I have not been able to figure how to make this work.
I believe I need some way to reinitialize the data table with the refreshed choices for the drop downs, whilst preserving any previously selected values. As noted above, I am unsure how to integrate updateSelectInput into the existing code.
Any help would be greatly appreciated.
Here is the current state of my code:
#********* LIBRARIES *************************************************
library(magrittr)
library(dplyr)
library(tidyselect)
library(shiny)
library(stringr)
library(purrr)
library(shinyjs)
library(zeallot)
library(DT)
#******** FUNCTIONS ***************************************************
# Creates the new data set / cars object
create_data2 <- function(){
#simulate data import
cars_df <- head(mtcars, 10)
#simulate creating meta table
cars_meta <- dplyr::tibble(car = rownames(cars_df), make = sub("([A-Za-z]+).*", "\\1", rownames(cars_df)), cars_df)
cars_meta$model <- NA
#simulate creating cars_list
names <- rownames(cars_df)
`%<-%` <- zeallot::`%<-%`
car <- list()
car[c("head", "m1", "m2")] %<-% data.frame(stringr::str_split(names, " ", simplify = TRUE))
car$m <- paste(car$m1, car$m2)
cars_list <- list()
for(h in car$head){
cars_list[[h]] <- list(car$m[car$head==h])
}
#simulate creating the cars_object
cars_object <- list()
cars_object$cars_df <- cars_df
cars_object$cars_meta <- cars_meta
cars_object$cars_list <- cars_list
return(cars_object)
}
# Updates the cars object with resultTbl
meta_table <- function(object, table){
tbl <- table
object$cars_meta <- tbl
return(object)
}
# Matches the models and makes of the cars
match_cars <- function(cars_object){
cv <- cars_object$cars_meta
car_match <- list()
for (car in cv$car){
x <- 1
for (model in cars_object$cars_list[[cv$make[cv$car == car]]][[1]]){
car_match[[paste0(car,"#",x)]][["model"]] <- model
x <- x + 1
}
}
model_applied <-
if(nrow(dplyr::bind_rows(car_match)) >0) {
dplyr::bind_rows(car_match) %>%
mutate(car = stringr::str_replace_all(names(car_match),"#\\d",""))
} else {
data.frame(car = "", drop = FALSE)
}
model_reduced <- model_applied %>%
dplyr::group_by(car) %>%
dplyr::slice(1) %>%
dplyr::ungroup()
cv <- cv %>%
select(-model) %>%
left_join(model_reduced, by = "car") %>%
select(car, make, mpg, model)
cars_object$cars_meta <- cv
cars_object$model_applied <- model_applied
return(cars_object)
}
# Adds a new make/model combination to cars_list of the cars object
new_model <- function(cars_object, make, new){
cars_object$cars_list[[make]] <- c(new, cars_object$cars_list[[make]][[1]])
return(cars_object)
}
#******** UI ********************************************************
mod_data_ui <- function(id) {
fluidPage(
actionButton(NS(id,"new_data"), "Load Data"),
br(),
DT::dataTableOutput(NS(id, 'dt')),
br(),
actionButton(NS(id, "commit_meta"), "Commit"),
br(),
verbatimTextOutput(NS(id,"results")),
br(),
uiOutput(NS(id,"make_set")),
br(),
uiOutput(NS(id, "model_value")),
br(),
uiOutput(NS(id, "save_model")),
br(),
verbatimTextOutput(NS(id,"meta"))
)
}
shiny_ui <- function() {
navbarPage(
title = div(span("Data",
style = "position: relative; top: 50%; transform: translateY(-50%);")),
tabPanel(
"Data Management",
mod_data_ui("data")
)
)
}
#**** SERVER ***********************************************************
mod_data_server <- function(id) {
shiny::moduleServer(id, function(input, output,session){
ns <- session$ns
v <- reactiveValues()
#place holders
selectInputIDmodel <- "model"
observeEvent(input$new_data, once = TRUE, {
data <- create_data2()
v$cars <- reactive({data})
selectInputIDmodel <<- paste0("sel_model", 1:nrow(v$cars()$cars_meta))
v$model_applied <- reactive({match_cars(v$cars())$model_applied})
v$initTbl <-
dplyr::tibble(
car = v$cars()$cars_meta$car,
make = v$cars()$cars_meta$make,
mpg = v$cars()$cars_meta$mpg,
model = sapply(selectInputIDmodel, function(x){as.character(selectInput(inputId = ns(x), label = "",
choices = v$model_applied()$model[v$model_applied()$car == v$cars()$cars_meta$car[which(selectInputIDmodel == x)]],
selected = v$cars()$cars_meta$model[which(selectInputIDmodel == x)]
))})
)
})
displayTbl <- reactive({
req(input$new_data)
dplyr::tibble(
car = v$cars()$cars_meta$car,
make = v$cars()$cars_meta$make,
mpg = v$cars()$cars_meta$mpg,
model = sapply(selectInputIDmodel, function(x){as.character(selectInput(inputId = ns(x), label = "",
choices = v$model_applied()$model[v$model_applied()$car == v$cars()$cars_meta$car[which(selectInputIDmodel == x)]],
selected = input[[x]]))})
)
})
output$dt <- DT::renderDataTable({
req(input$new_data)
DT::datatable(
v$initTbl, escape = FALSE, selection = 'none', rownames = FALSE,
options = list(paging = FALSE, ordering = FALSE, scrollx = TRUE, dom = "t",
preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')
)
)
})
dt_table_proxy <- DT::dataTableProxy(outputId = "dt")
observeEvent({sapply(selectInputIDmodel, function(x){input[[x]]})}, {
DT::replaceData(proxy = dt_table_proxy, data = displayTbl(), rownames = FALSE)
}, ignoreInit = TRUE)
v$resultTbl <- reactive({
dplyr::tibble(
car = v$cars()$cars_meta$car,
make = v$cars()$cars_meta$make,
mpg = v$cars()$cars_meta$mpg,
model = sapply(selectInputIDmodel, function(x){as.character(input[[x]])})
)
})
observeEvent(input$commit_meta, {
cars_updated <- meta_table(v$cars(), v$resultTbl())
v$cars <- reactive({cars_updated})
})
# add model manually
output$make_set <- renderUI({
req(input$new_data)
make <- v$cars()$cars_meta$make
#make_sel <- unique(make)
selectInput(NS(id, "make_set"), "Select Make", multiple = FALSE, choices = make)
})
output$model_value <- renderUI({
req(input$new_data)
textInput(NS(id, "model_value"), "Add Model Name")
})
output$save_model <- renderUI({
req(input$new_data)
actionButton(NS(id, "save_model"), "Save Model", style="color: #fff; background-color: #337ab7; border-color: #2e6da4")
})
observeEvent(input$save_model,{
car <- meta_table(v$cars(), v$resultTbl()) # This is the same step as under commit
v$cars <- reactive({match_cars(
new_model(
cars_object = car,
make = input$make_set,
new = input$model_value
)
)
})
v$model_applied <- reactive({match_cars(v$cars())$model_applied})
updateTextInput(session, "model_value", value = "")
})
output$meta <- renderPrint({
req (input$commit_meta > 0)
tf <- v$cars()$cars_meta
tf %>% print(n = Inf)
})
return(reactive(v))
})
}
shiny_server <- function(input, output, session) {
v <- mod_data_server("data")
}
#********* APP *******************************
svyStudyapp_app <- function(...) {
app <- shiny::shinyApp(
ui = shiny_ui,
server = shiny_server
)
shiny::runApp(app, ...)
}
use updateSelectInput inside an observeEvent or observe function. Pass in the Shiny session object, the input ID of the selectInput element and a vector of new choices.
like this
observeEvent(input$saveModelButton, {
updateSelectInput(session, "sel_model6", choices = c("V", "Other models"))
})

How to create dynamic number of observeEvent in another observeEvent?

Here I asked an similar question and got a working answer. But the solution does not work if 'actionButton' of sub segment is replace by 'selectInput'. On each selection of selectInput creates two outputs. Please help.. Thanks....
library(shiny)
ui <- fluidPage(
verbatimTextOutput("txt",placeholder = T), #"It is Created for Testing"
actionButton("addSeg", "Add a Segment"),
uiOutput("myUI")
)
server <- function(input, output, session) {
alld <- reactiveValues()
alld$ui <- list()
# Action to add new Segment
observeEvent(input$addSeg,{
new_id <- length(alld$ui) + 1
sub_name <- paste0("addSub_", new_id)
alld$ui[[new_id]] <- list(selectInput(sub_name,"Add a variable", choices = c("V1","V2"), selected = NULL))
observeEvent(input[[sub_name]], {
new_text_id <- length(alld$ui[[new_id]]) + 1
alld$ui[[new_id]][[new_text_id]] <- HTML(paste0("Variable ",input[[sub_name]]," added<br>"))
}, ignoreInit = TRUE)
})
output$myUI <- renderUI({alld$ui})
output$txt <- renderText({class(alld$ui)})
}
shinyApp(ui, server)
This behaviour occurs because the custom UI element is re-rendered every time a new element is added to the list. Once you click "V2" and the new text element is added, the selectInput itself re-renders and resets to V1, which is noticed by the observer you've created.
The following might be a solution for you:
observeEvent(input$addSeg,{
new_id <- length(alld$ui) + 1
sub_name <- paste0("addSub_", new_id)
alld$ui[[new_id]] <- list(
selectInput(sub_name,
"Add a variable",
choices = c("", "V1","V2"),
selected = "")
)
observeEvent(input[[sub_name]], {
if (input[[sub_name]] == "") return()
new_text_id <- length(alld$ui[[new_id]]) + 1
alld$ui[[new_id]][[new_text_id]] <- HTML(paste0("Variable ",input[[sub_name]]," added<br>"))
}, ignoreInit = TRUE)
})
What I've done here is add an empty option to your selectInputs, and a condition to the corresponding observer that it shouldn't do anything if the input is empty. This way, I'm harnessing the "resetting" behaviour to be useful instead of annoying.

selectizeInput filter all other menus based on the selection from another menu (every time a selection is made)

I have data that looks something like the data set Orange where there are columns that might contain duplicate values, however, each row is unique.
My code:
library(shiny)
library(DT)
library(data.table)
d <- copy(Orange)
col_names <- names(Orange)
user_friendly_names <- c('TreeNumber', 'TreeAge', 'Circumference')
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
h3("Filters:"),
uiOutput("filters"),
# Plot button
fluidRow(column(2, align = "right",
actionButton("plot_graph_button", "Plot")))
),
mainPanel(tableOutput("summary"))
)
)
server <- function(input, output) {
#### Create the filter lists for UI ####
output$filters <- renderUI({
if(is.null(col_names)) return(NULL)
lapply(1:length(col_names), function(i) {
col <- paste0(col_names[i])
alias <- user_friendly_names[i]
# Populate input with unique values from column
selectizeInput(inputId = alias, label = paste(alias,':'),
choices = c('All', unique(d[[col]])), selected = 'All', multiple = T)
})
})
output$summary <- renderTable({
# Do not show a plot when the page first loads
# Wait until the user clicks "Plot" button
if (input$plot_graph_button == 0){
return()
}
# Update code below everytime the "Plot" button is clicked
input$plot_graph_button
isolate({
# Fresh copy of the full data set every time "Plot" button is clicked
d <- copy(Orange)
# Filter data based on UI
for(f in 1:length(col_names)){
print(paste("This is loop # ", f))
if(eval(parse(text = paste0('is.null(input$',user_friendly_names[f],')')))){
# If the user deleted "All" but failed to pick anything else default to "All" - do not filter
break
}else{
if(eval(parse(text = paste0('input$',user_friendly_names[f]))) != "All"){
print("FALSE -- Input is not == ALL")
d <- d[d[[col_names[f]]] == unlist(eval(parse(text = paste0('input$',user_friendly_names[f])))), ]
}else{
print("TRUE -- Input is defaulted to ALL")
}
}
}
final_summary_table <<- d
})
})
}
shinyApp(ui = ui, server = server)
My issue is that these lists are able to select multiple inputs (which I want), however, I want to initially show all available choices in all menus (which it currently does) but what I need to change is I need to have it start filtering the other lists as soon as a selection is made (no matter which list the user goes to first) based on that unique rowed data set provided.
So, if the user goes to the 2nd list and chooses tree age of 1004 then the TreeNumber menu should change to c(1, 2, 3, 4, 5) - no change in this scenario but the Circumference menu should change to c(115, 156, 108, 167, 125), then if they pick a TreeAge now the menus get filtered down by both TreeAge and TreeNumber and so on.
Right now the way the code works is it doesn't filter anything until you click "Plot", so the user might think a search will yield a bunch of results, when in reality the combination may not exist.
Here is a good example of a search that you may expect to yield a lot of results, yet it only yields 1 row:
Please note: If you do not delete 'All' it will return 'All' even if you selected other options, it is a flaw in the code that I plan to address separately along with some other minor tweaks.
I also wanted to mention that I found this post Filter one selectInput based on selection from another selectInput? which is similar to mine, however, they are dealing with menus in a top-down approach and mine is going to be more flexible about which menu the user goes to first (also mine allows multiple selections).
server <- function(input, output, session) {
output$filters <- renderUI({
# ...
})
lapply(seq_along(d), function(i) {
observeEvent(input[[user_friendly_names[i]]], {
for (j in seq_along(d)[-i]) {
choices <- if ("All" %in% input[[user_friendly_names[i]]])
unique(d[[j]]) else
unique(d[[j]][d[[i]] %in% input[[user_friendly_names[i]]]])
choices <- c("All", choices)
selected <- intersect(choices, input[[user_friendly_names[j]]])
updateSelectInput(session = session, inputId = user_friendly_names[j],
choices = choices, selected = selected)
}
})
})
observeEvent(input$plot_graph_button, {
for (j in seq_along(d)) {
updateSelectInput(session = session, inputId = user_friendly_names[j],
choices = c("All", unique(d[[j]])), selected = "All")
}
})
output$summary <- renderTable({
# ...
})
}

How to prevent inputs made with renderUI from resetting after they are hidden and displayed again?

A common scenario for many of my shiny apps is that there is a large list of potentially interesting filter variables (often 10 to 20), but I want to avoid confusing the user with too many input widgets.
Therefore, my strategy is usually as follows:
1. Users may select filter variables. 2. If at least one filter variable is selected, a renderUI is triggered, which contains one input widget per selected variable. 3. The filter criteria are applied to the data and some output is generated.
The problem is that any change in step one (by adding or deleting a filter variable) eliminates all previously made choices from step two. This means that all input widgets are unintentionally reset to their default values. This prevents a smooth user experience. Any idea how to improve on this?
Here you can see what happens:
And here is the code to reproduce this behaviour:
library("shiny")
library("dplyr")
library("nycflights13")
df <- flights
filtervarsChoices <- c("origin","carrier")
originChoices <- unique(df$origin)
carrierChoices <- unique(df$carrier)
ui <- fluidPage(
h3("1. Select Filter variables"),
selectInput("filterVars", "Filter variables", filtervarsChoices, multiple = TRUE),
uiOutput("filterConditions"),
h3("Result"),
tableOutput("average")
)
server <- function(input, output, session) {
output$filterConditions <- renderUI({
req(input$filterVars)
tagList(
h3("2. Select Filter values"),
if ("origin" %in% input$filterVars) {
selectInput("originFilter", "Origin", originChoices, multiple = TRUE)
},
if ("carrier" %in% input$filterVars) {
selectInput("carrierFilter", "Carrier", carrierChoices, multiple = TRUE)
}
)
})
output$average <- renderTable({
if ("origin" %in% input$filterVars) {
df <- df %>% filter(origin %in% input$originFilter)
}
if ("carrier" %in% input$filterVars) {
df <- df %>% filter(carrier %in% input$carrierFilter)
}
df %>%
summarise(
"Number of flights" = n(),
"Average delay" = mean(arr_delay, na.rm = TRUE)
)
})
}
shinyApp(ui = ui, server = server)
The problem is that you render the UI element every time it is selected, and thus its selected choices are reset. We can solve this by only rendering the elements a single time, and showing or hiding them when applicable. We can do this with the show and hide functions from the shinyjs package, and by wrapping div's around the selectInputs as we create them. So each filter x gets a corresponding input called xFilter and a div wrapped around it called div_x.
Below is a working example. I have tried to make the code as general as possible, so that you would only have to supply additional elements in filtervarsChoices and in choices_list to extend with additional filters. I also modified the table that is outputted to show that the filters are working correctly.
Note that in the example below, hidden filters are still applied to the resulting data.frame. In order to only apply visible filters, the for loop should run over input$filterVars as shown by Till n the comments below.
I hope this helps!
library("shiny")
library("dplyr")
library("nycflights13")
library(shinyjs)
df <- flights
filtervarsChoices <- c("origin","carrier")
originChoices <- unique(df$origin)
carrierChoices <- unique(df$carrier)
# Create a list with the choices for the selectInputs.
# So the selectInput for 'origin', will get the choices defined in originChoices.
choices_list <- list('origin' = originChoices,
'carrier' = carrierChoices)
ui <- fluidPage(
column(width=3,
h3("1. Select Filter variables"),
selectInput("filterVars", "Filter variables", filtervarsChoices, multiple = TRUE),
uiOutput("filterConditions"),
h3("Result"),
tableOutput("average"),
useShinyjs()
),
column(width=3,
h3("Applied filters"),
htmlOutput('appliedfilters')
)
)
server <- function(input, output, session) {
# Render all selectInput elements.
output$filterConditions <- renderUI({
lapply(filtervarsChoices, function(x){
shinyjs::hidden(div(id=paste0('div_',x),
selectInput(paste0(x,"Filter"), x, choices_list[[x]], multiple = TRUE)
))})
})
# Show all divs that are selected, hide all divs that are not selected.
observeEvent(input$filterVars, ignoreNULL = F,
{
to_hide = setdiff(filtervarsChoices,input$filterVars)
for(x in to_hide)
{
shinyjs::hide(paste0('div_',x))
}
to_show = input$filterVars
for(x in to_show)
{
shinyjs::show(paste0('div_',x))
}
})
output$appliedfilters <- renderText({
applied_filters <- c()
for(x in filtervarsChoices) # for(x in input$filterVars)
{
if(!is.null(input[[paste0(x,'Filter')]]))
{
applied_filters[length(applied_filters)+1] = paste0(x,': ', paste(input[[paste0(x,'Filter')]],collapse=", "))
}
}
paste(applied_filters,collapse='<br>')
})
output$average <- renderTable({
# For all variables, filter if the input is not NULL.
# In the current implementation, all filters are applied, even if they are hidden again by the user.
# To make sure only visible filters are applied, make the loop run over input$filterVars instead of filterVarsChoices
for(x in filtervarsChoices) # for(x in input$filterVars)
{
if(!is.null(input[[paste0(x,'Filter')]]))
{
df <- df %>% filter(get(x) %in% input[[paste0(x,'Filter')]])
}
}
unique(df[,c('origin','carrier')])
})
}
shinyApp(ui = ui, server = server)

Render values of inputs in shiny so that these selections are deletable

To filter a data.frame with lots of variables I created a selectizeInput which allows you to select one of the columns of the data. This then creates another selectizeInput for the selected variable which can be used for subsetting the data. The selected value of the second selectizeInput is rendered below.
This is what it looks like
I want to render the selected values of these inputs so that these can be deleted by the user by clicking the black cross. Also a selection of var2 should not be deleted when the Filter selectizeInput is changed to var1.
So it should look like this (assuming the user previously selected value z in var2 and then value a in var1.
Anyone knows a good solution in shiny?
This is the code:
library(shiny)
data <- data.frame(var1 = c("a", "b"), var2 = c("y", "z"))
ui <- fluidPage(
selectizeInput("filter", label = "Filter",
multiple = FALSE, choices = c("var1", "var2")),
uiOutput("filter_var"),
uiOutput("selected_filter_value")
)
server <- function(input, output) {
observeEvent(input$filter, {
# dynamically generate selectizeInput for filter
output$filter_var <- renderUI({
selectizeInput(input$filter, label = input$filter,
choices = data[input$filter], multiple = TRUE)
})
})
# show selected filter values
# selected filter values should stay when choosing new input filter variable
# these should be deletable
observeEvent(input[[input$filter]], {
output$selected_filter_value <- renderUI({
textOutput("text_out")
})
output$text_out <- renderText({
paste0(input$filter, ": ", input[[input$filter]])
})
})
}
shinyApp(ui, server)
Well, I had to rearrange quite a lot and this whole problem is more about finding the right implementation for your case.
You can probably deduct most of it just looking at the code at the end of this post.
Main things explained: You didn't actually say what deleting means to you. So I just assumed you wanted the cells to not appear in the select boxes anymore. For that, I excluded NAs and replaced cells with an NA to show that they are deleted.
I rearranged the select values, such that we actually can delete certain cells, giving row and column names instead of just their values.
And most important, the buttons you wanted to create are dynamic UI elements with dynamic observers, which are then addressed to delete the certain cell.
Note: This solution is not optimal, since I specifically aimed to stay just on the R side of shiny. You can achieve a much more elegant and resource saving solution if you use JavaScript and shiny's custom messages.
Also: I did not address your request to let the selected values visible if the first select box changes. But this is a rather small issue, if you reconsider your setup. And I didn't want to diverge too much from you original code to not be confusing.
Code now:
library(shiny)
data <- data.frame(var1 = c("a", "b"), var2 = c("y", "z"))
ui <- fluidPage(
selectizeInput("filter", label = "Filter",
multiple = FALSE, choices = c("var1", "var2")),
uiOutput("filter_var"),
uiOutput("selected_filter_value")
)
server <- function(input, output) {
# Pulled out from original observeEvent
makeSecondInput <- function() {
output$filter_var <- renderUI({
# Names are not enough when wanting to delete data.frame rows (because of duplicates).
# So we instead use row numbers and set the actual values as labels.
choiceData <- na.exclude(data[input$filter])
choices <- rownames(choiceData)
names(choices) <- choiceData[, input$filter]
selectizeInput(input$filter, label = input$filter, selected = input[[input$filter]],
choices = choices, multiple = TRUE)
})
}
observeEvent(input$filter, {
makeSecondInput()
})
# Install a manual trigger to redraw input field, when an option is killed.
trigger <- reactiveVal()
observeEvent(trigger(), ignoreNULL = TRUE, {
makeSecondInput()
})
# Keep track of created observers, so dynamic creation does not wildly stack them up.
observersCreated <- character()
makeButtonObserver <- function(buttonname, colname, rowname) {
# For each delete-button created, install observer to delete data.frame cell.
observeEvent(input[[buttonname]], {
data[rowname, colname] <<- NA
# Force re-evaluation of observer above.
trigger(runif(1))
})
# Track that this button is equipped. (And re-creation of the same button does not add another obs.)
# Note: Observers DON'T get automagically removed after actionButton is no longer in the UI.
observersCreated <<- c(observersCreated, buttonname)
}
observeEvent(input[[input$filter]], {
output$selected_filter_value <- renderUI({
# Could be a list, so splitting that up.
lapply(input[[input$filter]], function(v) {
buttonname <- paste("kill", input$filter, v, sep = "_")
if (!(buttonname %in% observersCreated)) {
makeButtonObserver(buttonname, input$filter, v)
}
span(
paste0(input$filter, ": ", data[v, input$filter]),
actionButton(buttonname, "x")
)
})
})
})
}
shinyApp(ui, server)
This is what I currently have. There are still some issues which I couldn't solve.
Problems:
if I make some selections in input1, then switch from input1 to input2 and unclick one of the selections from input1 and then switch back to input1 these changes will be unmade
the checkboxes are rerendered when a new one is added and in this process sorted which changes the order
Code:
library(shiny)
library(shinyWidgets)
data <- data.frame(var1 = c("a", "b"), var2 = c("y", "z"))
ui <- fluidPage(
selectizeInput("filter", label = "Filter",
multiple = FALSE, choices = c("var1", "var2")),
uiOutput("filter_var"),
uiOutput("selected_filter_value")
)
server <- function(input, output, session) {
values <- reactiveValues(
filter_vals = list(var1 = list(), var2 = list()),
observers = NULL
)
# dynamically generate selectizeInput for variable selected in filter
# set selected values to previous selections
observeEvent(input$filter, {
output$filter_var <- renderUI({
selectInput(input$filter, label = input$filter,
selected = values$filter_vals[[input$filter]],
choices = data[input$filter], multiple = TRUE, selectize = TRUE)
})
})
# store selected values in list
observeEvent(input[[input$filter]], {
values$filter_vals[[input$filter]] <- input[[input$filter]]
})
# we need this because observeEvent is not triggered if input is empty after deleting all selections
observe({
if (is.null(input[[input$filter]])) {
values$filter_vals[[input$filter]] <- list()
}
})
# add an observer for newly created checkbox
# if checkbox is clicked delete entry in list
# keep a list of all existing observers
make_delete_observer <- function(name) {
observeEvent(input[[name]], {
req(input[[name]] == FALSE)
var <- stringr::str_split(name, "_")[[1]][1]
val <- as.integer(stringr::str_split(name, "_")[[1]][2])
values$filter_vals[[var]] <- intersect(values$filter_vals[[var]][-val],
values$filter_vals[[var]])
updateSelectInput(session, var, selected = values$filter_vals[[var]])
})
}
# render selected values which are stored in a list as checkboxes
# add an observeEvent for each checkbox
# store selected values in list
output$selected_filter_value <- renderUI({
req(values$filter_vals[[input$filter]])
req(any(sapply(values$filter_vals, length) > 0))
tag_list <- tagList()
for (i in seq_along(values$filter_vals)) {
for (j in seq_along(values$filter_vals[[i]])) {
new_input_name <- paste0(names(values$filter_vals)[i], "_", j)
new_input <- prettyCheckbox(
inputId = new_input_name, value = TRUE,
label = paste0(names(values$filter_vals)[i], ": ", values$filter_vals[[i]][j]),
icon = icon("close"), status = "danger", outline = FALSE, plain = TRUE
)
# create observer only if it does not exist yet
if (!(new_input_name %in% values$observers)) {
values$observers <- append(values$observers, new_input_name)
make_delete_observer(new_input_name)
}
tag_list <- tagAppendChild(tag_list, new_input)
}
}
tag_list
})
}
shinyApp(ui, server)

Resources