summaryrefslogtreecommitdiff
path: root/assets/js/app.js
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-17 19:41:31 -0500
committerChristian Cleberg <[email protected]>2026-04-17 19:41:31 -0500
commit623ae07ebacb9d596b225179727dc87641ad1cca (patch)
treef1045282b3369bf3f4b0249cd649b47a5d010dad /assets/js/app.js
parentfd412c5197286518ff9491a1b51e6319c9a22904 (diff)
downloadworld-incarceration-623ae07ebacb9d596b225179727dc87641ad1cca.tar.gz
world-incarceration-623ae07ebacb9d596b225179727dc87641ad1cca.tar.bz2
world-incarceration-623ae07ebacb9d596b225179727dc87641ad1cca.zip
phase 2-6: complete D3/TopoJSON migration
Replace ZingChart + jQuery with D3 v7 + TopoJSON. Implements all planned phases: choropleth map (NaturalEarth1 projection, YlOrRd gradient), hover tooltip, zoom/pan/reset, country detail modal, side-by-side compare, metric selector, label toggle, donut pie chart with per-country drilldown bar chart. Upgrades Bootstrap JS bundle from v4.5.3 (jQuery-dependent) to v5.0.0, fixes modal API calls to use getInstance/constructor pattern compatible with BS 5.0.0+. Removes jquery, zingchart ×3 library files.
Diffstat (limited to 'assets/js/app.js')
-rw-r--r--assets/js/app.js105
1 files changed, 100 insertions, 5 deletions
diff --git a/assets/js/app.js b/assets/js/app.js
index aca4ac9..7f109e7 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -1,14 +1,109 @@
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
-import * as topojson from "https://cdn.jsdelivr.net/npm/topojson-client@3/+esm";
import { countries } from "../data/countries.js";
import { isoNumericToAlpha3 } from "../data/iso-numeric-to-alpha3.js";
-import { initMap, applyGradient, resetGradient, setClickHandler } from "./map/choropleth.js";
-import { initTooltip } from "./map/tooltip.js";
+import {
+ initMap, applyGradient, resetGradient,
+ toggleLabels, setClickHandler, getCountryPaths,
+} from "./map/choropleth.js";
+import { initTooltip, attachTooltip } from "./map/tooltip.js";
import { initZoom, resetZoom } from "./map/zoom.js";
import { renderBarChart } from "./charts/bar.js";
import { renderPieChart } from "./charts/pie.js";
import { showCountryModal } from "./ui/modal.js";
import { initCompare } from "./ui/compare.js";
-import { initControls, getTop5 } from "./ui/controls.js";
+import { initControls, getTop5, formatMetricValue } from "./ui/controls.js";
-initMap("#map-container", countries, isoNumericToAlpha3);
+// Tracks which metric is currently active for the graph modal
+let currentMetric = "pop";
+
+async function main() {
+ const { svg, mapGroup } = await initMap(
+ "#map-container", countries, isoNumericToAlpha3
+ );
+
+ // --- Phase 3: tooltip ------------------------------------------------
+ initTooltip();
+ attachTooltip(getCountryPaths(), (event, d) => {
+ const alpha3 = isoNumericToAlpha3[+d.id];
+ const name = alpha3 && countries[alpha3] ? countries[alpha3].country : null;
+ return name ? `<strong>${name}</strong>` : null;
+ });
+
+ // --- Phase 3: zoom ---------------------------------------------------
+ initZoom(svg, mapGroup);
+
+ // --- Phase 3: country click → detail modal ---------------------------
+ setClickHandler((event, d) => {
+ const alpha3 = isoNumericToAlpha3[+d.id];
+ if (alpha3 && countries[alpha3]) showCountryModal(countries[alpha3]);
+ });
+
+ // --- Phase 5: metric buttons, label toggle, reset --------------------
+ initControls(
+ (metricKey) => {
+ currentMetric = metricKey;
+ applyGradient(metricKey);
+ // Refresh pie if graph modal is already open
+ if (document.getElementById("graphModal").classList.contains("show")) {
+ _renderPie();
+ } else {
+ // Clear stale chart so it re-renders fresh on next open
+ document.getElementById("myChart2").innerHTML = "";
+ }
+ },
+ () => toggleLabels(),
+ () => { resetGradient(); resetZoom(); }
+ );
+
+ // --- Phase 5: compare modal ------------------------------------------
+ initCompare(countries);
+
+ // --- Phase 4: graph modal — render pie on first open -----------------
+ document.getElementById("graphModal").addEventListener("show.bs.modal", () => {
+ if (!document.querySelector("#myChart2 svg")) {
+ _renderPie();
+ }
+ });
+}
+
+// Renders (or re-renders) the pie chart for the current metric
+function _renderPie() {
+ document.getElementById("myChart2").innerHTML = "";
+ document.getElementById("myNextChart").style.display = "none";
+ document.getElementById("myNextChart").innerHTML = "";
+ document.getElementById("myChart2").style.display = "";
+ renderPieChart("#myChart2", getTop5(countries, currentMetric), _showDrilldown);
+}
+
+// Replaces pie with a per-country metrics bar chart
+function _showDrilldown(alpha3) {
+ const c = countries[alpha3];
+ const metricKeys = ["pop", "rate", "females", "juveniles", "occupancy"];
+ const metricLabels = ["Population", "Rate/100k", "% Female", "% Juvenile", "Occupancy"];
+
+ const data = metricKeys.map((key, i) => {
+ const val = c[key];
+ if (typeof val !== "number" || isNaN(val)) return null;
+
+ const allVals = Object.values(countries)
+ .map(co => co[key])
+ .filter(v => typeof v === "number" && !isNaN(v));
+ const max = Math.max(...allVals);
+
+ return {
+ label: metricLabels[i],
+ value: (val / max) * 100,
+ displayValue: formatMetricValue(val, key),
+ };
+ }).filter(Boolean);
+
+ document.getElementById("myChart2").style.display = "none";
+ document.getElementById("myNextChart").style.display = "block";
+
+ renderBarChart("#myNextChart", data, {
+ title: c.country,
+ onBack: () => _renderPie(),
+ });
+}
+
+main();