如何为D3力导向图添加指向目标节点的箭头?
给D3力导向图连线添加指向目标节点的箭头
我正在改造一个D3力导向图代码库,当前已实现可拖拽的力导向图效果,需要将连线修改为末端带箭头的样式,箭头从源节点指向目标节点。
原始代码
JavaScript
/* eslint-disable no-undef */ var width = 600; var height = 600; var nodes = [ { color: "red", size: 5 }, { color: "orange", size: 10 }, { color: "yellow", size: 15 }, { color: "green", size: 20 }, { color: "blue", size: 25 }, { color: "purple", size: 30 } ]; var links = [ { source: "red", target: "orange" }, { source: "orange", target: "yellow" }, { source: "yellow", target: "green" }, { source: "green", target: "blue" }, { source: "blue", target: "purple" }, { source: "purple", target: "red" }, { source: "green", target: "red" } ]; var svg = d3 .select("svg") .attr("width", width) .attr("height", height); var linkSelection = svg .selectAll("line") .data(links) .enter() .append("line") .attr("stroke", "black") .attr("stroke-width", 1); var nodeSelection = svg .selectAll("circle") .data(nodes) .enter() .append("circle") .attr("r", d => d.size) .attr("fill", d => d.color) .call( d3 .drag() .on("start", dragStart) .on("drag", drag) .on("end", dragEnd) ); var simulation = d3.forceSimulation(nodes); simulation .force("center", d3.forceCenter(width / 2, height / 2)) .force("nodes", d3.forceManyBody()) .force( "links", d3 .forceLink(links) .id(d => d.color) .distance(d => 5 * (d.source.size + d.target.size)) ) .on("tick", ticked); function ticked() { nodeSelection.attr("cx", d => d.x).attr("cy", d => d.y); linkSelection .attr("x1", d => d.source.x) .attr("y1", d => d.source.y) .attr("x2", d => d.target.x) .attr("y2", d => d.target.y); } function dragStart(d) { simulation.alphaTarget(0.5).restart(); d.fx = d.x; d.fy = d.y; } function drag(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragEnd(d) { simulation.alphaTarget(0); d.fx = null; d.fy = null; }
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script> <svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" ></svg>
修改方案
要实现带箭头的连线,需要完成两步:定义SVG箭头标记,调整连线终点位置并绑定箭头。
1. 添加箭头标记定义
在创建SVG之后,添加<defs>元素定义可复用的箭头:
// 在svg变量定义后添加 svg.append("defs").append("marker") .attr("id", "arrowhead") .attr("viewBox", "0 -5 10 10") // 箭头的坐标范围 .attr("refX", 25) // 箭头相对于连线终点的偏移量,需匹配节点大小调整 .attr("refY", 0) .attr("markerWidth", 6) // 箭头宽度 .attr("markerHeight", 6) // 箭头高度 .attr("orient", "auto") // 自动根据连线方向旋转箭头 .append("path") .attr("d", "M0,-5L10,0L0,5") // 箭头形状路径 .attr("fill", "black");
2. 修改连线渲染逻辑
更新linkSelection的创建,添加箭头引用,并在ticked函数中调整连线终点到目标节点边缘(避免箭头被节点遮挡):
调整linkSelection初始化
var linkSelection = svg .selectAll("line") .data(links) .enter() .append("line") .attr("stroke", "black") .attr("stroke-width", 1) .attr("marker-end", "url(#arrowhead)"); // 绑定箭头
更新ticked函数中的连线位置计算
function ticked() { nodeSelection.attr("cx", d => d.x).attr("cy", d => d.y); linkSelection .attr("x1", d => d.source.x) .attr("y1", d => d.source.y) // 计算目标节点边缘的终点位置 .attr("x2", d => { const dx = d.target.x - d.source.x; const dy = d.target.y - d.source.y; const distance = Math.sqrt(dx * dx + dy * dy); const targetRadius = d.target.size; // 从目标中心向源节点方向退回一个半径的距离 return d.target.x - (dx / distance) * targetRadius; }) .attr("y2", d => { const dx = d.target.x - d.source.x; const dy = d.target.y - d.source.y; const distance = Math.sqrt(dx * dx + dy * dy); const targetRadius = d.target.size; return d.target.y - (dy / distance) * targetRadius; }); }
完整修改后的代码
JavaScript
/* eslint-disable no-undef */ var width = 600; var height = 600; var nodes = [ { color: "red", size: 5 }, { color: "orange", size: 10 }, { color: "yellow", size: 15 }, { color: "green", size: 20 }, { color: "blue", size: 25 }, { color: "purple", size: 30 } ]; var links = [ { source: "red", target: "orange" }, { source: "orange", target: "yellow" }, { source: "yellow", target: "green" }, { source: "green", target: "blue" }, { source: "blue", target: "purple" }, { source: "purple", target: "red" }, { source: "green", target: "red" } ]; var svg = d3 .select("svg") .attr("width", width) .attr("height", height); // 定义箭头标记 svg.append("defs").append("marker") .attr("id", "arrowhead") .attr("viewBox", "0 -5 10 10") .attr("refX", 25) .attr("refY", 0) .attr("markerWidth", 6) .attr("markerHeight", 6) .attr("orient", "auto") .append("path") .attr("d", "M0,-5L10,0L0,5") .attr("fill", "black"); var linkSelection = svg .selectAll("line") .data(links) .enter() .append("line") .attr("stroke", "black") .attr("stroke-width", 1) .attr("marker-end", "url(#arrowhead)"); var nodeSelection = svg .selectAll("circle") .data(nodes) .enter() .append("circle") .attr("r", d => d.size) .attr("fill", d => d.color) .call( d3 .drag() .on("start", dragStart) .on("drag", drag) .on("end", dragEnd) ); var simulation = d3.forceSimulation(nodes); simulation .force("center", d3.forceCenter(width / 2, height / 2)) .force("nodes", d3.forceManyBody()) .force( "links", d3 .forceLink(links) .id(d => d.color) .distance(d => 5 * (d.source.size + d.target.size)) ) .on("tick", ticked); function ticked() { nodeSelection.attr("cx", d => d.x).attr("cy", d => d.y); linkSelection .attr("x1", d => d.source.x) .attr("y1", d => d.source.y) .attr("x2", d => { const dx = d.target.x - d.source.x; const dy = d.target.y - d.source.y; const distance = Math.sqrt(dx * dx + dy * dy); const targetRadius = d.target.size; return d.target.x - (dx / distance) * targetRadius; }) .attr("y2", d => { const dx = d.target.x - d.source.x; const dy = d.target.y - d.source.y; const distance = Math.sqrt(dx * dx + dy * dy); const targetRadius = d.target.size; return d.target.y - (dy / distance) * targetRadius; }); } function dragStart(d) { simulation.alphaTarget(0.5).restart(); d.fx = d.x; d.fy = d.y; } function drag(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragEnd(d) { simulation.alphaTarget(0); d.fx = null; d.fy = null; }
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script> <svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" ></svg>
关键说明
refX的取值需要根据目标节点的大小调整,确保箭头不会被节点遮挡,这里设置为25适配最大节点(size=30)的边缘位置。orient="auto"确保箭头始终指向目标节点的方向,无论连线如何旋转。- 调整连线终点到目标节点边缘的计算,避免箭头被节点图形覆盖,保证视觉效果正确。
内容的提问来源于stack exchange,提问作者Alexander Mills
相关产品推荐
相关产品推荐

