流与网络 — 弦图、桑基图与邻接/弧线图¶
这一章讲什么: 树是网络的一个特例(单根、无环);本章放开限制—— 有环、有向、多根的通用网络怎么画。开篇先把数据的两种形态定清: 邻接矩阵(方阵)与 node-link 列表——它们决定你手里的工具能吃哪种输入。 然后是四种图:邻接热图、弦图/丝带图(chord/ribbon)、桑基图(Sankey)、弧线图(arc diagram)。
1. 顶层全景:数据形态 × 图形态¶
数据形态 A:邻接矩阵(n×n 方阵) 数据形态 B:node-link 列表
matrix[i][j] = i→j 的流量(0 表示无边) [{source, target, value}, …]
│ 可互换(matrixToLinks/linksToMatrix) │
▼ ▼
├─ 邻接热图(自写 layout) ├─ 弧线图(network-utils 的 arcDiagram)
├─ 弦图(d3.chord) └─ 桑基图(d3-sankey,只吃 DAG)
└─ 简单 chord(自定义)
选型主轴:双向关系 → 弦图;单向流程(可分可合不可回环)→ 桑基;
交叉多的密网 → 矩阵热图;小而顺序的网络 → 弧线图。
图的行话(数学叫图论):节点 = vertex、边 = edge;有向(航班出发数 ≠ 到达数)vs 无向(共同边界);自环(迁移图里的大洲内部迁移);DAG(有向无环图,桑基的前提); 节点的度(进出边数)与边的权重1。
2. 核心原理¶
2.1 邻接矩阵与 node-link 互换¶
主线例子的同一组数据,两种形态(注意列表只存非零边,source/target 是节点数组的下标)2:
const matrix = [ [0,6,2,0,0],
[8,0,5,0,0],
[0,1,0,0,0],
[0,1,0,0,1],
[0,0,0,0,0] ];
const labels = ['A','B','C','D','E'];
const edges = [ {source:0, target:1, value:6},
{source:0, target:2, value:2},
{source:1, target:0, value:8}, … ]; // 只有 7 条非零边
const nodes = [{label:'A'}, …, {label:'E'}];
// 互换工具(仓库 js/network-utils.js 有完整版):
function matrixToLinks(matrix) { … } // 矩阵 → 列表(摊平双循环,滤 0)
function linksToMatrix(nodes, links) { … } // 列表 → 矩阵(填空阵,填值)
对称性提示:无向图(含树)的矩阵对称或三角,只有 n(n−1)/2 个独立值; 对角线全 0 = 无自环3。
2.2 邻接热图:自写 layout 函数的样板¶
D3 不给邻接矩阵的现成 layout,原书自己写了一个 adjacencyMatrix()——
这正是第 08 章 chart-utils 教的「可复用布局」模式的实战版:
闭包函数 + size() 配置,吃 (labels, matrix),吐「每个格子一个对象
{x, y, w, h, [source, target, value]}」4:
function adjacencyMatrix() {
let w = 1, h = 1;
function layout(nodes, sourceMatrix) {
const cellW = w / nodes.length, cellH = h / nodes.length;
return sourceMatrix.flatMap((row, r) => row.map((value, c) => {
const rect = { x: c*cellW, y: r*cellH, w: cellW, h: cellH };
return value > 0 ? {…rect, source: nodes[r], target: nodes[c], value} : rect;
}));
}
layout.size = function(array) { … return layout; }
return layout;
}
const data = adjacencyMatrix().size([700,500])(labels, matrix);
// 25 个格子对象:有值的带 source/target/value,没值的纯白底
画出 25 个 rect(95% 尺寸 + 圆角),行标签压左侧、列标签压顶部(filter(d => d.x == 0)),
按 value 上色即成热图。交互一行流:classed("faded", …) 按行列坐标联动高亮5。
2.3 弦图/丝带图:d3.chord + d3.ribbon + d3.arc¶
d3.chord() 吃矩阵:吐出 chords(丝带数组,每对双向连接一条,所以是 k/2 条)
和 chords.groups(节点圆弧数组,n 个)。丝带端点与圆弧都带 startAngle/endAngle6:
const chord = d3.chord().padAngle(.3); // padAngle:组间留缝(弧度)
const chords = chord(matrix); // chords: 14 条丝带;chords.groups: 9 段弧
// chords[i] = {source: {index, subindex, value, startAngle, endAngle}, target: {…}}
// groups[i] = {index, value, startAngle, endAngle}
d3.ribbon() 出丝带路径(配半径),d3.arc() 出节点弧(内外半径错开成环带)7:
const ribbon = d3.ribbon().radius(700);
chart.selectAll('path.ribbon').data(chords).join("path")
.attr("d", ribbon).style("opacity", .3)
.style("fill", d => color(d.source.index));
const arc = d3.arc().innerRadius(radius+2).outerRadius(radius+30);
chart.selectAll('path.arc').data(chords.groups).join("path")
.attr("d", arc).style("fill", d => color(d.index));
标签照旧挂 arc.centroid(),旋转角取弧的中角 ×180/π(弧度转角度)。
排序三旋钮:sortGroups(d3.descending) 节点降序、sortSubgroups 组内丝带排序、
sortChords 叠压顺序8。
读法与边界:一条丝带恒代表一对连接(双向),所以单向拓扑、树都别用弦图; 只出不进的节点会消失。交互(悬停淡化其余 + tooltip)在这类图里「不是可选项」9。
2.4 桑基图:d3-sankey(独立模块)¶
桑基画单向流量:节点按层竖排,流带宽 = 流量,可分叉不可回环。 模块不在默认包里,引入时给主库换个前缀再合并10:
import * as d3_all from "https://cdn.skypack.dev/d3@7.9.0"; // CDN 链接(查阅于 2026-10-19)
import * as d3_sankey from "https://cdn.skypack.dev/d3-sankey@0.12.3"; // CDN 链接(查阅于 2026-10-19)
const d3 = Object.assign({}, d3_all, d3_sankey);
数据硬约束:DAG。links 的 target 下标必须大于 source(流向恒向前); 有环不会被检测,只会把页面挂死——原书原话「they simply hang the application」11。 布局与产出:
const sankey = d3.sankey().nodePadding(50).nodeWidth(30)
.extent([[50,50],[600,400]]).iterations(100);
const graph = sankey({nodes, links});
// 节点得到:x0,x1,y0,y1(宽 nodeWidth、高由流量算)、depth、sourceLinks/targetLinks、value
// 连边得到:y0,y1、width(流带厚度)
chart.selectAll("path.link").data(graph.links).join("path")
.attr("d", d3.sankeyLinkHorizontal()) // 横向流带生成器
.style("stroke-width", d => d.width);
nodeAlign 四种对齐(justify 默认/left/center/right);nodeId(访问器) 让 links 用
名字而非下标;update(graph) 在拖节点后重算流带;iterations 默认 32,加大(如 100)
常让布局更匀12。原书彩蛋:米纳德(Charles Minard)1869 年的拿破仑征俄图
——「史上最好的统计图」——就是一张地理版的桑基13。
2.5 弧线图:axis 上的小网络¶
节点排一条横轴,连接画成轴上(一个方向)或轴下(反方向)的弧——
方向感比弦图强,适合小而顺序的网络。D3 无内置生成器,用 network-utils.js 的
arcDiagram().width(800) 出坐标,nwu.curve() 出弧线路径14。
3. 作者的判断与证据¶
| 判断 | 性质 | 依据 |
|---|---|---|
| 「弦图适合小而双向,桑基适合单向流程」 | 选型主轴 | 章小结的原话对照15 |
| 「弦图里交互不是可选项」 | 经验之谈 | ribbon 缠成一团时的可读性论证9 |
| 「d3-sankey 非标准模块,版本差异大」 | 版本警示 | Important note:v5 与现行算法渲染不同10 |
4. 边界与局限¶
- 简单 chord 图(点+线)与弧线图都靠自定义 layout(warehouse 的 network-utils),D3 核心不供。
- 桑基的分层算法(怎么排层、怎么减少交叉)是黑箱,iterations 之外不可调。
- 力导向网络是下一章,本章的网络图全部「位置由结构直接决定」。
- 大规模网络(万级边)的渲染性能问题,本章只以热图/矩阵为答案,未展开。
判断(我们的,不是书里的): 本章的隐藏主线其实是「矩阵 vs 列表」这组数据结构选择: 矩阵适合稠密网(全互通、值需对照),列表适合稀疏网(边数 ≪ n²)。 选错形态的代价在 D3 里很直接——d3.chord 只认矩阵,d3.sankey 只认列表, 互换函数(network-utils)因此成了本章最实用的工具。教可视化库的同时, 这也是一节数据结构课。 如果错,会错在: 稠密/稀疏的界线随任务移动——同一批数据做探索(看全局密度)该用矩阵, 做叙事(讲一条流向)该用桑基;「形态决定图表」只在数据形态已定时成立。
5. 可带走的¶
- 网络数据两形态:邻接矩阵(稠密、看权重分布)与 node-link 列表(稀疏、喂布局器);
matrixToLinks / linksToMatrix互换;互换上手先想清有向/无向、自环;- 邻接热图 = 自写 layout(闭包 + size)出格子坐标,
flatMap摊矩阵; - chord 管线:
d3.chord().padAngle()(matrix)→ ribbon(半径)画丝带、arc 画节点弧、centroid 摆标签; - 弦图一条丝带 = 一对双向连接;单向/树形数据别用;
- 桑基硬约束:DAG(下标向前),有环即挂死;独立模块换前缀引入;
sankeyLinkHorizontal()出流带,stroke-width = d.width;nodeAlign 四种对齐;- 桑基节点可拖,
sankey.update(graph)重算流带; - 弧线图:方向分轴上下;network-utils 的 arcDiagram/curve;
- 交互(淡化其余 + tooltip)在弦图/桑基上是必需品,不是装饰。
6. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 图的术语与数据形态 | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:76(搜「vertices」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:90(搜「DAG」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:95(搜「degree」) |
| 邻接矩阵与 node-link | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:124(搜「[0, 6, 2, 0, 0]」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:144(搜「source: 0, target: 1」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:158(搜「matrixToLinks」) |
| 自写 adjacency layout | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:227(搜「function adjacencyMatrix()」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:253(搜「selectAll("rect")」) |
| 热图高亮交互 | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:291(搜「faded」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:306(搜「highlight(evt, d)」) |
| chord 布局与产出结构 | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:391(搜「d3.chord()」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:401(搜「[0,3,3,3,0」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:416(搜「padAngle(.3)」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:435(搜「subindex」) |
| ribbon 与 arc 渲染 | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:454(搜「d3.ribbon().radius(700)」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:481(搜「innerRadius(radius+2)」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:501(搜「textAngle」) |
| chord 排序与高亮 | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:534(搜「sortGroups(d3.descending)」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:550(搜「highlightNode」) |
| 桑基引入与硬约束 | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:649(搜「d3_all」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:659(搜「Important note」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:756(搜「hang the application」) |
| 桑基布局与画法 | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:696(搜「nodePadding(50)」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:709(搜「sankey({nodes: nodes」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:835(搜「sankeyLinkHorizontal()」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:854(搜「sankeyRight」) |
| 弧线图 | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:889(搜「arcDiagram().width(800)」) · text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:909(搜「nwu.curve()」) |
| 章小结(选型主轴) | Visualizing Flows and Networks | text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:938(搜「Summary」) |
-
出处:「Visualizing Flows and Networks」第 75–103 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:76,搜「vertices」;:86,搜「loop」;:90,搜「DAG」;:95,搜「degree」)。 ↩ -
出处:「Visualizing Flows and Networks」第 105–181 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:124,搜「[0, 6, 2, 0, 0]」;:144,搜「source: 0, target: 1」;:158,搜「matrixToLinks」;:170,搜「linksToMatrix」)。 ↩ -
出处:「Visualizing Flows and Networks」第 106–114 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:109,搜「diagonal」;:111,搜「symmetric or triangular」)。 ↩ -
出处:「Visualizing Flows and Networks」第 183–278 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:227,搜「function adjacencyMatrix()」;:242,搜「layout.size = function」)。 ↩ -
出处:「Visualizing Flows and Networks」第 286–323 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:306,搜「highlight(evt, d)」;:312,搜「reset()」)。 ↩ -
出处:「Visualizing Flows and Networks」第 350–446 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:391,搜「d3.chord()」;:401,搜「[0,3,3,3,0」;:416,搜「padAngle(.3)」;:422,搜「k/2 objects」;:434,搜「subindex」)。 ↩ -
出处:「Visualizing Flows and Networks」第 448–516 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:454,搜「d3.ribbon().radius(700)」;:481,搜「innerRadius(radius+2)」;:501,搜「textAngle」)。 ↩ -
出处:「Visualizing Flows and Networks」第 519–542 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:524,搜「padAngle(angle)」;:534,搜「sortGroups(d3.descending)」)。 ↩ -
出处:「Visualizing Flows and Networks」第 350–381 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:380,搜「bad choice for trees」;:367,搜「Highlighting and tooltips improve readability」)。 ↩↩ -
出处:「Visualizing Flows and Networks」第 639–664 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:649,搜「d3_all」;:655,搜「Object.assign」;:659,搜「Important note」)。 ↩↩ -
出处:「Visualizing Flows and Networks」第 747–791 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:756,搜「hang the application」)。 ↩ -
出处:「Visualizing Flows and Networks」第 667–736 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:677,搜「iterations(number)」;:681,搜「sankeyJustify」;:687,搜「update(graph)」)与第 794–859 段(:835,搜「sankeyLinkHorizontal()」;:854,搜「sankeyRight」)。 ↩ -
出处:「Visualizing Flows and Networks」第 605–637 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:611,搜「Minard」)。 ↩ -
出处:「Visualizing Flows and Networks」第 869–930 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:889,搜「arcDiagram().width(800)」;:909,搜「nwu.curve()」)。 ↩ -
出处:「Visualizing Flows and Networks」第 938–953 段(
text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:945,搜「Chord diagrams are ideal」)。 ↩