13 Shiny Applications
The last chapter gave us a quick overview on all kinds of output formats we can create from within R.
Specifically, among those outputs we got to know flexdashboards from the flexdashboard
package.
In this chapter, we will learn some other kind of dashboard which we will create using the shiny
package.
This package is quite versatile and allows us to use reactive programming to create dashboards and other applications that can actually react to a user’s input. For instance, take a look at this wedding dashboard that is written with Shiny. Notice that the login and password can be found on the corresponding GitHub repository. For convenience here is the login data from GitHub:
Login to access the app: welcome
Password to access the app: bigday
Password to access the tab dedicated to brides/grooms: onlyforbride
Of course, there is a lot of work involved to create such a dashboard and sadly, we won’t be able to learn everything there is to know about Shiny to create such a dashboard. Thus, we will start small and use Shiny to design a rather crude dashboard that looks like this.
As you can see this dashboard gives you a limited overview of Taylor Swift songs. Namely, it shows us all titles from a given album and we can filter all her songs by some attributes like “danceability”, “loudness” and so on. The corresponding data was acquired through the Spotify API and can be found here along with the code that extracted the data
13.1 A Minimal Example
If you take a look at our Taylor dashboard, then you will notice that there are lists where you can choose albums or attributes from. Similarly, there are also sliders you can adjust in order to filter the data w.r.t. to some attribute. In general, everything you see on the dashboard is part of the user interface (UI).
Basically, the UI is managing the interaction between the user (that is you) and the server (the machine that has to react if you, say, choose a different album from our list). Consequently, this is reflected in the code that creates the app and each Shiny application will contain the following lines of code.
library(shiny)
# Define UI
ui <- fluidPage(
)
# Define server
server <- function(input, output) {
}
# Run Shiny app
shinyApp(ui = ui, server = server)
If you want to try this out, simply copy-and-paste this into an R-script and save it in a new directory. Once it is saved, you will see that the usual “Run” button changes to “Run App”. Clicking this button will start your Shiny application.
As you may suspect, the line ui <- fluidPage()
controls what appears on the user interface.
Thus, starting this app won’t be particularly interesting since the UI contains nothing yet.
But what you may notice when you start the app is that your R console shows “Listening on http://127.0.0.1:4799” or similar.
This indicates that your R Session is currently running the app and is playing the part of the server, i.e. you cannot use the R console right now but interactions with your app are registered and executed (if possible).
Of course, what happens when a user interacts with the UI needs to be determined at some point.
This is exactly the point of server <- function(input, output){}
, i.e. everything that is coded between {
and }
determines how the server is supposed to react when the user does something.
Consequently, everything we code there is a template of what to do in case some event happens.
Notice that the user does not have to interact with the UI which is why a lot of code in the server might actually never run. So, what we programm there is a little bit from our usual style of programming since usually we give commands and expect them to be executed when that line of code is reached. Thus, this style of programming is labeled reactive programming.
Now, let us add some content to our UI.
If you take a look at our Taylor app again, you may notice that the pieces of our UI are aligned in three rows and two columns.
This is controlled with either elements like fluidRow()
and column()
or sidebarLayout()
in conjunction with sidebarPanel()
and mainPanel()
.
So in the case of our Taylor app, the general structure of our UI looks like this
ui <- fluidPage(
# Application title
titlePanel("Taylor Swift Data"),
# Sidebar with dropdown menu for album
sidebarLayout(
sidebarPanel(
# Album input here
),
# Main panel for songs of specific album
mainPanel(
# Album output here
)
),
hr(), # horizontal line
sidebarLayout(
# Sidebar panel for attribute choice and choise of range
sidebarPanel(
# Attribute input here
# Slider output here
),
# Main panel for filtered data table
mainPanel(
# Data table output here
)
),
# Another row for visualization of attribute w.r.t. albums
fluidRow(
mainPanel(
# Plot output here
)
)
)
If you were to take this UI and make it part of our previous app, then you would see that there are shaded areas now which are designated for the sidebar panels. Therefore, it becomes time to fill the panels.
13.2 Handling Input and Output
In order to fill the panels, we must understand how Shiny handles inputs and outputs.
First of all, notice that Shiny offers all kind of functions <type>Input()
where <type>
depends on the form in which the input data is to be selected.
Common choices are
All of these functions have their first two arguments in common.
More precisely, the first argument of any input function is inputID
and the second argument is the label
.
Whereas the former is used as identifier for later use and needs to be a unique variable name (i.e. a character with no spaces etc.), the latter is simply a label that is displayed in the app.
The remaining arguments of an input function are specific to the desired input type.
So, let us use these input functions to include our first input selection.
In our Taylor Swift app, we will add a dropdown menu so that we can select an album whose song titles are then to be displayed.
Thus, we extend our script by a list of album names extracted from the data set and add a selectInput()
to our UI.
Consequently, our code now looks as follows.
library(shiny)
library(tidyverse)
# Read data and extract album names
taylor <- read_csv("my_taylor.csv", col_types = "dddddddddddddcccccc")
albums <- unique(taylor$Album)
# Define UI
ui <- fluidPage(
# Application title
titlePanel("Taylor Swift Data"),
# Sidebar with dropdown menu for album
sidebarLayout(
sidebarPanel(
selectInput("chooseAlbum", label = "Album", choices = albums)
),
# Main panel for songs of specific album
mainPanel(
)
),
hr(), # horizontal line
sidebarLayout(
# Sidebar panel for attribute choice and choice of range
sidebarPanel(
),
# Main panel for filtered data table
mainPanel(
)
),
# Another row for visualization of attribute w.r.t. albums
fluidRow(
mainPanel(
)
)
)
# Define server
server <- function(input, output) {
}
# Run Shiny app
shinyApp(ui = ui, server = server)
If you run this app, you will notice that we can select albums from a dropdown menu now. Of course, we may choose different albums from this list but no album data will be displayed. That is because we have not told the server what to do with the newly selected album. Therefore, let us add an output to the server.
This is done in the function server()
where the inputs can be accessed through input$<inputID>
, where <inputID>
is the ID we determined when defining an input.
In this particular case, we can access the selected album value via input$chooseAlbum
.
So, this enables us to access a value from the input that is given through the UI.
Now, it becomes time to define a new output to be computed by the server.
This can be easily done in base R style91 via output$<outputId> <- ...
.
Similar to inputs, there are a bunch of Shiny functions that render, i.e. create, the output but are specific to the desired type, i.e. there are functions of the form render<Type>()
.
Here, we want to add a table that displays data for a given album.
Therefore, we use renderTable()
which simply acts as wrapper around the code that is to be recomputed whenever a new album is picked from our list.
Consequently, in order to let the server know what to compute whenever a user picks a new album, we adjust the server()
function as follows.
server <- function(input, output) {
output$albumTable <- renderTable({
taylor %>%
filter(Album == input$chooseAlbum) %>%
select(No, Song, Duration)
})
}
Notice that we used {}
instide of renderTable()
.
In general, this is always necessary whenever a code that spans more than one line of code is to be executed.
Technically, here we only have one line of code but still I find it convenient to simply always use the curly brackets here.
Now, if you change the server function of our app and reload the app, then you will notice that there is still no table present.
This happens because we have not adjusted the UI accordingly.
The server may know to recompute stuff whenever the selection from chooseAlbum
is changed but this does not mean that it knows how to display any results.
Consequently, we adjust the UI with output function of the form <type>Output()
.
As you may suspect, this function is paired with the corresponding render<Type>
function.
All this function needs is an outputId
so that is know which variable its designated output is coming from.
Here, the outputId
is simply albumTable
.
Thus, we may adjust the first main panel of our app to
ui <- fluidPage(
# Application title
titlePanel("Taylor Swift Data"),
# Sidebar with dropdown menu for album
sidebarLayout(
sidebarPanel(
selectInput("chooseAlbum", label = "Album", choices = albums)
),
# Main panel for songs of specific album
mainPanel(
tableOutput("albumTable")
)
),
# ...
# Remaining part stays unchanged
)
Now, you will see that your input actually changes the output that you can see on the UI. Also, if you take a look at your current code, then you will notice that the order in which code appears and the order in which code is actually executed is not necessarily the same. This is another aspect of reactive programming that takes some time getting used to.
13.3 Reactive Expressions
In the next part of our application we want to select a song attribute and filter songs according to whether the song’s attribute is in a given range. For this filtering, we want to use a slider that ranges from the attributes minmal value to its maximal value. Further, we want to generate a plot that visualizes the distribution of the selected song attribute across albums.
From this description alone you can already figure out a few things we have to keep in mind:
- We need to include another
selectInput()
to choose a song attribute. This should not be a problem. We are already familiar with how to do that. - In subsequent steps, we will always need to access the selected song attribute and the range from our slider to filter the data.
Doing this via
input$<inputId>
would generate a lot of code duplication which we must try to avoid. - The slider’s range depends on the attribute, i.e. on another input.
Consequently, this may mean that the slider UI is actually an output that is displayed in the UI, so things may not be as simple as using
sliderInput()
. - We need to add a table that consists of columns
Album
,Song
,Duration
and the selected attribute. This should be no problem at this point. For the sake of variation, we can make this a data table instead of a regular table.
To include another selection input, we simply extract the attributes from the data and add another selectInput()
to the UI. This will change our code to the following.
library(shiny)
library(tidyverse)
taylor <- read_csv("my_taylor.csv", col_types = "dddddddddddddcccccc")
albums <- unique(taylor$Album)
songAttributes <- taylor %>% select(danceability:tempo) %>% colnames()
ui <- fluidPage(
# Application title
titlePanel("Taylor Swift Data"),
# Sidebar with dropdown menu for album
sidebarLayout(
sidebarPanel(
selectInput("chooseAlbum", label = "Album", choices = albums)
),
# Main panel for songs of specific album
mainPanel(
tableOutput("albumTable")
)
),
hr(), # horizontal line
sidebarLayout(
# Sidebar panel for attribute choice and choice of range
sidebarPanel(
selectInput("chooseAttribute", label = "Attribute", choices = songAttributes)
),
# Main panel for filtered data table
mainPanel(
)
),
# Another row for visualization of attribute w.r.t. albums
fluidRow(
mainPanel(
)
)
)
# Define server
server <- function(input, output) {
output$albumTable <- renderTable(
taylor %>%
filter(Album == input$chooseAlbum) %>%
select(No, Song, Duration)
)
}
# Run Shiny app
shinyApp(ui = ui, server = server)
Next, let us save the selected attribute into a variable. Though, we need to keep in mind that the selected attribute could always change. Basically, we need something that constantly checks whether the selected attribute in the UI changes and makes sure that our variable changes its value whenever a new attribute is selected.
What we need is a variable that reacts to changes. To put this into the proper terminology, what we need is a reactive expression. These are easily defined on the server side of things via
my_var <- reactive({
### Code that computes variable's new value
})
For our current purpose of reusing the selected attribute, we simply extend the server function.
server <- function(input, output) {
output$albumTable <- renderTable(
taylor %>%
filter(Album == input$chooseAlbum) %>%
select(No, Song, Duration)
)
# Use sym() here to convert the character to a symbol for better functionality later
songAttribute <- reactive({sym(input$chooseAttribute)})
}
Now, we can use songAttribute
in our later code as part of the server and be sure that we always refer to the currently selected attribute.
Let me reiterate that this is only possible due to the reactive()
wrapper.
However, if we want to access the value of songAttribute
later on, then we have to make sure that we treat the variable like a function, i.e. we need to add ()
at the end.
To see how this plays out, let us add a data table to our application that always displays the column Album
, Song
, Duration
and the currently selected song attribute.
To do so, we add
dataTableOutput("AttributeTable")
to the second main panel of our UI and
output$AttributeTable <- renderDataTable({
taylor %>%
select(Album, Song, Duration, songAttribute())
# Notice the `()`.
})
to the server.
If you make these changes to our previous source code, you will notice that the table reacts to every changed attribute as expected.
However, the table is quite long and it is not sorted by the attribute’s value but this is something you can change via the options
argument of renderDataTable()
.
Also, this is something you have to do yourself in the exercises.
By the same procedure, we can also create new reactive expressions that correspond to the minimal and maximal values of the attribute’s range, so that we can use that for our slider later on. Similarly, we need standard values for the default position of the sliders. For that purpose we could compute two quantiles based on the selected attribute. In summary, we need to add the following reactive expressions
attributeValues <- reactive(pull(taylor, songAttribute()))
min_val <- reactive({min(attributeValues())})
max_val <- reactive({max(attributeValues())})
default1 <- reactive({quantile(attributeValues(), 0.1)})
default2 <- reactive({quantile(attributeValues(), 0.9)})
Finally, with the help of these reactive expressions we can render a slider.
Notice that we need to render here instead of simply putting sliderInput()
into the UI as the slider depends on the selected attribute.
This can be done with renderUI()
and inside of this function we use sliderInput()
as we normally would if we were to directly code it into the UI (after all that is the code that creates the UI).
Therefore, we add
output$attributeSliderOutput <- renderUI({
sliderInput("attributeSliderInput", label = "Range",
min = min_val(),
max = max_val(),
value = c(default1(), default2())
)
})
to the server and to the UI we simply add
uiOutput("attributeSliderOutput")
Overall, our code now looks like this.
library(shiny)
library(tidyverse)
taylor <- read_csv("my_taylor.csv", col_types = "dddddddddddddcccccc")
albums <- unique(taylor$Album)
songAttributes <- taylor %>% select(danceability:tempo) %>% colnames()
ui <- fluidPage(
# Application title
titlePanel("Taylor Swift Data"),
# Sidebar with dropdown menu for album
sidebarLayout(
sidebarPanel(
selectInput("chooseAlbum", label = "Album", choices = albums)
),
# Main panel for songs of specific album
mainPanel(
tableOutput("albumTable")
)
),
hr(), # horizontal line
sidebarLayout(
# Sidebar panel for attribute choice and choice of range
sidebarPanel(
selectInput("chooseAttribute", label = "Attribute", choices = songAttributes),
uiOutput("attributeSliderOutput")
),
# Main panel for filtered data table
mainPanel(
dataTableOutput("AttributeTable")
)
),
# Another row for visualization of attribute w.r.t. albums
fluidRow(
mainPanel(
)
)
)
# Define server
server <- function(input, output) {
output$albumTable <- renderTable(
taylor %>%
filter(Album == input$chooseAlbum) %>%
select(No, Song, Duration)
)
songAttribute <- reactive({sym(input$chooseAttribute)})
output$AttributeTable <- renderDataTable({
taylor %>%
select(Album, Song, Duration, songAttribute())
# Notice the `()`.
})
attributeValues <- reactive(pull(taylor, songAttribute()))
min_val <- reactive({min(attributeValues())})
max_val <- reactive({max(attributeValues())})
default1 <- reactive({quantile(attributeValues(), 0.1)})
default2 <- reactive({quantile(attributeValues(), 0.9)})
output$attributeSliderOutput <- renderUI({
sliderInput("attributeSliderInput", label = "Range",
min = min_val(),
max = max_val(),
value = c(default1(), default2())
)
})
}
# Run Shiny app
shinyApp(ui = ui, server = server)
Thus, we have already done most of the heavy lifting on the way to our final Taylor dashboard. I will leave the remaining steps up to you in the exercises.
Notice that this script already became quite long despite the fact that we create only a really simple app here. This is why is can be helpful to keep the UI and the server in separate files. By keeping two scripts ui.R and server.R in a single directory you can split up the code for the UI and the server. The easiest way to try that out is to simply create a new Shiny app via the green plus button in RStudio and indicate that you want the code stored in two separate files on the windows that appears when you hit that button.
13.4 Deploying a Shiny Application
Usually, once your app is finished, you will want to publish it somewhere. Unfortunately, publishing Shiny apps is not quite as easy as simply pushing some knitted Rmd-file to a repository. Simply said, what we need is to host the Shiny application on a server that knows how to run Shiny applications.
Fortunately, there is shinyapps.io where you can host your Shiny apps for free. This is also the service where our Taylor Swift app is currently hosted. All you need to do in order to release a Shiny app to shinyapps is to register an account and once you have that, you can use the RStudio publishing integration. Simply hit on the small blue icon nect to your “Run App” button and follow the instruction to log in to your shinyapps account and transfer the application online.
13.5 Exercises
13.5.1 Create an Account with shinyapps.io
Head over to shinyapps.io and sign up for a free account. You will need this account to publish your finalized Taylor Swift application later on.
Important: Make sure that the code which creates your shiny app is also available in the exercise repository.
13.5.2 Add a Headline
As you can see here, the sidebar panel for the song attribute should contain a heading “Filter by Attribute Range”. Include this headline.
13.5.3 Filter Table by Attribute
The last version of the Taylor Swift dashboard we created in this chapter included a slider but does not actually use the slider for anything. Change the code so that the attribute data table displays only those songs with an attribute value that is in the range determined by the sliders.
13.5.4 Use Options of Data Table
The “final” version of the dashboard makes sure that the data table displays at most 10 rows and does not give us the option to change that. Further, the table’s rows are arranged by the attribute’s value in descending order. Also, the table includes no search bar and does not allow for a change of number of rows.
Tweak with data table’s options so that your dashboard has the same output. Hint: You may have to find suitable options in the documentation.
13.5.5 Create and Display Plots
Take a look at the final plot here which uses geom_jitter
and alpha = 0.25
to plot Album
against the selected attribute.
Also, the large plotted points visualize an album’s median attribute value.
Further, the slider positions determine the range of the x-axis in this plot.
Use renderPlot()
and plotOutput()
to include this plot in your dashboard.
Hint: The medians can be computed like this: