I'm working on a shiny app and I'm running into difficulty with observeEvent() function when creating a complex expression of multiple inputs that all derive from selectInput().
My issue is some of the expressions within the observeEvent() function are triggered at startup, causing the event to prematurely execute (i.e. my actionButton() is disabled at startup, as it should be, but becomes enabled when at least one of the inputs are selected when ideally I would want it to become enabled only when ALL inputs are selected). As seen below:
observeEvent({
#input$cohort_file
input$cohort_IDvar
input$cohort_index_date
input$cohort_EOF_date
input$cohort_EOF_type
input$cohort_Y_name
input$cohort_L0
}, {
enable("set_cohort_button")
})
For reference, I'm using the shinyjs package by #daattali found on github to enable/disable actionButton().
All but the last input (i.e. input$cohort_L0) appear to be initialized at startup so observeEvent() enables actionButton only when input$cohort_L0 is selected. If you run my app and select input in sequential order from top to bottom, it appears that observeEvent() is working as intended. I only discovered that it wasn't working as intended when I decided to choose inputs at random and discovered that selecting input$cohort_L0 was the only input I needed to select to enable actionButton().
The UI portion of the code looks like this:
# Variable selection
selectInput('cohort_IDvar', 'ID', choices = ''),
selectInput('cohort_index_date', 'Index date', choices = ''),
selectInput('cohort_EOF_date', 'End of follow-up date', choices = ''),
selectInput('cohort_EOF_type', 'End of follow-up reason', choices = ''),
selectInput('cohort_Y_name', 'Outcome', choices = ''),
selectInput('cohort_L0', 'Baseline covariate measurements', choices = '', multiple=TRUE, selectize=TRUE),
And I'm using observe() to collect the column names of an upload data-set to direct them to selectInput() as follows:
### Collecting column names of dataset and making them selectable input
observe({
value <- c("",names(cohort_data()))
updateSelectInput(session,"cohort_IDvar",choices = value)
updateSelectInput(session,"cohort_index_date",choices = value)
updateSelectInput(session,"cohort_EOF_date",choices = value)
updateSelectInput(session,"cohort_EOF_type",choices = value)
updateSelectInput(session,"cohort_L0",choices = value)
})
I've looked into using the argument ignoreInit = TRUE but it does nothing for my case of having multiple expressions within observeEvent(). I've also looked into forcing no default selection in selectInput() but had no luck with that.
So my two-part question is how can I execute observEvent() when only ALL inputs are selected/how do I stop from the inputs from being initialized at startup?
My entire code:
library(shiny)
library(shinyjs)
ui <- fluidPage(
useShinyjs(),
navbarPage("Test",
tabPanel("Cohort",
sidebarLayout(
sidebarPanel(
fileInput("cohort_file", "Choose CSV File",
multiple = FALSE,
accept = c("text/csv",
"text/comma-separated-values,text/plain",
".csv")),
# Horizontal line ----
tags$hr(),
# Variable selection
selectInput('cohort_IDvar', 'ID', choices = ''),
selectInput('cohort_index_date', 'Index date', choices = ''),
selectInput('cohort_EOF_date', 'End of follow-up date', choices = ''),
selectInput('cohort_EOF_type', 'End of follow-up reason', choices = ''),
selectInput('cohort_Y_name', 'Outcome', choices = ''),
selectInput('cohort_L0', 'Baseline covariate measurements', choices = '', multiple=TRUE, selectize=TRUE),
# Horizontal line ----
tags$hr(),
disabled(
actionButton("set_cohort_button","Set cohort")
)
#actionButton("refresh_cohort_button","Refresh")
),
mainPanel(
DT::dataTableOutput("cohort_table"),
tags$div(id = 'cohort_r_template')
)
)
)
)
)
server <- function(input, output, session) {
################################################
################# Cohort code
################################################
cohort_data <- reactive({
inFile_cohort <- input$cohort_file
if (is.null(inFile_cohort))
return(NULL)
df <- read.csv(inFile_cohort$datapath,
sep = ',')
return(df)
})
rv <- reactiveValues(cohort.data = NULL)
rv <- reactiveValues(cohort.id = NULL)
rv <- reactiveValues(cohort.index.date = NULL)
rv <- reactiveValues(cohort.eof.date = NULL)
rv <- reactiveValues(cohort.eof.type = NULL)
### Creating a reactiveValue of the loaded dataset
observeEvent(input$cohort_file, rv$cohort.data <- cohort_data())
### Displaying loaded dataset in UI
output$cohort_table <- DT::renderDataTable({
df <- cohort_data()
DT::datatable(df,options=list(scrollX=TRUE, scrollCollapse=TRUE))
})
### Collecting column names of dataset and making them selectable input
observe({
value <- c("",names(cohort_data()))
updateSelectInput(session,"cohort_IDvar",choices = value)
updateSelectInput(session,"cohort_index_date",choices = value)
updateSelectInput(session,"cohort_EOF_date",choices = value)
updateSelectInput(session,"cohort_EOF_type",choices = value)
updateSelectInput(session,"cohort_L0",choices = value)
})
### Creating selectable input for Outcome based on End of Follow-Up unique values
observeEvent(input$cohort_EOF_type,{
updateSelectInput(session,"cohort_Y_name",choices = unique(cohort_data()[,input$cohort_EOF_type]))
})
### Series of observeEvents for creating vector reactiveValues of selected column
observeEvent(input$cohort_IDvar, {
rv$cohort.id <- cohort_data()[,input$cohort_IDvar]
})
observeEvent(input$cohort_index_date, {
rv$cohort.index.date <- cohort_data()[,input$cohort_index_date]
})
observeEvent(input$cohort_EOF_date, {
rv$cohort.eof.date <- cohort_data()[,input$cohort_EOF_date]
})
observeEvent(input$cohort_EOF_type, {
rv$cohort.eof.type <- cohort_data()[,input$cohort_EOF_type]
})
### ATTENTION: Following eventReactive not needed for example so commenting out
### Setting id and eof.type as characters and index.date and eof.date as Dates
#cohort_data_final <- eventReactive(input$set_cohort_button,{
# rv$cohort.data[,input$cohort_IDvar] <- as.character(rv$cohort.id)
# rv$cohort.data[,input$cohort_index_date] <- as.Date(rv$cohort.index.date)
# rv$cohort.data[,input$cohort_EOF_date] <- as.Date(rv$cohort.eof.date)
# rv$cohort.data[,input$cohort_EOF_type] <- as.character(rv$cohort.eof.type)
# return(rv$cohort.data)
#})
### Applying desired R function
#set_cohort <- eventReactive(input$set_cohort_button,{
#function::setCohort(data.table::as.data.table(cohort_data_final()), input$cohort_IDvar, input$cohort_index_date, input$cohort_EOF_date, input$cohort_EOF_type, input$cohort_Y_name, input$cohort_L0)
#})
### R code template of function
cohort_code <- eventReactive(input$set_cohort_button,{
paste0("cohort <- setCohort(data = as.data.table(",input$cohort_file$name,"), IDvar = ",input$cohort_IDvar,", index_date = ",input$cohort_index_date,", EOF_date = ",input$cohort_EOF_date,", EOF_type = ",input$cohort_EOF_type,", Y_name = ",input$cohort_Y_name,", L0 = c(",paste0(input$cohort_L0,collapse=","),"))")
})
### R code template output fo UI
output$cohort_code <- renderText({
paste0("cohort <- setCohort(data = as.data.table(",input$cohort_file$name,"), IDvar = ",input$cohort_IDvar,", index_date = ",input$cohort_index_date,", EOF_date = ",input$cohort_EOF_date,", EOF_type = ",input$cohort_EOF_type,", Y_name = ",input$cohort_Y_name,", L0 = c(",paste0(input$cohort_L0,collapse=","),"))")
})
### Disables cohort button when "Set cohort" button is clicked
observeEvent(input$set_cohort_button, {
disable("set_cohort_button")
})
### Disables cohort button if different dataset is loaded
observeEvent(input$cohort_file, {
disable("set_cohort_button")
})
### This is where I run into trouble
observeEvent({
#input$cohort_file
input$cohort_IDvar
input$cohort_index_date
input$cohort_EOF_date
input$cohort_EOF_type
input$cohort_Y_name
input$cohort_L0
}, {
enable("set_cohort_button")
})
### Inserts heading and R template code in UI when "Set cohort" button is clicked
observeEvent(input$set_cohort_button, {
insertUI(
selector = '#cohort_r_template',
ui = tags$div(id = "cohort_insertUI",
h3("R Template Code"),
verbatimTextOutput("cohort_code"))
)
})
### Removes heading and R template code in UI when new file is uploaded or when input is changed
observeEvent({
input$cohort_file
input$cohort_IDvar
input$cohort_index_date
input$cohort_EOF_date
input$cohort_EOF_type
input$cohort_Y_name
input$cohort_L0
}, {
removeUI(
selector = '#cohort_insertUI'
)
})
}
# Run the application
shinyApp(ui = ui, server = server)
The code chunk that you're passing to the observeEvent as the trigger event is
{
input$cohort_IDvar
input$cohort_index_date
input$cohort_EOF_date
input$cohort_EOF_type
input$cohort_Y_name
input$cohort_L0
}
This means that, just like any other reactive code block, when ANY of these values changes, that reactive block is considered invalidated and therefore the observer will trigger. So the behaviour you're seeing makes sense.
It sounds like what you want is to execute only when all values are set. That sounds like a great use of the req() function! Try something like this:
observe({
req(input$cohort_IDvar, input$cohort_index_date, input$cohort_EOF_date, ...)
enable("set_cohort_button")
})
Note that for shinyjs::enable() specifically, you can instead use the shinyjs::toggleState() function. I think in this case the req() function is the better option though.
Related
I'm working on a shiny app and trying to use renderDataTable and renderUI on the same page. The following codes work fine. But when I switch the order of 2. render select input and 3. render data table, it seems input$selected_variable cannot reflect what is selected on the shiny app. Neither the print(input$selected_variable) can print the selected value in the terminal, nor the textOutput can show the selected value on the app interface. Does anyone know why this may happen? Another follow-up question is that if I remove filter = 'top' in the renderDataTable, this issue won't occur either no matter which order is applied. Does this problem has anything to do with the filter = 'top'? (This filter = 'top' option in dataTable seemed also cause a few other weird behaviors when I tried out some other shiny app designs.)
if (interactive()) {
library(shiny)
library(DT)
shinyApp(
### ui
ui = fluidPage(
fileInput('input_data_file',
label = "Load input data file(s)",
multiple = TRUE,
buttonLabel = "Browse or Drop...",
placeholder = "(multiple) csv file(s)"),
dataTableOutput('input_table'),
uiOutput('params'),
verbatimTextOutput("text", placeholder = T),
plotlyOutput('plot')
),
### server
server = function(input, output) {
### values placeholder
values <- reactiveValues()
values$data <- NULL
### 1. upload data file
observeEvent(input$input_data_file, {
req(input$input_data_file)
file <- input$input_data_file
ext <- tools::file_ext(file$datapath)
req(file)
validate(need(ext == "csv", "Please upload a csv file"))
values$data <- read.csv(file$datapath, stringsAsFactors = FALSE)
})
### 2. render select input
output$params <- renderUI({
fluidRow(column(6, selectInput("selected_variable", "Variable:",
choices = setdiff(names(values$data), 'CustomerID')
)))
})
### 3. render data table
output$input_table <- DT::renderDataTable(
values$data,
filter = 'top', # removing this line can also solve the issue
options = list(scrollX=TRUE, scrollCollapse=TRUE, stateSave = TRUE, search = list(regex = TRUE)),
server = FALSE)
### 4. render text output
observe({
#req(input$selected_variable)
print(input$selected_variable)
output$text <-renderText({input$selected_variable})
})
}
)
}
I want a checkbox that selects all the rows displayed (displayed is key as this differs between the filters you have applied and the entire data table) in a standard DT::renderDataTable in Shiny.
Is there any DT extension that already does this? My coding skills are basic so I cannot write an equivalent Java or HTML code.
This is my app so far, any csv file is compatible for the select all purpose. At the moment there is a clunky way of creating another table of all the selected rows (manually selected one by one) - this is difficult when you want to select 30 animals all with the same characteristic.
library(shiny)
library(shinyjs)
library(DT)
library(dplyr)
library(data.table)
ui = pageWithSidebar(
headerPanel(""),
#This is where the full animal information file is input, as a ".txt" file.
sidebarPanel(
fileInput("ani", "Upload Animal Information File", accept = ".csv"),
br(),
numericInput("groups","Number of Ewe Groups", value = 1 ),
#This is a list of the table headers. These headers can be indivdually selected to be part of the concatenated "Unique ID" single column.
uiOutput("choose_columns"),
width = 2),
mainPanel(
DT::dataTableOutput("ani1"),
DT::dataTableOutput("selectedEwes")
))
server = function(input, output, session) {
animalinformation <- reactive({
file1 <- input$ani
if (is.null(file1))
return(NULL)
#This removes the Ewes and Status non-zero Rams from the displayed data, so that only live/at hand Rams are shown for selection.
isolate({
anifile <- read.csv(file1$datapath, header = TRUE)
anifile <- as.data.frame(anifile)
})
anifile
})
output$choose_columns <- renderUI({
if (is.null(animalinformation()))
return()
colnames <- names(animalinformation())
# Create the checkboxes and select them all by default
checkboxGroupInput("columns", "Choose Columns",
choices = colnames,
selected = colnames)
})
#This line is repsonsible for creating the table for display.
output$ani1 = DT::renderDataTable({
if (is.null(animalinformation()))
return()
if (is.null(input$columns) || !(input$columns %in% names(animalinformation()))) { return() }
{ datatable(animalinformation()[, input$columns, drop = F], filter = "top") }
})
ani1_selected <- reactive({
ids <- input$ani1_rows_selected
animalinformation()[ids,]
})
#This displays the table of selected rows from the table of Rams. This table can be downloaded or printed, or copied using the buttons that appear above the table, thanks to the 'Buttons' extension.
output$selectedEwes <- DT::renderDataTable({
datatable(
ani1_selected(),
selection = list(mode = "none"),
caption = "Copy to clipboard, download a .csv or print the following table of selected Ewes, using the above buttons.", extensions = 'Buttons', options = list(dom = 'Bfrtip', buttons = c('copy', 'csv', 'excel', 'pdf', 'print'))
)
})
}
shinyApp(ui = ui, server = server)
Any help would be much appreciated thanks.
Here is the simplest implementation I can think of. It takes advantage of the fact that DT will return the filter row indexes back to R, which is input$dt_rows_all in the below example. Moreover, it uses the DT::dataTableProxy() to control the row selection. Finally, it works in both the client mode and the server-side processing mode.
By the way, I want to mention that using javascript to mimic the selecting / deselecting events in DT won't change the related shiny binding values in R (e.g., input$dt_rows_selected). It's because DT has its own implementation of row selections (may change in the future but not yet at the time of writing). See rstudio/DT#366 if you want to know more.
library(shiny)
ui <- tagList(
DT::DTOutput("dt"),
checkboxInput("dt_sel", "sel/desel all"),
h4("selected_rows:"),
verbatimTextOutput("selected_rows", TRUE)
)
server <- function(input, output, session) {
dat <- reactive({iris})
output$dt <- DT::renderDT(dat(), server = TRUE)
dt_proxy <- DT::dataTableProxy("dt")
observeEvent(input$dt_sel, {
if (isTRUE(input$dt_sel)) {
DT::selectRows(dt_proxy, input$dt_rows_all)
} else {
DT::selectRows(dt_proxy, NULL)
}
})
output$selected_rows <- renderPrint(print(input$dt_rows_selected))
}
shiny::runApp(list(ui = ui, server = server))
For testing, please upload a csv file with 1+ column that can be converted to Date in the app.
My app generates date range inputs (input$daterange) dynamically depending on the date columns selected. I'd like to validate each input$daterange from 1 to n (the length of dt$datecols) to make sure the user won't select start date earlier than the oldest date, and end date later than the latest date in the corresponding column. I use lapply on observeEvent to do that.
For ease of debugging, I pass the value of input$daterange(i) to reactive values dt$daterange(i) and print dt$daterange1 (the first date range's value) to the console rendered to check whether the it is smaller or bigger than the min and max of the corresponding date column, as I did in the lapply function. Supposedly, when the check result is FALSE, lappy function shall display an error message warning the user the start or end date is not valid, which, however doesn't work. Please find my code below, please check the comments for explanation of problem.
library("shiny")
library("DT") # Datatable
library("rsconnect") # deploy to shinyapps.io
library("shinyjs") # use toggle button from shinyJS pacakage
library("stats")
library("zoo") # to use as.Date() on numeric value
ui <- fluidPage(
fluidRow(
column(4,
# file upload div
fileInput("file", "Choose a file",
accept=c(
"text/csv",
"text/comma-separated-values,text/plain",
".csv"
)),
# show ui for upload file control
uiOutput("ui")
),
column(4,
# no choices before a file uploaded
uiOutput("columnscontrol")
)
),
hr(),
fluidRow(
column(4,
uiOutput("datecolscontrol")),
column(6,
uiOutput("daterangescontrol"))
),
hr(),
dataTableOutput("datatbl"),
# print console for debugging (delete after completion)
verbatimTextOutput("print_con")
) #end of fluidPage (ui)
# server
server <- function(input, output, session) {
#########################################################
# upload & datatable output
#########################################################
# create dataset reactive objects
dt <- reactiveValues()
# reset all uis upon new file upload
observeEvent(input$file, {
# reset reactive values
dt$data = NULL
dt$df = NULL
dt$cols = NULL
dt$rows = NULL
dt$summary = NULL
dt$colchoices = NULL
dt$datecols = NULL
# remove columns div and datecols div when a new file uploaded
removeUI(selector = "div#columns_div")
removeUI(selector = "div#datecols_div")
# remove all <div> elements indside <div>#daterangescontrol:
removeUI(selector = "div#daterangescontrol div")
# generate upload file control ui once file uploaded
output$ui <- renderUI({
actionButton("readF", "Update")
})
})
# when read file button pressed:
observeEvent (input$readF, {
# store data to dt$data
file <- input$file
dt$data <- read.csv(file$datapath, header = TRUE)
# render columnscontrol
output$columnscontrol <- renderUI({
# get the col names of the dataset and assign them to a list
dt$colchoices <- mapply(list, names(dt$data))
# render column group checkbox ui after loading the data
# tags#div has the advantage that you can give it an id to make it easier to reference or remove it later on
tags$div(id = "columns_div",
checkboxGroupInput("columns", "", choices = NULL, selected = NULL))
})
# render div containing #datecols under datecolscontrol
output$datecolscontrol <- renderUI({
tags$div(id = "datecols_div",
selectInput("datecols", "Filter data by dates):", choices = NULL, multiple = TRUE, selected = NULL))
})
})
# update columns choices when dt$choices is ready
observeEvent(dt$colchoices, {
updateCheckboxGroupInput(session, "columns", "Select Columns:", choices = dt$colchoices, selected = dt$colchoices)
})
# the other reactivity on dt$cols is input$file (when new file uploaded, dt$data and dt$cols set to NULL)
# so that the following line set apart the reactivity of input$columns on dt$cols
observeEvent(input$columns, {
dt$cols <- input$columns
dt$df <- dt$data[dt$cols]
}, ignoreNULL = FALSE)
# upon any change of dt$df
observeEvent(dt$df, {
f <- dt$df
# render output$datatbl
output$datatbl <- DT::renderDataTable(
f, rownames = FALSE,
filter = 'top',
options = list(autoWidth = TRUE)
)
# update datecols choices with those columns can be converted to Date only:
dt$date_ok = sapply(f, function(x) !all(is.na(as.Date(as.character(x), format = "%Y-%m-%d"))))
dt$datecolchoices = colnames(f[dt$date_ok])
updateSelectInput(session, "datecols", "Filter data by dates:", choices = dt$datecolchoices, selected = NULL)
}, ignoreNULL = FALSE)
# whenver columns convertable to date updated to choices of input$datecols, convert the columns to Date in the dataset
observeEvent(dt$datecolchoices, {
dt$df[dt$date_ok] = lapply(dt$df[dt$date_ok], function(x) as.Date(as.character(x)))
})
# generate daterange uis per selected input$datecols
observeEvent(input$datecols, {
dt$datecols = input$datecols
dt$datecols_len = length(dt$datecols)
# render daterange ui(s) per selected datecols
output$daterangescontrol <- renderUI({
# when input$datecols is NULL, no daterange ui
if ( is.null(input$datecols) ) { return(NULL) }
# otherwise
else {
D = dt$df[dt$rows, dt$cols]
output = tagList()
for (i in 1:dt$datecols_len) {
output[[i]]= tagList()
output[[i]][[1]] = tags$div(id = paste("dateranges_div", i, sep = "_"),
dateRangeInput(paste0("daterange", i),
paste("Date range of", dt$datecols[[i]]),
start = min(D[[dt$datecols[[i]]]]),
end = max(D[[dt$datecols[[i]]]])))
}
# return output tagList() with ui elements
output
}
}) # end of renderUI
}, ignoreNULL = FALSE)
# loop observeEvent to check whether each input$daterange is valid:
#### why I can't just call lapply() without observe() as suggested in this post:
#### https://stackoverflow.com/questions/40038749/r-shiny-how-to-write-loop-for-observeevent
observe({
lapply( X = 1:dt$datecols_len,
FUN = function(i) {
observeEvent(input[[paste0("daterange", i)]], {
# update reactive values to test whether this loop is working
dt[[paste0("range",i)]] = input[[paste0("daterange", i)]]
range = dt[[paste0("range",i)]]
req(range)
#########################################
## CODE BLOCK WITH PROBLEM!!!
#########################################
# Why the following doesn't work, when I pick a date earlier than the oldest date
# no error message shows!
shiny::validate(
need( range[[1]] >= min(dt$df[[dt$datecols[[i]]]]), "The start date cannot be earlier than the oldest date!"),
need( range[[2]] <= max(dt$df[[dt$datecols[[i]]]]), "The end date cannot be later than the latest date!")
)
})
}
) # end of lapply
})
# rows displayed in input$datatbl (the rendered data table)
observeEvent( input$datatbl_rows_all, {
dt$rows <- input$datatbl_rows_all
})
#########################################################
# print console
#########################################################
output$print_con <- renderPrint({
req(input$daterange1)
list(
# to verify whether the observeEvent loop is working for input validation
# I used dt$range1 to check the first (input$daterange1) against the date range of the corresponding column of the dataset.
# It's supposed that when the check result is FALSE (either by selecting a start date earlier than the oldest date or selecting an end date later than the latest date),
# the code block with problem shall prompt an error message to warn the user
min(dt$range1) >= min(dt$df[[dt$datecols[[1]]]]),
max(dt$range1) <= max(dt$df[[dt$datecols[[1]]]])
)
})
} # end of shiny server function
shinyApp(ui = ui, server = server)
This may not be the exact answer you are looking for but I think it may simplify things. I would simply order your date column which would allow you to select the oldest and newest date. Then set your start and end dates to those two values (see ?dateRangeInput). Lubridate is also a great package for working with dates
I think the problem maybe related to the format of your dates.
please look at this post:
R: Shiny dateRangeInput format
you may need to use
format(range[[1]])
The goal
I am working on a Shiny app that allows the user to upload their own data and focus on the entire data or a subset by providing data filtering widgets described by the below graph
The select input "Variable 1" will display all the column names of the data uploaded by the user and the selectize input "Value" will display all the unique values of the corresponding column selected in "Variable 1". Ideally, the user will be able to add as many such rows ("Variable X" + "Value") as possible by some sort of trigger, one possibility being clicking the "Add more" action button.
A possible solution
After looking up online, I've found one promising solution given by Nick Carchedi pasted below
ui.R
library(shiny)
shinyUI(pageWithSidebar(
# Application title
headerPanel("Dynamically append arbitrary number of inputs"),
# Sidebar with a slider input for number of bins
sidebarPanel(
uiOutput("allInputs"),
actionButton("appendInput", "Append Input")
),
# Show a plot of the generated distribution
mainPanel(
p("The crux of the problem is to dynamically add an arbitrary number of inputs
without resetting the values of existing inputs each time a new input is added.
For example, add a new input, set the new input's value to Option 2, then add
another input. Note that the value of the first input resets to Option 1."),
p("I suppose one hack would be to store the values of all existing inputs prior
to adding a new input. Then,", code("updateSelectInput()"), "could be used to
return inputs to their previously set values, but I'm wondering if there is a
more efficient method of doing this.")
)
))
server.R
library(shiny)
shinyServer(function(input, output) {
# Initialize list of inputs
inputTagList <- tagList()
output$allInputs <- renderUI({
# Get value of button, which represents number of times pressed
# (i.e. number of inputs added)
i <- input$appendInput
# Return if button not pressed yet
if(is.null(i) || i < 1) return()
# Define unique input id and label
newInputId <- paste0("input", i)
newInputLabel <- paste("Input", i)
# Define new input
newInput <- selectInput(newInputId, newInputLabel,
c("Option 1", "Option 2", "Option 3"))
# Append new input to list of existing inputs
inputTagList <<- tagAppendChild(inputTagList, newInput)
# Return updated list of inputs
inputTagList
})
})
The downside
As pointed by Nick Carchedi himself, all the existing input widgets will undesirably get reset every time when a new one is added.
A promising solution for data subsetting/filtering in Shiny
As suggested by warmoverflow, the datatable function in DT package provides a nice way to filter the data in Shiny. See below a minimal example with data filtering enabled.
library(shiny)
shinyApp(
ui = fluidPage(DT::dataTableOutput('tbl')),
server = function(input, output) {
output$tbl = DT::renderDataTable(
iris, filter = 'top', options = list(autoWidth = TRUE)
)
}
)
If you are going to use it in your Shiny app, there are some important aspects that are worth noting.
Filtering box type
For numeric/date/time columns: range sliders are used to filter rows within ranges
For factor columns: selectize inputs are used to display all possible categories
For character columns: ordinary search boxes are used
How to obtain the filtered data
Suppose the table output id is tableId, use input$tableId_rows_all as the indices of rows on all pages (after the table is filtered by the search strings). Please note that input$tableId_rows_all returns the indices of rows on all pages for DT (>= 0.1.26). If you use the DT version by regular install.packages('DT'), only the indices of the current page are returned
To install DT (>= 0.1.26), refer to its GitHub page
Column width
If the data have many columns, column width and filter box width will be narrow, which makes it hard to see the text as report here
Still to be solved
Despite some known issues, datatable in DT package stands as a promising solution for data subsetting in Shiny. The question itself, i.e. how to dynamically append arbitrary number of input widgets in Shiny, nevertheless, is interesting and also challenging. Until people find a good way to solve it, I will leave this question open :)
Thank you!
are you looking for something like this?
library(shiny)
LHSchoices <- c("X1", "X2", "X3", "X4")
#------------------------------------------------------------------------------#
# MODULE UI ----
variablesUI <- function(id, number) {
ns <- NS(id)
tagList(
fluidRow(
column(6,
selectInput(ns("variable"),
paste0("Select Variable ", number),
choices = c("Choose" = "", LHSchoices)
)
),
column(6,
numericInput(ns("value.variable"),
label = paste0("Value ", number),
value = 0, min = 0
)
)
)
)
}
#------------------------------------------------------------------------------#
# MODULE SERVER ----
variables <- function(input, output, session, variable.number){
reactive({
req(input$variable, input$value.variable)
# Create Pair: variable and its value
df <- data.frame(
"variable.number" = variable.number,
"variable" = input$variable,
"value" = input$value.variable,
stringsAsFactors = FALSE
)
return(df)
})
}
#------------------------------------------------------------------------------#
# Shiny UI ----
ui <- fixedPage(
verbatimTextOutput("test1"),
tableOutput("test2"),
variablesUI("var1", 1),
h5(""),
actionButton("insertBtn", "Add another line")
)
# Shiny Server ----
server <- function(input, output) {
add.variable <- reactiveValues()
add.variable$df <- data.frame("variable.number" = numeric(0),
"variable" = character(0),
"value" = numeric(0),
stringsAsFactors = FALSE)
var1 <- callModule(variables, paste0("var", 1), 1)
observe(add.variable$df[1, ] <- var1())
observeEvent(input$insertBtn, {
btn <- sum(input$insertBtn, 1)
insertUI(
selector = "h5",
where = "beforeEnd",
ui = tagList(
variablesUI(paste0("var", btn), btn)
)
)
newline <- callModule(variables, paste0("var", btn), btn)
observeEvent(newline(), {
add.variable$df[btn, ] <- newline()
})
})
output$test1 <- renderPrint({
print(add.variable$df)
})
output$test2 <- renderTable({
add.variable$df
})
}
#------------------------------------------------------------------------------#
shinyApp(ui, server)
Now, I think that I understand better the problem.
Suppose the user selects the datasets::airquality dataset (here, I'm showing only the first 10 rows):
The field 'Select Variable 1' shows all the possible variables based on the column names of said dataset:
Then, the user selects the condition and the value to filter the dataset by:
Then, we want to add a second filter (still maintaining the first one):
Finally, we get the dataset filtered by the two conditions:
If we want to add a third filter:
You can keep adding filters until you run out of data.
You can also change the conditions to accommodate factors or character variables. All you need to do is change the selectInput and numericInput to whatever you want.
If this is what you want, I've solved it using modules and by creating a reactiveValue (tmpFilters) that contains all selections (variable + condition + value). From it, I created a list with all filters (tmpList) and from it I created the proper filter (tmpListFilters) to use with subset.
This works because the final dataset is "constantly" being subset by this reactiveValue (the tmpFilters). At the beginning, tmpFilters is empty, so we get the original dataset. Whenever the user adds the first filter (and other filters after that), this reactiveValue gets updated and so does the dataset.
Here's the code for it:
library(shiny)
# > MODULE #####################################################################
## |__ MODULE UI ===============================================================
variablesUI <- function(id, number, LHSchoices) {
ns <- NS(id)
tagList(
fluidRow(
column(
width = 4,
selectInput(
inputId = ns("variable"),
label = paste0("Select Variable ", number),
choices = c("Choose" = "", LHSchoices)
)
),
column(
width = 4,
selectInput(
inputId = ns("condition"),
label = paste0("Select condition ", number),
choices = c("Choose" = "", c("==", "!=", ">", ">=", "<", "<="))
)
),
column(
width = 4,
numericInput(
inputId = ns("value.variable"),
label = paste0("Value ", number),
value = NA,
min = 0
)
)
)
)
}
## |__ MODULE SERVER ===========================================================
filter <- function(input, output, session){
reactive({
req(input$variable, input$condition, input$value.variable)
fullFilter <- paste0(
input$variable,
input$condition,
input$value.variable
)
return(fullFilter)
})
}
# Shiny ########################################################################
## |__ UI ======================================================================
ui <- fixedPage(
fixedRow(
column(
width = 5,
selectInput(
inputId = "userDataset",
label = paste0("Select dataset"),
choices = c("Choose" = "", ls("package:datasets"))
),
h5(""),
actionButton("insertBtn", "Add another filter")
),
column(
width = 7,
tableOutput("finalTable")
)
)
)
## |__ Server ==================================================================
server <- function(input, output) {
### \__ Get dataset from user selection ------------------------------------
originalDF <- reactive({
req(input$userDataset)
tmpData <- eval(parse(text = paste0("datasets::", input$userDataset)))
if (!class(tmpData) == "data.frame") {
stop("Please select a dataset of class data.frame")
}
tmpData
})
### \__ Get the column names -----------------------------------------------
columnNames <- reactive({
req(input$userDataset)
tmpData <- eval(parse(text = paste0("datasets::", input$userDataset)))
names(tmpData)
})
### \__ Create Reactive Filter ---------------------------------------------
tmpFilters <- reactiveValues()
### \__ First UI Element ---------------------------------------------------
### Add first UI element with column names
observeEvent(input$userDataset, {
insertUI(
selector = "h5",
where = "beforeEnd",
ui = tagList(variablesUI(paste0("var", 1), 1, columnNames()))
)
})
### Update Reactive Filter with first filter
filter01 <- callModule(filter, paste0("var", 1))
observe(tmpFilters[['1']] <- filter01())
### \__ Other UI Elements --------------------------------------------------
### Add other UI elements with column names and update the filter
observeEvent(input$insertBtn, {
btn <- sum(input$insertBtn, 1)
insertUI(
selector = "h5",
where = "beforeEnd",
ui = tagList(variablesUI(paste0("var", btn), btn, columnNames()))
)
newFilter <- callModule(filter, paste0("var", btn))
observeEvent(newFilter(), {
tmpFilters[[paste0("'", btn, "'")]] <- newFilter()
})
})
### \__ Dataset with Filtered Results --------------------------------------
resultsFiltered <- reactive({
req(filter01())
tmpDF <- originalDF()
tmpList <- reactiveValuesToList(tmpFilters)
if (length(tmpList) > 1) {
tmpListFilters <- paste(tmpList, "", collapse = "& ")
} else {
tmpListFilters <- unlist(tmpList)
}
tmpResult <- subset(tmpDF, eval(parse(text = tmpListFilters)))
tmpResult
})
### \__ Print the Dataset with Filtered Results ----------------------------
output$finalTable <- renderTable({
req(input$userDataset)
if (is.null(tmpFilters[['1']])) {
head(originalDF(), 10)
} else {
head(resultsFiltered(), 10)
}
})
}
#------------------------------------------------------------------------------#
shinyApp(ui, server)
# End
If you are looking for a data subsetting/filtering in Shiny Module :
filterData from package shinytools can do the work. It returns an expression as a call but it can also return the data (if your dataset is not too big).
library(shiny)
# remotes::install_github("ardata-fr/shinytools")
library(shinytools)
ui <- fluidPage(
fluidRow(
column(
3,
filterDataUI(id = "ex"),
actionButton("AB", label = "Apply filters")
),
column(
3,
tags$strong("Expression"),
verbatimTextOutput("expression"),
tags$br(),
DT::dataTableOutput("DT")
)
)
)
server <- function(input, output) {
x <- reactive({iris})
res <- callModule(module = filterDataServer, id = "ex", x = x, return_data = FALSE)
output$expression <- renderPrint({
print(res$expr)
})
output$DT <- DT::renderDataTable({
datatable(data_filtered())
})
data_filtered <- eventReactive(input$AB, {
filters <- eval(expr = res$expr, envir = x())
x()[filters,]
})
}
shinyApp(ui, server)
You can also use lazyeval or rlang to evaluate the expression :
filters <- lazyeval::lazy_eval(res$expr, data = x())
filters <- rlang::eval_tidy(res$expr, data = x())
You need to check for existing input values and use them if available:
# Prevent dynamic inputs from resetting
newInputValue <- "Option 1"
if (newInputId %in% names(input)) {
newInputValue <- input[[newInputId]]
}
# Define new input
newInput <- selectInput(newInputId, newInputLabel, c("Option 1", "Option 2", "Option 3"), selected=newInputValue)
A working version of the gist (without the reset problem) can be found here: https://gist.github.com/motin/0d0ed0d98fb423dbcb95c2760cda3a30
Copied below:
ui.R
library(shiny)
shinyUI(pageWithSidebar(
# Application title
headerPanel("Dynamically append arbitrary number of inputs"),
# Sidebar with a slider input for number of bins
sidebarPanel(
uiOutput("allInputs"),
actionButton("appendInput", "Append Input")
),
# Show a plot of the generated distribution
mainPanel(
p("This shows how to add an arbitrary number of inputs
without resetting the values of existing inputs each time a new input is added.
For example, add a new input, set the new input's value to Option 2, then add
another input. Note that the value of the first input does not reset to Option 1.")
)
))
server.R
library(shiny)
shinyServer(function(input, output) {
output$allInputs <- renderUI({
# Get value of button, which represents number of times pressed (i.e. number of inputs added)
inputsToShow <- input$appendInput
# Return if button not pressed yet
if(is.null(inputsToShow) || inputsToShow < 1) return()
# Initialize list of inputs
inputTagList <- tagList()
# Populate the list of inputs
lapply(1:inputsToShow,function(i){
# Define unique input id and label
newInputId <- paste0("input", i)
newInputLabel <- paste("Input", i)
# Prevent dynamic inputs from resetting
newInputValue <- "Option 1"
if (newInputId %in% names(input)) {
newInputValue <- input[[newInputId]]
}
# Define new input
newInput <- selectInput(newInputId, newInputLabel, c("Option 1", "Option 2", "Option 3"), selected=newInputValue)
# Append new input to list of existing inputs
inputTagList <<- tagAppendChild(inputTagList, newInput)
})
# Return updated list of inputs
inputTagList
})
})
(The solution was guided on Nick's hints in the original gist from where you got the code of the promising solution)
I reproduced an example shiny app written by Yihui Xie (https://yihui.shinyapps.io/DT-rows/). The app uses DT::renderDataTable() which allows a row selection.
Everything works perfectly fine. I was however wondering if it's possible to reset the row selection (i.e. undo the click selection) ? I already tried it with an action button to reset s = input$x3_rows_selected (see script below).
With my current script,s = input$x3_rows_selected does indeed get emptied, I can however not refill it. Also the selected rows are still clicked (shaded)
Does anyone has an idea? Is there an option within DT::renderDataTable() to reset the selection? Or does anyone has an idea for a workaround?
Thank you!
Example form https://yihui.shinyapps.io/DT-rows/) with my modification (action button):
server.R
library(shiny)
library(DT)
shinyServer(function(input, output, session) {
# you must include row names for server-side tables
# to be able to get the row
# indices of the selected rows
mtcars2 = mtcars[, 1:8]
output$x3 = DT::renderDataTable(mtcars2, rownames = TRUE, server = TRUE)
# print the selected indices
selection <- reactive({
if (input$resetSelection)
vector() else input$x3_rows_selected
})
output$x4 = renderPrint({
if (length(selection())) {
cat("These rows were selected:\n\n")
output <- selection()
cat(output, sep = "\n")
}
})
})
ui.R
library(shiny)
shinyUI(
fluidPage(
title = 'Select Table Rows',
h1('A Server-side Table'),
fluidRow(
column(9, DT::dataTableOutput('x3')),
column(3, verbatimTextOutput('x4'),
actionButton('resetSelection',
label = "Click to reset row selection"
) # end of action button
) #end of column
)))
In the current development version of DT (>= 0.1.16), you can use the method selectRows() to clear selections. Please see the section "Manipulate An Existing DataTables Instance" in the documentation.
Here is a possible solution, maybe not the best but it works. It is based on re-create the datatable each time the action button is clicked, so the selected rows are removed.
library(shiny)
library(DT)
runApp(list(
server = function(input, output, session) {
mtcars2 = mtcars[, 1:8]
output$x3 = DT::renderDataTable({
# to create a new datatable each time the reset button is clicked
input$resetSelection
mtcars2
}, rownames = TRUE, server = TRUE
)
# print the selected indices
selection <- reactive ({
input$x3_rows_selected
})
output$x4 = renderPrint({
if (length(selection())) {
cat('These rows were selected:\n\n')
output <- selection()
cat(output, sep = '\n')
}
})
},
ui = shinyUI(fluidPage(
title = 'Select Table Rows',
h1('A Server-side Table'),
fluidRow(
column(9, DT::dataTableOutput('x3')),
column(3, verbatimTextOutput('x4'),
actionButton( 'resetSelection',label = "Click to reset row selection")
) #end of column
)
))
))