Skip to content

Instantly share code, notes, and snippets.

@rebeccasc
Last active March 28, 2024 19:38
Show Gist options
  • Save rebeccasc/b4ce160b99aa5316866d6866713d85dd to your computer and use it in GitHub Desktop.
Save rebeccasc/b4ce160b99aa5316866d6866713d85dd to your computer and use it in GitHub Desktop.
D3 multiple parents tree - collapsable, resizable
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 collapsable multiple parents tree</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
</head>
<body>
<div id="tree_view"></div>
<script src="tree.js"></script>
</body>
</html>
#tree_view {
width: 100%;
height: 100%;
margin-top: 30px;
}
.node {
cursor: pointer;
text-anchor: start;
}
.node rect {
stroke: gray;
stroke-width: 1.5px;
}
.node text {
font: 12px sans-serif;
}
.link, .mpLink {
fill: none;
stroke: #ccc;
}
// plot properties
let root;
let tree;
let diagonal;
let svg;
let duration = 750;
let treeMargin = { top: 0, right: 20, bottom: 20, left: 20 };
let treeWidth = window.innerWidth - treeMargin.right - treeMargin.left;
let treeHeight = window.innerHeight - treeMargin.top - treeMargin.bottom;
let treeDepth = 5;
let maxTextLength = 90;
let nodeWidth = maxTextLength + 20;
let nodeHeight = 36;
let scale = 1;
// tree data
let data = [
{
"name": "Root",
"parent": "null",
"children": [
{
"name": "Level 2: A",
"parent": "Top Level",
"children": [
{
"name": "A1",
"parent": "Level 2: A"
},
{
"name": "A2",
"parent": "Level 2: A"
}
]
},
{
"name": "Level 2: B",
"parent": "Top Level"
}
]
}
];
// additional links data array
let additionalLinks = []
/**
* Initialize tree properties
* @param {Object} treeData
*/
function initTree(treeData) {
// init
tree = d3.layout.tree()
.size([treeWidth, treeHeight]);
diagonal = d3.svg.diagonal()
.projection(function (d) { return [d.x + nodeWidth / 2, d.y + nodeHeight / 2]; });
svg = d3.select("div#tree_view")
.append("svg")
.attr("width", treeWidth + treeMargin.right + treeMargin.left)
.attr("height", treeHeight + treeMargin.top + treeMargin.bottom)
.attr("transform", `translate(${treeMargin.left},${treeMargin.top})scale(${scale},${scale})`);
root = treeData[0];
root.x0 = treeHeight / 2;
root.y0 = 0;
// fill additionalLinks array
let pairNode1 = tree.nodes(root).filter(function(d) {
return d['name'] === 'Level 2: B';
})[0];
let pairNode2 = tree.nodes(root).filter(function(d) {
return d['name'] === 'A2';
})[0];
let link = new Object();
link.source = pairNode1;
link.target = pairNode2;
link._source = pairNode1; // backup source
link._target = pairNode2; // backup target
additionalLinks.push(link)
// update
updateTree(root);
d3.select(self.frameElement).style("height", "500px");
// add resize listener
window.addEventListener("resize", function (event) {
resizeTreePlot();
});
}
/**
* Perform tree update. Update nodes and links
* @param {Object} source
*/
function updateTree(source) {
let i = 0;
let nodes = tree.nodes(root).reverse();
let links = tree.links(nodes);
nodes.forEach(function (d) { d.y = d.depth * 80; });
// ======== add nodes and text elements ========
let node = svg.selectAll("g.node")
.data(nodes, function (d) { return d.id || (d.id = ++i); });
let nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) { return `translate(${source.x0},${source.y0})`; })
.on("click", click);
nodeEnter.append("rect")
.attr("width", nodeWidth)
.attr("height", nodeHeight)
.attr("rx", 2)
.style("fill", function(d) { return d._children ? "#ace3b5": "#f4f4f9"; });
nodeEnter.append("text")
.attr("y", nodeHeight / 2)
.attr("x", 13)
.attr("dy", ".35em")
.text(function (d) { return d.name; })
.style("fill-opacity", 1e-6);
let nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) { return `translate(${d.x},${d.y})`; });
nodeUpdate.select("rect")
.attr("width", nodeWidth)
.style("fill", function(d) { return d._children ? "#ace3b5": "#f4f4f9"; });
nodeUpdate.select("text").style("fill-opacity", 1);
let nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) { return `translate(${source.x},${source.y})`; })
.remove();
nodeExit.select("rect")
.attr("width", nodeWidth)
.attr("rx", 2)
.attr("height", nodeHeight);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// ======== add links ========
let link = svg.selectAll("path.link")
.data(links, function (d) { return d.target.id; });
link.enter().insert("path", "g")
.attr("class", "link")
.attr("x", nodeWidth / 2)
.attr("y", nodeHeight / 2)
.attr("d", function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});
link.transition()
.duration(duration)
.attr("d", diagonal)
link.exit().transition()
.duration(duration)
.attr("d", function (d) {
let o = { x: source.x, y: source.y };
return diagonal({ source: o, target: o });
})
.remove();
// ======== add additional links (mpLinks) ========
let mpLink = svg.selectAll("path.mpLink")
.data(additionalLinks);
mpLink.enter().insert("path", "g")
.attr("class", "mpLink")
.attr("x", nodeWidth / 2)
.attr("y", nodeHeight / 2)
.attr("d", function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});
mpLink.transition()
.duration(duration)
.attr("d", diagonal)
.attr("stroke-width", 1.5)
mpLink.exit().transition()
.duration(duration)
.attr("d", function (d) {
let o = { x: source.x, y: source.y };
return diagonal({ source: o, target: o });
})
.remove();
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
/**
* Handle on tree node clicked actions
* @param {Object} d node
*/
function click(d) {
// update regular links
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
// update additional links
additionalLinks.forEach(function(link){
let sourceVisible = false;
let targetVisible = false;
tree.nodes(root).filter(function(n) {
if(n["name"] == link._source.name){
sourceVisible = true;
}
if(n["name"] == link._target.name){
targetVisible = true;
}
});
if(sourceVisible && targetVisible){
link.source = link._source;
link.target = link._target;
}
else if(!sourceVisible && targetVisible
|| !sourceVisible && !targetVisible){
link.source = d;
link.target = link.source;
}
else if(sourceVisible && !targetVisible){
link.source = link._source;
link.target = link.source;
}
});
// define more links behavior here...
updateTree(d);
}
/**
* Update tree dimension
*/
function updateTreeDimension() {
tree.size([treeWidth, treeHeight]);
svg.attr("width", treeWidth + treeMargin.right + treeMargin.left)
.attr("height", treeHeight + treeMargin.top + treeMargin.bottom)
.attr("transform", `translate(${treeMargin.left},${treeMargin.top})scale(${scale},${scale})`);
}
/**
* Resize the tree using current window dimension
*/
function resizeTreePlot() {
treeWidth = 0.9 * window.innerWidth - treeMargin.right - treeMargin.left;
treeHeight = (treeDepth + 2) * nodeHeight * 2;
updateTreeDimension();
updateTree(root);
}
// plot tree
initTree(data);
updateTree(root);
@rebeccasc
Copy link
Author

First add your data to the data object. Then, for each new multi parent node you need to create a new link object and add it to additionalLinks (see L68-L80). The impementation in click() should handle the basic operations. If your data set is large, there will probably occur some special cases where you need to define the additional collapse actions.

@Itachi3007
Copy link

Thanks a lot, the additional code that is there in click function works for basic collapse function too.
Your code has been of great help to me.
You are saviour.

@Beni-L
Copy link

Beni-L commented Nov 11, 2022

Hey Rebecca nice work! Two questions: is it possible to start the tree not with only one node? I'm looking for a way to visualize a family tree: seeing the ascendants on top (multiple parents) and the descendants on the bottom.
I'm new to D3.js: Is there are reason why you use version 3 and not version 7?

@rebeccasc
Copy link
Author

@Beni-L

is it possible to start the tree not with only one node? I'm looking for a way to visualize a family tree: seeing the ascendants on top (multiple parents) and the descendants on the bottom

It is possible but only through a trick. You can enter a root node and make it transparent using the corresponding css attribute. Then add two child nodes to the invisible root node and it will show up as two root nodes. If you want to build a family tree this project may be interesting: dTree demo -> dTree GitHub

Is there are reason why you use version 3 and not version 7?

Not really. When I started the project I used the latest version at that time. If the D3 API hasn't changed a lot you should be able to use the newest verison.

@Beni-L
Copy link

Beni-L commented Nov 12, 2022

Thanks for the fast reply, I think I found a solution in adapting https://plnkr.co/edit/P2Jcqh12cxVyToLdXun6?p=preview&preview.

@RikDekard
Copy link

Hi, need your help, how can I dynamically add new child nodes to a D3 family tree when a user clicks on a parent node?

@rebeccasc
Copy link
Author

@RikDekard,
you'll need to add some code to click(d) function which updates the tree data struct. The passed argument d at click(d) gives you information about the parent node name. The parent node name can then be searched in data and a child be added. Before returning the function you'll need to update and rerender the tree. Maybe this is already done in updateTree(d).

@RikDekard
Copy link

RikDekard commented Mar 28, 2024

Hi Reccasc, first of all thanks for your fast response, i know you re busy, so i appreciate that.
I have done what you suggested. I added a function to the code, element is added in data array, but it didn’t render the element.

  function click(d) {
     var node =   findNode(d.id, data[0].children) 
      node.children =[];
      node.children.push( {"name": "Dragon", "parent": `"${d.name}"`})
      console.log(node);
      console.log(data);
      updateTree(data);
  }

// finding node in data array, this function is not necessary
function findNode(id, array) {
for (const node of array) {
if (node.id === id) {
return node;
}
if (node.children) {
const result = findNode(id, node.children);
if (result) {
return result;
}
}
}
return null; // Node not found
}
what am I doing wrong?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment