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



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

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

100%
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:

TEXT 📖 Display only
data/
├── china.shp         # Geometric Information (Coordinates)
├── china.shx         # Shape Index
├── china.dbf         # Property Information (Name, Population, etc.)
└── china.prj         # Projection Information (Coordinate System)
⚠️ Note: The 4 files must be placed in the same directory. The shp file is the "main file," and the others are auxiliary files.

GeoJSON

Modern web maps commonly use GeoJSON (based on JSON):

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

R
install.packages("sf")
library(sf)

(3) Read the shapefile

R
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

R
# Read GeoJSON
cities <- st_read("data/cities.geojson")
# or from URL
cities <- st_read("https://example.com/cities.geojson")

(5) Write shapefile / GeoJSON

R
# 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”:

100%
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

R
# 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

R
# 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

R
library(ggplot2)
library(sf)

ggplot(china) +
  geom_sf() +
  labs(title = "Map of China")

(2) Coloring Maps (Coloring by Attribute)

R
# Color by Province
ggplot(china) +
  geom_sf(aes(fill = name)) +
  labs(title = "Provinces of China", fill = "Province") +
  theme_minimal()

(3) Geometric Simplification (Performance Optimization)

R
# 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

R
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

R
# 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)

R
# 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

R
# 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

R
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)

R
# 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)

R
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

R
# 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

R
# 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)

R 📖 Display only
# ============================================
# 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))
83 logic lines (exceeds 40-line limit, display only)

Expected Output:


❓ FAQ

Q Which should I choose, sf or sp?
A sf (Simple Features) is the modern standard for R spatial data, follows the tidyverse style, and integrates seamlessly with dplyr and ggplot2. Use sf for new code; the old sp code is no longer maintained.
Q How do I choose between 4326 and 3857?
A 4326 (WGS84 latitude and longitude) is for general use + GPS; 3857 (Web Mercator) is for map services + Google/OSM. ggplot2 + sf typically uses 4326, while Leaflet defaults to 3857.
Q What should I do if the map displays garbled Chinese characters?
A Add family = "SimHei" to theme(). Alternatively, borders(database = "world", regions = "China") already includes the Chinese names by default.
Q How do I create a 3D map?
A Use the 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.
Q Which should I choose for maps—leaflet or ggplot2?
A Use Leaflet for interactive web maps and ggplot2 for static report maps. If you need sharing via HTML pages, Leaflet is the first choice. For printed papers or PDF reports, ggplot2 is clearer and easier to format. Both can create choropleth maps; the difference is the interaction method.

📖 Summary


📝 Exercises

  1. Basic Exercise: Download the GeoJSON file for China’s provinces from Alibaba Cloud DataV, import it using st_read(), and use ggplot() + geom_sf() to draw a basic map of China (without filling in the colors).

  2. Basic Question: Color the map from the previous question by province (using aes(fill = name)), then add theme_void() and a themed design.

  3. 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.

  4. 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.

  5. 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.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏