Home > Software engineering >  Reverse the d3-hierarchy (d3-tree) graph to left side to show downstream as well
Reverse the d3-hierarchy (d3-tree) graph to left side to show downstream as well

Time:04-18

I have two sets of data one for upstream and one for downstream. Both upstream and downstream have same master node of John.

Upstream data

var upstreamData = [
  { name: "John", parent: "" },
  { name: "Ann", parent: "John" },
  { name: "Adam", parent: "John" },
  { name: "Chris", parent: "John" },
  { name: "Tina", parent: "Ann" },
  { name: "Sam", parent: "Ann" },
  { name: "Rock", parent: "Chris" },
  { name: "will", parent: "Chris" },
  { name: "Nathan", parent: "Adam" },
  { name: "Roger", parent: "Tina" },
  { name: "Dena", parent: "Tina" },
  { name: "Jim", parent: "Dena" },
  { name: "Liza", parent: "Nathan" }
];

Downstream data

var downstreamData = [
  { name: "John", parent: "" },
  { name: "Kat", parent: "John" },
  { name: "Amily", parent: "John" },
  { name: "Summer", parent: "John" },
  { name: "Loki", parent: "Kat" },
  { name: "Liam", parent: "Kat" },
  { name: "Tom", parent: "Amily" }
];

I am able to represent upstream data to the right side of the master node using d3 hierarchy and d3 tree, below is the image

enter image description here

How do I represent downstream data to left side of master node John, so that I can see both upstream and downstream data of john at once in same graph?

Below is the link to my codesandbox

enter image description here

So to flip this around so that the downstream tree is in the left-hand side and the upstream tree is on the right-hand side (and the root is centered) :

  • We need to halve the y coordinate (which is it's x) of the upstream node and add half of the innerWidth. For the root this puts in the centre, but for the descendants it puts them proportionally on the right hand side:
Array.from(nodesUpstream).forEach(n => n.y = (n.y * 0.5)   innerWidth / 2);

Then, do the same halving of the downstream node y coordinates (which are x really...) but *-1 which 'mirrors' them and then add innerWidth / 2 back. The root will still be in the centre, but now the descendants are proportionally on the left hand side and mirrored

Array.from(nodesDownstream).forEach(n => n.y = ((n.y * 0.5) * -1)   innerWidth / 2);

See the working snippet below with your OP data:

const nodeRadius = 6;
const width = 600; 
const height = 400; 
const margin = { top: 24, right: 24, bottom: 24, left: 24 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const svg = d3.select("body")
  .append("svg")
  .attr("width", width)
  .attr("height", height)
  .append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);

const rootName = "John";

const treeLayout = d3.tree().size([innerHeight, innerWidth]);

const stratified = d3.stratify()
  .id(function (d) { return d.name; })
  .parentId(function (d) { return d.parent; });
  
const linkPathGenerator = d3.linkHorizontal()
  .x((d) => d.y)
  .y((d) => d.x);
  
// create 2x trees 
const nodesUpstream = treeLayout(d3.hierarchy(stratified(upstreamData)).data);
const nodesDownstream = treeLayout(d3.hierarchy(stratified(downstreamData)).data);

// align the root node x and y
const nodesUpRoot = Array.from(nodesUpstream).find(n => n.data.name == rootName);
const nodesDownRoot = Array.from(nodesDownstream).find(n => n.data.name == rootName);
nodesDownRoot.x = nodesUpRoot.x;
nodesDownRoot.y = nodesUpRoot.y;

// NOTE - COMMENT OUT THIS STEP TO SEE THE INTEMEDIARY STEP
// for horizontal layout, flip x and y...
// right hand side (downstream): halve and add width / 2 to all y's (which are for x)
Array.from(nodesUpstream).forEach(n => n.y = (n.y / 2)   innerWidth / 2);
// left hand side (upstream): halve and negate all y's (which are for x) and add width / 2
Array.from(nodesDownstream).forEach(n => n.y = ((n.y / 2) * -1)   innerWidth / 2);

// render both trees
// index allows left hand and right hand side to separately selected and styled
[nodesUpstream, nodesDownstream].forEach(function(nodes, index) {

  // adds the links between the nodes
  // need to select links based on index to prevent bad rendering
  svg.selectAll(`links-${index}`)
    .data(nodes.links())
    .enter()
    .append("path")
    .attr("class", `link links-${index}`)
    .attr("d", linkPathGenerator);

  // adds each node as a group
  // need to select nodes based on index to prevent bad rendering
  var nodes = svg.selectAll(`.nodes-${index}`)
    .data(nodes.descendants())
    .enter()
    .append("g")
    .attr("class", `node nodes-${index}`) 
    .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
  nodes.append("circle")
    .attr("r", nodeRadius);

  // adds the text to the node
  nodes.append("text")
    .attr("dy", ".35em")
    .attr("y", -20)
    .style("text-anchor", "middle")
    .text(function(d) { return d.data.name; });

});
body {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: 0;
  overflow: hidden;
}

/* upstream */
path.links-0 {
  fill: none;
  stroke: #ff0000;
}

/* downstream */
path.links-1 {
  fill: none;
  stroke: #00ff00;
}

text {
  text-shadow: -1px -1px 3px white, -1px 1px 3px white, 1px -1px 3px white,
    1px 1px 3px white;
  pointer-events: none;
  font-family: "Playfair Display", serif;
}

circle {
  fill: blue;
}
<link href="https://fonts.googleapis.com/css?family=Playfair Display" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>
<script>
// Upstream data
var upstreamData = [
  { name: "John", parent: "" },
  { name: "Ann", parent: "John" },
  { name: "Adam", parent: "John" },
  { name: "Chris", parent: "John" },
  { name: "Tina", parent: "Ann" },
  { name: "Sam", parent: "Ann" },
  { name: "Rock", parent: "Chris" },
  { name: "will", parent: "Chris" },
  { name: "Nathan", parent: "Adam" },
  { name: "Roger", parent: "Tina" },
  { name: "Dena", parent: "Tina" },
  { name: "Jim", parent: "Dena" },
  { name: "Liza", parent: "Nathan" }
];
// Downstream data

var downstreamData = [
  { name: "John", parent: "" },
  { name: "Kat", parent: "John" },
  { name: "Amily", parent: "John" },
  { name: "Summer", parent: "John" },
  { name: "Loki", parent: "Kat" },
  { name: "Liam", parent: "Kat" },
  { name: "Tom", parent: "Amily" }
];
</script>

Which results in: enter image description here

There's 2 limitations: the root is drawn twice (you could skip labelling John for one of them I guess) and more importantly, the depth of the trees is not taken into account when re-laying-out the y coordinates. If you had a deeper upstream tree you would see this because it would still be laid out on the right hand half and be much more 'scrunched'.

  • Related