217 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			217 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | // src/components/MapaBsAs.tsx
 | ||
|  | import { useState, useMemo } from 'react'; | ||
|  | import { ComposableMap, Geographies, Geography } from 'react-simple-maps'; | ||
|  | import { Tooltip } from 'react-tooltip'; | ||
|  | import { useQuery } from '@tanstack/react-query'; | ||
|  | import axios from 'axios'; | ||
|  | import type { Feature, Geometry } from 'geojson'; | ||
|  | import { geoCentroid } from 'd3-geo'; // Para calcular el centro de cada partido
 | ||
|  | import { useSpring, animated } from 'react-spring'; // Para animar el zoom
 | ||
|  | 
 | ||
|  | //import geoUrl from '/partidos-bsas.topojson';
 | ||
|  | import './MapaBsAs.css'; | ||
|  | 
 | ||
|  | // --- Interfaces y Tipos ---
 | ||
|  | interface ResultadoMapa { | ||
|  |   partidoId: string; | ||
|  |   agrupacionGanadoraId: string; | ||
|  |   porcentajeGanador: number; // Nueva propiedad desde el backend
 | ||
|  | } | ||
|  | 
 | ||
|  | interface Agrupacion { | ||
|  |   id: string; | ||
|  |   nombre: string; | ||
|  | } | ||
|  | 
 | ||
|  | interface PartidoProperties { | ||
|  |   id: number; | ||
|  |   departamento: string; | ||
|  |   cabecera: string; // Asegúrate de que coincida con tu topojson
 | ||
|  |   provincia: string; | ||
|  | } | ||
|  | 
 | ||
|  | type PartidoGeography = Feature<Geometry, PartidoProperties> & { rsmKey: string }; | ||
|  | 
 | ||
|  | const PALETA_COLORES: { [key: string]: [number, number, number] } = { | ||
|  |   'default': [214, 214, 218] // RGB para el color por defecto
 | ||
|  | }; | ||
|  | 
 | ||
|  | const INITIAL_PROJECTION = { | ||
|  |   center: [-59.8, -37.0] as [number, number], | ||
|  |   scale: 5400, | ||
|  | }; | ||
|  | 
 | ||
|  | const MapaBsAs = () => { | ||
|  |   const [selectedPartido, setSelectedPartido] = useState<PartidoGeography | null>(null); | ||
|  |   const [projectionConfig, setProjectionConfig] = useState(INITIAL_PROJECTION); | ||
|  | 
 | ||
|  |   // --- Carga de Datos ---
 | ||
|  |   const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapa[]>({ | ||
|  |     queryKey: ['mapaResultados'], | ||
|  |     queryFn: async () => { | ||
|  |       const { data } = await axios.get('http://localhost:5217/api/Resultados/mapa'); | ||
|  |       return data; | ||
|  |     }, | ||
|  |   }); | ||
|  | 
 | ||
|  |   const { data: geoData, isLoading: isLoadingGeo } = useQuery({ | ||
|  |     queryKey: ['mapaGeoData'], | ||
|  |     queryFn: async () => { | ||
|  |       const { data } = await axios.get('/partidos-bsas.topojson');  | ||
|  |       return data; | ||
|  |     }, | ||
|  |   }); | ||
|  | 
 | ||
|  |   const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({ | ||
|  |     queryKey: ['catalogoAgrupaciones'], | ||
|  |     queryFn: async () => { | ||
|  |       const { data } = await axios.get('http://localhost:5217/api/Catalogos/agrupaciones'); | ||
|  |       return data; | ||
|  |     }, | ||
|  |   }); | ||
|  | 
 | ||
|  |   const { nombresAgrupaciones, coloresPartidos } = useMemo(() => { | ||
|  |     if (!agrupacionesData) return { nombresAgrupaciones: {}, coloresPartidos: {} }; | ||
|  | 
 | ||
|  |     const nombres = agrupacionesData.reduce((acc, agrupacion) => { | ||
|  |       acc[agrupacion.id] = agrupacion.nombre; | ||
|  |       return acc; | ||
|  |     }, {} as { [key: string]: string }); | ||
|  | 
 | ||
|  |     const colores = agrupacionesData.reduce((acc, agrupacion, index) => { | ||
|  |       const baseColor = [255, 87, 51, 51, 255, 87, 51, 87, 255, 255, 51, 161, 161, 51, 255, 255, 195, 0, 199, 0, 57, 144, 12, 63, 88, 24, 69]; | ||
|  |       acc[agrupacion.nombre] = [baseColor[index*3], baseColor[index*3+1], baseColor[index*3+2]]; | ||
|  |       return acc; | ||
|  |     }, {} as { [key: string]: [number, number, number] }); | ||
|  |      | ||
|  |     colores['default'] = [214, 214, 218]; | ||
|  |     return { nombresAgrupaciones: nombres, coloresPartidos: colores }; | ||
|  |   }, [agrupacionesData]); | ||
|  | 
 | ||
|  |   const animatedProps = useSpring({ | ||
|  |     to: { scale: projectionConfig.scale, cx: projectionConfig.center[0], cy: projectionConfig.center[1] }, | ||
|  |     config: { tension: 170, friction: 26 }, | ||
|  |   }); | ||
|  | 
 | ||
|  |   if (isLoadingResultados || isLoadingGeo || isLoadingAgrupaciones) { | ||
|  |     return <div>Cargando datos del mapa...</div>; | ||
|  |   } | ||
|  | 
 | ||
|  |   const getPartyStyle = (partidoIdGeo: string) => { | ||
|  |     const resultado = resultadosData?.find(r => r.partidoId === partidoIdGeo); | ||
|  |     if (!resultado) { | ||
|  |       return { fill: `rgb(${PALETA_COLORES.default.join(',')})` }; | ||
|  |     } | ||
|  |     const nombreAgrupacion = nombresAgrupaciones[resultado.agrupacionGanadoraId] || 'Otro'; | ||
|  |     const baseColor = coloresPartidos[nombreAgrupacion] || PALETA_COLORES.default; | ||
|  |      | ||
|  |     // Calcula la opacidad basada en el porcentaje. 0.4 (débil) a 1.0 (fuerte)
 | ||
|  |     const opacity = 0.4 + (resultado.porcentajeGanador / 100) * 0.6; | ||
|  |      | ||
|  |     return { fill: `rgba(${baseColor.join(',')}, ${opacity})` }; | ||
|  |   }; | ||
|  | 
 | ||
|  |   const handleGeographyClick = (geo: PartidoGeography) => { | ||
|  |     const centroid = geoCentroid(geo); | ||
|  |     setSelectedPartido(geo); | ||
|  |     setProjectionConfig({ | ||
|  |       center: centroid, | ||
|  |       scale: 18000, // Zoom más cercano
 | ||
|  |     }); | ||
|  |   }; | ||
|  | 
 | ||
|  |   const handleReset = () => { | ||
|  |     setSelectedPartido(null); | ||
|  |     setProjectionConfig(INITIAL_PROJECTION); | ||
|  |   }; | ||
|  |    | ||
|  |   return ( | ||
|  |     <div className="mapa-wrapper"> | ||
|  |       <div className="mapa-container"> | ||
|  |         <animated.div style={{ | ||
|  |           width: '100%', | ||
|  |           height: '100%', | ||
|  |           transform: animatedProps.scale.to(s => `scale(${s / INITIAL_PROJECTION.scale})`), | ||
|  |         }}> | ||
|  |           <ComposableMap | ||
|  |             projection="geoMercator" | ||
|  |             projectionConfig={{ | ||
|  |               scale: animatedProps.scale, | ||
|  |               center: [animatedProps.cx, animatedProps.cy], | ||
|  |             }} | ||
|  |             className="rsm-svg" | ||
|  |           > | ||
|  |             <Geographies geography={geoData}> | ||
|  |               {({ geographies }: { geographies: PartidoGeography[] }) => | ||
|  |                 geographies.map((geo) => { | ||
|  |                   const partidoId = String(geo.properties.id); | ||
|  |                   const partidoNombre = geo.properties.departamento; | ||
|  |                   const resultado = resultadosData?.find(r => r.partidoId === partidoId); | ||
|  |                   const agrupacionNombre = resultado ? (nombresAgrupaciones[resultado.agrupacionGanadoraId] || 'Desconocido') : 'Sin datos'; | ||
|  | 
 | ||
|  |                   return ( | ||
|  |                     <Geography | ||
|  |                       key={geo.rsmKey} | ||
|  |                       geography={geo} | ||
|  |                       data-tooltip-id="partido-tooltip" | ||
|  |                       data-tooltip-content={`${partidoNombre}: ${agrupacionNombre}`} | ||
|  |                       fill={getPartyStyle(partidoId).fill} | ||
|  |                       stroke="#FFF" | ||
|  |                       className="rsm-geography" | ||
|  |                       style={{ | ||
|  |                         default: { outline: 'none' }, | ||
|  |                         hover: { outline: 'none', stroke: '#FF5722', strokeWidth: 2, fill: getPartyStyle(partidoId).fill }, | ||
|  |                         pressed: { outline: 'none' }, | ||
|  |                       }} | ||
|  |                       onClick={() => handleGeographyClick(geo)} | ||
|  |                     /> | ||
|  |                   ); | ||
|  |                 }) | ||
|  |               } | ||
|  |             </Geographies> | ||
|  |           </ComposableMap> | ||
|  |         </animated.div> | ||
|  |         <Tooltip id="partido-tooltip" /> | ||
|  |       </div> | ||
|  | 
 | ||
|  |       <div className="info-panel"> | ||
|  |         <button onClick={handleReset}>Resetear Vista</button> | ||
|  |         {selectedPartido ? ( | ||
|  |           <div> | ||
|  |             <h3>{selectedPartido.properties.departamento}</h3> | ||
|  |             <p><strong>Cabecera:</strong> {selectedPartido.properties.cabecera}</p> | ||
|  |             <p><strong>ID:</strong> {selectedPartido.properties.id}</p> | ||
|  |             {/* Aquí mostrarías más datos del partido seleccionado */} | ||
|  |           </div> | ||
|  |         ) : ( | ||
|  |           <div> | ||
|  |             <h3>Provincia de Buenos Aires</h3> | ||
|  |             <p>Selecciona un partido para ver más detalles.</p> | ||
|  |           </div> | ||
|  |         )} | ||
|  |         <Legend colores={coloresPartidos} /> | ||
|  |       </div> | ||
|  |     </div> | ||
|  |   ); | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | // Componente de Leyenda separado
 | ||
|  | const Legend = ({ colores }: { colores: { [key: string]: [number, number, number] } }) => { | ||
|  |     return ( | ||
|  |         <div className="legend"> | ||
|  |             <h4>Leyenda</h4> | ||
|  |             {Object.entries(colores).map(([nombre, color]) => { | ||
|  |                 if (nombre === 'default') return null; | ||
|  |                 return ( | ||
|  |                     <div key={nombre} className="legend-item"> | ||
|  |                         <div className="legend-color-box" style={{ backgroundColor: `rgb(${color.join(',')})` }} /> | ||
|  |                         <span>{nombre}</span> | ||
|  |                     </div> | ||
|  |                 ); | ||
|  |             })} | ||
|  |         </div> | ||
|  |     ); | ||
|  | }; | ||
|  | 
 | ||
|  | export default MapaBsAs; |