# install.packages('pak')
::pak('mjfrigaard/shinypak') pak
2 Shiny
This chapter briefly reviews programming with Shiny’s reactive model and how it differs from regular R programming. Then, I’ll cover some of the unique behaviors of Shiny app projects (and why you might consider adopting them if you haven’t already).
2.1 Shiny apps
Reactivity is the underlying process that allows Shiny apps to update and respond to user interactions automatically. Developing Shiny apps involves harnessing the connection between inputs, reactivity, and outputs to control and predict the application’s behavior.
Shiny programming differs from regular R programming in several key aspects:
An Event-driven UI: Shiny apps require developers to design and develop a user interface (UI). User experience (UX) design is an entirely separate field, but as Shiny developers, we need to know enough to allow users to interact with and navigate our apps. The UI also captures each ‘event,’ meaning that the user’s actions (such as button clicks or input changes) trigger the application’s inputs, updates, or outputs.
- Regular R programming often involves executing predefined steps or functions without direct interaction or responses to user events.
A Reactive Server: In Shiny, the application’s behavior is determined by the dependencies between reactive inputs (i.e., the
inputId
s), reactive values, and outputs (i.e., theoutputId
s), allowing for automatic updates and propagation of changes throughout the application.- In standard R programming, we typically define a series of sequential steps (i.e., functions) that operate on data to generate output to the console or a typesetting system for technical and scientific publications (model results, graphs, tables, etc.) without accounting for reactivity or downstream changes.
Learning reactivity can be challenging when you start, but fortunately, there are excellent tutorials and articles to help you along the way!
Launch app with the shinypak
package:
launch('02.1_shiny-app')
If you’re creating a new application using the New Project Wizard, you’ll see the following:
Select the location of your Shiny app project, then pick a name and decide whether you want to use Git or renv
(I’ll be using Git).
After clicking Create Project, a new session will open with your project files.
Note that the only items in the new Shiny app project are app.R
and the sap.Rproj
file.
sap/
├── app.R
└── sap.Rproj
1 directory, 2 files
2.1.1 Boilerplate app.R
app.R
initially contains a boilerplate application, which we can launch by clicking on the Run App button:
The boilerplate ‘Old Faith Geyser Data’ app is a perfect example of what Shiny can do with a single app.R
file, but we’ll want to exchange this code for a slightly more advanced application (because most Shiny apps grow beyond an app.R
file).
2.2 Movie review data app
Most Shiny applications move beyond a single app.R
file. Knowing how to store any utility functions, data, documentation, and metadata will set you up for success as you transition to storing your app in an R package.
Launch app with the shinypak
package:
launch('02.2_movies-app')
I’m going to work through an example of some intermediate/advanced Shiny app features using the application from the Building Web Applications with Shiny course. This app is a great use case for the following reasons:
It has multiple input types that are collected in the UI
The graph output can be converted to a utility function
The app loads an external data file when it’s launched
The code is accessible (and comes from a trusted source)
2.2.1 App
The code below replaces the boilerplate ‘Old Faith Geyser Data’ app in app.R
:
2.2.1.1 app.R
show/hide movie review Shiny app
<- shiny::fluidPage(
ui theme = shinythemes::shinytheme("spacelab"),
::sidebarLayout(
shiny::sidebarPanel(
shiny::selectInput(
shinyinputId = "y",
label = "Y-axis:",
choices = c(
"IMDB rating" = "imdb_rating",
"IMDB number of votes" = "imdb_num_votes",
"Critics Score" = "critics_score",
"Audience Score" = "audience_score",
"Runtime" = "runtime"
),selected = "audience_score"
),::selectInput(
shinyinputId = "x",
label = "X-axis:",
choices = c(
"IMDB rating" = "imdb_rating",
"IMDB number of votes" = "imdb_num_votes",
"Critics Score" = "critics_score",
"Audience Score" = "audience_score",
"Runtime" = "runtime"
),selected = "critics_score"
),::selectInput(
shinyinputId = "z",
label = "Color by:",
choices = c(
"Title Type" = "title_type",
"Genre" = "genre",
"MPAA Rating" = "mpaa_rating",
"Critics Rating" = "critics_rating",
"Audience Rating" = "audience_rating"
),selected = "mpaa_rating"
),::sliderInput(
shinyinputId = "alpha",
label = "Alpha:",
min = 0, max = 1,
value = 0.4
),::sliderInput(
shinyinputId = "size",
label = "Size:",
min = 0, max = 5,
value = 3
),::textInput(
shinyinputId = "plot_title",
label = "Plot title",
placeholder = "Enter text to be used as plot title"
),::actionButton(
shinyinputId = "update_plot_title",
label = "Update plot title"
)
),::mainPanel(
shiny::br(),
shiny::p(
shiny"These data were obtained from",
::a("IMBD", href = "http://www.imbd.com/"), "and",
shiny::a("Rotten Tomatoes", href = "https://www.rottentomatoes.com/"), "."
shiny
),::p(
shiny"The data represent",
nrow(movies),
"randomly sampled movies released between 1972 to 2014 in the United States."
),::plotOutput(outputId = "scatterplot"),
shiny::hr(),
shiny::p(shiny::em(
shiny"The code for this Shiny application comes from",
::a("Building Web Applications with shiny",
shinyhref = "https://rstudio-education.github.io/shiny-course/"
)
))
)
)
)
<- function(input, output, session) {
server <- shiny::reactive({
new_plot_title ::toTitleCase(input$plot_title)
tools|>
}) ::bindEvent(input$update_plot_title,
shinyignoreNULL = FALSE,
ignoreInit = FALSE
)
$scatterplot <- shiny::renderPlot({
outputscatter_plot(
df = movies,
x_var = input$x,
y_var = input$y,
col_var = input$z,
alpha_var = input$alpha,
size_var = input$size
+
) ::labs(title = new_plot_title()) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
ggplot2
})
}
::shinyApp(ui = ui, server = server) shiny
2.2.2 Additional files
I’ve added the scatter_plot()
utility function in a new utils.R
file:
2.2.2.1 utils.R
show/hide scatter_plot()
<- function(df, x_var, y_var, col_var, alpha_var, size_var) {
scatter_plot ::ggplot(data = df,
ggplot2::aes(x = .data[[x_var]],
ggplot2y = .data[[y_var]],
color = .data[[col_var]])) +
::geom_point(alpha = alpha_var, size = size_var)
ggplot2
}
2.2.2.2 movies.RData
The movies.RData
dataset contains reviews from IMDB and Rotten Tomatoes
You can download these data here
2.2.3 Updated movies app project contents
The sap
project now contains the following files:
sap/
├── app.R
├── movies.RData
├── sap.Rproj
└── utils.R
2 directories, 4 files
To run the movies
app, we need to load the data and source the utils.R
file by adding the code below to the top of the app.R
file:
After installing the packages below, add a comment (#
) in front of these lines.
I’ve placed the header below in the top of the app.R
file:
# install ------------------------------------
# after installing, comment this out
<- c("shiny", "shinythemes", "stringr", "ggplot2", "rlang")
pkgs install.packages(pkgs, verbose = FALSE)
# packages ------------------------------------
library(shiny)
library(shinythemes)
library(stringr)
library(ggplot2)
library(rlang)
# data -----------------------------------------
load("movies.RData")
# utils ----------------------------------------
source("utils.R")
- 1
-
Comment out these lines after installing
pkgs
Clicking on Run App displays the movie review app:
2.3 Project folders
Now that we have a slightly more complex application in app.R
, I’ll add a few project folders we can include in our project that have unique built-in behaviors. These folders will help organize your files and make additional resources available to your app.
Launch app with the shinypak
package:
launch('02.3_proj-app')
2.3.1 R/
If your Shiny app relies on utility or helper functions (outside the app.R
file), you can place this code in an R/
folder. Any .R
files in the R/
folder will be automatically sourced when the application is run.
The function that makes this process (i.e., sourcing any .R
files in an R/
folder) possible is loadSupport()
. We’ll return to this function in a later chapter, because the R/
folder has a similar behavior (but different function) in R packages.1
2.3.2 www/
When you run a Shiny application, any static files (i.e., resources) under a www/
directory will automatically be made available within the application. This folder stores images, CSS or JavaScript files, and other static resources.
Following the conventional folder structure will also help set you up for success when/if you decide to convert it into an app-package.
2.4 Project files
2.4.1 README.md
Including a README.md
file in your root folder is a good practice for any project. README.md
should contain relevant documentation for running app.R
.
2.4.2 DESCRIPTION
DESCRIPTION
files play an essential role in R packages, but they are also helpful in Shiny projects if I want to deploy the app in showcase mode.
It’s always a good idea to leave at least one <empty final line>
in your DESCRIPTION
file.
After adding README.md
and a DESCRIPTION
file (listing DisplayMode: Showcase
), the movies app will display the code and documentation when the app launches.2
2.5 Project code
The following two items are considered best practices because they make your app more scalable by converting app.R
into functions.
2.5.1 Modules
Shiny modules are a ‘pair of UI and server functions’ designed to compartmentalize input and output IDs into distinct namespaces,
‘…a namespace is to an ID as a directory is to a file…’ -
shiny::NS()
help file.
Module UI functions typically wrap the layout, input, and output functions in shiny::tagList()
. Module server functions typically contain the ‘backend’ code in a Shiny server function. Both the UI and server module functions are linked by an id
argument, which is created using shiny::NS()
(namespace) in the UI function and called in the server function with shiny::moduleServer()
.
2.5.1.1 Variable inputs module
mod_var_input_ui()
creates a dedicated namespace for the inputId
s with shiny::NS()
:
2.5.1.1.1 R/mod_var_input.R
show/hide mod_var_input_ui()
<- function(id) {
mod_var_input_ui <- shiny::NS(id)
ns ::tagList(
shiny::selectInput(
shinyinputId = ns("y"),
label = "Y-axis:",
choices = c(
"IMDB rating" = "imdb_rating",
"IMDB number of votes" = "imdb_num_votes",
"Critics Score" = "critics_score",
"Audience Score" = "audience_score",
"Runtime" = "runtime"
),selected = "audience_score"
),::selectInput(
shinyinputId = ns("x"),
label = "X-axis:",
choices = c(
"IMDB rating" = "imdb_rating",
"IMDB number of votes" = "imdb_num_votes",
"Critics Score" = "critics_score",
"Audience Score" = "audience_score",
"Runtime" = "runtime"
),selected = "imdb_rating"
),::selectInput(
shinyinputId = ns("z"),
label = "Color by:",
choices = c(
"Title Type" = "title_type",
"Genre" = "genre",
"MPAA Rating" = "mpaa_rating",
"Critics Rating" = "critics_rating",
"Audience Rating" = "audience_rating"
),selected = "mpaa_rating"
),::sliderInput(
shinyinputId = ns("alpha"),
label = "Alpha:",
min = 0, max = 1, step = 0.1,
value = 0.5
),::sliderInput(
shinyinputId = ns("size"),
label = "Size:",
min = 0, max = 5,
value = 2
),::textInput(
shinyinputId = ns("plot_title"),
label = "Plot title",
placeholder = "Enter plot title"
)
) }
- 1
-
y
axis numeric variable - 2
-
x
axis numeric variable - 3
-
z
axis categorical variable
- 4
-
alpha
numeric value for points
- 5
-
size
numeric value for size
- 6
-
plot_title
text
mod_var_input_server()
returns these values in a reactive list with shiny::reactive()
:
show/hide mod_var_input_server()
<- function(id) {
mod_var_input_server
::moduleServer(id, function(input, output, session) {
shinyreturn(
reactive({
list(
"y" = input$y,
"x" = input$x,
"z" = input$z,
"alpha" = input$alpha,
"size" = input$size,
"plot_title" = input$plot_title
)
})
)
}) }
- 1
-
y
axis numeric variable - 2
-
x
axis numeric variable - 3
-
z
axis categorical variable
- 4
-
alpha
numeric value for points
- 5
-
size
numeric value for size
- 6
-
plot_title
text
2.5.1.2 Scatter-plot display module
mod_scatter_display_ui()
creates a dedicated namespace for the plot outputId
(as "scatterplot"
), along with some help text:
2.5.1.2.1 R/mod_scatter_display.R
show/hide mod_scatter_display_ui()
<- function(id) {
mod_scatter_display_ui <- shiny::NS(id)
ns ::tagList(
shiny::tags$br(),
shiny::tags$blockquote(
shiny::tags$em(
shiny::tags$h6("The data for this application comes from the ",
shiny::tags$a("Building web applications with Shiny",
shinyhref = "https://rstudio-education.github.io/shiny-course/"),
"tutorial"))
),::plotOutput(outputId = ns("scatterplot"))
shiny
) }
- 1
-
Namespaced module
id
for plot in UI
The code to render the output$scatterplot
is contained in the nested call to shiny::moduleServer()
in mod_scatter_display_server()
:
After 1) loading the movies
data, 2) assembling the returned values from mod_var_input_server()
, and creating the input()
reactive, 3) the scatter_plot()
utility function creates the plot
object and adds the plot_title()
and theme:
show/hide mod_scatter_display_server()
<- function(id, var_inputs) {
mod_scatter_display_server ::moduleServer(id, function(input, output, session) {
shiny
load("movies.RData")
<- shiny::reactive({
inputs <- tools::toTitleCase(var_inputs()$plot_title)
plot_title list(
x = var_inputs()$x,
y = var_inputs()$y,
z = var_inputs()$z,
alpha = var_inputs()$alpha,
size = var_inputs()$size,
plot_title = plot_title
)
})$scatterplot <- shiny::renderPlot({
output<- scatter_plot(
plot df = movies,
x_var = inputs()$x,
y_var = inputs()$y,
col_var = inputs()$z,
alpha_var = inputs()$alpha,
size_var = inputs()$size
)+
plot ::labs(
ggplot2title = inputs()$plot_title,
x = stringr::str_replace_all(
::toTitleCase(
toolsinputs()$x),
"_", " "),
y = stringr::str_replace_all(
::toTitleCase(
toolsinputs()$y),
"_", " ")
+
) ::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
ggplot2
})
}) }
- 1
-
loading the
movies
data - 2
-
assembling the returned values from
mod_var_input_server()
, and creating theinput()
reactive - 3
-
scatter_plot()
utility function creates theplot
object - 4
-
adds the
plot_title()
- 5
-
add
theme
to layers
Both UI and server module functions are combined into a single .R
file, and all modules are placed in the R/
folder so they are sourced when the application is run.
R/
├── mod_scatter_display.R
├── mod_var_input.R
└── utils.R
2.5.2 Standalone app function
Both module functions are combined in the ui
and server
arguments of shiny::shinyApp()
. The id
arguments ("vars"
and "plot"
) connect the UI functions to their server counterparts, and the output from mod_var_input_server()
is the var_inputs
argument in mod_scatter_display_server()
.
The call to shiny::shinyApp()
is wrapped in the launch_app()
function and placed in app.R
.
2.5.2.0.1 app.R
show/hide launch_app() in app.R
# install ------------------------------------
# after installing, comment this out
<- c("shiny", "shinythemes", "stringr", "ggplot2", "rlang")
pkgs install.packages(pkgs, verbose = FALSE)
# packages ------------------------------------
library(shiny)
library(shinythemes)
library(stringr)
library(ggplot2)
library(rlang)
<- function() {
launch_app ::shinyApp(
shinyui = shiny::fluidPage(
::titlePanel(
shiny::div(
shiny::img(
shinysrc = "shiny.png",
height = 60,
width = 55,
style = "margin:10px 10px"
), "Movies Reviews"
)
),::sidebarLayout(
shiny::sidebarPanel(
shinymod_var_input_ui("vars")
),::mainPanel(
shinymod_scatter_display_ui("plot")
)
)
),server = function(input, output, session) {
<- mod_var_input_server("vars")
selected_vars
mod_scatter_display_server("plot", var_inputs = selected_vars)
}
)
}launch_app()
- 1
- Header (comment this out after the packages are installed)
- 2
-
Load packages
- 3
- Variable input UI module
- 4
- Graph display UI module
- 5
- Variable input server module
- 6
- Graph display server module
Now, I can run the app with launch_app()
.
The deployed files of sap
are below:
sap/ # 02.3_proj-app branch
├── DESCRIPTION
├── R/
│ ├── mod_scatter_display.R
│ ├── mod_var_input.R
│ └── utils.R
├── README.md
├── app.R
├── movies.RData
├── sap.Rproj
├── rsconnect/
│ └── shinyapps.io/
│ └── user/
│ └── sap.dcf
└── www/
└── shiny.png
6 directories, 10 files
The rsconnect/
folder has been removed from the 02.3_proj-app
branch.
2.6 Additional features
Below are two additional ‘optional’ features that can be included with your Shiny application (I consider these ‘optional’ because they’re use depends on the specific needs and environment for each application).
2.6.1 Global variables and/or functions
Placing a global.R
file in your root folder (or in the R/
directory) causes this file to be sourced only once when the Shiny app launches, rather than each time a new user connects to the app. global.R
is commonly used for initializing variables, loading libraries, loading large data sets and/or performing initial calculations.
global.R
can be used to maintain efficiency and consistency across application sessions.
2.6.2 Project dependencies with renv
If you use renv
, keep track of your dependencies by regularly running renv::status()
and renv::snapshot()
.
2.7 Recap
This chapter has covered some differences between developing Shiny apps and regular R programming, creating new Shiny projects in Posit Workbench, and some practices to adopt that can make the transition to app-packages a little easier. The code used in this chapter is stored in the sap
repository.
In the next chapter, I’ll cover what makes a package a package, and some do’s and don’ts when converting a developed Shiny application into an R package.
Shiny introduced these features in version 1.3.2.9001, and you can read more about them in the section titled, ‘The
R/
directory’ in App formats and launching apps↩︎