javascriptsvgd3.jsd3-force-directed

How to change angle of nodes and links in d3-force?


I am new to d3-force, I find that it is difficult to change the angle of lines and nodes,you can run my code here.

Anyway, my code is very easy:

const width = 800;
const height = 400;
const centerX = width / 2;
const centerY = height / 2;

const graph = ({
  nodes: Array.from({length:8}, () => ({})),
  links: [
    {source: 1, target: 0},
    {source: 2, target: 0},
    {source: 3, target: 0},
    {source: 4, target: 0},
    {source: 5, target: 0},
    {source: 6, target: 0},
    {source: 7, target: 0},
  ]
});

const svg = d3.select("svg").attr("viewBox", [0, 0, width, height]),
    link = svg
      .selectAll(".link")
      .data(graph.links)
      .join("line"),
    node = svg
      .selectAll(".node")
      .data(graph.nodes)
      .join("g");
  node.append("circle")
    .attr("r", 12)
    .attr("cursor", "move")
    .attr("fill", "#ccc")
    .attr("stroke", "#000")
    .attr("stroke-width", "1.5px");
  node.append("text").attr("dy", 25).text(function(d) {return d.index})

  const simulation = d3
    .forceSimulation()
    .nodes(graph.nodes)
    .force("link", d3.forceLink(graph.links).distance(100))
    .force("charge", d3.forceManyBody().strength(-400))
    .force("center", d3.forceCenter(width / 2, height / 2))
    .stop();
  for (let i = 0, n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())); i < n; ++i) {  
    simulation.tick();
  }
  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)
      .attr("stroke", "#000")
      .attr("stroke-width", "1.5px")
    node
      .attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>
It generates svg like this: generated by d3.js

But I want this: what I want


I dig the source of d3-force here, find that it adds Math.PI * (3 - Math.sqrt(5)) every time.

Here is what I want:

  1. what is the angle? --> equally distributed by all nodes, so if we have n nodes, it will be Math.PI / (n-1)
  2. how about the order? --> keep the order in clockwise
  3. where is the first node? --> starts with Math.PI / (n-1) / 2
  4. what about the children of children? --> follow the same rule, but the half size of length of the parent link.

Solution

  • I wouldn't use d3-force, but just calculate the positions yourself, using some basic trigonometry:

    const width = 600;
    const height = 400;
    const centerX = width / 2;
    const centerY = height / 2;
    
    const graph = ({
      nodes: d3.range(8).map(i => ({ id: i })),
      links: [{
          source: 0,
          target: 1
        },
        {
          source: 0,
          target: 2
        },
        {
          source: 0,
          target: 3
        },
        {
          source: 0,
          target: 4
        },
        {
          source: 1,
          target: 5
        },
        {
          source: 1,
          target: 6
        },
        {
          source: 1,
          target: 7
        },
      ]
    });
    
    graph.root = graph.nodes[0];
    graph.nodes.forEach(n => {
      n.children = [];
    });
    
    // Replace ID's with references to the nodes themselves
    graph.links.forEach(l => {
      l.source = graph.nodes.find(n => n.id === l.source);
      l.target = graph.nodes.find(n => n.id === l.target);
      
      // Register the target as a child of the source
      l.source.children.push(l.target);
      l.target.parent = l.source;
    });
    
    // Place the nodes
    graph.nodes.forEach(n => {
      if(n.parent === undefined) {
        // root
        n.x = centerX;
        n.y = centerY;
        n.level = 0;
        return;
      }
    
      const parent = n.parent;
      n.level = parent.level + 1;
      const nSiblings = parent.children.length;
      const ithSibling = parent.children.indexOf(n);
    
      // Position the node
      const angle = 2 * Math.PI / nSiblings; // in radians
      const startAngle = - angle / 2;
      const radius = 200 - 60 * n.level;
      
      console.log(angle, startAngle);
    
      n.x = parent.x + radius * Math.cos((ithSibling + 1) * angle + startAngle);
      // Use a plus to keep the order clockwise, - for counterclockwise
      n.y = parent.y + radius * Math.sin((ithSibling + 1) * angle + startAngle);
    });
    
    
    const svg = d3.select("svg").attr("viewBox", [0, 0, width, height]),
      link = svg
      .selectAll(".link")
      .data(graph.links)
      .join("line"),
      node = svg
      .selectAll(".node")
      .data(graph.nodes)
      .join("g");
    node.append("circle")
      .attr("r", 12)
      .attr("cursor", "move")
      .attr("fill", "#ccc")
      .attr("stroke", "#000")
      .attr("stroke-width", "1.5px");
    node.append("text").attr("dy", 25).text(function(d) {
      return d.id
    });
    
    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)
      .attr("stroke", "#000")
      .attr("stroke-width", "1.5px")
    node
      .attr("transform", function(d) {
        return "translate(" + d.x + ", " + d.y + ")";
      });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
    <svg></svg>