Skip to content

Instantly share code, notes, and snippets.

@bumbeishvili
Last active September 22, 2022 12:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save bumbeishvili/dbc0beff4baf64674b0f05b94cb4462e to your computer and use it in GitHub Desktop.
Save bumbeishvili/dbc0beff4baf64674b0f05b94cb4462e 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://raw.githubusercontent.com/bumbeishvili/Assets/master/Projects/D3/Organization%20Chart/redesignedChartLongData.json",
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>
@JoDeeBarKnee
Copy link

Great example. I need help to get it to work for my project.

Can you inform me on how to modify the code to use my own JSON data within the code instead of the URL method?
var data = {My JSON Object}
instead of

d3.json(params.dataLoadUrl, function(data) {
params.data = data;
params.pristinaData = JSON.parse(JSON.stringify(data));

drawOrganizationChart(params);
})

@bumbeishvili
Copy link
Author

@JoDeeBarKnee

This is old code, please check this new one out - D3 Org Chart

Examples are also linked here, so you can play with them

@pbaviskar2612
Copy link

@bumbeishvili Could you please share the latest example of above Employee Hierarchy Chart with d3 version 7 as if we update the version 7 for above code example. it is throwing the error for many functions of D3.

@bumbeishvili
Copy link
Author

@pbaviskar2612 above won't work for v7 there is a brand new org chart linked above, which should work for v7, you can check examples of it

@pbaviskar2612
Copy link

pbaviskar2612 commented Sep 22, 2022

@bumbeishvili I checked the above link but not getting the exact employee hierarchy chart as above as above code example contains HTML and css code also but in link there is only JS file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment