首页
/ d3 密度估计详解:contourDensity 二维核密度估计与等高线绘制

d3 密度估计详解:contourDensity 二维核密度估计与等高线绘制

2026-09-05 12:34:31作者:晏闻田Solitary

本文围绕 d3 仓库中的密度估计文档 density.md 展开,系统讲解 d3.contourDensity() 的完整 API——包括坐标访问器、单元格大小、阈值生成、高斯核带宽等全部配置项——并说明密度估计结果的 GeoJSON 输出如何经由 d3.geoPath 渲染。读完后你将能够独立实现一个可避免大数据集过度重叠(overplotting)的密度等高线图,并理解 d3-contour 从点云到等高线多边形的底层计算链路。

什么是密度估计,为什么需要等高线

当数据集包含数万乃至数百万个点时,直接绘制散点图会导致严重的过度重叠:点的颜色互相覆盖,真实的数据分布反而被掩盖。密度等高线通过估计点云在各处的单位面积点数(number of points per square pixel),用一圈圈嵌套的等高线区域来呈现密度分布——内圈密度高、外圈密度低——从而在一张图中同时表达位置关系与密集程度。

d3 通过 contourDensity 方法实现了快速的两维核密度估计(two-dimensional kernel density estimation)。其典型应用场景包括:

  • Old Faithful 热泉的喷发时长与间隔时长的关系散点密度图;
  • 53,940 颗钻石的重量与价格关系密度图——这个量级的散点用普通 scatterplot 几乎无法分辨,而密度等高线清晰呈现出价格随重量递增的带状分布。

密度估计是 d3-contour 模块的两大能力之一;另一个是面向数值网格的通用等高线生成器 contours。两者共享同一套 marching squares 算法与 GeoJSON 输出格式,区别在于:contours 直接接收一个数值网格,而 contourDensity 接收原始点云,先自行把点分箱(binning)到网格上再做核平滑,最后再走同样的等高线提取流程。

在 d3 仓库中的位置

contourDensity 并非本仓库源码直接实现,而是来自上游 d3-contour 包的再导出。可以从仓库结构确认这一事实链:

  • package.json 声明依赖 "d3-contour": "^4.0.2",且发布版本为 d3 7.9.0;
  • src/index.jsexport * from "d3-contour";contourDensitycontours 等 API 原样提升到顶层 d3 命名空间;
  • test/d3-test.js 逐模块校验 “d3 exports everything from d3-contour”,确保任何 d3-contour 的导出都能从 d3 上直接访问;
  • docs/api.md 的 API 索引中列出了 d3.contourDensity*density*.x / y / weight / size / cellSize / thresholds / bandwidth / contours 共 9 个条目,即下文将逐一讲解的完整方法面。

contourDensity():构造密度估计器

contourDensity() 以默认设置构造一个新的密度估计器,返回一个可直接调用、也可继续链式配置的对象:

const density = d3.contourDensity()
    .x(d => d[0])
    .y(d => d[1])
    .size([width, height])
    .thresholds(20);

density(data):计算并返回 GeoJSON 等高线

调用 density(data) 时,对给定的数据数组 data 估计密度,返回一个 GeoJSON MultiPolygon 几何对象数组(该链接为文档原文引用的外部规范,仅作格式说明)。

关键语义:

  • 每个几何对象代表“估计密度 ≥ 对应阈值”的区域。第 i 个几何对象对应第 i 个阈值;每个几何对象自身的阈值暴露为 geometry.value 属性,可直接用于填充色阶映射;
  • 坐标来源:每个数据点的 x、y 坐标分别由 density.xdensity.y 访问器计算;
  • 权重density.weight 指定每个数据点的相对贡献,默认均为 1;
  • 精度边界:生成的等高线只在估计器定义的 size 范围内准确。

返回的 MultiPolygon 通常直接交给 d3.geoPath 渲染,投影使用 nullgeoIdentity(因为密度结果本身就是像素平面坐标,不需要地理投影)。一个最小渲染示例:

import {contourDensity, geoPath, scaleSequential, interpolateViridis} from "d3";

const density = contourDensity()
    .x(d => xScale(d.weight))
    .y(d => yScale(d.price))
    .size([width, height])
    .thresholds(20);

const polygons = density(diamonds);

const svg = ...;
svg.append("g")
    .selectAll("path")
    .data(polygons)
    .join("path")
    .attr("d", geoPath(null))          // null 投影 = 恒等变换
    .attr("fill", d => color(d.value)); // 用 geometry.value 驱动色阶

density.x(x) 与 density.y(y):坐标访问器

若指定参数 x / y,则设置坐标访问器;否则返回当前访问器。二者默认值分别是:

function x(d) {
  return d[0];
}
function y(d) {
  return d[1];
}

也就是说,默认情况下数据点被假定为 [x, y] 二元组数组。若数据是对象(如 {weight, price}),必须像上例那样显式设置访问器。访问器在每次调用 density(data) 时对每个点求值,因此可以依赖闭包中的比例尺把原始数据单位转换为像素单位。

density.weight(weight):点权重

weight 访问器决定每个数据点在密度场中的相对贡献,默认实现为:

function weight() {
  return 1;
}

即每个点贡献相同。当数据点本身携带强度信息(如样本置信度、事件计数、金额)时,可写成 .weight(d => d.count),等高线将反映加权后的密度而非简单点数。

density.size(size):估计器范围

size 指定为 [width, height] 形式的数组,其中 width 是 x 方向最大值、height 是 y 方向最大值,通常与 SVG/Canvas 的绘图区域尺寸一致。未指定时返回当前 size,默认值为 [960, 500]。再次强调:密度估计结果只在该 size 范围内有效,超出该范围的数据点不参与计算,渲染时也应把等高线裁剪到同一边界内。

density.cellSize(cellSize):网格粒度

cellSize 设置底部分箱网格中单个单元格的大小,取值必须是正整数,未指定时默认值为 4。两个关键细节:

  1. 单元格大小会向下取整到最近的 2 的幂(如传入 5 实际生效为 4,传入 10 生效为 8)——这一设计让核平滑可以借助位运算在 2 的幂大小的块上快速进行;
  2. 更小的 cell 产生更精细的等高线多边形,但计算成本更高

从这一行为可以推断实现上存在明显的精度/速度权衡旋钮:数据点分布平滑时默认的 4 已经足够;当需要分辨细长的低密度丝状结构时,可下调到 2 或 1,代价是计算时间上升。

density.thresholds(thresholds):阈值生成

若指定 thresholds,设置阈值生成器(可以是函数或数组);否则返回当前生成器,默认生成约二十个整齐取整的密度阈值

阈值数组的语义是 [x0, x1, …]:

  • 第一条生成的密度等高线对应“估计密度 ≥ x0”的区域;
  • 第二条对应“估计密度 ≥ x1”的区域;
  • 依此类推——每个阈值值恰好对应一个生成的 MultiPolygon 几何对象,阈值值即 geometry.value
  • 第一个值 x0 通常应大于零,否则最外层等高线会铺满整个 size 区域。

若传入的是数字 count 而非数组,则利用 d3.ticks 生成大约 count 个均匀分布的整齐阈值。阈值数量决定了等高线的“圈数”与渲染路径数量:20 个阈值即 20 条 MultiPolygon,逐层填充即可得到经典的嵌套色带效果。

density.bandwidth(bandwidth):高斯核带宽

bandwidth 设置高斯核的带宽(标准差)。未指定时返回当前值,默认值约为 20.4939…(即 √420,对应一组预计算高斯核的尺度)。两个约束:

  • 指定值必须非负
  • 指定值会被四舍五入到该实现所支持的最接近的离散值——这与 cellSize 取 2 的幂是同一类工程取舍:d3-contour 预先计算有限组高斯核系数,用量化后的带宽换取快速查表式的卷积,而非逐点计算连续高斯函数。

带宽是密度图最重要的形态参数:过大会抹平聚集结构、让峰消失;过小则每个点各自成峰,视觉上退化为模糊的散点。实践中建议以像素为单位,取明显小于数据簇间距的值(默认 20 左右适合 960×500 的典型画布)。

density.contours(data):复用网格,任意阈值切片

density.contours(data) 返回一个 contour(value) 函数,可在已给定数据上计算任意单阈值等高线而无需重算底层网格,适合交互式场景(拖动滑块实时改变阈值)。返回的 contour 函数还暴露 contour.max 属性,表示网格上的最大密度值,可用作阈值滑块的上限或色阶的 domain 上限:

const contour = density.contours(diamonds);
// 任意时刻:
const polygon = contour(contour.max / 2); // 密度达到峰值一半以上的区域

这与 contours.mdcontours.contour(values, threshold) 的单阈值计算相对应,只是输入从显式网格换成了点云。

与通用等高线生成器 contours() 的关系

理解 contourDensity 最快的方式,是把它看作三段流水线的组合:

  1. 分箱:按 sizecellSize 把点云累加到网格(weight 参与累加);
  2. 核平滑:以 bandwidth 指定的高斯核在网格上卷积,得到平滑密度场;
  3. 等高线提取:对密度场应用 marching squares,对每个 thresholds 阈值输出一个 MultiPolygon。

docs/d3-contour/contour.md 描述的 d3.contours() 直接跳过了前两步,接收现成的数值网格(如 GeoTIFF 解码的地表温度、图像像素值)做第 3 步。两者的输出格式一致(GeoJSON MultiPolygon + geometry.value),因此下游的 geoPath 渲染代码完全通用。marching squares 这一点也在 d3-contour 模块总览 中被明确说明:“This module computes contour polygons by applying marching squares to a rectangular grid of numeric values.”

默认值速查

方法 默认值 说明
density.x d => d[0] x 坐标访问器
density.y d => d[1] y 坐标访问器
density.weight () => 1 点权重
density.size [960, 500] 估计器像素范围,结果仅在其内准确
density.cellSize 4 分箱网格单元格大小,向下取整到 2 的幂
density.thresholds 约 20 个整齐阈值 每个阈值对应一个 MultiPolygon
density.bandwidth ≈ 20.4939… 高斯核标准差,量化到实现支持的最接近值,必须非负

小结

d3.contourDensity() 用“分箱 + 高斯核平滑 + marching squares”三步把点云转化为密度等高线,API 面共 9 个条目(构造器、density(data) 调用,以及 x / y / weight / size / cellSize / thresholds / bandwidth / contours 八个配置方法),输出统一的 GeoJSON MultiPolygon 并以 geometry.value 携带阈值。通过 src/index.js 的再导出与 package.jsond3-contour ^4.0.2 的依赖声明可以确认本文所述行为适用于 d3 7.x 系列;test/d3-test.js 则保证了这些 API 从顶层 d3 命名空间可稳定访问。对超过数万点的散点数据,密度等高线是在不丢失分布形态的前提下避免过度重叠的首选可视化手段。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384