krz/world-incarceration

An interactive map of world incarceration statistics.

clone: git clone https://gitbay.org/krz/world-incarceration.git

v2: assets/js/charts/bar.js · raw

 1import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
 2
 3const MARGIN = { top: 44, right: 90, bottom: 30, left: 150 };
 4
 5// data: [{label, value (0–100 normalized), displayValue}]
 6// opts: { title?, onBack? }
 7export function renderBarChart(container, data, opts = {}) {
 8	const { title, onBack } = opts;
 9	const el = typeof container === "string"
10		? document.querySelector(container)
11		: container;
12
13	el.innerHTML = "";
14
15	const totalWidth = Math.max(el.clientWidth || 500, 400);
16	const totalHeight = data.length * 52 + MARGIN.top + MARGIN.bottom;
17	const width = totalWidth - MARGIN.left - MARGIN.right;
18	const height = totalHeight - MARGIN.top - MARGIN.bottom;
19
20	const svg = d3.select(el).append("svg")
21		.attr("width", "100%")
22		.attr("viewBox", `0 0 ${totalWidth} ${totalHeight}`);
23
24	if (title) {
25		svg.append("text")
26			.attr("x", totalWidth / 2)
27			.attr("y", 22)
28			.attr("text-anchor", "middle")
29			.style("fill", "#fff")
30			.style("font-size", "14px")
31			.style("font-weight", "bold")
32			.text(title);
33	}
34
35	if (onBack) {
36		svg.append("text")
37			.attr("x", 8)
38			.attr("y", 22)
39			.style("fill", "#24ab48")
40			.style("font-size", "13px")
41			.style("cursor", "pointer")
42			.text("← Back")
43			.on("click", onBack);
44	}
45
46	const g = svg.append("g")
47		.attr("transform", `translate(${MARGIN.left},${MARGIN.top})`);
48
49	const yScale = d3.scaleBand()
50		.domain(data.map(d => d.label))
51		.range([0, height])
52		.padding(0.3);
53
54	const xScale = d3.scaleLinear()
55		.domain([0, 100])
56		.range([0, width]);
57
58	// Y axis (country/metric labels)
59	g.append("g")
60		.call(d3.axisLeft(yScale).tickSize(0))
61		.call(ax => ax.select(".domain").remove())
62		.selectAll("text")
63		.style("fill", "#ccc")
64		.style("font-size", "12px");
65
66	// X axis (percentage)
67	g.append("g")
68		.attr("transform", `translate(0,${height})`)
69		.call(d3.axisBottom(xScale).ticks(5).tickFormat(d => d + "%"))
70		.call(ax => ax.select(".domain").remove())
71		.selectAll("text")
72		.style("fill", "#666")
73		.style("font-size", "10px");
74
75	// Bars
76	g.selectAll(".bar")
77		.data(data)
78		.join("rect")
79		.attr("class", "bar")
80		.attr("y", d => yScale(d.label))
81		.attr("height", yScale.bandwidth())
82		.attr("x", 0)
83		.attr("width", d => xScale(Math.max(0, d.value)))
84		.attr("fill", "rgba(36, 171, 72, 0.8)");
85
86	// Value labels at end of bar
87	g.selectAll(".bar-label")
88		.data(data)
89		.join("text")
90		.attr("class", "bar-label")
91		.attr("x", d => xScale(Math.max(0, d.value)) + 4)
92		.attr("y", d => yScale(d.label) + yScale.bandwidth() / 2)
93		.attr("dy", "0.35em")
94		.style("fill", "#fff")
95		.style("font-size", "11px")
96		.text(d => d.displayValue);
97}