These EDS 430 (Intro to Shiny) materials are no longer maintained!
Please visit the new course website if you’re looking for the most up-to-date versions of these slides
Thank you
EDS 430 - Intro to
Building reactive apps & dashboards
Published: Oct 10, 2022
Last updated: Jul 05, 2024
Sam Csik |
Data Training Coordinator
National Center for Ecological Analysis & Synthesis
Master of Environmental Data Science |
Bren School of Environmental Science & Management
Prerequisites
This workshop assumes that participants have the following:
Table of Contents
We’re going to pack a lot into two days. Here’s what to expect:
(1) High-level overview of Shiny
What is Shiny? ~ Anatomy of a shiny app ~ Where to find examples
(2) Building shiny apps & dashboards
Setting up your repo & files ~ App #1 (single-file app) ~ App #2 (two-file app) ~ App #3 (shinydashboard) ~ Deploying apps
(3) Beautifying your user interface (UI)
Custom themes with bslib
~ Custom themes with fresh
~ Styling with CSS & Sass
(4) Improving your app’s user experience (UX)
Important UX considerations ~ Web accessibility
Debugging approaches ~ Testing apps
Writing functions ~ Shiny modules
(7) Wrap-up
Shiny alternatives ~ Words of wisdom ~ More resources
See the source code
You can reference the source code of all the apps we’ll be building/playing with throughout this workshop on GitHub.
Part 1: High-level overview of Shiny
What is Shiny?
Anatomy of a shiny app
Where to find examples
What is Shiny?
Shiny makes building web apps easy
“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.” - RStudio
Worldbank-Shiny app to visualize fertility rate vs. life expectancy from 1960 to 2015, by Andre Duarte
The anatomy of a Shiny app
The basic anatomy of a Shiny app
Shiny apps are composed in two parts: (1) a web page that displays the app to a user (i.e. the user interface, or UI for short), and (2) a computer that powers the app (i.e. the server)
The UI controls the layout and appearance of your app and is written in HTML (except we use functions from the shiny package to write that HTML). The server handles the logic of the app – in other words, it is a set of instructions that tells the webpage what to display when a user interacts with it.
Widgets are web elements that users can interact with via the UI
Widgets collect information from the user which is then used to update outputs created in the server.
Shiny comes with a set of of standard widgets (see left), but you can also explore widget extensions using a variety of other packages (e.g. {shinyWidgets}
, {DT}
, {plotly}
)
Reactivity: a brief intro
Reactivity is what makes Shiny apps responsive i.e. it lets the app instantly update itself whenever the user makes a change. At a very basic level, it looks something like this:
Check out Garrett Grolemund’s article, How to understand reactivity in R for a more detailed overview of Shiny reactivity.
Can I see an example please?
Example shiny apps built by some familiar folks
HydroTech Helper (video tutorial), by MEDS 2022 alumn, Daniel Kerstan, developed during his time as a USGS Hydrologic Technician – access real-time monitoring of USGS hydrology sites and equipment
Moorea Coral Reef LTER Shiny Application (source code), by MEDS 2022 alumni, Allie Cole, Felicia Cruz, Jake Eisaguirre & Charles Henrickson as part of their MEDS capstone project – visualize spatial and temporal patterns of coral reef stressors surrounding Moorea, French Polynesia
Marine Mammal Bycatch Impacts Exploration Tool (source code) by Dr. Megsie Siple and colleagues – compute population projections under different bycatch mortality levels
Novel-gazing (source code) by Dr. Megsie Siple – a fun app for exploring your Goodreads data, inspired by community ecology
The Shiny packages comes with 11 built-in examples
Check out the available Shiny app examples by running this code in your console:
Run the first example, which plots R’s built-in faithful
data set with a configurable number of bins:
Change the number of bins using the sliderInput
widget and watch the histogram re-render.
These working examples also come paired with source code for you to see how the app is built. For example, the sliderInput
is built with the following code:
Now let’s build our own!
Setting up your Shiny app
Create your GitHub repo
Let’s start by creating a GitHub repo to house our soon-to-be app(s), then we’ll clone our repo to our computer. I’m using RStudio to clone my repo in the example below, but you can also do this via the command line using git clone <repo-url>
.
Shiny app repo structure
Not much is required to make a functional app (which is awesome) – for a basic app, you really just need an app.R
file where you’ll write the code for your UI and server. To stay organized, we’ll place app.R
into a subdirectory (e.g. /myapp
), which will also house any dependencies (e.g. other scripts/files/etc.) used by app.R
.
All Shiny apps begin (in almost) the same way
You have the option of creating either a single-file app or two-file app, and they look nearly the same (we’ll see both formats in the coming slides).
Why two options? Before v0.10.2, Shiny apps needed to be split into two separate files, ui.R
and server.R
, that defined the UI and server components, respectively. With v0.10.2+, users can create a single-file app, app.R
, which contains both the UI and server components together. While it largely comes down to personal preference, a single-file format is best for smaller apps or when creating a reprex, while the two-file format is beneficial when writing large, complex apps where breaking apart code can make things a bit more navigable/maintainable.
Create a single-file Shiny app
You can create a single-file app using RStudio’s built-in Shiny app template (e.g. File > New Project… > New Directory > Shiny Application), but it’s just as easy to create it from scratch (and you’ll memorize the structure faster!). Let’s do that now.
1. In your project repo, create a subdirectory to house your app – I’m calling mine, single-file-app
.
2. Create a new R script inside /single-file-app
and name it app.R
– you must name your script app.R
. Copy/type the following code into app.R
, or use the shinyapp
snippet to automatically generate a shiny app template.
Tip: Use code sections (denoted by # some text ----
) to make navigating different sections of your app code a bit easier. Code sections will appear in your document outline (find the button at the top right corner of the script/editor panel).
Run your app
Once you have saved your app.R
file, the “Run” code button should turn into a “Run App” button that looks like: . Click that button to run your app (alternatively, run runApp("directory-name")
in your console – for me, that looks like, runApp("single-file-app")
)!
You won’t see much yet, as we have only built a blank app (but a functioning app, nonetheless!). In your RStudio console, you should see something like: Listening on http://127.0.0.1:XXXX
, which is the URL where your app can be found. 127.0.0.1 is a standard address that means “this computer,” and the last four digits represent a randomly assigned port number. You can click the “Open in Browser” button, , to see how your app will appear when viewed in your web browser.
You should also notice a red stop sign, , appear in the top right corner of your console indicating that R is busy–this is because your R session is currently acting as your Shiny app server and listening for any user interaction with your app. Because of this, you won’t be able to run any commands in the console until you quit your app. Do so by pressing the stop button.
Create a two-file Shiny app
In practice, you will likely find yourself opting for the the two-file format – code expands quickly, even when building relatively small apps. This two-file approach (well, three if you use a global.R
file, which is encouraged) will help to keep your code a bit more manageable.
1. In your project repo, create a new subdirectory to house your app – I’m calling mine, two-file-app
.
2. Create two new R scripts inside /two-file-app
named ui.R
and server.R
– you must name your scripts ui.R
and server.R
. Copy the following code into the respective files. Note: When splitting your UI and server into separate files, you do not need to include the shinyApp(ui = ui, server = server)
line of code (as required in your single-file app).
Part 2: Building shiny apps & dashboards
Setting up your repo & files
App #1 (single-file app) - source code
App #2 (two-file app) - source code
App #3 (shinydashboard) - source code
Deploying (& redeploying) apps
Building out your 1st app
Learning Objectives - App #1 (single-file app)
By the end of building out this first app, you should be a bit more familiar with:
writing a single-file (app.R
) shiny app
adding and styling text in the UI using tags
practicing data wrangling and visualization outside of your shiny app
following a general workflow for building reactive apps, which includes adding inputs and outputs to the UI, then writing the server instructions on how to assemble user input values into outputs
running and quitting apps in RStudio
Packages introduced:
shiny: framework for building our reactive app + standard widgets
DT: interactive datatable widgets (that can be made reactive using shiny!)
tidyverse: collection of packages for wrangling & visualizing data
palmerpenguins: data
Roadmap for App #1
We’ll start by building a small single-file app using data from the palmerpenguins package. We’ll build out the the following features:
(a) A title and subtitle
(b) A slider widget for users to select a range of penguin body masses
(c) A reactive scatterplot that updates based on user-supplied values
Add text in the UI
We’ll do this in the UI within fluidPage()
, a layout function that sets up the basic visual structure of the page and scales components in real time to fill all available browser width. Add a title and subtitle to your app (be sure to separate each with a comma, ,
), save, and run:
Recall that the UI is actually just an HTML document. We can style our text by adding static HTML elements using tags
– a list of functions that parallel common HTML tags (e.g. <h1>
== tags$h1()
) The most common tags also have wrapper functions (e.g. h1()
).
What are inputs and outputs?
Next, we will begin to add some inputs and outputs to our UI inside fluidPage()
(anything that you put into fluidPage()
will appear in our app’s user interface…and we want inputs and outputs to show up there!).
Inputs (or widgets) are the things that users can interact with (e.g. toggle, slide) and provide values to your app. The input functions below correspond to the widgets you see on slide #9. Outputs are the R objects that your user sees (e.g. tables, plots) and are what respond when a user interacts with/changes an input value.
The shiny
package comes with a number of input and output functions, but you can extend these with additional packages (e.g. shinyWidgets
, plotly
, DT
, etc.; more on those later).
Examples of Input Functions:
actionButton()
checkboxInput()
checkboxGroupInput()
dateInput()
dateRangeInput()
radioButtons()
selectInput()
sliderInput()
textInput()
See a full list of shiny
input functions
Examples of Output Functions:
dataTableOutput()
(inserts an interactive table)
imageOutput()
(inserts an image)
plotOutput()
(inserts a plot)
tableOutput()
(inserts a table)
textOutput()
(inserts text)
See a full list of shiny
output functions
Adding our reactive plot
Next, we’ll create a scatterplot of penguin bill lengths vs. penguin flipper lengths using the penguins
data set from the {palmerpengiuns}
package. We will make this scatterplot reactive by adding a sliderInput
that allows users to filter the displayed data points by selecting a range of penguin body masses (e.g. only plot bill and flipper lengths for penguins with body masses ranging from 4,500 grams to 6,000 grams).
To create a reactive plot, we will follow these steps:
1. Add an input (e.g. sliderInput
) to the UI that users can interact with
2. Add an output (e.g. plotOutput
) to the UI that creates a placeholder space to fill with our eventual reactive output
3. Tell the server how to assemble inputs into outputs
Input function syntax
All input functions have the same first argument, inputId
(NOTE: Id
not ID
), which is used to connect the front end of your app (the UI) with the back end (the server). For example, if your UI has an inputId = "name"
, the server function will access that input value using the syntax input$name
. The inputId
has two constraints: (1) it must be a simple string containing only letters, numbers, and underscores, (2) it must be unique within your app.
Most input functions have a second parameter called label
, which is used to create a human-readable label for the control, which will appear in the UI.
The remaining arguments are unique to each input function. Oftentimes, these include a value
parameter, which lets you set the default value of your widget, where applicable.
A couple examples:
Check out the interactive Shiny Widgets Gallery to learn how to implement the most common widgets.
Step 1: Add an input to your app
First let’s add a sliderInput()
that will allow users to select a range of penguin body masses (g).
When you run your app, you should see something similar to the image below. It’s operable, but does not yet have an associated output.
Output function syntax
Outputs in the UI create placeholders which are later filled by the server function.
Similar to input functions, all output functions take the same first argument, outputId
(again, note Id
not ID
), which connects the front end UI with the back end server. For example, if your UI contains an output function with an outputId = "plot"
, the server function will access it (or in other words, know to place the plot in that particular placeholder) using the syntax output$plot
.
A couple examples:
Step 2: Add an output to your app
Let’s now add a plotOutput()
, which will be updated based on the user inputs via the sliderInput()
, then run the app.
~/one-file-app/app.R
# user interface ----
ui <- fluidPage(
# ~ previous code omitted for brevity ~
# body mass slider input ----
sliderInput(inputId = "body_mass_input", label = "Select a range of body masses (g):",
min = 2700, max = 6300, value = c(3000, 4000)),
# body mass plot ouput ----
plotOutput(outputId = "bodyMass_scatterPlot")
)
Okay, it looks like nothing changed?? Remember, *Output()
functions create placeholders, but we have not yet written the server instructions on how to fill and update those placeholders. We can inspect the HTML and see that there is, in fact, a placeholder area awaiting our eventual output, which will be a plot named “bodyMass_scatterPlot”:
Rendering outputs
Each *Output()
function in the UI is coupled with a render*()
function in the server, which contains the “instructions” for creating the output based on user inputs (or in other words, the instructions for making your output reactive).
Examples of *Output()
functions and their corresponding render*()
functions:
Output function | Render function |
---|---|
dataTableOutput() |
renderDataTable() |
imageOutput() |
renderImage() |
plotOutput() |
renderPlot() |
tableOutput() |
renderTable() |
textOutput() |
renderText() |
Step 3: Tell the server how to assemble inputs into outputs
Now that we’ve designed our input/output in the UI, we need to write the server instructions (i.e. write the server function) on how to use the input value(s) (i.e. penguin body mass range via a slider input) to update the output (scatter plot).
The server function is defined with two arguments, input
and output
, both of which are list-like objects. You must define both of these arguments within the server function. input
contains the values of all the different inputs at any given time, while output
is where you’ll save output objects to display in the app.
This part can be intimidating, but if you follow these three rules, you will successfully create reactivity within your shiny app!
Rules:
1. Save objects you want to display to output$<id>
2. Build reactive objects using a render*()
function
3. Access input values with input$<id>
Rule 1: Save objects you want to display to output$<id>
~/one-file-app/app.R
# load packages ----
library(shiny)
# user interface ----
ui <- fluidPage(
# ~ previous code omitted for brevity ~
# body mass slider ----
sliderInput(inputId = "body_mass_input", label = "Select a range of body masses (g):",
min = 2700, max = 6300, value = c(3000, 4000)),
# body mass plot output ----
plotOutput(outputId = "bodyMass_scatterPlot")
)
# server instructions ----
server <- function(input, output) {
# render the scatter plot ----
output$bodyMass_scatterPlot <- # code to generate plot here
}
In our UI, we created a placeholder for our plot using the plotOutput()
function and gave it the Id "bodyMass_scatterplot"
. In our server, we will save our plot to the output argument by its outputId
.
Note: In the UI, our outputId
is quoted ("bodyMass_scatterPlot"
), but not in the server (bodyMass_scatterPlot
).
Rule 2: Build reactive objects with render*()
Use the appropriate render*()
function to make your output reactive (e.g. if you have a plotOutput
in your UI, you will need to use renderPlot()
in your server). Within your render*()
, write any code inside a set of curly braces, {}
. This allows you to include as many lines of code as it takes to build your object.
~/one-file-app/app.R
# load packages ----
library(shiny)
# user interface ----
ui <- fluidPage(
# ~ previous code omitted for brevity ~
# body mass slider ----
sliderInput(inputId = "body_mass_input", label = "Select a range of body masses (g):",
min = 2700, max = 6300, value = c(3000, 4000)),
# body mass plot output ----
plotOutput(outputId = "bodyMass_scatterPlot")
)
# server instructions ----
server <- function(input, output) {
# render the scatter plot ----
output$bodyMass_scatterPlot <- renderPlot({
# code to generate plot here
})
}
An Aside: Draft objects (e.g. plots) in a separate script first
I find it easier to experiment and draft my objects (e.g. plots) first in a separate script, then copy the code over to the server after. I want to make a plot that looks like this:
~/scratch/practice-script-app1-penguins.R
# load packages
library(palmerpenguins)
library(tidyverse)
# create plot
ggplot(na.omit(penguins),
aes(x = flipper_length_mm, y = bill_length_mm,
color = species, shape = species)) +
geom_point() +
scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
scale_shape_manual(values = c("Adelie" = 19, "Chinstrap" = 17, "Gentoo" = 15)) +
labs(x = "Flipper length (mm)", y = "Bill length (mm)",
color = "Penguin species", shape = "Penguin species") +
theme_minimal() +
theme(legend.position = c(0.85, 0.2),
legend.background = element_rect(color = "white"))
Tip: Save your practice script in a separate directory (i.e. not inside your app directory) – I typically save mine to something like ~/scratch/practice-script.R
.
Copy your plot code into the server
Copy your code over to your app, placing it inside the {}
(and make sure to add any additional required packages to the top of your app.R
script). Run your app. What do you notice?
~/one-file-app/app.R
# load packages ----
library(shiny)
library(palmerpenguins)
library(tidyverse)
# user interface ----
ui <- fluidPage(
# ~ previous code omitted for brevity ~
# body mass slider ----
sliderInput(inputId = "body_mass_input", label = "Select a range of body masses (g):",
min = 2700, max = 6300, value = c(3000, 4000)),
# body mass plot output ----
plotOutput(outputId = "bodyMass_scatterPlot")
)
# server instructions ----
server <- function(input, output) {
# render the scatter plot ----
output$bodyMass_scatterPlot <- renderPlot({
ggplot(na.omit(penguins),
aes(x = flipper_length_mm, y = bill_length_mm,
color = species, shape = species)) +
geom_point() +
scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
scale_shape_manual(values = c("Adelie" = 19, "Chinstrap" = 17, "Gentoo" = 15)) +
labs(x = "Flipper length (mm)", y = "Bill length (mm)",
color = "Penguin species", shape = "Penguin species") +
theme_minimal() +
theme(legend.position = c(0.85, 0.2),
legend.background = element_rect(color = "white"))
})
A non-reactive plot now lives in our plotOutput()
placeholder
We have a plot (yay!), but it isn’t reactive. We have not yet told the server how to update the plot based on user inputs via the sliderInput()
in the UI. Let’s do that next…
Practice filtering data in our separate script
First, create a new data frame where we filter the body_mass_g
column for observations within a specific range of values (in this example, values ranging from 3000 - 4000):
Then, plot the new filtered data frame:
~/scratch/practice-script-app1-penguins.R
# plot new, filtered data
ggplot(na.omit(body_mass_df), # plot 'body_mass_df' rather than 'penguins' df
aes(x = flipper_length_mm, y = bill_length_mm,
color = species, shape = species)) +
geom_point() +
scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
scale_shape_manual(values = c("Adelie" = 19, "Chinstrap" = 17, "Gentoo" = 15)) +
labs(x = "Flipper length (mm)", y = "Bill length (mm)",
color = "Penguin species", shape = "Penguin species") +
theme_minimal() +
theme(legend.position = c(0.85, 0.2),
legend.background = element_rect(color = "white"))
Which part of our code needs to be updated when a user changes the slider range input?
Rule 3: Access input values with input$<id>
Recall that in our UI, we gave our sliderInput()
an inputId = "body_mass_input"
.
~/one-file-app/app.R
# load packages (omitted for brevity) ----
# user interface ----
ui <- fluidPage(
# ~ previous code omitted for brevity ~
# body mass slider ----
sliderInput(inputId = "body_mass_input", label = "Select a range of body masses (g):",
min = 2700, max = 6300, value = c(3000, 4000)),
# body mass plot output ----
plotOutput(outputId = "bodyMass_scatterPlot")
)
# server instructions ----
server <- function(input, output) {
# render the scatter plot ----
output$bodyMass_scatterPlot <- renderPlot({
ggplot(na.omit(penguins, aes(...)) + # etc. (omitted for brevity)
})
}
Rule 3: Access input values with input$<id>
In our server, we can access the values of that slider input using the syntax, input$body_mass_input
. If you want your output to change according to the input values, substitute hard-coded values (e.g. 3725:5191) with the input values from the UI (e.g. input$body_mass_input[1]:input$body_mass_input[2]
).
Importantly, we need to use reactive()
to create reactive data frames that update with user inputs. When you call your reactive data frame in your ggplot, the data frame name must be followed by ()
.
~/one-file-app/app.R
# load packages (omitted for brevity) ----
# user interface ----
ui <- fluidPage(
# ~ previous code omitted for brevity ~
# body mass slider ----
sliderInput(inputId = "body_mass_input", label = "Select a range of body masses (g):",
min = 2700, max = 6300, value = c(3000, 4000)),
# body mass plot output ----
plotOutput(outputId = "bodyMass_scatterPlot")
)
# server instructions ----
server <- function(input, output) {
# filter body masses ----
body_mass_df <- reactive({
penguins |>
filter(body_mass_g %in% input$body_mass_input[1]:input$body_mass_input[2])
})
# render the scatter plot ----
output$bodyMass_scatterPlot <- renderPlot({
ggplot(na.omit(body_mass_df(), aes(...)) + # etc. (omitted for brevity)
})
}
Okay, RUN THAT APP!
You should now have a reactive Shiny app! Note that reactivity automatically occurs whenever you use an input value to render an output object.
Recap: We created our first reactive Shiny app following these steps:
1. We created an app.R
file in it’s own directory and began our app with the template, though you can also create a two-file Shiny app by using separate ui.R
and server.R
files.
2. We added an input to the fluidPage()
in our UI using an *Input()
function and gave it a unique inputId
(e.g. inputId = "unique_input_Id_name"
)
3. We created a placeholder for our reactive object by using an *Output()
function in the fluidPage()
of our UI and gave it an outputId
(e.g. outputId = "output_Id_name"
)
4. We wrote the server instructions for how to assemble inputs into outputs, following these rules:
save objects that you want to display to output$<id>
build reactive objects using a render*()
function (and similarly, build reactive data frames using reactive()
access input values with input$<id>
And we saw that reactivity automatically occurs whenever we use an input value to render an output object.
Exercise 1: Add another reactive widget
The {DT}
package provides an R interface to the JavaScript library DataTables (you may have already used the DT package in your knitted RMarkdown/Quarto HTML documents). DT datatables allow for filtering, pagination, sorting, and lots of other neat features for tables on your HTML pages.
Working alone or in groups, add a reactive DT
datatable to your app with a checkboxGroupInput
that allows users to select which year(s) to include in the table. Configure your checkboxGroupInput
so that the years 2007 and 2008 are pre-selected.
In the end, your app should look something like the example to the right.
See next slide for some tips on getting started!
Exercise 1: Tips
Tips:
Use ?checkboxGroupInput
to learn more about which arguments you need (remember, all inputs require an inputId
and oftentimes a label
, but there are others required to make this work as well)
Both shiny and DT packages have functions named dataTableOutput()
and renderDataTable()
– DT::renderDataTable()
allows you to create both server-side and client-side DataTables and supports additional DataTables features while shiny::renderDataTable()
only provides server-side DataTables. Be sure to use the one from the DT package using the syntax packageName::functionName()
.
There are lots of ways to customize DT tables, but to create a basic one, all you need is DT::dataTable(your_dataframe)
And remember to follow the steps outlined on the previous slides (jump back to slide 27):
1. Add an input (e.g. checkboxGroupInput
) to the UI that users can interact with
2. Add an output (e.g. DT::datatableOutput
) to the UI that creates a placeholder space to fill with our eventual reactive output
3. Tell the server how to assemble inputs into outputs following 3 rules:
3.1 Save objects you want to display to output$<id>
3.2 Build reactive objects using a render*()
function
3.3 Access input values with input$<id>
See next slide for a solution!
Exercise 1: A solution
Press the right arrow key to advance through the newly added lines of code.
~/one-file-app/app.R
# load packages ----
library(shiny)
library(palmerpenguins)
library(tidyverse)
library(DT)
# user interface ----
ui <- fluidPage(
# app title ----
tags$h1("My App Title"),
# app subtitle ----
p(strong("Exploring Antarctic Penguin Data")),
# body mass slider input ----
sliderInput(inputId = "body_mass_input", label = "Select a range of body masses (g)",
min = 2700, max = 6300, value = c(3000, 4000)),
# body mass plot output ----
plotOutput(outputId = "bodyMass_scatterPlot"),
# year input ----
checkboxGroupInput(inputId = "year_input", label = "Select year(s):",
choices = c("2007", "2008", "2009"), # or `unique(penguins$year_input)` | NOTE: update checkbox display name by using "New name" = "observation name" (e.g "The year 2007" = "2007")
selected = c("2007", "2008")),
# DT output ----
DT::dataTableOutput(outputId = "penguin_data")
)
# server instructions ----
server <- function(input, output) {
# filter body masses ----
body_mass_df <- reactive({
penguins |>
filter(body_mass_g %in% input$body_mass_input[1]:input$body_mass_input[2]) # return observations where body_mass_g is "in" the set of options provided by the user in the sliderInput
})
# render the scatterplot output ----
output$bodyMass_scatterPlot <- renderPlot({
ggplot(na.omit(body_mass_df()),
aes(x = flipper_length_mm, y = bill_length_mm,
color = species, shape = species)) +
geom_point() +
scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
scale_shape_manual(values = c("Adelie" = 19, "Chinstrap" = 17, "Gentoo" = 15)) +
labs(x = "Flipper length (mm)", y = "Bill length (mm)",
color = "Penguin species", shape = "Penguin species") +
theme_minimal() +
theme(legend.position = c(0.85, 0.2),
legend.background = element_rect(color = "white"))
}, alt = "A scatterplot of penguin Bill length (mm) vs. Flipper length (mm) for Adelie (orange circles), Chinstrap (purple triangles), and Gentoo (green squares) penguins."
)
# filter for years ----
years_df <- reactive({
penguins |>
filter(year %in% input$year_input) # return observations where year is "in" the set of options provided by the user via the checkboxGroupInput
})
# render the DT::datatable ----
output$penguin_data <- DT::renderDataTable({
DT::datatable(years_df(),
options = list(pagelength = 10),
rownames = FALSE)
})
}
# combine UI & server into an app ----
shinyApp(ui = ui, server = server)
Common mistakes to look out for
It’s inevitable that you’ll make mistakes here and there as you build out your app…and they can be frustrating to catch. A few that I find myself making over and over again are:
misspelling inputId
as inputID
(or outputId
as outputID
)
misspelling your inputId (or outputId) name in the server (e.g. UI: inputId = "myInputID"
, server: input$my_Input_ID
)
repeating inputId
s (each must be unique)
forgetting to separate UI elements with a comma, ,
forgetting the set of parentheses when calling the name of a reactive data frame in a plot (e.g. ggplot(my_reactive_df(), aes(...))
)
Break
05:00
Building out your 2nd app
Learning Objectives - App #2v1 (two-file app)
By the end of building out this second app, you should:
be comfortable creating a shiny app using the two-file (ui.R
& server.R
) format along with a global.R
file
understand how to use layout functions to customize the visual structure of your app’s UI
have more practice building reactive outputs – and placing them within the layout structure of your app
be able to create multiple inputs that control a given output
know how to import larger bodies of text using includeMarkdown()
(rather than writing & styling text within your UI)
successfully publish an app using shinyapps.io
Packages introduced:
shinyWidgets: extend shiny widgets with some different, fun options
lterdatasampler: data
Roadmap for App #2v1
We’ll be building out our two-file app using data from the lterdatasampler and palmerpenguins packages. We’ll focus on creating a functional app that has a more visually pleasing UI layout (and we’ll refine it’s appearance even further in v2). By the end of v1, we’ll have created:
(a) A navigation bar with two pages, one of which will contain two tabs (one tab for each plot)
(b) A pickerInput
and checkboxGroupButtons
for users to filter cutthroat trout data in a reactive scatterplot
(c) A pickerInput
for users to filter penguin data and a sliderInput
to adjust the number of bins in a reactive histogram
You’ll notice that there are some UI quirks (most notably, blank plots that appear when no data is selected) that can make the user experience less than ideal (and even confusing) – we’ll learn about ways to improve this in v2 of our app.
Two files? Try two panes!
We’ll be building out a two-file shiny app this time around. You can open multiple scripts up side-by-side by navigating to Tools > Global Options > Pane Layout > Add Column
This setup is certainly not required/necessary – organize your IDE however you work best!
Practice data wrangling, filtering & viz first!
Here’s what I’ve done in my ~scratch/practice_script_app2_lter.R
file:
practice_script_app2_lter.R
#..........................load packages.........................
library(lterdatasampler)
library(tidyverse)
#............custom ggplot theme (apply to both plots)...........
myCustomTheme <- theme_light() +
theme(axis.text = element_text(size = 12),
axis.title = element_text(size = 14, face = "bold"),
legend.title = element_text(size = 14, face = "bold"),
legend.text = element_text(size = 13),
legend.position = "bottom",
panel.border = element_rect(linewidth = 0.7))
#.......................wrangle trout data.......................
clean_trout <- and_vertebrates |>
filter(species == "Cutthroat trout") |>
select(sampledate, section, species, length_mm = length_1_mm, weight_g, channel_type = unittype) |>
mutate(channel_type = case_when(
channel_type == "C" ~ "cascade",
channel_type == "I" ~ "riffle",
channel_type =="IP" ~ "isolated pool",
channel_type =="P" ~ "pool",
channel_type =="R" ~ "rapid",
channel_type =="S" ~ "step (small falls)",
channel_type =="SC" ~ "side channel"
)) |>
mutate(section = case_when(
section == "CC" ~ "clear cut forest",
section == "OG" ~ "old growth forest"
)) |>
drop_na()
#..................practice filtering trout data.................
trout_filtered_df <- clean_trout |>
filter(channel_type %in% c("pool", "rapid")) |>
filter(section %in% c("clear cut forest"))
#........................plot trout data.........................
ggplot(trout_filtered_df, aes(x = length_mm, y = weight_g, color = channel_type, shape = channel_type)) +
geom_point(alpha = 0.7, size = 5) +
scale_color_manual(values = c("cascade" = "#2E2585", "riffle" = "#337538", "isolated pool" = "#DCCD7D",
"pool" = "#5DA899", "rapid" = "#C16A77", "step (small falls)" = "#9F4A96",
"side channel" = "#94CBEC")) +
scale_shape_manual(values = c("cascade" = 15, "riffle" = 17, "isolated pool" = 19,
"pool" = 18, "rapid" = 8, "step (small falls)" = 23,
"side channel" = 25)) +
labs(x = "Trout Length (mm)", y = "Trout Weight (g)", color = "Channel Type", shape = "Channel Type") +
myCustomTheme
We’ll use the and_vertebrates
data set from lterdatasampler to create a scatter plot of trout weights by lengths. When we move to shiny, we’ll build 2 inputs for filtering our data: one to select channel_type
and one to select section
.
practice_script_app2_lter.R
#..........................load packages.........................
library(palmerpenguins)
library(tidyverse)
#..................practice filtering for island.................
island_df <- penguins %>%
filter(island %in% c("Dream", "Torgesen"))
#........................plot penguin data.......................
ggplot(na.omit(island_df), aes(x = flipper_length_mm, fill = species)) +
geom_histogram(alpha = 0.6, bins = 25) +
scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
labs(x = "Flipper length (mm)", y = "Frequency",
fill = "Penguin species") +
myCustomTheme
We’ll use the penguins
data set from palmerpenguins to create a histogram of penguin flipper lengths. When we move to shiny, we’ll build 2 inputs for filtering our data: one to select island
and one to change the number of histogram bins.
A global.R
file can help you keep code organized
While not a requirement of a shiny app, a global.R
file will help reduce redundant code, increase your app’s speed, and help keep code more clearly organized. It works by running once when your app is first launched, making any logic, objects, etc. contained in it available to both the ui.R
and server.R
files (or, in the case of a single-file shiny app, the app.R
file). It’s a great place for things like:
loading packages
importing data
sourcing scripts (particularly functions – we’ll talk more about functions later)
data wrangling (though you’ll want to do any major data cleaning before bringing your data into your app)
building custom ggplot
themes
etc.
Reminder: global.R
must be saved to the same directory as your ui.R
and server.R
files.
We created a perfectly functional first app, but it’s not so visually pleasing
nothing really grabs your eye
inputs & outputs are stacked vertically on top of one another (which requires a lot of vertical scrolling)
widget label text is difficult to distinguish from other text
Before we jump into adding reactive outputs to our next app, we’ll first plan out the visual structure of our UI – first on paper, then with layout functions.
Layout functions provide the high-level visual structure of your app
Layouts are created using a hierarchy of function calls (typically) inside fluidPage()
. Layouts often require a series functions – container functions establish the larger area within which other layout elements are placed. See a few minimal examples of layout functions on the following slides (though more exist!).
Some useful layout function pairings:
# sidebar for inputs & main area for outputs within the sidebarLayout() container
sidebarLayout(
sidebarPanel(),
mainPanel()
)
# multi-row fluid layout (add any number of fluidRow()s to a fluidPage())
fluidRow(
column(4, ...),
column(8, ...)
)
# tabPanel()s to contain HTML components (e.g. inputs/outputs) within the tabsetPanel() container
tabsetPanel(
tabPanel()
)
# NOTE: can use navbarPage() in place of fluidPage(); creates a page with top-level navigation bar that can be used to toggle tabPanel() elements
navbarPage(
tabPanel()
)
Example UI layouts
Note: You can combine multiple layout function groups to really customize your UI – for example, you can create a navbar, include tabs, and also establish sidebar and main panel areas for inputs and outputs.
To create a page with a side bar and main area to contain your inputs and outputs (respectively), explore the following layout functions and read up on the sidebarLayout documentation:
To create a page with multiple rows, explore the following layout functions and check out the fluid layout documentation. Note that each row is made up of 12 columns. The first argument of the column()
function takes a value of 1-12 to specify the number of columns to occupy.
You may find that you eventually end up with too much content to fit on a single application page. Enter tabsetPanel()
and tabPanel()
. tabsetPanel()
creates a container for any number of tabPanel()
s. Each tabPanel()
can contain any number of HTML components (e.g. inputs and outputs). Find the tabsetPanel documentation here and check out this example:
You may also want to use a navigation bar (navbarPage()
) with different pages (created using tabPanel()
) to organize your application. Read through the navbarPage documentation and try running the example below:
Examples adapted from Mastering Shiny, Ch. 6, by Hadley Wickham:
Overview of layout functions used in App #2
Build a navbar with two pages
First, let’s build a UI that has a navigation bar with two tabs – one for background information and one to contain our data visualizations. To do this, we’ll use navbarPage()
instead of fluidPage()
to create our webpage.
Tip: It can be super helpful add code comments at the start and end of each UI element – for example, see # data viz tabPanel---
and # END data viz tabPanel
, below. Adding text that you will eventually replace with content (e.g. plots, tables, images, longer text) may help to visualize what you’re working towards as well.
ui.R
ui <- navbarPage(
title = "LTER Animal Data Explorer",
# (Page 1) intro tabPanel ----
tabPanel(title = "About this App",
"background info will go here" # REPLACE THIS WITH CONTENT
), # END (Page 1) intro tabPanel
# (Page 2) data viz tabPanel ----
tabPanel(title = "Explore the Data",
"reactive plots will go here" # REPLACE THIS WITH CONTENT
) # END (Page 2) data viz tabsetPanel
) # END navbarPage
Add two tabs to the “Explore the Data” page
Give your tabs the following titles: Trout
and Penguins
.
ui.R
ui <- navbarPage(
title = "LTER Animal Data Explorer",
# (Page 1) intro tabPanel ----
tabPanel(title = "LTER Animal Data Explorer",
"background info will go here" # REPLACE THIS WITH CONTENT
), # END (Page 1) intro tabPanel
# (Page 2) data viz tabPanel ----
tabPanel(title = "Explore the Data",
# tabsetPanel to contain tabs for data viz ----
tabsetPanel(
# trout tabPanel ----
tabPanel(title = "Trout",
"trout data viz here" # REPLACE THIS WITH CONTENT
), # END trout tabPanel
# penguin tabPanel ----
tabPanel(title = "Penguins",
"penguin data viz here" # REPLACE THIS WITH CONTENT
) # END penguin tabPanel
) # END tabsetPanel
) # END (Page 2) data viz tabPanel
) # END navbarPage
Add sidebar & main panels to the Trout
tab
We’ll eventually place our input in the sidebar and output in the main panel.
ui.R
ui <- navbarPage(
title = "LTER Animal Data Explorer",
# (Page 1) intro tabPanel ----
tabPanel(title = "About this App",
"background info will go here" # REPLACE THIS WITH CONTENT
), # END (Page 1) intro tabPanel
# (Page 2) data viz tabPanel ----
tabPanel(title = "Explore the Data",
# tabsetPanel to contain tabs for data viz ----
tabsetPanel(
# trout tabPanel ----
tabPanel(title = "Trout",
# trout sidebarLayout ----
sidebarLayout(
# trout sidebarPanel ----
sidebarPanel(
"trout plot input(s) go here" # REPLACE THIS WITH CONTENT
), # END trout sidebarPanel
# trout mainPanel ----
mainPanel(
"trout plot output goes here" # REPLACE THIS WITH CONTENT
) # END trout mainPanel
) # END trout sidebarLayout
), # END trout tabPanel
# penguin tabPanel ----
tabPanel(title = "Penguins",
"penguin data viz here" # REPLACE THIS WITH CONTENT
) # END penguin tabPanel
) # END tabsetPanel
) # END (Page 2) data viz tabPanel
) # END navbarPage
Exercise 2: Add sidebar and main panels to the Penguins
tab
I encourage you to type the code out yourself, rather than copy/paste! And be sure to add text where your input/output will eventually be placed. When you’re done, you app should look like this:
See next slide for a solution!
Exercise 2: A solution
ui.R
ui <- navbarPage(
title = "LTER Animal Data Explorer",
# (Page 1) intro tabPanel ----
tabPanel(title = "About this App",
"background info will go here" # REPLACE THIS WITH CONTENT
), # END (Page 1) intro tabPanel
# (Page 2) data viz tabPanel ----
tabPanel(title = "Animal Data Explorer",
# tabsetPanel to contain tabs for data viz ----
tabsetPanel(
# trout tabPanel ----
tabPanel(title = "Trout",
# trout sidebarLayout ----
sidebarLayout(
# trout sidebarPanel ----
sidebarPanel(
"trout plot input(s) go here" # REPLACE THIS WITH CONTENT
), # END trout sidebarPanel
# trout mainPanel ----
mainPanel(
"trout plot output goes here" # REPLACE THIS WITH CONTENT
) # END trout mainPanel
) # END trout sidebarLayout
), # END trout tabPanel
# penguin tabPanel ----
tabPanel(title = "Penguins",
# penguin sidebarLayout ----
sidebarLayout(
# penguin sidebarPanel ----
sidebarPanel(
"penguin plot input(s) go here" # REPLACE THIS WITH CONTENT
), # END penguin sidebarPanel
# penguin mainPanel ----
mainPanel(
"penguin plot output goes here" # REPLACE THIS WITH CONTENT
) # END penguin mainPanel
) # END penguin sidebarLayout
) # END penguin tabPanel
) # END tabsetPanel
) # END (Page 2) data viz tabPanel
) # END navbarPage
Some important things to remember when building your UI’s layout:
try creating a rough sketch of your intended layout before hitting the keyboard (I like to think of this as UI layout “pseudocode”)
keeping clean code is important – we haven’t even any added any content yet and our UI is already >70 lines of code!
use rainbow parentheses, code comments and plenty of space between lines to keep things looking manageable and navigable
use the keyboard shortcut, command
+ I
(Mac) or control
+ I
(Windows), to align messy code – this helps put those off-alignment parentheses back where they belong
things can get out of hand quickly – add one layout section at a time, run your app to check that things look as you intend, then continue
Add data viz: First up, trout
We’ll be using the and_vertebrates
dataset from the {lterdatasampler}
package to create our first reactive plot. These data contain coastal cutthroat trout (Oncorhynchus clarkii clarkii) lengths and weights collected in Mack Creek, Andrews Forest LTER. Original data can be found on the EDI Data Portal. Refer back to this slide to revisit our practice data wrangling & visualization script.
Image Source: Joseph R. Tomelleri, as found on the Western Native Trout Initiative
Add packages & wrangle data in global.R
In addition to the {lterdatasampler}
package, we’ll also be using the tidyverse for data wrangling/visualization, and the {shinyWidgets}
package to add a pickerInput
and a checkboxGroupInput
to our app.
Import those three packages at the top of your global.R
file
Add packages & wrangle data in global.R
We can also do the bulk of our data wrangling here, rather than in the server (to keep our server code a bit more manageable). If we were reading in a data file (e.g. .csv), we would do that here too. Our new data object clean_trout
, will now be available for us to call directly in our server (NOTE: we can easily copy our wrangling code over from our practice script).
global.R
# LOAD LIBRARIES ----
library(shiny)
library(lterdatasampler)
library(tidyverse)
library(shinyWidgets)
# DATA WRANGLING ----
clean_trout <- and_vertebrates |>
filter(species == "Cutthroat trout") |>
select(sampledate, section, species, length_mm = length_1_mm, weight_g, channel_type = unittype) |>
mutate(channel_type = case_when(
channel_type == "C" ~ "cascade",
channel_type == "I" ~ "riffle",
channel_type =="IP" ~ "isolated pool",
channel_type =="P" ~ "pool",
channel_type =="R" ~ "rapid",
channel_type =="S" ~ "step (small falls)",
channel_type =="SC" ~ "side channel"
)) |>
mutate(section = case_when(
section == "CC" ~ "clear cut forest",
section == "OG" ~ "old growth forest"
)) |>
drop_na()
Add a pickerInput
for selecting channel_type
to your UI
The channel_type
variable (originally called unittype
– we updated the name when wrangling data (see line 9 on previous slide)) represents the type of water body (cascade, riffle, isolated pool, pool, rapid, step (small falls), or side channel) data were collected in. We’ll start by building a shinyWidgets::pickerInput()
to allow users to filter data based on channel_type
.
Reminder: When we we designed our UI layout, we added a sidebarPanel
to our Trout tab with the placeholder text "trout plot input(s) go here"
. Replace that text with the code for your pickerInput
:
ui.R
# channel type pickerInput ----
pickerInput(inputId = "channel_type_input", label = "Select channel type(s):",
choices = unique(clean_trout$channel_type), # alternatively: choices = c("rapid", "cascade" ...)
options = pickerOptions(actionsBox = TRUE), # creates "Select All / Deselect All" buttons
selected = c("cascade", "pool"),
multiple = TRUE) # END channel type pickerInput
Save and run your app – a functional pickerInput
should now appear in your UI.
A shinyWidgets::pickerInput()
is functionally equivalent to shiny::selectInput()
, though it allows for greater customization and looks (in my opinion) a bit nicer.
Add a plot output to your UI
Next, we need to create a placeholder in our UI for our trout scatterplot to live. Because we’ll be creating a reactive plot, we can use the plotOutput()
function to do so.
Reminder: When we we designed our UI layout, we added a mainPanel
to our Trout tab with the placeholder text "trout plot output goes here"
. Replace that text with the code for your plotOuput()
:
Save and run your app – it won’t look different at first glance, but inspecting your app in a browser window (using Chrome, right click > Inspect) will reveal a placeholder box for your plot output to eventually live:
Tell the server how to assemble pickerInput
values into your plotOutput
Remember the three rules for building reactive outputs: (1) save objects you want to display to output$<id>
, (2) build reactive objects using a render*()
function, and (3) access input values with input$<id>
. When complete, your server should contain the following code:
server.R
server <- function(input, output) {
# filter trout data ----
trout_filtered_df <- reactive({
clean_trout |>
filter(channel_type %in% c(input$channel_type_input))
})
# trout scatterplot ----
output$trout_scatterplot <- renderPlot({
ggplot(trout_filtered_df(), aes(x = length_mm, y = weight_g, color = channel_type, shape = channel_type)) +
geom_point(alpha = 0.7, size = 5) +
scale_color_manual(values = c("cascade" = "#2E2585", "riffle" = "#337538", "isolated pool" = "#DCCD7D",
"pool" = "#5DA899", "rapid" = "#C16A77", "step (small falls)" = "#9F4A96",
"side channel" = "#94CBEC")) +
scale_shape_manual(values = c("cascade" = 15, "riffle" = 17, "isolated pool" = 19,
"pool" = 18, "rapid" = 8, "step (small falls)" = 23,
"side channel" = 25)) +
labs(x = "Trout Length (mm)", y = "Trout Weight (g)", color = "Channel Type", shape = "Channel Type") +
myCustomTheme
})
} # END server
A couple notes/reminders:
If needed, reference your practice script to remind yourself how you planned to filter and plot your data
Reactive data frames need a set of parentheses, ()
, following the name of the df (see ggplot(trout_filtered_df() ...)
)
For a cohesive appearance, save your ggplot theme parameters to a named object in global.R
(here, myCustomTheme
), then apply to all plots in your app. See the following slide for code.
Save a custom ggplot
theme to global.R
Rather than re-typing your ggplot theme parameters out for every plot in your app, do so once in global.R
, and save to an object name. You can then easily add your custom theme as a layer to each of your ggplots. Bonus: If you decide to modify your plot theme, you only have to do so in one place!
global.R
# LOAD LIBRARIES ----
library(shiny)
library(lterdatasampler)
library(palmerpenguins)
library(tidyverse)
library(shinyWidgets)
# DATA WRANGLING ----
clean_trout <- and_vertebrates |>
filter(species == c("Cutthroat trout")) |>
select(sampledate, section, species, length_mm = length_1_mm, weight_g, channel_type = unittype) |>
mutate(channel_type = case_when(
channel_type == "C" ~ "cascade",
channel_type == "I" ~ "riffle",
channel_type =="IP" ~ "isolated pool",
channel_type =="P" ~ "pool",
channel_type =="R" ~ "rapid",
channel_type =="S" ~ "step (small falls)",
channel_type =="SC" ~ "side channel"
)) |>
mutate(section = case_when(
section == "CC" ~ "clear cut forest",
section == "OG" ~ "old growth forest"
)) |>
drop_na()
# GGPLOT THEME ----
myCustomTheme <- theme_light() +
theme(axis.text = element_text(color = "black", size = 12),
axis.title = element_text(size = 14, face = "bold"),
legend.title = element_text(size = 14, face = "bold"),
legend.text = element_text(size = 13),
legend.position = "bottom",
panel.border = element_rect(colour = "black", fill = NA, linewidth = 0.7))
Run your app and try out your pickerInput
widget!
Add a second input that will update the same output
You can have more than one input control the same output. Let’s now add a checkboxGroupButtons
widget to our UI for selecting forest section
(either clear cut forest or old growth forest). Check out the function documentation for more information on how to customize the appearance of your buttons.
Be sure to add the widget to the same sidebarPanel
as our pickerInput
(and separate them with a comma!:
ui.R
# trout plot sidebarPanel ----
sidebarPanel(
# channel type pickerInput ----
pickerInput(inputId = "channel_type_input", label = "Select channel type(s):",
choices = unique(clean_trout$channel_type),
options = pickerOptions(actionsBox = TRUE),
selected = c("cascade", "pool"),
multiple = TRUE), # END channel type pickerInput
# section checkboxGroupButtons ----
checkboxGroupButtons(inputId = "section_input", label = "Select a sampling section(s):",
choices = c("clear cut forest", "old growth forest"),
selected = c("clear cut forest", "old growth forest"),
individual = FALSE, justified = TRUE, size = "sm",
checkIcon = list(yes = icon("ok", lib = "glyphicon"), no = icon("remove", lib = "glyphicon"))), # END section checkboxGroupInput
) # END trout plot sidebarPanel
Update your reactive df to also filter based on the new checkboxGroupInput
Return to your server to modify trout_filtered_df
– our data frame needs to be updated based on both the pickerInput
, which selects for channel_type
, and the checkboxGrouptInput
, which selects for forest section
:
server.R
# filter trout data ----
trout_filtered_df <- reactive({
clean_trout |>
filter(channel_type %in% c(input$channel_type_input)) |>
filter(section %in% c(input$section_input))
})
# trout scatterplot ----
output$trout_scatterplot <- renderPlot({
ggplot(trout_filtered_df(), aes(x = length_mm, y = weight_g, color = channel_type, shape = channel_type)) +
geom_point(alpha = 0.7, size = 5) +
scale_color_manual(values = c("cascade" = "#2E2585", "riffle" = "#337538", "isolated pool" = "#DCCD7D",
"pool" = "#5DA899", "rapid" = "#C16A77", "step (small falls)" = "#9F4A96",
"side channel" = "#94CBEC")) +
scale_shape_manual(values = c("cascade" = 15, "riffle" = 17, "isolated pool" = 19,
"pool" = 18, "rapid" = 8, "step (small falls)" = 23,
"side channel" = 25)) +
labs(x = "Trout Length (mm)", y = "Trout Weight (g)", color = "Channel Type", shape = "Channel Type") +
myCustomTheme
})
Run your app and try out your pickerInput
& checkboxGrouptInput
widgets!
Break
05:00
Add data viz: Next up, penguins
We’ll be using the penguins
dataset from the {palmerpenguins}
package to create our second reactive plot. These data contain penguin (genus Pygoscelis) body size measurements collected from three islands in the Palmer Archipelago, Antarctica, as part of the Palmer Station LTER. Original data can be found on the EDI Data Portal (Adélie data, Gentoo data, and Chinstrap data). Refer back to this slide to revisit our practice data wrangling & visualization script.
Artwork by @allison_horst
Exercise 3: Add a reactive plot to the ‘Penguins’ tab
Working alone or in groups, add a reactive histogram of penguin flipper lengths (using the penguins
data set from the {palmerpenguins}
package) to the Penguins tab. Your plot should have the following features and look like the example below, when complete:
data colored by penguin species
a shinyWidgets::pickerInput
that allows users to filter data based on island
, and that includes buttons to Select All / Deselect All island options at once
a shiny::sliderInput
that allows users to change the number of histogram bins and that by default, displays a histogram with 25 bins
the two widgets should be placed in the sidebarPanel
and the reactive histogram should be placed in the mainPanel
of the Penguins tab
See next slide for some tips on getting started!
Exercise 3: Tips
Tips:
Remember to load the palmerpenguins
package at the top of global.R
so that your app can find the data
Add your widgets to the sidebarPanel
and your plot output to the mainPanel
of the Penguins tab – look for that placeholder text we added earlier to help place your new code in the correct spot within your UI!
Try changing the histogram bin number in your practice code script first, before attempting to make it reactive
And remember to follow the our three steps for building reactive outputs (1. add input to UI, 2. add output to UI, 3. tell server how to assemble inputs into outputs)!
See next slide for a solution!
Exercise 3: A solution
# LOAD LIBRARIES ----
library(shiny)
library(lterdatasampler)
library(palmerpenguins)
library(tidyverse)
library(shinyWidgets)
# DATA WRANGLING ----
# trout data
clean_trout <- and_vertebrates |>
filter(species == c("Cutthroat trout")) |>
select(sampledate, section, species, length_mm = length_1_mm, weight_g, channel_type = unittype) |>
mutate(channel_type = case_when(
channel_type == "C" ~ "cascade",
channel_type == "I" ~ "riffle",
channel_type =="IP" ~ "isolated pool",
channel_type =="P" ~ "pool",
channel_type =="R" ~ "rapid",
channel_type =="S" ~ "step (small falls)",
channel_type =="SC" ~ "side channel"
)) |>
mutate(section = case_when(
section == "CC" ~ "clear cut forest",
section == "OG" ~ "old growth forest"
)) |>
drop_na()
# GGPLOT THEME ----
myCustomTheme <- theme_light() +
theme(#text = element_text(family = "mono"),
axis.text = element_text(color = "black", size = 12),
axis.title = element_text(size = 14, face = "bold"),
legend.title = element_text(size = 14, face = "bold"),
legend.text = element_text(size = 13),
legend.position = "bottom",
panel.border = element_rect(colour = "black", fill = NA, linewidth = 0.7))
ui <- navbarPage(
title = "LTER Animal Data Explorer",
# (Page 1) intro tabPanel ----
tabPanel(title = "About this App",
), # END (Page 1) intro tabPanel
# (Page 2) data viz tabPanel ----
tabPanel(title = "Explore the Data",
# tabsetPanel to contain tabs for data viz ----
tabsetPanel(
# trout tabPanel ----
tabPanel(title = "Trout",
# trout plot sidebarLayout ----
sidebarLayout(
# trout plot sidebarPanel ----
sidebarPanel(
# channel type pickerInput ----
pickerInput(inputId = "channel_type_input", label = "Select channel type(s):",
choices = unique(clean_trout$channel_type),
options = pickerOptions(actionsBox = TRUE),
selected = c("cascade", "pool"),
multiple = TRUE), # END channel type pickerInput
# # section checkboxGroupInput ----
checkboxGroupButtons(inputId = "section_input", label = "Select a sampling section(s):",
choices = c("clear cut forest", "old growth forest"),
selected = c("clear cut forest", "old growth forest"),
individual = FALSE, justified = TRUE, size = "sm",
checkIcon = list(yes = icon("ok", lib = "glyphicon"), no = icon("remove", lib = "glyphicon"))), # END section checkboxGroupInput
), # END trout plot sidebarPanel
# trout plot mainPanel ----
mainPanel(
plotOutput(outputId = "trout_scatterplot")
) # END trout plot mainPanel
) # END trout plot sidebarLayout
), # END trout tabPanel
# penguin tabPanel ----
tabPanel(title = "Penguins",
# penguin plot sidebarLayout ----
sidebarLayout(
# penguin plot sidebarPanel ----
sidebarPanel(
# island pickerInput ----
pickerInput(inputId = "penguin_island_input", label = "Select an island(s):",
choices = c("Torgersen", "Dream", "Biscoe"),
options = pickerOptions(actionsBox = TRUE),
selected = c("Torgersen", "Dream", "Biscoe"),
multiple = T), # END island pickerInput
# bin number sliderInput ----
sliderInput(inputId = "bin_num_input", label = "Select number of bins:",
value = 25, max = 100, min = 1), # END bin number sliderInput
), # END penguin plot sidebarPanel
# penguin plot mainPanel ----
mainPanel(
plotOutput(outputId = "flipperLength_histogram")
) # END penguin plot mainPanel
) # END penguin plot sidebarLayout
) # END penguin tabPanel
) # END tabsetPanel
) # END (Page 2) data viz tabPanel
) # END navbarPage
server <- function(input, output) {
# filter for channel types ----
trout_filtered_df <- reactive({
clean_trout |>
filter(channel_type %in% c(input$channel_type_input)) |>
filter(section %in% c(input$section_input))
})
# trout scatterplot ----
output$trout_scatterplot <- renderPlot({
ggplot(trout_filtered_df(), aes(x = length_mm, y = weight_g, color = channel_type, shape = channel_type)) +
geom_point(alpha = 0.7, size = 5) +
scale_color_manual(values = c("cascade" = "#2E2585", "riffle" = "#337538", "isolated pool" = "#DCCD7D",
"pool" = "#5DA899", "rapid" = "#C16A77", "step (small falls)" = "#9F4A96",
"side channel" = "#94CBEC")) +
scale_shape_manual(values = c("cascade" = 15, "riffle" = 17, "isolated pool" = 19,
"pool" = 18, "rapid" = 8, "step (small falls)" = 23,
"side channel" = 25)) +
labs(x = "Trout Length (mm)", y = "Trout Weight (g)", color = "Channel Type", shape = "Channel Type") +
myCustomTheme
})
# filter for island ----
island_df <- reactive({
penguins %>%
filter(island %in% input$penguin_island)
})
# render the flipper length histogram ----
output$flipperLength_histogram <- renderPlot({
ggplot(na.omit(island_df()), aes(x = flipper_length_mm, fill = species)) +
geom_histogram(alpha = 0.6, bins = input$bin_num_input) +
scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
labs(x = "Flipper length (mm)", y = "Frequency",
fill = "Penguin species") +
myCustomTheme
})
} # END server
Break
05:00
Lastly: add background/other important information text
It’s usually valuable (and important) to provide some background information/context for your app – the landing page of your app can be a great place for this. We’re going to add text to our app’s landing page (i.e. the About this App page) so that it looks like the example below:
Some important pieces for information to consider adding:
motivation for building the app
brief instructions for exploring the data
who maintains the app, where the code lives, how to submit issues/suggestions
Adding long text to the UI can get unruly
For example, I’ve added and formatted my landing page’s text directly in the UI using lots of nested tags
– I’ve done this inside the tabPanel
titled About this App (Note: I’ve formatted the layout of this page a bit using fluidRow
and column
s to create some white space around the edges. I’ve also created a faint gray horizontal line, using hr()
, beneath which I added a footnote):
ui.R
ui <- navbarPage(
title = "LTER Animal Data Explorer",
# (Page 1) intro tabPanel ----
tabPanel(title = "About this App",
fluidRow(
column(1),
column(10,
tags$h1("Welcome to the LTER Animal Data Explorer!"),
tags$br(),
tags$h4("Why did we build this app?"),
tags$p("This shiny app was built, in part, to provide users a way of exploring morphological characteristics of the different animal species found within NSF's", tags$a(href = "https://lternet.edu/", "Long Term Ecological Research (LTER)"), "sites...but primarily, it was built as a teaching tool for", tags$a(href = "https://bren.ucsb.edu/courses/eds-430", "EDS 430 (Intro to Shiny)"), "-- this workshop, taught through the", tags$a(href = "https://ucsb-meds.github.io/", "Master of Environmental Data Science (MEDS) program"), "at the", tags$a(href = "https://bren.ucsb.edu/", "Bren School of Environmental Science and Management"), "is a two-day coding-intensive course meant to meant to provide a introductory foundation in shiny app development."),
tags$br(),
tags$h4("Where's the data?"),
tags$p("Check out the", tags$strong("Explore the Data"), "page to find interactive data visualizations looking at Cutthroat trout of the", tags$a(href = "https://andrewsforest.oregonstate.edu/", "Andrews Forest LTER"), "and Adélie, Gentoo & Chinstrap penguins of the", tags$a(href = "https://pallter.marine.rutgers.edu/", "Palmer Station LTER."))
),
column(1)
), # END fluidRow
hr(),
em("This app is maintained by", tags$a(href = "https://samanthacsik.github.io/", "Samantha Csik"), "and is updated as needed for teaching purposes. Please report any issues", tags$a(href = "https://github.com/samanthacsik/EDS430-shiny-app/issues", "here."), "Source code can be found on", tags$a(href = "https://github.com/samanthacsik/EDS430-shiny-app", "GitHub."))
), # END (Page 1) intro tabPanel
# (Page 2) data viz tabPanel ----
tabPanel(title = "Explore the Data",
# tabsetPanel to contain tabs for data viz ----
tabsetPanel(
# trout tabPanel ----
tabPanel(title = "Trout",
# trout plot sidebarLayout ----
sidebarLayout(
# trout plot sidebarPanel ----
sidebarPanel(
# channel type pickerInput ----
pickerInput(inputId = "channel_type_input", label = "Select channel type(s):",
choices = unique(clean_trout$channel_type),
options = pickerOptions(actionsBox = TRUE),
selected = c("cascade", "pool"),
multiple = TRUE), # END channel type pickerInput
# # section checkboxGroupInput ----
checkboxGroupButtons(inputId = "section_input", label = "Select a sampling section:",
choices = c("clear cut forest", "old growth forest"),
selected = c("clear cut forest", "old growth forest"),
individual = FALSE, justified = TRUE, size = "sm",
checkIcon = list(yes = icon("ok", lib = "glyphicon"), no = icon("remove", lib = "glyphicon"))), # END section checkboxGroupInput
), # END trout plot sidebarPanel
# trout plot mainPanel ----
mainPanel(
plotOutput(outputId = "trout_scatterplot")
) # END trout plot mainPanel
) # END trout plot sidebarLayout
), # END trout tabPanel
# penguin tabPanel ----
tabPanel(title = "Penguins",
# penguin plot sidebarLayout ----
sidebarLayout(
# penguin plot sidebarPanel ----
sidebarPanel(
# island pickerInput ----
pickerInput(inputId = "penguin_island", label = "Select an island:",
choices = c("Torgersen", "Dream", "Biscoe"),
options = pickerOptions(actionsBox = TRUE),
selected = c("Torgersen", "Dream", "Biscoe"),
multiple = T), # END island pickerInput
# bin number sliderInput ----
sliderInput(inputId = "bin_num", label = "Select number of bins:",
value = 25, max = 100, min = 1), # END bin number sliderInput
), # END penguin plot sidebarPanel
# penguin plot mainPanel ----
mainPanel(
plotOutput(outputId = "flipperLength_histogram")
) # END penguin plot mainPanel
) # END penguin plot sidebarLayout
) # END penguin tabPanel
) # END tabsetPanel
) # END (Page 2) data viz tabPanel
) # END navbarPage
Instead, use includeMarkdown()
to read in text from separate .md
files
To maintain readability and an overall tidier-looking UI, you can write and style long bodies of text in separate markdown (.md
) files that you then read into your UI using the includeMarkdown()
function (Important: the includeMarkdown()
function requires the markdown package – be sure to add library(markdown)
to your global.R
file!).
I recommend saving those .md
files in a subdirectory named /text
within your app’s directory (e.g. ~/two-file-app/text/mytext.md
). See how I simplified my UI by saving my long landing page text to two new files, about.md
and footer.md
, then imported them into my UI using includeMarkdown()
.
ui.R
ui <- navbarPage(
title = "LTER Animal Data Explorer",
# (Page 1) intro tabPanel ----
tabPanel(title = "About this App",
# intro text fluidRow ----
fluidRow(
# use columns to create white space on sides
column(1),
column(10, includeMarkdown("text/about.md")),
column(1),
), # END intro text fluidRow
hr(), # creates light gray horizontal line
# footer text ----
includeMarkdown("text/footer.md")
), # END (Page 1) intro tabPanel
# (Page 2) data viz tabPanel ----
tabPanel(title = "Explore the Data",
# tabsetPanel to contain tabs for data viz ----
tabsetPanel(
# trout tabPanel ----
tabPanel(title = "Trout",
# trout plot sidebarLayout ----
sidebarLayout(
# trout plot sidebarPanel ----
sidebarPanel(
# channel type pickerInput ----
pickerInput(inputId = "channel_type_input", label = "Select channel type(s):",
choices = unique(clean_trout$channel_type),
options = pickerOptions(actionsBox = TRUE),
selected = c("cascade", "pool"),
multiple = TRUE), # END channel type pickerInput
# # section checkboxGroupInput ----
checkboxGroupButtons(inputId = "section_input", label = "Select a sampling section:",
choices = c("clear cut forest", "old growth forest"),
selected = c("clear cut forest", "old growth forest"),
individual = FALSE, justified = TRUE, size = "sm",
checkIcon = list(yes = icon("ok", lib = "glyphicon"), no = icon("remove", lib = "glyphicon"))), # END section checkboxGroupInput
), # END trout plot sidebarPanel
# trout plot mainPanel ----
mainPanel(
plotOutput(outputId = "trout_scatterplot")
) # END trout plot mainPanel
) # END trout plot sidebarLayout
), # END trout tabPanel
# penguin tabPanel ----
tabPanel(title = "Penguins",
# penguin plot sidebarLayout ----
sidebarLayout(
# penguin plot sidebarPanel ----
sidebarPanel(
# island pickerInput ----
pickerInput(inputId = "penguin_island", label = "Select an island:",
choices = c("Torgersen", "Dream", "Biscoe"),
options = pickerOptions(actionsBox = TRUE),
selected = c("Torgersen", "Dream", "Biscoe"),
multiple = T), # END island pickerInput
# bin number sliderInput ----
sliderInput(inputId = "bin_num_input", label = "Select number of bins:",
value = 25, max = 100, min = 1), # END bin number sliderInput
), # END penguin plot sidebarPanel
# penguin plot mainPanel ----
mainPanel(
plotOutput(outputId = "flipperLength_histogram")
) # END penguin plot mainPanel
) # END penguin plot sidebarLayout
) # END penguin tabPanel
) # END tabsetPanel
) # END (Page 2) data viz tabPanel
) # END navbarPage
text/about.md
## Welcome to the LTER Animal Data Explorer!
<br>
#### Why did we build this app?
This shiny app was built, in part, to provide users a way of exploring morphological characteristics of the different animal species found within NSF's [Long Term Ecological Research (LTER)](https://lternet.edu/) sites...but primarily, it was built as a teaching tool for [EDS 430 (Intro to Shiny)](https://bren.ucsb.edu/courses/eds-430) -- this workshop, taught through the [Master of Environmental Data Science (MEDS) program](https://ucsb-meds.github.io/) at the [Bren School of Environmental Science and Management](https://bren.ucsb.edu/), is a two-day coding-intensive course meant to meant to provide a introductory foundation in shiny app development.
<br>
#### Where's the data?
Check out the **Explore the Data** page to find interactive data visualizations looking at Cutthroat trout of the [Andrews Forest LTER](https://andrewsforest.oregonstate.edu/) and Adelie, Gentoo & Chinstrap penguins of the [Palmer Station LTER](https://pallter.marine.rutgers.edu/).
text/footer/md
*This app is maintained by [Samantha Csik](https://samanthacsik.github.io/) and is updated as needed for teaching purposes. Please report any issues [here](https://github.com/samanthacsik/EDS430-shiny-app/issues). Source code can be found on [GitHub](https://github.com/samanthacsik/EDS430-shiny-app).*
Run your app one more time to admire your beautiful creation!
Again, we have some UX/UI quirks to fix (most notably, blank plots when all widget options are deselected), which we’ll handle soon. But for now, we have a functioning app that we can practice deploying for the first time!
Code recap for app #2v1, so far:
Additionally, you should have a /text
folder within your app’s directory (/two-file-app
, if you named it as I did) that contains two markdown files, about.md
and footer.md
.
# LOAD LIBRARIES ----
library(shiny)
library(lterdatasampler)
library(palmerpenguins)
library(tidyverse)
library(shinyWidgets)
library(markdown)
# DATA WRANGLING ----
clean_trout <- and_vertebrates |>
filter(species == c("Cutthroat trout")) |>
select(sampledate, section, species, length_mm = length_1_mm, weight_g, channel_type = unittype) |>
mutate(channel_type = case_when(
channel_type == "C" ~ "cascade",
channel_type == "I" ~ "riffle",
channel_type =="IP" ~ "isolated pool",
channel_type =="P" ~ "pool",
channel_type =="R" ~ "rapid",
channel_type =="S" ~ "step (small falls)",
channel_type =="SC" ~ "side channel"
)) |>
mutate(section = case_when(
section == "CC" ~ "clear cut forest",
section == "OG" ~ "old growth forest"
)) |>
drop_na()
# GGPLOT THEME ----
myCustomTheme <- theme_light() +
theme(axis.text = element_text(color = "black", size = 12),
axis.title = element_text(size = 14, face = "bold"),
legend.title = element_text(size = 14, face = "bold"),
legend.text = element_text(size = 13),
legend.position = "bottom",
panel.border = element_rect(colour = "black", fill = NA, linewidth = 0.7))
ui <- navbarPage(
title = "LTER Animal Data Explorer",
# (Page 1) intro tabPanel ----
tabPanel(title = "About this App",
# intro text fluidRow ----
fluidRow(
# use columns to create white space on sides
column(1),
column(10, includeMarkdown("text/about.md")),
column(1),
), # END intro text fluidRow
hr(), # creates light gray horizontal line
# footer text ----
includeMarkdown("text/footer.md")
), # END (Page 1) intro tabPanel
# (Page 2) data viz tabPanel ----
tabPanel(title = "Explore the Data",
# tabsetPanel to contain tabs for data viz ----
tabsetPanel(
# trout tabPanel ----
tabPanel(title = "Trout",
# trout plot sidebarLayout ----
sidebarLayout(
# trout plot sidebarPanel ----
sidebarPanel(
# channel type pickerInput ----
pickerInput(inputId = "channel_type_input", label = "Select channel type(s):",
choices = unique(clean_trout$channel_type),
options = pickerOptions(actionsBox = TRUE),
selected = c("cascade", "pool"),
multiple = TRUE), # END channel type pickerInput
# # section checkboxGroupInput ----
checkboxGroupButtons(inputId = "section_input", label = "Select a sampling section(s):",
choices = c("clear cut forest", "old growth forest"),
selected = c("clear cut forest", "old growth forest"),
individual = FALSE, justified = TRUE, size = "sm",
checkIcon = list(yes = icon("ok", lib = "glyphicon"), no = icon("remove", lib = "glyphicon"))), # END section checkboxGroupInput
), # END trout plot sidebarPanel
# trout plot mainPanel ----
mainPanel(
plotOutput(outputId = "trout_scatterplot")
) # END trout plot mainPanel
) # END trout plot sidebarLayout
), # END trout tabPanel
# penguin tabPanel ----
tabPanel(title = "Penguins",
# penguin plot sidebarLayout ----
sidebarLayout(
# penguin plot sidebarPanel ----
sidebarPanel(
# island pickerInput ----
pickerInput(inputId = "penguin_island", label = "Select an island(s):",
choices = c("Torgersen", "Dream", "Biscoe"),
options = pickerOptions(actionsBox = TRUE),
selected = c("Torgersen", "Dream", "Biscoe"),
multiple = T), # END island pickerInput
# bin number sliderInput ----
sliderInput(inputId = "bin_num_input", label = "Select number of bins:",
value = 25, max = 100, min = 1), # END bin number sliderInput
), # END penguin plot sidebarPanel
# penguin plot mainPanel ----
mainPanel(
plotOutput(outputId = "flipperLength_histogram")
) # END penguin plot mainPanel
) # END penguin plot sidebarLayout
) # END penguin tabPanel
) # END tabsetPanel
) # END (Page 2) data viz tabPanel
) # END navbarPage
server <- function(input, output) {
# filter for channel types ----
trout_filtered_df <- reactive({
clean_trout |>
filter(channel_type %in% c(input$channel_type_input)) |>
filter(section %in% c(input$section_input))
})
# trout scatterplot ----
output$trout_scatterplot <- renderPlot({
ggplot(trout_filtered_df(), aes(x = length_mm, y = weight_g, color = channel_type, shape = channel_type)) +
geom_point(alpha = 0.7, size = 5) +
scale_color_manual(values = c("cascade" = "#2E2585", "riffle" = "#337538", "isolated pool" = "#DCCD7D",
"pool" = "#5DA899", "rapid" = "#C16A77", "step (small falls)" = "#9F4A96",
"side channel" = "#94CBEC")) +
scale_shape_manual(values = c("cascade" = 15, "riffle" = 17, "isolated pool" = 19,
"pool" = 18, "rapid" = 8, "step (small falls)" = 23,
"side channel" = 25)) +
labs(x = "Trout Length (mm)", y = "Trout Weight (g)", color = "Channel Type", shape = "Channel Type") +
myCustomTheme
})
# filter for island ----
island_df <- reactive({
penguins |>
filter(island %in% input$penguin_island)
})
# render the flipper length histogram ----
output$flipperLength_histogram <- renderPlot({
ggplot(na.omit(island_df()), aes(x = flipper_length_mm, fill = species)) +
geom_histogram(alpha = 0.6, bins = input$bin_num_input) +
scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
labs(x = "Flipper length (mm)", y = "Frequency",
fill = "Penguin species") +
myCustomTheme
})
} # END server
Deploying apps with shinyapps.io
Connect your shinyapps.io account to RStudio
Go to shinyapps.io and login or create an account (if you don’t already have one) – I created my account and login with GitHub. To use shinyapps.io, you first need to link your account with RStudio on your computer. Follow the instructions on shinyapps.io when you first create your account to install the {rsconnect}
package and authorize your account:
Deploy your app to shinyapps.io
Once your account has been authorized, run rsconnect::deployApp("<app_directory_name>")
in your console to deploy your app to shinyapps.io. Here, we’ll run rsconnect::deployApp("two-file-app")
to deploy the app we’ve been working on.
Once deployed, a browser will open to your application. The URL will take the form: https://username.shinyapps.io/your_app_directory_name. You should also now see an /rsconnect
folder within your app’s directory – this is generated when an application bundle is successfully deployed and contains a DCF file with information on the deployed content (i.e. the name, title, server address, account, URL, and time). This /rsconnect
folder should be added and committed into version control (i.e. push it to GitHub!) so that future re-deployments target the same endpoint (i.e. your app’s URL).
The shinyapps.io dashboard
Your shinyapps.io dashboard provides tons of information about your application metrics, instance (the virtualized server that your app is hosted on) and worker (a special type of R process that an Application Instance runs to service requests to an application) settings, plan management, and more. The free plan (the plan we’re using here today) allows you to deploy five Shiny apps. You are able to archive and/or delete once-deployed apps to make space for new ones.
Check out the shinyapps.io user guide for more information on hosting your apps on shinyapps.io.
Other ways to host your Shiny apps
shinyapps.io is not the only Shiny app hosting service (though it’s the easiest to get started with and the only one we’ll be covering in detail in this workshop).
Posit also offers the following:
Shiny server is an open source server which you can deploy for free on your own hardware. It requires more setup and configuration, but can be used without a fee. The Bren and NCEAS servers are configured with Shiny Server for hosting for some in-house apps.
Posit connect is a paid product that provides an advanced suite of services for hosting Shiny apps, Quarto and R Markdown reports, APIs, and more.
So how should I host my app?
The Bren compute team will work with groups to deploy and maintain apps on in-house servers for up to 6 months after capstone/GP presentations or until they break.
If you and/or your client wish to continue using your app after this time, we recommend one of the following two options:
Preferred: Stick with the free tier of shinyapps.io, if you can! This is by far the most straightforward option that requires no server maintenance for you or your client. If your app exceeds the limitations set by the free tier (e.g. requires more active hours, needs more RAM or instances to support high traffic usage, etc.), you/your client have the option to upgrade to a paid tier – there are 5 paid plan types. Check out the shinyapps.io user guide for more information. Consider setting aside your allocated capstone/GP funds to help support a paid shinyapps.io plan.
If you have a server-savvy client, they may want to deploy/host your app using their own infrastructure. If your client plans to pursue this option, but does not yet have a their own server configured to do so, we recommend directing them to the online instructions for getting started with Shiny Server. PLEASE NOTE that Bren staff (including the compute team) are unable to provide technical support for clients in server configuration and app deployment/maintenance.
IMPORTANT: Hosting on a server means that shiny applications will be prone to breaking as updates to server software are made. It is important to have an application maintenance plan in place. This may mean identifying who is responsible for maintaining code, or even deciding to decommission applications and archive the code repository when appropriate.
Improving user experience
Learning Objectives - App #2v2 (two-file app)
By the end of this section, you should:
understand how to provide users with helpful error messages using validate()
know how to add customizable loading animations to alert users when reactive objects are re-rendering
know how to add alternate (alt) text to rendered plots
understand how to republish an app using shinyapps.io
Packages introduced:
shinycssloaders: add loading animations to shiny outputs
Roadmap for App #2v2
We’ll be refining our two-file app with a focus on creating a more user-friendly experience. When finished with v2, we’ll have added:
(a) user-friendly validation error messages that appear when widgets are used to deselect all data
(b) loading animations for both two reactive plots
(c) alternate (alt) text for all data visualizations
Take out any guesswork for your app’s users
It’s important to remove any possible points of confusion for successfully using your app.
In version 1 of our published app, you’ll notice that users are able to (1) Deselect All data using the pickerInput
s for both the trout and penguin plots, and (2) “uncheck” both clear cut forest and old growth forest sampling section buttons using the checkboxGroupInput
. When any of these actions are taken by the user, all data are removed from the plot, leaving a completely blank box behind.
While this response is expected (and normal), we can generate a user-friendly validation error message to provide clear guidance to our users on what is expected by the app in order to display data.
Writing validation tests
validate()
tests a condition and returns an error if that conditions fails. It’s used in conjunction with need()
, which takes an expression that returns TRUE
or FALSE
, along with a character string to return if the condition is FALSE
.
Place your validation test(s) at the start of any reactive()
or render*()
expression that calls input$data
. For example, we can add two validation tests inside the reactive that generates our trout_filtered_df
– we’ll need two separate validation tests, one for each of our inputs where users can deselect all data.
server.R
server <- function(input, output) {
# filter for channel types ----
trout_filtered_df <- reactive({
validate(
need(length(input$channel_type_input) > 0, "Please select at least one channel type to visualize data for."),
need(length(input$section_input) > 0, "Please select at least one section (clear cut forest or old growth forest) to visualize data for.")
)
clean_trout |>
filter(channel_type %in% c(input$channel_type_input)) |>
filter(section %in% c(input$section_input))
})
}
& Exercise 4: Add a validation test for your penguin histogram
To Do:
Construct a validation test that displays a clear but succinct message when a user deselects all islands using the pickerWidget
Tips:
Despite having two inputs, we only need one validation test for our Penguins plot. Why is this?
See next slide for a solution!
Exercise 4: A solution
server.R
server <- function(input, output) {
# filter for channel types ----
trout_filtered_df <- reactive({
validate(
need(length(input$channel_type_input) > 0, "Please select at least one channel type to visualize data for.")
)
validate(
need(length(input$section_input) > 0, "Please select at least one section (clear cut forest or old growth forest) to visualize data for.")
)
clean_trout |>
filter(channel_type %in% c(input$channel_type_input)) |>
filter(section %in% c(input$section_input))
})
# trout scatterplot ----
output$trout_scatterplot <- renderPlot({
ggplot(trout_filtered_df(), aes(x = length_mm, y = weight_g, color = channel_type, shape = channel_type)) +
geom_point(alpha = 0.7, size = 5) +
scale_color_manual(values = c("cascade" = "#2E2585", "riffle" = "#337538", "isolated pool" = "#DCCD7D",
"pool" = "#5DA899", "rapid" = "#C16A77", "step (small falls)" = "#9F4A96",
"side channel" = "#94CBEC")) +
scale_shape_manual(values = c("cascade" = 15, "riffle" = 17, "isolated pool" = 19,
"pool" = 18, "rapid" = 8, "step (small falls)" = 23,
"side channel" = 25)) +
labs(x = "Trout Length (mm)", y = "Trout Weight (g)", color = "Channel Type", shape = "Channel Type") +
myCustomTheme
})
# filter for island ----
island_df <- reactive({
validate(
need(length(input$penguin_island) > 0, "Please select at least one island to visualize data for.")
)
penguins %>%
filter(island %in% input$penguin_island)
})
# render the flipper length histogram ----
output$flipperLength_histogram <- renderPlot({
ggplot(na.omit(island_df()), aes(x = flipper_length_mm, fill = species)) +
geom_histogram(alpha = 0.6, bins = input$bin_num) +
scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
labs(x = "Flipper length (mm)", y = "Frequency",
fill = "Penguin species") +
myCustomTheme
})
} # END server
Add loading animations to re-rendering outputs
The {shinycssloaders}
package makes it easy to add visual indicators to outputs as they’re loading or re-rendering. This can be particularly helpful if you have outputs that take a few seconds to render – it alerts users that their updated inputs were recognized and that the app is working to re-render outputs.
Check out the demo app to start designing your own “spinner” (choose style, color, size).
We can pipe the withSpinner()
function directly into our plotOutput
s in ui.R
(be sure to load the package in global.R
first) – here, we define the spinner color and style (there are 8 different spinner type
s to choose from) and adjust the size of the penguin plot spinner.
Include alt text for all data visualizations
Alt text are written descriptions added to images, and importantly, to data visualizations, to help more users understand the content. Assistive technologies (e.g. screen readers) read alt text out loud for users to hear. When alt text is successfully added, the alt
tag (along with your text) should appear in the HTML (right click on your app’s data viz to Inspect and ensure that it was added).
We’ll talk a bit more about alt text later on, but for now we can add alt text easily to our data visuzliations using the alt
argument. Place this outside of the {}
but inside the ()
of renderPlot{()}
. For example, we can add alt text to our trout and penguin plots in server.R
:
server.R
# render trout scatterplot ----
output$trout_scatterplot <- renderPlot({
ggplot(trout_filtered_df(), aes(x = length_mm, y = weight_g, color = channel_type, shape = channel_type)) +
geom_point(alpha = 0.7, size = 5) +
scale_color_manual(values = c("cascade" = "#2E2585", "riffle" = "#337538", "isolated pool" = "#DCCD7D",
"pool" = "#5DA899", "rapid" = "#C16A77", "step (small falls)" = "#9F4A96",
"side channel" = "#94CBEC")) +
scale_shape_manual(values = c("cascade" = 15, "riffle" = 17, "isolated pool" = 19,
"pool" = 18, "rapid" = 8, "step (small falls)" = 23,
"side channel" = 25)) +
labs(x = "Trout Length (mm)", y = "Trout Weight (g)", color = "Channel Type", shape = "Channel Type") +
myCustomTheme
},
alt = "A scatterplot of the relationship between cutthroat trout lengths (mm) (x-axis) and weights (g) (y-axis), with data points colored and shaped based on the water channel type from which they were collected. Trout tend to be longer, but weight less in waterways within the old growth forest. Trout tend to be shorter, but weight more in waterways within the clear cut forest."
) # END render trout scatterplot
# render flipperLength hisogram ----
output$flipperLength_histogram <- renderPlot({
ggplot(na.omit(island_df()), aes(x = flipper_length_mm, fill = species)) +
geom_histogram(alpha = 0.6, bins = input$bin_num) +
scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
labs(x = "Flipper length (mm)", y = "Frequency",
fill = "Penguin species") +
myCustomTheme
},
alt = "A histogram of penguin flipper lengths (mm), with data colored by penguin species. Flipper lengths tend to be smallest on Adélie penguins and largest on Gentoo penguins."
) # END render flipperLength histogram
Redeploying apps with shinyapps.io
Redeploying is similar to deploying for the first time
Just a few quick steps before your updates are live at your URL from earlier:
1. double check to make sure any required packages are being imported in global.R
2. Rerun rsconnect::deployApp("<app-directory-nam")>
in your console and type Y
when you see the prompt Update application currently deployed at https://githubUserName.shinyapps.io/yourAppName? [Y/n]
3. Give it a minute (or few) to deploy. Your deployed app will open in a browser window once processed
4. Push all your files (including the /rsconnect
directory) to GitHub
Check out my deployed app at https://samanthacsik.shinyapps.io/two-file-app/
Break
05:00
Building dashboards with shinydashboard
Learning Objectives - App #3 (shinydashboard)
After this section, you should:
understand the general workflow for pre-processing, saving & reading data into an app
be comfortable building out a dashboard UI using shinydashboard layout functions
understand how to add static images to your app
feel comfortable creating a basic reactive leaflet
map
Packages introduced:
shinydashboard: provides an alternative UI framework for easily building dashboard-style shiny applications
leaflet: for building interactive maps
Roadmap for App #3
In this section, we’ll be building a shinydashboard using data downloaded from the Arctic Data Center. We’ll be building out the following features:
(a) a dashboardHeader
with the name of your app
(b) a dashboardSidebar
with two menuItem
s
(c) a landing page with background information about your app
(d) an interactive and reactive leaflet
map
But first, what do we mean by a shiny “dashboard”?
shinydashboard
is just an alternative framework for building shiny apps. In other words, shiny dashboards are just shiny apps, but with some different UI elements that make building apps with a classic “dashboard” feel to them a little bit easier.
The most basic shinydashboard is made up of a header, a sidebar, and a body
The main difference between a shiny app and a shinydashboard are the UI elements. Rather than a fluidPage()
(as used in our previous shiny apps), we’ll create a dashboardPage()
, which expects three main parts: a header, a sidebar, and a body. Below is the most minimal possible UI for a shinydashboard page (you can run this code in an app.R
file, if you wish).
#..............................setup.............................
library(shiny)
library(shinydashboard)
#...............................ui...............................
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(),
dashboardBody()
)
#.............................server.............................
server <- function(input, output) {}
#......................combine ui & server.......................
shinyApp(ui, server)
Example shiny dashboards built by some familiar folks
Bren Student Data Explorer (source code), by MEDS 2022 alum, Halina Do-Linh, during her Bren Summer Fellowship (and continued by future MEDS students!) – explore Bren school student demographics and career outcomes
Sam’s Strava Stats (source code), by yours truly, Sam Csik – a new and ongoing side project exploring my Strava hiking/biking/walking data
Channel Islands National Park’s Kelp Forest Monitoring Program (source code), by MEDS 2022 alum, Cullen Molitor – explore subtidal monitoring data collected from our closest National Park
The Outdoor Equity App (source code), developed by MEDS 2022 alumni Halina Do-Linh & Clarissa Boyajian as part of their MEDS capstone project – analyze patterns in the access and demand of visitors at reservable overnight sites
Visualizing human impacts on at-risk marine biodiversity (source code, developed by MESM 2022 alum, Ian Brunjes & Dr. Casey O’Hara) – explore how human activities and climate change impact marine biodiversity worldwide
Setup our shiny dashboard
First, create a subdirectory called /shinydashboard
and add a ui.R
, server.R
, and global.R
file.
Add the server function to server.R
and the three main UI components (header, sidebar, and body) to our dashboard page. You can do so just as the example a few slides back, or alternatively, you can split the UI into separate pieces, then combine them into a dashboardPage
the end of ui.R
(as shown below) – this can help with organization as you app grows in complexity.
We’ll set our dashboard aside for now while we work on downloading and pre-processing our data, as well as practice creating our data visualization outside of our app.
#........................dashboardHeader.........................
header <- dashboardHeader()
#........................dashboardSidebar........................
sidebar <- dashboardSidebar()
#..........................dashboardBody.........................
body <- dashboardBody()
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
As always, let’s start with the data
Building an app doesn’t make much sense if we don’t know what we’re going to put in it. So, just like the last two apps, we’ll start with some data wrangling and practice data visualization.
Unlike our last two apps, however, we’ll be working with tabular data from the Arctic Data Center, which we’ll download, process, save, then finally, read into our application. This process will likely be more similar to what you’ll encounter when working on your own applications moving forward. Take a few minutes to review the metadata record for the following data set, and download FCWO_lakemonitoringdata_2011_2022_daily.csv:
Christopher Arp, Matthew Whitman, Katie Drew, and Allen Bondurant. 2022. Water depth, surface elevation, and water temperature of lakes in the Fish Creek Watershed in northern Alaska, USA, 2011-2022. Arctic Data Center. doi:10.18739/A2JH3D41P.
Pre-processing data is critical
Where you choose to store the data used by your Shiny app will depend largely on the type and size of the file(s) and who “owns” those data. It is likely that you’ll be working with data stored in a database or on a server. This is outside the scope of this workshop, but I suggest reading Dean Attali’s article, Persistent data storage in Shiny apps to start. Because we are going to be working with a relatively small data set, we’ll be downloading and storing our data locally (i.e. on our machines and in our GitHub repo).
Regardless of where you choose to store your data, you can help your application more quickly process inputs/outputs by providing it only as much data as needed to run. This means pre-processing your data.
FCWO_lakemonitoringdata_2011_2022_daily.csv
contains 8 attributes (variables) and 18,894 observations collected from a set of 11 lakes located in the Fish Creek Watershed in northern Alaska between 2011-2022. We’ll download and save the file to a raw_data/
folder in the root directory of our repository. We’ll then pre-process the data in a separate script(s) saved to scratch/
and save a cleaned/processed version of the data to our app’s directory, /shinydashboard/data/lake_data_processed.csv
. Your repository structure should look similar to example on the right:
The Goal:
Our goal is to create a leaflet
map with makers placed on each of the 11 unique lakes where data were collected. When clicked, a marker should reveal the lake name, elevation (in meters, above sea level), average depth of the lake (in meters), and average lake bed temperature (in degrees Celsius). To do so, we’ll need a data frame that looks like the example below:
Process lake data & save new file
NOTE: In this example exercise, I’ve removed all rows with missing values (i.e. NaN
s in the Depth
column & NA
s in the BedTemperature
column) before calculating averages. This is NOT good practice – exploring and thinking critically about missing data is an important part of data analysis, and in a real-life scenario, you should consider the most appropriate method for handling them.
scratch/data_processing_app3_shinydashboard.R
#....................SETUP & DATA PROCESSING.....................
# load packages ----
library(tidyverse)
library(leaflet)
# read in raw data ----
lake_monitoring_data <- read_csv("raw_data/FCWO_lakemonitoringdata_2011_2022_daily.csv")
# calculate avg depth & temp ----
avg_depth_temp <- lake_monitoring_data |>
select(Site, Depth, BedTemperature) |>
filter(Depth != "NaN") |> # remove NaN (missing data) from Depth
drop_na(BedTemperature) |> # remove NAs (missing data) from BedTemperature
group_by(Site) |>
summarize(
AvgDepth = round(mean(Depth), 1),
AvgTemp = round(mean(BedTemperature), 1))
# join avg depth & temp to original data (match rows based on 'Site') ---
lake_monitoring_data <- full_join(lake_monitoring_data, avg_depth_temp)
# get unique lakes observations (with corresponding lat, lon, elev, avgDepth, avgTemp) for mapping ----
unique_lakes <- lake_monitoring_data |>
select(Site, Latitude, Longitude, Elevation, AvgDepth, AvgTemp) |>
distinct()
# save processed data to your app's data directory ----
write_csv(unique_lakes, "shinydashboard/data/lake_data_processed.csv")
A note on file types
Oftentimes, you may choose to save your processed data frame as a .rds
file (a data file format, native to R, which stores a single R object). .rds
file are relatively small (and therefore take up little storage space), take less time to import/export, and preserve data types and classes (e.g. factors and dates), eliminating the need to redefine data types after loading the file. Bear in mind that this increased speed and space-saving may come at the cost of generality – you can’t open a .rds
file outside of R or read it in with another programming language (e.g. Python).
While we’ll be sticking to .csv
files in this workshop, it’s worth experimenting with .rds
when you begin working with your own (likely larger) data. You can read in (readRDS()
) and write out to (saveRDS()
) .rds
files as easily as .csv
files.
Draft leaflet
map
There are lots of ways to customize leaflet
maps. We’ll be keeping ours relatively simple, but check out the Leaflet for R documentation for more ways to get creative with your maps.
scratch/practice_script_app3_shinydashboard.R
#....................SETUP & DATA PROCESSING.....................
# omitted for brevity (see slide 112 for code)
#..........................PRACTICE VIZ..........................
leaflet() |>
# add tiles
addProviderTiles("Esri.WorldImagery") |>
# set view over AK
setView(lng = -152.048442, lat = 70.249234, zoom = 6) |>
# add mini map
addMiniMap(toggleDisplay = TRUE, minimized = TRUE) |>
# add markers
addMarkers(data = unique_lakes,
lng = unique_lakes$Longitude, lat = unique_lakes$Latitude,
popup = paste("Site Name:", unique_lakes$Site, "<br>",
"Elevation:", unique_lakes$Elevation, "meters (above SL)", "<br>",
"Avg Depth:", unique_lakes$AvgDepth, "meters", "<br>",
"Avg Lake Bed Temperature:", unique_lakes$AvgTemp, "deg Celsius"))
Practice filtering leaflet
observations
We’ll eventually build three sliderInput
s to filter lake makers by Elevation
, AvgDepth
, and AvgTemp
. Practice filtering here first (and be sure to update the data frame name in your leaflet code!):
scratch/practice_script_app3_shinydashboard.R
#....................SETUP & DATA PROCESSING.....................
# omitted for brevity (see slide 112 for code)
#.......................PRACTICE FILTERING.......................
filtered_lakes <- unique_lakes |>
filter(Elevation >= 8 & Elevation <= 20) |>
filter(AvgDepth >= 2 & AvgDepth <= 3) |>
filter(AvgTemp >= 4 & AvgTemp <= 6)
#..........................PRACTICE VIZ..........................
leaflet() |>
# add tiles
addProviderTiles("Esri.WorldImagery", # make note of using appropriate tiles
options = providerTileOptions(maxNativeZoom = 19, maxZoom = 100)) |>
# add mini map
addMiniMap(toggleDisplay = TRUE, minimized = TRUE) |>
# set view over AK
setView(lng = -152.048442, lat = 70.249234, zoom = 6) |>
# add markers
addMarkers(data = filtered_lakes,
lng = filtered_lakes$Longitude, lat = filtered_lakes$Latitude,
popup = paste("Site Name:", filtered_lakes$Site, "<br>",
"Elevation:", filtered_lakes$Elevation, "meters (above SL)", "<br>",
"Avg Depth:", filtered_lakes$AvgDepth, "meters", "<br>",
"Avg Lake Bed Temperature:", filtered_lakes$AvgTemp, "deg Celsius"))
Sketch out our dashboard UI
I want my dashboard to have two menu items: a welcome page with some background information, and a dashboard page with my reactive map. All elements will be placed inside box
es, the primary building blocks of shinydashboards (more on that soon).
Add a title
& menuItem
s
First, add a title to dashboardHeader()
and make more space using titleWidth
, if necessary.
Next, we’ll build our dashboardSidebar()
. Add a sidebarMenu()
that contains two menuItem
s. Be sure to provide each menuItem()
with text
as you’d like it to appear in your app (for me, that’s Welcome and Dashboard), and a tabName
which will be used to place dashboardBody()
content in the appropriate menuItem()
. Optionally, you can provide an icon. By default, icon()
uses icons from FontAwesome.
ui.R
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Fish Creek Watershed Lake Monitoring",
titleWidth = 400
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Welcome", tabName = "welcome", icon = icon("star")),
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody()
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
Add tabItems
to your dashboardBody
Next, we’ll create tabItems
in our dashboardBody
– we’ll make a tabItem
(singular) for each menuItem
in our dashboardSidebar
. In order to match a menuItem
and a tabItem
, ensure that they have matching a tabName
(e.g. any content added to the dashboard tabItem
will appear under the dashboard menuItem
).
ui.R
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Fish Creek Watershed Lake Monitoring",
titleWidth = 400
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Welcome", tabName = "welcome", icon = icon("star")),
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# tabItems ----
tabItems(
# welcome tabItem ----
tabItem(tabName = "welcome",
"background info here"
), # END welcome tabItem
# dashboard tabItem ----
tabItem(tabName = "dashboard",
"dashboard content here"
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
Add box
es to contain UI content (part 1)
Boxes are the primary building blocks of shinydashboards and can contain almost any Shiny UI element (e.g. text, inputs, outputs). Start by adding two side-by-side boxes to our dashboard tab inside a fluidRow()
. Together, their widths will add up to 12 (the total width of a browser page). These boxes will eventually contain our sliderInput
s and our leafletOutput
.
ui.R
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Fish Creek Watershed Lake Monitoring",
titleWidth = 400
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Welcome", tabName = "welcome", icon = icon("star")),
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# tabItems ----
tabItems(
# welcome tabItem ----
tabItem(tabName = "welcome",
"background info here"
), # END welcome tabItem
# dashboard tabItem ----
tabItem(tabName = "dashboard",
# fluidRow ----
fluidRow(
# input box ----
box(width = 4,
"sliderInputs here"
), # END input box
# leaflet box ----
box(width = 8,
"leafletOutput here"
) # END leaflet box
) # END fluidRow
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
Add box
es to contain UI content (part 2)
Lastly, add boxes to our welcome tab We’ll use column
s to place one box on the left-hand side of our page, and two stacked boxes on the right-hand side. Each column
will take up half the page (Note: For column-based layouts, use NULL
for the box width, as the width is set by the column that contains the box). We can create two fluidRow
s within the right-hand column to stack two boxes vertically.
ui.R
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Fish Creek Watershed Lake Monitoring",
titleWidth = 400
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Welcome", tabName = "welcome", icon = icon("star")),
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# tabItems ----
tabItems(
# welcome tabItem ----
tabItem(tabName = "welcome",
# left-hand column ----
column(width = 6,
# background info box ----
box(width = NULL,
"background info here"
), # END background info box
), # END left-hand column
# right-hand column ----
column(width = 6,
# first fluidRow ----
fluidRow(
# data source box ----
box(width = NULL,
"data citation here"
) # END data source box
), # END first fluidRow
# second fluiRow ----
fluidRow(
# disclaimer box ----
box(width = NULL,
"disclaimer here"
) # END disclaimer box
) # END second fluidRow
) # END right-hand column
), # END welcome tabItem
# dashboard tabItem ----
tabItem(tabName = "dashboard",
# fluidRow ----
fluidRow(
# input box ----
box(width = 4,
"sliderInputs here"
), # END input box
# leaflet box ----
box(width = 8,
"leaflet output here"
) # END leaflet box
) # END fluidRow
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
Read data into global.R
& add necessary packages
Remember to load your pre-processed data, which should live in the /data
folder within your app’s directory.
Add a sliderInput
& leafletOutput
to the UI
Start by adding just one sliderInput
(for selecting a range of lake Elevation
s) to the left-hand box in the dashboard tab. Then, add a leafletOutput
to create a placeholder space for our map, along with a Spinner animation (from the shinycssloaders package). While we’re here, we can also add title
s to each box.
ui.R
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Fish Creek Watershed Lake Monitoring",
titleWidth = 400
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Welcome", tabName = "welcome", icon = icon("star")),
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# tabItems ----
tabItems(
# welcome tabItem ----
tabItem(tabName = "welcome",
# left-hand column ----
column(width = 6,
# box ----
box(width = NULL,
"background info here"
) # END box
), # END left-hand column
# right-hand column ----
column(width = 6,
# first fluidRow ----
fluidRow(
# data source box ----
box(width = NULL,
"data citation here"
) # END data source box
), # END first fluidRow
# second fluiRow ----
fluidRow(
# disclaimer box ----
box(width = NULL,
"disclaimer here"
) # END disclaimer box
) # END second fluidRow
) # END right-hand column
), # END welcome tabItem
# dashboard tabItem ----
tabItem(tabName = "dashboard",
# fluidRow ----
fluidRow(
# input box ----
box(width = 4,
title = tags$strong("Adjust lake parameter ranges:"),
# sliderInputs ----
sliderInput(inputId = "elevation_slider_input", label = "Elevation (meters above SL):",
min = min(lake_data$Elevation), max = max(lake_data$Elevation),
value = c(min(lake_data$Elevation), max(lake_data$Elevation)))
), # END input box
# leaflet box ----
box(width = 8,
title = tags$strong("Monitored lakes within Fish Creek Watershed:"),
# leaflet output ----
leafletOutput(outputId = "lake_map") |> withSpinner(type = 1, color = "#4287f5")
) # END leaflet box
) # END fluidRow
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
Assemble inputs & outputs in server.R
Remember to reference your practice data viz script and to follow our three steps for creating reactive outputs. And don’t forget to add ()
following each reactive data frame called in your leaflet map!
server.R
server <- function(input, output) {
# filter lake data ----
filtered_lakes <- reactive ({
lake_data |>
filter(Elevation >= input$elevation_slider_input[1] & Elevation <= input$elevation_slider_input[2])
})
# build leaflet map ----
output$lake_map <- renderLeaflet({
leaflet() |>
# add tiles
addProviderTiles("Esri.WorldImagery") |>
# set view over AK
setView(lng = -152.048442, lat = 70.249234, zoom = 6) |>
# add mini map
addMiniMap(toggleDisplay = TRUE, minimized = TRUE) |>
# add markers
addMarkers(data = filtered_lakes(),
lng = filtered_lakes()$Longitude, lat = filtered_lakes()$Latitude,
popup = paste("Site Name:", filtered_lakes()$Site, "<br>",
"Elevation:", filtered_lakes()$Elevation, "meters (above SL)", "<br>",
"Avg Depth:", filtered_lakes()$AvgDepth, "meters", "<br>",
"Avg Lake Bed Temperature:", filtered_lakes()$AvgTemp, "deg Celsius"))
})
}
Run your app & test out your first widget
If all is good, you should see something similar to this:
Exercise 5: Add two more sliderInput
s to filter for AvgDepth
& AvgTemp
To Do:
Add two more sliderInput
s, one for AvgDepth
and one for AvgTemp
beneath our first Elevation sliderInput
in the UI
Update our reactive data frame so that all three widgets filter the leaflet map
See next slide for a solution!
Exercise 5: A solution
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Fish Creek Watershed Lake Monitoring",
titleWidth = 400
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Welcome", tabName = "welcome", icon = icon("star")),
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# tabItems ----
tabItems(
# welcome tabItem ----
tabItem(tabName = "welcome",
# left-hand column ----
column(width = 6,
# box ----
box(width = NULL,
"background info here"
) # END box
), # END left-hand column
# right-hand column ----
column(width = 6,
# first fluidRow ----
fluidRow(
# data source box ----
box(width = NULL,
"data citation here"
) # END data source box
), # END first fluidRow
# second fluiRow ----
fluidRow(
# disclaimer box ----
box(width = NULL,
"disclaimer here"
) # END disclaimer box
) # END second fluidRow
) # END right-hand column
), # END welcome tabItem
# dashboard tabItem ----
tabItem(tabName = "dashboard",
# fluidRow ----
fluidRow(
# input box ----
box(width = 4,
title = tags$strong("Adjust lake parameter ranges:"),
# sliderInputs ----
sliderInput(inputId = "elevation_slider_input", label = "Elevation (meters above SL):",
min = min(lake_data$Elevation), max = max(lake_data$Elevation),
value = c(min(lake_data$Elevation), max(lake_data$Elevation))),
sliderInput(inputId = "depth_slider_input", label = "Average depth (meters):",
min = min(lake_data$AvgDepth), max = max(lake_data$AvgDepth),
value = c(min(lake_data$AvgDepth), max(lake_data$AvgDepth))),
sliderInput(inputId = "temp_slider_input", label = "Average lake bed temperature (degrees C):",
min = min(lake_data$AvgTemp), max = max(lake_data$AvgTemp),
value = c(min(lake_data$AvgTemp), max(lake_data$AvgTemp)))
), # END input box
# leaflet box ----
box(width = 8,
title = tags$strong("Monitored lakes within Fish Creek Watershed:"),
# leaflet output ----
leafletOutput(outputId = "lake_map") |> withSpinner(type = 1, color = "#4287f5")
) # END leaflet box
) # END fluidRow
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
server <- function(input, output) {
# filter lake data ----
filtered_lakes <- reactive ({
lake_data |>
filter(Elevation >= input$elevation_slider_input[1] & Elevation <= input$elevation_slider_input[2]) |>
filter(AvgDepth >= input$depth_slider_input[1] & AvgDepth <= input$depth_slider_input[2]) |>
filter(AvgTemp >= input$temp_slider_input[1] & AvgTemp <= input$temp_slider_input[2])
})
# build leaflet map ----
output$lake_map <- renderLeaflet({
leaflet() |>
# add tiles
addProviderTiles("Esri.WorldImagery") |>
# set view over AK
setView(lng = -152.048442, lat = 70.249234, zoom = 6) |>
# add mini map
addMiniMap(toggleDisplay = TRUE, minimized = TRUE) |>
# add markers
addMarkers(data = filtered_lakes(),
lng = filtered_lakes()$Longitude, lat = filtered_lakes()$Latitude,
popup = paste("Site Name:", filtered_lakes()$Site, "<br>",
"Elevation:", filtered_lakes()$Elevation, "meters (above SL)", "<br>",
"Avg Depth:", filtered_lakes()$AvgDepth, "meters", "<br>",
"Avg Lake Bed Temperature:", filtered_lakes()$AvgTemp, "deg Celsius"))
})
}
Break
05:00
& Exercise 6: Add titles & text to Welcome page boxes
To Do:
Add titles to each box
Create a /text
folder within your app’s directory and add three markdown (.md
) files. Write/format text for the background info (left), data citation (top-right), and disclaimer (bottom-right) boxes. Example text below:
text/intro.md
The [Fish Creek Watershed Observatory (FCWO)](http://www.fishcreekwatershed.org/) is a focal watershed within the [National Petroleum Reserve in Alaska (NPR-A)](https://www.blm.gov/programs/energy-and-minerals/oil-and-gas/about/alaska/NPR-A). Targeted lake and stream monitoring within the watershed provide site-specific data prior to and after the establishment of new petroleum development, as well as insight into dynamics related to climate change and variability. Eleven lakes of interest (Harry Potter, Hipbone, Iceshove, L9817, L9819, L9820, Little Alaska, Lower Snowman, M9925, Middle Snowman, and Serenity) are featured in this dashboard.
text/citation.md
Data presented in this dashboard were collected as part of the [Fish Creek Watershed Observatory](http://www.fishcreekwatershed.org/) are archived and publicly accessible on the NSF [Arctic Data Center](https://arcticdata.io/). **Citation:**
*Christopher Arp, Matthew Whitman, Katie Drew, and Allen Bondurant. 2022. Water depth, surface elevation, and water temperature of lakes in the Fish Creek Watershed in northern Alaska, USA, 2011-2022. Arctic Data Center [doi:10.18739/A2JH3D41P](https://arcticdata.io/catalog/view/doi%3A10.18739%2FA2JH3D41P).*
Tips:
Titles can include icons! For example: title = tagList(icon("icon-name"), strong("title text here"))
Exercise 6: A solution
Press the right arrow key to advance through the newly added lines of code.
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Fish Creek Watershed Lake Monitoring",
titleWidth = 400
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Welcome", tabName = "welcome", icon = icon("star")),
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# tabItems ----
tabItems(
# welcome tabItem ----
tabItem(tabName = "welcome",
# left-hand column ----
column(width = 6,
# box ----
box(width = NULL,
title = tagList(icon("water"), strong("Monitoring Fish Creek Watershed")),
includeMarkdown("text/intro.md")
) # END box
), # END left-hand column
# right-hand column ----
column(width = 6,
# first fluidRow ----
fluidRow(
# data source box ----
box(width = NULL,
title = tagList(icon("table"), strong("Data Source")),
includeMarkdown("text/citation.md")
) # END data source box
), # END first fluidRow
# second fluiRow ----
fluidRow(
# disclaimer box ----
box(width = NULL,
title = tagList(icon("triangle-exclamation"), strong("Disclaimer")),
includeMarkdown("text/disclaimer.md")
) # END disclaimer box
) # END second fluidRow
) # END right-hand column
), # END welcome tabItem
# dashboard tabItem ----
tabItem(tabName = "dashboard",
# fluidRow ----
fluidRow(
# input box ----
box(width = 4,
title = tags$strong("Adjust lake parameter ranges:"),
# sliderInputs ----
sliderInput(inputId = "elevation_slider", label = "Elevation (meters above SL):",
min = min(lake_data$Elevation), max = max(lake_data$Elevation),
value = c(min(lake_data$Elevation), max(lake_data$Elevation))),
sliderInput(inputId = "depth_slider", label = "Average depth (meters):",
min = min(lake_data$AvgDepth), max = max(lake_data$AvgDepth),
value = c(min(lake_data$AvgDepth), max(lake_data$AvgDepth))),
sliderInput(inputId = "temp_slider", label = "Average lake bed temperature (degrees C):",
min = min(lake_data$AvgTemp), max = max(lake_data$AvgTemp),
value = c(min(lake_data$AvgTemp), max(lake_data$AvgTemp)))
), # END input box
# leaflet box ----
box(width = 8,
title = tags$strong("Monitored lakes within Fish Creek Watershed:"),
# leaflet output ----
leafletOutput(outputId = "lake_map") |> withSpinner(type = 1, color = "#4287f5")
) # END leaflet box
) # END fluidRow
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
Add a static image
As a final touch, let’s add an image to the Welcome page, inside the left-hand box beneath our intro text. First, create a /www
folder inside your app’s directory (refer back to this slide for a description of this special directory). Download the map of the Fish Creek Watershed from FCWO’s website here and save it to your /www
directory.
Next, use the img
tag to add your image. Supply a file path, relative to your /www
directory, using the src
argument, and alt text using the alt
argument.
ui.R
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Fish Creek Watershed Lake Monitoring",
titleWidth = 400
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Welcome", tabName = "welcome", icon = icon("star")),
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# tabItems ----
tabItems(
# welcome tabItem ----
tabItem(tabName = "welcome",
# left-hand column ----
column(width = 6,
# box ----
box(width = NULL,
title = tagList(icon("water"), strong("Monitoring Fish Creek Watershed")),
includeMarkdown("text/intro.md"),
tags$img(src = "FishCreekWatershedSiteMap_2020.jpeg",
alt = "A map of Northern Alaksa, showing Fish Creek Watershed located within the National Petroleum Reserve.")
) # END box
), # END left-hand column
# right-hand column ----
column(width = 6,
# first fluidRow ----
fluidRow(
# data source box ----
box(width = NULL,
title = tagList(icon("table"), strong("Data Source")),
includeMarkdown("text/citation.md")
) # END data source box
), # END first fluidRow
# second fluiRow ----
fluidRow(
# disclaimer box ----
box(width = NULL,
title = tagList(icon("triangle-exclamation"), strong("Disclaimer")),
includeMarkdown("text/disclaimer.md")
) # END disclaimer box
) # END second fluidRow
) # END right-hand column
), # END welcome tabItem
# dashboard tabItem ----
tabItem(tabName = "dashboard",
# fluidRow ----
fluidRow(
# input box ----
box(width = 4,
title = tags$strong("Adjust lake parameter ranges:"),
# sliderInputs ----
sliderInput(inputId = "elevation_slider", label = "Elevation (meters above SL):",
min = min(lake_data$Elevation), max = max(lake_data$Elevation),
value = c(min(lake_data$Elevation), max(lake_data$Elevation))),
sliderInput(inputId = "depth_slider", label = "Average depth (meters):",
min = min(lake_data$AvgDepth), max = max(lake_data$AvgDepth),
value = c(min(lake_data$AvgDepth), max(lake_data$AvgDepth))),
sliderInput(inputId = "temp_slider", label = "Average lake bed temperature (degrees C):",
min = min(lake_data$AvgTemp), max = max(lake_data$AvgTemp),
value = c(min(lake_data$AvgTemp), max(lake_data$AvgTemp)))
), # END input box
# leaflet box ----
box(width = 8,
title = tags$strong("Monitored lakes within Fish Creek Watershed:"),
# leaflet output ----
leafletOutput(outputId = "lake_map") |> withSpinner(type = 1, color = "#4287f5")
) # END leaflet box
) # END fluidRow
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
Unfortunately, our image doesn’t look so great as-is…
Use in-line CSS to adjust the image size
We can use in-line CSS to style our image element, as shown below (see style
argument). It’s okay if you don’t fully understand what’s going on here for now – we’ll talk in greater detail about how CSS (and Sass) can be used to customize the appearance of your apps in just a bit.
I’ve also added a caption below our image that links to the image source, and used in-line CSS to center my text within the box.
ui.R
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Fish Creek Watershed Lake Monitoring",
titleWidth = 400
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Welcome", tabName = "welcome", icon = icon("star")),
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# tabItems ----
tabItems(
# welcome tabItem ----
tabItem(tabName = "welcome",
# left-hand column ----
column(width = 6,
# box ----
box(width = NULL,
title = tagList(icon("water"), strong("Monitoring Fish Creek Watershed")),
includeMarkdown("text/intro.md"),
tags$img(src = "FishCreekWatershedSiteMap_2020.jpeg",
alt = "A map of Northern Alaksa, showing Fish Creek Watershed located within the National Petroleum Reserve.",
style = "max-width: 100%;"),
tags$h6(tags$em("Map Source:", tags$a(href = "http://www.fishcreekwatershed.org/", "FCWO")),
style = "text-align: center;")
) # END box
), # END left-hand column
# right-hand column ----
column(width = 6,
# first fluidRow ----
fluidRow(
# data source box ----
box(width = NULL,
title = tagList(icon("table"), strong("Data Source")),
includeMarkdown("text/citation.md")
) # END data source box
), # END first fluidRow
# second fluiRow ----
fluidRow(
# disclaimer box ----
box(width = NULL,
title = tagList(icon("triangle-exclamation"), strong("Disclaimer")),
includeMarkdown("text/disclaimer.md")
) # END disclaimer box
) # END second fluidRow
) # END right-hand column
), # END welcome tabItem
# dashboard tabItem ----
tabItem(tabName = "dashboard",
# fluidRow ----
fluidRow(
# input box ----
box(width = 4,
title = tags$strong("Adjust lake parameter ranges:"),
# sliderInputs ----
sliderInput(inputId = "elevation_slider", label = "Elevation (meters above SL):",
min = min(lake_data$Elevation), max = max(lake_data$Elevation),
value = c(min(lake_data$Elevation), max(lake_data$Elevation))),
sliderInput(inputId = "depth_slider", label = "Average depth (meters):",
min = min(lake_data$AvgDepth), max = max(lake_data$AvgDepth),
value = c(min(lake_data$AvgDepth), max(lake_data$AvgDepth))),
sliderInput(inputId = "temp_slider", label = "Average lake bed temperature (degrees C):",
min = min(lake_data$AvgTemp), max = max(lake_data$AvgTemp),
value = c(min(lake_data$AvgTemp), max(lake_data$AvgTemp)))
), # END input box
# leaflet box ----
box(width = 8,
title = tags$strong("Monitored lakes within Fish Creek Watershed:"),
# leaflet output ----
leafletOutput(outputId = "lake_map") |> withSpinner(type = 1, color = "#4287f5")
) # END leaflet box
) # END fluidRow
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
Check out your finished dashboard!
There’s a ton more to learn about building shinydashboards. Check out the documentation to find instructions on adding components like infoBox
es and valueBox
es, building inputs in the sidebar, easy ways to update the color theme using skins, and more.
Complete code for our dashboard thus far:
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Fish Creek Watershed Lake Monitoring",
titleWidth = 400
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Welcome", tabName = "welcome", icon = icon("star")),
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# ---- set theme using {fresh} ----
# fresh::use_theme("shinydashboard_fresh_theme.css"),
# tabItems ----
tabItems(
# welcome tabItem ----
tabItem(tabName = "welcome",
# left-hand column ----
column(width = 6,
# box ----
box(width = NULL,
title = tagList(icon("water"), strong("Monitoring Fish Creek Watershed")),
includeMarkdown("text/intro.md"),
tags$img(src = "FishCreekWatershedSiteMap_2020.jpeg",
alt = "A map of Northern Alaksa, showing Fish Creek Watershed located within the National Petroleum Reserve.",
style = "max-width: 100%;"),
tags$h6(tags$em("Map Source:", tags$a(href = "http://www.fishcreekwatershed.org/", "FCWO")),
style = "text-align: center;")
) # END box
), # END left-hand column
# right-hand column ----
column(width = 6,
# first fluidRow ----
fluidRow(
# data source box ----
box(width = NULL,
title = tagList(icon("table"), strong("Data Source")),
includeMarkdown("text/citation.md")
) # END data source box
), # END first fluidRow
# second fluiRow ----
fluidRow(
# disclaimer box ----
box(width = NULL,
title = tagList(icon("triangle-exclamation"), strong("Disclaimer")),
includeMarkdown("text/disclaimer.md")
) # END disclaimer box
) # END second fluidRow
) # END right-hand column
), # END welcome tabItem
# dashboard tabItem ----
tabItem(tabName = "dashboard",
# fluidRow ----
fluidRow(
# input box ----
box(width = 4,
title = tags$strong("Adjust lake parameter ranges:"),
# sliderInputs ----
sliderInput(inputId = "elevation_slider_input", label = "Elevation (meters above SL):",
min = min(lake_data$Elevation), max = max(lake_data$Elevation),
value = c(min(lake_data$Elevation), max(lake_data$Elevation))),
sliderInput(inputId = "depth_slider_input", label = "Average depth (meters):",
min = min(lake_data$AvgDepth), max = max(lake_data$AvgDepth),
value = c(min(lake_data$AvgDepth), max(lake_data$AvgDepth))),
sliderInput(inputId = "temp_slider_input", label = "Average lake bed temperature (degrees C):",
min = min(lake_data$AvgTemp), max = max(lake_data$AvgTemp),
value = c(min(lake_data$AvgTemp), max(lake_data$AvgTemp)))
), # END input box
# leaflet box ----
box(width = 8,
title = tags$strong("Monitored lakes within Fish Creek Watershed:"),
# leaflet output ----
leafletOutput(outputId = "lake_map") |> withSpinner(type = 1, color = "#4287f5")
) # END leaflet box
) # END fluidRow
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
server <- function(input, output) {
# filter lake data ----
filtered_lakes <- reactive ({
lake_data |>
filter(Elevation >= input$elevation_slider_input[1] & Elevation <= input$elevation_slider_input[2]) |>
filter(AvgDepth >= input$depth_slider_input[1] & AvgDepth <= input$depth_slider_input[2]) |>
filter(AvgTemp >= input$temp_slider_input[1] & AvgTemp <= input$temp_slider_input[2])
})
# build leaflet map ----
output$lake_map <- renderLeaflet({
leaflet() |>
# add tiles
addProviderTiles("Esri.WorldImagery") |>
# set view over AK
setView(lng = -152.048442, lat = 70.249234, zoom = 6) |>
# add mini map
addMiniMap(toggleDisplay = TRUE, minimized = TRUE) |>
# add markers
addMarkers(data = filtered_lakes(),
lng = filtered_lakes()$Longitude, lat = filtered_lakes()$Latitude,
popup = paste("Site Name:", filtered_lakes()$Site, "<br>",
"Elevation:", filtered_lakes()$Elevation, "meters (above SL)", "<br>",
"Avg Depth:", filtered_lakes()$AvgDepth, "meters", "<br>",
"Avg Lake Bed Temperature:", filtered_lakes()$AvgTemp, "deg Celsius"))
})
}
Part 3: Beautifying your user interface (UI)
Custom themes with bslib
Custom themes with fresh
Styling with CSS & Sass
Learning Objectives - Themeing/Styling Apps
By the end of this section, you should be equipped with:
a number of different approaches for themeing and styling your shiny apps and dashboards
a basic understanding of how to apply CSS & Sass styling to your app
Packages introduced:
bslib: provides tools for customizing Bootstrap themes directly from R for shiny apps and RMarkdowns
fresh: provides tools for creating custom themes for use with shiny, shinydashboard
, and bs4Dash
apps
Creating custom themes
Using the {bslib}
package to theme Shiny apps
The {bslib}
package provides tools for customizing Bootstrap themes directly from R, making custom themeing for Shiny apps (and R Markdown docs!) quite easy.
Pros:
easy to use
includes a real-time themeing widget to try out themes before applying them to your own app
plays well with the thematic package for matching plot styling to app
bslib does more than just themeing! Check out the December 2022 announcement of new UI components made possible with the latest package release
Cons:
does not work with shinydashboard
(bslib is only intended for use with shiny
apps)
styling is constrained by the arguments available to bs_theme()
Let’s practice applying new themes using bslib
to our one-file-app
(i.e. App #1)
Apply a pre-built theme with {bslib}
By default, Shiny uses the Bootstrap v3 theme (which is not so exciting). Change the theme to a slightly more modern Bootstrap v5 theme by setting the theme
argument of fluidPage()
to bslib::bs_theme(version = 5)
, or supply bs_theme()
with a pre-built bootswatch theme, as shown below (for a list of theme names, run bootswatch_themes()
in your console):
Create a custom theme with {bslib}
Alternatively, you can fully customize your own theme. Explore the bslib
vignette for detailed instructions. A small example here:
Be sure to check out the interactive themeing widget to test custom color/font/etc. combos by running bs_theme_preview()
in your console, or visit the hosted version here. You can also call bs_themer()
within your server
function to open the theme customization UI alongside your own app.
Use {thematic} to extend your theme to plots
You probably noticed that our scatterplot looks a little silly against the darker background of our themed app. Enter the {thematic}
package, which is built to help simplify plot themeing. Call thematic_shiny()
before launching your app to generate plots that reflect your application’s bs_theme()
. For example:
Read the vignette to learn more about using the thematic
package to help match plot fonts to the fonts applied across your app.
Using the {fresh}
package to theme Shiny apps & dashboards
The {fresh}
package provides tools for creating custom themes to use in Shiny apps and dashboards – set parameters of your theme using create_theme()
, generate a stylesheet based off your specifications, and apply your stylesheet to your app.
Pros:
easy to use
supports theme creation for both shiny apps and dashboards (and also flexdashboards and {b4dash}
applications)
Cons:
styling is constrained by the variables available to create_theme()
Let’s practice applying new themes using fresh
to our two-file-app
(i.e. App #2) and our shinydashbaord
(i.e. App #3)
A general workflow for using fresh
themes
Whether you’re working on a shiny app or a shiny dashboard, you’ll need the following:
1. a /www
folder within your app’s directory – this is where we’ll save the stylesheet (a .css
file) that fresh
will generate for us
2. a separate script for building our theme using the create_theme()
function – I recommend saving this to ~/scratch
(it seemed to cause issues when saved anywhere within my app directory)
Importantly, create_theme()
takes different variables to set the parameters of your theme, depending on what type of app you’re building: for shiny apps, you’ll need to use bs_vars_*
variables, and for shiny dashboards you’ll use adminlte_*
variables (examples on the following slides).
There are also a couple ways to apply your finished theme to your app, but we’ll use the method of generating a .css
file, then calling that file in our app.
Creating a fresh
theme for two-file-app
In this example, we update the colors of our app’s body, navbar, and tabPanels using the appropriate fresh
variables for shiny apps. We specify a file path, two-file-app/www
(you’ll need to create the /www
directory, since we don’t have one yet), where our stylesheet (e.g. shiny-fresh-theme.css
, as shown here) file will be saved to.
Of course, these color combos are not recommended, but chosen purely for demonstration purposes .
~/R/create-fresh-theme-shiny.R
# load library ----
library(fresh)
# create theme -----
create_theme(
# you can supply a bootstrap theme to begin with
theme = "default",
# global styling
bs_vars_global(
body_bg = "#D2D0CA", # beige
text_color = "#F23ACB", # hot pink
link_color = "#0E4BE3" # royal blue
),
bs_vars_navbar(
default_bg = "#13CC13", # lime green
default_color = "#66656C" # gray
),
# tabPanels
bs_vars_tabs(
border_color = "#F90909" # red
),
# generate css file
output_file = "two-file-app/www/shiny-fresh-theme.css"
)
Apply a fresh
theme to our app
To apply our theme, provide the theme
argument of your fluidPage()
or navbarPage()
with the name of our stylesheet. Note: shiny knows to look in the /www
directory, so you can omit that from your file path, as shown below:
Creating a fresh
theme for our shinydashboard
In this example, we update the colors of our app’s header, body, and sidebar using the appropriate fresh
variables for shiny dashboards. We specify a file path, shinydashboard/www
where our stylesheet (e.g. shinydashboard-fresh-theme.css
, as shown here) file will be saved to. Again, these color combos are not recommended, but chosen purely for demonstration purposes.
~/R/create-fresh-theme-shinydashboard.R
# load libraries ----
library(fresh)
# create theme ----
create_theme(
# change "light-blue"/"primary" color
adminlte_color(
light_blue = "#150B5A" # dark blue
),
# dashboardBody styling (includes boxes)
adminlte_global(
content_bg = "#E7B5B5" # blush pink
),
# dashboardSidebar styling
adminlte_sidebar(
width = "400px",
dark_bg = "#57F8F3", # light blue
dark_hover_bg = "#BF21E6", # magenta
dark_color = "#F90000" # red
),
# generate css file
output_file = "shinydashboard/www/shinydashboard-fresh-theme.css"
)
Apply a fresh
theme to our dashboard
To apply our theme, use the fresh::use_theme()
function inside your dashboardBody
, providing it with the name of your stylesheet. Note: shiny knows to look in the /www
directory, so you can omit that from your file path, as shown below:
Check out the complete source code for the shinydashboard here (NOTE: applied themes are commented out).
Styling apps with CSS & Sass
Using Sass & CSS to style Shiny apps & dashboards
You can write your own stylesheets using CSS and Sass to fully customize your apps, from background colors, to font styles, to size and shape of elements, and more. Unlike bslib
and fresh
, these are languages, meaning they can be a bit more challenging to get started with (but the payoff it big!).
Pros:
applies to any web page (not just shiny apps)
allows you to customize pretty much any aspect of your app
can be combined with themes generated using bslib
or fresh
to fine-tune your app’s styling
Cons:
a steeper learning curve/generally more complex than packages like bslib
and fresh
We’ll review a little bit about CSS & Sass, then practice writing and applying custom styling to apps and dashboards.
Resources for a deeper dive
We’ll be doing a rather high-level and quick overview of Sass & CSS today, though I encourage you to check out the Customizing Quarto Websites workshop, which takes a much deeper dive (the information in that workshop is largely applicable here).
W3Schools is my favorite online resource for all-things CSS – in addition to really digestible descriptions and examples, they also offer interactive tutorials to get your hands on updating (and breaking) code (in a safe space, of course).
What even is CSS? Sass?
CSS (Cascading Style Sheets) is a programming language that allows you to control how HTML elements look (e.g. colors, font styles, etc.) on a webpage.
Sass (Syntactically Awesome Stylesheets) is a CSS extension language and CSS preprocessor – meaning Sass needs to be converted (aka compiled) to CSS before it can be interpreted by your web browser.
CSS is a rule-based language
CSS is a rule-based language, meaning that it allows you to define groups of styles that should be applied to particular elements or groups of elements on a web page. For example, “I want all level one headings (<h1>
or tags$h1()
as written in Shiny) in my app to be green with a bit of extra space between each letter” could be coded as:
Selectors select the HTML element(s) you want to style (e.g. level one headings, <h1>
)
Declarations sit inside curly brackets, {}
, and are made up of property and value pairs. Each pair specifies the property of the HTML element(s) you’re selecting (e.g. the color property of the element <h1>
), and a value you’d like to assign to that property (e.g. green)
A property and it’s corresponding value are separated by a colon, :
. Declarations end with a semicolon, ;
There are a variety of CSS selectors – check out some of the basics that will take you far in styling your apps, starting on this slide of the Customizing Quarto Websites workshop.
3 ways to add CSS styling to your apps
You can (1) add styling directly to tags
, (2) add CSS rules to your header
, and/or (3) build a stylesheet that is applied to your app. Creating a stylesheet is often the preferred approach.
Add styling directly to tags. It’s best not to use a lot of these! It’s easy to lose track of your “in-line” styling in large projects, you can’t reuse rules easily, it’s hard to keep styling consistent, and it’s difficult to implement large stylistic changes.
#..............................setup.............................
library(shiny)
#...............................ui...............................
ui <- fluidPage(
# text color = purple
tags$h1("My app title",
style = "color: #711EBA;"),
# text color = blue; increase space between letters
tags$h3("Section 1",
style = "color: #1E4DBA; letter-spacing: 4px;"),
# no styling
tags$h3("Section 2"),
# increase border thickness and color green; round corners
tags$button("This is a button",
style = "border: 2px solid #1EBA38; border-radius: 5px")
)
#.............................server.............................
server <- function(input, output) {}
#......................combine ui & server.......................
shinyApp(ui, server)
Add CSS rules to your app’s header (tags$head
). This is a little bit better than option 1 since it allows for the reuse of rules, however, styles can’t be cached (i.e. saved for future usage when you reopen your app). Note: explore Google fonts here and check out this slide for instructions on selecting a font.
#..............................setup.............................
library(shiny)
#...............................ui...............................
ui <- fluidPage(
tags$head(
tags$style("
@import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
h1 {font-family: 'Lobster', cursive;}
h3 {color: blue;}
.wide-letters {letter-spacing: 4px;}
"
) # END styles
), # END head
tags$h1("My app title"),
tags$h3("Section 1"),
tags$h3(class = "wide-letters", "Section 2"),
tags$button("This is a button")
)
#.............................server.............................
server <- function(input, output) {}
#......................combine ui & server.......................
shinyApp(ui, server)
Build a stylesheet (a .css
file) inside your app’s www/
directory and apply your styles to your app’s header (for shinydashboards, include your header inside dashboardBody()
. This is the most ideal approach – it allows for style reuse, caching, and keeps styling contained in one spot. Tip: use touch styles.css
in the terminal to create a new .css
file.
# app.R
#..............................setup.............................
library(shiny)
#...............................ui...............................
ui <- fluidPage(
tags$head(
tags$link(rel = "stylesheet", type = "text/css", href = "styles.css")
),
tags$h1("My app title"),
tags$h3("Section 1"),
tags$h3(class = "wide-letters", "Section 2"),
tags$button("This is a button")
)
#.............................server.............................
server <- function(input, output) {}
#......................combine ui & server.......................
shinyApp(ui, server)
# www/styles.R
/*import google fonts (Josephine Slab (serif) & Heebo (sans serfi))*/
@import url('https://fonts.googleapis.com/css2?family=Heebo:wght@300&family=Josefin+Slab:wght@300&display=swap');
/* element selectors */
h1 {
font-family: 'Heebo', sans-serif;
color: #179A1F; /* green */
}
h3 {
font-family: 'Josefin Slab', serif;
color: #CA781C; /* orange */
}
button {
background-color: #FCF982; /* yellow */
}
/* class selectors */
.wide-letters {
letter-spacing: 4px;
}
Let’s practice on a small dashboard first:
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Penguin Dashboard"
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# tabItems ----
tabItems(
# dashboard tabItem ----
tabItem(tabName = "dashboard",
# fluidRow ----
fluidRow(
# input box ----
box(width = 4,
checkboxGroupInput(
inputId = "penguin_species",
label = "Filter by species: ",
choices = c("Adelie", "Chinstrap", "Gentoo"),
selected = c("Adelie", "Chinstrap", "Gentoo")
) # END checkboxGroupInput
), # END input box
# output box ----
box(width = 8,
plotOutput(outputId = "penguin_plot")
) # END output box
), # END fluidRow
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
server <- function(input, output) {
# penguin spp reactive df----
penguin_spp <- reactive({
palmerpenguins::penguins %>%
filter(species %in% input$penguin_species) %>%
select(species, island, bill_length_mm, bill_depth_mm)
}) # END penguin spp reactive df
# plot ----
output$penguin_plot <- renderPlot({
ggplot(penguin_spp(), aes(x = bill_length_mm, y = bill_depth_mm, color = species)) +
geom_point() +
scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4"))
}) # END plot
}
What if I want to style an element, but don’t know how to target it?
Oftentimes, you’ll have to do a bit of exploration to determine how to target specific elements for styling. In either your app viewer or web browser, right click on an element of interest and choose Inspect (or Inspect Element) to open up the underlying HTML and CSS. You can make temporary edits to your app (e.g. adding a background color, changing font sizes, etc.) to see how they look first, then copy the appropriate CSS rule into your stylesheet to apply to your app.
Inspect & identify how to update box styling
For example, let’s say I want to change the color of this shinydashboard’s boxes and the color of the box text.
First, we need to determine which type of HTML element creates our box. Right clicking on a box and choosing Inspect Element pulls up the HTML and CSS files underlying the app. Hovering over different parts of the HTML highlights different elements in the UI. The box is highlighted when I hover over <div class="box-body">
– this tells me that boxes are formed using the <div>
HTML element and they’re assigned a class called box-body
.
Next, we can (temporarily) adjust the CSS rules that style these boxes to see how they work. I can hop down to the CSS file (here, that’s located in the bottom half of my sidebar, but depending on the size of your window/layout, the HTML and CSS might be side-by-side) and find the .box-body
class selector. You can add property/value pairs and/or update existing property values to adjust the appearance of our box. Notice that changing the .box-body
class selector updates both boxes – upon inspecting the box containing our plot, you’ll notice that it is also of class, box-body
. Therefore changes to this class selector will apply to both boxes. This process is purely for testing purposes – refreshing your app will remove any of these changes.
Finally, apply box styling to our dashboard
Now that we know that we can use the box-body
class to customize the appearance of our boxes, let’s create a stylesheet and add our new rules. The shinydashboard
framework already provides the “standard” styling for boxes, contained in the box-body
class. Anything we specify in our own stylesheet will build upon or modify existing styling.
Remember to create a header and link your stylesheet within dashboardBody()
to apply our styles.
#........................dashboardHeader.........................
header <- dashboardHeader(
# add title ----
title = "Penguin Dashboard"
) # END dashboardHeader
#........................dashboardSidebar........................
sidebar <- dashboardSidebar(
# sidebarMenu ----
sidebarMenu(
menuItem(text = "Dashboard", tabName = "dashboard", icon = icon("gauge"))
) # END sidebarMenu
) # END dashboardSidebar
#..........................dashboardBody.........................
body <- dashboardBody(
# link stylesheet
tags$head(
tags$link(rel = "stylesheet", type = "text/css", href = "styles.css"),
),
# tabItems ----
tabItems(
# dashboard tabItem ----
tabItem(tabName = "dashboard",
# fluidRow ----
fluidRow(
# input box ----
box(width = 4,
checkboxGroupInput(
inputId = "penguin_species",
label = "Filter by species: ",
choices = c("Adelie", "Chinstrap", "Gentoo"),
selected = c("Adelie", "Chinstrap", "Gentoo")
) # END checkboxGroupInput
), # END input box
# output box ----
box(width = 8,
plotOutput(outputId = "penguin_plot")
) # END output box
), # END fluidRow
) # END dashboard tabItem
) # END tabItems
) # END dashboardBody
#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
What about Sass?
Okay, we wrote and applied some CSS styling to our apps, but what about Sass? You don’t need to write any Sass at all, however, it provides a number of benefits, including helping to reduce repetition.
For example, let’s say you’re working on an app that uses three primary colors throughout:
You might imagine how often you’ll need to type those HEX codes out as you developing your stylesheet…it can get annoying rather quickly.
We can define and reference Sass variables throughout our stylesheet
Sass allows us to define variables (in the form $var-name: value;
) for our colors to reference instead of writing out their HEX codes each time:
If you decide that you actually like a different shade of teal better, you’ll only need to update the hex code where you first define the $teal
Sass variable, saving lots of time.
Sass for Shiny workflow
To style apps using both Sass and CSS, you’ll follow this general workflow:
1. Create a .scss
file inside ~/myapp/www
using the touch
command in the terminal (e.g. cd
into the appropriate directory, then touch styles.scss
). Write both your Sass variables and CSS rules in your .scss
file (Note: you can write both Sass & CSS in a .scss
file, but only CSS in a .css
file)
2. Compile (i.e. convert) Sass to CSS in global.R
(or, if using a one-file app, at the top of your script before you define your UI) using the the sass()
function from the {sass}
package – this will generate a .css
file that our shiny app can actually use. Be sure to save your .css
file to your app’s /www
directory.
3. Apply your styles to your app by linking to to your .css
file in your app’s header.
Let’s build our Sass file
We’ll practice on our two-file-app
– first, remove any reference to your bslib
or fresh
themes that we practiced applying earlier so that we’re starting off with just the default shiny styling.
Next, create your Sass file within two-file-app/www/
. Then, using your terminal, cd
into ~/two-file-app/www
and use the touch
command to create a .scss
file (I’m calling mine my-sass-styles.scss
).
Add Sass variables and CSS rules to my-sass-styles.scss
:
~/two-file-app/www/my-sass-styles.scss
// import 2 fonts
@import url('https://fonts.googleapis.com/css2?family=Karma&family=Prompt:wght@200&display=swap');
// fonts
$font-family-serif: 'Karma', serif;
$font-family-sans-serif: 'Prompt', sans-serif;
// colors
$green: #8ca376;
$blue: #525cd1;
$orange: #E59C5E;
$yellow: #f0eaa5;
$white: #f1f7eb;
// element selectors
body {
background-color: $green;
color: $white;
}
h2 {
letter-spacing: 5px;
font-family: $font-family-serif;
}
h4 {
color: $blue;
font-family: $font-family-serif;
}
p {
font-family: $font-family-sans-serif;
}
a {
color: $orange;
}
// class selectors
.navbar-default {
background-color: $yellow;
}
.btn.default.active {
background-color: $green;
}
Then, compile Sass to CSS
Because web browsers can only interpret CSS (not Sass), we need to compile our Sass to CSS. To do this, we can use the sass()
function from the {sass}
package. We can do this in global.R
. The sass()
function requires two arguments: a sass file input
and a file path + named .css
file output
.
We also need to apply our styles to our app by linking this newly-generated .css
file in our app’s header.
Note: After running your app, you should see a my-sass-styles.css
file appear in www/
– it should look quite familiar, except all of our Sass variables have been converted to CSS.
@import url("https://fonts.googleapis.com/css2?family=Karma&family=Prompt:wght@200&display=swap");
body {
background-color: #8ca376;
color: #f1f7eb;
}
h2 {
letter-spacing: 5px;
font-family: "Karma", serif;
}
h4 {
color: #525cd1;
font-family: "Karma", serif;
}
p {
font-family: "Prompt", sans-serif;
}
a {
color: #E59C5E;
}
.navbar-default {
background-color: #f0eaa5;
}
.btn.default.active {
background-color: #8ca376;
}
Part 4: Improving your app’s user experience (UX)
Important UX considerations
Web accessibility
UX/UI matters
Learning Objectives - UX/UI Design
After this section, you should:
have a checklist of considerations to reference each time you build an app
have a few additional resources to dive deeper into UX/UI design
Tips for designing your Shiny apps
Chapters 6 and 7 of Engineering Production-Grade Shiny Apps, by Colin Fay, Sébastien Rochette, Vincent Guyader, and Cervan Girard provide a list of considerations as you embark on your app-building journey. Some of their suggestions are summarized below, but check out the book for greater detail, examples, and additional considerations:
Simplicity is gold: using the application shouldn’t require reading a manual, and interfaces should be as self-explanatory as possible.
Adapt a defensive programming mindset: your app should always fail gracefully and informatively (e.g. provide users with a helpful error message)
Build a self-explanatory app: consider the following three suggestions for doing so – (a) remember the rule of least surprise (in UI design, always do the least surprising thing e.g. we often assume that underlined text is clickable, so if you include underlined text in your UI, there’s a good chance a user will try clicking on it). (b) think about progression (design a clear pattern of moving forward for your user), and (c) related to b, make sure that if an input is necessary, it is made clear to your user. Check out the {shinyjs}
package for implementing nifty ways to improve the user experience of your shiny apps.
Avoid feature-creep: feature-creep is the process of adding features to an app that complicates its usage and maintenance – this includes adding too much reactivity and too much interactivity (e.g. plotly
) – interactivity adds visual noise, so it’s best to not make elements interactive if there is no value is gained.
Additional UX/UI resources
Outstanding User Interfaces with Shiny, by David Granjon
Shiny Developer Series, Episode 20: Outstanding User Interfaces with David Granjon
15 User Experience Principles and Theories, by Pathum Goonawardene
Building accessible apps
Learning Objectives - Accessibility
By the end of this section, you should:
have a general understanding of what web accessibility means and who it can benefit (spoiler alert: it benefits us all!)
know how to make a few small tweaks/updates to your application to make it more accessible for all users
have a few great resources to turn to to learn more
What is web accessibility?
From the World Wide Web Consortium (W3C)’s Introduction to Web Accessibility:
Web accessibility means that websites, tools, and technologies are designed and developed so that people with disabilities can use them. More specifically, people can:
perceive, understand, navigate, and interact with the Web
contribute to the Web
Web accessibility encompasses all disabilities that affect access to the Web, including: auditory, cognitive, neurological, physical, speech, and visual
Web accessibility also benefits people without disabilities, for example:
people using mobile phones, smart watches, smart TVs, and other devices with small screens, different input modes, etc.
older people with changing abilities due to ageing
people with “temporary disabilities” such as a broken arm or lost glasses
people with “situational limitations” such as in bright sunlight or in an environment where they cannot listen to audio
people using a slow Internet connection, or who have limited or expensive bandwidth
Check out the A11Y Project for lots of great tutorials and information about web accessibility.
Small changes can lead to tangible increases in functionality for all users
Ensuring that your shiny apps are accessible can feel overwhelming – but considering even a few small changes can have a large impact on user experience.
The following suggestions have been borrowed and adapted from Ch. 6.3 - Web Accessibility from Engineering Production-Grade Shiny Apps, by Colin Fay, Sèbastien Rochette, Vincent Guyader, & Cervan Girard:
Use HTML elements appropriately (e.g. consider hierarchy)
Include alt text for graphical elements
Consider navigation from a mobility perspective
Use colorblind-friendly palettes
Image Source: Accessibility Stack
Use HTML elements appropriately
Screen readers use HTML elements to understand web page organization. Header elements create hierarchy within a webpage and are used by screen readers (i.e. devices used by those with visual impairments) to understand a page’s organizational structure. An <h1>
element is more important (hierarchically speaking) than an <h2>
, which is more important than an <h3>
, and so on.
# load packages ----
library(shiny)
# user interface ----
ui <- fluidPage(
tags$h1("This is my app's title"),
tags$h2("Section 1"),
tags$h3("Sub-section a"),
tags$h3("Sub-section b"),
tags$h3("Sub-section c"),
tags$h2("Section 2"),
)
# server instructions ----
server <- function(input, output) {}
# combine UI & server into an app ----
shinyApp(ui = ui, server = server)
Ideally, you would only have one <h1>
element (e.g. your app’s title), a small number of <h2>
elements, more <h3>
elements, and so on. See the minimal example, to the left.
You should not rely on headers for styling purposes – for example, you should not use a level-one header elsewhere in your app just because you want larger text. Instead, use CSS to increase text size (refer to the Customizing Quarto Websites workshop for instruction on how to construct CSS selectors for styling HTML elements).
Include alt text with all graphical elements
All images and graphical elements should include alternative (alt) text that describe the image and/or information being represented. This text won’t appear in the UI, but is detected and read by screen readers.
Include the alt
argument (similar to adding the alt
attribute to an HTML element) when using renderPlot()
to add alt text to your reactive plots – the alt
argument lives outside of the {}
but inside the ()
. For example:
Tips on writing alt text for data visualizations
A good rule of thumb for writing alt text for data visualizations is alt=“Chart type of type of data where reason for including chart” (see this post by Amy Cesal for more). One example:
alt=“Bar chart of gun murders per 100,000 people where America’s murder rate is 6 times worse than Canada, and 30 times Australia”
For more great tips on how and when to use alt text, check out this article by the A11Y Project. For examples of how to construct good alt text, take a peek at this resource by Datawrapper.
Consider UI navigation for those with mobility impairments
For users with mobility impairments, using a mouse to navigate a UI packed with widgets may be challenging – some users may even be exclusively using a keyboard to navigate the web.
Ideally, actions required of your user can be done using a keyboard (e.g. pressing a button in the UI) – however from a new shiny developer standpoint, this may be technically challenging to implement (the authors of Engineering Production-Grade Shiny Apps suggest the {nter}
package for building shiny action buttons that can be triggered by pressing enter
, however, at the time of building this workshop, the package source code hadn’t been updated since 2019).
At a minimum, consider spacing out and/or limiting the number of widgets on any given page to make navigation with a mouse as easy as possible.
Use colorblind-friendly palettes
About 1 in 12 males and 1 in 200 females have some form of colorblindness (Wikipedia). Ensuring that your color choices are distinguishable from one another and/or providing an additional non-color-based way (e.g. patterns, shapes) of distinguishing between treatments/variables/etc. can greatly help with interpretation of data visualizations.
There are lots of great colorblind-accessible palettes and resources (check out this one by Alex Phillips to start). Google Chrome also has a built-in vision deficiency emulator (see gif, below right; right click in your web browser > Inspect > Rendering (add tab by clicking on the three stacked dots if it’s not already open) > scroll down and choose emulation type from drop down where it says “Emulate vision deficiencies”).
Using Google Chrome’s vision deficiency emulator to view webpages as seen by those with vision deficiencies
Part 5: Debugging & testing
Debugging approaches
Testing apps
Debugging
Learning Objectives - Debugging
After this section, you should:
understand some of the challenges associated with debugging shiny applications
be introduced to a few approaches and tools for debugging shiny applications, including using diagnostic messages and the reactlog
package.
Packages introduced:
reactlog: a reactivity visualizer for shiny
Debugging can be challenging
Shiny apps can be particularly challenging to debug for a few reasons:
Shiny is reactive, so code execution isn’t as linear as other code that you’re likely more familiar with (e.g. analytical pipelines written in “normal” R scripts, where each line of code is executed in succession)
Shiny code runs behind a web server and the Shiny framework itself, which can obscure what’s going on
While there are a number of different tools/strategies for debugging Shiny apps, I find myself turning to one (or more) of these approaches most often:
isolating pesky errors (typos, missing commas, unmatched parentheses) in the UI by commenting out code from the outside in
reducing your app to just problematic code by commenting out as much correctly-functioning code as possible
adding diagnostic messages to my reactives
using reactlog
to visualize reactivity errors
We’ll touch on each of these, briefly, but be sure to check out the Shiny article, Debugging Shiny applications and Mastering Shiny Ch. 5.2, by Hadley Wickham for more approaches, details, and examples.
Track down pesky UI errors by commenting out code from the outside in
Many of us experienced the frustrations of finding unmatched parentheses, typos, missing commas, etc. when building out our UI layout for App #2, and tracking down the issue can require some patience and persistence.
My preferred approach for troubleshooting a situation like this is to comment out all code moving from the highest-level layout function (e.g. navbarPage()
) inwards, re-running your app each time you un-comment the next little bit of code, until you find the place where your app breaks.
For example, if I were to trouble shoot the UI for App #2, I’d comment out everything except ui <- navbarPage(title = "LTER Animal Data Explorer"
and the ending ) # END navbarPage
, then run my app to make sure an empty app with a gray navbar and title at the top appears. It does? Great. Next, un-comment the two tabPanel()
s that create the “About this App” and “Explore the Data” pages. Works? Add a little bit more back in now, and continue this process. I like to un-comment/re-run all layout function code first, then begin adding back the inputs and outputs one by one. See a short, but incomplete demo to the right:
Ultimately, taking your time, adding lots of code comments to mark the ending parentheses of each function, and leaving space between lines of code so that you can more easily see what’s going on will save you lots of headache!
What about “larger” errors?
Oftentimes, you’ll need to identify larger, more complex errors, like why an output isn’t rendering correctly or even appearing in your app at all.
I often turn to two strategies:
(1) commenting out everything except the UI elements and server logic where I believe the issue is stemming from, and
(2) adding diagnostic messages to my reactives
(3) …and on rare occasions, I’ll try using the {reactlog}
package to help visualize my app’s reactivity in an attempt to identify the problem.
To demo these approaches, we’ll use two pre-constructed apps as examples: (1) reactlog-working
(a small app that’s functioning as intended) and (2) reactlog-broken
(the same small app that’s not functioning as intended).
I’m building an app that should look like this…
In Tab 1, both the image and text should update whenever a new radio button is chosen. In Tab 2, the scatterplot should update so that only data points for penguins with body masses within our chosen range are displayed. Find the source code for this functioning app here.
…but let’s say it actually looks like this:
In Tab 1, only the image updates whenever a new radio button is chosen, and text is missing altogether. In Tab 2, the scatterplot updates as expected whenever the body mass range is changed. Find the source code for this buggy app here.
Next, add messages to your reactives
You can insert diagnostic messages within your reactives using message()
– here, I add a short message where each text and image output should be rendered. I can run my app and see messages successfully (or in the case of a broken app, unsuccessfully) print in my RStudio console as I interact with the app. You’ll notice that the each image message (e.g. “Displaying all penguins image”) prints when a new radioButton is selected, but those associated with the text outputs do not. This tells me that code is not being executed, beginning with first if
statement inside renderText
and that this is a good starting location for reviewing code (e.g. carefully crosschecking all inputId
s and outputId
s in that section).
# load packages ----
library(shiny)
library(reactlog)
# ui ----
ui <- fluidPage(
tabsetPanel(
# tab 1 ----
tabPanel(title = "Tab 1",
# radio button input ----
radioButtons(
inputId = "img", label = "Choose a penguin to display:",
choices = c("All penguins", "Sassy chinstrap", "Staring gentoo", "Adorable adelie"),
selected = "All penguins"),
# text output ----
textOutput(outputId = "penguin_text"),
# img output ----
imageOutput(outputId = "penguin_img")
), # END tab 1
# tab 2 ----
tabPanel(title = "Tab 2"#,
# # body mass slider input ----
# sliderInput(inputId = "body_mass", label = "Select a range of body masses (g)",
# min = 2700, max = 6300, value = c(3000, 4000)),
# # body mass plot output ----
# plotOutput(outputId = "bodyMass_scatterPlot")
) # END tab 2
) # END tabsetPanel
) # END fluidPage
# server ----
server <- function(input, output){
# render penguin text ----
output$penguins_text <- renderText({
if(input$img == "All penguins"){
message("Printing all penguins text")
"Meet all of our lovely penguins species!"
}
else if(input$img == "Sassy chinstrap"){
message("Printing chinstrap text")
"Chinstraps get their name from the thin black line that runs under their chins"
}
else if(input$img == "Staring gentoo"){
message("Printing gentoo text")
"Gentoos stand out because of their bright orange bills and feet"
}
else if(input$img == "Adorable adelie"){
message("Printing adelie text")
"Adelie penguins are my personal favorite <3"
}
}) # END renderText
# render penguin images ----
output$penguin_img <- renderImage({
if(input$img == "All penguins"){
message("Displaying all penguins image")
list(src = "www/all_penguins.jpeg", height = 240, width = 300)
}
else if(input$img == "Sassy chinstrap"){
message("Displaying chinstrap image")
list(src = "www/chinstrap.jpeg", height = 240, width = 300)
}
else if(input$img == "Staring gentoo"){
message("Displaying all gentoo image")
list(src = "www/gentoo.jpeg", height = 240, width = 300)
}
else if(input$img == "Adorable adelie"){
message("Displaying all adelie image")
list(src = "www/adelie.gif", height = 240, width = 300)
}
}, deleteFile = FALSE) # END renderImage
# # filter body masses ----
# body_mass_df <- reactive({
# penguins |>
# filter(body_mass_g %in% input$body_mass[1]:input$body_mass[2]) # return observations where body_mass_g is "in" the set of options provided by the user in the sliderInput
# }) # END filter body masses
# # render the scatterplot output ----
# output$bodyMass_scatterPlot <- renderPlot({
#
# ggplot(na.omit(body_mass_df()),
# aes(x = flipper_length_mm, y = bill_length_mm,
# color = species, shape = species)) +
# geom_point() +
# scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
# scale_shape_manual(values = c("Adelie" = 19, "Chinstrap" = 17, "Gentoo" = 15)) +
# labs(x = "Flipper length (mm)", y = "Bill length (mm)",
# color = "Penguin species", shape = "Penguin species") +
# theme_minimal() +
# theme(legend.position = c(0.85, 0.2),
# legend.background = element_rect(color = "white"))
#
# }) # END render scatterplot
} # END server
# combine UI & server into an app ----
shinyApp(ui = ui, server = server)
If helpful, use reactlog to visualize reactivity
reactlog
is a package/tool that provides:
“A snapshot of the history (log) of all reactive interactions within a shiny application” - Barret Schloerke in his 2019 RSTUDIO::CONF talk, Reactlog 2.0: Debugging the state of Shiny
Reactivity can be confusing. I recommend watching Barret Schloerke’s talk, linked above, and reading through the Shiny Reactlog vignette as you get started.
Using reactlog
reactlog
should already be installed as a dependency of shiny
(but be sure to import the package at the top of your script using library(reactlog)
). When enabled, it provides an interactive browser-based tool to visualize reactive dependencies and executions in your app.
To use reactlog
, follow these steps:
1. Load the reactlog library in you console (library(reactlog)
)
2. Call reactlog_enable()
in your console
3. Run your app, interact with it, then quit your app
4. Launch reactlog by running shiny::reactlogShow()
in your console (or use the keyboard shortcut cmd
/ctrl
+ F3
)
5. Use your <-
and ->
arrow keys (or and ) to move forward and backward through your app’s reactive life cycle
Read about the components of the status bar and the meaning of different reactive states in the reactlog
vignette.
Using reactlog to visualize reactivity in a correctly-functioning app
To visualize the reactive life cycle of the reactlog-working
app, I’ll first load the reactlog library, then call reactlog_enable()
in my console. Next, I’ll run my app and interact with it. By default, All penguins is selected. For demonstration purposes, I’ll click down the list (Sassy chinstrap, Staring gentoo, and finally Adorable adelie). When done, I’ll stop my app, then run shiny::reactlogShow()
in the console to open the reactlog visualizer in a browser window.
Note: I’ve left the scatter plot on Tab 2 (and it’s related UI elements) commented out (as we practiced in the earlier few slides) for this demo – the reactlog package has many features that allow you to explore reactive dependencies across your whole app, but it can get complicated quickly. For demo purposes, we’re going to work with this “smaller” version of our app, which contains just the problematic code.
Interpreting reactlog (used with our correctly-functioning app)
There’s a lot to take when looking at the reactlog viewer, so let’s take it one step at a time:
(1) The radioButton
input defaults to show the All penguins image and associated text. When we launch reactlog, our input , reactive expression , and outputs are Ready, meaning the calculated values are available (defaults in this case) and reactive elements have finished executing (i.e. the image and text is displayed). This Ready state is indicated by the green icons.
(2) I (the user) then updated the input by choosing Sassy chinstrap, invalidating (i.e. resetting) the input and thereby invalidating any dependencies – in this case both the image and text outputs. This Invalidating state is indicated by the gray icons.
(3) Once all dependencies are invalidated, the reactive elements can begin Calculating (i.e. executing) based on the new input (Sassy chinstrap). Elements are colored yellow when they are being calculated, then green when calculations are complete and the reactive element has been updated. In this example, first the image and then the text are calculated and updated.
(4) These same steps are repeated when I select the Staring gentoo, then Adorable adelie radioButtons
Using reactlog to visualize reactivity in a broken app
Let’s try out reactlog on our intentionally broken app (reactlog-broken
, where our image changes when a radioButton user input is updated, but our text doesn’t appear). As in our functioning app, the All penguins image is selected by default. For demonstration purposes, I’ll select each option moving down the list (Sassy chinstrap, Staring gentoo, Adorable adelie) before launching reactlog.
Similar to our functioning app, the default input, All penguins, and image output are Ready (green). However, in this example our text output is not a dependency of our application’s input – there’s no linkage and the text output is Invalidated (gray).
As we click down the list of radioButtons, the image output is invalidated, then updated accordingly, but the text output remains disconnected from our input.
So what’s the issue with our app?
Evidence from our diagnostic messages and reactlog
suggests that we should make sure that our UI and server are actually able to communicate about our desired text output. After careful inspection of our textOutput()
and renderText()
code, we find that a spelling error is to blame:
Our outputId
in the UI is set to penguin_text
:
But we call penguins_text
when rendering our output in the server:
By updating our outputId
to match in both the UI and the server, we fix our app.
Testing
Learning Objectives for Testing
After this section, you should:
understand some of the reasons why apps break and the benefit of having automated tests
have a basic understanding of how to use the shinytest2
package to create regression tests
know how to rerun tests
Packages introduced:
shinytest2: provides tools for creating and running automated tests on Shiny applications
It’s almost inevitable that apps will break – there are lots of reasons why this happens, but to name a few:
an upgraded R package(s) has a different behavior (this includes shiny
) – this is especially relevant for those apps hosted on servers, where server software (including packages) may be updated by system administrators
you make changes to your app
an external data source stops working or returns data in a different format than expected by your app
It can save a lot of time and headache (for you and your collaborators) to have an automated system that checks if your app is working as expected.
Enter the {shinytest2}
package
The {shinytest2}
package is a useful tool for conducting regression testing on shiny apps – or in other words, testing existing app behavior for consistency over time.
From the shinytest2
documentation:
“shinytest2
uses testthat
’s snapshot-based testing strategy. The first time it runs a set of tests for an application, it performs some scripted interactions with the app and takes one or more snapshots of the application’s state. These snapshots are saved to disk so that future runs of the tests can compare their results to them.”
Rather than having to write tests by hand, you can interact with your app via the “app recorder” and shinytest2
will record the test code automatically for you. Simply rerun tests to check for consistency.
shinytest2
resources & demos
The following demo comes straight from the {shinytest2}
vignette, though a similar app and testing workflow is demoed by Barret Schloerke in his recorded talk, Getting Started with {shinytest2} Part I || Example + basics.
Additional resources:
Barret Schloerke’s rstudio::conf(2022) talk, {shinytest2}: Unit testing for Shiny applications (recording)
Barret Schloerke’s 2022 Appsilon Shiny Confernce talk, {shinytest2} Testing Shiny with {testthat} (recording & GitHub repo)
Shiny testing overview, by Winston Chang – this article discussed the shinytest2
predecessor, shinytest
(which is now entering maintenance mode), but provides some helpful context and is worth a read
Let’s test the following app
This small app accepts a text input for users to type their name. When the “Greet” button is pressed, the app returns a short greeting message that says, “Hello name!
To get started, create a subdirectory called ~/testing_app
, add a file named app.R
, and drop this code in your file. Take a moment to try out the app.
~/testing_app/app.R
Testing using shinytest2
Recording tests requires the following steps:
(1) Run record_test(<app-directory>)
to launch the app recorder in browser window
(2) Interact with your application and tell the recorder to make an expectation (e.g. an expected value when inputX is updated) on the state at various points
(3) Quit the recorder to save and execute your tests
To test our app specifically, we’ll do the following:
(1) run shinytest2::record_test("testing_app")
in the console to launch the recorder in a browser window
(2) interact with your app by first typing a name (e.g. Sam), then pressing the “Greet” button to display the output text
(3) click the “Expect Shiny values” button in the recorder app sidebar to set an expectation (this will record inputs, outputs, and exported values)
(4) give your test a name in the recorder app sidebar, then click “Save test and exit” - this will save the recorded test and setup the testing infrastructure, if it doesn’t exit already
Creating our first test
Following the steps on the previous slide, creating your test should look similar to this:
Note: Your test is automatically run as soon as you save and exit the recorder. See the results of your test in your console (it should pass!).
Test files are generated automatically
After recording your first test, a /tests
folder is generated, containing a number of different files and subdirectories. Some important files to note:
(located at <app-directory>/tests/testthat/setup-shinytest2.R
); For more complex apps, you’ll often have support files (e.g. those contained in <app-directory>/R
and/or global.R
) – content from those files will be stored here so that it is made accessible to your test(s). Since we don’t have any support files for our rather small/somewhat simple app, you should only see the following:
(located at <app-directory>/tests/testthat/test-shinytest2.R
); This test script contains your recorded test, and should automatically open when you finish recording and save your test. You can manually modify this test (e.g. add additional interactions and expectations), if you wish. Yours should look similar to this:
(located at <app-directory>/tests/testthat/_snaps/shinytest2/*_.png
); This is a screenshot of your app from when app$expect_values()
was called – this file should be tracked using git so that you know how your app visually changes over time. My .png
file looks like this:
(located at <app-directory>/tests/testthat/_snaps/shinytest2/*.json
); This is a JSON representation of the state of the app when app$expect_values()
was called – you’ll see the state of all input, output, and export values at the time of the snapshot (we don’t have any exports in our example app, but we do have a name input and a greeting output). This file should be tracked with git so that you have a record of your expected results. Your .json
file should look something like this:
Tips for testing
Record subsequent tests following the same workflow, giving each a unique name. Run test_app("path/to/app")
to run all test scripts in your app’s tests/testhat
directory.
Use record_test()
fairly often – Barret Schloerke argues that you should make a test recording for each feature of your app (many little recordings are encouraged!)
Limit testing to objects under your control. For example, let’s say you have a reactive data frame that you then send to a DT::datatable
– if package maintainers update the DT
package, your output might change which could lead to false positive failed tests. Instead, test just your data frame that gets sent to DT
.
This is only a brief intro to shinytest2
! Dig into the documentation to learn more.
Part 6: Streamlining code
Writing functions
Shiny modules
Writing functions
Learning Objectives - Functions
By the end of this section, you should:
understand the benefits of turning UI elements and server logic into functions
know where to write/save your functions
successfully turn a repeated input into a function
successfully turn a piece of server logic into a function
Why write functions
Functions are useful for a wide variety of reasons. Most notably:
reducing redundancy
reducing complexity
increasing code comprehension
increasing testability
Importantly, functions can live outside of your app file(s) (i.e. app.R
or ui.R
, server.R
and global.R
), helping you to break up/streamline your code. Hadley Wickham recommends creating a folder called /R
inside your app’s directory (e.g. ~/app-directory/R/...
) and:
(a) storing larger functions in their own files (e.g. ~/app-directory/R/{function-name}.R
) and/or
(b) creating a utils.R
file (e.g ~/app-directory/R/utils.R
) to store smaller, simpler functions all in one script.
You can source your function files into global.R
so that your functions are made available for use throughout your app. NOTE: As of Shiny version 1.5.0, any scripts stored in ~/app-directory/R/
will be automatically sourced when your application is loaded (meaning you don’t need to source()
them into global.R
, if you’re running at least Shiny v1.5.0).
Create a small app for function practice
Create a new subdirectory called ~/functions-app
and add your ui.R
, server.R
, and global.R
files with the following code. Run your app to see how it functions.
~/functions-app/ui.R
ui <- fluidPage(
tags$h1("Demoing Functions"),
# tabsetPanel ----
tabsetPanel(
# scatterplot tab ----
tabPanel("Scatterplot",
# species (scatterplot) pickerInput ----
pickerInput(inputId = "penguin_species_scatterplot_input", label = "Select a species:",
choices = c("Adelie", "Chinstrap", "Gentoo"),
options = pickerOptions(actionsBox = TRUE),
selected = c("Adelie", "Chinstrap", "Gentoo"),
multiple = T),
# scatterplot output ----
plotOutput(outputId = "penguin_scatterplot")
), # END scatterplot tab
# histogram tab ----
tabPanel("Histogram",
# species (histogram) pickerInput ----
pickerInput(inputId = "penguin_species_histogram_input", label = "Select a species:",
choices = c("Adelie", "Chinstrap", "Gentoo"),
options = pickerOptions(actionsBox = TRUE),
selected = c("Adelie", "Chinstrap", "Gentoo"),
multiple = TRUE),
# scatterplot output ----
plotOutput(outputId = "penguin_histogram")
) # END histogram tab
) # END tabsetPanel
) # END fluidPage
~/functions-app/server.R
server <- function(input, output) {
# filter penguin species (scatterplot) ----
filtered_spp_scatterplot <- reactive ({
penguins |>
filter(species %in% input$penguin_species_scatterplot_input)
})
# render the scatterplot output ----
output$penguin_scatterplot <- renderPlot({
ggplot(na.omit(filtered_spp_scatterplot()),
aes(x = bill_length_mm, y = bill_depth_mm,
color = species, shape = species)) +
geom_point() +
geom_smooth(method = "lm", se = FALSE, aes(color = species)) +
scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
scale_shape_manual(values = c("Adelie" = 19, "Chinstrap" = 17, "Gentoo" = 15)) +
labs(x = "Flipper length (mm)", y = "Bill length (mm)",
color = "Penguin species", shape = "Penguin species")
})
# filter penguin species (histogram) ----
filtered_spp_histogram <- reactive ({
penguins |>
filter(species %in% input$penguin_species_histogram_input)
})
# render the histogram output ----
output$penguin_histogram <- renderPlot({
ggplot(na.omit(filtered_spp_histogram()),
aes(x = flipper_length_mm, fill = species)) +
geom_histogram() +
scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
labs(x = "Flipper length (mm)", y = "Frequency",
fill = "Penguin species")
})
} # END server
Identify code duplication in ui.R
Let’s first focus on the UI – where do we have nearly identically duplicated code?
~/functions-app/ui.R
ui <- fluidPage(
tags$h1("Demoing Functions"),
# tabsetPanel ----
tabsetPanel(
# scatterplot tab ----
tabPanel("Scatterplot",
# species (scatterplot) pickerInput ----
pickerInput(inputId = "penguin_species_scatterplot_input", label = "Select a species:",
choices = c("Adelie", "Chinstrap", "Gentoo"),
options = pickerOptions(actionsBox = TRUE),
selected = c("Adelie", "Chinstrap", "Gentoo"),
multiple = TRUE),
# scatterplot output ----
plotOutput(outputId = "penguin_scatterplot")
), # END scatterplot tab
# histogram tab ----
tabPanel("Histogram",
# species (histogram) pickerInput ----
pickerInput(inputId = "penguin_species_histogram_input", label = "Select a species:",
choices = c("Adelie", "Chinstrap", "Gentoo"),
options = pickerOptions(actionsBox = TRUE),
selected = c("Adelie", "Chinstrap", "Gentoo"),
multiple = TRUE),
# scatterplot output ----
plotOutput(outputId = "penguin_histogram")
) # END histogram tab
) # END tabsetPanel
) # END fluidPage
Write a function for adding a pickerInput
to select for penguin species
This app includes two pickerInputs
, both of which allow users to select which penguin species to display data for. The only difference between both pickerInput
s is the inputId
. Let’s write a function for our penguin species pickerInput that we can use in place of these two, rather long, chunks of code.
First, create an /R
folder inside your functions-app
directory. Then, add a new script to this folder. I’m calling mine penguinSpp_pickerInput.R
.
Since the only difference between our original two pickerInput
s are their inputId
s, we can write a function that takes inputId
as an argument (Recall that inputId
s must be unique within an app, so it makes sense that both of our pickerInput
s have different inputId
s).
Once written, source()
your function script into global.R
(if necessary) to make your function available for use in your app.
~/functions-app/R/penguinSpp_pickerInput.R
Apply your function in ui.R
Finally, replace your original UI code for building both pickerInput
s with our penguinSpp_pickerInput()
function, save, and run your app. It should look exactly the same as before!
~/functions-app/ui.R
ui <- fluidPage(
tags$h1("Demoing Functions"),
# tabsetPanel ----
tabsetPanel(
# scatterplot tab ----
tabPanel("Scatterplot",
# species (scatterplot) pickerInput ----
penguinSpp_pickerInput(inputId = "penguin_species_scatterplot_input"),
# scatterplot output ----
plotOutput(outputId = "penguin_scatterplot")
), # END scatterplot tab
# histogram tab ----
tabPanel("Histogram",
# species (histogram) pickerInput ----
penguinSpp_pickerInput(inputId = "penguin_species_histogram_input"),
# scatterplot output ----
plotOutput(outputId = "penguin_histogram")
) # END histogram tab
) # END tabsetPanel
) # END fluidPage
We reduced code redundancy and increased readability!
By turning our pickerInput
code into a function, we not only reduced ten lines of UI code into two, but we also made our UI code a bit easier to read – our function, penguinSpp_pickerInput()
tells a reader/collaborator/future you exactly what that line of code is meant to do, which is create a pickerInput
that allows users to select penguin species. Even without code comments or additional context, one may deduce what that line of code does.
Turn reactives & rendered outputs into functions
Next, let’s see where we can streamline our server code using functions. We have two discrete sections of code – (1) a reactive data frame and scatterplot output and (2) a reactive data frame and histogram output.
~/functions-app/server.R
server <- function(input, output) {
# filter penguin species (scatterplot) ----
filtered_spp_scatterplot <- reactive ({
penguins |>
filter(species %in% input$penguin_species_scatterplot)
})
# render the scatterplot output ----
output$penguin_scatterplot <- renderPlot({
ggplot(na.omit(filtered_spp_scatterplot()),
aes(x = bill_length_mm, y = bill_depth_mm,
color = species, shape = species)) +
geom_point() +
geom_smooth(method = "lm", se = FALSE, aes(color = species)) +
scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
scale_shape_manual(values = c("Adelie" = 19, "Chinstrap" = 17, "Gentoo" = 15)) +
labs(x = "Flipper length (mm)", y = "Bill length (mm)",
color = "Penguin species", shape = "Penguin species")
})
# filter penguin species (histogram) ----
filtered_spp_histogram <- reactive ({
penguins |>
filter(species %in% input$penguin_species_histogram)
})
# render the histogram output ----
output$penguin_histogram <- renderPlot({
ggplot(na.omit(filtered_spp_histogram()),
aes(x = flipper_length_mm, fill = species)) +
geom_histogram() +
scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
labs(x = "Flipper length (mm)", y = "Frequency",
fill = "Penguin species")
})
} # END server
Create a function to build our scatterplot
First, create a new file in ~/function-app/R
and name it build-penguin-scatterplot.R
(or a name that is succinct/clear – I’m going to name my function similarly).
The goal of my function is to filter the penguins
data based on the user input and render our ggplot scatterplot. To start, I’m going to cut/paste both the code to generate the reactive filtered_spp_scatterplot
data frame and the renderPlot()
code from server.R
into our build_penguin_scatterplot()
function.
Important: In isolation, our function does not know about the user input (input
is not in our global environment, it’s only known within the server()
function). Therefore, we must pass input
as an argument to our function.
Note that in R, functions return the last executed line – when we run build_penguin_scatterplot()
in our server, it will return the object created by renderPlot()
.
~/functions-app/R/build-penguin-scatterplot.R
build_penguin_scatterplot <- function(input) {
# filter penguin species (scatterplot) ----
filtered_spp_scatterplot <- reactive ({
penguins |>
filter(species %in% input$penguin_species_scatterplot_input)
})
# render the scatterplot output ----
output$penguin_scatterplot <- renderPlot({
ggplot(na.omit(filtered_spp_scatterplot()),
aes(x = bill_length_mm, y = bill_depth_mm,
color = species, shape = species)) +
geom_point() +
geom_smooth(method = "lm", se = FALSE, aes(color = species)) +
scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
scale_shape_manual(values = c("Adelie" = 19, "Chinstrap" = 17, "Gentoo" = 15)) +
labs(x = "Flipper length (mm)", y = "Bill length (mm)",
color = "Penguin species", shape = "Penguin species")
})
}
Now use your function inside the server
Remember, the output of build_penguin_scatterplot()
is renderPlot()
, which is used to build our reactive scatterplot. Following our rules for creating reactivity, we need to save our function’s output to output$penguin_scatterplot
. In doing so, we reduced 23 lines of code to 1 inside our server function.
~/functions-app/server.R
server <- function(input, output) {
# filter data & create penguin scatterplot ----
output$penguin_scatterplot <- build_penguin_scatterplot(input)
# filter penguin species (histogram) ----
filtered_spp_histogram <- reactive ({
penguins |>
filter(species %in% input$penguin_species_histogram)
})
# render the histogram output ----
output$penguin_histogram <- renderPlot({
ggplot(na.omit(filtered_spp_histogram()),
aes(x = flipper_length_mm, fill = species)) +
geom_histogram() +
scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
labs(x = "Flipper length (mm)", y = "Frequency",
fill = "Penguin species")
})
} # END server
Build a function to create our histogram
We can repeat a similar process to create a function for building our histogram:
~/functions-app/R/build-penguin-histogram.R
build_penguin_histogram <- function(input) {
# filter penguin spp ----
filtered_spp_histogram <- reactive ({
penguins |>
filter(species %in% input$penguin_species_histogram_input)
})
# render histogram ----
renderPlot({
ggplot(na.omit(filtered_spp_histogram()),
aes(x = flipper_length_mm, fill = species)) +
geom_histogram() +
scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
labs(x = "Flipper length (mm)", y = "Frequency",
fill = "Penguin species")
})
}
Writing modules
Learning Objectives - Modules
By the end of this section, you should have an intro-level understanding of:
what is a shiny module and when it might make sense to build one
the structure of a shiny module
how to use a module
Packages introduced:
gapminder: data
The utility of modules is best demonstrated by taking a look at an example app
This app, developed by Garrett Grolemund & Joe Cheng’s as part of their Modules lesson, taught at the 2016 Shiny Developer Conference, is a prime candidate for modularization.
It uses the gapminder
data set to display life expectancy by GDP per capita from 1952 to 2007 for Africa, the Americas, Asia, Europe, Oceania, and all regions collectively. The only difference between each tab is the subset of data displayed.
The code for this app isn’t particularly complex, but it’s repetitive and long
~/modularized-app/app.R
# app.R
#..............................setup.............................
library(shiny)
library(gapminder)
library(dplyr)
# Note: This code creates data sets to use in each tab.
# It removes Kuwait since Kuwait distorts the gdp scale
all_data <- filter(gapminder, country != "Kuwait")
africa_data <- filter(gapminder, continent == "Africa")
americas_data <- filter(gapminder, continent == "Americas")
asia_data <- filter(gapminder, continent == "Asia", country != "Kuwait")
europe_data <- filter(gapminder, continent == "Europe")
oceania_data <- filter(gapminder, continent == "Oceania")
#...............................ui...............................
ui <- fluidPage(
# app title ----
titlePanel("Gapminder"),
# continent tabsetPanel ----
tabsetPanel(id = "continent",
# All tab ----
tabPanel(title = "All",
plotOutput(outputId = "all_plot"),
sliderInput(inputId = "all_year", label = "Select Year",
value = 1952, min = 1952, max = 2007, step = 5,
animate = animationOptions(interval = 500))
), # END All tab
# Africa tab ----
tabPanel(title = "Africa",
plotOutput(outputId = "africa_plot"),
sliderInput(inputId = "africa_year", label = "Select Year",
value = 1952, min = 1952, max = 2007, step = 5,
animate = animationOptions(interval = 500))
), # END Africa tab
# Americas tab ----
tabPanel(title = "Americas",
plotOutput(outputId = "americas_plot"),
sliderInput(inputId = "americas_year", label = "Select Year",
value = 1952, min = 1952, max = 2007, step = 5,
animate = animationOptions(interval = 500))
), # END Americas tab
# Asia tab ----
tabPanel(title = "Asia",
plotOutput(outputId = "asia_plot"),
sliderInput(inputId = "asia_year", label = "Select Year",
value = 1952, min = 1952, max = 2007, step = 5,
animate = animationOptions(interval = 500))
), # END Asia tab
# Europe tab ----
tabPanel(title = "Europe",
plotOutput(outputId = "europe_plot"),
sliderInput(inputId = "europe_year", label = "Select Year",
value = 1952, min = 1952,
max = 2007, step = 5, animate = animationOptions(interval = 500))
), # END Europe
# Oceania tab ----
tabPanel(title = "Oceania",
plotOutput(outputId = "oceania_plot"),
sliderInput(inputId = "oceania_year", label = "Select Year",
value = 1952, min = 1952, max = 2007, step = 5,
animate = animationOptions(interval = 500))
) # END Oceania tab
) # END continent tabsetPanel
) # END fluidPage
#.............................server.............................
server <- function(input, output) {
# ---- collect one year of data ----
ydata_all <- reactive({
filter(all_data, year == input$all_year)
})
ydata_africa <- reactive({
filter(africa_data, year == input$africa_year)
})
ydata_americas <- reactive({
filter(americas_data, year == input$americas_year)
})
ydata_asia <- reactive({
filter(asia_data, year == input$asia_year)
})
ydata_europe <- reactive({
filter(europe_data, year == input$europe_year)
})
ydata_oceania <- reactive({
filter(oceania_data, year == input$oceania_year)
})
# ---- compute plot ranges ----
xrange_all <- range(all_data$gdpPercap)
yrange_all <- range(all_data$lifeExp)
xrange_africa <- range(africa_data$gdpPercap)
yrange_africa <- range(africa_data$lifeExp)
xrange_americas <- range(americas_data$gdpPercap)
yrange_americas <- range(americas_data$lifeExp)
xrange_asia <- range(asia_data$gdpPercap)
yrange_asia <- range(asia_data$lifeExp)
xrange_europe <- range(europe_data$gdpPercap)
yrange_europe <- range(europe_data$lifeExp)
xrange_oceania <- range(oceania_data$gdpPercap)
yrange_oceania <- range(oceania_data$lifeExp)
# ---- render plots ----
# render all countries ----
output$all_plot <- renderPlot({
# draw background plot with legend
plot(all_data$gdpPercap, all_data$lifeExp, type = "n",
xlab = "GDP per capita", ylab = "Life Expectancy",
panel.first = {
grid()
text(mean(xrange_all), mean(yrange_all), input$all_year,
col = "grey90", cex = 5)
}
)
# build legend
legend("bottomright", legend = levels(all_data$continent),
cex = 1.3, inset = 0.01, text.width = diff(xrange_all)/5,
fill = c("#E41A1C99", "#377EB899", "#4DAF4A99", "#984EA399", "#FF7F0099")
)
# Determine bubble colors
cols <- c("Africa" = "#E41A1C99",
"Americas" = "#377EB899",
"Asia" = "#4DAF4A99",
"Europe" = "#984EA399",
"Oceania" = "#FF7F0099")[ydata_all()$continent]
# add bubbles
symbols(ydata_all()$gdpPercap, ydata_all()$lifeExp,
circles = sqrt(ydata_all()$pop), bg = cols, inches = 0.5, fg = "white",
add = TRUE)
})
# render africa ----
output$africa_plot <- renderPlot({
# draw background plot with legend
plot(africa_data$gdpPercap, africa_data$lifeExp, type = "n",
xlab = "GDP per capita", ylab = "Life Expectancy",
panel.first = {
grid()
text(mean(xrange_africa), mean(yrange_africa), input$africa_year,
col = "grey90", cex = 5)
}
)
# build legend
legend("bottomright", legend = levels(africa_data$continent),
cex = 1.3, inset = 0.01, text.width = diff(xrange_africa)/5,
fill = c("#E41A1C99", "#377EB899", "#4DAF4A99", "#984EA399", "#FF7F0099")
)
# Determine bubble colors
cols <- c("Africa" = "#E41A1C99",
"Americas" = "#377EB899",
"Asia" = "#4DAF4A99",
"Europe" = "#984EA399",
"Oceania" = "#FF7F0099")[ydata_africa()$continent]
# add bubbles
symbols(ydata_africa()$gdpPercap, ydata_africa()$lifeExp,
circles = sqrt(ydata_africa()$pop), bg = cols, inches = 0.5, fg = "white",
add = TRUE)
})
# render americas ----
output$americas_plot <- renderPlot({
# draw background plot with legend
plot(americas_data$gdpPercap, americas_data$lifeExp, type = "n",
xlab = "GDP per capita", ylab = "Life Expectancy",
panel.first = {
grid()
text(mean(xrange_americas), mean(yrange_americas), input$americas_year,
col = "grey90", cex = 5)
}
)
# build legend
legend("bottomright", legend = levels(americas_data$continent),
cex = 1.3, inset = 0.01, text.width = diff(xrange_americas)/5,
fill = c("#E41A1C99", "#377EB899", "#4DAF4A99", "#984EA399", "#FF7F0099")
)
# Determine bubble colors
cols <- c("Africa" = "#E41A1C99",
"Americas" = "#377EB899",
"Asia" = "#4DAF4A99",
"Europe" = "#984EA399",
"Oceania" = "#FF7F0099")[ydata_americas()$continent]
# add bubbles
symbols(ydata_americas()$gdpPercap, ydata_americas()$lifeExp,
circles = sqrt(ydata_americas()$pop), bg = cols, inches = 0.5, fg = "white",
add = TRUE)
})
# render asia ----
output$asia_plot <- renderPlot({
# draw background plot with legend
plot(asia_data$gdpPercap, asia_data$lifeExp, type = "n",
xlab = "GDP per capita", ylab = "Life Expectancy",
panel.first = {
grid()
text(mean(xrange_asia), mean(yrange_asia), input$asia_year,
col = "grey90", cex = 5)
}
)
# build legend
legend("bottomright", legend = levels(asia_data$continent),
cex = 1.3, inset = 0.01, text.width = diff(xrange_asia)/5,
fill = c("#E41A1C99", "#377EB899", "#4DAF4A99", "#984EA399", "#FF7F0099")
)
# Determine bubble colors
cols <- c("Africa" = "#E41A1C99",
"Americas" = "#377EB899",
"Asia" = "#4DAF4A99",
"Europe" = "#984EA399",
"Oceania" = "#FF7F0099")[ydata_asia()$continent]
# add bubbles
symbols(ydata_asia()$gdpPercap, ydata_asia()$lifeExp,
circles = sqrt(ydata_asia()$pop), bg = cols, inches = 0.5, fg = "white",
add = TRUE)
})
# render europe ----
output$europe_plot <- renderPlot({
stop("Error: Don't look at Europe")
# draw background plot with legend
plot(europe_data$gdpPercap, europe_data$lifeExp, type = "n",
xlab = "GDP per capita", ylab = "Life Expectancy",
panel.first = {
grid()
text(mean(xrange_europe), mean(yrange_europe), input$europe_year,
col = "grey90", cex = 5)
}
)
# build legend
legend("bottomright", legend = levels(europe_data$continent),
cex = 1.3, inset = 0.01, text.width = diff(xrange_europe)/5,
fill = c("#E41A1C99", "#377EB899", "#4DAF4A99", "#984EA399", "#FF7F0099")
)
# Determine bubble colors
cols <- c("Africa" = "#E41A1C99",
"Americas" = "#377EB899",
"Asia" = "#4DAF4A99",
"Europe" = "#984EA399",
"Oceania" = "#FF7F0099")[ydata_europe()$continent]
# add bubbles
symbols(ydata_europe()$gdpPercap, ydata_europe()$lifeExp,
circles = sqrt(ydata_europe()$pop), bg = cols, inches = 0.5, fg = "white",
add = TRUE)
})
# render oceania ----
output$oceania_plot <- renderPlot({
# draw background plot with legend
plot(oceania_data$gdpPercap, oceania_data$lifeExp, type = "n",
xlab = "GDP per capita", ylab = "Life Expectancy",
panel.first = {
grid()
text(mean(xrange_oceania), mean(yrange_oceania), input$oceania_year,
col = "grey90", cex = 5)
}
)
# build legend
legend("bottomright", legend = levels(oceania_data$continent),
cex = 1.3, inset = 0.01, text.width = diff(xrange_oceania)/5,
fill = c("#E41A1C99", "#377EB899", "#4DAF4A99", "#984EA399", "#FF7F0099")
)
# Determine bubble colors
cols <- c("Africa" = "#E41A1C99",
"Americas" = "#377EB899",
"Asia" = "#4DAF4A99",
"Europe" = "#984EA399",
"Oceania" = "#FF7F0099")[ydata_oceania()$continent]
# add bubbles ----
symbols(ydata_oceania()$gdpPercap, ydata_oceania()$lifeExp,
circles = sqrt(ydata_oceania()$pop), bg = cols, inches = 0.5, fg = "white",
add = TRUE)
})
} # END server
# Run the application
shinyApp(ui = ui, server = server)
Repeated code sections
Taking a closer look at the gapminder app code, we’ll see that the following sections of code are repeated for each region (6 times total; only code sections for “all” regions shown below):
tabPanel
(UI)
reactive data frame (server)
reactive data frame (server)
calculating date ranges (server)
Repeated code sections (cont.)
renderPlot({})
(server)
# "All" plot (repeated 5 more times for each subregion)
output$all_plot <- renderPlot({
# draw background plot with legend
plot(all_data$gdpPercap, all_data$lifeExp, type = "n",
xlab = "GDP per capita", ylab = "Life Expectancy",
panel.first = {
grid()
text(mean(xrange_all), mean(yrange_all), input$all_year,
col = "grey90", cex = 5)
}
)
# build legend
legend("bottomright", legend = levels(all_data$continent),
cex = 1.3, inset = 0.01, text.width = diff(xrange_all)/5,
fill = c("#E41A1C99", "#377EB899", "#4DAF4A99", "#984EA399", "#FF7F0099")
)
# Determine bubble colors
cols <- c("Africa" = "#E41A1C99",
"Americas" = "#377EB899",
"Asia" = "#4DAF4A99",
"Europe" = "#984EA399",
"Oceania" = "#FF7F0099")[ydata_all()$continent]
# add bubbles
symbols(ydata_all()$gdpPercap, ydata_all()$lifeExp,
circles = sqrt(ydata_all()$pop), bg = cols, inches = 0.5, fg = "white",
add = TRUE)
})
Enter Shiny modules
A shiny module is a piece of a shiny app – it can’t be run directly, but instead is included as part of a larger app. While functions work well for code that that is either completely on the client (UI) side or completely on the server side, modules can be written for code that spans both.
Modules can represent inputs, outputs, or both (we’ll be building a module that represents both). Motivation for building modules can range from enabling reuse of code (once created, modules can be reused within the same app or even across different apps), to breaking up a large, complex app into smaller, separate components.
Modules help to solve a namespacing problem – recall that all Id
s (e.g. inputId
s) must be unique across your app. Namespacing is a system for organizing objects with identical names (similar to namespacing functions from particular packages using the syntax package::function()
e.g. plyr::arrange()
vs dplyr::arrange()
).
What do modules look like?
Modules are a coding pattern, organized into two functions: one that creates the UI elements and one that loads the server logic. They can look a bit different, depending on your module, but they generally follow this pattern:
myModule.R
#..........................ui function...........................
myModuleUI <- function(id) {
ns <- NS(id)
tagList(
# inputs with ids wrapped in ns() (e.g. `sliderInput(id = ns("slider"))`)
# outputs with ids wrapped in ns() (e.g. `plotOutput(id = "ns(plot"))`)
)
}
#........................server function.........................
myModuleServer <- function(id, ...) { # where `...` includes any number of additional parameters
moduleServer(id, function(input, output, session) {
# server logic
})
}
Where should I define/save my module?
Part of the appeal of creating modules is breaking your long app.R
(or ui.R
& server.R
) scripts into smaller pieces. Creating a separate R script to house a given module (both the UI and Server function components) is typically the best course of action (a good naming convention is giving it a descriptive name with the suffix “Module” e.g. gapModule.R
). There are a variety of places you can write and/or save your modularized code to, but I recommend one of the following two options (at least while we’re just getting started on our shiny modules journey):
(1) save your modularized code script inside your app’s directory (e.g. ~/app-directory/myModule.R
)
If you choose this option, call source("myModule.R")
from global.R
(if using ui.R/server.R) or app.R
.
(2) save your modularized code script inside the /R
subdirectory of your application (e.g. ~/app-directory/R/myModule.R
)
If you choose this option, your module will automatically be sourced (as of Shiny 1.5.0) when the application is loaded.
Breaking down the UI function:
The UI part of a module needs to do two things: (1) return a shiny element (e.g. an input & output), and (2) assign module elements to a unique namespace using NS()
. NS()
provides an easy way to help with namespacing within your module, ensuring that each time your module is called, a unique id
is assigned.
The UI function for our gapminder module will look like this (NOTE: code comments below denote the general order of operations I followed when writing this UI function):
~/modularized-app/R/gapModule.R
# step 1: a good function naming convention is a descriptive base name, suffixed by `UI`
gapModuleUI <- function(id) { # step 2: the first argument to a UI function should always be `id` -- this is the namespace for the module
ns <- NS(id) # step 3: the function body starts with the statement `ns <- NS(id)`
tagList( # step 4: surrounding all inputs & outputs in `tagList()`, which ensures that they are ALL returned (not necessary if you're just returning a single element)
plotOutput(outputId = ns("plot")), # step 5.1: wrap outputId in `ns()`
sliderInput(inputId = ns("year"), label = "Select Year", # step 5.2: wrap inputId in `ns()`
value = 1952, min = 1952, max = 2007, step = 5,
animate = animationOptions(interval = 500))
) # END taglist
} # END gapModuleUI function
Wrapping our input and output Id
s in ns()
will create unique Ids each time our module is called, preventing things from overwriting one one another. For example, if we call gapModuleUI(id = "myFirstModuleCall")
, our outputId
will be set to myFirstModuleCall-plot
and our inputId
will be set to myFirstModuleCall-year
. Calling our module a second time (e.g. gapModuleUI(id = "mySecondModuleCall")
) will generate two new unique Ids (e.g. mySecondModuleCall-plot
& mySecondModuleCall-year
).
Breaking down the Server function:
The server part of a module looks very similar to a normal (i.e. non-modular) Shiny app server function. Begin by defining your module server function name and provide it with the first required parameter, id
, along with any other necessary parameters (we also need to pass our particular function a data
parameter to differentiate between data subsets (e.g. All vs. Africa vs. Asia etc.)).
Next, call moduleServer()
inside your server function and pass it the id
variable, along with the module function. The module function must have three parameters: input
, output
, and session
. You do not have to use ns()
to refer to inputs and outputs here. Copy server code from our original app, plop it inside the module function, and sub in our data
parameter where ever a data frame subset is called.
~/modularized-app/R/gapModule.R
# step 1: a good function naming convention is a descriptive base name, suffixed by `Server`
gapModuleServer <- function(id, data) { # step 2: the first argument to a server function should always be `id`, followed by any other necessary arguments; here we include a 'data' parameter, since we need to be able to tell our server function which data subset to plot in each tab
moduleServer(id, function(input, output, session) { # step 3: call `moduleServer()`, and pass it two things -- (a) a string id that corresponds with the id used to call the module's UI function, and (b) a module server function (this MUST use the three arguments: input, output, and session)
# step 4: copy server logic into the module function (only need to do this ONCE, not 6x); update inputIds (now 'year', rather than 'all_year' etc.) & sub in 'data' parameter for hard-coded data subsets
# creactive df to collect one year of data ----
ydata <- reactive({
filter(data, year == input$year)
}) # END reactive df
# set slider range ----
xrange <- range(data$gdpPercap)
yrange <- range(data$lifeExp)
# render plot (NOTE: plotting with base R, so this looks a bit different than you may be used to) -----
output$plot <- renderPlot({
# draw background plot with legend
plot(data$gdpPercap, data$lifeExp, type = "n",
xlab = "GDP per capita", ylab = "Life Expectancy",
panel.first = {
grid()
text(mean(xrange), mean(yrange), input$year,
col = "grey90", cex = 5)
})
# build legend
legend("bottomright", legend = levels(data$continent),
cex = 1.3, inset = 0.01, text.width = diff(xrange)/5,
fill = c("#E41A1C99", "#377EB899", "#4DAF4A99",
"#984EA399", "#FF7F0099"))
# determine bubble colors
cols <- c("Africa" = "#E41A1C99",
"Americas" = "#377EB899",
"Asia" = "#4DAF4A99",
"Europe" = "#984EA399",
"Oceania" = "#FF7F0099")[ydata()$continent]
# add bubbles
symbols(ydata()$gdpPercap, ydata()$lifeExp, circles = sqrt(ydata()$pop),
bg = cols, inches = 0.5, fg = "white", add = TRUE)
}) # END renderPlot
}) # END moduleServer
} # END server function
Now let’s use our module:
First, since we’ve saved our gapModule.R
file to our app’s directory, we’ll need to source it at the top of our app.R
file:
~/modularized-app/app.R
#..............................setup.............................
library(shiny)
library(gapminder)
library(dplyr)
source("gapModule.R")
# Note: This code creates data sets to use in each tab.
# It removes Kuwait since Kuwait distorts the gdp scale
all_data <- filter(gapminder, country != "Kuwait")
africa_data <- filter(gapminder, continent == "Africa")
americas_data <- filter(gapminder, continent == "Americas")
asia_data <- filter(gapminder, continent == "Asia", country != "Kuwait")
europe_data <- filter(gapminder, continent == "Europe")
oceania_data <- filter(gapminder, continent == "Oceania")
Now let’s use our module:
Next, let’s use our module’s UI function. We’ll need to define/name each of our tabPanel
s (one for each of our six regions), but rather than building a plotOutput
and sliderInput
inside each tabPanel
(each with unique Id
s), we can instead call our gapModuleUI()
function, and ensure that each time we call it to supply a unique character string for our id
parameter.
~/modularized-app/app.R
#..............................setup.............................
library(shiny)
library(gapminder)
library(dplyr)
source("gapModule.R")
# Note: This code creates data sets to use in each tab.
# It removes Kuwait since Kuwait distorts the gdp scale
all_data <- filter(gapminder, country != "Kuwait")
africa_data <- filter(gapminder, continent == "Africa")
americas_data <- filter(gapminder, continent == "Americas")
asia_data <- filter(gapminder, continent == "Asia", country != "Kuwait")
europe_data <- filter(gapminder, continent == "Europe")
oceania_data <- filter(gapminder, continent == "Oceania")
#...............................ui...............................
ui <- fluidPage(
# app title ----
titlePanel("Gapminder"),
# continent tabsetPanel ----
tabsetPanel(id = "continent",
tabPanel(title = "All", gapModuleUI(id = "all")),
tabPanel(title = "Africa", gapModuleUI(id = "africa")),
tabPanel(title = "Americas", gapModuleUI(id = "americas")),
tabPanel(title = "Asia", gapModuleUI(id = "asia")),
tabPanel(title = "Europe", gapModuleUI(id = "europe")),
tabPanel(title = "Oceania", gapModuleUI(id = "oceania"))
) # END continent tabsetPanel
) # END fluidPage
Now let’s use our module:
Finally, we can re-write our server. Rather than writing out the lengthy code required to make each plot six times over, we can instead call our gapModuleServer()
function, supplying each call with id
s that match those used in gapModuleUI()
, along with the appropriate data subset. Now, run your app! If written correctly, your app should run exactly the same as your initial version.
~/modularized-app/app.R
#..............................setup.............................
library(shiny)
library(gapminder)
library(dplyr)
source("gapModule.R")
# Note: This code creates data sets to use in each tab.
# It removes Kuwait since Kuwait distorts the gdp scale
all_data <- filter(gapminder, country != "Kuwait")
africa_data <- filter(gapminder, continent == "Africa")
americas_data <- filter(gapminder, continent == "Americas")
asia_data <- filter(gapminder, continent == "Asia", country != "Kuwait")
europe_data <- filter(gapminder, continent == "Europe")
oceania_data <- filter(gapminder, continent == "Oceania")
#...............................ui...............................
ui <- fluidPage(
# app title ----
titlePanel("Gapminder"),
# continent tabsetPanel ----
tabsetPanel(id = "continent",
tabPanel(title = "All", gapModuleUI(id = "all")),
tabPanel(title = "Africa", gapModuleUI(id = "africa")),
tabPanel(title = "Americas", gapModuleUI(id = "americas")),
tabPanel(title = "Asia", gapModuleUI(id = "asia")),
tabPanel(title = "Europe", gapModuleUI(id = "europe")),
tabPanel(title = "Oceania", gapModuleUI(id = "oceania"))
) # END continent tabsetPanel
) # END fluidPage
#.............................server.............................
server <- function(input, output) {
gapModuleServer(id = "all", data = all_data)
gapModuleServer(id = "africa", data = africa_data)
gapModuleServer(id = "americas", data = americas_data)
gapModuleServer(id = "asia", data = asia_data)
gapModuleServer(id = "europe", data = europe_data)
gapModuleServer(id = "oceania", data = oceania_data)
} # END server
# Run the application
shinyApp(ui = ui, server = server)
Additional module resources
We’ve barely scratched the surface of modules. Continue on with some of the following resources:
Modularizing Shiny App Code & associated materials, by Garrett Grolemund & Joe Cheng at the 2016 Shiny Developer’s Conference – NOTE: This 2016 talk is an excellent introduction to modules and is definitely worth a watch, especially because we just explored the exact example demoed by G. Grolemund. Please note, however, that Shiny modules were overhauled in 2020 with the introduction of moduleServer()
. The code on the previous slides has been updated to reflect those changes, and therefore differs slightly from what’s taught in this video.
Mastering Shiny, Ch. 19 - Shiny Modules, by Hadley Wickham
Modularizing Shiny app code, by Winston Chang
Effective use of Shiny modules in application development, by Eric Nantz at rstudio::conf(2019)
Part 7: Wrap-up
Shiny alternatives
Words of wisdom
More resources
Consider if you need Shiny at all
Additional data presentation frameworks
Shiny is awesome, but depending on your goals and end users, you may not need the full functionality that shiny provides. Importantly, you also can potentially save yourself (and your clients) the stress of deploying and maintaining shiny apps by first considering other options:
Embed interactive htmlwidgets into your R Markdown & Quarto markdown documents that range from geo-spatial mapping with leaflet to generating network graph diagrams with DiagrammeR. Check out the htmlwidgets for R - gallery for many more options.
Compose multiple widgets into a dashboard using flexdashboard. Based in R Markdown, this framework allows you to produce dynamic dashboards using tools you are already familiar with. Find example projects and their source code.
Embed reactive shiny components (e.g. inputs & outputs) in Quarto documents. By using some fun new code chunk options, you can instruct Quarto to spin up it’s own self-contained shiny server to run your reactives. Read the Quarto documentation to learn more and check out some teaching examples.
Example flexdashboards built by some familiar folks
Energy Siting Dashboard (source code), developed by MEDS 2022 alumni Paloma Cartwright, Joe DeCesaro, Daniel Kerstan & Desik Somasundaram as part of their MEDS capstone project – explore predictions of the most suitable locations for large, utility-scale wind and solar projects across the United States
@ADELPHIRESEARCH TWEETS Dashboard (source code), developed by R-Lady Shannon Pileggi as part of a job interview – read about this clever approach to showcasing your skills to a potential employer in Shannon’s blogpost, A job interview presentation inspired by the R community.
Final Thoughts & Additional Resources
Some takeaway messages that are worth keeping in mind
Oftentimes, the most time consuming part of building a shiny app is deciding on how to present the data visually – make your data visualizations first, outside of shiny, refine, decide which variables you think are important enough to make reactive, etc. THEN, build them into your shiny application.
Get your data in the most wrangled form possible before loading it into your app to avoid unnecessary slowdowns.
Code expands quickly – stay organized! Create a repository map (see an example in this README), so you and your collaborators know where files live and what they do. Use rainbow parentheses, add extra space between sections of code, and include clear code comments to denote the start and end of parentheses. This will help save headaches later on.
Keep in mind the considerations for good UX/UI design. You’re building an app for the user first and foremost – be sure to assess often if your app is going to meet their needs.
Before taking the plunge, consider if you really need shiny at all – maintaining apps can be challenging. What other options might you have for sharing your data with end users?
Great Shiny resources
A quick Google search will yield lots of online resources, forum discussions, video tutorials, etc. for building Shiny applications. Many are linked throughout these slides, but here’s an attempt at putting some of those that I referenced most often all in one spot (along with some that we didn’t have time to cover).
Books
Mastering Shiny, by Hadley Wickham
Engineering Production-Grade Shiny Apps, by Colin Fay, Sébastien Rochette, Vincent Guyader & Cervan Girard.
Building Web Apps with R, by Lisa DeBruine – an short course (paired with an online book with instructions, resources, etc.)
Tutorials
Building Shiny apps - an interactive tutorial, by Dean Attali
Speeding Up R Shiny, by Jakub Sobolewski in R bloggers – details methods on improving app performance
Allison Horst’s The Basics of Building Shiny Apps in R workshop
Tools
R Shiny & FontAwesome Icons – How to Use Them in Your Dashboards, by Dario Radečić in R Bloggers – instructions for setting up your fontawesome kit
Shiny UI Editor and Nick Strayer’s rstudio::conf(2022) talk introducing it – a visual tool for building the UI portion of a Shiny application that generates clean and human-readable code (currently in Alpha, as of January 2023)
The {golem}
package provides an opinionated framework for building production-grade shiny applications and is a part of a growing ecosystem of packages called the {golemverse}
. There are lots of accompanying learning materials, including the book, Engineering Production Grade Shiny Apps, by Colin Fay, Sébastien Rochette, Vincent Guyader, and Cervan Girard.
And don’t forget about Posit’s own excellent resources
Posit/RStudio’s great instructional resources, examples, and help documentation:
A thoughtfully organized Articles page
The Shiny User Showcase, a collection of Shiny apps and their source code developed by the Shiny developer community – many of these featured apps are winners or honorable mentions of the annual Shiny contest!
Shiny Demos, a series of apps created by the Shiny developers to highlight specific features of the shiny package – these are excellent resources to turn to when you are learning how to implement a new type of widget, working on the layout of your app, and more.
Start by commenting out functioning code
Even though this is a relatively small/simple app, there is still code that, for lack of a better term, gets in the way. After a quick assessment, my reactive scatterplot on Tab 2 appears to be working as expected. To help simplify the amount of code I need to look at, I’ll start by commenting out all UI elements (
sliderInput
&plotOutput
) and server logic for building that reactive plot.Note: As you begin building more complex apps, you may have reactives that depend on other reactives – it’s important to think about these dependencies when commenting out parts of your app for debugging purposes.
app.R