Spaces:
Running
Running
| Below is a pragmatic “tinkerer’s tour” for the D3 + TopoJSON canvas world-map we just built. | |
| I’ll point out the core building blocks (“knobs”) and show how changing each one affects the experience, so you can remix or extend the tool with confidence. | |
| --- | |
| 1. File Anatomy (what lives where) | |
| Section Purpose Main Knobs | |
| <style> Colors, fonts, layout, line thickness CSS custom properties (--water, --country, …); global ctx.lineWidth later in JS | |
| Global JS prologue Sets up high-DPI canvas, projection, path generator dpr, projection.scale(), projection.translate() | |
| Data loader Fetches TopoJSON (countries-110m.json) and converts to GeoJSON URLs array & dataset resolution | |
| Zoom / Pan Creates a d3.zoom() on the canvas scaleExtent([min,max]), double-tap reset handler | |
| Hit-testing helpers Uses geoContains for precise picks can swap to faster but coarser schemes | |
| render() The drawing pipeline fill / stroke colors, line width, paint order | |
| Event handlers hover / click / double-click logic where you wire callbacks, e.g. show tooltip | |
| Because it’s plain HTML, everything is in one file—open it in any code editor and you’ll see the pieces in that order. | |
| --- | |
| 2. The Visual Knobs | |
| 2.1 Colors & Fonts | |
| All map colors are CSS variables at the top of <style>: | |
| :root { | |
| --water: #a0d3ff; /* oceans */ | |
| --country: #f7f7f7; /* default land */ | |
| --country-hover: #d0d0d0; /* under pointer */ | |
| --country-selected: #7d7d7d; | |
| --stroke: #444; /* border lines */ | |
| } | |
| Change them once and both the base fill and subsequent re-renders pick up the new palette automatically. | |
| 2.2 Line Thickness | |
| Search for: | |
| ctx.lineWidth = .18; | |
| Increase this for chunkier borders; decrease for hairlines. | |
| > Tip: On retina/4-K screens we already multiply by devicePixelRatio, so 0.18 is genuinely thin but still visible. | |
| 2.3 Projection Size & Center | |
| The projection is resized on every resizeCanvas() call: | |
| projection | |
| .translate([w / 2, h / 2]) // center | |
| .scale(Math.min(w / 5.5, h / 3)); // overall size | |
| Lower the divisor (5.5, 3) → larger globe footprint. | |
| Change geoNaturalEarth1() to any other D3 projection (geoMercator, geoOrthographic, …) for a different look. | |
| --- | |
| 3. Interaction Knobs | |
| 3.1 Zoom & Pan Limits | |
| d3.zoom().scaleExtent([1, 16]) | |
| First value = minimum zoom (1 = fit-canvas). | |
| Second value = maximum zoom (we raised it from 8 → 16). | |
| 3.2 Reset Gesture | |
| We override D3’s default double-click to make it a double-tap reset: | |
| canvas.addEventListener('dblclick', () => { | |
| currentT = d3.zoomIdentity; // zero out transform | |
| d3.select(canvas) | |
| .transition().duration(250) | |
| .call(d3.zoom().transform, d3.zoomIdentity); | |
| }); | |
| Change the event (dblclick) or the easing/duration for a different feel. | |
| 3.3 Hit-testing Logic | |
| Hit-testing is the expensive part: | |
| const countryAt = (ev) => | |
| countries.find(c => d3.geoContains(c, pointerLatLon(ev))); | |
| For performance-first, replace with bounding-box checks or a spatial index (rbush). | |
| For accuracy, keep geoContains, especially important when you zoom deep. | |
| --- | |
| 4. Data Knobs | |
| 4.1 Switching to Higher-Resolution Borders | |
| countries-110m.json ≈ ~110 m resolution (about 50 KB). | |
| Replace with: | |
| countries-50m.json (finer, ~200 KB) | |
| countries-10m.json (very fine, ~1 MB) | |
| const sources = [ | |
| "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json", | |
| ... | |
| ]; | |
| More points → smoother borders and heavier CPU while zooming. | |
| 4.2 Adding Sub-Nation Shapes | |
| Load provinces, states, whatever—just fetch a different TopoJSON layer and draw another pass above the country fill. | |
| --- | |
| 5. Extending the Tool (quick recipes) | |
| Goal What to Tweak | |
| Show country names on hover Inside the mousemove handler, when hovered changes, position a <div> tooltip at the pointer and fill hovered.properties.name. | |
| Persist selection elsewhere window.SELECTED_COUNTRY_ID already exposes the current pick; just read it from other scripts. | |
| Add a search-box Keep a map (id → feature) in memory, then on search zoom to the country using d3.geoPath.bounds() → compute a fitting transform. | |
| Switch to WebGL for 100k shapes Replace canvas commands with PixiJS or regl if you need insane performance. | |
| --- | |
| 6. Minimal “Hello World” Variant (for experiments) | |
| <script type="module"> | |
| import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm"; | |
| import * as topojson from "https://cdn.jsdelivr.net/npm/topojson@3/+esm"; | |
| const canvas = document.querySelector("canvas"); | |
| const ctx = canvas.getContext("2d"); | |
| const projection = d3.geoNaturalEarth1() | |
| .translate([400, 250]) | |
| .scale(200); | |
| const path = d3.geoPath(projection, ctx); | |
| const world = await d3.json( | |
| "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json" | |
| ); | |
| const countries = topojson.feature(world, world.objects.countries).features; | |
| path({type:"FeatureCollection", features:countries}); | |
| ctx.fillStyle = "#ddd"; | |
| ctx.fill(); | |
| </script> | |
| Drop that into an HTML file with a <canvas width="800" height="500"> tag—instant static map. | |
| --- | |
| 7. Where to Go from Here | |
| D3’s “Geo Projections” docs – learn the knobs on every projection’s API. | |
| ObservableHQ examples – live notebooks for hit-testing, zoom, labeling, etc. | |
| TopoJSON documentation – see how to convert or simplify your own shapefiles. | |
| Happy mapping—and feel free to ask if you want deeper dives into any of these levers! | |