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.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
Ntizar/NtizarBrainMasterMind
آخر نشاط في المصدر
٣٠ يونيو ٢٠٢٦ في ١٢:٤٦
لغة SKILL.md المكتشفة
الإسبانية
النجوم
٢
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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; }); ```
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub