krz/world-incarceration
An interactive map of world incarceration statistics.
clone: git clone https://gitbay.org/krz/world-incarceration.git
1import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
2import { countries } from "../data/countries.js";
3import { isoNumericToAlpha3 } from "../data/iso-numeric-to-alpha3.js";
4import {
5 initMap, applyGradient, resetGradient,
6 toggleLabels, setClickHandler, getCountryPaths,
7} from "./map/choropleth.js";
8import { initTooltip, attachTooltip } from "./map/tooltip.js";
9import { initZoom, resetZoom } from "./map/zoom.js";
10import { renderBarChart } from "./charts/bar.js";
11import { renderPieChart } from "./charts/pie.js";
12import { showCountryModal } from "./ui/modal.js";
13import { initCompare } from "./ui/compare.js";
14import { initControls, getTop5, formatMetricValue } from "./ui/controls.js";
15
16// Tracks which metric is currently active for the graph modal
17let currentMetric = "pop";
18
19async function main() {
20 const { svg, mapGroup } = await initMap(
21 "#map-container", countries, isoNumericToAlpha3
22 );
23
24 // --- Phase 3: tooltip ------------------------------------------------
25 initTooltip();
26 attachTooltip(getCountryPaths(), (event, d) => {
27 const alpha3 = isoNumericToAlpha3[+d.id];
28 const name = alpha3 && countries[alpha3] ? countries[alpha3].country : null;
29 return name ? `<strong>${name}</strong>` : null;
30 });
31
32 // --- Phase 3: zoom ---------------------------------------------------
33 initZoom(svg, mapGroup);
34
35 // --- Phase 3: country click → detail modal ---------------------------
36 setClickHandler((event, d) => {
37 const alpha3 = isoNumericToAlpha3[+d.id];
38 if (alpha3 && countries[alpha3]) showCountryModal(countries[alpha3]);
39 });
40
41 // --- Phase 5: metric buttons, label toggle, reset --------------------
42 initControls(
43 (metricKey) => {
44 currentMetric = metricKey;
45 applyGradient(metricKey);
46 // Refresh pie if graph modal is already open
47 if (document.getElementById("graphModal").classList.contains("show")) {
48 _renderPie();
49 } else {
50 // Clear stale chart so it re-renders fresh on next open
51 document.getElementById("myChart2").innerHTML = "";
52 }
53 },
54 () => toggleLabels(),
55 () => { resetGradient(); resetZoom(); }
56 );
57
58 // --- Phase 5: compare modal ------------------------------------------
59 initCompare(countries);
60
61 // --- Phase 4: graph modal — render pie on first open -----------------
62 document.getElementById("graphModal").addEventListener("show.bs.modal", () => {
63 if (!document.querySelector("#myChart2 svg")) {
64 _renderPie();
65 }
66 });
67}
68
69// Renders (or re-renders) the pie chart for the current metric
70function _renderPie() {
71 document.getElementById("myChart2").innerHTML = "";
72 document.getElementById("myNextChart").style.display = "none";
73 document.getElementById("myNextChart").innerHTML = "";
74 document.getElementById("myChart2").style.display = "";
75 renderPieChart("#myChart2", getTop5(countries, currentMetric), _showDrilldown);
76}
77
78// Replaces pie with a per-country metrics bar chart
79function _showDrilldown(alpha3) {
80 const c = countries[alpha3];
81 const metricKeys = ["pop", "rate", "females", "juveniles", "occupancy"];
82 const metricLabels = ["Population", "Rate/100k", "% Female", "% Juvenile", "Occupancy"];
83
84 const data = metricKeys.map((key, i) => {
85 const val = c[key];
86 if (typeof val !== "number" || isNaN(val)) return null;
87
88 const allVals = Object.values(countries)
89 .map(co => co[key])
90 .filter(v => typeof v === "number" && !isNaN(v));
91 const max = Math.max(...allVals);
92
93 return {
94 label: metricLabels[i],
95 value: (val / max) * 100,
96 displayValue: formatMetricValue(val, key),
97 };
98 }).filter(Boolean);
99
100 document.getElementById("myChart2").style.display = "none";
101 document.getElementById("myNextChart").style.display = "block";
102
103 renderBarChart("#myNextChart", data, {
104 title: c.country,
105 onBack: () => _renderPie(),
106 });
107}
108
109main();