Files
OPSV---Dashboard-de-Siniest…/src/pages/SecHistorica.jsx
T
vforchino b587ea7328 feat: múltiples mejoras y correcciones
- Agrega SecMapa con visualización geográfica de siniestros
- Actualiza exportación a PDF con nuevas secciones
- Corrige colores de interfaz (modo claro/oscuro)
- Corrige cálculo de víctimas fatales en SecSintesis usando campo 'fallecidos'
- Corrige serie histórica para reflejar año filtrado correctamente
2026-05-11 12:13:00 -03:00

267 lines
10 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo } from 'react'
import { calcularKPIs } from '../utils/calculos'
import SerieHistorica, {
SERIE_HISTORICA,
getPoblacionAnual,
} from '../components/charts/SerieHistorica'
import PorLocalidad from '../components/charts/PorLocalidad'
import PorTipoSiniestro from '../components/charts/PorTipoSiniestro'
import FranjaHoraria from '../components/charts/FranjaHoraria'
import KPICard from '../components/ui/KPICard'
import ChartCard from '../components/ui/ChartCard'
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts'
import { useChartTheme } from '../hooks/useChartTheme'
import { COLOR } from '../utils/colores'
function calcularPorcentaje(base, actual) {
if (!base || base === 0) return '0%'
const diff = actual - base
const sign = diff >= 0 ? '▲' : '▼'
return `${sign} ${Math.abs(((diff / base) * 100).toFixed(1))}%`
}
export default function SecHistorica({ siniestros, year }) {
const yearNum = Number(year)
const kpis = calcularKPIs(siniestros)
const { tickColor, gridColor, tooltipBg, tooltipBorder, tooltipLabel } =
useChartTheme()
const serieComparativa = useMemo(() => {
const base = [...SERIE_HISTORICA]
const yaExiste = base.some((row) => row.ano === yearNum)
if (!yaExiste && kpis.total > 0) {
const pob = getPoblacionAnual(yearNum)
const tasa = Number(((kpis.victimas / pob) * 100000).toFixed(2))
return [
...base,
{ ano: yearNum, siniestros: kpis.total, victimas: kpis.victimas, tasa },
].sort((a, b) => a.ano - b.ano)
}
return base
}, [yearNum, kpis.total, kpis.victimas])
const yearActualData = useMemo(
() => serieComparativa.find((row) => row.ano === yearNum) ?? null,
[serieComparativa, yearNum],
)
const prevYearData = useMemo(
() => serieComparativa.find((row) => row.ano === yearNum - 1) ?? null,
[serieComparativa, yearNum],
)
const victimasPorAno = useMemo(
() => serieComparativa.map((row) => ({ ano: row.ano, victimas: row.victimas })),
[serieComparativa],
)
const dataTabla = serieComparativa
const serieParaExtremos = useMemo(() => {
const base = [...SERIE_HISTORICA]
const existe2025 = base.some((row) => row.ano === 2025)
if (!existe2025) base.push({ ano: 2025, siniestros: 0, victimas: 21, tasa: 6.27 })
return base.filter((row) => row.ano !== 2020 && row.victimas != null).sort((a, b) => a.ano - b.ano)
}, [])
const victimasActual = yearActualData?.victimas ?? kpis.victimas ?? null
const comparativoVictimas =
prevYearData?.victimas != null && victimasActual != null
? calcularPorcentaje(prevYearData.victimas, victimasActual)
: '—'
const serieVictimasValidas = serieComparativa.filter(
(row) => row.ano !== 2020 && row.victimas != null,
)
const maxEntry = serieParaExtremos.length > 0
? serieParaExtremos.reduce((max, row) => (max == null || row.victimas > max.victimas ? row : max), null)
: null
const maxHistorico = maxEntry?.victimas ?? null
const maxAno = maxEntry?.ano ?? null
const minEntry = serieParaExtremos.length > 0
? serieParaExtremos.reduce((min, row) => (min == null || row.victimas < min.victimas ? row : min), null)
: null
const minHistorico = minEntry?.victimas ?? null
const minAno = minEntry?.ano ?? null
const serieOrdenada = [...serieVictimasValidas].sort((a, b) => a.ano - b.ano)
const ultimos10 = serieOrdenada.filter((row) => row.ano <= yearNum).slice(-10)
const promedio10 = ultimos10.length > 0
? (ultimos10.reduce((acc, row) => acc + row.victimas, 0) / ultimos10.length).toFixed(1)
: null
const rango10Desde = ultimos10[0]?.ano ?? null
const rango10Hasta = ultimos10[ultimos10.length - 1]?.ano ?? null
return (
<div className="space-y-8">
{/* KPIs */}
<div data-pdf-block className="grid gap-4 xl:grid-cols-5">
<KPICard
label={yearActualData ? `Víctimas fatales (${yearActualData.ano})` : 'Víctimas fatales'}
value={victimasActual ?? '—'}
color={COLOR.red}
/>
<KPICard
label={prevYearData && yearActualData ? `Variación vs. año anterior (${prevYearData.ano}${yearActualData.ano})` : 'Variación vs. año anterior'}
value={comparativoVictimas}
color={COLOR.gold}
/>
<KPICard
label={maxAno ? `Máximo histórico de víctimas (${maxAno})` : 'Máximo histórico de víctimas'}
value={maxHistorico ?? '—'}
color={COLOR.navy}
/>
<KPICard
label={minAno ? `Mínimo histórico de víctimas (${minAno})` : 'Mínimo histórico de víctimas'}
value={minHistorico ?? '—'}
color={COLOR.blue}
/>
<KPICard
label={rango10Desde && rango10Hasta ? `Promedio últimos 10 años (${rango10Desde}${rango10Hasta})` : 'Promedio últimos 10 años'}
value={promedio10 ?? '—'}
color={COLOR.green}
/>
</div>
{/* Serie histórica + barras de víctimas */}
<div className="grid gap-6 xl:grid-cols-[1.5fr_1fr] items-start">
<ChartCard
kicker="Análisis histórico"
title="Evolución de la tasa de siniestralidad"
subtitle="Serie histórica de siniestros y tasa de víctimas fatales cada 100.000 habitantes."
height="lg"
>
<SerieHistorica
year={yearNum}
siniestrosActual={kpis.total}
victimasActual={kpis.victimas}
/>
</ChartCard>
<ChartCard
kicker="Víctimas fatales"
title="Evolución anual"
subtitle="Cantidad de víctimas fatales por año en el periodo histórico registrado."
height="lg"
>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={victimasPorAno} margin={{ top: 10, right: 20, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridColor} vertical={false} />
<XAxis dataKey="ano" tick={{ fill: tickColor, fontSize: 12 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: tickColor, fontSize: 12 }} axisLine={false} tickLine={false} />
<Tooltip
contentStyle={{
background: tooltipBg,
border: `1px solid ${tooltipBorder}`,
borderRadius: 16,
boxShadow: '0 10px 30px rgba(0,0,0,0.08)',
}}
labelStyle={{ color: tooltipLabel, fontWeight: 700 }}
itemStyle={{ color: tickColor }}
/>
<Bar dataKey="victimas" fill={COLOR.red} radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
<div className="mt-4 space-y-1">
<p className="text-sm text-amber-600/80">
<span className="font-semibold">Nota:</span> el año 2020 no se incluye en la
serie histórica. Las restricciones de circulación por la pandemia de COVID-19
generaron una reducción atípica de la movilidad vehicular, por lo que los datos
no son comparables con el resto de la serie.
</p>
</div>
</div>
</ChartCard>
</div>
{/* Tipo, franja y localidad */}
<div className="grid gap-6 xl:grid-cols-3">
<ChartCard
kicker="Características"
title="Por tipo de siniestro"
subtitle="Distribución según el tipo de evento vial registrado en el periodo."
height="md"
>
<PorTipoSiniestro siniestros={siniestros} />
</ChartCard>
<ChartCard
kicker="Condiciones temporales"
title="Franja horaria"
subtitle="Cantidad de siniestros según la franja horaria en que ocurrieron."
height="md"
>
<FranjaHoraria siniestros={siniestros} />
</ChartCard>
<ChartCard
kicker="Distribución territorial"
title="Top 10 localidades"
subtitle="Las 10 localidades con mayor cantidad de siniestros en el periodo seleccionado."
height="md"
>
<PorLocalidad siniestros={siniestros} />
</ChartCard>
</div>
{/* Tabla histórica */}
<div data-pdf-block className="rounded-[28px] border border-opsv-border bg-opsv-surface p-6 shadow-sm">
<div className="mb-6">
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-opsv-muted">
Tabla histórica
</p>
<h3 className="mt-2 text-xl font-black text-opsv-navy">
Histórico de siniestros
</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-left text-sm" style={{ borderCollapse: 'collapse' }}>
<thead>
<tr className="border-b-2 border-opsv-border">
<th className="pb-3 pr-6 font-bold uppercase tracking-wider text-opsv-navy">Año</th>
<th className="pb-3 pr-6 font-bold uppercase tracking-wider text-opsv-navy">Siniestros</th>
<th className="pb-3 pr-6 font-bold uppercase tracking-wider text-opsv-navy">Víctimas</th>
<th className="pb-3 font-bold uppercase tracking-wider text-opsv-navy">Tasa</th>
</tr>
</thead>
<tbody>
{dataTabla.map((row) => (
<tr
key={row.ano}
className={`border-b border-opsv-border/60 transition-colors hover:bg-opsv-bg ${
row.ano === yearNum && !SERIE_HISTORICA.find((r) => r.ano === yearNum)
? 'bg-blue-500/5 font-semibold'
: ''
}`}
>
<td className="py-3 pr-6 font-medium text-opsv-navy">{row.ano}</td>
<td className="py-3 pr-6 text-opsv-text">{row.siniestros}</td>
<td className="py-3 pr-6 text-opsv-text">{row.victimas ?? '—'}</td>
<td className="py-3 text-opsv-text">{row.tasa ?? '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}