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; |