The below is a "working" D3 animation.
Child nodes successfully disappear on mouse-click... HOWEVER, duplicates nodes ("circles") are added. If you play around with running the following code, then collapsing and opening nodes you will see nodes appearing on top of other ones!
The duplication of circle-elements can be seen in chrome-inspection too.
Just simple collapse and open is what we're after here. Your help is much appreciated ! Thank you.
(ignore the node labels - they're not important)
<html>
<head>
<style>
.node {
cursor: pointer;
font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
font-weight: 300;
}
.node .text {
fill: white;
}
.ORG .circle {
fill: #1d3649;
}
.EMR .circle {
fill: #B2D0F5;
stroke: #5596e6;
stroke-dasharray: 3px, 3px;
opacity: .5;
}
.EMR .circle:hover {
fill: #5596e6;
}
.link {
fill: none;
stroke: #eee;
stroke-width: 1.5px;
font: 10px sans-serif;
}
.link.active {
stroke: #ddd;
stroke-width: 2;
}
.arrow {
fill: #666;
}
.arrow.active {
stroke-width: 0 !important;
}
</style>
</head>
<body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var dataset = {
"nodes": [{
"id": 223,
"type": "Parent",
"properties": {
}
}, {
"id": 136525,
"type": "Child",
"properties": {
"patient": "6090",
"batch": "70"
}
}, {
"id": 146525,
"type": "Child",
"properties": {
"patient": "6090",
"batch": "70"
}
}, {
"id": 156525,
"type": "Child",
"properties": {
"patient": "6090",
"batch": "70"
}
}, {
"id": 166525,
"type": "Child",
"properties": {
"patient": "6090",
"batch": "70"
}
}, {
"id": 176525,
"type": "Child",
"properties": {
"patient": "6090",
"batch": "70"
}
}, {
"id": 136448,
"type": "Child",
"properties": {
"patient": "6094",
"batch": "70"
}
}, {
"id": 136328,
"type": "Child",
"properties": {
"patient": "6082",
"batch": "70"
}
}, {
"id": 136305,
"type": "Child",
"properties": {
"patient": "6096",
"batch": "70"
}
}, {
"id": 136303,
"type": "Child",
"properties": {
"patient": "6093",
"batch": "70"
}
}, {
"id": 136299,
"type": "Child",
"properties": {
"patient": "6091",
"batch": "70"
}
}, {
"id": 136261,
"type": "Child",
"properties": {
"patient": "6089",
"batch": "70"
}
}, {
"id": 136212,
"type": "Child",
"properties": {
"patient": "6087",
"batch": "70"
}
}, {
"id": 136115,
"type": "Child",
"properties": {
"patient": "6078",
"batch": "70"
}
}, {
"id": 136113,
"type": "Child",
"properties": {
"patient": "6088",
"batch": "70"
}
}, {
"id": 135843,
"type": "Child",
"properties": {
"patient": "6072",
"batch": "70"
}
}, {
"id": 555,
"type": "Grandchild",
"properties": {
}
}],
"edges": [{
"id": 0,
"from": 136113,
"to": 555,
"properties": {
}
},{
"id": 0,
"from": 136525,
"to": 555,
"properties": {
}
},{
"id": 0,
"from": 146525,
"to": 555,
"properties": {
}
},{
"id": 0,
"from": 156525,
"to": 555,
"properties": {
}
},{
"id": 0,
"from": 166525,
"to": 136448,
"properties": {
}
},{
"id": 0,
"from": 176525,
"to": 223,
"properties": {
}
},{
"id": 0,
"from": 223,
"to": 136525,
"properties": {
}
}, {
"id": 0,
"from": 223,
"to": 136448,
"properties": {
}
}, {
"id": 0,
"from": 223,
"to": 136328,
"properties": {
}
}, {
"id": 0,
"from": 223,
"to": 136305,
"properties": {
}
}, {
"id": 0,
"from": 136525,
"to": 136303,
"properties": {
}
}, {
"id": 0,
"from": 223,
"to": 136299,
"properties": {
}
}, {
"id": 0,
"from": 223,
"to": 136261,
"properties": {
}
}, {
"id": 0,
"from": 223,
"to": 136212,
"properties": {
}
}, {
"id": 0,
"from": 223,
"to": 136115,
"properties": {
}
}, {
"id": 0,
"from": 223,
"to": 136113,
"properties": {
}
}, {
"id": 0,
"from": 223,
"to": 135843,
"properties": {
}
}]
}
var width = 0.975 * $(window).width(),
height = 0.95 * $(window).height();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.size([width, height])
//gravity(0.2)
.linkDistance(height / 6)
.charge(function(node) {
if (node.type !== 'ORG') return -2000;
return -30;
});
// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", function(d) {
return d;
})
.attr("viewBox", "0 -5 10 10")
.attr("refX", 12)
.attr("refY", 0)
.attr("markerWidth", 9)
.attr("markerHeight", 5)
.attr("orient", "auto")
.attr("class", "arrow")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var json = dataset;
var edges = [];
json.edges.forEach(function(e) {
var sourceNode = json.nodes.filter(function(n) {
return n.id === e.from;
})[0],
targetNode = json.nodes.filter(function(n) {
return n.id === e.to;
})[0];
edges.push({
source: sourceNode,
target: targetNode,
value: e.id
});
});
var colors = {};
colors[23] = "lightblue";
colors[25] = "lightgreen";
colors[48] = "lightyellow";
colors[28] = "lightblue";
colors[5] = "lightgreen";
colors[3] = "lightyellow";
colors[99] = "lightblue";
colors[61] = "lightgreen";
colors[12] = "lightyellow";
for(var i=0; i<json.nodes.length; i++) {
json.nodes[i].collapsing = 0;
json.nodes[i].collapsed = false;
json.nodes[i].radius = json.nodes[i].id % 100;
if(colors[json.nodes[i].radius] != undefined)
json.nodes[i].color = colors[json.nodes[i].radius];
else
json.nodes[i].color = "lightbrown";
}
var link = svg.selectAll(".link");
var node = svg.selectAll(".node");
force.on("tick", function() {
// make sure the nodes do not overlap the arrows
link.attr("d", function(d) {
// Total difference in x and y from source to target
diffX = d.target.x - d.source.x;
diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
// x and y distances from center to outside edge of target node
offsetX = (diffX * d.target.radius) / pathLength;
offsetY = (diffY * d.target.radius) / pathLength;
return "M" + d.source.x + "," + d.source.y + "L" + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
update();
function update(){
var nodes = json.nodes.filter(function(d) {
return d.collapsing == 0;
});
var links = edges.filter(function(d) {
return d.source.collapsing == 0 && d.target.collapsing == 0;
});
link = svg.selectAll(".link").data(links);
link.exit().remove();
link.enter().append("path")
.attr("class", "link")
.attr("marker-end", "url(#end)");
node = svg.selectAll(".node").data(nodes);
node.exit().remove();
node.enter().append("g")
.attr("class", function(d) {
return "node " + d.type
});
node.append("circle")
.attr("class", "circle")
.attr("r", function(d) {
//d.radius = 30;
return d.radius
})
.attr("fill", function(d) {
//d.radius = 30;
return d.color;
})
.attr("stroke", function(d) {
//d.radius = 30;
return "darkgray";
});
// return a radius for path to use
node.append("text")
.attr("x", 0)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("class", "text")
.text(function(d) {
return d.type
});
// On node hover, examine the links to see if their
// source or target properties match the hovered node.
node.on('mouseover', function(d) {
link.attr('class', function(l) {
if (d === l.source || d === l.target)
return "link active";
else
return "link inactive";
});
});
// Set the stroke width back to normal when mouse leaves the node.
node.on('mouseout', function() {
link.attr('class', "link");
})
.on('click', click);
/** this is NOT the problem **/
function click(d) {
if (!d3.event.defaultPrevented) {
var inc = d.collapsed ? -1 : 1;
recurse(d);
function recurse(sourceNode){
//check if link is from this node, and if so, collapse
edges.forEach(function(l) {
if (l.source.id === sourceNode.id){
l.target.collapsing += inc;
recurse(l.target);
}
});
}
d.collapsed = !d.collapsed;
}
update();
}
force
.nodes(nodes)
.links(links)
.start();
}
</script>
</body>
</html>
[code based on this: http://jsfiddle.net/sheilak/9wvmL8q8/
The problem is here:
node.enter().append("g")...
node.append("circle")...
node.append("text")...
Although you are adding the group to the enter
selection, you're adding the circle and text to the node
selection which is the transitioning elements
try:
var nodeEnter = node.enter().append("g")...
nodeEnter.append("circle")...
nodeEnter.append("text")...
In this case, nodeEnter
will be equal to the last created element in the chain, which in this case is the g
element.