interactive-world-map / tool_tutorial.txt
iurbinah's picture
Create tool_tutorial.txt
e1984f8 verified
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!