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