把 GeoJSON 画成能印刷的地图全图:投影、画布映射与三种导出格式
--- title: 把 GeoJSON 画成能印刷的地图全图:投影、画布映射与三种导出格式 ---
网页地图截个屏放进 PPT,投影仪一打就糊。年报要 300dpi,设计师还想在 Illustrator 里单独改某个省的填色。这时候要的不是切片地图,是一张从 GeoJSON 直接渲染出来的静态全图。
静态出图别用 Web 墨卡托
Leaflet、MapLibre 默认的 EPSG:3857 是给瓦片拼接用的,代价是纬度越高面积放得越大,倍数是 sec²φ。50°N 附近约 2.42 倍,20°N 附近约 1.13 倍,两头差了两倍多。画全国图的结果就是黑龙江看着比实际大一圈,海南被压小。屏幕上滑来滑去没人计较,印在纸上做面积对比就是硬伤。
静态制图该用等积投影。国内常用 Albers 等积圆锥,中央经线 105°E,双标准纬线 25°N 和 47°N。d3-geo 里对应 geoConicEqualArea:
import { geoConicEqualArea, geoPath } from 'd3-geo';
const projection = geoConicEqualArea()
.parallels([25, 47])
.rotate([-105, 0])
.center([0, 36]);
rotate 的第一个分量取中央经线的负值,center 是旋转之后坐标系里的位置,这组参数就是把图幅中心放在 105°E、36°N。画单个省份时,把这三个参数换成该省的中心经纬度,形变会小很多。
让边界正好填满画布
缩放比和平移量不用手算,fitExtent 会按要素的经纬度范围反推:
const width = 1600, height = 1200, pad = 40;
projection.fitExtent(
[[pad, pad], [width - pad, height - pad]],
geojson
);
const path = geoPath(projection).digits(2);
digits 是 d3-geo 3.1 之后才有的,控制输出路径的小数位数。全国图在 1600px 画布上留两位小数,肉眼看不出区别,SVG 体积却能省下一大截。想更狠一点,渲染前先跑一轮抽稀,见 Douglas-Peucker 抽稀。
拼 SVG 字符串:
const paths = geojson.features
.map(f => `<path d="${path(f)}" data-adcode="${f.properties.adcode}"/>`)
.join('\n');
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
<g fill="#e8f0fb" stroke="#3b6fb5" stroke-width="0.8" stroke-linejoin="round" fill-rule="evenodd">
${paths}
</g>
</svg>`;
adcode 写进 data- 属性,后面用脚本按代码批量改填色就很方便,做分级设色也是这个套路。
三个容易翻车的地方
空洞被填实
面里的内环表示飞地或者空洞,SVG 默认的 nonzero 填充规则要求外环和内环绕向相反才挖得出洞。数据环向不规范时洞就被填满了,改用 fill-rule="evenodd" 能绕开这个依赖。想从源头解决,按 RFC 7946 规范纠正绕向。
远海岛屿把图拉小
fitExtent 认的是全部要素的包围盒,一个远处的小岛就能让陆地主体缩到画面中间一小块。解决办法是先按面积过滤掉零星小面,或者只拿主体要素去 fit,小岛单独画。逐个要素查包围盒用 path.bounds(feature)。
线宽随缩放变化
viewBox 一缩放,描边就跟着变粗变细。浏览器里加 vector-effect="non-scaling-stroke" 能锁死线宽,可 Office 和一部分矢量编辑器不认这个属性。交付印刷稿,还是按最终尺寸直接给绝对线宽稳妥。
栅格化到 300dpi
SVG 转 PNG 用 sharp,底层走 librsvg:
import sharp from 'sharp';
import { writeFileSync } from 'node:fs';
writeFileSync('china.svg', svg);
await sharp(Buffer.from(svg), { density: 300 })
.png()
.toFile('china.png');
density 的基准是 72dpi,设成 300 相当于把 SVG 的标称尺寸放大约 4.17 倍,1600×1200 出来是 6667×5000 左右。反着算目标尺寸也行:A4 幅面 210mm 宽,300dpi 下要 210 ÷ 25.4 × 300 ≈ 2480px,高度 297mm 对应 3508px。
要在 Word 和 PPT 里改颜色,导 EMF
Office 对 SVG 的支持只能算能看,取消组合之后经常丢样式。EMF 是 Windows 原生的矢量格式,插进 PPT 右键取消组合,每个省就是一个独立形状,直接用主题色填。Inkscape 命令行能批量转:
inkscape china.svg --export-type=emf --export-filename=china.emf
| 格式 | 类型 | 适用场景 | 短板 |
|---|---|---|---|
| PNG | 位图 | 网页、微信文章、快速插图 | 放大失真,300dpi 全国图动辄十几 MB |
| SVG | 矢量 | 网页嵌入、Illustrator / Figma 二次设计 | Office 兼容性差 |
| EMF | 矢量 | Word、PPT 里拆分填色 | 主要在 Windows 生态可用 |
| 矢量 | 印刷、论文投稿 | 需专业软件才能编辑 |
出图前的顺序
先合并到目标层级,再抽稀,最后投影出图。顺序反了,相邻边界会被简化成不同形状,图上就冒出细缝,成因在拓扑合并 dissolve 那篇讲过。静态出图和网页渲染终归是两条路子,网页端怎么取舍见 前端渲染大体积 GeoJSON。
省、市、区县三级的高清全图和对应的 GeoJSON 源数据,GeoJSON 中国 上就能找到,PNG、SVG、EMF 都是现成的。自己搭一套渲染链路之前,这笔账值得先算算。