地图(上)— GeoJSON、投影与专题地图¶
这一章讲什么(上半): D3 的地图能力全部建立在 d3-geo 模块上,而 d3-geo 又建立在 GeoJSON 这个开放数据格式上。本篇走通三件事:把 GeoJSON/TopoJSON 画成 SVG/Canvas、 理解并配置投影(一百多个函数、五大管道阶段)、画两种最经典的专题地图 (分级设色 choropleth 与点密度/分级圆点)。下半篇(交互地图:动画、缩放、旋转)见下一篇。
1. 顶层全景:一块数据 → 一张图¶
GeoJSON / TopoJSON 文件
│ d3.json 加载;TopoJSON 先 topojson.feature() 转回 GeoJSON
▼
投影函数 d3.geoXxx() —— [经度,纬度] → [x,y] 像素(可 invert 逆算)
│ 配进 d3.geoPath()
▼
geoPath(GeoJSON 对象) → path 字符串(SVG)或 Canvas 命令
│
▼
<path d=…> × N(国家/城市/经纬网)
图说:geoPath 是图形的门,投影是门里的换算器,GeoJSON 是门外 standard 的数据形状。
2. GeoJSON:地图的字母表¶
RFC 7946 定义六种几何,坐标一律 [经度, 纬度] 二元数组(经度在前!)—— 这是最容易写反的地方1:
| 类型 | 形状 | 典型用途 |
|---|---|---|
| Point / MultiPoint | 一个点 / 点数组 | 城市、采样点 |
| LineString / MultiLineString | 折线 / 折线组 | 航线、道路、河流 |
| Polygon | 一个或多个环(首环外边界、其余是洞) | 行政区、带湖的国土 |
| MultiPolygon | Polygon 数组 | 本土 + 岛屿的国家 |
| Sphere(D3 私有) | 无坐标,代表整个球 | 海洋背景、裁剪掩膜 |
D3 的扩展:{type:"Sphere"} 常常直接当 datum——一张天蓝色圆盘垫底,
正射投影下它就是圆;面片化的投影(interrupted)还拿它当裁剪掩膜2。
TopoJSON:把相邻多边形共享的边界抽成「弧段(arcs)」只存一份,
原书的世界地图从 924kB 缩到 102kB(约 9 倍)。代价是要用 topojson-client
转换:topojson.feature(tjdata, tjdata.objects['world']) 提取成
FeatureCollection 后,代码与 GeoJSON 完全一样。配套的 mesh()(公共边界线)、
merge()(合并成一块)、neighbors()(相邻关系)各有妙用3。
3. geoPath:地图版的路径生成器¶
d3.geoPath() 与其他形状生成器同构:配投影、吃 GeoJSON、吐 path 字符串:
const geoPath = d3.geoPath().projection(d3.geoMercator());
// 画一个国家(单 Feature 直接喂)
svg.append("path").attr("d", geoPath(feature));
// 画世界(FeatureCollection:join features 数组)
svg.selectAll("path").data(data.features).join("path").attr("d", geoPath);
专属方法三个:pointRadius(n或函数)(Point 几何渲染成的小圆半径)、
centroid(obj)(投影像素质心——摆标签用)、bounds(obj)(投影像素包围盒)。
配 context(ctx) 即切 Canvas 模式:每 feature beginPath() → geoPath(feature) →
fill/stroke,千级要素时内存天壤之别4。
主线例子(马达加斯加):拿 d3.geoCentroid(geometry) 得地理质心
(返回 [46.74, -19.32]——经度纬度),喂给 geoMercator().center(质心).scale(1500),
一个国家的地图就居中放大了。更现代的做法是让投影自己 fit(见 §5)5。
4. 上下文图形:经纬网与圆¶
d3.geoGraticule() 生成器吐一个 MultiLineString(经纬网),step([10,10]) 控制间隔
(默认 10°);再叠一个 step([0,90]) 的粗线版就是赤道。换投影时经纬网自动跟着变——
因为它们走同一个 geoPath6。
d3.geoCircle() 生成「球面上的圆」Polygon:center([lon,lat]).radius(角度 0–90°)。
90° = 半球——画晨昏线、热带圈(地轴倾角 23.5°)、以及 Tissot 指示圆全靠它7。
5. 投影:一百个函数与一条五段管道¶
5.1 选型:先分三大类,再按「保什么」挑¶
| 大类 | 投影面 | 成员 | 特点 |
|---|---|---|---|
| 圆柱 | 卷起的纸筒 | Mercator、等积圆柱 | 赤道附近失真小;Mercator 保角(航海用),面积失真吓人 |
| 圆锥 | 纸锥 | Albers、Lambert 双标准纬线 | 中纬度地区最佳;parallels([15,45]) 配两条纬线 |
| 方位 | 平面贴球 | 正射 orthographic、gnomonic、等距方位 | 极地/半球图;正射有 3D 感 |
没有完美投影,只有保什么的取舍:保形(conformal,Mercator:局部形状角度对、面积错)、 等积(equal-area:面积对、形状歪——专题地图比大小必须用等积)、 等距(equidistant:特定方向距离对)。Tissot 指示圆(球面等大的圆投影后变形) 是比较失真的标准仪器:圆变大 = 面积失真,圆变椭圆 = 形状失真8。
常用特例:rotate([0,±90,0]) 把投影转成极地视角;geoAlbersUsa() 是美国本土
+阿拉斯加+夏威夷的复合投影;geoInterruptedHomolosine() 等断裂投影需要额外的
包围球裁剪(用 Sphere 生成 clipPath)9。
5.2 五段管道与配置方法¶
geoPath 渲染一条几何,内部走五个阶段——理解它们,配置方法才有归处10:
① 旋转(rotate([yaw,pitch,roll])) ← 转的是「球」,投影前;三个欧拉角
② 预裁剪(clipAngle(度)) ← 球面小圆裁剪;正射默认 90°(藏背面半球)
③ 投影算法(geoXxx 本身) ← 经纬度 → 平面坐标
④ 平面变换(center/translate/scale/angle) ← 转的是「图」,投影后;像素单位
⑤ 后裁剪(clipExtent([[x,y],[x,y]])) ← 视口矩形裁剪;Mercator 默认 ±85°
图说:rotate 改变你「看哪里」,center/translate 改变图「摆哪里」,clipAngle/clipExtent 决定「剪掉什么」。
center([lon,lat]) 与 translate([x,y]) 都能让图居中,但前者是地理坐标(管道早期)、
后者是像素(管道末端);angle() 平面旋转、scale() 按投影单位缩放,锚点都是地理中心。
rotate 是投影前的三轴旋转(欧拉角)——要看不跨太平洋的航线或让南极居中,就转它11。
5.3 fit 一族:别再手算 scale¶
fitExtent([[minX,minY],[maxX,maxY]], 对象) / fitSize([w,h], 对象) /
fitWidth/fitHeight:自动算好 scale 与 translate,把任意 GeoJSON(或 Sphere)
嵌进指定视口——它们覆盖 center/scale/translate 的一切手动配置。
马达加斯加一例的手动 .center(质心).scale(1500) 应改写为:
d3.geoBounds(obj) 返回球面包围盒、它本身就是合法 GeoJSON,可以继续喂给 fit 方法12。
6. 专题地图¶
6.1 Choropleth(分级设色图)¶
地图当底,区域颜色当数据。原书的口径值得背:比面积就用等积投影
(geoEqualEarth),比例尺别用线性——两个十亿人口国 + 平均四千万的数据,
线性色阶全挤在一头;解法 scaleSequentialPow(配色).exponent(0.25)(四次方根压缩),
domain 反转让均值黄绿、高值红13:
const color = d3.scaleSequentialPow(d3.interpolateRdYlGn).exponent(0.25)
.domain([popMax, popMin]); // 反装:大人口 = 红
svg.selectAll("path.feature").data(data.features).join("path")
.attr("d", geoPath)
.style("fill", d => color(d.properties.pop));
换主题 = 换数据源挂到 feature.properties(用 ISO 三字母码匹配): HDI、人口密度都是同一行代码换字段。上游数据准备(把属性并回 GeoJSON)通常该在服务器做, 在浏览器这一侧(客户端)合并只是教学演示14。
6.2 点密度图与分级圆点图¶
点密度:每点 = 一个地点(GeoNames 十万城镇),pointRadius(0.5)、
用 MultiPoint/Point FeatureCollection 避免十万 DOM 节点——人的聚居形状直接显形15。
分级圆点:每城一圆,半径用 scaleSqrt(面积∝人口,视觉才不失真)、 人口升序排(小点画在上层不被大点盖)、透明度反向补偿(大城半透明防糊)、 颜色用反转的幂次顺序色阶16:
const radius = d3.scaleSqrt().domain([min,max]).range([.1, 15]);
geoPath.pointRadius(d => d.properties ? radius(d.properties.pop) : 0);
7. 量测工具¶
| 要量什么 | 球面(与投影无关) | 平面(随投影变) |
|---|---|---|
| 长度/周长 | d3.geoLength(obj) → 弧度 |
geoPath.measure(obj) → 像素 |
| 两点距离 | d3.geoDistance(a, b) → 弧度 |
—— |
| 面积 | d3.geoArea(obj) → 球面度 |
geoPath.area(obj) → 像素² |
| 包围盒 | d3.geoBounds(obj) → [[lon,lat],[lon,lat]] |
geoPath.bounds(obj) → 像素 |
| 质心 | d3.geoCentroid(obj) → [lon,lat] |
geoPath.centroid(obj) → 像素 |
单位换算一把尺:长度 ÷ 2π × 40075km = 公里;面积 ÷ 4π × 5.1 亿 km² = 平方公里。
geoPath.area 只有在等积投影下才与真实面积成比例——Mercator 里格陵兰比
南美洲「大」,球面量测揭穿这个谎言17。
三个球面小工具:geoInterpolate(a,b) 造两点间插值器(航线动画、tooltip 摆中点)、
geoRotation([yaw,pitch,roll]) 造旋转函数(带 invert)、geoContains(对象,点)
判定点落在哪个多边形里(悬停城市 → 高亮所属国)18。
8. 作者的判断与证据¶
| 判断 | 性质 | 依据 |
|---|---|---|
| 「比面积用等积投影」 | 硬规则 | 专题地图一节的投影选择13 |
| 「TopoJSON 文件小一个量级」 | 量化事实 | 924kB → 102kB 的同图对照3 |
| 「没有完美投影,只有取舍」 | 领域共识 | conformal/equal-area/equidistant 三分法8 |
9. 边界与局限¶
- 投影的数学(各投影的公式与推导)全书没有——一百个函数当目录用;
- 数据准备的 GIS 功夫(坐标系转换、shapefile 清洗、拓扑修复)明确推给离线工具与 mapshaper;
- d3-geo 不跟随 RFC 7946 全部细节(缠绕顺序 winding order),第 20 章给了修补工具清单;
- Canvas 地图的交互(取点、悬停)成本只提了一句,方案(geoContains/四叉树)散在练习里。
判断(我们的,不是书里的): 本篇反复出现一个模式:「数据 → 函数 → 图形」三段式 在地理数据上是最纯粹的——GeoJSON 是跨工具的标准中间格式,d3-geo 的每个函数 都是「吃标准、吐标准」的纯函数。这也是 D3 地图与「地图 SDK」(Leaflet/Mapbox) 的本质区别:D3 不给瓦片、不给底图样式,它给你的是投影与几何的计算内核; 想要现成的地图开发工具包(SDK,如 Leaflet/MapBox),书里也明说去找 d3-tile 或 Leaflet 集成。 如果错,会错在: 对「只要一张能缩放的交互地图」的需求,D3 是杀鸡用牛刀—— 瓦片方案的成本低一个量级;D3 地图的价值在「数据驱动」的部分(专题、流、自定义投影), 纯导航地图该用现成 SDK。这个边界书里散在几处,值得合并记住。
10. 可带走的¶
- GeoJSON 六种几何 + D3 的 Sphere;坐标永远是 [经度, 纬度];
- TopoJSON = 共享弧段,文件小 ~9 倍;
topojson.feature(数据, 数据.objects.key)转回; d3.geoPath().projection(投影)是渲染入口;datum 单要素、join features 数组;- pointRadius 画点、centroid 摆标签、context 切 Canvas;
- geoGraticule 经纬网(同 geoPath 自动适配投影)、geoCircle 球面圆(Tissot/晨昏线);
- 投影三大类(圆柱/圆锥/方位);保形 vs 等积 vs 等距;比面积必须等积;
- Tissot 圆:变大 = 面积失真,变扁 = 形状失真;
- 管道五段:rotate(球面)→ clipAngle → 算法 → center/translate/scale/angle(平面)→ clipExtent;
fitExtent/fitSize(视口, 对象)自动配比例,替代手算 center+scale;- choropleth:等积投影 + 幂次顺序色阶(exponent 0.25 压人口);
- 点密度 pointRadius 0.5;分级圆点 scaleSqrt + 小点在上层;
- 量测两套:球面(geoLength/geoArea/geoDistance,弧度制)与平面(geoPath 方法,像素);
- geoInterpolate/geoRotation/geoContains 三个球面小工具。
11. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| d3-geo 函数总表 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:94(搜「d3.geoPath()」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:97(搜「100+」) |
| GeoJSON 六类型 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:121(搜「GeoJSON geometries」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:127(搜「Point」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:149(搜「-35.16, -5.92」) |
| geoPath 与马达加斯加 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:160(搜「d3.geoPath()」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:201(搜「46.74, -19.32」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:206(搜「scale(1500)」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:301(搜「world-lowres.geojson」) |
| Sphere 类型 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:316(搜「Sphere」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:323(搜「lightblue」) |
| 投影调用与 invert | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:328(搜「different projections」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:336(搜「invert()」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:350(搜「geoOrthographic().rotate」) |
| 城市点位两种画法 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:364(搜「Placing points」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:410(搜「geocities.features」) |
| TopoJSON 与 topojson-client | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:423(搜「TopoJSON」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:429(搜「102 kB」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:459(搜「feature(data,key)」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:476(搜「tjdata.objects['world']」) |
| Canvas 渲染 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:485(搜「Canvas context」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:511(搜「forEach(feature」) |
| 经纬网与 geoCircle | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:524(搜「geoGraticule()」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:544(搜「step([0,90])」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:580(搜「geoCircle()」) |
| choropleth | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:620(搜「Creating a choropleth」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:629(搜「geoEqualEarth」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:637(搜「exponent(0.25)」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:662(搜「Promise.all」) |
| 点密度与分级圆点 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:694(搜「dot density」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:726(搜「pointRadius(.5)」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:752(搜「cities.sort」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:770(搜「scaleSqrt」) |
| 投影三大类与 Tissot | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:816(搜「Three main types」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:830(搜「Tissot」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:845(搜「conformal」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:873(搜「parallel(45)」) |
| 管道与 fit | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:913(搜「stream pipeline」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:985(搜「fitExtent(box,object)」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1004(搜「fitExtent([[50, 50]」) |
| 平面变换与裁剪 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:1017(搜「center([lon,lat])」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1057(搜「clipAngle(angle)」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1071(搜「clipAngle(20)」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1102(搜「geoInterruptedHomolosine」) |
| rotate(欧拉角) | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:1127(搜「rotate([yaw,pitch,roll])」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1133(搜「Euler angles」) |
| 量测工具两套 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:1157(搜「measuring tools」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1183(搜「radians」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1204(搜「40075」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1227(搜「510072000」) |
| 球面小工具 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:1258(搜「geoInterpolate(a,b)」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1259(搜「geoRotation」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1262(搜「geoContains」) |
| 章小结 | Cartographical Visualizations | text/27-ch19-chapter-19-cartographical-visualizations.txt:1856(搜「Summary」) |
-
出处:「Cartographical Visualizations」第 121–157 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:127,搜「Point」;:140,搜「Sphere」;:145,搜「longitude comes first」)。几何表照原书 Table 19.2 转写。 ↩ -
出处:「Cartographical Visualizations」第 316–326 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:323,搜「lightblue」;:1097,搜「clipping mask」)。 ↩ -
出处:「Cartographical Visualizations」第 423–483 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:429,搜「102 kB」;:459,搜「feature(data,key)」;:476,搜「tjdata.objects['world']」)。 ↩↩ -
出处:「Cartographical Visualizations」第 159–213 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:166,搜「d3.geoPath()」;:187,搜「M386.142」;:485,搜「Canvas context」;:511,搜「forEach(feature」)。 ↩ -
出处:「Cartographical Visualizations」第 198–213 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:201,搜「46.74, -19.32」;:206,搜「scale(1500)」)。 ↩ -
出处:「Cartographical Visualizations」第 522–578 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:530,搜「d3.geoGraticule()」;:544,搜「step([0,90])」;:564,搜「lines()」)。 ↩ -
出处:「Cartographical Visualizations」第 580–601 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:580,搜「geoCircle()」;:587,搜「radius, in degrees」)。 ↩ -
出处:「Cartographical Visualizations」第 809–858 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:816,搜「Three main types」;:830,搜「Tissot」;:845,搜「conformal」;:850,搜「gnomonic」)。 ↩↩ -
出处:「Cartographical Visualizations」第 859–909 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:867,搜「rotate([0,±90,0])」;:873,搜「parallel(45)」;:882,搜「geoAlbers」;:1094,搜「interrupted projections」)。 ↩ -
出处:「Cartographical Visualizations」第 911–927 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:913,搜「stream pipeline」;:917,搜「first stage rotates」)。 ↩ -
出处:「Cartographical Visualizations」第 1010–1147 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:1017,搜「center([lon,lat])」;:1024,搜「translate([x,y])」;:1126,搜「rotate([yaw,pitch,roll])」;:1132,搜「Euler angles」)。 ↩ -
出处:「Cartographical Visualizations」第 975–1007 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:985,搜「fitExtent(box,object)」;:1003,搜「fitExtent([[50, 50]」;:1249,搜「used in fitting methods」)。 ↩ -
出处:「Cartographical Visualizations」第 614–652 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:627,搜「equal-area projection」;:631,搜「linear scale would be a poor choice」;:637,搜「exponent(0.25)」;:646,搜「style("fill"」)。 ↩↩ -
出处:「Cartographical Visualizations」第 654–692 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:656,搜「prepare the GeoJSON」;:662,搜「Promise.all」;:676,搜「undefined」)。 ↩ -
出处:「Cartographical Visualizations」第 694–733 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:700,搜「geonameid」;:711,搜「MultiPoint」;:726,搜「pointRadius(.5)」)。 ↩ -
出处:「Cartographical Visualizations」第 735–790 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:752,搜「cities.sort」;:770,搜「scaleSqrt」;:771,搜「opacity」;:778,搜「pointRadius(d => d.properties」)。 ↩ -
出处:「Cartographical Visualizations」第 1157–1300 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:1174,搜「projection-independent」;:1205,搜「40075」;:1228,搜「510072000」;:1213,搜「equal-area projection」)。 ↩ -
出处:「Cartographical Visualizations」第 1253–1318 段(
text/27-ch19-chapter-19-cartographical-visualizations.txt:1258,搜「geoInterpolate(a,b)」;:1287,搜「geoRotation([20,0,0])」;:1307,搜「geoContains(country」)。 ↩