简介Rstudio地图绘制配套代码包面向需要制作省级或区域专题地图的数据分析师与科研人员用于解决R语言空间可视化的常见难题。代码围绕矢量与栅格数据处理、坐标系设置、底图导入及数据填充展开完整演示借助ggplot2与ggspatial绘制中国省级地图的流程并覆盖分组分段、图例调整、比例尺与指北针添加等实用细节。压缩包共6个文件以3个R脚本为主辅以Markdown说明文档与项目配置文件整体仅7KB轻量规范可直接对照代码学习或修改复用。目前已有64人学习下载适合想快速掌握Rstudio批量制图、减少重复踩坑的入门到进阶用户。代码包另附循环批量制图实现思路便于一次生成多幅区域地图提升出图效率。1. 一张看似空白的地图画布问题在哪里在 RStudio 里画地图跟画折线图完全是两种体验。你可能会以为把 shapefile 读进来再用 ggplot 加一个几何图层地图就能直接出来但我第一次这么做时画布上什么都没有连边界线都没出现。问题几乎都不在绘图函数本身而在于手里的数据到底是“地图”还是“普通表格”坐标系有没有定义几何类型对不对合并字段名能不能对上这些前置问题不解决后面全是做无用功。这篇教程就围绕这几件事展开先用 sf 包读入边界数据并理清坐标参考系EPSG再用 ggplot2 和 leaflet 分别画出静态与交互式地图。代码都可以直接复制到 RStudio 里跑参数含义和修改方向我会同步说明。适合正在做区域分析、门店选址、用户地理分布这类工作的数据分析从业者也适合刚接触空间数据、想完整走一遍流程的 R 用户。2. 把地图当数据库来读sf 对象与坐标参考系入门2.1 为什么 sf 是绘图前必须先搞懂的一层早年的 R 生态里空间数据主要靠 sp 包管理它在那个年代解决了“有没有”的问题但对象结构跟 tidyverse 的数据框思维差得很远字段操作要专门函数很不顺手。sf 包的出现把空间数据拉回到了普通表格的轨道上一个 sf 对象本质上就是一个 data.frame只不过多了两个东西——一个几何列通常叫 geometry和一个空间参考信息。几何列里存的不是简单数字而是一个个要素及其几何类型多点、线、多边形、多多边形。一个省级区划文件每一行对应一个省geometry 那一列存的就是这个省的边界多边形。正因为它是 data.frame 的子类你可以直接用 dplyr 做筛选、聚合、连接操作完之后几何信息还会跟着走。这个设计让地图绘制的整个流程都顺了先当普通数据处理最后再用绘图函数把它画出来。还有一点要在动手前明确你读进来的地图数据不等于直接能画。很多 shapefile 文件是几年前从测绘数据转出来的坐标单位可能是经纬度也可能是投影后的米甚至源文件里根本没有写清楚坐标系统。如果你不去确认坐标系就绘图画出来的地图可能是个歪的、偏的甚至缩成一条线。所以 sf 不仅是读数据的工具也是你排查地图问题的第一把尺子。2.2 读入矢量数据st_read 的最小可运行命令最常见的矢量数据格式是 Shapefile它其实是一个由 .shp、.dbf、.shx、.prj 等文件组成的文件组读取时必须指向 .shp 主文件。sf 包里的 st_read 函数负责这件事最小的命令是这样library(sf) # 读入省级行政区划边界 province_sf - st_read( dsn data/china_province.shp, quiet TRUE )dsn 参数可以是文件路径也可以是目录名指向目录时st_read 会尝试自动识别目录里的矢量文件。加上 quiet TRUE 是让它在读入时不把完整的要素清单和属性字段说明打印出来省得早期调试时控制台被刷屏。如果你第一次跑不确定路径对不对可以去掉 quiet它会打印每个图层的要素数量、字段类型和坐标系对排查非常有用。读进来之后建议立刻做三件事看结构、看边界值、看坐标系。# 查看对象类型和字段 class(province_sf) names(province_sf) # 看几何范围和要素数 st_bbox(province_sf) nrow(province_sf) # 看坐标系定义 st_crs(province_sf)st_bbox 会返回 xmin、ymin、xmax、ymax 四个值。如果这四个数在 -180 到 180 和 -90 到 90 之间说明数据用的是经纬度坐标如果数值是百万级别那多半是投影坐标系。这一步判断比盯着任何文档都管用。除了 Shapefile日常还会遇到 GeoJSON 和 CSV 转坐标的情况。GeoJSON 用 st_read 直接读CSV 则需要先用普通数据框读入再通过 st_as_sf 把经纬度列变成空间点# 读取 GeoJSON city_geo - st_read(data/city_points.geojson, quiet TRUE) # 读取 CSV 并转成空间点 cities_df - read.csv(data/cities.csv) cities_sf - st_as_sf( cities_df, coords c(lng, lat), crs 4326 )st_as_sf 的 coords 参数写的是经纬度列名crs 直接指定 4326WGS84因为 CSV 里的经纬度通常就是 WGS84 坐标系。这一步看着简单却能避免后面一大堆对齐问题没有坐标参考系的空间对象在合并、绘制时会被 R 当作普通图形结果自然对不上。2.3 坐标系与投影EPSG 4326 和 3857 的差别及转换坐标系是地图绘制里最容易翻车的环节。日常听到的“经纬度坐标”正式说法是 WGS84 地理坐标系EPSG 编号 4326单位是度。它描述的是地球表面上的点适合存储和交换但不适合直接做距离运算和面积计算——你在高纬度地区画一个看似方形的格网它的真实面积跟低纬度同样大小的格网差很多。另一种常见坐标系是 Web 墨卡托投影EPSG 编号 3857单位是米。在线底图服务比如 OpenStreetMap 的瓦片用的就是它。它的特点是让地图看起来是“平的”纬度越高放大感越强越靠北的地方被拉伸得越大。两个坐标系之间需要转换转换动作本身并不会改你的数据内容只是换一种方式描述同一个地理位置。在 R 里转换用 st_transform而给数据“补登记”坐标系用 st_set_crs两者千万别搞混# 数据本来没有坐标系定义但坐标确实像经纬度 st_crs(province_sf) # 返回 NA province_sf - st_set_crs(province_sf, 4326) # 已确认是 WGS84转成 Web 墨卡托 province_3857 - st_transform(province_sf, crs 3857) # 想验证转换是否成功 st_crs(province_3857)$epsg # 应输出 3857st_set_crs 只修改坐标系标签不重算坐标值st_transform 才会真正把经纬度重投影成米。如果你读到的数据本来就有 .prj 文件st_read 会自动读取坐标系一般不需要手动设置。真正常见的翻车场景是从某些爬虫脚本或旧系统拿到的数据坐标列也叫 lng 和 lat但实际上数据已经被某种投影处理过你却直接按 4326 设置画出来就偏到别处去了。遇到这种先看范围再决定是转还是设。3. 用 ggplot2 sf 画第一张静态地图图层、配色与标注3.1 图层结构从边界线到多个几何对象的叠加ggplot2 从 3.0 版本开始支持 sf 对象核心就是 geom_sf 这个图层函数。画地图的思路跟画折线图一样先告诉 ggplot 数据在哪然后用 geom 描述图形。区别在于geom_sf 里的数据自带几何信息你不需要像画散点图那样指定 x 和 y。第一张地图先只画边界。用一个浅灰填充加深灰边线确认数据本身是完好的library(ggplot2) ggplot(province_sf) geom_sf(fill #efedea, color #999999, linewidth 0.3) theme_void()theme_void 会把背景、坐标轴、网格全部清掉只留地图本身这是出地图最常见的主题选择。linewidth 参数控制边界线粗细0.3 到 0.6 是省级边界的常用范围fill 和 color 分别是填充色和边线色。如果你有多个图层要叠比如底图边界加城市点就把多个 geom_sf 放进同一个 ggplot 调用里ggplot() geom_sf(data province_sf, fill #efedea, color #999999) geom_sf(data cities_sf, color #d73027, size 2) theme_void()注意第一个 geom_sf 里没有再写 aes因为这里只是画固定样式的底图城市点数据如果本身带有经纬度坐标会按照它的几何列自动落在正确位置。多个图层叠加时底图在前、点线要素在后这个顺序不要反了否则边界线会把城市点盖住。3.2 与业务数据合并从行政边界到分布热力图边界数据本身只有地理轮廓真正的地图分析需要把业务数据销量、人口、客户数合并到对应区域上。合并的核心是字段匹配。假设你有一份各省销量数据字段名叫“省”值写成“北京”“上海”这种简称而边界数据里的 NAME 字段写的是“北京市”“上海市”直接合并就会大批量失败。正确做法是先看清边界数据里的字段值长什么样再统一口径library(dplyr) # 模拟业务数据 sales_data - data.frame( province c(北京, 上海, 广东, 浙江, 江苏), sales c(1200, 980, 870, 650, 590), stringsAsFactors FALSE ) # 先看一眼边界数据的行政名称字段 head(province_sf$NAME)如果 NAME 是“北京市”这样的全称就把它处理成跟业务数据一致的简写或者反过来。处理方式可以灵活关键是保证两边能对上# 方案一截掉重复的行政后缀 province_sf$NAME_short - gsub(省|市|自治区|壮族自治区|回族自治区|维吾尔自治区, , province_sf$NAME) # 方案二给业务数据加一列全称 sales_data$province_full - paste0(sales_data$province, 省) # 我一般用方案一边界数据统一处理一次后面所有业务表都受益处理完之后再合并map_data - province_sf %% left_join(sales_data, by c(NAME_short province)) # 合并结果里会出现大量 NA表示该省没有业务数据 sum(is.na(map_data$sales))left_join 的含义是保留左边边界数据的全部行右边的业务数据按匹配字段拼进来。匹配不上的省sales 字段会变成 NA。这一步如果是 0 个 NA反而要警惕也许你的业务数据覆盖了全国也许只是合并字段恰好同名字段但含义不同最好抽查几个省看看数值对不对。绘图时直接用 aes(fill sales)缺失值默认显示为灰色或透明后面我会讲怎么单独处理。3.3 出图细节色带、标题和边框合并完成后的数据里既有几何信息又有数值字段直接用 geom_sf 上色。这里有个细节scale_fill_distiller 是 ggplot2 里专门给连续变量用的色带函数它直接调用 ColorBrewer 配色方案比手动列颜色靠谱得多因为你不用考虑色相过渡是否自然ggplot(map_data) geom_sf(aes(fill sales), color white, linewidth 0.2) scale_fill_distiller( palette RdYlBu, na.value #f0f0f0, direction 1 ) theme_minimal() labs( title 某产品各省销售分布, subtitle 数据周期2024 年 1 月至 6 月, fill 销售额万元 )palette 指定配色方案RdYlBu 是红黄蓝三色渐变数值越大越红、越小越蓝地图用户最熟悉的一套语义。direction 1 表示颜色从低到高按方案正序排列如果你发现高值反而变成了蓝色就是因为这个参数被设成了 -1。na.value 处理缺失省份的颜色用浅灰 #f0f0f0 是最低调的选择不会喧宾夺主。如果想在地图上标出各省名字可以用 st_centroid 计算多边形中心点再用 geom_text 加文字标签。要注意的是中心点坐标需要从 sf 对象里抽出来# 计算中心点并提取坐标 centers - st_centroid(map_data) %% mutate( lng st_coordinates(.)[, 1], lat st_coordinates(.)[, 2] ) ggplot(map_data) geom_sf(aes(fill sales), color white, linewidth 0.2) geom_text( data centers, aes(x lng, y lat, label NAME_short), size 3, color #333333 ) scale_fill_distiller(palette RdYlBu, na.value #f0f0f0) theme_void()这里最需要注意的是ggplot 主图层的数据是 map_data但 geom_text 用的是另一个数据框 centers所以必须在 geom_text 里用 data 单独指定。st_centroid 对某些形状特殊的省可能会把中心点算到省界之外尤其是那些狭长的省份这种情况最好是人工微调坐标或者放弃标注。4. 把地图变成交互页面用 leaflet 处理省级与点数据4.1 从静态到动态leaflet 的图层模型ggplot2 画的是静态图片适合放进报告和 PPT。但当你要给业务同事做演示让他们自己缩放、悬停、点击查看数据静态图就不够用了。leaflet 包是 R 里最常用的交互式地图方案它在底层封装了 Leaflet 的 JavaScript 库R 端渲染好之后在 RStudio 的 Viewer 面板里就能直接拖拽缩放也可以导出成独立 HTML 文件。leaflet 的使用逻辑和 ggplot2 完全不同。ggplot2 是“数据到几何图层”的思维而 leaflet 是“底图瓦片 上层叠加要素”的思维。你首先要加一层底图通常是在线瓦片服务library(leaflet) # 最简单的一张交互底图 leaflet() %% addTiles() %% setView(lng 105, lat 36, zoom 4)leaflet() 生成一个空地图对象addTiles() 添加 OpenStreetMap 的标准瓦片。setView 设置打开页面时的初始中心和缩放级别lng 105、lat 36 差不多是中国的几何中心位置zoom 4 能看到全国。如果不调用 setViewleaflet 会根据你后续添加的数据范围自动推算视野但手动设置可以避免页面打开时视野偏到海洋里去。把 sf 对象传给 leaflet 时leaflet 会自动识别几何类型并按要素数量决定用 addPolygons 还是 addCircles。如果你直接map_data_4326 - st_transform(map_data, crs 4326) leaflet(data map_data_4326) %% addTiles()会发现省级多边形已经被画出来了只是没有填充色只有默认的边线。这一步有个我踩过多次的点leaflet 要求数据必须是经纬度坐标系EPSG:4326如果你拿 WGS84 经纬度的边界数据直接传进去没问题但如果数据是 3857 或其他投影坐标需要先 st_transform 转回 4326否则图形会投影到错误的位置。4.2 用 addPolygons 做分色地图颜色映射与弹窗纯边线没有信息量我们要做的是把业务数据映射成颜色填充。leaflet 里做这件事需要一个颜色函数colorNumeric 接收一个调色板和一个数值范围返回一个把具体数值映射成十六进制颜色码的函数。# 生成颜色映射函数 pal - colorNumeric( palette RdYlBu, domain map_data_4326$sales, na.color #f0f0f0 ) # 画分色多边形 leaflet(data map_data_4326) %% addTiles() %% addPolygons( fillColor ~pal(sales), fillOpacity 0.7, color #666666, weight 1, popup ~paste0(NAME_short, , sales, 万), label ~NAME_short )fillColor 接受颜色函数或颜色向量。这里用 ~pal(sales)波浪号开头的写法是 leaflet 的公式语法表示在数据环境中计算相当于函数式的写法。color、weight 控制边线颜色和粗细fillOpacity 控制填充透明度0.6 到 0.8 之间既能让色块醒目又不至于把底图上的地名完全盖住。popup 是点击弹出的 HTML 内容label 是悬停时的提示文本。新建 map 用颜色图例方便读者看数。addLegend 的用法leaflet(data map_data_4326) %% addTiles() %% addPolygons( fillColor ~pal(sales), fillOpacity 0.7, color #666666, weight 1, popup ~paste0(NAME_short, , sales, 万) ) %% addLegend( bottomleft, pal pal, values ~sales, title 销售额万 )位置参数写在第一个参数里可以是 bottomleft bottomright topright topleft 四个值。图例的数值范围由 values 自动计算跟前面 colorNumeric 的 domain 对应起来就不会出现图例和地图颜色不一致的情况。除了多边形点数据也很常用。比如你在一个城市里有几十家门店或客户点可以用 addCircleMarkers 把它们标在地图上store_data - data.frame( lon c(113.12, 104.06, 120.15, 116.40), lat c(23.02, 30.67, 30.28, 39.90), name c(广州中心店, 成都西二环店, 杭州湖滨店, 北京东直门店), sales_quarter c(320, 285, 300, 410), stringsAsFactors FALSE ) leaflet(store_data) %% addTiles() %% addCircleMarkers( lng ~lon, lat ~lat, radius ~sqrt(sales_quarter / 10), popup ~paste0(name, br本季销售额, sales_quarter), color #d73027, fillOpacity 0.8 )radius 用平方根转换来控制圆圈大小是常见做法因为直接按销售额线性映射最大的点和最小的点差距会大到不协调。addCircleMarkers 的 lng 和 lat 参数直接指向原始数据框的列名不需要再构建 sf 对象这是点数据相对便捷的原因。leaflet 地图可以保存为 HTML 独立文件用 htmlwidgets 包的 saveWidget 就行map_obj - leaflet(data map_data_4326) %% addTiles() %% addPolygons(fillColor ~pal(sales)) htmlwidgets::saveWidget(map_obj, file output/sales_map.html)拿到 HTML 文件发给任何人用浏览器打开就能看不需要对方安装 R也不需要联网之外的额外环境。日常方案里这个文件就是我交付演示的主要形式。5. RStudio 地图绘制的 5 个高频坑从乱码到不匹配5.1 现象一地图整体偏离画出来在某个角落现象读入 shapefile 后直接绘图地图出现在了画布右下角或某个莫名其妙的位置边界严重变形或者底图瓦片和你的数据完全对不上。原因坐标系没有定义或定义错误。st_read 读入的文件如果没有 .prj 文件st_crs 会返回 NA此时坐标值本身仍然是经纬度但 R 不知道绘图时按默认直角坐标处理结果自然全部偏移。解决先看 st_bbox 的范围判断数据单位再决定用 st_set_crs 还是 st_transform。st_bbox(province_sf) # 数值在 -180~180、-90~90 区间 - 经纬度数据可能缺坐标系定义 province_sf - st_set_crs(province_sf, 4326) # 数值在几百万级别 - 投影坐标需要查清楚原始投影编号 province_sf - st_transform(province_sf, crs 4326)如果你能确认数据的原始投影是某种通用墨卡托用 st_transform 转成 4326如果你只是猜测最好不要盲转先找到数据说明文件当年的元数据再动手。5.2 现象二中文文字全部变成方块现象ggplot2 画出的地图上省的名称、标题和图例全部显示为空心方块图形本身没有问题。原因R 在输出图形时默认找不到对应中文的字体或找不到支持中文的字体路径。尤其是在 Linux 服务器上跑的 R系统汉字字体缺失很常见。解决确认系统里有没有中文字体。Ubuntu 下常用 Noto Sans CJKmacOS 用 PingFang SCWindows 用 Microsoft YaHei。然后在 theme 里指定字族ggplot(map_data) geom_sf(aes(fill sales)) theme_minimal() theme( text element_text(family Noto Sans CJK SC) )对于写在 plot 里的中文标注这个办法足够。如果你连系统层面都没有中文字体可以用 showtext 包配合一个字体文件解决它能把指定字体嵌入到 R 输出的图中library(showtext) font_add(my_cn_font, fonts/simhei.ttf) showtext_auto() # 之后照常绘图text 里写 family my_cn_font 即可showtext_auto() 开启之后R 会拦截图形设备并做字体渲染对日常出图很有用。它的坑在于某些版本的 ggplot 里showtext 会把图例和标题字体也一并替换注意检查最终 PDF 或 PNG 效果。5.3 现象三多边形数量巨大绘图时间久到离谱现象读完一张全国性边界数据map 操作要好几分钟尤其是 ggplot2 作图时整个绘图窗口卡很久内存占用一路飙升。原因边界数据精度过高。一套全国省市边界可能包含几十万个多边形顶点ggplot2 在绘制之前会把所有 geometry 拆到数据框里逐点渲染顶点越多越吃力。解决先做几何简化再绘图。sf 里用 st_simplify但有个细节必须注意dTolerance 的单位跟当前坐标系的单位一致所以如果是经纬度数据dTolerance 的单位是度不好控制简化力度。我通常先转成投影坐标3857 或等面积投影用米为单位简化再转回 4326# 投影后按米简化再转回经纬度 map_simple - map_data %% st_transform(3857) %% st_simplify(dTolerance 500, preserveTopology TRUE) %% st_transform(4326)preserveTopology TRUE 防止简化过程产生拓扑错误比如多边形出现自相交或缝隙。500 米是一个相对保守的容差值在省份级别的地图上视觉上几乎无感如果还是卡可以逐步增大到 1000、2000 试。简化之后检查一次要素数量和图形效果确认边界没有明显失真。5.4 现象四合并后大片区域没有数据全是 NA现象left_join 之后地图上大片空白只有一个或几个省有颜色其他全是灰色。原因匹配字段值不一致。最常见的三种情况是一个是“北京”一个是“北京市”一个字段是字符串一个是因子且水平不同或者一个字段里带了空格、全角字符。dplyr 的 join 按值精确匹配任何一边多了个空格都匹配不上。解决合并前先检查字段唯一值再处理。# 看两边的唯一值差集 unique(map_data$NAME_short) unique(sales_data$province) # 去掉空格和全角空格 library(stringr) sales_data$province - str_trim(sales_data$province) map_data$NAME_short - str_trim(map_data$NAME_short) # 重新合并 map_data - province_sf %% left_join(sales_data, by c(NAME_short province))如果两边是乱码或者编码不一致还需要先解决读取时的编码问题。Shapefile 属性表是 DBF 格式很多中文字段在生成时用的是 GBK 编码R 默认按 UTF-8 读可能会读出乱码。st_read 里可以指定编码province_sf - st_read( data/china_province.shp, options ENCODINGGBK, quiet TRUE )如果读出来依然乱码可以试 ENCODINGGB18030。这事跟文件生成方的环境相关不存在一个万能编码但 GBK 和 GB18030 是国内 Shapefile 最常碰到的两种。5.5 现象五地图画出来了但填充色全部同一种颜色现象geom_sf(aes(fill sales)) 写完整个地图只有一个色块没有任何颜色深浅差异。原因sales 字段在合并后变成了字符型或逻辑型ggplot2 按离散变量处理或者是合并字段名写错地图里 fill 进去的其实是某个常量列。解决先确认字段类型再决定用连续色带还是离散色带。class(map_data$sales) # 应该是 numeric 或 integer # 如果是 character先转数值 map_data$sales - as.numeric(map_data$sales) # 如果是连续变量用 scale_fill_distiller # 如果是分类变量用 scale_fill_manual还有一个更隐蔽的场景你在地图数据上做了 group_by 忘了 ungroup导致合并后的 geometry 被折叠成了多个 MULTIPOLYGON 的分组对象这时候 ggplot 里的 aes(fill sales) 实际是在每个分组里只用了第一个值。解决办法是合并之后不要对 sf 对象做聚合操作或者聚合后记得用 st_union 把几何合并再填充但这就涉及空间聚合的完整逻辑了日常做展示地图没必要走这么深。6. 收尾技巧用三行命令自查数据再定制一套地图主题最后分享两个我每次做地图都会用的固定动作。第一个是数据自查三行连招在地图绘制前必须跑一遍能挡掉大多数翻车事故。第一步看几何类型是否正常第二步看坐标范围是否符合预期第三步看字段有没有缺失值# 自查几何类型分布 table(st_geometry_type(map_data)) # 自查坐标边界 st_bbox(map_data) # 自查缺失值数量 colSums(is.na(st_drop_geometry(map_data)))st_drop_geometry 的作用是把 sf 对象转成普通数据框方便处理非几何字段这样 is.na 检查才不会把 geometry 列也卷进来。这套自查跑完没有异常再开始画图基本不会出现前面章节里的那些大坑。第二个动作是封装一个可复用的地图主题。项目里地图往往要批量出每张图的背景、图例位置、边线粗细如果都要手写一遍很容易前后不一致。我会写一个函数像给项目定模板一样固定下来theme_map - function(base_size 12) { theme_void(base_size base_size) theme( plot.title element_text( face bold, hjust 0.5, size base_size 4 ), plot.subtitle element_text( hjust 0.5, size base_size - 2, color #555555 ), legend.position bottom, legend.key.width unit(1.2, cm), legend.title element_text(size base_size - 2), panel.border element_blank() ) }封装好之后每张地图只需要三行就能出统一风格ggplot(map_data) geom_sf(aes(fill sales), color white, linewidth 0.2) scale_fill_distiller(palette RdYlBu, na.value #f0f0f0) theme_map()如果项目里要用自定义配色色带也可以封装。比如我想让高值在一套品牌色里使用可以把颜色写进一个向量再交给 scale_fill_gradientn 处理连续色带brand_colors - c(#f7fbff, #c6dbef, #6baed6, #2171b5) ggplot(map_data) geom_sf(aes(fill sales), color white) scale_fill_gradientn( colors brand_colors, na.value #f0f0f0 ) theme_map()地图绘制这个方向入门门槛其实不高真正的成本都在数据上坐标系有没有定对字段能不能匹配上几何要不要简化。这些细节一次处理干净后面换数据、换区域、换指标都是流水线操作。我自己的习惯是先把数据自查三行连招写成脚本头任何新的地图数据进来先跑一遍确认无误再往下走这个习惯帮我省掉了大量排查时间。希望帮到你。本文还有配套的精品资源点击获取