R: R Maps and Spatial Data
Last updated: 2026-08-26
Geospatial data is becoming increasingly important in real-world projects—such as pandemic maps, Didi ride-hailing heatmaps, and JD.com delivery coverage areas. In this lesson, we’ll learn about map visualization in R: using sf to process vector data, ggplot2 to create maps, and Leaflet to build interactive maps.
After completing this lesson, you’ll be able to create four types of maps using R: static maps (ggplot2 + sf), choropleth maps, point density maps, and interactive maps (leaflet).
1. What You'll Learn
- Fundamentals of Spatial Data (Vector/Raster, Shapefile, Coordinate Systems)
- The sf package reads and processes shapefiles
- Creating maps with ggplot2 and geom_sf
- CRS Coordinate Systems (WGS84 / Web Mercator)
- Choropleth map
- Leaflet interactive map
- In Practice: Heat Maps of Sales in Chinese Cities
2. The Story Behind a Logistics Heat Map
(1) Pain Point: Sales data lacks a sense of scale
Alice is in charge of national sales. The manager asked, "Which province has the best sales?" After looking at the spreadsheet for a while, the manager said, "Show me a map."
(2) Solution using R
library(sf)
library(ggplot2)
# 1. View a Map of China
china <- st_read("china.shp")
# 2. Consolidate Sales Data
china_sales <- china |>
left_join(sales_by_province, by = c("name" = "province"))
# 3. Line Chart of Categorical Statistics
ggplot(china_sales) +
geom_sf(aes(fill = sales)) +
scale_fill_gradient(low = "lightblue", high = "darkred") +
labs(title = "National Sales Distribution", fill = "Sales")
3 lines of code → a publication-quality map of sales in China.
3. Fundamentals of Spatial Data
(1) Vector vs. Raster
graph LR
A[Spatial Data] --> B[Vector Data<br/>Vector]
A --> C[Raster Data<br/>Raster]
B --> D[pt Point<br/>Line<br/>Polygon]
C --> E[Pixel Pixel<br/>Satellite image/Temperature Map]
style A fill:#fff3cd
style B fill:#cce5ff
style C fill:#d4edda
This lesson covers vector data (points, lines, and surfaces).
(2) What is a shapefile?
Shapefile = ESRI's vector data standard, consisting of four files:
data/
├── china.shp # Geometric Information (Coordinates)
├── china.shx # Shape Index
├── china.dbf # Property Information (Name, Population, etc.)
└── china.prj # Projection Information (Coordinate System)
GeoJSON
Modern web maps commonly use GeoJSON (based on JSON):
{
"type": "Feature",
"properties": {"name": "Beijing", "value": 1000},
"geometry": {
"type": "Point",
"coordinates": [116.4, 39.9]
}
}
4. The sf package: Spatial Data Framework
(1) Why use SF?
| Bag | Features | Recommendation |
|---|---|---|
| sf | Modern, R tidyverse style, ggplot2 integration | ⭐⭐⭐ |
| sp | Old-style, R basic syntax | ❌ Not recommended for new code |
| raster | For raster data only | For raster |
(2) Installation and Loading
install.packages("sf")
library(sf)
(3) Read the shapefile
library(sf)
# Read shapefile
china <- st_read("data/china.shp")
# Reading layer `china' from data source `data/china.shp' using driver `ESRI Shapefile'
# Simple feature collection with 34 features and 5 fields
# Geometry type: MULTIPOLYGON
# Dimension: XY
# Bounding box: xmin: 73.5 ymin: 3.5 xmax: 135 ymax: 53.5
# CRS: 4326 ← WGS84 Latitude and Longitude
# View Structure
print(china)
# It basically means data.frame More geometry col
# A tibble: 34 × 6
# name population ... geometry
# <chr> <int> <MULTIPOLYGON>
# Geometric Information
st_geometry(china)
st_crs(china) # Coordinate System
st_bbox(china) # Bounding Box
(4) Reading GeoJSON
# Read GeoJSON
cities <- st_read("data/cities.geojson")
# or from URL
cities <- st_read("https://example.com/cities.geojson")
(5) Write shapefile / GeoJSON
# Write shapefile
st_write(china, "output/china_new.shp")
# Write GeoJSON
st_write(china, "output/china.geojson")
5. CRS Coordinate System
(1) What is CRS?
CRS (Coordinate Reference System)—Mapping the Earth’s “spherical” surface onto a “plane”:
graph LR
A[Earth's Surface] -->|WGS84 4326<br/>Latitude and Longitude| B[Cartesian coordinates]
A -->|Web Mercator 3857<br/>m| B
A -->|China 2000<br/>m| B
(2) Common CRS Codes
| EPSG | Name | Purpose |
|---|---|---|
| 4326 | WGS84 | GPS / General (Most Common) |
| 3857 | Web Mercator | Google Maps, OpenStreetMap |
| 4490 | CGCS2000 | China National Coordinate System |
| 32650 | UTM 50N | High-precision local |
(3) Coordinate System Transformation
# View Current CRS
st_crs(china)
# Coordinate Reference System:
# EPSG: 4326
# proj4string: "+proj=longlat +datum=WGS84 ..."
# Convert to Web Mercator (3857)
china_3857 <- st_transform(china, 3857)
# Convert to China 2000
china_4490 <- st_transform(china, 4490)
(4) Latitude and Longitude Extraction
# Extracting Latitude and Longitude from Geometric Objects
coords <- st_coordinates(china)
# X = Longitude, Y = Latitude
6. Creating Maps with ggplot2 and geom_sf
(1) Base Map
library(ggplot2)
library(sf)
ggplot(china) +
geom_sf() +
labs(title = "Map of China")
(2) Coloring Maps (Coloring by Attribute)
# Color by Province
ggplot(china) +
geom_sf(aes(fill = name)) +
labs(title = "Provinces of China", fill = "Province") +
theme_minimal()
(3) Geometric Simplification (Performance Optimization)
# World Map (>100,000 coordinate points) use st_simplify to simplify
china_simple <- st_simplify(china, dTolerance = 1000)
# Drawing (10x faster)
ggplot(china_simple) + geom_sf()
(4) Theme Customization
ggplot(china) +
geom_sf(fill = "lightblue", color = "white", linewidth = 0.3) +
labs(title = "Map of China") +
theme_void() + # Hide Axes
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
legend.position = "none"
)
7. Choropleth Map
(1) Merge Data + Create a Categorical Chart
# Simulated Sales Data
sales_by_province <- data.frame(
name = c("Beijing", "Shanghai", "Guangdong", "Jiangsu", "Zhejiang", "Sichuan", "Hubei", "Shaanxi"),
sales = c(5000, 4500, 4000, 3500, 3000, 2500, 2000, 1500)
)
# Merge into Map
china_sales <- china |>
left_join(sales_by_province, by = "name")
# Draw a categorical bar chart
ggplot(china_sales) +
geom_sf(aes(fill = sales), color = "white", linewidth = 0.3) +
scale_fill_gradient(
low = "lightyellow",
high = "darkred",
na.value = "gray90"
) +
labs(title = "National Sales Distribution", fill = "Sales (10,000 yuan)") +
theme_void() +
theme(
plot.title = element_text(face = "bold", hjust = 0.5, size = 16),
legend.position = "right"
)
(2) Categorization (Continuous → Discrete)
# Use cut to divide continuous values into 5 levels
china_sales <- china_sales |>
mutate(sales_level = cut(sales,
breaks = c(0, 1000, 2000, 3000, 5000),
labels = c("Low", "Mid", "High", "Very high"),
include.lowest = TRUE))
ggplot(china_sales) +
geom_sf(aes(fill = sales_level), color = "white", linewidth = 0.3) +
scale_fill_brewer(palette = "YlOrRd", na.value = "gray90") +
labs(title = "National Sales Grades", fill = "Level") +
theme_void()
8. Point Density Plot
# City Points (Latitude and Longitude + Sales)
cities <- data.frame(
name = c("Beijing", "Shanghai", "Guangzhou", "Shenzhen"),
lon = c(116.4, 121.5, 113.3, 114.1),
lat = c(39.9, 31.2, 23.1, 22.5),
sales = c(5000, 4500, 4000, 3000)
) |>
st_as_sf(coords = c("lon", "lat"), crs = 4326)
# Draw a point on the map
ggplot() +
geom_sf(data = china, fill = "lightgray", color = "white") +
geom_sf(data = cities, aes(size = sales), color = "red", alpha = 0.7) +
scale_size_continuous(range = c(2, 12), name = "Sales") +
labs(title = "Map of City Sales Locations") +
theme_void()
9. Leaflet Interactive Map
(1) Installation and Basics
install.packages("leaflet")
library(leaflet)
# Basic Interactive Map
m <- leaflet() |>
addTiles() |> # OpenStreetMap Base Map
setView(lng = 116.4, lat = 39.9, zoom = 4) # Beijing as the center
m # Show in RStudio Viewer
(2) Add a mark (label)
# Add a city
leaflet(cities) |>
addTiles() |>
addMarkers(
lng = ~lon, lat = ~lat,
popup = ~paste0("<b>", name, "</b><br>Sales: ", sales, " 10,000 yuan"),
label = ~name
)
(3) Add circles (by size)
leaflet(cities) |>
addTiles() |>
addCircles(
lng = ~lon, lat = ~lat,
radius = ~sqrt(sales) * 1000, # By Sales Radius
color = "red",
fillOpacity = 0.5,
popup = ~paste0(name, ": ", sales, "10,000 yuan")
)
(4) Graded Coloring
# Use sf Data
leaflet(china_sales) |>
addTiles() |>
addPolygons(
fillColor = ~colorNumeric("YlOrRd", sales)(sales),
fillOpacity = 0.7,
color = "white",
weight = 1,
popup = ~paste0("<b>", name, "</b><br>Sales: ", sales, " 10,000 yuan")
) |>
addLegend(pal = colorNumeric("YlOrRd", china_sales$sales),
values = ~sales, title = "Sales")
(5) Save as HTML
# Save as a standalone HTML file (Shareable)
library(htmlwidgets)
saveWidget(m, "interactive_map.html", selfcontained = FALSE)
10. Complete Example: National Sales Heat Map
Below is an example of a complete workflow that ties together all the map-related concepts covered in this lesson.
▶ Example: 4 City Sales Maps (Static + Interactive)
# ============================================
# 4 City Sales Map
# Features: ggplot2 Static Map + leaflet Interactive Map
# ============================================
library(sf)
library(ggplot2)
library(dplyr)
library(leaflet)
library(htmlwidgets)
# 1. Prepare data
# 4 City Points (Latitude and Longitude)
cities <- data.frame(
name = c("Beijing", "Shanghai", "Guangzhou", "Shenzhen"),
lon = c(116.4074, 121.4737, 113.2644, 114.0579),
lat = c(39.9042, 31.2304, 23.1291, 22.5431),
sales = c(5000, 4500, 4000, 3000),
region = c("Northern China", "East China", "South China", "South China")
) |>
st_as_sf(coords = c("lon", "lat"), crs = 4326)
# Simplified Polygons of China's Provinces (Sample data here)
china_provinces <- data.frame(
name = c("Beijing", "Shanghai", "Guangdong Province"),
lat = c(39.9, 31.2, 23.1),
lon = c(116.4, 121.5, 113.3)
)
# 2. Static Map: ggplot2 + sf
p_static <- ggplot() +
# Simplify: use geom_point Replace geom_sf (no shapefile)
borders(database = "world", regions = "China",
fill = "lightgray", color = "white") +
geom_sf(data = cities, aes(size = sales, color = region),
alpha = 0.7) +
geom_text(data = as.data.frame(st_coordinates(cities)),
aes(X, Y, label = cities$name),
vjust = -1.5, size = 4, fontface = "bold") +
scale_size_continuous(range = c(3, 12), name = "Sales (10,000 yuan)") +
scale_color_brewer(palette = "Set1", name = "Region") +
labs(title = "China 4 Sales Distribution in Major Cities",
subtitle = "Data: 2024 Q4",
caption = "Base Map: Natural Earth | Chart: R + ggplot2") +
coord_sf(xlim = c(100, 125), ylim = c(20, 45)) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", hjust = 0.5, size = 16),
plot.subtitle = element_text(hjust = 0.5, color = "gray40"),
legend.position = "bottom"
)
print(p_static)
# 3. Save Static Image
ggsave("static_sales_map.png", p_static,
width = 10, height = 8, dpi = 300)
cat("=== The static map has been saved: static_sales_map.png ===\n")
# 4. Interactive Map: leaflet
cities_df <- as.data.frame(st_coordinates(cities)) |>
rename(lon = X, lat = Y) |>
cbind(name = cities$name, sales = cities$sales, region = cities$region)
m_interactive <- leaflet(cities_df) |>
addTiles() |>
setView(lng = 114, lat = 33, zoom = 4) |>
addCircleMarkers(
lng = ~lon, lat = ~lat,
radius = ~sqrt(sales) / 5,
color = ~case_when(
region == "Northern China" ~ "red",
region == "East China" ~ "blue",
region == "South China" ~ "green"
),
fillOpacity = 0.6,
stroke = TRUE,
weight = 2,
popup = ~paste0(
"<div style='font-family: Arial; font-size: 14px;'>",
"<b>", name, "</b><br>",
"Region: ", region, "<br>",
"Sales: ", sales, " 10,000 yuan<br>",
"Longitude: ", round(lon, 2), "<br>",
"Latitude: ", round(lat, 2),
"</div>"
),
label = ~paste0(name, ": ", sales, "10,000 yuan")
) |>
addLegend(
"bottomright",
colors = c("red", "blue", "green"),
labels = c("Northern China", "East China", "South China"),
title = "Region"
) |>
addScaleBar(position = "bottomleft")
# 5. Show (in RStudio Viewer)
m_interactive
# 6. Save as HTML
saveWidget(m_interactive, "interactive_sales_map.html",
selfcontained = TRUE)
cat("=== The interactive map has been saved: interactive_sales_map.html ===\n")
# 7. Output Statistics
cat("\n=== Sales Statistics ===\n")
print(cities_df |> select(name, region, sales))
Expected Output:
- Static map (PNG, 300 dpi, showing sales by 4 dot sizes)
- Interactive Map (HTML, tooltips appear on mouse hover)
❓ FAQ
- Map of China: Alibaba Cloud DataV.GeoAtlas (https://datav.aliyun.com/portal/school/atlas/area_selector)
- World Map: Natural Earth (https://www.naturalearthdata.com/)
- Detailed China map: https://github.com/longwosion/geojson-map-china
family = "SimHei" to theme(). Alternatively, borders(database = "world", regions = "China") already includes the Chinese names by default.rayshader package (which converts ggplot2 plots to 3D) or plotly’s 3D scatter plot combined with a map background. We won’t go into detail on this in this lesson.📖 Summary
- Spatial data = vectors (points/lines/polygons) + rasters (pixels); this lesson covers vectors
- A Shapefile consists of four files (.shp, .shx, .dbf, and .prj); GeoJSON is a modern web format
- sf is the modern standard for R spatial data, in the tidyverse style
- Commonly used CRS: 4326 (WGS84 latitude and longitude) / 3857 (Web Mercator)
ggplot2 +
geom_sf()to draw static maps; choropleth usesaes(fill = value)+scale_fill_gradient - leaflet for interactive maps: addTiles (base map) + addMarkers/addCircles (points) + addPolygons (regions)
borders(database = "world")Quickly Draw a World Map (No Shapefile Required)- Save:
ggsave()PNG/PDF for static content;saveWidget()HTML for interactive content - R Map Applications: Business Geographic Distribution, Pandemic, Logistics, Customer Distribution, Heat Maps
📝 Exercises
-
Basic Exercise: Download the GeoJSON file for China’s provinces from Alibaba Cloud DataV, import it using
st_read(), and useggplot() + geom_sf()to draw a basic map of China (without filling in the colors). -
Basic Question: Color the map from the previous question by province (using
aes(fill = name)), then addtheme_void()and a themed design. -
Basic Exercise: Use
borders(database = "world", regions = "China")+geom_point(data = cities, aes(lon, lat, size = sales))to plot a map of Chinese cities, with custom latitude and longitude coordinates for 4 cities. -
Advanced Exercise: Simulate sales data for 5 cities (including latitude/longitude, sales figures, and categories), and use Leaflet to create an interactive map: ① Map sales figures to circle size; ② Map categories to color; ③ Add tooltips to display details; ④ Add a legend and a scale. Save as HTML.
-
Challenge: Complete Workflow—Download China’s provincial GeoJSON data and simulate sales data for the 31 provinces: ① Merge into the map using a LEFT JOIN ② Use
cut()to divide into 5 categories ③ Create a categorized statistical chart using ggplot2 ④ Format the chart (title/subtitle/Chinese font) ⑤ Create an interactive version using Leaflet (including a legend) ⑥ Save as PNG and HTML. Take a screenshot and save it.