Search code examples
d3.js

how to create horizontal strait graph in d3 from left to right according to the order of the nodes


I want to create a simple d3 horizontal strait graph.

It would contain nodes, links and lines for each link.

How can I define correctly coordinates of nodes/ edges in the tick to achieve it (without manually setting the coordinates).

Do I need to do some pre calculation before?

I basically want the 1st node to appear on the far left and the last node in the chain (graph) (it's a striat line) to appear on the far right of the chain (graph).

How can I determine the order of the nodes in the graph to align them.

Update

here's the fiddle I reached so far:

here i just hard coded the y axis, which i want dynamically.

and my nodes are also not in order.

  const nodes = [
  { id: 'A' },
  { id: 'B' },
  { id: 'C' },
];

const edges = [
  { source: 'A', target: 'B' },
  { source: 'B', target: 'C' },
];

const svg = d3.select("svg");

// Create arrowhead marker
svg.append('defs').append('marker')
    .attr('id', 'arrowhead')
    .attr('viewBox', '-0 -5 10 10')
    .attr('refX', 15)
    .attr('refY', 0)
    .attr('orient', 'auto')
    .attr('markerWidth', 10)
    .attr('markerHeight', 10)
    .attr('xoverflow', 'visible')
    .append('svg:path')
    .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
    .attr('fill', '#ccc')
    .style('stroke','none');

const simulation = d3.forceSimulation(nodes)
    .force("link", d3.forceLink(edges).id(d => d.id).distance(150))
    .force("charge", d3.forceManyBody().strength(-100))
    .force("y", d3.forceY(200))
    .force("x", d3.forceX(200));
    //.force("center", d3.forceCenter(400, 200));

const link = svg.selectAll(".link")
    .data(edges)
    .enter().append("line")
    .attr("class", "link")
    .attr('marker-end', 'url(#arrowhead)');

const node = svg.selectAll(".node")
    .data(nodes)
    .enter().append("circle")
    .attr("class", "node")
    .attr("r", 20);

simulation.on("tick", () => {
    link.attr("x1", d => d.source.x)
        .attr("y1", d => 20)
        .attr("x2", d => d.target.x)
        .attr("y2", d => 20);

    node.attr("cx", d => d.x)
        .attr("cy", d => 20);
});

https://jsfiddle.net/g87s23L5/3/

Ideally I'm looking for a way to use the built-in forces of d3


Solution

  • When you create simulation, I've adjusted the force("x", ...) to position the nodes based on their index. This will ensure that the nodes appear in the order from left to right.

    This is the fixed code:

        <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>D3 Horizontal Straight Graph</title>
      <script src="https://d3js.org/d3.v6.min.js"></script>
      <style>
        .node {
          fill: #3498db;
          stroke: #2980b9;
          stroke-width: 2px;
          cursor: pointer;
        }
    
        .link {
          fill: none;
          stroke: #ccc;
          stroke-width: 2px;
          cursor: pointer;
          marker-end: url(#arrowhead); /* Add arrowhead to links */
    
    .line {
      stroke: black;
        stroke-width: 2px;
    }
        }
      </style>
    </head>
    <body>
      <svg width="800" height="400"></svg>
      <script>
         const nodes = [
          { id: 'A' },
          { id: 'B' },
          { id: 'C' },
        ];
    
        const edges = [
          { source: 'A', target: 'B' },
          { source: 'B', target: 'C' },
        ];
    
    const svg = d3.select("svg");
    
        // Create arrowhead marker
        svg.append('defs').append('marker')
            .attr('id', 'arrowhead')
            .attr('viewBox', '-0 -5 10 10')
            .attr('refX', 15)
            .attr('refY', 0)
            .attr('orient', 'auto')
            .attr('markerWidth', 10)
            .attr('markerHeight', 10)
            .attr('xoverflow', 'visible')
            .append('svg:path')
            .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
            .attr('fill', '#ccc')
            .style('stroke','none');
    
        const simulation = d3.forceSimulation(nodes)
            .force("link", d3.forceLink(edges).id(d => d.id).distance(150))
            .force("charge", d3.forceManyBody().strength(-100))
            .force("y", d3.forceY(200))
            .force("x", d3.forceX((d, i) => i * 200)); // Position nodes based on index
    
        const link = svg.selectAll(".link")
            .data(edges)
            .enter().append("line")
            .attr("class", "link")
            .attr('marker-end', 'url(#arrowhead)');
    
        const node = svg.selectAll(".node")
            .data(nodes)
            .enter().append("circle")
            .attr("class", "node")
            .attr("r", 20);
    
        simulation.on("tick", () => {
            link.attr("x1", d => d.source.x)
                .attr("y1", d => 50)// you can adjust y axis to make it viewable
                .attr("x2", d => d.target.x)
                .attr("y2", d => 50);// you can adjust y axis to make it viewable
    
            node.attr("cx", d => d.x)
                .attr("cy", d => 50);// you can adjust y axis to make it viewable
        });
      </script>
    </body>
    </html>

    You can check on my fiddle here