Skip to content

Instantly share code, notes, and snippets.

@herodrigues
Forked from fgeorges/README.md
Created October 26, 2020 20:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save herodrigues/90b5a51d88e8749678a7d61c66b4157b to your computer and use it in GitHub Desktop.
Save herodrigues/90b5a51d88e8749678a7d61c66b4157b to your computer and use it in GitHub Desktop.
Graph with expandable nodes, with D3

Note: A solution has been found, see below.

I am trying to create an SVG graph of nodes with D3, with "expandable nodes." That is, when one clicks on a node, its siblings are added to the graph (or more precisely: its siblings in the graph data are added to the graph visualization.)

The visualization uses a d3 force simulation (using D3 v5). When a node (an SVG circle) is clicked, the simulation is stopped (in case it was still running, to make sure there is no new node with no coordinates added in the middle of a run,) the corresponding nodes and links are added to the corresponding arrays, then the graph setup is played again.

But something goes wrong as when a node is clicked, a lot of new nodes are added to the visualization, and are not correctly linked to each other. Also, if I wait long enough for the simulation to stop, the nodes are all added at the same spot, over the clicked node (they are closer and closer as the simulation goes.)

So I guess there is something wrong in the way I update the visualiaztion, but I cannot figure it out. I was not able to find any example doing exactly that. Any idea?

The initial graph when the page is loaded:

initial graph

The graph after clicking one of the orange circles (the blue one is the root, already expanded):

graph after clicking

Note: You get to click quite fast on one of the orange buttons, before the simulation comes to an end.

See https://bl.ocks.org/fgeorges/755474088065f1aa47583996d971b4fa, and https://stackoverflow.com/questions/55207182/graph-with-expandable-nodes-with-d3.

Solution

A solution is in expandable-graph-solution.js. The original code suffered the following problems:

  • it did not update the alpha, so the simulation did not run again after it already reached the end once;
  • it used variables to cache the sets of links and nodes;
  • it referenced circle.node, but the class node was never set on node circles.
const data = {
name: "top", children: [
{name: "I", children: [
{name: "I.a"}, {name: "I.b"}, {name: "I.c"}]},
{name: "II", children: [
{name: "II.a"}, {name: "II.b"}]},
{name: "III", children: [
{name: "III.a"}, {name: "III.b"}, {name: "III.c"}, {name: "III.d"}]}
]
};
const width = 1000;
const heigth = 800;
const colors = d3.scaleOrdinal(d3.schemeCategory10);
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", heigth);
const force = d3.forceSimulation()
.force("charge", d3.forceManyBody())
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(heigth / 2));
// at first, add the top node, and its children by using expand()
const nodes = [];
const links = [];
nodes.push(data);
expand(data);
setup();
function setup() {
force.nodes(nodes);
force.force("link",
d3.forceLink(links).strength(1).distance(100));
// SOLUTION: do not use variables for the links
svg.selectAll("line.link")
.data(links)
.enter().insert("line")
// SOLUTION: add the class attribute
.attr("class", 'link')
.style("stroke", "#999")
.style("stroke-width", "1px");
// SOLUTION: do not use variables for the nodes
svg.selectAll("circle.node")
.data(nodes)
.enter().append("circle")
// SOLUTION: add the class attribute
.attr("class", "node")
.attr("r", 4.5)
.style("fill", function(d) {
return colors(d.parent && d.parent.name);
})
.style("stroke", "#000")
.on("click", function(datum) {
force.stop();
expand(datum);
setup();
// SOLUTION: reset alpha, for the simulation to actually run again
if ( force.alpha() < 0.05 ) {
force.alpha(0.05);
}
force.restart();
});
force.on("tick", function(e) {
// SOLUTION: do not use variables for the links and nodes
svg.selectAll("line.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; });
svg.selectAll("circle.node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
}
function expand(node) {
console.log(`Expand ${node.name}, expanded: ${node.expanded}`);
if ( ! node.expanded ) {
(node.children || []).forEach(function(child) {
console.log(` - child ${child.name}`);
child.parent = node;
// pop up around the "parent" node
child.x = node.x;
child.y = node.y;
// add the node, and its link to the "parent"
nodes.push(child);
links.push({ source: node, target: child });
});
node.expanded = true;
}
}
const data = {
name: "top", children: [
{name: "I", children: [
{name: "I.a"}, {name: "I.b"}, {name: "I.c"}]},
{name: "II", children: [
{name: "II.a"}, {name: "II.b"}]},
{name: "III", children: [
{name: "III.a"}, {name: "III.b"}, {name: "III.c"}, {name: "III.d"}]}
]
};
const width = 800;
const heigth = 600;
const colors = d3.scaleOrdinal(d3.schemeCategory10);
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", heigth);
const force = d3.forceSimulation()
.force("charge", d3.forceManyBody())
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(heigth / 2));
// at first, add the top node, and its children by using expand()
const nodes = [];
const links = [];
nodes.push(data);
expand(data);
setup();
function setup() {
force.nodes(nodes);
force.force("link",
d3.forceLink(links).strength(1).distance(100));
const linkElems = svg.selectAll("line")
.data(links)
.enter().insert("line")
.style("stroke", "#999")
.style("stroke-width", "1px");
const nodeElems = svg.selectAll("circle.node")
.data(nodes)
.enter().append("circle")
.attr("r", 4.5)
.style("fill", function(d) {
return colors(d.parent && d.parent.name);
})
.style("stroke", "#000")
.on("click", function(datum) {
force.stop();
expand(datum);
setup();
force.restart();
});
force.on("tick", function(e) {
linkElems.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; });
nodeElems.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
};
function expand(node) {
if ( ! node.expanded ) {
(node.children || []).forEach(function(child) {
child.parent = node;
// pop up around the "parent" node
child.x = node.x;
child.y = node.y;
// add the node, and its link to the "parent"
nodes.push(child);
links.push({ source: node, target: child });
});
node.expanded = true;
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Expandable graph test</title>
<script type="text/javascript" src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
</body>
<script type="text/javascript" src="expandable-graph.js"></script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment