问题描述
我使用带有强制布局的 d3.js v6 来表示网络图。 我正在添加和删除节点,但是当我重新启动模拟时,所有节点都跳到左上角的位置,然后又回到原来的位置。
我有以下代码片段,它准确地显示了我的意思,我在网上看到了其他可以正常工作的示例,但无法找到我做错了什么,非常感谢任何帮助。
var dataset = {
nodes: [
{
id: 1
},{
id: 2
}
],links: [{
id: 1,source: 1,target: 2
}]
};
let switchBool = false;
let svg = d3.select('svg')
.attr('width','100%')
.attr('height','100%');
const width = svg.node()
.getBoundingClientRect().width;
const height = svg.node()
.getBoundingClientRect().height;
console.log(`${width},${height}`);
svg = svg.append('g');
svg.append('g')
.attr('class','links');
svg.append('g')
.attr('class','nodes');
const simulation = d3.forceSimulation();
initSimulation();
let link = svg.select('.links')
.selectAll('line');
loadLinks();
let node = svg.select('.nodes')
.selectAll('.node');
loadNodes();
restartSimulation();
function initSimulation() {
simulation
.force('link',d3.forceLink())
.force('charge',d3.forceManyBody())
.force('collide',d3.forceCollide())
.force('center',d3.forceCenter())
.force('forceX',d3.forceX())
.force('forceY',d3.forceY());
simulation.force('center')
.x(width * 0.5)
.y(height * 0.5);
simulation.force('link')
.id((d) => d.id)
.distance(100)
.iterations(1);
simulation.force('collide')
.radius(10);
simulation.force('charge')
.strength(-100);
}
function loadLinks() {
link = svg.select('.links')
.selectAll('line')
.data(dataset.links,(d) => d.id)
.join(
enter => enter.append('line').attr('stroke','#000000'),);
}
function loadNodes() {
node = svg.select('.nodes')
.selectAll('.node')
.data(dataset.nodes,(d) => d.id)
.join(
enter => {
const nodes = enter.append('g')
.attr('class','node')
nodes.append('circle').attr('r',10);
return nodes;
},);
}
function restartSimulation() {
simulation.nodes(dataset.nodes);
simulation.force('link').links(dataset.links);
simulation.alpha(1).restart();
simulation.on('tick',ticked);
}
function ticked() {
link
.attr('x1',(d) => d.source.x)
.attr('y1',(d) => d.source.y)
.attr('x2',(d) => d.target.x)
.attr('y2',(d) => d.target.y);
node.attr('transform',(d) => `translate(${d.x},${d.y})`);
}
function updateData() {
switchBool = !switchBool;
if (switchBool) {
dataset.nodes.push({id: 3});
dataset.links.push({id: 2,target: 3});
} else {
dataset.nodes.pop();
dataset.links.pop();
}
loadLinks();
loadNodes();
restartSimulation();
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<div>
<button onclick="updateData()">Add/Remove</button>
<svg></svg>
</div>
解决方法
这是因为您使用了 d3.forceCenter()
,它不会将节点强制为中心点:
中心力均匀地平移节点,使得均值 所有节点的位置(如果所有节点都相等,则为质心 weight) 位于给定位置⟨x,y⟩。 (docs)
因此,如果您的两个节点位于 d3.forceCenter 中心点的正下方/正上方,则质量是平衡的。引入一个新节点,整个力必须被转换,以便质心是中心。这个翻译就是你看到的跳跃。
移除 forceCenter 并使用 d3.forceX 和 d3.forceY 指定中心值,它们会将节点推向指定的 x 和 y 值:
var dataset = {
nodes: [
{
id: 1
},{
id: 2
}
],links: [{
id: 1,source: 1,target: 2
}]
};
let switchBool = false;
let svg = d3.select('svg')
.attr('width','100%')
.attr('height','100%');
const width = svg.node()
.getBoundingClientRect().width;
const height = svg.node()
.getBoundingClientRect().height;
console.log(`${width},${height}`);
svg = svg.append('g');
svg.append('g')
.attr('class','links');
svg.append('g')
.attr('class','nodes');
const simulation = d3.forceSimulation();
initSimulation();
let link = svg.select('.links')
.selectAll('line');
loadLinks();
let node = svg.select('.nodes')
.selectAll('.node');
loadNodes();
restartSimulation();
function initSimulation() {
simulation
.force('link',d3.forceLink())
.force('charge',d3.forceManyBody())
.force('collide',d3.forceCollide())
.force('forceX',d3.forceX().x(width/2))
.force('forceY',d3.forceY().y(height/2));
simulation.force('link')
.id((d) => d.id)
.distance(100)
.iterations(1);
simulation.force('collide')
.radius(10);
simulation.force('charge')
.strength(-100);
}
function loadLinks() {
link = svg.select('.links')
.selectAll('line')
.data(dataset.links,(d) => d.id)
.join(
enter => enter.append('line').attr('stroke','#000000'),);
}
function loadNodes() {
node = svg.select('.nodes')
.selectAll('.node')
.data(dataset.nodes,(d) => d.id)
.join(
enter => {
const nodes = enter.append('g')
.attr('class','node')
nodes.append('circle').attr('r',10);
return nodes;
},);
}
function restartSimulation() {
simulation.nodes(dataset.nodes);
simulation.force('link').links(dataset.links);
simulation.alpha(1).restart();
simulation.on('tick',ticked);
}
function ticked() {
link
.attr('x1',(d) => d.source.x)
.attr('y1',(d) => d.source.y)
.attr('x2',(d) => d.target.x)
.attr('y2',(d) => d.target.y);
node.attr('transform',(d) => `translate(${d.x},${d.y})`);
}
function updateData() {
switchBool = !switchBool;
if (switchBool) {
dataset.nodes.push({id: 3});
dataset.links.push({id: 2,target: 3});
} else {
dataset.nodes.pop();
dataset.links.pop();
}
loadLinks();
loadNodes();
restartSimulation();
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<div>
<button onclick="updateData()">Add/Remove</button>
<svg></svg>
</div>
实际上我刚刚找到了问题的解决方案。
我的 forceX
和 forceY
都带有默认参数,这意味着有一个力将节点推向 (0,0),更改了这段代码我能够修复它:
.force('x',d3.forceX().x(width * 0.5))
.force('y',d3.forceY().y(height * 0.5));