Create dynamic number of input elements with R/Shiny
Solution 1
You could handle generation of the UI element in server.R
, so you have something like:
ui.R
----
shinyUI( pageWithSideBar(
...
selectInput("numIndividuals", ...)
uiOutput("sliders"),
...
))
and
server.R
--------
shinyServer( function(input, output, session) {
output$sliders <- renderUI({
numIndividuals <- as.integer(input$numIndividuals)
lapply(1:numIndividuals, function(i) {
sliderInput(...)
})
})
})
When I have UI elements that depend on values from other UI elements, I find it easiest to generate them in server.R
.
It's useful to understand that all of the _Input
functions just generate HTML. When you want to generate that HTML dynamically it makes sense to move it to server.R
. And perhaps the other thing worth emphasizing is that it's okay to return a list
of HTML 'elements' in a renderUI
call.
Solution 2
You can access dynamically named variables from shiny using this syntax:
input[["dynamically_named_element"]]
So in your example above, if you initialise your slider elements as so
# server.R
output$sliders <- renderUI({
members <- as.integer(input$members) # default 2
max_pred <- as.integer(input$max_pred) # default 5000
lapply(1:members, function(i) {
sliderInput(inputId = paste0("ind", i), label = paste("Individual", i),
min = 0, max = max_pred, value = c(0, 500), step = 100)
})
})
# ui.R
selectInput("num", "select number of inputs", choices = seq(1,10,1))
uiOutput("input_ui")
You can print the values to a table using the following
# server.R
output$table <- renderTable({
num <- as.integer(input$num)
data.frame(lapply(1:num, function(i) {
input[[paste0("ind", i)]]
}))
})
# ui.R
tableOutput("table")
See here for a working Shiny example. Working gist here.
Source: Joe Cheng's first answer, about half way down this thread
Solution 3
You could generate the sidebar with do.call
and lapply
, something like:
# create the first input, which isn't dynamic
sel.input = selectInput(inputId = "class", label = "Choose plan type:",
list("Employee only" = "emp", "Employee and spouse" = "emp_spouse",
"Employee and child" = "emp_child", "Employee and family" = "emp_fam"))
num.individuals = 5 # determine the number of individuals here
# concatenate the select input and the other inputs
inputs = c(list(sel.input), lapply(1:num.individuals, function(i) {
sliderInput(inputId = paste0("ind", i), label = paste("Individual", i), min = 0, max = 20000, value = c(0, 2500), step = 250)
}))
sidebar.panel = do.call(sidebarPanel, inputs)
![Hendy](https://i.stack.imgur.com/Y7uk3.jpg?s=256&g=1)
Hendy
Updated on August 31, 2020Comments
-
Hendy almost 4 years
I'm writing a Shiny app for visualizing insurance benefit plans at my company. Here is what I'd like to happen:
- I'll have a
selectInput
orsliderInput
where the user will choose the number of individuals on their medical plan - A matching number of double sided sliders will appear (one for each member)
- They can then input their estimates for best/worst case medical expenses for each member on their plan
- I have code that will take those estimates and create side by side plots illustrating the forecast cost on the three plan offerings so they can decide which one is least expensive based on their estimates
Here's my current
ui.R
file with hard coded inputs, simulating a family of four:shinyUI(pageWithSidebar( headerPanel("Side by side comparison"), sidebarPanel( selectInput(inputId = "class", label = "Choose plan type:", list("Employee only" = "emp", "Employee and spouse" = "emp_spouse", "Employee and child" = "emp_child", "Employee and family" = "emp_fam")), sliderInput(inputId = "ind1", label = "Individual 1", min = 0, max = 20000, value = c(0, 2500), step = 250), sliderInput(inputId = "ind2", label = "Individual 2", min = 0, max = 20000, value = c(0, 2500), step = 250), sliderInput(inputId = "ind3", label = "Individual 3", min = 0, max = 20000, value = c(0, 2500), step = 250), sliderInput(inputId = "ind4", label = "Individual 4", min = 0, max = 20000, value = c(0, 2500), step = 250) ), mainPanel( tabsetPanel( tabPanel("Side by Side", plotOutput(outputId = "main_plot", width = "100%")), tabPanel("Summary", tableOutput(outputId = "summary")) ) )))
Here's what it looks like (the transparent end sections are the result of HSA contributions from two of the plans. I thought it was a nice way to show both the premiums and medical expenses while showing the impact of the company HSA contribution. Thus, you'd just compare the length of the solid colors).
I've seen examples like this where the UI input itself is fixed (in this case, one
checkboxGroupInput
exists, but its contents are tailored based on the choice from another UI input), but I've not seen examples of tailoring the number (or, say, type) of input elements spawned as the result of another UI input's contents.Any suggestions on this (is it even possible)?
My last resort will be to create, say, 15 input sliders and initialize them to zero. My code will work just fine, but I'd like to clean up the interface by not having to create that many sliders just for the occasional user who has a very large family.
Update based on Kevin Ushay's answer
I tried to go the
server.R
route and have this:shinyServer(function(input, output) { output$sliders <- renderUI({ members <- as.integer(input$members) # default 2 max_pred <- as.integer(input$max_pred) # default 5000 lapply(1:members, function(i) { sliderInput(inputId = paste0("ind", i), label = paste("Individual", i), min = 0, max = max_pred, value = c(0, 500), step = 100) }) }) })
Immediately afterwards, I try and extract the values out of
input
for each individual's expenses:expenses <- reactive({ members <- as.numeric(input$members) mins <- sapply(1:members, function(i) { as.numeric(input[[paste0("ind", i)]])[1] }) maxs <- sapply(1:members, function(i) { as.numeric(input[[paste0("ind", i)]])[2] }) expenses <- as.data.frame(cbind(mins, maxs)) })
Lastly, I have two functions that create objects to store a data frame for plotting based on the low and high medical expense estimates. They're called
best_case
andworst_case
and both need theexpenses
object to work, so I call it as my first line as I learned from this questionbest_case <- reactive({ expenses <- expenses() ... )}
I got some errors, so I used
browser()
to step through theexpenses
bit and noticed peculiar things likeinput$ind1
not seeming to exist from within theexpenses
function.I also played around with various
print()
statements in it to see what was happening. The most striking is when I doprint(names(input))
as the very first line in the function:[1] "class" "max_pred" "members" [1] "class" "ind1" "ind2" "max_pred" "members"
I get two outputs, which I believe is due to the defining of
expenses
and subsequent calling of it. Strangely... I don't get a third whenworst_case
uses the exact sameexpenses <- expense()
line.If I do something like
print(expenses)
inside of myexpenses
function, I also get duplicates:# the first mins maxs 1 NA NA 2 NA NA # the second mins maxs 1 0 500 2 0 500
Any tips on why my
input
elements forind1
andind2
wouldn't show up untilexpenses
is called the second time and thus prevent the data frame from being created correctly? - I'll have a
-
Hendy over 10 yearsI liked the idea of putting it in
server.R
, so I did. From the UI side, everything seemed to work great until I then wrote code to actually extract the values. I edited my answer to explain the result. Would you care to take a look at the issues I'm encountering? Many thanks for the assistance! -
Hendy over 10 yearsI essentially used this idea, but put it in
server.R
like Kevin suggested above. Unfortunately, I'm having issues actually accessing the values stored in the result. I edited my answer to elaborate. -
Hendy over 10 yearsKevin, if you add a bit about checking for the dynamically created inputs being
NULL
, I'd like to accept this answer as I prefer how it's living in shiny server as sort of a programmatic choice. For more understanding on theNULL
check, see mailing list thread. The objectinput$dynamic-name
doesn't exist on the first go around, so if you try to do something likelapply()
with it, you get an error because it doesn't exist yet. -
Kevin Ushey over 10 yearsCan I ask you to edit my answer? I'm not exactly sure where the
NULL
check should go. Do you mean something likeif (!(is.null(input$numIndividuals))) { ... } else { return(NULL) }
? -
Kevin Ushey over 10 yearsAlternatively, it's entirely acceptable to write your own answer (take the parts of mine that got you started) and accept it as correct.
-
Hendy over 10 yearsHa! Didn't think of that. I still think my real code is a mess and that I'm not grasping the intricacies of reactive inputs, but it now works thanks to your post and the mailing list. I think it runs pretty slow, as one has to do
if(is.null(input$obj)) { } else { }
lines in pretty much anything that might cause an error ifinput$obj
doesn't exist yet (which it doesn't on the first run through). -
user3570398 over 9 yearsTry looking here: stackoverflow.com/questions/22160372/…. That approach doesn't require knowing input$numIndividuals in advance, and moreover, you can save the previous inputs.
-
Nikos Alexandris over 8 yearsThanks for emphasising that all
Input
functions generate HTML code. -
Jeff Parker over 5 yearsAny direction on saving the values as a vector instead of a data.frame?
-
christopherlovell over 5 years@JeffParker perhaps this helps? stackoverflow.com/questions/7070173/…
-
Jeff Parker over 5 years
sapply(1:num, function(i) { input[[paste0("ind", i)]] })
did the trick.