跳到主要内容

力导向图

力导向图(Force-Directed Graph)是一种以物理模拟方式来展示网络关系的可视化技术。与树形图或层次结构图不同,力导向图不需要预先定义数据的层次结构,数据点会根据相互之间的作用力自动调整位置,形成一个动态的、有机的布局。

力导向图特别适合展示社交网络、组织结构、万物互联的关系数据。在这些场景中,各个实体之间的关系是网状的而非层级化的,力导向图能够自然地将这种关系结构可视化。

核心概念:力的模拟

力导向图的基本原理是模拟物理世界中物体之间的相互作用。在力导向图算法中有几种基本的力:

库仑斥力(Coulomb Repulsion):所有节点之间都存在相互排斥的力,这确保了节点不会重叠在一起。斥力的大小与节点之间距离的平方成反比,距离越近斥力越大。

胡克引力(Hooke Attraction):相连的节点之间存在相互吸引的力,类似弹簧的机制。引力的大小与距离成正比(满足胡克定律),这使得相连的节点保持适当的距离。

向心重力(Centering Force):所有节点被轻微地拉向中心,防止整个图形过度分散。这个力通常比较弱,只起到整体定位的作用。

碰撞检测(Collision Detection):节点之间不能重叠,这个约束通过检测节点的重叠并施加排斥力来实现。

理解这些力的作用原理是调优力导向图的基础。

基础力导向图示例

让我们从最简单的例子开始:一个公司组织架构的力导向图。

// 节点数据:公司员工
const nodes = [
{id: 'CEO', group: 1, label: '首席执行官'},
{id: 'CTO', group: 2, label: '首席技术官'},
{id: 'CFO', group: 2, label: '首席财务官'},
{id: 'COO', group: 2, label: '首席运营官'},
{id: 'Tech1', group: 3, label: '技术团队成员 A'},
{id: 'Tech2', group: 3, label: '技术团队成员 B'},
{id: 'Finance1', group: 3, label: '财务团队成员 A'},
{id: 'Finance2', group: 3, label: '财务团队成员 B'},
{id: 'Ops1', group: 3, label: '运营团队成员 A'},
{id: 'Ops2', group: 3, label: '运营团队成员 B'}
];

// 边数据:汇报关系
const links = [
{source: 'CEO', target: 'CTO'},
{source: 'CEO', target: 'CFO'},
{source: 'CEO', target: 'COO'},
{source: 'CTO', target: 'Tech1'},
{source: 'CTO', target: 'Tech2'},
{source: 'CFO', target: 'Finance1'},
{source: 'CFO', target: 'Finance2'},
{source: 'COO', target: 'Ops1'},
{source: 'COO', target: 'Ops2'}
];

// 创建力导向模拟器
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id).distance(80)) // 连线引力
.force('charge', d3.forceManyBody().strength(-300)) // 节点间斥力
.force('center', d3.forceCenter(400, 300)) // 中心引力
.force('collision', d3.forceCollide().radius(30)); // 碰撞检测

这段代码创建了一个力导向模拟器,包含了三个核心力:link 力维持相连节点的适当距离;charge 力让所有节点相互排斥避免重叠;center 力将节点拉向画布中心防止图形过度发散。

现在需要创建 SVG 元素并渲染节点和连线:

// 尺寸设置
const width = 800;
const height = 600;

const svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height);

// 绘制连线
const link = svg.append('g')
.selectAll('line')
.data(links)
.join('line')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.attr('stroke-width', 2);

// 绘制节点
const node = svg.append('g')
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('r', 15)
.attr('fill', d => d3.schemeCategory10[d.group])
.attr('stroke', '#fff')
.attr('stroke-width', 2)
.call(drag(simulation)); // 添加拖拽交互

// 添加节点标签
const label = svg.append('g')
.selectAll('text')
.data(nodes)
.join('text')
.text(d => d.id)
.attr('font-size', '12px')
.attr('text-anchor', 'middle')
.attr('dy', 30);

// 模拟更新回调:每次_tick 时更新图形位置
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);

node
.attr('cx', d => d.x)
.attr('cy', d => d.y);

label
.attr('x', d => d.x)
.attr('y', d => d.y);
});

当 simulation 启动后,d3 会不断计算各节点的位置并触发 tick 事件。在 tick 回调中,我们需要更新所有图形元素的 x、y 坐标以反映最新的计算结果。

拖拽交互

拖拽是力导向图的核心交互功能,它允许用户手动调整节点位置,这在分析网络结构时非常有用:

function drag(simulation) {
function dragstarted(event) {
if (!event.active) {
simulation.alphaTarget(0.3).restart();
}
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}

function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}

function dragended(event) {
if (!event.active) {
simulation.alphaTarget(0);
}
event.subject.fx = null;
event.subject.fy = null;
}

return d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
}

拖拽的工作原理是:当用户开始拖拽某个节点时,触发 alphaTarget(0.3) 让模拟器重新激活并保持一定的活跃度;拖拽过程中,持续更新被拖拽节点的 fx(固定 x 坐标)和 fy(固定 y 坐标);拖拽结束后,释放固定坐标,让节点重新参与力的模拟。

网络图常用参数调优

力导向图的视觉效果很大程度上取决于各参数的设置。以下是一些常用的调优策略:

斥力强度

// 强斥力:让节点分布更分散,适合稀疏网络
d3.forceManyBody().strength(-500)

// 弱斥力:节点分布更紧凑,适合密集网络
d3.forceManyBody().strength(-100)

连线距离

// 短连线:紧密关联的节点会靠近
d3.forceLink().distance(50)

// 长连线:相关度低的节点会分开
d3.forceLink().distance(150)

碰撞半径

// 设置碰撞半径为节点半径加缓冲
d3.forceCollide().radius(d => d.radius + 5)

分组着色

在实际网络图中,通常需要对不同类型的节点使用不同的颜色以便区分:

// 根据节点所属分组着色
const colorScale = d3.scaleOrdinal()
.domain([1, 2, 3])
.range(['#ff6b6b', '#4ecdc4', '#45b7d1']);

node.attr('fill', d => colorScale(d.group));

完整的网络图可视化

完整示例:展示一个简单的社交网络关系图。包含用户之间的关注关系,通过不同颜色区分不同类型的用户(普通用户、意见领袖、管理员等)。

// 社交网络模拟数据
const socialData = {
nodes: [
{id: 'user1', name: '张三', type: '普通用户'},
{id: 'user2', name: '李四', type: '普通用户'},
{id: 'user3', name: '王五', type: '意见领袖'},
{id: 'user4', name: '赵六', type: '普通用户'},
{id: 'user5', name: '孙七', type: '管理员'},
{id: 'user6', name: '周八', type: '普通用户'},
{id: 'user7', name: '吴九', type: '意见领袖'},
{id: 'user8', name: '郑十', type: '普通用户'}
],
links: [
{source: 'user1', target: 'user2'},
{source: 'user1', target: 'user3'},
{source: 'user2', target: 'user3'},
{source: 'user2', target: 'user4'},
{source: 'user3', target: 'user5'},
{source: 'user3', target: 'user7'},
{source: 'user4', target: 'user6'},
{source: 'user5', target: 'user7'},
{source: 'user6', target: 'user8'},
{source: 'user7', target: 'user8'}
]
};

// 颜色映射
const typeColors = {
'普通用户': '#3498db',
'意见领袖': '#e74c3c',
'管理员': '#f39c12'
};

// 创建模拟器
const sim = d3.forceSimulation(socialData.nodes)
.force('link', d3.forceLink(socialData.links)
.id(d => d.id)
.distance(100))
.force('charge', d3.forceManyBody().strength(-400))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(30));

// 绘制连线(带箭头)
const defs = svg.append('defs');
const arrow = defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 20) // 偏移量,让箭头不遮挡节点
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('xorient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#999');

svg.append('g')
.selectAll('line')
.data(socialData.links)
.join('line')
.attr('stroke', '#999')
.attr('stroke-width', 1.5)
.attr('marker-end', 'url(#arrowhead)');

// 绘制节点
const circle = svg.append('g')
.selectAll('circle')
.data(socialData.nodes)
.join('circle')
.attr('r', 20)
.attr('fill', d => typeColors[d.type])
.attr('stroke', '#fff')
.attr('stroke-width', 2)
.call(drag(sim));

// 添加标签
svg.append('g')
.selectAll('text')
.data(socialData.nodes)
.join('text')
.text(d => d.name)
.attr('text-anchor', 'middle')
.attr('dy', 35)
.attr('font-size', '12px');

// 节点类型图例
const legend = svg.selectAll('.legend')
.data(Object.entries(typeColors))
.join('g')
.attr('transform', (d, i) => `translate(20,${i * 25 + 20})`);

legend.append('circle')
.attr('r', 10)
.attr('fill', d => d[1]);

legend.append('text')
.attr('x', 20)
.attr('y', 4)
.text(d => d[0])
.attr('font-size', '12px');

这个示例展示了力导向图的几个高级特性:使用 marker 定义箭头标记来表示关系的方向;节点根据 type(普通用户、意见领袖、管理员)使用不同的颜色;添加了图例来解释颜色含义。

静态力导向图

有时你不需要动画效果,而是希望得到一个静态的网络图以便导出或打印。此时可以在模拟达到稳定后停止它:

// 模拟一定代数后停止
const sim = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).distance(80))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2));

// 在 300 代后停止模拟(此时通常已经稳定)
sim.stop();
for (let i = 0; i < 300; ++i) sim.tick();

// 现在所有节点都有了稳定的 x, y 坐标
// 绘制静态图形...

这种方式可以在不运行动画的情况下快速得到力导向图的布局结果,特别适合需要将结果导出为图片的场景。

总结

力导向图是网络关系可视化的核心工具。D3 通过 d3-force 模块提供了完整的物理模拟引擎,包括节点间斥力(forceManyBody)、连线引力(forceLink)、中心引力(forceCenter)和碰撞检测(forceCollide)。理解这些力的作用原理是调优力导向图的关键。拖拽交互让用户能够手动探索网络结构,这在分析复杂关系数据时特别有用。