krz/world-incarceration
An interactive map of world incarceration statistics.
clone: git clone https://gitbay.org/krz/world-incarceration.git
main: 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}