Skip to content

Instantly share code, notes, and snippets.

@juancortr
Created November 30, 2016 03:21
Show Gist options
  • Save juancortr/6cd03d19fc962c10c52394ccd31b9255 to your computer and use it in GitHub Desktop.
Save juancortr/6cd03d19fc962c10c52394ccd31b9255 to your computer and use it in GitHub Desktop.
D3.js Drag and Drop, Zoomable, Panning, Collapsible Tree with auto-sizing.
license: mit
// Get JSON data
treeJSON = d3.json("flare.json", function(error, treeData) {
createTree(treeData,"#treecontainerDetalle",false);
createTree(treeData,"#treecontainer",true);
});
{
"name": "Uniandes",
"value": 3.5,
"type": "black",
"level": "red",
"children": [
{
"name": "Pregrado",
"value": 3,
"type": "grey",
"level": "red",
"children": [
{
"name": "Facultad de Ingeniería",
"value": 6,
"type": "steelblue",
"level": "orange"
},
{
"name": "Facultad de mecánica",
"value": 6,
"type": "steelblue",
"level": "red"
}
]
},
{
"name": "Maestría",
"value": 3,
"type": "grey",
"level": "green",
"children": [
{
"name": "Facultad de Ingeniería",
"value": 6,
"type": "steelblue",
"level": "orange"
},
{
"name": "Facultad de mecánica",
"value": 6,
"type": "steelblue",
"level": "red"
}
]
}
,
{
"name": "Posgrado",
"value": 3,
"type": "grey",
"level": "green",
"children": [
{
"name": "Facultad de Ingeniería",
"value": 6,
"type": "steelblue",
"level": "orange"
},
{
"name": "Facultad de mecánica",
"value": 6,
"type": "steelblue",
"level": "red"
}
]
}
]
}
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
#treecontainer svg {
overflow: visible;
}
#treecontainerDetalle{
border:1px solid black;
}
.node {
cursor: pointer;
}
.overlay{
// background-color:#FFF;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font-size:10px;
font-family:sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.templink {
fill: none;
stroke: red;
stroke-width: 3px;
}
</style>
<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="tree.js"></script>
<script src="dndTree.js"></script>
<body>
<div id="treecontainer"></div>
<div id="treecontainerDetalle"></div>
</body>
</html>
function createTree(treeData,container, isDrag){
var xScale = d3.scale.linear().domain([0,5]).range([0, 100]);
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// variables for drag/drop
var selectedNode = null;
var draggingNode = null;
var anchuraNodos=50;
var root;
var containertag=container.replace("#","");
// Misc. variables
var i = 0;
var duration = 1000;
// size of the diagram
var viewerWidth = $(document).width();
var viewerHeight = 300;//$(document).height();
var tree = d3.layout.tree()
.size([viewerHeight, viewerWidth]);
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.y, d.x];
});
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.name.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
// Sort the tree initially incase the JSON isn't in a sorted order.
sortTree();
// define the baseSvg, attaching a class for styling and the zoomListener
var baseSvg = d3.select(container).append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("id",containertag + "svg")
// Append a group which holds all nodes and which the zoom Listener can act upon.
var svgGroup = baseSvg.append("g").attr("id",containertag + "g");
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
// Define the drag listeners for drag/drop behaviour of nodes.
dragListener = d3.behavior.drag()
.on("dragstart", function(d) {
if (d == root) {
return;
}
dragStarted = isDrag;
nodes = tree.nodes(d);
d3.event.sourceEvent.stopPropagation();
})
.on("drag", function(d) {
if (d == root) {
return;
}
if (dragStarted) {
domNode = this;
initiateDrag(d,domNode);
}
d.x0 += d3.event.dy;
d.y0 += d3.event.dx;
var node = d3.select(this);
node.attr("transform", "translate(" + d.y0 + "," + d.x0 + ")");
}).on("dragend", function(d) {
if (d == root) {
return;
}
domNode = this;
//transferir el nodo a otro
if (selectedNode) {
// now remove the element from the parent, and insert it into the new elements children
var index = draggingNode.parent.children.indexOf(draggingNode);
if (index > -1) {
draggingNode.parent.children.splice(index, 1);
}
if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') {
if (typeof selectedNode.children !== 'undefined') {
selectedNode.children.push(draggingNode);
} else {
selectedNode._children.push(draggingNode);
}
} else {
selectedNode.children = [];
selectedNode.children.push(draggingNode);
}
// Make sure that the node being added to is expanded so user can see added node is correctly moved
expand(selectedNode);
sortTree();
endDrag();
} else {
endDrag();
}
});
// Define the root
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = 0;
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
function update(source) {
svgGroup=d3.select(container+"svg g");
// Compute the new height, function counts total children of root node and sets tree height accordingly.
// This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
// This makes the layout more consistent.
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * anchuraNodos; // 25 pixels per line
tree = tree.size([newHeight, viewerWidth]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
d.y = (d.depth * (maxLabelLength * 5));
});
// Update the nodes…
node = svgGroup.selectAll(container + " g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter;
if (isDrag){
nodeEnter = node.enter().append("g").call(dragListener)
}
else{
nodeEnter = node.enter().append("g")
}
nodeEnter.attr("class", "node")
.attr("id",containertag + "-gnode")
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
nodeEnter.append("rect")
.attr("class","shadow")
.attr("id",containertag + "-rect")
.attr("width", 20)
.attr("height", 30)
.attr("rx", 2)
.attr("ry", 2)
.transition()
.duration(duration*2)
.attr("width", function (d) {return xScale(d.value);})
.style("fill", function(d){return d.level});
nodeEnter.append("text")
.attr("x", 0)
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.name;
})
.style("fill", "white");
// Update the text to reflect whether node has children or not.
node.select(container + ' text')
.attr("x", 0)
.attr("text-anchor","start")
.text(function(d) {
return d.name;
});
// Change the circle fill depending on whether it has children and is collapsed
node.select(container + " rect.shadow")
.attr("x",-10)
.attr("y",-15)
.attr("width", 20)
.attr("rx", 2)
.attr("ry", 2)
.attr("width", function (d) {return xScale(d.value);})
.style("fill", function(d){return d.level});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Fade the text in
nodeUpdate.select(container + " text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select(container +" rect")
.attr("width", 0)
.attr("height", 0)
.attr("rx", 0)
.attr("ry", 0);
nodeExit.select(container +" text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll(container + " path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Helper functions for collapsing and expanding nodes.
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function expand(d) {
if (d._children) {
d.children = d._children;
d.children.forEach(expand);
d._children = null;
}
}
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 4;
y = y * scale + viewerHeight / 2;
d3.select(container + ' g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
// Toggle children function
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
}
return d;
}
// Toggle children on click.
function click(d) {
if(d3.event.defaultPrevented) return;
d = toggleChildren(d);
update(d);
//centerNode(d);
}
function initiateDrag(d, domNode) {
container="#"+domNode.id.split("-")[0]
draggingNode = d;
d3.select(domNode).attr('class', 'node activeDrag');
svgGroup.selectAll(container + " g.node").sort(function(a, b) { // select the parent and sort the path's
if (a.id != draggingNode.id) return 1; // a is not the hovered element, send "a" to the back
else return -1; // a is the hovered element, bring "a" to the front
});
// if nodes has children, remove the links and nodes
if (nodes.length > 1) {
// remove link paths
links = tree.links(nodes);
nodePaths = svgGroup.selectAll(container + " path.link")
.data(links, function(d) {
return d.target.id;
}).remove();
// remove child nodes
nodesExit = svgGroup.selectAll(container + " g.node")
.data(nodes, function(d) {
return d.id;
}).filter(function(d, i) {
if (d.id == draggingNode.id) {
return false;
}
return true;
}).remove();
}
// remove parent link
parentLink = tree.links(tree.nodes(draggingNode.parent));
svgGroup.selectAll(container + ' path.link').filter(function(d, i) {
if (d.target.id == draggingNode.id) {
return true;
}
return false;
});//.remove();
dragStarted = null;
}
function endDrag() {
container="#"+domNode.id.split("-")[0]
selectedNode = null;
d3.select(domNode).attr('class', 'node');
// now restore the mouseover event or we won't be able to drag a 2nd time
if (draggingNode !== null) {
update(root);
//centerNode(draggingNode);
draggingNode = null;
}
}
// A recursive helper function for performing some setup by walking through all nodes
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
// sort the tree according to the node names
function sortTree() {
tree.sort(function(a, b) {
return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
});
}
// Define the zoom function for the zoomable tree
function zoom() {
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment