diff options
| author | Christian Cleberg <[email protected]> | 2026-04-17 19:41:31 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-17 19:41:31 -0500 |
| commit | 623ae07ebacb9d596b225179727dc87641ad1cca (patch) | |
| tree | f1045282b3369bf3f4b0249cd649b47a5d010dad /assets/js/app.js | |
| parent | fd412c5197286518ff9491a1b51e6319c9a22904 (diff) | |
| download | world-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.js | 105 |
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(); |
