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:
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.
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>