我正在使用力布局来表示有向未加权网络。我的灵感来自以下示例:http ://bl.ocks.org/mbostock/1153292
我试图制作不同大小的节点,但我有一个小问题。用于在每个链接上绘制箭头的标记指向圆的中心。如果圆圈太大,它会完全覆盖箭头。
我该如何处理?
我正在使用力布局来表示有向未加权网络。我的灵感来自以下示例:http ://bl.ocks.org/mbostock/1153292
我试图制作不同大小的节点,但我有一个小问题。用于在每个链接上绘制箭头的标记指向圆的中心。如果圆圈太大,它会完全覆盖箭头。
我该如何处理?
如果您将使用 a<line>
而不是<path>
,则以下内容应该适合您,我在我当前的解决方案中使用它。它基于@ɭɘ ɖɵʊɒɼɖ江戸解决方案:
在您的tick
事件侦听器中:
linkElements.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) {
return getTargetNodeCircumferencePoint(d)[0];
})
.attr("y2", function(d) {
return getTargetNodeCircumferencePoint(d)[1];
});
function getTargetNodeCircumferencePoint(d){
var t_radius = d.target.nodeWidth/2; // nodeWidth is just a custom attribute I calculate during the creation of the nodes depending on the node width
var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;
var gamma = Math.atan2(dy,dx); // Math.atan2 returns the angle in the correct quadrant as opposed to Math.atan
var tx = d.target.x - (Math.cos(gamma) * t_radius);
var ty = d.target.y - (Math.sin(gamma) * t_radius);
return [tx,ty];
}
我确信可以修改此解决方案以容纳<path>
元素,但是我还没有尝试过。
您可以通过节点的半径偏移链接的目标,即调整代码
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
通过更改 和 的值d.target.x
来d.target.y
考虑半径(这需要成为数据的一部分,例如d.target.radius
)。也就是说,将箭头的末端偏移圆半径。
At the end I've decided to create a marker for each link (instead of one per class). This solution has the advantage of defining the offset of each marker, depending on the target node which, in my own case, is refX.
// One marker for link...
svg.append("svg:defs").selectAll("marker")
.data(force.links())
.enter().append("svg:marker")
.attr("id", function(link, idx){ return 'marker-' + idx})
.attr("viewBox", "0 -5 10 10")
.attr("refX", function(link, idx){
return 10 + link.target.size;
})
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", function(link){
if(link.type == 'in')
return "green";
return "blue";
});
Now there is one little problem with the fact that the line is curve. This means that the marker/arrow should be translated not only on the X axis, but also on the Y axis, of a value which probably depends on the ray of the curve...
回答有点晚了,但结合所有以前的答案,我想出了一个全面的解决方案,适用于 d3 v4,因为 Angular 是用 TypeScript 编写的(以防你发现缺少全局变量感到好奇)。下面是一个包含要包含的关键组件的片段(因为我的整个生产代码太长并且在 NDA 下)。关键思想被注释为代码注释。最终结果如下所示:
首先,由于您尝试制作不同大小的节点,我将假设您的节点数据中有一个 radius 属性。假设它是一个这样的对象数组:
{
id: input.name,
type: input.type,
radius: input.radius
}
然后附加标记。请注意,每个箭头(或标记)的大小为 10,其中一半为 5。您可以将其分配为变量,如 @ɭɘ-ɖɵʊɒɼɖ-江戸 在他的回答中所做的那样,但我太懒了。
let marker = svg.append("defs")
.attr("class", "defs")
.selectAll("marker")
// Assign a marker per link, instead of one per class.
.data(links, function (d) { return d.source.id + "-" + d.target.id; });
// Update and exit are omitted.
// Enter
marker = marker
.enter()
.append("marker")
.style("fill", "#000")
// Markers are IDed by link source and target's name.
// Spaces stripped because id can't have spaces.
.attr("id", function (d) { return (d.source.id + "-" + d.target.id).replace(/\s+/g, ''); })
// Since each marker is using the same data as each path, its attributes can similarly be modified.
// Assuming you have a "value" property in each link object, you can manipulate the opacity of a marker just like a path.
.style("opacity", function (d) { return Math.min(d.value, 1); })
.attr("viewBox", "0 -5 10 10")
// refX and refY are set to 0 since we will use the radius property of the target node later on, not here.
.attr("refX", 0)
.attr("refY", 0)
.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.merge(marker);
然后,路径可以使用其 ID 引用每个单独的标记:
let path = svg.append("g")
.attr("class", "paths")
.selectAll("path")
.data(links, function (d) { return d.source.id + "-" + d.target.id; });
// Update and exit are omitted.
// Enter
path = path
.enter()
.append("path")
.attr("class", "enter")
.style("fill", "none")
.style("stroke", "#000")
.style("stroke-opacity", function (d) { return Math.min(d.value, 1); })
// This is how to connect each path to its respective marker
.attr("marker-end", function(d) { return "url(#" + (d.source.id + "-" + d.target.id).replace(/\s+/g, '') + ")"; })
.merge(path);
如果您想要更多功能,可以修改一件可选的事情:允许您的 .on("tick", ticked) 侦听器接收更多变量以测试边界。例如,svg 的宽度和高度。
.on("tick", function () { ticked(node, path, width, height) })
这是您的新勾选功能,基于@ɭɘ-ɖɵʊɒɼɖ-江戸的回答:
ticked(node, path, width, height) {
node
.attr("transform", function(d){return "translate(" + Math.max(d.radius, Math.min(width - d.radius, d.x)) + "," + Math.max(d.radius, Math.min(height - d.radius, d.y)) + ")"});
path
.attr("d", d => {
let dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy),
gamma = Math.atan2(dy, dx), // Math.atan2 returns the angle in the correct quadrant as opposed to Math.atan
sx = Math.max(d.source.radius, Math.min(width - d.source.radius, d.source.x + (Math.cos(gamma) * d.source.radius) )),
sy = Math.max(d.source.radius, Math.min(height - d.source.radius, d.source.y + (Math.sin(gamma) * d.source.radius) )),
// Recall that 10 is the size of the arrow
tx = Math.max(d.target.radius, Math.min(width - d.target.radius, d.target.x - (Math.cos(gamma) * (d.target.radius + 10)) )),
ty = Math.max(d.target.radius, Math.min(height - d.target.radius, d.target.y - (Math.sin(gamma) * (d.target.radius + 10)) ));
// If you like a tighter curve, you may recalculate dx dy dr:
//dx = tx - sx;
//dy = ty - sy;
//dr = Math.sqrt(dx * dx + dy * dy);
return "M" + sx + "," + sy + "A" + dr + "," + dr + " 0 0,1 " + tx + "," + ty;
});
}
正如@joshua-comeau 所提到的,在计算 sx 和 sy 时它应该是一个加号。
这是我的解决方案:
首先,我计算与路径水平轴的角度 ( gamma
)。然后我得到半径的 X 分量 ( Math.cos(gamma) * radius
) 和 Y 分量 ( Math.sin(gamma) * radius
)。然后通过这些组件偏移路径的末端。
function linkArc(d) {
var t_radius = calcRadius(d.target.size);
var s_radius = calcRadius(d.source.size);
var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;
var gamma = Math.atan(dy / dx);
var tx = d.target.x - (Math.cos(gamma) * t_radius);
var ty = d.target.y - (Math.sin(gamma) * t_radius);
var sx = d.source.x - (Math.cos(gamma) * s_radius);
var sy = d.source.y - (Math.sin(gamma) * s_radius);
return "M" + sx + "," + sy + "L" + tx + "," + ty;
}
首先你会注意到我没有使用弧,但原理应该是一样的。我的节点也有一个 size 属性,我可以从中计算圆的直径。
最后我的标记被定义为:
var arrowsize = 10;
var asHalf = arrowsize / 2;
svg.append("defs").selectAll("marker")
.data(["arrowhead"])
.enter().append("marker")
.attr("id", function (d) {
return d;
})
.attr("viewBox", "0 -5 " + arrowsize + " " + arrowsize)
.attr("refX", arrowsize)
.attr("refY", 0)
.attr("markerWidth", 9)
.attr("markerHeight", 9)
.attr("orient", "auto")
.attr("class", "arrowhead-light")
.append("path")
.attr("d", "M 0," + (asHalf * -1) + " L " + arrowsize + ",0 L 0," + asHalf);
我还没有找到一种方法来控制标记的每个副本。