Shiny Apps

class notes

App Development in R: Basic Principles

On our typical R code, we use imperative programming, that is to say, a programming style characterized by explicit sequences of commands designed to carry out various tasks1 Such as importing and exporting data sets, creating new objects, or applying transformations according to a specific set of instructions.. In other words, we would issue a specific command and have it carried out immediately.

In order to develop apps, we will be switching to reactive programming, a paradigm that allows us to automatically update outputs when inputs change. We will therefore express our commands as functions of user input (that is to say, our programs will react to the choices and actions of the user) (Wickham 2021).

Shiny apps can be easily designed using R language; they allow those not versed in web design to quickly build a reactive site to explore information2 You can find some examples here.. A Shiny app has two main components (Wickham 2021):

In this interactive setup, users will manipulate the ui (sending inputs to the server that will reflect their choices and actions), which, in turn, will cause the server to update the ui’s display (by running R code according to the instructions defined in our program and presenting an updated version of the outputs).

In a Shiny app, user interface elements and server-side functions are associated with each other through unique corresponding ids4 Besides being unique, ids must follow the same naming conventions as variables (no spaces, special characters, etc.). Each specific user input is identified with a unique id which is later used to reference and manipulate that specific ui element on the server side5 Consider, for instance, a simple app where the user is prompted to provide their name and date of birth: we would assign each input an id, for example “name” and “date_birth”. We would later refer to these user inputs in our functions using this ids (for example, in order to display a personalized greeting message by addressing each user by their name or wishing them a happy birthday on the right date)..

Implementation

The main package we will be using in order to build apps in R is the shiny package6 Throughout this section, we’ll be drawing on R’s official documentation on the subject.. In order to get our shiny app to work7 We will usually save this script with a name such as app.R, we will first create the objects server and ui8 Note that you may use any names you like for this objects, you will simply need to make sure to refer to the object in the same way when calling the shinyApp() function. For example: shinyApp(ui = my_original_ui_name, server = my_original_server_name), and we will then proceed to combine both elements using the shinyApp(ui,server) function9 For comprehensive information on various Shiny app design options, please refer to the Shiny Cheatsheet.. Basically, the structure of our code will be the following:

library(shiny)

ui <- fluidPage(
  # app appearance
)

server <- function(input, output) {
  # app's internal mechanisms
}

shinyApp(ui = ui, server = server)

After completing the app design, we can initiate it by clicking on the Run App button located in the top right corner of the RStudio interface10 You can also run shiny::runApp()..

User Interface

The user interface will include the inputs, outputs and layout functions that will define the general appearance of the app11 Note that elements within the ui will be separated by commas!. For example:

ui <- fluidPage(
  titlePanel("The app's title"),
  sidebarLayout(
    sidebarPanel(
      numericInput(inputId = "my_first_input",
                  label = "A clear description for the user to see"
                  #,... other parameters
                  ),
      textInput(inputId = "my_second_input",
                  label = "A clear description for the user to see"
                #,... other parameters
                ),
      actionButton(inputId = "go",
                  label = "Update!"
                  #,... other parameters
                 )
    ),
    mainPanel(
      plotOutput(outputId = "my_first_output")
    )
  )
)

Layout

Layout functions can be combined or nested in order to design our desired layout. In our example, the fluidPage() function defines the general layout of the site: a fluid page layout consists of rows which in turn include columns12 This is the most commonly used layout (although there are other options to explore!). However, note that after defining our titlePanel(), nested inside our fluid page you can find another layout function: sidebarLayout(). This is a predefined layout featuring a sidebar13 We’ll include its contents within the sidebarPanel() function. (typically used for inputs) and a main panel14 We’ll include its contents within the mainPanel() function. (where you will usually find the outputs).

Structure of a basic app with sidebar (Wickham 2021)
Structure of a basic app with sidebar (Wickham 2021)

Using the shinythemes package15 The bslib package also offers a function to change the app’s layout. In this case, the syntaxis is theme = bs_theme(…)., we can also change the theme of the app using theme = shinytheme("the_theme_we_want")16 You can explore the available themes here. inside our general layout function17 In our example, fluidPage(..., theme = shinytheme("the_theme_we_want"))..

Considering a Shiny app’s ui is an HTML document, it is possible to include tags such as h1("Header 1") for a level 1 header, br() to insert a line break or a(href="", "link") to define a hyperlink18 Try running names(tags) to get a complete list of all possible tags..

Inputs and outputs

Multiple input functions exist, including numericInput(), textInput(), selectInput(), and checkboxInput(), among others: each one corresponds to a different type of input to be collected from the user (a numeric value, a string, etc.). All input functions share two core parameters: inputId for specifying the unique identifier and label to inform the user the expected input content. Additionally, each input type may have further parameters tailored to its specific characteristics, such as potential values (a vector of options for select inputs, maximum or minimum values for numeric inputs, etc.) and default values.

Similarly, output functions such as plotOutput(), dataTableOutput(), imageOutput() or textOutput() will specify the type of output. In this case, the main parameter will be outputId, used to specify the unique identifier.

When designing visualizations in a Shiny app, it is usually a good idea to create interactive visualizations using the plotly package.

Server

The server object will be defined as a function with two main arguments19 It can optionally include session as a parameter: an environment that can be used to access information and functionality relating to the session. For more details, visit this site.:

For example:

server <- function(input, output) {
  
  output$my_first_output <- renderPlot({
    hist(c(1:input$my_first_input), main = input$my_second_input)
  })
}

In order to create our outputs, we will resort to render functions within the server. These functions are responsible for dynamically generating the content of each output element based on the inputs (in order to do so, we will refer to each input using the syntax input$input_id). Note that there must be a correspondence between the type of output defined in the ui and the render function.20 For example: a plotOutput will be rendered with a renderPlot() function, an imageOutput will be rendered with a renderImage() function, etc.

In our example, renderPlot() is a reactive function, which is triggered every time the input changes and re-generates the output (in our example, a plot)21 In addition to generating output based on the inputs provided, certain render functions, like renderDataTable(), offer additional features, such as the possibility to download the rendered table in multiple formats.. In other words, everything that is wrapped between the braces is going to be run each time the input changes.

We may not want everything to re-run and update every single time any input changes but only in response to specific events (for example, update only if the user clicks on our “Update!” actionButton). In this case, we may resort to functions such as eventReactive(), which will create the reactive expression with code in its second argument (that is to say, the expression between curly brackets) when reactive values in the first argument change (in this case, input$go)22 Similarly, a frequent use of the observe() function is to update a certain input’s options considering a different input. For instance, consider a restaurant reservation app featuring two inputs: “restaurant” and “available_time_slots”. Since each restaurant has its unique set of available time slots, it’s essential to ensure that the time slot options align with the selected restaurant. Therefore, we will want to update the time slot options every time the user chooses a different restaurant.. For example:

server <- function(input, output) {
     
    plot <- eventReactive(input$go,
                          {hist(c(1:input$my_first_input), 
                                main = input$my_second_input)
  })
    
    output$my_first_output <- renderPlot({
    plot()
  })
  
}

Notice that plot is now a reactive expression, and we therefore have to call it like a function (that is to say, plot()) inside renderPlot(). However, while it looks like we are calling a function, a reactive expression has an important difference: it only runs the first time it is called and then it stores its result until it needs to be updated (Wickham 2021).

Data preprocessing

Usually, our app won’t solely depend on user inputs but will also make use of additional databases23 For example, the findings of a scientific article we authored, which we intend to share with the wider scientific community. that we load ourselves (the developers). To prevent excessive processing times (and ensure smooth app performance), we can preprocess much of this data externally and then simply load the prepared databases for the app server to operate on.

Thus, in our working directory we will typically have a file named prepare_data.R where we will handle all of these preprocessing tasks.

Taking our apps to the next level

As you aim to create more complex apps, you’ll discover that this basic script design can become cumbersome. It may result in excessively lengthy and intricate scripts, riddled with multiple nested functions (and long lists of ids24 Keep in mind that ids must be unique, and any accidental repetition of an id will cause the app to fail!), which can increase the likelihood of errors when attempting to make modifications to the app.The solution: modules25 In this class, we won’t extensively cover the implementation of modules, but you can refer to this text for more comprehensive instructions..

A module is basically a pair of ui and server functions, but these functions are constructed in a special way that creates a “namespace” (“spaces” of “names” that are isolated from the rest of the app). Namespacing makes it easier to understand how your app works because you can write, analyse, and test individual components in isolation (Wickham 2021).

In order to get your app to work, in your main app.R script you will summon all your module’s ui and server functions:

library(shiny)

  ui <- fluidPage(
    module1_ui("id1"),
    module2_ui("id2")
    #,.....
  )
  
  server <- function(input, output, session) {
    module1_server("id1")
    module2_server("id2")
    #.....
  }
  
  shinyApp(ui, server)  

Note that you should use the same id in both the module’s ui and server, otherwise the two pieces will not be connected. However, the ids you use to identify inputs and outputs inside each module don’t need to be globally unique, they just have to be unique inside each module.

Recap: Directory structure when working with modules

app.R

prepare_data.R

R /

   module1.R

   module2.R

    ….

Sharing your app

Shiny apps are incredibly useful tools in the following scenarios:

So, how can we share our app? There are two main options, and the choice between them will depend on the specific circumstances:

  1. Using a shiny server27 For more details, visit this site.

    Shiny Server is an open-source backend program that enables you to host your apps within a controlled environment, such as within your organization. It will host each app at its own web address and automatically start the app when a user visits the address (when the user leaves, Shiny Server will automatically stop the app).

  1. Publishing your app to https://www.shinyapps.io/

    This option is easier, it only requires setting up an account in the site and deploying your app. However, the free option comes with certain limitations, such as restrictions on the number of apps that can be hosted and the monthly active hours of the app, which refer to the amount of time when a user is interacting with the app.

Discussion

Tools such as Shiny apps can help us in the task of democratizing data analysis tools and fostering the participation of the general public in scientific research. Traditionally, data analysis capabilities were confined to experts and researchers with specialized knowledge. However, tools such as Shiny apps have broken down these barriers, which allows more engagement from readers and the general public, fostering a collaborative and inclusive approach to data-analysis.

One of the significant benefits of using Shiny apps lies in the customization of access to data. Users can not only access raw data but also possess the capacity to perform data analysis according to their needs and interests even if lacking data literacy. While raw data might be rich in information, presenting it in a clear and interpretable manner is vital. In this regard, Shiny apps excel in providing tools that distill complex data into intuitive, user-friendly displays.

References

Wickham, Hadley. 2021. Mastering Shiny: Build Interactive Apps, Reports, and Dashboards Power by R. First Edition. Sebastopol, CA: O’REILLY.