拓冰建站拓冰建站
首页 / 资讯中心 / 正文

R绘制全球样点分布图:底图选型、投影转换与论文级美化

你有没有过这种经历手里攒了一两百个来自全球不同区域的采样点坐标导师或者审稿人一句“补一张样点分布图”你就得从零开始折腾地图。用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中低快速看分布趋势精确度和空间操作能力偏弱rnaturalearthsf110m/50m/10m论文级静态地图首次使用需要下载数据对网络有要求rworldmapSpatialPolygonsDataFrame中低快速绘制国家级别填色图偏老部分函数维护不积极tmapsf/sp均可视数据源而定交互式地图和快速制图起步门槛比ggplot2高一点leaflet浏览器渲染在线瓦片网页交互地图只适合在线环境不适合期刊静态图ggmap在线底图高局部区域卫星背景需要API key且服务条款有使用限制raster/terra自带示例raster视数据而定栅格背景叠加点主要用于气候、环境插值结果展示如果是做全球样点的静态成图rnaturalearth ggplot2 sf这个组合目前最省心。它在论文、基金本子、汇报PPT里都能用风格统一再往上的美化也都在ggplot2体系内不用切换思维。2. 样点数据的整理与坐标基础先把经纬度变成“能用”的空间对象2.1 全球样点的标准表结构画图之前先确认一下数据表够不够规范。我处理过很多“看起来能用”的坐标数据最常见的隐患是表头写得不清楚、经纬度顺序弄反或者混入了缺测值。这里给一个比较稳妥的样点表结构字段示例说明site_idS001样点唯一编号lon120.15经度单位度E为正W为负lat30.28纬度单位度N为正S为负groupForest分组信息比如生态系统类型value23.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编号是4326library(sf) sf_points - st_as_sf(sample_data, coords c(lon, lat), crs 4326)注意这里coords c(lon, lat)是“先经度后纬度”。我在实际中遇到过不止一次有人把纬度放在第一列转换出来的点全都在奇怪的位置画完地图整个分布彻底变形。还有一种情况是样点坐标来自当地投影坐标系比如UTM某个分带。你需要先声明它原来的CRS再转换到WGS84sf_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读取栅格后先检查CRSlibrary(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度左右。这样就得到一张最基础的全球样点图。接下里的优化方向取决于你的数据样点是否分了很多组有没有一个数值需要展示图是要投期刊还是放PPT3.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控制最大点sizerange c(1, 6)比较常用值再大就会互相遮挡反而看不出密度差异。3.3 换投影用Robinson避免高纬度变形WGS84经纬度投影等距圆柱投影在高纬度地区形变明显俄罗斯、加拿大北部和北欧会被拉得很宽。很多期刊和报告更倾向于用Robinson投影或Mollweide投影因为它们能更真实地呈现全球样点的空间格局。换投影非常容易p - p coord_sf(crs projrobin, 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 projstere lon_00 lat_090 lat_ts70)不过极地投影在展示全球样点时并不常用更多用于高纬区域放大图。你只需要知道“换投影就是改一行参数”就够了具体方案根据投稿期刊惯例来定。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一步搞定全球尺度地图通常不需要指北针和比例尺因为整个地球的投影方向已经很清楚。但如果你的样点图截取的是某个区域比如东南亚、非洲南部加上比例尺和指北针会显得更规范。这里推荐ggspatiallibrary(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或直接输出PDFggsave(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 projrobin)。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输出矢量图。这套流程平时跑下来相当稳定偶尔碰到小问题也多数集中在数据清洗和底图下载这两步。如果你在实操中遇到文档里查不到的报错比如某块区域的边界显示异常、底图包更新后函数参数变了欢迎在评论里一起交流这类实际环境里的问题往往比任何标准教程都更有记录价值。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门