一、力导向图核心概念
力导向图是一种通过物理模拟来展示复杂关系网络的图表类型,特别适合表现社交网络、知识图谱、系统拓扑等关系型数据。其核心原理是通过模拟粒子间的物理作用力(电荷斥力、弹簧引力等)自动计算节点的最优布局。
核心API详解
1. 力模拟系统
const simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-100)) // 节点间作用力
.force("link", d3.forceLink(links).id(d => d.id)) // 连接线作用力
.force("center", d3.forceCenter(width/2, height/2)) // 向心力
.force("collision", d3.forceCollide().radius(20)); // 碰撞检测
2. 关键作用力类型
力类型 | 作用描述 | 常用配置方法 |
---|---|---|
forceManyBody | 节点间电荷力(正为引力,负为斥力) | .strength() |
forceLink | 连接线弹簧力 | .distance().id().strength() |
forceCenter | 向中心点的引力 | .x().y() |
forceCollide | 防止节点重叠的碰撞力 | .radius().strength() |
forceX/Y | 沿X/Y轴方向的定位力 | .strength().x()/.y() |
3. 动态控制方法
simulation
.alpha(0.3) // 设置当前alpha值(0-1)
.alphaTarget(0.1) // 设置目标alpha值
.alphaDecay(0.02) // 设置衰减率(默认0.0228)
.velocityDecay(0.4)// 设置速度衰减(0-1)
.restart() // 重启模拟
.stop() // 停止模拟
.tick() // 手动推进模拟一步
二、增强版力导向图实现
👇 具体代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>D3.js 力导向图 Demo</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: 0.6;
}
.link-text {
font-size: 10px;
fill: #333;
pointer-events: none;
}
.node-text {
font-size: 12px;
font-weight: bold;
pointer-events: none;
}
.tooltip {
position: absolute;
padding: 8px;
background: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 4px;
pointer-events: none;
font-size: 12px;
}
.controls {
margin-bottom: 15px;
}
button {
padding: 5px 10px;
margin-right: 10px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="controls">
<button id="reset">重置布局</button>
<button id="addNode">添加随机节点</button>
<span>斥力强度: <input type="range" id="charge" min="-500" max="-50" value="-200"></span>
</div>
<svg width="800" height="600"></svg>
<div class="tooltip"></div>
<script>
// 示例数据 - 人物和地点关系网络
const nodes = [
{ id: 1, name: "北京", type: "location", size: 15 },
{ id: 2, name: "上海", type: "location", size: 15 },
{ id: 3, name: "广州", type: "location", size: 15 },
{ id: 4, name: "张三", type: "person", size: 10 },
{ id: 5, name: "李四", type: "person", size: 10 },
{ id: 6, name: "王五", type: "person", size: 10 },
{ id: 7, name: "CEO", type: "role", size: 12 },
{ id: 8, name: "CTO", type: "role", size: 12 }
];
const links = [
{ source: 1, target: 4, relation: "居住", value: 2 },
{ source: 2, target: 5, relation: "工作", value: 1.5 },
{ source: 3, target: 6, relation: "出生", value: 2.5 },
{ source: 4, target: 5, relation: "朋友", value: 1 },
{ source: 5, target: 6, relation: "同事", value: 1.2 },
{ source: 4, target: 7, relation: "职位", value: 1.8 },
{ source: 5, target: 8, relation: "职位", value: 1.8 },
{ source: 7, target: 8, relation: "合作", value: 1.5 }
];
// 初始化SVG
const svg = d3.select('svg');
const width = +svg.attr('width');
const height = +svg.attr('height');
const tooltip = d3.select('.tooltip');
// 创建画布
const g = svg.append('g');
// 颜色比例尺
const colorScale = d3.scaleOrdinal()
.domain(["location", "person", "role"])
.range(["#66c2a5", "#fc8d62", "#8da0cb"]);
// 创建力导向图模拟
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-200))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(d => d.size + 5));
// 创建连接线
const link = g.append('g')
.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link')
.attr('stroke-width', d => Math.sqrt(d.value));
// 创建连接线文字
const linkText = g.append('g')
.selectAll('.link-text')
.data(links)
.enter().append('text')
.attr('class', 'link-text')
.text(d => d.relation);
// 创建节点组
const node = g.append('g')
.selectAll('.node')
.data(nodes)
.enter().append('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded)
)
.on('mouseover', showTooltip)
.on('mouseout', hideTooltip);
// 添加节点圆形
node.append('circle')
.attr('r', d => d.size)
.attr('fill', d => colorScale(d.type))
.attr('stroke', '#fff')
.attr('stroke-width', 2);
// 添加节点文字
node.append('text')
.attr('class', 'node-text')
.attr('dy', 4)
.text(d => d.name);
// 模拟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);
linkText
.attr('x', d => (d.source.x + d.target.x) / 2)
.attr('y', d => (d.source.y + d.target.y) / 2);
node
.attr('transform', d => `translate(${d.x},${d.y})`);
});
// 拖拽事件处理
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
// 工具提示
function showTooltip(event, d) {
tooltip.transition()
.duration(200)
.style('opacity', 0.9);
tooltip.html(`<strong>${d.name}</strong><br/>类型: ${d.type}`)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 28) + 'px');
// 高亮相关节点和连接线
node.select('circle').attr('opacity', 0.2);
d3.select(this).select('circle').attr('opacity', 1);
link.attr('stroke-opacity', 0.1);
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr('stroke-opacity', 0.8)
.attr('stroke', '#ff0000');
}
function hideTooltip() {
tooltip.transition()
.duration(500)
.style('opacity', 0);
// 恢复所有元素样式
node.select('circle').attr('opacity', 1);
link.attr('stroke-opacity', 0.6)
.attr('stroke', '#999');
}
// 交互控制
d3.select('#reset').on('click', () => {
simulation.alpha(1).restart();
nodes.forEach(d => {
d.fx = null;
d.fy = null;
});
});
d3.select('#addNode').on('click', () => {
const newNodeId = nodes.length + 1;
const types = ["person", "location", "role"];
const newNode = {
id: newNodeId,
name: `节点${newNodeId}`,
type: types[Math.floor(Math.random() * types.length)],
size: Math.random() * 10 + 8
};
nodes.push(newNode);
// 随机连接到现有节点
if (nodes.length > 1) {
const randomTarget = Math.floor(Math.random() * (nodes.length - 1)) + 1;
const relations = ["朋友", "同事", "居住", "工作", "合作", "管理"];
links.push({
source: newNode.id,
target: randomTarget,
relation: relations[Math.floor(Math.random() * relations.length)],
value: Math.random() * 2 + 0.5
});
}
// 更新图形
updateGraph();
});
d3.select('#charge').on('input', function() {
simulation.force('charge').strength(+this.value);
simulation.alpha(0.3).restart();
});
// 更新图形函数
function updateGraph() {
// 更新节点
const nodeUpdate = node.data(nodes, d => d.id);
// 移除不再需要的节点
nodeUpdate.exit().remove();
// 添加新节点
const newNode = nodeUpdate.enter()
.append('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded)
)
.on('mouseover', showTooltip)
.on('mouseout', hideTooltip);
newNode.append('circle')
.attr('r', d => d.size)
.attr('fill', d => colorScale(d.type))
.attr('stroke', '#fff')
.attr('stroke-width', 2);
newNode.append('text')
.attr('class', 'node-text')
.attr('dy', 4)
.text(d => d.name);
// 合并更新
node = newNode.merge(nodeUpdate);
// 更新连接线
const linkUpdate = link.data(links, d => `${d.source.id}-${d.target.id}`);
// 移除不再需要的连接线
linkUpdate.exit().remove();
// 添加新连接线
const newLink = linkUpdate.enter()
.append('line')
.attr('class', 'link')
.attr('stroke-width', d => Math.sqrt(d.value));
// 合并更新
link = newLink.merge(linkUpdate);
// 更新连接线文字
const linkTextUpdate = linkText.data(links, d => `${d.source.id}-${d.target.id}`);
// 移除不再需要的连接线文字
linkTextUpdate.exit().remove();
// 添加新连接线文字
const newLinkText = linkTextUpdate.enter()
.append('text')
.attr('class', 'link-text')
.text(d => d.relation);
// 合并更新
linkText = newLinkText.merge(linkTextUpdate);
// 更新力导向图
simulation.nodes(nodes);
simulation.force("link").links(links);
simulation.alpha(1).restart();
}
</script>
</body>
</html>
👇 实现效果
小结
核心实现要点
1. 力模拟系统构建:
- 多力组合实现复杂布局(电荷力+弹簧力+向心力+碰撞力)
- 参数调优实现不同视觉效果
2. 动态交互体系:
- 拖拽行为与物理模拟的协调
- 动态alpha值控制模拟过程
- 实时tick更新机制
3. 可视化增强:
- 基于类型的颜色编码
- 交互式高亮关联元素
- 动态工具提示显示
高级特性实现
1. 动态数据更新:
- 节点/连接的实时添加
- 模拟系统的热更新
2. 交互控制面板:
- 力参数实时调节
- 布局重置功能
3. 视觉优化:
- 智能碰撞检测
- 连接线权重可视化
- 焦点元素高亮