d3 密度估计详解:contourDensity 二维核密度估计与等高线绘制
本文围绕 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.js 中
export * from "d3-contour";将contourDensity、contours等 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.x与density.y访问器计算; - 权重:
density.weight指定每个数据点的相对贡献,默认均为 1; - 精度边界:生成的等高线只在估计器定义的
size范围内准确。
返回的 MultiPolygon 通常直接交给 d3.geoPath 渲染,投影使用 null 或 geoIdentity(因为密度结果本身就是像素平面坐标,不需要地理投影)。一个最小渲染示例:
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。两个关键细节:
- 单元格大小会向下取整到最近的 2 的幂(如传入 5 实际生效为 4,传入 10 生效为 8)——这一设计让核平滑可以借助位运算在 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.md 中 contours.contour(values, threshold) 的单阈值计算相对应,只是输入从显式网格换成了点云。
与通用等高线生成器 contours() 的关系
理解 contourDensity 最快的方式,是把它看作三段流水线的组合:
- 分箱:按
size与cellSize把点云累加到网格(weight参与累加); - 核平滑:以
bandwidth指定的高斯核在网格上卷积,得到平滑密度场; - 等高线提取:对密度场应用 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.json 中 d3-contour ^4.0.2 的依赖声明可以确认本文所述行为适用于 d3 7.x 系列;test/d3-test.js 则保证了这些 API 从顶层 d3 命名空间可稳定访问。对超过数万点的散点数据,密度等高线是在不丢失分布形态的前提下避免过度重叠的首选可视化手段。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00