javascripthtmlsvgd3.jsd3-force-directed

How to update d3-force elements?


I want to update d3-force elements when click, but I find something wrong. What I want is:

  1. generate all links first
  2. then generate all nodes, so that the node can override the link

So when clicked the elements should be:

<svg>
  <line ...></line>
  <line ...></line>
  <line ...></line>
  <line ...></line>
  <g><circle ...></circle></g>
  <g><circle ...></circle></g>
  <g><circle ...></circle></g>
  <g><circle ...></circle></g>
  <g><circle ...></circle></g>
</svg>

But I get:

<svg>
  <g><circle ...></circle></g>
  <line ...></line>
  <line ...></line>
  <line ...></line>
  <line ...></line>
  <g><circle ...></circle></g>
  <g><circle ...></circle></g>
  <g><circle ...></circle></g>
  <g><circle ...></circle></g>
</svg>

const width = 800;
const height = 400;

const svg = d3.select("svg").attr("viewBox", [0, 0, width, height]);
let node = svg.selectAll(".node");
let link = svg.selectAll(".link");

const simulation = d3
  .forceSimulation()
  .force("center", d3.forceCenter(width / 2, height / 2))
  .on("tick", (d) => {
    link
      .attr("x1", d => d.source.x)
      .attr("y1", d => d.source.y)
      .attr("x2", d => d.target.x)
      .attr("y2", d => d.target.y);

    node
      .attr("transform", d => `translate(${d.x}, ${d.y})`);
  });

const root = {
  x: width / 2,
  y: height / 2,
  level: 0,
  id: "1"
};

function genChildren(p) {
  const dist = 100 / (p.level + 1);
  const nodes = [{
      x: p.x - dist,
      y: p.y,
      level: p.level + 1,
      id: p.id + "1"
    },
    {
      x: p.x + dist,
      y: p.y,
      level: p.level + 1,
      id: p.id + "2"
    },
    {
      x: p.x,
      y: p.y - dist,
      level: p.level + 1,
      id: p.id + "3"
    },
    {
      x: p.x,
      y: p.y + dist,
      level: p.level + 1,
      id: p.id + "4"
    },
  ];
  const links = nodes.map(v => ({
    source: p.id,
    target: v.id
  }));
  return {
    nodes,
    links
  };
}


function update(nodes, links) {
  link = link.data(links)
    .join(enter => {
      return enter.append("line")
        .attr("stroke", "#000")
        .attr("stroke-width", "1.5px")
        .call(enter => enter.transition().attr("stroke-opacity", 1));
    }, update => update, exit => exit.remove());
  node = node.data(nodes, d => d.id)
    .join(
      enter => {
        const g = enter.append("g");
        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) => {
      if (d.level !== 0) {
        return;
      }
      const childrenInfo = genChildren(root)
      const nodes = d.active ? [root] : [root].concat(childrenInfo.nodes);
      const links = d.active ? [] : childrenInfo.links;
      d.active = !d.active;
      update(nodes, links);
    });

  simulation.nodes(nodes).force("link", d3.forceLink(links).id(node => node.id).strength(-0.01));

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

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


Solution

  • You could have separate g elements for the nodes and the links, add the links to the first one, and the nodes to the second. Or, you could just use node.raise() to put those elements after all the links in the DOM:

    const width = 800;
    const height = 400;
    
    const svg = d3.select("svg").attr("viewBox", [0, 0, width, height]);
    let node = svg.selectAll(".node");
    let link = svg.selectAll(".link");
    
    const simulation = d3
      .forceSimulation()
      .force("center", d3.forceCenter(width / 2, height / 2))
      .on("tick", (d) => {
        link
          .attr("x1", d => d.source.x)
          .attr("y1", d => d.source.y)
          .attr("x2", d => d.target.x)
          .attr("y2", d => d.target.y);
    
        node
          .attr("transform", d => `translate(${d.x}, ${d.y})`);
      });
    
    const root = {
      x: width / 2,
      y: height / 2,
      level: 0,
      id: "1"
    };
    
    function genChildren(p) {
      const dist = 100 / (p.level + 1);
      const nodes = [{
          x: p.x - dist,
          y: p.y,
          level: p.level + 1,
          id: p.id + "1"
        },
        {
          x: p.x + dist,
          y: p.y,
          level: p.level + 1,
          id: p.id + "2"
        },
        {
          x: p.x,
          y: p.y - dist,
          level: p.level + 1,
          id: p.id + "3"
        },
        {
          x: p.x,
          y: p.y + dist,
          level: p.level + 1,
          id: p.id + "4"
        },
      ];
      const links = nodes.map(v => ({
        source: p.id,
        target: v.id
      }));
      return {
        nodes,
        links
      };
    }
    
    
    function update(nodes, links) {
      link = link.data(links)
        .join(enter => {
          return enter.append("line")
            .attr("stroke", "#000")
            .attr("stroke-width", "1.5px")
            .call(enter => enter.transition().attr("stroke-opacity", 1));
        }, update => update, exit => exit.remove());
      node = node.data(nodes, d => d.id)
        .join(
          enter => {
            const g = enter.append("g");
            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(),
        )
        .raise()
        .attr("transform", function(d) {
          return "translate(" + d.x + ", " + d.y + ")";
        })
        .on("click", (ev, d) => {
          if (d.level !== 0) {
            return;
          }
          const childrenInfo = genChildren(root)
          const nodes = d.active ? [root] : [root].concat(childrenInfo.nodes);
          const links = d.active ? [] : childrenInfo.links;
          d.active = !d.active;
          update(nodes, links);
        });
    
      simulation.nodes(nodes).force("link", d3.forceLink(links).id(node => node.id).strength(-0.01));
    
      if (simulation.alpha() <= 1) {
        simulation.alpha(1);
        simulation.restart();
      }
    }
    
    update([root], [])
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
    <svg></svg>