I'm fresh bee for D3-visulization. Currently working with creating D3 tree layout visualization for data lineage. In a data lineage flow, a child node can be derived from more than one parent. Here is the example. In below example, a 'DevLead' may work with 2 managers.
var data = [
{ "name": "Director", "parent": "null", "depth": 0 },
{ "name": "Manager1", "parent": "Director", "depth": 1 },
{ "name": "Manager2", "parent": "Director", "depth": 1 },
{ "name": "DevLead", "parent": "Manager1", "depth": 2 },
{ "name": "DevLead", "parent": "Manager2", "depth": 2 }
];
Getting output refer below image attached.
I'd like to see 'DevLead' children should show only one, and there should be a derivation from 'Manager1' and 'Manager2'. Could any one help with this.
D3 Tree Layout does not exactly supports multiple parents
What Can you do?
Use network graph instead - downside is that node positioning is
hard
I had similar requirements and tried building network graph similar
with tree layout, but when there are many nodes, it gets messy ...
you can check it on
codepen
use hack on tree layout - draw additional link from other node
check this example
another hack using hidden nodes - jsfiddle
Also, I think, these links will help you further :
Family Tree in d3.js
d-tree library - data with multiple parents
If you go with first option, here, you can play with this snippet by removing and adding nodes in data
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<link rel="shortcut icon" type="image/x-icon" href="https://production-assets.codepen.io/assets/favicon/favicon-8ea04875e70c4b0bb41da869e81236e54394d63638a1ef12fa558a4a835f1164.ico" />
<link rel="mask-icon" type="" href="https://production-assets.codepen.io/assets/favicon/logo-pin-f2d2b6d2c61838f7e76325261b7195c27224080bc099486ddd6dccb469b8e8e6.svg" color="#111" />
<title>CodePen - A Pen by dato</title>
</head>
<body translate="no" >
<script src='https://d3js.org/d3.v3.min.js'></script>
<script>
var width = window.innerWidth - 20,
height = window.innerHeight - 20,
radius = 30;
var min_zoom = 0.1;
var max_zoom = 7;
var zoom = d3.behavior.zoom().scaleExtent([min_zoom, max_zoom])
var fill = d3.scale.category20();
var force = d3.layout.force()
.charge(-8000)
.linkDistance(200)
.size([width, height]);
force.drag().on("dragstart", dragstarted)
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var chart = svg.append('g');
var json = {
"nodes": [{
"name": "node0"
}, {
"name": "node1"
}, {
"name": "node2"
}, {
"name": "node3"
}, {
"name": "node4"
}, {
"name": "node5"
}, {
"name": "node6"
}, {
"name": "node7"
}, {
"name": "node8"
}, {
"name": "node9"
}, {
"name": "node10"
}, {
"name": "node11"
}, {
"name": "node12"
}, {
"name": "node13"
}, {
"name": "node14"
}, {
"name": "node15"
}, {
"name": "node16"
}, {
"name": "node17"
}, {
"name": "node18"
}, {
"name": "node19"
}, {
"name": "node20"
}, {
"name": "node21"
}, {
"name": "node22"
}, {
"name": "node23"
}, {
"name": "node24"
}, {
"name": "node25"
}, {
"name": "node26"
}, {
"name": "node27"
}, {
"name": "node28"
}, {
"name": "node29"
}, {
"name": "node30"
}, {
"name": "node31"
}, {
"name": "node32"
}, {
"name": "node33"
}, {
"name": "node34"
}, {
"name": "node35"
}, {
"name": "node36"
}, {
"name": "node37"
}, {
"name": "node38"
}, {
"name": "node39"
}, {
"name": "node40"
}, {
"name": "node41"
}, {
"name": "node42"
}, {
"name": "node43"
}, {
"name": "node44"
}, {
"name": "node45"
}, {
"name": "node46"
}, {
"name": "node47"
}, {
"name": "node48"
}, {
"name": "node49"
}, {
"name": "node50"
}, {
"name": "node51"
}, {
"name": "node52"
}, {
"name": "node53"
}, {
"name": "node54"
}, {
"name": "node55"
}, {
"name": "node56"
}, {
"name": "node57"
}, {
"name": "node58"
}, {
"name": "node59"
}, {
"name": "node60"
}, {
"name": "node61"
}, {
"name": "node62"
}, {
"name": "node63"
}, {
"name": "node64"
}, {
"name": "node65"
}, {
"name": "node66"
}, {
"name": "node67"
}, {
"name": "node68"
}, {
"name": "node69"
}, {
"name": "node70"
}, {
"name": "node71"
}, {
"name": "node72"
}, {
"name": "node73"
}, {
"name": "node74"
}, {
"name": "node75"
}, {
"name": "node76"
}, {
"name": "node77"
}, {
"name": "node78"
}, {
"name": "node79"
}, {
"name": "node80"
}, {
"name": "node81"
}, {
"name": "node82"
}, {
"name": "node83"
}, {
"name": "node84"
}, {
"name": "node85"
}, {
"name": "node86"
}, {
"name": "node87"
}, {
"name": "node88"
}, {
"name": "node89"
}, {
"name": "node90"
}, {
"name": "node91"
}, {
"name": "node92"
}, {
"name": "node93"
}, {
"name": "node94"
}, {
"name": "node95"
}, {
"name": "node96"
}, {
"name": "node97"
}, {
"name": "node98"
}, {
"name": "node99"
}],
"links": [ {
"source": 0,
"target": 1
}, {
"source": 0,
"target": 2
}, {
"source": 1,
"target": 3
}, {
"source": 1,
"target": 4
}, {
"source": 2,
"target": 5
}, {
"source": 2,
"target": 6
}, {
"source": 3,
"target": 7
}, {
"source": 3,
"target": 8
}, {
"source": 4,
"target": 9
}, {
"source": 4,
"target": 10
}, {
"source": 5,
"target": 11
}, {
"source": 5,
"target": 12
}, {
"source": 6,
"target": 13
}, {
"source": 6,
"target": 14
}, {
"source": 7,
"target": 15
}, {
"source": 7,
"target": 16
}, {
"source": 8,
"target": 17
}, {
"source": 8,
"target": 18
}, {
"source": 9,
"target": 19
}, {
"source": 9,
"target": 20
}, {
"source": 10,
"target": 21
}, {
"source": 10,
"target": 22
}, {
"source": 11,
"target": 23
}, {
"source": 11,
"target": 24
}, {
"source": 12,
"target": 25
}, {
"source": 12,
"target": 26
}, {
"source": 13,
"target": 27
}, {
"source": 13,
"target": 28
}, {
"source": 14,
"target": 29
}, {
"source": 14,
"target": 30
}, {
"source": 15,
"target": 31
}, {
"source": 15,
"target": 32
}, {
"source": 16,
"target": 33
}, {
"source": 16,
"target": 34
}, {
"source": 17,
"target": 35
}, {
"source": 17,
"target": 36
}, {
"source": 18,
"target": 37
}, {
"source": 18,
"target": 38
}, {
"source": 19,
"target": 39
}, {
"source": 19,
"target": 40
}, {
"source": 20,
"target": 41
}, {
"source": 20,
"target": 42
}, {
"source": 21,
"target": 43
}, {
"source": 21,
"target": 44
}, {
"source": 22,
"target": 45
}, {
"source": 22,
"target": 46
}, {
"source": 23,
"target": 47
}, {
"source": 23,
"target": 48
}, {
"source": 24,
"target": 49
}, {
"source": 24,
"target": 50
}, {
"source": 25,
"target": 51
}, {
"source": 25,
"target": 52
}, {
"source": 26,
"target": 53
}, {
"source": 26,
"target": 54
}, {
"source": 27,
"target": 55
}, {
"source": 27,
"target": 56
}, {
"source": 28,
"target": 57
}, {
"source": 28,
"target": 58
}, {
"source": 29,
"target": 59
}, {
"source": 29,
"target": 60
}, {
"source": 30,
"target": 61
}, {
"source": 30,
"target": 62
}, {
"source": 31,
"target": 63
}, {
"source": 31,
"target": 64
}, {
"source": 32,
"target": 65
}, {
"source": 32,
"target": 66
}, {
"source": 33,
"target": 67
}, {
"source": 33,
"target": 68
}, {
"source": 34,
"target": 69
}, {
"source": 34,
"target": 70
}, {
"source": 35,
"target": 71
}, {
"source": 35,
"target": 72
}, {
"source": 36,
"target": 73
}, {
"source": 36,
"target": 74
}, {
"source": 37,
"target": 75
}, {
"source": 37,
"target": 76
}, {
"source": 38,
"target": 77
}, {
"source": 38,
"target": 78
}, {
"source": 39,
"target": 79
}, {
"source": 39,
"target": 80
}, {
"source": 40,
"target": 81
}, {
"source": 40,
"target": 82
}, {
"source": 41,
"target": 83
}, {
"source": 41,
"target": 84
}, {
"source": 42,
"target": 85
}, {
"source": 42,
"target": 86
}, {
"source": 43,
"target": 87
}, {
"source": 43,
"target": 88
}, {
"source": 44,
"target": 89
}, {
"source": 44,
"target": 90
}, {
"source": 45,
"target": 91
}, {
"source": 45,
"target": 92
}, {
"source": 46,
"target": 93
}, {
"source": 46,
"target": 94
}, {
"source": 47,
"target": 95
}, {
"source": 47,
"target": 96
}, {
"source": 48,
"target": 97
}, {
"source": 48,
"target": 98
}, {
"source": 49,
"target": 99
},{
"source": 0,
"target": 99
}]
}
var link = chart.selectAll("line")
.data(json.links)
.enter()
.append("line")
.attr("stroke", function(d) {
return 'blue'
})
var node = chart.selectAll("circle")
.data(json.nodes)
.enter().append("circle")
.attr("r", radius - .75)
.style("fill", function(d) {
return fill(d.group);
})
.style("stroke", function(d) {
return d3.rgb(fill(d.group)).darker();
})
.on('mouseover', d => console.log(d))
.call(force.drag);
function dragstarted() {
d3.event.sourceEvent.stopPropagation();
}
zoom.on("zoom", function(d) {
var evt = d3.event;
debugger;
/*
var dcx = (window.innerWidth/2-d.x*zoom.scale());
var dcy = (window.innerHeight/2-d.y*zoom.scale());
*/
var dcx = evt.translate[0]
var dcy = evt.translate[1]
zoom.translate([dcx, dcy]);
chart.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");;
});
force
.nodes(json.nodes)
.links(json.links)
.on("tick", tick)
.start();
svg.call(zoom)
function tick(e) {
console.log(e)
var k = 6 * e.alpha;
// Push sources up and targets down to form a weak tree.
link
.each(function(d,i) {
d.source.y -= k * 60, d.target.y += k * 100;
/*
if(i%2==1){
d.source.x -= 0.4/k
}else{
d.source.x += 0.4/k
}
*/
})
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
</script>
</body>
</html>
use hack on tree layout - draw additional link from other node
check this example
If a hack is the solution you're looking for the implementation below might be helpful. It's a d3 collapsable tree based on Rob Schmueckers Blog Multiple Parent Nodes D3.js example which can handle multiple parent links and the basic tree events. It's typically split into index.html building the html structure, a stylesheet style.css and the actual script tree.js
index.html :
Define a div with id tree_view later holding the tree. Call tree.js to create the tree.
<!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> <!-- div holding tree -->
<script src="tree.js"></script> <!-- script to create tree -->
</body>
</html>
style.css :
#tree_view {
width: 100%;
height: 100%;
margin-top: 30px;
}
/* basic node */
.node {
cursor: pointer;
text-anchor: start;
}
/* rectangle node */
.node rect {
stroke: gray;
stroke-width: 1.5px;
}
/* node text */
.node text {
font: 12px sans-serif;
}
/* standard links (.link) and multiparent links (.mpLink) */
.link, .mpLink {
fill: none;
stroke: #ccc;
}
tree.js : (full code below)
Basically you need to define a new link object for each additional link you want to add. Each link needs a source and target node. The backup nodes are necessary for event handling:
let link = new Object();
link.source = pairNode1;
link.target = pairNode2;
link._source = pairNode1; // backup source
link._target = pairNode2; // backup target
additionalLinks.push(link)
Now you can handle all additional links in the update process (updateTree(source)):
// ======== 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();
and define the on event behavior in method click(d).
The full tree.js code:
// 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 (multiparent) 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);
Here you can find the full demo. You can also fork the project.
Using d3js, I need to draw(append) circles, not all together but with less then one second of distance. So one circle in x position, another one in y position after 0.5 second.
Use setTimeout. Here is the working code snippet.
var nodes = [{
"name": "6",
"x": 207,
"y": 305
}, {
"name": "7",
"x": 404,
"y": 310
}, {
"name": "8",
"x": 420,
"y": 510
}, {
"name": "9",
"x": 540,
"y": 126
}, {
"name": "10",
"x": 350,
"y": 150
}, {
"name": "11",
"x": 177,
"y": 320
}, {
"name": "12",
"x": 200,
"y": 190
}, {
"name": "13",
"x": 170,
"y": 150
}, {
"name": "14",
"x": 107,
"y": 510
}, {
"name": "15",
"x": 104,
"y": 150
}, {
"name": "16",
"x": 104,
"y": 150
}, {
"name": "17",
"x": 310,
"y": 160
}, {
"name": "18",
"x": 120,
"y": 110
}, {
"name": "19",
"x": 619,
"y": 145
}, {
"name": "20",
"x": 148,
"y": 107
}, {
"name": "21",
"x": 575,
"y": 107
}];
var width = 500,
height = 400;
var color = d3.scale.category20();
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
nodes.forEach(function(d, i) {
setTimeout(function() {
svg.append("circle")
.datum(d)
.attr("class", "node")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", "10")
.style("fill", function(d) {
return color(i);
});
}, 500 * i);
});
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.overlay {
fill: none;
pointer-events: all;
}
#map{
border: 2px #555 dashed;
width:500px;
height:400px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body>
<div id="map"></div>
</body>
You can use the standard javascript methods setTimeout or setInterval to append the circles one by one with a variable delay depending on the circle index.
Or, you could create all the circles on enter normally using the standard d3 syntax but with opacity set to 0 and just add a .transition() with delay dependent on the index that sets opacity to 1
Here's a working jsfiddle of the latter option: http://jsfiddle.net/pg5m3m3n/5/
Extract:
canvas.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr({
'cx': function(d) { return d.x; },
'cy': function(d) { return d.y; },
'r': 10,
'opacity': 0
})
.transition().delay(function(d,i) { return i*50; })
.attr('opacity',1);
The pros of this is that it uses d3 syntax and it's just 2 lines of code more than the normal append, the con is that the circles are actually added immediately and only become visible one by one, which may give performance issues if the number of circles to append is huge.