Code Club S02E11: Shiny Bright Like a Diamond
An Introduction to ShinyR
What will we go over today
- What is Shiny, and why would I want to use it?
- Understanding the principles of user interfaces and servers
- The structure of a basic Shiny app
Interactive Applications with R
Shiny is a package that builds interactive html-based applications using the R coding language and capabilities.
The goal of this session is to provide a basic introduction to the structure of simple Shiny apps. For more detailed information as you work to build more complicated apps, I recommend you reference the Shiny tutorial page.
The code for a Shiny app contains three main parts.
- A section defining the user interface (UI)
- A section defining the server
- A piece of code that combines the UI and server sections
Let’s talk through some examples to illustrate what a UI and server are.
A Conceptual Example
Imagine you are in need for directions from Columbus, OH to Wooster, OH.
You decide to use a website such as Google maps to give you precise directions for the quickest route.
You, the user, type in your location (Columbus) and your destination (Wooster), and as if by magic, turn-by-turn directions pop up on your screen.
Everything you have just interacted with and experienced is the User Interface of the maps application. Think of this as the front-end of the application.
The Server is everything going on behind the scenes- the lines of code and calculations- invisible to the user- that take the user input and produce the results that are then presented back to the user.
A more grounded example
The OSU Infectious Disease Institute maintains a webapp that tracked COVID-19 statistics in Ohio from March 2020 to March 2021. Click or navigate to: https://covidmap.osu.edu/
This is a nice demonstration of a “data dashboard”, which is a very common use of the Shiny package.
It provides a way for a user of virtually any skill level to interact with their COVID-19 data set. It may look very fancy, but it was all put together using Shiny and many R commands you already know.
What function would the app use to pair down the data to the selected date range? What R package are they using the plot the daily counts of new cases?
You can apply what you have already learned in code club to make a fancy, interactive web app for your own data sets!
We just need to learn how to code R within the Shiny environment.
Let’s start with a simple Shiny app
First, take a second to install Shiny, and for the purpose of our demonstration today, I recommend you also pre-load the tidyverse (I am assuming you already have that installed)
install.packages("shiny")
library(shiny)
library(tidyverse)
You can use a common structure to set up the basic parts of almost any shiny app. Below you will see that we are creating an object for the UI, another for the server, and then we combine them with the shinyApp
call. Before we start building the app, we need to tell it where to live on our computer.
ui <- ...
server <- ...
shinyApp(ui = ui, server = server)
Inside your directory for code club, create a folder for this week. In this folder, save a new R script with the name app.R
Shiny uses the directory as a sort of bundle to run your app from. The name of your directory is the name of the app, the app.R file is the code, and there are other file types (with specific names) that you can add to the directory later as your app gets more complicated.
dir.create("S02E12")
Let’s make an app!
We are going to start simple, using a data set about large pumpkins, squashes, and tomatoes.
First, let’s download the data and do a little cleanup.
#download the dataframe from github. This data can also be found in the `tidytuesdayR` package.
pumpkins <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-10-19/pumpkins.csv')
#the year and type of crop are combined into a single column, separated by a "-". Need to split.
#also, there are some rows in the data frame that contain information about the number of entries
#need to remove these interceding rows
pumpkins <- pumpkins %>%
#separate the year column
separate(col = id, into = c("year", "vegetable"), sep = "-") %>%
#find and tag the rows that do not have data
mutate(delete = str_detect(place, "\\d*\\s*Entries")) %>%
#filter out the rows that do not have data
filter(delete==FALSE) %>%
#remove the tagging column
select(-delete)
#Rename the vegetables to their actual names
pumpkins$vegetable <- pumpkins$vegetable %>%
str_replace("^F$", "Field Pumpkin") %>%
str_replace("^P$", "Giant Pumpkin") %>%
str_replace("^S$", "Giant Squash") %>%
str_replace("^W$", "Giant Watermelon") %>%
str_replace("^L$", "Long Gourd") %>%
str_replace("^T$", "Tomato")
#get rid of commas in the weight_lbs column
pumpkins$weight_lbs <- as.numeric(gsub(",","",pumpkins$weight_lbs))
#look at the data structure
head(pumpkins)
User Interface
Now that we have our data sorted out, let’s start building out our user interface
The UI portion of your code is where you will define the appearance and physical layout of your app, and in Shiny, layouts are commonly defined using fluidPage()
. Fluid pages scale to fill the available window size of your browser, and are formatted to a grid with 12 equally spaced columns and as many rows of equal height that you need for your app. Within the `fluidPage()' command, you define how different components of your app physically fit within the browser window.
There are some handy pre-sets that we can use to build an app more quickly, but just know that the interface you build is very customizable with enough effort. For the purpose of this demonstration, we will use the preset commands that define a sidebarPanel()
for user inputs and a `mainPanel()' for data output.
ui <- fluidPage(
sidebarPanel(
),
mainPanel(
)
)
server <- function(input,output){
}
shinyApp(ui=ui,server=server)
This is the basic layout of our app. It may read a little funny because I am leaving space to put in different components.
Now, back to our example dataset. The World Pumpkin Committee is very interested in being able to view the weight distribution of each vegetable by year. Let’s make them an app for that!
We will add a place for them to select the year in sidebarPanel()
Shiny has a number of data input options. The one you pick, will depend on what information you want to get from the user. No matter the input, you will need to specify the InputID
. This piece of information is very important, as it is how the server references the inputs in the UI.
For this example, we have a defined set of inputs (this competition has only been going on for a set number of years), so we will use selectInput()
. We will also define the choices as the years avilable in our dataset.
ui <- fluidPage(
sidebarPanel(
#here we are naming the input as 'year', the box will display 'Select Year', and
#the user will choose from the competition years as referenced from the
#pumpkins dataframe
selectInput(inputId = "year", label = "Select Year", choices = unique(pumpkins$year))
),
mainPanel(
)
)
server <- function(input,output){
}
shinyApp(ui=ui,server=server)
We now have the input side of our UI, but we also need to define what and where our output will be by essentially adding a placeholder for our output. There are many options in Shiny, but we will be using a plotOutput()
for this example. The main thing that needs to be set is the OutputID
. Similar to the InputID
, this tells the server where to place our results.
ui <- fluidPage(
sidebarPanel(
#here we are naming the input as 'year', the box will display 'Select Year', and
#the user will choose from the competition years as referenced from the
#pumpkins dataframe
selectInput(inputId = "year", label = "Select Year", choices = unique(pumpkins$year))
),
mainPanel(
#placeholder for a plot to generate
plotOutput(outputId = "weight_distribution")
)
)
server <- function(input,output){
}
shinyApp(ui=ui,server=server)
Run this whole chunk of code. Notice you will create a ui
object and a server
function that are combined with shinyApp
. Once you run that last piece, a built in web browser will appear and run your app.
When you are not using the app, be sure to fully exit the browser window, otherwise it will keep running in the background.
Server
Now that we have our UI built, let’s move on to the server. This is where you will code what R should do with inputs from the UI side and how results should be presented to the user.
Notice the server is built as a function()
with input
and output
objects.
For our example. we basically want to use the inputID
“year” to filter our list, then we want to plot the weight distribution of each vegetable.This is done using code you are likely already familiar with.
ui <- fluidPage(
sidebarPanel(
#here we are naming the input as 'year', the box will display 'Select year', and
#the user will choose from the competition years as referenced from the
#pumpkins dataframe
selectInput(inputId = "year", label = "Select Year", choices = unique(pumpkins$year))
),
mainPanel(
#placeholder for a plot to generate
plotOutput(outputId = "weight_distribution")
)
)
server <- function(input,output){
#notice the callback to our outputID
output$weight_distribution <- renderPlot(
pumpkins %>%
filter(year==input$year) %>%
ggplot(aes(vegetable, weight_lbs)) +
geom_boxplot()
)
}
shinyApp(ui=ui,server=server)
Notice how the outputID
from the ui
section is referenced as output$weight_distribution
on the server side. Do you see where our inputID
is referenced on the server side?
Also notice how the code is wrapped within a renderPlot
command. This is a special Shiny command specific to plot outputs. There are other commands for other types of outputs, such as renderTable
for table outputs.
These commands are also special because they are “reactive”, meaning they react to changes in user inputs and re-execute the code when those changes occur. This is a simple explanation of a somewhat complex topic. For a more in-depth explanation, see this explanation.
Re-run the code, and you should now see a plot that will respond to changes in the year input box.
Exercise 1
Now it is your turn to add on to the app. The World Pumpkin Committee would like to also have the app generate a table that contains the weight, grower name, city, and state/province of each first place vegetable for the year selected in the input.
Bonus Explore renderTable
to see how you can change the number of significant figures displayed.
Hints (click here)
Focus on the server side of the app, you already have the input from the UI that you need. Use renderTable()
.
Solution (click here)
ui <- fluidPage(
sidebarPanel(
#here we are naming the input as 'year', the box will display 'Select year', and
#the user will choose from the competition years as referenced from the
#pumpkins dataframe
selectInput(inputId = "year", label = "Select Year", choices = unique(pumpkins$year))
),
mainPanel(
#placeholder for a plot to generate
plotOutput(outputId = "weight_distribution"),
tableOutput(outputId = "winner_table")
)
)
server <- function(input,output){
#notice the callback to our outputID
output$weight_distribution <- renderPlot(
pumpkins %>%
filter(year==input$year) %>%
ggplot(aes(vegetable, weight_lbs)) +
geom_boxplot()
)
#create the output for the table using renderTable
output$winner_table <- renderTable(
pumpkins %>%
filter(year==input$year) %>%
filter(place == "1") %>%
select(vegetable, weight_lbs, grower_name, city, state_prov) %>%
#I just wanted to rename the columns to look nicer
rename("weight (lbs)"=weight_lbs, "grower name"=grower_name, "state/provice"=state_prov),
#setting significant figures as an option under renderTable
digits = 1,
#adding a caption to be fancy
caption = "Table of Winners",
caption.placement = "top"
)
}
shinyApp(ui=ui,server=server)
Exercise 2
Create a new app that allows a user to visualize how changing the value of “m” in y=mx+b affects the slope of a line.
Hints (click here)
You will need to define the values of x. You can do this within the server function (i.e. a <- tibble(a=-100:100)
).
Also note that changing the slope of a line may not be so noticable because plots will automatically adjust to the scale of the data. Consider locking your coordinates so you notice changing slopes more easily.
Solution (click here)
ui <- fluidPage(
sidebarPanel(
#I chose sliderInput here, you can choose another input type
sliderInput(inputId = "slope", min = -10, max = 10, value = 2, label = "Slope")
),
mainPanel(
plotOutput(outputId = "graph")
)
)
server <- function(input,output){
a <- tibble(a=-100:100)
output$graph <- renderPlot(
ggplot(a, aes(a, input$slope*a))+
geom_line()+
coord_cartesian(xlim = c(-25,25), ylim = c(-100,100))
)
}
shinyApp(ui=ui,server=server)
Exercise 3
Add on to your app from Exercise 2 by providing another input for user to adjust the y-intercept.
Solution (click here)
ui <- fluidPage(
sidebarPanel(
sliderInput(inputId = "slope", min = -10, max = 10, value = 2, label = "Slope"),
sliderInput(inputId = "intercept", min = -10, max = 10, value = 2, label = "Y-Intercept")
),
mainPanel(
plotOutput(outputId = "graph")
)
)
server <- function(input,output){
a <- tibble(a=-100:100)
output$graph <- renderPlot(
ggplot(a, aes(a, input$slope*a+input$intercept))+
geom_line()+
coord_cartesian(xlim = c(-25,25), ylim = c(-100,100))
)
}
shinyApp(ui=ui,server=server)