以前提到出版级世界地图,我的第一反应是打开ArcGIS,导入图层、调坐标系、再导出图片。直到有一次做课题需要批量出图,在GIS软件里来来回回折腾了大半天,才突然意识到:其实我每天写数据分析用的R语言,早就把这活儿干完了。现在我常用ggplot2配合ggmapcn这个包,几分钟就能生成一张可以直接放进论文的地图底图,投影切换、边界数据、配色样式全部由代码自动完成,数据更新后重跑一遍脚本就能得到新图。今天这篇就分享我用R语言+ggplot2+ggmapcn绘制世界地图的完整流程,重点讲清楚5种投影的区别,以及如何用代码做出出版级效果的实用技巧。
这套方法最适合两类人:一是科研工作者和数据分析师,经常需要做“数据+地图”的叠加展示,不想为了画一张底图去打开重型的GIS桌面软件;二是刚开始接触空间可视化的R用户,希望用一套代码解决从数据加载到出图的全过程。我不打算把每个函数讲成文档,而是按我实际操作时的思路来拆解,包括我踩过的坑、试出来的经验,以及那些网上很少写明白的细节。
1. 项目思路拆解:为什么偏偏用R语言
1.1 GIS软件能做的事,R为什么也能做
很多人一听“地图绘制”,默认就是ArcGIS、QGIS这些GIS软件的专属领域。确实,论图层管理、地理配准、空间分析,专业GIS工具仍然有不可替代的优势。但是,当你的需求只是“把一张世界底图画出来,换换投影,加点数据”时,GIS软件反而显得笨重。启动要等半天,操作界面层级复杂,想批量处理不同投影的版本还得一个个手动设置,更别提版本授权和盗版风险。
R语言的好处在于它是一个统计计算与可视化的统一环境。地图在R里本质上就是一个数据框,或者是一个带几何列的sf对象。绘制地图就是一次ggplot2的几何图层映射,跟画柱状图、散点图没有本质区别。所有操作都写在脚本里,参数透明,可重复执行,出图结果完全可控。而且R社区里围绕空间数据处理的包非常成熟:sf负责读矢量数据,ggplot2负责绘制,ggmapcn这类包则把常见的地图数据和投影接口封装得更顺手。
1.2 ggplot2与ggmapcn的分工
ggplot2应该是R里最出名的绘图包,它采用“图层叠加”的语法,地图可以被当成一个特殊的几何图层画出来。但ggplot2本身不管地图数据从哪里来,也不内置世界边界。这就需要额外提供地理数据。如果全部手写,光是下载、清理各国边界的数据就要花不少时间。
ggmapcn就是在这里起到衔接作用。它相当于一个“地图数据+绘图增强”的便捷工具集:一方面内置了经过拓扑检查的常见地图边界数据,包括世界和中国的分国界、省界等;另一方面针对ggplot2做了一些适配,比如更简便的投影参数设置、中文主题支持、边界样式的快速配置。用我常说的大白话就是:ggmapcn负责把“面粉”(地图数据)准备好,ggplot2负责把它做成“面包”(出版级地图)。两者配合,才能既快又好。
1.3 一次出图的完整流程预览
我先给你一个完整流程的鸟瞰,后面每一部分再展开细节。整个项目大概分成4步:
- 安装并加载
ggplot2、ggmapcn、sf等R包; - 从ggmapcn中取到世界地图边界数据,转成sf对象;
- 用
ggplot()+geom_sf()完成基础底图,并用coord_sf()或ggmapcn的投影函数切换不同投影; - 调整主题、颜色、标注、比例尺等细节,用
ggsave()导出300dpi以上的高清图片。
听起来是不是很简单?实际做起来也确实简单,但有几个关键点如果没搞清楚,后面画出来的图会出现边界断裂、图形变形、比例尺缺失等问题。我把这些问题全部放在下文里,对照代码替你们踩一遍。
2. 环境准备与地图数据接入
2.1 安装R包时的那些坑
R包安装本身不复杂,一行代码的事:
install.packages(c("ggplot2", "ggmapcn", "sf", "patchwork"))但有几个坑值得提前说。
第一个是sf包的依赖问题。sf是空间矢量数据处理的核心,它有一些底层依赖,比如GDAL、GEOS、PROJ。在Windows上,通常直接安装二进制版本就能用;但如果你的R是自定义编译版本,或者Linux下缺少系统库,就要提前处理。我遇到过最典型的情况是安装后加载报错,提示“libgdal.so.XX: cannot open shared object file”。这种通常意味着系统环境里缺GDAL库,最简单的解决办法是重新安装R,或者用install.packages("sf", type = "binary"),再不行就用Docker跑R环境。如果你只是在Windows上学习,大可放心,直接安装即可。
第二个是ggmapcn的安装来源。因为不是所有包都在CRAN上,所以可能要用devtools::install_github()来装。具体名称要用带作者仓库的完整地址,我用的版本是通过以下方式安装的:
install.packages("remotes") remotes::install_github("tidyverse/ggplot2") # 确保最新版ggplot2 remotes::install_github("YourGitHubName/ggmapcn") # 按实际地址替换这里提个醒:如果你在国内网络环境,GitHub安装可能不稳定,建议配置镜像源,或者下载zip包后本地安装。装好之后,用library(ggmapcn)验证,不报错就可以继续。
2.2 世界边界数据从哪里来
地图边界数据是整个绘制的地基。市面上的开源数据源很多,最常见的有Natural Earth、GADM、OpenStreetMap导出数据。Natural Earth数据精度适中、样式美观,是出版级地图的首选之一。如果你的项目里已经用了rnaturalearth包,也可以直接获取,但ggmapcn另一个便利之处是它把世界边界数据封装成了一个开箱即用的函数,省去联网下载的流程。
我习惯这样获取数据:
library(ggmapcn) world_map <- ggmapcn::world_borders()这个函数返回的是一个sf对象,包含全球国家/地区的面边界,每一行代表一个地理实体,附带name、continent等属性列。拿到之后,用class(world_map)看一眼,确认是sf和data.frame,就可以进入ggplot2流程了。
还要强调一点:使用真实可信的地图数据,不仅影响到出图的美观度,更影响到科学的准确性和合规性。尤其涉及国界时,一定要使用官方或国际通用的可信数据源,不要自己手工描边。
2.3 ggmapcn的基本用法
用ggmapcn时,我推荐先了解下面几个常用功能。它们不复杂,却能让代码简洁很多。
首先是快速绘制世界地图:
p <- ggmapcn::world_map() |> ggplot() + geom_sf()如果你不需要细调,这个p就是一个最基础的世界地图,坐标轴默认是经纬度,看上去是等距矩形投影。
其次是配合投影参数。ggmapcn可以接受缩写的投影名称,比如"mercator"、"robin"、"mollweide"等。实际上它就是帮你把coord_sf(crs = ...)包装了一层,少写很多记忆成本。不过我个人更推荐直接使用标准CRS编号或proj4字符串,因为这样更通用,也便于从其他GIS软件迁移过来。比如:
coord_sf(crs = "+proj=robin +lon_0=0 +datum=WGS84")这种方式清晰,报错也容易排查。
ggmapcn还有一个有价值的功能是主题适配。它自带一个干净的清奇主题,类似theme_minimal()但针对地图做了优化,比如去掉不必要的网格线和坐标轴刻度,让地图主体突出。后面画出版级图的时候,我会在这个基础上再手动微调。
3. 5种投影原理与对比图实现
3.1 投影的本质:怎么把球面摊平
地球是一个近似球体,而地图是平面的。要把弯曲的地球表面画到一张纸上,必然会发生变形。所谓地图投影,就是一套把经纬度坐标转换成平面坐标的数学规则。没有任何一种投影能做到面积、形状、距离、方向全部不变,只能根据用途决定保留哪些特性、牺牲哪些特性。
这一点经常被新手忽略。很多人直接把经纬度丢进ggplot2,默认画出来的地图在低纬度地区看着还行,一到高纬度就会发现格陵兰岛比非洲还大,这就是墨卡托投影在面积上的夸张效果。所以当你需要展示全球数据时,选择一个合适的投影,比调什么配色都重要。
为了让你直观感受,我用同一个世界边界数据,在R里批量生成了5种投影的对比图:等距矩形投影(Plate Carree)、墨卡托投影(Mercator)、罗宾逊投影(Robinson)、摩尔维德投影(Mollweide)和温克尔三重投影(Winkel Tripel)。
3.2 5种常见投影的风格差异
等距矩形投影(Plate Carree)
这是最简单的一种投影,经线和纬线各自等间距垂直相交,经纬度直接作为平面坐标。好处是完全保形,坐标计算简单,很多卫星影像和全球网格数据直接用这种投影;缺点是面积和形状在高纬度变形明显,不适合做展示型世界地图。它的CRS编号可以直接用EPSG:4326地理坐标系来表示,但在ggplot里如果你不做坐标转换,画出来的就是这个样子。
墨卡托投影(Mercator)
墨卡托是航海时代最重要的投影之一。它属于圆柱投影,保角性能很好,航向线画成直线,因此也是Web地图服务最常用的投影(Web Mercator,EPSG:3857)。缺点也很突出:面积变形随纬度增加急剧增大,两极地区被严重放大。广告里那种“格陵兰和非洲一样大”的误解,几乎都是它造成的。适合导航、局部区域的精细分析,不适合全球数据的面状展示。
罗宾逊投影(Robinson)
罗宾逊投影是一种伪圆柱投影,不是等角也不是等积,但它在面积、形状、距离之间做了折中,整体视觉非常舒服。很多权威地图集和媒体机构都用它来展示全球分布,适合数据密度比较均匀的世界地图。在实际使用中,我经常把“罗宾逊投影”作为全球数据图的默认选项。
摩尔维德投影(Mollweide)
摩尔维德是一种等积伪圆柱投影,面积变形很小,适合展示全球性的人口密度、生物多样性、碳排放等面状数据。它的缺点是高纬度地区形状扭曲较明显,而且图像两侧像被压扁的橄榄球。如果你需要强调“面积对比”,摩尔维德是很严谨的选择。
温克尔三重投影(Winkel Tripel)
温克尔三重投影同样是兼顾多种变形的一种投影,1998年之后被国家地理学会用于世界地图,视觉真实感强。它既不像墨卡托那样夸张放大极地,也不像摩尔维德那样产生明显的橄榄球轮廓。如果要出版一张带经纬网的世界地图,温克尔三重几乎是“不会错”的选择。
3.3 用ggmapcn+ggplot2批量生成对比图
要实现5种投影的对比图,关键代码其实很短。我先把世界地图数据取出来,再逐一设置不同的coord_sf(crs = ...)参数,最后用patchwork包拼图。
library(ggplot2) library(ggmapcn) library(sf) library(patchwork) world_map <- ggmapcn::world_borders() # 5种投影的crs定义 projections <- list( "Plate Carree" = "+proj=longlat +datum=WGS84 +no_defs", "Mercator" = "+proj=merc +datum=WGS84 +no_defs", "Robinson" = "+proj=robin +lon_0=0 +datum=WGS84", "Mollweide" = "+proj=moll +lon_0=0 +datum=WGS84", "Winkel Tripel" = "+proj=wintri +lon_0=0 +datum=WGS84" ) plot_list <- lapply(names(projections), function(proj_name) { ggplot(world_map) + geom_sf(fill = "grey90", color = "grey40", linewidth = 0.2) + coord_sf(crs = projections[[proj_name]]) + ggtitle(proj_name) + theme_void() + theme( plot.title = element_text(size = 12, face = "bold", hjust = 0.5) ) }) combined <- wrap_plots(plot_list, ncol = 2) ggsave("world_projections_compare.png", combined, width = 10, height = 8, dpi = 300)这段代码里,我用了theme_void()把坐标轴、网格等无关元素删掉,只留地图轮廓和标题,这样5种投影的对比更直观。运行之后,你会看到同一份边界在不同投影下的明显差异,这就是投影对视觉传达的真实影响。
在实际工作中,我不会只出图,还会在某些图中添加经纬网。比如罗宾逊投影加经纬网后,能让读者知道全球范围的相对位置。经纬网在ggplot2中有多种方式实现,最简单的办法是用coord_sf(label_graticule = TRUE)或geom_sf_label_graticule(),但我个人更推荐用annotation_spatial等低级接口,因为可定制性更高。初学者先用自带功能即可,等熟练之后再深挖。
4. 从基础图到出版级地图的颜值提升
4.1 主题、配色与标注的细节
真正能放进文章配图的地图,至少要满足3个条件:边界清晰、配色不刺眼、信息层级合理。默认的grey90背景加grey40边界其实已经够用,但如果你想要更精致的风格,可以在这基础上手动调整。
我常用的地图配色方案是:
map_theme <- theme_void() + theme( plot.background = element_rect(fill = "white", color = NA), panel.background = element_rect(fill = "#eaf3f8", color = NA), plot.title = element_text(size = 14, face = "bold", hjust = 0.5), plot.subtitle = element_text(size = 10, hjust = 0.5), plot.margin = margin(10, 10, 10, 10) )这个主题给了海洋一个浅蓝色背景,大陆保持灰色,视觉上更接近传统地图。如果你想展示点数据,比如全球站点分布,就把geom_sf()中的fill留空,使用geom_point()叠加一个散点图层。注意,点数据的坐标需要与你投影的坐标系统保持一致,通常我会先转成sf对象,再用geom_sf()绘制,省去手动投影转换的麻烦。
标注这块容易被忽略。世界地图上的国家名称并不需要全部标注,否则图面会挤成一团。我只在图上标注那些在论文讨论中会被提到的国家或地区。如果需要自动标注,可以用geom_sf_text(),配合aes(label = name),然后通过check_overlap = TRUE来避免文字重叠。实测下来,对于全球图,这个参数还是会产生部分重叠,最好只保留少数标签:
ggplot(world_map) + geom_sf(fill = "grey90", color = "grey40", linewidth = 0.2) + geom_sf_text(aes(label = name), size = 2, check_overlap = TRUE, family = "sans") + coord_sf(crs = "+proj=robin +lon_0=0 +datum=WGS84") + map_theme4.2 添加比例尺与指北针
很多刚用R出图的朋友会跑来问:地图上怎么加比例尺和指北针?ggplot2本身没有内置的简单函数,但借助ggspatial包就能轻松解决。ggspatial::annotation_scale()负责比例尺,ggspatial::annotation_north_arrow()负责指北针。这两个函数是注释型图层,不会影响地图主体数据。
library(ggspatial) ggplot(world_map) + geom_sf(fill = "grey90", color = "grey40", linewidth = 0.2) + coord_sf(crs = "+proj=robin +lon_0=0 +datum=WGS84") + annotation_scale(location = "br", width_hint = 0.2) + annotation_north_arrow(location = "tl", which_north = "true") + map_theme使用比例尺时要注意一个问题:不同的投影下,比例尺的换算关系不一样,尤其是全球尺度,图上距离与实地距离的关系很复杂。annotation_scale()只能提供一个近似的比例尺,更适合区域尺度。对于世界地图,如果期刊没有硬性要求,我通常不加数值比例尺,只画指北针和经纬网,读者通过经纬度也能判断方向和距离。
还有一个细节:指北针的方向。which_north = "true"指北针指向正北,但如果地图投影不是等角投影,正北方向在图上可能不是严格朝向图顶部。这个不影响大多数场合,但严谨的制图要求下需要谨慎处理。
4.3 导出300dpi图片的正确姿势
出版级地图最硬性的指标之一就是分辨率。常规插图至少需要300dpi,如果图片要印刷成大开本,建议600dpi。用ggsave()设置即可:
ggsave( "world_map_final.png", plot = final_map, width = 8, height = 5, dpi = 300, bg = "white" )这里有一个容易被忽略的要点:PNG适合网页和PPT,如果投期刊,最好同时导出PDF矢量图。矢量图不会因缩放而失真,后期排版也更加灵活。用ggsave()把扩展名改成.pdf,它会自动保存为矢量格式:
ggsave("world_map_final.pdf", plot = final_map, width = 8, height = 5, device = cairo_pdf)如果你导出的图要插入Word,也可以导出高分辨率EMF,不过R对EMF的支持相对弱一些,我优先推荐PDF,再在软件里转成位图或直接用排版系统插入。
5. 实战中踩过的坑与解决思路
5.1 地图边界不闭合或出现飞线
第一次用ggplot2画世界地图时,很多人会发现某些国家的边界出现异常的直线,横穿大陆或海洋,尤其在高纬度地区,像有一条线从加拿大一侧连到另一侧。这通常是因为数据中的经度跨过了±180°线,ggplot在连接顶点时直接画了一条“横跨太平洋”的直线。
解决办法一般有两种。一是调整地图数据的裁剪策略,把跨日期变更线的几何对象在合适的地方断开。用sf包时,可以尝试st_wrap_dateline()函数对数据进行处理:
library(sf) world_map <- st_wrap_dateline(world_map, options = c("WRAPDATELINE=YES", "DATELINEPOLICY=RIGHT"))二是把投影中心移到合适的经度,例如当展示亚太地区时,用+lon_0=150的投影,就能把跨线区域移到图形边缘,避免中央飞线。这个方法在很多情况下比处理数据更高效。
5.2 投影范围导致图形空白
换投影后,图形四周可能出现一片空白,特别是摩尔维德、罗宾逊这类投影,边缘是椭圆或曲线,和默认的矩形画布不匹配。这时你需要调整绘图区域或裁掉多余空白。在ggplot2中,通常在coord_sf()里设置xlim和ylim,不过要注意这些参数在投影坐标下使用,不同投影的范围值差别很大。
更省事的办法是用ggmapcn封装好的“按图层计算范围”功能,或者手动用coord_sf(crs = ..., expand = FALSE)来减少空白。我的经验是先画出原图,再用layer_data()提取图形坐标范围,根据结果微调参数。
5.3 中文标签乱码
R语言在Windows上一个老问题就是中文乱码,地图标签尤其严重。若你把国家名写成中文,图中可能会出现一个个方块。这多半是字体匹配问题。解决办法是显式指定中文字体,例如使用Windows自带的“微软雅黑”或Mac里的“PingFang SC”:
theme( text = element_text(family = "Microsoft YaHei") )在geom_sf_text()里也可以单独设置family参数。如果你希望一劳永逸,可以在脚本开头用windowsFonts()注册字体:
windowsFonts(YaHei = windowsFont("Microsoft YaHei"))然后统一使用family = "YaHei"。还有一个容易踩的坑:部分Linux服务器没有中文字体,即便代码对了,最终图片也会缺字。这时要在服务器上安装中文字体包,或者终端绘图时使用无中文的标签,等后期在排版软件里再补。
5.4 出图效率与数据缓存
全球边界数据动辄几十兆。每次重新运行脚本都从零绘制,非常浪费时间,尤其是在反复调整样式阶段。我的习惯是第一次运行后把sf对象缓存为RDS文件,后续直接读取。
saveRDS(world_map, "world_map.rds") world_map <- readRDS("world_map.rds")这样能省去数据加载和拓扑处理的时间。另一个提升效率的小技巧是:如果只是调颜色和主题,不需要重新投影计算,直接修改图层参数即可;如果换了投影,建议先画出没有标签的底图,确认无误后再叠加标签和数据点,否则每次全量重绘会卡顿明显。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方向 |
|---|---|---|
| 安装sf包失败 | 系统缺GDAL/PROJ依赖 | 使用R二进制安装包,或更新系统库 |
| 加载ggmapcn报错 | 版本不兼容或未安装依赖 | 安装最新ggplot2与sf,用remotes重装 |
| 边界出现跨屏直线 | 经度跨±180°线 | 使用st_wrap_dateline()或调整投影中心经度 |
| 中文标签显示为方块 | 字体未指定或缺失 | 注册中文字体,tagret family参数 |
| 导出图片模糊 | dpi不够或压缩过度 | ggsave设置dpi=300以上,导出PDF矢量版 |
| 图例信息太多遮挡地图 | 全球数据点过多 | 使用分面、筛选或小地图放大局部区域 |
这个表是我日常排查问题时的浓缩,你可以直接收藏。平时遇到问题,先对照表里最接近的一条,能省不少搜索时间。
最后再分享一个小经验:不要在一开始就追求“所有投影都要用”,先根据地图用途判断。如果是展示全球相对位置,罗宾逊或温克尔三重最稳妥;如果数据分析中涉及面积对比,摩尔维德更符合科学直觉;如果只是做底图用于导航或匹配Web地图风格,再用墨卡托。每次画完图之后,问自己一句“这个变形会不会误导读者”,你就会发现自己对地图投影的理解越来越深。用R做地图这件事,真正上手之后,你再也不会想回去手动点GIS软件了。