Welcome to R Square

ggplot2 + sf + 高德地图数据绘制中国地图

楚新元 / 2026-06-20


最近在小红书上看到了 2021 年中国艾滋病发病率地图,尽管有个别细节上的瑕疵,但是瑕不掩瑜,这幅图总体上比较专业,一时技痒,打算复刻这幅图。下面我们一窥这幅图全貌。

这幅图有几个亮点:

这幅图几个小小的瑕疵:

下面我们开始复刻这幅图。

中国地图做法

统计之都上有很多关于 R 语言画中国地图的文章和讨论。黄湘云的《R 语言画中国地图》一文中较全面介绍了中国地图的好几种做法。

先跑通整个工具链

从数据的可获得性、完整性和权威性三个维度看,高德平台的地图数据最容易获取,也比较完整,自带南海诸岛、钓鱼岛及其附属岛屿,无需额外添加南海诸岛和九段线图层,另外该地图数据还贴心的加入了各省区的行政中心和地理位置中心的经纬度,权威性自然与官方权威部门的数据不能比,但是这个数据有大厂背书,背后有无数的高德地图用户,还是值得信赖的,作为学习和交流使用完全没有问题,因此本次中国地图绘制使用高德平台地图数据。

# ------------------------------
# 加载 R 包
# ------------------------------

library(sf)
library(ggplot2)

# ------------------------------
# 获取数据地图
# ------------------------------

# 数据来源:https://datav.aliyun.com/portal/school/atlas/area_selector
# 数据版本:areas_v3
# 行政区划范围:中华人民共和国
# 数据粒度:省
url = "https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json"

# 下载数据方便离线使用
if (!dir.exists("data")) dir.create("data")
dest = "./data/中华人民共和国省级地图数据.json"
if (!file.exists(dest)) download.file(url, dest)

# ------------------------------
# 地图数据和艾滋病数据并合并
# ------------------------------

# 高德的中国行政区划 GeoJSON 是平面投影适配的矢量边界
# 禁用 s2 球面几何,使用传统 GEOS 平面笛卡尔几何
sf_use_s2(FALSE)

# 读取中国省级地图数据
cn_raw = read_sf(dest)

# ------------------------------
# 生成最终地图
# ------------------------------

ggplot() +
  geom_sf(data = cn_raw)

实际数据映射到地图上

# 艾滋病发病率数据(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_prov_aids = merge(cn_raw, prov_aids, by = "name", all.x = TRUE)
# 将发病率拆分为区间
cn_raw_prov_aids = within(cn_raw_prov_aids, {
    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_prov_aids_data = st_as_sf(
  cn_raw_prov_aids, 
  sf_column_name = "geometry"
)
# 加载必要的 dplyr 函数
use("dplyr", c("mutate", "select", "case_when"))

# 定义每个区间的颜色
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"   # 黑色
)

# 几何质心处标签位置微调
adjust_dict = tibble::tribble(
  ~short_name, ~lon_offset, ~lat_offset,
  "甘肃",       0.3,        -1.1, 
  "内蒙古",    -4.0,        -3.5, 
  "陕西",       0.0,        -1.0, 
  "河北",       1.0,         0.0, 
  "北京",       0.0,         1.2, 
  "天津",       1.2,         0.0, 
  "辽宁",       0.7,         0.0, 
  "吉林",       0.0,        -0.6, 
  "黑龙江",     1.0,        -1.5, 
  "云南",       0.0,        -0.5, 
  "海南",      -2.5,        -0.8, 
  "澳门",      -0.8,        -1.8, 
  "香港",       0.8,        -1.4, 
  "台湾",       2.5,        -0.5, 
  "上海",       1.5,         0.7
)

# 确定标签位置、内容和颜色
cn_raw_prov_aids |> 
  subset(level == "province") |>
  mutate(
    c_lon = vapply(center, `[`, numeric(1), 1),
    c_lat = vapply(center, `[`, numeric(1), 2),
    geo_lon = vapply(centroid, `[`, numeric(1), 1),
    geo_lat = vapply(centroid, `[`, numeric(1), 2)
  ) |> 
  st_drop_geometry() |>
  select(name, value, c_lon, c_lat, geo_lon, geo_lat) |>
  mutate(
    # 根据全称生成简称
    short_name = gsub("(省|自治区|市|特别行政区)$", "", name),
    short_name = gsub("(壮族|回族|维吾尔)", "", short_name),
    # 河北、甘肃几何质心缺失,先用行政中心补齐
    geo_lon = ifelse(short_name %in% c("河北", "甘肃"), c_lon, geo_lon),
    geo_lat = ifelse(short_name %in% c("河北", "甘肃"), c_lat, geo_lat),
  ) |> 
  # 匹配微调参数并计算
  merge(adjust_dict, by = "short_name", all.x = TRUE) |>
  mutate(
    geo_lon = geo_lon + ifelse(is.na(lon_offset), 0, lon_offset),
    geo_lat = geo_lat + ifelse(is.na(lat_offset), 0, lat_offset)
  ) |> 
  select(-lon_offset, -lat_offset) |> 
  # 生成标签内容并确定颜色
  mutate(
    label = paste(short_name, format(value, nsmall = 2), sep = "\n"), 
    type = ifelse(
      # 标签在外部的一律用黑色;在内部的,数值小颜色浅用黑色,数值大颜色深用白色
      value < 3 | short_name %in% c("海南", "澳门", "香港", "台湾", "上海", "天津"), 
      "black_font", "white_font"
    )
  ) -> prov_center
ggplot() +
  # 绘制中国地图最底层图层
  geom_sf(
    data = cn_prov_aids_data, 
    aes(fill = interval), 
    color = "gray60", size = 0.25
  ) + 
  
  # 标注各省区简称和艾滋病发病率
  geom_text(
    data = prov_center, 
    aes(x = geo_lon, y = geo_lat, label = label, color = type),
    size = 3, 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,  # 隐藏缺失值图例
  ) + 
  
  # 设置坐标系和主题
  coord_sf() + 
  theme_void()

再出发,美化地图

完成了上面的图,剩下的工作除了南海九段线插图外,都没什么技术含量,但是参数调整比较耗费时间。我们先修改图例位置和顺序、加入注释说明、砍掉信息密度极低的下半部分、调整图片尺寸。

#| fig.width: 9
#| fig.height: 8
ggplot() +
  # 绘制中国地图最底层图层
  geom_sf(
    data = cn_prov_aids_data, 
    aes(fill = interval), 
    color = "gray60", size = 0.25
  ) + 
  
  # 标注各省区简称和艾滋病发病率
  geom_text(
    data = prov_center, 
    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(73, 135), ylim = c(16, 54)) + 

  # 正上方添加图例注解
  annotate(
    "text", x = 106, y = 53, 
    label = "艾 滋 病 发 病 率(1/10万)",
    family = "SimHei", size = 6
  ) + 
  annotate(
    "text", x = 97, y = 51, 
    label = "HIV/AIDS",
    family = "SimHei", size = 4
  ) + 
  
  # 左小角添加注解说明
  annotate(
    "text", x = 81.5, y = 24, 
    label = "全国总计发病率\n4.27",
    family = "SimHei", size = 4
  ) + 
  annotate(
    "text", x = 81.5, y = 20, 
    label = "2022年(2021年数据)\n中国卫生健康统计年鉴\n香港、澳门数据缺失",
    family = "SimHei", size = 3.5
  ) + 
  
  # 设置地图主题
  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),
  )

注意:为了追求视觉效果,南海下半部分被砍了,但是必须保证地图完整,这就需要在地图上加一个插图专门显示南海岛礁和九段线等。

添加南海九段线插图

# 插图画布尺寸和主图放置起点(经纬度坐标)
inset_w = 9
inset_h = 9
inset_x0 = 87
inset_y0 = 17

# 能囊括完整南海四至的范围(包含全部岛礁、九段线)
nh_lon_min = 104.5
nh_lon_max = 122.5
nh_lat_min = 3.0  # 官方声明范围南纬 3°
nh_lat_max = 23.5

# 提取全部几何坐标矩阵
all_coords = as.data.frame(st_coordinates(cn_raw_prov_aids))
colnames(all_coords) = c("lon", "lat", "L1", "L2", "L3")

# 筛选能囊括南海范围的坐标点
nanhai_raw = subset(
  all_coords,
  lon >= nh_lon_min & lon <= nh_lon_max & 
  lat >= nh_lat_min & lat <= nh_lat_max
)

# 生成南海插图地图数据
nanhai_df = data.frame(
  # 坐标映射:南海真实经纬度 → 插图坐标
  lon = (nanhai_raw$lon - nh_lon_min) / 
    (nh_lon_max - nh_lon_min) * inset_w + inset_x0,
  lat = (nanhai_raw$lat - nh_lat_min) / 
    (nh_lat_max - nh_lat_min) * inset_h + inset_y0,
  
  # 属性映射与多边形分组
  interval = cn_raw_prov_aids$interval[nanhai_raw$L3],
  group = paste(nanhai_raw$L1, nanhai_raw$L2, nanhai_raw$L3, sep = "_")
)
#| fig.width: 9
#| fig.height: 8
ggplot() +
  # 绘制中国地图最底层图层
  geom_sf(
    data = cn_prov_aids_data, 
    aes(fill = interval), 
    color = "gray60", size = 0.25
  ) + 
  
  # 绘制南海插图背景
  geom_rect(
    aes(
      xmin = inset_x0, xmax = inset_x0 + inset_w + 0.3,
      ymin = inset_y0 - 0.3, ymax = inset_y0 + inset_h),
      fill = "white", color = "black", linewidth = 0.25
  ) + 
  
  # 插图内绘制南海诸岛
  geom_polygon(
    data = nanhai_df,
    aes(x = lon, y = lat, group = group, fill = interval),
    color = "gray60", linewidth = 0.25
  ) + 
  
  # 标注各省区简称和艾滋病发病率
  geom_text(
    data = prov_center, 
    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(73, 135), ylim = c(16, 54)) + 

  # 正上方添加图例注解
  annotate(
    "text", x = 106, y = 53, 
    label = "艾 滋 病 发 病 率(1/10万)",
    family = "SimHei", size = 6
  ) + 
  annotate(
    "text", x = 97, y = 51, 
    label = "HIV/AIDS",
    family = "SimHei", size = 4
  ) + 
  
  # 左小角添加注解说明
  annotate(
    "text", x = 81.5, y = 24, 
    label = "全国总计发病率\n4.27",
    family = "SimHei", size = 4
  ) + 
  annotate(
    "text", x = 81.5, y = 20, 
    label = "2022年(2021年数据)\n中国卫生健康统计年鉴\n香港、澳门数据缺失",
    family = "SimHei", size = 3.5
  ) + 
  
  # 设置地图主题
  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),
  )

附上完整代码方便重现

#'
#' 科学绘制中国地图
#'
#' @author 楚新元
#' @date 2026-06-21
#'

# ------------------------------
# 加载 R 包
# ------------------------------

library(sf)
library(ggplot2)
use("dplyr", c("mutate", "select", "case_when"))

# ------------------------------
# 获取数据地图
# ------------------------------

# 数据来源:https://datav.aliyun.com/portal/school/atlas/area_selector
# 数据版本:areas_v3
# 行政区划范围:中华人民共和国
# 数据粒度:省
url = "https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json"

# 下载数据方便离线使用
if (!dir.exists("data")) dir.create("data")
dest = "./data/中华人民共和国省级地图数据.json"
if (!file.exists(dest)) download.file(url, dest)

# ------------------------------
# 地图数据和艾滋病数据并合并
# ------------------------------

# 高德的中国行政区划 GeoJSON 是平面投影适配的矢量边界
# 禁用 s2 球面几何,使用传统 GEOS 平面笛卡尔几何
sf_use_s2(FALSE)

# 读取中国省级地图数据
cn_raw = read_sf(dest)

# 艾滋病发病率数据(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_prov_aids = merge(cn_raw, prov_aids, by = "name", all.x = TRUE)

# 将发病率拆分为区间
cn_raw_prov_aids = within(cn_raw_prov_aids, {
    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_prov_aids_data = st_as_sf(
  cn_raw_prov_aids, 
  sf_column_name = "geometry"
)

# ------------------------------
# 设置填充颜色和标签位置、内容、颜色
# ------------------------------

# 定义每个区间的颜色
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"   # 黑色
)

# 几何质心处标签位置微调
adjust_dict = tibble::tribble(
  ~short_name, ~lon_offset, ~lat_offset,
  "甘肃",       0.3,        -1.1, 
  "内蒙古",    -4.0,        -3.5, 
  "陕西",       0.0,        -1.0, 
  "河北",       1.0,         0.0, 
  "北京",       0.0,         1.2, 
  "天津",       1.2,         0.0, 
  "辽宁",       0.7,         0.0, 
  "吉林",       0.0,        -0.6, 
  "黑龙江",     1.0,        -1.5, 
  "云南",       0.0,        -0.5, 
  "海南",      -2.5,        -0.8, 
  "澳门",      -0.8,        -1.8, 
  "香港",       0.8,        -1.4, 
  "台湾",       2.5,        -0.5, 
  "上海",       1.5,         0.7
)

# 确定标签位置、内容和颜色
cn_raw_prov_aids |> 
  subset(level == "province") |>
  mutate(
    c_lon = vapply(center, `[`, numeric(1), 1),
    c_lat = vapply(center, `[`, numeric(1), 2),
    geo_lon = vapply(centroid, `[`, numeric(1), 1),
    geo_lat = vapply(centroid, `[`, numeric(1), 2)
  ) |> 
  st_drop_geometry() |>
  select(name, value, c_lon, c_lat, geo_lon, geo_lat) |>
  mutate(
    # 根据全称生成简称
    short_name = gsub("(省|自治区|市|特别行政区)$", "", name),
    short_name = gsub("(壮族|回族|维吾尔)", "", short_name),
    # 河北、甘肃几何质心缺失,先用行政中心补齐
    geo_lon = ifelse(short_name %in% c("河北", "甘肃"), c_lon, geo_lon),
    geo_lat = ifelse(short_name %in% c("河北", "甘肃"), c_lat, geo_lat),
  ) |> 
  # 匹配微调参数并计算
  merge(adjust_dict, by = "short_name", all.x = TRUE) |>
  mutate(
    geo_lon = geo_lon + ifelse(is.na(lon_offset), 0, lon_offset),
    geo_lat = geo_lat + ifelse(is.na(lat_offset), 0, lat_offset)
  ) |> 
  select(-lon_offset, -lat_offset) |> 
  # 生成标签内容并确定颜色
  mutate(
    label = paste(short_name, format(value, nsmall = 2), sep = "\n"), 
    type = ifelse(
      # 标签在外部的一律用黑色;在内部的,数值小颜色浅用黑色,数值大颜色深用白色
      value < 3 | short_name %in% c("海南", "澳门", "香港", "台湾", "上海", "天津"), 
      "black_font", "white_font"
    )
  ) -> prov_center

# ------------------------------
# 南海插图参数设置和矢量坐标转换
# ------------------------------

# 插图画布尺寸和主图放置起点(经纬度坐标)
inset_w = 9
inset_h = 9
inset_x0 = 87
inset_y0 = 17

# 能囊括完整南海四至的范围(包含全部岛礁、九段线)
nh_lon_min = 104.5
nh_lon_max = 122.5
nh_lat_min = 3.0  # 官方声明范围南纬 3°
nh_lat_max = 23.5

# 提取全部几何坐标矩阵
all_coords = as.data.frame(st_coordinates(cn_raw_prov_aids))
colnames(all_coords) = c("lon", "lat", "L1", "L2", "L3")

# 筛选能囊括南海范围的坐标点
nanhai_raw = subset(
  all_coords,
  lon >= nh_lon_min & lon <= nh_lon_max & 
  lat >= nh_lat_min & lat <= nh_lat_max
)

# 生成南海插图地图数据
nanhai_df = data.frame(
  # 坐标映射:南海真实经纬度 → 插图坐标
  lon = (nanhai_raw$lon - nh_lon_min) / 
    (nh_lon_max - nh_lon_min) * inset_w + inset_x0,
  lat = (nanhai_raw$lat - nh_lat_min) / 
    (nh_lat_max - nh_lat_min) * inset_h + inset_y0,
  
  # 属性映射与多边形分组
  interval = cn_raw_prov_aids$interval[nanhai_raw$L3],
  group = paste(nanhai_raw$L1, nanhai_raw$L2, nanhai_raw$L3, sep = "_")
)

# ------------------------------
# 生成最终地图
# ------------------------------

ggplot() +
  # 绘制中国地图最底层图层
  geom_sf(
    data = cn_prov_aids_data, 
    aes(fill = interval), 
    color = "gray60", size = 0.25
  ) + 
  
  # 绘制南海插图背景
  geom_rect(
    aes(
      xmin = inset_x0, xmax = inset_x0 + inset_w + 0.3,
      ymin = inset_y0 - 0.3, ymax = inset_y0 + inset_h),
      fill = "white", color = "black", linewidth = 0.25
  ) + 
  
  # 插图内绘制南海诸岛
  geom_polygon(
    data = nanhai_df,
    aes(x = lon, y = lat, group = group, fill = interval),
    color = "gray60", linewidth = 0.25
  ) + 
  
  # 标注各省区简称和艾滋病发病率
  geom_text(
    data = prov_center, 
    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(73, 135), ylim = c(16, 54)) + 

  # 正上方添加图例注解
  annotate(
    "text", x = 106, y = 53, 
    label = "艾 滋 病 发 病 率(1/10万)",
    family = "SimHei", size = 6
  ) + 
  annotate(
    "text", x = 97, y = 51, 
    label = "HIV/AIDS",
    family = "SimHei", size = 4
  ) + 
  
  # 左小角添加注解说明
  annotate(
    "text", x = 81.5, y = 24, 
    label = "全国总计发病率\n4.27",
    family = "SimHei", size = 4
  ) + 
  annotate(
    "text", x = 81.5, y = 20, 
    label = "2022年(2021年数据)\n中国卫生健康统计年鉴\n香港、澳门数据缺失",
    family = "SimHei", size = 3.5
  ) + 
  
  # 设置地图主题
  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),
  )
注意 2026-06-21

1.南海的最南端必须按照官方声明范围南纬 3° 来,其它参数可以设置的相对宽松点,只要确保将南海九段线包括进来即可;2.南海插图矢量坐标转换这一步,将空间几何绘图数据转为数据框完全没必要,最优雅的方式应该是利用 sf 包的函数直接处理,结果仍然是空间几何绘图数据,而不是数据框;3.上面的代码生成的图作为日常学习交流使用完全没问题,但是在学术论文和正式场合,建议还是使用官方权威数据来源,另外,我们会发现它和国内权威媒体通常使用的地图形状有些差异,我们的图看起来比较扁,这其实是因为投影不同而造成的,权威媒体地图采用的投影方式为兰伯特正方位等积投影(Lambert azimuthal equal-area projection)。

环境信息

xfun::session_info()
#> R version 4.6.0 (2026-04-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Arch Linux, Positron 2026.06.0
#> 
#> Locale:
#>   LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>   LC_TIME=zh_CN.UTF-8        LC_COLLATE=en_US.UTF-8    
#>   LC_MONETARY=zh_CN.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>   LC_PAPER=zh_CN.UTF-8       LC_NAME=C                 
#>   LC_ADDRESS=C               LC_TELEPHONE=C            
#>   LC_MEASUREMENT=zh_CN.UTF-8 LC_IDENTIFICATION=C       
#> 
#> Package version:
#>   base64enc_0.1.6    blogdown_1.24      bookdown_0.47      bslib_0.11.0      
#>   cachem_1.1.0       class_7.3-23       classInt_0.4-11    cli_3.6.6         
#>   compiler_4.6.0     cpp11_0.5.5        DBI_1.3.0          digest_0.6.39     
#>   dplyr_1.2.1        e1071_1.7-17       evaluate_1.0.5     farver_2.1.2      
#>   fastmap_1.2.0      fontawesome_0.5.3  fs_2.1.0           generics_0.1.4    
#>   ggplot2_4.0.3      glue_1.8.1         graphics_4.6.0     grDevices_4.6.0   
#>   grid_4.6.0         gtable_0.3.6       highr_0.12         htmltools_0.5.9   
#>   httpuv_1.6.17      isoband_0.3.0      jpeg_0.1-11        jquerylib_0.1.4   
#>   jsonlite_2.0.0     KernSmooth_2.23-26 knitr_1.51         labeling_0.4.3    
#>   later_1.4.8        lifecycle_1.0.5    magrittr_2.0.5     MASS_7.3.65       
#>   memoise_2.0.1      methods_4.6.0      mime_0.13          otel_0.2.0        
#>   pillar_1.11.1      pkgconfig_2.0.3    promises_1.5.0     proxy_0.4-29      
#>   R6_2.6.1           rappdirs_0.3.4     RColorBrewer_1.1-3 Rcpp_1.1.1-1.1    
#>   rlang_1.2.0        rmarkdown_2.31     s2_1.1.11          S7_0.2.2          
#>   sass_0.4.10        scales_1.4.0       servr_0.33         sf_1.1-1          
#>   stats_4.6.0        tibble_3.3.1       tidyselect_1.2.1   tinytex_0.60      
#>   tools_4.6.0        units_1.0-1        utf8_1.2.6         utils_4.6.0       
#>   vctrs_0.7.3        viridisLite_0.4.3  withr_3.0.3        wk_0.9.5          
#>   xfun_0.59          yaml_2.3.12