08-12-2021

What we’ll discuss

  1. shiny 101
  2. Building a shiny app
  3. The user interface (UI)
  4. The server
  5. Take-aways

shiny 101

Why use shiny?

  • To create apps!
  • Because you want to:
    • host a standalone app on a webpage;
    • add interactivity to an R Markdown document (like this one!);
    • build a dashboard.
  • And make your R workflows:
    • interactive (point-and-click style);
    • reproducible for non-coders;
    • look instantly professional.

What is shiny?

  • An R package for building interactive web apps.
  • A collection of wrapper functions to write “app languages”:
    • geared toward R users who have zero experience with web development;
    • no knowledge of HTML / CSS / JavaScript required;
    • but you can extend it with CSS themes, htmlwidgets, and JavaScript actions.
  • Developed by RStudio, so documentation and support are more or less guaranteed.

How to build a shiny app?

We have two options:

A. Create a file called app.R and add shiny components (file name and components are non-negotiable!)

B. In RStudio: File → New file → R Markdown → Shiny

The template app

Which components does a shiny app need?

  • A user interface (UI):
    • the visible, interactive part;
    • e.g., a web app or dashboard.
  • A server:
    • the invisible, processing part;
    • e.g., your own computer or shinyapps.io.

You can see this in the default shiny structure:

library(shiny)
ui <- fluidPage()
server <- function(input, output) {}
shinyApp(ui = ui, server = server)

Building a shiny app

Case study: BC liquor store

Data on products sold by BC Liquor Store (source: OpenDataBC).

## Rows: 6,132
## Columns: 7
## $ Type            <chr> "WINE", "WINE", "WINE", "WINE", "WINE", "WINE", "WINE"~
## $ Subtype         <chr> "TABLE WINE RED", "TABLE WINE WHITE", "TABLE WINE RED"~
## $ Country         <chr> "CANADA", "CANADA", "CANADA", "CANADA", "UNITED STATES~
## $ Name            <chr> "COPPER MOON - MALBEC", "DOMAINE D'OR - DRY", "SOMMET ~
## $ Alcohol_Content <dbl> 14.0, 11.5, 12.0, 11.0, 13.5, 11.0, 12.5, 12.0, 11.5, ~
## $ Price           <dbl> 30.99, 32.99, 29.99, 33.99, 36.99, 34.99, 119.00, 32.9~
## $ Sweetness       <int> 0, 0, 0, 1, 0, 0, 0, 0, 0, NA, 0, NA, NA, 0, 0, 2, 0, ~

Case study: BC liquor store

We’re going to create a shiny app to visualize these data interactively.

Let’s start with the empty shiny structure:

library(shiny)
ui <- fluidPage()
server <- function(input, output) {}
shinyApp(ui = ui, server = server)
Note: to make sure R can find the data for your app (e.g., load it on the server side, or let the user upload a dataset)!

Our app

The UI

Adding a title

ui <- fluidPage(
  titlePanel("BC Liquor Store prices")
)

The shiny function titlePanel()

  • adds a visible big title-like text to the top of the page;
  • sets the “official” title of the web page (i.e., displayed at the name of the tab in the browser).

Our app

Adding some text

To render text in our app, we can just add character/string objects inside fluidPage():

ui <- fluidPage(
  titlePanel("BC Liquor Store prices"),
  "BC Liquor Store", 
  "prices"
)

Our app

Adding formatted text

For formatted text, shiny has many functions that are wrappers around HTML tags. For example:

  • h1(): top-level header;
  • h2(): secondary header;
  • strong(): bold text;
  • em(): italicized text;
  • br(): line break;
  • img(): image;
  • a(): hyperlink, etc.
Note: if you already know HTML, you don’t need to use these wrapper functions!

Adding formatted text

Let’s replace the UI part of our code with the following:

ui <- fluidPage(
  titlePanel("BC Liquor Store prices"),
  h2("BC"),
  "Liquor",
  br(),
  em("Store"),
  strong("prices")
)

Our app

Adding a layout

The simple sidebar layout:

  • provides a two-column layout with a smaller sidebar and a larger main panel;
  • visually separates the input and output of the app.

We’ll replace the formatted text by a sidebar layout:

ui <- fluidPage(
  titlePanel("BC Liquor Store prices"),
  sidebarLayout(
    sidebarPanel("[inputs]"),
    mainPanel("[outputs]")
  )
)

Our app

Adding an input element

Inputs allow users to interact with a shiny app.

We’ve seen two types already:

  • selectInput() creates a dropdown menu (e.g., number of bins in the template app);
  • sliderInput() creates a numeric scale (e.g., bandwidth adjustment in the template app).

Adding an input element

Can you guess what kind of element these input functions will create?

  • textInput();
  • dateInput();
  • checkboxInput().

Adding input elements


Adding an input element

If we want to add an input element for the variable price in our app, which function should be use?

  • radioButtons(): choosing a specific number;
  • sliderInput(): choosing a range of values on the slider.
sliderInput(
  inputId = "priceInput", 
  label = "Price", 
  min = 0, 
  max = 100, 
  value = c(25, 40), 
  pre = "$"
)

Adding an input element

All input functions have the same first two arguments:

  • inputId, the name by which shiny will refer to this input when you want to retrieve its current value;
  • label, which specifies the text displayed right above the input element.

These argument names are typically dropped from the ...Input() function call:

sliderInput("priceInput", "Price", min = 0, max = 100, value = c(25, 40), pre = "$")
Note: Every input in your app must have a unique inputId; the app will not work properly otherwise! So keep your inputIds simple and sensible.

Adding an input element

The resulting UI code looks like:

ui <- fluidPage(
  titlePanel("BC Liquor Store prices"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("priceInput", "Price", 
                  min = 0, max = 100, value = c(25, 40), pre = "$")
      ),
    mainPanel("[outputs]")
  )
)

Our app

Adding more input elements

Let’s create input elements for the variables country and product type as well. Which input function(s) should we use if we want to restrict the user to only a few choices?

We’ll use a dropdown list for the countries Canada, France and Italy with selectInput():

selectInput("countryInput", "Country",
            choices = c("CANADA", "FRANCE", "ITALY"))

And for product type, we’ll specify choices with radioButtons():

radioButtons("typeInput", "Product type",
            choices = c("BEER", "REFRESHMENT", "SPIRITS", "WINE"),
            selected = "WINE")

Adding more input elements

The full UI code is now:

ui <- fluidPage(
  titlePanel("BC Liquor Store prices"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("priceInput", "Price", 0, 100, c(25, 40), pre = "$"),
      radioButtons("typeInput", "Product type",
                  choices = c("BEER", "REFRESHMENT", "SPIRITS", "WINE"),
                  selected = "WINE"),
      selectInput("countryInput", "Country",
                  choices = c("CANADA", "FRANCE", "ITALY"))
    ),
    mainPanel("[outputs]")
  )
)

Our app

Adding an output element

Outputs are shown in the UI, but created on the server side.

That’s why we add placeholders for the outputs in the UI.

Placeholders:

  • Determine where an output will be;
  • Give outputs a unique ID to link it to the server;
  • Won’t actually show anything, yet.

Let’s add a figure as output in our app:

mainPanel(
  plotOutput("coolplot")
)

Our app

Adding another output element

The placeholder doesn’t show anything, because we haven’t created any figure yet on the server side.

But first, let’s add another output element:

mainPanel(
  plotOutput("coolplot"),
  br(),
  br(),
  tableOutput("our_table")
)
Note: we added a few line breaks br() between the two outputs, so that they aren’t crammed on top of each other.

The complete UI

ui <- fluidPage(
  titlePanel("BC Liquor Store prices"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("priceInput", "Price", 0, 100, c(25, 40), pre = "$"),
      radioButtons("typeInput", "Product type",
                  choices = c("BEER", "REFRESHMENT", "SPIRITS", "WINE"),
                  selected = "WINE"),
      selectInput("countryInput", "Country",
                  choices = c("CANADA", "FRANCE", "ITALY"))
    ),
    mainPanel(
      plotOutput("coolplot"),
      br(), 
      br(),
      tableOutput("our_table")
    )
  )
)

Our app

The server

The server function

library(shiny)
ui <- fluidPage()
server <- function(input, output) {}
shinyApp(ui = ui, server = server)

The server function:

  • requires* input and output IDs from the UI;
  • builds output objects via render...() functions;
  • saves the generated output into an output list.
*exceptions apply!

Building some random output

Let’s use the exception to the rule to develop our server step-by-step.

Instead of input from the UI, we’ll use static data to fill in the placeholder plot:

server <- function(input, output) {
  output$coolplot <- renderPlot({
  ggplot() +
    geom_histogram(aes(x = rnorm(100))) +
    ggtitle("Histogram of 100 random numbers (static)")
  })
}

Our app

Building some random output

To make the figure interactive, we have to link the server to the UI inputs.

Let’s use the price input to create some random data, interactively:

server <- function(input, output) {
  output$coolplot <- renderPlot({
  ggplot() +
    geom_histogram(aes(x = rnorm(input$priceInput[2]))) +
    ggtitle(paste("Histogram of", input$priceInput[2], "random numbers (interactive)"))
  })
}

So whenever the maximum price input changes, the plot updates and shows the specified number of points.

Our app

Building the actual output

Now that we’ve seen the basics of interactivity, let’s plot the case study data.

We’ll create a histogram of the percentage alcohol in the beverages.

Ultimately, the plot should match the input elements interactively. But first, we’ll plot a static version (briefly ignoring the inputs again):

server <- function(input, output) {
  output$coolplot <- renderPlot({
    bcl %>% 
      ggplot(aes(Alcohol_Content)) +
        geom_histogram() +
        ggtitle("Histogram of alcohol content (static)")
  })
}

Our app

Building the actual output

To incorporate interactivity, we’re going to filter the data based on the values of priceInput, typeInput, and countryInput:

server <- function(input, output) {
  output$coolplot <- renderPlot({
  bcl %>%
    filter(Price >= input$priceInput[1],
           Price <= input$priceInput[2],
           Type == input$typeInput,
           Country == input$countryInput
    ) %>% 
    ggplot(aes(Alcohol_Content)) +
      geom_histogram() +
      ggtitle("Histogram of alcohol content (interactive)")
})
}

Our app

Building the actual output

To complete our app we need to build some output for the table placeholder, and add it to the server:

output$our_table <- renderTable({
  bcl %>%
    filter(Price >= input$priceInput[1],
           Price <= input$priceInput[2],
           Type == input$typeInput,
           Country == input$countryInput
    ) %>% 
    select(c("Name", "Price", "Type", "Country", "Alcohol_Content"))
})

Our app

Intermezzo

Reactivity

Short break from our app to talk about a crucial concept in shiny: reactivity.

Reactivity enables your outputs to react to changes in inputs.

On the most basic level, it means that when the value of a variable x changes, anything that relies on x (i.e. has x in it) gets re-evaluated.

Consider the following code

x <- 5
y <- x + 1
x <- 10

What is the value of y?

Reactivity

What is the value of y?

x <- 5
y <- x + 1
x <- 10

In ordinary programming, the value of y is still 6.

In reactive programming, however, x and y are reactive expressions. Now, the value of y updates reactively, and becomes 11.

Reactivity is the foundation for the responsiveness of shiny apps.

Reactivity

In our server, we implicitly use reactivity when we filter the data for our outputs:

bcl %>%
  filter(Price >= input$priceInput[1],
         Price <= input$priceInput[2],
         Type == input$typeInput,
         Country == input$countryInput
  )

Whenever one of the inputs changes, our outputs change with it. But, this part of code is duplicated, because we didn’t use a reactive variable.

Reactivity

We can avoid code duplication by:

  • defining a reactive variable that will hold the filtered dataset;
  • using that variable in the render...() functions.
filtered <- reactive({
  bcl %>%
    filter(Price >= input$priceInput[1],
           Price <= input$priceInput[2],
           Type == input$typeInput,
           Country == input$countryInput
    )
})

Reactivity

What is going on behind the scenes?

  • The price input changes →
  • shiny ‘looks’ at the reactive(s) that depend on price →
  • filtered() is re-evaluated →
  • shiny ‘looks’ at the reactive(s) that depend on filtered() →
  • The two render...() functions are re-executed →
  • The plot and the table output are updated.

This can be visualized in a dependency tree, to show what value depends on what other value.

Reactivity

The server (continued)

The final app

server <- function(input, output) {
  filtered <- reactive({
    bcl %>%
      filter(Price >= input$priceInput[1],
             Price <= input$priceInput[2],
             Type == input$typeInput,
             Country == input$countryInput
      )
  })
  output$coolplot <- renderPlot({
    ggplot(filtered(), aes(Alcohol_Content)) +
      geom_histogram() +
      ggtitle("Histogram of alcohol content")
  })
  output$our_table <- renderTable({
    filtered() %>% 
      select(c("Name", "Price", "Type", "Country", "Alcohol_Content"))
  })
}

Our app

Advanced topics

Make your app more advanced:

  • Change input element options from the server side with update...() functions;
  • Use more complex layouts, such as tabs or dashboards;
  • Modularize your app;
  • Make the output elements ‘clickable’ with plotly and datatable.

Take aways

Tips

In general:

  • Keep It Simple, Stupid;
  • Don’t rush into coding when you should be thinking;
  • Use a design/UI-first approach;
  • If you copy something just ONCE, make it a function;
  • Avoid unnecessary complexity and ‘feature creep’.

Before building a shiny app, think about:

  • Who are the end users of your app? Are they tech-literate?
  • In what context will the app be used? On what machines (e.g., because of screen size)?

Tips

Build the front-end and the back-end separately.

Front-end:

  • Work on the general appearance first, anything that does not rely on computation (e.g., tabs, inputs, outputs);
  • Use mock data and/or text (build an ‘ipsum-app’);
  • Make the app self-evident; the main usage of the app should not require reading any manual.

Tips

Back-end:

  • Use sensible non-reactive defaults while developing (e.g., data <- mtcars instead of data <- reactive(...).);
  • Think about what could to be ‘hard coded’ in the final app too, because of the reactivity vs. speed trade-off;
  • Extract the complex (but non-reactive) processing functions and put them in separate files;
  • Add user feedback to make server-side requirements explicit (e.g., input validation, pop-up messages, loading icons).

Tips

Deploy your app on shinyapps.io:

  • You’ll have a link to use/share the app online;
  • With a free account, your app will be public;
  • If your app is too popular, you will need to pay (for an upgrade that is, not automatically);
  • Advanced: You can tweak your app to cache certain outputs, or have several users in one session (like Google Drive documents);
Note: You could also host your app on your own website. Or don’t deploy it at all (e.g., for privacy reasons).

Tips

If you want your Shiny app to last, you should make it robust:

  • Run the app in the viewer panel, a separate window, and your browser;
  • Monkey test it (i.e., click EVERYTHING);
  • Provide the wrong inputs (e.g., a corrupt data file, a file with the ‘wrong’ extension, an ‘impossible’ numeric input, etc.);
  • Advanced: Use the golem framework for production-grade shiny apps (but decide up-front!).

Tips

Check out my app!

Final remarks and conclusions

  • shiny allows you to build interactive (web) apps from R;
  • shiny apps consist of two parts, the user interface (UI) and the server:
    • In the UI, you design what is shown to the user,
    • In the server, you do all the modeling and building of the outputs,
    • You link the UI and the server to make the app interactive,
    • To optimize these interactions, you can use reactive expressions;
  • This is only the tip of the iceberg, there are many more things you can do with shiny;
  • Good luck with your assignment!