function splitBar(
_data,
{
bind = null,
numberFormat = d3.format(",.0f"),
margin = { top: 10, right: 20, bottom: 10, left: 50 },
height = 300,
width = 500,
dataColumn,
dataRow,
value,
colour = "#00AEEF"
} = {}
) {
bind.selectAll("svg").remove();
const data = Array.from(
d3.group(_data, (d) => d[dataColumn]),
([key, value]) => ({ category: key, value: value })
);
const formatLabel = (d) => d3.format(numberFormat)(d);
const xText = (d) => (d[value] === null ? "--" : formatLabel(d[value]));
const column = (d) => d.category;
const columnScale = d3
.scaleBand()
.range([0, width - margin.right])
.domain(data.map(column))
.paddingInner(0.075);
const columnValue = (d) => columnScale(column(d));
// setup for the x value for bars and labels
const x = (d) => +d[value];
const xScale = d3
.scaleLinear()
.range([0, columnScale.bandwidth()])
.domain([0, d3.max(_data, (d) => d.value)]);
// setup for y values of bars
const y = (d) => d[dataRow];
const yScale = d3
.scaleBand()
.range([height - margin.bottom, 0])
.domain(_data.map(y).reverse())
.padding(0.2);
// make the svg
const svg = bind
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const g = svg
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
// create the columns
const gColumn = g
.append("g")
.attr("class", "columns")
.selectAll(".column")
.data(data)
.join("g")
.attr("class", "column")
.attr("transform", (d) => `translate(${columnValue(d)}, ${margin.top})`);
gColumn
.append("text")
.attr("x", 0)
.attr("y", -10)
.attr("dy", "0.32em")
.attr("fill", "#454545")
.text(column)
.each((d) => d3.select(this).call(wrap, xScale.range()[1]));
// create the bars
const bars = gColumn.append("g").attr("class", "bars");
// underlying bars that go from 0 - max as they serve as a background
bars
.selectAll(".bar-underlying")
.data((d) => d.value)
.join("rect")
.attr("class", "bar bar-underlying")
.attr("x", 0)
.attr("y", (d) => yScale(y(d)))
.attr("width", xScale.range()[1])
.attr("height", yScale.bandwidth())
.attr("fill", (d) => {
if (d[dataColumn] === "Total" || d[dataRow] === "Total") {
return "none";
} else {
return "#e5e5e3";
}
});
// overlying bars that represent the value of each category
bars
.selectAll(".bar-overlying")
.data((d) => d.value)
.join("rect")
.attr("class", "bar bar-overlying")
.attr("x", 0)
.attr("y", (d) => yScale(y(d)))
.attr("width", (d) => (d[value] !== null ? xScale(x(d)) : 0))
.attr("height", yScale.bandwidth())
.attr("fill", (d) => {
if (d[dataColumn] === "Total" || d[dataRow] === "Total") {
return "none";
} else {
return colour;
}
});
// make the data labels and position them accordingly
gColumn
.append("g")
.attr("class", "labels")
.selectAll(".label")
.data((d) => d.value)
.enter()
.append("text")
.attr("class", "label")
.attr("fill", "#454545")
.text((d) => xText(d))
.each(positionLabel);
// make the y axis - serves as row labels
g.append("g")
.attr("class", "axis axis-y")
.attr("transform", `translate(0, ${margin.top})`)
.call(d3.axisLeft(yScale))
.call((g) => g.select(".domain").remove())
.call((g) => g.selectAll("line").remove())
.selectAll(".tick text")
.each(function (d) {
d3.select(this).call(wrap, margin.left);
});
// select the labels and colour them
d3.selectAll(".label-white").attr("fill", "#fafafa");
d3.selectAll(".label-na").attr("fill", "#454545");
// helper function that positiones the labels inside/outside of the overlying bars (depending on value)
function positionLabel(d) {
const xValue = xScale(x(d));
const xMax = xScale.range()[1];
if (xValue < 0.5 * xMax) {
d3.select(this)
.classed("label-white", false)
.attr("x", xValue)
.attr("dx", 2);
} else {
d3.select(this).classed("label-white", true).attr("x", 0).attr("dx", 4);
}
if (d[dataColumn] === "Total" || d[dataRow] === "Total") {
d3.select(this)
.classed("label-white", false)
.classed("label-total", true);
}
if (d["value"] === null) {
d3.select(this).classed("label-white", false).classed("label-na", true);
}
d3.select(this)
.attr("y", yScale(y(d)) + yScale.bandwidth() / 2)
.attr("dy", "0.33em");
}
// helper function to break long labels on y axis into multiple rows
function wrap(text, width) {
text.each(function () {
const text = d3.select(this);
const words = text.text().split(/\s+/).reverse();
let word = "";
let line = [];
const lineHeight = 1.1; // ems
const x = text.attr("x");
let tspan = text.text(null).append("tspan").attr("x", x);
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("dy", lineHeight + "em")
.text(word);
}
}
});
const breaks = text.selectAll("tspan").size();
text.attr("y", function () {
return +text.attr("y") + -6 * (breaks - 1);
});
}
return svg.node();
}