TKK_E32231405/app/dashboard/kelola-data/[id]/PerkembanganChart.tsx

240 lines
12 KiB
TypeScript

'use client'
import { useState, useMemo } from 'react'
import {
AreaChart, Area,
XAxis, YAxis, CartesianGrid, Tooltip,
ResponsiveContainer,
} from 'recharts'
import { Ruler, Weight, ChevronDown, Activity } from 'lucide-react'
interface HasilItem {
tinggi_badan: number | null
berat_badan: number | null
z_score: number | null
tanggal_upload: string | null
}
interface Props {
data: HasilItem[]
}
const START_YEAR = 2026
function MiniTooltip({ active, payload, label, unit, color }: any) {
if (!active || !payload?.length) return null
return (
<div className={`bg-white border-2 rounded-xl shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] px-3 py-2 text-xs`}
style={{ borderColor: color }}>
<p className="font-bold text-gray-700 mb-1">{label}</p>
<p className="font-black" style={{ color }}>
{payload[0]?.value ?? '-'} <span className="font-normal text-gray-400">{unit}</span>
</p>
</div>
)
}
export function PerkembanganChart({ data }: Props) {
const availableYears = useMemo(() => {
const currentYear = new Date().getFullYear()
const dataYears = Array.from(
new Set(
data
.map(d => d.tanggal_upload ? new Date(d.tanggal_upload).getFullYear() : null)
.filter(Boolean)
)
) as number[]
const maxYear = Math.max(currentYear, ...dataYears, START_YEAR)
return Array.from(
new Set([
...Array.from({ length: maxYear - START_YEAR + 1 }, (_, i) => START_YEAR + i),
...dataYears,
])
).filter(y => y >= START_YEAR).sort((a, b) => a - b)
}, [data])
const [selectedYear, setSelectedYear] = useState<number>(availableYears[0] ?? START_YEAR)
const chartData = useMemo(() => {
return data
.filter(d => {
if (!d.tanggal_upload) return false
return new Date(d.tanggal_upload).getFullYear() === selectedYear
})
.map(d => ({
label: new Date(d.tanggal_upload!).toLocaleDateString('id-ID', { day: 'numeric', month: 'short' }),
// Store full date for sorting
_date: new Date(d.tanggal_upload!).getTime(),
tinggi: d.tinggi_badan,
berat: d.berat_badan,
zscore: d.z_score,
}))
.sort((a, b) => a._date - b._date)
}, [data, selectedYear])
const hasData = chartData.length > 0
return (
<div className="flex flex-col gap-4">
{/* Section header + filter */}
<div className="flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center gap-2">
<div className="flex gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-blue-400 mt-[3px]" />
<div className="w-2.5 h-2.5 rounded-full bg-emerald-400 mt-[3px]" />
</div>
<p className="text-xs font-bold uppercase tracking-widest text-gray-500">
Grafik Perkembangan Balita
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-widest text-gray-400">Periode:</span>
<div className="relative">
<select
value={selectedYear}
onChange={e => setSelectedYear(Number(e.target.value))}
className="appearance-none border-2 border-black rounded-lg pl-3 pr-8 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(y => (
<option key={y} value={y}>{y}</option>
))}
</select>
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 pointer-events-none text-gray-500" />
</div>
</div>
</div>
{/* Charts Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Tinggi Badan */}
<div className="rounded-xl border-2 border-blue-100 bg-blue-50/30 p-4">
<div className="flex items-center gap-2 mb-3">
<div className="w-8 h-8 rounded-lg bg-blue-100 border border-blue-200 flex items-center justify-center">
<Ruler className="w-4 h-4 text-blue-600" />
</div>
<div>
<p className="text-sm font-bold text-blue-800">Tinggi Badan</p>
<p className="text-[10px] text-blue-400">Dalam satuan cm</p>
</div>
</div>
{!hasData ? (
<div className="h-36 flex items-center justify-center text-gray-300 text-xs">
Tidak ada data untuk tahun {selectedYear}
</div>
) : (
<ResponsiveContainer width="100%" height={160}>
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="tinggiGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.25} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#dbeafe" />
<XAxis dataKey="label" tick={{ fontSize: 10, fontWeight: 600 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10 }} axisLine={false} tickLine={false} />
<Tooltip content={<MiniTooltip unit="cm" color="#3b82f6" />} />
<Area
type="monotone"
dataKey="tinggi"
stroke="#3b82f6"
strokeWidth={2.5}
fill="url(#tinggiGrad)"
dot={{ r: 4, fill: '#3b82f6', stroke: 'white', strokeWidth: 2 }}
activeDot={{ r: 6 }}
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
{/* Berat Badan */}
<div className="rounded-xl border-2 border-emerald-100 bg-emerald-50/30 p-4">
<div className="flex items-center gap-2 mb-3">
<div className="w-8 h-8 rounded-lg bg-emerald-100 border border-emerald-200 flex items-center justify-center">
<Weight className="w-4 h-4 text-emerald-600" />
</div>
<div>
<p className="text-sm font-bold text-emerald-800">Berat Badan</p>
<p className="text-[10px] text-emerald-400">Dalam satuan kg</p>
</div>
</div>
{!hasData ? (
<div className="h-36 flex items-center justify-center text-gray-300 text-xs">
Tidak ada data untuk tahun {selectedYear}
</div>
) : (
<ResponsiveContainer width="100%" height={160}>
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="beratGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.25} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#d1fae5" />
<XAxis dataKey="label" tick={{ fontSize: 10, fontWeight: 600 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10 }} axisLine={false} tickLine={false} />
<Tooltip content={<MiniTooltip unit="kg" color="#10b981" />} />
<Area
type="monotone"
dataKey="berat"
stroke="#10b981"
strokeWidth={2.5}
fill="url(#beratGrad)"
dot={{ r: 4, fill: '#10b981', stroke: 'white', strokeWidth: 2 }}
activeDot={{ r: 6 }}
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
{/* Z-Score Chart */}
<div className="rounded-xl border-2 border-purple-100 bg-purple-50/30 p-4 md:col-span-2">
<div className="flex items-center gap-2 mb-3">
<div className="w-8 h-8 rounded-lg bg-purple-100 border border-purple-200 flex items-center justify-center">
<Activity className="w-4 h-4 text-purple-600" />
</div>
<div>
<p className="text-sm font-bold text-purple-800">Z-Score (SD)</p>
<p className="text-[10px] text-purple-400">Standar Deviasi Pertumbuhan</p>
</div>
</div>
{!hasData ? (
<div className="h-36 flex items-center justify-center text-gray-300 text-xs text-center border-2 border-dashed border-purple-100/50 rounded-lg">
Tidak ada data untuk tahun {selectedYear}
</div>
) : (
<ResponsiveContainer width="100%" height={160}>
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="zscoreGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#9333ea" stopOpacity={0.25} />
<stop offset="95%" stopColor="#9333ea" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#f3e8ff" />
<XAxis dataKey="label" tick={{ fontSize: 10, fontWeight: 600 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10 }} axisLine={false} tickLine={false} domain={['auto', 'auto']} />
<Tooltip content={<MiniTooltip unit="SD" color="#9333ea" />} />
<Area
type="monotone"
dataKey="zscore"
stroke="#9333ea"
strokeWidth={2.5}
fill="url(#zscoreGrad)"
dot={{ r: 4, fill: '#9333ea', stroke: 'white', strokeWidth: 2 }}
activeDot={{ r: 6 }}
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
</div>
</div>
)
}