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); |
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?
Thanks for the fast reply, I think I found a solution in adapting https://plnkr.co/edit/P2Jcqh12cxVyToLdXun6?p=preview&preview.