Colin Rundel
Shiny is an R package that makes it easy to build interactive web apps straight from R. You can host standalone apps on a webpage or embed them in R Markdown documents or build dashboards. You can also extend your Shiny apps with CSS themes, htmlwidgets, and JavaScript actions.
Server
+
⇄
Client / Browser
+ +
demos/demo1.R
library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
shinyApp(
ui = fluidPage(
titlePanel("Temperature Forecasts"),
sidebarLayout(
sidebarPanel(
radioButtons(
"city", "Select a city",
choices = c("Washington", "New York", "Los Angeles", "Chicago")
)
),
mainPanel( plotOutput("plot") )
)
),
server = function(input, output, session) {
output$plot = renderPlot({
d %>%
filter(city %in% input$city) %>%
ggplot(aes(x=time, y=temperature, color=city)) +
geom_line()
})
}
)
Copy the code from the previous slide (or open `exercises/ex1.R) and try running it in your console.
Check that you are able successfully run the shiny app and are able to interact with it.
If everything is working try modifying the code (e.g. try adding or removing a city from radioButtons()
).
What happens if you add a city that is not in the weather.csv
data set?
A couple of quick tips:
If you can find the data, make sure you have opened the workshop’s RStudio project
Make sure you have the latest versions of shiny
and tidyverse
installed
If you are stuck, ask a neighbor for help and or raise your hand
Tabsets - see tabsetPanel()
Navbars and navlists
navlistPanel()
navbarPage()
We’ve just seen a number of alternative input widgets, starting from the code in exercises/ex2.R
try changing the radioButton()
input to something else.
What happens if you use an input capable of selecting multiple values
checkboxGroupInput()
selectInput(multiple = TRUE)
? demos/demo1.R
library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
shinyApp(
ui = fluidPage(
titlePanel("Temperature Forecasts"),
sidebarLayout(
sidebarPanel(
radioButtons(
"city", "Select a city",
choices = c("Washington", "New York", "Los Angeles", "Chicago")
)
),
mainPanel( plotOutput("plot") )
)
),
server = function(input, output, session) {
output$plot = renderPlot({
d %>%
filter(city %in% input$city) %>%
ggplot(aes(x=time, y=temperature, color=city)) +
geom_line()
})
}
)
Our inputs and outputs are defined by our ui
elements.
demos/demo1.R
library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
shinyApp(
ui = fluidPage(
titlePanel("Temperature Forecasts"),
sidebarLayout(
sidebarPanel(
radioButtons(
"city", "Select a city",
choices = c("Washington", "New York", "Los Angeles", "Chicago")
)
),
mainPanel( plotOutput("plot") )
)
),
server = function(input, output, session) {
output$plot = renderPlot({
d %>%
filter(city %in% input$city) %>%
ggplot(aes(x=time, y=temperature, color=city)) +
geom_line()
})
}
)
The “reactive” logic is defined by our server
function - shiny takes care of figuring out what depends on what.
demos/demo2.R
library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
shinyApp(
ui = fluidPage(
titlePanel("Temperature Forecasts"),
sidebarLayout(
sidebarPanel(
radioButtons(
"city", "Select a city",
choices = c("Washington", "New York", "Los Angeles", "Chicago")
),
checkboxInput("forecast", "Highlight forecasted data", value = FALSE)
),
mainPanel( plotOutput("plot") )
)
),
server = function(input, output, session) {
output$plot = renderPlot({
g = d %>%
filter(city %in% input$city) %>%
ggplot(aes(x=time, y=temperature, color=city)) +
geom_line()
if (input$forecast) {
g = g + geom_rect(inherit.aes = FALSE,
data = d %>%
filter(forecast) %>%
group_by(forecast) %>%
summarize(xmin = min(time)),
aes(xmin=xmin),
ymin = -Inf, ymax = Inf, xmax=Inf,
alpha = 0.25, color = NA, fill = "yellow"
)
}
g
})
}
)
With these additions, what should our reactive graph look like now?
With these additions, what should our reactive graph look like now?
Starting with the demo 2 code in exercises/ex3.R
add a tableOutput()
to the app’s mainPanel()
.
Once you have done that you should then add logic to the server
function to render a table that shows the daily min and max temperature for each day of the week.
renderTable()
lubridate::wday()
is likely to be useful along with group_by()
& summarize()
Another (more detailed) way of seeing the reactive graph (dynamically) for your app is using the reactlog
package.
Run the following to log and show all of the reactive events occuring within ex3_soln.R
,
demos/demo3.R
library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
d_vars = d %>%
select(where(is.numeric)) %>%
names()
shinyApp(
ui = fluidPage(
titlePanel("Weather Forecasts"),
sidebarLayout(
sidebarPanel(
radioButtons(
"city", "Select a city",
choices = c("Washington", "New York", "Los Angeles", "Chicago")
),
selectInput(
"var", "Select a variable",
choices = d_vars, selected = "temperature"
)
),
mainPanel(
plotOutput("plot"),
tableOutput("minmax")
)
)
),
server = function(input, output, session) {
output$plot = renderPlot({
d %>%
filter(city %in% input$city) %>%
ggplot(aes(x=time, y=.data[[input$var]], color=city)) +
ggtitle(input$var) +
geom_line()
})
output$minmax = renderTable({
d %>%
filter(city %in% input$city) %>%
mutate(
day = lubridate::wday(time, label = TRUE, abbr = FALSE),
date = as.character(lubridate::date(time))
) %>%
group_by(date, day) %>%
summarize(
`min` = min(.data[[input$var]]),
`max` = max(.data[[input$var]]),
.groups = "drop"
)
})
}
)
.data
& .env
These are an excellent option for avoiding some of the complexity around NSE with rlang
(e.g. {{
, !!
, enquo()
, etc.) when working with functions built with the tidy eval framework (e.g. dplyr
and ggplot2
).
.data
retrieves data-variables from the data frame..env
retrieves env-variables from the environment.
For more details see the rlang .data and .env pronouns article.
reactive()
& observe()
demos/demo3.R
library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
d_vars = d %>%
select(where(is.numeric)) %>%
names()
shinyApp(
ui = fluidPage(
titlePanel("Weather Forecasts"),
sidebarLayout(
sidebarPanel(
radioButtons(
"city", "Select a city",
choices = c("Washington", "New York", "Los Angeles", "Chicago")
),
selectInput(
"var", "Select a variable",
choices = d_vars, selected = "temperature"
)
),
mainPanel(
plotOutput("plot"),
tableOutput("minmax")
)
)
),
server = function(input, output, session) {
output$plot = renderPlot({
d %>%
filter(city %in% input$city) %>%
ggplot(aes(x=time, y=.data[[input$var]], color=city)) +
ggtitle(input$var) +
geom_line()
})
output$minmax = renderTable({
d %>%
filter(city %in% input$city) %>%
mutate(
day = lubridate::wday(time, label = TRUE, abbr = FALSE),
date = as.character(lubridate::date(time))
) %>%
group_by(date, day) %>%
summarize(
`min` = min(.data[[input$var]]),
`max` = max(.data[[input$var]]),
.groups = "drop"
)
})
}
)
demos/demo4.R
library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
d_vars = d %>%
select(where(is.numeric)) %>%
names()
shinyApp(
ui = fluidPage(
titlePanel("Weather Forecasts"),
sidebarLayout(
sidebarPanel(
radioButtons(
"city", "Select a city",
choices = c("Washington", "New York", "Los Angeles", "Chicago")
),
selectInput(
"var", "Select a variable",
choices = d_vars, selected = "temperature"
)
),
mainPanel(
plotOutput("plot"),
tableOutput("minmax")
)
)
),
server = function(input, output, session) {
d_city = reactive({
d %>%
filter(city %in% input$city)
})
output$plot = renderPlot({
d_city() %>%
ggplot(aes(x=time, y=.data[[input$var]], color=city)) +
ggtitle(input$var) +
geom_line()
})
output$minmax = renderTable({
d_city() %>%
mutate(
day = lubridate::wday(time, label = TRUE, abbr = FALSE),
date = as.character(lubridate::date(time))
) %>%
group_by(date, day) %>%
summarize(
`min` = min(.data[[input$var]]),
`max` = max(.data[[input$var]]),
.groups = "drop"
)
})
}
)
Are an example of a “reactive conductor” as they exist in between sources (e.g. an input) and endpoints (e.g. an output).
As such a reactive()
depends on various upstream inputs and can be used to generate output.
Their primary use is similar to a function in an R script, they help to
avoid repeating yourself
decompose complex computations into smaller / more modular steps
can improve computational efficiency by breaking up / simplifying reactive dependencies
reactive()
tipsCode written similarly to render*()
functions
If react_obj = reactive({...})
then any consumer must access value using react_obj()
and not react_obj
think of react_obj
as a function that returns the current value
Common cause of everyone’s my favorite R error ,
## Error: object of type 'closure' is not subsettable`
Like input
s reactive expressions may only be used within a reactive context (e.g. render*()
, reactive()
, observer()
, etc.)
## Error: Operation not allowed without an active reactive context. (You tried to do something that can only be done from inside a reactive expression or observer.)
observer()
These are constructed in the same way as a reactive()
however an observer does not return a value, as such they are used for their side effects.
The side effects in most cases involve sending data to the client broswer, e.g. updating a UI element
While not obvious given their syntax - the results of the render*()
functions are observers.
demos/demo5.R
library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
d_vars = d %>%
select(where(is.numeric)) %>%
names()
shinyApp(
ui = fluidPage(
titlePanel("Weather Forecasts"),
sidebarLayout(
sidebarPanel(
selectInput(
"state", "Select a state",
choices = sort(unique(d$state))
),
selectInput(
"city", "Select a city",
choices = c(),
multiple = TRUE
),
selectInput(
"var", "Select a variable",
choices = d_vars, selected = "temperature"
)
),
mainPanel(
plotOutput("plot"),
tableOutput("minmax")
)
)
),
server = function(input, output, session) {
d_city = reactive({
req(input$city)
d %>%
filter(city %in% input$city)
})
observe({
cities = d %>%
filter(state %in% input$state) %>%
pull(city) %>%
unique() %>%
sort()
updateSelectInput(
inputId = "city",
choices = cities
)
})
output$plot = renderPlot({
d_city() %>%
ggplot(aes(x=time, y=.data[[input$var]], color=city)) +
ggtitle(input$var) +
geom_line()
})
output$minmax = renderTable({
d_city() %>%
mutate(
day = lubridate::wday(time, label = TRUE, abbr = FALSE),
date = as.character(lubridate::date(time))
) %>%
group_by(date, day) %>%
summarize(
`min` = min(.data[[input$var]]),
`max` = max(.data[[input$var]]),
.groups = "drop"
)
})
}
)
req()
You may have notices that the App initializes with Arizona selected for the state but no initial selection for the city. Due to this we have some warnings generated initially:
This can be a common occurrence, particularly at initialization (or if a user enters bad / unexpected input).
A good way to protect against this is to validate inputs - the simplest way is to use req()
which checks if a value is truthy. Non-truthy values prevent further execution of the reactive code (and downstream consumer’s code).
More detailed validation and error reporting is possible with validate()
and need()
.
Using the code from demo 4 as a starting point add another observer to the app that updates the selectInput()
for var
such that any variables that are constant (0 variance), for the currently selected cities, are removed.
For example, many cities (e.g. Mesa, AZ) will report a visibility of 10 miles for every hour of the forecast - therefore this variable should not be selectable from the var
input.
Hint - think about what inputs / reactives would make the most sense to use for this.
bindEvent()
For both observers and reactive expressions Shiny will automatically determine reactive dependencies for you - in some cases this is not what we want.
To explicitly control the reactive dependencies of reactive expressions, render functions, and observers we can modify them using bindEvent()
where the dependencies are explicitly provided via the ...
argument.
Similar effects can be achieved via observeEvent()
/ eventReactive()
but these have been soft deprecated as of Shiny 1.6.
Note - when binding a reactive you must use the functional form, i.e. react()
and not react
downloadButton()
& downloadHandler()
These are the UI and server components needed for downloading a file from your Shiny app. The downloaded file can be of any arbitrary type and content.
downloadButton()
is a special case of an actionButton()
with specialized server syntax.
Specifically, within the server definition the downloadHandler()
is attached to the button’s id via output
, e.g.
The handler then defines the filename
function for generating a default filename and content
function for writing the download file’s content to a temporary file, which can then be served by Shiny for downloading.
demos/demo6.R
library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
d_vars = d %>%
select(where(is.numeric)) %>%
names()
shinyApp(
ui = fluidPage(
titlePanel("Weather Forecasts"),
sidebarLayout(
sidebarPanel(
selectInput(
"state", "Select a state",
choices = sort(unique(d$state))
),
selectInput(
"city", "Select a city",
choices = c(),
multiple = TRUE
),
selectInput(
"var", "Select a variable",
choices = d_vars, selected = "temperature"
),
downloadButton("download")
),
mainPanel(
plotOutput("plot"),
tableOutput("minmax")
)
)
),
server = function(input, output, session) {
output$download = downloadHandler(
filename = function() {
paste0(
paste(input$city,collapse="_"),
".csv"
)
},
content = function(file) {
readr::write_csv(d_city(), file)
}
)
d_city = reactive({
req(input$city)
d %>%
filter(city %in% input$city)
})
observe({
cities = d %>%
filter(state %in% input$state) %>%
pull(city) %>%
unique() %>%
sort()
updateSelectInput(
inputId = "city",
choices = cities
)
})
output$plot = renderPlot({
d_city() %>%
ggplot(aes(x=time, y=.data[[input$var]], color=city)) +
ggtitle(input$var) +
geom_line()
})
output$minmax = renderTable({
d_city() %>%
mutate(
day = lubridate::wday(time, label = TRUE, abbr = FALSE),
date = as.character(lubridate::date(time))
) %>%
group_by(date, day) %>%
summarize(
`min` = min(.data[[input$var]]),
`max` = max(.data[[input$var]]),
.groups = "drop"
)
})
}
)
These are a popup window element that allow us to present important messages (e.g. warnings or errors) or other UI elements in a way that does not permanently clutter up the main UI of an app.
The modal dialog consists of a number of Shiny UI elements (static or dynamic) and only displays when it is triggered (usually by something like an action button or link).
They differ from other UI elements we’ve seen so far as they are usually defined within an app’s server component not the ui.
demos/demo7.R
library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
d_vars = d %>%
select(where(is.numeric)) %>%
names()
shinyApp(
ui = fluidPage(
titlePanel("Weather Forecasts"),
sidebarLayout(
sidebarPanel(
selectInput(
"state", "Select a state",
choices = sort(unique(d$state))
),
selectInput(
"city", "Select a city",
choices = c(),
multiple = TRUE
),
selectInput(
"var", "Select a variable",
choices = d_vars, selected = "temperature"
),
actionButton("download_modal", "Download")
),
mainPanel(
plotOutput("plot"),
tableOutput("minmax")
)
)
),
server = function(input, output, session) {
observe({
showModal(modalDialog(
title = "Download data",
checkboxGroupInput(
"dl_vars", "Select variables to download",
choices = names(d), selected = names(d), inline = TRUE
),
footer = list(
downloadButton("download"),
modalButton("Cancel")
)
))
}) %>%
bindEvent(input$download_modal)
output$download = downloadHandler(
filename = function() {
paste0(
paste(input$city,collapse="_"),
".csv"
)
},
content = function(file) {
readr::write_csv(
d_city() %>%
select(input$dl_vars),
file
)
}
)
d_city = reactive({
req(input$city)
d %>%
filter(city %in% input$city)
})
observe({
cities = d %>%
filter(state %in% input$state) %>%
pull(city) %>%
unique() %>%
sort()
updateSelectInput(
inputId = "city",
choices = cities
)
})
output$plot = renderPlot({
d_city() %>%
ggplot(aes(x=time, y=.data[[input$var]], color=city)) +
ggtitle(input$var) +
geom_line()
})
output$minmax = renderTable({
d_city() %>%
mutate(
day = lubridate::wday(time, label = TRUE, abbr = FALSE),
date = as.character(lubridate::date(time))
) %>%
group_by(date, day) %>%
summarize(
`min` = min(.data[[input$var]]),
`max` = max(.data[[input$var]]),
.groups = "drop"
)
})
}
)
fileInput()
demos/demo8.R
library(tidyverse)
library(shiny)
shinyApp(
ui = fluidPage(
titlePanel("Temperature Forecasts"),
sidebarLayout(
sidebarPanel(
fileInput("upload", "Upload a file")
),
mainPanel(
tableOutput("widget"),
tableOutput("data")
)
)
),
server = function(input, output, session) {
output$widget = renderTable({
input$upload
})
output$data = renderTable({
readr::read_csv(input$upload$datapath)
})
}
)
fileInput()
widgetThis widget behaves a bit differently than the others we have seen - once a file is uploaded it returns a data frame with one row per file and the columns,
name
- the original filename (from the client’s system)
size
- file size in bytes
type
- file mime type, usually determined by the file extension
datapath
- location of the temporary file on the server
Given this data frame your app’s server code is responsible for the actual process of reading in the uploaded file.
fileInput()
hintsinput$upload
will default to NULL
when the app is loaded, using req(input$upload)
for downstream consumers is a good idea
Files in datapath
are temporary and should be treated as ephemeral
type
is at best a guess - validate uploaded files and write defensive code
The accept
argument helps to limit file types but cannot prevent bad uploads
Starting with the app version from Demo 5 (code available in exercises/ex5.R
) replace the preloading of the weather data (d
and d_vars
) with reactive versions that are populated via a fileInput()
widget.
You should then be able to get the same app behavior as before once data/weather.csv
is uploaded. You can also check that your app works with the data/sedona.csv
and data/gayload.csv
datasets as well.
Hint - remember that anywhere that uses either d
or d_vars
will now need to use d()
and d_vars()
instead.
RStudioConf 2022 - Getting Started with Shiny