在绘制节点链接图时,有时会存在这样的需求:我们需要固定某些节点的坐标,而其它节点遵循力导向布局,坐标位置不断变换,直到得到稳定的布局结果。
效果图如下图所示,红色节点是固定了坐标的节点,灰色节点没有固定坐标,在力导向布局过程中红色节点的坐标始终不会发生变化。
在线演示:https://codepen.io/yangkui/pen/YzPGjgP
在 D3.js 中,如果想要某个节点固定在一个位置,可以指定以下两个额外的属性:
- fx - 节点的固定 x-位置
- fy - 节点的固定 y-位置
每次 tick 结束后,节点的 node.x 会被重新设置为 node.fx 并且 node.vx 会被设置为 0;同理 node.y 会被重新替换为 node.fy 并且 node.vy 被设置为 0;如果想要某个节点解除固定,则将 node.fx 和 node.fy 设置为 null 或者删除这两个属性。
下面是使用D3.js V5版本的实现代码:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Force layouttitle>
<style>
.link {
stroke: #000;
stroke-width: 1.5px;
}
.unfixedNode {
cursor: move;
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
}
.fixedNode {
cursor: move;
fill: red;
stroke: #000;
stroke-width: 1.5px;
}
style>
head>
<body>
<script src="https://d3js.org/d3.v5.min.js">script>
<script>
// fixed是固定坐标的节点,unfixed是没有固定坐标的节点。
var graph ={
"nodes": [
{"id": 0, "category": "unfixed"},
{"id": 1, "category": "ufixed"},
{"id": 2, "category": "unfixed"},
{"id": 3, "category": "fixed","x": 467, "y": 314},
{"id": 4, "category": "unfixed"},
{"id": 5, "category": "fixed","x": 425, "y": 207},
{"id": 6, "category": "unfixed"},
{"id": 7, "category": "unfixed"},
{"id": 8, "category": "unfixed"},
{"id": 9, "category": "fixed","x": 539, "y": 222},
{"id": 10, "category": "unfixed"},
{"id": 11, "category": "unfixed"},
{"id": 12, "category": "unfixed"}
],
"links": [
{"source": 0, "target": 3},
{"source": 1, "target": 3},
{"source": 2, "target": 3},
{"source": 3, "target": 4},
{"source": 4, "target": 5},
{"source": 5, "target": 6},
{"source": 5, "target": 7},
{"source": 5, "target": 8},
{"source": 9, "target": 4},
{"source": 9, "target": 11},
{"source": 9, "target": 10},
{"source": 9, "target": 12},
{"source": 8, "target": 3},
]
}
var nodesCopy = JSON.parse(JSON.stringify(graph.nodes));
var width = 960,
height = 500;
var force = d3.forceSimulation()
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", tick);
force
.nodes(graph.nodes)
.force("link", d3.forceLink(graph.links).id(function(d) {
return d.id;
}));
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("class", d => {
if (d.category == "fixed") {
return "fixedNode";
} else {
return "unfixedNode";
}
})
.attr("r", 12)
.call(d3.drag()
.on("start", dragstart)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(d => d.id);
function tick() {
node.attr("cx", function (d) {
if (d.category == 'fixed') {
d.fx = nodesCopy[d.id].x;
}
return d.x;
})
.attr("cy", function (d) {
if (d.category == 'fixed') {
d.fy = nodesCopy[d.id].y;
};
return d.y;
});
link.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
}
function dragstart(d) {
if (!d3.event.active) {
force.alphaTarget(.1).restart();
}
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) {
force.alphaTarget(0);
}
d.fx = null;
d.fy = null;
}
force.on("end", function () {
console.log(graph.nodes)
})
script>
body>
html>