使用 D3.js 绘制 GeoJSON 地理地图与热力图

作者:闭包旅人

时间:2026-10-30

浏览:620

本文详细讲解如何使用 D3.js 处理 GeoJSON 矢量地理数据,通过投影函数与路径生成器绘制地理边界,并分别实现区域分级着色热力图与离散点密度热力图,涵盖交互缩放与工程调优。

文章目录
前端地理可视化指南
技术栈: D3.js v7 / GeoJSON / SVG 主题: 地理投影、区域着色、空间点热力
在前端数据可视化项目中,将地理边界数据与业务指标结合呈现是最常见的需求之一。利用 D3.js 提供的投影矩阵(Projection)与地理路径生成器(geoPath),开发者可以将球体经纬度准确映射到网页平面像素,绘制出高精度的基础地图;进而通过区域分级着色(Choropleth)或离散点空间密度热力(Point Density Heatmap),直观呈现数据的地域聚集特征。本文将详细演示这一全流程开发步骤、完整代码实现与关键避坑注意事项。
使用 D3.js 绘制 GeoJSON 地理地图与热力图
图 0: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">)。
使用 D3.js 绘制 GeoJSON 地理地图与热力图
图 1:GeoJSON 地理数据经由 D3 投影与路径生成器转换到 SVG DOM 的流水线
重要注意:经纬度坐标顺序约定
根据 GeoJSON 国际规范(RFC 7946),坐标数组必须按照 [经度 Longitude, 纬度 Latitude] 的顺序排列,对应数学坐标系中的 [X, Y]。中文日常口语常说“纬经度”,如果直接写成 [lat, lon],绘制出来的地图会产生 90 度旋转并严重畸变。

二、搭建基础地图渲染工程

在实际编写代码时,为了让地图根据容器大小自适应居中,不需要反复手动调试 center 和 scale,D3.js 提供了极其便利的 fitSize API。只需传入容器宽高和 GeoJSON 数据,D3 会自动计算最佳比例尺与平移偏移量。

下面是构建一个响应式 SVG 地图容器并异步加载 GeoJSON 数据进行基础多边形绘制的关键流程:

使用 D3.js 绘制 GeoJSON 地理地图与热力图
图 2:D3.js 地理地图与热力图构建操作流程图

基础地图绘制完整代码示例

// 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)

与分级着色图受制于行政边界不同,另一种业务常见场景是:拥有成千上万个离散经纬度点(如网约车实时接单位置、交通事故高发点、基站信号上报)。这类数据需要呈现无视边界的平滑空间热力分布。

使用 D3.js 绘制 GeoJSON 地理地图与热力图
图 3:区域分级统计图(Choropleth)与点密度热力图(Point Density)原理与实现对比

在纯 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);
使用 D3.js 绘制 GeoJSON 地理地图与热力图
图 4:D3 地理可视化分层架构与生产上线自查清单

核心工程化建议与避坑要点

技术环节 典型问题 / 陷阱 标准排查与优化对策
数据预处理 全国或省级 GeoJSON 文件体积动辄数十兆,造成网页首屏加载阻塞。 使用 TopoJSON 格式替代原生 GeoJSON,消除共用边界冗余,或通过 mapshaper 抽稀简化至原始大小的 10%~20%。
坐标转换 从高德/百度采集的数据直接绘制,与标准 WGS84 边界出现偏移。 统一进行坐标系转换(如 GCJ-02 / BD-09 转 WGS84),确保数据投影底座严格一致。
渲染性能 在 SVG 中渲染超过 5000 个高频热力点时出现严重掉帧卡顿。 对超大规模点数据,改用 HTML5 Canvas 绘制热力图层,或使用 d3-hexbin 空间网格聚合后再按六边形渲染。
视口适配 窗口大小改变(Resize)后地图被截断或偏离视野中心。 监听窗口 ResizeObserver,重新执行 projection.fitSize 并更新所有已存在的 path 与 circle。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章