summaryrefslogtreecommitdiff
path: root/assets/js
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-17 12:40:55 -0500
committerChristian Cleberg <[email protected]>2026-04-17 12:40:55 -0500
commitfd412c5197286518ff9491a1b51e6319c9a22904 (patch)
tree8418e1e7b590907a9328919edf2fd7765a4b8d3d /assets/js
parent9b6de370a0b6cb95f46c29f5e1827f9c418ef6cf (diff)
downloadworld-incarceration-fd412c5197286518ff9491a1b51e6319c9a22904.tar.gz
world-incarceration-fd412c5197286518ff9491a1b51e6319c9a22904.tar.bz2
world-incarceration-fd412c5197286518ff9491a1b51e6319c9a22904.zip
phase 2 implement
Diffstat (limited to 'assets/js')
-rw-r--r--assets/js/app.js4
-rw-r--r--assets/js/map/choropleth.js86
2 files changed, 84 insertions, 6 deletions
diff --git a/assets/js/app.js b/assets/js/app.js
index 51ff0b4..aca4ac9 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -2,7 +2,7 @@ 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, setClickHandler } from "./map/choropleth.js";
+import { initMap, applyGradient, resetGradient, setClickHandler } from "./map/choropleth.js";
import { initTooltip } from "./map/tooltip.js";
import { initZoom, resetZoom } from "./map/zoom.js";
import { renderBarChart } from "./charts/bar.js";
@@ -11,4 +11,4 @@ import { showCountryModal } from "./ui/modal.js";
import { initCompare } from "./ui/compare.js";
import { initControls, getTop5 } from "./ui/controls.js";
-// Phase 2+: wire modules together here
+initMap("#map-container", countries, isoNumericToAlpha3);
diff --git a/assets/js/map/choropleth.js b/assets/js/map/choropleth.js
index e7d5d3d..9316f29 100644
--- a/assets/js/map/choropleth.js
+++ b/assets/js/map/choropleth.js
@@ -1,4 +1,82 @@
-// Phase 2: choropleth map rendering, gradient coloring
-export function initMap(container, countries, isoLookup) {}
-export function applyGradient(metricKey) {}
-export function setClickHandler(fn) {}
+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";
+
+const WIDTH = 960;
+const HEIGHT = 500;
+const DEFAULT_FILL = "rgba(36, 171, 72, 0.8)";
+const NO_DATA_FILL = "#444";
+
+// Module-level state shared across exported functions
+let svg, mapGroup, projection, pathGen, features;
+let _countries, _isoLookup;
+let _clickHandler = null;
+
+export async function initMap(container, countries, isoLookup) {
+ _countries = countries;
+ _isoLookup = isoLookup;
+
+ svg = d3.select(container)
+ .append("svg")
+ .attr("viewBox", `0 0 ${WIDTH} ${HEIGHT}`)
+ .attr("preserveAspectRatio", "xMidYMid meet")
+ .style("width", "100%")
+ .style("height", "100%");
+
+ mapGroup = svg.append("g").attr("id", "map-layer");
+
+ const world = await d3.json("./assets/data/world-110m.json");
+ const geojson = topojson.feature(world, world.objects.countries);
+ features = geojson.features;
+
+ projection = d3.geoNaturalEarth1().fitSize([WIDTH, HEIGHT], geojson);
+ pathGen = d3.geoPath().projection(projection);
+
+ mapGroup.selectAll("path")
+ .data(features)
+ .join("path")
+ .attr("class", "country")
+ .attr("d", pathGen)
+ .attr("fill", d => _fillForFeature(d, null, null))
+ .attr("stroke", "#111")
+ .attr("stroke-width", 0.5)
+ .on("click", (event, d) => {
+ if (_clickHandler) _clickHandler(event, d);
+ });
+
+ return { svg, mapGroup, projection };
+}
+
+export function applyGradient(metricKey) {
+ const values = Object.values(_countries)
+ .map(c => c[metricKey])
+ .filter(v => typeof v === "number" && !isNaN(v));
+
+ const [min, max] = d3.extent(values);
+ const colorScale = d3.scaleSequential()
+ .domain([min, max])
+ .interpolator(d3.interpolateYlOrRd);
+
+ mapGroup.selectAll("path.country")
+ .transition()
+ .duration(400)
+ .attr("fill", d => _fillForFeature(d, metricKey, colorScale));
+}
+
+export function resetGradient() {
+ mapGroup.selectAll("path.country")
+ .transition()
+ .duration(400)
+ .attr("fill", d => _fillForFeature(d, null, null));
+}
+
+export function setClickHandler(fn) {
+ _clickHandler = fn;
+}
+
+function _fillForFeature(d, metricKey, colorScale) {
+ const alpha3 = _isoLookup[+d.id];
+ if (!alpha3 || !_countries[alpha3]) return NO_DATA_FILL;
+ if (!metricKey) return DEFAULT_FILL;
+ const val = _countries[alpha3][metricKey];
+ return typeof val === "number" && !isNaN(val) ? colorScale(val) : NO_DATA_FILL;
+}