用 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], // 首尾闭合
],
],
};
如何绘制?
- 这不是 SVG 坐标,不能直接画;
- 必须用投影函数
projection()
把[经度, 纬度]
转成[x, y]
; - 然后用
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 的 FeatureCollection
、Polygon
、MultiLineString
等类型,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.js | D3 主库,支持数据驱动的 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
会自动生成包括scale
和translate
的变换矩阵; 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 path
的 d
属性字符串,从而实现可视化绘制。
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()
渲染; - 样式为灰色虚线,表示经线纬线;
- 网格线不会遮挡地图,可提供更直观的地理参考。