Creating Interactive Apps

Overview

Shiny is a popular tool that allows users to build interactive web applications without the normally pre-requisite web development expertise. In addition to Shiny apps being simpler to build for the programmer they are often used to allow visitors to perform coding tasks without ever actually writing code. These are huge advantages because they reduce or eliminate significant technical barriers in developing truly interactive applications.

In synthesis contexts, Shiny can be used for a variety of valuable purposes. You can use it to develop dashboards for sharing data with related communities, allow your team to quickly “play with” exploratory graphs, or even to create a data submission portal (as is the case with some Research Coordination Networks or “RCNs”).

Note that Shiny can be built in either R or Python ‘under the hood’ but for the purposes of this module we’ll focus on R.

Learning Objectives

After completing this topic you will be able to:

  • Define the three fundamental components of a Shiny app
  • Explain benefits and limitations of interactive approaches to data exploration
  • Generate an interactive app with Shiny
  • Use text formatting methods in a Shiny app
  • Explore available Shiny layout options
  • Create a Shiny app
  • Describe (briefly) the purpose of deploying a Shiny app

Needed Packages

If you’d like to follow along with the code chunks included throughout this module, you’ll need to install the following packages:

# Note that these lines only need to be run once per computer
## So you can skip this step if you've installed these before
install.packages("tidyverse")
install.packages("shiny")
install.packages("htmltools")
install.packages("lterdatasampler")

We’ll load the Tidyverse meta-package here to have access to many of its useful tools when we need them later as well as the shiny package.

# Load needed libraries
library(tidyverse); library(shiny)

Shiny Fundamentals

All Shiny apps are composed of three pieces: a user interface (UI), a server, and a call to the shinyApp function. The user interface includes everything that the user sees and can interact with; note that this includes both inputs and outputs. The server is responsible for all code operations performed on user inputs in order to generate outputs specified in the UI. The server is not available to the user. Finally, the shinyApp function simply binds together the UI and server and creates a living app. The app appears either in your RStudio or in a new tab on a web browser depending on your settings.

For those of you who write your own functions, you may notice that the syntax of Shiny is very similar to the syntax of functions. If you have not already, your quality of life will benefit greatly if you turn on “rainbow parentheses” in RStudio (Tools Global Options Code Display Check “Use rainbow parentheses” box).

Let’s consider an artificially simple Shiny app so you can get a sense for the fundamental architecture of this tool.

# Define the UI
basic_ui <- shiny::fluidPage(
  "Hello there!"
)

# Define the server
basic_server <- function(input, output){ }

# Generate the app
shiny::shinyApp(ui = basic_ui, server = basic_server)
1
The fluidPage function is important for leaving flexibility in UI layout which we’ll explore later in the module
2
Because this app has no inputs or outputs, it doesn’t need anything in the ‘server’ component (though it still does require an empty server!)

If you copy and run the above code, you should see an app that is a blank white page with “Hello there!” written in the top left in plain text. Congratulations, you have now made your first Shiny app! Now, your reason for exploring this module likely involves an app that actually does something but the fundamental structure of all apps–even skeletal apps like this one–is the same. More complicated apps will certainly have more content in the UI and server sections but all Shiny apps will have this tripartite structure.

Interactive Apps

Now that we’ve covered non-reactive apps, let’s create an interactive one! It is important to remember that the user interface needs to contain both the inputs the user can make and the outputs determined by those inputs. The server will be responsible for turning the inputs into outputs but if you want your interactive app to actually show the user the interactivity you need to be careful to include the outputs in the UI.

Essentially all Shiny UI functions use the same syntax of <value class>Input or <value class>Output. So, determining how you want the user to engage with your app is sometimes as straightforward as identifying the class of the value you want them to interact with. Shiny calls these helper functions “widgets”.

Let’s consider an app that accepts a single number and returns the square root of that number.

# Define the UI ---- 
reactive_ui <- shiny::fluidPage(
  
  # Create input
  shiny::numericInput(inputId = "num_in",
                      label = "Type a number",
                      value = 16),
  
  # Include some plain text for contextualizing the output
  "Square root is: ",
  
  # Create output
  shiny::textOutput(outputId = "num_out")
  
) # Close UI

# Define server ----
reactive_server <- function(input, output){
  
  # Reactively accept the input and take the square root of it
  root <- shiny::reactive({
    sqrt(x = input$num_in)
  })
  
  # Make that value an output of the server/app
  output$num_out <- shiny::renderText(
    expr = root()
  ) 
  
} # Close server

# Generate the app ----
shiny::shinyApp(ui = reactive_ui, server = reactive_server)
1
Note that the argument name is capital “I” but lowercase “d”. Typing inputID is a common and frustrating source of error for Shiny app developers
2
Every element of the UI–except the last one–needs to end with a comma
3
All reactive elements (i.e., those that change as soon as the user changes an input) need to be specified inside of reactive with both parentheses and curly braces
4
The name of this input exactly matches the inputId we defined in the UI. That it is an input is defined by our use of the numericInput widget
5
The name of this output exactly matches the outputId we told the UI to expect.
6
Reactive elements essentially become functions in their own right! So, when we want to use them, we need to include empty parentheses next to their name

We included a lot of footnote annotations in that code chunk to help provide context but there are a few small comments that are worthwhile to bring up at this stage.

  1. UI outputs and server renders must match

The widget you use in the UI to return an output must correspond to the function used in the server to generate that output. In this example, we use textOutput in the UI so in the server we use renderText. Essentially all widgets in Shiny use this <class>Output versus render<Class> syntax which can be a big help to visual checks that your app is written correctly. You will need to be sure that whatever the ‘class’ is, it is lowercase in the UI but title case in the server (i.e., only first letter capitalized).

  1. Use section header format

This app is relatively short but we think effectively hints at how long and convoluted purpose-built Shiny apps can easily become. So, we recommend using section headers in your Shiny app code. You can do this by putting either four hyphens or four hashtags at the end of a comment line (e.g., # Section 1 #### or # My header ----). Headings defined in this way will appear in the bottom left of the “Source” pane of RStudio next to a light orange hashtag symbol. Clicking the text in that area will open a drop-down menu showing all headings in your current file and clicking one of the other headings will instantly jump you to that heading. This can be incredibly convenient when you’re trying to navigate a several hundred line long Shiny app. While rainbow parentheses can be useful for avoiding typos within a section, section headers make it much easier to avoid typos across sections.

If you don’t use headings already (or your cursor is on a line before the first heading), the relevant bit of the “Source” pane will just say “(Top Level)” and will not have the golden hashtag symbol.

Including Data

You can also use your Shiny app to work with a full data table! When running your app locally, you only need to read in the data as you normally would then run the app. By having read in the data you will ensure the object is in your environment and accessible to the app. However, keep in mind this will only work in “local” (i.e., non-deployed) contexts. See our–admittedly brief–discussion of deployment at the end of this module.

Let’s explore an example using data about fiddler crabs (Minuca pugnax) at the Plum Island Ecosystems (PIE) LTER site from the lterdatasampler R package. The app we’re about to create will make a graph between any two (numeric) columns.

# Load lterdatasampler package
library(lterdatasampler)

# Load fiddler crab data
data(pie_crab)

# Define the UI ---- 
data_ui <- shiny::fluidPage(
  
  # Let the user choose which the X axis
  shiny::selectInput(inputId = "x_vals",
              label = "Choose the X-axis",
              choices = setdiff(x = names(pie_crab),
                                y = c("date", "site", "name")),
              selected = "latitude"),
  
  # Also the Y axis
  shiny::selectInput(inputId = "y_vals",
              label = "Choose the Y-axis",
              choices = setdiff(x = names(pie_crab),
                                y = c("date", "site", "name")),
              selected = "size"),
  
  # Return the desired plot
  shiny::plotOutput(outputId = "crab_graph")
              
) # Close UI

# Define the server ----
data_server <- function(input, output){
  
  # Reactively identify X & Y axess
  picked_x <- shiny::reactive({ input$x_vals })
  picked_y <- shiny::reactive({ input$y_vals })
  
  # Create the desired graph
  output$crab_graph <- shiny::renderPlot(
    
    ggplot(pie_crab, aes(x = .data[[picked_x()]], y = .data[[picked_y()]])) +
      geom_point(aes(fill = .data[[picked_x()]]), pch = 21, size = 2.5) +
      labs(x = stringr::str_to_title(picked_x()),
           y = stringr::str_to_title(picked_y())) +
      theme_bw()
    
  ) # Close plot rendering
  
} # Close server

# Generate the app ----
shiny::shinyApp(ui = data_ui, server = data_server)
1
Note the loading of the data is done outside of the app! You can have the app load its own data but that is more complicated than this example needs to be.
2
To make our life easier in the server we can exclude non-number columns
3
See how we’re reactively grabbing both axes?
4
ggplot2 requires special syntax to specify axes with quoted column names (which is how reactive Shiny elements from that widget are returned)

Layouts

Experimenting with different app layouts can be a fun step in the process of making an app that is as effective as possible! We do recommend that during app development you stick with a very simple user interface because it’ll be easier to make sure your inputs and outputs work as desired. Once you are satisfied with those elements you can relatively easily chengs the UI to help guide users through your app.

As implied by that preface, layouts are exclusively an element of the user interface! This is great when you have an app with a complicated server component because you won’t need to mess with that at all to get the UI looking perfect. In the examples below, we’ll generate a non-interactive app so that we can really emphasize the ‘how to’ perspective of using different layouts.

Tab Panels

If you feel that your app is better represented in separate pages, tab panels may be a better layout choice! The result of this layout is a series of discrete tabs along the top of your app. If the user clicks one of them they’ll be able to look at a separate chunk of your app. Inputs in any tab are available to the app’s server and can be outputs in any tab (remember that their is a shared server so it is impossible for it to be otherwise!). Generally it may be a good idea to have inputs and outputs in the same tab so that users can see the interactive app responding to their inputs rather than needing to click back and forth among tabs to see the results of their inputs. For example, you could have an app where users choose what goes on either axis of several graph types and put each graph type on its own tab of the larger Shiny app.

# Define the UI
tabs_ui <- shiny::fluidPage(
  
# Define the layout type
  shiny::tabsetPanel(
  
    # Define what goes in the first tab
    shiny::tabPanel(title = "Tab 1",
                    "Hello from the first tab!"
                    ),
    
    # And in the second
    shiny::tabPanel(title = "Tab 2",
                    "Welcome to the second tab!"
                    ),
    
    # And so on
    shiny::tabPanel(title = "Tab 3",
                    "Hello yet again!"
                    ) ) )

# Define the server
basic_server <- function(input, output){ }

# Generate the app
shiny::shinyApp(ui = tabs_ui, server = basic_server)
1
This function is comparable to sidebarLayout in that if you want stuff above/below the tab panel area you’ll need to be outside of this function’s parentheses but still in the fluidPage parentheses
2
Again, just like the sidebarLayout subfunctions, you’ll need a comma after each UI element except the last one
3
Here we’re closing all of the nested UI functions

Other Layouts

We just briefly covered two layout options but hopefully this is a nice indication for the kind of flexibility in user interface that you can expect of Shiny apps! For more information, check out Posit’s Shiny Application Layout Guide. That resource has some really nice examples of these and other layout options that will be well worth checking out as you begin your journey into Shiny.

Text Formatting

Beyond making your app have an intuitive layout it can be really helpful to be able to do even simple text formatting to assist your app’s users. For instance, you may want to use sub-headings within the same UI layout component but still want to draw a distinction between two sets of inputs. Additionally you may want to emphasize some tips for best results or hyperlink to your group’s other products. All of these can be accomplished using text formatting tools that are readily available within Shiny.

# Load the `htmltools` library
library(htmltools)

# Define the UI
text_ui <- shiny::fluidPage(
  
  # Let's make some headings
  htmltools::h1("This is a Big Heading"),
  htmltools::h3("Smaller heading"),
  htmltools::h5("Even smaller heading!"), 
  
  # Now we'll format more text in various (non-heading) ways
  htmltools::strong("Bold text"),
  
  htmltools::br(),

  htmltools::a(href = "https://lter.github.io/ssecr/mod_interactivity.html",
               "This text is hyperlinked",
               target = "_blank"),
  
  htmltools::br(),
  
  htmltools::code("This is 'code' text") )

# Define the server
basic_server <- function(input, output){ }

# Generate the app
shiny::shinyApp(ui = text_ui, server = basic_server)
1
Headings (of any size) automatically include a line break after the heading text
2
The br function creates a line break
3
When the target argument is set to “_blank” it will open a new tab when users click the hyperlinked text. This is ideal because if a user left your app to visit the new site they would lose all of their inputs
4
Code text looks like this

Deployment

When Shiny apps are only being used by those in your team, keeping them as a code script works well. However, if you’d like those outside of your team to be able to find your app as they would any other website you’ll need to deploy your Shiny app. This process is outside of the scope of this module but is often the end goal of Shiny app development.

Take a look at Posit’s instructions for deployment for more details but essentially “deployment” is the process of getting your local app hosted on shinyapps.io which gives it a link that anyone can use to access/run your app on their web browser of choice.

Additional Interactivity Resources

Papers & Documents

Workshops & Courses

Websites

  • Posit’s Shiny website