编程语言研究书库

流与网络 — 弦图、桑基图与邻接/弧线图

流与网络 — 弦图、桑基图与邻接/弧线图

这一章讲什么: 树是网络的一个特例(单根、无环);本章放开限制—— 有环、有向、多根的通用网络怎么画。开篇先把数据的两种形态定清: 邻接矩阵(方阵)与 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. 核心原理

主线例子的同一组数据,两种形态(注意列表只存非零边,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. 可带走的

  1. 网络数据两形态:邻接矩阵(稠密、看权重分布)与 node-link 列表(稀疏、喂布局器);
  2. matrixToLinks / linksToMatrix 互换;互换上手先想清有向/无向、自环;
  3. 邻接热图 = 自写 layout(闭包 + size)出格子坐标,flatMap 摊矩阵;
  4. chord 管线:d3.chord().padAngle()(matrix) → ribbon(半径)画丝带、arc 画节点弧、centroid 摆标签;
  5. 弦图一条丝带 = 一对双向连接;单向/树形数据别用;
  6. 桑基硬约束:DAG(下标向前),有环即挂死;独立模块换前缀引入;
  7. sankeyLinkHorizontal() 出流带,stroke-width = d.width;nodeAlign 四种对齐;
  8. 桑基节点可拖,sankey.update(graph) 重算流带;
  9. 弧线图:方向分轴上下;network-utils 的 arcDiagram/curve;
  10. 交互(淡化其余 + tooltip)在弦图/桑基上是必需品,不是装饰。

6. 原文地图

主题原书章原文位置
图的术语与数据形态Visualizing Flows and Networkstext/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-linkVisualizing Flows and Networkstext/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 layoutVisualizing Flows and Networkstext/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 Networkstext/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 Networkstext/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 Networkstext/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 Networkstext/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 Networkstext/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 Networkstext/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 Networkstext/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 Networkstext/25-ch17-chapter-17-visualizing-flows-and-networks.txt:938(搜「Summary」)

Footnotes

  1. 出处:「Visualizing Flows and Networks」第 75–103 段(text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:76,搜「vertices」;:86,搜「loop」;:90,搜「DAG」;:95,搜「degree」)。 ↩

  2. 出处:「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」)。 ↩

  3. 出处:「Visualizing Flows and Networks」第 106–114 段(text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:109,搜「diagonal」;:111,搜「symmetric or triangular」)。 ↩

  4. 出处:「Visualizing Flows and Networks」第 183–278 段(text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:227,搜「function adjacencyMatrix()」;:242,搜「layout.size = function」)。 ↩

  5. 出处:「Visualizing Flows and Networks」第 286–323 段(text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:306,搜「highlight(evt, d)」;:312,搜「reset()」)。 ↩

  6. 出处:「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」)。 ↩

  7. 出处:「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」)。 ↩

  8. 出处:「Visualizing Flows and Networks」第 519–542 段(text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:524,搜「padAngle(angle)」;:534,搜「sortGroups(d3.descending)」)。 ↩

  9. 出处:「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」)。 ↩ ↩2

  10. 出处:「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」)。 ↩ ↩2

  11. 出处:「Visualizing Flows and Networks」第 747–791 段(text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:756,搜「hang the application」)。 ↩

  12. 出处:「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」)。 ↩

  13. 出处:「Visualizing Flows and Networks」第 605–637 段(text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:611,搜「Minard」)。 ↩

  14. 出处:「Visualizing Flows and Networks」第 869–930 段(text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:889,搜「arcDiagram().width(800)」;:909,搜「nwu.curve()」)。 ↩

  15. 出处:「Visualizing Flows and Networks」第 938–953 段(text/25-ch17-chapter-17-visualizing-flows-and-networks.txt:945,搜「Chord diagrams are ideal」)。 ↩

On this page