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.

Zur Installation springen

Quellinformationen

Repository
Ntizar/NtizarBrainMasterMind
Letzte Quellaktivität
30. Juni 2026 um 12:46
Erkannte Sprache von SKILL.md
Spanisch
Sterne
2
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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; }); ```
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen