Skip to main content

leaflet-canvas-choropleth

Patrón completo de choropleth interactivo con Leaflet + Canvas renderer. Miles de polígonos sin lag. 4 escalas de color, pane system, lazy dataset switching. Base para dashboards geodatos vanilla JS.

Aller à l'installation

Informations de source

Dépôt
Ntizar/NtizarBrainMasterMind
Dernière activité de la source
30 juin 2026 à 12:46
Langue détectée de SKILL.md
espagnol
Étoiles
2
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
leaflet-canvas-choropleth
description
Patrón completo de choropleth interactivo con Leaflet + Canvas renderer. Miles de polígonos sin lag. 4 escalas de color, pane system, lazy dataset switching. Base para dashboards geodatos vanilla JS.
version
1.0.0
author
Mastermind
tags
["leaflet","choropleth","canvas","geodata","map","vanilla-js","dashboard"]
source
espanatlas.es
# Leaflet Canvas Choropleth — Patrón de Referencia ## Cuándo usar - Dashboard con mapa de áreas coloreadas por datos (municipios, barrios, países) - >200 polígonos (Canvas obligatorio) - Cambio de métrica en tiempo real - Datos vanilla JS sin frameworks ## Stack mínimo ```html <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js"></script> ``` ## 1. Mapa base con Canvas renderer ```javascript // CRÍTICO: preferCanvas:true + renderer: L.canvas() const map = L.map('map', { center: [40.1, -3.7], zoom: 6, preferCanvas: true, // Canvas por defecto zoomSnap: 0.25, zoomDelta: 0.25, wheelPxPerZoomLevel: 80 // zoom más suave con rueda }); // Basemap dark L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', { attribution: '© CartoDB © OSM', maxZoom: 19, subdomains: 'abcd' }).addTo(map); ``` ## 2. Pane system (z-index controlado) ```javascript // Labels siempre encima del choropleth map.createPane('labelsPane'); map.getPane('labelsPane').style.zIndex = 450; map.getPane('labelsPane').style.pointerEvents = 'none'; L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}{r}.png', { pane: 'labelsPane', attribution: '', maxZoom: 19, subdomains: 'abcd' }).addTo(map); ``` **Por qué:** Sin panes, las etiquetas del basemap quedan debajo del choropleth. Con pane, controlas el orden de capas independientemente del orden de inserción. ## 3. 4 escalas de color ```javascript // Escala RdBu divergente (ColorBrewer 9 paradas) const STOPS = [ [5,48,97],[33,102,172],[67,147,195],[146,197,222],[247,247,247], [244,165,130],[214,96,77],[178,24,43],[103,0,31] ]; // 1. DIVERGENTE: azul → blanco → rojo (variaciones porcentuales) function getColor(val, lo, hi) { if (val == null) return '#1e293b'; const absMax = Math.max(Math.abs(lo), Math.abs(hi), 0.001); const t = Math.max(-1, Math.min(1, val / absMax)); const s = (t + 1) / 2; // normalizar -1..1 → 0..1 const idx = s * (STOPS.length - 1); const li = Math.floor(idx), hi_i = Math.min(li + 1, STOPS.length - 1); const f = idx - li; const [r, g, b] = [0,1,2].map(i => Math.round(STOPS[li][i] + (STOPS[hi_i][i] - STOPS[li][i]) * f) ); return `rgb(${r},${g},${b})`; } // 2. SECUENCIAL: blanco → rojo (tasas absolutas) function getColorSeq(val, lo, hi) { if (val == null) return '#1e293b'; const t = Math.max(0, Math.min(1, (val - lo) / (hi - lo || 1))); const idx = 4 + t * 4; // usa stops 4→8 (blanco→rojo) const li = Math.floor(idx), hi_i = Math.min(li + 1, STOPS.length - 1); const f = idx - li; const [r, g, b] = [0,1,2].map(i => Math.round(STOPS[li][i] + (STOPS[hi_i][i] - STOPS[li][i]) * f) ); return `rgb(${r},${g},${b})`; } // 3. SCORE: rojo → verde (0-100, scores/índices) function getColorScore(val) { if (val == null || !isFinite(val)) return '#1e293b'; const t = Math.max(0, Math.min(1, Number(val) / 100)); const stops = [[220,38,38],[249,115,22],[245,158,11],[132,204,22],[22,163,74]]; const idx = t * (stops.length - 1); const li = Math.floor(idx), hi_i = Math.min(li + 1, stops.length - 1); const f = idx - li; const [r,g,b] = [0,1,2].map(i => Math.round(stops[li][i] + (stops[hi_i][i] - stops[li][i]) * f) ); return `rgb(${r},${g},${b})`; } // 4. RISK: verde → rojo oscuro (0-100, riesgos) function getColorRisk(val) { if (val == null || !isFinite(val)) return '#1e293b'; const t = Math.max(0, Math.min(1, Number(val) / 100)); const stops = [[22,163,74],[132,204,22],[245,158,11],[249,115,22],[220,38,38],[127,29,29]]; const idx = t * (stops.length - 1); const li = Math.floor(idx), hi_i = Math.min(li + 1, stops.length - 1); const f = idx - li; const [r,g,b] = [0,1,2].map(i => Math.round(stops[li][i] + (stops[hi_i][i] - stops[li][i]) * f) ); return `rgb(${r},${g},${b})`; } // Percentil para rangos robustos function pct95(arr, p) { const s = arr.filter(v => v != null).sort((a,b) => a-b); return s[Math.floor(s.length * p / 100)] ?? 0; } ``` ## 4. Categorías con colores fijos ```javascript const CAT_COLORS = { elecciones: { PSOE:'#ef4444', PP:'#2563eb', VOX:'#65a30d', SUMAR:'#ec4899' }, tipos: { A:'#f87171', B:'#f97316', C:'#fbbf24', D:'#94a3b8', E:'#34d399' }, }; // Función para buscar color categórico function categoricalColor(metric, value) { return CAT_COLORS[metric]?.[value] || '#94a3b8'; } ``` ## 5. GeoJSON layer con Canvas + estilo dinámico ```javascript let geoLayer, curMetric = 'variacion', lo = 0, hi = 0; function styleF(feat) { const d = feat.properties.data || {}; let fill; if (curMetric in CAT_COLORS) { fill = categoricalColor(curMetric, d[curMetric]); } else if (curMetric.includes('score') || curMetric.includes('index')) { fill = getColorScore(d[curMetric]); } else if (curMetric.includes('risk') || curMetric.includes('flood')) { fill = getColorRisk(d[curMetric]); } else { fill = getColor(d[curMetric], lo, hi); // divergente por defecto } return { fillColor: fill, fillOpacity: d.cod ? 0.83 : 0.07, color: '#0f172a', weight: 0.35, opacity: 0.8 }; } // Crear layer una sola vez geoLayer = L.geoJSON(geoData, { style: styleF, onEachFeature: (feat, layer) => { layer.on('mouseover', () => layer.setStyle({ weight: 2, color: '#c7d2fe', fillOpacity: 0.95 })); layer.on('mouseout', () => geoLayer.resetStyle(layer)); layer.on('click', () => showMunicipalityInfo(feat.properties.cod)); }, renderer: L.canvas() // ← CRÍTICO para rendimiento }).addTo(map); ``` ## 6. Cambio de métrica SIN reconstruir (el truco clave) ```javascript // NO HACER (lento): // map.removeLayer(geoLayer); // geoLayer = L.geoJSON(nuevoGeo, { style: styleF }).addTo(map); // SÍ HACER (10ms): async function updateMap() { curMetric = document.getElementById('metric-select').value; // Cargar dataset si no está cacheado const ds = datasetForMetric(curMetric); if (DATASET_STATUS[ds] !== 'loaded') { await loadDataset(ds); } // Calcular rangos const vals = Object.values(IDX).map(d => d[curMetric]).filter(Number.isFinite); lo = pct95(vals, 5); hi = pct95(vals, 95); // RE-COLOREAR sin tocar geometría ← ¡la magia! geoLayer.setStyle(styleF); // Actualizar leyenda updateLegend(); } ``` ## 7. Tooltip informativo ```javascript function onEachFeature(feat, layer) { layer.bindTooltip(() => { const d = feat.properties; return `<b>${d.nom}</b><br>${curMetric}: ${formatValue(d[curMetric])}`; }, { sticky: true, className: 'custom-tooltip' }); } ``` ## Pitfalls 1. **Canvas + interactividad:** Los eventos de mouse en Canvas son por bounding box, no por forma exacta. Para polígonos muy pequeños, usar `tolerance` en `L.canvas({tolerance: 5})` 2. **No recrear el layer:** `setStyle()` es 50x más rápido que destruir+crear 3. **Percentiles, no min/max:** Usar pct5/pct95 para rangos, no min/max (outliers rompen la escala) 4. **Liberar memoria TopoJSON:** `topo = null` tras `topojson.feature()` 5. **Canvas no soporta CSS:** Los estilos CSS no funcionan en Canvas renderer 6. **clientWidth=0 en tabs lazy:** Cuando un contenedor está oculto (`display:none`), `container.clientWidth` es 0 → invisibles. Fix: `Math.max(600, container.clientWidth)` o `requestAnimationFrame` + retry. 7. **GeoJSON property names:** SpainLayers usa `id`/`name`, no `CODMUN`/`NOMBRE`. Siempre verificar y mapear propiedades al cargar. 8. **❌ NUNCA circle markers para choropleth** — David: "los cp en vez de puntos deberían ser áreas". Polígonos reales SIEMPRE. Si no hay GeoJSON, generar centroides con Douglas-Peucker como último recurso. 9. **❌ NUNCA dark theme para dashboards públicos** — David odia fondos oscuros y neón. Fondo claro, sidebar blanca, acentos en borders. 10. **❌ NUNCA "liquid glass" genérico** — backdrop-filter masivo = "diseño de IA". Usar cards sólidas con sombra sutil. ## GeoJSON de municipios españoles — SpainLayers ### Fuente fiable: `AlexGPlay/SpainLayers` Tras probar 15+ fuentes (datos.comunidad.madrid, opendata-pmm, semmler23, Fonsloper, GADM, etc.), la **única fuente que funciona** para GeoJSON de municipios españoles a nivel provincial: ``` https://raw.githubusercontent.com/AlexGPlay/SpainLayers/master/municipalities/{PROVINCIA_CODE}.geojson ``` - **Madrid:** `28.geojson` → 179 municipios, ~5.9MB - **Barcelona:** `08.geojson` - **Valencia:** `46.geojson` - **Código provincia:** 2 dígitos (01-52 + 57) ### Propiedades (¡OJO al mapeo!) ```json { "type": "Feature", "properties": { "id": "28079", // ← usa "id", NO "CODMUN" "name": "Madrid" // ← usa "name", NO "NOMBRE" }, "geometry": { "type": "MultiPolygon", "coordinates": [...] } } ``` **Pitfall crítico:** Si tu dataset usa `CODMUN` o `NOMBRE`, hay que mapear: ```javascript // Mapear propiedades al cargar geo.features.forEach(f => { f.properties.CODMUN = f.properties.id; f.properties.NOMBRE = f.properties.name; }); ```
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub