15  Graphical interfaces with Shiny

You have had a preview of a shiny interface in the previous section with the interactive parameter input in a Rmarkdown file.

Using the shiny package, you can actually easily build an interactive graphical user interface (GUI) in which you will be able to set parameters (values, files…), visualize the outputs (plots, images, tables…), and write files as output. This is very useful when you have to always repeat the same task with a varying input parameter, for example.

15.1 Stand-alone shiny application

A shiny application is an app.R file (it must be named like that) containing 3 elements:

  1. ui: definition of the interface layout (where are the buttons, text input, plot output, etc.) and the input parameters
  2. server: definition of the various actions to perform with the input parameters
  3. shinyApp(ui, server): launches the shiny app with the above defined parameters

In Rstudio, create a new “Shiny web app”. It will create an app.R file containing this:

library(shiny)
# Define UI for application that draws a histogram
ui <- fluidPage(
    # Application title
    titlePanel("Old Faithful Geyser Data"),
    # Sidebar with a slider input for number of bins 
    sidebarLayout(
        sidebarPanel(
            sliderInput("bins",
                        "Number of bins:",
                        min = 1,
                        max = 50,
                        value = 30)
        ),
        # Show a plot of the generated distribution
        mainPanel(
           plotOutput("distPlot")
        )
    )
)
# Define server logic required to draw a histogram
server <- function(input, output, session) {
    output$distPlot <- renderPlot({
        # generate bins based on input$bins from ui.R
        x    <- faithful[, 2]
        bins <- seq(min(x), max(x), length.out = input$bins + 1)

        # draw the histogram with the specified number of bins
        hist(x, breaks = bins, col = 'darkgray', border = 'white')
    })
}
# Run the application 
shinyApp(ui = ui, server = server)

Run it by clicking “Run App”: a window opens and you can pan the slider and see the resulting output.

In case you want to clean up the code, you can separate app.R into ui.R and server.R. No need to add the shinyApp(ui = ui, server = server) line in that case.

All user-defined functions and variable definitions can be defined in a global.R file that will be sourced by default when launching the app.

15.1.1 The layout

In the ui <- fluidPage(...) item, you define the layout of your application. In the above example:

  • titlePanel("Title") creates a title
  • sidebarLayout() separates the layout in a short one on the left (sidebarPanel()) and a main one on the right (mainPanel())
  • sliderInput("name_of_slider", "text to display", min=min_value, max=max_value, value=current_value, step=step_value) creates a slider to input a value. This value will be retrieved by input$name_of_slider in the server() function.
  • plotOutput("name_of_plot") plots the result of output$name_of_plot defined in the server() function.

See the guide to application layout for more layout options. I also recommend taking a look at the packages shinydashboard and shinymaterial.

15.1.2 The server

In the server <- function(input, output){...} function, you define the various actions and outputs in reaction to an input change.

In the above example, we define output$distPlot as a renderPlot() function whose results depends on input$bins. The plot is rendered in the ui by plotOutput("distPlot").

15.1.3 Various useful functions

15.1.3.1 Input

15.1.3.1.1 Buttons
# # # # # # # # # 
# In ui:
actionButton("button_name", "Text to display")
# # # # # # # # # 
# In server:
observeEvent(input$button_name, {
    # do something
})
# or
some_function <- eventReactive(input$button_name, {
                               # do something
                               })
15.1.3.1.2 Checkbox
# # # # # # # # # 
# In ui:
checkboxInput("checkbox_name", "Text to display", value=FALSE)
# # # # # # # # # 
# In server:
input$checkbox_name #TRUE or FALSE

15.1.3.1.3 Text/numeric
# # # # # # # # # 
# In ui:
textInput("text_name", 
            label = "Text to display", 
            value = "initial value", 
            width = '100%')

textAreaInput("text_name", 
            label="Text to display", 
            value = "initial_value", 
            rows = 5) %>%
            shiny::tagAppendAttributes(style = 'width: 100%;')

numericInput("value_name", 'Text to display', value=0)
# # # # # # # # # 
# In server, retrieve it as:
input$text_name
input$value_name

15.1.3.1.4 Slider
# # # # # # # # # 
# In ui:
sliderInput("slider_name", "Text to display",
            min = 1,
            max = 50,
            step= 1,
            value = 30)
# # # # # # # # # 
# In server, retrieve it as:
input$slider_name

15.1.3.1.5 File
# # # # # # # # # 
# In ui:
fileInput("file_in", 
          "Choose input file:", accept = c(".txt") 
          )
# # # # # # # # # 
# In server, retrieve it as:
input$file_in$datapath
# For example, read it as a data.frame with myData():
myData <- reactive({
        inFile <- input$file_in
        if (is.null(inFile)) {
            return(NULL)
        } else {
            return(read.table(inFile$datapath, header=TRUE))
        }
    })

15.1.3.2 Output

15.1.3.2.1 Display a plot
# # # # # # # # # 
# In ui:
plotOutput("plot_name", height = 600,
           click = "plot_click", # to retrieve the click position
           dblclick = "plot_dblclick", # to retrieve the double click position
           hover = "plot_hover", # to retrieve the mouse position
           brush = "plot_brush" # to retrieve the rectangle coordinates
           )
# # # # # # # # # 
# In server:
output$plot_name <- renderPlot({
        # do plot:
        plot(...)
        # or
        ggplot(...)
    })

If you want an interactive plot, use plotlyOutput() and renderPlotly() instead.

15.1.3.2.2 Display text
# # # # # # # # # 
# In ui:
textOutput("text_to_display")
# Verbatim text (fixed width characters):
verbatimTextOutput("text_to_display")
# # # # # # # # # 
# In server:
output$text_to_display <- renderText({ "some text" })
output$text_to_display <- renderPrint({ "some text" })

15.1.3.2.3 Display a table
# # # # # # # # # 
# In ui:
tableOutput("table_to_display")
# # # # # # # # # 
# In server:
output$table_to_display <- renderTable({ df })

Or in case you want interactive tables, use the package datatable:

library(DT)
# # # # # # # # # 
# In ui:
dataTableOutput("table_to_display")
# # # # # # # # # 
# In server:
output$table_to_display <- renderDataTable({ df })

15.1.3.2.4 Reactive events

In case you want the plots or text display to react to a change in input value, you can wrap the corresponding code in the reactive() function on the server side:

# # # # # # # # # 
# In ui:
fileInput("file_in", 
          "Choose input file:", accept = c(".txt") 
          ),
checkboxInput("header", "Header?", value=TRUE),
selectInput("menu", "Columns to display", 
            choices=1, selected = 1, multiple = TRUE),
tableOutput("table")
# # # # # # # # # 
# In server:
myData <- reactive({
        inFile <- input$file_in
        if (is.null(inFile)) {
            return(NULL)
        } else {
            df <- read.table(inFile$datapath, header=input$header)
            updateSelectInput(session, "menu", choices=1:ncol(df), selected=input$menu)
            return(df)
        }
    })
output$table <- renderTable( myData()[,sort(as.numeric(input$menu))] )

The various input default values can be updated using the following functions on the server side:

# Dropdown menu
updateSelectInput(session, "menu_name", choices=new_choices)
# Text
updateTextInput(session, "text_name", value = new_value)
# Numeric
updateNumericInput(session, "value_name", value = new_value)

15.1.3.2.5 Writing a file

This is not a function of shiny, but you may want to write a text file. If this comes from a data.frame, you can use the function write.table():

df <- data.frame(x=1:10,y=sin(1:10))
write.table(df, "test.dat", quote=FALSE, row.names=FALSE)

For other forms of printing, look into the write() function:

toprint <- paste("hello", "world")
outfile <- file("file_name.txt", encoding="UTF-8")
write(toprint, file=outfile)
close(outfile)

You can for example write a Rmd file that you will render (as pdf, etc…) using render():

rmarkdown::render("file_name.Rmd")

15.1.4 Example

Create a new shiny app with the following code, and play around with it. The input file should be the tidy population.txt.

library(shiny)
library(tidyverse)
library(plotly)
library(DT)

ui <- fluidPage(
    titlePanel("City population in France"),
    sidebarLayout(
        sidebarPanel(
            fileInput("file_in", "Choose input file:",
                      accept = c(".txt") ),
            selectInput("sel_city", "City:", choices = "", multiple = TRUE)
        ),
        mainPanel(
            tabsetPanel(
                tabPanel("Plot", plotlyOutput("cityplot", height = "400px")),
                tabPanel("Table", dataTableOutput("table"))
            )
        )
    )
)

server <- function(input, output, session) {

    # myData() returns the data if a file is provided
    myData <- reactive({
        inFile <- input$file_in
        if (is.null(inFile)) {
            return(NULL)
        } else {
            df <- read.table(inFile$datapath, header=TRUE)
            # in case something changes,
            # update the city input selection list
            updateSelectInput(session, "sel_city",
                              choices = unique(df$city),
                              selected = unique(df$city)[1])
            return(df)
        }
    })

    # plot the pop vs year for the selected cities
    output$cityplot <- renderPlotly({
        df <- myData()
        if(is.null(df)) return(NULL)
        p <- df %>% 
            filter(city %in% input$sel_city) %>%
            ggplot(aes(x=year, y=pop, size=pop, color=city)) +
                geom_point() +
                geom_smooth(method="lm", alpha=0.1,
                            show.legend = FALSE,
                            aes(fill=city)) +
                ggtitle(paste0("Population in ",
                               paste(input$sel_city, collapse = ", ")
                               ))+
                labs(x="Year", y="Population")+
                theme_light()
        ggplotly(p, dynamicTicks = TRUE)
    })

    # show data as a table
    output$table <- renderDataTable({
        df <- myData() %>% filter(city %in% input$sel_city)
        if(is.null(df)) return(NULL)
        df <- pivot_wider(df, names_from=year, values_from=pop)
        datatable(df, rownames = FALSE)
    })

}

shinyApp(ui = ui, server = server)

This will render like this.

15.2 Rmarkdown-embedded shiny application

A shiny application can even be embedded inside a Rmarkdown document by providing runtime: shiny in the YAML header. A short example here, try to compile it:

---
title: "Test"
output: html_document
runtime: shiny
---

This is a test Rmarkdown document.

`r ''````{r, echo=FALSE, message=FALSE}
library(ggplot2)
library(plotly)
df <- read.table("Data/population.txt", header=TRUE)


shinyApp(
  ui = fluidPage(
    selectInput("city", "City:", choices = unique(df$city)),
    plotlyOutput("cityplot", height = 600)
  ),

  server = function(input, output) {
    output$cityplot = renderPlotly({
      p <- ggplot(data=subset(df,city==input$city), 
                aes(x=year, y=pop, size=pop)) +
            geom_point() + 
            geom_smooth(method="lm", alpha=0.1, show.legend = FALSE) + 
            ggtitle(paste("Population in ",input$city,sep=""))+
            labs(x="Year", y="Population")+
            theme_light()
      ggplotly(p)
    })
  }
)
```

The only “problem” with this solution is that the html file that is produced will not run the shiny app by itself, you have to open the Rmd file in Rstudio and hit “Run Document”.

Another solution consists in deploying your app on shinyapps.io and embedding the page in your document with:

`r ''````{r, echo=FALSE}
knitr::include_app("https://cbousige.shinyapps.io/shiny_example/", 
                    height = "800px")
```

15.3 Deploying your shiny app

There are 4 ways to deploy your app: passing the app.R file to your users, deploying to shinyapps.io, deploying on your own server, or building an executable with Electron.

15.3.1 Passing the app.R file to your users

This option is certainly easy: just send your app.R file (or Rmd file with shiny embedded app) as well as any other files needed (e.g. global.R) to your users, explain to them how to run it, and voilà.

However, this needs a little bit of know-how from the users: they need to install R and Rstudio, install the needed packages, and run the app.

A good option to remove the “package-installing” step is to define a function check.package() that will check if the package is installed, install it if needed, and load it:

check.packages <- function(pkg){
    new.pkg <- pkg[!(pkg %in% installed.packages()[, "Package"])]
    if (length(new.pkg)) 
        install.packages(new.pkg, dependencies = TRUE)
    sapply(pkg, require, character.only = TRUE)
}
# Usage:
check.packages("ggplot2")

15.3.2 Deploying to shinyapps.io

Applications deployed on shinyapps.io will be accessible from anywhere through a weblink. See for example my application to determine the pressure from a ruby Raman spectrum or the expected Raman shift for a given pressure and laser wavelength. Your application will however be public and you will have some limitations in the number of online applications and time of use (if you don’t pay a fee, see here for the various plans).

  • First, create an account on shinyapps.io
  • Follow the steps described here to:
    • Configure RSconnect: in your shinyapps.io dashboard, click your name, then Tokens, and create a token for a new app. Copy the text in the popup window.
    • Deploy the app from the Rstudio window by clicking on the “Publish” button in the top right corner of the interface. Follow the steps along the shinyapps.io way.

Note that in that case, you should not have any install.package() command in your code. Most packages are supported by shinyapps.io.

15.3.3 Deploying on your own Linux server

This option is more advanced and I’m not going into details for that, but you have a number of tutorials online. See e.g. here, here or here.

You might consider this option if you work in a company that want to handle privately its data (which sounds plausible) and not pay the shinyapps.io fee to password protect the app. In that case, just work with the IT department to get it running.

15.3.4 Building an executable

On Windows, there is this possibility that looks nice but that I never tried because I don’t have Windows: RInno.

On any platform: there is the possibility described here with the corresponding github page. This option is actually awesome and a quite recent possibility. However, since the produced application will contain R and the needed packages, the executable file is quite heavy.

15.4 Further reading

15.5 Exercises

Exercise 1
  • Create a new empty app with a blank user-interface and run it.
  • Add a title, a left panel and a main panel
  • Add an input numerical value defaulting to 1 and with a step of 0.05, name it “bw”
  • Add a slider input from 0 to 1e3 by steps of 1e2 defaulting to 5e2, name it “N_val”
  • Add a plot of the density of rnorm(N_val) with bandwidth bw
  • Make sure bw>0, otherwise don’t produce the plot
Solution
library(shiny)

ui <- fluidPage(
    titlePanel("Some title"),
    sidebarLayout(
        sidebarPanel(
            numericInput("bw", "Enter bandwidth:", 1, step=0.05),
            sliderInput("N_val", "Number of points:", 
                        min = 0, max = 1e4, step= 1e2, value = 5e2)
        ),
        mainPanel(
            plotOutput("plot", height = 600)
        )
    )
)

server <- function(input, output, session) {
    output$plot <- renderPlot({
                        if(input$bw==0) return(NULL)
                        plot(density(rnorm(input$N_val), bw=abs(input$bw)))
                    })
}

shinyApp(ui = ui, server = server)
Exercise 2

Create a shiny application that will:

  • read an input (through a file dialog) Raman spectrum from a ruby (XPdata.zip)
  • fit the data by two Lorentzians
  • plot the data interactively
  • ask for the laser wavelength as an input and give 568.189 nm as default
  • write the corresponding pressure on the page using the Pruby() function defined in myfunc.R found in XPdata.zip.
  • insert a button that will, when pressed, render a pdf report displaying the laser wavelength, the plot, the fit and the pressure found:
    • write a separate Rmd file with the proper parameters
    • render the Rmd file as a pdf (see the render() function and this help)