Syed Arefinul Haque
Syed Arefinul Haque

Reputation: 1325

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/

Upvotes: 2

Views: 1570

Answers (1)

Mehdi
Mehdi

Reputation: 7403

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>

Upvotes: 2

Related Questions