EDS 430: Part 3

Building Shiny dashboards


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


The {shinydashboard} package provides additional UI layout functions that make building apps with a more classic “dashboard” feel a bit easier. You’ll always need to import {shiny} alongside {shinydashboard} (i.e. it’s not a full replacement for {shiny}).

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


Live dashboards:

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

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

Dashboards which are no longer deployed:

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

Setup your 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 – I typically recommend splitting the UI into separate pieces, then combining 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.

~/shinydashboard/ui.R
#........................dashboardHeader.........................
header <- dashboardHeader()

#........................dashboardSidebar........................
sidebar <- dashboardSidebar()

#..........................dashboardBody.........................
body <- dashboardBody()

#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)
~/shinydashboard/server.R
server <- function(input, output) {}
~/shinydashboard/global.R
# LOAD LIBRARIES ----
library(shiny)
library(shinydashboard)

Be sure to run your app after each new addition!



From here on out, you should be running your app after each new addition to check that things work as expected – aim to do so after each slide where code is added.


I don’t explicitly call to do so on each slide, but this is a super important practice to get into!


Remember to give yourself proper space between lines and add annotations at the start and end of each parentheses – I highly recommend following along with the formatting I use throughout these materials.

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 Nathan Stephens’ article, Where to store your Shiny application data to start. Because we are going to be working with a relatively small data set today, we’ll bundle our data file with our dashboard inside our repository.

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,994 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


~/scratch/data_processing_app3_shinydashboard.R
#...............................................................................
#                                                                              .
#  For simplicity, I've removed all rows with missing values (i.e. `NaN`s      .
#  in the `Depth` column & `NA`s in the `BedTemperature` column) before        .
#  calculating averages. However, 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 .
#                                                                              .
#...............................................................................

#....................SETUP & DATA PROCESSING.....................

# load packages ----
library(tidyverse)

# read in raw data ----
lake_raw <- read_csv(here::here("raw_data", "FCWO_lakemonitoringdata_2011_2022_daily.csv"))

# calculate avg depth & temp ----
avg_depth_temp <- lake_raw |> 
  select(Site, Depth, BedTemperature) |> 
  filter(Depth != "NaN") |>  # remove NaN ("not a number") 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') ---
joined_dfs <- full_join(lake_raw, avg_depth_temp)

# get unique lakes observations (with corresponding lat, lon, elev, avgDepth, avgTemp) for mapping ----
unique_lakes <- joined_dfs |> 
  select(Site, Latitude, Longitude, Elevation, AvgDepth, AvgTemp) |> 
  distinct()

# save processed data to your app's data directory ----
write_csv(x = unique_lakes, file = here::here("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.............................

# load packages ----
library(tidyverse)
library(leaflet)

# read in data ----
lake_data <- read_csv(here::here("shinydashboard", "data", "lake_data_processed.csv"))

#..........................PRACTICE VIZ..........................

leaflet() |> 
  
  # add tiles
  addProviderTiles(providers$Esri.WorldImagery) |> 
  
  # set view over AK
  setView(lng = -152.048442, lat = 70.249234, zoom = 6) |> 
  
  # add mini map
  addMiniMap(toggleDisplay = TRUE, minimized = FALSE) |> 
  
  # add markers
  addMarkers(data = lake_data, 
             lng = lake_data$Longitude, lat = lake_data$Latitude,
             popup = paste0("Site Name: ", lake_data$Site, "<br>",
                           "Elevation: ", lake_data$Elevation, " meters (above SL)", "<br>",
                           "Avg Depth: ", lake_data$AvgDepth, " meters", "<br>",
                           "Avg Lake Bed Temperature: ", lake_data$AvgTemp, "\u00B0C")) # NOTE: Unicode for degree symbol icon

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

# load packages ----
library(tidyverse)
library(leaflet)

# read in data ----
lake_data <- read_csv(here::here("shinydashboard", "data", "lake_data_processed.csv"))

#.......................PRACTICE FILTERING.......................

filtered_lakes <- lake_data |> 
  filter(Elevation >= 8 & Elevation <= 20) |> 
  filter(AvgDepth >= 2 & AvgDepth <= 3) |> 
  filter(AvgTemp >= 4 & AvgTemp <= 6)

#..........................PRACTICE VIZ..........................

leaflet() |> 
  
  # add tiles
  addProviderTiles(providers$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 = paste0("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, "\u00B0C")) # NOTE: Unicode for degree symbol icon

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.

~/shinydashboard/ui.R
#........................dashboardHeader.........................
header <- dashboardHeader(
  
  # 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).

~/shinydashboard/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.

~/shinydashboard/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.

~/shinydashboard/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 fluidRow ----
                   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.

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


Some important notes:

  • Avoid using here::here() in your shiny apps, as it can cause some unexpected issues (read more about it in this discussion)
  • The file path specified for reading in our data is relative to the app files (ui.R, server.R). It’s important to note, however, that you won’t be able to run your code line-by-line (like in a typical script; the file path won’t be recognized) – this is expected!.

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.

~/shinydashboard/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 box ----
                   box(width = NULL,
                       
                       "background info here"

                   ) # END background 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 fluidRow ----
                   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_output") |> 
                    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!

~/shinydashboard/server.R
server <- function(input, output) {
  
  # filter lake data ----
  filtered_lakes_df <- reactive ({
    
    lake_data |>
      filter(Elevation >= input$elevation_slider_input[1] & Elevation <= input$elevation_slider_input[2])
    
  })
  
  
  
  # build leaflet map ----
  output$lake_map_output <- renderLeaflet({
    
    leaflet() |>
      
      # add tiles
      addProviderTiles(providers$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_df(),
                 lng = filtered_lakes_df()$Longitude, lat = filtered_lakes_df()$Latitude,
                 popup = paste("Site Name:", filtered_lakes_df()$Site, "<br>",
                               "Elevation:", filtered_lakes_df()$Elevation, "meters (above SL)", "<br>",
                               "Avg Depth:", filtered_lakes_df()$AvgDepth, "meters", "<br>",
                               "Avg Lake Bed Temperature:", filtered_lakes_df()$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


~/shinydashboard/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 box ----
                   box(width = NULL,
                       
                       "background info here"

                   ) # END background 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 fluidRow ----
                   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_output") |> 
                    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)
~/shinydashboard/server.R
server <- function(input, output) {
  
  # filter lake data ----
  filtered_lakes_df <- 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_output <- renderLeaflet({
    
    leaflet() |>
      
      # add tiles
      addProviderTiles(providers$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_df(),
                 lng = filtered_lakes_df()$Longitude, lat = filtered_lakes_df()$Latitude,
                 popup = paste("Site Name:", filtered_lakes_df()$Site, "<br>",
                               "Elevation:", filtered_lakes_df()$Elevation, "meters (above SL)", "<br>",
                               "Avg Depth:", filtered_lakes_df()$AvgDepth, "meters", "<br>",
                               "Avg Lake Bed Temperature:", filtered_lakes_df()$AvgTemp, "deg Celsius"))
    
  })
  
}

& Exercise 6: Add titles & text to Welcome page boxes


To Do:

Add titles to each box on the Welcome page

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:

~/shinydashboard/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.
~/shinydashboard/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).*
~/shinydashboard/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.

~/shinydashboard/global.R
# 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")
~/shinydashboard/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 box ----
                   box(width = NULL,
                       
                       title = tagList(icon("water"), strong("Monitoring Fish Creek Watershed")),
                       includeMarkdown("text/intro.md")

                   ) # END background 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 fluidRow ----
                   fluidRow(

                     # disclaimer box ----
                     box(width = NULL,

                         title = tagList(icon("triangle-exclamation"), strong("Disclaimer")),
                         includeMarkdown("text/disclaimer.md")

                     ) # END disclaimer box

                   ) # END second fluidRow

            ) # END right-hand column

    ), # END welcome tabItem

    # dashboard tabItem ----
    tabItem(tabName = "dashboard",

            # fluidRow ----
            fluidRow(

              # input box ----
              box(width = 4,
                  
                  title = tags$strong("Adjust lake parameter ranges:"),

                  # sliderInputs ----
                  sliderInput(inputId = "elevation_slider", label = "Elevation (meters above SL):",
                              min = min(lake_data$Elevation), max = max(lake_data$Elevation),
                              value = c(min(lake_data$Elevation), max(lake_data$Elevation))),

                  sliderInput(inputId = "depth_slider", label = "Average depth (meters):",
                              min = min(lake_data$AvgDepth), max = max(lake_data$AvgDepth),
                              value = c(min(lake_data$AvgDepth), max(lake_data$AvgDepth))),

                  sliderInput(inputId = "temp_slider", label = "Average lake bed temperature (degrees C):",
                              min = min(lake_data$AvgTemp), max = max(lake_data$AvgTemp),
                              value = c(min(lake_data$AvgTemp), max(lake_data$AvgTemp)))

              ), # END input box

              # leaflet box ----
              box(width = 8,

                  title = tags$strong("Monitored lakes within Fish Creek Watershed:"),

                  # leaflet output ----
                  leafletOutput(outputId = "lake_map") |> withSpinner(type = 1, color = "#4287f5")

              ) # END leaflet box

            ) # END fluidRow

    ) # END dashboard tabItem

  ) # END tabItems
  
) # END dashboardBody

#..................combine all in dashboardPage..................
dashboardPage(header, sidebar, body)

Add a static image


As a final touch, let’s add an image to the Welcome page, inside the left-hand box beneath our intro text. First, create a www/ folder inside your app’s directory (refer back to lecture 1.2 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 add alt text using the alt argument.

~/shinydashboard/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 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 Alaska, showing Fish Creek Watershed located within the National Petroleum Reserve.") 

                   ) # END background 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 fluidRow ----
                   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)

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,

                   # background 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 Alaska, 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 background 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 fluidRow ----
                   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:


global.R
# 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")
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 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 Alaska, 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 background 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 fluidRow ----
                   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_output") |> 
                    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.R
server <- function(input, output) {
  
  # filter lake data ----
  filtered_lakes_df <- 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_output <- renderLeaflet({
    
    leaflet() |>
      
      # add tiles
      addProviderTiles(providers$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_df(),
                 lng = filtered_lakes_df()$Longitude, lat = filtered_lakes_df()$Latitude,
                 popup = paste("Site Name:", filtered_lakes_df()$Site, "<br>",
                               "Elevation:", filtered_lakes_df()$Elevation, "meters (above SL)", "<br>",
                               "Avg Depth:", filtered_lakes_df()$AvgDepth, "meters", "<br>",
                               "Avg Lake Bed Temperature:", filtered_lakes_df()$AvgTemp, "deg Celsius"))
    
  })
  
}

End part 3

Up next: styling apps

05:00