Search code examples
d3.js

How set source and target position independently in d3.js links?


I'm new to d3.js and I would like to create a visualization where boxes are connected with links.

I managed to create boxes and links. Here is a minimal example of what I did for now:

<!DOCTYPE html>
<div id="container"></div>
<script type="module">
import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';

const data = { name: 'a', y: 2, children: [
    { name: 'b', y: 1 },
    { name: 'c', y: 3, children: [
        { name: 'd', y: 2 },
        { name: 'e', y: 4 },
    ]}
]}

const root = d3.hierarchy(data);
const width = 400;
const height = 200;
const box_width = 3;
const box_spacing = 5;

const svg = d3.create('svg')
            .attr('width', width)
            .attr('height', height)
            .attr('viewBox', [0, 0, 20, 10])
            .attr('style', 'max-width: 100%; height: auto; font: 1px mono;');

        const node = svg.append('g')
            .selectAll()
            .data(root.descendants())
            .join('g')
            .attr('transform', (d) => `translate(${d.depth * box_spacing},${d.data.y})`);

        node.append('rect')
            .attr('width', box_width)
            .attr('height', 1)
            .attr('fill', '#ccc')
            .attr('y', -0.5);

        node.append('text')
            .text((d) => d.data.name)
            .attr('y', 0.4);

        svg.append('g') // link
            .attr('fill', 'none')
            .attr('stroke', 'black')
            .attr('stroke-width', 0.1)
            .selectAll()
            .data(root.links())
            .join('path')
            .attr('d', d3.linkHorizontal()
                .x((d) => d.depth * box_spacing)
                .y((d) => d.data.y));

container.append(svg.node());
</script>

The links are connected to each other:

enter image description here

But I want the links to start at the right of each box.

So I guess I need to append the box width to source x position, but in .attr('d', d3.linkHorizontal().x((d) => ...).y((d) => ...), I don't know how to define a different position for the source and the target.


Solution

  • First off, it probably makes sense to use d3.tree to compute the layout of the tree. That will add x and y attributes to each node.

    Second, when you call d3.linkHorizontal, you can compare x or y properties of the of the d argument to the link.source.x or y to tell which is which and then add the box_width on as appropriate. That portion looks like so:

    .attr("d", (link) =>
      d3
        .linkHorizontal()
        .x((d) => (d.y == link.source.y ? d.y + box_width : d.y))
        .y((d) => d.x)(link)
    );
    

    Here it is all together:

    <div id="container"></div>
    <script type="module">
    import * as d3 from 'https://cdn.jsdelivr.net/npm/d3@7/+esm';
    
    const data = { name: 'a', y: 2, children: [
        { name: 'b', y: 1 },
        { name: 'c', y: 3, children: [
            { name: 'd', y: 2 },
            { name: 'e', y: 4 },
        ]}
    ]}
    
    const root = d3.hierarchy(data);
    const width = 400;
    const height = 200;
    const box_width = 3;
    const box_spacing = 5;
    
    const svg = d3
        .create("svg")
        .attr("viewBox", [0, 0, 20, 10])
        .style("max-width", `${width}px`)
        .style("font", "1px mono")
        .style("border", "solid 1px black");
    
    d3.tree().size([10, 20 - box_width])(root);
    
    const node = svg.append('g')
        .selectAll()
        .data(root.descendants())
        .join('g')
        .attr("transform", (d) => `translate(${d.y},${d.x})`);
    
            node.append('rect')
                .attr('width', box_width)
                .attr('height', 1)
                .attr('fill', '#ccc')
                .attr('y', -0.5);
    
            node.append('text')
                .text((d) => d.data.name)
                .attr('y', 0.4);
    
            svg.append('g') // link
                .attr('fill', 'none')
                .attr('stroke', 'black')
                .attr('stroke-width', 0.1)
                .selectAll()
                .data(root.links())
                .join('path')
        .attr("d", (link) =>
          d3
            .linkHorizontal()
            .x((d) => (d.y == link.source.y ? d.y + box_width : d.y))
            .y((d) => d.x)(link)
        );
    
    container.append(svg.node());
    </script>