Splicing array removes wrong node in force-directed graph

I got a D3 graph in place. Where a click on a node should delete this node and the related link. A hover on each node will display the name of it. Further a console.log confirms the name of the node, which was deleted. The problem I got is, as soon as I try to delete a node which is not the last one from the array, the console confirms the deletion but the node just replaces another one. Where is my thinking mistake?

enter image description here

enter image description here

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Playground</title>
    <!-- favcon -->
    <link rel="icon" href="https://networkrepository.com/favicon.png">
    <!-- call external d3.js framework -->
    <script src="https://d3js.org/d3.v4.js"></script>
    <!-- import multiselection framework -->
    <script src="https://d3js.org/d3-selection-multi.v1.js"></script>
    <!-- import "font awesome" stylesheet https://fontawesome.com/ -->
    <script src="https://kit.fontawesome.com/39094309d6.js" crossorigin="anonymous"></script>
</head>

<style>
    body {
        overflow: hidden;
        margin: 0px;
    }

    .canvas {
        background-color: rgb(220, 220, 220);
    }

    .link {
        stroke: rgb(0, 0, 0);
        stroke-width: 1px;
    }

    circle {
        background-color: whitesmoke;
    }
</style>

<body>
    <!-- create svg root element as a canvas -->
    <svg id="svg"></svg>

    <!-- call script where the main application is written -->
    <script>

        var graph = {
            "nodes": [
                {
                    "id": 0,
                    "type": "company",
                    "name": "Company",
                    "context": [
                        {
                            "name": "Company"
                        }
                    ],
                    "icon": "\uf1ad"
                },
                {
                    "id": 1,
                    "type": "software",
                    "name": "Software_1",
                    "context": [
                        {
                            "name": "Software_1"
                        }
                    ],
                    "icon": "\uf7b1",
                    "parent": 1
                },
                {
                    "id": 2,
                    "type": "software",
                    "name": "Software_2",
                    "context": [
                        {
                            "name": "Software_2"
                        }
                    ],
                    "icon": "\uf78d",
                    "parent": 1
                },
                {
                    "id": 3,
                    "type": "software",
                    "name": "Software_3",
                    "context": [
                        {
                            "name": "Software_3"
                        }
                    ],
                    "icon": "\ue084",
                    "parent": 1
                },
                {
                    "id": 4,
                    "type": "software",
                    "name": "Software_4",
                    "context": [
                        {
                            "name": "Software_4"
                        }
                    ],
                    "icon": "\ue084",
                    "parent": 1
                },
                {
                    "id": 5,
                    "type": "software",
                    "name": "Software_5",
                    "context": [
                        {
                            "name": "Software_5"
                        }
                    ],
                    "icon": "\ue084",
                    "parent": 4
                },
                {
                    "id": 6,
                    "type": "software",
                    "name": "Software_6",
                    "context": [
                        {
                            "name": "Software_6"
                        }
                    ],
                    "icon": "\ue084",
                    "parent": 4
                },
                {
                    "id": 7,
                    "type": "software",
                    "name": "Software_7",
                    "context": [
                        {
                            "name": "Software_7"
                        }
                    ],
                    "icon": "\ue084",
                    "parent": 5
                },
                {
                    "id": 8,
                    "type": "software",
                    "name": "Software_8",
                    "context": [
                        {
                            "name": "Software_8"
                        }
                    ],
                    "icon": "\ue084",
                    "parent": 5
                }
            ],
            "links": [
                {
                    "source": 1,
                    "target": 0,
                    "type": "uses"
                },
                {
                    "source": 2,
                    "target": 0,
                    "type": "uses"
                },
                {
                    "source": 3,
                    "target": 0,
                    "type": "uses"
                },
                {
                    "source": 4,
                    "target": 0,
                    "type": "uses"
                },
                {
                    "source": 5,
                    "target": 3,
                    "type": "uses"
                },
                {
                    "source": 6,
                    "target": 3,
                    "type": "uses"
                },
                {
                    "source": 7,
                    "target": 4,
                    "type": "uses"
                },
                {
                    "source": 8,
                    "target": 4,
                    "type": "uses"
                }
            ]
        }

        // declare initial variables
        var svg = d3.select("svg")
        width = window.innerWidth
        height = window.innerHeight
        node = null
        link = null


        // define cavnas area to draw everything
        svg = d3.select("svg")
            .attr("class", "canvas")
            .attr("width", width)
            .attr("height", height)
            .call(d3.zoom().on("zoom", function () {
                svg.attr("transform", d3.event.transform)
            }))
            .append("g")

        // remove zoom on dblclick listener
        d3.select("svg").on("dblclick.zoom", null)

        // append markers to svg
        svg.append('defs').append('marker')
            .attrs({
                'id': 'arrowhead',
                'viewBox': '-0 -5 10 10',
                'refX': 14,
                'refY': 0,
                'orient': 'auto',
                'markerWidth': 30,
                'markerHeight': 30,
                'xoverflow': 'visible'
            })
            .append('svg:path')
            .attr('d', 'M 0,-2 L 4 ,0 L 0,2')
            .attr('fill', 'black')
            .style('stroke', 'none');

        var linksContainer = svg.append("g").attr("class", "linksContainer")
        var nodesContainer = svg.append("g").attr("class", "nodesContainer")

        // iniital force simulation
        var simulation = d3.forceSimulation()
            .force("link", d3.forceLink().id(function (d) { return d.id; }).distance(100))
            .force("charge", d3.forceManyBody().strength(-400))
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("attraceForce", d3.forceManyBody().strength(70));

        //create links
        link = linksContainer.selectAll(".link")
            .data(graph.links)
            .enter()
            .append("line")
            .attr("class", "link")
            .style("pointer-events", "none")
            .attr('marker-end', 'url(#arrowhead)')

        linkPaths = linksContainer.selectAll(".linkPath")
            .data(graph.links)
            .enter()
            .append('path')
            .style("pointer-events", "none")
            .attrs({
                'class': 'linkPath',
                'id': function (d, i) { return 'linkPath' + i }
            })


        linkLabels = linksContainer.selectAll(".linkLabel")
            .data(graph.links)
            .enter()
            .append('text')
            .style("pointer-events", "none")
            .attrs({
                'class': 'linkLabel',
                'id': function (d, i) { return 'linkLabel' + i },
                'font-size': 12,
                'fill': 'black'
            })

        linkLabels.append('textPath')
            .attr('xlink:href', function (d, i) { return '#linkPath' + i })
            .style("text-anchor", "middle")
            .style("pointer-events", "none")
            .attr("startOffset", "50%")
            .text(function (d) { return d.type })

        node = nodesContainer.selectAll(".node")
            .data(graph.nodes)
            .enter()
            .append("g")
            .attr("class", "node")
            .attr("stroke", "white")
            .attr("stroke-width", "2px")
            .call(d3.drag()
                .on("start", dragStarted)
                .on("drag", dragged)
                .on("end", dragEnded)
            )

        node.append("circle")
            .attr("r", 30)
            .style("fill", "whitesmoke")
            .on("click", removeNode)

        node.append("title")
            .text(function (d) { return d.name })

        node.append("text")
            .style("class", "icon")
            .attr("font-family", "FontAwesome")
            .attr("dominant-baseline", "central")
            .attr("text-anchor", "middle")
            .attr("font-size", 30)
            .attr("fill", "black")
            .attr("stroke-width", "0px")
            .attr("pointer-events", "none")
            .text(function (d) { return d.icon })

        simulation
            .nodes(graph.nodes)
            .on("tick", ticked);

        simulation
            .force("link")
            .links(graph.links)



        function addNode(d) {
            var newid = graph.nodes.length + 1

            graph.links.push({ source: newid, target: d.id, type: "uses" })

            graph.nodes.push({
                "id": newid,
                "type": "software",
                "name": "Node",
                "context": [
                    {
                        "name": d.name
                    }
                ],
                "icon": "\ue084",
                "parent": d.id,
            })

            link = linksContainer.selectAll(".link")
                .data(graph.links)
                .enter()
                .append("line")
                .attr("class", "link")
                .style("pointer-events", "none")
                .attr('marker-end', 'url(#arrowhead)')
                .style("display", "block")
                .merge(link)

            linkPaths = linksContainer.selectAll(".linkPath")
                .data(graph.links)
                .enter()
                .append('path')
                .style("pointer-events", "none")
                .attrs({
                    'class': 'linkPath',
                    'fill-opacity': 1,
                    'stroke-opacity': 1,
                    'id': function (d, i) { return 'linkPath' + i }
                })
                .merge(linkPaths)

            linkLabels = linksContainer.selectAll(".linkLabel")
                .data(graph.links)
                .enter()
                .append('text')
                .style("pointer-events", "none")
                .attrs({
                    'class': 'linkLabel',
                    'id': function (d, i) { return 'linkLabel' + i },
                    'font-size': 12,
                    'fill': 'black'
                })
                .merge(linkLabels)

            linkLabels.append('textPath')
                .attr('xlink:href', function (d, i) { return '#linkPath' + i })
                .style("text-anchor", "middle")
                .style("pointer-events", "none")
                .attr("startOffset", "50%")
                .text(function (d) { return d.type })
                .merge(linkLabels)

            node = nodesContainer.selectAll(".node")
                .data(graph.nodes)
                .enter()
                .append("g")
                .attr("class", "node")
                .attr("stroke", "white")
                .attr("stroke-width", "2px")
                .call(d3.drag()
                    .on("start", dragStarted)
                    .on("drag", dragged)
                    .on("end", dragEnded)
                )
                .merge(node)

            node.append("circle")
                .attr("r", 30)
                .style("fill", "whitesmoke")
                .on("click", removeNode)
                .merge(node)

            node.append("text")
                .style("class", "icon")
                .attr("font-family", "FontAwesome")
                .attr("dominant-baseline", "central")
                .attr("text-anchor", "middle")
                .attr("font-size", 30)
                .attr("fill", "black")
                .attr("stroke-width", "0px")
                .attr("pointer-events", "none")
                .text(function (d) { return d.icon })
                .merge(node)

            simulation.nodes(graph.nodes);
            simulation.force("link").links(graph.links);

            //reheat the simulation
            simulation.alpha(0.3).restart()
        }

        function removeNode(d) {
            var indexOfNodes = graph.nodes.indexOf(d)


            var indexOfLinks = graph.links.findIndex(element => element.source.id == d.id)

            graph.links.splice(indexOfLinks, 1)

            linksContainer.selectAll(".link")
                .data(graph.links)
                .exit()
                .remove()

            linkPaths
                .data(graph.links)
                .exit()
                .remove()

            graph.nodes.splice(indexOfNodes, 1)

            nodesContainer.selectAll(".node")
                .data(graph.nodes)
                .exit()
                .remove()

            simulation.nodes(graph.nodes);
            simulation.force("link").links(graph.links);

            //reheat the simulation
            simulation.alpha(0.3).restart()

            console.log("Node: " + d.name + " deleted.")

        }

        function ticked() {
            // update link positions
            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; });

            // update node positions
            node
                .attr("transform", function (d) { return "translate(" + d.x + ", " + d.y + ")"; });

            linkPaths.attr('d', function (d) {
                return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
            });

            linkLabels.attr('transform', function (d) {
                if (d.target.x < d.source.x) {
                    var bbox = this.getBBox();

                    rx = bbox.x + bbox.width / 2;
                    ry = bbox.y + bbox.height / 2;
                    return 'rotate(180 ' + rx + ' ' + ry + ')';
                }
                else {
                    return 'rotate(0)';
                }
            });
        }


        function dragStarted(d) {
            if (!d3.event.active) simulation.alphaTarget(0.3).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 = undefined;
            d.fy = undefined;
        }
    </script>
</body>

</html>

1 thought on “Splicing array removes wrong node in force-directed graph”

  1. Unless you use a key function…

    .data(graph.nodes, d => d.id)
    

    …the data() method will join data points by their indices.

    Here is your code with that change only:

    <!DOCTYPE html>
    <html>
    
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
    
      <title>Playground</title>
      <!-- favcon -->
      <link rel="icon" href="https://networkrepository.com/favicon.png">
      <!-- call external d3.js framework -->
      <script src="https://d3js.org/d3.v4.js"></script>
      <!-- import multiselection framework -->
      <script src="https://d3js.org/d3-selection-multi.v1.js"></script>
      <!-- import "font awesome" stylesheet https://fontawesome.com/ -->
      <script src="https://kit.fontawesome.com/39094309d6.js" crossorigin="anonymous"></script>
    </head>
    
    <style>
      body {
        overflow: hidden;
        margin: 0px;
      }
      
      .canvas {
        background-color: rgb(220, 220, 220);
      }
      
      .link {
        stroke: rgb(0, 0, 0);
        stroke-width: 1px;
      }
      
      circle {
        background-color: whitesmoke;
      }
    </style>
    
    <body>
      <!-- create svg root element as a canvas -->
      <svg id="svg"></svg>
    
      <!-- call script where the main application is written -->
      <script>
        var graph = {
          "nodes": [{
              "id": 0,
              "type": "company",
              "name": "Company",
              "context": [{
                "name": "Company"
              }],
              "icon": "\uf1ad"
            },
            {
              "id": 1,
              "type": "software",
              "name": "Software_1",
              "context": [{
                "name": "Software_1"
              }],
              "icon": "\uf7b1",
              "parent": 1
            },
            {
              "id": 2,
              "type": "software",
              "name": "Software_2",
              "context": [{
                "name": "Software_2"
              }],
              "icon": "\uf78d",
              "parent": 1
            },
            {
              "id": 3,
              "type": "software",
              "name": "Software_3",
              "context": [{
                "name": "Software_3"
              }],
              "icon": "\ue084",
              "parent": 1
            },
            {
              "id": 4,
              "type": "software",
              "name": "Software_4",
              "context": [{
                "name": "Software_4"
              }],
              "icon": "\ue084",
              "parent": 1
            },
            {
              "id": 5,
              "type": "software",
              "name": "Software_5",
              "context": [{
                "name": "Software_5"
              }],
              "icon": "\ue084",
              "parent": 4
            },
            {
              "id": 6,
              "type": "software",
              "name": "Software_6",
              "context": [{
                "name": "Software_6"
              }],
              "icon": "\ue084",
              "parent": 4
            },
            {
              "id": 7,
              "type": "software",
              "name": "Software_7",
              "context": [{
                "name": "Software_7"
              }],
              "icon": "\ue084",
              "parent": 5
            },
            {
              "id": 8,
              "type": "software",
              "name": "Software_8",
              "context": [{
                "name": "Software_8"
              }],
              "icon": "\ue084",
              "parent": 5
            }
          ],
          "links": [{
              "source": 1,
              "target": 0,
              "type": "uses"
            },
            {
              "source": 2,
              "target": 0,
              "type": "uses"
            },
            {
              "source": 3,
              "target": 0,
              "type": "uses"
            },
            {
              "source": 4,
              "target": 0,
              "type": "uses"
            },
            {
              "source": 5,
              "target": 3,
              "type": "uses"
            },
            {
              "source": 6,
              "target": 3,
              "type": "uses"
            },
            {
              "source": 7,
              "target": 4,
              "type": "uses"
            },
            {
              "source": 8,
              "target": 4,
              "type": "uses"
            }
          ]
        }
    
        // declare initial variables
        var svg = d3.select("svg")
        width = window.innerWidth
        height = window.innerHeight
        node = null
        link = null
    
    
        // define cavnas area to draw everything
        svg = d3.select("svg")
          .attr("class", "canvas")
          .attr("width", width)
          .attr("height", height)
          .call(d3.zoom().on("zoom", function() {
            svg.attr("transform", d3.event.transform)
          }))
          .append("g")
    
        // remove zoom on dblclick listener
        d3.select("svg").on("dblclick.zoom", null)
    
        // append markers to svg
        svg.append('defs').append('marker')
          .attrs({
            'id': 'arrowhead',
            'viewBox': '-0 -5 10 10',
            'refX': 14,
            'refY': 0,
            'orient': 'auto',
            'markerWidth': 30,
            'markerHeight': 30,
            'xoverflow': 'visible'
          })
          .append('svg:path')
          .attr('d', 'M 0,-2 L 4 ,0 L 0,2')
          .attr('fill', 'black')
          .style('stroke', 'none');
    
        var linksContainer = svg.append("g").attr("class", "linksContainer")
        var nodesContainer = svg.append("g").attr("class", "nodesContainer")
    
        // iniital force simulation
        var simulation = d3.forceSimulation()
          .force("link", d3.forceLink().id(function(d) {
            return d.id;
          }).distance(100))
          .force("charge", d3.forceManyBody().strength(-400))
          .force("center", d3.forceCenter(width / 2, height / 2))
          .force("attraceForce", d3.forceManyBody().strength(70));
    
        //create links
        link = linksContainer.selectAll(".link")
          .data(graph.links)
          .enter()
          .append("line")
          .attr("class", "link")
          .style("pointer-events", "none")
          .attr('marker-end', 'url(#arrowhead)')
    
        linkPaths = linksContainer.selectAll(".linkPath")
          .data(graph.links)
          .enter()
          .append('path')
          .style("pointer-events", "none")
          .attrs({
            'class': 'linkPath',
            'id': function(d, i) {
              return 'linkPath' + i
            }
          })
    
    
        linkLabels = linksContainer.selectAll(".linkLabel")
          .data(graph.links)
          .enter()
          .append('text')
          .style("pointer-events", "none")
          .attrs({
            'class': 'linkLabel',
            'id': function(d, i) {
              return 'linkLabel' + i
            },
            'font-size': 12,
            'fill': 'black'
          })
    
        linkLabels.append('textPath')
          .attr('xlink:href', function(d, i) {
            return '#linkPath' + i
          })
          .style("text-anchor", "middle")
          .style("pointer-events", "none")
          .attr("startOffset", "50%")
          .text(function(d) {
            return d.type
          })
    
        node = nodesContainer.selectAll(".node")
          .data(graph.nodes, d => d.id)
          .enter()
          .append("g")
          .attr("class", "node")
          .attr("stroke", "white")
          .attr("stroke-width", "2px")
          .call(d3.drag()
            .on("start", dragStarted)
            .on("drag", dragged)
            .on("end", dragEnded)
          )
    
        node.append("circle")
          .attr("r", 30)
          .style("fill", "whitesmoke")
          .on("click", removeNode)
    
        node.append("title")
          .text(function(d) {
            return d.name
          })
    
        node.append("text")
          .style("class", "icon")
          .attr("font-family", "FontAwesome")
          .attr("dominant-baseline", "central")
          .attr("text-anchor", "middle")
          .attr("font-size", 30)
          .attr("fill", "black")
          .attr("stroke-width", "0px")
          .attr("pointer-events", "none")
          .text(function(d) {
            return d.icon
          })
    
        simulation
          .nodes(graph.nodes)
          .on("tick", ticked);
    
        simulation
          .force("link")
          .links(graph.links)
    
    
    
        function addNode(d) {
          var newid = graph.nodes.length + 1
    
          graph.links.push({
            source: newid,
            target: d.id,
            type: "uses"
          })
    
          graph.nodes.push({
            "id": newid,
            "type": "software",
            "name": "Node",
            "context": [{
              "name": d.name
            }],
            "icon": "\ue084",
            "parent": d.id,
          })
    
          link = linksContainer.selectAll(".link")
            .data(graph.links)
            .enter()
            .append("line")
            .attr("class", "link")
            .style("pointer-events", "none")
            .attr('marker-end', 'url(#arrowhead)')
            .style("display", "block")
            .merge(link)
    
          linkPaths = linksContainer.selectAll(".linkPath")
            .data(graph.links)
            .enter()
            .append('path')
            .style("pointer-events", "none")
            .attrs({
              'class': 'linkPath',
              'fill-opacity': 1,
              'stroke-opacity': 1,
              'id': function(d, i) {
                return 'linkPath' + i
              }
            })
            .merge(linkPaths)
    
          linkLabels = linksContainer.selectAll(".linkLabel")
            .data(graph.links)
            .enter()
            .append('text')
            .style("pointer-events", "none")
            .attrs({
              'class': 'linkLabel',
              'id': function(d, i) {
                return 'linkLabel' + i
              },
              'font-size': 12,
              'fill': 'black'
            })
            .merge(linkLabels)
    
          linkLabels.append('textPath')
            .attr('xlink:href', function(d, i) {
              return '#linkPath' + i
            })
            .style("text-anchor", "middle")
            .style("pointer-events", "none")
            .attr("startOffset", "50%")
            .text(function(d) {
              return d.type
            })
            .merge(linkLabels)
    
          node = nodesContainer.selectAll(".node")
            .data(graph.nodes)
            .enter()
            .append("g")
            .attr("class", "node")
            .attr("stroke", "white")
            .attr("stroke-width", "2px")
            .call(d3.drag()
              .on("start", dragStarted)
              .on("drag", dragged)
              .on("end", dragEnded)
            )
            .merge(node)
    
          node.append("circle")
            .attr("r", 30)
            .style("fill", "whitesmoke")
            .on("click", removeNode)
            .merge(node)
    
          node.append("text")
            .style("class", "icon")
            .attr("font-family", "FontAwesome")
            .attr("dominant-baseline", "central")
            .attr("text-anchor", "middle")
            .attr("font-size", 30)
            .attr("fill", "black")
            .attr("stroke-width", "0px")
            .attr("pointer-events", "none")
            .text(function(d) {
              return d.icon
            })
            .merge(node)
    
          simulation.nodes(graph.nodes);
          simulation.force("link").links(graph.links);
    
          //reheat the simulation
          simulation.alpha(0.3).restart()
        }
    
        function removeNode(d) {
          var indexOfNodes = graph.nodes.indexOf(d)
    
    
          var indexOfLinks = graph.links.findIndex(element => element.source.id == d.id)
    
          graph.links.splice(indexOfLinks, 1)
    
          linksContainer.selectAll(".link")
            .data(graph.links)
            .exit()
            .remove()
    
          linkPaths
            .data(graph.links)
            .exit()
            .remove()
    
          graph.nodes.splice(indexOfNodes, 1)
    
          nodesContainer.selectAll(".node")
            .data(graph.nodes, d => d.id)
            .exit()
            .remove()
    
          simulation.nodes(graph.nodes);
          simulation.force("link").links(graph.links);
    
          //reheat the simulation
          simulation.alpha(0.3).restart()
    
          console.log("Node: " + d.name + " deleted.")
    
        }
    
        function ticked() {
          // update link positions
          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;
            });
    
          // update node positions
          node
            .attr("transform", function(d) {
              return "translate(" + d.x + ", " + d.y + ")";
            });
    
          linkPaths.attr('d', function(d) {
            return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
          });
    
          linkLabels.attr('transform', function(d) {
            if (d.target.x < d.source.x) {
              var bbox = this.getBBox();
    
              rx = bbox.x + bbox.width / 2;
              ry = bbox.y + bbox.height / 2;
              return 'rotate(180 ' + rx + ' ' + ry + ')';
            } else {
              return 'rotate(0)';
            }
          });
        }
    
    
        function dragStarted(d) {
          if (!d3.event.active) simulation.alphaTarget(0.3).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 = undefined;
          d.fy = undefined;
        }
      </script>
    </body>
    
    </html>
    Reply

Leave a Comment