编程语言研究书库

地图(上)— GeoJSON、投影与专题地图

地图(上)— 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一个或多个环(首环外边界、其余是洞)行政区、带湖的国土
MultiPolygonPolygon 数组本土 + 岛屿的国家
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.geoMercator().fitExtent([[50,50],[w-50,h-50]], feature);

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. 可带走的

  1. GeoJSON 六种几何 + D3 的 Sphere;坐标永远是 [经度, 纬度];
  2. TopoJSON = 共享弧段,文件小 ~9 倍;topojson.feature(数据, 数据.objects.key) 转回;
  3. d3.geoPath().projection(投影) 是渲染入口;datum 单要素、join features 数组;
  4. pointRadius 画点、centroid 摆标签、context 切 Canvas;
  5. geoGraticule 经纬网(同 geoPath 自动适配投影)、geoCircle 球面圆(Tissot/晨昏线);
  6. 投影三大类(圆柱/圆锥/方位);保形 vs 等积 vs 等距;比面积必须等积;
  7. Tissot 圆:变大 = 面积失真,变扁 = 形状失真;
  8. 管道五段:rotate(球面)→ clipAngle → 算法 → center/translate/scale/angle(平面)→ clipExtent;
  9. fitExtent/fitSize(视口, 对象) 自动配比例,替代手算 center+scale;
  10. choropleth:等积投影 + 幂次顺序色阶(exponent 0.25 压人口);
  11. 点密度 pointRadius 0.5;分级圆点 scaleSqrt + 小点在上层;
  12. 量测两套:球面(geoLength/geoArea/geoDistance,弧度制)与平面(geoPath 方法,像素);
  13. geoInterpolate/geoRotation/geoContains 三个球面小工具。

11. 原文地图

主题原书章原文位置
d3-geo 函数总表Cartographical Visualizationstext/27-ch19-chapter-19-cartographical-visualizations.txt:94(搜「d3.geoPath()」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:97(搜「100+」)
GeoJSON 六类型Cartographical Visualizationstext/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 Visualizationstext/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 Visualizationstext/27-ch19-chapter-19-cartographical-visualizations.txt:316(搜「Sphere」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:323(搜「lightblue」)
投影调用与 invertCartographical Visualizationstext/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 Visualizationstext/27-ch19-chapter-19-cartographical-visualizations.txt:364(搜「Placing points」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:410(搜「geocities.features」)
TopoJSON 与 topojson-clientCartographical Visualizationstext/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 Visualizationstext/27-ch19-chapter-19-cartographical-visualizations.txt:485(搜「Canvas context」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:511(搜「forEach(feature」)
经纬网与 geoCircleCartographical Visualizationstext/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()」)
choroplethCartographical Visualizationstext/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 Visualizationstext/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」)
投影三大类与 TissotCartographical Visualizationstext/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)」)
管道与 fitCartographical Visualizationstext/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 Visualizationstext/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 Visualizationstext/27-ch19-chapter-19-cartographical-visualizations.txt:1127(搜「rotate([yaw,pitch,roll])」) · text/27-ch19-chapter-19-cartographical-visualizations.txt:1133(搜「Euler angles」)
量测工具两套Cartographical Visualizationstext/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 Visualizationstext/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 Visualizationstext/27-ch19-chapter-19-cartographical-visualizations.txt:1856(搜「Summary」)

Footnotes

  1. 出处:「Cartographical Visualizations」第 121–157 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:127,搜「Point」;:140,搜「Sphere」;:145,搜「longitude comes first」)。几何表照原书 Table 19.2 转写。 ↩

  2. 出处:「Cartographical Visualizations」第 316–326 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:323,搜「lightblue」;:1097,搜「clipping mask」)。 ↩

  3. 出处:「Cartographical Visualizations」第 423–483 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:429,搜「102 kB」;:459,搜「feature(data,key)」;:476,搜「tjdata.objects['world']」)。 ↩ ↩2

  4. 出处:「Cartographical Visualizations」第 159–213 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:166,搜「d3.geoPath()」;:187,搜「M386.142」;:485,搜「Canvas context」;:511,搜「forEach(feature」)。 ↩

  5. 出处:「Cartographical Visualizations」第 198–213 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:201,搜「46.74, -19.32」;:206,搜「scale(1500)」)。 ↩

  6. 出处:「Cartographical Visualizations」第 522–578 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:530,搜「d3.geoGraticule()」;:544,搜「step([0,90])」;:564,搜「lines()」)。 ↩

  7. 出处:「Cartographical Visualizations」第 580–601 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:580,搜「geoCircle()」;:587,搜「radius, in degrees」)。 ↩

  8. 出处:「Cartographical Visualizations」第 809–858 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:816,搜「Three main types」;:830,搜「Tissot」;:845,搜「conformal」;:850,搜「gnomonic」)。 ↩ ↩2

  9. 出处:「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」)。 ↩

  10. 出处:「Cartographical Visualizations」第 911–927 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:913,搜「stream pipeline」;:917,搜「first stage rotates」)。 ↩

  11. 出处:「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」)。 ↩

  12. 出处:「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」)。 ↩

  13. 出处:「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"」)。 ↩ ↩2

  14. 出处:「Cartographical Visualizations」第 654–692 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:656,搜「prepare the GeoJSON」;:662,搜「Promise.all」;:676,搜「undefined」)。 ↩

  15. 出处:「Cartographical Visualizations」第 694–733 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:700,搜「geonameid」;:711,搜「MultiPoint」;:726,搜「pointRadius(.5)」)。 ↩

  16. 出处:「Cartographical Visualizations」第 735–790 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:752,搜「cities.sort」;:770,搜「scaleSqrt」;:771,搜「opacity」;:778,搜「pointRadius(d => d.properties」)。 ↩

  17. 出处:「Cartographical Visualizations」第 1157–1300 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:1174,搜「projection-independent」;:1205,搜「40075」;:1228,搜「510072000」;:1213,搜「equal-area projection」)。 ↩

  18. 出处:「Cartographical Visualizations」第 1253–1318 段(text/27-ch19-chapter-19-cartographical-visualizations.txt:1258,搜「geoInterpolate(a,b)」;:1287,搜「geoRotation([20,0,0])」;:1307,搜「geoContains(country」)。 ↩

On this page