This is a d3.js diagram which implements a workaround to enable multiple parent nodes for d3 collapsable trees. The idea is based on this static version: Multiple Parent Nodes D3.js.
- See also live demo
Example - Graph | Example - Graph collapsed |
---|---|
This is a d3.js diagram which implements a workaround to enable multiple parent nodes for d3 collapsable trees. The idea is based on this static version: Multiple Parent Nodes D3.js.
Example - Graph | Example - Graph collapsed |
---|---|
<!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); |
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.
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?
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.
Thanks for the fast reply, I think I found a solution in adapting https://plnkr.co/edit/P2Jcqh12cxVyToLdXun6?p=preview&preview.
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?
@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)
.
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?
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.