ESAT
Last active
November 21, 2016 05:44
-
-
Save tizon9804/8540d89cc896927d35030cad2508324d to your computer and use it in GitHub Desktop.
D3.js Drag and Drop, Zoomable, Panning, Collapsible Tree with auto-sizing.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Get JSON data | |
treeJSON = d3.json("flare.json", function(error, treeData) { | |
createTree(treeData,"#treecontainerDetalle",false); | |
createTree(treeData,"#treecontainer",true); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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" | |
} | |
] | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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