Con marca y dashboards
This commit is contained in:
111
components/dashboard/paraguay-map.tsx
Normal file
111
components/dashboard/paraguay-map.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isSuppressed, type PrevalenciaData, type SuppressedPayload } from '@/lib/dashboard-types'
|
||||
import { queries } from '@/lib/dashboard-queries'
|
||||
import type { DashboardFilters } from '@/lib/dashboard-types'
|
||||
import { WidgetShell } from './widget-shell'
|
||||
|
||||
// Tile-map de Paraguay (fallback aprobado en el prompt cuando no se consigue SVG fiel)
|
||||
// col/row en grilla 5×6
|
||||
const TILES = [
|
||||
{ name: 'Alto Paraguay', col: 2, row: 0 },
|
||||
{ name: 'Boquerón', col: 0, row: 1 },
|
||||
{ name: 'Presidente Hayes', col: 1, row: 1 },
|
||||
{ name: 'Concepción', col: 2, row: 1 },
|
||||
{ name: 'San Pedro', col: 3, row: 1 },
|
||||
{ name: 'Amambay', col: 4, row: 1 },
|
||||
{ name: 'Asunción (Capital)', col: 1, row: 2 },
|
||||
{ name: 'Cordillera', col: 2, row: 2 },
|
||||
{ name: 'Caaguazú', col: 3, row: 2 },
|
||||
{ name: 'Canindeyú', col: 4, row: 2 },
|
||||
{ name: 'Central', col: 1, row: 3 },
|
||||
{ name: 'Guairá', col: 2, row: 3 },
|
||||
{ name: 'Alto Paraná', col: 4, row: 3 },
|
||||
{ name: 'Ñeembucú', col: 0, row: 4 },
|
||||
{ name: 'Misiones', col: 1, row: 4 },
|
||||
{ name: 'Paraguarí', col: 2, row: 4 },
|
||||
{ name: 'Caazapá', col: 3, row: 4 },
|
||||
{ name: 'Itapúa', col: 2, row: 5 },
|
||||
]
|
||||
|
||||
function lerp(t: number, a: string, b: string): string {
|
||||
// simple teal interpolation: #E7E6E5 (light) → #43BBC8 (teal)
|
||||
const parseHex = (h: string) => [
|
||||
parseInt(h.slice(1,3),16), parseInt(h.slice(3,5),16), parseInt(h.slice(5,7),16)
|
||||
]
|
||||
const ca = parseHex(a); const cb = parseHex(b)
|
||||
const r = Math.round(ca[0] + (cb[0]-ca[0])*t)
|
||||
const g = Math.round(ca[1] + (cb[1]-ca[1])*t)
|
||||
const bv= Math.round(ca[2] + (cb[2]-ca[2])*t)
|
||||
return `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${bv.toString(16).padStart(2,'0')}`
|
||||
}
|
||||
|
||||
interface Props { surveyId: string; filters: DashboardFilters; onDeptoClick: (d: string) => void }
|
||||
|
||||
export function ParaguayMap({ surveyId, filters, onDeptoClick }: Props) {
|
||||
const [data, setData] = useState<PrevalenciaData | SuppressedPayload | null>(null)
|
||||
const [tooltip, setTooltip] = useState<{ name: string; pct: number | null; n: number | null } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setData(null)
|
||||
queries.prevalencia(surveyId, filters)
|
||||
.then(d => setData(d as PrevalenciaData | SuppressedPayload))
|
||||
.catch(() => {})
|
||||
}, [surveyId, JSON.stringify(filters)])
|
||||
|
||||
const deptoMap = new Map(
|
||||
(!data || isSuppressed(data)) ? [] :
|
||||
(data as PrevalenciaData).data.map(d => [d.depto, d])
|
||||
)
|
||||
|
||||
const status = !data ? 'loading' : isSuppressed(data) ? 'suppressed' : 'ok'
|
||||
const n = !isSuppressed(data) && data ? (data as PrevalenciaData).n_total : undefined
|
||||
|
||||
return (
|
||||
<WidgetShell title="🗺️ Prevalencia de cuidadores por departamento" status={status} n={n} colSpan={6}>
|
||||
<div className="db-map-wrapper" role="img" aria-label="Mapa de Paraguay con prevalencia de cuidadores por departamento">
|
||||
{tooltip && (
|
||||
<div className="db-map-tooltip" aria-live="polite">
|
||||
<strong>{tooltip.name}</strong>
|
||||
{tooltip.pct !== null
|
||||
? <> · {tooltip.pct}% · n={tooltip.n}</>
|
||||
: <> · Datos insuficientes</>}
|
||||
</div>
|
||||
)}
|
||||
<div className="db-tile-grid">
|
||||
{TILES.map(tile => {
|
||||
const d = deptoMap.get(tile.name)
|
||||
const suppressed = !d || d.suppressed
|
||||
const pct = d?.pct_cuidadores ?? null
|
||||
const color = suppressed ? '#2a3547' : lerp((pct ?? 0)/100, '#E7E6E5', '#43BBC8')
|
||||
const isSelected = filters.depto === tile.name
|
||||
return (
|
||||
<button
|
||||
key={tile.name}
|
||||
className={`db-tile${isSelected ? ' db-tile-selected' : ''}${suppressed ? ' db-tile-suppressed' : ''}`}
|
||||
style={{
|
||||
gridColumn: tile.col + 1,
|
||||
gridRow: tile.row + 1,
|
||||
background: color,
|
||||
}}
|
||||
onClick={() => onDeptoClick(isSelected ? '' : tile.name)}
|
||||
onMouseEnter={() => setTooltip({ name: tile.name, pct, n: d?.n ?? null })}
|
||||
onMouseLeave={() => setTooltip(null)}
|
||||
title={suppressed ? `${tile.name} — Datos insuficientes` : `${tile.name} — ${pct}% (n=${d?.n})`}
|
||||
aria-label={suppressed
|
||||
? `${tile.name}: datos insuficientes`
|
||||
: `${tile.name}: ${pct}% cuidadores, n=${d?.n}`}
|
||||
aria-pressed={isSelected}
|
||||
>
|
||||
<span className="db-tile-name">{tile.name.replace(' (Capital)','')}</span>
|
||||
{!suppressed && pct !== null && (
|
||||
<span className="db-tile-pct">{pct}%</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</WidgetShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user