Skip to content

Instantly share code, notes, and snippets.

@gangesh
Last active January 26, 2020 16:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gangesh/15be9804f102e83a7de47f26f7878487 to your computer and use it in GitHub Desktop.
Save gangesh/15be9804f102e83a7de47f26f7878487 to your computer and use it in GitHub Desktop.
Employees Hierarchy Chart using d3.js
license: mit
<!DOCTYPE html>
<html lang="en" >
<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 - Redesigned - Company Employees Hierarchy Chart </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel='stylesheet prefetch' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.css'>
<link rel='stylesheet prefetch' href='https://fonts.googleapis.com/css?family=Roboto'>
<style>
.full-container {
background-color: red;
width: 100%;
height: 100%;
font-family: 'Roboto', sans-serif;
}
/* ######################## DEPARTMENT INFO ############################*/
.department-information {
font-family: 'Roboto', sans-serif;
display:none;
box-shadow: 0 0 5px #999999;
position: absolute;
max-width: 200px;
top: 60px;
left: 20px;
padding: 10px;
background-color: white;
}
.department-information .dept-name {
color: #26a69a;
font-weight: bold;
}
.department-information .dept-description {
margin-top: 10px;
color: #959b9a;
font-size: 13px;
}
.department-information .dept-emp-count {
margin-top: 10px;
color: #959b9a;
font-size: 13px;
}
/* ############################## SEARCHBOX ######################################### */
.user-search-box {
overflow: hidden;
position: absolute;
right: 0;
height: 100%;
top: 0;
width: 0;
background-color: white;
border: 1px solid #c7dddb;
font-family: 'Roboto', sans-serif;
font-size: 14px;
line-height: 1.5;
}
::-webkit-input-placeholder {
/* WebKit, Blink, Edge */
color: #bcbcc4;
opacity: 0.5;
}
:-moz-placeholder {
/* Mozilla Firefox 4 to 18 */
color: #bcbcc4;
opacity: 0.5;
}
::-moz-placeholder {
/* Mozilla Firefox 19+ */
color: #bcbcc4;
opacity: 0.5;
}
:-ms-input-placeholder {
/* Internet Explorer 10-11 */
color: #bcbcc4;
opacity: 0.5;
}
.user-search-box .input-box {
width: 100%;
height: 200px;
top: 0;
background-color: #e8efee;
}
.user-search-box .close-button-wrapper i {
margin: 10px;
margin-left: 9%;
font-size: 60px;
font-weight: 400;
color: #aa1414;
}
.user-search-box input {
color: gray !important;
background-color: transparent;
border: none;
border-bottom: 1px solid #9e9e9e;
border-radius: 0;
outline: none;
height: 3rem;
width: 100%;
font-size: 1rem;
margin: 0 0 20px 0;
padding: 0;
box-shadow: none;
box-sizing: content-box;
transition: all 0.3s;
}
.user-search-box input:focus {
border-bottom: 1px solid #26a69a;
box-shadow: 0 1px 0 0 #26a69a;
}
.user-search-box .result-header {
background-color: white;
font-weight: 700;
padding: 12px;
color: gray;
border-top: 2px solid #d3e8e5;
border-bottom: 1px solid #d3e8e5;
}
.user-search-box .result-list {
position: absolute;
max-height: 100%;
min-width: 100%;
overflow: auto;
}
.user-search-box .buffer {
width: 100%;
height: 400px;
}
.user-search-box .list-item {
clear: both;
background-color: white;
position: relative;
background-color: white;
width: 100%;
height: 100px;
border-top: 1px solid #d3e8e5;
}
.user-search-box .list-item a {
display: inline;
margin: 0;
}
.user-search-box .list-item .image-wrapper {
float: left;
width: 100px;
height: 100px;
}
.user-search-box .list-item .image {
width: 70px;
height: 70px;
margin-left: 15px;
margin-top: 15px;
border-radius: 5px;
}
.user-search-box .list-item .description {
padding: 15px;
padding-left: 0px;
float: left;
width: 180px;
}
.user-search-box .list-item .buttons {
padding: 15px;
padding-left: 0px;
float: left;
width: auto;
}
.user-search-box .list-item .description .name {
font-size: 15px;
color: #aa1414;
font-weight: 900;
margin: 0;
padding: 0;
letter-spacing: 1px;
}
.user-search-box .list-item .description .position-name {
color: #59525b;
letter-spacing: 1px;
font-size: 12px;
font-weight: 900;
margin: 0;
margin-top: 3px;
padding: 0;
}
.user-search-box .list-item .description .area {
color: #91a4a5;
letter-spacing: 1px;
font-size: 12px;
font-weight: 400;
margin: 0;
margin-top: 3px;
padding: 0;
}
.user-search-box .list-item .btn-locate{
margin-top:30px;
}
.user-search-box .list-item .btn-search-box{
font-size:10px;
}
.user-search-box .close-button-wrapper i:hover {
color: black;
cursor: pointer;
}
.user-search-box .input-wrapper {
width: 80%;
margin: 0 auto;
}
.user-search-box .input-bottom-placeholder {
margin-top: -16px;
color: #bcbcc4;
letter-spacing: 1px;
}
/* ############################### Tooltip css ########################### */
.profile-image-wrapper {
background-size: 210px;
margin: 30px;
border-radius: 50%;
width: 210px;
height: 210px;
}
.customTooltip-wrapper {
font-family: 'Roboto', sans-serif;
opacity: 0;
/* NEW */
display: none;
position: absolute;
}
.customTooltip {
background: white;
box-shadow: 0 0 5px #999999;
color: #333;
position: absolute;
font-size: 12px;
left: 130px;
text-align: center;
top: 95px;
z-index: 10;
text-align: left;
}
.tooltip-hr {
width: 70px;
background-color: #91a4a5;
height: 1px;
margin-left: auto;
margin-right: auto;
margin-top: -17px;
margin-bottom: 25px;
}
.tooltip-desc {
padding-left: 10px;
margin-top: -20px;
margin-left: 20px;
overflow: auto;
}
.tooltip-desc .name {
color: #962828;
font-weight: 900;
letter-spacing: 1px;
font-size: 24px;
font-weight: bold;
margin-bottom: 2px;
text-decoration: none;
}
.tooltip-desc .name:hover {
text-decoration: underline;
}
.tooltip-desc .position {
color: #59525b;
letter-spacing: 1px;
font-size: 17px;
font-weight: 500;
margin-bottom: 2px;
margin-top: 0px;
}
.tooltip-desc .area {
color: #91a4a5;
letter-spacing: 1px;
font-size: 16px;
font-weight: 400;
margin-bottom: 2px;
margin-top: 7px;
}
.tooltip-desc .office {
color: #91a4a5;
line-height: 160%;
font-size: 14px;
font-weight: 400;
margin-bottom: -10px;
margin-top: -5px;
}
.tooltip-desc .tags-wrapper .title {
display: inline-block;
float: left;
}
.tooltip-desc .tags-wrapper .tags {
display: inline-block;
float: left;
}
.bottom-tooltip-hr {
width: 100%;
background-color: #58993e;
height: 3px;
margin-left: auto;
margin-right: auto;
margin-top: -17px;
}
.btn-tooltip-department {
margin-top: 20px;
}
.btn.disabled {
background-color: #DFDFDF !important;
box-shadow: none;
color: #9F9F9F !important;
cursor: default;
}
.btn {
border: none;
border-radius: 2px;
height: 36px;
line-height: 36px;
outline: 0;
text-transform: uppercase;
vertical-align: middle;
-webkit-tap-highlight-color: transparent;
text-decoration: none;
color: #fff;
background-color: #26a69a;
text-align: center;
letter-spacing: .5px;
transition: .2s ease-out;
cursor: pointer;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}
.btn:hover {
box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
}
.btn.disabled:hover {
box-shadow: none;
}
/* ####################################### TAGS ###################################### */
.tags {
list-style: none;
margin-top: -9px;
margin-left: 5px;
overflow: hidden;
padding: 0;
}
.tags-wrapper {
font-size: 2.28rem;
line-height: 110%;
margin: 1.14rem 0 0.912rem 0;
}
.tags-wrapper .title {
color: #91a4a5;
font-size: 24px;
}
.tags li {
float: left;
}
.tag {
font-size: 11px;
background: #E1ECF4;
border-radius: 2px;
color: ##39739d;
display: inline-block;
height: 20px;
line-height: 20px;
padding: 0 5px 0 5px;
position: relative;
margin: 0 5px 5px 0;
text-decoration: none;
-webkit-transition: color 0.2s;
}
/* ############################# Buttons ############################################*/
.btn-search {
top: 80px;
}
.btn-fullscreen {
top: 20px;
}
.btn-back {
top: 20px;
left: 20px;
display: none;
}
.btn-show-my-self {
top: 50px;
}
.btn-action {
position: absolute;
right: 25px;
height: 26px;
color: white;
background-color: #aa1414;
border: 1px solid black;
border-radius: 12px;
cursor: pointer;
font-size: 15px;
font-family: 'Roboto', sans-serif;
}
.btn-action:focus {
outline: 0;
background-color: #aa1414;
}
.btn-action:hover {
background-color: #490b0b;
}
.btn-action i {
font-size: 14px;
}
.btn-action .icon {
background-color: #c19e45;
padding: 5px 6px 5px 6px;
border-radius: 11px;
margin-right: -7px;
}
/* ############################################## SVG ################################# */
.nodeHasChildren {
fill: white;
}
.nodeDoesNotHaveChildren {
fill: white;
}
.nodeRepresentsCurrentUser {
stroke: Chartreuse;
stroke-width: 3;
}
text {
fill: dimgray;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.node {
cursor: pointer;
}
.node-collapse {
stroke: grey;
}
.node-collapse-right-rect {
fill: #70c645;
stroke: #70c645;
}
.node text {
fill: white;
font-family: "Segoe UI", Arial, sans-serif;
font-size: 10px;
}
.node circle {
stroke-width: 1px;
stroke: #70c645;
fill: #70c645;
}
.node-group .emp-name {
fill: #962828;
font-size: 12px;
font-weight: 600
}
.node-group .emp-position-name {
fill: #59525b;
font-size: 11px;
}
.node-group .emp-area {
fill: #91a4a5;
font-size: 10px;
}
.node-group .emp-count,
.node-group .emp-count-icon {
fill: #91a4a5;
font-size: 12px;
}
</style>
</head>
<body translate="no" >
<div id="full-container">
<button class="btn-action btn-fullscreen" onclick="params.funcs.toggleFullScreen()">Fullscreen <span class='icon'/> <i class="fa fa-arrows-alt" aria-hidden="true"></i></span></button>
<button class="btn-action btn-show-my-self" onclick="params.funcs.showMySelf()"> Show myself <span class='icon'/> <i class="fa fa-user" aria-hidden="true"></i></span></button>
<button class=" btn-action btn-search" onclick="params.funcs.search()"> Search <span class='icon'/> <i class="fa fa-search" aria-hidden="true"></i></span></button>
<button class=" btn-action btn-back" onclick="params.funcs.back()"> Back <span class='icon'/> <i class="fa fa-arrow-left" aria-hidden="true"></i></span></button>
<div class="department-information">
<div class="dept-name">
dept name
</div>
<div class="dept-emp-count">
dept description test, this is department description
</div>
<div class="dept-description">
dept description test, this is department description
</div>
</div>
<div class="user-search-box">
<div class="input-box">
<div class="close-button-wrapper"><i onclick="params.funcs.closeSearchBox()" class="fa fa-times" aria-hidden="true"></i></div>
<div class="input-wrapper">
<input type="text" class="search-input" placeholder="Search" />
<div class="input-bottom-placeholder">By Firstname, Lastname, Tags</div>
</div>
<div>
</div>
</div>
<div class="result-box">
<div class="result-header"> RESULTS </div>
<div class="result-list">
<div class="buffer"></div>
</div>
</div>
</div>
<div id="svgChart"></div>
<!--
<button class="btn btn-expand" onclick="params.funcs.expandAll()">Expand All</button>
-->
</div>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js'></script>
<script >
var params = {
selector: "#svgChart",
dataLoadUrl: "https://gist.githubusercontent.com/gangesh/322dc627682e983304ea5a0861979515/raw/88376b759cf4c3195fd01c2444ffd7711f5c0d11/gistfile1.txt",
chartWidth: window.innerWidth-40,
chartHeight: window.innerHeight - 40,
funcs: {
showMySelf: null,
search: null,
closeSearchBox: null,
clearResult: null,
findInTree: null,
reflectResults: null,
departmentClick: null,
back: null,
toggleFullScreen: null,
locate:null
},
data: null
}
d3.json(params.dataLoadUrl, function(data) {
params.data = data;
params.pristinaData = JSON.parse(JSON.stringify(data));
drawOrganizationChart(params);
})
function drawOrganizationChart(params) {
listen();
params.funcs.showMySelf = showMySelf;
params.funcs.expandAll = expandAll;
params.funcs.search = searchUsers;
params.funcs.closeSearchBox = closeSearchBox;
params.funcs.findInTree = findInTree;
params.funcs.clearResult = clearResult;
params.funcs.reflectResults = reflectResults;
params.funcs.departmentClick = departmentClick;
params.funcs.back = back;
params.funcs.toggleFullScreen = toggleFullScreen;
params.funcs.locate=locate;
var attrs = {
EXPAND_SYMBOL: '\uf067',
COLLAPSE_SYMBOL: '\uf068',
selector: params.selector,
root: params.data,
width: params.chartWidth,
height: params.chartHeight,
index: 0,
nodePadding: 9,
collapseCircleRadius: 7,
nodeHeight: 80,
nodeWidth: 210,
duration: 750,
rootNodeTopMargin: 20,
minMaxZoomProportions: [0.05, 3],
linkLineSize: 180,
collapsibleFontSize: '10px',
userIcon: '\uf007',
nodeStroke: "#ccc",
nodeStrokeWidth: '1px'
}
var dynamic = {}
dynamic.nodeImageWidth = attrs.nodeHeight * 100 / 140;
dynamic.nodeImageHeight = attrs.nodeHeight - 2 * attrs.nodePadding;
dynamic.nodeTextLeftMargin = attrs.nodePadding * 2 + dynamic.nodeImageWidth
dynamic.rootNodeLeftMargin = attrs.width / 2;
dynamic.nodePositionNameTopMargin = attrs.nodePadding + 8 + dynamic.nodeImageHeight / 4 * 1
dynamic.nodeChildCountTopMargin = attrs.nodePadding + 14 + dynamic.nodeImageHeight / 4 * 3
var tree = d3.layout.tree().nodeSize([attrs.nodeWidth + 40, attrs.nodeHeight]);
var diagonal = d3.svg.diagonal()
.projection(function(d) {
debugger;
return [d.x + attrs.nodeWidth / 2, d.y + attrs.nodeHeight / 2];
});
var zoomBehaviours = d3.behavior
.zoom()
.scaleExtent(attrs.minMaxZoomProportions)
.on("zoom", redraw);
var svg = d3.select(attrs.selector)
.append("svg")
.attr("width", attrs.width)
.attr("height", attrs.height)
.call(zoomBehaviours)
.append("g")
.attr("transform", "translate(" + attrs.width / 2 + "," + 20 + ")");
//necessary so that zoom knows where to zoom and unzoom from
zoomBehaviours.translate([dynamic.rootNodeLeftMargin, attrs.rootNodeTopMargin]);
attrs.root.x0 = 0;
attrs.root.y0 = dynamic.rootNodeLeftMargin;
if (params.mode != 'department') {
// adding unique values to each node recursively
var uniq = 1;
addPropertyRecursive('uniqueIdentifier', function(v) {
return uniq++;
}, attrs.root);
}
expand(attrs.root);
if (attrs.root.children) {
attrs.root.children.forEach(collapse);
}
update(attrs.root);
d3.select(attrs.selector).style("height", attrs.height);
var tooltip = d3.select('body')
.append('div')
.attr('class', 'customTooltip-wrapper');
function update(source, param) {
// Compute the new tree layout.
var nodes = tree.nodes(attrs.root)
.reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * attrs.linkLineSize;
});
// Update the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++attrs.index);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
})
var nodeGroup = nodeEnter.append("g")
.attr("class", "node-group")
nodeGroup.append("rect")
.attr("width", attrs.nodeWidth)
.attr("height", attrs.nodeHeight)
.attr("data-node-group-id",function(d){
return d.uniqueIdentifier;
})
.attr("class", function(d) {
var res = "";
if (d.isLoggedUser) res += 'nodeRepresentsCurrentUser ';
res += d._children || d.children ? "nodeHasChildren" : "nodeDoesNotHaveChildren";
return res;
});
var collapsiblesWrapper =
nodeEnter.append('g')
.attr('data-id', function(v) {
return v.uniqueIdentifier;
});
var collapsibleRects = collapsiblesWrapper.append("rect")
.attr('class', 'node-collapse-right-rect')
.attr('height', attrs.collapseCircleRadius)
.attr('fill', 'black')
.attr('x', attrs.nodeWidth - attrs.collapseCircleRadius)
.attr('y', attrs.nodeHeight - 7)
.attr("width", function(d) {
if (d.children || d._children) return attrs.collapseCircleRadius;
return 0;
})
var collapsibles =
collapsiblesWrapper.append("circle")
.attr('class', 'node-collapse')
.attr('cx', attrs.nodeWidth - attrs.collapseCircleRadius)
.attr('cy', attrs.nodeHeight - 7)
.attr("", setCollapsibleSymbolProperty);
//hide collapse rect when node does not have children
collapsibles.attr("r", function(d) {
if (d.children || d._children) return attrs.collapseCircleRadius;
return 0;
})
.attr("height", attrs.collapseCircleRadius)
collapsiblesWrapper.append("text")
.attr('class', 'text-collapse')
.attr("x", attrs.nodeWidth - attrs.collapseCircleRadius)
.attr('y', attrs.nodeHeight - 3)
.attr('width', attrs.collapseCircleRadius)
.attr('height', attrs.collapseCircleRadius)
.style('font-size', attrs.collapsibleFontSize)
.attr("text-anchor", "middle")
.style('font-family', 'FontAwesome')
.text(function(d) {
return d.collapseText;
})
collapsiblesWrapper.on("click", click);
nodeGroup.append("text")
.attr("x", dynamic.nodeTextLeftMargin)
.attr("y", attrs.nodePadding + 10)
.attr('class', 'emp-name')
.attr("text-anchor", "left")
.text(function(d) {
return d.name.trim();
})
.call(wrap, attrs.nodeWidth);
nodeGroup.append("text")
.attr("x", dynamic.nodeTextLeftMargin)
.attr("y", dynamic.nodePositionNameTopMargin)
.attr('class', 'emp-position-name')
.attr("dy", ".35em")
.attr("text-anchor", "left")
.text(function(d) {
var position = d.positionName.substring(0,27);
if(position.length<d.positionName.length){
position = position.substring(0,24)+'...'
}
return position;
})
nodeGroup.append("text")
.attr("x", dynamic.nodeTextLeftMargin)
.attr("y", attrs.nodePadding + 10 + dynamic.nodeImageHeight / 4 * 2)
.attr('class', 'emp-area')
.attr("dy", ".35em")
.attr("text-anchor", "left")
.text(function(d) {
return d.area;
})
nodeGroup.append("text")
.attr("x", dynamic.nodeTextLeftMargin)
.attr("y", dynamic.nodeChildCountTopMargin)
.attr('class', 'emp-count-icon')
.attr("text-anchor", "left")
.style('font-family', 'FontAwesome')
.text(function(d) {
if (d.children || d._children) return attrs.userIcon;
});
nodeGroup.append("text")
.attr("x", dynamic.nodeTextLeftMargin + 13)
.attr("y", dynamic.nodeChildCountTopMargin)
.attr('class', 'emp-count')
.attr("text-anchor", "left")
.text(function(d) {
if (d.children) return d.children.length;
if (d._children) return d._children.length;
return;
})
nodeGroup.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("id", "clip-rect")
.attr("rx", 3)
.attr('x', attrs.nodePadding)
.attr('y', 2 + attrs.nodePadding)
.attr('width', dynamic.nodeImageWidth)
.attr('fill', 'none')
.attr('height', dynamic.nodeImageHeight - 4)
nodeGroup.append("svg:image")
.attr('y', 2 + attrs.nodePadding)
.attr('x', attrs.nodePadding)
.attr('preserveAspectRatio', 'none')
.attr('width', dynamic.nodeImageWidth)
.attr('height', dynamic.nodeImageHeight - 4)
.attr('clip-path', "url(#clip)")
.attr("xlink:href", function(v) {
return v.imageUrl;
})
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(attrs.duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
//todo replace with attrs object
nodeUpdate.select("rect")
.attr("width", attrs.nodeWidth)
.attr("height", attrs.nodeHeight)
.attr('rx', 3)
.attr("stroke", function(d){
if(param && d.uniqueIdentifier== param.locate){
return '#a1ceed'
}
return attrs.nodeStroke;
})
.attr('stroke-width', function(d){
if(param && d.uniqueIdentifier== param.locate){
return 6;
}
return attrs.nodeStrokeWidth})
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(attrs.duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
nodeExit.select("rect")
.attr("width", attrs.nodeWidth)
.attr("height", attrs.nodeHeight)
// Update the links…
var link = svg.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("x", attrs.nodeWidth / 2)
.attr("y", attrs.nodeHeight / 2)
.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(attrs.duration)
.attr("d", diagonal)
;
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(attrs.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;
});
if(param && param.locate){
var x;
var y;
nodes.forEach(function(d) {
if (d.uniqueIdentifier == param.locate) {
x = d.x;
y = d.y;
}
});
// normalize for width/height
var new_x = (-x + (window.innerWidth / 2));
var new_y = (-y + (window.innerHeight / 2));
// move the main container g
svg.attr("transform", "translate(" + new_x + "," + new_y + ")")
zoomBehaviours.translate([new_x, new_y]);
zoomBehaviours.scale(1);
}
if (param && param.centerMySelf) {
var x;
var y;
nodes.forEach(function(d) {
if (d.isLoggedUser) {
x = d.x;
y = d.y;
}
});
// normalize for width/height
var new_x = (-x + (window.innerWidth / 2));
var new_y = (-y + (window.innerHeight / 2));
// move the main container g
svg.attr("transform", "translate(" + new_x + "," + new_y + ")")
zoomBehaviours.translate([new_x, new_y]);
zoomBehaviours.scale(1);
}
/*################ TOOLTIP #############################*/
function getTagsFromCommaSeparatedStrings(tags) {
return tags.split(',').map(function(v) {
return '<li><div class="tag">' + v + '</div></li> '
}).join('');
}
function tooltipContent(item) {
var strVar = "";
strVar += " <div class=\"customTooltip\">";
strVar += " <!--";
strVar += " <div class=\"tooltip-image-wrapper\"> <img width=\"300\" src=\"https:\/\/raw.githubusercontent.com\/bumbeishvili\/Assets\/master\/Projects\/D3\/Organization%20Chart\/cto.jpg\"> <\/div>";
strVar += "-->";
strVar += " <div class=\"profile-image-wrapper\" style='background-image: url(" + item.imageUrl + ")'>";
strVar += " <\/div>";
strVar += " <div class=\"tooltip-hr\"><\/div>";
strVar += " <div class=\"tooltip-desc\">";
strVar += " <a class=\"name\" href='" + item.profileUrl + "' target=\"_blank\"> " + item.name + "<\/a>";
strVar += " <p class=\"position\">" + item.positionName + " <\/p>";
strVar += " <p class=\"area\">" + item.area + " <\/p>";
strVar += "";
strVar += " <p class=\"office\">" + item.office + "<\/p>";
strVar += " <button class='" + (item.unit.type == 'business' ? " disabled " : "") + " btn btn-tooltip-department' onclick='params.funcs.departmentClick(" + JSON.stringify(item.unit) + ")'>" + item.unit.value + "</button>";
strVar += " <h4 class=\"tags-wrapper\"> <span class=\"title\"><i class=\"fa fa-tags\" aria-hidden=\"true\"><\/i>";
strVar += " ";
strVar += " <\/span> <ul class=\"tags\">" + getTagsFromCommaSeparatedStrings(item.tags) + "<\/ul> <\/h4> <\/div>";
strVar += " <div class=\"bottom-tooltip-hr\"><\/div>";
strVar += " <\/div>";
strVar += "";
return strVar;
}
function tooltipHoverHandler(d) {
var content = tooltipContent(d);
tooltip.html(content);
tooltip.transition()
.duration(200).style("opacity", "1").style('display', 'block');
d3.select(this).attr('cursor', 'pointer').attr("stroke-width", 50);
var y = d3.event.pageY;
var x = d3.event.pageX;
//restrict tooltip to fit in borders
if (y < 220) {
y += 220 - y;
x += 130;
}
if(y>attrs.height-300){
y-=300-(attrs.height-y);
}
tooltip.style('top', (y - 300) + 'px')
.style('left', (x - 470) + 'px');
}
function tooltipOutHandler() {
tooltip.transition()
.duration(200)
.style('opacity', '0').style('display', 'none');
d3.select(this).attr("stroke-width", 5);
}
nodeGroup.on('click', tooltipHoverHandler);
nodeGroup.on('dblclick', tooltipOutHandler);
function equalToEventTarget() {
return this == d3.event.target;
}
d3.select("body").on("click", function() {
var outside = tooltip.filter(equalToEventTarget).empty();
if (outside) {
tooltip.style('opacity', '0').style('display', 'none');
}
});
}
// Toggle children on click.
function click(d) {
d3.select(this).select("text").text(function(dv) {
if (dv.collapseText == attrs.EXPAND_SYMBOL) {
dv.collapseText = attrs.COLLAPSE_SYMBOL
} else {
if (dv.children) {
dv.collapseText = attrs.EXPAND_SYMBOL
}
}
return dv.collapseText;
})
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
//########################################################
//Redraw for zoom
function redraw() {
//console.log("here", d3.event.translate, d3.event.scale);
svg.attr("transform",
"translate(" + d3.event.translate + ")" +
" scale(" + d3.event.scale + ")");
}
// ############################# Function Area #######################
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
x = text.attr("x"),
y = text.attr("y"),
dy = 0, //parseFloat(text.attr("dy")),
tspan = text.text(null)
.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", ++lineNumber * lineHeight + dy + "em")
.text(word);
}
}
});
}
function addPropertyRecursive(propertyName, propertyValueFunction, element) {
if (element[propertyName]) {
element[propertyName] = element[propertyName] + ' ' + propertyValueFunction(element);
} else {
element[propertyName] = propertyValueFunction(element);
}
if (element.children) {
element.children.forEach(function(v) {
addPropertyRecursive(propertyName, propertyValueFunction, v)
})
}
if (element._children) {
element._children.forEach(function(v) {
addPropertyRecursive(propertyName, propertyValueFunction, v)
})
}
}
function departmentClick(item) {
hide(['.customTooltip-wrapper']);
if (item.type == 'department' && params.mode != 'department') {
//find third level department head user
var found = false;
var secondLevelChildren = params.pristinaData.children;
parentLoop:
for (var i = 0; i < secondLevelChildren.length; i++) {
var secondLevelChild = secondLevelChildren[i];
var thirdLevelChildren = secondLevelChild.children ? secondLevelChild.children : secondLevelChild._children;
for (var j = 0; j < thirdLevelChildren.length; j++) {
var thirdLevelChild = thirdLevelChildren[j];
if (thirdLevelChild.unit.value.trim() == item.value.trim()) {
clear(params.selector);
hide(['.btn-action']);
show(['.btn-action.btn-back', '.btn-action.btn-fullscreen', '.department-information']);
set('.dept-name', item.value);
set('.dept-emp-count', "Employees Quantity - " + getEmployeesCount(thirdLevelChild));
set('.dept-description', thirdLevelChild.unit.desc);
params.oldData = params.data;
params.data = deepClone(thirdLevelChild);
found = true;
break parentLoop;
}
}
}
if (found) {
params.mode = "department";
params.funcs.closeSearchBox();
drawOrganizationChart(params);
}
}
}
function getEmployeesCount(node) {
var count = 1;
countChilds(node);
return count;
function countChilds(node) {
var childs = node.children ? node.children : node._children;
if (childs) {
childs.forEach(function(v) {
count++;
countChilds(v);
})
}
}
}
function reflectResults(results) {
var htmlStringArray = results.map(function(result) {
var strVar = "";
strVar += " <div class=\"list-item\">";
strVar += " <a >";
strVar += " <div class=\"image-wrapper\">";
strVar += " <img class=\"image\" src=\"" + result.imageUrl + "\"\/>";
strVar += " <\/div>";
strVar += " <div class=\"description\">";
strVar += " <p class=\"name\">" + result.name + "<\/p>";
strVar += " <p class=\"position-name\">" + result.positionName + "<\/p>";
strVar += " <p class=\"area\">" + result.area + "<\/p>";
strVar += " <\/div>";
strVar += " <div class=\"buttons\">";
strVar += " <a target='_blank' href='"+result.profileUrl+"'><button class='btn-search-box btn-action'>View Profile<\/button><\/a>";
strVar += " <button class='btn-search-box btn-action btn-locate' onclick='params.funcs.locate("+result.uniqueIdentifier+")'>Locate <\/button>";
strVar += " <\/div>";
strVar += " <\/a>";
strVar += " <\/div>";
return strVar;
})
var htmlString = htmlStringArray.join('');
params.funcs.clearResult();
var parentElement = get('.result-list');
var old = parentElement.innerHTML;
var newElement = htmlString + old;
parentElement.innerHTML = newElement;
set('.user-search-box .result-header', "RESULT - " + htmlStringArray.length);
}
function clearResult() {
set('.result-list', '<div class="buffer" ></div>');
set('.user-search-box .result-header', "RESULT");
}
function listen() {
var input = get('.user-search-box .search-input');
input.addEventListener('input', function() {
var value = input.value ? input.value.trim() : '';
if (value.length < 3) {
params.funcs.clearResult();
} else {
var searchResult = params.funcs.findInTree(params.data, value);
params.funcs.reflectResults(searchResult);
}
});
}
function searchUsers() {
d3.selectAll('.user-search-box')
.transition()
.duration(250)
.style('width', '350px')
}
function closeSearchBox() {
d3.selectAll('.user-search-box')
.transition()
.duration(250)
.style('width', '0px')
.each("end", function() {
params.funcs.clearResult();
clear('.search-input');
});
}
function findInTree(rootElement, searchText) {
var result = [];
// use regex to achieve case insensitive search and avoid string creation using toLowerCase method
var regexSearchWord = new RegExp(searchText, "i");
recursivelyFindIn(rootElement, searchText);
return result;
function recursivelyFindIn(user) {
if (user.name.match(regexSearchWord) ||
user.tags.match(regexSearchWord)) {
result.push(user)
}
var childUsers = user.children ? user.children : user._children;
if (childUsers) {
childUsers.forEach(function(childUser) {
recursivelyFindIn(childUser, searchText)
})
}
};
}
function back() {
show(['.btn-action']);
hide(['.customTooltip-wrapper', '.btn-action.btn-back', '.department-information'])
clear(params.selector);
params.mode = "full";
params.data = deepClone(params.pristinaData)
drawOrganizationChart(params);
}
function expandAll() {
expand(root);
update(root);
}
function expand(d) {
if (d.children) {
d.children.forEach(expand);
}
if (d._children) {
d.children = d._children;
d.children.forEach(expand);
d._children = null;
}
if (d.children) {
// if node has children and it's expanded, then display -
setToggleSymbol(d, attrs.COLLAPSE_SYMBOL);
}
}
function collapse(d) {
if (d._children) {
d._children.forEach(collapse);
}
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
if (d._children) {
// if node has children and it's collapsed, then display +
setToggleSymbol(d, attrs.EXPAND_SYMBOL);
}
}
function setCollapsibleSymbolProperty(d) {
if (d._children) {
d.collapseText = attrs.EXPAND_SYMBOL;
} else if (d.children) {
d.collapseText = attrs.COLLAPSE_SYMBOL;
}
}
function setToggleSymbol(d, symbol) {
d.collapseText = symbol;
d3.select("*[data-id='" + d.uniqueIdentifier + "']").select('text').text(symbol);
}
/* recursively find logged user in subtree */
function findmySelf(d) {
if (d.isLoggedUser) {
expandParents(d);
} else if (d._children) {
d._children.forEach(function(ch) {
ch.parent = d;
findmySelf(ch);
})
} else if (d.children) {
d.children.forEach(function(ch) {
ch.parent = d;
findmySelf(ch);
});
};
}
function locateRecursive(d,id) {
if (d.uniqueIdentifier == id) {
expandParents(d);
} else if (d._children) {
d._children.forEach(function(ch) {
ch.parent = d;
locateRecursive(ch,id);
})
} else if (d.children) {
d.children.forEach(function(ch) {
ch.parent = d;
locateRecursive(ch,id);
});
};
}
/* expand current nodes collapsed parents */
function expandParents(d) {
while (d.parent) {
d = d.parent;
if (!d.children) {
d.children = d._children;
d._children = null;
setToggleSymbol(d, attrs.COLLAPSE_SYMBOL);
}
}
}
function toggleFullScreen() {
if ((document.fullScreenElement && document.fullScreenElement !== null) ||
(!document.mozFullScreen && !document.webkitIsFullScreen)) {
if (document.documentElement.requestFullScreen) {
document.documentElement.requestFullScreen();
} else if (document.documentElement.mozRequestFullScreen) {
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullScreen) {
document.documentElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
}
d3.select(params.selector + ' svg').attr('width', screen.width).attr('height', screen.height);
} else {
if (document.cancelFullScreen) {
document.cancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
}
d3.select(params.selector + ' svg').attr('width', params.chartWidth).attr('height', params.chartHeight);
}
}
function showMySelf() {
/* collapse all and expand logged user nodes */
if (!attrs.root.children) {
if (!attrs.root.isLoggedUser) {
attrs.root.children = attrs.root._children;
}
}
if (attrs.root.children) {
attrs.root.children.forEach(collapse);
attrs.root.children.forEach(findmySelf);
}
update(attrs.root, {centerMySelf:true});
}
//locateRecursive
function locate(id){
/* collapse all and expand logged user nodes */
if (!attrs.root.children) {
if (!attrs.root.uniqueIdentifier == id) {
attrs.root.children = attrs.root._children;
}
}
if (attrs.root.children) {
attrs.root.children.forEach(collapse);
attrs.root.children.forEach(function(ch){
locateRecursive(ch,id)
});
}
update(attrs.root, {locate:id});
}
function deepClone(item) {
return JSON.parse(JSON.stringify(item));
}
function show(selectors) {
display(selectors, 'initial')
}
function hide(selectors) {
display(selectors, 'none')
}
function display(selectors, displayProp) {
selectors.forEach(function(selector) {
var elements = getAll(selector);
elements.forEach(function(element) {
element.style.display = displayProp;
})
});
}
function set(selector, value) {
var elements = getAll(selector);
elements.forEach(function(element) {
element.innerHTML = value;
element.value = value;
})
}
function clear(selector) {
set(selector, '');
}
function get(selector) {
return document.querySelector(selector);
}
function getAll(selector) {
return document.querySelectorAll(selector);
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment