Last active
March 7, 2023 18:35
-
-
Save mph006/c8dcc39978d839fe14bc to your computer and use it in GitHub Desktop.
Org Chart (Flat CSV Load)
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
//The "hard stuff" (pan,zoom, drag, drop) comes from Mike Bostocks block #4063570 and is not my work | |
In this exercise you need to take an array of tabular data and generate a tree structure which will act as an org chart. | |
The only information you have in the tabular data is the associate name, and his/her manager's name. | |
The root of the tree will have an undefined value for manager. | |
The hierarchical representation of this data could be used in something like a D3 dendrogram to visualize your organizational structure. | |
** Edit as of June 28th 2016 this block is a lot less interesting | |
** D3 v4.0 introduced d3.stratify() which converts tabular data to hierarchial data in one clean function | |
** Check it out: https://github.com/d3/d3-hierarchy#stratify |
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
Associate | Manager | |
---|---|---|
Matt Herman | John Smith | |
Jane Doe | John Smith | |
Adam Brown | John Smith | |
Susan Jones | John Smith | |
Mike Harris | John Smith | |
John Smith | Colin Kraus | |
Colin Kraus | Ashley Carlin | |
Ashley Carlin | Lia McDermott | |
Evan Park | Lia McDermott | |
Lia McDermott |
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"> | |
<title>Basic Org Chart</title> | |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js" charset="utf-8"></script> | |
<link rel="stylesheet" type="text/css" href="./style.css"/> | |
<body> | |
<div id="tree-container"></div> | |
</body> | |
<script type="text/javascript" src="./tree.js"></script> | |
<!DOCTYPE html> | |
<meta charset="utf-8"> |
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
.node { | |
cursor: pointer; | |
} | |
.overlay{ | |
background-color:#EEE; | |
} | |
.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; | |
} | |
.ghostCircle.show{ | |
display:block; | |
} | |
.ghostCircle, .activeDrag .ghostCircle{ | |
display: none; | |
} |
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
//Cluster dendrogram pan, zoom, drag and drop functionality borrowed from #4063570 | |
function treeify(list, idAttr, parentAttr, childrenAttr) { | |
if (!idAttr) idAttr = 'id'; | |
if (!parentAttr) parentAttr = 'Manager'; | |
if (!childrenAttr) childrenAttr = 'children'; | |
var treeList = []; | |
var lookup = {}; | |
list.forEach(function(obj) { | |
lookup[obj["Associate"]] = obj; | |
obj[childrenAttr] = []; | |
}); | |
list.forEach(function(obj) { | |
if (obj[parentAttr] != undefined) { | |
lookup[obj[parentAttr]][childrenAttr].push(obj); | |
} | |
else { | |
treeList.push(obj); | |
} | |
}); | |
return treeList; | |
}; | |
d3.csv("data_org.csv", function(error, data) { | |
if (error) throw error; | |
console.log(data); | |
data.forEach(function(d){ | |
d.Associate = d.Associate.replace("@acme.com","").split(".").join(" "); | |
if(d.Manager !== undefined)d.Manager = d.Manager.replace("@acme.com","").split(".").join(" "); | |
}); | |
var root = treeify(data)[0]; | |
console.log(root); | |
// Calculate total nodes, max label length | |
var totalNodes = 0; | |
var maxLabelLength = 0; | |
// variables for drag/drop | |
var selectedNode = null; | |
var draggingNode = null; | |
// var vertical = truel | |
// panning variables | |
var panSpeed = 200; | |
var panBoundary = 20; // Within 20px from edges will pan when dragging. | |
// Misc. variables | |
var i = 0; | |
var duration = 750; | |
// size of the diagram | |
var viewerWidth = $(document).width(); | |
var viewerHeight = $(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]; | |
}); | |
// 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); | |
} | |
} | |
} | |
// Call visit function to establish maxLabelLength | |
visit(root, function(d) { | |
totalNodes++; | |
maxLabelLength = Math.max(d.Associate.length, maxLabelLength); | |
}, function(d) { | |
return d.children && d.children.length > 0 ? d.children : null; | |
}); | |
// sort the tree according to the node names | |
function sortTree() { | |
tree.sort(function(a, b) { | |
return b.Associate.toLowerCase() < a.Associate.toLowerCase() ? 1 : -1; | |
}); | |
} | |
// Sort the tree initially incase the JSON isn't in a sorted order. | |
sortTree(); | |
// TODO: Pan function, can be better implemented. | |
function pan(domNode, direction) { | |
var speed = panSpeed; | |
if (panTimer) { | |
clearTimeout(panTimer); | |
translateCoords = d3.transform(svgGroup.attr("transform")); | |
if (direction == 'left' || direction == 'right') { | |
translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed; | |
translateY = translateCoords.translate[1]; | |
} else if (direction == 'up' || direction == 'down') { | |
translateX = translateCoords.translate[0]; | |
translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed; | |
} | |
scaleX = translateCoords.scale[0]; | |
scaleY = translateCoords.scale[1]; | |
scale = zoomListener.scale(); | |
svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")"); | |
d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")"); | |
zoomListener.scale(zoomListener.scale()); | |
zoomListener.translate([translateX, translateY]); | |
panTimer = setTimeout(function() { | |
pan(domNode, speed, direction); | |
}, 50); | |
} | |
} | |
// Define the zoom function for the zoomable tree | |
function zoom() { | |
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); | |
} | |
// 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); | |
function initiateDrag(d, domNode) { | |
draggingNode = d; | |
d3.select(domNode).select('.ghostCircle').attr('pointer-events', 'none'); | |
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle show'); | |
d3.select(domNode).attr('class', 'node activeDrag'); | |
svgGroup.selectAll("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("path.link") | |
.data(links, function(d) { | |
return d.target.id; | |
}).remove(); | |
// remove child nodes | |
nodesExit = svgGroup.selectAll("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('path.link').filter(function(d, i) { | |
if (d.target.id == draggingNode.id) { | |
return true; | |
} | |
return false; | |
}).remove(); | |
dragStarted = null; | |
} | |
// define the baseSvg, attaching a class for styling and the zoomListener | |
var baseSvg = d3.select("#tree-container").append("svg") | |
.attr("width", viewerWidth) | |
.attr("height", viewerHeight) | |
.attr("class", "overlay") | |
.call(zoomListener); | |
// Define the drag listeners for drag/drop behaviour of nodes. | |
dragListener = d3.behavior.drag() | |
.on("dragstart", function(d) { | |
if (d == root) { | |
return; | |
} | |
dragStarted = true; | |
nodes = tree.nodes(d); | |
d3.event.sourceEvent.stopPropagation(); | |
// it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none'); | |
}) | |
.on("drag", function(d) { | |
if (d == root) { | |
return; | |
} | |
if (dragStarted) { | |
domNode = this; | |
initiateDrag(d, domNode); | |
} | |
// get coords of mouseEvent relative to svg container to allow for panning | |
relCoords = d3.mouse($('svg').get(0)); | |
if (relCoords[0] < panBoundary) { | |
panTimer = true; | |
pan(this, 'left'); | |
} else if (relCoords[0] > ($('svg').width() - panBoundary)) { | |
panTimer = true; | |
pan(this, 'right'); | |
} else if (relCoords[1] < panBoundary) { | |
panTimer = true; | |
pan(this, 'up'); | |
} else if (relCoords[1] > ($('svg').height() - panBoundary)) { | |
panTimer = true; | |
pan(this, 'down'); | |
} else { | |
try { | |
clearTimeout(panTimer); | |
} catch (e) { | |
} | |
} | |
d.x0 += d3.event.dy; | |
d.y0 += d3.event.dx; | |
var node = d3.select(this); | |
node.attr("transform", "translate(" + d.y0 + "," + d.x0 + ")"); | |
updateTempConnector(); | |
}).on("dragend", function(d) { | |
if (d == root) { | |
return; | |
} | |
domNode = this; | |
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(); | |
} | |
}); | |
function endDrag() { | |
selectedNode = null; | |
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle'); | |
d3.select(domNode).attr('class', 'node'); | |
// now restore the mouseover event or we won't be able to drag a 2nd time | |
d3.select(domNode).select('.ghostCircle').attr('pointer-events', ''); | |
updateTempConnector(); | |
if (draggingNode !== null) { | |
update(root); | |
centerNode(draggingNode); | |
draggingNode = null; | |
} | |
} | |
// 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; | |
} | |
} | |
var overCircle = function(d) { | |
selectedNode = d; | |
updateTempConnector(); | |
}; | |
var outCircle = function(d) { | |
selectedNode = null; | |
updateTempConnector(); | |
}; | |
// Function to update the temporary connector indicating dragging affiliation | |
var updateTempConnector = function() { | |
var data = []; | |
if (draggingNode !== null && selectedNode !== null) { | |
// have to flip the source coordinates since we did this for the existing connectors on the original tree | |
data = [{ | |
source: { | |
x: selectedNode.y0, | |
y: selectedNode.x0 | |
}, | |
target: { | |
x: draggingNode.y0, | |
y: draggingNode.x0 | |
} | |
}]; | |
} | |
var link = svgGroup.selectAll(".templink").data(data); | |
link.enter().append("path") | |
.attr("class", "templink") | |
.attr("d", d3.svg.diagonal()) | |
.attr('pointer-events', 'none'); | |
link.attr("d", d3.svg.diagonal()); | |
link.exit().remove(); | |
}; | |
// 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 / 2; | |
y = y * scale + viewerHeight / 2; | |
d3.select('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; // click suppressed | |
d = toggleChildren(d); | |
update(d); | |
centerNode(d); | |
} | |
function update(source) { | |
// 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) * 25; // 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 * 10)); //maxLabelLength * 10px | |
// alternatively to keep a fixed scale one can set a fixed depth per level | |
// Normalize for fixed-depth by commenting out below line | |
// d.y = (d.depth * 500); //500px per level. | |
}); | |
// Update the nodes… | |
node = svgGroup.selectAll("g.node") | |
.data(nodes, function(d) { | |
return d.id || (d.id = ++i); | |
}); | |
// Enter any new nodes at the parent's previous position. | |
var nodeEnter = node.enter().append("g") | |
.call(dragListener) | |
.attr("class", "node") | |
.attr("transform", function(d) { | |
return "translate(" + source.y0 + "," + source.x0 + ")"; | |
}) | |
.on('click', click); | |
nodeEnter.append("circle") | |
.attr('class', 'nodeCircle') | |
.attr("r", 0) | |
.style("fill", function(d) { | |
return d._children ? "lightsteelblue" : "#fff"; | |
}); | |
nodeEnter.append("text") | |
.attr("x", function(d) { | |
return d.children || d._children ? -10 : 10; | |
}) | |
.attr("dy", ".35em") | |
.attr('class', 'nodeText') | |
.attr("text-anchor", function(d) { | |
return d.children || d._children ? "end" : "start"; | |
}) | |
.text(function(d) { | |
return d.Associate; | |
}) | |
.style("fill-opacity", 0); | |
// phantom node to give us mouseover in a radius around it | |
nodeEnter.append("circle") | |
.attr('class', 'ghostCircle') | |
.attr("r", 30) | |
.attr("opacity", 0.2) // change this to zero to hide the target area | |
.style("fill", "red") | |
.attr('pointer-events', 'mouseover') | |
.on("mouseover", function(node) { | |
overCircle(node); | |
}) | |
.on("mouseout", function(node) { | |
outCircle(node); | |
}); | |
// Update the text to reflect whether node has children or not. | |
node.select('text') | |
.attr("x", function(d) { | |
return d.children || d._children ? -10 : 10; | |
}) | |
.attr("text-anchor", function(d) { | |
return d.children || d._children ? "end" : "start"; | |
}) | |
.text(function(d) { | |
return d.Associate; | |
}); | |
// Change the circle fill depending on whether it has children and is collapsed | |
node.select("circle.nodeCircle") | |
.attr("r", 4.5) | |
.style("fill", function(d) { | |
return d._children ? "lightsteelblue" : "#fff"; | |
}); | |
// 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("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("circle") | |
.attr("r", 0); | |
nodeExit.select("text") | |
.style("fill-opacity", 0); | |
// Update the links… | |
var link = svgGroup.selectAll("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; | |
}); | |
} | |
// Append a group which holds all nodes and which the zoom Listener can act upon. | |
var svgGroup = baseSvg.append("g"); | |
// Define the root | |
root = root; | |
root.x0 = viewerHeight / 2; | |
root.y0 = 0; | |
// Layout the tree initially and center on the root node. | |
update(root); | |
centerNode(root); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment