Elements of a Shiny App
Vocabulary used by 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
andserver
components. Theui
controls what the app looks like. It stands for “User Interface.” Theserver
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.library(shiny) ui <- fluidPage( titlePanel("Hello!"), textInput("name", "Enter your name") # first arg is ID, second is label ) server <- function(input, output) {} app <- shinyApp(ui, server)
-
The UI elements can be further broken down into Inputs, Outputs, and Descriptors[1], 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 aresidebarLayout
,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 thename
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 theserver
. Allrender*
functions do two things,- They make inputs from the
ui
available for computation. - They generate HTML code that allows the results of the computation to appear in a UI output.
For (2), we will add a
textOutput
element to theui
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)
- They make inputs from the
-
There are a few points worth noting. First, the
renderText
component was able to refer to the value entered in the textbox usinginput$name
. This was possible becausename
was the ID that we gave to thetextInput
component. It would not have worked if we had usedinput$text
outside of arender*
function: this is what we mean by therender*
functions making the UI inputs available for computation. Finally, we were able to refer to the rendered output in the UI by adding atextOutput
component. By giving this component the idprinted_name
, we were able to tell it to look into the server for a rendered output namedprinted_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 thename
text input, because we told it to depend oninput$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 thesamples()
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)
[1] I like to use these names to keep everything organized, but they are not standard in the community.