Three.js案例
用 D3 绘制地图的核心利器

用 D3 绘制地图的核心利器

在数据可视化领域,D3.js 几乎是绕不开的存在。而在 D3 的众多模块中,d3-geo 是专门处理地理数据投影与路径生成的核心工具。

d3-geo 是 D3.js 的一个子模块,专注于:

  • 地理投影(projection)转换,例如墨卡托投影(Mercator)、正射投影(Orthographic)等;
  • 地理路径生成器(path generator),将 GeoJSON/TopoJSON 转为 SVG path;
  • 计算地理形状,例如大圆路径、最近点、面积等;
  • 辅助函数,如地理坐标插值、距离计算、角度转换等。

简而言之:它是地图可视化的底层引擎。

核心概念解析

d3-geo 之所以强大,是因为它抽象出了地理可视化的五大核心构件:地理投影、路径生成器、地理图形、插值算法、地理运算

1. 地理投影(Geo Projection)

作用:将球面地理坐标 [经度, 纬度] 转换为平面坐标 [x, y]

这是地图绘制中最基础也最关键的一步。d3-geo 提供了丰富的投影函数,如:

  • geoMercator():墨卡托投影(常用于 Web 地图)
  • geoOrthographic():正射投影(地球仪效果)
  • geoNaturalEarth1():自然地理比例,更美观
  • geoConicEqualArea():等面积圆锥投影
  • geoAzimuthalEquidistant():等距方位投影(适合雷达)

d3-geo 中,投影函数如 geoNaturalEarth1() 接收 [经度, 纬度] 作为输入,输出 [x, y] 的屏幕坐标。

const projection = d3
  .geoNaturalEarth1()
  .scale(150)
  .center([0, 30]) // 平移中心点
  .rotate([0, 0]) // 地球旋转
  .translate([width / 2, height / 2]); // 投影到 SVG 居中

核心理解:

  • .center([0, 30]) 意味着经度 0、纬度 30 是投影的中心点
  • .translate([width / 2, height / 2]) 把这个中心点映射到 SVG 的正中心。
  • 其他地理坐标会根据自然地理投影的比例关系,被压缩变形后映射到 [x, y]

投影函数本质上就是:

[x, y] = projection([lon, lat]);

我们假设:

const width = 960;
const height = 500;

示例输出对照表

经纬度 [lon, lat]projection([lon, lat]) 输出结果 [x, y](近似值)
[0, 30][480, 250](即 SVG 中心)
[0, 0][480, ~376]
[90, 0][619, ~376]
[-90, 0][340, ~376]
[180, 0][755, ~376]
[-180, 0][204, ~376]
[0, 90][480, ~113](靠近北极)
[0, -90][480, ~640](靠近南极)

注意:这些坐标是根据 geoNaturalEarth1 投影公式大致推算的,具体像素位置会受到投影类型非线性变化影响。

2. 地理路径生成器(Geo Path)

作用:把地理数据(GeoJSON),转换成 SVG 路径字符串,直接绘图。

假设你有一块地理区域,比如一个矩形:

它横跨经度 120 到 130,纬度 30 到 40,就像地图上的一个小方块。

我们用 GeoJSON 语言描述它:

const geojson = {
  type: "Polygon",
  coordinates: [
    [
      [120, 30],
      [130, 30],
      [130, 40],
      [120, 40],
      [120, 30], // 首尾闭合
    ],
  ],
};

如何绘制?

  1. 这不是 SVG 坐标,不能直接画;
  2. 必须用投影函数 projection()[经度, 纬度] 转成 [x, y]
  3. 然后用 path() 把转换后的坐标,拼成 SVG 能读懂的 d 字符串。

用代码串起来:

const projection = d3.geoNaturalEarth1().scale(150).center([0, 30]).translate([400, 200]); // 居中到 SVG
const path = d3.geoPath().projection(projection);

此时:

const d = path(geojson);
console.log(d);
// 输出:M585.2,188.4L634.1,188.4L634.1,162.3L585.2,162.3Z

这个字符串是 SVG <path> 元素的 d 属性值,它的含义是:

  • [120,30] 开始(被投影为 [585.2,188.4]
  • 依次连线到 [130,30][130,40][120,40]
  • 最后闭合(Z 表示闭合路径)

实际绘图代码如下:

svg
  .append("path")
  .datum(geojson)
  .attr("d", path) // ← 自动转为 SVG path 字符串
  .attr("fill", "#66bb6a")
  .attr("stroke", "#333");

3. 地理图形(Geo Shape)

GeoJSON 的 FeatureCollectionPolygonMultiLineString 等类型,d3-geo 都能支持渲染。

支持类型包括:

  • Point
  • MultiPoint
  • LineString
  • MultiLineString
  • Polygon
  • MultiPolygon
  • GeometryCollection

每种类型都能通过 path 自动转为 SVG 路径,无需手写计算逻辑

4. 地理插值(Geo Interpolation)

作用:在两个地理坐标之间,找到“中间位置”。用于绘制动态路径,比如飞线动画。

举个例子

假设你要画一架飞机,从中国上海 [120, 30] 飞往哈萨克斯坦阿拉木图 [100, 50],你希望看到飞机慢慢移动的轨迹,那就需要计算出:

  • 0% 时的位置(刚起飞)
  • 50% 时的位置(飞到一半)
  • 100% 时的位置(飞到终点)

这个时候,你可以使用:

const interpolate = d3.geoInterpolate([120, 30], [100, 50]);

这个 interpolate 是一个函数,输入一个比例值(0 到 1),输出当前位置的经纬度。

示例代码

const interpolate = d3.geoInterpolate([120, 30], [100, 50]);
 
console.log(interpolate(0.0)); // → 起点 [120, 30]
console.log(interpolate(0.5)); // → 中点 [110, 40]
console.log(interpolate(1.0)); // → 终点 [100, 50]

这个函数帮你处理了球面上的插值(也叫“大圆插值”),而不是简单的线性连线,这样路线才会像真实地球上的飞行路径。

5. 地理运算(Geo Utilities)

除了投影与渲染,d3-geo 还提供了一系列几何工具方法,适用于复杂的地理计算,例如:

  • d3.geoDistance(a, b):计算两点之间的大圆距离(弧度)
  • d3.geoLength(geojson):计算路径长度
  • d3.geoArea(geojson):计算多边形面积(球面单位)
  • d3.geoCentroid(geojson):获取图形质心(球心中心点)

举个例子,计算中国的面积和质心:

const area = d3.geoArea(chinaGeoJson);
const centroid = d3.geoCentroid(chinaGeoJson);

这些计算对构建数据驱动的地图分析型可视化至关重要。

案例:实现可缩放的世界地图

准备工作:引入依赖

在 HTML 中加入以下两段 <script>

<!-- D3.js 主库,支持地图绘制、缩放交互、路径生成等功能 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
 
<!-- TopoJSON 工具库,用于将 TopoJSON 转换为 GeoJSON -->
<script src="https://d3js.org/topojson.v3.min.js"></script>

为何引入这两个库?

依赖名称功能说明
d3.v7.min.jsD3 主库,支持数据驱动的 SVG 地图绘制
topojson.v3.min.js将 TopoJSON 格式转为 GeoJSON,便于渲染

HTML 与 SVG 容器结构

<body> 中插入一个 SVG,作为地图的渲染容器:

<svg width="960" height="500"></svg>

后续地图所有元素都会绘制在这个 SVG 内。

D3 实现过程详解

1. 基础配置

在 SVG 内部添加一个 <g> 标签,用来容纳所有地理图形,并为之后的缩放和平移操作做准备。

const width = 960;
const height = 500;
 
const svg = d3.select("svg");
const g = svg.append("g"); // 所有地图图形绘制在 group 内
  • 我们通过 d3.select() 选择 HTML 中的 SVG 元素;
  • 在其内部添加一个 <g> 元素(SVG 中的“图层”组);
  • 后续国家边界、经纬网格线、标注等都会添加在这个 <g> 中;
  • 最关键的是:缩放操作的变换(transform)也将绑定在 <g> 上,一步变动,整体生效。

2. 启用缩放和平移功能

允许用户通过鼠标滚动对地图进行缩放,通过拖拽实现地图移动。

const zoom = d3
  .zoom()
  .scaleExtent([1, 8]) // 最小 1 倍,最大 8 倍缩放
  .on("zoom", (event) => {
    g.attr("transform", event.transform);
  });
 
svg.call(zoom); // 绑定 zoom 行为
  • d3.zoom() 是 D3 内置的缩放器;
  • scaleExtent([1, 8]) 控制缩放级别;
  • 事件 zoom 中,event.transform 会自动生成包括 scaletranslate 的变换矩阵;
  • g.attr("transform", event.transform) 将缩放和平移应用到地图 <g> 层,实现整体变换;
  • 最终效果是:你滚动滚轮时地图会放大缩小,点击拖动时地图会跟随鼠标移动。

3. 设定地理投影方式

使用墨卡托投影将 [经度, 纬度] 映射为 [x, y] 像素坐标。

const projection = d3
  .geoMercator()
  .scale(140) // 控制地图大小
  .translate([width / 2, height / 1.5]); // 居中显示
  • 地球是球形,SVG 是平面,需要“投影”来做几何转换;
  • geoMercator() 是一种投影方式,能将经纬度平铺为 2D 地图(Web 地图最常见);
  • scale() 控制地图大小,translate() 控制地图居中位置;
  • 所有国家边界(经纬度坐标)都将通过这个投影函数转换成屏幕坐标。

想要换投影类型,只需把 geoMercator() 替换为如 geoNaturalEarth1()geoOrthographic() 等其他投影函数即可。

4. 构建路径生成器

将 GeoJSON/TopoJSON 中的地理图形结构,转换为 SVG pathd 属性字符串,从而实现可视化绘制。

const path = d3.geoPath().projection(projection);
  • d3.geoPath() 是一个路径构建器;
  • 它读取 GeoJSON 数据结构(如多边形、线);
  • 搭配投影函数后,能够将每个地理点映射为 [x, y],再生成类似 M...L...Z 的 SVG 路径;
  • 可用于 .attr("d", path),直接绘制国家边界、经纬网格等。

5. 加载并绘制地图数据

从远程加载世界地图数据(TopJSON 格式),解析为 GeoJSON,逐一渲染每个国家边界。

d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json").then(worldData => {
  const countries = topojson.feature(worldData, worldData.objects.countries).features;
 
  g.selectAll("path")
    .data(countries)
    .join("path")
    .attr("class", "country")
    .attr("d", path)
    .attr("fill", d => `hsl(${Math.random() * 360}, 70%, 80%)`)
    .attr("stroke", "#fff")
    .attr("stroke-width", 0.5)
  • d3.json() 加载地图数据(这是 TopoJSON 格式,体积更小);
  • 使用 topojson.feature() 方法将 TopoJSON 解码为标准 GeoJSON;
  • 遍历国家数组,每个国家是一个多边形;
  • 使用 .attr("d", path) 自动计算边界路径;
  • 使用 hsl() 随机上色,确保每个国家有不同颜色,可在后续换成数据驱动的配色方案。

6. 添加国家交互高亮

当用户将鼠标悬停在某个国家时,高亮显示其边界;移开时恢复原样。

.on("mouseover", function (event, d) {
  d3.select(this)
    .attr("stroke", "darkorange")
    .attr("stroke-width", 1);
})
.on("mouseout", function () {
  d3.select(this)
    .attr("stroke", "#fff")
    .attr("stroke-width", 0.5);
});
  • mouseover 事件触发时,我们选中当前国家路径元素;
  • 修改它的 stroke 颜色与宽度,让它“高亮”;
  • mouseout 时恢复默认样式;
  • 这个交互体验对于用户识别地图区域非常有用,尤其在地图密集或配色较淡时。

7. 添加经纬度网格线(Graticule)

在地图上绘制规则的经纬线网格,帮助用户理解地理位置与比例关系。

g.append("path")
  .datum(d3.geoGraticule())
  .attr("class", "graticule")
  .attr("d", path)
  .attr("fill", "none")
  .attr("stroke", "#ccc")
  .attr("stroke-width", 0.3)
  .attr("stroke-dasharray", "2,2");
  • d3.geoGraticule() 生成一张虚拟网格数据(GeoJSON 格式);
  • 使用 path() 渲染;
  • 样式为灰色虚线,表示经线纬线;
  • 网格线不会遮挡地图,可提供更直观的地理参考。