I would like to render a force-directed graph in d3js v4 and provide a function for dynamically adding new nodes and links to the simulation. My first attempt (see below) still has some major issues:
I think I know all the examples (e.g., [1,2]) demonstrating similar functionality for v3, but I would like to do it with v4.
Thanks for your help!
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 Test</title>
<script type="text/javascript" src="js/d3.js"></script>
<script type="text/javascript" src="js/graph.js"></script>
<style type="text/css">
.links line {
stroke: #aaa;
}
.nodes circle {
pointer-events: all;
stroke: none;
stroke-width: 40px;
}
</style>
</head>
<body>
<div class="chart-container" style="max-width: 1000px;"></div>
<button class="expand">Expand graph</button>
<script type="text/javascript">
//sample graph dataset
var graph = {
"nodes": [
{"id": "A"},
{"id": "B"},
{"id": "C"},
{"id": "D"},
{"id": "E"},
{"id": "F"}
],
"links": [
{"source": "B", "target": "A"},
{"source": "C", "target": "A"},
{"source": "D", "target": "A"},
{"source": "D", "target": "C"},
{"source": "E", "target": "A"},
{"source": "F", "target": "A"}
]
}
//graph container
var targetElement = document.querySelector('.chart-container');
var graph = new Graph(targetElement, graph);
d3.selectAll('button.expand').on('click', function (){
var nodes = [
{"id": "G", "group": 1}
];
var links = [
{"source": "F", "target": "G", "value": 1}
];
graph.expandGraph(links, nodes);
});
</script>
</body>
</html>
graph.js
var Graph = function(targetElement, graph) {
var self = this,
width = targetElement.offsetWidth,
height = width / 2,
svg = d3.select(targetElement).append('svg')
.attr("width", width)
.attr("height", height),
simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2)),
link = svg.append("g")
.attr("class", "links")
.selectAll("line"),
node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle"),
update = function() {
// Redefine and restart simulation
simulation.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
// Update links
link = link.data(graph.links);
// Enter links
link = link.enter().append("line");
// Exit any old links
link.exit().remove();
// Update the nodes
node = node.data(graph.nodes);
// Enter any new nodes
node = node.enter().append("circle")
.attr("r", 5)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Exit any old nodes
node.exit().remove();
function ticked() {
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; });
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
},
dragstarted = function(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
},
dragged = function(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
},
dragended = function(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
},
expandGraph = function(links, nodes) {
for (var i=0; i < nodes.length; i++) {
console.log('adding node', nodes[i]);
graph.nodes.push(nodes[i]);
}
for (var i=0; i < links.length; i++) {
console.log('adding link', links[i]);
graph.links.push(links[i]);
}
update();
};
// Public functions
this.expandGraph = expandGraph;
update();
};
You forgot to merge() your nodes. I have updated your code quickly:
graph.js
var Graph = function(targetElement, graph) {
var self = this,
width = targetElement.offsetWidth,
height = width / 2,
svg = d3.select(targetElement).append('svg')
.attr("width", width)
.attr("height", height),
simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2)),
linkGroup = svg.append("g")
.attr("class", "links"),
nodeGroup = svg.append("g")
.attr("class", "nodes"),
update = function() {
// Redefine and restart simulation
simulation.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
// Update links
link = linkGroup
.selectAll("line")
.data(graph.links);
// Enter links
linkEnter = link
.enter().append("line");
link = linkEnter
.merge(link);
// Exit any old links
link.exit().remove();
// Update the nodes
node = nodeGroup.selectAll("circle").data(graph.nodes);
// Enter any new nodes
nodeEnter = node.enter().append("circle")
.attr("r", 5)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node = nodeEnter.merge(node);
// Exit any old nodes
node.exit().remove();
function ticked() {
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; });
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
},
dragstarted = function(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
},
dragged = function(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
},
dragended = function(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
},
expandGraph = function(links, nodes) {
for (var i=0; i < nodes.length; i++) {
console.log('adding node', nodes[i]);
graph.nodes.push(nodes[i]);
}
for (var i=0; i < links.length; i++) {
console.log('adding link', links[i]);
graph.links.push(links[i]);
}
update();
};
// Public functions
this.expandGraph = expandGraph;
update();
};
Actually I don't understand this new function 100%, but you always need to merge your new links and nodes (i.e. linkEnter and nodeEnter) with your existing graph. If you don't do this, your old Graph kind of dies...
Mike Bostock made an example how to use merge here:
https://bl.ocks.org/mbostock/3808218
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With