绘制中国地图进阶
楚新元 / 2026-06-23
在上一篇文章《ggplot2 + sf + 高德地图数据绘制中国地图》,笔者介绍了如何利用 ggplot2 + sf + 高德地图数据绘制中国地图。本文继续讨论绘制中国地图问题,重点解决以下两个问题:其一,数据来源由第三方地图服务切换为权威官方网站,以确保数据的规范性和权威性;其二,坐标系采用 2000 国家大地坐标系(CGCS2000),投影方式选用 Albers 等面积圆锥投影(基准面:CGCS2000),其中中央经线设定为 105°E,标准纬线分别取 25°N 和 47°N,从而更好地满足专业学术论文投稿要求和正式场合展示。
数据来源
如果只是一般的学习交流使用,我建议你使用高德地图平台的数据,地图数据完整且包含各省区行政中心和质心经纬度坐标,非常容易获取,如果是学术论文投稿和正式场合,那必须从权威官方网站获取。以下两个网站是目前最权威的数据来源,推荐使用。
自然资源部标准地图服务系统:http://bzdt.ch.mnr.gov.cn,如果你需要一个即拿即用、符合规范的地图图片,这是最直接的选择。但需注意,其提供的并非可直接用于 GIS 分析的 shp、geojson 等格式数据。
国家地理信息公共服务平台:https://www.tianditu.gov.cn/,“天地图”提供的是可用于 GIS 分析的基础地理信息数据(如行政区划边界等),数据详尽,坐标系统采用 CGCS2000 坐标系,同时,该网站提供 API 接口,便于开发者集成。
本文采用国家地理信息公共服务平台提供的数据。注册后依次点击数据资源 $\rightarrow$ 行政区域可视化,进入下载页面后,选择地区:中国,下载区域:省,下载格式:GeoJSON,然后点击下载数据按钮即可。
生成地图
#'
#' 科学绘制中国地图(CGCS2000 Albers)
#'
#' @author 楚新元
#' @date 2026-06-23
#'
# ------------------------------
# 加载 R 包
# ------------------------------
library(sf)
library(ggplot2)
use("dplyr", c("mutate", "select", "coalesce"))
# ------------------------------
# 获取数据地图
# ------------------------------
# 数据来源:https://cloudcenter.tianditu.gov.cn/dataSource
# 数据更新时间:2025 年 9 月
# 数据资源:行政区域可视化
# 数据粒度:省
# 数据格式: GenJSON
# Geodetic CRS: China Geodetic Coordinate System 2000
# ------------------------------
# 地图数据和艾滋病数据并合并
# ------------------------------
# 天地图的中国行政区划 GeoJSON 是平面投影适配的矢量边界
# 禁用 s2 球面几何,使用传统 GEOS 平面笛卡尔几何
sf_use_s2(FALSE)
# 读取中国省级地图数据
cn_raw = read_sf("data/中国_省.geojson")
# 艾滋病发病率数据(1/10万)
prov_aids = tibble::tribble(
~name, ~value,
"北京市", 2.59,
"天津市", 2.28,
"河北省", 1.36,
"山西省", 1.92,
"内蒙古自治区", 1.66,
"辽宁省", 2.30,
"吉林省", 2.78,
"黑龙江省", 1.70,
"上海市", 1.60,
"江苏省", 1.83,
"浙江省", 2.64,
"安徽省", 1.83,
"福建省", 2.58,
"江西省", 3.85,
"山东省", 1.01,
"河南省", 3.05,
"湖北省", 2.57,
"湖南省", 4.54,
"广东省", 3.61,
"广西壮族自治区", 14.19,
"海南省", 2.32,
"重庆市", 11.99,
"四川省", 13.95,
"贵州省", 10.48,
"云南省", 9.12,
"西藏自治区", 0.82,
"陕西省", 2.23,
"甘肃省", 2.00,
"青海省", 3.19,
"宁夏回族自治区", 1.47,
"新疆维吾尔自治区", 5.43,
"台湾省", 6.09,
"香港特别行政区", NA,
"澳门特别行政区", NA
)
# 合并数据(地图数据 + 艾滋病数据)
cn_raw |>
merge(prov_aids, by = "name", all.x = TRUE) |>
mutate(
interval = cut(
value,
breaks = c(
0.82, 1.02, 1.36, 1.93, 2.00, 2.79, 3.05, 4.55,
5.43, 9.13, 10.48, 12.00, 13.95, 14.20
),
right = FALSE,
labels = c(
"0.82 - 1.01", NA, "1.36 - 1.92", NA,
"2.00 - 2.78", NA, "3.05 - 4.54", NA,
"5.43 - 9.12", NA, "10.48 - 11.99", NA,
"13.95 - 14.19"
)
)
) -> cn_map_cgcs2000
# CGCS2000 Albers 等面积投影
albers_crs = "
+proj=aea \
+lat_1=25 +lat_2=47 \
+lat_0=0 +lon_0=105 \
+x_0=0 +y_0=0 \
+ellps=GRS80 \
+towgs84=0,0,0,0,0,0,0 \
+units=m +no_defs
"
# 执行坐标转换:CGCS2000 → CGCS2000 Albers
cn_map = st_transform(cn_map_cgcs2000, crs = albers_crs)
# ------------------------------
# 设置填充颜色和标签位置、内容、颜色
# ------------------------------
# 定义每个区间的颜色
interval_colors = c(
"0.82 - 1.01" = "#fbcdcd", # 浅粉
"1.36 - 1.92" = "#f38b88", # 深粉
"2.00 - 2.78" = "#f8413b", # 红色
"3.05 - 4.54" = "#c00203", # 深红
"5.43 - 9.12" = "#790102", # 暗红
"10.48 - 11.99" = "#340100", # 黑红
"13.95 - 14.19" = "#0d0101" # 黑色
)
# 自定义各省区标签位置(基于高德地图几何质心微调)
geo_coords = tibble::tribble(
~short_name, ~geo_lon, ~geo_lat,
"上海", 122.93874, 31.77256,
"云南", 101.48511, 24.50864,
"内蒙古", 110.07743, 40.83109,
"北京", 116.41995, 41.38994,
"台湾", 123.47149, 23.24945,
"吉林", 126.17121, 43.10395,
"四川", 102.69345, 30.67454,
"天津", 118.54704, 39.28804,
"宁夏", 106.16987, 37.29133,
"安徽", 117.22688, 31.84925,
"山东", 118.18776, 36.37609,
"山西", 112.30444, 37.61818,
"广东", 113.42992, 23.33464,
"广西", 108.79440, 23.83338,
"新疆", 85.29471, 41.37180,
"江苏", 119.48651, 32.98399,
"江西", 115.73297, 27.63611,
"河北", 115.50246, 38.04547,
"河南", 113.61972, 33.90265,
"浙江", 120.10991, 29.18147,
"海南", 107.25486, 18.88977,
"湖北", 112.27130, 30.98753,
"湖南", 111.71165, 27.62922,
"澳门", 112.76699, 20.35931,
"甘肃", 104.12356, 34.95804,
"福建", 118.00647, 26.06993,
"西藏", 88.38828, 31.56375,
"贵州", 106.88045, 26.82637,
"辽宁", 123.30499, 41.29971,
"重庆", 107.88390, 30.06730,
"陕西", 108.88711, 34.26366,
"青海", 96.04353, 35.72640,
"香港", 114.93436, 20.97737,
"黑龙江", 128.69303, 46.54046
)
# 确定标签位置、内容和颜色
cn_map_cgcs2000 |>
st_drop_geometry() |>
mutate(
# 根据全称生成简称
short_name = gsub("(省|自治区|市|特别行政区)$", "", name),
short_name = gsub("(壮族|回族|维吾尔)", "", short_name),
# 生成标签内容并确定颜色
label = paste(short_name, format(value, nsmall = 2), sep = "\n"),
type = ifelse(
# 标签在外部的一律用黑色;在内部的,数值小颜色浅用黑色,数值大颜色深用白色
value > 3 & !short_name %in% c("海南", "澳门", "香港", "台湾", "上海", "天津"),
"white_font", "black_font"
)
) |>
# 匹配各省区标签位置
merge(geo_coords, by = "short_name", all.y = TRUE) |>
# 各省区标签位置转为 CGCS2000 Albers
st_as_sf(
coords = c("geo_lon", "geo_lat"), crs = 4326
) |>
st_transform(albers_crs) |>
mutate(
geo_lon = st_coordinates(geometry)[, 1],
geo_lat = st_coordinates(geometry)[, 2]
) |>
st_drop_geometry() |>
select(
label, type, geo_lon, geo_lat
) -> prov_labels
# ------------------------------
# 南海插图参数设置和矢量坐标转换
# ------------------------------
# 能囊括完整南海四至的范围(CGCS2000 Albers)
nh_lon_min = 0
nh_lon_max = 1840000
nh_lat_min = 290000
nh_lat_max = 2780000
# 确定插图画布尺寸
nh_w = nh_lon_max - nh_lon_min # 经度宽度
nh_h = nh_lat_max - nh_lat_min # 纬度高度
nh_aspect_ratio = nh_h / nh_w
inset_w = 600000
inset_h = inset_w * nh_aspect_ratio
# 插图放置起点(平面直角坐标)
inset_x0 = -1500000
inset_y0 = 1890659
# 生成南海插图地图数据
clip_bbox = st_bbox(
c(
xmin = nh_lon_min, ymin = nh_lat_min,
xmax = nh_lon_max, ymax = nh_lat_max
),
crs = albers_crs
)
scale_matrix = matrix(
c(
inset_w / (nh_lon_max - nh_lon_min), 0,
0, inset_h / (nh_lat_max - nh_lat_min)
),
ncol = 2
)
cn_map |>
st_make_valid() |>
st_crop(clip_bbox) |>
mutate(
geometry = (geometry - c(nh_lon_min, nh_lat_min)) *
scale_matrix + c(inset_x0, inset_y0)
) |>
st_set_geometry("geometry") |>
st_set_crs(albers_crs) -> nh_inset
# ------------------------------
# 生成最终地图
# ------------------------------
ggplot() +
# 绘制中国地图最底层图层
geom_sf(
data = cn_map, aes(fill = interval),
color = "gray60", linewidth = 0.25
) +
# 绘制南海插图背景
geom_rect(
aes(
xmin = inset_x0, xmax = inset_x0 + inset_w,
ymin = inset_y0, ymax = inset_y0 + inset_h
),
fill = "white", color = "black", linewidth = 0.25
) +
# 插图内绘制南海诸岛
geom_sf(
data = nh_inset, aes(fill = interval),
color = "gray60", linewidth = 0.25
) +
# 标注各省区简称和艾滋病发病率
geom_text(
data = prov_labels,
aes(x = geo_lon, y = geo_lat, label = label, color = type),
size = 4, family = "SimHei", hjust = 0.5
) +
scale_color_manual(
values = c("black_font" = "black", "white_font" = "white"),
guide = "none"
) +
# 各省区艾滋病发病率数值按区间填充颜色
scale_fill_manual(
values = interval_colors,
na.value = "white", # 缺失值省区填充白色
na.translate = FALSE, # 隐藏缺失值图例
) +
# 设置地图标题、图例、范围和坐标系
labs(title = "全国艾滋病总计发病率") +
guides(fill = guide_legend(reverse = TRUE, title = NULL)) +
coord_sf(
xlim = c(-2700000, 2300000),
ylim = c(1600000, 6000000),
expand = FALSE,
crs = albers_crs
) +
# 正上方添加图例注解
annotate(
"text", x = 12432.82, y = 5643263,
label = "艾 滋 病 发 病 率(1/10万)",
family = "SimHei", size = 6
) +
annotate(
"text", x = -622313.28, y = 5449290,
label = "HIV/AIDS",
family = "SimHei", size = 4
) +
# 左小角添加注解说明
annotate(
"text", x = -2000000, y = 2585919,
label = "全国总计发病率\n4.27",
family = "SimHei", size = 5
) +
annotate(
"text", x = -2000000, y = 2159607,
label = "2022年(2021年数据)\n中国卫生健康统计年鉴\n香港、澳门数据缺失",
family = "SimHei", size = 4
) +
# 设置地图主题
theme_void() +
theme(
plot.title = element_text(
family = "Noto Serif CJK SC", color = "#31415b",
size = 30, face = "bold", hjust = 0.5, vjust = 1.5
),
legend.background = element_blank(),
legend.position = c(0.53, 0.80),
legend.key.size = unit(0.50, "cm"),
legend.text = element_text(family = "SimHei", size = 11)
)

