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
This commit is contained in:
2026-05-11 12:13:00 -03:00
parent ca7b159657
commit b587ea7328
25 changed files with 1672 additions and 710 deletions
+94 -140
View File
@@ -8,6 +8,7 @@ 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,
@@ -31,29 +32,20 @@ export default function SecHistorica({ siniestros, year }) {
const yearNum = Number(year)
const kpis = calcularKPIs(siniestros)
// Hook se usa DENTRO del componente
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,
},
{ ano: yearNum, siniestros: kpis.total, victimas: kpis.victimas, tasa },
].sort((a, b) => a.ano - b.ano)
}
return base
}, [yearNum, kpis.total, kpis.victimas])
@@ -68,8 +60,7 @@ export default function SecHistorica({ siniestros, year }) {
)
const victimasPorAno = useMemo(
() =>
serieComparativa.map((row) => ({ ano: row.ano, victimas: row.victimas })),
() => serieComparativa.map((row) => ({ ano: row.ano, victimas: row.victimas })),
[serieComparativa],
)
@@ -77,20 +68,9 @@ export default function SecHistorica({ siniestros, year }) {
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)
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
@@ -104,128 +84,87 @@ export default function SecHistorica({ siniestros, year }) {
(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 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 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 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 históricas */}
<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'
}
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'
}
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'
}
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'
}
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'
}
label={rango10Desde && rango10Hasta ? `Promedio últimos 10 años (${rango10Desde}${rango10Hasta})` : 'Promedio últimos 10 años'}
value={promedio10 ?? '—'}
color={COLOR.green}
/>
</div>
{/* Gráfico de tasa + barras de víctimas */}
<div className="grid gap-6 xl:grid-cols-[1.5fr_1fr]">
<SerieHistorica
year={yearNum}
siniestrosActual={kpis.total}
victimasActual={kpis.victimas}
/>
<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">
Víctimas fatales
</p>
<h3 className="mt-2 text-xl font-black text-opsv-navy">
Evolución anual
</h3>
</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}
/>
<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,
@@ -239,15 +178,47 @@ export default function SecHistorica({ siniestros, year }) {
<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>
</div>
</ChartCard>
</div>
{/* Distribución por tipo, franja y localidad */}
{/* Tipo, franja y localidad */}
<div className="grid gap-6 xl:grid-cols-3">
<PorTipoSiniestro siniestros={siniestros} />
<FranjaHoraria siniestros={siniestros} />
<PorLocalidad siniestros={siniestros} />
<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 */}
@@ -261,24 +232,13 @@ export default function SecHistorica({ siniestros, year }) {
</h3>
</div>
<div className="overflow-x-auto">
<table
className="min-w-full text-left text-sm"
style={{ borderCollapse: 'collapse' }}
>
<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>
<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>
@@ -286,21 +246,14 @@ export default function SecHistorica({ siniestros, year }) {
<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)
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 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>
))}
@@ -308,6 +261,7 @@ export default function SecHistorica({ siniestros, year }) {
</table>
</div>
</div>
</div>
)
}