As probably most companies, we have lists of our employees with their names, entry date, leaving date and more. And once in a while we need graphical overviews of the number of people joining and leaving as well as the evolution of our overall staff numbers. In order to get this easily produced from our HR files, I created this little D3 scripts.
xxxxxxxxxx
<meta charset="utf-8">
<style>
.bar rect {
fill: steelblue;
}
.barLeaving rect {
fill: rgb(145, 152, 158);
}
.bar text {
fill: #fff;
font: 10px sans-serif;
}
.barLeaving text {
fill: #fff;
font: 10px sans-serif;
}
.tooltip text {
font: 10px sans-serif;
text-align: left;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.js"></script>
<script>
/**
* Tooltip taken from here: https://bl.ocks.org/mjfoster83/7c9bdfd714ab2f2e39dd5c09057a55a0
*/
console.log('****** Starting to process ******');
var totalWidth = 1020,
totalHeight = 600,
animationDuration = 2000; // That's rather slow, show off mode ;)
var parseDate = d3.timeParse('%Y-%m-%d'), //"%m/%d/%Y %H:%M:%S %p"
formatCount = d3.format(",.0f");
// Some vars that need to be global
var tooltip;
var tooltipText;
var margin = {
top: 10,
right: 30,
bottom: 30,
left: 30
},
width = totalWidth - margin.left - margin.right,
height = totalHeight - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("align", "center")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("employees.csv", type, function (error, data) {
if (error) throw error;
// Scaling our x axis
var minTime = d3.min(data, function (d) {
return d.Starting_day
});
var maxTime = d3.max(data, function (d) {
return d.Starting_day
});
var x = d3.scaleTime()
.domain([minTime, maxTime])
.rangeRound([0, width]);
// Drawing the c axis
var xAxis = d3.axisBottom(x);
var bandSize = xAxis.tickSizeOuter() + xAxis.tickPadding();
svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(-" + bandSize + "," + height + ")")
.call(xAxis);
// Transforming the individual data entries to monthly bins
var histogramJoining = d3.histogram()
.value(function (d) {
return d.Starting_day;
})
.domain(x.domain())
.thresholds(x.ticks(d3.timeMonth));
var binsJoining = histogramJoining(data);
var histogramLeaving = d3.histogram()
.value(function (d) {
return d.Leaving_day;
})
.domain(x.domain())
.thresholds(x.ticks(d3.timeMonth));
var binsLeaving = histogramLeaving(data);
// And daily bins for the summed values
var histogramSum = d3.histogram()
.value(function (d) {
return d.Starting_day;
})
.domain(x.domain())
.thresholds(x.ticks(d3.timeDay));
var binsSum = histogramSum(data);
// Create 2 scales: One for the joining/leaving bars, one for the total staff graph
var yJoinAndLeave = d3.scaleLinear()
.range([height, 0]);
var ySummed = d3.scaleLinear()
.range([height, 0]);
// Getting the yJoinAndLeave axis scaled
var maxValue = d3.max(binsJoining, function (d) {
return d.length;
});
var minValue = d3.min(binsLeaving, function (d) {
return -d.length;
});
yJoinAndLeave.domain([minValue, maxValue]);
// Scaling the ySummed axis
var minSummedValue = 0; // We start with a staff of 0
var maxSummedValue = d3.max(data, function (d) {
return summedValue(data, d.Starting_day)
})
ySummed.domain([minSummedValue, maxSummedValue]);
// Drawing the rectangles of joining staff
var bar = svg.selectAll(".bar")
.data(binsJoining)
.enter().append("g")
.attr("class", "bar");
bar.append("rect")
.attr("x", function (d) {
return x(d.x0)
})
.attr("width", function (d) {
return x(d.x1) - x(d.x0) - 1;
})
.attr("y", yJoinAndLeave(0))
.attr("height", height - yJoinAndLeave(0 + minValue))
.on("mouseover", function (d) {
tooltip.style("display", null);
d.oldStyle = d3.select(this).style("fill");
d3.select(this)
.style("fill", "orange");
renderText(getTooltipText(d));
})
.on("mouseout", function (d) {
tooltip.style("display", "none");
d3.select(this)
.style("fill", d.oldStyle);
})
.transition()
.duration(animationDuration)
.attr("y", function (d) {
return yJoinAndLeave(d.length)
})
.attr("height", function (d) {
return height - yJoinAndLeave(d.length + minValue);
})
// Writing the no of cases in joining bars
bar.append("text")
.attr("dy", ".75em")
.attr("y", function (d) {
return yJoinAndLeave(d.length) + 6
})
.attr("x", function (d) {
return x(d.x0) + (x(d.x1) - x(d.x0)) / 2;
})
.attr("text-anchor", "middle")
.text(function (d) {
if (d.length == 0) return '';
return formatCount(d.length);
});
// Drawing the rectangles of leaving staff
var barLeaving = svg.selectAll(".barLeaving")
.data(binsLeaving)
.enter().append("g")
.attr("class", "barLeaving");
barLeaving.append("rect")
.attr("x", function (d) {
return x(d.x0)
})
.attr("width", function (d) {
return x(d.x1) - x(d.x0) - 1;
})
.attr("y", yJoinAndLeave(0))
.attr("height", height - yJoinAndLeave(minValue))
.on("mouseover", function (d) {
tooltip.style("display", null);
d.oldStyle = d3.select(this).style("fill");
d3.select(this)
.style("fill", "orange");
renderText(getTooltipText(d));
})
.on("mouseout", function (d) {
tooltip.style("display", "none");
d3.select(this)
.style("fill", d.oldStyle);
})
.transition()
.duration(animationDuration)
.attr("height", function (d) {
return height - yJoinAndLeave(d.length + minValue);
})
// Writing the no of cases in leaving bars
barLeaving.append("text")
.attr("dy", ".75em")
.attr("y", function (d) {
return yJoinAndLeave(0) + height - yJoinAndLeave(d.length + minValue) - 12;
})
.attr("x", function (d) {
return x(d.x0) + (x(d.x1) - x(d.x0)) / 2;
})
.attr("text-anchor", "middle")
.text(function (d) {
if (d.length == 0) return '';
return formatCount(d.length);
});
// Draw the Y scale of summed numbers
svg.append("g")
.call(d3.axisLeft(ySummed))
.append("text")
.attr("fill", "#000")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Staff");
// Drawing the line of staff size
var sumLine = d3.line()
.x(function (d) {
return x(d.x0);
})
.y(function (d) {
var summedValueData = summedValue(data, d.x0);
return ySummed(summedValueData);
});
svg.append("path")
.datum(binsSum)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 2.5)
.attr("d", sumLine);
// Prep the tooltip, initial display is hidden
tooltip = svg.append("g")
.attr("class", "tooltip")
tooltipText = tooltip.append("text")
.attr("x", 30)
.attr("dy", "1.2em")
.attr("z-index", 9999)
});
// Calculates the staff size on a given day
function summedValue(data, date) {
var sum = d3.sum(data, function (d) {
var value = 0;
if (d.Starting_day <= date) value++;
if (d.Leaving_day && d.Leaving_day <= date) value--;
return value;
})
return sum;
}
// Transforming date values to proper types when reading file
function type(d) { // Reading the date in the format as they are in the CSV file
d.Starting_day = parseDate(d.Starting_day);
d.Leaving_day = parseDate(d.Leaving_day);
return d;
}
// Creates the text lines as tspans into the text element
// Tabken from here: https://jarrettmeyer.com/2018/06/05/svg-multiline-text-with-tspan
function renderText(textString) {
tooltip.selectAll("tspan.text").remove();
tooltipText.selectAll("tspan.text")
.data(d => textString.split("\n"))
.enter()
.append("tspan")
.attr("class", "text")
.text(d => d)
.attr("x", 20)
.attr("dx", 10)
.attr("dy", 22);
}
// Create the text that will be displayed in the tooltip
function getTooltipText(d) {
var description = '';
var separator = "\n";
d.forEach(function (employee) {
description += employee['Full_name'].trim() + separator;
})
description = description.substr(0, description.length - separator.length);
console.log('Description: ' + description);
return description;
}
</script>
</body>
https://d3js.org/d3.v4.js