2

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

https://codesandbox.io/s/d3-practice-forked-y69kkw?file=/src/index.js

Thanks in advance!

young_minds1
  • 1,181
  • 3
  • 10
  • 25

1 Answers1

1

I've adapted my answer to this question so it suits your data structure.

This method has key steps:

  1. Remember that for a horizontal layout you flip x and y...
  2. Compute both tree layouts for upstream and downstream
  3. Make the root nodes have the same x and y
  4. Re-compute the y coordinate for every node such that the root is in the center and the downsteam branches work leftward and the upstream branches work right-ward.
  5. Draw both trees

If you skip step 3 then you end up with this (where red is upstream and green is downstream):

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 (upstream): 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 (downstream): 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'.

Edit

To fix node widths (according to depth) then you can use this:

const depthFactor = 60;
Array.from(nodesUpstream).forEach(n => n.y = (n.depth * depthFactor) + innerWidth / 2);
Array.from(nodesDownstream).forEach(n => n.y = (innerWidth / 2) - (n.depth * depthFactor));

Example:

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;

// for horizontal layout, flip x and y...
const depthFactor = 60;
Array.from(nodesUpstream).forEach(n => n.y = (n.depth * depthFactor) + innerWidth / 2);
Array.from(nodesDownstream).forEach(n => n.y = (innerWidth / 2) - (n.depth * depthFactor));

// 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 gives:

enter image description here

Robin Mackenzie
  • 18,801
  • 7
  • 38
  • 56
  • Thank you. It is what i need. Could please help me out, how do i give fix node length for all nodes. tried doing changing `Array.from(nodesUpstream).forEach(n => n.y = (n.y * 0.5) + innerWidth / 2);` and also tried `nodes.forEach(function (d) { d.y = d.depth * 180; }); ` but isn't giving the desired result. – young_minds1 Apr 18 '22 at 15:04
  • 1
    @young_minds1 - see my edit – Robin Mackenzie Apr 19 '22 at 10:05
  • Thank you. it was such easy fix for you. but was not able to do it on my own.will have to learn d3. – young_minds1 Apr 19 '22 at 13:49