/* Mapa de calor de comunas (Valparaíso, RM y O'Higgins). * * La geometría se sirve desde /geo y se baja una sola vez por sesión: son * 241 KB que no cambian nunca. Se dibuja como SVG inline, sin librerías, para * que funcione igual en el monitor de supervisores y en el tablero de guías. * * Uso: * MapaComunas.dibujar(contenedor, comunas, { campo: 'n' }) * donde `comunas` es la lista que arma graficos.resumir() en "comunas_todas": * cada item con { clave, nombre, n, monto, entregadas, rechazadas }. */ window.MapaComunas = (function () { const URL_GEO = '/geo/zona_centro_comunas.geojson'; // Escala secuencial: el tono más oscuro es donde más se concentra const PALETA = ['#cfe3f5', '#9dc7e8', '#68a5d5', '#3a7fbd', '#1b5a95', '#0b3c6b']; let geo = null; let cargando = null; // Misma normalización que graficos.py: sin tildes, en mayúsculas y con los // espacios colapsados. Si las dos no calzan, la comuna se pinta como si no // tuviera entregas. const clave = s => (s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '') .toUpperCase().replace(/\s+/g, ' ').trim(); const esca = s => String(s === null || s === undefined ? '' : s) .replace(/[&<>"]/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[m])); /* Contenido de la etiqueta flotante de una comuna. * * Vive acá y no en cada pantalla para que el tablero de guías y el monitor * de supervisores muestren exactamente lo mismo al pasar el mouse. * `d` es un item de "comunas_todas" y `fmt` formatea moneda. */ function tooltip(d, fmt) { if (!d) return ''; const fila = (etq, val, cls) => `
Cargando mapa…
'; return cargar().then(() => pintar(cont, comunas || [], campo, fmt)) .catch(() => { cont.innerHTML = 'No se pudo cargar el mapa.
'; }); } function pintar(cont, comunas, campo, fmt) { const datos = {}; comunas.forEach(c => { datos[c.clave] = c; }); // Solo las comunas con movimiento. Al dejar fuera las 90 y tantas sin // despacho, el recuadro se ajusta a donde sí se reparte y el mapa se ve // mucho más grande: en un televisor esa diferencia es la que permite // distinguir una comuna de otra. const features = geo.features.filter(f => (datos[clave(f.properties.comuna)] || {})[campo]); if (!features.length) { cont.innerHTML = 'Todavía no hay despachos con comuna.
'; return; } // Proyección equirectangular con corrección por latitud: sin multiplicar // el largo por cos(lat), Chile sale más ancho de lo que es. let minLon = 180, maxLon = -180, minLat = 90, maxLat = -90; const recorrer = (f, cb) => { const g = f.geometry; const polis = g.type === 'Polygon' ? [g.coordinates] : g.coordinates; polis.forEach(p => p.forEach(anillo => anillo.forEach(cb))); }; features.forEach(f => recorrer(f, c => { if (c[0] < minLon) minLon = c[0]; if (c[0] > maxLon) maxLon = c[0]; if (c[1] < minLat) minLat = c[1]; if (c[1] > maxLat) maxLat = c[1]; })); const kLat = Math.cos((minLat + maxLat) / 2 * Math.PI / 180); const ANCHO = 1000; const esc = ANCHO / ((maxLon - minLon) * kLat); const ALTO = Math.round((maxLat - minLat) * esc); const px = c => ((c[0] - minLon) * kLat * esc).toFixed(1) + ' ' + ((maxLat - c[1]) * esc).toFixed(1); // Cortes por cuantiles y no lineales: una comuna concentra cientos de // guías y la mitad tiene menos de diez; con escala lineal el mapa queda // de un solo tono salvo una mancha. const valores = features .map(f => (datos[clave(f.properties.comuna)] || {})[campo] || 0) .filter(v => v > 0).sort((a, b) => a - b); const cortes = []; for (let i = 1; i < PALETA.length && valores.length; i++) { const v = valores[Math.floor(i * valores.length / PALETA.length)]; if (v > (cortes[cortes.length - 1] || 0)) cortes.push(v); } const tono = v => { let i = 0; while (i < cortes.length && v >= cortes[i]) i++; return PALETA[i]; }; const trazos = features.map(f => { const nombre = f.properties.comuna; const d = datos[clave(nombre)]; const val = d ? (d[campo] || 0) : 0; const g = f.geometry; const polis = g.type === 'Polygon' ? [g.coordinates] : g.coordinates; const ruta = polis.map(p => p.map(anillo => 'M' + anillo.map(px).join('L') + 'Z').join('')).join(''); return `