1 Instructions

This practical covers interactive geospatial visualisation using leaflet and Shiny. There are two worked examples:

For each example you will first build a standalone leaflet map (no Shiny needed — run the code directly in the console or in an R script), then wrap it in a Shiny app that adds a dropdown selector. This mirrors the static first, then reactive principle from the notes.


2 Example 1: Northern Ireland Super Data Zones

2.1 Part A: Standalone leaflet map

Download the Northern Ireland Super Data Zone boundaries from NISRA at https://www.nisra.gov.uk/publications/super-data-zone-boundaries-gis-format (choose the ESRI Shapefile option), or from Canvas. Extract the files into a folder geography-sdz2021-esri-shapefile/ in your working directory.

  1. Load the NI SDZ shapefile, transform it to WGS84 (which leaflet requires), and inspect the result.

    library(sf)
    
    ni_sdz <- read_sf("geography-sdz2021-esri-shapefile/SDZ2021.shp") |>
      st_transform(4326)
    
    names(ni_sdz)
    st_crs(ni_sdz)$epsg

    Answer: The shapefile has columns SDZ2021_cd, SDZ2021_nm, DEA2014_cd, DEA2014_nm, LGD2014_cd, LGD2014_nm, Area_ha, Perim_km, and geometry. After st_transform(4326) the CRS is EPSG 4326 (WGS84), which leaflet requires.

    The shapefile contains only boundary and administrative metadata. The accompanying census dataset ni-census-sdz2021.csv (available on Canvas) records population counts and density per Super Data Zone. If you wanted to create a choropleth map (colouring zones by population density), you would need to join the two datasets on SDZ2021_cd.

  2. Build a minimal leaflet map of the NI SDZ polygons using leaflet(), addTiles(), and addPolygons().

    Before writing the leaflet code, think about the equivalent static map you would create with ggplot2: what function draws polygon features, and what provides the background? How does the leaflet approach mirror that structure?

    library(leaflet)
    
    leaflet(ni_sdz) |>
      addTiles() |>
      addPolygons()

    Answer: The ggplot2 equivalent would be ggplot(ni_sdz) + geom_sf() with a rnaturalearth background added via a separate geom_sf() layer. In leaflet, addTiles() replaces the background layer and addPolygons() replaces geom_sf(). The result is already pannable and zoomable, unlike the static ggplot2 output.

    Run this in the console (not inside a Shiny app). The result should be a pannable, zoomable map of Northern Ireland with all Super Data Zone boundaries drawn.

  3. Add a popup to the map so that clicking a polygon reveals the SDZ name and district. Use paste0() to build the HTML string.

    leaflet(ni_sdz) |>
      addTiles() |>
      addPolygons(
        color       = "steelblue",
        fillOpacity = 0.4,
        weight      = 1,
        popup = ~paste0(
          "<b>", SDZ2021_nm, "</b><br>",
          "District: ", LGD2014_nm, "<br>",
          "DEA: ",      DEA2014_nm
        )
      )

    Click any Super Data Zone to verify the pop-up appears. Note that popup (click to reveal) and label (hover tooltip) can be used interchangeably; for polygons, clicking is often more convenient than hovering.

2.2 Part B: Shiny app for the NI data

  1. Write a Shiny app that provides a selectInput for LGD2014_nm (the 11 NI councils) and filters ni_sdz to the selected district inside renderLeaflet(), showing the pop-ups from question 3 for the filtered data.

    Load the data once outside the server function. Inside renderLeaflet(), use filter(LGD2014_nm == input$district).

    library(shiny)
    library(leaflet)
    library(dplyr)
    library(sf)
    
    ni_sdz <- read_sf("geography-sdz2021-esri-shapefile/SDZ2021.shp") |>
      st_transform(4326)
    
    ui <- fluidPage(
      title = "NI Super Data Zones",
      selectInput(
        inputId  = "district",
        label    = "Select Local Government District:",
        choices  = sort(unique(ni_sdz$LGD2014_nm)),
        selected = "Belfast"
      ),
      leafletOutput("map", height = 600)
    )
    
    server <- function(input, output, session) {
      output$map <- renderLeaflet({
        ni_sel <- ni_sdz |> filter(LGD2014_nm == input$district)
    
        leaflet(ni_sel) |>
          addTiles() |>
          addPolygons(
            color       = "steelblue",
            fillOpacity = 0.4,
            weight      = 1,
            popup = ~paste0(
              "<b>", SDZ2021_nm, "</b><br>",
              "District: ", LGD2014_nm, "<br>",
              "DEA: ",      DEA2014_nm
            )
          )
      })
    }
    
    shinyApp(ui = ui, server = server)
  2. Try changing the selectInput to use DEA2014_nm (District Electoral Areas) instead of LGD2014_nm. How does the number of choices and the granularity of the filtered map change?

    Answer: Replacing LGD2014_nm with DEA2014_nm gives around 80 choices instead of 11, each covering a smaller geographic area. The filtered maps show fewer polygons but at finer resolution, making it easier to inspect individual Super Data Zones within a single electoral area.


3 Example 2: Submarine Cables

3.1 Part A: Standalone leaflet map

Download submarine_cables.geojson from https://github.com/lifewinning/submarine-cable-taps or from Canvas and save it to your working directory.

  1. Load the cables GeoJSON file with read_sf(). Drop the Z/M dimensions with st_zm(). Inspect the key columns (name, color, length, rfs, owners).

    library(sf)
    
    cables <- read_sf("submarine_cables.geojson") |>
      st_zm(drop = TRUE, what = "ZM")
    
    names(cables)
    head(cables$owners)
  2. Build a minimal standalone leaflet map of the cables using addPolylines(). Use color = ~color to colour each cable with its own hex string (no palette function needed). Do not add any labels or pop-ups yet.

    Before writing the leaflet code, think about the equivalent static map with ggplot2: what function draws linestring features? What argument maps the color column to the line colour, and what scale ensures the hex strings are used directly?

    library(leaflet)
    
    leaflet(cables) |>
      addTiles() |>
      addPolylines(color = ~color, weight = 2, opacity = 0.8)

    Answer: The ggplot2 equivalent is ggplot() + geom_sf(data = cables, aes(colour = color)) + scale_colour_identity(). In leaflet, addPolylines(color = ~color) replaces both geom_sf() and scale_colour_identity() — the tilde formula passes the hex strings directly without a palette mapping. The result is immediately interactive: no fixed bounding box is needed as the user can zoom and pan across the globe.

  3. Extend the map with hover labels. Follow the pattern from the notes: use highlightOptions() to thicken the hovered cable, and wrap the label HTML strings with lapply(..., htmltools::HTML). Include the cable name, length, rfs, and owners in the label.

    leaflet(cables) |>
      addTiles() |>
      addPolylines(
        color   = ~color,
        weight  = 2,
        opacity = 0.8,
        highlightOptions = highlightOptions(
          weight       = 5,
          bringToFront = TRUE
        ),
        label = ~lapply(
          paste0(
            "<b>", name,   "</b><br>",
            "Length: ",  length, "<br>",
            "Ready: ",   rfs,    "<br>",
            "Owners: ",  owners
          ),
          htmltools::HTML
        )
      )

    Verify that hovering over a cable highlights it and shows the tooltip.

3.2 Part B: Shiny app for the cables data

The rfs column records the ready-for-service date as a string (e.g. "March 2001" or "1989"). A selectInput for rfs lets users explore which cables became operational in a particular period.

  1. Write a Shiny app that provides a selectInput for rfs (use sort(unique(cables$rfs)) for the choices), filters the cables to the selected value inside renderLeaflet(), and draws the filtered cables with colour, highlight, and hover labels from question 8.

    The structure is identical to Example 1: replace addPolygons() with addPolylines() and filter on rfs instead of LGD2014_nm.

    library(shiny)
    library(leaflet)
    library(dplyr)
    library(sf)
    
    cables <- read_sf("submarine_cables.geojson") |>
      st_zm(drop = TRUE, what = "ZM")
    
    ui <- fluidPage(
      title = "Submarine Cables by Ready-for-Service Date",
      selectInput(
        inputId  = "rfs_date",
        label    = "Select ready-for-service date:",
        choices  = sort(unique(cables$rfs)),
        selected = sort(unique(cables$rfs))[1]
      ),
      leafletOutput("map", height = 600)
    )
    
    server <- function(input, output, session) {
      output$map <- renderLeaflet({
        cables_sel <- cables |> filter(rfs == input$rfs_date)
    
        leaflet(cables_sel) |>
          addTiles() |>
          addPolylines(
            color   = ~color,
            weight  = 3,
            opacity = 0.9,
            highlightOptions = highlightOptions(
              weight       = 6,
              bringToFront = TRUE
            ),
            label = ~lapply(
              paste0(
                "<b>", name,   "</b><br>",
                "Length: ",  length, "<br>",
                "Ready: ",   rfs,    "<br>",
                "Owners: ",  owners
              ),
              htmltools::HTML
            )
          )
      })
    }
    
    shinyApp(ui = ui, server = server)
  2. Explore a few different rfs values. Do any dates have many cables? Do some have only one? What does this tell you about the history of submarine cable deployment?

    Answer: Most early dates (1989–1995) have only one or two cables, reflecting the small number of international connections at the time. Dates in the early 2000s often have several cables, corresponding to the telecom boom. Selecting a recent date may show no cables if no cable in the dataset has that exact rfs string — a reminder that real data requires cleaning before analysis.


4 Summary

Key concepts from this practical:

Functions introduced:

Function Purpose
leaflet() Initialise a leaflet map
addTiles() Add OpenStreetMap background tiles
addPolygons() Draw polygon/multipolygon features
addPolylines() Draw linestring features
highlightOptions() Configure hover highlight behaviour
leafletOutput() Leaflet output placeholder in Shiny UI
renderLeaflet() Render a leaflet map in Shiny server
st_transform(4326) Reproject to WGS84 for leaflet