A Shiny App tutorial using the Cooperative Election Study
By Brian Schaffner
Shiny Apps are interactive web based tools that allow users to analyze data with a simple point and click interface. These can be built in R studio using R code. You can find examples of a variety of different Shiny Apps I have created using the Cooperative Election Study data here: http://https://cces.gov.harvard.edu/explore.
For this tutorial, I will work through the logic of creating a Shiny App from the 2022 Cooperative Election Study common content dataset. To begin, I have downloaded the .csv file for the 2022 Cooperative Election study from the Dataverse (https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/PR4L8P) into a folder on my desktop.
Starting an R script for a Shiny App
I typically start a new Shiny App R script by copying a previous version I have created and then adapting it to what I want to do with the new Shiny App. However, since this will be your first Shiny App, you can open a new Shiny R script in R Studio by clicking on File
→→ New File
→→ Shiny Web app...
You’ll then get a pop-up window that looks like this:
Keep the default selection of Single File and give your shiny app a name and then save it into the same directory where you saved the .csv file for the CES data. Your Shiny R script and data need to live in the same folder.
Once you click Create
it will populate a new R script called app.R which looks like this:
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',
xlab = 'Waiting time to next eruption (in mins)',
main = 'Histogram of waiting times')
})
}
# Run the application
shinyApp(ui = ui, server = server)
This is a generic Shiny App that is already set up to run. So we are going to edit this file to do what we want with our own Shiny App. But first, make note of a few things. On the top right of your script window there is now a green arrow that says Run App
. If you click this, it will run your script and a new pop-up window will show a preview of what your app looks like. In this case, it will show you the generic app that comes with the default shiny code (something about Old Faithful Geyser eruptions).
The structure of the shiny app code
There are two main parts of a Shiny app script — the user interface (UI) and the output (Server). The UI is basically the script that defines the layout of the interface that users will see and what kinds of options users will have when they interact with the interface. The Server side is basically what the script will do once the user interacts with the interface.
UI code
In the example above, the UI code indicates that the user will see a slider scale that they can move which will allow the user to select anywhere between 1 and 50 bins for the histgram, with the default being set to 30. This is defined with the sliderInput
function. The UI code also defines where the output will appear, which is the PlotOutput
part of the command.
The key thing you need to know is that you are defining the input variable here. So, the way to read the sliderInput
part of the code is that you are using a slider scale to determine the value of a variable called bins
. That variable can range from 1 to 50 and its default (or starting) value is 30.
There are other tools you can use to collect user input. These are listed in the Shiny Cheat Sheet that I’ll link to at the end of this tutorial.
Server code
The server side of the code is where R takes the user’s input and produces something from it. Usually, I use the input to produce a graph of some kind, but you can just create tables, text output, or whatever. Note in the image that follows that when you use the user input you need to attach it to the input
object. So, recall that the default code produces an input called bins, and here it is incorporated into the code as input$bins
. The server side produces some output as an object, in this case output$distplot
. Recall that distplot
is used in the UI section of the code because the plot is what is being displayed to the user.
Creating a new app from the existing code
So now that we’ve described the basics, let’s create a Shiny App for the CES data by adjusting the default code that R has given us. Our Shiny App will allow people to explore the percentage of people who say the police make them feel unsafe by several different demographic variables — party, race, gender, community type, and citizenship status. I’m going to start by adding some code to the top of my script which pulls in the data and then recodes these variables in the format I want them.
I put all of this above the UI portion of the code because I want all these transformations completed before I get to the part where the users interact with the data. Note that I am not setting a working directory here. For Shiny Apps, you should just place the R script and the data in the same folder and when you run the app it will automatically default to that folder. You do not want to have file paths in your Shiny app because when you upload it to the web those paths no longer point to anywhere valid and they will break the app. While you are building the app you can set the working directory manually (or by typing directly into the console).
Note that I also added commands for the packages I’m going to use in this app. You need to make sure the R script for your app loads any packages you need it to use. It should also always load the shiny
package.
One other thing to note is that I’ve created a variable that takes on the same value for all respondents (All adults
). This is so that respondents can see results for everyone without a demographic breakdown if they want that. Hopefully the logic of doing this will become clear as we continue writing code below.
library(shiny)
library(ggplot2)
library(car)
library(dplyr)
# Bring in data
dat <- read.csv("CES22_Common.csv")
# Recode variables that we will be using for demographic breakdowns
dat$race5 <- car::recode(dat$race, "1='White'; 2='Black'; 3='Hispanic'; 4='Asian';
5:9='Another race'")
dat$party <- car::recode(dat$pid7, "1:3='Democrat'; 4='Ind/other'; 5:7='Republican';
8='Ind/other'")
dat$gender <- car::recode(dat$gender4, "1='Man'; 2='Woman'; 3:4='Non-binar/other'")
dat$community <- car::recode(dat$urbancity, "1='City'; 2='Suburb'; 3:5='Town/Rural'")
dat$citizen <- car::recode(dat$cit1, "1='US Citizen'; 2='Not a US Citizen'")
# Create a variable that simply equals "All adults" for every respondent
dat$all <- "All adults"
# Recode unsafe variable so that it equals 1 for unsafe and zero otherwise
dat$unsafe <- car::recode(dat$CC22_307, "1:2=0; 3:4=1")
Ok, now I’m going to edit the UI portion of the code. I’ll maintain the layout as it is, but just keep in mind that there are choices you can make here (see the Shiny cheat sheet for details).
First, I edit the title so that it fits what the app will be presenting. That’s straightforward. Then I want to program the type of input the user will be able to make. The sliderInput
doesn’t make sense here, because what I want the user to do is select from the various demographic variables that we created. We could go with either radio buttons or a dropdown list, but let’s go with the dropdown list. This means we need to change the sliderInput
function to selectInput
.
I also need to list the variable names that users can choose since that is the input they will be selecting. I’m using the variables I created above with the recoding, but users might not know what those variable names mean so I’m also giving each one a label using the =
sign. Here is what that code looks like now:
ui <- fluidPage(
# Application title
titlePanel("Which Americans feel unsafe from the police"),
# Sidebar with a dropdown menu to select variable to break out
sidebarLayout(
sidebarPanel(
selectInput("demo",
"Break down the results by:",
choices=c("All adults"="all",
"Race/ethnicity"="race5",
"Partisanship"="party",
"Gender"="gender",
"Community type"="community",
"Citizenship status"="citizen"))
),
# Show a plot of the generated graph
mainPanel(
plotOutput("myPlot")
)
)
)
Ok, now I’m going to edit the server side of the code. First, I need to change the name of the plot I’m going to output to match how I changed it above (I’m calling it myPlot
now instead of distPlot
). Then, inside the renderPlot
parentheses is where I need to do all of the work to create this plot. The first lines create a new temporary data frame called toplot
which is created by taking the weighted mean of the unsafe
variable by values of the variable the user selects. The user selected variable appears in the group_by
section of the code because it is that variable that we want to generate our means by. To pull the variable that the user has selected, we include write get(input$demo)
which tells R to get the variable name that the respondent selected above and use it here. We also use the group=
code before that command in order to tell R to call this variable group
when it performs the aggregation. That will make the plotting easier in our next step because it means our variable names for plotting will always be the same. (Our code defines it so that the y-variable is always called result
and the x-variable is always called group
).
Then we use simple ggplot commands to create the graph. This graph becomes the output of the renderPlot
function and is assigned to output$myPlot
which is then the graph that appears in the user interface.
# Define server logic required to draw graph
server <- function(input, output) {
output$myPlot <- renderPlot({
# transform data to plot from
toplot <- dat %>% group_by(group=get(input$demo)) %>%
summarise(result = weighted.mean(unsafe, commonweight, na.rm=T))
ggplot(toplot, aes(x=group, y=result*100)) +
geom_bar(stat="identity", fill="navy") +
ylim(0,60) + theme_minimal() + ylab("Percent") + xlab("")
})
}
Putting it all together
I’ve pasted the full code for the app below, but first an image to hopefully demonstrate how the UI side and server side talk to each other.
Here is what the full app code looks like:
#
# 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)
library(ggplot2)
library(car)
library(dplyr)
# Bring in data
dat <- read.csv("CES22_Common.csv")
# Recode variables that we will be using for demographic breakdowns
dat$race5 <- car::recode(dat$race, "1='White'; 2='Black'; 3='Hispanic'; 4='Asian';
5:9='Another race'")
dat$party <- car::recode(dat$pid7, "1:3='Democrat'; 4='Ind/other'; 5:7='Republican';
8='Ind/other'")
dat$gender <- car::recode(dat$gender4, "1='Man'; 2='Woman'; 3:4='Non-binar/other'")
dat$community <- car::recode(dat$urbancity, "1='City'; 2='Suburb'; 3:5='Town/Rural'")
dat$citizen <- car::recode(dat$cit1, "1='US Citizen'; 2='Not a US Citizen'")
# Create a variable that simply equals "All adults" for every respondent
dat$all <- "All adults"
# Recode unsafe variable so that it equals 1 for unsafe and zero otherwise
dat$unsafe <- car::recode(dat$CC22_307, "1:2=0; 3:4=1")
# Define UI for application that creates a bar graph
ui <- fluidPage(
# Application title
titlePanel("Which Americans feel unsafe from the police"),
# Sidebar with dropdown list of input for variable to graph by
sidebarLayout(
sidebarPanel(
selectInput("demo",
"Break down the results by:",
choices=c("All adults"="all",
"Race/ethnicity"="race5",
"Partisanship"="party",
"Gender"="gender",
"Community type"="community",
"Citizenship status"="citizen"),
selected="all")
),
# Show a plot of the generated graph
mainPanel(
plotOutput("myPlot")
)
)
)
# Define server logic required to draw graph
server <- function(input, output) {
output$myPlot <- renderPlot({
# transform data to plot from
toplot <- dat %>% group_by(group=get(input$demo)) %>%
summarise(result = weighted.mean(unsafe, commonweight, na.rm=T))
# make the graph
ggplot(toplot, aes(x=group, y=result*100)) +
geom_bar(stat="identity", fill="navy") +
ylim(0,60) + theme_minimal() + ylab("Percent") + xlab("")
})
}
# Run the application
shinyApp(ui = ui, server = server)
Now publish the app!
Now that you’ve created the app, you can make it public. First, go to https://www.shinyapps.io/ and create a free account. A free account allows you to post up to five shiny apps at any one time. Your user name will become part of your URL. So, for example, for CES Shiny Apps we have an account that starts with cooperativeelectionstudy
in the URL (e.g. https://cooperativeelectionstudy.shinyapps.io/CumulativeHouseVote/).
Once you’ve created your free account, go to the Shiny code in your R Studio and click on the blue icon to the right of the Run App
button. Then click on Publish Application...
. A pop-up window will appear. The first time you do this, you will need to click on Add new account
on the right-hand side to link your Shiny Apps account to your R Studio. Once you do that, you can publish your app to that account! On the left-hand side only select the files you need for your app to run (in this case you would only need the R script for the app and the data file). It will take a minute or two for the app to publish, but once it does a new web browser should open with your app running in it.
You can see an example of this app here: https://cooperativeelectionstudy.shinyapps.io/tutorial_example/
Tips
- Keep in mind that your Shiny app is pretty much running your entire script every time somebody uses it. Things that operate slowly on R Studio will also be slow in the online Shiny App. For this reason, I often create a pared down version of whatever dataset my app uses to make it quicker to load the data in.
- I used ggplot to make the graphs, but you can pretty much use any graphing package that you’d like here. I often use plotly for my Shiny Apps because they are a bit more user friendly (you can hover a mouse over the bars to see information and you can also download an image of the graph easily).
- You can set up your app so that users have the ability to make multiple inputs. Here is an example that allows for three inputs: https://cooperativeelectionstudy.shinyapps.io/racism_sexism/. The main thing to keep in mind when you do have multiple inputs is to make sure you give each a different variable name so that R won’t get confused.
A few additional resources
- The Shiny cheat sheet is very handy: https://github.com/rstudio/cheatsheets/raw/main/shiny.pdf
- Shiny tutorial and learning resources: https://shiny.rstudio.com/tutorial/
- Mastering Shiny online book by Hadley Wickham: https://mastering-shiny.org/index.html