Shiny App

Shiny

Objectives

  1. Understand how a Shiny apps/web apps function
  2. Prepare data for use in an app
  3. Generate and style a user interface (UI)
  4. Define the server-side functionality needed for the app to function
  5. Develop your first web app by following a step-by-step guide
  6. Be introduced to additional resources

What is Shiny?

This module was created by Davide Zoppolato, a PhD student in the Department of Geology and Geography at West Virginia University.

Shiny is a powerful package for developing interactive web apps with R. Other than R, you do not need any prior knowledge of coding languages to use Shiny. Shiny helps you to quickly visualize your data and analyses with the creation of graphically appealing dashboards. With Shiny, users can interact with, visualize, and explore data and associated analyses.

The web offers a powerful means to share data in an interactive manner. For example, data and/or the results of an analysis could be presented as a dashboard or embedded into an existing webpage. Traditionally, developing such interfaces or tools has required some knowledge of web development (i.e., HTML, CSS, and JavaScript). Further, being able to work with databases or have interactive elements on the page may require some server-side development. As scientists and analysts, using web development and web technologies can present a steep learning curve.

Tools such as Shiny allow for the production of interactive web content directly within R without the need to learn web development technologies and languages. However, customization may require some knowledge of HTML and CSS. Shiny allows for the production of front-facing interfaces that are intuitive to use and interactive and the defining of server-side functionality to support this interface. It allows those familiar with R to take advantage of the web to share, visualize, and even collect data. As a side note, Dash offers similar functionality within Python, and Shiny is now being developed for Python by RStudio.

Setup of Shiny

First, we need to install the Shiny App package. In the latest version of RStudio, the Shiny App package is installed by default.

Do you remember how to install a package?

  1. Run install.packages(‘shiny’) in the console (the console is located at the bottom left in RStudio).
  2. Or, in the file browser tab (bottom right) look for packages, click install, and type “shiny”.

Now, we can create our first Shiny App. Go to File –> New File then click Shiny Web App.

We have two options under Application Type:

1)Single file (app.R) = the app will be developed in a single R document. 2)Multiple file (ui.R/server.R) = the app will be developed in two different R documents; one will be dedicated to the user interface (i.e., the design) and the other to the back-end (i.e., what the user will not see but is required to define the functionality of the app).

For this tutorial, we will use the first option: Single file (app.R). This option is the best solution when you need to develop a simple web app. In the case of more complex web apps, it is better to keep the user interface and server-side components separate.

Name your first app WVU.

Once the package is installed never forget to call the library in the first line of your code. Your app.R file will look like this. Execute the code to see the example app. Note that we have commented out shinyApp(ui = ui, server = server) in all the code blocks in this module in order to be able to render the Markdown file to a webpage. You will need to uncomment these lines in order to view the resulting apps.

#
# This is a Shiny web application. You can run the application by clicking
# the 'Run App' button above.
#
# Find out more about building applications with Shiny here:
#
#    http://shiny.rstudio.com/
#

library(shiny)

# Define UI for application that draws a histogram
ui <- fluidPage(

    # Application title
    titlePanel("Old Faithful Geyser Data"),

    # Sidebar with a slider input for number of bins 
    sidebarLayout(
        sidebarPanel(
            sliderInput("bins",
                        "Number of bins:",
                        min = 1,
                        max = 50,
                        value = 30)
        ),

        # Show a plot of the generated distribution
        mainPanel(
           plotOutput("distPlot")
        )
    )
)

# Define server logic required to draw a histogram
server <- function(input, output) {

    output$distPlot <- renderPlot({
        # generate bins based on input$bins from ui.R
        x    <- faithful[, 2]
        bins <- seq(min(x), max(x), length.out = input$bins + 1)

        # draw the histogram with the specified number of bins
        hist(x, breaks = bins, col = 'darkgray', border = 'white')
    })
}

# Run the application 
#shinyApp(ui = ui, server = server)

Before going deeper into the code, let’s explore the different examples of what we can do with Shiny.

Shiny provides 10 different examples:

  • “01_hello”
  • “02_text”
  • “03_reactivity”
  • “04_mpg”
  • “05_sliders”
  • “06_tabsets”
  • “07_widgets”
  • “08_html”
  • “09_upload”
  • “10_download”
  • “11_timer”

To see the examples, use the function runExample(” “). Try to run the 11 examples provided by Shiny.

#runExample("01_hello")

You can find all the files with instructions by running the following: system.file(“examples”, package=“shiny”).

The West Virginia View App: Data

For this tutorial, we will use data prepared by Prof. Maxwell. You can download the data by clicking the Download Data link on the bottom left of this page. Once the data are downloaded, you need to extract the compressed folder. The folder contains one CSV file (us_county_data.csv), one text file, (us_county_data_description), and one shapefile (counties_with_table). The first step of app creation is to get to know the data. In this tutorial, will use the shapefile (counties_with_table).

Did you know that 80% of a data scientist’s time is dedicated to cleaning and preparing data? Luckily, Dr. Maxwell already created and prepared the data for the tutorial. Let’s have a look at the description of the data in us_county_data_description .

Description: This dataset was created by Prof. Maxwell for use in his courses. A variety of attributes have been summarized at the county-level. A total of 3,104 records are provided representing the majority of the counties in the contiguous United States. A few counties were excluded due to their small size.

The first step is to set the working directory with setwd(). The working directory is the location on your computer that you want to use for the project. We suggest creating a specific folder in the main drive (C:) called “R_course” and then a subfolder dedicated to the project tutorial. The data that you extracted before should be copied into a new subfolder called “data”. It should look something like this: **C:_course*. Remember to use backslashes or double up the forward slashes in the file path.

After setting the working directory, you do not need to use the long path anymore; you only need to add the name of the file with the extension. In our case when we read the shapefile with st_read() from the sf package, we use “counties_with_table.shp”.

Do you know that column names in a shapefiles have a limited length of 10 characters? Oh no! Our columns names are truncated. Inspect the data with head() to make sure you understand the columns names.

In the tutorial, we will use the following data:

  • STATE_A = State abbreviation
  • STATE_N = State name in which county occurs
  • med_inc = Median county household income from United States Census American Community Survey (ACS)
  • tempmn = 30-year normal mean annual temperature averaged over county extents; data from 4 km spatial resolution PRISM data (https://www.prism.oregonstate.edu/normals/); units = Degrees Celsius
  • per_dev = percent of county that is developed; derived from 30 m spatial resolution 2019 National Land Cover Database (https://www.mrlc.gov/)
  • POPULAT= Estimated county population from United States Census American Community Survey (ACS)

We will filter out counties occurring in West Virginia, Virginia, or Maryland using dplyr and the state abbreviations (“WV”, “VA”, “MD”), which are in the “STATE_A” column.

Now that our dataset is filtered, remember to always project your geospatial data to Web Mercator for use in Leaflet. If you do not remember why you need to transform geospatial data, review the Interactive maps with Leaflet module. Now that the data are in R, we are ready to work on the User Interface and the Server functions of Shiny.

library(dplyr)
library(sf)


setwd("D:/mydata/shiny/")
us <- st_read("counties_with_table.shp")

head(us)

us <- st_read("counties_with_table.shp") 

us <- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')

us <- st_transform(us, crs=4326)

User Interface (UI)

The first part of a Shiny App is the user interface (UI). This is the graphical interface of your app that users will interact with. Copy and paste the following code in app.R and then run it.

#
# This is a Shiny web application. You can run the application by clicking
# the 'Run App' button above.
#
# Find out more about building applications with Shiny here:
#
#    http://shiny.rstudio.com/
#

library(shiny)

# Define UI for application that draws a histogram
ui <- fluidPage()

# Define server logic required to draw a histogram
server <- function(input, output) {
  
}

# Run the application 
#shinyApp(ui = ui, server = server)

Oh no! An empty page. Let’s add some content to the page. The UI in Shiny is stored in the ui variable. All the content should be included within fluidPage(). All the content displayed is fluid - meaning it will automatically resize based on the display used, so the developer doesn’t have to specify this behavior manually. To accomplish this, Shiny uses the Bootstrap CSS library in the background.

The app is designed with a sidebar layout where the left side of the page consists of a menu which impacts the content shown to the right.

We will use the following elements to build our interface:

  • fluidPage()

  • titlePanel()

  • sidebarLayout()

  • sidebarPanel()

  • selectInput()

  • mainPanel()

  • plotOutput()

  • fluidRow()

The first step in developing the UI is to decide how our data and visualization will be organized. I suggest to draw on a piece of paper the desired layout of the app and then proceed with its construction step-by-step. For more advanced users, it is possible to prepare mock-ups and proofs of concept with Figma.

The UI will be included in a variable called ui. You can see from the image that the layout is included in a black box. That black box is called a fluidPage in Shiny terminology. All components of the app will be nested within this fluidPage() so we do not need to worry about specifying large, medium, and small layouts for the APP. Shiny will resize the content dynamically.

Let’s now add a title to our APP -West Virginia, Virginia, and Maryland - with titlePanel(). Next, define the tabset (tabsetPanel) and three empty tabs (tabPanel).

You will notice that by default the sidebarPanel() includes options associated with selectInput() that impact the content rendered in mainPanel().

library(shiny)

# Define UI 
ui <- fluidPage(
  
  
  titlePanel("West Virginia, Virginia and Maryland"),

  tabsetPanel(
    
    tabPanel("Data explorer",
             fluidRow('',choices = '',''),
             hr(),
             DT::dataTableOutput("table")
    ),
    
    tabPanel("Sum plots",sidebarLayout(    
      sidebarPanel(),
      mainPanel(
        plotOutput("boxplot")))),
    
    tabPanel("Interactive Map",  sidebarLayout(    
      sidebarPanel(
        selectInput('',choices = '',''),
        selectInput('',choices = '', '')
      , 
      ),
      mainPanel(
         plotOutput("map"))))  
  ),)

server <- function(input, output) {
  
}

# Run the application 
#shinyApp(ui = ui, server = server)

SelectInput()

Now that we have the structure of the app, we need to allow for updating relative to user specifications using selectInput(). A list of the available control widgets and the associated code to add to your app can be found here.

The user will have the choice to select one of the three states in the Data Explorer tab. In the Sum Plot tab, we compare the three States graphically with ggplot2. The last tab, Interactive Map, will have two sets of inputs: one allows the user to select one of the three states and the second allows for selecting a variable: “Temperature”, “Income”, or “Population”. To define this functionality, we need to use selectInput().

The function selectInput() accepts the following arguments:

  • inputId = This is the point of connection between the UI and Server. Be concise with the name and never use the same name in different tabs. In our case, we opt for countryInput1 for the Data Explorer, inc_pop_temp for the plots, and countryInput for the map.

  • label = The label helps the user understand what to select among the possible choices.

  • choices = When we want to use different words as choices, we must use the c() function to create a vector (i.e., choices = c(‘West Virginia’, ‘Virginia’, ‘Maryland’)). In this case, the inputID will show the first value of the vector as the selected value in the drop down menu. To avoid this issue, we can add an empty value at the beginning of the vector: choices = c(’‘,’West Virginia’, ‘Virginia’, ‘Maryland’).

  • selected = Selected relates to the choice that we want to be selected in the input. The default is NULL.

  • multiple = With multiple you can allow the user to select multiple inputs. The default is FALSE.

When non-numeric, arguments should always be inserted between quotation marks. See the example below:

selectInput(inputId = ‘inc_pop_temp’, label = ‘Select a variable’, choices = c(’‘,“Income”, “Population vs Development”, ’Median Temperature’)

R scripts could be included to simplify the code in selectInput. One R function that we find pretty handy is unique(). Unique() allows for removing all the duplicate values and return a vector with only the unique value of a row. In the app, we use unique(us$STATE_N) to populate the choice argument. “STATE_N” is the column containing all the state names.

mainPanel and Output

The mainPanel(), which currently consists of white space on the right side of the layout, is where the output of Shiny is plotted. We will use the following three output functions in our app. We need to assign an ID to the output that will be the second element to connect the UI with the server function. To sum up, we have two elements that help us to connect the UI and Server: the inputID (e.g. West Virginia) and the outputID (e.g. the map of West Virginia).

  1. dataTableOutput(“table”) = The output is a table. We use this to render the attribute table of the counties in the Data Explorer tab.
  2. plotOutput(“boxplot”) = The output in this case is a visualization. We use this in the Sum Plot tab to render ggplot2 graphs.
  3. leafletOutput(“map”) = The output is an interactive map made with the support of Leaflet.

Other useful Output functions are:

  • textOutput() = renders text
  • imageOutput() = renders images
  • htmlOutput() = renders HTML code
library(sf)
library(dplyr)
library(leaflet)
library(shiny)


setwd("D:/mydata/shiny/")
us <- st_read("counties_with_table.shp")
Reading layer `counties_with_table' from data source 
  `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
Simple feature collection with 3104 features and 37 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Geodetic CRS:  WGS 84
us <- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')
us <- st_transform(us, crs=4326)

# Define UI 
ui <- fluidPage(
  titlePanel("West Virginia, Virginia and Maryland"),
  tabsetPanel(
    tabPanel("Data explorer",
             fluidRow(
               column(3,
                      selectInput("countryInput1", "Choose a State",
                                  choices = unique(us$STATE_N),
                                  selected = "West Virginia"), 
               ), column(9,),
             ),
            
             dataTableOutput("table")
    ),
  
    tabPanel("Sum plots",sidebarLayout(    
      sidebarPanel(
        
        selectInput(inputId = 'inc_pop_temp',
                    label = 'Select a variable',
                    choices = c('',"Income", "Population vs Development", 'Median Temperature'), ),
      ),
      mainPanel(
        plotOutput("boxplot")))),

    
    tabPanel("Interactive Map",  sidebarLayout(    
      sidebarPanel(
        selectInput("countryInput", "Choose a State",
                    choices = unique(us$STATE_N),
                    selected = "West Virginia"),
        selectInput(inputId = "varInput", "Choose a variable",
                    choices = c('',"Temperature", 'Population', 'Income'))
      ,),
      mainPanel(
        leafletOutput("map"))))  
  ),)


server <- function(input, output) {
  
}

# Run the application 
#shinyApp(ui = ui, server = server)

HTML, CSS, STYLE

Do you know HTML, CSS, and/or Bootstrap? If not, you may want to check out the WV View Client-Side Web GIS course. You can customize almost every aspect of your app. Have a look at this comprehensive guide: Outstanding User Interfaces with Shiny for info on how to improve the design of your Shiny app. If you don’t want to bother learning new coding languages, you can still improve your app design with the ShinyWidgets library.

Keep in mind that Shiny also supports HTML attributes, so you can quickly add margins; align the text; and change font weight, size, and face. Below is a list of some useful tags in Shiny.

  • h1(), h2(), h3(), h4(), h5(), h6() = the different levels of the headings in your app. h1 is the highest and H6 is the lowest. Never skip heading levels (i.e., after using h1, your second subheading should be h2 as opposed to h6).

  • p() = stands for paragraph

  • em () = stands for emphasized and is used to display the text in italics

  • strong() = makes the text bold

  • br() = br stands for break line, and it adds a horizontal blank line

*img(src = “image path”) = img stands for images, and this tag is used to include an image in Shiny. For src, you should add the source of the image and remember that R is confused by single / . Always add a double slash to avoid issues in reading the path // or convert forward slashes to backslashes.

Additional attributes can be specified within the brackets. Here are some examples.

  • h1(‘Title’, align = ‘center/left/right’) = For align we have three options (“center”, “left”, and “right”). Example: h1(“West Virginia, Virginia and Maryland”, align = “center”)

  • img(src = “image path”, height = ‘75%’, width = ‘100%’) = You can specify the height and the width of content, such as an image. There are different methods available to specify the height and width of the content; you can use % (must be included in quotation marks) or number of pixels (just indicate the number) or a combination of both (for example, you can specify only the % of the width and indicate the height in pixels). In our app we use this last option to add the West Virginia View logo: img(src = “http://www.wvview.org//images//header.png”, height = 110, width = ‘100%’)

*h1(‘Title’, style=“color:red;font-family:‘times’; font-size: 70”) = In the style we can specify several attributes. Careful, here the syntax follows CSS as opposed to HTML (hint: to declare a variable we use : and not the equals sign; to add other attributes we use ; as opposed to a comma. As we did in our app, the style attribute should be used only for minor changes, such as to change the title color: h1(“West Virginia, Virginia and Maryland”,align = “center”, style = “color:#0d793e”). It is better to write a separate CSS file if you want to style the app extensively. For more info on CSS and HTML, check out the West Virginia View Client-Side Web GIS course.

Now, try to run the code below in the APP file and see our updated User Interface.

library(sf)
library(leaflet)
library(leaflet.extras)
library(leaflet.esri)
library(dplyr)
library(RColorBrewer)
library(htmlwidgets)
library(htmltools)
library(ggplot2)
library(plotly)
library(bslib)

# Define UI 
ui <- fluidPage(
  
  titlePanel(h1("West Virginia, Virginia and Maryland",align = "center", style = "color:#0d793e")),
  br(),
 
  br(),
  tabsetPanel(
    
    #data exploration
    
    tabPanel("Data explorer", br(), 
             fluidRow(
               column(3,
                      selectInput("countryInput1", "Choose a State",
                                  choices = unique(us$STATE_N),
                                  selected = "West Virginia"), 
               ), column(9, img(src = "http://www.wvview.org//images//header.png",  position= 'right', height = 140, width = '100%' ),),
             ),
             hr(),
             DT::dataTableOutput("table")
    ),
    
    tabPanel("Sum plots",br(), sidebarLayout(    
      sidebarPanel(
        
        selectInput(inputId = 'inc_pop_temp',
                    label = 'Select a variable',
                    choices = c('',"Income", "Population vs Development", 'Median Temperature'), ),
        img(src = "http://www.wvview.org//images//header.png", height = 110, width = '100%' ),
      ),
      mainPanel(
        plotOutput("boxplot")))),
    
    tabPanel("Interactive Map",  sidebarLayout(    
      sidebarPanel(
        selectInput("countryInput", "Choose a State",
                    choices = unique(us$STATE_N),
                    selected = "West Virginia"),
        selectInput(inputId = "varInput", "Choose a variable",
                    choices = c('',"Temperature", 'Population', 'Income'))
      , 
      img(src = "http://www.wvview.org//images//header.png", height = 110, width = '100%' ),),
      mainPanel(
        leafletOutput("map",  width = "100%", height = "500px"))))  
  ),)

server <- function(input, output) {
  
}

# Run the application 
#shinyApp(ui = ui, server = server)

Bootstrap themes for Shiny

Our UI still looks pretty basic; however, we do not have time to write a separate CSS file to style it. Fortunately, the bslib package offers a simple solution. Let’s install the package install.packages(“bslib”) and add library(bslib) at the top of the app file. Now, we can opt for one of the fully customizable themes available here. You should include, immediately after the fluidPage, the following code to apply the theme to your APP: theme = bs_theme(version = 4, bootswatch = “spacelab”). Now, change the theme of your app to one of the followings:

“cerulean”, “cosmo”, “cyborg”, “darkly”, “flatly”, “journal”, “litera”, “lumen”, “lux”, “materia”, “minty”, “pulse”, “sandstone”, “simplex”, “sketchy”, “slate”, “solar”, “spacelab”, “superhero”, “united”, or “yeti”

Our final interface looks great. Now, we can move on to the preparation of data followed by the more tricky Server function.

library(sf)
library(leaflet)
library(leaflet.extras)
library(leaflet.esri)
library(dplyr)
library(RColorBrewer)
library(htmlwidgets)
library(htmltools)
library(ggplot2)
library(plotly)
library(bslib)


setwd("D:/mydata/shiny/")
us <- st_read("counties_with_table.shp")
Reading layer `counties_with_table' from data source 
  `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
Simple feature collection with 3104 features and 37 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Geodetic CRS:  WGS 84

head(us)
Simple feature collection with 6 features and 37 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -88.02913 ymin: 30.22093 xmax: -85.04883 ymax: 34.26049
Geodetic CRS:  WGS 84
  FIPS           NAME STATE_N STATE_A STATE_F COUNTY_ POPULAT POP_SQM    SQMI
1 1001 Autauga County Alabama      AL       1       1   58805    97.3  604.37
2 1003 Baldwin County Alabama      AL       1       3  231767   141.9 1633.14
3 1005 Barbour County Alabama      AL       1       5   25223    27.9  904.52
4 1007    Bibb County Alabama      AL       1       7   22293    35.6  626.17
5 1009  Blount County Alabama      AL       1       9   59134    90.9  650.63
6 1011 Bullock County Alabama      AL       1      11   10357    16.6  625.14
  SUB_REG med_ncm hoshlds  pr_dsk_  pr_smrt   pr_n_cm  pr_ntrn  pr_brdb
1 E S Cen   57982   21559 74.13609 81.00561  8.571826 82.79605 82.70792
2 E S Cen   61756   84047 77.73865 84.63598  8.239437 85.52358 85.06907
3 E S Cen   34990    9322 52.45655 68.69770 20.220983 64.99678 64.63205
4 E S Cen   51721    7259 56.17854 73.61896 16.779171 76.16752 76.12619
5 E S Cen   48922   21205 66.86631 76.43952 14.963452 80.03301 79.62273
6 E S Cen   33866    3429 55.23476 69.26218 20.326626 62.78798 60.62992
   pr_n_nt       dem   precip   tempmn  tempmin  tempmax    dptmn    vpdmin
1 15.33930 105.61176 1412.167 17.95529 11.75894 24.15424 12.50141 0.7703529
2 11.91952  31.40693 1712.929 19.31788 13.76320 24.87489 14.37255 0.6897403
3 28.96374 115.25758 1400.435 18.39159 11.94106 24.84402 12.73129 0.8700758
4 20.93952 125.28409 1422.227 17.46602 11.06034 23.87443 12.08955 0.7509091
5 18.48621 246.04255 1492.224 16.07128 10.05840 22.08702 11.05468 0.5855319
6 30.32954 125.88889 1397.928 18.02481 11.58802 24.46444 12.43086 0.8122222
    vpdmax      sol  per_for  per_dev    per_wet    per_crp  pr_pst_   strm_ln
1 16.62553 15.63682 49.71597 6.815302 10.8697204 3.14176012 20.79571 49.599790
2 15.45636 16.28429 33.13072 9.449728 30.6357571 9.19548790 10.80689 74.558900
3 18.27205 15.98689 56.58767 4.227699  8.8715967 4.53634268 12.82374 57.307449
4 16.81307 15.46614 70.28501 5.258896  6.5578020 0.06282062 10.07816 45.351825
5 14.45106 15.07543 54.95027 8.577787  0.6341722 1.18411362 30.43296  4.118788
6 17.46272 15.92222 54.64416 2.983319 13.0312271 2.03029122 17.64950 52.344239
      strm_dn     pr_krst    rail_dn  road_dn                       geometry
1 0.031686857  0.00000000 0.09007046 1.890543 MULTIPOLYGON (((-86.41312 3...
2 0.017626996  0.00000000 0.02088778 1.784091 MULTIPOLYGON (((-87.56491 3...
3 0.024462179 14.91452991 0.04713053 1.090589 MULTIPOLYGON (((-85.25784 3...
4 0.027964351 11.05620754 0.09748651 1.792521 MULTIPOLYGON (((-87.06574 3...
5 0.002444205 18.19798459 0.03741401 1.935772 MULTIPOLYGON (((-86.45302 3...
6 0.032329116  0.06184292 0.03130953 1.245289 MULTIPOLYGON (((-85.89766 3...

us <- st_read("counties_with_table.shp") 
Reading layer `counties_with_table' from data source 
  `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
Simple feature collection with 3104 features and 37 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Geodetic CRS:  WGS 84

us <- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')

us <- st_transform(us, crs=4326)


# Define UI 
ui <- fluidPage(
  theme = bs_theme(version = 4, bootswatch = "spacelab"),
  
  titlePanel(h1("West Virginia, Virginia and Maryland",align = "center", style = "color:#0d793e")),
  br(),
 
  br(),
  tabsetPanel(
    
    tabPanel("Data explorer", br(), 
             fluidRow(
               column(3,
                      selectInput("countryInput1", "Choose a State",
                                  choices = unique(us$STATE_N),
                                  selected = "West Virginia"), 
               ), column(9, img(src = "http://www.wvview.org//images//header.png",  position= 'right', height = 140, width = '100%' ),),
             ),
             hr(),
             DT::dataTableOutput("table")
    ),
  
    tabPanel("Sum plots",br(), sidebarLayout(    
      sidebarPanel(
        
        selectInput(inputId = 'inc_pop_temp',
                    label = 'Select a variable',
                    choices = c('',"Income", "Population vs Development", 'Median Temperature'), ),
        img(src = "http://www.wvview.org//images//header.png", height = 110, width = '100%' ),
      ),
      mainPanel(
        plotOutput("boxplot")))),
   
    tabPanel("Interactive Map",  sidebarLayout(    
      sidebarPanel(
        selectInput("countryInput", "Choose a State",
                    choices = unique(us$STATE_N),
                    selected = "West Virginia"),
        selectInput(inputId = "varInput", "Choose a variable",
                    choices = c('',"Temperature", 'Population', 'Income'))
      , 
      img(src = "http://www.wvview.org//images//header.png", height = 110, width = '100%' ),),
      mainPanel(
        leafletOutput("map",  width = "100%", height = "500px"))))  
  ),)

server <- function(input, output) {
  
}

# Run the application 
#shinyApp(ui = ui, server = server)

Server

The Server variable from Shiny is what makes the app interactive. It is a function that takes as input what the user selects in the UI in order to provide an output. If you don’t remember how functions work in R, review the R Language Part II module.

Let’s start from the two points of connection between the Server and UI: the input of the user and the output ID. inputId is the unique ID that we assign to the input provided by the user in the UI, and you can find it in the selectInput() function. The output ID is the unique ID that we assign to the output, and it is included where you see ….Output().

In the APP we assign the following:

  • Tab 1 Data Explorer

inputID = ‘countryInput1’ outputID = ‘table’

  • Tab 2 Sum Plots

inputID = ‘inc_pop_temp’ outputID = ‘boxplot’

  • Tab 3 Interactive Map

inputID = ‘countryInput’ outputID = ‘map’

Tab 1: Data Exploration

We will start with the easiest tab to build: the Data Explorer. We used countryInput1 to allow the user to select among the three States: West Virginia, Virginia, and Maryland. The output of the Data Explorer is a data table.

First, we need to define the reactivity of the input. Every time that the input changes, Shiny will change the output accordingly. This is a major strength of Shiny and other declarative programming languages. Instead of defining in the code what the app must do, you define an abstract pattern. Shiny then interprets the pattern in order to provide a command to the computer. Shiny will not re-run all the code; instead, it runs only the lines needed to update the output, saving time and computing power. This feature is called laziness. This is similar to what we did when designing the UI; instead of writing a complex CSS file, we copied and pasted a single line of code from Google. Minimum effort, maximum result.

To define the input as reactive we need to connect the input provided by the user with our data. In the case of the Data Explorer, we need to connect the choice of the state with the state name (“STATE_N”) in the data. We can do this by simply accessing the column of the dataset us and equate it with the input of the user. Now, we need to store the expression in the reactive function.

Wait…We never define a variable called input, what does input$ mean?

With input$, Shiny understands that every time the user inputs something, it must wake up. In our case, Shiny will filter the data based on the state name selected by the user.

The output section works in a similar manner. Shiny, using output$, will access the outputID ‘table’ when the input is changed. In the Data Explorer we only need to call the DT::renderDataTable function and specify that the data to be used to render the table is selectedCountry1. Do you remember how to collect the inputs provided by the user in R? It is simple: just add () after the variable name!

Now we have all the elements to render our table data and to connect the input of the user to the UI. Let’s run the code below.

library(sf)
library(leaflet)
library(leaflet.extras)
library(leaflet.esri)
library(dplyr)
library(RColorBrewer)
library(htmlwidgets)
library(htmltools)
library(ggplot2)
library(plotly)
library(bslib)


setwd("D:/mydata/shiny/")
us <- st_read("counties_with_table.shp")
Reading layer `counties_with_table' from data source 
  `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
Simple feature collection with 3104 features and 37 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Geodetic CRS:  WGS 84

us <- st_read("counties_with_table.shp") 
Reading layer `counties_with_table' from data source 
  `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
Simple feature collection with 3104 features and 37 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Geodetic CRS:  WGS 84

us <- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')

us <- st_transform(us, crs=4326)


# Define UI 
ui <- fluidPage(
  theme = bs_theme(version = 4, bootswatch = "spacelab"),
  
  titlePanel(h1("West Virginia, Virginia and Maryland",align = "center", style = "color:#0d793e")),
  br(),
 
  br(),
  tabsetPanel(
    
    #data exploration
    
    tabPanel("Data explorer", br(), 
             fluidRow(
               column(3,
                      selectInput("countryInput1", "Choose a State",
                                  choices = unique(us$STATE_N),
                                  selected = "West Virginia"), 
               ), column(9, img(src = "http://www.wvview.org//images//header.png",  position= 'right', height = 140, width = '100%' ),),
             ),
             hr(),
             DT::dataTableOutput("table")
    ),
 
  ),)




server <- function(input, output) {
  
  selectedCountry1 <- reactive({
  us[us$STATE_N == input$countryInput1, ] 
})

  output$table <- DT::renderDataTable({ data = selectedCountry1()

  })
  
}

# Run the application 
#shinyApp(ui = ui, server = server)

Tab 2: Visualization and Shiny

In the second tab we integrate ggplot2 with Shiny. We will generate a plot comparing the income, population, and temperature of the three states.

Do you remember how to use ggplot2? If not, do not worry; you can review the modules associated with ggplot2.

We save the three plots in three variables called inc_p, temp_p, and pop_p, which are defined at the top of our Shiny script (before the UI and server). In this way the visualization will be created by Shiny before rendering the app, thus saving processing time and speeding up the rendering of the graphs.

Let’s now connect the input and output of the UI to the server. The input ID for the second tab is inc_pop_temp, which is the abbreviation of our three variables: income, population, and temperature. We named the output in the function of the UI plotOutput(” boxplot ”). First, we need to define the output of the boxplot output$boxplot. As explained above, Shiny is helping us by providing standardized functions to visualize the different output types (render + the type of the output). Before we used renderDataTable, now we will use the general function renderPlot(). renderPlot() can be used any time that we need to transform an expression to a graph or other type of visualization. We have three choices -“Income”, “Population vs Development”, or ‘Median Temperature’- that the user can select in the UI to plot three different graphs. Let’s use an If Statement to define how the graph to render is selected. This is not the most concise manner to render the plots but we want to show you how to use if statements within a Shiny renderPlot(). When writing if statements and loops, it is a best practice to begin with a general description of what we want to accomplish:

If the user select ‘something’ in the check box, Shiny must render ‘something’.

Thus, the input of the user, input$, in the Input ID of the Tab, inc_pop_temp, will be matched (to match we can use %in% ) to the text of the choice associated with ‘Income’. Then, we only need to include the variable that stores the graph for income _inc_p__. We will do the same for the remainder of the statement by using else if. Easy, right?


library(sf)
library(leaflet)
library(leaflet.extras)
library(leaflet.esri)
library(dplyr)
library(RColorBrewer)
library(htmlwidgets)
library(htmltools)
library(ggplot2)
library(plotly)
library(bslib)


setwd("D:/mydata/shiny/")

us <- st_read("counties_with_table.shp") 
Reading layer `counties_with_table' from data source 
  `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
Simple feature collection with 3104 features and 37 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Geodetic CRS:  WGS 84

us <- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')

us <- st_transform(us, crs=4326)

#ggplot graphs

inc_p <- ggplot(us, aes(x=STATE_A, y=med_ncm, fill=STATE_A)) +
  geom_boxplot() #Income

temp_p <- ggplot(us, aes(x=tempmn, ..density.., fill=STATE_A))+
  geom_density(alpha=0.5) #Temperature

pop_p <- ggplot(us, aes(x=POPULAT, y=per_dev, color= STATE_A, size= med_ncm))+
  geom_point() #Population vs Development



# Define UI 
ui <- fluidPage(
  theme = bs_theme(version = 4, bootswatch = "spacelab"),
  
  titlePanel(h1("West Virginia, Virginia and Maryland",align = "center", style = "color:#0d793e")),
  br(),
 
  br(),
  tabsetPanel(
    
    
    #Data Visualization 
    
    tabPanel("Sum plots",br(), sidebarLayout(    
      sidebarPanel(
        
        selectInput(inputId = 'inc_pop_temp',
                    label = 'Select a variable',
                    choices = c('',"Income", "Population vs Development", 'Median Temperature'), ),
        img(src = "http://www.wvview.org//images//header.png", height = 110, width = '100%' ),
      ),
      mainPanel(
        plotOutput("boxplot")))),
    
       
    
  ),)




server <- function(input, output) {
  

  output$boxplot <- renderPlot({
    if (input$inc_pop_temp %in% "Income") {inc_p} else if (input$inc_pop_temp %in% "Median Temperature") {
      temp_p
    } 
    
    else if (input$inc_pop_temp %in% "Population vs Development") {
      pop_p
    } 

  })
  
}

# Run the application 
#shinyApp(ui = ui, server = server)

Tab 3: Mapping with Leaflet and Shiny

Leaflet is a JavaScript library that allows us to build beautiful and interactive maps. Please review the Leaflet module is you need a refresher.

in the final tab, we will render interactive maps. We already declared the input and output in the UI:

inputID = ‘countryInput’ outputID = ‘map’

The user will have the following choices:

States = “West Virginia”, “Virginia”, or “Maryland” Variable = “Income”, “Population”, or “Temperature”

The reactive element allows the user to select among the states and will be the same as the one that we used for the Data Exploration Tab. Here, you must be careful. We cannot give the reactive the same name as used before. So, let’s use selectedCountry1. If you use the same name, the input for selecting the country in the map will not work. Why? Because you told Shiny to use the input of the Data Explorer. Shiny is lazy, it does not re-run the server function when you open the third tab. Instead, it uses as input the content of the first tab. If the names are the same, to change the state in the third tab you would need to go to the Data Explorer and change it there. With this caveat in mind, we can now move to the second option: “Income”, “Population”, or “Temperature”. In this case, we do not need to create an additional reactive element; all the information is stored in the columns of the dataset, so we can easily access them with the base R $ operator. What we still need to do is include in the top of the script - before the UI - the color palettes for representing the three variables (Remember to use the specific column of interest as the domain).

We can now move on to the output that will be included in the variable output$map. We use the renderLeaflet function to create a basic map. In this way, we will not ask Shiny to render the map every time the map is updated. See below the base maps that we will include in Shiny.

library(sf)
library(leaflet)
library(leaflet.extras)
library(leaflet.esri)
library(dplyr)
library(RColorBrewer)
library(htmlwidgets)
library(htmltools)
library(ggplot2)
library(plotly)
library(bslib)

setwd("D:/mydata/shiny/")

us <- read.csv("us_county_data.csv")


us <- st_read("counties_with_table.shp") %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')
Reading layer `counties_with_table' from data source 
  `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
Simple feature collection with 3104 features and 37 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Geodetic CRS:  WGS 84


us <- st_transform(us, crs=4326)

temp_pal <- colorBin(palette="YlOrRd", domain=us$tempmn, bin=5)
pop_pal <- colorNumeric(palette= 'Purples', domain= us$POPULAT)
ncm_pal <- colorNumeric(palette= 'Greens', domain= us$med_ncm)



leaflet('map', options = leafletOptions(zoomControl= FALSE)) %>%
    
    htmlwidgets::onRender("function(el, x) {
      L.control.zoom({ position: 'topright' }).addTo(this)
  }") %>%
    addTiles(group = "OSM") %>%
    
    addProviderTiles("Esri.NatGeoWorldMap", group="ESRI") %>%
    
    addProviderTiles("CartoDB.DarkMatter", group= "CartoDB") %>%
    addLayersControl(baseGroups = c("CartoDB","OSM", "ESRI")) %>%
    
    addLegend(position="bottomright", pal=temp_pal, values=us$tempmn, title="Temperature")%>%
    addLegend(position="bottomright", pal=pop_pal, values=us$POPULAT, title="Population")%>%
    addLegend(position="bottomleft", pal=ncm_pal, values=us$med_ncm, title="Income in $")%>%
    setView(lat= 39, lng=-80, zoom=6)

We have the base map but we need to add content to the map based on the user’s choices. To do so, we can use observe(). This function makes reactive the changes to the base map made by the user. What’s the difference between observe() and reactive()? Observe() is not lazy, meaning it will run the entire code and not the bare minimum in what is referred to as the observed environment. For understanding the concept of observed environment, think about our case. We have a base map but we want to add a layer showing income, population, or temperature to the map. The observed environment is only that section of the code that adds the layer and nothing else.

Now, we have all elements to build the Interactive Map. Run the final code and test the app. We include comments in the script below to help you to understand the different functions.



#import libraries used in the APP
library(shiny)
library(sf)
library(leaflet)
library(dplyr)
library(RColorBrewer)
library(htmlwidgets)
library(htmltools)
library(ggplot2)
library(bslib)

#set the working directory

setwd("D:/mydata/shiny/")


#save the shapefile with the attribute table in the variable us

us <- st_read("counties_with_table.shp") 
Reading layer `counties_with_table' from data source 
  `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
Simple feature collection with 3104 features and 37 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Geodetic CRS:  WGS 84

#filter the data set by the three countries selected in the APP

us <- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')


#transform the geometry to Web Mercator

us <- st_transform(us, crs=4326)

# fist graph on ggplot = Income

inc_p <- ggplot(us, aes(x=STATE_A, y=med_ncm, fill=STATE_A)) +
  geom_boxplot() 

# second graph on ggplot = Temperature

temp_p <- ggplot(us, aes(x=tempmn, ..density.., fill=STATE_A))+
  geom_density(alpha=0.5)

# third graph on ggplot = Population vs Development

pop_p <- ggplot(us, aes(x=POPULAT, y=per_dev, color= STATE_A, size= med_ncm))+
  geom_point()

#color palette for Temperature Income and Population

temp_pal <- colorBin(palette="YlOrRd", domain=us$tempmn, bin=5)
pop_pal <- colorNumeric(palette= 'Purples', domain= us$POPULAT)
ncm_pal <- colorNumeric(palette= 'Greens', domain= us$med_ncm)


# Define UI for the APP

ui <- fluidPage(
  theme = bs_theme(version = 4, bootswatch = "spacelab"), # theme from the library(bslib)
  
  titlePanel(h1("West Virginia, Virginia and Maryland",align = "center", style = "color:#0d793e")), # Main title of the App use h1
  br(),
  br(),
  tabsetPanel(#the tabset panel layout will include the three tab
    
    
    # Tab 1: Data Exploration
    
    tabPanel("Data explorer", # title of the first tab
             br(), 
             fluidRow(
               column(3,
                      selectInput(inputId = "countryInput1", #unique input ID
                                  label= "Choose a State",
                                  choices = unique(us$STATE_N),
                                  selected = "West Virginia"), 
               ), column(9, 
                         img(src = "http://www.wvview.org//images//header.png",  position= 'right', height = 140, width = '100%' ) #logo of WVU
                         ,),
             ),
             hr(),
             DT::dataTableOutput("table") #output ID
    ),
    
    #Tab 2: Visualization 
    
    tabPanel("Sum plots", #title of the second tab
             br(), 
             sidebarLayout(#left section of the page used by the user to select input    
      sidebarPanel(
        
        selectInput(inputId = 'inc_pop_temp',#unique input ID
                    label = 'Select a variable',
                    choices = c('',"Income", "Population vs Development", 'Median Temperature'), ),
        img(src = "http://www.wvview.org//images//header.png", height = 110, width = '100%' ),
      ),
      mainPanel( # right section of the page 
        plotOutput("boxplot")#output ID
        ))),
     
    #Tab 3: Interactive Map
    
    tabPanel("Interactive Map", #title of the third tab  
             sidebarLayout(    
      sidebarPanel(#left section of the page used by the user to select input
        selectInput(inputId = "countryInput", #unique input ID
                    label= "Choose a State",
                    choices = unique(us$STATE_N),
                    selected = "West Virginia"),
        selectInput(inputId = "varInput", #unique input ID
                    label= "Choose a variable",
                    choices = c('',"Temperature", 'Population', 'Income'))
      , 
      img(src = "http://www.wvview.org//images//header.png", height = 110, width = '100%' ),),
      mainPanel(# right section of the page 
        leafletOutput("map", #output ID  
                      width = "100%", height = "500px"))))  
  ),)

# Define server logic 

server <- function(input, output) {
  
  selectedCountry1 <- reactive({#reactive expression for the Data Explorer
    us[us$STATE_N == input$countryInput1, ] #match input of the user with the state name
  })
  
   selectedCountry <- reactive({ #reactive expression for the Interactive Map
    us[us$STATE_N == input$countryInput, ] #match input of the user with the state name
  })
 
  output$table <- DT::renderDataTable({ #Data Explorer tab output
    
    data = selectedCountry1()
  
  })
  
  output$boxplot <- renderPlot({ #Visualization tab output
    if (input$inc_pop_temp %in% "Income") # If input of the user is Income
      {inc_p} #then print Income graph
    else if (input$inc_pop_temp %in% "Median Temperature")# If input of the user is Median Temperature
      {temp_p} #then print Temperature graph
    
    else if (input$inc_pop_temp %in% "Population vs Development")# If input of the user is Pop vs Dev
      {pop_p} #then print Pop vs Dev graph

  })
 
output$map <- renderLeaflet({#Interactive Map tab output
  leaflet('map', #base map
          options = leafletOptions(zoomControl= FALSE)) %>%
    
    htmlwidgets::onRender("function(el, x) {
      L.control.zoom({ position: 'topright' }).addTo(this)
  }") %>%
    addTiles(group = "OSM") %>%
    
    addProviderTiles("Esri.NatGeoWorldMap", group="ESRI") %>%
    
    addProviderTiles("CartoDB.DarkMatter", group= "CartoDB") %>%
    addLayersControl(baseGroups = c("CartoDB","OSM", "ESRI")) %>%
    
    addLegend(position="bottomright", pal=temp_pal, values=us$tempmn, title="Temperature")%>%
    addLegend(position="bottomright", pal=pop_pal, values=us$POPULAT, title="Population")%>%
    addLegend(position="bottomleft", pal=ncm_pal, values=us$med_ncm, title="Income in $")%>%
    setView(lat= 39, lng=-80, zoom=6)
})

observe({#observer
  
    state_popup <- paste0("<strong>County: </strong>", #popup 
                          selectedCountry()$NAME,
                          "<br><strong> Temperature: </strong>",
                          round(selectedCountry()$tempmn,1),"&#x2103",
                          "<br><strong> Median Income: </strong>",
                          selectedCountry()$med_ncm,'$',
                          "<br><strong> Population: </strong>",
                          selectedCountry()$POPULAT)
    
    high_opt <- highlightOptions(weight = 3, color = "white", bringToFront = FALSE) #highlight when user select county

  if (input$varInput %in% "Temperature") {
    leafletProxy("map", data = selectedCountry()) %>%
      #understand when to clearshapes
      # clearShapes() %>%
      addPolygons(fillColor =  temp_pal(selectedCountry()$tempmn),
                  popup = state_popup,
                  col="#302E2D",
                  fillOpacity = 1,
                  weight = 1,
                  highlight = high_opt )
  }
   else if (input$varInput %in% "Income") {
      leafletProxy("map", data = selectedCountry()) %>%
        #understand when to clearshapes
        # clearShapes() %>%
       
        addPolygons(fillColor =  ncm_pal(selectedCountry()$med_ncm),
                    popup = state_popup,
                    col="#302E2D",
                    fillOpacity = 1,
                    weight = 1,
                    highlight = high_opt )
     
    }
    else if (input$varInput %in% "Population") {
      leafletProxy("map", data = selectedCountry()) %>%
        #understand when to clearshapes
        # clearShapes() %>%
        addPolygons(fillColor =  pop_pal(selectedCountry()$POPULAT),
                    popup = state_popup,
                    col="#302E2D",
                    fillOpacity = 1,
                    weight = 1,
                    highlight = high_opt )
    }
    
 
})

}

# Run the application 
#shinyApp(ui = ui, server = server)

Additional Resources

In this tutorial we only cover the basic functions of Shiny. Shiny is rapidly becoming a go-to software within the data analytics industry. Below you can find a list of additional resources: