b587ea7328
- 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
267 lines
10 KiB
React
267 lines
10 KiB
React
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>
|
||
)
|
||
} |