Introduction to Shiny Apps CSEE

Introduction to interactive app development with R Shiny.
Technical
Fellow contributed
EN
Authors
Affiliations

Nikki Moore

McGill University

Andrew MacDonald

Université de Sherbrooke

Jake Lawlor

McGill University

Published

July 6, 2025

1 Introduction

Why do you want to use Shiny?

There are many reasons to consider using Shiny for a project:

  • Sharing results from a paper with your readers
  • Helping you explore a model, mathematics, or simulations
  • Letting non R users use R.

Objectives of this workshop

  • Give an overview of how and why to use a Shiny app
  • Provide an introduction to a few important Shiny concepts
  • Give you time to practice on your own!

Some examples of Shiny in practice!

2 Background content

Lists, functions & plotting in R

To ease into things, we will familiarize ourselves with the data that we will use to create a Shiny app together. At the same time, we will practice indexing lists, plotting, and making functions. By the end of this section, we will have made a plot that we will add interactivity to using Shiny.

The data

We will be using a dataset on networks of scientific collaboration within the ecology and evolution departments of 25 Canadian universities. For each university, we have a count of the number of co-authored publications between each pair of ecology and evolution professors.

Lucas Eckert has kindly provided us with this data, and will be giving a talk on patterns of collaboration on Wednesday July 9th at 4:00pm (don’t miss it!).

Let’s explore the data:

## read in the data 
intra_list <- readr::read_rds(here::here("posts/2025-07-06-CSEE-intro-shiny/intra-list_clean.RDS"))

## what type of object are the data are stored in?
class(intra_list)
[1] "list"

As you can see, the data are stored in a list object.

In R lists act as containers. Unlike atomic vectors, the contents of a list are not restricted to a single mode and can encompass any mixture of data types. Lists are sometimes called generic vectors, because the elements of a list can by of any type of R object, even lists containing further lists. This property makes them fundamentally different from atomic vectors.

A list is a special type of vector. Each element can be a different type.

Create lists using list():

x <- list(5, "a", TRUE)
x
[[1]]
[1] 5

[[2]]
[1] "a"

[[3]]
[1] TRUE

The content of elements of a list can be retrieved by using double square brackets.

x[[1]]
[1] 5

Elements of a list can be named (i.e. lists can have the names attribute).

names(x) <- c("number", "string", "boolean")
names(x)
[1] "number"  "string"  "boolean"

And elements can be accessed by their names:

x[["string"]]
[1] "a"
x$string
[1] "a"

In our data list, each element of the list stores the collaboration network within one university, and the names attribute tells which university.

EXERCISE

Try indexing intra_list to get the collaboration network for McGill University. What is the shape of the resulting dataset? (list, matrix, data frame, vector etc)

## get the names of elements on our list
names(intra_list)
 [1] "Carleton University"                 "Dalhousie University"               
 [3] "McGill University"                   "McMaster University"                
 [5] "Memorial University of Newfoundland" "Queen's University"                 
 [7] "Simon Fraser University"             "Trent University"                   
 [9] "Université de Montréal"              "Université du Québec à Montréal"    
[11] "Université du Québec à Rimouski"     "Université Laval"                   
[13] "University of Alberta"               "University of British Columbia"     
[15] "University of Calgary"               "University of Guelph"               
[17] "University of Manitoba"              "University of New Brunswick"        
[19] "University of Ottawa"                "University of Saskatchewan"         
[21] "University of Toronto"               "University of Victoria"             
[23] "University of Waterloo"              "University of Western Ontario"      
[25] "University of Windsor"              
## access element containing data for McGill University
network <-  intra_list[['McGill University']]

network[1:5, 1:5]
                     abouheif_ehab_mcgill barrett_rowan_mcgill
abouheif_ehab_mcgill                    0                    0
barrett_rowan_mcgill                    0                    0
bell_graham_mcgill                      0                    4
bennett_elena_mcgill                    0                    0
boivin_guy_mcgill                       0                    0
                     bell_graham_mcgill bennett_elena_mcgill boivin_guy_mcgill
abouheif_ehab_mcgill                  0                    0                 0
barrett_rowan_mcgill                  4                    0                 0
bell_graham_mcgill                    0                    0                 0
bennett_elena_mcgill                  0                    0                 0
boivin_guy_mcgill                     0                    0                 0

Let’s look at the format of the list elements:

## view the data:
View(network)

Each university collaboration network is represented by a 2D matrix (similar to one used to represent species interaction networks) where rows and columns represent researchers and cells contain the number of shared publications between two researchers.

3 Plotting

Our shiny app will feature visualizations of this dataset. Here is a short introduction to plotting in ggplot2, followed by a demonstration of a network plot using ggraph.

We are going to be using functions from the ggplot2 package to create visualizations of data. Functions are predefined bits of code that automate more complicated actions. R itself has many built-in functions, but we can access many more by loading other packages of functions and data into R.

If you don’t yet have this package installed, run this line of code:

install.packages("ggplot2")

And load the package using the library() function:

# load package
library(ggplot2)
library(palmerpenguins)

Let’s also load a dataset to practice making a plot with:

data("penguins")
knitr::kable(head(penguins))
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
Adelie Torgersen 39.1 18.7 181 3750 male 2007
Adelie Torgersen 39.5 17.4 186 3800 female 2007
Adelie Torgersen 40.3 18.0 195 3250 female 2007
Adelie Torgersen NA NA NA NA NA 2007
Adelie Torgersen 36.7 19.3 193 3450 female 2007
Adelie Torgersen 39.3 20.6 190 3650 male 2007

ggplot2 is a powerful package that allows you to create complex plots from tabular data (data in a table format with rows and columns). The gg in ggplot2 stands for “grammar of graphics”, and the package uses consistent vocabulary to create plots of widely varying types. Therefore, we only need small changes to our code if the underlying data changes or we decide to make a box plot instead of a scatter plot. This approach helps you create publication-quality plots with minimal adjusting and tweaking.

ggplot plots are built step by step by adding new layers, which allows for extensive flexibility and customization of plots.

We use the ggplot() function to create a plot. In order to tell it what data to use, we need to specify the data argument. An argument is an input that a function takes, and you set arguments using the = sign.

ggplot(data = penguins)

We get a blank plot because we haven’t told ggplot() which variables we want to correspond to parts of the plot. We do this using the mapping argument. We can specify the “mapping” of variables to plot elements, such as x/y coordinates, size, or shape, by using the aes() function.

ggplot(data = penguins, mapping = aes(x = bill_length_mm, y = bill_depth_mm))

Now we’ve got a plot with x and y axes corresponding to variables from the iris data. However, we haven’t specified how we want the data to be displayed. We do this using geom_ functions, which specify the type of geometry we want, such as points, lines, or bars. We can add a geom_point() layer to our plot by using the + sign. We indent onto a new line to make it easier to read, and we have to end the first line with the + sign.

ggplot(data = penguins, mapping = aes(x = bill_length_mm, y = bill_depth_mm)) +
  geom_point()
Warning: Removed 2 rows containing missing values or values outside the scale range
(`geom_point()`).

Plotting collaboration networks

We want our app to display the collaboration network for a university that the user of our Shiny app will select, so let’s start by making a plot of the collaboration network for McGill University.

ggplot likes tidy data, where each row of the data is an observation and each column represents different variables describing that observation. Because our network data are not in this format, we need the help of another package called tidygraph to plot them. Let’s install and load it:

# install.packages("tidygraph")
library(tidygraph)

Attaching package: 'tidygraph'
The following object is masked from 'package:stats':

    filter
library(ggraph)
# Create graph
graph <- as_tbl_graph(intra_list[["McGill University"]]) |> 
    mutate(degree = centrality_degree(mode = 'in'))

# plot using ggraph
ggraph(graph, layout = 'circle') + 
    geom_edge_fan(aes(alpha = after_stat(index)), show.legend = FALSE) + 
    geom_node_point(aes(size = degree)) + 
    theme_graph(foreground = 'steelblue',
                fg_text_colour = 'white')

Writing functions

If we only had one university’s network to plot, we could stop here… but we have 24 others to plot. Let’s wrap our code in a function so that we can repeat several operations with a single command.

You can write your own functions in order to make repetitive operations using a single command. Let’s start by defining our function plot_network and the input parameter(s) that the user will feed to the function. Our function will take two arguments: university (a string defining the name of the university to plot) and network_list (the list of collaboration networks).

Afterwards you will define the operation that you desire to program in the body of the function within curly braces { }. Finally, you need to assign the result (or output) of your function in the return() statement.

plot_network <-  function(university, network_list) {
  
  # ~~~~ operations to make our plot ~~~~~ #
  
  return()
}
EXERCISE

Write a function called plot_network. In the body of the function, index the network_list to obtain the matrix of collaborations for the university and then produce and return a network plot for that university.

plot_network <-  function(university, network_list) {

  
  return()
}

Then, using what you know about the arguments it needs, call the function to make a plot of Trent University’s collaboration network.

SOLUTION
plot_network <-  function(university, network_list) {
  
   ## index the network list for the selected university 
  one_uni_matrix = network_list[[university]]
 
  graph <- as_tbl_graph(one_uni_matrix) |> 
    mutate(degree = centrality_degree(mode = 'in'))
  
  # plot using ggraph
  one_uni_plot <- ggraph(graph, layout = 'circle') + 
    geom_edge_fan(aes(alpha = after_stat(index)), show.legend = FALSE) + 
    geom_node_point(aes(size = degree)) + 
    theme_graph(foreground = 'steelblue',
                fg_text_colour = 'white')
  
  return(one_uni_plot)
}
## call the function
plot_network(university = "Trent University", network_list = intra_list)

4 Summary statistics

Shiny can be useful for performing calculations. However we can reduce the complexity of our app by performing calculations beforehand and simply using Shiny to subset and display the results.

We have a database of summary statistics that Lucas calculated using the collaboration dataset:

stats <-  readr::read_rds(here::here(
  "posts/2025-07-06-CSEE-intro-shiny/intra-stats_clean.RDS"
  ))

knitr::kable(head(stats))
inst n_pi mean_collab_prop mean_degree
Carleton University 21 0.1289936 1.7142857
Dalhousie University 34 0.1646219 1.1764706
McGill University 42 0.1558925 4.0476190
McMaster University 11 0.0605210 0.5454545
Memorial University of Newfoundland 31 0.1212651 1.4838710
Queen’s University 24 0.1552643 1.8333333

Let’s make a simple function which returns a table of summary statistics for just one chosen university:

make_stat_table <- function(university, stats_dataframe){
  
  ## get stats for the chosen school
  df <-  stats_dataframe[which(stats_dataframe$inst == university),]
  
  ## reformat the data frame
  new_df <-  data.frame(
    Statistic = c("Number of P.I.s", "Mean collab prop", "Mean degree"), 
    Value = c(df$n_pi, df$mean_collab_prop, df$mean_degree)
  )
  
  ## display table
  return(new_df)
}

make_stat_table("Trent University", stats_dataframe = stats)
         Statistic      Value
1  Number of P.I.s 19.0000000
2 Mean collab prop  0.2722764
3      Mean degree  2.4210526

With these tools, we’re ready to start building a Shiny app!

5 Hello Shiny!

Here is a minimal Shiny app which displays the collaboration dataset.

library(shiny)
library(tidygraph)
library(ggraph)
library(ggplot2)
library(tidyverse)

## read in Lucas's data
intra_list <-  readRDS("intra-list_clean.RDS")
## it is a list with 25 elements containing the data for each of the 25 Canadian universities with the most eco/evo PIs
## each element is a matrix where each node is a PI who received an NSERC discovery grant through the ecology and evolution stream between 2013-22
## each edge in the matrix represents the number of co-authored publications between each PI

## read in an extra data frame of summary statistics about each university 
stats <-  readRDS("intra-stats_clean.RDS")
## n_pi = number of PIs who received an NSERC discovery grants
## mean_collab_prop = mean of the proportions of each’s researchers publications that come from intra-institution collaborations
## mean_degree = mean number of collaborators

plot_network <-  function(university, network_list) {
  
  ## index the network list for the selected university 
  one_uni_matrix = network_list[[university]]
  
  graph <- as_tbl_graph(one_uni_matrix) |> 
    mutate(degree = centrality_degree(mode = 'in'))
  
  # plot using ggraph
  one_uni_plot <- ggraph(graph, layout = 'circle') + 
    geom_edge_fan(aes(alpha = after_stat(index)), show.legend = FALSE) + 
    geom_node_point(aes(size = degree)) + 
    theme_graph(foreground = 'steelblue',
                fg_text_colour = 'white')
  
  return(one_uni_plot)
}

make_stat_table <- function(university, stats_dataframe){
  
  ## get stats for the chosen school
  df <-  stats_dataframe[which(stats_dataframe$inst == university),]
  
  ## reformat the data frame
  new_df <-  data.frame(
    Statistic = c("Number of P.I.s", "Mean collab prop", "Mean degree"), 
    Value = c(df$n_pi, df$mean_collab_prop, df$mean_degree)
  )
  
  ## display table
  return(new_df)
}


ui <- fluidPage(
  
  ## write title for app: 
  titlePanel("Intra-university collaboration networks"),
  
  ## add a dropdown menu where user can select a university
  selectInput(
    inputId = "dropdown_input",
    label = "Choose a school:",
    choices = names(intra_list)
  ),
  
  ## display network plot for chosen university
  plotOutput("schoolPlot"),
  
  ## display some stats for the chosen university
  tableOutput("schoolStats")
)

# Define server logic required to draw a histogram
server <- function(input, output) {
  
  ## define plot output
  output$schoolPlot <- renderPlot({
    
    ## get the chosen school from input into dropdown menu
    choice = input$dropdown_input 
    
    ## display network plot for the school that was chosen 
    plot_network(university = choice, network_list = intra_list)
  })
  
  ## display some statistics for the chosen school
  output$schoolStats <- renderTable({
    ## get the chosen school from input into dropdown menu
    choice = input$dropdown_input 
    
    make_stat_table(university = choice, stats_dataframe = stats)
  })
}

## run the app
shinyApp(ui = ui, server = server)
EXERCISE: Think-Pair-Share

Before we walk through the structure of a Shiny app, take a moment to look at the code above. We’ll each take some time to Think about the code and guess at what it does. Then we’ll encourage you to turn to your neighbours (Pair) and to discuss what you think the app does, and what it looks like (Share). Consider the following questions as you work through this exercise.

  • What does the app look like? How many visual elements are on the screen?
  • What are the two parts of a Shiny app?
  • How do the two parts of a Shiny app “talk” to each other?

6 How a Shiny app works

Building blocks

We’ve now seen the basic building blocks of a Shiny app:

  • The user interface, which determines how the app “looks”. This is how we tell Shiny where to ask for user inputs, and where to put any outputs we create.
  • Reactive values, which are values that change according to user inputs. These are values that affect the outputs we create in the Shiny app, such as tables or plots.
  • The server, where we use reactive values to generate some outputs.

IDs

The user interface and server communicate through IDs that we assign to inputs from the user and outputs from the server.

Figure 1: Shiny apps use identifiers (“id”) to link a visual element in the UI to objects on the server. The programmer chooses the names for these IDs. Here the colour (orange or blue) indicates different IDs; in a real app the orange squares would both be the same ID (e.g. user_selection) and the blue squares would both be a different id (e.g. app_output).

We use an ID (in orange) to link the user input in the UI to the reactive values used in the server

The same ID refers to the value input by the user ..

.. and to the visual element the user interacts with
Figure 2: The orange-coloured ID refers to the selection dropdown. In the UI portion it refers to the visual element the user interacts with. In the server portion it refers to the value given by the user.

We use another ID (in blue) to link the output created in the server to the output shown in the user interface:

The same ID refers to an object created on the server

and visual part of the app
Figure 3: The blue-coloured ID refers to the network figure. In the UI portion it refers to the plot image that the user sees. In the server portion it refers to the R object created by the renderSomething function.

Organisation

These elements can all be placed in one script named app.R or separately in scripts named ui.R and server.R. The choice is up to you, although it becomes easier to work in separate ui.R and server.R scripts when the Shiny app becomes more complex.

Example 1: Everything in app.R

Example 2: Split things into ui.R and server.R
Figure 4: Shiny apps can be created in a single file or split into two for easier organization

7 Constructing a Shiny app using shinyDashboards: taking advantage of good defaults

Now that we understand the basic structure of a Shiny app, we will use the extension shinyDashboards to construct a custom Shiny App to visualize the collaboration dataset. First, we need a few additional packages.

# load packages
library(shiny)
library(shinydashboard)  # dashboard layout package

Attaching package: 'shinydashboard'
The following object is masked from 'package:graphics':

    box
library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
library(ggplot2)

Using ShinyDashboard

We will create our app using defaults from the ShinyDashboard package, which always includes three main components: a header, using dashboardHeader(), a sidebar, using dashboardSidebar(), and a body, using dashboardBody(). These are then added together using the dashboardPage() function.

Building these elements is less like usual R coding, and more like web design, since we are, in fact, designing a unser interface for a web app. Here, we’ll make a basic layout before populating it.

library(shiny)
library(shinydashboard)
# create the header of our app
header <- dashboardHeader(
  title = "Exploring Collaborations",
  titleWidth = 350 # since we have a long title, we need to extend width element in pixels
)
# create dashboard body - this is the major UI element
body <- dashboardBody(
  # make first row of elements (actually, this will be the only row)
  fluidRow(
    # make first column, 25% of page - width = 3 of 12 columns
    column(width = 3,
           # Box 1: text explaining what this app is
           #-----------------------------------------------
           box( width = NULL,
                status="primary", # this line can change the automatic color of the box.
                title = NULL,
                p("here, we'll include some info about this app")
           ), # end box 1
           
           # box 2 : input for selecting University
           #-----------------------------------------------
           box(width = NULL, status = "primary",
               title  = "Selection Criteria", solidHeader = TRUE, 
               
               p("here, we'll add a UI element for selecting the University"),
               
           ), # end box 2
           
           # box 3: Table of 
           #------------------------------------------------
           box(width = NULL, status = "primary",
               solidHeader = TRUE, collapsible = T,
               title = "Summary Statistics",
               p("here, we'll add a table of summary stats for the chosen University")
           ) # end box 3
    ), # end column 1
    
    # second column - 75% of page (9 of 12 columns)
    #--------------------------------------------------
    column(width = 9,
           # Box 4: ggplot2 figure
           box(width = NULL, background = "light-blue", height = 850,
               p("a ggplot of the network within one University"),
           ) # end box with map
    ) # end second column
    
  ) # end fluidrow
) # end body


# add elements together
ui <- dashboardPage(
  skin = "blue",
  header = header,
  sidebar = dashboardSidebar(disable = TRUE), # here, we only have one tab of our app, so we don't need a sidebar
  body = body
)

server <- function(input, output){}

## run the app
shinyApp(ui = ui, server = server)

This Shiny app code produces the app below. It is also in the course folder as app_shinydashboard_blank.png.

EXERCISE: Populating the layout

Lets practice putting the parts of a Shiny app together. Starting from the blank dashboard above, add in the UI and server components from our previous minimal app.

Box 1, the information about the app, can include the following text:

for each of the 25 Canadian universities with the most eco/evo PIs
each element is a matrix where each node is a PI who received an NSERC
discovery grant through the ecology and evolution stream between 2013-22 
each edge in the matrix represents the number of co-authored publications
between each PI

BONUS exercise: add a new box which contains the sentence “The selected University is X” where X is replaced with whatever the user selects from the dropdown menu

A finished version of this app is in the course folder under the name app_02_shinydashboard.R

8 Reducing duplication: introducing reactive()

In the code we’ve written so far, we reference the same input value in three different parts of the app. We can approach this in a different way, by creating an intermediate reactive value that depends on the user input. All the reactive elements (the figure and the text box) will then depend on this new value, instead of the original input. Note that in this case this is NOT necessary, but in many more advanced Shiny projects this becomes essential. Read more about it here:

Here is the server code from app_02_shinydashboard.R, which we just wrote:

server <- function(input, output) {
  ## define plot output
  output$schoolPlot <- renderPlot({
    
    ## get the chosen school from input into dropdown menu
    choice = input$dropdown_input 
    
    ## display network plot for the school that was chosen 
    plot_network(university = choice, network_list = intra_list)
  })
  
  ## display some statistics for the chosen school
  output$schoolStats <- renderTable({
    
    ## get the chosen school from input into dropdown menu
    choice = input$dropdown_input 
    
    make_stat_table(university = choice, stats_dataframe = stats)
  })
  
  output$choice_name <- renderText({input$dropdown_input})
}

And here is a version using reactive()

server <- function(input, output) {
  
    ## get the chosen school from input into dropdown menu
  choice <- reactive(input$dropdown_input)
  
  ## display network plot for the school that was chosen 
  output$schoolPlot <- renderPlot({
    plot_network(university = choice(), network_list = intra_list)
  })
  
  ## display some statistics for the chosen school
  output$schoolStats <- renderTable({
    make_stat_table(university = choice(), stats_dataframe = stats)
  })
  
  # get the school name to print that too
  output$choice_name <- renderText(choice())
}

9 Adding one more figure: University-level statistics

In this section, we’ll practice adding something to the Server side of the app as well. Here is a ggplot2 figure, based on the stats table, showing a relationship between two University-level variables:

library(dplyr)
stats |> 
  mutate(is_selected = inst == "Dalhousie University") |> 
  ggplot(aes(x = mean_collab_prop, y = mean_degree, size = n_pi, colour = is_selected)) +
  guides(colour = "none") +
  geom_point() + 
  scale_colour_manual(values = c("black", "red"))

EXERCISE

Add this figure to our app. Connect it to the input selected by the user, so that the selected point changes along with everything else when a University is chosen.

Solution is in the app app_04_highlight_figure.R

10 Where can my Shiny app live?

  • On your computer
    • In an R package
    • As a file
  • On a server
No matching items

Citation

BibTeX citation:
@online{moore2025,
  author = {Moore, Nikki and MacDonald, Andrew and Lawlor, Jake},
  title = {Introduction to {Shiny} {Apps} {CSEE}},
  date = {2025-07-06},
  url = {https://bios2.github.io/posts/2025-07-06-CSEE-intro-shiny/},
  langid = {en}
}
For attribution, please cite this work as:
Moore, Nikki, Andrew MacDonald, and Jake Lawlor. 2025. “Introduction to Shiny Apps CSEE.” BIOS2 Education Resources. July 6, 2025. https://bios2.github.io/posts/2025-07-06-CSEE-intro-shiny/.