I am trying to create a d3 force directed network graph where based on a given network I can update the network by modifying the links and nodes and re-update them in the svg.
I tweaked the code so that a g element can be created which enclose each node circle so that I can add a text inside that same g element.
But now the labels are working perfectly but when I transition from graph 3 to graph 1 by clicking button 3 first then clicking button 1, the links that are redundant (a->d, a->f) are removed perfectly but the nodes that are redundant (e and f) stays in the svg.
I could not figure out whether it was a wrong selection or do I need some tweaking in the tick() function?
Here is the code:
var height = 200;
var width = 200;
const graph = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 }
]
}
const graph2 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 }
]
}
const graph3 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 },
{ "name": "e", "group": 4 },
{ "name": "f", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 },
{ "source": "f", "target": "a", "value": 1 }
]
}
var simulation = d3.forceSimulation()
.force("ct", d3.forceCenter(height / 2, width / 2))
.force("link", d3.forceLink().id(function(d) { return d.name; })
.distance(50).strength(2))
.force("charge", d3.forceManyBody().strength(-240))
// use forceX and forceY instead to change the relative positioning
// .force("centering", d3.forceCenter(width/2, height/2))
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(height / 2))
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");
function start(graph) {
var linkElements = svg.select(".links").selectAll(".link").data(graph.links);
linkElements.enter().append("line").attr("class", "link");
linkElements.exit().remove();
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
.enter().append("g")
.attr("class", "node");
var circles = nodeElements.append("circle")
.attr("r", 8);
var labels = nodeElements.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
nodeElements.exit().remove();
simulation.nodes(graph.nodes);
simulation.force("link").links(graph.links);
simulation.alphaTarget(0.1).restart();
}
function tick() {
var nodeElements = svg.select(".nodes").selectAll(".node");
var linkElements = svg.select(".links").selectAll(".link");
nodeElements.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
linkElements.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; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.1).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
start(graph);
document.getElementById('btn1').addEventListener('click', function() {
start(graph);
});
document.getElementById('btn2').addEventListener('click', function() {
start(graph2);
});
document.getElementById('btn3').addEventListener('click', function() {
start(graph3);
});
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
stroke-width: 1.5px;
}
text {
font-family: sans-serif;
font-size: 10px;
fill: #000000;
}
<body>
<div>
<button id='btn1'>1</button>
<button id='btn2'>2</button>
<button id='btn3'>3</button>
</div>
</body>
<script src="https://d3js.org/d3.v5.min.js"></script>
Here is the jsfiddle version of the code: https://jsfiddle.net/syedarehaq/myd0h5w1/
In the provided code, variable nodeElements
contains the enter selection, rather than the whole data binding selection.
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
.enter().append("g")
.attr("class", "node");
nodeElements
declaration should not contain the .enter
bit - it should be just like linkSelection
variable declaration:
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
Then, in order to append the new circles and texts only to the entering g
elements, adapt as follows:
var enterSelection = nodeElements.enter().append("g")
.attr("class", "node");
var circles = enterSelection.append("circle")
.attr("r", 8);
var labels = enterSelection.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
The exit
function call is now working as expected.
Demo in the snippet below.
var height = 200;
var width = 200;
const graph = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 }
]
}
const graph2 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 }
]
}
const graph3 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 },
{ "name": "e", "group": 4 },
{ "name": "f", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 },
{ "source": "f", "target": "a", "value": 1 }
]
}
var simulation = d3.forceSimulation()
.force("ct", d3.forceCenter(height / 2, width / 2))
.force("link", d3.forceLink().id(function(d) { return d.name; })
.distance(50).strength(2))
.force("charge", d3.forceManyBody().strength(-240))
// use forceX and forceY instead to change the relative positioning
// .force("centering", d3.forceCenter(width/2, height/2))
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(height / 2))
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");
function start(graph) {
var linkElements = svg.select(".links").selectAll(".link").data(graph.links);
linkElements.enter().append("line").attr("class", "link");
linkElements.exit().remove();
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
var enterSelection = nodeElements.enter().append("g")
.attr("class", "node");
var circles = enterSelection.append("circle")
.attr("r", 8);
var labels = enterSelection.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
nodeElements.exit().remove();
simulation.nodes(graph.nodes);
simulation.force("link").links(graph.links);
simulation.alphaTarget(0.1).restart();
}
function tick() {
var nodeElements = svg.select(".nodes").selectAll(".node");
var linkElements = svg.select(".links").selectAll(".link");
nodeElements.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
linkElements.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; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.1).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
start(graph);
document.getElementById('btn1').addEventListener('click', function() {
start(graph);
});
document.getElementById('btn2').addEventListener('click', function() {
start(graph2);
});
document.getElementById('btn3').addEventListener('click', function() {
start(graph3);
});
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
stroke-width: 1.5px;
}
text {
font-family: sans-serif;
font-size: 10px;
fill: #000000;
}
<body>
<div>
<button id='btn1'>1</button>
<button id='btn2'>2</button>
<button id='btn3'>3</button>
</div>
</body>
<script src="https://d3js.org/d3.v5.min.js"></script>