Shiny App
Shiny
Objectives
- Understand how a Shiny apps/web apps function
- Prepare data for use in an app
- Generate and style a user interface (UI)
- Define the server-side functionality needed for the app to function
- Develop your first web app by following a step-by-step guide
- 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?
- Run install.packages(‘shiny’) in the console (the console is located at the bottom left in RStudio).
- 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
<- fluidPage(
ui
# 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
<- function(input, output) {
server
$distPlot <- renderPlot({
output# generate bins based on input$bins from ui.R
<- faithful[, 2]
x <- seq(min(x), max(x), length.out = input$bins + 1)
bins
# 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/")
<- st_read("counties_with_table.shp")
us
head(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) us
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
<- fluidPage()
ui
# Define server logic required to draw a histogram
<- function(input, output) {
server
}
# 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
<- fluidPage(
ui
titlePanel("West Virginia, Virginia and Maryland"),
tabsetPanel(
tabPanel("Data explorer",
fluidRow('',choices = '',''),
hr(),
::dataTableOutput("table")
DT
),
tabPanel("Sum plots",sidebarLayout(
sidebarPanel(),
mainPanel(
plotOutput("boxplot")))),
tabPanel("Interactive Map", sidebarLayout(
sidebarPanel(
selectInput('',choices = '',''),
selectInput('',choices = '', '')
,
),mainPanel(
plotOutput("map"))))
),)
<- function(input, output) {
server
}
# 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).
- dataTableOutput(“table”) = The output is a table. We use this to render the attribute table of the counties in the Data Explorer tab.
- plotOutput(“boxplot”) = The output in this case is a visualization. We use this in the Sum Plot tab to render ggplot2 graphs.
- 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/")
<- st_read("counties_with_table.shp")
us `counties_with_table' from data source
Reading layer `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
3104 features and 37 fields
Simple feature collection with : MULTIPOLYGON
Geometry type: XY
Dimension: xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Bounding box: WGS 84
Geodetic CRS<- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')
us <- st_transform(us, crs=4326)
us
# Define UI
<- fluidPage(
ui 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"))))
),)
<- function(input, output) {
server
}
# 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
<- fluidPage(
ui
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(),
::dataTableOutput("table")
DT
),
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"))))
),)
<- function(input, output) {
server
}
# 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/")
<- st_read("counties_with_table.shp")
us `counties_with_table' from data source
Reading layer `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
3104 features and 37 fields
Simple feature collection with : MULTIPOLYGON
Geometry type: XY
Dimension: xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Bounding box: WGS 84
Geodetic CRS
head(us)
6 features and 37 fields
Simple feature collection with : MULTIPOLYGON
Geometry type: XY
Dimension: xmin: -88.02913 ymin: 30.22093 xmax: -85.04883 ymax: 34.26049
Bounding box: WGS 84
Geodetic CRS
FIPS NAME STATE_N STATE_A STATE_F COUNTY_ POPULAT POP_SQM SQMI1 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_brdb1 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 vpdmin1 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_ln1 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 geometry1 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...
<- st_read("counties_with_table.shp")
us `counties_with_table' from data source
Reading layer `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
3104 features and 37 fields
Simple feature collection with : MULTIPOLYGON
Geometry type: XY
Dimension: xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Bounding box: WGS 84
Geodetic CRS
<- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')
us
<- st_transform(us, crs=4326)
us
# Define UI
<- fluidPage(
ui 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(),
::dataTableOutput("table")
DT
),
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"))))
),)
<- function(input, output) {
server
}
# 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/")
<- st_read("counties_with_table.shp")
us `counties_with_table' from data source
Reading layer `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
3104 features and 37 fields
Simple feature collection with : MULTIPOLYGON
Geometry type: XY
Dimension: xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Bounding box: WGS 84
Geodetic CRS
<- st_read("counties_with_table.shp")
us `counties_with_table' from data source
Reading layer `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
3104 features and 37 fields
Simple feature collection with : MULTIPOLYGON
Geometry type: XY
Dimension: xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Bounding box: WGS 84
Geodetic CRS
<- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')
us
<- st_transform(us, crs=4326)
us
# Define UI
<- fluidPage(
ui 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(),
::dataTableOutput("table")
DT
),
),)
<- function(input, output) {
server
<- reactive({
selectedCountry1 $STATE_N == input$countryInput1, ]
us[us
})
$table <- DT::renderDataTable({ data = selectedCountry1()
output
})
}
# 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/")
<- st_read("counties_with_table.shp")
us `counties_with_table' from data source
Reading layer `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
3104 features and 37 fields
Simple feature collection with : MULTIPOLYGON
Geometry type: XY
Dimension: xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Bounding box: WGS 84
Geodetic CRS
<- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')
us
<- st_transform(us, crs=4326)
us
#ggplot graphs
<- ggplot(us, aes(x=STATE_A, y=med_ncm, fill=STATE_A)) +
inc_p geom_boxplot() #Income
<- ggplot(us, aes(x=tempmn, ..density.., fill=STATE_A))+
temp_p geom_density(alpha=0.5) #Temperature
<- ggplot(us, aes(x=POPULAT, y=per_dev, color= STATE_A, size= med_ncm))+
pop_p geom_point() #Population vs Development
# Define UI
<- fluidPage(
ui 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")))),
),)
<- function(input, output) {
server
$boxplot <- renderPlot({
outputif (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/")
<- read.csv("us_county_data.csv")
us
<- st_read("counties_with_table.shp") %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')
us `counties_with_table' from data source
Reading layer `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
3104 features and 37 fields
Simple feature collection with : MULTIPOLYGON
Geometry type: XY
Dimension: xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Bounding box: WGS 84
Geodetic CRS
<- st_transform(us, crs=4326)
us
<- colorBin(palette="YlOrRd", domain=us$tempmn, bin=5)
temp_pal <- colorNumeric(palette= 'Purples', domain= us$POPULAT)
pop_pal <- colorNumeric(palette= 'Greens', domain= us$med_ncm)
ncm_pal
leaflet('map', options = leafletOptions(zoomControl= FALSE)) %>%
::onRender("function(el, x) {
htmlwidgets 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
<- st_read("counties_with_table.shp")
us `counties_with_table' from data source
Reading layer `D:\mydata\shiny\counties_with_table.shp' using driver `ESRI Shapefile'
3104 features and 37 fields
Simple feature collection with : MULTIPOLYGON
Geometry type: XY
Dimension: xmin: -124.7625 ymin: 24.52108 xmax: -66.94961 ymax: 49.38449
Bounding box: WGS 84
Geodetic CRS
#filter the data set by the three countries selected in the APP
<- us %>% filter(STATE_A=='WV'| STATE_A=='VA'| STATE_A =='MD')
us
#transform the geometry to Web Mercator
<- st_transform(us, crs=4326)
us
# fist graph on ggplot = Income
<- ggplot(us, aes(x=STATE_A, y=med_ncm, fill=STATE_A)) +
inc_p geom_boxplot()
# second graph on ggplot = Temperature
<- ggplot(us, aes(x=tempmn, ..density.., fill=STATE_A))+
temp_p geom_density(alpha=0.5)
# third graph on ggplot = Population vs Development
<- ggplot(us, aes(x=POPULAT, y=per_dev, color= STATE_A, size= med_ncm))+
pop_p geom_point()
#color palette for Temperature Income and Population
<- colorBin(palette="YlOrRd", domain=us$tempmn, bin=5)
temp_pal <- colorNumeric(palette= 'Purples', domain= us$POPULAT)
pop_pal <- colorNumeric(palette= 'Greens', domain= us$med_ncm)
ncm_pal
# Define UI for the APP
<- fluidPage(
ui 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(),
::dataTableOutput("table") #output ID
DT
),
#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
<- function(input, output) {
server
<- reactive({#reactive expression for the Data Explorer
selectedCountry1 $STATE_N == input$countryInput1, ] #match input of the user with the state name
us[us
})
<- reactive({ #reactive expression for the Interactive Map
selectedCountry $STATE_N == input$countryInput, ] #match input of the user with the state name
us[us
})
$table <- DT::renderDataTable({ #Data Explorer tab output
output
= selectedCountry1()
data
})
$boxplot <- renderPlot({ #Visualization tab output
outputif (input$inc_pop_temp %in% "Income") # If input of the user is Income
#then print Income graph
{inc_p} else if (input$inc_pop_temp %in% "Median Temperature")# If input of the user is Median Temperature
#then print Temperature graph
{temp_p}
else if (input$inc_pop_temp %in% "Population vs Development")# If input of the user is Pop vs Dev
#then print Pop vs Dev graph
{pop_p}
})
$map <- renderLeaflet({#Interactive Map tab output
outputleaflet('map', #base map
options = leafletOptions(zoomControl= FALSE)) %>%
::onRender("function(el, x) {
htmlwidgets 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
<- paste0("<strong>County: </strong>", #popup
state_popup selectedCountry()$NAME,
"<br><strong> Temperature: </strong>",
round(selectedCountry()$tempmn,1),"℃",
"<br><strong> Median Income: </strong>",
selectedCountry()$med_ncm,'$',
"<br><strong> Population: </strong>",
selectedCountry()$POPULAT)
<- highlightOptions(weight = 3, color = "white", bringToFront = FALSE) #highlight when user select county
high_opt
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:
- Mastering Shiny = The most comprehensive book out there on Shiny. From intermediate to advanced level.
- Outstanding User Interfaces with Shiny = This book focuses on the UI and the integration of Shiny with front end web technologies.
- Get started with mapboxer: Mapbox GL JS for R = A short guide on how to bring the power of mapbox into Shiny.
- Shiny Leaflet Tutorial = A useful tutorial to continue to learn on how to improve your mapping with Leaflet.
- Interactive Web-Based Data Visualization with R, plotly, and shiny = This book covers data visualization in R and has a specific section on Shiny. Plotly and Shiny work great together to make your graphs interactive.