你有没有过这种经历:手里攒了一两百个来自全球不同区域的采样点坐标,导师或者审稿人一句“补一张样点分布图”,你就得从零开始折腾地图。用R绘制全球样点的地理信息图,算是空间可视化里比较基础也特别常用的需求,但越是基础的需求,越容易在底图数据、坐标系统、投影方式这些环节上翻车。这篇文章会从底图怎么选开始,带你完整跑通“全球样点分布图”这条链路:数据怎么整理、代码怎么写、图怎么调,以及我在实际项目中踩过的坑和解决思路。无论你是生态学、环境科学背景,还是做生物地理、气候研究的,只要能拿到经纬度坐标,这套方法基本能直接套用。
1. 底图选型:全球地图数据从哪来,比直接画经纬度强在哪
1.1 直接用geom_point画经纬度的问题
很多人拿到经纬度后的第一反应是打开ggplot2,写一句geom_point(aes(x = lon, y = lat)),然后发现图出来了,但国界线、海岸线一概没有,读者根本不知道样点落在哪个大洲。于是大家会去搜索“R 地图数据”,接着看到map_data("world")这类老办法,把底图数据当普通多边形画出来。
map_data("world")能用,但有两个实际问题:第一,它返回的是data.frame,本质是ggplot2比较早期的地图体系,想要用sf提供的投影变换、空间操作就得多绕路;第二,它的几何精度和边界细节一般,做彩色大图或者需要裁切局部区域时会觉得不够用。早期我写论文初稿时就用过这个方案,图的“完成度”始终差一口气,最明显的是北极圈附近的轮廓非常毛糙。
所以现在的项目里我基本不用map_data作为全球样点图底图。这个场景需要的是真正意义上的“空间数据底图”:既能快速读取,又适合在ggplot2里直接绘制,最好还带现成的国境线和湖泊边界。
1.2 rnaturalearth:当前最顺手的全球底图方案
我目前最常用的是rnaturalearth包。它包装的是Natural Earth这个公开地理数据集,提供不同分辨率的全球行政区划、海岸线、河流湖泊数据。关键点是它返回的是sf对象,可以直接被geom_sf()使用,配合sf包做投影变换也特别顺畅。
安装很简单:
install.packages("rnaturalearth") # 如果要用较大比例尺的数据,还需要安装 install.packages("rnaturalearthdata")画第一张底图的代码非常短:
library(ggplot2) library(rnaturalearth) library(sf) world <- ne_countries(scale = "medium", returnclass = "sf") ggplot(data = world) + geom_sf(fill = "grey90", color = "grey50", linewidth = 0.2) + theme_minimal()scale参数控制分辨率:"small"是110m、"medium"是50m、"large"是10m。全球范围的样点图用"small"或"medium"就够了,"large"的数据量会明显增加,除非你要放大到某个区域,否则没必要。
1.3 其他底图方案横向对比
除了rnaturalearth,业界还有一些其他的底图路径,各有各的适用场景。
| 方案 | 返回格式 | 分辨率 | 适合场景 | 需要注意的地方 |
|---|---|---|---|---|
map_data("world") | data.frame | 中低 | 快速看分布趋势 | 精确度和空间操作能力偏弱 |
rnaturalearth | sf | 110m/50m/10m | 论文级静态地图 | 首次使用需要下载数据,对网络有要求 |
rworldmap | SpatialPolygonsDataFrame | 中低 | 快速绘制国家级别填色图 | 偏老,部分函数维护不积极 |
tmap | sf/sp均可 | 视数据源而定 | 交互式地图和快速制图 | 起步门槛比ggplot2高一点 |
leaflet | 浏览器渲染 | 在线瓦片 | 网页交互地图 | 只适合在线环境,不适合期刊静态图 |
ggmap | 在线底图 | 高 | 局部区域卫星背景 | 需要API key,且服务条款有使用限制 |
raster/terra自带示例 | raster | 视数据而定 | 栅格背景+叠加点 | 主要用于气候、环境插值结果展示 |
如果是做全球样点的静态成图,rnaturalearth + ggplot2 + sf这个组合目前最省心。它在论文、基金本子、汇报PPT里都能用,风格统一,再往上的美化也都在ggplot2体系内,不用切换思维。
2. 样点数据的整理与坐标基础:先把经纬度变成“能用”的空间对象
2.1 全球样点的标准表结构
画图之前,先确认一下数据表够不够规范。我处理过很多“看起来能用”的坐标数据,最常见的隐患是表头写得不清楚、经纬度顺序弄反,或者混入了缺测值。这里给一个比较稳妥的样点表结构:
| 字段 | 示例 | 说明 |
|---|---|---|
| site_id | S001 | 样点唯一编号 |
| lon | 120.15 | 经度,单位度,E为正,W为负 |
| lat | 30.28 | 纬度,单位度,N为正,S为负 |
| group | Forest | 分组信息,比如生态系统类型 |
| value | 23.5 | 需要映射到点大小或颜色的数值 |
读取之后先做质量检查:
library(dplyr) sample_data <- read.csv("global_samples.csv", stringsAsFactors = FALSE) |> filter(!is.na(lon), !is.na(lat)) |> filter(lon >= -180 & lon <= 180, lat >= -90 & lat <= 90)这一步看起来基础,但极其重要。经纬度越界通常意味着坐标写反了,或者数据来自某种投影坐标而没有转换。清洗后再进入绘图流程,能省掉后面大量排错时间。
2.2 用sf定义坐标参考系
把经纬度变成真正的“空间对象”,推荐使用sf。它可以把普通数据框转换为点要素,并指定坐标参考系(CRS)。
全球GPS和大多数样点坐标默认都是WGS84坐标系,EPSG编号是4326:
library(sf) sf_points <- st_as_sf(sample_data, coords = c("lon", "lat"), crs = 4326)注意这里coords = c("lon", "lat")是“先经度后纬度”。我在实际中遇到过不止一次,有人把纬度放在第一列,转换出来的点全都在奇怪的位置,画完地图整个分布彻底变形。
还有一种情况是样点坐标来自当地投影坐标系,比如UTM某个分带。你需要先声明它原来的CRS,再转换到WGS84:
sf_points_utm <- st_as_sf(sample_data, coords = c("x", "y"), crs = 32650) # 假设是UTM zone 50N sf_points_wgs84 <- st_transform(sf_points_utm, crs = 4326)转换完成后再取回经纬度,就可以接上后面的ggplot2流程了。实际上只要底图是sf对象、点也是sf对象,你甚至可以直接用geom_sf(data = sf_points)画点,不必手动把经纬度取出来。不过手动维护一个经纬度列,在需要和统计图拼版时更灵活,所以我一般两种方式都会保留。
2.3 叠加环境栅格时的投影一致性问题
全球样点图经常不只是画点,还要叠一层环境背景,比如年均温、降水、植被指数。这时候最常犯的错是“各画各的”:点用WGS84,栅格是另一个投影,结果图上点没有落在该在的位置。
用terra或raster读取栅格后,先检查CRS:
library(terra) env_raster <- rast("wc2.1_10m_bio_1.tif") crs(env_raster, describe = TRUE)如果栅格CRS和点的CRS不一致,就project()转换。对大多数全球尺度图,我会把环境栅格先聚合到0.5度或1度分辨率,再用geom_raster()或geom_tile()画背景,这样既能让地图有信息量,又不会让文件体积变得不可控。
另一个细节:栅格数据往往会有NoData值,不处理干净,图上会出现整片空白或者诡异的色块。我习惯先minmax(env_raster)看看数值范围,再用na.rm = TRUE处理,确保背景色是连续的。
3. 核心绘图:从一张“能看”的全球样点图到“能投稿”的成品图
3.1 第一版:底图加散点
先把最核心的绘图代码跑通。假设样点数据已经整理成sample_data这个数据框,至少包含lon、lat。第一版代码如下:
library(ggplot2) p <- ggplot() + geom_sf(data = world, fill = "grey92", color = "grey50", linewidth = 0.15) + geom_point(data = sample_data, aes(x = lon, y = lat), size = 1.5, alpha = 0.6, color = "#d1495b") + coord_sf(xlim = c(-180, 180), ylim = c(-85, 85), expand = FALSE) + theme_minimal() p这里面coord_sf()是关键。没有它,ggplot2会直接把经纬度当普通平面坐标处理,图的纵横比、比例看起来都别扭;加上它之后,地图会保持经纬度网格的对应关系,裁剪范围也更干净。xlim和ylim设成地球表面的大致范围,能避免南极附近出现一大片空白。如果你不想显示南极,可以把ylim下限定在-60度左右。
这样就得到一张最基础的全球样点图。接下里的优化方向取决于你的数据:样点是否分了很多组?有没有一个数值需要展示?图是要投期刊还是放PPT?
3.2 分组着色、气泡大小和数据驱动美学
如果样点来自不同生态系统类型、不同年份或不同项目,用颜色区分是最直接的方式:
p <- ggplot() + geom_sf(data = world, fill = "grey92", color = "grey50", linewidth = 0.15) + geom_point(data = sample_data, aes(x = lon, y = lat, color = group), size = 1.8, alpha = 0.65) + scale_color_manual(values = c("Forest" = "#1b9e77", "Grassland" = "#d95f02", "Wetland" = "#7570b3")) + coord_sf(xlim = c(-180, 180), ylim = c(-60, 85), expand = FALSE) + theme_minimal() + theme(legend.position = "bottom") p如果你还有一个连续的数值字段,比如物种丰富度、土壤碳含量,可以用size映射成气泡大小:
p <- ggplot() + geom_sf(data = world, fill = "grey92", color = "grey50", linewidth = 0.15) + geom_point(data = sample_data, aes(x = lon, y = lat, size = richness, color = group), alpha = 0.6) + scale_size_continuous(range = c(1, 6), name = "Richness") + scale_color_manual(values = c("Forest" = "#1b9e77", "Grassland" = "#d95f02", "Wetland" = "#7570b3")) + coord_sf(xlim = c(-180, 180), ylim = c(-60, 85), expand = FALSE) + theme_minimal() p两条经验:第一,点的透明度建议设置在0.5到0.7之间。全球样点经常会出现欧洲西部、东亚这种样点密集的区域,完全实心的点会把底图盖得严严实实。第二,气泡图要通过scale_size_continuous控制最大点size,range = c(1, 6)比较常用,值再大就会互相遮挡,反而看不出密度差异。
3.3 换投影:用Robinson避免高纬度变形
WGS84经纬度投影(等距圆柱投影)在高纬度地区形变明显,俄罗斯、加拿大北部和北欧会被拉得很宽。很多期刊和报告更倾向于用Robinson投影或Mollweide投影,因为它们能更真实地呈现全球样点的空间格局。
换投影非常容易:
p <- p + coord_sf(crs = "+proj=robin", xlim = c(-180, 180), ylim = c(-60, 85), expand = FALSE)此时geom_point()里的lon、lat仍保持不变,coord_sf()会负责把它们投影到目标CRS下。需要注意,coord_sf()的xlim、ylim在非经纬度投影下可能会表现出不同的边界效果,具体数值可能需要微调。我可以先把expand = FALSE去掉看看整体范围,再按需要收紧。
如果你的数据集中在热带或南北半球中低纬度,Robinson投影是特别稳妥的选择;如果研究重点是两极,则要考虑极方位投影:
p + coord_sf(crs = "+proj=stere +lon_0=0 +lat_0=90 +lat_ts=70")不过极地投影在展示全球样点时并不常用,更多用于高纬区域放大图。你只需要知道“换投影就是改一行参数”就够了,具体方案根据投稿期刊惯例来定。
4. 论文级细节:配色、图例、比例尺、指北针与高分辨率导出
4.1 学术配色别乱来:常用方案与透明度
全球样点分布图的读者通常是评审专家或合作者,配色直接影响图的专业度。我强烈建议采用色盲友好的配色方案。最省事的两个选择:
- 离散分组变量:
RColorBrewer的Set2、Dark2,或者直接手动指定R中常见的</>颜色向量。 - 连续数值变量:
viridis系列,或者scale_color_gradient2()做发散色阶。
我的常用做法是手动命名颜色,因为实际图例很少只有一个分组,而包默认配色往往对“图例颜色与分组对应”这件事不够直观。
scale_color_manual( values = c("Forest" = "#1b9e77", "Grassland" = "#d95f02", "Wetland" = "#7570b3", "Cropland" = "#e7298a") )透明度的处理也属于配色的一部分。对散点地图,透明度0.6是起点值;如果样点数量超过500,建议再降到0.4,同时把点大小调小到1.2左右。否则阴影叠影会让人误判样点密度。
4.2 图例、网格线与主题细节
图例位置默认在右侧,但对全球地图来说,右侧会占掉不少纵向空间,尤其是图名比较长的时候。我习惯把图例放到底部:
theme_minimal() + theme(legend.position = "bottom", legend.box = "horizontal", panel.grid.major = element_line(color = "grey80", linewidth = 0.2))coord_sf默认会画出经纬网格线,如果觉得网格线太突出,就把它们的颜色调浅一点。有些期刊要求地图必须带经纬度刻度,这种浅灰色网格既能满足要求,又不会抢样点的视觉重心。
空间数据要呈现比例感,地图的空白区域需要控制一下。用coord_sf(xlim = ..., ylim = ...)把视野框到样点实际分布的范围内,尽量不要让整个太平洋空在那里,除非你的研究尺度真的需要展示全球全貌。
4.3 比例尺和指北针用ggspatial一步搞定
全球尺度地图通常不需要指北针和比例尺,因为整个地球的投影方向已经很清楚。但如果你的样点图截取的是某个区域,比如东南亚、非洲南部,加上比例尺和指北针会显得更规范。
这里推荐ggspatial:
library(ggspatial) p + annotation_scale(location = "bl", width_hint = 0.25) + annotation_north_arrow(location = "tl", which_north = "true", style = north_arrow_fancy_orienteering)需要提醒一下:在非等距投影(比如Robinson)下,比例尺的长度会因纬度变化而失真,所以我把比例尺主要用在局部区域图上。全球样点图如果真的需要比例尺,建议在图注里说明投影信息。
图片输出方面,如果要投期刊,推荐导出高分辨率PNG或直接输出PDF:
ggsave("global_samples.png", p, width = 10, height = 6, dpi = 300, bg = "white") cairo_pdf("global_samples.pdf", width = 10, height = 6) print(p) dev.off()cairo_pdf在macOS和Linux上都比默认的PDF设备更稳定,字体嵌入也更规范。投稿时如果期刊要求矢量图,PDF是首选;如果只是放在Word里,300dpi的PNG完全够用。
5. 实测中的翻车记录:底图下载、点重叠、字体和内存问题
5.1 rnaturalearth下载失败,别急着怪网络
第一次使用ne_countries()时,R会在后台下载Natural Earth数据并缓存到本地目录。这里最常见的报错是超时,尤其在公司网络或校园网环境下容易发生。
我的处理方式是先设置下载超时时间:
options(timeout = 300) world <- ne_countries(scale = "medium", returnclass = "sf")如果仍然失败,可以手动从Natural Earth官网下载对应分辨率的海陆数据,再用sf::read_sf()读取本地文件。这样最可靠,而且之后每次绘图都不需要再联网,跑脚本的速度也会快一点。
5.2 几百个点堆在一起看不清:透明度和分箱方案
当样点数量超过1000,或者大量采样点集中在个别热点区域,散点图会出现严重的重叠。这时有两个方向:
第一种思路是保持散点,但降低透明度、缩小点尺寸,让堆叠区域通过颜色深度呈现密度感。第二种思路是放弃散点,改用网格计数或hexbin分箱:
ggplot() + geom_sf(data = world, fill = "grey92", color = "grey50", linewidth = 0.15) + geom_bin2d(data = sample_data, aes(x = lon, y = lat), bins = 60) + scale_fill_viridis_c(option = "C", name = "Sample count") + coord_sf(xlim = c(-180, 180), ylim = c(-60, 85), expand = FALSE)全球样点密度图用geom_bin2d会得到非常直观的“热点区域”视觉效果。缺点是会丢失单个点的精确位置,所以适合做“全球采样强度”展示,而不是“每个样点的具体分布”。
如果既想保留散点又想提升渲染性能,可以尝试scattermore包。它能把大量点极快地画出来,底层实现比较高效,适合上万个点。
5.3 PDF导出字体和中文字体丢失
如果图例或标题里包含中文,导出PDF再插入Word或AI时,经常出现字体丢失或乱码。原因是R默认PDF设备的字体支持有限。
我的建议是双速方案:正式投稿的图尽量用英文标签,这是学术圈的通行做法。如果确实需要中文字体,建议用showtext包把字体嵌入:
library(showtext) font_add("SimHei", regular = "simhei.ttf") showtext_auto() # 然后在ggplot中设置 theme(text = element_text(family = "SimHei"))showtext对中文字体的嵌入支持比系统自带PDF设备好很多,导出前先开启showtext_auto(),再ggsave即可。还有一个小提醒:Windows下需要先确认字体文件路径,否则函数会找不到字体。
5.4 大数据量的内存与渲染优化
全球范围的高分辨率底图本身就不小,scale = "large"的ne_countries()数据量能让ggplot在渲染时出现明显的卡顿。如果是放在服务器上跑脚本,问题不大;但本地笔记本上做探索性分析,建议始终用scale = "small"或"medium",等最终出图时再考虑高分辨率。
另外,最终导出时如果PNG尺寸很大,ggplot的内存占用会非常夸张。我见过一个项目里连续导出几张600dpi的全球地图后,RStudio直接卡死。这时候可以分步执行:先保存一个中间RDS文件保存绘图数据,再在新会话里读取并渲染。
saveRDS(sample_data, "sample_data.rds") saveRDS(world, "world.rds")这样可以避免每次跑图前都重新读取大文件、重新连接数据库。
6. 扩展玩法:分面地图、插图拼接与组合图版
6.1 facet_wrap分面:按分组或时期拆图
如果样点需要按年份、生态系统类型或采样项目分别展示,facet_wrap()非常高效。它会把每个分组各画一张全球地图,同时保持相同的坐标范围,方便对比空间格局:
p_facet <- ggplot() + geom_sf(data = world, fill = "grey92", color = "grey50", linewidth = 0.15) + geom_point(data = sample_data, aes(x = lon, y = lat, color = group), size = 1.2, alpha = 0.6) + facet_wrap(~group, ncol = 2) + coord_sf(xlim = c(-180, 180), ylim = c(-60, 85), expand = FALSE) + theme_void() + theme(legend.position = "none") p_facet分面时theme_void()特别好用,它会把每个小图的坐标轴、网格线隐藏,只保留地图本身,版面干净很多。唯一要注意的是分面标题默认排在小图的顶部,如果分组名很长,可以考虑用labeller调整一下标签,否则会挤压地图区域。
6.2 全局小地图inset:用patchwork拼接
有些论文需要在主图旁边放一张全球小地图,用来指示研究区在全球的位置。这个需求可以直接用patchwork完成。
先画出主图和inset小图:
library(patchwork) inset_map <- ggplot(data = world) + geom_sf(fill = "grey85", color = NA) + annotate("rect", xmin = 70, xmax = 140, ymin = 15, ymax = 55, fill = NA, color = "#d1495b", linewidth = 0.6) + theme_void() p_main + inset_element(inset_map, left = 0.7, bottom = 0.7, right = 1, top = 1)inset_element()的位置参数是相对整个画布的,四个值分别对应左、下、右、上。通过这个方式可以把小地图嵌在主图右上角或左下角。实际使用中,inset地图也可以放在主图区域的空白海域位置,比如南太平洋区域,这样不会遮挡样点。
inset小地图的投影最好和主图保持一致,否则红色选区框和实际位置的对应关系会奇怪。如果主图使用了Robinson投影,inset也用coord_sf(crs = "+proj=robin")。
6.3 与α多样性等统计图拼版:地图+数据图的组合版式
生态学论文里特别常见的版式是“地图 + 统计图”的组合:上方是样点分布地图,下方是α多样性指数、箱线图或趋势曲线。这样做的好处是读者一眼就能把空间分布和统计结果对应起来。
library(patchwork) p_alpha <- ggplot(alpha_data, aes(x = group, y = shannon)) + geom_boxplot(aes(fill = group), alpha = 0.6) + scale_fill_manual(values = c("Forest" = "#1b9e77", "Grassland" = "#d95f02", "Wetland" = "#7570b3")) + theme_minimal() + theme(legend.position = "none") p_map + p_alpha + plot_layout(widths = c(2, 1))除了箱线图,也可以用稀疏曲线、β多样性排序图。patchwork的优势是能灵活控制每张图的宽度比例和排列顺序,调整起来非常直观。我习惯先把地图画得大一些,因为它承载的“空间位置”信息需要足够的展示空间,统计图放在旁边作为补充说明即可。
如果是多行多列的复杂组合图,plot_layout(nrow = 2, heights = c(3, 2))可以控制行高,基本能覆盖期刊Figure的绝大多数排版需求。组合图导出时记得用ggsave(..., width = 12, height = 9)这类较大尺寸,保证小图内部的文字不会因为过度压缩而失真。
我做全球样点图这两年,最常用的一套流程就是:rnaturalearth拿底图、sf定义坐标、ggplot2出图、ggspatial加比例尺指北针、最后用cairo_pdf输出矢量图。这套流程平时跑下来相当稳定,偶尔碰到小问题,也多数集中在数据清洗和底图下载这两步。如果你在实操中遇到文档里查不到的报错,比如某块区域的边界显示异常、底图包更新后函数参数变了,欢迎在评论里一起交流,这类实际环境里的问题,往往比任何标准教程都更有记录价值。