Splicing array removes wrong node in force-directed graph

Viewed 79

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 Answers

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>

Related