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
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
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'sx
) of the upstream node and add half of theinnerWidth
. 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>
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'.