d3.jsd3-force-directed

Updating nodes and links with labels in d3 force directed network graph is not removing the nodes properly


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/


Solution

  • 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>