javascriptsvgd3.jsd3-force-directed

How should I remove nodes in d3-force?


I want to remove node when click this node, so I just remove the node when click with nodes = nodes.filter(v => v.id !== d.id).Actually, it works at the first time, but it doesn't work next.

Here is my code:

uuid = function uuid() {
  var template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
  return template.replace(/[xy]/g, c => {
    var r = (Math.random() * 16) | 0;
    if (c === 'y') r = (r & 3) | 8;
    return r.toString(16);
  });
}

const width = 800;
const height = 400;
const count = 10;
let nodes = Array.from({length: count}).map((_, i) => ({x: width / count * i + 20, y: height / 2, id: uuid()}));
const svg = d3.select("svg").attr("viewBox", [0, 0, width, height]);
let node = svg
    .selectAll(".node");


const simulation = d3
  .forceSimulation()
  .nodes(nodes)
  .force("center", d3.forceCenter(width / 2, height / 2))
  .stop();

function update() {  
  node = node.data(nodes, d=> d.id)
    .join("g")
    .append("circle")
    .attr("r", 12)
    .attr("cursor", "move")
    .attr("fill", "#ccc")
    .attr("stroke", "#000")
    .attr("stroke-width", "1.5px")
    .attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";})
    .on("click", (ev, d) => {
      nodes = nodes.filter(v => v.id !== d.id);
      update();
  });
  
  simulation.nodes(nodes);

  if (simulation.alpha() <= 1) {
    simulation.alpha(1);
    simulation.restart();
  }
}

update()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>


Solution

  • You only want to append a circle for the new nodes, for any other nodes, appending a circle means that the next time you click, there's actually two circles to remove, not one!

    .join() can be passed three functions, that you can apply to the enter, update, and exit selections. These are executed, the return values are joined, and then you can do things to all selections combined.

    In this case, I moved all the one-time circle logic to the enter part, being careful not to return the circle, but the g element that you first appended. If you return the circle, it will combine these with the existing `g elements and the logic will break.

    uuid = function uuid() {
      var template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
      return template.replace(/[xy]/g, c => {
        var r = (Math.random() * 16) | 0;
        if (c === 'y') r = (r & 3) | 8;
        return r.toString(16);
      });
    }
    
    const width = 800;
    const height = 400;
    const count = 10;
    let nodes = Array.from({length: count}).map((_, i) => ({x: width / count * i + 20, y: height / 2, id: uuid()}));
    const svg = d3.select("svg").attr("viewBox", [0, 0, width, height]);
    let node = svg
        .selectAll(".node");
    
    const simulation = d3
      .forceSimulation()
      .nodes(nodes)
      .force("center", d3.forceCenter(width / 2, height / 2))
      .stop();
    
    function update() {  
      console.log(nodes.length);
      node = node.data(nodes, d=> d.id)
        .join(
          enter => {
            const g = enter
              .append("g")
              .attr("class", "node");
            g
              .append("circle")
              .attr("r", 12)
              .attr("cursor", "move")
              .attr("fill", "#ccc")
              .attr("stroke", "#000")
              .attr("stroke-width", "1.5px");
            return g;
          },
          update => update,
          exit => exit.remove()
        )
        .attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";})
        .on("click", (ev, d) => {
          nodes = nodes.filter(v => v.id !== d.id);
          update();
      });
      
      simulation.nodes(nodes);
    
      if (simulation.alpha() <= 1) {
        simulation.alpha(1);
        simulation.restart();
      }
    }
    
    update()
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
    <svg></svg>