d3 地理投影完全指南:从 projection(point) 到 fitExtent 与自定义投影的完整实践
2026/9/15 18:27:06 网站建设 项目流程

d3 地理投影完全指南:从 projection(point) 到 fitExtent 与自定义投影的完整实践

【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3

本篇基于 d3 官方文档中的投影(Projections)章节,系统讲解 d3-geo 中投影的完整 API 体系:点投影与反投影、流式投影管线、球面/视口裁剪、缩放与旋转等几何变换、自动适配的 fit 系列方法,以及用 geoProjection / geoProjectionMutator 自定义投影的进阶手法。读完你不仅能把任意 GeoJSON 正确投影渲染到 SVG/Canvas,还能按地理学原理派生出自己的新投影。

1. 投影是什么:从球面多边形到平面多边形

投影(Projection)把球面上的多边形几何(经纬度坐标)转换为平面上的多边形几何(像素坐标)。在 d3 中,投影是连接"地理数据"与"屏幕像素"的桥梁:所有投影都实现统一的接口——点变换、反向点变换与流式变换(stream),再由 geoPath 消费其输出生成 SVG path 数据或绘制到 Canvas。

当前仓库为 d3 v7.9.0 的主包(见 package.json),它通过依赖d3-geo: ^3.1.1提供全部地理能力,并在 src/index.js 中export * from "d3-geo"d3.geoXxx系列 API 暴露给使用者。文档侧对应 docs/d3-geo/ 目录下的各篇说明。

d3 内置实现了三大类标准投影,各自成篇:

  • 方位投影(Azimuthal):把球面直接投到平面上,如geoAzimuthalEqualArea()(方位等积)、geoAzimuthalEquidistant()(方位等距)、geoGnomonic()(正射/斜射)、geoOrthographic()(正视)、geoStereographic()(立体);
  • 圆锥投影(Conic):把球面投到圆锥再展开,有两条"标准纬线"参数,如geoConicConformal()(圆锥等角,默认 parallels 为 [30°, 30°] 时退化为平顶)、geoConicEqualArea()(Albers 等积圆锥)、geoConicEquidistant()(圆锥等距)、geoAlbers()geoAlbersUsa()(美国专用的复合投影,阿拉斯加按 0.35× 缩小面积,因此不支持 center/rotate/clipAngle/clipExtent);
  • 圆柱投影(Cylindrical):把球面投到包裹圆柱再展开,如geoEquirectangular()(等距圆柱/平板投影)、geoMercator()(球面墨卡托,默认 clipExtent 使世界投影到正方形、纬度约 ±85° 处裁剪)、geoTransverseMercator()(横轴墨卡托)、geoEqualEarth()(等积)、geoNaturalEarth1()(伪圆柱,Neither 等积也非等角,小比例尺世界图美观)。

需要更多投影时,可以使用独立生态中的 d3-geo-projection / d3-geo-polygon 等扩展包;也可以按本文第 7 节的方法实现自定义投影。

2. 点投影与反投影:projection(point) 与 projection.invert(point)

最基础的用法是把单个经纬度点投影为像素点:

projection([116.4, 39.9]); // => [x, y](通常单位是像素)

规则:

  • 输入point必须是两元素数组 [longitude, latitude],单位为
  • 返回新的数组 [x, y](通常单位是像素),代表该点的投影位置;
  • 若该点没有定义的投影位置(例如落在投影的裁剪范围之外),可能返回 null——因此在批量投影时应当做 null 判断。

可逆投影还实现projection.invert(point)

  • 输入投影后的点 [x, y](像素),输出 [longitude, latitude](度);
  • 同样可能返回 null;
  • 该方法只存在于可逆投影上——不可逆投影(例如某些复合投影)调用会报错,使用前可以用"invert" in projection之类的结构判断。

这两个方法是"点级"接口,适合做坐标拾取、tooltip 定位等离散操作;而真正渲染整幅地图时用的是流式接口(下一节),因为流式接口避免了中间表示物化,开销更低。

3. projection.stream:流式投影管线

projection.stream(stream)返回一个"投影流":对指定的输出stream包装后,任何输入几何都会先投影、再流式写入输出流。一次典型的投影包含多级几何变换,按顺序为:

  1. 输入几何先转换为弧度
  2. 沿三轴旋转(对应rotate);
  3. 裁剪:小圆裁剪(small circle)或沿反经线(antimeridian)切割;
  4. 最后投影到平面,伴随自适应重采样(adaptive resampling)、缩放(scale)与平移(translate)。

流(Stream)本身是一组必须实现的方法(详见 Streams 文档):point(x, y, z)lineStart()/lineEnd()polygonStart()/polygonEnd()sphere()。流是有状态的:一个 point 的含义取决于它是否处于 lineStart/lineEnd 之间,线(line)与环(ring)则由 polygonStart 区分。例如输入一个 GeoJSON Polygon,投影流上会发生这样的方法序列:

stream.polygonStart(); stream.lineStart(); stream.point(0, 0); stream.point(0, 1); stream.point(1, 1); stream.point(1, 0); // 注意:GeoJSON 里冗余的闭合坐标不会被 point 再传一次,而是由 lineEnd 隐含 stream.lineEnd(); stream.polygonEnd();

这个流接口是 d3 地理模块"低层但高效"的关键:geoPath、geoArea、geoBounds 等都是不同的输出流实现,共用同一条投影管线。

4. 裁剪体系:preclip、postclip、clipAngle、clipExtent

裁剪分两个阶段,对应投影管线中不同的位置:

4.1 球面裁剪 preclip / clipAngle

projection.preclip(preclip):设置(或不带参数时返回)投影的球面裁剪函数。preclip是一个接收投影流、返回裁剪后流的函数,常用于沿反经线切割沿小圆裁剪

projection.clipAngle(angle)是其最常用的封装:

  • 指定angle(度):把投影的裁剪圆半径设为以投影中心为圆心、半径为angle的小圆,返回投影本身(链式调用);
  • 指定 null:切换为沿反经线切割模式(而非小圆裁剪);
  • 不指定:返回当前裁剪角,默认 null。

小圆裁剪与视口裁剪(clipExtent)相互独立,二者可同时生效。例如方位投影文档中的示例geoGnomonic().clipAngle(74 - 1e-4)(见 docs/d3-geo/azimuthal.md)就是用小圆把地图限制在可视范围内,- 1e-4是消除浮点边界毛刺的惯用写法。

d3 提供两个现成的球面裁剪函数:

  • geoClipAntimeridian:把跨越反经线的线/多边形切成两段、各在一侧,通常用作前裁剪(pre-clipping);
  • geoClipCircle(angle):生成以小圆为界的裁剪函数,小圆半径为angle、圆心为投影中心,通常用作前裁剪。

4.2 平面裁剪 postclip / clipExtent

projection.postclip(postclip):设置(或返回)投影的笛卡尔(平面)裁剪函数。后裁剪发生在投影到平面之后,用于把投影限制到某个范围(如矩形视口)。

projection.clipExtent(extent)是其封装:

  • extent指定为 [[x₀, y₀], [x₁, y₁]](像素),x₀ 为视口左侧、y₀ 为顶部、x₁ 为右侧、y₁ 为底部;
  • 指定 null 表示不做视口裁剪;
  • 不指定参数时返回当前视口裁剪范围,默认 null。

对应的平面裁剪函数是geoClipRectangle(x0, y0, x1, y1):生成把几何限制在矩形 [[x0, y0], [x1, y1]] 内的裁剪函数,通常用作后裁剪(post-clipping)。圆柱投影文档中geoMercator()的"默认 clipExtent 使世界投影到正方形"正是用到了它(见 docs/d3-geo/cylindrical.md)。

一个常用套路是"小圆裁剪 + 略宽于画布的矩形裁剪"叠加,例如geoStereographic().clipAngle(135 - 1e-4).clipExtent([[-1, -1], [width + 1, height + 1]]):小圆决定"看多大范围",矩形裁剪负责把越界的线条干净地切掉。

5. 几何变换:scale、translate、center、rotate、angle 与反射

投影返回的像素坐标 = 原始投影结果经过"旋转 → 缩放 → 平移"之后的值。以下 API 均可链式调用,且都能"设值/取值"两用(带参数设置并返回投影,不带参数返回当前值):

API说明默认值
projection.scale(s)缩放因子,与投影点间距离成线性关系;但不同投影之间绝对缩放值不等价因投影而异
projection.translate([tx, ty])平移偏移,决定投影中心在像素坐标系中的位置[480, 250],即把 ⟨0°,0°⟩ 放在 960×500 区域中央
projection.center([λ, φ])投影中心(经纬度,度)⟨0°, 0°⟩
projection.rotate([λ, φ, γ])三轴球面旋转,角度单位度,γ(roll)可省略;对应 yaw / pitch / roll 三轴[0, 0, 0]
projection.angle(a)投影之后的平面旋转角(度)。注意:渲染时旋转(如 Canvas 的 context.rotate)通常比投影时旋转更快
projection.reflectX(r)是否对输出的 x 维取反。可用于天球/天文数据的"仰视"显示(北朝上时赤经向东指向左)false
projection.reflectY(r)是否对输出的 y 维取反。用于把"正 y 向上"的空间参考系(如标准地理坐标系)转换到"正 y 向下"的 Canvas/SVG 显示坐标系false
projection.precision(p)自适应重采样阈值(像素),对应 Douglas–Peucker 距离;调小得到更平滑的线条但点更多√0.5 ≈ 0.70710…

其中rotate是最常用也最直观的变换:给方位投影传rotate([110, -40])等价于"把视角转到东经 110°、南纬 40°"。本仓库文档站的 WorldMap 组件 就实现了一个可交互的三轴旋转演示:鼠标水平拖动时用projection.rotate([rotate[0] + (x1 - x0) / width * 20, rotate[1], rotate[2]])增量修改 λ 轴并用requestAnimationFrame节流重绘(见 WorldMap.vue 的rerender函数),随后统一调用path(outline)path(graticule)path(feature)刷新三条 path。这个组件同时展示了文档中各投影示例(如geoAzimuthalEqualArea().rotate([110, -40]).fitExtent([[1, 1], [width - 1, height - 1]], {type: "Sphere"}))在真实运行环境里的形态。

关于precision的取舍:自适应重采样是投影把"球面上的弧"变成"平面折线"的机制——采样越密,折线越逼近真实曲线。文档里几乎所有世界地图示例都带.precision(0.2),比默认值更精细;绘制小尺寸缩略图时可以保持默认甚至调大以节省点数。

6. 自动适配:fitExtent、fitSize、fitWidth、fitHeight

手动调 scale/translate 很痛苦,d3 提供了一组"给定一个 GeoJSON 对象,自动算出合适的 scale 与 translate"的便捷方法:

projection.fitExtent([[x0, y0], [x1, y1]], object); // 把 object 居中放进 extent projection.fitSize([width, height], object); // 等价于 fitExtent([[0, 0], [width, height]], object) projection.fitWidth(width, object); // 高度按 object 的宽高比自动确定 projection.fitHeight(height, object); // 宽度按 object 的宽高比自动确定

几个重要细节:

  • 计算新的 scale/translate 时忽略当前已设置的 clipExtent
  • 用于计算object包围盒的 precision 是在有效缩放 150下计算的——意味着包围盒的精度与最终 scale 无关,fit 的结果稳定可复现。

官方文档给出的横轴墨卡托(新泽西州 State Plane)配置示例值得完整保留,它同时演示了 rotate + fitExtent 的组合:

var projection = d3.geoTransverseMercator() .rotate([74 + 30 / 60, -38 - 50 / 60]) // 中心经度西经 74°30′、纬度北纬 38°50′ .fitExtent([[20, 20], [940, 480]], nj); // 在 960×500 画布中留 20px 内边距

而在本仓库的组件里可以看到更简洁的惯用形态——直接对{type: "Sphere"}做 fit,把"整个可见地球"适配进画布(见 docs/d3-geo/azimuthal.md 中的投影示例字符串):

d3.geoAzimuthalEquidistant() .rotate([110, -40]) .fitExtent([[1, 1], [width - 1, height - 1]], {type: "Sphere"}) .precision(0.2)

fitWidth/fitHeight则适合"响应式"场景:容器宽度已知时约束宽,高度由数据宽高比推出,反之亦然。

7. 自定义投影:Raw Projections、geoProjection 与 geoProjectionMutator

7.1 Raw projection 的约定

Raw projection(原始投影)是点变换函数,是自定义投影的底层积木,通常传给geoProjectiongeoProjectionMutator。约定:

  • 输入lambda(λ,经度)、phi(φ,纬度),单位是弧度(不是度!);
  • 输出 [x, y],通常是无量纲的"单位投影"坐标(以原点为中心的近似单位方内);
  • 接口为project(lambda, phi),若暴露project.invert(x, y),则包装后的投影也自动获得projection.invert

Raw projection 故意保持"裸露",一方面方便派生相关投影(改公式即可),另一方面它不需要自己做缩放/平移(由 scale/translate/center 自动施加),也不需要做球面旋转(rotate 会先于投影应用)。

7.2 geoProjection(project):一步包装

geoProjection(project)用给定的 raw projection 构造一个完整投影。最经典的例子是球面墨卡托:

var mercator = d3.geoProjection(function(x, y) { return [x, Math.log(Math.tan(Math.PI / 4 + y / 2))]; });

两行核心公式就完成了墨卡托的纬度拉伸y = ln(tan(π/4 + φ/2));旋转、缩放、裁剪、流接口全部由包装器补齐。若该函数没有invert,得到的投影也不支持反投影。

7.3 geoProjectionMutator(factory):带参数的可变异投影

圆锥投影有两个可配置的标准纬线,这类"带参数"的投影要用geoProjectionMutator(factory):它接收一个raw projection 工厂函数(factory 必须返回一个 raw projection),返回一个mutate函数——每当参数变化时调用它,即可把投影内部使用的 raw projection 重新赋值,mutate 的返回值就是包装后的投影。

文档给出的两段完整示例(工厂 + 可变异投影)是理解这套模式的最佳材料:

// y0 和 y1 代表两条标准纬线 function conicFactory(phi0, phi1) { return function conicRaw(lambda, phi) { return […, …]; // 圆锥投影的坐标公式 }; }
function conicCustom() { var phi0 = 29.5, phi1 = 45.5, mutate = d3.geoProjectionMutator(conicFactory), projection = mutate(phi0, phi1); projection.parallels = function(_) { return arguments.length ? mutate(phi0 = +_[0], phi1 = +_[1]) : [phi0, phi1]; }; return projection; }

要点:自定义 accessorparallels([a, b])在设值时重新调用mutate,从而在不重建投影对象的前提下替换内部 raw projection——之前设置的 scale/translate/clip 等状态保留。惯例上mutate函数本身不对外暴露。d3 内置的geoConicEqualAreaRawgeoConicConformalRaw等(见 docs/d3-geo/conic.md)就是这种工厂形态;geoAlbers()本质上是对geoConicEqualArea的"美国中心"预设配置。

8. 超越投影:geoTransform 与 geoIdentity

8.1 geoTransform(methods)

geoTransform(methods)methods对象上定义的方法实现任意变换;未定义的方法走"直通"实现(把输入原样转发给输出流)。文档中的两个例子分别覆盖了"点级变换"与"仿射矩阵":

// 反射 y 维(等价于 projection.reflectY 的流式写法) const reflectY = d3.geoTransform({ point(x, y) { this.stream.point(x, -y); } });
// 仿射矩阵变换 function matrix(a, b, c, d, tx, ty) { return d3.geoTransform({ point(x, y) { this.stream.point(a * x + b * y + tx, c * x + d * y + ty); } }); }

从源码结构看,变换与投影共享同一套流协议:geoTransform 返回的对象实现了projection.stream,因此可以传给path.projection(...)直接参与渲染;但它只实现了投影接口的一个子集,语义上表达的是"任意几何变换"而非"球面到平面的投影"。

8.2 geoIdentity()

geoIdentity()恒等变换,用于对平面几何直接做缩放、平移与裁剪。它实现的投影方法包括:scale、translate、fitExtent、fitSize、fitWidth、fitHeight、clipExtent、angle、reflectX、reflectY。典型用途是处理本来就"在平面上"的数据(本地平面坐标、建筑图、遥感影像像素坐标):

const projection = d3.geoIdentity().reflectY(true).scale(10).translate([0, height]); const path = d3.geoPath(projection);

9. 与 geoPath 组合:从投影到渲染

投影本身只产坐标/流,真正把地图画出来要靠 geoPath。组合方式按渲染目标分两种:

const path = d3.geoPath(projection); // SVG:返回 path 数据字符串 const path = d3.geoPath(projection, context); // Canvas:绘制到 2D context

path 支持 Point / MultiPoint / LineString / MultiLineString / Polygon / MultiPolygon / GeometryCollection / Feature / FeatureCollection,以及特殊的Sphere类型(无坐标,渲染地球轮廓)。多个 feature 可包进一个 FeatureCollection 用单条 path 渲染(快),或按 feature join 成多条 path(利于交互与样式)。

本仓库 WorldMap.vue 组件是"投影 + path + 裁剪 + 旋转"全链路的最小真实实现,值得完整阅读:

const outline = {type: "Sphere"}; const graticule = d3.geoGraticule10(); // render() 内部: const path = d3.geoPath(projection); function update() { svg.selectAll("[name='outline']").attr("d", path(outline)); // 地球轮廓(受 clipAngle 影响) svg.selectAll("[name='graticule']").attr("d", path(graticule)); // 10° 经纬网格 svg.selectAll("[name='feature']").attr("d", path(feature)); // 陆地 }

配合模板中的四条<path>(填充轮廓、经纬网、陆地、描边轮廓),整个组件仅用投影的 rotate + fitExtent + precision + clip 系列 API 就完成了交互式世界地图——这正是第 5、6 节 API 的落地形态。

10. 实战速查

需求推荐 API 组合
世界地图,整球可见fitExtent([[1,1],[w-1,h-1]], {type:"Sphere"})+ 合适的投影(等积/伪圆柱)
某区域地图,自动适配fitSize([w, h], geojsonObject)(fit 时忽略 clipExtent,包围盒按有效 scale 150 计算)
只限宽或限高(响应式)fitWidth/fitHeight,另一边按对象宽高比推出
小圆窗口式地图(正射、斜射)clipAngle(θ - 1e-4)定范围 + 略宽画布的clipExtent切边
跨反经线国家(俄罗斯、太平洋岛国)默认反经线切割(clipAngle 为 null 时),或自定义 preclip
换个视角看地球rotate([λ, φ, γ]),文档组件里有拖动增量旋转的现成写法
平面坐标数据geoIdentity()+ scale/translate/clipExtent
自定义投影geoProjection(rawFn);带参数则geoProjectionMutator(factory)
点在屏幕外/裁剪区外projection(point)/invert(point)返回 null,渲染前需判空

最后提醒两个容易踩的坑:其一,raw projection 的输入输出单位是弧度,而projection(point)rotatecenter等对外 API 全部是,二者不要混用;其二,scale 的绝对值在不同投影之间不可比,换投影后应重新用 fit 系列方法适配,而不是沿用旧 scale。按本文的管线理解——"弧度化 → 三轴旋转 → 小圆/反经线前裁剪 → 投影 + 自适应重采样 → scale/translate → 平面后裁剪"——绝大多数投影配置问题都能定位到具体是哪一级出了问题。

参考路径:docs/d3-geo/projection.md(本文主体依据)、docs/d3-geo/stream.md、docs/d3-geo/path.md、docs/d3-geo/azimuthal.md、docs/d3-geo/conic.md、docs/d3-geo/cylindrical.md、docs/components/WorldMap.vue、src/index.js、package.json。

【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询