interactive-world-map / index.html
iurbinah's picture
Update index.html
6a02e56 verified
<!DOCTYPE html><html lang="en">
<head>
<meta charset="utf-8" />
<title>World Map – Touch-Friendly Canvas</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> <!-- D3 & TopoJSON --> <script src="https://d3js.org/d3.v7.min.js"></script> <script src="https://unpkg.com/topojson@3"></script> <style>
:root {
--water: #a0d3ff;
--country: #f7f7f7;
--country-hover: #d0d0d0;
--country-selected: #7d7d7d;
--stroke: #444;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--water);
overflow: hidden;
-webkit-tap-highlight-color: transparent;
}
canvas { display: block; touch-action: none; }
.info-box {
position: absolute; top: 1rem; left: 1rem;
background: rgba(255,255,255,.85); backdrop-filter: blur(6px);
padding: .75rem 1rem; border-radius: .5rem;
box-shadow: 0 4px 15px rgba(0,0,0,.12);
font-size: .9rem; user-select: none;
}
.info-box strong { font-weight: 600; color:#333; }
#country-id {
font-family: "Courier New", monospace; background:#e9ecef;
padding:0 .35rem; border-radius:4px; margin-left:.35rem; color:#c7254e;
}
</style></head>
<body>
<div class="info-box"><strong>Selected Country ID:</strong><span id="country-id">None</span></div>
<canvas id="mapCanvas"></canvas><script>
(async function () {
/* Hi-DPI Canvas ------------------------------------------------------ */
const canvas = document.getElementById('mapCanvas');
const ctx = canvas.getContext('2d');
let dpr = window.devicePixelRatio || 1;
function resizeCanvas () {
const w = window.innerWidth, h = window.innerHeight;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.setTransform(dpr,0,0,dpr,0,0);
projection.translate([w/2, h/2]).scale(Math.min(w/5.5, h/3));
render();
}
/* Projection & Path -------------------------------------------------- */
const projection = d3.geoNaturalEarth1();
const path = d3.geoPath(projection, ctx);
/* State -------------------------------------------------------------- */
let countries = [], hovered=null, selected=null, currentT=d3.zoomIdentity;
window.SELECTED_COUNTRY_ID = null;
/* Robust data loader (jsDelivr primary, unpkg fallback) -------------- */
async function fetchWorld () {
const sources = [
'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json',
'https://unpkg.com/world-atlas@2/world/110m.json'
];
for (const src of sources) {
try { return await d3.json(src); } catch (_) { /* continue */ }
}
throw new Error('All sources failed');
}
try {
const world = await fetchWorld();
countries = topojson.feature(world, world.objects.countries).features;
resizeCanvas();
} catch (err) {
console.error('Map data load failed', err);
document.querySelector('.info-box').textContent = 'Error loading map data';
}
/* Zoom & Pan --------------------------------------------------------- */
d3.select(canvas)
.call(
d3.zoom().scaleExtent([1,16]).on('zoom', (ev)=>{ currentT=ev.transform; render(); })
)
.on('dblclick.zoom', null); // disable default dblclick
canvas.addEventListener('dblclick', ()=>{
currentT=d3.zoomIdentity;
d3.select(canvas).transition().duration(250).call(d3.zoom().transform,d3.zoomIdentity);
render();
});
/* Pointer helpers ---------------------------------------------------- */
function pointerLatLon (ev) {
const [x,y]=d3.pointer(ev, canvas); const [ux,uy]=currentT.invert([x,y]);
return projection.invert([ux,uy]);
}
const countryAt=(ev)=>countries.find(c=>d3.geoContains(c, pointerLatLon(ev)));
/* Events ------------------------------------------------------------- */
d3.select(canvas)
.on('mousemove', (ev)=>{ const c=countryAt(ev); if(c!==hovered){ hovered=c; canvas.style.cursor=c?'pointer':'default'; render(); } })
.on('mouseout', ()=>{ hovered=null; canvas.style.cursor='default'; render(); })
.on('click', (ev)=>{
const c=countryAt(ev); if(!c) return;
selected = selected && selected.id===c.id ? null : c;
window.SELECTED_COUNTRY_ID = selected ? selected.id : null;
document.getElementById('country-id').textContent = window.SELECTED_COUNTRY_ID ?? 'None';
render();
});
/* Render ------------------------------------------------------------- */
function render () {
if(!countries.length) return;
const w=canvas.width/dpr, h=canvas.height/dpr;
ctx.save(); ctx.clearRect(0,0,w,h);
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--water');
ctx.fillRect(0,0,w,h);
ctx.translate(currentT.x, currentT.y); ctx.scale(currentT.k, currentT.k);
ctx.beginPath(); path({type:'FeatureCollection', features:countries});
ctx.fillStyle=getComputedStyle(document.documentElement).getPropertyValue('--country'); ctx.fill();
ctx.strokeStyle=getComputedStyle(document.documentElement).getPropertyValue('--stroke'); ctx.lineWidth=.18; ctx.stroke();
if(hovered){ ctx.beginPath(); path(hovered); ctx.fillStyle=getComputedStyle(document.documentElement).getPropertyValue('--country-hover'); ctx.fill(); }
if(selected){ ctx.beginPath(); path(selected); ctx.fillStyle=getComputedStyle(document.documentElement).getPropertyValue('--country-selected'); ctx.fill(); }
ctx.restore();
}
/* Resize listener ---------------------------------------------------- */
window.addEventListener('resize', ()=>{ if(window.devicePixelRatio!==dpr) dpr=window.devicePixelRatio||1; resizeCanvas(); });
})();
</script></body>
</html>