Download frontend/map/interactive-india-map.html from Ndg07/heatmap: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/spaces/Ndg07/heatmap/resolve/main/frontend/map/interactive-india-map.html
- Command line
-
hf download hf://spaces/Ndg07/heatmap/frontend/map/interactive-india-map.html
-
curl -L -o interactive-india-map.html https://huggingface.co/spaces/Ndg07/heatmap/resolve/main/frontend/map/interactive-india-map.html
12.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Interactive India Map</title> | |
| <link rel="icon" type="image/x-icon" href="/Misinfo.ico"> | |
| <style> | |
| :root{--bg:#0f1720;--panel:#0b1220;--accent:#3b82f6} | |
| body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:#0f172a;color:#e2e8f0;margin:0;padding:0;display:flex;height:100vh;overflow:hidden;overflow-x:hidden;max-width:100vw} | |
| #left{flex:1;display:flex;flex-direction:column} | |
| header{padding:12px 16px;background:linear-gradient(90deg,rgba(255,255,255,0.02),transparent);display:flex;gap:12px;align-items:center} | |
| header h1{font-size:18px;margin:0} | |
| #mapwrap{flex:1;overflow:hidden;position:relative} | |
| #svgroot{width:100%;height:100%;touch-action:none;cursor:grab;background:linear-gradient(180deg,#071027, #082035)} | |
| #tooltip{position:absolute;pointer-events:none;padding:8px 10px;background:#fff;color:#041026;border-radius:6px;font-size:13px;box-shadow:0 6px 18px rgba(4,8,16,0.6);display:none} | |
| aside{width:320px;background:var(--panel);border-left:1px solid rgba(255,255,255,0.03);padding:18px;box-sizing:border-box} | |
| .state-name{font-size:20px;font-weight:600;margin-bottom:6px} | |
| .meta{font-size:13px;opacity:0.85} | |
| .controls{display:flex;gap:8px;align-items:center} | |
| .btn{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:6px 8px;border-radius:8px;color:inherit;cursor:pointer} | |
| input[type=search]{flex:1;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);background:rgba(255,255,255,0.02);color:inherit} | |
| .legend{margin-top:12px;font-size:13px} | |
| .hint{margin-top:18px;opacity:0.7;font-size:13px} | |
| /* highlight style for svg paths */ | |
| .map-path{fill:#88A4BC;stroke:#ffffff;stroke-width:0.6} | |
| .map-path:hover{fill:#3B729F} | |
| .selected{fill:var(--accent) } | |
| footer{padding-top:12px;font-size:12px;opacity:0.8} | |
| /* Mobile Responsiveness */ | |
| @media (max-width: 768px) { | |
| body { display: block; } | |
| #left { width: 100%; height: 60vh; } | |
| aside { | |
| width: 100%; | |
| border-left: none; | |
| border-top: 1px solid rgba(255,255,255,0.1); | |
| padding: 12px; | |
| height: 40vh; | |
| overflow-y: auto; | |
| } | |
| header { padding: 8px 12px; } | |
| header h1 { font-size: 16px; } | |
| .controls { gap: 8px; } | |
| input[type=search] { font-size: 14px; } | |
| .btn { font-size: 12px; padding: 4px 6px; } | |
| .state-name { font-size: 18px; } | |
| .meta { font-size: 12px; } | |
| .legend { font-size: 12px; } | |
| .hint { font-size: 12px; } | |
| } | |
| @media (max-width: 480px) { | |
| #left { height: 50vh; } | |
| aside { height: 50vh; padding: 8px; } | |
| header { padding: 6px 8px; } | |
| header h1 { font-size: 14px; } | |
| .controls { flex-direction: column; gap: 6px; } | |
| input[type=search] { padding: 6px; } | |
| .btn { padding: 3px 6px; font-size: 11px; } | |
| .state-name { font-size: 16px; } | |
| .meta { font-size: 11px; } | |
| .legend { font-size: 11px; margin-top: 8px; } | |
| .hint { font-size: 11px; margin-top: 12px; } | |
| footer { font-size: 10px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="left"> | |
| <header> | |
| <h1>Interactive India Map</h1> | |
| <div class="controls" style="flex:1"> | |
| <input id="search" type="search" placeholder="Search state (e.g. Karnataka)" /> | |
| <button id="reset" class="btn">Reset</button> | |
| </div> | |
| </header> | |
| <div id="mapwrap"> | |
| <div id="svgroot">Loading map...</div> | |
| <div id="tooltip"></div> | |
| </div> | |
| </div> | |
| <aside> | |
| <div class="state-name" id="stateName">Click a state</div> | |
| <div class="meta" id="stateDesc">State description will appear here. Hover or click a state on the map.</div> | |
| <div class="legend" id="legend"></div> | |
| <div class="hint">Use mouse wheel to zoom, drag to pan. On mobile: pinch to zoom (if supported).</div> | |
| <footer>Files required: <code>in.svg</code> and <code>mapdata.js</code> in same folder.</footer> | |
| </aside> | |
| <script> | |
| // Basic utility and UI | |
| const svgRoot = document.getElementById('svgroot'); | |
| const tooltip = document.getElementById('tooltip'); | |
| const stateNameEl = document.getElementById('stateName'); | |
| const stateDescEl = document.getElementById('stateDesc'); | |
| const searchInput = document.getElementById('search'); | |
| const resetBtn = document.getElementById('reset'); | |
| // We'll load the SVG file (in.svg) and inject it so we can attach listeners | |
| async function loadSVG(){ | |
| try{ | |
| const res = await fetch('/assets/map/in.svg'); | |
| if(!res.ok) throw new Error('SVG not found. Put in.svg beside this HTML file.'); | |
| const text = await res.text(); | |
| svgRoot.innerHTML = text; | |
| initMap(); | |
| }catch(e){ | |
| svgRoot.innerHTML = '<div style="padding:18px;color:salmon">Error loading in.svg: '+e.message+'</div>'; | |
| } | |
| } | |
| // Load optional mapdata.js (user provided). It should define simplemaps_countrymap_mapdata | |
| function loadMapData(){ | |
| return new Promise((resolve)=>{ | |
| if(window.simplemaps_countrymap_mapdata){ | |
| resolve(window.simplemaps_countrymap_mapdata); | |
| }else{ | |
| // try to dynamically load mapdata.js | |
| const s = document.createElement('script'); | |
| s.src = 'mapdata.js'; | |
| s.onload = ()=> resolve(window.simplemaps_countrymap_mapdata || {}); | |
| s.onerror = ()=> resolve({}); | |
| document.head.appendChild(s); | |
| } | |
| }) | |
| } | |
| // Map interaction state (pan/zoom) | |
| let scale = 1, tx = 0, ty = 0; | |
| let isPanning = false, startX=0, startY=0; | |
| let svgEl; | |
| let mapData = {}; | |
| let selectedEl = null; | |
| function initMap(){ | |
| // find the actual <svg> | |
| svgEl = svgRoot.querySelector('svg'); | |
| if(!svgEl){ svgRoot.innerHTML = '<div style="padding:18px;color:salmon">in.svg does not contain an <svg> root.</div>'; return; } | |
| // wrap paths with class map-path for styling | |
| const paths = svgEl.querySelectorAll('path, polygon, rect, circle'); | |
| paths.forEach(p => { | |
| if(!p.id) return; | |
| p.classList.add('map-path'); | |
| }); | |
| // initial transform group | |
| let g = document.createElementNS('http://www.w3.org/2000/svg','g'); | |
| // move all children into g | |
| while(svgEl.firstChild){ g.appendChild(svgEl.firstChild); } | |
| svgEl.appendChild(g); | |
| svgEl.dataset.viewbox = svgEl.getAttribute('viewBox') || ''; | |
| // pan/zoom handlers | |
| svgRoot.addEventListener('wheel', e=>{ | |
| e.preventDefault(); | |
| const delta = -e.deltaY * 0.001; | |
| const oldScale = scale; | |
| scale = Math.min(8, Math.max(0.5, scale * (1 + delta))); | |
| // zoom towards cursor | |
| const rect = svgRoot.getBoundingClientRect(); | |
| const cx = e.clientX - rect.left; | |
| const cy = e.clientY - rect.top; | |
| tx -= (cx/oldScale - cx/scale); | |
| ty -= (cy/oldScale - cy/scale); | |
| updateTransform(); | |
| }, {passive:false}); | |
| svgRoot.addEventListener('mousedown', e=>{ | |
| isPanning = true; startX = e.clientX; startY = e.clientY; svgRoot.style.cursor='grabbing'; | |
| }); | |
| window.addEventListener('mousemove', e=>{ | |
| if(!isPanning) return; | |
| tx += (e.clientX - startX)/scale; ty += (e.clientY - startY)/scale; startX = e.clientX; startY = e.clientY; updateTransform(); | |
| }); | |
| window.addEventListener('mouseup', ()=>{ isPanning=false; svgRoot.style.cursor='grab'; }); | |
| // load map data and attach state listeners | |
| loadMapData().then(md=>{ | |
| mapData = md || {}; | |
| attachStateHandlers(); | |
| }); | |
| // pointer hover for tooltip | |
| svgRoot.addEventListener('mousemove', e=>{ | |
| const t = e.target; | |
| if(t && t.classList && t.classList.contains('map-path')){ | |
| const id = t.id; | |
| const info = lookupStateInfo(id); | |
| showTooltip(e.clientX, e.clientY, info.name+(info.desc?('\n'+info.desc):'')); | |
| } else { hideTooltip(); } | |
| }); | |
| svgRoot.addEventListener('mouseleave', hideTooltip); | |
| // click to select | |
| svgRoot.addEventListener('click', e=>{ | |
| const t = e.target; | |
| if(t && t.classList && t.classList.contains('map-path')){ | |
| selectState(t.id, t); | |
| } | |
| }); | |
| // search | |
| searchInput.addEventListener('keydown', e=>{ if(e.key==='Enter') doSearch(); }); | |
| resetBtn.addEventListener('click', resetView); | |
| } | |
| function updateTransform(){ | |
| const g = svgEl.querySelector('g'); | |
| g.setAttribute('transform', `translate(${tx} ${ty}) scale(${scale})`); | |
| } | |
| function showTooltip(cx, cy, html){ | |
| tooltip.style.display='block'; | |
| tooltip.innerText = html; | |
| const mapRect = svgRoot.getBoundingClientRect(); | |
| const left = Math.min(mapRect.width - 180, Math.max(8, cx - mapRect.left + 12)); | |
| const top = Math.max(8, cy - mapRect.top + 12); | |
| tooltip.style.left = left+'px'; tooltip.style.top = top+'px'; | |
| } | |
| function hideTooltip(){ tooltip.style.display='none'; } | |
| function lookupStateInfo(id){ | |
| const empty = {name:id || 'Unknown', desc:''}; | |
| if(!id) return empty; | |
| if(mapData && mapData.state_specific && mapData.state_specific[id]){ | |
| const s = mapData.state_specific[id]; | |
| return {name: s.name || id, desc: (s.description && s.description!=='default')?s.description:''}; | |
| } | |
| // fallback: try matching by common names | |
| return {name: id, desc:''}; | |
| } | |
| function selectState(id, el){ | |
| if(selectedEl) selectedEl.classList.remove('selected'); | |
| selectedEl = el; | |
| selectedEl.classList.add('selected'); | |
| const info = lookupStateInfo(id); | |
| stateNameEl.innerText = info.name || id; | |
| stateDescEl.innerText = info.desc || (mapData.main_settings && mapData.main_settings.state_description) || 'No description available.'; | |
| } | |
| function attachStateHandlers(){ | |
| // populate legend from mapData if available | |
| if(mapData && mapData.legend && Array.isArray(mapData.legend.entries) && mapData.legend.entries.length){ | |
| const legend = document.getElementById('legend'); | |
| legend.innerHTML = '<strong>Legend</strong><br>' + mapData.legend.entries.join(', '); | |
| } | |
| // add pointer cursors | |
| const paths = svgEl.querySelectorAll('.map-path'); | |
| paths.forEach(p=>{ | |
| p.style.cursor='pointer'; | |
| // attempt to set title attribute for accessibility | |
| const info = lookupStateInfo(p.id); | |
| p.setAttribute('data-name', info.name||p.id); | |
| // if SVG path has no fill set or fill=none, set a default fill | |
| if(!p.getAttribute('fill') || p.getAttribute('fill')==='none'){ | |
| p.setAttribute('fill','#88A4BC'); | |
| } | |
| }); | |
| } | |
| function doSearch(){ | |
| const q = searchInput.value.trim().toLowerCase(); | |
| if(!q) return; | |
| // try to find a state in mapdata | |
| const m = mapData.state_specific || {}; | |
| for(const k of Object.keys(m)){ | |
| const name = (m[k].name||'').toLowerCase(); | |
| if(name.includes(q) || k.toLowerCase()===q){ | |
| // find element with id k | |
| const el = svgEl.querySelector('#'+CSS.escape(k)); | |
| if(el){ | |
| // center view on bbox | |
| const bbox = el.getBBox(); | |
| // simple centering: move translate so bbox is roughly center | |
| const rect = svgRoot.getBoundingClientRect(); | |
| const cx = bbox.x + bbox.width/2; const cy = bbox.y + bbox.height/2; | |
| // compute desired tx/ty so that cx,cy appear near center of container | |
| const targetScreenX = rect.width/2; const targetScreenY = rect.height/2; | |
| tx = targetScreenX/scale - cx; ty = targetScreenY/scale - cy; | |
| updateTransform(); | |
| selectState(k, el); | |
| return; | |
| } | |
| } | |
| } | |
| alert('No state found for "'+q+'"'); | |
| } | |
| function resetView(){ scale = 1; tx = 0; ty = 0; updateTransform(); if(selectedEl) { selectedEl.classList.remove('selected'); selectedEl=null; stateNameEl.innerText='Click a state'; stateDescEl.innerText='State description will appear here.'} } | |
| // initial load | |
| loadSVG(); | |
| </script> | |
| </body> | |
| </html> | |