使用 D3.js 绘制 GeoJSON 地理地图与热力图
本文详细讲解如何使用 D3.js 处理 GeoJSON 矢量地理数据,通过投影函数与路径生成器绘制地理边界,并分别实现区域分级着色热力图与离散点密度热力图,涵盖交互缩放与工程调优。
一、GeoJSON 格式与 D3 地理投影核心机制
要在 Web 页面上绘制地图,首先必须了解地理数据载体 GeoJSON。它是一种基于 JSON 语法的开放地理空间数据交换格式,顶层通常为 FeatureCollection(要素集合),包含若干个 Feature。每个要素内部拥有几何信息 geometry(如 Polygon 多边形、MultiPolygon 复合多边形、Point 经纬度点)以及附带的业务属性 properties(如地区名称、行政区划编码等)。
地球表面是一个三维球面,经纬度坐标由经度(Longitude, -180° ~ 180°)和纬度(Latitude, -90° ~ 90°)组成;而屏幕渲染则是二维平面的笛卡尔坐标系。D3.js 解决这一转换的核心依赖于两个模块:
- 地理投影函数(d3.geoProjection):例如最常用的
d3.geoMercator()(墨卡托投影)或d3.geoAlbers()(阿伯斯等面积投影)。它负责将经纬度坐标[lon, lat]计算并输出为画布上的像素坐标[x, y]。 - 路径生成器(d3.geoPath):接收配置好的投影函数作为参数,解析 GeoJSON 中的各类几何体,并将其转换为 SVG 路径的绘制语法指令(即
<path d="M... L... Z">)。
[经度 Longitude, 纬度 Latitude] 的顺序排列,对应数学坐标系中的 [X, Y]。中文日常口语常说“纬经度”,如果直接写成 [lat, lon],绘制出来的地图会产生 90 度旋转并严重畸变。
二、搭建基础地图渲染工程
在实际编写代码时,为了让地图根据容器大小自适应居中,不需要反复手动调试 center 和 scale,D3.js 提供了极其便利的 fitSize API。只需传入容器宽高和 GeoJSON 数据,D3 会自动计算最佳比例尺与平移偏移量。
下面是构建一个响应式 SVG 地图容器并异步加载 GeoJSON 数据进行基础多边形绘制的关键流程:
基础地图绘制完整代码示例
// 1. 初始化容器尺寸与 SVG
const width = 800;
const height = 500;
const svg = d3.select("#map-container")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.style("width", "100%")
.style("height", "auto");
// 2. 创建主图层分组,便于后续平移缩放 (Zoom)
const mapGroup = svg.append("g").attr("class", "map-layer");
// 3. 定义墨卡托投影与路径生成器
const projection = d3.geoMercator();
const pathGenerator = d3.geoPath().projection(projection);
// 4. 异步加载 GeoJSON 地图数据
d3.json("geojson-data.json").then((geoData) => {
// 自动根据容器尺寸与地理边界调整中心点与缩放级别
projection.fitSize([width, height], geoData);
// 5. 绑定要素数据并渲染 SVG path
mapGroup.selectAll("path.district")
.data(geoData.features)
.join("path")
.attr("class", "district")
.attr("d", pathGenerator)
.attr("fill", "#E2E8F0")
.attr("stroke", "#4F6272")
.attr("stroke-width", 1)
.attr("vector-effect", "non-scaling-stroke");
});
上述代码中设置了 vector-effect: non-scaling-stroke,其关键作用是确保后续在地图放大或缩小交互时,边界描边线条保持恒定的物理粗细,而不会随着视图缩放变得粗重失真。
三、实现区域分级着色热力图(Choropleth Map)
在政务统计、销售大屏或疫情态势展示中,最常见的“热力图”本质上是区域分级统计图(Choropleth Map)。它的特点是将每个行政区域(多边形)赋予一个聚合标量(如各区县的人口总数、气温均值、销售总额),并通过连续色阶(Color Scale)将该数值映射为区域多边形的填充颜色。
1. 属性数据与地理几何体的关联
通常业务指标来自于后端独立的接口或 JSON/CSV,与 GeoJSON 的几何文件是分离的。最佳做法是将业务数据转换为以 adcode(行政区划代码)或区域名称为键的 Map 字典,在 D3 遍历 features 时进行 O(1) 复杂度的快速查询:
// 假设业务指标数组: [ { id: "110101", name: "东城区", value: 85.6 }, ... ]
const dataMap = new Map(businessData.map(d => [d.id, d.value]));
// 获取极值用于构建色阶域 (Domain)
const values = businessData.map(d => d.value);
const colorScale = d3.scaleSequential()
.domain([d3.min(values), d3.max(values)])
.interpolator(d3.interpolateBlues); // 蓝阶渐变插值器
2. 绑定颜色与鼠标交互提示框
// 创建全局悬浮 Tooltip 容器
const tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("padding", "8px 12px")
.style("background", "rgba(15, 23, 42, 0.85)")
.style("color", "#FFFFFF")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("display", "none");
// 渲染并绑定色阶与鼠标事件
mapGroup.selectAll("path.district")
.data(geoData.features)
.join("path")
.attr("class", "district")
.attr("d", pathGenerator)
.attr("fill", d => {
const val = dataMap.get(d.properties.adcode);
return val !== undefined ? colorScale(val) : "#E2E8F0"; // 无数据时兜底灰
})
.attr("stroke", "#FFFFFF")
.attr("stroke-width", 0.8)
.on("mouseover", function(event, d) {
d3.select(this)
.attr("stroke", "#F59E0B")
.attr("stroke-width", 2)
.raise(); // 提升层级,避免描边被临近图元遮盖
const val = dataMap.get(d.properties.adcode) ?? "无数据";
tooltip.style("display", "block")
.html(`<strong>${d.properties.name}</strong><br/>数值: ${val}`);
})
.on("mousemove", function(event) {
tooltip.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function() {
d3.select(this)
.attr("stroke", "#FFFFFF")
.attr("stroke-width", 0.8);
tooltip.style("display", "none");
});
四、实现点要素空间密度热力图(Point Density Heatmap)
与分级着色图受制于行政边界不同,另一种业务常见场景是:拥有成千上万个离散经纬度点(如网约车实时接单位置、交通事故高发点、基站信号上报)。这类数据需要呈现无视边界的平滑空间热力分布。
在纯 SVG 环境下实现离散点热力图,最优雅轻量的方式是利用 <radialGradient>(径向渐变)并叠加透明度(Opacity)与混合模式。当多个热力点在空间上临近时,半透明径向辐射会自然发生累加,形成中心红橙、边缘蓝绿的经典热力晕染效果。
SVG 径向渐变点热力实现代码
// 1. 在 SVG defs 中声明热力径向渐变元
const defs = svg.append("defs");
const radialGrad = defs.append("radialGradient")
.attr("id", "heat-spot-grad")
.attr("cx", "50%").attr("cy", "50%").attr("r", "50%");
radialGrad.append("stop").attr("offset", "0%").attr("stop-color", "#EF4444").attr("stop-opacity", 0.8);
radialGrad.append("stop").attr("offset", "40%").attr("stop-color", "#F97316").attr("stop-opacity", 0.5);
radialGrad.append("stop").attr("offset", "75%").attr("stop-color", "#EAB308").attr("stop-opacity", 0.2);
radialGrad.append("stop").attr("offset", "100%").attr("stop-color", "#3B82F6").attr("stop-opacity", 0);
// 2. 创建热力图专用层 (置于基础地图层之上)
const heatLayer = svg.append("g").attr("class", "heatmap-layer");
// 3. 假设离散经纬度采样点: [ [116.40, 39.90, weight], ... ]
const pointData = [
{ lon: 116.4074, lat: 39.9042, weight: 1.0 },
{ lon: 116.4150, lat: 39.9120, weight: 0.8 },
{ lon: 116.3980, lat: 39.8980, weight: 0.9 },
// ... 其他点要素
];
// 4. 遍历点数据并使用投影函数转换为平面坐标
heatLayer.selectAll("circle.heat-spot")
.data(pointData)
.join("circle")
.attr("class", "heat-spot")
.attr("cx", d => projection([d.lon, d.lat])[0])
.attr("cy", d => projection([d.lon, d.lat])[1])
.attr("r", d => 25 * (d.weight || 1)) // 半径受权重影响
.attr("fill", "url(#heat-spot-grad)")
.style("mix-blend-mode", "screen"); // 应用滤色混合模式增强叠加质感
五、缩放平移交互与工程调优架构
地图可视化必须支持鼠标滚轮缩放与按住平移(Pan & Zoom)。在 D3 中,推荐使用 d3.zoom(),将缩放行为挂载到根 SVG 上,并在 zoom 回调中统一更新主包装分组 <g> 的 transform 属性。
// 配置平移缩放行为
const zoom = d3.zoom()
.scaleExtent([1, 10]) // 限制最小与最大缩放倍数
.translateExtent([[0, 0], [width, height]]) // 限制视界不能拖出画布区域
.on("zoom", (event) => {
// 统一变换地图层与热力图层所在的根包装组
mapGroup.attr("transform", event.transform);
heatLayer.attr("transform", event.transform);
});
svg.call(zoom);
核心工程化建议与避坑要点
| 技术环节 | 典型问题 / 陷阱 | 标准排查与优化对策 |
|---|---|---|
| 数据预处理 | 全国或省级 GeoJSON 文件体积动辄数十兆,造成网页首屏加载阻塞。 | 使用 TopoJSON 格式替代原生 GeoJSON,消除共用边界冗余,或通过 mapshaper 抽稀简化至原始大小的 10%~20%。 |
| 坐标转换 | 从高德/百度采集的数据直接绘制,与标准 WGS84 边界出现偏移。 | 统一进行坐标系转换(如 GCJ-02 / BD-09 转 WGS84),确保数据投影底座严格一致。 |
| 渲染性能 | 在 SVG 中渲染超过 5000 个高频热力点时出现严重掉帧卡顿。 | 对超大规模点数据,改用 HTML5 Canvas 绘制热力图层,或使用 d3-hexbin 空间网格聚合后再按六边形渲染。 |
| 视口适配 | 窗口大小改变(Resize)后地图被截断或偏离视野中心。 | 监听窗口 ResizeObserver,重新执行 projection.fitSize 并更新所有已存在的 path 与 circle。 |
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38








