Gentle introduction
A REMINDER TO HARRIET TO REMIND US TO START RECORDING
Make sure you have the shiny
package installed on your computer. You can use code below or the “Packages” tab in lower right panel in Rstudio.
install.packages('shiny')
Have a good understanding of what Shiny apps can do and how it might be useful for you
Understand the core structure of Shiny apps: UI, Server, Input/Output, Reactivity
Learn how to create and run an App locally as well as deploy on ARI’s shiny account
Feel confident enough to grab example code and add to your app 🤞
Of course there is cheatsheet to check out…
knitr::include_graphics("shiny.pdf")
The Rproject contains everything that we have done, including today.
For background information on the dataset, see dataset summary
As we have progressed throughout the week, we have been building up our Rproject to include all the parts of the workflow we introduced on Day 1.
In Day 1, we created our 1) Rproject, 2) folder structure, 3) stored our data, 4) created importData.R
script, and 5) saved Analysis Ready DataSets (ARDS)
In Day 2, we 1) used the ards
files to visualise our data, 2) ran statistical models using R scripts, 3) checked models, and then outputted predictions and parameters estimates.
In Day 3, we used those result files to produce a Quarto report document.
Today, we take the “last” step (for our workshop) and create a shiny App to provide interactive results.
Well, I was going to go into basketball coach mode and give a pep talk about shiny looking intimidating, baby steps, etc.. But, I figured if you have made it to this day, nothing will scare you off.
Though I will still say my final note of my pep talk…“Once you get beyond the foreignness of the code, we guarantee that with a little guidance, you can easily get started making your own apps in minutes. It is just about getting the basic structure and then having the confidence to do ”trial-and-error” with borrowed code. Just be forewarned, shiny Apps can become addicting!!!”
I will be using AI a fair a bit today. I have found really useful for getting started with shiny apps and for the basics (mostly what we covering here), it can really help. As noted in other days, it is helpful to have a few terminology/concepts under your belt when working with AI for shiny app creation. I will try to demonstrate this through this session. I will just work with Copilot (and maybe Claude for comparison?). I have more info in the page on AI extra topic.
We will work through an example of building a simple app that displays a graph depending on the conditions selected.
I am going to work with the built-in mtcars
dataset for the example. I want an user to select a the number of cylinders a car has and plot the relationship between horsepower(hp) and fuel efficiency (mpg).
head(mtcars)
mpg cyl disp hp drat wt qsec vs am gear carb
Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
library(ggplot2)
ggplot(mtcars, aes(hp, mpg) ) + geom_point() + geom_smooth(method='lm') +
facet_wrap(~cyl) + labs(x='hp (horsepower)', y='mpg (mile/gallon)')
In Copilot, I typed the following:
build me a shiny app using the mtcars dataset. The UI will have a side panel that will allow the user to select the cyl type and the main panel will have two tabs. The first tab will show a plot of the relationship between hp and mpg for the cyl selected by the user. The plot will also have the linear regression line and confidence band that was created using lm(). The second tab will have the linear model outputs extracted from the model using broom::tidy()”
The coding output are shown below:
# set global objects
library(shiny)
library(ggplot2)
library(broom)
# Define UI
ui <- fluidPage(
titlePanel("MTCars Analysis"),
sidebarLayout(
sidebarPanel(
selectInput("cyl", "Select Cylinder Type:",
choices = unique(mtcars$cyl))
),
mainPanel(
tabsetPanel(
tabPanel("Plot", plotOutput("hp_mpg_plot")),
tabPanel("Model Summary", tableOutput("model_summary"))
)
)
)
)
# Define server logic
server <- function(input, output) {
filtered_data <- reactive({
mtcars[mtcars$cyl == input$cyl, ]
})
output$hp_mpg_plot <- renderPlot({
data <- filtered_data()
model <- lm(mpg ~ hp, data = data)
ggplot(data, aes(x = hp, y = mpg)) +
geom_point() +
geom_smooth(method = "lm", se = TRUE) +
labs(title = paste("HP vs MPG for", input$cyl, "Cylinders"),
x = "Horsepower (hp)", y = "Miles per Gallon (mpg)")
})
output$model_summary <- renderTable({
data <- filtered_data()
model <- lm(mpg ~ hp, data = data)
tidy(model)
})
}
# Run the application
shinyApp(ui = ui, server = server)
It does run successfully and the UI looks like the following:
ONE vs MULTIPLE files
For your app setup, you can either select a single file called app.R
[like we did in the example] or you can create two files: ui.R
and server.R
. What this is doing is just breaking up the one file into two, otherwise basically the same. The advantage of the latter is in larger apps and for reusing code (modularise). We will stick with one file method for rest of today.
Shiny app has a straightforward structure comprising four main components: 1) global heading; 2) ui
; 3) server
; and 4) shinyApp
.
Here is the basic structure, stripping away the guts.
# 1) Define extra packages and global objects (e.g. your dataset, model results, formats...)
library(shiny)
library(tidyverse)
# 2) Define UI for application [how it looks to the user]
ui <- fluidPage(
)
# 3) Define server logic [steps taken when app starts and user clicks something]
server <- function(input, output) {
}
# 4) Run the application
shinyApp(ui = ui, server = server)
Visual model of the file…
Let’s go through each component…
This is what the user will be using to explore your data/results/visualizations.
First step is determining what kind of layout you want. Single panel, main panel with sidepanel, tabs. May want to check Shiny gallery or layout for ideas.
In our example, AI produced the following UI code using fluidPage()
then breaking into sidebarLayout()
and then again into sidebarPanel()
and mainPanel()
. Within the mainPanel()
section, it added tabsetPanel()
to get the tabs.
# 2) Define UI for application [how it looks to the user]
ui <- fluidPage(
titlePanel("MTCars Analysis"),
sidebarLayout(
sidebarPanel(
selectInput("cyl", "Select Cylinder Type:",
choices = unique(mtcars$cyl))
),
mainPanel(
tabsetPanel(
tabPanel("Plot", plotOutput("hp_mpg_plot")),
tabPanel("Model Summary", tableOutput("model_summary"))
)
)
)
)
Once layout is figured out, now you add in the input objects. In our example that was a drop-down menu that allowed the user to pick cylinder size.
Check the cheatsheet for basic options for inputs or search Shiny gallery
Few key takeways
basically creating html using simple functions
functions are separated by commas!!!!
And so many nested brackets!!!!
In the UI, an input list is created from the UI design and it is used to send user inputs to the server.
Takeways
like an R list
setup by the UI
can only be changed by the user (immutable from being changed in the server code)
The server is the heart of the app. Let revisit the visual model…
Three main key bits…
You have the inputs coming in that will be used
R code that will take the inputs and convert to new output(s)
the output list is created and exported back to UI
Let’s look a little closer at the server code
# Define server logic
server <- function(input, output) {
filtered_data <- reactive({
filter(mtcars, cyl == input$cyl)
})
output$hp_mpg_plot <- renderPlot({
data <- filtered_data()
model <- lm(mpg ~ hp, data = data)
ggplot(data, aes(x = hp, y = mpg)) +
geom_point() +
geom_smooth(method = "lm", se = TRUE) +
labs(title = paste("HP vs MPG for", input$cyl, "Cylinders"),
x = "Horsepower (hp)", y = "Miles per Gallon (mpg)")
})
output$model_summary <- renderTable({
data <- filtered_data()
model <- lm(mpg ~ hp, data = data)
tidy(model)
})
}
Key takeways
not separated by commas
more like R programming but order does not matter between components!!!!!
Within renderXXX or reactive() we have R code and order does matter here. Note the double-brackets needed when multiple lines
Our server
created output$hp_mpg_plot
using renderPlot
and output$model_summary
using renderTable
and the ui
outputs using plotOutput
and tableOutput
. You get these pairs of renderXXX({}) and xxxOutput().
# does not run...just showing R code snippets that pair up
output$hp_mpg_plot <- renderPlot({...}) # code in server side
tabPanel("Plot", plotOutput("hp_mpg_plot")) # code in ui side
output$model_summary <- renderTable({...}) # code in server side
tabPanel("Model Summary", tableOutput("model_summary")) # code in ui side
Look at the cheatsheet for a partial list of pairings…
Note - this is not a comprehensive list and packages might have then own renderXxx() and xxxOutput()
Couple more examples…
# leaflet package (see below in the lecture)
leaflet::renderLeaflet() # server side for leaflets
leaflet::leafletOutput() # UI side for leaflets
# plotly package (see below in the lecture)
renderPlotly() # server side for plotly
plotlyOutput() # UI side for plotly
work in steps
to test UI, you can often comment out all of the server content
once you got UI working, work on the server
with server, you can create temporary substitutes variable to test the code:
The style of programming used in shiny apps is called “reactive programming”. The purpose of this programming style is to keep inputs and outputs in sync. To do this efficiency, you only want to update outputs that need to be changed. So the rule is “change if and only if inputs change”.
In our example we had one such case. This reactive function was then used in both renderXXX functions.
The two key components of this are called:
lazy - only do work when called to [“procrastinate as long as possible”]
cache - save the results produced when first called and use that until something changes
You will see a variety of functions that are doing “extra” bits in the programming:
shiny::reactive() # create function for repeated active (e.g. filtering dataset for multiple outputs )
shiny::reactiveValues() # create a new reactive "list" like input and output
shiny::observe() # performs a side effect, changes to input changes
shiny::observeEvent() # performs when an event happens like submit button (e.g. input/output)
We will not go into depth here but just want you to be aware and when you see them, know that they are doing extra stuff and a bit of efficiency stuff. For more complicated/sophisicated apps, they are essential to get a handle of.
Here is an analogy that I came across that hopefully helps. Think of the app as a restuarant…
UI (User Interface): Think of this as the menu of the restaurant. It lists all the dishes (inputs and outputs) that the customers (users) can choose from.
Server: This is the kitchen where all the cooking happens. The chefs (server functions) take the orders (inputs) from the customers and prepare the dishes (outputs). The kitchen processes the raw ingredients (data) and transforms them into delicious meals (results).
Reactive Expressions: These are like the recipes that the chefs follow. They define how to prepare each dish based on the ingredients and cooking methods. If an ingredient changes, the recipe ensures the dish is updated accordingly.
Reactive Values: These are the ingredients in the kitchen. They can be fresh produce, spices, or any other items that the chefs need to prepare the dishes. If the ingredients change, the dishes will also change.
Observers: These are like the waitstaff who keep an eye on the dining room. They monitor the customers and ensure that their needs are met promptly. If a customer needs something, the waitstaff (observers) take action to address it.
Thanks to Nev and Jim in particular 👏, we now have a ARI shiny Professional account (called arisci). This account is available to all ARI staff though we do have limits on the number of account users (max = 25 user accounts) but it can have an unlimited number of apps and has 10,000 usage hours (per month). The shiny apps are hosted on https://www.shinyapps.io/ and have https://arisci.shinyapps.io/XXAppNameXX address (e.g. https://arisci.shinyapps.io/ibisTracker/).
Nev/Jim have created a live document detailing the process. I will go key aspects below.
Chat with others in your area and decide whether it may be useful to have a program-wide (similar) account for multiple users. If you are likely a higher user, it may be best to have your own account. If just playing around, you can get a few account on shiny.io
Email Jim Thomson [“The Gatekeeper”] requesting
You will receive an invite to create an account. For your account, you will use your email (or designated person) as the username and then set a password
All account holders can access all the apps on the shared account. To ensure that you do not mess with other user’s apps accidentally please use the rsconnect package for all you uploading, modification, archiving and deletion of apps – your connection will have its own token. See detailed info.
Click here for step-by-step. Overview is shown below:
If you prefer, you can use rsconnect package via code. Get the name, token, and secret info from the token page when logged into https://www.shinyapps.io/.
rsconnect::setAccountInfo(name='arisci',
token='XXXXX',
secret='XXXXX' )
You can use the publish approach via Rstudio (GUI approach):
Or you can do the rsconnect way (just make sure the current directory is where the app is or you need to specify location)
To prevent deleting of others apps, you should archive/delete the app using rsconnect code below. You can just type into Rconsole and run, assuming that you have setup your shiny connection (as shown above). You first need to archive the app and then delete:
rsconnect::terminateApp(appName = 'appName') # this will archive the app.
rsconnect::purgeApp(appName='appName') # this will delete it off the server
Check Nev/Jim live document on the current process for this.
You can do via deployApp:
rsconnect::deployApp(appDir = "your app", appVisibility = "private")
Or you can go onto shiny.io 1) go to your app info; 2) click on users, and 3) finally public.
If private, you will need to authorise users which you can do using below:
# you can add specific users
rsconnect::addAuthorizedUser(email= user@emailaddress.com)
If the user does not have an account on shinyApps.io they will need to create one. They will be sent an email with an invitation link.
As noted about, search the Shiny gallery to get ideas of additional options of what you can do. On this website, it provides all the ui/server code on a GitHub repository, so you can just grab the code.
Another good place to search for additional extensions for shiny is Shiny Awesome which has a curated list and some of the stuff, is well, awesome! (and overwhelming) The links often lead you to GitHub pages.
As Shiny apps are all about interactivity, you may want to use interactive plots. Check the following webpage for a examples of packages that have interactivity.
I will show some interactive figures that might be of interest below:
mapview
packageThe mapview package can be used to set up a simple, quick leaflet. This would be most useful when the leaflet is created on startup (not adding in reactivity like adding or deleting data being shown). For instance, you might want to show all the sites surveyed and attach metadata (e.g. dates surveyed, number of fish caught, number of plots) to that point.
Useful shiny functions for Server/UI…
# how it is supposed work
mapview::renderMapview() # server side
mapview::mapviewOutput() # UI side
# I have issues and used leaflet instead example below
sf_m <- mapview::breweries
your_map <- mapview::mapview(sf_m)
output$map <- leaflet::renderLeaflet(your_map@map) # grabs the leaflet
# then you - server side
leaflet::leafletOutput( 'map' ) # UI side
leaflet
packageNow, if you are going to build maps that will change with user input, it is best to build from “scratch” using leaflet
package (and probably leaflet.extra
package + others)
When you have user inputs affecting the map shown, you want to try to avoid rebuilding the map object and rather just modify the elements that the user wants changed (e.g. drop lines and add points instead). This requires using reactive programming and observe()
functions. Check out Nev’s ibis app that does this.
Useful shiny functions for Server/UI…
leaflet::leafletOutput() # UI side
leaflet::renderLeaflet() # server side - creates the basemap
leaflet::leafletProxy() # this is the server function that updates the map
Now, plotly
package does a great job at interactive plots. You can write you code in ggplot and then just convert as shown in code below.
library(ggplot2)
library(plotly)
f <- ggplot( cars, aes(speed, dist) ) + geom_point() + geom_smooth()
plotly::ggplotly(f)
Useful shiny functions for Server/UI…
plotly::renderPlotly()
plotly::plotlyOutput()
Again, you can build your plots using ggplot and convert. I like this one for linking up plots…below, put your cursor over a dot on the left or a bar on the right. If you know CSS, it can be pretty powerful…
library(ggiraph)
library(tidyverse)
library(patchwork)
mtcars_db <- rownames_to_column(mtcars, var = "carname")
# First plot: Scatter plot
fig_pt <- ggplot(
data = mtcars_db,
mapping = aes(
x = disp, y = qsec,
tooltip = carname, data_id = carname
)
) +
geom_point_interactive(
size = 3, hover_nearest = TRUE
) +
labs(
title = "Displacement vs Quarter Mile",
x = "Displacement", y = "Quarter Mile"
) +
theme_bw()
# Second plot: Bar plot
fig_bar <- ggplot(
data = mtcars_db,
mapping = aes(
x = reorder(carname, mpg), y = mpg,
tooltip = paste("Car:", carname, "<br>MPG:", mpg),
data_id = carname
)
) +
geom_col_interactive(fill = "skyblue") +
coord_flip() +
labs(
title = "Miles per Gallon by Car",
x = "Car", y = "Miles per Gallon"
) +
theme_bw()
# Combine the plots using patchwork
combined_plot <- fig_pt + fig_bar + plot_layout(ncol = 2)
# Combine the plots using cowplot
# combined_plot <- cowplot::plot_grid(fig_pt, fig_bar, ncol=2)
# Create a single interactive plot with both subplots
interactive_plot <- girafe(ggobj = combined_plot)
# Set options for the interactive plot
girafe_options(
interactive_plot,
opts_hover(css = "fill:cyan;stroke:black;cursor:pointer;"),
opts_selection(type = "single", css = "fill:red;stroke:black;")
)
Useful shiny functions for Server/UI…
ggiraph::renderGirafe()
ggiraph::ggiraphOutput()
For useful resources: https://shiny.posit.co/r/articles/
Good for basic shiny https://mastering-shiny.org/
For high level apps, Engineering Production-Grade Shiny Apps