summaryrefslogtreecommitdiff
path: root/assets/js/map
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/map
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/map')
-rw-r--r--assets/js/map/choropleth.js31
-rw-r--r--assets/js/map/tooltip.js32
-rw-r--r--assets/js/map/zoom.js21
3 files changed, 78 insertions, 6 deletions
diff --git a/assets/js/map/choropleth.js b/assets/js/map/choropleth.js
index 9316f29..5262764 100644
--- a/assets/js/map/choropleth.js
+++ b/assets/js/map/choropleth.js
@@ -10,6 +10,7 @@ const NO_DATA_FILL = "#444";
let svg, mapGroup, projection, pathGen, features;
let _countries, _isoLookup;
let _clickHandler = null;
+let _labelsVisible = false;
export async function initMap(container, countries, isoLookup) {
_countries = countries;
@@ -43,9 +44,33 @@ export async function initMap(container, countries, isoLookup) {
if (_clickHandler) _clickHandler(event, d);
});
+ // Country name labels, hidden by default
+ mapGroup.selectAll("text.country-label")
+ .data(features)
+ .join("text")
+ .attr("class", "country-label")
+ .attr("transform", d => {
+ const [x, y] = pathGen.centroid(d);
+ return isNaN(x) || isNaN(y) ? "translate(-9999,-9999)" : `translate(${x},${y})`;
+ })
+ .attr("text-anchor", "middle")
+ .attr("dy", "0.35em")
+ .style("font-size", "7px")
+ .style("fill", "#fff")
+ .style("pointer-events", "none")
+ .style("display", "none")
+ .text(d => {
+ const alpha3 = _isoLookup[+d.id];
+ return alpha3 && _countries[alpha3] ? _countries[alpha3].country : "";
+ });
+
return { svg, mapGroup, projection };
}
+export function getCountryPaths() {
+ return mapGroup.selectAll("path.country");
+}
+
export function applyGradient(metricKey) {
const values = Object.values(_countries)
.map(c => c[metricKey])
@@ -69,6 +94,12 @@ export function resetGradient() {
.attr("fill", d => _fillForFeature(d, null, null));
}
+export function toggleLabels() {
+ _labelsVisible = !_labelsVisible;
+ mapGroup.selectAll("text.country-label")
+ .style("display", _labelsVisible ? "block" : "none");
+}
+
export function setClickHandler(fn) {
_clickHandler = fn;
}
diff --git a/assets/js/map/tooltip.js b/assets/js/map/tooltip.js
index c8f112a..c74418b 100644
--- a/assets/js/map/tooltip.js
+++ b/assets/js/map/tooltip.js
@@ -1,3 +1,29 @@
-// Phase 3: hover tooltip
-export function initTooltip() {}
-export function attachTooltip(selection, getContent) {}
+import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
+
+let tooltip;
+
+export function initTooltip() {
+ tooltip = d3.select("body")
+ .append("div")
+ .attr("class", "map-tooltip");
+}
+
+export function attachTooltip(selection, getContent) {
+ selection
+ .on("mouseover", (event, d) => {
+ const content = getContent(event, d);
+ if (content) {
+ tooltip.style("opacity", 1).html(content);
+ } else {
+ tooltip.style("opacity", 0);
+ }
+ })
+ .on("mousemove", (event) => {
+ tooltip
+ .style("left", (event.pageX + 12) + "px")
+ .style("top", (event.pageY - 28) + "px");
+ })
+ .on("mouseout", () => {
+ tooltip.style("opacity", 0);
+ });
+}
diff --git a/assets/js/map/zoom.js b/assets/js/map/zoom.js
index aec278b..cba7c2c 100644
--- a/assets/js/map/zoom.js
+++ b/assets/js/map/zoom.js
@@ -1,3 +1,18 @@
-// Phase 3: zoom and pan behavior
-export function initZoom(svg, mapGroup) {}
-export function resetZoom(svg, zoomBehavior) {}
+import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
+
+let _svg, _zoom;
+
+export function initZoom(svg, mapGroup) {
+ _svg = svg;
+ _zoom = d3.zoom()
+ .scaleExtent([0.5, 8])
+ .on("zoom", (event) => {
+ mapGroup.attr("transform", event.transform);
+ });
+ svg.call(_zoom);
+ return _zoom;
+}
+
+export function resetZoom() {
+ _svg.transition().duration(750).call(_zoom.transform, d3.zoomIdentity);
+}