Vocabulary used by the R Shiny Library, and a few example apps.
All Shiny apps are made up from the same few building blocks. These notes review the main types of blocks. When reading code from more complex apps, it can be helpful to try to classify pieces of the code into these types of blocks.
The highest level breakdown of Shiny app code is between ui
and server
components. The ui
controls what the app looks like. It stands for “User
Interface.” The server
controls what the app does. For example, the app
below defines a title and textbox where users can type. But it does not do
anything, since the server is empty.
The UI elements can be further broken down into Inputs, Outputs, and
Descriptors1, all grouped together by an organizing
layout function. Inputs are UI elements that users can manipulate to prompt
certain types of computation. Outputs are parts of the interface that reflects
the result of a server
computation. Descriptors are parts of the page that
aren’t involved in computation, but which provide narrative structure and guide
the user.
For example, in the toy app above, titlePage
is a descriptor providing
some title text. textInput
is an input element allowing users to enter
text. fluidPage
is a layout function that arranges these elements on a
continuous page (some other layout functions are sidebarLayout
,
navbarPage
, flowLayout
, …)
An important point is that all input and output elements must be given a
unique ID. This is always the first argument of a *Input
or *Output
function
defined in Shiny. The ID tags are how different parts of the application are
able to refer to one another. For example, if we wanted to refer to the text the
user entered in the application above, we could refer to the name
ID.
Let’s see how to (1) make user inputs cause some sort of computation and (2)
have the result of that computation appear to the user. For (1), we will add a
renderText
element to the server
. All render*
functions do two things,
ui
available for computation.For (2), we will add a textOutput
element to the ui
layout defined
above. Let’s look at the code,
library(shiny)
ui <- fluidPage(
titlePanel("Hello!"),
textInput("name", "Enter your name"),
textOutput("printed_name")
)
server <- function(input, output) {
output$printed_name <- renderText({
paste0("Welcome to shiny, ", input$name, "!")
})
}
app <- shinyApp(ui, server)
There are a few points worth noting. First, the renderText
component was
able to refer to the value entered in the textbox using input$name
. This was
possible because name
was the ID that we gave to the textInput
component. It
would not have worked if we had used input$text
outside of a render*
function: this is what we mean by the render*
functions making the UI inputs
available for computation. Finally, we were able to refer to the rendered output
in the UI by adding a textOutput
component. By giving this component the id
printed_name
, we were able to tell it to look into the server for a rendered
output named printed_name
and fill it in.
An even deeper idea is that the code did not simply run linearly, from top of
the script to the bottom. If that were all the code did, then it would have run
once at the beginning, and it would never have updated when you entered your
name. Instead, it ran every time you typed into the textbox. This is the
“reactive programming” paradigm, and it is what makes interactive visualization
possible. renderText
knows to rerun every time something is entered into the
name
text input, because we told it to depend on input$name
. We will explore
the idea of reactivity in more depth in the next lecture, but for now, just
remember that the order in which code is executed is not simply determined by
the order of lines in a file.
Let’s look at a few more examples, just to get a feel for things. The app
below updates a plot of random normal variables given a mean specified by the
user. We’ve introduced a new type of input, a numericInput
, which captures
numbers. We’ve also added a new output, plotOutput
, allowing with its
accompanying renderer, renderPlot
(remember, UI outputs are always paired with
server renderers).
library(shiny)
library(tidyverse)
ui <- fluidPage(
titlePanel("Random Normals"),
numericInput("mean", "Enter the mean", 0), # 0 is the default
plotOutput("histogram")
)
server <- function(input, output) {
output$histogram <- renderPlot({
data.frame(values = rnorm(100, input$mean)) %>%
ggplot() +
geom_histogram(aes(values))
})
}
app <- shinyApp(ui, server)
We can make the plot depend on several inputs. The code below allows the user to change the total number of data points and the variance, this time using slider inputs. I recommend taking a look at different inputs on the shiny cheatsheet, though be aware that there are many extensions built by the community.
library(shiny)
library(tidyverse)
ui <- fluidPage(
titlePanel("Random Normals"),
numericInput("mean", "Enter the mean", 0),
sliderInput("n", "Enter the number of samples", 500, min=1, max=2000),
sliderInput("sigma", "Enter the standard deviation", 1, min=.1, max=5),
plotOutput("histogram")
)
server <- function(input, output) {
output$histogram <- renderPlot({
data.frame(values = rnorm(input$n, input$mean, input$sigma)) %>%
ggplot() +
geom_histogram(aes(values), bins = 100) +
xlim(-10, 10)
})
}
app <- shinyApp(ui, server)
We can also make the app return several outputs, not just a plot. The code below attempts to print the data along in addition to the histogram, but it makes a crucial mistake (can you spot it?).
library(shiny)
library(tidyverse)
ui <- fluidPage(
titlePanel("Random Normals"),
numericInput("mean", "Enter the mean", 0),
sliderInput("n", "Enter the number of samples", 500, min=1, max=2000),
sliderInput("sigma", "Enter the standard deviation", 1, min=.1, max=5),
plotOutput("histogram"),
dataTableOutput("dt")
)
server <- function(input, output) {
output$histogram <- renderPlot({
data.frame(values = rnorm(input$n, input$mean, input$sigma)) %>%
ggplot() +
geom_histogram(aes(values), bins = 100) +
xlim(-10, 10)
})
output$dt <- renderDataTable({
data.frame(values = rnorm(input$n, input$mean, input$sigma))
})
}
app <- shinyApp(ui, server)
The issue is that this code reruns rnorm
for each output. So, even though
the interfaces suggests that the printed samples are the same as the ones in the
histogram, they are actually different. To resolve this, we need a way of
storing an intermediate computation which (1) depends on the inputs but (2)
feeds into several outputs. Whenever we encounter this need, we can use a
reactive expression. It is a type of server element that depends on the input
and can be referred to directly by outputs, which call the reactive expression
like a function. For example, the code below generates the random normal samples
a single time, using the samples()
reactive expression.
library(shiny)
library(tidyverse)
ui <- fluidPage(
titlePanel("Random Normals"),
numericInput("mean", "Enter the mean", 0),
sliderInput("n", "Enter the number of samples", 500, min=1, max=2000),
sliderInput("sigma", "Enter the standard deviation", 1, min=.1, max=5),
plotOutput("histogram"),
dataTableOutput("dt")
)
server <- function(input, output) {
samples <- reactive({
data.frame(values = rnorm(input$n, input$mean, input$sigma))
})
output$histogram <- renderPlot({
ggplot(samples()) +
geom_histogram(aes(values), bins = 100) +
xlim(-10, 10)
})
output$dt <- renderDataTable(samples())
}
app <- shinyApp(ui, server)
Finally, a good practice is to move as much non-app related code to separate functions. This makes the flow of the app more transparent. The clearer the delineation between “computation required for individual app components” and “relationship across components,” the easier the code will be to understand and extend.
library(shiny)
library(tidyverse)
### Functions within app components
generate_data <- function(n, mean, sigma) {
data.frame(values = rnorm(n, mean, sigma))
}
histogram_fun <- function(df) {
ggplot(df) +
geom_histogram(aes(values), bins = 100) +
xlim(-10, 10)
}
### Defines the app
ui <- fluidPage(
titlePanel("Random Normals"),
numericInput("mean", "Enter the mean", 0),
sliderInput("n", "Enter the number of samples", 500, min=1, max=2000),
sliderInput("sigma", "Enter the standard deviation", 1, min=.1, max=5),
plotOutput("histogram"),
dataTableOutput("dt")
)
server <- function(input, output) {
samples <- reactive({
generate_data(input$n, input$mean, input$sigma)
})
output$histogram <- renderPlot(histogram_fun(samples()))
output$dt <- renderDataTable(samples())
}
app <- shinyApp(ui, server)
For attribution, please cite this work as
Sankaran (2024, Feb. 17). STAT 436 (Spring 2024): Elements of a Shiny App. Retrieved from https://krisrs1128.github.io/stat436_s24/website/stat436_s24/posts/2024-12-27-week04-01/
BibTeX citation
@misc{sankaran2024elements, author = {Sankaran, Kris}, title = {STAT 436 (Spring 2024): Elements of a Shiny App}, url = {https://krisrs1128.github.io/stat436_s24/website/stat436_s24/posts/2024-12-27-week04-01/}, year = {2024} }