城市地图集制作指南:数据清洗、坐标系与开源工具实战 Atlas这个词在地图相关词汇里应该算得上最有故事感的一个。它既是古希腊神话里用肩膀撑起天空的泰坦神也指把世界装订成册的地图集。第一次见到这个词是在一本旧地图册的书脊上封面上只有一个单词打开以后一个城市的风貌、河流走向、道路脉络全被安排得明明白白。后来我逐渐意识到atlas的本质其实是“有组织的信息合集”这也是我这些年一直喜欢做地图项目的原因。这篇内容适合想用地图讲一点故事的人不管你是做数据可视化、做城市观察还是纯粹想把某个区域的POI整理成一份有质感的小册子都可以参考。我会把一个地图集项目的完整链路拆开讲清楚从数据怎么整理、坐标系为什么不能乱用到地图配色怎么不翻车再到最后怎样输出一份能拿得出手的作品。整条路径不依赖重型商业软件绝大多数环节用开源工具和一段普通代码就能完成。1. 一张atlas的底层逻辑内容、媒介和表达很多人在做“地图”和做“地图集”之间没有明确区分上手就开始堆点堆到最后像一张密密麻麻的仪表盘信息全有但读者看完就忘。真正的地图集作品我在实际项目里总结下来至少要同时处理好三个层次。1.1 地图集的三个层次底图、数据层、故事线第一个层次是底图也就是地理基底。它负责提供空间参照让人知道“我在哪”。底图不能抢戏一个常见的错误是把底图做得五颜六色专题数据反而看不清。第二个层次是数据层这是地图集的主题内容可以是咖啡店分布、某区域的人流密度、骑行路线或者任何你想表达的空间信息。第三个层次是故事线这一点最容易被忽略。地图集的“集”字意味着它不只是一张图而是多张图之间有编辑意图读者翻过去的时候应该能感受到你想强调什么、沿着什么顺序展开。举个我自己的例子。我做过一份某个街区的地图集数据本身只是几十家餐饮店的评分和品类。最开始我按照“点大小评分”的逻辑去做结果画面很乱。后来我换了个思路把故事线定为“从早餐到夜宵”每一页是一类业态底图完全一致但数据层依次切换。这样读者跟着翻页就像沿着时间线逛了一遍街区。故事线一旦清晰之前很多视觉决策都变得顺理成章。我建议在动手前用三句话写下这张地图集的“编辑说明”这个作品给谁看、他想获得什么信息、翻完之后应该留下什么印象。这三句话不需要给任何人看但能帮你过滤掉大量无效工作。1.2 技术路线怎么选静态渲染、交互地图还是打印小册子地图集的交付形式决定了整个技术栈。我通常把项目分成三类静态渲染图、交互网页、适合打印的PDF小册子。三者不是互斥的很多项目会同时用到两种形式但一开始你最好想清楚核心交付是什么。静态渲染图适合快速验证设计方向用QGIS搭好样式导出高分辨率图片一般用于封面、海报或报告配图。交互网页适合信息量大、需要读者自己探索的场景技术选型上我更偏向MapLibre GL它轻量、开源样式配置灵活没有太多授权负担。打印小册子是最有“atlas”仪式感的形式但也是最容易被低估的环节因为地图上的文字大小、线条粗细都要以实际打印尺寸为标准屏幕上看得很舒服的元素印到A4纸上可能糊成一团。我也做过一条偷懒的路径先用交互网页把数据层调试到位再把这套样式平移到QGIS布局管理器导出PDF。这样做的好处是交互环境中能快速验证颜色、层级、标注避让是否合理省去了反复打印测试的成本。如果你没有打印需求可以跳过小册子部分但前两类的沉淀一定不会白费。2. 数据准备这个环节直接决定成败地图项目里最不性感但最要命的就是数据准备。一个地图集项目的时间分配我见过太多人把80%时间花在调整样式上结果最后卡在数据质量上。实际上合理的时间分配应该是数据处理占六成设计排版占三成部署发布占一成。2.1 数据源与字段设计别拿到什么就喂什么数据来源通常有三类公开数据、开源社区数据、自己采集的数据。无论来自哪里都需要先想清楚字段结构。我常用的最小字段集是名称、类别、经度、纬度、备注、链接。经纬度建议分开存成两个字段不要合并成一个坐标字符串否则后续清洗和筛选会非常痛苦。举个例子假设你要做一份城市咖啡地图集原始数据表里可能混杂着“咖啡”“奶茶”“甜品”多个分类。你一步到位只保留咖啡会让后期想扩展维度时很被动。更好的做法是保留完整的业态分类用category字段区分后期通过过滤来控制显示内容。这样今天做“咖啡地图集”明天想切换成“甜品地图集”只需要换一下过滤条件数据完全复用。有个安全前提我必须专门说个人地图项目务必使用来源明确、合法合规的数据不要抓取涉及人群精确位置或敏感设施的信息。地图能承载的内容很多但边界也很清楚做作品不用冒着踩线的风险。2.2 坐标系和投影入门必须过的一关坐标系是地图新手翻车率最高的地方没有之一。很多人把经纬度数据直接扔进地图引擎结果点显示到了海中央或者跟底图错位好几条街。要彻底搞懂这个问题可以做一个生活化类比地球是橘子的果肉球面是不可展开的你要把皮剥下来平铺成一张纸就必须在某个位置“撕开”不同撕法就是不同投影。实际使用中你只需要分清两类坐标。一类是WGS84经纬度也就是GPS设备直接给出的数据通常用EPSG:4326表示大多数外部数据源拿回来都是这个格式。另一类是Web墨卡托投影坐标通常用EPSG:3857表示几乎所有网页电子地图底图都基于这个投影。存储数据时我强烈建议统一用EPSG:4326因为经纬度是人类可读的、全球统一的显示时交给地图引擎自动转换到EPSG:3857。很多人犯的错误是看到某API返回了“x”“y”字段就以为是经纬度其实是投影坐标直接当作纬度经度用点自然就飞了。遇到这类数据先判断数值范围经度在-180到180之间纬度在-90到90之间绝对值一旦超过这个范围说明大概率是投影坐标需要先做转换再入库。2.3 清洗坐标数据的标准动作清洗数据不是要把数据洗得多漂亮而是确保地图引擎能吃下去且不乱跑。我的标准动作只有四步但每一步都有明确目的。第一步是去重按“名称经纬度”组合判断避免同一家店因为数据源叠加出现两次。第二步是剔除缺失值只要名称、经纬度任意一项为空直接整行丢弃因为地图上不能显示一个没有坐标的信息点。第三步是范围校验经纬度越界的数据多写一句纠正逻辑比如经纬度写反的情况。第四步是统一坐标系入库前确保所有数据都是EPSG:4326。我给你一段可以直接用的数据清洗代码使用GeoPandas处理输出GeoJSON这也是后续所有地图工具都能读取的通用格式import pandas as pd import geopandas as gpd # 原始数据读取注意编码格式 df pd.read_csv(poi_raw.csv, encodingutf-8-sig) # 第1步丢弃关键字段为空的行 df df.dropna(subset[name, lat, lng]) # 第2步范围校验经纬度写反时自动交换 mask (df[lat].between(-90, 90)) (df[lng].between(-180, 180)) # 这里用更严格的区间判断超出合理城市范围的数据也可以手动抽查 df df[mask] # 第3步按名称与坐标去重 df df.drop_duplicates(subset[name, lat, lng]) # 第4步构造GeoDataFrame指定坐标系为WGS84 gdf gpd.GeoDataFrame( df, geometrygpd.points_from_xy(df[lng], df[lat]), crsEPSG:4326 ) # 输出GeoJSON供QGIS、MapLibre等工具使用 gdf.to_file(poi_clean.geojson, driverGeoJSON)这一步做完你的数据就已经有了一个非常扎实的起点。接下来进入设计环节我会告诉你怎样让数据真正“好看”。3. 地图设计好质感来自克制地图设计跟普通平面设计最大的区别是信息必须准确优先美感其次。一个我常说的比喻是地图的视觉是给读者铺的红毯红毯太花人会摔跤红毯太平人会迷路。设计中最重要的不是技巧而是做减法。3.1 版式与视觉层级先定骨架再谈细节拿到数据后不要急着调颜色先确定版式骨架。如果是网页考虑地图占屏多大、是否有侧边信息栏、是否有图例和比例尺。如果做打印小册子先定页面的幅面A4竖版、A4横版还是方形开本这直接决定了地图上的最大可用尺寸。我的经验是信息密度越高的地图周围留白越要多。地图集不是数据大屏不适合把边角全部填满。每一页地图的标题、说明文字、图例、比例尺、指北针都应该有固定的位置整套作品的风格统一性远大于单页的惊艳。你可以先画几张纸质草图把每个区块的占位确定下来再进入工具操作。这个习惯帮我避免过无数次“调一个下午最后推倒重来”的窘境。3.2 专题符号与配色选择宁可保守不要翻车配色是地图质感的最大变量也是新手最容易失控的地方。我一直推荐一套保守方案底图用低饱和度的浅灰、浅米色数据层用一两个饱和色辅助信息用深灰。这样至少不会翻车。等审美积累够了再尝试更大胆的撞色。如果你是做分类数据比如不同业态的咖啡店、不同类型的历史建筑优先用色相去区分而不是深浅。因为人眼对色相差异最敏感。如果是连续数据比如评分、价格、销售额那就用同一色相由浅到深的渐变色。把颜色数量和指标数量对应起来不要一个指标用三个颜色。关于符号点数据的常见参数是半径大小、填充颜色、描边颜色、描边宽度。描边颜色和描边宽度经常被忽略但它们对“精致感”影响巨大。一个白色细描边能让任何颜色的点从底图上浮起来视觉上立刻专业很多。3.3 标注排版与避让细节决定是否耐看文字标注是地图里最考验耐心的环节。地图上最难看的效果就是标注互相压盖、或标注压住了关键数据点。很多工具自带碰撞检测但远不是万能的。我习惯在设置里调两个参数一个是标注碰撞开关开启另一个是让重复元素不重复显示同一地点只出现一次文字。另外一个很实用的小技巧是给文字加光晕。在QGIS和MapLibre里都可以配置文字周围包一圈半透明的白色描边底图再复杂文字也能保持可读。字体选择上中文字体的可选项比英文少得多我习惯用思源黑体系列干净且免费可商用。英文部分可以用无衬线字体尽量避免在低分辨率屏幕上使用细体细线文字放大后容易显得单薄。还要注意地图上的“文字层级”通常只给最重要的点标注名称次要信息放图形里或者延迟到点击交互才显示。一屏之内如果超过二十个文字标注读者的认知负担就会明显上升不自觉地开始感到烦躁。4. 实操从零完成一份城市咖啡地图集理论说了不少现在走一遍完整流程。以“城市咖啡地图集”为例目标产出一份交互网页同时预留打印版输出。我假设你的原始数据已经放在一个CSV文件里字段包括名称、地址、经度、纬度、评分和业态分类。4.1 用QGIS把数据变成第一张图QGIS是一款完全免费的开源GIS桌面软件处理地图数据和快速样式验证非常顺手。打开QGIS之后新建项目先把坐标系设置为EPSG:4326或者选择“开箱即用的项目模板”。然后把刚才清洗好的poi_clean.geojson直接拖入图层区域QGIS会自动显示点数据。接下来做最简单的样式验证右键图层进入属性选择“分类”按category字段分类每个类别指定一个符号。你会立刻看到数据的大致分布。这一步的目的不是定稿而是确认数据有没有明显问题比如某个点飞到了城市外面、某个分类点数量意外极端。我建议在这个环节花5分钟做一次“目检”比后面到网页上调试半天再发现数据错误要省时得多。如果要做印刷版QGIS的“布局管理器”足够生成一张完整的图幅可以设置纸张尺寸、添加指北针、比例尺、图例。导出PDF之前把地图比例尺锁定比如1:10000或1:20000不要让每一页的比例尺乱跳否则整套小册子的空间尺度感会崩塌。4.2 用MapLibre做出可交互页面进入网页交互阶段我推荐MapLibre GL JS。它是一个开源地图引擎不需要引入重型商业SDK只需要一个HTML文件就能跑起来。核心思路是先定义底图源再定义数据源最后把数据源渲染成图层。一个最简示例是这样的底图瓦片地址请替换成你信任且可用的服务地址!DOCTYPE html html langzh-CN head meta charsetutf-8 / title咖啡地图集/title meta nameviewport contentwidthdevice-width, initial-scale1 / script srchttps://unpkg.com/maplibre-gl3/dist/maplibre-gl.js/script link hrefhttps://unpkg.com/maplibre-gl3/dist/maplibre-gl.css relstylesheet / style body { margin: 0; } #map { width: 100vw; height: 100vh; } /style /head body div idmap/div script const map new maplibregl.Map({ container: map, style: { version: 8, sources: { baseTiles: { type: raster, tiles: [https://your-tile-server.example.com/{z}/{x}/{y}.png], tileSize: 256, attribution: 你的底图来源 }, cafes: { type: geojson, data: poi_clean.geojson } }, layers: [ { id: base-layer, type: raster, source: baseTiles }, { id: cafe-points, type: circle, source: cafes, filter: [, category, 咖啡], paint: { circle-radius: [ interpolate, [linear], [get, rating], 3.0, 4, 5.0, 12 ], circle-color: #a64d2e, circle-stroke-width: 1.5, circle-stroke-color: #ffffff } } ] }, center: [113.27, 23.13], zoom: 13 }); /script /body /html这个示例里我用评分字段动态控制了圆的半径评分高的咖啡店看起来更大。interpolate表达式是MapLibre里非常好用的功能作用是把一个连续值映射成另一个连续值相当于给数据一个线性变换比手动分成几个固定档位要自然得多。如果你想实现点击弹出信息只需要再加一段事件处理map.on(click, cafe-points, (e) { const feature e.features[0]; new maplibregl.Popup() .setLngLat(feature.geometry.coordinates) .setHTML(b feature.properties.name /bbr/评分 feature.properties.rating) .addTo(map); });当你的点数量特别大时比如几万以上直接把GeoJSON作为数据源会导致页面卡顿。解决办法是启动聚合把临近的点合并成一个聚合点用户放大后才会逐渐拆开。MapLibre的GeoJSON源原生支持聚合配置map.addSource(cafes-cluster, { type: geojson, data: poi_clean.geojson, cluster: true, clusterMaxZoom: 14, clusterRadius: 50 });4.3 输出打印版小册子的思路打印版的核心要求是“所见即所得”千万不要用浏览器直接打印网页地图因为网页地图的缩放级别和页面排版不可控。我的经验是交互版用于探索打印版单独在QGIS布局管理器里制作。布局管理器的做法是选择纸张尺寸添加地图、图例、标题、比例尺。地图窗口里可放入多个已经调整好范围的视图每个视图对应一页。每一页应当保持同一个比例尺这样读者翻页的时候能直观感受到空间尺度的一致性。一个加分项是让每一页的视野略微覆盖上一页的边界形成连续翻阅的过渡就像真正的地图册一样。导出PDF时我会把背景图层的标注略微降一点透明度既保留空间参照又不跟专题信息抢注意力。最终PDF建议嵌入字体这样别人打开时不会因为缺字体出现方块。5. 常见问题与排查技巧实录地图项目出问题是常态不出问题才要怀疑自己是不是没用心。我把自己踩过的坑整理成了一份速查表按现象、原因、解决办法三个维度列出来方便你对照排查。现象大概率原因解决思路点全部出现在海洋或空白区域经纬度字段用反了或混入了投影坐标检查字段范围和坐标系统一为EPSG:4326地图上只有一个点其他点消失了GeoJSON里混入了空geometry或非法值清洗数据时做非空校验点与底图错位几条街数据坐标系和底图坐标系不一致将数据重投影到EPSG:4326或EPSG:3857中文标签显示为方块字体缺失或渲染环境不支持中文字体指定系统字体或嵌入思源黑体标注互相压盖严重没有开启碰撞检测或标注密度过高打开碰撞检测启用重复元素去除提高最小距离地图加载非常卡顿数据量太大直接加载GeoJSON启用聚合或先用工具切瓦片样式配置不生效图层顺序问题或过滤器写法错误检查图层的source和filter语法把底图图层放在最下面导出PDF字体走样字体未嵌入在导出设置中勾选嵌入字体5.1 现场排查的三个小技巧除了表格里的常规解法我再分享三个现场排查时特别好用的小技巧。第一个是“图层命名法”在QGIS和MapLibre中图层名称不要叫layer1、layer2直接用像“咖啡店-点-评分渐变”这样的名字调试时一眼就能定位到问题图层项目兄妹接手也不会一头雾水。第二个是“单层检查法”遇到样式混乱时把所有图层全隐藏只打开该打开的图层从底图开始依次往上加。当加某个图层出现问题的那一步停下问题基本就定位了。地图是图层嵌套系统很多问题的根源就出在某一层的透明度或过滤条件上。第三个是“颜色打印测试法”本地预览颜色没问题的图纸打印出来后往往偏灰或者偏深因为屏幕是发光体而纸张是反光体。重要作品一定要试打印一次用实际成品检验对比度是否足够。5.2 三个值得长期坚持的好习惯最后说三个我在多次项目里沉淀下来的好习惯虽然和具体技术无关但长期受益。第一个是每次项目建一个独立目录原始数据、清洗脚本、样式快照、导出文件分开存放。地图项目改动频率极高没有清晰的目录会让你在两周后连自己写了什么都找不到。第二个是每改一版样式就导出一次静态图存到“版本记录”文件夹。这能让你有勇气去尝试新方向反正可以随时退回旧版本。第三个是数据更新要留痕在GeoJSON或CSV源文件里加一个updated_at字段记录这条数据最后更新时间地图集一旦面向公开场合标注这些信息会显得很专业也方便被读者质疑时快速响应。我在实际做地图集项目时最大的感受是真正决定作品高度的不是渲染引擎多炫、也不是配色多复杂而是你对内容的理解和克制。地图集的仪式感来自它在信息洪流里替你做了筛选替你安排了看世界的顺序。先想明白要讲什么再去碰工具这条路永远不会绕远。