Collapsible Network(Graph) Diagram with D3
D3 stands for Data-Driven Documents. It is a library mainly used for data visualization on the web. This article will cover the use of D3JS (v7) to create the graph or network diagram using nodes and links.
Most of us have already seen the collapsible network diagram but in the form of the tree structure. The example of the tree structure is below
treeData = {
name: 'parent',
value: 5,
children: [
{name: 'child_1', value: 10},
{name: 'child_2', value: 15},
{name: 'child_3', value: 20},
]
}
In the above-mentioned case, the collapse function will be as simple as mentioned below:
// Collapse the node and all it's children
function collapse(d) {
if(d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
The above function stores the children temporarily into _children variable and then switch between children and _children.
However, the network diagram of forced rendering looks like the following:
graph={
"nodes": [
{"name": "A"},
{"name": "B"},
{"name": "C"},
{"name": "D"},
{"name": "E"},
],
"links": [
{"source": "A", "target": "B"},
{"source": "E", "target": "D"},
{"source": "C", "target": "E"},
{"source": "B", "target": "E"},
{"source": "A", "target": "D"},
]
}
The reason it is more challenging is that we do not have a single children variable to find all the children of a node. Furthermore, we are not aware of the depth of the path of the links.
To overcome the problem and to find a time-efficient solution, we are going to make some changes to our graph data.
graph={
"nodes": [
{"name": "A", show:true, showChildren:true, hasChildren:true},
{"name": "B", show:true, showChildren:true, hasChildren:true},
{"name": "C", show:true, showChildren:true, hasChildren:true},
{"name": "D", show:true, showChildren:false, hasChildren:false},
{"name": "E", show:true, showChildren:true, hasChildren:true},
],
"links": [
{"source": "A", "target": "B", show:true},
{"source": "E", "target": "D", show:true},
{"source": "C", "target": "E", show:true},
{"source": "B", "target": "E", show:true},
{"source": "A", "target": "D", show:true},
]
}
- show: The boolean value will help us determine whether to show or hide the node or link.
- showChildren: This will elaborate our code whether to show the children of the node or not.
- hasChildren: This will help us to make our code efficient by determining the nodes that have children.
Let’s take the example and create a network diagram:
drawNetwork = () => {/* ... PLEASE FILL IN THE REMAINING CODE BY YOURSELF ... */// making nodes and link part, functions below
const nodes = drawNodes(graph);
const links = drawLinks(graph);// the simulation part
const simulation = d3.forceSimulation(data.nodes.filter(n => n.show))
.force("link", d3.forceLink(data.links.filter(n => n.show)).id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(-7000))
.force("center", d3.forceCenter((width - margin.right - margin.left)/2.5, (height - margin.top - margin.bottom)/2.5))
.on("tick", ticked);// please set width and height by yourself as well
Note: The above-mentioned code should not be exactly like yours as it is an example and created for a different use case that is beyond this article scope.
Now, creating the nodes on which the on click collapse will work:
node = svg.append("g")
.selectAll("text")
.data(data.nodes.filter(n => n.show))
.enter()
.append("text")
.attr("text-anchor", 'middle')
.attr("class", "fas cmdb_element")
.attr('font-size', '20px')
.attr('cursor', 'move')
.attr('fill', function(d) { return determineColor(d, ICON_COLOR, CIRCLE_COLOR) })
.text(function(d) { return String.fromCodePoint(parseInt(d.icon, 16))})
.on("mouseover", function() {
d3.selectAll(`#relation_${this.__data__.id}`).style('visibility', 'visible');
d3.selectAll(`#line_${this.__data__.id}`).style('stroke',
TEXT_COLOR)
})
.on("mouseout", function(){
nodeRel.style("visibility", "hidden");
d3.selectAll(`#line_${this.__data__.id}`).style('stroke', ICON_COLOR);
})
.on('click', function(d){ toggleChildren(this.__data__)});
Note: This node code is handling the icons inside the circular node. This is not part of the example though help someone with a similar use case. The bold code is our main concern in this example.
The toggleChildren will perform the collapse functionality as below:
// Toggle children on click.function toggleChildren(d) {
if (d.showChildren){
toggleVisibilityOfNodesAndLinks(d.id, false);
d.showChildren = false;
}
else{
if(d.hasChildren && !d.cache){
fetch_relationships(d.id); // Api Call to fetch further nodes
d.cache = true; // Using cache to extra avoid api call
}
toggleVisibilityOfNodesAndLinks(d.id, true);
d.showChildren = true;
}
updateNetwork();
}
And the toggleVisibilityOfNodesAndLinks:
function toggleVisibilityOfNodesAndLinks(d, visibility){
const parents = [d];
let loopCount = 0;
let visited = []; // to track visited nodes
while (parents.length > 0) {
const parent = parents.shift();
visited.push(parent);
data.links.forEach(data_link => {
if ((data_link.source.id || data_link.source) === parent) {
data_link.show = visibility;
if(data_link.target && data_link.target.hasChildren && !data_link.target.showChildren) data_link.target.show = visibility;
if (!visited.includes(data_link.target.id || data_link.target) && (data_link.target.show !== visibility))
{
parents.push(data_link.target.id || data_link.target);
}
}
});
data.nodes.forEach(node => {
if (node.id === parent && loopCount !== 0) {
node.show = visibility;
data.links.forEach(data_link => ((data_link.target.id || data_link.target) == node.id && data_link.source.showChildren == true) ? data_link.show = visibility : data_link.show);
}
});
loopCount += 1;
}
};
Finally, the updateNetwork:
// Remove and Reset everythingfunction updateNetwork(){
svg.selectAll('circle').remove();
svg.selectAll('line').remove();
svg.selectAll('text').remove();
svg.selectAll('rect').remove();
svg.selectAll('marker').remove(); this.drawNetwork(); // Create Network Again
}
Code Explanation:
- toggleChildren: This function is a wrapper method to call the toggleVisibilityOfNodesAndLinks in case showChildren of a node is true. After then, we set the showChildren to false. In case if showChildren is set to false then we check if it hasChildren. If yes we call the API to get the further children.
- toggleVisibilityOfNodesAndLinks: This method is the brain of the whole code. This method is based on the algorithmic logic of BFS(Breath First Search). It hides/shows the link originated from the parent itself. It adds the nodes that the link points to as a parent. It hides/shows the nodes that are the parent (except for the first one). It repeats until we have no parent. The visited collection is used to track the visited nodes so that our code doesn’t go into the infinite loop. The (data_link.target.id || data_link.target) is used because when new node is added via API we do not have target.id. The visibility of node_link is changed only when showChildren of the node is set to true otherwise it is unnecessary to change the visibility of the link.
- updateNetwork: This method is re-rendering the whole network to display the updated graph data.
Demo:
Adding the screenshot of my project below:
This concludes this article. I would suggest exploring even more about d3.js and looking for further examples.
Feel free to comment about any ambiguities and confusion, I’ll be happy to help. Do comment if this article is of any sort of help to you or your friends.