I am making an organization chart in d3.js (version 5). I want it to be as dynamic as possible, so when there are changes, I can easily update the input files and it will re-render. I have a simplified reprex below and in this CodePen in which I have hard-coded the input data.
Here is a quick explanation of what I'm trying to achieve:
const tree
is an array of objects that represent the hierarchical portion of the visualization:- a
ROOT
, - 3 Managers, and
- 6 projects
- a
const staff
is an array of objects representing staff.searchObj
andfindIndicesOfMatches
work in a.map()
to replace the names of staff on a project with the object representing them (along with properties I will use as I continue to develop this org chart)- I determine the tree layout and render the tree. The parent nodes expand to cover their children, as desired.
- The last step, which is where I'm stuck, is that I want to loop through the leaf nodes, append a
g
, and render additionalrect
s based on thestaff
property. I'm currently trying to use.each()
on the nodes, checking to ensure they are leaves (if(!d.children)
), append ag
, and build out the representation of staff on the project.
What I'm unsure of is how to change the bound data to the staff
property. Nothing I've tried works so far. Currently, I'm getting a single rect
in the g
, even for projects with no staff.
Any ideas?
index.html
:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="./org-chart.js"></script>
<style>
body {
font-family: 'Helvetica';
color: #666;
font: 12px sans-serif;
}
</style>
</head>
<body>
<div id="viz"></div>
</body>
</html>
org-chart.js
:
const tree = [
{
id: 'ROOT',
parent: null
},
{
id: 'Manager 1',
parent: 'ROOT'
},
{
id: 'Manager 2',
parent: 'ROOT'
},
{
id: 'Manager 3',
parent: 'ROOT'
},
{
id: 'Project 1',
parent: 'Manager 1',
staff: ['Staff 1']
},
{
id: 'Project 2',
parent: 'Manager 1',
staff: ['Staff 1', 'Staff 2']
},
{
id: 'Project 3',
parent: 'Manager 2',
staff: ['Staff 2', 'Staff 3', 'Staff 4']
},
{
id: 'Project 4',
parent: 'Manager 2',
staff: ['Staff 2', 'Staff 3', 'Staff 5']
},
{
id: 'Project 5',
parent: 'Manager 2',
staff: []
},
{
id: 'Project 6',
parent: 'Manager 3',
staff: ['Staff 4', 'Staff 5']
}
];
const staff = [
{ name: 'Staff 1', office: 'Office 1' },
{ name: 'Staff 2', office: 'Office 2' },
{ name: 'Staff 3', office: 'Office 3' },
{ name: 'Staff 4', office: 'Office 4' },
{ name: 'Staff 5', office: 'Office 5' }
];
function searchObj(obj, query) {
for (var key in obj) {
var value = obj[key];
if (typeof value === 'object') {
searchObj(value, query);
}
if (value === query) {
return true;
}
}
return false;
}
function findIndicesOfMatches(arrayOfObjects, query) {
booleanArray = arrayOfObjects.map(el => {
return searchObj(el, query);
});
const reducer = (accumulator, currentValue, index) => {
return currentValue ? accumulator.concat(index) : accumulator;
};
return booleanArray.reduce(reducer, []);
}
// Join tree and staff data
const joinedData = tree.map(el => {
if ('staff' in el) {
newStaffArray = el.staff.map(s => {
const staffIndex = findIndicesOfMatches(staff, s);
return staff[staffIndex];
});
return { ...el, staff: newStaffArray };
} else {
return el;
}
});
console.log('joinedData');
console.log(joinedData);
// Sizing variables
const margin = { top: 50, right: 50, bottom: 90, left: 90 },
width = 1000,
height = 200,
node_height = 25,
leaf_node_width = 100;
// Draw function
drawOrgChart = data => {
const svg = d3
.select('#viz')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.bottom + margin.top);
const stratify = d3
.stratify()
.parentId(d => d.parent)
.id(d => d.id);
const tree = d3
.tree()
.size([width, height])
.separation(d => leaf_node_width * 0.5);
const dataStratified = stratify(data);
var nodes = d3.hierarchy(dataStratified);
root_node = tree(nodes);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
var nodes = g
.selectAll('.node')
.data(nodes.descendants(), d => d.id)
.enter()
.append('g')
.attr('class', function(d) {
return 'node' + (d.children ? ' node--internal' : ' node--leaf');
})
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
var rect_colors = ['grey', 'blue', 'green', 'maroon'];
nodes
.append('rect')
.attr('height', node_height)
.attr('width', d => {
const extent = d3.extent(d.leaves().map(d => d.x));
return extent[1] - extent[0] + leaf_node_width;
})
.attr('fill', '#ffffff')
.attr('stroke', d => {
return rect_colors[d.depth];
})
.attr('transform', d => {
const first_leaf_x = d.leaves()[0].x;
return `translate(${-(d.x - first_leaf_x + leaf_node_width / 2)},0)`;
})
.attr('rx', 5)
.attr('ry', 5);
nodes
.append('text')
.attr('dy', '.35em')
.attr('x', d => 0)
.attr('y', node_height / 2)
.style('text-anchor', 'middle')
.text(function(d) {
return d.data.data.id;
});
// This is the bit I can't figure out:
// I'd like to append additional elements to
// the leaf nodes based on the 'staff' property
console.log(nodes.data());
nodes.each(function(d, j) {
if (!d.children) {
const staff = d.data.data.staff;
console.log(staff);
d3.select(this)
.append('g')
.selectAll('rect')
.data([staff])
.enter()
.append('rect')
.attr('x', 0)
.attr('y', (p, i) => 30 * (i + 1))
.attr('height', node_height)
.attr('width', leaf_node_width)
.attr('transform', `translate(-${leaf_node_width / 2},0)`)
.attr('stroke', 'red')
.attr('fill', '#efefef80');
}
});
};
document.addEventListener('DOMContentLoaded', function() {
drawOrgChart(joinedData);
});