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