diff options
| author | Christian Cleberg <[email protected]> | 2026-04-17 12:40:55 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-17 12:40:55 -0500 |
| commit | fd412c5197286518ff9491a1b51e6319c9a22904 (patch) | |
| tree | 8418e1e7b590907a9328919edf2fd7765a4b8d3d /assets/js | |
| parent | 9b6de370a0b6cb95f46c29f5e1827f9c418ef6cf (diff) | |
| download | world-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.js | 4 | ||||
| -rw-r--r-- | assets/js/map/choropleth.js | 86 |
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; +} |
