Chapter 10 Dynamic UI
Exercise 10.1.5.1
Complete the user interface below with a server function that updates input$date
so that you can only select dates in input$year
.
<- fluidPage(
ui numericInput("year", "year", value = 2020),
dateInput("date", "date")
)
Solution.
Solution
This solution was a little wonky because it required shinyjs for the dateInput to properly update. I opened up an issue here since I think this is not the most intuitive answer.
library(shiny)
library(shinyjs)
<- fluidPage(
ui useShinyjs() ,
numericInput("year", "year", value = 2020),
dateInput("date", "date", value = Sys.Date())
)
<- function(input, output, session) {
server
observeEvent(input$year, {
req(input$year) # stop if year is blank
<- range(as.Date(paste0(input$year, "-01-01")),as.Date(paste0(input$year, "-12-31")))
daterange updateDateInput(session, "date", min = daterange[1], max = daterange[2] )
delay(250, # delay 250ms
updateDateInput(session,"date",
value = daterange[1]
))
})
}
shinyApp(ui = ui, server = server)
Exercise 10.1.5.2
Complete the user interface below with a server function that updates input$county
choices based on input$state
. For an added challenge, also change the label from “County” to “Parrish” for Louisana and “Borrough” for “Alaska”.
library(openintro)
<- unique(county$state)
states
<- fluidPage(
ui selectInput("state", "State", choices = states),
selectInput("county", "County", choices = NULL)
)
Solution.
Solution
We can use updateSelectInput
to filter the county choices based on the user selected state. By making the label of input$county
a reactive, we can use switch
to change the label when either Alaska or Louisiana is selected.
library(shiny)
library(tidyverse)
library(openintro)
<- unique(county$state)
states <- unique(county$state)
counties
<- fluidPage(
ui selectInput("state", "State", choices = states),
selectInput("county", "County", choices = NULL)
)
<- function(input, output, session) {
server
<- reactive({
label switch(input$state,
"Alaska" = "Burrough",
"Louisiana" = "Parish",
"County")
})
observeEvent( input$state, {
updateSelectInput(session, "county", label = label(),
choices = county %>%
filter(state == input$state) %>%
select(name) %>%
distinct())
})
}
shinyApp(ui = ui, server = server)
Exercise 10.1.5.3
Complete the user interface below with a server function that updates input$country
choices based on the input$continent
. Use output$data
to display all matching rows.
library(gapminder)
<- unique(gapminder$continent)
continents
<- fluidPage(
ui selectInput("continent", "Continent", choices = continents),
selectInput("country", "Country", choices = NULL),
tableOutput("data")
)
Solution.
Solution
As the question above, we are filtering the country input based on the continent by using updateSelectInput
in the server. By storing the selected data in a reactive, selected_data()
we can use the same filtered dataset for our selectInput
and the table, reducing code redundancy.
library(shiny)
library(gapminder)
<- unique(gapminder$continent)
continents
<- fluidPage(
ui selectInput("continent", "Continent", choices = c("", as.character(continents))),
selectInput("country", "Country", choices = NULL),
tableOutput("data")
)
<- function(input, output, session) {
server
<- reactive({
selected_data if(input$continent %in% continents) {
%>%
gapminder filter(continent == input$continent)
else {
}
gapminder
}
})
observeEvent( input$continent, {
updateSelectInput(session, "country", "Country",
choices = selected_data() %>%
select(country) %>%
distinct())
})
$data <- renderTable({
outputselected_data() %>%
filter(country == input$country)
})
}
shinyApp(ui = ui, server = server)
Exercise 10.1.5.4
Extend the previous app so that you can also choose to select no continent, and hence see all countries. You’ll need to add “” to the list of choices, and then handle that specially when filtering.
Solution.
Solution
Initially setting the choices to c("", as.character(continents))
allows the user to see all the Country options prior to a continent being selected. That said, once a continent is selected this ""
option is no longer available.
library(shiny)
library(gapminder)
<- unique(gapminder$continent)
continents
<- fluidPage(
ui selectInput("continent", "Continent", choices = c("", as.character(continents))),
# @tanho63:
# selectInput("continent", "Continent", choices = c("All", as.character(continents))),
selectInput("country", "Country", choices = NULL),
tableOutput("data")
)
<- function(input, output, session) {
server
<- reactive({
selected_data if(input$continent %in% continents) {
%>%
gapminder filter(continent == input$continent)
else {
}
gapminder
}
})
observeEvent( input$continent, {
# @tanho63:
updateSelectInput(session, "country",
choices = unique(selected_data()$country))
})
$data <- renderTable({
outputselected_data() %>%
filter(country == input$country)
})
}
shinyApp(ui = ui, server = server)
Exercise 10.1.5.5
What is at the heart of the problem described at https://community.rstudio.com/t/29307?
Exercise 10.2.3.1
Use a hidden tabset to show additional controls only if the user checks an “advanced” check box.
Solution.
Solution
library(shiny)
<- fluidPage(
ui sidebarLayout(
sidebarPanel(
checkboxInput("moreControls",
label = "Show advanced controls?",
value = FALSE
)
),mainPanel(
tabsetPanel(
id = "basic",
type = "hidden",
tabPanelBody("panel1",
numericInput("basicControl", label = "Basic:", 0),
)
),tabsetPanel(
id = "advanced",
type = "hidden",
tabPanelBody("emptyPanel", style = "display: none"),
tabPanelBody("panel2",
numericInput("advancedCotrol", label = "Advanced:", 1)
)
)
)
)
)
<- function(input, output, session) {
server observeEvent(input$moreControls, {
if (input$moreControls) {
updateTabsetPanel(session, "advanced", selected = "panel2")
else {
} updateTabsetPanel(session, "advanced", selected = "emptyPanel")
}
})
}
shinyApp(ui, server)
Exercise 10.2.3.2
Create an app that plots ggplot(diamonds, aes(carat))
but allows the user to choose which geom to use: geom_histogram()
, geom_freqpoly()
, or geom_density()
. Use a hidden tabset to allow the user to select different arguments depending on the geom: geom_histogram()
and geom_freqpoly()
have a binwidth
argument; geom_density()
has a bw
argument.
Solution.
Solution
library(shiny)
library(ggplot2)
<- fluidPage(
ui sidebarLayout(
sidebarPanel(
selectInput("geom", "Geom function to use",
choices = c("histogram", "freqpoly", "density")
),tabsetPanel(
id = "params",
type = "hidden",
tabPanel("histogram",
numericInput("hist_bw",
label = "Binwidth", value = 0.1,
min = 0.1, max = 5, step = 0.1
)
),tabPanel("freqpoly",
numericInput("freqpoly_bw",
label = "Binwidth", value = 0.1,
min = 0.1, max = 5, step = 0.1
)
),tabPanel("density",
numericInput("density_bw",
label = "Standard deviation of smoothing kernel",
value = 0.01, min = 0.01, max = 1, step = 0.01
)
)
)
),mainPanel(
plotOutput("gg")
)
)
)
<- function(input, output, session) {
server observeEvent(input$geom, {
updateTabsetPanel(inputId = "params", selected = input$geom)
})
<- reactive({
gg_args switch(input$geom,
histogram = geom_histogram(binwidth = input$hist_bw),
freqpoly = geom_freqpoly(binwidth = input$freqpoly_bw),
density = geom_density(bw = input$density_bw)
)
})
$gg <- renderPlot({
outputggplot(diamonds, aes(carat)) +
gg_args()
})
}
shinyApp(ui, server)
Exercise 10.2.3.3
Modify the app you created in the previous exercise to allow the user to choose whether each geom is shown or not (i.e. instead of always using one geom, they can picked 0, 1, 2, or 3). Make sure that you can control the binwidth of the histogram and frequency polygon independently.
Solution.
Solution
library(shiny)
library(ggplot2)
<- c("histogram", "freqpoly", "density")
geom_choices
# ----------------------------------------------- #
# Generate the necessary code (as a string) #
# for ggplot, after having chosen one of #
# the available geom functions in the ui #
# ----------------------------------------------- #
# A list could be used, working only with the three
# provided geom choices, but a fuction will be used,
# to provide a template for a possible generalization
# when working with more geom choices and parameters.
<- function(geom_choice) {
geom_choice_code # Using %in% could be a more general case,
# considering more possible geom options.
if (geom_choice == "density") {
return(paste0(
# Simple density
# "geom_", geom_choice, "(bw = input$", geom_choice, "_bw)"
# Match density to the histogram's y-scale
"geom_density(
color = 'blue',
bw = input$density_bw,
aes(y = ..density.. * (nrow(diamonds) * input$histogram_bw))
)"
))
}
return(paste0(
"geom_", geom_choice, "(",
# Improve histograms' visibility
ifelse(geom_choice == "histogram", "fill = 'transparent', color = 'red', ", ""),
"binwidth = input$", geom_choice, "_bw)"
))
}
<- fluidPage(
ui sidebarLayout(
sidebarPanel(
selectInput("geom",
label = "Geom function to use", multiple = TRUE,
choices = geom_choices, selected = "histogram"
),tabsetPanel(
id = "histogram",
type = "hidden",
tabPanelBody("histogram_empty", style = "display: none"),
tabPanelBody("histogram_params",
# This parameter will also be used to scale the y-axis
# when plotting density, so that all graphics
# have a similar y-axis of "count"
numericInput("histogram_bw",
label = "Histogram's binwidth", value = 0.15,
min = 0.1, max = 5, step = 0.1
)
)
),tabsetPanel(
id = "freqpoly",
type = "hidden",
tabPanelBody("freqpoly_empty", style = "display: none"),
tabPanelBody("freqpoly_params",
numericInput("freqpoly_bw",
label = "freqpoly's binwidth", value = 0.15,
min = 0.1, max = 5, step = 0.1
)
)
),tabsetPanel(
id = "density",
type = "hidden",
tabPanelBody("density_empty", style = "display: none"),
tabPanelBody("density_params",
numericInput("density_bw",
label = "Standard deviation of smoothing kernel",
value = 0.15, min = 0.01, max = 1, step = 0.01
)
)
)
),mainPanel(
plotOutput("gg")
)
)
)
<- function(input, output, session) {
server # ------------------------------------------ #
# Update inputs' visibility in sidebar panel #
# ------------------------------------------ #
observeEvent(input$geom, {
# Get non selected geom functions, in order
# to hide their respective parameters
<- setdiff(geom_choices, input$geom)
non_selected ::map(
purrr
geom_choices, ~ updateTabsetPanel(
inputId = .x,
selected = paste0(
.x, ifelse(.x %in% non_selected, "_empty", "_params")
)
)
)# Run this code also when the select input is cleared
ignoreNULL = FALSE)
},
# ------------------------ #
# Retrieve code for ggplot #
# ------------------------ #
<- reactive({
gg_args req(input$geom)
::map_chr(input$geom, geom_choice_code) |>
purrrpaste(collapse = " + ")
})
$gg <- renderPlot({
outputeval(parse(text = paste0(
"ggplot(diamonds, aes(carat)) + ",
gg_args(), " + ",
"labs(y = 'Count')"
)))
})
}
shinyApp(ui, server)
Exercise 10.3.4.1
Take this very simple app based on the initial example in the chapter:
<- fluidPage(
ui selectInput("type", "type", c("slider", "numeric")),
uiOutput("numeric")
)<- function(input, output, session) {
server $numeric <- renderUI({
outputif (input$type == "slider") {
sliderInput("n", "n", value = 0, min = 0, max = 100)
else {
} numericInput("n", "n", value = 0, min = 0, max = 100)
}
}) }
How could you instead implement it using dynamic visibility? If you implement dynamic visibility, how could you keep the values in sync when you change the controls?
Solution.
Solution
library(shiny)
<- tagList(
parameter_tabs $style("#params { display:none; }"),
tagstabsetPanel(id = "params",
tabPanel("slider",
sliderInput("my_slider", "n", value = 0, min = 0, max = 100)
),tabPanel("numeric",
numericInput("my_numeric", "n", value = 0, min = 0, max = 100)
)
)
)
<- fluidPage(
ui sidebarLayout(
sidebarPanel(
selectInput("my_selector", "Input Type",
choices = c("slider", "numeric")
),
parameter_tabs,
),mainPanel()
)
)
<- function(input, output, session) {
server
# if slider changes, update numeric
observeEvent( input$my_slider, {
updateNumericInput(session, "my_numeric", value = isolate(input$my_slider))
})
# if numeric changes update slider
observeEvent( input$my_numeric, {
updateSliderInput(session, "my_slider", value = isolate(input$my_numeric))
})
observeEvent(input$my_selector, {
updateTabsetPanel(session, "params", selected = input$my_selector)
})
}
shinyApp(ui = ui, server = server)
Exercise 10.3.4.2
Explain how this app works. Why does the password disappear when you click the enter password button for the second time?
<- fluidPage(
ui actionButton("go", "Enter password"),
textOutput("text")
)<- function(input, output, session) {
server observeEvent(input$go, {
showModal(modalDialog(
passwordInput("password", NULL),
title = "Please enter your password"
))
})
$text <- renderText({
outputif (!isTruthy(input$password)) {
"No password"
else {
} "Password entered"
}
}) }
Exercise 10.3.4.3
Add support for date and date-time columns make_ui()
and filter_var()
.
Solution.
Solution
In order to complete this, I had to
- make a new dummy dataframe I called
x
in order to test for dates - include checking for
is.Date
in themake_ui
andfilter_var
functions - Change
tableOutput
andrenderTable
toDT::renderTableOutput
andDT::renderTableOutput
becauserenderTable
was rendering the dates as numbers and I think this could be because it usesxtable()
for HTML table rendering?
# 8.4.3.2
library(shiny)
library(purrr)
library(tidyverse)
<- function(x, var) {
make_ui if (is.numeric(x)) {
<- range(x, na.rm = TRUE)
rng sliderInput(var, var, min = rng[1], max = rng[2], value = rng)
else if (is.factor(x)) {
} <- levels(x)
levs selectInput(var, var, choices = levs, selected = levs, multiple = TRUE)
else if (lubridate::is.Date(x)) {
} <- range(x, na.rm = TRUE)
rng dateInput(var, var, min = rng[1], max = rng[2], value = rng[1])
else {
} # No control, so don't filter
NULL
}
}
<- function(x, val) {
filter_var if (is.numeric(x)) {
!is.na(x) & x >= val[1] & x <= val[2]
else if (is.factor(x)) {
} %in% val
x else if (lubridate::is.Date(x)) {
} %in% val
x else {
} TRUE
}
}
library(shiny)
<- keep(ls("package:datasets"), ~ is.data.frame(get(.x, "package:datasets")))
dfs
# add a dataframe with dates in it since I cant find one in the datasets above
# rep 5 dates five times, each include 1 factor a-e
<- data.frame(date = c(rep(as.Date("2020/1/1"), 5),
x rep(as.Date("2020/2/2"), 5),
rep(as.Date("2020/3/3"), 5),
rep(as.Date("2020/4/4"), 5),
rep(as.Date("2020/5/5"), 5)),
fac = as.factor(c("a", "b", "c", "d", "e")))
<- fluidPage(
ui sidebarLayout(
sidebarPanel(
#selectInput("dataset", label = "Dataset", choices = c(dfs, "x")),
uiOutput("filter")
),mainPanel(
::dataTableOutput("data")
DT
)
)
)
<- function(input, output, session) {
server
# data is either my dummy dataset or from datasets
<- reactive(x)
data
<- reactive(names(data()))
vars
$filter <- renderUI(
output# take eahc column name and make ui
# data()[[.x]] is each column
# and .x is each column name (vars())
map(vars(), ~ make_ui(data()[[.x]], .x))
)
<- reactive({
selected # take each column name and filer var
# with the first argument the column in the data
# and the second argument the input$vars()
# so for date check that input[[date]] in data[[1]]
<- map(vars(), ~ filter_var(data()[[.x]], input[[.x]]))
each_var
# notes from @mapaulacaldas
# collapse list of TRUE and FALSE using `&`
# conditions <- list(TRUE, TRUE, TRUE, FALSE)
# purrr::reduce(conditions, `&`) ==
# ((conditions[[1]] & conditions[[2]]) & conditions[[3]]) & conditions[[4]]
reduce(each_var, `&`)
})
# subset the data by the vars that are true
$data <- DT::renderDataTable(data()[selected(), ])
output
}# Run the application
shinyApp(ui = ui, server = server)
Exercise 10.3.4.4
(Advanced) If you know the S3 OOP system, consider how you could replace the if blocks in make_ui()
and filter_var()
with generic functions.
Solution.
Solution
library(shiny)
library(purrr)
<- function(obj, var) { UseMethod("make_ui") }
make_ui
<- function(x, var) {
make_ui.numeric <- range(x, na.rm = TRUE)
rng sliderInput(var, var, min = rng[1], max = rng[2], value = rng)
}
<- function(x, var) {
make_ui.factor <- levels(x)
levs selectInput(var, var, choices = levs, selected = levs, multiple = TRUE)
}
<- function(x, var) { NULL }
make_ui.default
<- function(x, val) { UseMethod("filter_var") }
filter_var <- function(x, val) { !is.na(x) & x >= val[1] & x <= val[2] }
filter_var.numeric <- function(x, val) { x %in% val }
filter_var.factor <- function(x, val) { TRUE }
filter_var.default
<- keep(ls("package:datasets"), ~ is.data.frame(get(.x, "package:datasets")))
dfs
<- fluidPage(
ui sidebarLayout(
sidebarPanel(
selectInput("dataset", label = "Dataset", choices = dfs),
uiOutput("filter")
),mainPanel(
tableOutput("data")
)
)
)<- function(input, output, session) {
server <- reactive({
data get(input$dataset, "package:datasets")
})
<- reactive(names(data()))
vars
$filter <- renderUI(
outputmap(vars(), ~ make_ui(data()[[.x]], .x))
)
<- reactive({
selected <- map(vars(), ~ filter_var(data()[[.x]], input[[.x]]))
each_var reduce(each_var, `&`)
})
$data <- renderTable(head(data()[selected(), ], 12))
output
}
shinyApp(ui = ui, server = server)
Exercise 10.3.4.5
(Hard) Make a wizard that allows the user to upload their own dataset. The first page should handle the upload. The second should handle reading it, providing one drop down for each variable that lets the user select the column type. The third page should provide some way to get a summary of the dataset.
Solution.
Solution
I wasn’t really sure what was meant by “some way to get a summary of the dataset” So I’m just using the summary function.
library(shiny)
library(readr)
<- function(name_of_vector) {
make_dropdown selectInput(inputId = name_of_vector, label = name_of_vector, choices =
c("numeric", "character", "logical"))
}
<- fluidPage(
ui $style("#wizard { display:none; }"),
tagstabsetPanel(id = "wizard",
tabPanel("page1",
fileInput("data_input", "input"),
actionButton("page12", "next")
),tabPanel("page2",
sidebarLayout(
sidebarPanel(
uiOutput("type_of")
),mainPanel(
tableOutput('type_table')
)),actionButton("page21", "prev"),
actionButton("page23", "next")
),tabPanel("page3",
tableOutput("summary_table"),
actionButton("page32", "prev")
)
)
)
<- function(input, output, session) {
server
################ WIZARD ###############################
<- function(page) {
switch_tab updateTabsetPanel(session, "wizard", selected = page)
}
observeEvent(input$page12, switch_tab("page2"))
observeEvent(input$page21, switch_tab("page1"))
observeEvent(input$page23, switch_tab("page3"))
observeEvent(input$page32, switch_tab("page2"))
##################### FILE INPUT #######################
<- reactive({
dat req(input$data_input)
read.csv(input$data_input$datapath)
})
##################### TABLE TYPE #######################
# make a dropdown using the names of each column
$type_of <- renderUI({ map(names(dat()), ~ make_dropdown(.x)) })
output
# switch the type of column based on the input
# name of vector == "Sepal.Length"
# vector == Sepal.Length
<- function(vector, name_of_vector) {
change_type switch(input[[name_of_vector]],
"numeric" = vector <- as.numeric(vector),
"character" = vector <- as.character(vector),
"logical" = vector <- as.complex(vector)
)
}
# convert the supplied data to a list
# use imap because it is a condensed version o map
# with two arguments == x & name_of_x
# so we don't need to supply it arguments beyond the list!
<- reactive({
dfdat() %>%
as.list() %>%
imap(change_type) %>%
as_tibble()
})
# create an output of the data's names
# and their types
$type_table <- renderTable(data.frame(
outputnames = names(df()),
type = map_chr(df(), function(x) typeof(x)))
)
##################### TABLE OUTPUT #####################
$summary_table <- renderTable( summary(df()) )
output
}
shinyApp(ui = ui, server = server)