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);
@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