D3 强制布局 - 链接超出容器的边界,同时拖动节点

D3 force layout - Links exceed the boundaries of the container, while dragging the nodes

我创建了一个 d3-force 布局。仅允许在边界框内拖动节点。当节点靠近边界时,节点根据以下函数保持在其位置固定:

function boundX(x) {
    return Math.max(0, Math.min(width - (nodeWidth+padding), x));
}

function boundY(y){
    return Math.max(0, Math.min(height - (nodeHeight+padding), y))
}

节点通过折线连接。每条多段线由两个线段表征。第一段由源节点的 (d.source.x+nodeWidth/2, d.source.y+nodeHeight/2) 坐标定义,它是节点的中点和直线与目标节点的交点。第二段从与目标节点的交点开始,到目标节点的中点(d.target.x+nodeWidth/2)结束。直线与目标节点的交点是沿折线放置标记的位置。这是代码的一部分 - 在 tick 函数中 - 负责计算交点并绘制线:

    function tick() {

    link.attr("points", function(d) {

        var interForw = pointOnRect(d.source.x, d.source.y,
            d.target.x - nodeWidth / 2, d.target.y - nodeHeight / 2,
            d.target.x + nodeWidth / 2, d.target.y + nodeHeight / 2);

        if(d.direction==="forward") {
        return boundXInter((d.source.x+nodeWidth/2) + " "
            + boundYInter(((d.source.y) + nodeHeight / 2) + ","
            + boundXInter(((interForw.x+nodeWidth/2))) + " "
            + boundYInter((interForw.y) + nodeHeight / 2) + ","
            + boundXInter(d.target.x+nodeWidth/2) + " "
            + boundYInter((d.target.y) + nodeHeight / 2);
}

这些是定义链接边界的函数:

    function boundXInter(x) {
    return Math.max(nodeWidth/2, Math.min(width - (nodeWidth/2+padding),x));
}
function boundYInter(y){
    return Math.max(nodeHeight/2, Math.min(height - (nodeHeight/2+padding), y));
}

当两个节点一个位于另一个节点下方时,如第一张图片所示。它的行为符合预期。

但是,当节点如下图所示放置时,如果用户继续拖动节点,即使他们不允许进一步移动边界,节点也会被阻止,但链接会继续移动直到width-nodeWidth/2点,根据boundXInter函数。

我想要实现的是交点(标记),在这种情况下,直线的第一段不会比实际位置移动得更远,如第三张图所示。我希望它是固定的,而不是线段延伸到 width-nodeWidth/2 位置,如下图所示。重新格式化 boundXInter 函数可能会完成这项工作。但是,我尝试了很多组合,但没有。我想提一下,如果用户停止拖动链接 return 到所需的状态(如第二张图所示)

有什么想法吗?在这种情况下,我该怎么做才能得到正确的结果?

您可以在此处找到工作片段:https://jsfiddle.net/yx2grm4s/39/

您混合了相对于中心和相对于矩形左上角的建模。相对于中心(就是 node 位置)做比较好。除了边界检查之外,不要更改节点位置。矩形、标签、link和点只是相对于节点位置的装饰。

在更新 "in between node" 内容之前还要先绑定节点位置,这样 永远不会 需要再次绑定。在框周围使用漂亮均匀的填充。

删除了代码重复。

完成运行代码:https://jsfiddle.net/y0eox2vn/1/

基本代码部分

var link = svg.append("g")
    .selectAll(".link")
    .data(links)
    .enter()
    // .append("line")
    .append("polyline")
    .attr("class", "link")
    .style("stroke-width","1")
    .style("stroke","black")
    .style("fill","none")
    .attr("id", function (d, i) { return 'link'+i; });

var markerFor = svg.append("defs")
    .selectAll("marker")
    .data(["forward"])
    .enter()
    .append("marker")
    .attr("id", "dirArrowFor")
    .attr("viewBox", "0 -5 10 10")
    .attr("markerUnits", "strokeWidth")
    .attr("markerWidth", 10)
    .attr("markerHeight", 10)
    .attr("refX",10)
    .attr("refY", 0)
    .attr("overflow", "visible")
    .attr("orient", "auto")
    .append("path")
    .attr("d", "M0,-5L10,0L0,5")
    .style("fill", "#000000");

    link.attr("marker-mid", checkDir);

 var linkNode = svg.append("g").selectAll(".link")
    .data(links)
    .enter()
    .append("circle")
    .attr("class","link-node")
    .attr("r",4)
    .style("fill","#c00");

linkNode.append("title")
    .text(function(d) { return d.linkingWord; });

 var node = svg.append("g").selectAll(".node")
    .data(nodes)
    .enter()
    .append("rect")
    .attr("class","node")
    .attr("width", conceptWidth)
    .attr("height", conceptHeight)
    .attr("rx",20)
    .attr("ry",20)
    .style('fill',function(d){ return d.color;})
    .call(d3.drag()
        .on("start", dragStarted)
        .on("drag", dragged)
        .on("end", dragEnded));

var labels = svg.append("g")
    .selectAll(".labels")
    .data(nodes)
    .enter()
    .append("text")
    .attr("class", "labels")
    .text(function(d){ return d.name;})
    .style("text-anchor","middle")
    .attr("dy", 5);

var force = d3.forceSimulation()
    .force("collision", d3.forceCollide(conceptWidthHalf +1).iterations(1))
    .force("link", d3.forceLink().id(function(d){ return d.name;}))
    .on("tick", tick);

force.nodes(nodes);
force.force("link").links(links);

function interForwRev(d) {
    var interForw = pointOnRect(d.source.x, d.source.y,
        d.target.x - conceptWidthHalf, d.target.y - conceptHeightHalf,
        d.target.x + conceptWidthHalf, d.target.y + conceptHeightHalf);

    var interRev = pointOnRect(d.target.x, d.target.y,
        d.source.x - conceptWidthHalf, d.source.y - conceptHeightHalf ,
        d.source.x + conceptWidthHalf, d.source.y + conceptHeightHalf);
    return [interForw, interRev];
}

function tick() {

    node.attr("x", function(d) { d.x=boundX(d.x); return d.x - conceptWidthHalf;  })
        .attr("y", function(d) { d.y=boundY(d.y); return d.y - conceptHeightHalf; });

    labels.attr("x", function(d) { return d.x;  })
          .attr("y", function(d) { return d.y; });

    linkNode.attr("cx", function (d) {
        var interFR = interForwRev(d);
        var interForw = interFR[0];
        var interRev  = interFR[1];

        return d.cx = (interForw.x + interRev.x)*0.5;
    })
    .attr("cy", function (d) {
        var interFR = interForwRev(d);
        var interForw = interFR[0];
        var interRev  = interFR[1];

        return d.cy = (interForw.y + interRev.y)*0.5;
    });

    // update the links after the nodes so we are already bounded by the box
    link.attr("points", function(d) {
        var interFR = interForwRev(d);
        var interForw = interFR[0];
        var interRev  = interFR[1];

        if(d.direction==="forward") {
            return `${d.source.x} ${d.source.y},${interForw.x} ${interForw.y},${d.target.x} ${d.target.y}`;
        }
    });
}

function boundX(x) {
    return Math.max(conceptWidthHalf+padding, Math.min(width - (conceptWidthHalf+padding), x));
}

function boundY(y){
    return Math.max(conceptHeightHalf+padding, Math.min(height - (conceptHeightHalf+padding), y))
}

// NO other bound function needed

function dragStarted(d) {
    if (!d3.event.active) force.alphaTarget(0.03).restart();

    d.fx = d.x;
    d.fy = d.y;
}