我正在尝试动态地将节点添加到 D3 力定向图。我相信我已经很接近了,但不理解一些与 D3 中的选择如何工作相关的关键概念。
如果我直接调用我的
addData
函数并立即添加所有节点,一切都会按预期进行。
但是,如果我使用
addDataInChunks
,则会添加前两个节点和第一个链接,但之后图形中不会出现更多节点。此外,一旦将第二个块添加到图表中,拖动节点的能力就会丢失。
我做错了什么?我的代码需要如何更改才能正常工作?
const nodeData = [
{
id: 'node 1',
added: false,
},
{
id: 'node 2',
added: false,
},
{
id: 'node 3',
added: false,
},
{
id: 'node 4',
added: false,
},
]
const linkData = [
{
linkID: 'link 1',
added: false,
source: 'node 1',
target: 'node 2',
},
{
linkID: 'link 2',
added: false,
source: 'node 1',
target: 'node 3',
},
{
linkID: 'link 3',
added: false,
source: 'node 3',
target: 'node 4',
},
]
//
//
//
let svg = null
let node = null
let link = null
let simulation = null
const width = 750
const height = 400
const nodeCount = 10
const svgNS = d3.namespace('svg:text').space
function setupGraph() {
svg = d3.select('#chart').call(d3.zoom().on('zoom', zoomed)).append('g')
node = svg.append('g').attr('stroke', '#fff').attr('stroke-width', 1.5)
link = svg.append('g').attr('stroke', '#999').attr('stroke-opacity', 0.6)
}
const simulationNodes = []
const simulationLinks = []
function addData(addNodes, addLinks) {
const links = addLinks.map((d) => ({ ...d }))
const nodes = addNodes.map((d, index) => ({ ...d }))
console.log(`🚀 ~ nodes:`, nodes)
console.log(`🚀 ~ links:`, links)
simulationNodes.push(...nodes)
simulationLinks.push(...links)
simulation = d3
.forceSimulation(simulationNodes)
.force(
'link',
d3
.forceLink(simulationLinks)
.id((d) => d.id)
.distance((d) => 50)
)
.force('charge', d3.forceManyBody().strength(-400))
.force('x', d3.forceX())
.force('y', d3.forceY())
.on('tick', ticked)
node = node
.selectAll()
.data(nodes)
.join((enter) => {
return enter.append((d) => {
const circleElement = document.createElementNS(svgNS, 'circle')
circleElement.setAttribute('r', 16)
circleElement.setAttribute('fill', '#318631')
circleElement.setAttribute('stroke', '#7CC07C')
circleElement.setAttribute('stroke-width', '3')
return circleElement
})
})
node.append('title').text((d) => `hello ${d.id}`)
node.call(d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended))
link = link.selectAll().data(links).join('line').attr('stroke-width', 1)
}
setupGraph()
// addData(nodeData, linkData)
async function addDataInChunks(allNodes, allLinks) {
const timeout = 7000
let nodeChunk = allNodes.slice(0, 2)
let linkChunk = allLinks.slice(0, 1)
addData(nodeChunk, linkChunk)
await new Promise((r) => setTimeout(r, timeout))
//
nodeChunk = allNodes.slice(2, 3)
linkChunk = allLinks.slice(1, 2)
addData(nodeChunk, linkChunk)
await new Promise((r) => setTimeout(r, timeout))
//
nodeChunk = allNodes.slice(3, 4)
linkChunk = allLinks.slice(2, 3)
addData(nodeChunk, linkChunk)
await new Promise((r) => setTimeout(r, timeout))
console.log('addDataInChunks finished')
}
addDataInChunks(nodeData, linkData)
//
// Misc Functions
//
function zoomed(transform) {
const t = transform.transform
const container = this.getElementsByTagNameNS(svgNS, 'g')[0]
const transformString = 'translate(' + t.x + ',' + t.y + ') scale(' + t.k + ')'
container.setAttribute('transform', transformString)
}
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.each(function (d) {
this.setAttribute('cx', d.x)
this.setAttribute('cy', d.y)
})
}
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart()
event.subject.fx = event.subject.x
event.subject.fy = event.subject.y
}
function dragged(event) {
event.subject.fx = event.x
event.subject.fy = event.y
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0)
event.subject.fx = null
event.subject.fy = null
}
.graph {
width: 750px;
height: 400px;
}
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
<svg ref="chart" id="chart" class="graph" style="background-color: #141621"></svg>
以下是我的更改摘要:
首先,您需要
selectAll
位于元素的“父级”上。您正在选择没有意义的选择。这在第一次循环中有效,因为所有数据都在“输入”,但随后 d3 将找不到已经存在的绑定数据。您还应该在元素或类上明确 selectAll
。
其次,当您附加并跟踪
simulationNodes
和 simulationLinks
时,您需要将相同的附加数据传递给选择。否则,d3无法计算输入和更新数据。
第三,您需要管理模拟的生命周期。这意味着您需要停止它并在修改其数据后重新启动它。
第四,我清理了您的数据连接。我发现与
d3
混合使用原生 svg 方法很奇怪。我还修改了它们,使其具有显式的输入、更新和退出方法。您没有使用出口,但为了完整性我将其包括在内。
最后,我做了一些常规清理,例如更清晰的变量名称,而不是在每次数据修改时重新创建模拟并将图形移动到 svg 的中心。
<!DOCTYPE html>
<html>
<head>
<style>
.graph {
width: 750px;
height: 400px;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
<svg
ref="chart"
id="chart"
class="graph"
style="background-color: #141621"
></svg>
<script>
const nodeData = [
{
id: 'node 1',
added: false,
},
{
id: 'node 2',
added: false,
},
{
id: 'node 3',
added: false,
},
{
id: 'node 4',
added: false,
},
];
const linkData = [
{
linkID: 'link 1',
added: false,
source: 'node 1',
target: 'node 2',
},
{
linkID: 'link 2',
added: false,
source: 'node 1',
target: 'node 3',
},
{
linkID: 'link 3',
added: false,
source: 'node 3',
target: 'node 4',
},
];
//
//
//
let svg = null;
const width = 750;
const height = 400;
const nodeCount = 10;
function setupGraph() {
svg = d3
.select('#chart')
.call(d3.zoom().on('zoom', zoomed))
.append('g');
nodeGroup = svg
.append('g')
.attr('stroke', '#fff')
.attr('stroke-width', 1.5);
linkGroup = svg
.append('g')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6);
}
// nodes and links are the d3 selections
let nodes = null;
let links = null;
// currentNodes and currentLinks is the current data
const currentNodes = [];
const currentLinks = [];
// the link simulation
const linkSim = d3
.forceLink()
.id((d) => d.id)
.distance((d) => 50);
// overall simulation
const simulation = d3
.forceSimulation()
.force('link', linkSim)
.force('charge', d3.forceManyBody().strength(-400))
.force('x', d3.forceX())
.force('y', d3.forceY())
.force('center', d3.forceCenter(width / 2, height / 2))
.on('tick', ticked);
function addData(addNodes, addLinks) {
addNodes = addNodes.map((d, index) => ({ ...d }));
addLinks = addLinks.map((d) => ({ ...d }));
currentNodes.push(...addNodes);
currentLinks.push(...addLinks);
nodes = nodeGroup
.selectAll('circle')
.data(currentNodes, (d) => d)
.join(
(enter) =>
enter
.append('circle')
.attr('r', 16)
.attr('fill', '#318631')
.attr('stroke', '#7CC07C')
.attr('stroke-width', '3'),
(update) => update,
(exit) => exit.remove()
);
nodes.append('title').text((d) => `hello ${d.id}`);
nodes.call(
d3
.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended)
);
links = linkGroup
.selectAll('line')
.data(currentLinks)
.join(
(enter) => enter.append('line').attr('stroke-width', 1),
(update) => update,
(exit) => exit.remove()
);
// stop and start the simulation
simulation.stop();
simulation.nodes(currentNodes);
linkSim.links(currentLinks);
simulation.alpha(0.3).restart();
}
setupGraph();
async function addDataInChunks(allNodes, allLinks) {
const timeout = 7000;
let nodeChunk = allNodes.slice(0, 2);
let linkChunk = allLinks.slice(0, 1);
addData(nodeChunk, linkChunk);
await new Promise((r) => setTimeout(r, timeout));
//
nodeChunk = allNodes.slice(2, 3);
linkChunk = allLinks.slice(1, 2);
addData(nodeChunk, linkChunk);
await new Promise((r) => setTimeout(r, timeout));
//
nodeChunk = allNodes.slice(3, 4);
linkChunk = allLinks.slice(2, 3);
addData(nodeChunk, linkChunk);
await new Promise((r) => setTimeout(r, timeout));
console.log('addDataInChunks finished');
}
addDataInChunks(nodeData, linkData);
//
// Misc Functions
//
function zoomed(transform) {
const t = transform.transform;
const container = this.getElementsByTagNameNS(svgNS, 'g')[0];
const transformString =
'translate(' + t.x + ',' + t.y + ') scale(' + t.k + ')';
container.setAttribute('transform', transformString);
}
function ticked() {
links
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y);
nodes.each(function (d) {
this.setAttribute('cx', d.x);
this.setAttribute('cy', d.y);
});
}
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
</script>
</body>
</html>