1. Introduction to Shiny

What is Shiny?

Shiny is an R package that makes it easy to build interactive web apps directly from R.
It bridges the gap between statistical computing and user-friendly interfaces, enabling data scientists to create dynamic dashboards and applications without needing HTML, CSS, or JavaScript knowledge.

You can learn more at the official Shiny website.

Shiny App Structure

Every Shiny app has two main parts:

  • UI (User Interface): Describes how the app looks
  • Server: Contains instructions to build outputs based on inputs
library(shiny)

# Define UI
ui <- fluidPage(
  titlePanel("Hello Shiny!"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("obs", "Number of observations:", min = 1, max = 1000, value = 500)
    ),
    mainPanel(
      plotOutput("distPlot")
    )
  )
)

# Define server logic
server <- function(input, output) {
  output$distPlot <- renderPlot({
    hist(rnorm(input$obs))
  })
}

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

Save this in an .R file and click Run App from RStudio, or run it inline using RMarkdown with:

shinyApp(ui = ui, server = server)
πŸ’‘ Tip: Explore built-in Shiny examples
runExample("01_hello")      # a histogram\
runExample("02_text")       # tables and data frames\
runExample("03_reactivity") # a reactive expression\
runExample("04_mpg")        # global variables\
runExample("05_sliders")    # slider bars\
runExample("06_tabsets")    # tabbed panels\
runExample("07_widgets")    # help text and submit buttons\
runExample("08_html")       # Shiny app built from HTML\
runExample("09_upload")     # file upload wizard\
runExample("10_download")   # file download wizard\
runExample("11_timer")      # an automated timer\

2. Inputs and Outputs

Shiny apps are built around user inputs and rendered outputs. Users interact with the app through inputs like sliders, text boxes, and dropdowns β€” and in response, Shiny displays outputs like text, tables, or plots.

Basic Inputs

Shiny provides a variety of input functions. Here are some of the most commonly used:

sliderInput("obs", "Number of observations:", min = 1, max = 1000, value = 500)
textInput("caption", "Caption:", "My Plot")
selectInput("dataset", "Choose a dataset:", choices = c("rock", "pressure", "cars"))

Each input has an ID (e.g., β€œobs”, β€œcaption”) used to reference it in the server() function.

Basic Outputs

To display content dynamically, Shiny provides output functions:

textOutput("caption")     # Displays reactive text
plotOutput("distPlot")    # Displays a reactive plot
tableOutput("dataTable")  # Displays a static table

These are placed in the UI and linked to corresponding render functions in the server.

Reactivity

The magic of Shiny is its reactive programming model. When an input changes, outputs that depend on it are automatically updated.

output$caption <- renderText({
  input$caption
})

output$distPlot <- renderPlot({
  hist(rnorm(input$obs))
})

These render functions:

Start with render*() (e.g., renderText, renderPlot)

Are paired with output elements like textOutput() or plotOutput()

πŸ’‘ Tip: Use reactive expressions to avoid repetition

You can use reactive() to store a block of code that is used in multiple places:

data <- reactive({
  rnorm(input$obs)
})

output$distPlot <- renderPlot({
  hist(data())
})

This way, if you need to reuse the same logic across outputs, you write it only once.

Minimal Working App Example

Code

library(shiny)

ui <- fluidPage(
  titlePanel("Reactive Inputs and Outputs"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("obs", "Number of observations:", min = 1, max = 1000, value = 500),
      textInput("caption", "Plot title:", "My Histogram")
    ),
    mainPanel(
      textOutput("caption"),
      plotOutput("distPlot")
    )
  )
)

server <- function(input, output) {
  output$caption <- renderText({ input$caption })

  output$distPlot <- renderPlot({
    hist(rnorm(input$obs), main = input$caption)
  })
}

shinyApp(ui = ui, server = server)

Output

3. Creating Tables and Plots

Displaying tables and plots is one of the most common uses of Shiny apps. You can show both static tables and interactive plots, depending on the user needs.

Displaying Tables

Shiny supports two main ways to show tabular data:

tableOutput() + renderTable()

For simple, static tables:

output$summary <- renderTable({
  head(cars)
})

In the UI:

tableOutput("summary")

Displaying Plots

Using renderPlot() and plotOutput()
The most common way to show plots is with base R or ggplot2.

output$distPlot <- renderPlot({
  hist(rnorm(input$obs))
})

UI:

plotOutput("distPlot")

You can also use ggplot2:

output$gg <- renderPlot({
  library(ggplot2)
  ggplot(mtcars, aes(x = mpg, y = wt)) +
    geom_point()
})

Minimal Working Example

Code

library(shiny)
library(DT)
library(ggplot2)
library(plotly)

ui <- fluidPage(
  titlePanel("Tables and Plots"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("obs", "Number of observations:", 1, 1000, 500)
    ),
    mainPanel(
      tabsetPanel(
        tabPanel("Table", DT::dataTableOutput("datatable")),
        tabPanel("Plot", plotOutput("distPlot")),
        tabPanel("Interactive Plot", plotly::plotlyOutput("interactivePlot"))
      )
    )
  )
)

server <- function(input, output) {
  output$datatable <- DT::renderDataTable({
    head(cars, 10)
  })

  output$distPlot <- renderPlot({
    hist(rnorm(input$obs))
  })

  output$interactivePlot <- plotly::renderPlotly({
    plot_ly(data = mtcars, x = ~mpg, y = ~wt, type = "scatter", mode = "markers")
  })
}

shinyApp(ui = ui, server = server)

Output

4. Working with Spatial Data in Shiny

Shiny can be extended to handle spatial data using packages like leaflet, sf, and mapview. These tools allow users to explore geographic data interactively β€” zoom, pan, click, and even draw on maps.

Displaying Interactive Maps with Leaflet

The leaflet package is the easiest way to display maps in Shiny. It integrates smoothly with Shiny inputs and outputs.

ui <- fluidPage(
  titlePanel("Simple Leaflet Map"),
  leafletOutput("map")
)

server <- function(input, output, session) {
  output$map <- renderLeaflet({
    leaflet() %>%
      addTiles() %>%
      addMarkers(lng = -81.6557, lat = 30.3322, popup = "Jacksonville, FL")
  })
}

Run the app

Mapping Spatial Data

If you’re working with shapefiles or spatial objects (e.g., sf), load them and add them directly to the map.

library(sf)

nc <- st_read(system.file("shape/nc.shp", package="sf"))
leaflet(nc) %>%
  addTiles() %>%
  addPolygons()

In Shiny:

output$map <- renderLeaflet({
  leaflet(nc) %>%
    addTiles() %>%
    addPolygons(color = "blue", weight = 2)
})
πŸ’‘ Tip: Use reactive inputs to filter spatial data


You can dynamically filter features based on user input. For example:

filtered_data <- reactive({
  nc[nc$AREA > input$area_thresh, ]
})

output$map <- renderLeaflet({
  leaflet(filtered_data()) %>%
    addTiles() %>%
    addPolygons()
})

Use this with a sliderInput() to let users control which polygons are shown.

You can dynamically filter features based on user input. For example:

filtered_data <- reactive({
  nc[nc$AREA > input$area_thresh, ]
})

output$map <- renderLeaflet({
  leaflet(filtered_data()) %>%
    addTiles() %>%
    addPolygons()
})

Use this with a sliderInput() to let users control which polygons are shown.

##Spatial Input: Map Clicks and Selections You can also capture user input on the map β€” clicks, bounds, or shapes.

observeEvent(input$map_click, {
  click <- input$map_click
  showModal(modalDialog(
    title = "You clicked on the map!",
    paste("Longitude:", click$lng, "<br>Latitude:", click$lat)
  ))
})

This opens up a wide range of interactive GIS applications.

Minimal Working App

Code

library(shiny)
library(leaflet)

ui <- fluidPage(
  titlePanel("Spatial Data in Shiny"),
  leafletOutput("map"),
  verbatimTextOutput("click_info")
)

server <- function(input, output, session) {
  output$map <- renderLeaflet({
    leaflet() %>%
      addTiles() %>%
      addMarkers(lng = -81.6557, lat = 30.3322, popup = "Jacksonville, FL")
  })

  output$click_info <- renderPrint({
    if (is.null(input$map_click)) return("Click on the map")
    paste("Latitude:", input$map_click$lat, 
          "Longitude:", input$map_click$lng)
  })
}

shinyApp(ui, server)

Output

5. Spatio-temporal Visualizations

Spatio-temporal apps allow users to explore how geographic phenomena evolve over time. In Shiny, this can be done using time sliders, animated maps, and reactive filtering of time-stamped spatial data.

Packages like leaflet, mapdeck, gganimate, and shinyWidgets help bring these visualizations to life.

Time-based Filtering with Slider Input

You can use a sliderInput() with leaflet or other spatial data to allow users to select a time window.

Example: Filtering points by year

library(shiny)
library(leaflet)

# Sample data
years <- 2010:2020
locations <- data.frame(
  year = rep(years, each = 1),
  lat = runif(length(years), 25, 30),
  lon = runif(length(years), -85, -80)
)

ui <- fluidPage(
  titlePanel("Spatio-temporal Viewer"),
  sliderInput("year", "Select Year:", min = 2010, max = 2020, value = 2015, step = 1, sep = ""),
  leafletOutput("map")
)

server <- function(input, output, session) {
  output$map <- renderLeaflet({
    leaflet() %>% addTiles()
  })

  observe({
    year_data <- subset(locations, year == input$year)
    leafletProxy("map") %>%
      clearMarkers() %>%
      addCircleMarkers(data = year_data, lng = ~lon, lat = ~lat, radius = 6, color = "red")
  })
}

shinyApp(ui, server)

6. Best Practices & Resources

As you build more complex Shiny apps, it’s helpful to follow best practices and learn from reliable, up-to-date resources.


βœ… Best Practices

  • Modularize your code using functions and modules for reusable UI/server components
  • Use reactive() and observe() carefully β€” avoid unnecessary re-computation
  • Separate UI and server logic to make code more maintainable
  • Use reactiveValues() for shared state instead of global variables
  • Add loading indicators (shinycssloaders, shinybusy) to improve user experience
  • Profile and test apps with profvis() and shiny::reactlog()