D3力导向图中如何在tick函数中根据节点组设置对应属性?
D3力导向图中如何在tick函数中根据节点组设置对应属性?
嘿,我明白你遇到的困扰了——不同形状的节点需要不同的位置属性,直接链式调用attr('cx', ...)对矩形完全不起作用,而且尝试用each的时候还摸不准怎么获取到当前DOM节点对吧?别着急,咱们一步步来解决:
问题根源
首先你要知道:SVG的<rect>元素是用x和y属性定义左上角位置的,而<circle>用的是cx和cy定义中心位置;另外你之前在each里如果用了箭头函数,this会绑定到外部的上下文,而不是当前的DOM节点,这就是为什么你调用this.setAttribute没效果的原因。
解决方案1:用each结合普通函数处理不同节点
把each里的箭头函数改成普通函数,这样this就会指向当前的DOM节点,然后根据数据里的group判断节点类型,设置对应的属性:
function ticked() { 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) // 改用普通函数,this指向当前DOM节点 node.each(function(d) { if (d.group === 1) { // 矩形要居中,所以x/y要减去宽高的一半(16/2=8) d3.select(this) .attr('x', d.x - 8) .attr('y', d.y - 8); } else { d3.select(this) .attr('cx', d.x) .attr('cy', d.y); } }); }
解决方案2:用transform统一设置位置(更简洁)
其实还有个更省心的方法:不管是圆还是矩形,都用transform的translate来设置位置,这样不用区分节点类型。只需要在创建节点的时候,把矩形的初始位置设为左上角在原点(x=-8、y=-8),圆形保持默认,这样translate之后所有节点的中心都会对齐到力导向计算的x/y坐标:
修改节点创建代码:
const node = svg .append('g') .attr('stroke', '#fff') .attr('stroke-width', 1.5) .selectAll() .data(nodes) .join((enter) => { return enter.append((d) => { if (d.group === 1) { const rectElement = document.createElementNS(svgNS, 'rect') rectElement.setAttribute('width', 16) rectElement.setAttribute('height', 16) rectElement.setAttribute('fill', color(d.group)) // 提前把矩形的左上角移到原点,方便后续translate居中 rectElement.setAttribute('x', -8) rectElement.setAttribute('y', -8) return rectElement } else { const circleElement = document.createElementNS(svgNS, 'circle') circleElement.setAttribute('r', 16) circleElement.setAttribute('fill', color(d.group)) return circleElement } }) })
然后ticked函数就变得超级简单:
function ticked() { 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) // 统一设置transform,不用区分节点类型 node.attr('transform', (d) => `translate(${d.x}, ${d.y})`) }
这个方法更符合D3的风格,代码也更简洁易维护~
完整修改后的代码
<style> .graph { width: 1000px; height: 400px; } </style> <script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script> <svg id="chart" class="graph"></svg> <script> const width = 1000 const height = 400 const svgNS = d3.namespace('svg:text').space const node_data = Array.from({ length: 5 }, () => ({ group: Math.floor(Math.random() * 3), })) const edge_data = Array.from({ length: 10 }, () => ({ source: Math.floor(Math.random() * 5), target: Math.floor(Math.random() * 5), value: Math.floor(Math.random() * 10) + 1, })) const links = edge_data.map((d) => ({ ...d })) const nodes = node_data.map((d, index) => ({ id: index, ...d })) const color = d3.scaleOrdinal(d3.schemeCategory10) const svg = d3.select('#chart') const simulation = d3 .forceSimulation(nodes) .force( 'link', d3 .forceLink(links) .id((d) => d.id) .distance((d) => 100) ) .force('charge', d3.forceManyBody()) .force('center', d3.forceCenter(width / 2, height / 2)) .on('tick', ticked) const link = svg .append('g') .attr('stroke', '#999') .attr('stroke-opacity', 0.6) .selectAll() .data(links) .join('line') .attr('stroke-width', (d) => Math.sqrt(d.value)) const node = svg .append('g') .attr('stroke', '#fff') .attr('stroke-width', 1.5) .selectAll() .data(nodes) .join((enter) => { return enter.append((d) => { if (d.group === 1) { const rectElement = document.createElementNS(svgNS, 'rect') rectElement.setAttribute('width', 16) rectElement.setAttribute('height', 16) rectElement.setAttribute('fill', color(d.group)) rectElement.setAttribute('x', -8) rectElement.setAttribute('y', -8) return rectElement } else { const circleElement = document.createElementNS(svgNS, 'circle') circleElement.setAttribute('r', 16) circleElement.setAttribute('fill', color(d.group)) return circleElement } }) }) function ticked() { 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('transform', (d) => `translate(${d.x}, ${d.y})`) } </script>
备注:内容来源于stack exchange,提问作者Eric G




