Xiaofan Liang Created in 2024; Updated in 2025

Learning Objectives

By the end of this practical lab you will be able to:

CHEATSHEETS! You are encouraged to download RStudio Cheatsheets. You can find them on RStudio website here, or a comprehensive list (more than what has been listed) here. For example, here you can find base R syntax, dplyr, ggplot2, sf, leaflet.

Functions Package Tasks
leaflet() leaflet Initializes a Leaflet map object.
addTiles() leaflet Adds a default OpenStreetMap (OSM) basemap to the Leaflet map.
addCircleMarkers() leaflet Adds circle markers to the map, useful for representing point data.
addProviderTiles() leaflet Adds a tile layer from various providers (e.g., CartoDB, Stamen, etc.).
setView() leaflet Sets the initial map view by defining the latitude, longitude, and zoom level.
addLegend() leaflet Adds a legend to the map for better visualization of data categories.
leafletProxy() leaflet Modifies an existing Leaflet map without redrawing it completely, improving performance.
clearMarkers() leaflet Removes all existing markers from the map before adding new ones.
clearControls() leaflet Clears map controls such as legends and overlays.
fluidPage() shiny Defines a fluid and responsive UI layout for the Shiny app.
titlePanel() shiny Adds a title to the Shiny app interface.
absolutePanel() shiny Creates a fixed-position panel for UI components in Shiny.
selectInput() shiny Creates a dropdown menu for user selection in the Shiny UI.
leafletOutput() shiny Creates a UI placeholder for rendering a Leaflet map in a Shiny app.
renderLeaflet() shiny Generates and updates the Leaflet map dynamically in the Shiny app.
reactive() shiny Creates a reactive expression that updates based on user input.
observe() shiny Monitors reactive expressions and executes actions when input values change.
shinyApp() shiny Runs the Shiny app by combining the UI and server components.
rsconnect::setAccountInfo() rsconnect Sets authentication details for deploying a Shiny app on shinyapps.io.
rsconnect::deployApp() rsconnect Deploys the Shiny app to the shinyapps.io server.
rsconnect::showLogs() rsconnect Displays logs for debugging deployment issues on shinyapps.io.

For Python users, you may reference Folium for visualization with leaflet capacity. You can also explore leafmap, a Python package for geospatial analysis and interactive mapping in a Jupyter environment. leafmap (link to documentation here) is developed by Prof. Qiusheng Wu, who is well-known for sharing open-source tutorials and developing packages for geospatial analysis and visualization in Python (see Prof. Wu’s Youtube Channel).

The other alternative to leaflet R package is to use mapgl, which makes the latest versions of Mapbox GL JS and MapLibre GL JS available to R users. The package interface is designed to make the powerful capabilities of both libraries available in R mapping projects. It allows you to also use mapgl with shiny and build story maps. This package is released in Jan, 2025, so could be a little unstable and need more time for specific tutorials. As of now, we would still go with leaflet as it has a longer history.

For this lab, we are going to build a little web dashboard that shows the OSM amenity data in Ann Arbor

Setting environment, installing and loading libraries

The new package in this lab is leaflet, shiny, and `rsconnect``.

leaflet R package allows you to interactive with Leaflet JS, a javascript-based language for web map visualization. You can use leaflet in R, Python, or its original form in Javascript.

shiny allows you to design data-driven interactive UI (User Interface) component (e.g., update web visualization based on a data filter).

rsconnect allows you to deploy to the web map with a limited free tier service at shinyapp.io.

#replace my path with yours to the Lab 5 folder 
# setwd("/Users/xfliang/University of Michigan Dropbox/Xiaofan Liang/UM_Teaching/URP535_Urban_Informatics/W25/Lab/Lab10/")
setwd("/Users/susancheng/Desktop/Labs/Lab10")

#install.packages('tidyverse')
library(tidyverse)
#install.packages('tmap') 
library(tmap)
#install.packages('sf') 
library(sf)
#install.packages('tmap') 
library(osmdata)
#install.packages('tmap') 
library(osmdata)
#install.packages('leaflet') 
library(leaflet) # for visualizing leaflet maps
#install.packages("shiny")
library(shiny) # for writing a shiny app
# install.packages("rsconnect")  
library(rsconnect)# for deploying a shiny app

Retrieve OSM amenity data for Ann Arbor

To find out all OSM features for the key amenity, you can check out OSM wiki.

# Get all food-related amenities in Ann Arbor
q <- opq(bbox = getbb("Ann Arbor, US")) %>%
  add_osm_features(features = c(
    "amenity" = "restaurant",
    "amenity" = "bar",
    "amenity" = "biergarden",
    "amenity" = "fast_food",
    "amenity" = "food_court",
    "amenity" = "cafe",
    "amenity" = "pub",
    "amenity" = "ice_cream"
  )) %>% 
  osmdata_sf() 

# only take point geometry and drop entries that do not have values in name and amenity category
amenity_point <- q$osm_points %>% 
  select(osm_id, name, amenity, geometry) %>% 
  drop_na(name, amenity)

head(amenity_point)
## Simple feature collection with 6 features and 3 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: -83.74839 ymin: 42.25572 xmax: -83.68812 ymax: 42.28785
## Geodetic CRS:  WGS 84
##              osm_id                 name    amenity                   geometry
## 305088494 305088494       Casey's Tavern        pub POINT (-83.74434 42.28785)
## 541900696 541900696            Starbucks       cafe POINT (-83.68812 42.25572)
## 560917683 560917683  Good Time Charley's        bar POINT (-83.73485 42.27482)
## 695043656 695043656 Real Seafood Company restaurant POINT (-83.74839 42.27837)
## 748964467 748964467     Pita Kabob Grill restaurant POINT (-83.74112 42.27797)
## 749007070 749007070   Ashley's Ann Arbor        pub  POINT (-83.74095 42.2781)

Preview Leaflet Map before Deployment (on the Web)

A Simple Comparison: leaflet vs. tmap

Let’s create the simplest leaflet map first. The basic map component includes data for mapping, a base map tile (the default is the OpenStreetMap tile), and how you want to visualize the data. In leaflet, the points are called CircleMarkers.

leaflet(data = amenity_point) %>%
  # Adds a default OpenStreetMap basemap.
  addTiles() %>%
  # Adds interactive markers to the map.
  addCircleMarkers()

This is an equivalent web map you can create using tmap v4. With this simple demonstration, the result and complexity of code looks pretty similar.

tmap_mode('view')
## ℹ tmap mode set to "view".
tm_shape(amenity_point) + 
  # change the default tmap basemap to OpenStreetMap to be consistent for comparison
  tm_basemap("OpenStreetMap.Mapnik") + 
  tm_symbols(fill='blue', fill_alpha = 0.5)

The major difference between tmap and leaflet map is that leaflet is built for interactive web map, so excels at interactivity. Here is a comparison table for the two:

leaflet strength:

  • Designed primarily for interactive web maps.
  • Highly customizable—you can add popups, legends, color scales, and tooltips.
  • Integrates well with shiny for dashboard applications.

leaflet weakness:

  • Limited support for publication-ready static maps.
  • Styling and design are more focused on interactivity rather than high-quality static outputs.
  • Works primarily with WGS 84 (EPSG:4326) coordinate reference system, so projections can be limited.

tmap v4 strength:

  • Dual mode: Can create static maps for publication (tmap_mode("plot")) or interactive maps (tmap_mode("view")).
  • Supports advanced cartographic visualization (e.g., choropleth, panel maps and facet mapping).
  • Works well with projected CRS (not just WGS 84), making it more flexible for spatial analysis.

tmap v4 weakness:

  • Less customization for interactivity compared to leaflet (e.g., no easy layer control).

Adding Advanced Aesthetics to leaflet

We would like to add aesthetics and interactivity to the leaflet map, by changing to a grey color base map, varying the color of the amenity based on amenity types, adding pop up to show the amenity name, and adding a legend.

Both tmap and leaflet accepts basemaps in the names listed here. This link helps you see what the basemap looks like by toggling to different basemap names and see the effects. An example of changing basemap for tmap is shown in the section above. This tutorial provides more details on how to call various basemaps with leaflet.

# Checking how many unique values are needed to assign color by the amenity type
amenity_type <- unique(amenity_point$amenity)
amenity_type
## [1] "pub"        "cafe"       "bar"        "restaurant" "fast_food" 
## [6] "ice_cream"
amenity_colors <- c(
  "pub" = "red",
  "cafe" = "blue",
  "bar" = "darkgreen",
  "restaurant" = "orange",
  "fast_food" = "brown",
  "ice_cream" = "purple"
)

# Initializes the Leaflet map using the amenity_point dataset.
leaflet(data = amenity_point) %>%
  # Use the CartoDB.Positron basemap instead of OSM basemap 
  addProviderTiles(providers$CartoDB.Positron) %>%
  # Adds interactive markers to the map.
  addCircleMarkers(
    # Extracts longitude values from the sf geometry column.
    lng = st_coordinates(amenity_point)[,1], 
    # Extracts latitude values from the sf geometry column.
    lat = st_coordinates(amenity_point)[,2], 
    # Displays a popup with amenity details when clicked.
    popup = ~paste0("<b>", name, "</b><br>", amenity),
    # Defines marker size.
    radius = 3,  
    # Vectorize color mapping 
    color = ~unname(amenity_colors[amenity]), 
    # Make markers slightly transparent for better visibility
    fillOpacity = 0.8,  
  ) %>%
  addLegend(
    # Position the legend at the bottom right of the map
    "bottomright",  
    # Use the predefined colors assigned to each amenity type
    colors = amenity_colors,  
    # Display corresponding labels (amenity names)
    labels = names(amenity_colors),  
    # Add a title to the legend
    title = "Amenity Type",  
    # Ensure the legend is fully visible
    opacity = 1  
  ) %>%
  # Set the initial map view centered on Ann Arbor
  setView(lng = -83.7430, lat = 42.2808, zoom = 12)  

Try zooming in and out of the map and click on the circles / points to see the pop up!

If you want to see more examples of using leaflet for mapping, such as Choropleths, Lines and Shapes, or even with Raster Images, please go to official pacakge documentation and click on Articles to see a series of tutorials.

Deploy a Static Web Map from leaflet

The output in the previous section is what we called a “static web map”. This is different from the “static map” like what is produced by tmap in the plot mode.

In the “static web map”, you can still interact with the map components, but you cannot give input to the map on the interface (e.g., filter to see only amenities that are restaurant) and expect the map to respond dynamically. If you purpose is simply to display data and allow users to SEE what the data are like (e.g., with pop up), then you DON’T NEED an interactive web map. The code above is sufficient for your purpose.

At this point, you can publish this map in RPub like we have done with tmap in the last lab and receive a RPub link.

The other option is to export the map in the HTML format and host on GitHub Pages. This tutorial shows you how to save leaflet map into an HTML widget. You will need knowledge of how GitHub works to deploy in this approach.

(OPTIONAL) Publishing Leaflet Map with Shiny App

Shiny App is an interactive application where users can filter, update, or analyze spatial data in real-time, which is what we refer to here as an interactive web map. For instance, users can filter data, change layers, and run analysis with sliders, dropdowns, and buttons. It is great for making data dashboards with leaflet.

Define the Shiny UI

UI (User Interface) determines what components users see on the web page. Here for the UI, we want to add a title, a sidebar that allows users to filter restaurants based on amenity types, and display the interactive map.

# ----- Define the Shiny UI ----- # 

# Define the UI for a Shiny web application
ui <- fluidPage(
  
  # Set the title of the application
  titlePanel("Interactive Amenity Map of Ann Arbor"),
  
  # Create a Leaflet map output with a height of 700 pixels
  leafletOutput("map", height = "700"),
  
  # Create an absolute panel (floating panel) for UI controls
  absolutePanel(
    
    # Position the panel 70 pixels from the top and 30 pixels from the right
    top = 70, right = 30,   
    
    # Allow the panel to be draggable by the user
    draggable = TRUE,   
    
    # Apply custom styling: high z-index to ensure it's above other elements, 
    # white background, padding, and rounded corners
    style = "z-index:500; background-color: white; padding: 10px; border-radius: 5px;",
    
    # Create a dropdown menu (select input) for choosing the type of amenity to display
    selectInput(
      inputId = "selected_amenity",  # The ID used to access this input in the server logic
      label = "Select Amenity Type:", # The label displayed above the dropdown
      choices = c("All", unique(amenity_point$amenity)), # Populate dropdown with unique amenity types plus "All"
      selected = "All"  # Default selected option is "All"
    )
  )
)

Define the Shiny Server

Servers determines how you want the user inputs to interact with data and how you want to map to response to user inputs. Here, we define the server functions to be two things:

  • Filters amenities dynamically based on user selection, and
  • Updates the Leaflet map without reloading.
# Define the server logic for the Shiny application

server <- function(input, output, session) {
  
  # Define a named vector to assign colors to different amenity types
  amenity_colors <- c(
    "pub" = "red",       # Assign red color to pubs
    "cafe" = "blue",     # Assign blue color to cafes
    "bar" = "darkgreen", # Assign dark green color to bars
    "restaurant" = "orange", # Assign orange color to restaurants
    "fast_food" = "brown",   # Assign brown color to fast food places
    "ice_cream" = "purple"   # Assign purple color to ice cream shops
  )

  # Create a reactive expression to filter the dataset based on the selected amenity type
  filtered_data <- reactive({
    if (input$selected_amenity == "All") {
      return(amenity_point)   # If "All" is selected, return the entire dataset
    } else {
      return(amenity_point %>% filter(amenity == input$selected_amenity)) # Otherwise, filter by selected amenity
    }
  })

  # Render the initial Leaflet map with a tile layer and legend
  output$map <- renderLeaflet({
    leaflet(amenity_point) %>%  # Create a Leaflet map using the full dataset
      addProviderTiles(providers$CartoDB.Positron) %>%  # Add a light-colored basemap
      setView(lng = -83.7430, lat = 42.2808, zoom = 12) %>%  # Center the map on Ann Arbor with zoom level 12
      addLegend(
        "bottomright",  # Position the legend at the bottom right
        colors = amenity_colors,  # Use the predefined colors for amenities
        labels = names(amenity_colors),  # Use the names of amenities as legend labels
        title = "Amenity Type",  # Set the title of the legend
        opacity = 1  # Set the legend opacity to fully visible
      )
  })

  # Observe changes in the selected amenity type and update the map markers dynamically
  observe({
    leafletProxy("map", data = filtered_data()) %>%  # Use leafletProxy to update the existing map
      clearMarkers() %>%  # Remove existing markers before adding new ones
      addCircleMarkers(
        lng = ~st_coordinates(geometry)[,1],  # Extract longitude from geometry column
        lat = ~st_coordinates(geometry)[,2],  # Extract latitude from geometry column
        popup = ~paste0("<b>", name, "</b><br>", amenity),  # Create a popup with the name and amenity type
        radius = 3,  # Set marker size
        color = ~unname(amenity_colors[amenity]),  # Assign color based on amenity type
        fillOpacity = 0.8  # Set marker fill opacity
      )
  })

  # Observe changes and update the legend dynamically when the selection changes
  observe({
    leafletProxy("map") %>%  # Use leafletProxy to modify the existing map
      clearControls() %>%   # Remove the existing legend before adding a new one
      addLegend(
        "bottomright",  # Position the legend at the bottom right
        colors = amenity_colors,  # Use the predefined colors for amenities
        labels = names(amenity_colors),  # Use the names of amenities as legend labels
        title = "Amenity Type",  # Set the title of the legend
        opacity = 1  # Set the legend opacity to fully visible
      )
  })
}

Deploy the Shiny App Locally

Deploy a web map on the public internet domain using Shiny App requires a server. You can test to see what that App look like by deploying it locally first. This is a standard practice in app development. The local deployment is free and fast, though only you can see the outcome. The code below will pop up a new window that shows you what the deployment looks like.

Feel free to adjust the aesthetics and variables. This is a tutorial that introduces more about using leaflet with R Shiny app, such as how to understand layers, how to add inputs/events, etc.

shinyApp(ui, server)

To further deploy it on the web rather than locally, you will have to save all lines that start from loading libraries, importing data and setting up ui and server into one file. Let’s first export our data. You can export as either shp or geojson. For simplicity, we export the data as geojson, so that it only has one file (instead of a series of files as in shp).

# export amenity_point to save at the local folder 
st_write(amenity_point, 'a2_amenity/amenity_point.geojson')

IMPORTANT

Next, copy all the code below into a new R script named “app.R” that will be deployed to a the Shiny App server (next section). You can consider this as “exporting” all the environment/library, data, and code, to a server so that the code will be run in the cloud. Since deployment will send everything in the same folder as app.R to the cloud, we created a separate folder called a2_amenity to separate lab materials (which we don’t want to send to the cloud) from app.R. This folder name will also become part of the domain, so you can customize what you want to show in the domain by changing the folder name.

library(sf)
library(shiny)
library(leaflet)
library(tidyverse)

amenity_point = st_read('a2_amenity/amenity_point.geojson')

ui <- fluidPage(
  titlePanel("Interactive Amenity Map of Ann Arbor"),
  leafletOutput("map", height = "700"),
  absolutePanel(
    top = 70, right = 30,  
    draggable = TRUE,  
    style = "z-index:500; background-color: white; padding: 10px; border-radius: 5px;",
    selectInput(
      inputId = "selected_amenity", 
      label = "Select Amenity Type:", 
      choices = c("All", unique(amenity_point$amenity)),
      selected = "All" 
    )
  )
)

server <- function(input, output, session) {
  amenity_colors <- c(
    "pub" = "red",
    "cafe" = "blue",
    "bar" = "darkgreen",
    "restaurant" = "orange",
    "fast_food" = "brown",
    "ice_cream" = "purple"
  )
  filtered_data <- reactive({
    if (input$selected_amenity == "All") {
      return(amenity_point)  
    } else {
      return(amenity_point %>% filter(amenity == input$selected_amenity))
    }
  })
  output$map <- renderLeaflet({
    leaflet(amenity_point) %>%
      addProviderTiles(providers$CartoDB.Positron) %>%
      setView(lng = -83.7430, lat = 42.2808, zoom = 12) %>%
      addLegend(
        "bottomright",
        colors = amenity_colors,  
        labels = names(amenity_colors),  
        title = "Amenity Type",
        opacity = 1
      )
  })
  observe({
    leafletProxy("map", data = filtered_data()) %>%
      clearMarkers() %>%
      addCircleMarkers(
        lng = ~st_coordinates(geometry)[,1],  
        lat = ~st_coordinates(geometry)[,2],  
        popup = ~paste0("<b>", name, "</b><br>", amenity),  
        radius = 3,  
        color = ~unname(amenity_colors[amenity]),  
        fillOpacity = 0.8
      )
  })
  observe({
    leafletProxy("map") %>%
      clearControls() %>%  
      addLegend(
        "bottomright",
        colors = amenity_colors,  
        labels = names(amenity_colors),  
        title = "Amenity Type",
        opacity = 1
      )
  })
}

shinyApp(ui, server)

Deploy the Shiny App Locally on a Public Internet Domain

shinyapps.io is a free hosting service for Shiny apps, which is developed by the same company that develops R. After deployment, you would be able to view the interactive web map at a domain like below: https://your-username.shinyapps.io/your-app-name/.

Here is what my link looks like and you can see what the final deployment would look like: https://xiaofanliang.shinyapps.io/a2_amenity/

  1. Create a shinyapps.io Account. Go to https://www.shinyapps.io and sign up.

  2. Install and load the rsconnect package if not already installed or loaded.

  3. Find your name, token, and secret values by going to Account -> Tokens -> Add Token (or Show). Replace shiny_name, shiny_token, and shiny_secret with your own name, token, and secret values.

library(rsconnect)
# don't run this line. You won't have access to the file shiny_key.R as it stores the instructor's key.
source("shiny_key.R")
rsconnect::setAccountInfo(name=shiny_name,
                          token=shiny_token,
                          secret=shiny_secret)
  1. Deploy your app. In RStudio, make sure to set your working directory to the folder containing app.R. We reset the working directory to point to the a2_amenity folder.
setwd("/Users/xfliang/University of Michigan Dropbox/Xiaofan Liang/UM_Teaching/URP535_Urban_Informatics/W25/Lab/Lab10/a2_amenity/")

# This will upload and publish your app on shinyapps.io.; adding "::" between package and function ensures that the function `deployApp()` come from the package `rsconnect`
rsconnect::deployApp()

Debugging Shiny App

If there is any error for dployment, run the following line to see where the errors occur.

rsconnect::showLogs()

It is also always a good practice to make sure your local deployment is correct before you deploy to shinyapps.io. The reason is that local deployment is faster so you can adapt based on error message faster.

Some of the common problems for Shiny App includes:

  • Did not include all libraries needed in the app.R
  • Did not include the line shinyApp(ui, server) at the end of the app.R

Practice Questions

Q1: Implement New Leaflet Marker Technique

Explore leaflet documentation - Add markers to leaflet page. Pick one feature to implement in addition to the amenity map in the lab (colored by amenity type), such as customizing marker icons, marker cluster, or customize the circle markers by radius.

amenity_icons <- iconList(
  pub = makeIcon(
    iconUrl = "https://cdn-icons-png.flaticon.com/512/1146/1146002.png",
    iconRetinaUrl = "https://cdn-icons-png.flaticon.com/512/1146/1146002.png",
    iconWidth = 10, iconHeight = 10
  ),
  cafe = makeIcon(
    iconUrl = "https://static.thenounproject.com/png/1149467-200.png",
    iconRetinaUrl = "https://static.thenounproject.com/png/1149467-200.png",
    iconWidth = 10, iconHeight = 10
  ),
  bar = makeIcon(
    iconUrl = "https://cdn-icons-png.flaticon.com/512/3514/3514280.png",
    iconRetinaUrl = "https://cdn-icons-png.flaticon.com/512/3514/3514280.png",
    iconWidth = 10, iconHeight = 10
  ),
  restaurant = makeIcon(
    iconUrl = "https://static.thenounproject.com/png/323504-200.png",
    iconRetinaUrl = "https://static.thenounproject.com/png/323504-200.png",
    iconWidth = 10, iconHeight = 10
  ),
  fast_food = makeIcon(
    iconUrl = "https://cdn-icons-png.flaticon.com/512/1046/1046886.png",
    iconRetinaUrl = "https://cdn-icons-png.flaticon.com/512/1046/1046886.png",
    iconWidth = 10, iconHeight = 10
  ),
  ice_cream = makeIcon(
    iconUrl = "https://static.thenounproject.com/png/43136-200.png",
    iconRetinaUrl = "https://static.thenounproject.com/png/43136-200.png",
    iconWidth = 10, iconHeight = 10
  )
)

leaflet(data = amenity_point) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addMarkers(
    lng = st_coordinates(amenity_point)[,1], 
    lat = st_coordinates(amenity_point)[,2], 
    popup = ~paste0("<b>", name, "</b><br>", amenity),
    icon = ~amenity_icons[amenity]
  ) %>%
  addLegend(
    "bottomright",  
    colors = c("red", "blue", "darkgreen", "orange", "brown", "purple"),  
    labels = c("Pub", "Cafe", "Bar", "Restaurant", "Fast Food", "Ice Cream"),  
    title = "Amenity Type",  
    opacity = 1  
  ) %>%
  setView(lng = -83.7430, lat = 42.2808, zoom = 12)

Q2: Implement a Choropleth

Explore leaflet documentation - Choropleths page. Use MI_counties.shp in Lab 4 and replicate a similar Choropleth as in the tutorial. The following features should present, while the rest is up to you:

  • Vary the color of the polygon by a variable.
  • Add highlight border when the mouse passes over the polygon.
  • Have a legend

Q3: Deploy

Deploy the leaflet map in Q1 and the choropleth map Q2 to RPub or as a Shiny App (Advanced dynamic feature with Shiny App is optional). Save the links to submit to the Quiz.