'use client'
import { useState, useMemo } from 'react'
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
LineChart,
Line,
Area,
AreaChart,
} from 'recharts'
import { TrendingUp, TrendingDown, Minus, BarChart2, Activity, Percent } from 'lucide-react'
interface RawData {
status_stunting: boolean
tanggal_upload: string
nama_posyandu: string
}
interface Props {
data: RawData[]
availableYears: number[]
}
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Ags', 'Sep', 'Okt', 'Nov', 'Des']
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const stunting = payload.find((p: any) => p.dataKey === 'stunting')?.value ?? 0
const normal = payload.find((p: any) => p.dataKey === 'normal')?.value ?? 0
const total = stunting + normal
const pct = total > 0 ? ((stunting / total) * 100).toFixed(1) : '0.0'
return (
{label}
Stunting
{stunting}
Normal
{normal}
Prevalensi
{pct}%
)
}
return null
}
export function StuntingChart({ data, availableYears }: Props) {
const [selectedYear, setSelectedYear] = useState(availableYears[0] ?? 2026)
const [chartType, setChartType] = useState<'bar' | 'area'>('bar')
const [selectedPosyandu, setSelectedPosyandu] = useState('all')
// Unique posyandu list
const posyanduList = useMemo(() => {
return Array.from(new Set(data.map(d => d.nama_posyandu).filter(Boolean))).sort()
}, [data])
// Active data after posyandu filter
const filteredData = useMemo(() => {
if (selectedPosyandu === 'all') return data
return data.filter(d => d.nama_posyandu === selectedPosyandu)
}, [data, selectedPosyandu])
// Process data for the selected year into 12 months
const chartData = useMemo(() => {
return MONTHS.map((month, idx) => {
const monthNum = idx + 1
const filtered = filteredData.filter(d => {
if (!d.tanggal_upload) return false
const date = new Date(d.tanggal_upload)
return date.getFullYear() === selectedYear && date.getMonth() + 1 === monthNum
})
const stunting = filtered.filter(d => d.status_stunting === true).length
const normal = filtered.filter(d => d.status_stunting === false).length
const total = stunting + normal
const prevalensi = total > 0 ? parseFloat(((stunting / total) * 100).toFixed(1)) : 0
return { month, stunting, normal, total, prevalensi }
})
}, [filteredData, selectedYear])
// Summary stats for selected year
const totalStunting = chartData.reduce((s, d) => s + d.stunting, 0)
const totalNormal = chartData.reduce((s, d) => s + d.normal, 0)
const totalAll = totalStunting + totalNormal
const prevalensiTotal = totalAll > 0 ? ((totalStunting / totalAll) * 100).toFixed(1) : '0.0'
// Trend: compare current year vs previous year prevalensi
const prevYearTotal = filteredData.filter(d => {
if (!d.tanggal_upload) return false
return new Date(d.tanggal_upload).getFullYear() === selectedYear - 1
})
const prevStunting = prevYearTotal.filter(d => d.status_stunting === true).length
const prevAll = prevYearTotal.length
const hasPrevData = prevAll > 0
const prevPrevalensi = hasPrevData ? (prevStunting / prevAll) * 100 : null
const currPrevalensiNum = totalAll > 0 ? (totalStunting / totalAll) * 100 : null
const trend: 'up' | 'down' | 'stable' | null =
hasPrevData && currPrevalensiNum !== null
? currPrevalensiNum > prevPrevalensi! ? 'up'
: currPrevalensiNum < prevPrevalensi! ? 'down'
: 'stable'
: null
return (
{/* Dynamic Card Title */}
{selectedPosyandu === 'all'
? 'Data Tren Stunting Semua Posyandu'
: `Data Tren Stunting — ${selectedPosyandu}`}
{selectedPosyandu === 'all'
? 'Menampilkan data dari semua posyandu · Pilih posyandu untuk melihat per lokasi'
: `Filter aktif: ${selectedPosyandu} · Pilih "Semua Posyandu" untuk melihat keseluruhan`}
{/* Controls Row */}
{/* Filters */}
{/* Year Filter */}
Periode:
setSelectedYear(Number(e.target.value))}
className="border-2 border-black rounded-lg px-3 py-1.5 text-sm font-bold bg-white focus:outline-none shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] cursor-pointer"
>
{availableYears.map(year => (
{year}
))}
{/* Posyandu Filter */}
Posyandu:
setSelectedPosyandu(e.target.value)}
className="border-2 border-black rounded-lg px-3 py-1.5 text-sm font-bold bg-white focus:outline-none shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] cursor-pointer max-w-[200px] truncate"
>
Semua Posyandu
{posyanduList.map(name => (
{name}
))}
{/* Chart Type Toggle */}
setChartType('bar')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${chartType === 'bar' ? 'bg-black text-white' : 'text-gray-600 hover:text-black'}`}
>
Bar
setChartType('area')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${chartType === 'area' ? 'bg-black text-white' : 'text-gray-600 hover:text-black'}`}
>
Area
{/* Stats Row */}
{/* Total Data */}
Total Data
{totalAll}
pemeriksaan
{/* Stunting */}
Stunting
{totalStunting}
kasus
{/* Normal */}
Normal
{totalNormal}
anak
{/* Prevalensi */}
Prevalensi
{prevalensiTotal}%
{trend !== null && (
{trend === 'up' && <>Naik vs {selectedYear - 1} >}
{trend === 'down' && <>Turun vs {selectedYear - 1} >}
{trend === 'stable' && <>Sama vs {selectedYear - 1} >}
)}
{/* Chart */}
Tren Stunting Semua Posyandu — {selectedYear}
Berdasarkan tanggal upload data
{totalAll === 0 ? (
Tidak ada data untuk tahun {selectedYear}
) : (
{chartType === 'bar' ? (
} cursor={{ fill: '#f8fafc' }} />
val === 'stunting' ? 'Stunting' : 'Normal'}
/>
) : (
} />
val === 'stunting' ? 'Stunting' : 'Normal'}
/>
)}
)}
{/* Prevalensi (%) Line Chart */}
Prevalensi Stunting Per Bulan (%) — {selectedYear}
Persentase kasus stunting dari total pemeriksaan per bulan
{totalAll === 0 ? (
Tidak ada data untuk ditampilkan
) : (
`${v}%`}
domain={[0, 100]}
/>
{
if (active && payload && payload.length) {
const val = payload[0]?.value as number
return (
{label} {selectedYear}
Prevalensi:
= 20 ? 'text-red-600' : 'text-emerald-600'}`}>{val}%
{val >= 20 ? '⚠️ Di atas ambang batas (20%)' : '✓ Di bawah ambang batas (20%)'}
)
}
return null
}}
/>
(
= 20 ? '#ef4444' : '#10b981'}
stroke="white"
strokeWidth={2}
/>
)}
activeDot={{ r: 8, stroke: '#f59e0b', strokeWidth: 2 }}
/>
)}
● Merah = prevalensi ≥ 20% (tinggi) | ● Hijau = prevalensi < 20% (aman)
Bulan
Total
Stunting
Normal
Prevalensi
{chartData.map((row, idx) => (
{row.month} {selectedYear}
{row.total}
{row.stunting}
{row.normal}
{row.total > 0 ? (
= 20
? 'bg-red-50 border-red-200 text-red-700'
: 'bg-emerald-50 border-emerald-200 text-emerald-700'
}`}>
{row.prevalensi}%
) : (
—
)}
))}
)
}