diff options
Diffstat (limited to 'assets/js/map/choropleth.js')
| -rw-r--r-- | assets/js/map/choropleth.js | 86 |
1 files changed, 82 insertions, 4 deletions
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; +} |
