Mapa Madrid

Dashboard Demográfico Madrid * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: ‘Segoe UI’, Tahoma, Geneva, Verdana, sans-serif; background: #f5f5f5; } .container { max-width: 1800px; margin: 0 auto; padding: 20px; } header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 10px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } h1 { font-size: 2.5em; margin-bottom: 10px; } .subtitle { font-size: 1.1em; opacity: 0.9; } .stats-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 20px; } .summary-card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; transition: transform 0.2s; } .summary-card:hover { transform: translateY(-2px); } .summary-value { font-size: 2em; font-weight: bold; color: #667eea; } .summary-label { font-size: 0.85em; color: #666; margin-top: 5px; } .filter-panel { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; } .filter-grid { display: grid; grid-template-columns: 2fr 2fr 1fr; gap: 15px; align-items: end; } .filter-group { display: flex; flex-direction: column; } .filter-group label { font-weight: bold; color: #333; margin-bottom: 5px; font-size: 0.9em; } .filter-group select { padding: 10px; border: 2px solid #667eea; border-radius: 6px; font-size: 0.95em; background: white; cursor: pointer; } .btn-reset { padding: 11px 20px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.95em; font-weight: bold; transition: background 0.2s; } .btn-reset:hover { background: #5568d3; } .dashboard-grid { display: grid; grid-template-columns: 350px 1fr; gap: 20px; } .stats-panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-height: 850px; overflow-y: auto; } .map-container { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } #map { height: 750px; border-radius: 8px; border: 2px solid #e0e0e0; } .legend { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-top: 15px; border: 1px solid #e0e0e0; } .legend-title { font-weight: bold; margin-bottom: 10px; color: #333; font-size: 0.95em; } .legend-item { display: flex; align-items: center; margin-bottom: 7px; font-size: 0.82em; } .legend-color { width: 22px; height: 16px; border-radius: 3px; margin-right: 8px; border: 1px solid #ddd; } .section-list { margin-top: 15px; max-height: 450px; overflow-y: auto; } .section-item { padding: 10px; background: #f8f9fa; margin-bottom: 7px; border-radius: 6px; cursor: pointer; transition: all 0.2s; border-left: 4px solid #667eea; } .section-item:hover { background: #e9ecef; transform: translateX(5px); } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } .section-code { font-weight: bold; color: #667eea; font-size: 0.9em; } .section-stats { font-size: 0.75em; color: #666; } .info-box { padding: 15px; background: #e3f2fd; border-left: 4px solid #2196F3; border-radius: 4px; margin-bottom: 20px; font-size: 0.92em; } .loading { text-align: center; padding: 40px; color: #667eea; font-size: 1.2em; } .loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid #667eea; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } footer { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 20px; text-align: center; } .footer-content { color: #666; font-size: 0.9em; } .footer-author { font-weight: bold; color: #667eea; margin-top: 10px; font-size: 1.1em; } .footer-divider { width: 60px; height: 3px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); margin: 15px auto; border-radius: 2px; } .error-message { background: #fee; border-left: 4px solid #f44; padding: 15px; margin: 20px 0; border-radius: 4px; color: #c00; } @media (max-width: 1200px) { .filter-grid { grid-template-columns: 1fr; } .dashboard-grid { grid-template-columns: 1fr; } #map { height: 500px; } }

📊 Dashboard Demográfico de Madrid

Todas las Secciones Censales | 21 Distritos | 2,450 Secciones con Datos
3,339,931
Habitantes
1,332,275
Hogares
2.51
Tamaño Medio
2,450
Secciones Visibles
🏛️ Filtrar por Distrito: 📍 Todos los Distritos (Madrid Completo) 01. Centro 02. Arganzuela 03. Retiro 04. Salamanca 05. Chamartín 06. Tetuán 07. Chamberí 08. Fuencarral-El Pardo 09. Moncloa-Aravaca 10. Latina 11. Carabanchel 12. Usera 13. Puente de Vallecas 14. Moratalaz 15. Ciudad Lineal 16. Hortaleza 17. Villaverde 18. Villa de Vallecas 19. Vicálvaro 20. San Blas-Canillejas 21. Barajas
📊 Métrica a Visualizar: Habitantes Total Hogares Tamaño Medio del Hogar Hogares de 1 Persona Hogares de 2 Personas Hogares de 3 Personas Hogares de 4 Personas Hogares de 5 o Más Mujer Sola 16-64 años Hombre Solo 16-64 años Mujer Sola 65+ años Hombre Solo 65+ años Mujer con Menores Hombre con Menores Dos Adultos + 1 Menor Dos Adultos + 2 Menores
ℹ️ Instrucciones: Selecciona un distrito para filtrar el mapa. Elige una métrica para visualizarla con colores. Haz clic en cualquier sección para ver detalles. | Mostrando: Madrid Completo
🎨 Leyenda del Mapa
Cargando…

📋 Secciones (0)

Cargando secciones…

🗺️ Mapa Interactivo de Madrid

Cargando mapa…
https://unpkg.com/leaflet@1.9.4/dist/leaflet.js // Variables globales let datosCompletos = {}; let madridGeoJSON = null; let map = null; let sectionsLayer = null; let metricaActual = ‘habitantes’; let distritoActual = ‘todos’; let seccionesFiltradas = []; // Función para mostrar errores function mostrarError(mensaje) { const errorContainer = document.getElementById(‘error-container’); errorContainer.innerHTML = `
⚠️ Error: ${mensaje}
Por favor, asegúrate de que los archivos datos_madrid_completo.json y madrid_completo.geojson están en la misma carpeta que este HTML.
`; } // Función para cargar JSON async function cargarDatos() { try { console.log(‘Cargando datos JSON…’); const response = await fetch(‘datos_madrid_completo.json’); if (!response.ok) throw new Error(‘No se pudo cargar datos_madrid_completo.json’); datosCompletos = await response.json(); console.log(‘✓ Datos cargados:’, Object.keys(datosCompletos).length, ‘secciones’); return true; } catch (error) { console.error(‘Error cargando datos:’, error); mostrarError(‘No se pudo cargar el archivo datos_madrid_completo.json. ‘ + error.message); return false; } } // Función para cargar GeoJSON async function cargarGeoJSON() { try { console.log(‘Cargando GeoJSON…’); const response = await fetch(‘madrid_completo.geojson’); if (!response.ok) throw new Error(‘No se pudo cargar madrid_completo.geojson’); madridGeoJSON = await response.json(); console.log(‘✓ GeoJSON cargado:’, madridGeoJSON.features.length, ‘features’); return true; } catch (error) { console.error(‘Error cargando GeoJSON:’, error); mostrarError(‘No se pudo cargar el archivo madrid_completo.geojson. ‘ + error.message); return false; } } // Función para obtener valor de métrica function getValor(codigo, metrica) { const d = datosCompletos[codigo]; if (!d) return null; if (metrica === ‘habitantes’) return d.habitantes; if (metrica === ‘total_hogares’) return d.total_hogares; if (metrica === ‘tamano_medio_hogar’) return d.tamano_medio_hogar; if (metrica === ‘hogares_1persona’) return d.hogares_por_tamano[‘1’]; if (metrica === ‘hogares_2personas’) return d.hogares_por_tamano[‘2’]; if (metrica === ‘hogares_3personas’) return d.hogares_por_tamano[‘3’]; if (metrica === ‘hogares_4personas’) return d.hogares_por_tamano[‘4’]; if (metrica === ‘hogares_5omas’) { return Object.entries(d.hogares_por_tamano) .filter(([k]) => parseInt(k) >= 5 || k === ’15_y_mas’) .reduce((sum, [, v]) => sum + v, 0); } if (d.composicion_hogar && d.composicion_hogar[metrica] !== undefined) { return d.composicion_hogar[metrica]; } return null; } // Calcular percentiles function calcularPercentiles(valores) { const sorted = valores.filter(v => v !== null && !isNaN(v)).sort((a, b) => a – b); if (sorted.length === 0) return { p20: 0, p40: 0, p60: 0, p80: 0 }; return { p20: sorted[Math.floor(sorted.length * 0.2)], p40: sorted[Math.floor(sorted.length * 0.4)], p60: sorted[Math.floor(sorted.length * 0.6)], p80: sorted[Math.floor(sorted.length * 0.8)] }; } // Función de color function getColor(valor, metrica) { if (valor === null || valor === undefined || isNaN(valor)) return ‘#cccccc’; const valores = seccionesFiltradas.map(k => getValor(k, metrica)); const p = calcularPercentiles(valores); if (valor >= p.p80) return ‘#d73027’; if (valor >= p.p60) return ‘#fc8d59’; if (valor >= p.p40) return ‘#fee090’; if (valor >= p.p20) return ‘#91cf60’; return ‘#1a9850’; } // Estilo del polígono function getStyle(feature) { const codigo = feature.properties.codigo; // Si hay filtro de distrito y no coincide, gris claro if (distritoActual !== ‘todos’ && !codigo.startsWith(distritoActual)) { return { fillColor: ‘#f0f0f0’, weight: 1, opacity: 0.3, color: ‘#ccc’, fillOpacity: 0.2 }; } return { fillColor: getColor(getValor(codigo, metricaActual), metricaActual), weight: 2, opacity: 1, color: ‘white’, fillOpacity: 0.7 }; } // Crear popup function crearPopup(codigo) { const d = datosCompletos[codigo]; if (!d) return ‘

Sin datos disponibles

‘; const comp = d.composicion_hogar || {}; const h5mas = Object.entries(d.hogares_por_tamano || {}) .filter(([k]) => parseInt(k) >= 5 || k === ’15_y_mas’) .reduce((sum, [, v]) => sum + v, 0); const unipersonales = (comp.mujer_sola_16_64 || 0) + (comp.hombre_solo_16_64 || 0) + (comp.mujer_sola_65_mas || 0) + (comp.hombre_solo_65_mas || 0); const monoparentales = (comp.mujer_con_menores || 0) + (comp.hombre_con_menores || 0); let html = `

Sección ${codigo}

📊 Datos Generales
Habitantes:${d.habitantes.toLocaleString()}
Hogares:${d.total_hogares.toLocaleString()}
Tamaño medio:${d.tamano_medio_hogar.toFixed(2)}
👥 Por Tamaño
1 pers:${d.hogares_por_tamano[‘1’] || 0} 2 pers:${d.hogares_por_tamano[‘2’] || 0}
3 pers:${d.hogares_por_tamano[‘3’] || 0} 4 pers:${d.hogares_por_tamano[‘4’] || 0}
5+ pers:${h5mas}
`; if (Object.keys(comp).length > 0) { html += ` 🏠 Composición
Unipersonales (${unipersonales})
Mujer 16-64:${comp.mujer_sola_16_64 || 0}
Hombre 16-64:${comp.hombre_solo_16_64 || 0}
Mujer 65+:${comp.mujer_sola_65_mas || 0}
Hombre 65+:${comp.hombre_solo_65_mas || 0}
Monoparentales (${monoparentales})
Mujer + menores:${comp.mujer_con_menores || 0}
Hombre + menores:${comp.hombre_con_menores || 0}
`; } html += ‘
‘; return html; } // Filtrar secciones function filtrarSecciones() { if (distritoActual === ‘todos’) { seccionesFiltradas = Object.keys(datosCompletos); } else { seccionesFiltradas = Object.keys(datosCompletos).filter(k => k.startsWith(distritoActual)); } document.getElementById(‘secciones-filtradas’).textContent = seccionesFiltradas.length.toLocaleString(); document.getElementById(‘contador-secciones’).textContent = `(${seccionesFiltradas.length})`; } // Cargar capa en el mapa function cargarCapa() { if (!madridGeoJSON || !map) return; if (sectionsLayer) { map.removeLayer(sectionsLayer); } sectionsLayer = L.geoJSON(madridGeoJSON, { style: getStyle, onEachFeature: function(feature, layer) { const codigo = feature.properties.codigo; // Solo activar interacción si está en el filtro if (distritoActual === ‘todos’ || codigo.startsWith(distritoActual)) { layer.bindPopup(crearPopup(codigo)); layer.on(‘mouseover’, function() { this.setStyle({ weight: 4, color: ‘#333’, fillOpacity: 0.9 }); }); layer.on(‘mouseout’, function() { sectionsLayer.resetStyle(this); }); } } }).addTo(map); // Ajustar zoom if (distritoActual !== ‘todos’) { const bounds = L.latLngBounds(); sectionsLayer.eachLayer(function(layer) { if (layer.feature.properties.codigo.startsWith(distritoActual)) { bounds.extend(layer.getBounds()); } }); if (bounds.isValid()) { map.fitBounds(bounds, { padding: [50, 50] }); } } else { map.setView([40.4168, -3.7038], 11); } } // Actualizar leyenda function actualizarLeyenda() { const valores = seccionesFiltradas.map(k => getValor(k, metricaActual)); const p = calcularPercentiles(valores); const fmt = (n) => metricaActual === ‘tamano_medio_hogar’ ? n.toFixed(2) : Math.round(n); document.getElementById(‘legend-content’).innerHTML = `
Muy Alto (≥ ${fmt(p.p80)})
Alto (${fmt(p.p60)} – ${fmt(p.p80)})
Medio (${fmt(p.p40)} – ${fmt(p.p60)})
Bajo (${fmt(p.p20)} – ${fmt(p.p40)})
Muy Bajo (< ${fmt(p.p20)})
`; } // Generar lista de secciones function generarListaSecciones() { const lista = document.getElementById(‘section-list’); lista.innerHTML = »; const seccionesOrdenadas = seccionesFiltradas.sort(); const maxMostrar = 100; const mostrar = seccionesOrdenadas.slice(0, maxMostrar); if (seccionesOrdenadas.length > maxMostrar) { const info = document.createElement(‘div’); info.style.cssText = ‘padding: 10px; background: #fff3cd; border-radius: 6px; margin-bottom: 10px; font-size: 0.85em; text-align: center;’; info.innerHTML = `Mostrando ${maxMostrar} de ${seccionesOrdenadas.length} secciones`; lista.appendChild(info); } mostrar.forEach(codigo => { const d = datosCompletos[codigo]; if (!d) return; const valor = getValor(codigo, metricaActual); const div = document.createElement(‘div’); div.className = ‘section-item’; const valorFmt = valor !== null ? (metricaActual === ‘tamano_medio_hogar’ ? valor.toFixed(2) : valor.toLocaleString()) : ‘N/A’; div.innerHTML = `
${codigo} ${valorFmt}
${d.habitantes.toLocaleString()} hab. | ${d.total_hogares} hogares
`; div.onclick = function() { sectionsLayer.eachLayer(function(layer) { if (layer.feature.properties.codigo === codigo) { map.fitBounds(layer.getBounds(), { padding: [50, 50] }); setTimeout(() => layer.openPopup(), 300); } }); }; lista.appendChild(div); }); } // Actualizar todo function actualizarTodo() { filtrarSecciones(); cargarCapa(); actualizarLeyenda(); generarListaSecciones(); } // Resetear filtros function resetearFiltros() { document.getElementById(‘distrito-select’).value = ‘todos’; document.getElementById(‘metric-select’).value = ‘habitantes’; distritoActual = ‘todos’; metricaActual = ‘habitantes’; document.getElementById(‘estado-filtro’).textContent = ‘| Mostrando: Madrid Completo’; actualizarTodo(); } // Event listeners document.getElementById(‘distrito-select’).addEventListener(‘change’, function(e) { distritoActual = e.target.value; const texto = distritoActual === ‘todos’ ? ‘Madrid Completo’ : e.target.options[e.target.selectedIndex].text; document.getElementById(‘estado-filtro’).textContent = ‘| Mostrando: ‘ + texto; actualizarTodo(); }); document.getElementById(‘metric-select’).addEventListener(‘change’, function(e) { metricaActual = e.target.value; actualizarTodo(); }); // Inicialización async function inicializar() { console.log(‘Iniciando aplicación…’); // Cargar datos const datosOk = await cargarDatos(); if (!datosOk) return; const geoJsonOk = await cargarGeoJSON(); if (!geoJsonOk) return; // Inicializar mapa map = L.map(‘map’).setView([40.4168, -3.7038], 11); L.tileLayer(‘https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png’, { attribution: ‘© OpenStreetMap contributors’, maxZoom: 19 }).addTo(map); // Primera actualización actualizarTodo(); console.log(‘✓ Dashboard cargado completamente | Creado por David Antizar’); } // Ejecutar al cargar la página window.addEventListener(‘load’, inicializar);