搜索文章

输入关键词开始搜索

如何在拖动节点时,让连线随之移动

D3.js#D3#数据可视化#AI

这是用 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 的类型,能少走很多弯路。