Create a dynamic number of UI elements in Shiny with purrr
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Introduction
purrr
is an incredibly powerful package that has greatly enhanced my R programming abilities. purrr
has applications in pretty much any situation. One of the most useful situations, IMHO, is in the creation of a dynamic number of shiny
UI elements. This can be extremely useful if you want to be able to create a dynamic number of ui elements (whether that be inputs or outputs) based on either user selection or the data being used. This post will walk through how to create a dynamic number of plots depending on how many parameters the user selects.
Background
Before we get started, let’s load the libraries we will be using.
library(dataRetrieval) library(tidyverse) library(lubridate) library(shiny) library(shinyjs) library(plotly)
Data
In this post, I will be using the R package dataRetrieval
provided by the USGS to access their public API. If you wish to know more about how to use this package I would recommend checking out the package’s vignette. The data used in this example app is daily water quality averages for three parameters (Temperature, Conductivity, and Dissolved Oxygen) measured in the Delaware River at the Ben Franklin Bridge in Philadelphia, PA. The code to pull the site info is placed at the top of the script outside of the ui and server code and looks like this:
# usgs site number of ben franklin bridge site site <- "01467200" site_info <- whatNWISdata(siteNumbers = site, service = "dv", statCd = "00003") param_info <- site_info$parm_cd %>% unique() %>% readNWISpCode() site_meta <- site_info %>% select(site_no, station_nm, parm_cd) %>% left_join(param_info %>% select(parameter_cd, srsname, parameter_units), by = c("parm_cd" = "parameter_cd")) %>% # these are the parameters with data at this site filter(parm_cd %in% c("00010", "00095", "00300")) param_choices <- site_meta$parm_cd names(param_choices) <- site_meta$srsname
The actual data is queried from the API using the readNWISdv
function and reformatted to be easy to graph inside an eventReactive
function at the top of the server code. This looks like this:
wq_data <- eventReactive(input$submit, { req(input$parameter, input$date) raw_data <- readNWISdv( siteNumbers = site, parameterCd = input$parameter, startDate = input$date[[1]], endDate = input$date[[2]] ) output <- raw_data %>% select(-contains("_cd")) %>% gather(key = "parameter", value = "result", contains("X_")) %>% mutate(parameter = str_replace_all(parameter, "X_|_00003", "")) %>% left_join(site_meta, by = c("parameter" = "parm_cd", "site_no")) return(output) })
Plotting function
Since the focus of this post is how to generate multiple plots and the data format after basic manipulation is the same for all three parameters, I have defined my plot generation as a function named wq_plotly()
outside of the server code. This functions looks like this:
wq_plotly <- function(data){ data %>% plot_ly( x = ~Date, y = ~result, type = "scatter", mode = "lines+markers", marker = list( size = 4, color = "blue" ), line = list( color = "blue" ), hoverinfo = "text", text = ~paste( "Site:", station_nm, "<br>Parameter:", srsname, "<br>Date Time:", format(Date), "<br>Result:", result, "<br>Units:", parameter_units ) ) %>% layout( title = paste( unique(data$station_nm), "<br>", unique(data$srsname), paste0("(", unique(data$parameter_units), ")") ), titlefont = list( size = 10 ), xaxis = list( title = "" ), yaxis = list( title = "" ), margin = list( t = 40 ) ) }
UI
So the first part of the app is the ui code. This part is actually somewhat straightforward because all of the magic is gonna happen in our server code. We have to define our inputs for which parameters to graph (named input$parameter
) and the desired date range (named input$date
) and create an actionButton
so that the user controls when new graphs are generated (my personal preference). I create these all within a single fluidRow
with each in its own column
.
After that, I create a new fluidRow
and simply have a uiOutput
(with an id of graphs_ui
) in it. This uiOutput
will be created in our server code and contain all of the ui elements for our plots.
Here is the full ui code:
ui <- shinyUI( fluidPage( tags$head( tags$style(HTML('.shiny-input-container{margin-top: 20px;}')) ), div( fluidRow( column( 4, selectInput( inputId = "parameter", label = "Select Parameter(s):", choices = param_choices, multiple = TRUE ) ), column( 4, dateRangeInput( inputId = "date", label = "Select Date Range:", start = Sys.Date() - days(31), end = Sys.Date() ) ), column( 4, actionButton( inputId = "submit", label = "Apply Changes!", style = "margin:40px;" ) ) ), fluidRow( div( id = "plot-container", uiOutput( outputId = "graphs_ui" ) ) ) ) ) )
Server
Now for the fun! The first part of the server code, which was shown above, is the wq_data
reactive expression to query our data set. The next part involves generating a reactive object that contains a list of our graphs (a vector would work too). This is important because it is what will be used to generate the different inputs. Since I defined my graphing function above, I can call that with a combination of dplyr::group_by
, tidyr::nest
, dplyr::mutate
, purrr::map
, and dplyr::pull
. This combination allows us to create unique graphs for each parameter and store them in a single list. The code looks like this:
graphs <- eventReactive(input$submit, { req(wq_data()) wq_data() %>% group_by(parameter) %>% nest() %>% mutate( graphs = map(data, wq_plotly) ) %>% arrange(parameter) %>% pull(graphs) })
It is important to note that I have only tested this on plotly
, ggplot2
, and base graphics. This method only works with plotly
and ggplot2
. It does not work with base graphics because base plots cannot be saved as objects.
Now that we have our list of graphs, we need to create our outputs. However, since we are giving the user the ability to choose how many parameters they want to create graphs for, how do we know how many outputs to create? With purrr
and our list of graphs, we don’t need to know how many outputs need to be created. By iterating over our list of graphs with iwalk
we can create as many outputs as there are graphs.
In this case, we want to use purrr::iwalk
(which is a variant of walk
) because we want to use both the graph and its index. Using iwalk(x, ...)
is the same as using walk2(x, seq_along(x), ...)
. We use walk
instead of map
because we are not returning anything from the overall iteration, but rather only using it for its side effects. Using walk
instead of map
is similar to using an observe
function instead of a reactive
function. To further connect the comparison, we will use our iwalk
function inside of an observeEvent
function.
Here is what the output generation looks like:
observeEvent(input$submit, { req(graphs()) iwalk(graphs(), ~{ output_name <- paste0("plot_", .y) output[[output_name]] <- renderPlotly(.x) }) })
There are two simple pieces to our iwalk
function. First, we define a unique outputId
using the index of the plot in our list. Then using that outputId
we can render our plotly object.
Note: defining an output using output[["my_output_id"]]
is the same as defining it as output$my_output_id
.
Finally, the last step of the server code is to create the ui elements! Remember, in our ui code, we defined the uiOutput
for our graphs with an id of graphs_ui
. So now, we will use renderUI
and purrr::imap
to create our ui elements. Since render*
functions are similar to reactive
functions, in that they return their output, we use imap
rather than iwalk
.
Here is what our ui generation looks like:
output$graphs_ui <- renderUI({ req(graphs()) plots_list <- imap(graphs(), ~{ tagList( plotlyOutput( outputId = paste0("plot_", .y) ), br() ) }) tagList(plots_list) })
As you can see, we use the index of our plot list again to call each individual plot outputId
. It is important to notice the tagList(plots_list)
call at the end of the renderUI
function. Since the output of imap
is a list, plots_list
is a list of ui elements and is not valid to be entered directly into the UI code. tagList
takes care of this for us and combines multiple ui elements into one.
Combining all of these elements, our final shiny app looks like this:
Conclusion
Being able to create a dynamic number of ui elements, whether they are inputs or outputs, is an incredibly powerful tool when trying to scale your shiny apps! The method shown here was applied to creating a dynamic number of graphs based on the users input, but it is certainly not limited to that! You can see an example of how to apply this to creating textInput
and numericInput
dynamically based on column names of an uploaded dataset in this RStudio Community thread. Learning and using purrr
can dramatically increase your capabilities within general R programming and building shiny applications!
Finally, the full code for the shiny app looks like this:
library(dataRetrieval) library(tidyverse) library(lubridate) library(shiny) library(shinyjs) library(plotly) # usgs site number of ben franklin bridge site site <- "01467200" site_info <- whatNWISdata(siteNumbers = site, service = "dv", statCd = "00003") param_info <- site_info$parm_cd %>% unique() %>% readNWISpCode() site_meta <- site_info %>% select(site_no, station_nm, parm_cd) %>% left_join(param_info %>% select(parameter_cd, srsname, parameter_units), by = c("parm_cd" = "parameter_cd")) %>% filter(parm_cd %in% c("00010", "00095", "00300")) param_choices <- site_meta$parm_cd names(param_choices) <- site_meta$srsname wq_plotly <- function(data){ data %>% plot_ly( x = ~Date, y = ~result, type = "scatter", mode = "lines+markers", marker = list( size = 4, color = "blue" ), line = list( color = "blue" ), hoverinfo = "text", text = ~paste( "Site:", station_nm, "<br>Parameter:", srsname, "<br>Date Time:", format(Date), "<br>Result:", result, "<br>Units:", parameter_units ) ) %>% layout( title = paste( unique(data$station_nm), "<br>", unique(data$srsname), paste0("(", unique(data$parameter_units), ")") ), titlefont = list( size = 10 ), xaxis = list( title = "" ), yaxis = list( title = "" ), margin = list( t = 40 ) ) } ui <- shinyUI( fluidPage( tags$head( tags$style(HTML('.shiny-input-container{margin-top: 20px;}')) ), div( fluidRow( column( 4, selectInput( inputId = "parameter", label = "Select Parameter(s):", choices = param_choices, multiple = TRUE ) ), column( 4, dateRangeInput( inputId = "date", label = "Select Date Range:", start = Sys.Date() - days(31), end = Sys.Date() ) ), column( 4, actionButton( inputId = "submit", label = "Apply Changes!", style = "margin:40px;" ) ) ), fluidRow( div( id = "plot-container", uiOutput( outputId = "graphs_ui" ) ) ) ) ) ) server <- shinyServer( function(input, output, session){ session$onSessionEnded(stopApp) # query data from USGS API wq_data <- eventReactive(input$submit, { req(input$parameter, input$date) raw_data <- readNWISdv( siteNumbers = site, parameterCd = input$parameter, startDate = input$date[[1]], endDate = input$date[[2]] ) output <- raw_data %>% select(-contains("_cd")) %>% gather(key = "parameter", value = "result", contains("X_")) %>% mutate(parameter = str_replace_all(parameter, "X_|_00003", "")) %>% left_join(site_meta, by = c("parameter" = "parm_cd", "site_no")) return(output) }) # create a list of graphs - with one for each parameter selected graphs <- eventReactive(input$submit, { req(wq_data()) wq_data() %>% group_by(parameter) %>% nest() %>% mutate( graphs = map(data, wq_plotly) ) %>% arrange(parameter) %>% pull(graphs) }) # use purrr::iwalk to create a dynamic number of outputs observeEvent(input$submit, { req(graphs()) iwalk(graphs(), ~{ output_name <- paste0("plot_", .y) output[[output_name]] <- renderPlotly(.x) }) }) # use renderUI to create a dynamic number of output ui elements output$graphs_ui <- renderUI({ req(graphs()) plots_list <- imap(graphs(), ~{ tagList( plotlyOutput( outputId = paste0("plot_", .y) ), br() ) }) tagList(plots_list) }) } ) shinyApp(ui = ui, server = server)
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.