I want to update d3-force
elements when click, but I find something wrong.
What I want is:
So when clicked the elements should be:
<svg>
<line ...></line>
<line ...></line>
<line ...></line>
<line ...></line>
<g><circle ...></circle></g>
<g><circle ...></circle></g>
<g><circle ...></circle></g>
<g><circle ...></circle></g>
<g><circle ...></circle></g>
</svg>
But I get:
<svg>
<g><circle ...></circle></g>
<line ...></line>
<line ...></line>
<line ...></line>
<line ...></line>
<g><circle ...></circle></g>
<g><circle ...></circle></g>
<g><circle ...></circle></g>
<g><circle ...></circle></g>
</svg>
const width = 800;
const height = 400;
const svg = d3.select("svg").attr("viewBox", [0, 0, width, height]);
let node = svg.selectAll(".node");
let link = svg.selectAll(".link");
const simulation = d3
.forceSimulation()
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", (d) => {
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})`);
});
const root = {
x: width / 2,
y: height / 2,
level: 0,
id: "1"
};
function genChildren(p) {
const dist = 100 / (p.level + 1);
const nodes = [{
x: p.x - dist,
y: p.y,
level: p.level + 1,
id: p.id + "1"
},
{
x: p.x + dist,
y: p.y,
level: p.level + 1,
id: p.id + "2"
},
{
x: p.x,
y: p.y - dist,
level: p.level + 1,
id: p.id + "3"
},
{
x: p.x,
y: p.y + dist,
level: p.level + 1,
id: p.id + "4"
},
];
const links = nodes.map(v => ({
source: p.id,
target: v.id
}));
return {
nodes,
links
};
}
function update(nodes, links) {
link = link.data(links)
.join(enter => {
return enter.append("line")
.attr("stroke", "#000")
.attr("stroke-width", "1.5px")
.call(enter => enter.transition().attr("stroke-opacity", 1));
}, update => update, exit => exit.remove());
node = node.data(nodes, d => d.id)
.join(
enter => {
const g = enter.append("g");
g.append("circle")
.attr("r", 12)
.attr("cursor", "move")
.attr("fill", "#ccc")
.attr("stroke", "#000")
.attr("stroke-width", "1.5px");
return g;
},
update => update,
exit => exit.remove(),
)
.attr("transform", function(d) {
return "translate(" + d.x + ", " + d.y + ")";
})
.on("click", (ev, d) => {
if (d.level !== 0) {
return;
}
const childrenInfo = genChildren(root)
const nodes = d.active ? [root] : [root].concat(childrenInfo.nodes);
const links = d.active ? [] : childrenInfo.links;
d.active = !d.active;
update(nodes, links);
});
simulation.nodes(nodes).force("link", d3.forceLink(links).id(node => node.id).strength(-0.01));
if (simulation.alpha() <= 1) {
simulation.alpha(1);
simulation.restart();
}
}
update([root], [])
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>
You could have separate g
elements for the nodes and the links, add the links to the first one, and the nodes to the second. Or, you could just use node.raise()
to put those elements after all the links in the DOM:
const width = 800;
const height = 400;
const svg = d3.select("svg").attr("viewBox", [0, 0, width, height]);
let node = svg.selectAll(".node");
let link = svg.selectAll(".link");
const simulation = d3
.forceSimulation()
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", (d) => {
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})`);
});
const root = {
x: width / 2,
y: height / 2,
level: 0,
id: "1"
};
function genChildren(p) {
const dist = 100 / (p.level + 1);
const nodes = [{
x: p.x - dist,
y: p.y,
level: p.level + 1,
id: p.id + "1"
},
{
x: p.x + dist,
y: p.y,
level: p.level + 1,
id: p.id + "2"
},
{
x: p.x,
y: p.y - dist,
level: p.level + 1,
id: p.id + "3"
},
{
x: p.x,
y: p.y + dist,
level: p.level + 1,
id: p.id + "4"
},
];
const links = nodes.map(v => ({
source: p.id,
target: v.id
}));
return {
nodes,
links
};
}
function update(nodes, links) {
link = link.data(links)
.join(enter => {
return enter.append("line")
.attr("stroke", "#000")
.attr("stroke-width", "1.5px")
.call(enter => enter.transition().attr("stroke-opacity", 1));
}, update => update, exit => exit.remove());
node = node.data(nodes, d => d.id)
.join(
enter => {
const g = enter.append("g");
g.append("circle")
.attr("r", 12)
.attr("cursor", "move")
.attr("fill", "#ccc")
.attr("stroke", "#000")
.attr("stroke-width", "1.5px");
return g;
},
update => update,
exit => exit.remove(),
)
.raise()
.attr("transform", function(d) {
return "translate(" + d.x + ", " + d.y + ")";
})
.on("click", (ev, d) => {
if (d.level !== 0) {
return;
}
const childrenInfo = genChildren(root)
const nodes = d.active ? [root] : [root].concat(childrenInfo.nodes);
const links = d.active ? [] : childrenInfo.links;
d.active = !d.active;
update(nodes, links);
});
simulation.nodes(nodes).force("link", d3.forceLink(links).id(node => node.id).strength(-0.01));
if (simulation.alpha() <= 1) {
simulation.alpha(1);
simulation.restart();
}
}
update([root], [])
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>