如何在拖动节点时,让连线随之移动
这是用 D3.js 做关系图时遇到的一个具体需求:节点可以拖动,拖动后连在这个节点上的线要跟着移动。这里记录的是当时采用的方案,核心是利用 JavaScript 的对象引用传递特性来实现【数据驱动】。
这个需求的关键不在于单独修改线的位置,而是让节点和连线都从同一份数据重新计算。拖动时修改节点坐标,再调用 render 函数,连线自然会根据节点的新位置更新。
如果想要做得更自动化一点,可以加入观察者模式,通过Object.defineProperty自动监测属性的变化,自动调用render方法,这样你只需关注数据的增删改查即可。
注意数据必须是对象,不能是基本数据类型。
给被拖拽的元素设置id属性
比如我是将 circle 和 text 用一个 g 包裹起来了,那么我就给 g 元素设置 id 属性:
function drawNodesAndTexts(nodes) {
let nodeGroups = getContainer().selectAll('g')
.data(nodes)
.enter()
.append('g')
// 关键代码在这一行
.attr('id', n => `node_${n.id}`)
.attr('transform', (d) => {
return `translate(${d.x}, ${d.y})`;
})
// 给整个组绑定拖拽事件
.call(drag());
drawNodes(nodeGroups)
drawText(nodeGroups)
}
给线条设置属性,其值为关联的节点的ID
function drawRelationships(relationships) {
let container = getContainer();
container.selectAll('line')
.data(relationships)
.enter()
.append('line')
.attr('x1', r => {
return nodeMap[r.from].x;
})
.attr('y1', r => {
return nodeMap[r.from].y;
})
.attr('x2', r => {
return nodeMap[r.to].x;
})
.attr('y2', r => {
return nodeMap[r.to].y;
})
// 关键代码是下面的两行
.attr('from', r => r.from)
.attr('to', r => r.to)
.attr('stroke', 'black')
.attr('stroke-width', '2px')
}
在drag处理函数中,修改line的x1、y1、x2、y2属性
function move(d)
{
let x = d3.event.x,
y = d3.event.y;
if (!inBoundaries(x, y)) {
return false;
}
d3.select('#coordinate').html(`x=${x}, y=${y}`)
let element = d3.select(this);
element.raise()
.attr('transform', () => {
return `translate(${d.x = x}, ${d.y = y})`;
})
let id = element.attr('id');
if ('undefined' === typeof id) {
return;
}
let idInfo = id.split('_')
if ('undefined' === typeof idInfo[1]) {
return;
}
id = idInfo[1];
// 【经验教训】通过属性筛选元素,属性的值必须是一个字符串,不能是数字
d3.selectAll(`line[from="${id}"]`)
.attr('x1', x)
.attr('y1', y)
d3.selectAll(`line[to="${id}"]`)
.attr('x2', x)
.attr('y2', y)
}
经验教训
这次最值得记住的一点是:通过属性选择器(line[from="..."])筛选元素时,属性的值必须是字符串,不能是数字,否则选不到元素,连线就不动了。调试这类问题,先用 console.log 确认 id 的类型,能少走很多弯路。