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;
}
我创建了一个 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;
}