ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RStudio地图绘制教程:sf与ggplot2空间可视化全流程指南

RStudio地图绘制教程:sf与ggplot2空间可视化全流程指南

简介:针对R语言地图绘制的实操代码包,面向数据分析和地理信息可视化用户,专注解决从数据准备到地图输出的完整流程,包括矢量与栅格数据读取、坐标系设置、中国省级底图导入和属性填充,并结合ggplot2与ggspatial完成地图渲染。包体结构精简,压缩包仅7KB,共6个文件,含3个R脚本、1个Markdown说明文档、1个inscode配置和1个gitignore,代码量虽小但覆盖读取数据、分组分段、图例配置、比例尺与指北针添加等关键环节,可直接运行或修改复用。目前已有64人浏览学习,适合具备一定R基础、想系统上手地图绘制的数据分析与GIS爱好者。通过学习代码示例,不仅能掌握省级地图数据的处理方法和批量制图的循环写法,还能理解地理坐标系与投影坐标系的选择对出图效果的影响,是一份轻量而实用的入门参考资料。

1. RStudio地图绘制教程能解决什么:一整套可直接改用的空间可视化代码包

如果你在RStudio里折腾过地图,大概率遇到过这几个问题:读进来的行政区划数据画出来是变形的、经纬度点和区域边界对不上、中文字体在导出PDF时变成方块、分面图里的图例顺序莫名其妙翻转。这通常不是你的代码有错,而是从数据清洗、坐标系转换到图层叠加的链路里,缺少一套能直接复用的规范流程。这份RStudio地图绘制教程代码包,解决的正是从GeoJSON边界数据到可发表统计地图的完整链路问题,它面向有R语言基础、但没系统做过空间可视化的数据分析师和科研人员,提供从sf对象构建、坐标系纠偏、ggplot2图层叠加到批量导出的全套可运行脚本。

2. 先搞清楚三个问题再动手:坐标系、数据源与统计图叠加方式

初学者拿到地图数据后最常见的翻车,是不看坐标系就开画,结果区域边界和散点图完全错位。地图绘制和普通统计图最大的差异在于:它同时涉及空间参照、几何对象和业务属性三类信息。在写第一行绘图代码之前,有三个问题必须确认清楚,否则后边每一步都是填坑。

2.1 空间数据的两种形态:sf对象与经纬度DataFrame

R语言里处理空间数据,实际工作中最常遇到的形态是两种:一种是sf包创建的sf对象,本质上是把几何信息(点、线、面)作为一列特殊变量塞进数据框里;另一种是普通数据框,只有lon和lat两列经纬度。这两者的适用边界完全不同。

# 读取GeoJSON边界数据,返回sf对象 library(sf) boundary <- st_read("city_boundary.geojson", quiet = TRUE) # 查看几何类型和坐标系 st_geometry_type(boundary) st_crs(boundary) # 手工构造经纬度数据框 stations <- data.frame( station_id = c("A01", "A02", "A03"), lon = c(110.12, 110.45, 110.33), lat = c(30.21, 30.08, 30.42), value = c(48.5, 62.3, 55.1) )

st_read返回的对象可以直接用ggplot2的geom_sf绘制,但前提是你需要确认边界数据本身带合法的几何字段。经纬度数据框则不能直接进入geom_sf,必须先转换成sf对象。这里的关键区别是:sf对象有明确的空间参照,可以参与裁剪、合并、缓冲区分析;经纬度数据框只是普通表格,好在它可以直接作为散点图层叠加在底图上。日常经验是,底图用sf对象,业务点位用经纬度数据框,两者在绘图层各司其职,不需要强行把所有数据都转成sf。

2.2 三套坐标系的取舍:WGS84、GCJ02与投影坐标系

坐标系是地图绘制里最容易变成玄学的地方,实测中至少有一半的偏移问题出在这里。国际公开数据多用WGS84经纬度,国内某在线地图服务返回的坐标是GCJ02火星坐标,两者之间差几百米,在街道尺度上直接画出来就是点位漂到马路对面。所以动手前必须确认数据来源和坐标类型。

# 查看当前数据的坐标系信息 st_crs(boundary)$input # 如果发现是GCJ02,需要用下方函数纠偏到WGS84 # 注意:这是一个极简示例,完整版在教程代码包的coordinate_fix.R中 gcj02_to_wgs84 <- function(lon, lat) { # 为避免演示代码过长,这里只保留函数骨架 # 实际运行时请使用包内完整实现 data.frame(lon = lon, lat = lat) } # 投影坐标转换示例:WGS84经纬度转UTM 50N投影 boundary_utm <- st_transform(boundary, crs = 32650)

投影坐标系的选择原则是:区域范围大、关注面积和距离时用UTM或Albers等距投影;只想把经纬度和散点对上、不做空间计算时,直接用WGS84经纬度画反而最省事。我的建议是:先用WGS84经纬度把图跑通,确认点位对得上,再做投影转换,不要一次性叠太多变换。

2.3 数据源怎么选:行政区划GeoJSON、在线瓦片与公开统计数据的匹配

数据源的质量直接决定成图效果。行政区划边界最常见的来源是公开GeoJSON仓库,但下载后一定要检查几何是否闭合、属性表里是否有你需要的行政区代码字段。如果你打算把业务数据按区域填色,就需要把业务表的区域名字段和GeoJSON属性表的区域名字段完全对齐——这里的坑在于两个表一个用“市辖区”一个用“区”,或者一个带“省”字一个不带。

# 检查边界数据的字段名 names(boundary) # 常见标准字段:name, adcode, geometry # 业务数据与边界数据合并前,先统一区域字段格式 library(dplyr) biz_data <- biz_data %>% mutate(region = stringr::str_replace_all(region, "省|市", "")) # 合并并检查是否有匹配不上的区域 merged <- boundary %>% left_join(biz_data, by = c("name" = "region"))

在线瓦片作为底图的做法在R里也能实现,但受网络环境影响大。我会优先选择把瓦片缓存到本地;在没有离线瓦片的情况下,动态地图用某开源交互地图库兜底,静态发布图则直接用GeoJSON边界代替底图,等值线加在地界之上。教程代码包里给出的是一个通用的数据体检脚本,跑一遍就能看到每个区域的属性匹配率和缺失情况。

3. 把静态地图画出来:从数据清洗到导出高分辨率PNG的全流程

静态区域填色图是日常出镜率最高的地图类型,也是这份教程代码包的核心内容。整个流程可以拆成四步:清洗数据、合并边界、绘制图层、调整输出细节。每一步都有固定的写法,照着抄就能跑通。

3.1 数据清洗与字段统一:过滤缺失几何、修正类型、处理空值

拿到边界数据后先不要急着画,先做三件体检:几何字段是否全有效、属性字段是否有NA、业务表主键是否和边界表主键能匹配上。缺失几何会导致geom_sf直接报错,NA值会导致填色图出现空洞,字段匹配不上则会出现整片区域无数据的白板。

# 过滤缺失几何,并保留需要的字段 library(dplyr) boundary_clean <- boundary %>% filter(!st_is_empty(geometry)) %>% select(name, adcode, geometry) %>% mutate(name = as.character(name)) # 业务数据的区域字段统一为字符型 biz_data <- biz_data %>% mutate(region = as.character(region)) # 合并并检查匹配情况 plot_data <- boundary_clean %>% left_join(biz_data, by = c("name" = "region")) # 查看哪些区域没有匹配上业务数据 plot_data %>% filter(is.na(value)) %>% pull(name)

这段代码里最关键的是st_is_empty这个函数,它能识别出几何字段为空的行,这些行在绘图时会直接让整个图层崩溃。as.character这一步容易被忽略:如果区域编号一边是字符另一边是数值,left_join会静默产生大量NA,排查难度极高。建议每次合并后都跑一遍filter(is.na(value)),把匹配不上的区域打印出来看。

3.2 用geom_sf叠加图层:区域填充、边界线宽与空白边距控制

底图画法是整个流程里最稳定的部分,ggplot2的geom_sf对sf对象的支持已经很完善。区域填充图的典型写法是这样的:

library(ggplot2) p <- ggplot(plot_data) + geom_sf(aes(fill = value), color = "grey40", linewidth = 0.3) + scale_fill_viridis_c(option = "plasma", na.value = "grey90") + theme_minimal(base_size = 14) + theme( axis.text = element_blank(), axis.ticks = element_blank(), panel.grid = element_blank(), legend.position = "bottom", plot.margin = margin(10, 10, 10, 10) ) + labs(title = "某区域监测值分布", fill = "监测值") ggsave("map_basic.png", p, width = 8, height = 6, dpi = 300)

这里color控制边界线颜色,linewidth控制边界粗细,na.value决定缺失区域的填充色。theme_minimal之后必须把axis.text和panel.grid都清掉,否则会显示经纬度网格,破坏地图的完整感。ggsave的dpi设成300是为了保证后续印刷或放大不模糊,宽高比建议先按8:6出草稿,定稿后再微调。

3.3 色板与图例的精细控制:连续变量分段、离散变量配色与图例位置

填色图最容易翻车的地方是图例。连续变量的色板如果跨度大,默认的图例刻度间隔可能不合理;离散变量则会遇到图例顺序翻转的问题——ggplot2会按字母序排列分类,而不是按你业务上的自然顺序。解决方法是提前把分类变量转成因子并指定水平顺序。

# 连续变量按分位数分段,转成离散因子 plot_data <- plot_data %>% mutate(value_group = cut(value, breaks = quantile(value, probs = c(0, 0.25, 0.5, 0.75, 1), na.rm = TRUE), include.lowest = TRUE )) # 离散配色,并指定图例顺序 p_discrete <- ggplot(plot_data) + geom_sf(aes(fill = value_group), color = "grey40", linewidth = 0.3) + scale_fill_brewer(palette = "YlOrRd", na.value = "grey90") + theme_void() + theme(legend.position = "bottom") + labs(title = "某区域监测值四分位分布") ggsave("map_discrete.png", p_discrete, width = 8, height = 6, dpi = 300)

cut函数默认左开右闭,include.lowest = TRUE保证最小值能落入第一段,否则会出现一个NA分组。离散分段的优势是图例更易读,劣势是丢掉了数值细节,所以报告用的图建议保留一个连续版本做附录,正文用分段版本。

4. 分面与业务图层叠加:把时间、点位和轨迹压进同一张图

单张区域图只能承载一个维度。真实项目里经常要把多个月份的监测数据并排对比,或者把监测点位、物流轨迹叠到底图上。这两个需求对应的分别是分面图和业务图层叠加,也是教程代码包里第二个模块的内容。

4.1 facet_wrap分面出图:多时段并列与自由比例尺的设置

分面是把同一个区域的多张图拼到一块的标准做法。操作上并不复杂,只需要在ggplot对象上追加一个facet_wrap,但参数设置有几个容易忽略的细节:多张图的色板范围是否要统一?图例是否要共享?每张图的坐标轴是否要固定?

# 构造多时点数据:假设有1月、2月、3月三个时段 map_data_month <- bind_rows( plot_data %>% mutate(month = "2024-01"), plot_data %>% mutate(month = "2024-02"), plot_data %>% mutate(month = "2024-03") ) p_facet <- ggplot(map_data_month) + geom_sf(aes(fill = value), color = "grey40", linewidth = 0.2) + scale_fill_viridis_c(option = "plasma", limits = range(map_data_month$value, na.rm = TRUE)) + facet_wrap(~month, ncol = 3) + theme_void() + theme(legend.position = "bottom") ggsave("map_facet.png", p_facet, width = 12, height = 5, dpi = 300)

limits参数是在分面场景里必须手动设置的,否则每个分面会独立计算色板范围,导致不同月份之间的颜色没有可比性。ncol = 3控制每行放几张图,当时段数是奇数时这样排比较均衡。theme_void()在分面场景里比theme_minimal()更合适,因为去掉坐标轴后整张图更干净。

4.2 叠加业务点位与路径:geom_point与geom_path的坐标对齐

点位和路径叠加最容易出问题的是坐标系不统一。如果你的底图是WGS84,业务点位坐标也必须是WGS84。实际操作中业务系统导出的点位常常是GCJ02,这种错位在视觉上表现为点位整体朝某个方向偏移,离边界越远偏移越明显。

# 把业务点位转成sf对象,确保与底图坐标系一致 library(sf) stations_sf <- st_as_sf(stations, coords = c("lon", "lat"), crs = 4326 ) # 底图和点位叠加 p_points <- ggplot() + geom_sf(data = plot_data, fill = "grey95", color = "grey40", linewidth = 0.3) + geom_sf(data = stations_sf, aes(size = value), color = "#D73027", alpha = 0.8) + scale_size_continuous(range = c(2, 8)) + theme_void() + theme(legend.position = "bottom") ggsave("map_points.png", p_points, width = 8, height = 6, dpi = 300)

点位图叠加的注意点:先画底图,再画点位,点位的图层顺序在上层;alpha建议设置在0.7到0.9之间,避免点太多时互相遮挡到看不清边界。路径图同理,用geom_path连接点位时注意group参数要指定路径编号,否则ggplot会把所有点连成一条线。

4.3 保存分面图的两种方式:剪裁空白边距与调整多图拼版

出图阶段最容易被人忽略的是空白边距。分面图因为坐标轴被隐藏,经常会在上下左右留下大面积空白,直接放进报告里显得很业余。这里我一般会用coord_sf配合xlim和ylim把显示范围裁剪到边界数据的外接框附近,或者用theme(plot.margin = margin(0, 0, 0, 0))压掉边距。

p_facet_cropped <- p_facet + coord_sf( xlim = c(st_bbox(plot_data)[1], st_bbox(plot_data)[3]), ylim = c(st_bbox(plot_data)[2], st_bbox(plot_data)[4]) ) ggsave("map_facet_cropped.png", p_facet_cropped, width = 12, height = 5, dpi = 300, bg = "white")

st_bbox获取边界数据的外接框四个角点,coord_sf把绘图区域裁剪到这个范围,两个结合能有效消掉多余空白。注意coord_sf的参数顺序:xlim是经度范围,ylim是纬度范围,写反的话图形会被裁得只剩一半。

5. 地图绘制的五个隐蔽坑位:坐标系偏移、字体豆腐块与图例失灵

地图绘制里真正消耗时间的不是画图本身,而是排坑。下面是这几年反复踩过的五类问题,每一条都按现象、原因、解决的思路记录,能帮你少走一个月的弯路。

5.1 现象:点位整体偏移,离边界越远越明显

原因是底图用的是WGS84,点位坐标却是GCJ02火星坐标,两者差值约几百米。解决方式是在数据清洗阶段就把点位坐标批量纠偏。教程代码包里的coordinate_fix.R提供了一个可用的批处理版本,处理完后再用st_as_sf指定坐标系为4326。切记不要在画图之后再做纠偏,因为已经进入ggplot管线的数据改起来更麻烦。

5.2 现象:中文标题和图例变成方块

原因是R自带的PDF和PNG设备对中文字体支持有限,尤其是Windows环境下经常缺中文字体。解决方式有两种:一是用showtext包动态加载系统字体,二是导出前手动指定family参数并确保字体名称正确。我一般用前者,在脚本开头统一加载字体配置,这样整个项目的图风格一致。

5.3 现象:离散图例顺序翻转,数据段的顺序和业务逻辑相反

原因是ggplot2会按因子水平的顺序排列图例,如果你的字段是字符型,它默认按字母序排列。比如“高、中、低”会排成“低、中、高”。解决方式是在mutate阶段就把字段转成有序因子,用factor(levels = c("低", "中", "高"))显式指定顺序。图例名称用labs(fill = "...")修改,不要直接在字段名里带中文。

5.4 现象:区域填色溢出边界,颜色漫到相邻区域

原因是边界数据本身存在几何错误,常见的有自相交多边形和未闭合的环。解决方式是先用st_make_valid()处理几何,再去做合并和绘图。这个函数是sf包自带的,处理速度很快,强烈建议在读取GeoJSON后默认跑一遍。

5.5 现象:导出的PNG模糊或空白过多

原因是dpi设置过低,或者coord_sf没有裁剪显示范围。低dpi的问题用ggsave(dpi = 300)解决;空白过多的问题用第4.3节里的coord_sf配合st_bbox压边距。另外一个容易被忽略的点是ggsave的bg参数,默认是白色,但如果你的主题背景是透明的,导出后会出现网格背景,所以要显式指定bg = "white"。

6. 把出图流程固化成模板:每次动手先跑数据体检的四个习惯

地图绘制和普通数据分析有一个本质区别:数据问题几乎不会报错,只会以错位、空白、变形的方式呈现在图上。所以我现在接到任何地图任务,第一件事不是打开RStudio写ggplot,而是先跑一遍教程代码包里的00_check_data.R体检脚本,确认边界几何有效、坐标系明确、字段匹配率达标,然后再谈配色和出图。

# 数据体检脚本的简化版骨架 library(sf) library(dplyr) # 第一步:检查几何有效性 cat("几何有效比例:", mean(st_is_valid(boundary)), "\n") # 第二步:检查坐标系 print(st_crs(boundary)$input) # 第三步:检查字段匹配率 match_rate <- mean(plot_data$value 不是 NA) cat("业务字段匹配率:", round(match_rate * 100, 2), "%\n")

这段骨架代码里有几个习惯值得坚持:体检和绘图分离,体检脚本只输出诊断信息,不做任何可视化;所有坐标系转换集中在同一个脚本里,绘图脚本不再出现st_transform,避免同事拿到代码后各自为政;每个输出文件统一用日期前缀命名,比如20240628_map_basic.png,方便回溯版本。从那以后我每次替别人排查地图问题,第一句话都是先问:你的数据体检脚本跑过了吗?如果对方答不上来,那问题多半就出在数据源头而不是绘图语句上。这套流程希望帮到你,让你的地图出图从玄学变成稳定可复现的流水线。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进