D3.js可视化地图难吗?投影算法怎么写?
D3.js可视化地图难吗?投影算法怎么写? 这个问题通常出现在两类场景:一是前端同学拿到 GeoJSON 后不知道为什么地图不显示或位置跑偏;二是 GIS 同学想用 D3.js 自定义地图效果,却被投影、坐标转换和路径生成绕晕。本文用一个可复现的流程,把 D3.js 地图可视化的核心逻辑讲清楚:数据是什么坐标、投影做了什么、D3.js 如何把经纬度变成屏幕像素,以及什么时候需要自己写投影算法。

引言:D3.js可视化地图到底难在哪里
D3.js 做地图并不一定难,真正容易出错的是“地理坐标到屏幕坐标”的转换。普通柱状图只需要把数值映射到像素位置,而地图多了一层空间参考:GeoJSON 通常存储的是经纬度坐标,浏览器屏幕使用的是平面像素坐标,二者不能直接画在一起。
所以,D3.js可视化地图的关键不是会不会写 SVG,而是能不能理解下面三件事:
- 数据坐标是什么:GeoJSON 中的坐标通常是经度、纬度,例如
[116.39, 39.90]。 - 投影算法做什么:把球面或椭球面上的经纬度转换成二维平面坐标。
- D3.js 如何渲染:用
d3.geoPath()把投影后的坐标生成 SVG 的path。
如果只是做中国省界、世界地图、城市边界展示,通常不需要自己写投影算法,直接使用 D3.js 内置投影就够了。如果你要做特殊投影、局部工程坐标转换、游戏地图坐标或非标准地图,才需要理解并编写自定义投影。
背景:为什么GeoJSON不能直接画成地图
很多人第一次用 D3.js 加载 GeoJSON 时,会遇到几个典型问题:
- 地图没有显示,SVG 里有路径但看不到。
- 地图显示在画布外面,只露出一角。
- 中国地图被压扁、拉伸或方向不对。
- 点位和行政区边界对不上。
- 使用 Web Mercator 底图时,D3.js 绘制的矢量图层发生偏移。
这些问题大多不是 D3.js 本身的问题,而是坐标系和投影没有处理好。GeoJSON 标准常见使用 WGS84 经纬度坐标,也就是经度在前、纬度在后。浏览器中的 SVG 坐标则是左上角为原点,向右是 x 增大,向下是 y 增大。
经纬度坐标不能直接当作像素坐标使用。例如北京的经纬度约为 [116.39, 39.90],如果直接作为 SVG 的 x=116.39、y=39.90,它只是屏幕左上角附近的一个点,而不是地图上的正确位置。
原理:D3.js投影算法怎么把经纬度变成像素
D3.js 地图投影的基本输入是经纬度,输出是屏幕坐标。可以理解为:
[经度, 纬度] --投影函数--> [x像素, y像素]
在 D3.js 中,投影对象通常这样创建:
const projection = d3.geoMercator()
.center([104, 35])
.scale(800)
.translate([width / 2, height / 2]);
这段代码里有三个非常重要的参数:
- center:指定投影中心,经常用于把目标区域移动到画布中间。
- scale:控制地图缩放大小,值越大地图越大。
- translate:控制投影结果在 SVG 画布中的平移位置。
D3.js 内置投影会负责数学变换。以常见的墨卡托投影为例,它的大致思想是:
x = R * λ
y = R * ln(tan(π / 4 + φ / 2))
其中 λ 是经度弧度,φ 是纬度弧度,R 是缩放相关系数。这个公式不是最终屏幕坐标,因为 D3.js 还会继续应用 scale、translate、裁剪等处理。
因此,实际开发中不建议手动把经纬度套公式再画 SVG。更稳妥的方法是让 d3.geoProjection 或 D3.js 内置投影处理转换,然后用 d3.geoPath() 生成路径。
步骤:用D3.js完成一个GeoJSON地图可视化
步骤1:准备正确的GeoJSON数据
地图边界数据建议使用 GeoJSON。一个行政区面要素通常长这样:
{
"type": "Feature",
"properties": {
"name": "示例区域"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[116.1, 39.7],
[116.5, 39.7],
[116.5, 40.1],
[116.1, 40.1],
[116.1, 39.7]
]
]
}
}
检查 GeoJSON 时重点看三点:
- 坐标顺序是否为
[经度, 纬度],不要写成[纬度, 经度]。 - 数据范围是否正常,例如中国范围经度大约在 73 到 135,纬度大约在 18 到 54。
- 面要素是否闭合,即 Polygon 的首尾坐标是否一致。
步骤2:创建SVG容器
const width = 900;
const height = 600;
const svg = d3.select("#map")
.append("svg")
.attr("width", width)
.attr("height", height);
这里的 #map 是页面上的容器。真实项目中,可以根据父元素宽度动态设置 width 和 height,但初学阶段建议先使用固定尺寸,方便排查投影问题。
步骤3:选择合适的D3.js地图投影
如果绘制中国、省市行政区或一般 Web 地图专题图,可以先从 d3.geoMercator() 开始:
const projection = d3.geoMercator()
.center([104, 35])
.scale(750)
.translate([width / 2, height / 2]);
如果绘制世界地图,可以使用:
const projection = d3.geoNaturalEarth1()
.scale(160)
.translate([width / 2, height / 2]);
如果绘制局部城市地图,center 应设置为该城市附近的经纬度。例如北京可以使用:
const projection = d3.geoMercator()
.center([116.4, 39.9])
.scale(60000)
.translate([width / 2, height / 2]);
局部城市地图需要更大的 scale,否则地图会非常小。
步骤4:用geoPath生成SVG路径
const path = d3.geoPath().projection(projection);
d3.geoPath() 的作用是把 GeoJSON 的点、线、面坐标转换成 SVG 可绘制的路径字符串。你不需要自己遍历每一个坐标点生成 M、L、Z 命令。
步骤5:加载并绘制GeoJSON
d3.json("/data/china-provinces.geojson").then(function(geojson) {
svg.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("d", path)
.attr("fill", "#dceeff")
.attr("stroke", "#3388cc")
.attr("stroke-width", 1);
svg.selectAll("text")
.data(geojson.features)
.enter()
.append("text")
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.attr("font-size", 12)
.attr("fill", "#333")
.text(function(d) {
return d.properties.name;
});
});
这段代码完成了两个任务:先绘制行政区边界,再用 path.centroid(d) 计算每个区域的近似中心点,用于放置名称标注。
步骤6:自动适配地图范围
手动调 center 和 scale 很容易反复试错。更推荐的方式是根据 GeoJSON 自动计算合适的缩放和平移。
d3.json("/data/china-provinces.geojson").then(function(geojson) {
const projection = d3.geoMercator();
const path = d3.geoPath().projection(projection);
projection.fitSize([width, height], geojson);
svg.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("d", path)
.attr("fill", "#dceeff")
.attr("stroke", "#3388cc")
.attr("stroke-width", 1);
});
projection.fitSize([width, height], geojson) 会根据数据范围自动设置投影参数。这是 D3.js可视化地图中非常实用的方法,尤其适合行政区边界、流域边界和项目范围图。
步骤:自定义D3.js投影算法怎么写
多数情况下不需要自己写投影算法。但如果你确实要实现一个简单投影,可以使用 d3.geoProjection()。它接收一个“原始投影函数”,输入经纬度弧度,输出平面坐标。
示例1:写一个简单的等经纬投影
等经纬投影也可以理解为把经度映射到 x,把纬度映射到 y。它不适合严肃量测,但适合解释投影函数的基本结构。
function rawEquirectangular(lambda, phi) {
return [lambda, phi];
}
const projection = d3.geoProjection(rawEquirectangular)
.scale(150)
.translate([width / 2, height / 2]);
const path = d3.geoPath().projection(projection);
这里的 lambda 和 phi 都是弧度,不是角度。D3.js 会把 GeoJSON 中的经纬度角度转换为弧度后传给原始投影函数。
示例2:给自定义投影增加反算函数
如果需要鼠标点击屏幕坐标后反查经纬度,就需要提供 invert 方法。下面是一个简化示例:
function rawEquirectangular(lambda, phi) {
return [lambda, phi];
}
rawEquirectangular.invert = function(x, y) {
return [x, y];
};
const projection = d3.geoProjection(rawEquirectangular)
.scale(150)
.translate([width / 2, height / 2]);
有了反算能力后,可以通过下面的方式把鼠标像素位置转换回经纬度:
svg.on("click", function(event) {
const point = d3.pointer(event);
const lonlat = projection.invert(point);
console.log(lonlat);
});
注意,真实投影的反算公式通常比正算公式更复杂。对于生产项目,不建议随意自定义投影,除非你非常清楚数据的坐标系统和数学变换。
常见坑:D3.js地图不显示、跑偏、变形怎么排查
坑1:经纬度顺序写反
GeoJSON 坐标顺序是 [经度, 纬度]。很多来自表格的数据会写成纬度在前、经度在后。如果顺序写反,中国数据可能会跑到异常位置,甚至完全看不到。
快速判断方法:
- 中国经度通常是 73 到 135。
- 中国纬度通常是 18 到 54。
- 如果第一列大量是 30、40,第二列大量是 110、120,很可能写反了。
坑2:GeoJSON不是WGS84经纬度
D3.js 的地理投影通常假定输入是经纬度。如果你的数据来自投影坐标系,例如 CGCS2000 高斯投影、Web Mercator 米制坐标或地方坐标,直接传给 d3.geoMercator() 会出错。
排查方法是查看坐标数值:
- 经纬度通常是几十到一百多。
- Web Mercator 坐标通常是几百万到上千万。
- 高斯投影坐标也通常是数十万或数百万。
如果数据不是经纬度,应先用 QGIS、GDAL、ArcGIS Pro 或后端服务转换到 WGS84,再交给 D3.js 渲染。
坑3:scale太小或center不合适
地图不显示不一定是数据错误,可能只是被投影到了画布外。初学者可以优先使用 fitSize,减少手动调参。
projection.fitSize([width, height], geojson);
如果必须手动设置参数,可以先打印数据范围,确认中心点是否接近目标区域。
坑4:与在线底图叠加时坐标不一致
如果你用 D3.js 绘制 SVG 图层,同时叠加 Leaflet、OpenLayers 或 Mapbox 底图,需要特别注意投影体系。Web 地图底图通常使用 Web Mercator,前端地图框架会负责经纬度到瓦片坐标的转换。
这种情况下,不建议单独使用一个固定的 D3.js 投影画覆盖物。更稳妥的做法是:
- 让地图框架管理视图范围、缩放级别和坐标转换。
- D3.js 只负责在覆盖层中绘制 SVG 或 Canvas。
- 每次地图缩放、平移后,重新计算要素的屏幕位置。
坑5:行政区简化后拓扑出现裂缝
为了提高前端性能,GeoJSON 常会做简化处理。但如果简化方法不当,相邻行政区边界可能出现缝隙或重叠。建议使用保持拓扑关系的工具处理,例如 TopoJSON 工作流,或在 GIS 软件中谨慎设置简化容差。
方法比较:内置投影、自定义投影和GIS预处理怎么选
| 方法 | 适用场景 | 优点 | 注意事项 |
|---|---|---|---|
| D3.js内置投影 | 世界地图、中国地图、省市行政区专题图 | 上手快,代码少,和 geoPath 配合稳定 | 需要确认输入数据是经纬度坐标 |
| projection.fitSize自动适配 | 单个区域、项目范围、任意GeoJSON边界 | 不用反复调整 scale 和 translate | 多图层叠加时要共用同一个投影 |
| 自定义d3.geoProjection | 特殊投影、教学演示、非标准坐标变换 | 灵活,可完全控制投影逻辑 | 需要理解弧度、正算、反算和变形问题 |
| GIS软件预处理 | 坐标转换、裁剪、简化、拓扑修复 | 结果可控,适合生产数据处理 | 前端仍需选择合适投影进行显示 |
| Leaflet或OpenLayers叠加D3 | 需要在线底图、缩放、平移交互的WebGIS | 底图和交互能力强,D3负责可视表达 | D3图层必须跟随地图视图更新 |
简单总结:普通专题地图先用 D3.js 内置投影;地图大小位置不好调时用 fitSize;需要和在线底图联动时,让 Leaflet 或 OpenLayers 管坐标;只有在特殊需求下才写自定义投影算法。
检查清单:发布前确认D3.js地图可视化是否可靠
- 数据格式:确认 GeoJSON 能被正常解析,
features数组存在。 - 坐标顺序:确认坐标为
[经度, 纬度]。 - 坐标范围:确认不是米制投影坐标误当成经纬度。
- 投影选择:中国或局部区域可从
d3.geoMercator()开始。 - 范围适配:优先尝试
projection.fitSize()。 - 图层一致:边界、点位、线数据必须使用同一个投影。
- 标注位置:用
path.centroid()时注意狭长、多岛、多面区域可能不理想。 - 性能优化:大 GeoJSON 应做简化、切片或按需加载。
- 交互验证:鼠标悬停、高亮、点击查询要确认绑定的是正确要素。
- 底图叠加:与 WebGIS 底图叠加时,要确认投影和视图更新机制一致。
FAQ:D3.js可视化地图常见问题
D3.js可视化地图难吗?
如果只是绘制 GeoJSON 行政区边界、做颜色分级图或添加简单交互,D3.js可视化地图不算难。难点主要在投影、坐标系和数据预处理。掌握 d3.geoMercator()、d3.geoPath() 和 projection.fitSize() 后,大多数基础地图都可以完成。
D3.js投影算法一定要自己写吗?
不一定。实际项目中,大多数地图可视化直接使用 D3.js 内置投影即可。只有在特殊投影、非标准坐标、教学演示或自定义地图坐标系统中,才需要自己写投影算法。
为什么我的GeoJSON地图在D3.js里不显示?
常见原因有四个:GeoJSON 路径加载失败、坐标不是经纬度、投影参数导致地图在画布外、SVG 容器宽高为 0。建议先检查浏览器控制台,再使用 projection.fitSize() 自动适配数据范围。
D3.js可以直接加载Shapefile吗?
D3.js 通常不直接加载 Shapefile。建议先用 QGIS、GDAL、ArcGIS Pro 或后端脚本把 Shapefile 转成 GeoJSON 或 TopoJSON。前端直接处理 Shapefile 会增加依赖和复杂度,不利于网页加载性能。
D3.js地图和Leaflet地图有什么区别?
D3.js 更擅长自定义可视化表达,例如复杂符号、统计图形和 SVG 交互。Leaflet 更擅长 WebGIS 地图浏览,例如瓦片底图、缩放、平移和图层管理。如果项目需要在线底图,通常用 Leaflet 管地图,用 D3.js 做覆盖层可视化。
点数据和面数据为什么对不上?
最常见原因是两份数据坐标系不一致,或其中一份经纬度顺序写反。点、线、面图层必须使用同一坐标基准和同一投影函数。绘制时也要确保所有图层共用同一个 projection。
D3.js地图需要考虑坐标系吗?
需要。虽然 D3.js 不像专业 GIS 软件那样完整管理坐标参考系统,但它的地图投影默认处理经纬度数据。如果输入数据是投影坐标,必须先转换,否则地图位置和形状都会出错。
结论:先用内置投影解决问题,再理解投影算法
D3.js可视化地图的核心不是把每个坐标点手动画出来,而是建立正确的流程:准备 GeoJSON,经纬度坐标进入投影函数,d3.geoPath() 生成 SVG 路径,最后根据业务需求添加颜色、标注和交互。
对于 GIS 学生、初级 GIS 工程师和 WebGIS 开发者,建议先掌握 D3.js 内置投影和 fitSize 自动适配,再进一步学习自定义 d3.geoProjection。这样既能快速完成实际地图可视化,又能逐步理解投影算法背后的坐标转换逻辑。
如果你的目标是生产级 WebGIS,不要把所有空间处理都放在前端。坐标转换、裁剪、简化和拓扑修复更适合在 QGIS、GDAL、PostGIS 或后端服务中完成,D3.js 则专注于把已经处理好的空间数据表达得更清楚、更易交互。