I am currently working on a project where i need to create a bowtie diagram from data coming from an application.
From a bit of research, the library that looks the best to acheive this is D3.js
I have played about with / looked at these examples:
Collapsible Tree: https://observablehq.com/@d3/collapsible-tree
Hierarchy Chart: https://bl.ocks.org/willzjc/a11626a31c65ba5d319fcf8b8870f281
Here is a basic bowtie diagram example that i would like to try and replicate:
As you can see in the image - i need multiple top level items and/or a double tree that can open each side/tree independently with data flowing from left to right (red side) and right to left (blue side) of the top level item.
Is this acheivable using D3.js?
Tutorials about d3.js only cover standard charts like bar charts and the tree examples online that i have found only have 1 top level item.
Any help, advice or pointers in the right direction would be greatly appreciated.
There's useful material to consider:
<g>
s and shift one 'over'Array.from
to get a list of the nodes from the d3.tree
method without traversing the hierarchy.The 2nd one isn't quite similar enough to your question so adapted the 3rd to use principles from the 1st:
d3.tree
method usefully computes the right x
s and y
s for the positioning of the nodes (except for 'horizontal' rendering you flip use of x
and y
in the code per the Mike Bostock block). We want to keep the understanding of the relative positions between nodes but do a translation on both RH tree (center in g
) and LH tree (flip vertically left of center of g
).y
coordinates (now meaning x
) should have width / 2
added to shift them to the vertical center of the g
and then halved to keep 'branch' lengths within the width
.y
coordinates (now meaning x
) should be halved (same deal for 'branch lengths' in RH tree) and negated to flip the node positions to the left hand side of the root node (where you have opted to choose e.g. the LH tree's root position).Here's the demo:
// useful links
// https://bl.ocks.org/mbostock/3184089
// https://bl.ocks.org/d3noob/72f43406bbe9e104e957f44713b8413c
var treeDataL = {
"name": "Root",
"children": [
{
"name": "L_A_1",
"children": [
{"name": "L_B_1"},
{"name": "L_B_2"}
]
},
{
"name": "L_A_2",
"children": [
{"name": "L_B_3"},
{"name": "L_B_4"}
]
}
]
}
var treeDataR = {
"name": "Root",
"children": [
{
"name": "R_A_1",
"children": [
{"name": "R_B_1"},
{"name": "R_B_2"},
{"name": "R_B_3"}
]
},
{
"name": "R_A_2",
"children": [
{"name": "R_B_3"},
{"name": "R_B_4"},
{"name": "R_B_5"},
{"name": "R_B_6"}
]
}
]
}
// set the dimensions and margins of the diagram
var margin = {top: 50, right: 50, bottom: 50, left: 50},
width = 400 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// declares a tree layout and assigns the size
var tree = d3.tree()
.size([width, height]);
// create 2x trees using d3 hierarchy
// this is where the computation of coordinates takes place
var nodesL = tree(d3.hierarchy(treeDataL));
var nodesR = tree(d3.hierarchy(treeDataR));
// get arrays of nodes - need v6
nodesLArray = Array.from(nodesL);
nodesRArray = Array.from(nodesR);
// switch out nodesR root for nodesL
// here the choice is to assign coords of root of LH tree to RH
nodesLRoot = nodesLArray.find(n => n.data.name == "Root");
nodesRRoot = nodesRArray.find(n => n.data.name == "Root");
nodesRRoot.x = nodesLRoot.x;
nodesRRoot.y = nodesLRoot.y;
// this is kinda like the 'important bit'
// REMEMBER for horizontal layout, flip x and y...
// LH: halve and negate all y's in nodesL add width / 2
nodesLArray.forEach(n => n.y = ((n.y * 0.5) * -1) + width / 2);
// RH: halve and add width / 2 to all y's nodesR
nodesRArray.forEach(n => n.y = (n.y * 0.5) + width / 2);
// now sticking a bit more closely to the tutorial in link 3
// append svg
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
// align g with margin
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// render both trees
[nodesL, nodesR].forEach(function(nodes, i) {
// adds the links between the nodes
// need to select links based on index to prevent bad rendering
var link = g.selectAll(`links${i}`)
.data( nodes.descendants().slice(1))
.enter()
.append("path")
.attr("class", `link links${i}`) // note two classes
.attr("d", function(d) {
// x and y flipped here to achieve horizontal placement
return `M${d.y},${d.x}C${d.y},${(d.x + d.parent.x) / 2} ${d.parent.y},${(d.x + d.parent.x) / 2} ${d.parent.y},${d.parent.x}`
});
// adds each node as a group
// need to select nodes based on index to prevent bad rendering
var node = g.selectAll(`.nodes${i}`)
.data(nodes.descendants())
.enter()
.append("g")
.attr("class", `node nodes${i}`) // note two classes
.attr("transform", function(d) {
// x and y flipped here to achieve horizontal placement
return `translate(${d.y},${d.x})`;
});
// adds the circle to the node
node.append("circle")
.attr("r", 10);
// adds the text to the node
node.append("text")
.attr("dy", ".35em")
.attr("y", function(d) { return d.children ? -20 : 20; })
.style("text-anchor", "middle")
.text(function(d) { return d.data.name; });
});
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 10px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
The demo is basic and you might need to look into tree.nodeSize
to achieve node placements accommodating boxes containing text etc. I think the principle of updating the y
(being x
) coordinates still applies to flip the LH tree around.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With