library(shiny)
# 1. Business Logic
# This code only needs to run once - when the App opens
# It does not have to react to any user inputs
# 2. Define the UI for the app to get inputs and display outputs
ui <- fluidPage(
## Stuff for defining user-interface
## e.g. Title,
## Table of contents
## Location of inputs (sliders and buttons)
)
# 3. Define server logic to manipulate input objects, data,
# to produce output objects
server <- function(input, output) {
## Stuff for running R code
## e.g. Run a linear model.
## Manipulate a data frame
## Make a plot.
}
# Create the application
shinyApp(ui = ui, server = server)
10 Shiny Basics
R Shiny, Shiny UI, Shiny Server, shiny input elements, shiny output elements
10.1 Introduction
10.1.1 Learning Outcomes
- Develop basic Shiny Apps using the {shiny} package.
- Create new apps using the appropriate directory structure.
- Create various elements in the user interface portion of the app code.
- Connect User Input IDs and Output IDs to create dynamic elements in the User Interface.
10.1.2 References:
- R for Data Science 2nd Edition Wickham, Cetinkaya-Rundel, and Grolemund (2023)
- Mastering Shiny Wickham (2022)
- {shiny} package Chang et al. (2022)
- {DT} package Xie, Cheng, and Tan (2023)
10.1.2.1 Other References
- Shiny Cheatsheet.
- Shiny Website Posit (2023)
- Shiny Examples.
- Shiny Videos on YouTube
- Engineering Production-Grade Shiny Apps Fay et al. (2023)
10.2 Overview of Shiny
As a data scientist, you will collaborate with researchers / managers / clients who do not know or have access to R or python.
You may need to allow them to explore the data/analysis themselves, or you want to interactively explore the data together.
Shiny lets you build an app you can run in a browser while R (or Python) runs underneath.
As examples:
- The Shiny package has eleven built-in examples that each demonstrate how Shiny works, e.g.,
shiny::runExample("01_hello")
. - Folks showcase some pretty sophisticated interactive data visualizations Shiny Demo Gallery.
- If you get really good at Shiny, you can make sophisticated websites with it: Better Shoes.
Shiny acts as a wrapper if you will for HTML and CSS as it translates your R code into HTML and CSS sufficient to run an app.
- You can build apps without learning HTML, CSS, or JavaScript.
- However, just a little knowledge can help you customize your app to achieve the look and feel you want.
Shiny Apps Support Interactive Exploration and Visualization through three visible operations:
- Allow the user to choose inputs.
- Execute code to create input objects and manipulate them to produce output objects.
- Display static and dynamic output back to the user.
Behind the scenes, Shiny apps employ Reactive Programming to execute multiple tasks:
- Track object updates and the dependencies among objects.
- React to input and output object updates.
- Synchronize across all object updates.
Shiny apps have two main parts as seen in Figure 10.1.
- The user interface (UI) runs in a web browser.
- The shiny app server code runs on a machine set up as shiny server (your local computer for now).
10.3 Creating Shiny Apps
You write shiny apps in .R files.
- Basic apps have all the code in one file that should be named
app.R
. - More complicated (or older) apps may use two files: one for the user interface code and one for the server code.
- Even more complicated apps use multiple files organized as Shiny Modules.
- It’s a trade off between complexity and maintainability.
We will work with one file for now.
10.3.1 Directory or Folder Structure for {shiny} Apps.
Since the main code must be in a file with a specific name, e.g., app.R
, you can’t manage or distinguish apps by file name of the file with your code.
- Use a distinct top-level folder name to distinguish between apps, e.g.
myfirstapp
.
The following structure is recommended for your {shiny} apps to allow you to manage them AND to prepare for success:).
Folder - myfirstapp\
data-raw\
: for original (messy) data and files with code to clean it.data\
: for cleaned processed data. Each file in this directory should be a .Rds file (created byreadr::write_rds()
.images\
a sub-directory for any images needed by the app.tests\
: for test code and filesvignettes\
: R Markdown files for your vignette
Recommend using this structure for your final project.
If you want to create your app as a package, you will use a R\
: for all files with code needed to run the app - see Chapter 20 Mastering Shiny - Packages and Engineering Production Grade Shiny.
10.3.2 File Naming Conventions
Since all code files for running the app are together at the top level of the folder, a file naming convention can help organize the files for easier maintenance.
The following may be useful for more complicated apps.
app_*.R
(or app_ui.R and app_server.R) file(s) contain the top level functions defining your user interface and your server function.If your app is large enough to use modules or external functions and utilities suggest the following:
fct_*.R
files contains the business logic, potentially large functions. They are the backbone of the application and may not be specific to a given module.mod_*.R
files contain a unique module (if you use them). Many Shiny apps contain a series of tabs, or at least a tab-like pattern, so number them according to their step in the application.- Tabs are almost always named in the user interface, so use the tab-name as the file name.
- For example, if the first tab is called “Import”, name its file
mod_01_import.R
.
utils_*.R
files contain small helper functions.- For example, you might have a
not_na()
, which isnot_na() <- Negate(is.na)
, anot_null()
, or other small tools used application-wide.
- For example, you might have a
10.3.3 Structure of a {Shiny} App.
Every app.R
File has the same basic structure with three main sections followed by the line with the function call to create the app object.
- Business Logic Section
- User Interface Section
- Server Section
The sections of the app.R
File Handle Different Tasks
- Business Logic
- Implements code that does not depend upon, or need to, react to user input or later analysis, e.g.,
- Load the shiny library (and other libraries),
- Load pre-determined data files,
- Manipulate the pre-loaded data to tidy, clean, and create the information you need for the user (if not already done and saved in the data folder as a
.Rds
file). - Define functions to simplify your later code.
- All of this code should be able to be run in a normal
.R
script file to debug or potentially operate separately.
- User Interface
- Defines the user interface using a page layout function such as the
fluidPage()
function.- The user interface defines things like page layout (title, headers, table of contents), and sources of input (number sliders and buttons).
- Saves the output of
fluidPage()
to a variable (calledui
above).
- Server Logic
- The code to do everything else required to respond to user inputs and create new output values.
- Creates the server logic as a single R function e.g.,
server <- function(){...}
.- This is just regular R code wrapped in some special Shiny functions.
- The shiny functions allow access to the input objects in the shiny app environment.
- You can use input to manipulate data frames, fit statistical models, or make plots, etc..
- Last Line
- Calls the
shinyApp()
function, with the names of the outputs fromfluid_page()
and the server function.- This is the same for all
app.R
files.
- This is the same for all
- Note: Shiny has several UI Page Layout Functions besides
fluidPage()
10.3.4 Your First Shiny App
10.3.4.1 Use RStudio to Create a Directory and app.R
File for a new Shiny App
You can create a basic Shiny app in RStudio by clicking File/New "Shiny Web App...
:
A pop-up window will ask for the name of your new shiny app and the directory where you want it created.
- The name needs to follow the R conventions for variable naming.
- The name cannot start with a number or use special characters
RStudio will create a new folder under the directory you specified with the name you entered for your App.
Let’s call our shiny app my_first_app
.
RStudio provides a complete starter template file app.R
under the new directory.
- It should look like this:
#
# This is a Shiny web application. You can run the application by clicking
# the 'Run App' button above.
#
# Find out more about building applications with Shiny here:
#
# http://shiny.rstudio.com/
#
library(shiny)
# Define UI for application that draws a histogram
ui <- fluidPage(
# Application title
titlePanel("Old Faithful Geyser Data"),
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
sliderInput("bins",
"Number of bins:",
min = 1,
max = 50,
value = 30
)
),
# Show a plot of the generated distribution
mainPanel(
plotOutput("distPlot")
)
)
)
# Define server logic required to draw a histogram
server <- function(input, output) {
output$distPlot <- renderPlot({
# generate bins based on input$bins from ui.R
x <- faithful[, 2]
bins <- seq(min(x), max(x), length.out = input$bins + 1)
# draw the histogram with the specified number of bins
hist(x, breaks = bins, col = "darkgray", border = "white")
})
}
# Run the application
shinyApp(ui = ui, server = server)
10.3.4.2 Running your App
With this app.R
file open, click on "
Run App"
at the top right of your Source
panel.
Notice your console is busy.
You cannot run R code in the console while the shiny app is running.
Your console should say something like “
Listening on http://127.0.0.1:6342
”, but the url will be different.- You can put the URL into your web browser to see the running Shiny App.
- Don’t worry, it’s still on your computer. The URL just points to a location on your machine.
Exit out of the Shiny App by:
- Clicking the stop sign on your Viewer Tab, or,
- Hitting CTRL + C while you are in the console, or,
- Closing the Shiny App window.
10.4 User Interface Input Elements
The user interface is defined by everything within the fluidPage()
section.
fluidpage()
has a simple default layout with a sidebar for inputs and a large main area for output.
Shiny provides a number of page layout functions besides fluidpage()
.
- Each serves as a template for creating common layouts for user interface elements to collect user inputs.
- These include:
fillPage()
,fixedPage()
,flowLayout()
,navbarPage()
,sidebarLayout()
,splitLayout()
, andverticalLayout()
.
There can be hierarchical (nested) layouts within each of these to enable very rich and complex UIs.
10.4.1 Let’s Create a Bare Bones Template
Create a new shiny app called “tryouts
”.
To make it bare bones:
- Delete all the template code.
- Start typing
shinyapp
in the file and selectshinyapp {snippet}
from the auto-complete list to generate the following.
- Recommend using the above as a template for all future Shiny apps.
- It is the minimal amount of coded needed to start a Shiny app.
Running this app should just produce a blank HTML page in the browser.
10.4.2 User Input functions
Populate fluidPage()
, with calls to shiny functions for input elements with each call separated by a comma.
Four common arguments to fluidpage()
are calls to functions for different types of UI input elements:
textInput()
numericInput()
sliderInput()
selectInput()
Each of these input functions requires at least three arguments:
inputId
: The name of the input object. Theserver()
function will access user inputs through theinputId
, so it needs to follow the conventions for variable naming.- character strings with no spaces and can’t start with a number
label
: The name displayed on the web-app to the user.value
: A default input.
Each input function may also take many more arguments specific to that type of input.
- The page layout function creates an R list of the
inputIds
so the server function can find them.
A difficult error to debug is when an inputId
in the server side has a typo in it.
The server function just ignores the unknown ID but does not stop or generate an error message.
If you are having trouble with an app not doing what you expect and there is no error message, check the id
name is spelled the same in the UI section and the server section.
10.4.3 Get Text Inputs with text_input()
, passwordInput()
and textAreaInput()
textInput()
accepts one line of text from the user.
passwordInput()
accepts one line of text which is not displayed on the screen as it is entered. (NOTE: This is not a secure way to collect passwords by itself.)
textAreaInput()
accepts multiple lines of text.
Add these elements to your tryouts
app:
Running the app, you should get something like this:
Later on, we’ll cover how to access these inputs in the server function()
.
- Change the
width
andheight
arguments intextAreaInput()
. What does it do?
It changes the default width and height of the text box.
- Change the
value
argument intextInput()
. What does it do?
It changes the default text in the text box.
10.4.4 Get Numeric Inputs with numericInput()
and sliderInput()
numericInput()
creates a text box that only accepts numeric values.
sliderInput()
creates a numeric slider.
- Giving the
value
argument one number results in a one-sided slider. - Giving the
value
argument a vector of two numbers results in a two-sided slider (min, max).
library(shiny)
ui <- fluidPage(
numericInput("num", "Number one", value = 0, min = 0, max = 100),
sliderInput("num2", "Number two", value = 50, min = 0, max = 100),
sliderInput("rng", "Range", value = c(10, 20), min = 0, max = 100)
)
server <- function(input, output) {
}
shinyApp(ui = ui, server = server)
Running the app, you should get something like this:
- Only use sliders for small ranges where the exact number is not important.
What does the animate
option do when you set it to TRUE
?
If you push play, the slider slowly slides up until it reaches the maximum value.
- You can see more on sliders at Using Sliders.
10.4.5 Get Date Inputs with dateInput()
and dateRangeInput()
Use dateInput()
to collect a single date.
Use dateRangeInput()
to collect two dates.
Running the app, you should get something like this:
10.4.7 Select Columns of a Data Frame with varSelectInput()
varSelectInput()
allows a user to select a variable (column) from a data frame already loaded in the environment.
- We will do this a lot!
Running the app, you should get something like:
varSelectInput()
gives the server function access to a name
object.
- You may not have worked with these as programming objects.
- You can read about them using
help(name)
.
input$var_name
is a name (aka symbol) object in the Shiny environment.
When input$var_name
is the ID for a varSelectInput()
(selecting a data frame variable) you have to treat it differently in tidyverse and Base R functions.
If you want to use the input$var_name
as a variable name in a tidyverse function, you need to prefix the input$
with the injection operator (aka bang-bang operator) !!
from the {rlang} package.
- The
!!
in!!input$var_name
modifies the environment “name” object ( a character value) into a data frame variable name before it is evaluated in the tidyverse function.
To use the input$var_name
in a base R function (that does not use data masking), use it inside the [[]]
operator as in [[input$var_name]]
.
The !!
is also known as the “Unquote” operator in that it removes the “quotes” from a “quoted” function or expression.
- In this case, we are using an “indirect reference” expression. The
input$var
variable contains a value which is also a variable name from the data frame. - This is part of R’s approach to meta-programming discussed in Advanced R Chapter 19
- Unquoting allows you to selectively evaluate parts of the expression that would otherwise be quoted.
- Use
!!
to unquote a single argument in a function call. !!
takes a single expression, evaluates it, and fits it into the evaluation of the overarching function (the abstract syntax tree (AST)).
We’ll do an example of this in the “Putting Inputs and Outputs Together” section, Section 10.6.
10.4.8 Get Binary (TRUE/FALSE) Inputs with checkboxInput()
Use checkboxInput()
to get a TRUE
/FALSE
or Yes/No answer.
Running the app, you should get something like this:
10.4.9 Get File Names for Inputs with fileInput()
fileInput()
allows users to input one or more file names.
Running the app, you should get something like this:
10.5 User Interface Output Elements
Output functions create placeholders for the values of the objects created in the server function()
(like plots and tables) to be displayed as output in the User Interface.
Each output function’s first argument is its outputId
, just like the input elements.
- The server
function()
can access this element as an element of theoutput
list. - For example, if the first argument is
outputId="plot"
, the server function can insert a plot intooutput$plot
.
Each UI output function has an associated render function in the server function()
.
- A render function first uses the code in the R expression (that you wrote inside
{ }
) to create an R output object, e.g., text, or a table, or a plot. The function then “renders” the object into HTML code that will generate the output produced by the expression and passes it to theui()
function which will pass the HTML to the browser to display the output. - As an example, if you create a
renderPlot()
expression to create a plot object, say usingggplot2()
, therenderPlot()
function will generate the HTML for displaying the plot and pass to theui()
function to put in theoutput$plot
placeholder in the user interface.
10.5.1 Display Text Output with textOutput()
or verbatimTextOutput()
Use textOutput()
to display text.
Use verbatimTextOutput()
to display code.
Create text objects in the server function()
with either renderText()
or renderPrint()
.
renderText()
will display text returned by code. Functions can only return one thing.renderPrint()
will display text printed by code. Functions can print multiple things.- Use
verbatimTextOuput()
withrenderPrint()
to print results from outputs oft.test()
orlm()
.
Running the app, you should get something like this:
Change the outputId
from “text” to something else. Make sure the Shiny App still works.
10.5.2 Display Output Tables with tableOutput()
or dataTableOutput()
tableOutput()
prints an entire table created in the server by renderTable()
.
- Should only be used for small tables.
Running the app, you should get something like this:
dataTableOutput()
displays a dynamic table created in the server by renderDT()
.
Shiny used to use the renderDataTable()
function to create dynamic interactive tables. However that has been superseded and replaced by renderDT()
.
The renderDT()
function is from the {DT} package (Data Tables package) which provides access to the JavaScript library DataTables. This package has more options for customizing your interactive tables.
You will need to install the package using the console and for each app, load the {DT} package in the business logic section with library(DT)
.
Running the app, you should get something like this:
You can change the appearance of dataTableOutput()
by passing arguments as a list to the options
argument in renderDT()
.
- You can find these options at: https://datatables.net/reference/option/.
N
10.5.3 Output Plots to the UI with plotOutput()
plotOutput()
displays plots created by renderPlot()
in the server()
function.
- Note we need to load {ggplot2} as well as {shiny}.
library(shiny)
library(ggplot2)
ui <- fluidPage(
plotOutput("plot", width = "60%")
)
server <- function(input, output, session) {
output$plot <- renderPlot({
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point() +
theme_bw() +
xlab("Displacement") +
ylab("Highway MPG")
})
}
shinyApp(ui = ui, server = server)
Running the app, you should get something like this:
Change the height and width of the plot by changing the argument values in renderPlot()
.
Change the height and width of the plot by changing the argument values in plotOutput()
.
Show code
10.6 Putting Inputs and Outputs Together
Let’s build a simple shiny app that lets the user choose two variables from mtcars
and then plots them.
- Start with the app template and make it bare bones.
- Load {shiny} and {ggplot2} at the beginning of the app, in the business logic section, before the
ui <- fluidpage(...)
. - Add the UI inputs and outputs.
- Add the server code to render the plot and assign to an output object.
library(shiny)
library(ggplot2)
ui <- fluidPage(
varSelectInput("var1", "Variable 1", data = mtcars),
varSelectInput("var2", "Variable 2", data = mtcars),
plotOutput("plot")
)
server <- function(input, output) {
output$plot <- renderPlot({
ggplot(mtcars, aes(x = !!(input$var1), y = !!input$var2)) +
geom_point()
})
}
shinyApp(ui = ui, server = server)
Running the app, you should get something like this:
Notice how in the server function()
,
We accessed the inputs via the
input
variable.- These inputs must be used inside a render function to obtain reactivity.
We set the outputs to the
output()
variable.Notice the use of the unquoting function
!!
in front ofinput$var1
andinput$var2
.- You have to do this to use data frame variable names in functions from the tidyverse ({ggplot}, {dplyr}, {tidyr}, etc) (but not base R where variable names are evaluated differently).
Let the user choose whether they want “red” points or “blue” points.
library(shiny)
library(ggplot2)
ui <- fluidPage(
varSelectInput("var1", "Variable 1", data = mtcars),
varSelectInput("var2", "Variable 2", data = mtcars),
radioButtons("color", "What Color?",
choiceNames = c("Red", "Blue"),
choiceValues = c("red", "blue")
),
plotOutput("plot")
)
server <- function(input, output) {
output$plot <- renderPlot({
ggplot(mtcars, aes(x = !!input$var1, y = !!input$var2)) +
geom_point(color = input$color) #- Notice the non-use of `!!`
})
}
shinyApp(ui = ui, server = server)
Running the app, you should get something like this:
10.6.1 Identifying varSelectInput()
Data Frame Variables in Base R Functions
The tidyverse uses a special way of creating environments which allows it to use “tidy evaluation” when evaluating tidyverse functions.
- This has the benefit of speeding up interactive analysis of data as you do not have to fully specify the variable names inside some tidyverse functions that use “data masking”.
- However, it has the downside of making it hard to program with tidyverse functions as you have to be careful about how you identify the variables indirectly.
- Shiny makes things a bit more complicated as the objects that get passed between input and output are not “normal” variables.
You must identify the input variables differently in tidyverse functions than in Base R functions.
The following example uses the Base R (non-tidyverse) functions is.factor()
and lm()
in the server()
function.
- Note the use of the
[[]]
subset operator which matches the character “name object” to the names of the variable. - See Chapter 12 of Mastering Shiny for a discussion of data variables and environment variables.
- See help for
[[
as well.
library(shiny)
ui <- fluidPage(
varSelectInput("y", "y", data = iris, selected = "Sepal.Length"),
varSelectInput("x", "x", data = iris, selected = "Sepal.Width"),
checkboxInput("show_str", "Show str(X) if X is factor?"),
verbatimTextOutput("test_str"),
verbatimTextOutput("lm_results")
)
server <- function(input, output, session) {
output$test_str <- renderPrint({
if (input$show_str) {
if (is.factor(iris[[input$x]])) {
# str(input$x) # shows without [[ ]]
str(iris[[input$x]])
} # if factor
} # end if show_str
}) # end render Print
output$lm_results <- renderPrint({
lmout <- lm(iris[[input$y]] ~ iris[[input$x]], data = iris)
print(summary(lmout))
})
}
shinyApp(ui, server)
10.6.2 Variations of Text and Data Output
Given the different types of text elements, it can be confusing to ensure the input and output objects align with respect to formats.
Consider the following examples of alignment in the code and the results in Figure 10.2.
Notice what happens when you use broom::tidy()
to convert the list output from the analysis of variance function aov
to a data frame.
library(shiny)
library(broom)
data("mtcars")
aout <- aov(mpg ~ as.factor(cyl), data = mtcars)
ui <- fluidPage(
textOutput("character_text"),
textOutput("character_variable"),
verbatimTextOutput("code"),
tableOutput("my_tidy_table"),
dataTableOutput("my_data_table")
)
server <- function(input, output, session) {
output$character_text <- renderText({
glue::glue("These are the names of the {length(mtcars)} variables in `mtcars`")
})
output$character_variable <- renderText({
names(mtcars)
})
output$code <- renderPrint({
aout
})
output$my_tidy_table <- renderTable({
tidy(aout)
})
output$my_data_table <- renderDataTable({
tidy(aout)
})
}
shinyApp(ui = ui, server = server)
10.6.3 Adjusting Font Size for Plot Axis Titles and Axis Text
Shiny can default to plot axis labels that are too small to read.
The following code is one example of creating a theme as seen in Figure 10.3.
- Note the theme object is created in the business logic section, as a variable you can add to multiple plots instead of typing the same theme code over and over.
We will cover much more about themes later.
library(shiny)
library(ggplot2)
my_theme <- theme(
axis.title.x = element_text(size = rel(1.5), color = "red"),
axis.title.y = element_text(size = rel(2.5), color = "blue"),
axis.text.x = element_text(size = 14, face = "bold.italic"),
axis.text.y = element_text(size = 16, angle = 180)
)
ui <- fluidPage(
varSelectInput("var1", "Variable 1", data = mtcars),
varSelectInput("var2", "Variable 2", data = mtcars),
plotOutput("plot")
)
server <- function(input, output) {
output$plot <- renderPlot({
ggplot(mtcars, aes(x = !!(input$var1), y = !!input$var2)) +
geom_point() +
my_theme
})
}
shinyApp(ui = ui, server = server)