D3.js 具有动态偏移箭头的力导向图?
D3.js force-directed graph with dynamically offset arrows?
我在 D3.js 中有一个力导向图,其中节点半径与该数据的 属性 成正比(例如,综合浏览量),link 宽度与 link 成正比属性 的 linking 数据(例如点击次数)。我想给 link 曲线一个方向指示器。问题是 links 移动到数据节点的 center,所以如果我使用 marker-end
,我得到:
(数据节点通常用一种颜色填充 linked 到另一个数据类别...)
我创建我的 ~~arcs~~ 曲线使用:
positionLink = (d) => {
const offset = 100;
const midpoint_x = (d.source.x + d.target.x) / 2;
const midpoint_y = (d.source.y + d.target.y) / 2;
const dx = d.source.x - d.target.x;
const dy = d.source.y - d.target.y;
// Perpendicular vector
const nx = -dy;
const ny = dx;
const norm_length = Math.sqrt((nx*nx)+(ny*ny));
const normx = nx / norm_length;
const normy = ny / norm_length;
const offset_x = parseFloat(midpoint_x + offset * normx.toFixed(2));
const offset_y = parseFloat(midpoint_y + offset * normy.toFixed(2));
const arc = `M ${d.source.x.toFixed(2)} ${d.source.y.toFixed(2)} S ${offset_x} ${offset_y} ${d.target.x.toFixed(2)} ${d.target.y.toFixed(2)}`;
return arc;
};
我的代码调用的 arc
是 SVG“S”路径,这是一个“平滑曲线”,但我并不特别喜欢它:我只需要将弧线从每个弧线拉开另一个,以便我可以显示一个方向和另一个方向的数据之间的差异。
如何定位贝塞尔曲线与圆的交点?
(由于曲线的目标是圆心,我想这可以改写为“距离终点 r
处的贝塞尔曲线的值”)
如果我有一点,我可以把它变成箭头的顶点。
(如果我在那个点上有贝塞尔曲线的斜率就更好了,这样我就可以真正对齐它,但我认为我可以通过将它对齐到中点和锚点之间的线来逃脱...... .)
考虑以下迭代方法:
使用 path.getPointAtLength
,您可以遍历路径,直到找到与圆心正好 r
的点,然后使用这些坐标重新绘制路径。
const data = [{
x: 50,
y: 100,
r: 20
}, {
x: 100,
y: 30,
r: 5
}];
const links = [{
source: data[0],
target: data[1]
},
{
source: data[1],
target: data[0]
}
];
positionLink = (source, target) => {
const offsetPx = 100;
const midpoint = {
x: (source.x + target.x) / 2,
y: (source.y + target.y) / 2
};
const dx = source.x - target.x;
const dy = source.y - target.y;
// Perpendicular vector
const nx = -dy;
const ny = dx;
const norm_length = Math.sqrt((nx * nx) + (ny * ny));
const normx = nx / norm_length;
const normy = ny / norm_length;
const offset = {
x: parseFloat(midpoint.x + offsetPx * normx.toFixed(2)),
y: parseFloat(midpoint.y + offsetPx * normy.toFixed(2)),
};
const arc = `M ${source.x.toFixed(2)} ${source.y.toFixed(2)} S ${offset.x} ${offset.y} ${target.x.toFixed(2)} ${target.y.toFixed(2)}`;
return arc;
};
euclidean = (point, other) => Math.sqrt(point.x * other.x + point.y * other.y);
findPointAtLength = (path, point, fromEnd) => {
// For the target we need to start at the other side of the path
let offset = point.r;
if (fromEnd) {
const totalLength = path.getTotalLength();
offset = totalLength - offset;
}
let current = path.getPointAtLength(offset);
// Gradually increase the offset until we're exactly
// `r` away from the circle centre
while (euclidean(point, current) < point.r) {
offset += 1;
current = path.getPointAtLength(offset);
}
return {
x: current.x,
y: current.y
};
};
// Use function because we want access to `this`,
// which points to the current path HTMLElement
positionLinkAtEdges = function(d) {
// First, place the path in the old way
d3.select(this).attr("d", positionLink(d.source, d.target));
// Then, position the path away from the source
const source = findPointAtLength(this, d.source, false);
const target = findPointAtLength(this, d.target, true);
return positionLink(source, target);
}
const svg = d3.select("svg").append("g");
svg
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.r);
svg
.selectAll("path")
.data(links)
.enter()
.append("path")
.attr("d", positionLinkAtEdges)
.attr("marker-end", "url(#triangle)");
g circle,
g path {
fill: none;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg>
<defs>
<marker id="triangle" viewBox="0 0 10 10"
refX="10" refY="5"
markerUnits="strokeWidth"
markerWidth="10" markerHeight="10"
orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#f00"/>
</marker>
</defs>
</svg>
我在 D3.js 中有一个力导向图,其中节点半径与该数据的 属性 成正比(例如,综合浏览量),link 宽度与 link 成正比属性 的 linking 数据(例如点击次数)。我想给 link 曲线一个方向指示器。问题是 links 移动到数据节点的 center,所以如果我使用 marker-end
,我得到:
(数据节点通常用一种颜色填充 linked 到另一个数据类别...)
我创建我的 ~~arcs~~ 曲线使用:
positionLink = (d) => {
const offset = 100;
const midpoint_x = (d.source.x + d.target.x) / 2;
const midpoint_y = (d.source.y + d.target.y) / 2;
const dx = d.source.x - d.target.x;
const dy = d.source.y - d.target.y;
// Perpendicular vector
const nx = -dy;
const ny = dx;
const norm_length = Math.sqrt((nx*nx)+(ny*ny));
const normx = nx / norm_length;
const normy = ny / norm_length;
const offset_x = parseFloat(midpoint_x + offset * normx.toFixed(2));
const offset_y = parseFloat(midpoint_y + offset * normy.toFixed(2));
const arc = `M ${d.source.x.toFixed(2)} ${d.source.y.toFixed(2)} S ${offset_x} ${offset_y} ${d.target.x.toFixed(2)} ${d.target.y.toFixed(2)}`;
return arc;
};
我的代码调用的 arc
是 SVG“S”路径,这是一个“平滑曲线”,但我并不特别喜欢它:我只需要将弧线从每个弧线拉开另一个,以便我可以显示一个方向和另一个方向的数据之间的差异。
如何定位贝塞尔曲线与圆的交点?
(由于曲线的目标是圆心,我想这可以改写为“距离终点 r
处的贝塞尔曲线的值”)
如果我有一点,我可以把它变成箭头的顶点。
(如果我在那个点上有贝塞尔曲线的斜率就更好了,这样我就可以真正对齐它,但我认为我可以通过将它对齐到中点和锚点之间的线来逃脱...... .)
考虑以下迭代方法:
使用 path.getPointAtLength
,您可以遍历路径,直到找到与圆心正好 r
的点,然后使用这些坐标重新绘制路径。
const data = [{
x: 50,
y: 100,
r: 20
}, {
x: 100,
y: 30,
r: 5
}];
const links = [{
source: data[0],
target: data[1]
},
{
source: data[1],
target: data[0]
}
];
positionLink = (source, target) => {
const offsetPx = 100;
const midpoint = {
x: (source.x + target.x) / 2,
y: (source.y + target.y) / 2
};
const dx = source.x - target.x;
const dy = source.y - target.y;
// Perpendicular vector
const nx = -dy;
const ny = dx;
const norm_length = Math.sqrt((nx * nx) + (ny * ny));
const normx = nx / norm_length;
const normy = ny / norm_length;
const offset = {
x: parseFloat(midpoint.x + offsetPx * normx.toFixed(2)),
y: parseFloat(midpoint.y + offsetPx * normy.toFixed(2)),
};
const arc = `M ${source.x.toFixed(2)} ${source.y.toFixed(2)} S ${offset.x} ${offset.y} ${target.x.toFixed(2)} ${target.y.toFixed(2)}`;
return arc;
};
euclidean = (point, other) => Math.sqrt(point.x * other.x + point.y * other.y);
findPointAtLength = (path, point, fromEnd) => {
// For the target we need to start at the other side of the path
let offset = point.r;
if (fromEnd) {
const totalLength = path.getTotalLength();
offset = totalLength - offset;
}
let current = path.getPointAtLength(offset);
// Gradually increase the offset until we're exactly
// `r` away from the circle centre
while (euclidean(point, current) < point.r) {
offset += 1;
current = path.getPointAtLength(offset);
}
return {
x: current.x,
y: current.y
};
};
// Use function because we want access to `this`,
// which points to the current path HTMLElement
positionLinkAtEdges = function(d) {
// First, place the path in the old way
d3.select(this).attr("d", positionLink(d.source, d.target));
// Then, position the path away from the source
const source = findPointAtLength(this, d.source, false);
const target = findPointAtLength(this, d.target, true);
return positionLink(source, target);
}
const svg = d3.select("svg").append("g");
svg
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.r);
svg
.selectAll("path")
.data(links)
.enter()
.append("path")
.attr("d", positionLinkAtEdges)
.attr("marker-end", "url(#triangle)");
g circle,
g path {
fill: none;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg>
<defs>
<marker id="triangle" viewBox="0 0 10 10"
refX="10" refY="5"
markerUnits="strokeWidth"
markerWidth="10" markerHeight="10"
orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#f00"/>
</marker>
</defs>
</svg>