D3 set afterwards added node position

Question: How can I change the initial position of added nodes?

The snippet below shows 3 nodes with a context-menu. Its possible to add/remove nodes. I would like to change the initial position of an added node. I tried to set the x and y coords with .attr("x", value) as well as .attr("cx", value). Both attempts doesn´t work out.

I am aware how to push the into the array but the answer of the initial question is unclear for me.

<!DOCTYPE html>
<html>

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

    <title>D3 JOIN Test</title>
    <!-- call external d3.js framework -->
    <script src="https://d3js.org/d3.v6.js"></script>
</head>

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

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

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

    #context-menu {
        font-family: "Open Sans", sans-serif;
        position: fixed;
        z-index: 10000;
        width: 190px;
        background: whitesmoke;
        border: 2px;
        border-radius: 6px;
        border-color: white;
        border-style: solid;
        transform: scale(0);
        transform-origin: top left;
    }

    #context-menu.active {
        transform: scale(1);
        transition: transform 200ms ease-in-out;
    }

    #context-menu .item {
        padding: 8px 10px;
        font-size: 15px;
        color: black;
    }

    #context-menu .item i {
        display: inline-block;
        margin-right: 5px;
    }

    #context-menu hr {
        margin: 5px 0px;
        border-color: whitesmoke;
    }

    #context-menu .item:hover {
        background: lightblue;
    }
</style>

<body>
    <!-- right click context menu -->
    <div id="context-menu">
        <div id="addObject" class="item">
            <i class="fa fa-plus-circle"></i> Add Node
        </div>
        <div id="removeObject" class="item">
            <i class="fa fa-minus-circle"></i> Remove Node
        </div>
    </div>

    <svg id="svg"> </svg>

    <script>
        var graph = {
            "nodes": [
                {
                    "id": 1,
                },
                {
                    "id": 2,
                },
                {
                    "id": 3,
                }
            ],
            "links": [
                {
                    "source": 1,
                    "target": 2,
                },
                {
                    "source": 2,
                    "target": 3,
                },
                {
                    "source": 3,
                    "target": 1,
                }
            ]
        }

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

        // 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 (event) {
                svg.attr("transform", event.transform)
            }))
            .append("g")

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

        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(200))
            .force("charge", d3.forceManyBody().strength(-100))
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("attraceForce", d3.forceManyBody().strength(70));

        initialze()

        function initialze() {
            links = linksContainer.selectAll(".link")
                .data(graph.links)
                .join("line")
                .attr("class", "link")

            nodes = nodesContainer.selectAll(".node")
                .data(graph.nodes, d => d.id)
                .join("circle")
                .attr("class", "node")
                .attr("r", 30)
                .attr("fill", "whitesmoke")
                .attr("stroke", "white")
                .call(d3.drag()
                    .on("start", dragStarted)
                    .on("drag", dragged)
                    .on("end", dragEnded)
                )
                .on("mouseenter", mouseEnter)
                .on("mouseleave", mouseLeave)
                .on("contextmenu", contextMenu)

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

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

        function mouseEnter(event, d) {
            d3.select(this).style("fill", "lightblue")
        }

        function mouseLeave(event, d) {
            d3.select(this).style("fill", "whitesmoke")
        }

        function contextMenu(event, d) {
            thisElement = d

            event.preventDefault()

            var contextMenu = document.getElementById("context-menu")
            contextMenu.style.top = event.clientY + "px"
            contextMenu.style.left = event.clientX + "px"
            contextMenu.classList.add("active")

            window.addEventListener("click", function () {
                contextMenu.classList.remove("active")
            })

            document.getElementById("addObject").addEventListener("click", addNode)
            document.getElementById("removeObject").addEventListener("click", removeNode)
        }


        function addNode() {
            var newID = Math.floor(Math.random() * 100000)

            graph.nodes.push({ "id": newID, })

            initialze()

            simulation.alpha(0.3).restart()
        }


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

            graph.nodes.splice(indexOfNodes, 1)

            initialze()

            simulation.alpha(0.3).restart()
        }

        function ticked() {
            links
                .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;
                });

            nodes
                .attr("transform", function (d) {
                    return "translate(" + d.x + ", " + d.y + ")";
                });
        }

        function dragStarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }

        function dragEnded(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = undefined;
            d.fy = undefined;
        }

    </script>
</body>

</html>

Answer

For a D3 force simulation you don’t need to change the x, cx or whatever node attribute, you need to set the x and y property in the node datum’s object. In your case, for instance, setting the new node to show up at (400, 100) is as simple as:

graph.nodes.push({ "id": newID, x:400, y:100})

Here is your code with that change:

<!DOCTYPE html>
<html>

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

  <title>D3 JOIN Test</title>
  <!-- call external d3.js framework -->
  <script src="https://d3js.org/d3.v6.js"></script>
</head>

<style>
  body {
    overflow: hidden;
    margin: 0px;
  }
  
  .canvas {
    background-color: rgb(220, 220, 220);
  }
  
  .link {
    stroke: rgb(0, 0, 0);
    stroke-width: 2px;
  }
  
  #context-menu {
    font-family: "Open Sans", sans-serif;
    position: fixed;
    z-index: 10000;
    width: 190px;
    background: whitesmoke;
    border: 2px;
    border-radius: 6px;
    border-color: white;
    border-style: solid;
    transform: scale(0);
    transform-origin: top left;
  }
  
  #context-menu.active {
    transform: scale(1);
    transition: transform 200ms ease-in-out;
  }
  
  #context-menu .item {
    padding: 8px 10px;
    font-size: 15px;
    color: black;
  }
  
  #context-menu .item i {
    display: inline-block;
    margin-right: 5px;
  }
  
  #context-menu hr {
    margin: 5px 0px;
    border-color: whitesmoke;
  }
  
  #context-menu .item:hover {
    background: lightblue;
  }
</style>

<body>
  <!-- right click context menu -->
  <div id="context-menu">
    <div id="addObject" class="item">
      <i class="fa fa-plus-circle"></i> Add Node
    </div>
    <div id="removeObject" class="item">
      <i class="fa fa-minus-circle"></i> Remove Node
    </div>
  </div>

  <svg id="svg"> </svg>

  <script>
    var graph = {
      "nodes": [{
          "id": 1,
        },
        {
          "id": 2,
        },
        {
          "id": 3,
        }
      ],
      "links": [{
          "source": 1,
          "target": 2,
        },
        {
          "source": 2,
          "target": 3,
        },
        {
          "source": 3,
          "target": 1,
        }
      ]
    }

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

    // 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(event) {
        svg.attr("transform", event.transform)
      }))
      .append("g")

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

    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(200))
      .force("charge", d3.forceManyBody().strength(-100))
      .force("center", d3.forceCenter(width / 2, height / 2))
      .force("attraceForce", d3.forceManyBody().strength(70));

    initialze()

    function initialze() {
      links = linksContainer.selectAll(".link")
        .data(graph.links)
        .join("line")
        .attr("class", "link")

      nodes = nodesContainer.selectAll(".node")
        .data(graph.nodes, d => d.id)
        .join("circle")
        .attr("class", "node")
        .attr("r", 30)
        .attr("fill", "whitesmoke")
        .attr("stroke", "white")
        .call(d3.drag()
          .on("start", dragStarted)
          .on("drag", dragged)
          .on("end", dragEnded)
        )
        .on("mouseenter", mouseEnter)
        .on("mouseleave", mouseLeave)
        .on("contextmenu", contextMenu)

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

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

    function mouseEnter(event, d) {
      d3.select(this).style("fill", "lightblue")
    }

    function mouseLeave(event, d) {
      d3.select(this).style("fill", "whitesmoke")
    }

    function contextMenu(event, d) {
      thisElement = d

      event.preventDefault()

      var contextMenu = document.getElementById("context-menu")
      contextMenu.style.top = event.clientY + "px"
      contextMenu.style.left = event.clientX + "px"
      contextMenu.classList.add("active")

      window.addEventListener("click", function() {
        contextMenu.classList.remove("active")
      })

      document.getElementById("addObject").addEventListener("click", addNode)
      document.getElementById("removeObject").addEventListener("click", removeNode)
    }


    function addNode() {
      var newID = Math.floor(Math.random() * 100000)

      graph.nodes.push({
        "id": newID,
        x: 400,
        y: 100
      })

      initialze()

      simulation.alpha(0.3).restart()
    }


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

      graph.nodes.splice(indexOfNodes, 1)

      initialze()

      simulation.alpha(0.3).restart()
    }

    function ticked() {
      links
        .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;
        });

      nodes
        .attr("transform", function(d) {
          return "translate(" + d.x + ", " + d.y + ")";
        });
    }

    function dragStarted(event, d) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }

    function dragged(event, d) {
      d.fx = event.x;
      d.fy = event.y;
    }

    function dragEnded(event, d) {
      if (!event.active) simulation.alphaTarget(0);
      d.fx = undefined;
      d.fy = undefined;
    }
  </script>
</body>

</html>

Leave a Reply

Your email address will not be published. Required fields are marked *