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 The word 'Shiny' in cursive lettering, which is the logo used by Posit for the shiny package.

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


Slides & source code available on GitHub

Prerequisites


This workshop assumes that participants have the following:

R/RStudio installed & a basic familiarity with the language
A GitHub profile & git installed/configured

A blue hexagon with the word 'Shiny' in cursive lettering printed across the center.

You have the required R packages installed. You can install/update them all at once by running:
install.packages(pkgs = c("shiny", "shinydashboard", "shinyWidgets", "DT", "leaflet", "shinycssloaders", "tidyverse", "bslib", "fresh", "sass", "reactlog", "shinytest2", "palmerpenguins", "lterdatasampler", "gapminder", "markdown"))
No prior Shiny experience necessary!

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

(5) Debugging & Testing

Debugging approaches ~ Testing apps

(6) Streamlining code

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 GitHub.

Part 1: High-level overview of Shiny


What is Shiny?

Anatomy of a shiny app

Where to find examples

What is Shiny?

Think interactive web pages built by people who love to code in R (i.e. hopefully many of you!), no JavaScript experience necessary.

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

A gif of Andre Duarte's 'Worldbank-Shiny' app. On the lefthand side of the app, the title 'Gapminder Interactive Plot' sits above a series of three widgets. The first is a dropdown menu where the user can select a region (e.g. Europe & Central Asia) or view all regions at the same time. The next two widgets are slider inputs -- the first allows the user to select a year between 1960 and 2014, and the second allows the user to select a population size between 500 and 5000. On the right hand side of the app is a bubble plot of Fertility Rate vs. Life Expectancy, which updates as inputs are changed by the user. Hovering a bubble displays thge corresponding Country, Region, Population, Life Expectancy, and Fertility Rate.

Worldbank-Shiny app to visualize fertility rate vs. life expectancy from 1960 to 2015, by Andre Duarte

The anatomy of a Shiny app

What does a Shiny app look like under the hood?

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)

A simple schematic of a Shiny app, which includes the User Interface (UI, colored in blue) and the Server (colored in orange). The UI creates what the user will see and interact with, while the server builds the outputs that react and update based on user inputs.

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


Examples of Shiny's pre-built widget options. These include buttons, single checkbox, checkbox groups, date input, date range, file input, numeric input, radio buttons, select box, sliders, and text input. The default color scheme is black and gray with selections highlighted in blue.

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:

A schematic of Shiny reactivity. The UI is represented by a light blue box. Inside the blue UI box, there is a radio button widget that says, 'Make a choice:' and three round radio buttons beneath it. Underneath that, there is a placeholder space for a reactive output to be created by the server. The server is to the left of the UI and is represented by an orange box. At a basic level, reactivity occurs after the following steps: (1) A widget gets information from a user which (2) is then passed to the server where it is used to update a data frame based on the users choice. (3) The new data frame is used to update outputs in the server, and (4) those outputs are then rendered in the UI.

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?

I’m glad you asked! There are lots of great examples online, including those developed by Bren alumni, as well as built-in teaching examples.

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:

library(shiny)
runExample(example = NA)

Run the first example, which plots R’s built-in faithful data set with a configurable number of bins:

runExample("01_hello")

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:

# Input: Slider for the number of bins ----
sliderInput(inputId = "bins",
            label = "Number of bins:",
             min = 1,
             max = 50,
             value = 30)

Now let’s build our own!

Play Setting up your Shiny app

Let’s start with some standard operating procedures – things you’ll do each time you begin a new 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>.

A gif demonstrating how to set up a GitHub repo and how to clone that repo to your computer. Start by clicking on the 'Repositories' tab from your GitHub profile, then click the green 'New' button. Give your repo a name, check the box next to 'Add a README file', Add a .gitigore by choosing 'R' from the drop down menu, then click the green 'Create repository' button. From your repo landing page, click the green 'Code' button, then copy the URL to your clipboard. In RStudio, select 'New Project' from the top left 'Project' button, select 'Version Control', then 'Git', and paste your URL in the 'Repository URL field'. Your repo name should be auto completed in the 'Project directory name:' field, but if not, press the 'Tab' key. Click 'Create Project' to complete the process.

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.

A visual representation of a basic shiny app repository file/folder structure.

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).

Shiny apps can be built using a single app.R file, or using ui.R plus server.R, and most often a global.R in conjunction.

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.

# load packages ----
library(shiny)

# user interface ----
ui <- fluidPage()

# server instructions ----
server <- function(input, output) {}

# combine UI & server into an app ----
shinyApp(ui = ui, server = server)

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: A green, right facing triangular arrow next to the words 'Run App'. 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, A button found on the top left-hand side of the RStudio viewer window that says 'Open in Browser' next to a small browser window icon with an arrow pointing up and to the right., to see how your app will appear when viewed in your web browser.

You should also notice a red stop sign, A red hexagon with the word 'STOP' printed in white across the center., 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.

The RStudio viewer showing a running app consisting of only a blank white screen since no elements have been added yet.

RStudio running our blank app. In the console, we see the text 'Listening on http://127.0.0.1:6341' and a red stop sign indicating that RStudio is busy.

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).

ur.R
# user interface ----
ui <- fluidPage()
server.R
# server instructions ----
server <- function(input, output) {}

3. Lastly, let’s create a global.R file within /two-file-app and add dependencies (right now, that’s just loading the shiny package). Run your app as we did earlier.

global.R
# load libraries ----
library(shiny)

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

Here, we’ll create our first reactive objects and establish a general Shiny coding workflow.

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

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

A gif of our current Shiny app, demonstrating reactivity. At the top left of our app is the title, 'My App Title' in large header font. Beneath it is a subtitle, 'Exploring Antarctic Penguins and Temperatures'. Below the subtitle is the slider input with the label, 'Select a range of body masses (g)'. A gray horizontal slider bar ranges from the values 2,700 to 6,300. The interactive slider value selectors are two round white circles, which, when moved apart from one another highlight the selected value range in blue. The user is adjusting the slider value selectors and the scatterplot of penguin bill length (mm) vs. flipper length (mm) is automatically updating.

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:

~/one-file-app/app.R
# user interface ----
ui <- fluidPage(
  
  # app title ----
  "My App Title",
  
  # app subtitle ----
  "Exploring Antarctic Penguin Data"
  
  )

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()).

~/one-file-app/app.R
# user interface ----
ui <- fluidPage(
  
  # app title ----
  tags$h1("My App Title"), # alternatively, you can use the `h1()` wrapper function
  
  # app subtitle ----
  p(strong("Exploring Antarctic Penguin Data")) # alternatively, `tags$p(tags$strong("text"))`
  
  )

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

A gif of our current Shiny app, demonstrating reactivity. At the top left of our app is the title, 'My App Title' in large header font. Beneath it is a subtitle, 'Exploring Antarctic Penguins and Temperatures'. Below the subtitle is the slider input with the label, 'Select a range of body masses (g)'. A gray horizontal slider bar ranges from the values 2,700 to 6,300. The interactive slider value selectors are two round white circles, which, when moved apart from one another highlight the selected value range in blue. The user is adjusting the slider value selectors and the scatterplot of penguin bill length (mm) vs. flipper length (mm) is automatically updating.

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:

sliderInput(inputId = "body_mass_input", label = "Select a range of body masses (g):", value = c(3000, 4000), ...)

selectInput(inputId = "island_input", label = "Choose and island:", ...)

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).

~/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))
  )

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.

A basic Shiny app, with a title that says 'My App Title', a subtitle that says, 'Exploring Palmer Penguins and Antarctic Temperatures, and a slider input with a label that says 'Select a range of body masses (g)'. The slider bar has a minimum value of 2,700 and a maximum value of 6,300, and the moveable slider selectors currently range from 3,000 to 4,000.

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:

plotOutput(outputId = "bodyMass_scatterPlot")

dataTableOutput(outputId = "penguin_data")

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”:

A Google Chrome browser window with our Shiny app open on the left-hand side and the underlying HTML document open on the right. The app looks the same as before, except hovering over the HTML associated with our new plotOutput highlights a blue square region beneath the sliderInput. This highlighted region is the placeholder where our plot will eventually be rendered.

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"))

A scatterplot with Flipper length (mm) on the x-axis and Bill length (mm) on the y-axis. Data points are colored by penguins species: Adelie in orange circles, Chinstrap in purple triangles, and Gentoo in green squares.

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…

A user changes the sliderInput meant associated with the penguin data scatterplot, but the plot does not update (i.e. it is not yet reactive).

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):

~/scratch/practice-script-app1-penguins.R
# load packages
library(palmerpenguins)
library(tidyverse)

# filter penguins df for observations where body_mass_g >= 3000 & <= 4000
body_mass_df <- penguins |> 
  filter(body_mass_g %in% 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"))

A scatterplot of penguin Bill length (mm) vs. Flipper length (mm) for Adelie (orange circles), Chinstrap (purple triangles), and Gentoo (green squares) penguins. For all species, bill length tends to increase with flipper length.

Which part of our code needs to be updated when a user changes the slider range input?


~/scratch/practice-script-app1-penguins.R
body_mass_df <- penguins |> 
  filter(body_mass_g %in% 3000:4000) # 3000:4000 needs to be update-able (or in other words, reactive)!

For example:

body_mass_df <- penguins |> 
  filter(body_mass_g %in% 2857:5903)

Our Shiny app with just a title, subtitle, and slider input, where the input values are at a minimum of 2,857 and a maximum of 5,903.

body_mass_df <- penguins |> 
  filter(body_mass_g %in% 3725:5191)

Our Shiny app with just a title, subtitle, and slider input, where the input values are at a minimum of 3,725 and a maximum of 5,191.

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.

A gif of our current Shiny app, demonstrating reactivity. At the top left of our app is the title, 'My App Title' in large header font. Beneath it is a subtitle, 'Exploring Antarctic Penguins and Temperatures'. Below the subtitle is the slider input with the label, 'Select a range of body masses (g)'. A gray horizontal slider bar ranges from the values 2,700 to 6,300. The interactive slider value selectors are two round white circles, which, when moved apart from one another highlight the selected value range in blue. The user is adjusting the slider value selectors and the scatterplot of penguin bill length (mm) vs. flipper length (mm) is automatically updating.

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.

Question 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.

A gif of our current Shiny app, demonstrating the newly added DT::datatable. The user is able to select which years (2007, 2008, 2009) to display data for by clicking on one more more checkboxes.


See next slide for some tips on getting started!

Lightbulb 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 inputIds (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(...)))


A gif of Britney Spears in her iconic red spandex outfit singing 'Oops I did it again.'

Break

Give your eyes a break from the computer screen!

05:00

Building out your 2nd app

Up until now, we’ve been adding our text and widgets in a pretty unstructured way – elements are stacked on top of one another within a single column. Next, we’ll learn how to customize the layout of our app to make it a bit more visually pleasing.

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

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


An app titled 'LTER Animal Data Explorer'. The user starts on the 'About this App' page, which includes a couple paragraphs of intro text. The user then clicks on the 'Explore the Data' page which has two tabs: Trout and Penguins. On the Trout tab, we see a scatterplot with Trout Length (mm) on the x-axis and Trout Weight (g) on the y-axis. The user first filters data by Channel Type using a drop down list selector widget, then by Forest Section using two buttons that toggle on data from clear cut vs. old growth forest. In the Penguin tab, we see a histogram displaying Flipper Lengths (mm) of three penguin species. The user uses a drop down list selector widget to filter data by Island, then uses a slider widget to adjust the number of histogram bins.

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

RStudio with three columns. (From left to right): (1) a source pane with `ui.R` open (2) a second source pane on the top with `server.R` open and the console on the bottom, (3) Environment/History/Git/etc. on the top and Files/Plots/Packages/etc. on the bottom

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
A scatterplot with Trout Length (mm) on the x-axis and Trout Weight (g) on the y-axis. Data points are colored and shaped by Channel Type, with data collected from pools represented with green diamonds and data collected from rapids represented by red stars.

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
A histogram of penguin Flipper lengths (mm) with bars colored by species. Adelie are orange and Chinstrap are purple.

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

A gif of our first Shiny app. The title, 'My App Title' is in large black text at the top. Underneath, the subtitle, 'Exploring Antarctc Penguin Data' is in smaller black text. Immediately beneath, there is a sliderInput with black text above it that says, 'Select a range of body masses (g). The user is sliding both upper and lower ends to select a range of body masses to display data for. The scatterplot, with Flipper length (mm) on the x-axis and Bill length (mm) on the y-axis, updates accordingly. The user then scrolls down to a DT datatable. Immediately below the first plot is text that says 'Select year(s):' with checkboxes next to the years 2007, 2008, and 2009. The boxes next to 2007 and 2008 are already checked. The user also selects the box next to 2009. The user then uses a drop down menu to display 50 entries (rather than the default 10). The user clicks on the 'bill_length_mm' column to rearrange the data in descending order.


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:

fluidPage(
  titlePanel(
    # app title/description
  ),
  sidebarLayout(
    sidebarPanel(
      # inputs here
    ),
    mainPanel(
      # outputs here
    )
  )
)
A simplified schematic of a Shiny app with a sidebar layout. The page as a whole is created with the fluidPage() function. The titlePanel() function creates a row at the top of the page for a title. The sidebarLayout() function creates a new row below titlePanel(). Within the sidebarLayout(), there are two side-by-side columns created using the sidebarPanel() function (to the left) and the mainPanel() function (to the right).

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.

fluidPage(
  fluidRow(
    column(4, 
      ...
    ),
    column(8, 
      ...
    )
  ),
  fluidRow(
    column(6, 
      ...
    ),
    column(6, 
      ...
    )
  )
)
A simplified schematic of a Shiny app with a multi-row layout. The page as a whole is created with the fluidPage() function. Within that, the fluidRow() function is used twice to create two stacked (one atop the other) rows on the page. Within each fluidRow are two side-by-side columns, each created using the column() function. Each row is made up of 12 columns. The column() function takes a value of 1-12 as the first arguement to specify how many of those 12 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:

tabsetPanel(
  tabPanel("Tab 1", 
    # an input
    # an output
  ),
  tabPanel("Tab 2"),
  tabPanel("Tab 3")
)
A simplified schematic of a Shiny app with a tabsetPanel layout. The page as a whole is created with the fluidPage() function. Within that, the tabsetPanel() function creates a container within which three tabPanel()s ('Tab 1', 'Tab 2', 'Tab 3') are defined (for this particular example). Tab 1 is highlighted and has placeholder text which says '# an input' and then on the line below, '# an output'.

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:

navbarPage(
  title = "My app",
  tabPanel(title = "Tab 1",
           # an input
           # an output
           ),
  tabPanel(title = "Tab 2")
)
A simplified schematic of a Shiny app with a navbarPage layout. The page as a whole is created with the navbarPage() function. A top-level navigation bar can be used to toggle between two tabPanel()s ('Tab 1', 'Tab 2'), which are defined for this particular example. Tab 1 is highlighted and has placeholder text which says '# an input' and then on the line below, '# an output'.

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

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:

Our new app, which does not yet contain any content (e.g inputs and outputs), but does have an organized layout structure. There is a navbar across the top of the page, with the title 'My app title' on the left-hand side of the navbar. There are two pages on the navbar: one says 'Welcome to my app' and the second says 'Explore the data'. The user clicks into the second page to reveal two tabs named 'Trout', and 'Bison'. Each tab has a gray box (a sidebar) on the lefthand side and a main panel space to the right.


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.


A drawing of the right-side profile of a coastal cutthroat trout.

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

global.R
# LOAD LIBRARIES ----
library(shiny)
library(lterdatasampler) 
library(tidyverse)
library(shinyWidgets) 

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():

ui.R
plotOutput(outputId = "trout_scatterplot")

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:

The Trout tab of our app, which contains only a pickerInput with the variables, cascade and pool, selected. There is no visible plot yet, however using Google Chrome's developer tools shows a div (i.e. a box) placeholder where our plot will eventually appear.

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!


A user navigates to the 'Explore the Data' tab of our app, 'LTER Animal Data Explorer.' There is a scatterplot with Trout Length (mm) on the x-axis, and Trout Weight (g) on the y-axis. Points are colored and shaped by channel type. There is a pickerWidget to the left of the plot with the channel_types 'cascade' and 'pool' already selected. The user clicks other channel types on/off to see how the data points on the plot change.

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!


Our updated app, which includes the same trout scatterplot, but this time, two inputs: one selectInput that allows the user to filter for channel_type, and one checkboxGroupInput, which includes two buttons to select/deselect data collected from the clear cut forest section and the old growth forest section.

Break

Up next: your turn to add a reactive penguin plot to our “Penguin” tab!

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.


A cartoon drawing of Chinstrap (atop a purple background), Gentoo (atop a green background) and Adélie (atop an orange background) penguins.

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

A user navigates to the Penguins tab of our app to reveal a histogram of penguins flipper lengths. Data are colored by species (Adelie = orange, Chinstrap = purple, Gentoo = green). The user uses a pickerInput to filter data based on the island where they were collected. The user also adjust the bin number using a sliderInput.

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

Next, we’ll finish up v1 of our app by adding some intro text to the landing page

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:


The landing page (i.e. the 'About this App' page) of ourapp, which includes a large h1 title that says 'Welcome to the LTER Animal Data Explorer!' followed by two subsections titled, 'Why did we build this app?' and 'Where's the data?'. A faint gray horizontal divider line separates these sections from the footer at the bottom of the page, which tells the user that the app is maintained by Samantha Csik, is updated as needed for teaching purposes, to report issues at a link, and that source code is found on GitHub.

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 columns 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!


An app titled 'LTER Animal Data Explorer'. The user starts on the 'About this App' page, which includes a couple paragraphs of intro text. The user then clicks on the 'Explore the Data' page which has two tabs: Trout and Penguins. On the Trout tab, we see a scatterplot with Trout Length (mm) on the x-axis and Trout Weight (g) on the y-axis. The user first filters data by Channel Type using a drop down list selector widget, then by Forest Section using two buttons that toggle on data from clear cut vs. old growth forest. In the Penguin tab, we see a histogram displaying Flipper Lengths (mm) of three penguin species. The user uses a drop down list selector widget to filter data by Island, then uses a slider widget to adjust the number of histogram bins.

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

Sharing your Shiny app with others isn’t so easy when it just lives on your computer (and your R session has to act as the server that powers it). We’ll now learn how to host your app using shinyapps.io, a free service for sharing your Shiny apps online.

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:

shinyapps.io displays setup instructions: (1) INSTALL RSCONNECT: The `rsconnect` package can be installed directly from CRAN. To make sure you have the latest version run following code in your R console: `install.packages('rsconnect')`. (2) AUTHORIZE ACCOUNT: The `rsconnect` package must be authorized to your account using a token and secret. To do this, click the copy button below and we'll copy the whole command you need to your clipboard. Just paste it into your console to authorize your account. Once you've entered the command successfully in R, that computer is now authorized to deploy applications to your shinyapps.io account. (3) DEPLOY: Once the `rsconnect` package has been configured, you're ready to deploy your first application. If you haven't written any applications yet, you can also checkout the Getting Started Guide for instruction son how to deploy our demo application. Run the following code in your R console: `library(rsconnect)`, then `rsconnect::deployApp('path/to/your/app')`.

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).

Our shiny app, now hosted at the URL https://samanthacsik.shinyapps.io/two-file-app which functions the same as when it was hosted locally.

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.

The shinyapps.io dashboard displaying information about our two-file-app, now hosted at https://samanthacsik.shinyapps.io/two-file-app/. You can select different menu items from the navbar at the top of the page, including Overview, Metrics, URLs, Settings, Users, Logs, Restart, Archive, and Trash.

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

Our two-file-app is looking pretty good! It’s functional and deployed via shinyapps.io. Next, we’ll focus on making some minor tweaks that can help to improve usability

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:

Box Open 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

Our most up to date app, which includes loading animations that signal to the user whenever a plot output is re-rendering, validation error messages that alert users to what is necessary to display data (in the case that they 'Deselect All' data using the available widgets), and alt text for both 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 pickerInputs 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.

A sign hanging on a glass door that is divided vertically -- the left-hand side is colored blue and reads 'Don't Push' with the words stacked on top of one another. The right-hand side is colored red and reads 'Pull Only' with the words stacked on top of one another. However, when the sign is read left to right, top to bottom, it appears to say 'Don't Pull Push Only'.

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 plotOutputs 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 types to choose from) and adjust the size of the penguin plot spinner.


ui.R
plotOutput(outputId = "trout_scatterplot") |> 
  withSpinner(color = "#006792", type = 1)

plotOutput(outputId = "flipperLength_histogram") |> 
  withSpinner(color = "#4BA4A4", type = 4, size = 2)
Loading animations appear each time an input value is changed and plot outputs are re-rendered. The trout plot has a blue animation of three perpendicular vertical bars that undulate in length. The penguin plot has a green trailing circular pattern of small circles that grow and shrink in size as they rotate.

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

Now that we’ve added some updates to our app, it’s time to redeploy our newest version.

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/

Mug Hot Break

Next, we’ll start building out a shiny dashboard!

05:00

Building dashboards with shinydashboard

Shiny alone is powerful and flexible, however it can take a lot of work to create a sleek/modern UI. shinydashboard provides a “template” for quickly building visually appealing dashboard apps.

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 menuItems

(c) a landing page with background information about your app

(d) an interactive and reactive leaflet map

A completed shinydashboard with intro text on the welcome (landing) page, dividing among boxes on the screen, and a map of the area of interest. On the dashboard page, we see three slider widgets in a box on the left-hand side of the page, and a reactive leaflet map on the right-hand side.

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.

A simple shinydashboard with two boxes (one containing a histogram and one containing a sliderInput) in the body. The header reads 'Basic tabs' and the sidebar has two menu items: Dashboard and Widgets.

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)
The most basic shinydashboard UI, with the default coloring -- a dashboardHeader in light blue, a dashboardSidebar in dark blue, and a dashboardBody in off-white.

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)
server <- function(input, output) {}
# LOAD LIBRARIES ----
library(shiny)
library(shinydashboard)

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.

The NSF Arctic Data Center Logo, which is a drawing depicting a blue mountain scape with green Northern Lights overhead.

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:

A schematic of the repository structure, as described.

The Goal:


An interactive leaflet map with 11 markers placed on different lakes throughout Northeast Alaska. Clicking on a marker reveals details about the lake including the name, elevation, average depth, and average lake bed temperature.

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:

An example of our desired final data frame, with 6 attributes (Site, Latitude, Longitude, Elevation, AvgDepth, AvgTemp) and 11 rows of observations, one for each lake surveyed.

Process lake data & save new file


NOTE: In this example exercise, I’ve removed all rows with missing values (i.e. NaNs in the Depth column & NAs 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.

The R language logo overlaid on top of a database stack -- often used to represent the rds data file format.

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 sliderInputs 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 boxes, the primary building blocks of shinydashboards (more on that soon).


A rough sketch of our dashboard welcome page. A header has a place for the app's title in the top left corner and there's a sidebar on the left-hand side of the page with our two pages (welcome and dashboard). The welcome page has a box on the left-hand side that will contain background info and maybe a photo. The right-hand side of the page has two stacked boxes. The top box will contain data citation information and the bottom box will contain a disclaimer.
A sketch of the dashboard page of our app, which will have two side-by-side boxes. The left-hand box will contain three sliderInputs and the right-hand box will contain our reactive leaflet map.

Add a title & menuItems


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 menuItems. 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 boxes 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 sliderInputs 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 boxes to contain UI content (part 2)


Lastly, add boxes to our welcome tab We’ll use columns 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 fluidRows 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.

global.R
# LOAD LIBRARIES ----
library(shiny)
library(shinydashboard)
library(tidyverse)
library(leaflet)
library(shinycssloaders)

# READ IN DATA ----
lake_data <- read_csv("data/lake_data_processed.csv")

Add a sliderInput & leafletOutput to the UI


Start by adding just one sliderInput (for selecting a range of lake Elevations) 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 titles 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:

Our Fish Creek Watershed Lake Monitoring dashboard. The 'Welcome' page has a box on the left for background info, and two stacked boxes on the right for data citation and disclaimer info. The 'Dashboard' tab has a sliderInput in the left-hand box, where users can select a range of elevations. When a range is selected, the blue markers on the leaflet map in the right-hand box are filtered accordingly. Clicking on the markers reveals more information about the specific site, including Site Name, Elevation, Avg Depth, and AvgTemp.

Exercise 5: Add two more sliderInputs to filter for AvgDepth & AvgTemp


To Do:

Add two more sliderInputs, 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

Up next: Adding background information and an image to our Welcome tab

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).*
text/disclaimer.md
This app is build for demonstration/teaching purposes only and is not paid for or endorsed by the Fish Creek Watershed Observatory or affiliates in any way. The data as presented here are not intended for publication nor scientific interpretation. 


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)
# LOAD LIBRARIES ----
library(shiny)
library(shinydashboard)
library(tidyverse)
library(leaflet)
library(shinycssloaders)
library(markdown)

# READ IN DATA ----
lake_data <- read_csv("data/lake_data_processed.csv")

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…


Our Welcome page, with a map of Fish Creek Watershed beneath the intro text. The image is extremely large, spilling out of the box and across the page.

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!


Our completed dashboard, with our image styled so that it fits within the background info box on the Welcome page, and three functional sliderInputs that update the leaflet map on the Dashboard page.

There’s a ton more to learn about building shinydashboards. Check out the documentation to find instructions on adding components like infoBoxes and valueBoxes, 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"))
    
  })
  
}
# LOAD LIBRARIES ----
library(shiny)
library(shinydashboard)
library(tidyverse)
library(shinycssloaders)
library(leaflet)
library(markdown)

# READ IN DATA ----
lake_data <- read_csv("data/lake_data_processed.csv")

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

We’ve built some really cool apps so far, but they all have a pretty standard and similar appearance. In this section, we’ll explore two packages for creating custom themes for your apps.

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):


~/one-file-app/ui.R
ui <- fluidPage(
  
  theme = bslib::bs_theme(bootswatch = "solar")
  
  # ~ additional UI code omitted for brevity ~
  
)

Check out the complete source code for App #1 here (NOTE: applied themes are commented out).

A shiny app depicting a title, subtitle sliderInput, scatterplot, checkboxGroupInput, and DT datatable, all stacked vertically. The pre-build bootswatch 'solar' theme has been applied, turning the background color dark blue and changing widget colors to dark gray or yellow. The background of the scatterplot is still white.

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:


~/one-file-app/ui.R
ui <- fluidPage(
  
  theme = bslib::bs_theme(
    bg = "#A36F6F", # background color
    fg = "#FDF7F7", # foreground color
    primary = "#483132", # primary accent color
    base_font = font_google("Pacifico"))
  
  # ~ additional UI code omitted for brevity ~
  
)

Check out the complete source code for App #1 here (NOTE: applied themes are commented out).

A shiny app depicting a title, subtitle sliderInput, scatterplot, checkboxGroupInput, and DT datatable, all stacked vertically. A custom theme, created using bs_theme() has been applied, turning the background color pink, changing widget colors to a dark magenta, and applying a cursive white font styling to all text. The background of the scatterplot is still white.

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:


~/one-file-app/ui.R
thematic::thematic_shiny()

ui <- fluidPage(
  
  theme = bslib::bs_theme(
    bg = "#A36F6F", # background color
    fg = "#FDF7F7", # foreground color
    primary = "#483132", # primary accent color
    base_font = font_google("Pacifico"))
  
  # ~ additional UI code omitted for brevity ~
  
)

Check out the complete source code for App #1 here (NOTE: applied themes are commented out).

A shiny app depicting a title, subtitle sliderInput, scatterplot, checkboxGroupInput, and DT datatable, all stacked vertically. A custom theme, created using bs_theme() has been applied, turning the background color pink, changing widget colors to a dark magenta, and applying a cursive white font styling to all text. The background of the scatterplot is now the same color (pink) as the background.

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:



~/two-file-app/ui.R
# navbar page ----
ui <- navbarPage(

  theme = "shiny_fresh_theme.css"
  
  # ~ additional UI code omitted for brevity ~
  
) # END navbarPage

Check out the complete source code for App #2 here (NOTE: applied themes are commented out).

An app with a tan-colored background, lime green navbar, bright pink text, and blue links, showcasing how different components of a shiny app can be modified using the fresh packages.

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:



~/shinydashboard/ui.R
body <- dashboardBody(
  
  # set theme
  fresh::use_theme("shinydashboard_fresh_theme.css")
  
  # ~ additional dashboardBody code omitted for brevity ~
  
)

Check out the complete source code for the shinydashboard here (NOTE: applied themes are commented out).

A shinydashboard with a dark blue dashboardHeader, pink dashboardBody, light blue dashboardSidebar, red sidebar text, and bright pink sidebar menuItem highlights, showcasing how shinydashboards can be customized using the fresh package.

Styling apps with CSS & Sass

bslib & fresh are great ways to get started on your app customization journeys, but knowing some CSS & Sass can help you really fine-tune the appearance of your apps

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).


The W3Schools logo, which is a green 'W' with a green '3' to the right.

What even is CSS? Sass?


The CSS 3 logo.

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.


The Sass logo

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:


An example of a CSS rule that modifies all h1 elements so that the text is green and that there's a bit more space between each letter.

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)
A basic app with a large purple level 1 header that reads, 'My App', a blue level 2 header that reads, 'Section 1' with a bit of extra space between letters, an un-styled level 2 header that reads, 'Section 2', and a button with a green border that reads, 'This is a button'.

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)
A basic app with a large black level 1 header written in a cursive font that reads, 'My App', a blue level 2 header that reads, 'Section 1', a blue level 2 header that reads, 'Section 2' with a bit of extra space between letters, and an un-styled button that reads, 'This is a button'.

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;
}
A basic app with a large green level 1 header that reads, 'My App' written in a sans-serif font, an orange level 2 header that reads, 'Section 1' with a bit of extra space between letters and written in a serif font, an orange level 2 header that reads, 'Section 2' written in a serf font, and a button with a yellow background that reads, 'This is a button'.

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 
  
}
# LOAD LIBRARIES ----
library(shiny)
library(shinydashboard)
library(tidyverse)
library(palmerpenguins)

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.

A shiny dashboard, opened in the RStudio viewer, with the Inspect pane open to the right. The top half of the inspect pane shows the underlying HTML and the bottom half shows the underlying CSS styles.

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.



Check out the complete code for this small example dashboard here.

An example shinydashboard with default colors (light blue dashboardHeader, dark blue dashboardSidebar, gray/blue dashboardBody) and two boxes, one containing a checkboxGroupInput and the other containing a scatterplot -- the box colors are colored orange, rather than the default white.
.box-body {
  background-color: #FCAE82; /* light orange */
  color: #6368C6; /* purple-blue */
}
#........................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:

A color palette consisting of three colors: dark blue, teal, and dark gray.

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:

// define Sass vars 

$darkblue: #053660;
$teal: #147C91;
$darkgray: #333333;

// use vars in CSS rules

h1 {
  color: $darkblue;
}

.button-styling {
  background: $teal;
  color: $darkblue; 
  border-color: $darkgray;
}

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.

A chart showing the Sass to CSS workflow -- begin with writing Sass variables and CSS rule in a .scss file, then use the sass R package to compile sass to css, and finally use your compiled .css file to apply your styles to your app.

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.



Check out the complete code for App #2 here (NOTE: applied themes are commented out).

Our app, with our sass and css styling applied. The navbar is yellow, body is green, level 4 headings are blue, text is white, hyperlinks are orange, and header and body text font styles have been changed from the default.
# LOAD LIBRARIES ----
library(shiny)
library(sass)
# ~ additional libraries omitted for brevity ~

# COMPILE CSS ----
sass(
  input = sass_file("www/my_sass_styles.scss"),
  output = "www/my_sass_styles.css"
)

# ~ additional global.R objects omitted for brevity ~
# navbar page ----
ui <- navbarPage(
  
  # add css file ----
  header = tags$head(
    tags$link(rel = "stylesheet", type = "text/css", href = "my_sass_styles.css")
  )
  
  # ~ additional UI elements omitted for brevity ~
)
@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

When designing your app, it’s critically important that you consider your user’s needs and how they will interact with your app – it doesn’t matter how innovative you back-end computations are if people don’t understand how to use your app!

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

Consider web accessibilty guidelines to ensure that your app is usable by all

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

In the center, we see a computer monitor with the world wide web symbol on the screen. There is a circle around the computer made up of four smaller circles, each containing on of the following symbols: and eye, a hand with a finger touching something, a human head with a brain, and an ear.

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:

# in your server
renderPlot({

    ggplot(data(), aes(x = var1, y = var2)) + 
      geom_point() 
    
  }, alt = "Alt text description"
  
  )

Similarly, use the alt argument within tags$img when adding static images to your app. For example:

# in your UI
tags$img(src = "file/path/to/img",
         width = "100px", height = "100px",
         alt = "Alt text for image")

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:

Bar chart of gun murders per 100,000 people where America's murder rate is 6 times worse than Canada, and 30 times Australia.

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.

A computer mouse and keyboard that's animated to show the left mouse button clicking and the left-hand side shift button being pressed.

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”).


Four color palette wheels showing the difference in perceived colors for those with normal vision, protanopia, deuteranopia, and tritanopia.

Four different forms of colorblindness. Image Source: Venngage

A gif of a user opening up the developer tools pane in Google Chrome and emulating how users with various vision deficiencies would see the webpage.

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

Like any code, you’re bound to run into errors as you’re developing your shiny app(s). However, Shiny can be particularly challenging to debug. In this section, we’ll review a few approaches for solving pesky issues.

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:

An example of commenting code from the outside in, demoed in RStudio as described above.

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.

A functioning shiny app with two tabs. The first tab has radioButtons that when selected, update the penguin image and description text. The second tab has a sliderInput and scatterplot.

…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.

The same shiny app as on the previous slide, but this time when the radioButtons are updated, only the image changes -- no text appears.

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.

# 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"){
      "Meet all of our lovely penguins species!"
    }
    else if(input$img == "Sassy chinstrap"){
      "Chinstraps get their name from the thin black line that runs under their chins"
    }
    else if(input$img == "Staring gentoo"){
      "Gentoos stand out because of their bright orange bills and feet"
    }
    else if(input$img == "Adorable adelie"){
      "Adelie penguins are my personal favorite <3"
    }
  }) # END renderText
  
  
  # render penguin images ----
  output$penguin_img <- renderImage({
    
    if(input$img == "All penguins"){
      list(src = "www/all_penguins.jpeg", height = 240, width = 300)
    }
    else if(input$img == "Sassy chinstrap"){
      list(src = "www/chinstrap.jpeg", height = 240, width = 300)
    }
    else if(input$img == "Staring gentoo"){
      list(src = "www/gentoo.jpeg", height = 240, width = 300)
    }
    else if(input$img == "Adorable adelie"){
      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)
The same app as on the previous slides, except the sliderInput and scatterplot code has been commented out so that those elements don't appear when the app is run.

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 inputIds and outputIds 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)
We see our app.R file with 'message()'s inserted throughout our reactive elements. When our app is run and the user updates the radioButtons, we see the associated messages appear in the console (or not appear, if code is broken).

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

The reactlog hex sticker design.

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 A gray, left-facing play button and A gray, right-facing play button) 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:

ui <- fluidPage(
  
  # ~ previous code excluded for brevity ~
  
  # text output ----
  textOutput(outputId = "penguin_text")
  
)

But we call penguins_text when rendering our output in the server:

server <- function(input, output){
  
  # ~ previous code excluded for brevity ~
  
  # render penguin text ----
  output$penguins_text <- renderText({
    
    # ~ code excluded for brevity ~
    
  })
}

By updating our outputId to match in both the UI and the server, we fix our app.

Testing

Creating automated tests for your apps can save time and effort, ensuring that they continue working as expected.

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

Why test our Shiny apps?

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.

A hex that's split down the middle horizontally. The top half is the blue Shiny hex and the bottom half looks like the red {testthat} hex, except it reads 'test2'.

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
library(shiny)

ui <- fluidPage(
  
  textInput("name", "What is your name?"),
  actionButton("greet", "Greet"),
  textOutput("greeting")
  
)

server <- function(input, output, session) {
  
  output$greeting <- renderText({
    req(input$greet)
    paste0("Hello ", isolate(input$name), "!")
    
  })
  
}

shinyApp(ui, server)

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:

A user begins recording a test by typing shinytest2::record_test('testing_app') in the RStudio console, which opens up the app in an iframe with the recorder in a pane to the right. The user types 'Sam' into the text box, clicks the 'Greet' button, then clicks 'Expect Shiny values' in the recorder. Finally, the user names and saves the test, which then runs automatically once the recorder is quit.

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:

# Load application support files into testing environment
shinytest2::load_app_env()

(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:

library(shinytest2)

test_that("{shinytest2} recording: sam-test", {
  app <- AppDriver$new(name = "sam-test", height = 509, width = 657)
  app$set_inputs(name = "Sam")
  app$click("greet")
  app$expect_values()
})

(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:

A screenshot from my app, with the text 'Sam' printed in the text input box beneath the prompt, 'What is your name?'. Below the text box is a button that says 'Greet'. Below that, the text, 'Hello Sam!' is printed.

(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:

{
  "input": {
    "greet": 1,
    "name": "Sam"
  },
  "output": {
    "greeting": "Hello Sam!"
  },
  "export": {

  }
}

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

Functions have many benefits and can improve your code base, particularly as your app grows in complexity

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.

A shiny app with two tabs. The first tab contains a pickerInput that allows the user to select which penguin species they want to visualize data for, along with a scatterplot. The second tab also contains a pickerInput with the exact same penguin species options, along with a histogram.
~/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
~/functions-app/global.R
# load packages ----
library(shiny)
library(shinyWidgets)
library(palmerpenguins)
library(tidyverse)

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 pickerInputs 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 pickerInputs are their inputIds, we can write a function that takes inputId as an argument (Recall that inputIds must be unique within an app, so it makes sense that both of our pickerInputs have different inputIds).

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
penguinSpp_pickerInput <- function(inputId) {
  pickerInput(inputId = inputId, label = "Select a species:",
              choices = c("Adelie", "Gentoo", "Chinstrap"),
              options = pickerOptions(actionsBox = TRUE),
              selected = c("Adelie", "Gentoo", "Chinstrap"),
              multiple = TRUE)
}
~/functions-app/global.R
# load packages ----
library(shiny)
library(shinyWidgets)
library(palmerpenguins)
library(tidyverse)

# IMPORT FUNCTIONS ----
source("R/penguinSpp_pickerInput.R") # will source automatically with Shiny v1.5.0

Apply your function in ui.R


Finally, replace your original UI code for building both pickerInputs 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")
    
  })
  
}
~/functions-app/server.R
server <- function(input, output) {
  
  # filter data & create penguin scatterplot ----
  output$penguin_scatterplot <- build_penguin_scatterplot(input)
  
  # filter data & create penguin histogram ----
  output$penguin_histogram <- build_penguin_histogram(input)

} # END server

Writing modules

We can take our code abstraction a step further and bundle repeated UI & server components together into modules, streamlining our code and increasing efficiency.

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:

Box Open 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 gapminder app, which has 6 tabs, one for each global region. A bubble plot takes up the width of the ap and displays Life Expectancy by GDP per capita, where each bubble represents a country and the size of bubbles represent the population size of that country. An automated sliderInput advances through the years (1952-2007), and the plot updates accordingly.

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)

# "All" tabPanel (repeated 5 more times for each subregion) 
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)))

reactive data frame (server)

# "All" reactive data frame (repeated 5 more times for each subregion)
ydata_all <- reactive({
  filter(all_data, year == input$all_year)
})

reactive data frame (server)

# "All" reactive data frame (repeated 5 more times for each subregion)
ydata_all <- reactive({
  filter(all_data, year == input$all_year)
})

calculating date ranges (server)

# "All" date range (repeated 5 more times for each subregion)
xrange_all <- range(all_data$gdpPercap)
yrange_all <- range(all_data$lifeExp)

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 Ids (e.g. inputIds) 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 Ids 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 tabPanels (one for each of our six regions), but rather than building a plotOutput and sliderInput inside each tabPanel (each with unique Ids), 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 ids 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)

A beginners guide to Shiny modules, by Emily Riederer

Part 7: Wrap-up


Shiny alternatives

Words of wisdom

More resources

Consider if you need Shiny at all

While a well-developed shiny app is fun and appealing, it’s worth having a conversation about whether shiny is truly necessary, or if taking an alternative approach to sharing your data might be better.

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:

Logos for htmlwidgets, flexdashboard, and quarto.

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

We’ve covered a lot in this workshop, and we’ve only just begun to scratch the surface – we’ll end with some final thoughts/words of wisdom, along with some resources that are worth returning to as you begin your deep dive into shiny app development.

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.

I’m excited to see what Shiny new apps you all create!

A gif of sparkles that appear to be moving out of a dark background towards the viewer.