3

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:

enter image description here

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.

S.B
  • 311
  • 2
  • 22

1 Answers1

3

There's useful material to consider:

  1. Tree drawing orientation - which links to a block showing that a left-to-right orientation can be achieved by altering use of x and y coordinates of the node
  2. Similar question to yours - accepted answer says to draw two trees in two <g>s and shift one 'over'
  3. Simple D3 v6 example - v6 is useful because it allows 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:

  • You want two trees - one going left-to-right from the root and one going right-to-left from the root
  • The d3.tree method usefully computes the right xs and ys 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).
  • The exception being the root node e.g. where you have more nodes on the 'left' vs the 'right' the root will be in a slightly different position. You have to choose one root node for both trees.
  • You can recompute the coordinates such that:
    • in the right hand tree, the 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.
    • in the left hand tree, the 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.

Robin Mackenzie
  • 18,801
  • 7
  • 38
  • 56