首页
/ D3.js d3-chord 弦图实战:用 chord 布局与 ribbon 形状绘制节点间的双向流

D3.js d3-chord 弦图实战:用 chord 布局与 ribbon 形状绘制节点间的双向流

2026-09-05 09:50:21作者:秋阔奎Evelyn

本文围绕 d3 仓库中的 d3-chord 模块文档 展开,讲解如何用 d3.chord() 将 n×n 流量矩阵转化为环状弦图布局、用 d3.ribbon() 生成连接各节点的缎带形状,并结合仓库中可运行的官方示例 ExampleChord.vue 完整复现一张带刻度、带排序、带悬停提示的弦图,覆盖从数据建模、布局参数、形状生成器到 Canvas 渲染上下文的全部 API。

弦图与流量矩阵:把 n×n 矩阵映射到圆周

弦图(chord diagram)用于可视化一组节点之间的流动关系,例如有限状态之间的转移概率。d3 的弦图布局用大小为 n×n 的方阵表示流动,其中 n 是图中节点数:每个元素 matrix[i][j] 表示从第 i 个节点流向第 j 个节点的流量。矩阵元素必须是非负数,无流动时可为零。

d3 官方文档用一份假想的染发人群数据集(数据风格仿照 Circos 表格)说明这一模型:矩阵的每行、每列对应一种发色(black、blond、brown、red),每个值表示从一种颜色染到另一种颜色的人数,矩阵对角线表示保持原色的人数。例如 5,871 个黑发的人染成了金色,而 1,951 个金发的人染成了黑色。数据如下(见 d3-chord.md 原文):

const matrix = [
  // to black, blond, brown, red
  [11975,  5871, 8916, 2868], // from black
  [ 1951, 10048, 2060, 6171], // from blond
  [ 8010, 16145, 8090, 8045], // from brown
  [ 1013,   990,  940, 6907]  // from red
];

弦图的绘制方式可以概括为两步:把人群按起始颜色沿圆周排布,再在每种颜色之间绘制缎带(ribbon)。缎带的起点宽度和终点宽度分别正比于对应起始色和结束色的人数;缎带本身的颜色则约定为两端流量中较大值那一方的颜色。

核心布局 API:chord() 与 chord(matrix)

构造函数与布局计算

d3.chord() 以默认设置创建一个新的弦图布局:

const chord = d3.chord();

调用 *chord*(*matrix*) 会对指定的 n×n 方阵计算弦图布局,其中矩阵表示 n 个节点构成的有向网络中的流动。返回值是 chords 数组,每个 chord 表示节点 ij 之间(i 可与 j 相等)合并后的双向流动,结构为:

{
  source: { startAngle, endAngle, value, index }, // 源子组
  target: { startAngle, endAngle, value, index }  // 目标子组
}

每个 source / target 子组(subgroup)的属性含义为:

  • startAngle / endAngle — 起始角与结束角,单位为弧度;
  • value — 流量值 matrix[i][j]
  • index — 节点下标 i

这些 chords 通常直接传给 ribbon 形状生成器 来渲染网络关系。

返回数组有两个重要约定,直接影响后续遍历逻辑:

  1. 只包含 matrix[i][j]matrix[j][i] 非零的 chord;
  2. 只包含唯一的 chord:chord ij 同时表示 ijji 的双向流动,不会出现重复的 ji;且 ij 的选取使得 chord 的 source 总是 matrix[i][j]matrix[j][i] 中较大的那一方。

此外,chords 数组上挂载了次级数组 chords.groups,长度为 n,其中每个 group 代表节点 i 的总出流(即 matrix[i][0 … n-1] 的合计),属性为:

  • startAngle / endAngle — 弧度制的起始角与结束角;
  • value — 节点 i 的总出流值;
  • index — 节点下标 i

groups 通常传给 d3.arc() 在弦图圆周外圈生成环状分段(donut 弧段)。

四个可配置参数

方法 作用 默认值
*chord*.padAngle(angle) 相邻 group 之间的角度间隙(弧度) 0
*chord*.sortGroups(compare) 按总出流对 group 排序的比较函数 null
*chord*.sortSubgroups(compare) 对每个 group i 内部的子组(matrix[i][0 … n-1])排序 null
*chord*.sortChords(compare) 按合并流量对 chord 排序,只影响 z 序(叠放顺序) null

四个比较器均接受比较函数或 null(表示不排序),官方文档建议搭配 d3.ascendingd3.descending 使用。注意 sortChords 不改变几何位置,只改变渲染时的前后层叠关系,这在调试缎带遮挡问题时很有用。

有向布局:chordDirected() 与 chordTranspose()

d3 v7 在 d3-chord 中新增了两个布局变体(见 CHANGES.md 中 d3 v7.0.0 对 d3-chord 的记录):

  • d3.chordDirected() — 有向流动的弦图布局。从 ij 的 chord 仅由 matrix[i][j] 生成,不合并反向流量,适合表现有方向性的数据流,需配合 ribbonArrow() 使用;
  • d3.chordTranspose() — 转置弦图布局。从源码结构看,其效果等价于对矩阵转置后布局,文档指出它“用于突出出向(而非入向)流动”。

缎带形状生成器:ribbon() 全家桶

ribbon 文档 定义了两种缎带:ribbon 表示双向流动,ribbonArrow 表示单向流动(后者适合 chordDirected)。

基本用法

const ribbon = d3.ribbon();

调用 *ribbon*(*...arguments*) 时,参数会被连同 this 对象一起传递给各个访问器函数。默认设置下期望传入一个 chord 对象:

ribbon({
  source: {startAngle: 0.7524114, endAngle: 1.1212972, radius: 240},
  target: {startAngle: 1.8617078, endAngle: 1.9842927, radius: 240}
})
// => "M164.0162810494058,-175.21032946354026A240,240,0,0,1,216.1595644740915,-104.28347273835429Q0,0,229.9158815306728,68.8381247563705A240,240,0,0,1,219.77316791012538,96.43523560788266Q0,0,164.0162810494058,-175.21032946354026Z"

若已设置 context,则路径以一系列 path method 调用渲染到该上下文并返回 void;否则返回 SVG path 数据字符串。

访问器一览

方法 默认访问器 说明
*ribbon*.source(source) d => d.source 源访问器
*ribbon*.target(target) d => d.target 目标访问器
*ribbon*.radius(radius) d => d.radius 同时设置源与目标半径
*ribbon*.sourceRadius(radius) d => d.radius 仅源半径
*ribbon*.targetRadius(radius) d => d.radius 仅目标半径
*ribbon*.startAngle(angle) d => d.startAngle 起始角(弧度)
*ribbon*.endAngle(angle) d => d.endAngle 结束角(弧度)
*ribbon*.padAngle(angle) () => 0 相邻缎带之间的角度间隙
*ribbon*.context(context) null 渲染上下文(Canvas 2D 等)

角度约定与 d3 其他极坐标形状一致:以弧度表示,0 指向 -y(12 点钟方向),正角顺时针增加。

固定半径的用法示例——设置 .radius(240) 之后,source 和 target 数据中就不再需要 radius 属性:

const ribbon = d3.ribbon().radius(240);

ribbon({
  source: {startAngle: 0.7524114, endAngle: 1.1212972},
  target: {startAngle: 1.8617078, endAngle: 1.9842927}
});

关于 sourceRadius / targetRadius 的独立设置,文档给出了一条重要约定:在非对称(有向)弦图中,目标半径通常要比源半径内缩,从而在定向链接末端与其所属的 group 弧段之间留出间隙,视觉上是区分“流入/流出”的关键手段。

ribbonArrow() 与 headRadius

d3.ribbonArrow() 创建带箭头的单向缎带生成器,其余访问器与 ribbon 一致,另有一个特有方法:

*ribbonArrow*.headRadius(radius)

设置箭头头部半径访问器,默认访问器固定返回 10

Canvas 渲染

*ribbon*.context(context) 是 d3 v4 引入的能力之一(CHANGES.md 明确记录:“A new ribbon.context method lets you render chord diagrams to Canvas”)。传入 Canvas 2D 上下文后,*ribbon* 不再返回字符串,而是直接以 path method 调用序列绘制到画布上;这与 d3-path 的通用约定一致,使同一份布局数据可以同时驱动 SVG 与 Canvas 两种输出。

完整实战示例:带刻度与排序的 SVG 弦图

仓库文档站中的 ExampleChord.vued3-chord.md 页面顶部弦图的完整实现,它演示了前述几乎全部 API 的组合方式。核心逻辑如下(已去除 Vue 模板语法,保留全部 d3 调用):

const width = 688;
const height = 688;
const outerRadius = Math.min(width, height) * 0.5 - 30; // 314
const innerRadius = outerRadius - 20;                    // 294

const matrix = [
  // to black, blond, brown, red
  [11975,  5871, 8916, 2868], // from black
  [ 1951, 10048, 2060, 6171], // from blond
  [ 8010, 16145, 8090, 8045], // from brown
  [ 1013,   990,  940, 6907]  // from red
];

const colors = ["#000000", "#ffdd89", "#957244", "#f26223"];
const names = ["black", "blond", "brown", "red"];

// 根据总流量生成刻度步长与带前缀的格式化器
const sum = d3.sum(matrix.flat());
const tickStepMinor = d3.tickStep(0, sum, 100);
const tickStepMajor = d3.tickStep(0, sum, 20);

function groupTicks(d, step) {
  const k = (d.endAngle - d.startAngle) / d.value;
  return d3.range(0, d.value, step).map(value => {
    return {value: value, angle: value * k + d.startAngle};
  });
}

const formatValue = d3.formatPrefix(",.0", tickStepMinor);

// 外圈 group 弧段:使用 chord.groups
const arc = d3.arc()
    .innerRadius(innerRadius)
    .outerRadius(outerRadius);

// 内圈缎带:chord 输出直接喂给 ribbon
const ribbon = d3.ribbon()
    .radius(innerRadius);

// 布局:0.05 弧度组间隙 + 组内子组按降序
const chords = d3.chord()
    .padAngle(0.05)
    .sortSubgroups(d3.descending)
  (matrix);

SVG 模板部分由两层组成:

<svg :width="width" :height="height" :viewBox="[-width/2, -height/2, width, height].join(' ')" fill="currentColor" style="font: 10px sans-serif;">
  <!-- 外圈:每个 group 一段弧 + 数值刻度 -->
  <g v-for="d in chords.groups">
    <path :d="arc(d)" :fill="colors[d.index]" :stroke="colors[d.index]">
      <title>{{ d.value.toLocaleString("en-US") }} {{ names[d.index] }}</title>
    </path>
    <g v-for="t in groupTicks(d, tickStepMinor)"
       :transform="`rotate(${t.angle * 180 / Math.PI - 90}) translate(${outerRadius},0)`">
      <line stroke="currentColor" x2="6" />
      <text v-if="t.value % tickStepMajor === 0" x="8" dy="0.35em"
            :transform="t.angle > Math.PI ? 'rotate(180) translate(-16)' : null"
            :text-anchor="t.angle > Math.PI ? 'end' : null">{{ formatValue(t.value) }}</text>
    </g>
  </g>
  <!-- 内圈:每条 chord 一条缎带,颜色取 target 一侧(即较大流量方) -->
  <g fill-opacity="0.67" stroke="var(--vp-c-bg)">
    <path v-for="d in chords" :d="ribbon(d)" :fill="colors[d.target.index]">
      <title>
        {{ d.source.value.toLocaleString("en-US") }} {{ names[d.source.index] }} → {{ names[d.target.index] }}…
      </title>
    </path>
  </g>
</svg>

该示例有几个值得复用的细节:

  1. 半径分层outerRadiusinnerRadius 相差 20px,group 弧段占据外环(d3.arc 的内外半径),缎带贴在内半径上(d3.ribbon().radius(innerRadius)),形成典型的“外环分组 + 内圈流”的弦图结构;
  2. 刻度映射groupTicks 利用 k = (endAngle - startAngle) / value 把 group 的数值线性映射回角度,d3.tickStep(0, sum, count) 根据总量自动挑选合适的刻度步长,再配合 d3.formatPrefix 输出带 SI 前缀的标签;
  3. 角度翻转处理:当刻度角度超过 π 时(圆下半侧),文字执行 rotate(180) 并将 text-anchor 改为 end,保证标签始终朝外可读;
  4. 颜色约定落地:缎带填充色取 colors[d.target.index]。因为 chord() 已保证 source 一侧是双向流量中较大的一方,所以这里等价于“取较大流量方的颜色”这一文档约定;
  5. 排序策略:只用了 .padAngle(0.05).sortSubgroups(d3.descending),即组按默认顺序、组内子组按出流降序——这是让大流量靠组起点聚集、读图更直观的常用组合。

仓库中的模块组织与版本说明

从仓库结构看,本项目的 d3 包是一个聚合入口:src/index.js 第 4 行 export * from "d3-chord"; 将 d3-chord 的全部 API 重新导出,因此安装聚合包后可直接使用 d3.chord()d3.chordDirected()d3.ribbon()d3.ribbonArrow()package.json 显示当前版本为 7.9.0,依赖 d3-chord: ^3.0.1,即本文所依据的 d3-chord v3 API(含 v7 新增的 chordDirectedchordTransposeribbonArrowribbon.padAngleribbon.sourceRadiusribbon.targetRadius,见 CHANGES.md)。

文档站中完整的 API 索引见 docs/api.md 的 d3-chord 小节,其列出的条目与 docs/d3-chord/chord.mddocs/d3-chord/ribbon.md 一一对应;官方文档中各 API 标注的独立源码文件位于 d3-chord 子模块的 src/chord.jssrc/ribbon.js,本仓库通过依赖引入而非内联源码。若需要本地预览文档站点(含本文示例的交互式弦图),可按 package.jsondocs:dev 脚本运行 VitePress 开发服务器,相关构建脚本均基于 node >= 12package.jsonengines 字段)。

小结

d3-chord 的 API 面虽小,但职责划分清晰:chord() 负责把 n×n 非负流量矩阵折算为角度(groups 与 chords 两级结构,并内置双向合并、唯一性、source 取大值的约定),ribbon() 负责把角度区间转成路径(双向缎带或带箭头单向缎带,可输出 SVG 字符串或直写 Canvas context)。掌握 padAnglesortGroups / sortSubgroups / sortChords 三个维度的排序控制,以及 sourceRadius / targetRadius 分离带来的内缩间隙,再配合 ExampleChord.vue 中“arc 外环 + ribbon 内圈 + 角度刻度”的组合方式,即可在 d3 v7 环境下快速构建出可用于状态转移、流量桑基类场景的弦图。

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

项目优选

收起
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