Build an Interactive Map with leaflet
Leaflet is the most popular open-source JavaScript library for interactive maps. The R package leaflet brings this power to your R workflows, letting you create zoomable, pannable maps that work in RStudio, Shiny, and HTML documents.
In this project, you will build a complete map application that displays location data with custom markers, popups, and layer controls.
What you will build
By the end of this guide, you will have an interactive map that:
- Displays markers for multiple locations
- Shows custom popups with information
- Includes layer controls to toggle different data types
- Uses professional tile providers
- Handles geographic data from CSV files
This pattern applies to any location-based data visualization you need to create.
Project setup
First, create a new R script and install the necessary packages:
# Install required packages
install.packages(c("leaflet", "tidyverse", "readr"))
# Load libraries
library(leaflet)
library(tidyverse)
library(readr)
The leaflet package handles the mapping, while tidyverse and readr help you work with location data.
Preparing your data
For this project, you will create a simple dataset of locations. In real projects, this data might come from a CSV file, database, or API:
# Create a tibble with location data
locations <- tibble(
name = c("Times Square", "Central Park", "Statue of Liberty",
"Empire State Building", "Brooklyn Bridge"),
lat = c(40.7580, 40.7829, 40.6892, 40.7484, 40.7061),
lng = c(-73.9855, -73.9654, -74.0445, -73.9857, -73.9969),
type = c("attraction", "park", "landmark", "building", "landmark"),
description = c("The bright lights of NYC",
"Urban oasis in Manhattan",
"Symbol of freedom",
"Iconic skyscraper",
"Historic bridge")
)
locations
The tibble contains latitude, longitude, and descriptive information for each location.
Creating your first map
Start with the leaflet() function to initialize a map, then add tiles:
# Initialize the map
map <- leaflet() %>%
addTiles() # Add OpenStreetMap tiles by default
map
This gives you a basic interactive map. Now add your location data:
# Add markers for each location
map <- leaflet(data = locations) %>%
addTiles() %>%
addMarkers(~lng, ~lat, popup = ~name)
map
The ~ notation refers to columns in your data frame. Each marker now has a popup showing the location name.
Customizing markers
Default markers work, but you can customize them for a professional look:
# Create a custom map with custom markers
map <- leaflet(data = locations) %>%
addTiles() %>%
addCircleMarkers(
~lng, ~lat,
radius = 8,
color = "red",
fillColor = "orange",
fillOpacity = 0.7,
popup = paste0(
"<b>", locations$name, "</b><br>",
locations$description
)
)
map
Circle markers work better for dense data and give a cleaner look.
Adding layer controls
For more complex maps, add layer controls to let users toggle different types of locations:
# Create separate groups for each location type
map <- leaflet(data = locations) %>%
addTiles() %>%
# Add markers by type
addCircleMarkers(
data = filter(locations, type == "attraction"),
~lng, ~lat,
group = "Attractions",
radius = 8, color = "blue",
popup = ~name
) %>%
addCircleMarkers(
data = filter(locations, type == "park"),
~lng, ~lat,
group = "Parks",
radius = 8, color = "green",
popup = ~name
) %>%
addCircleMarkers(
data = filter(locations, type == "landmark"),
~lng, ~lat,
group = "Landmarks",
radius = 8, color = "red",
popup = ~name
) %>%
addCircleMarkers(
data = filter(locations, type == "building"),
~lng, ~lat,
group = "Buildings",
radius = 8, color = "purple",
popup = ~name
) %>%
# Add layer control
addLayersControl(
overlayGroups = c("Attractions", "Parks", "Landmarks", "Buildings"),
options = layersControlOptions(collapsed = FALSE)
)
map
Now users can toggle each category on and off.
Using custom tile providers
OpenStreetMap is great, but there are many other tile providers for different styles:
# Use CartoDB positron tiles (clean, light theme)
map <- leaflet(data = locations) %>%
addProviderTiles("CartoDB.Positron") %>%
addCircleMarkers(
~lng, ~lat,
radius = 8,
color = "steelblue",
fillColor = "lightblue",
fillOpacity = 0.8,
popup = paste0(
"<b>", locations$name, "</b><br>",
"<i>", locations$type, "</i><br>",
locations$description
)
) %>%
addLegend(
position = "bottomright",
colors = c("blue", "green", "red", "purple"),
labels = c("Attraction", "Park", "Landmark", "Building"),
title = "Location Types"
)
map
CartoDB tiles work well for data visualization because they do not compete with your data.
Complete map function
Wrap everything in a reusable function for production use:
create_location_map <- function(data, tile_provider = "CartoDB.Positron") {
# Define colors by type
colors <- c(
"attraction" = "blue",
"park" = "green",
"landmark" = "red",
"building" = "purple"
)
# Build the map
map <- leaflet(data = data) %>%
addProviderTiles(tile_provider) %>%
addCircleMarkers(
~lng, ~lat,
radius = 8,
color = ~colors[type],
fillColor = ~colors[type],
fillOpacity = 0.7,
popup = paste0(
"<b>", data$name, "</b><br>",
"<i>", data$type, "</i><br>",
data$description
),
group = ~type
) %>%
addLayersControl(
overlayGroups = unique(data$type),
options = layersControlOptions(collapsed = FALSE)
) %>%
addLegend(
position = "bottomright",
colors = unique(colors[unique(data$type)]),
labels = unique(data$type),
title = "Location Types"
)
return(map)
}
# Use the function
create_location_map(locations)
This function is flexible enough to work with any location data you load.
Saving your map
To save the map as an HTML file for sharing:
# Save to HTML
htmlwidgets::saveWidget(
create_location_map(locations),
file = "nyc-locations-map.html",
selfcontained = TRUE
)
This creates a standalone HTML file you can share with anyone.
Adding controls
Leaflet maps in R support interactive controls. addLayersControl() adds a layer switcher for toggling base maps and overlay layers. addLegend() creates a color legend linked to a colorNumeric() or colorFactor() palette. addScaleBar() adds a distance scale. addMiniMap() embeds a small overview map in the corner. addSearchOSM() from leaflet.extras adds a geocoding search box powered by OpenStreetMap.
GeoJSON and sf integration
leaflet() accepts sf objects directly via addPolygons(), addPolylines(), and addCircleMarkers(), no coordinate extraction needed. For GeoJSON files, geojsonio::geojson_read() reads them into R, and geojson_sp() converts to SpatialPolygonsDataFrame for leaflet. The leafletProxy() function in Shiny updates an existing map without re-rendering it from scratch, essential for performance when updating data based on user input.
Performance with large datasets
Leaflet renders all markers in the browser. For thousands of points, addMarkers(clusterOptions = markerClusterOptions()) groups nearby markers into clusters that expand on click. For heat maps of point density, addHeatmap() from leaflet.extras is more performant than individual markers. For polygon datasets with many vertices, simplify with sf::st_simplify(preserveTopology = TRUE, dTolerance = 100) before passing to leaflet, reducing vertex count dramatically improves pan and zoom performance.
Custom tile providers
Beyond the default OpenStreetMap tiles, addTiles(urlTemplate = "...") accepts any tile server URL. addProviderTiles("CartoDB.Positron") from leaflet.providers adds a clean light basemap. addProviderTiles("Esri.WorldImagery") shows satellite imagery. Layer multiple tile sets with multiple addTiles() or addProviderTiles() calls, controlled by addLayersControl() to let users switch between them.
Getting started with leaflet
The leaflet R package creates interactive maps as htmlwidgets. They render in Shiny apps, R Markdown documents, Quarto reports, and the RStudio viewer. The map object is built by chaining function calls starting with leaflet().
leaflet() initializes the map. addTiles() adds a base tile layer (OpenStreetMap by default). addMarkers(lng = -0.1276, lat = 51.5074, popup = "London") adds a single marker. Return the object from a code chunk and it renders automatically in HTML output.
For data frames, pass the data to leaflet(data) and use formula syntax to reference columns: leaflet(cities) %>% addCircleMarkers(lng = ~longitude, lat = ~latitude, label = ~name, radius = ~population/100000). The ~ syntax evaluates column references in the data frame context.
Tile providers
addProviderTiles(providers$CartoDB.Positron) uses CartoDB’s minimal light basemap, often better for data overlays than the visually busy OpenStreetMap tiles. providers$CartoDB.DarkMatter for dark backgrounds. providers$Esri.WorldImagery for satellite imagery.
addWMSTiles("url", layers = "layer_name") adds Web Map Service tiles for specialized data layers (geology, land cover, weather) from external servers. Many government agencies publish WMS services.
leaflet.extras::addFullscreenControl() adds a fullscreen button. addMeasure() adds a measurement tool. These extensions from leaflet.extras expand the widget’s capabilities.
Choropleth maps
Choropleth maps color polygons by a data attribute. Join attribute data to the polygon sf object, then map the attribute to fill color.
pal <- colorNumeric("Blues", domain = sf_data$value)
leaflet(sf_data) %>%
addTiles() %>%
addPolygons(
fillColor = ~pal(value),
fillOpacity = 0.7,
weight = 1,
color = "white",
popup = ~paste(name, ":", round(value, 1))
) %>%
addLegend(pal = pal, values = ~value, title = "Value")
colorNumeric() maps a continuous range to a color ramp. colorBin() divides into equal-width bins. colorQuantile() divides by quantiles. colorFactor() maps categorical values. Each returns a palette function that takes values and returns hex colors.
Heatmaps and density layers
leaflet.extras::addHeatmap(lng = ~lon, lat = ~lat, intensity = ~count) creates a density heatmap that responds to zoom. Points aggregate at low zoom and disperse at high zoom. The blur, radius, and minOpacity parameters control appearance.
For static heatmaps that do not change with zoom, compute a kernel density estimate in R and add it as a raster layer. MASS::kde2d(x, y) returns a grid; convert to a raster and add with addRasterImage().
Managing multiple layers
addLayersControl(baseGroups = c("Light", "Satellite"), overlayGroups = c("Points", "Polygons")) adds a layer control widget. Assign layers to groups with the group argument in each add*() call. The layer control lets users toggle layers on and off interactively.
clearMarkers(), clearShapes(), clearImages() remove specific layer types. In Shiny, leafletProxy("map_id") %>% clearMarkers() %>% addMarkers(...) updates markers without re-rendering the entire map, preserving zoom and pan state.
fitBounds(lng1, lat1, lng2, lat2) zooms to a bounding box. flyTo(lng, lat, zoom) animates the map to a new center. In Shiny, use these via leafletProxy() to respond to user actions without rebuilding the map.
Leaflet in R
The leaflet package creates interactive web maps in R. Maps render in the browser using the Leaflet JavaScript library, which provides tile-based mapping with zoom and pan controls. The R package is an htmlwidget, so leaflet maps work in R Markdown documents, Quarto documents, Shiny applications, and the RStudio viewer pane. A rendered HTML file containing a leaflet map is fully self-contained and works in any browser.
Leaflet maps start with a tile layer that provides the base map background. The setView function positions the initial center and zoom level. From there, you add layers: markers for points, polylines for routes, polygons for areas, and heatmaps for density. Each layer function accepts data and aesthetic arguments, with the same column-reference pattern as ggplot2.
Markers and popups
The addMarkers function adds pin markers at specified coordinates. The popup argument accepts HTML strings that appear when a marker is clicked. A popup can contain a formatted data summary, formatted values, a small table, or even a chart embedded as a base64 image, that gives the user full context about the location without leaving the map. Popups are the primary interactivity mechanism for point data.
addCircleMarkers draws circular markers with controllable size and color. Unlike pin markers, which are always the same size, circle markers can be sized proportionally to a data attribute, a circle whose radius represents a numeric value communicates that value visually. This is the leaflet equivalent of a bubble chart.
Layer groups and controls
addLayersControl adds a layer selector widget that lets users toggle layers on and off. Grouping related markers with a group argument and listing all groups in the overlay groups argument of the layers control creates a filterable map. A map showing multiple business categories — restaurants, cafes, hotels — with a layer control lets users focus on the type they are interested in.
The fitBounds function adjusts the map view to include all data, which is more useful than a fixed center and zoom for data that might span different geographic areas in different uses. Using it at the end of a map construction pipeline ensures that all markers are visible regardless of their geographic spread.
Summary
You have built a complete interactive map application with:
- Location data from a data frame
- Custom circle markers with popups
- Layer controls for filtering by type
- Professional CartoDB tiles
- A reusable function for production use
- HTML export capability
The leaflet package makes it straightforward to create professional maps. The skills transfer directly to Shiny applications, R Markdown documents, and standalone HTML files.
See also
r-interactive-leaflet— Introduction to leaflet basicsr-spatial-sf— Working with spatial data in Rr-ggplot2-extensions— Advanced ggplot2 and map visualizations