From 623ae07ebacb9d596b225179727dc87641ad1cca Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Fri, 17 Apr 2026 19:41:31 -0500 Subject: phase 2-6: complete D3/TopoJSON migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- assets/js/map/choropleth.js | 31 +++++++++++++++++++++++++++++++ assets/js/map/tooltip.js | 32 +++++++++++++++++++++++++++++--- assets/js/map/zoom.js | 21 ++++++++++++++++++--- 3 files changed, 78 insertions(+), 6 deletions(-) (limited to 'assets/js/map') 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); +} -- cgit v1.2.3