江山时序拆解:数据从哪来,零依赖 SVG 怎么渲染 2400 年疆域
DeanChensj/jiangshan-map,中文名「江山时序」,一个从战国(公元前 403 年)到今天的中国历史交互地图:拖时间轴看 18 个朝代的疆域涨落,点任何古城看它两千年改名换姓的沿革,搜一个人(苏轼、玄奘、王阳明)把他一生的行迹画在变迁的疆域上。2026 年 7 月建仓,4496 star、516 fork、320 个 commit,最近一次推送就在昨天——还在活跃迭代。GPL-3.0,作者 Dean Chen,一个喜欢历史地理的程序员。
这个项目的出身本身就是故事的一部分:作者在博客里自述,最初给 AI 的 prompt 只有一句——「帮我做一个能够拖动时间轴的中国历史地图」,然后两周半提交了两百多次代码。现在它是 GitHub Trending 日榜第二的仓库。市面上拆「vibe coding 能不能写正经软件」的文章很多,这个仓库是最好的实证样本之一,因为它把最难的两件事都做成了:数据从哪来(历史地理数据的版权雷区它踩得很干净),以及怎么把 2MB 的疆域几何渲染到零依赖的浏览器里。
一句话结论:直接拿来用(教学、写作、做内容配图)完全值得,深链和 embed 设计得很好;但你要二次开发,必须先搞清它的许可红线——郡县边界那层数据是哈佛 CHGIS 的,学术用途、不可再分发,连从它的仓库里转载都不行。 下面按"数据从哪来 → 怎么渲染 → 怎么保质量"的顺序拆。
本文提纲
- 数据从哪来:三层来源与一条许可红线
- 渲染层:零依赖 vanilla ES6 + SVG
- 数据管线:197 个快照源文件与 VLQ 编码
- 质量围栏:审计棘轮与自动修边
- 知识层:纪年、君主与诗人行迹
- AI 原生设计:MCP server、Agent Skill 与 llms.txt
- 横向比较
- 失效边界
- 成本
数据从哪来:三层来源与一条许可红线
这是最多人问的问题,仓库 data/atlas/README.md 给了完整交代,而且交代得比大多数学术项目还诚实。疆域几何分三层,来源完全不同:
MERMAID_BLOCK_0
第一层,郡/州/府级边界(prefectures),来自学术界的两大 GIS 数据集:CHGIS V6(哈佛费正清中心 + 复旦中国历史地理研究所合作项目)和 Robert M. Hartwell 中国历史 GIS。这是全项目里最"权威"的一层数据,也是最烫手的一层——原始许可是学术、非商业用途、不得再分发,明确不适用 GPL-3.0。项目对此的处理相当规范:scripts/export_geojson.js 默认导出时剔除这一层,必须显式加 --with-prefectures 才会带上,每个 GeoJSON 文件的头部都嵌着这段许可声明。
第二层,政权轮廓(polities)——战国七雄、三国群雄、辽宋夏金这些政权的疆域多边形,来自开源数据集 aourednik/historical-basemaps(GPL-3.0),这是可以合法再分发的部分。
第三层,一级政区(regions:刺史部、道、路、省),依据史籍手工绘制,README 直说早期版本的来源没有记录——这是数据溯源最诚实的一句话,多数项目会含糊过去。
在此之上,是整个项目真正的护城河:全部几何被简化到 0.01° 精度后,参照谭其骧《中国历史地图集》逐张手工核对调整。注意法律上的分寸——README 明确写"本仓库不包含谭图的任何图像或数据",谭图只作核对参照;贡献指南要求边界修正的 PR 必须引用谭图的具体册数和图幅。原话还有一句值得引用的免责:"Approximate boundaries, not surveyed borders"(近似边界,非测绘成果),这句话也被嵌进了每个数据文件的 license 字段。
人文数据则是另一条管线:纪年表、君主世系、诗人行迹、城池沿革,对照《资治通鉴》和各朝正史手工整理。作者博客里有两个真实的数据纠错案例——放大地图才发现辽国都城上京被画在了辽国疆域外面,只好回头补齐辽北疆域;横跨欧亚的蒙古帝国在罗布泊莫名其妙破了个窟窿,查了半天才补上。历史 GIS 的数据质量问题不是学术假设,是这种项目每天踩的具体坑。
渲染层:零依赖 vanilla ES6 + SVG
技术栈是我见过最反直觉的选择:零运行时依赖、零 npm 包、零地图库。没有 Leaflet、没有 MapLibre、没有 D3,Mercator 投影是 js/app.js 里自己实现的,标注防碰撞布局自己写的,渲染用原生 SVG。整个 package.json 里没有一个 dependency——CONTRIBUTING.md 开头就写着项目定位:"zero-dependency, historically rigorous interactive atlas"。
跑起来就是一个纯静态站:
git clone https://github.com/DeanChensj/jiangshan-map.git
cd jiangshan-map
python3 -m http.server 8000
不需要 npm install(没有可安装的依赖),任何静态服务器都能跑,仓库自带 Dockerfile 和 PWA manifest,也可以装到手机桌面。
为什么敢这么干?因为它的需求恰好是一个"自绘地图"的甜蜜点:不需要瓦片(历史疆域是矢量多边形,不是卫星影像)、不需要第三方底图(羊皮纸质感是自己画的样式层)、地理范围固定(就以中国为中心)。砍掉地图库换来的好处是实打实的:加载快(没有库的冷启动)、离线可用、永远不会遇到上游地图库的 breaking change、渲染行为完全可控。
代价是全部基础设施自己扛:投影计算、缩放平移手势、标注防重叠、移动端触摸事件、ARIA 无障碍,全在 app.js 里手写——这个文件 668KB,是整个项目最大的单文件。但注意这笔账:它是"一次写完就稳定"的基础设施代码,不是"每天都要改"的业务代码,零依赖在这个场景是正收益。
代码组织上还有个细节值得一提:缓存失效不靠改文件名或加时间戳,而是 scripts/sync_version.js 给每个资源算 SHA-256 前 8 位写进 index.html:
<script src="js/app.js?v=ea34709c" data-v-snapshots="811da97a" data-v-i18n="2e539e7d">
连懒加载的数据 bundle 都有独立的版本号属性,改数据不用刷全站缓存。这是把"零依赖的克制"贯彻到了工程细节里。
数据管线:197 个快照源文件与 VLQ 编码
数据怎么从"可编辑源"变成"浏览器里可渲染的产物",是全项目最精彩的一段工程。
源层是 data/atlas/ 下 18 个朝代目录、197 个 GeoJSON 快照文件,每个文件对应时间轴上的一个"地图快照"(比如 zhanguo/s_zhanguo_b.geojson 是公元前 300 年燕将秦开辟辽东那一张)。快照文件的设计解决了一个历史地图特有的痛点:同一个政权在相邻快照间往往不变。它的做法是把每个要素(feature)存在最早用到它的那个快照里,后续快照只存引用——文件里 "polities" 数组里出现的是 "s_zhanguo_a/齐" 这样的跨文件指针,裸 id "燕" 则指向本文件内的要素。改一条齐国边界,所有引用它的快照全部跟着变;要单独改某一阶段,就把要素复制成新 id。这是版本共享数据结构在地理数据上的应用,避免了 197 个快照 × 每个几十个政权的爆炸式冗余。
构建层是 scripts/build_atlas.js(同样零依赖),把源文件编译成两个产物:
js/atlas_boot.js(72KB):首屏快照,打开页面立刻能画;js/atlas_snapshots.js(2MB):全部快照,拓扑去重 + VLQ 增量编码,按需懒加载。
VLQ(Variable-Length Quantity)编码在文件头部就能看到实现——一套 base64 风格的字母表,把地理坐标的增量编码成紧凑字符串:
const A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-';
配合"拓扑去重"(相邻政权共享的边界顶点链只存一份,正好也是后面质量审计的基础),2MB 装下了 2400 年的全部疆域几何。对一个零构建工具链的静态站来说,这就是它自己的"自制压缩格式"。
知识层和数据层是分开的三个大文件:js/map_data.js(575KB,18 个时期的元数据、阶段规则、古城坐标、河流、关隘、里程碑)、js/lore_data.js(802KB,纪年、君主、人物行迹、城市沿革、事件)、js/i18n_dict.js(403KB,中英对照词典)+ zh_hant.js 繁体。这些是手写维护的源,不经过构建。
MERMAID_BLOCK_1
质量围栏:审计棘轮与自动修边
历史疆域多边形有个天然的视觉雷区:相邻政权共享边界一旦顶点不齐,放大就会出现缝隙或重叠。手画 197 张快照,靠人眼保证顶点链完全一致是不可能的。这个项目的回答是一套自建的 QA 机器,我觉得是全仓库工程含金量最高的部分。
审计层两个脚本。audit_topology.js 对每张快照统计核心政权(main/rival/protectorate 级,neighbor 级豁免)之间的边界缝隙和重叠——顶点数和中点数,缝隙小于约 4 公里的都算——然后对照 topology_baseline.json 的基线,任何一项超过基线,CI 直接挂。关键设计在基线的语义:--update 只能把基线调低,不能调高。这是个标准的棘轮(ratchet)机制——问题数量单调不增,修一个少一个,想偷偷倒退没门。audit_prefectures.js 用同样的思路管郡县层:在 0.05° 网格上数郡县互相重叠和越出政权轮廓的格子数,同样对着只降不升的基线。
修复层是 scripts/repair_topology.js:对小于约 3 公里的浅缝隙和浅重叠做自动修边——政权按 main > rival > protectorate 排优先级,再按面积,低优先级的边界吸附到高优先级的边界上,两侧共享同一条顶点链。但它的克制是设计出来的:任何会让其他共享该要素的快照变差、产生尖刺或捏点、或者移动领土超过约 6 公里的改动,跳过并报告,留给人手工处理。自动修边只修确定安全的,不确定的一律上交。
验收层是 scripts/validate_atlas.js——451KB 的验证套件,比应用主逻辑的相当部分还大。本地迭代用 --fast 约 6 秒出结果(语法、哈希、schema、i18n、关键 UI 路径),提 PR 前跑全量(含全部拓扑审计和子进程冒烟测试)。
把这套东西合起来看:数据质量不是靠作者火眼金睛,是靠"基线只降不升的棘轮 + 克制的自动修复 + 6 秒反馈循环"的机制保出来的。对任何做手工数据集的项目(历史地图、方言分布、方言字典、古籍数字化),这套范式可以直接搬。
知识层:纪年、君主与诗人行迹
几何之外的内容数据有两个设计决策值得单独讲。
纪年正统观。时间轴显示哪家的年号,是历史地图的隐含意识形态问题。这个项目的选择写在 lore_data.js 的结构里:主线纪年遵循《资治通鉴》的正统 conventions——三国取曹魏、十六国取东晋、南北朝取南朝、五代取中原五代、宋辽金夏并列取宋;同时蜀汉、孙吴、北朝、辽、金、西夏、忽必烈之前的蒙古这些"并立自家年号"的政权,放进平行的 REGIONAL_ERA_TRACKS,事件按其发生地所属政权的年号标注。用数据结构把"谁的纪年是主线"这个史学争议显式化了——比在页面角落写一行免责声明体面得多。
人物行迹。张骞、玄奘、李白、苏轼、王阳明等人的完整行迹(每个几十个节点,含史料原文和诗歌引文、双语的)作为结构化数据存在 lore_data.js 里,播放时每前进一步,时间轴年份、在位君主、疆域形态全部同步变化。这是"点人溯事"功能的数据底座,也是它区别于静态历史地图的最大体验差异。
AI 原生设计:MCP server、Agent Skill 与 llms.txt
这个项目对 AI 的态度不是"用 AI 写的"这么简单——它把"被 AI 使用"做成了功能面。
llms.txt(13KB):机器可读的项目协议说明,给 LLM 和爬虫读的"这个站有什么、深链怎么拼"的说明书;scripts/mcp_server.js:一个 MCP server,AI agent 可以通过标准协议查询地图;skills/jiangshan-atlas/:一个 Agent Skill(SKILL.md + 零依赖的 Python CLI 截图/巡检工具),装进 Claude Code 或其他 agent 后,用自然语言就能操作这张地图。
作者举的例子很传神:问一句"雪拥蓝关马不前",agent 会定位到韩愈被贬潮州、立马蓝关的公元 819 年,在图上标出蓝关;问"问汝平生功业,黄州惠州儋州",它把苏轼一贬再贬的路线串起来画出来。古诗地名 → 历史坐标 → 地图深链,这条链路对一个静态网站来说是很超前的设计——它默认未来的读者里有相当一部分是 agent。
再配合精心的深链系统(?year=208&place=赤壁 直接跳到赤壁之战那一年的战场)和 embed 模式(&embed=1 生成 iframe 嵌入代码,还有 from/to 限定时间轴范围、lock=1 锁死年份),这让它的内容可以被别人的文章、课件、笔记引用——SEO 的 topics/ 目录下还有 35 个按朝代和战役预构建的静态落地页。
横向比较
跟 CHGIS / 谭其骧《中国历史地图集》比。 那是学术权威和纸质经典,精确但不可交互、检索门槛高、普通人根本用不起来。江山时序明确定位自己是"示意性复原"而非测绘成果,用交互性和可读性换精确性——它是把学术成果翻译成大众可用形态的转译层,不试图替代原典。
跟 GitHub 上其他历史地图项目比。 aourednik/historical-basemaps 提供全球历史政权的原始多边形(江山时序的数据来源之一),但没有交互产品;各种"中国朝代地图"项目大多是静态图集或用现成地图库拼的 demo。江山时序的差异点在于知识层的厚度——纪年、君主、人物行迹、城市沿革这套人文数据,加上前面那套质量围栏,是 demo 和产品之间的距离。
跟商业历史地图产品(比如各种历史地图 App)比。 商业产品通常数据闭源、更新随缘。这里是 GPL-3.0 的完整源码 + 可编辑的数据源 + 明确的贡献流程(CONTRIBUTING.md 细到"PR 请引用谭图册数图幅")。它的护城河不是数据独占(数据来源本来就是公开的),是整理、核对、纠错的累计工作量——那个只降不升的基线就是工作量的账本。
失效边界
一、许可红线必须再说一遍。 郡县边界源自 CHGIS V6 和 Hartwell 数据集,学术、非商业、不得再分发,不适用 GPL-3.0。你 fork 这个仓库自己跑没问题,但把郡县层导出去商用或再发布,就是侵权。仓库的 export_geojson.js 默认剔除这层、每个数据文件嵌许可声明,都是给你的提示——别绕过它。政权轮廓层是 GPL-3.0 传染的,拿去做闭源产品同样不行。
二、"近似边界"就是近似边界。 README 反复强调这是示意性复原:0.01° 精度、手工调整、游牧政权和羁縻地区的边界本来就是动态过渡带。用它讲历史故事、配文章插图、做课程演示都是对的;拿它做严肃的历史地理学术论证、或者在敏感边界问题上引用它当依据,都是错误用法。
三、零依赖是双刃剑。 对使用者是福音(克隆即用),对贡献者是门槛:没有框架的抽象层,改渲染逻辑要直接读 668KB 的 app.js;没有 npm 生态,想加功能就得像作者一样自己写。这个项目的可贡献面主要在数据层(地理纠错、翻译、人物数据),不是代码层——事实上它的贡献指南重点也全在数据规范上。
四、单人维护的可持续性。 320 个 commit、四个月、大半是深夜和周末。数据纠错类 issue(12 个 open)的消化速度取决于一个人的历史地理知识余量。好在数据源文件的设计(跨快照引用 + 严格 schema + 自动验证)让外部贡献的门槛足够低——这可能是它为长期存活做的最重要设计。
成本
- 钱:免费,GPL-3.0(注意上面的许可例外)。线上版 https://deanchen.me/jiangshan-map/ 直接用。
- 机器:纯静态站,任何静态托管都行;2MB 的数据 bundle 懒加载,首屏只拉 boot 快照,普通网络无压力。
- 二次开发的时间成本:想清楚你要动哪一层——只改文案和人物数据,照 CONTRIBUTING.md 的规范填 JSON 即成;要动疆域几何,得先学会它的快照引用机制,跑通 build + audit + repair 那套流程,引用谭图册数图幅,等 CI 的棘轮放行。前者一小时,后者是真心投入一个历史地理 mini 论文的开始。
参考文档与链接
- DeanChensj/jiangshan-map — 本文拆解的原仓库,GPL-3.0
- data/atlas/README.md — 数据来源与许可的官方声明,本文"数据从哪来"一节的依据
- CONTRIBUTING.md — 仓库结构、数据规范、审计流程的完整文档
- 从历史课本里的插图,到 GitHub 1k star — 作者自述的项目故事
- CHGIS V6 — 哈佛费正清中心 + 复旦史地所,郡县层数据来源
- aourednik/historical-basemaps — 政权轮廓层来源,GPL-3.0
- 在线版 — 直接可玩,支持中英繁三语
你拖时间轴的第一个年份会停在哪儿?我去的是前 260 年的长平。评论区聊聊你最想让哪一年的地图出现在屏幕上。
作者: itech001
来源: 公众号:AI人工智能时代(the-ai-era)
网站: https://www.theaiera.top/
关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top
本文首发于 AI 人工智能时代,转载请注明出处。

浙公网安备 33010602011771号