Files
dashboard-cpsp/components/weight/DailyWeightGainChart.tsx
T
2026-08-26 13:46:05 +07:00

194 lines
6.5 KiB
TypeScript

import React, { useMemo } from 'react';
import {
Bar,
CartesianGrid,
ComposedChart,
Legend,
Line,
ReferenceLine,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import type { ChickenWeight } from '../../types/api.ts';
import { formatDateId } from '../../utils/format.ts';
import { formatWeight, getTargetGainForAge } from '../../utils/weight.ts';
interface DailyWeightGainChartProps {
rows: ChickenWeight[];
totalDays: number;
}
type GainStatus = 'above' | 'near' | 'below' | 'none';
type ChartPoint = {
day: number;
date: string;
gain: number | null;
targetGain: number | null;
status: GainStatus;
};
const DailyWeightGainChart: React.FC<DailyWeightGainChartProps> = ({ rows, totalDays }) => {
const chartData = useMemo<ChartPoint[]>(
() =>
[...rows].reverse().map((row) => {
const targetGain = getTargetGainForAge(row.age, totalDays);
const gain = row.average_daily_gain;
let status: GainStatus = 'none';
if (gain != null && targetGain != null) {
const threshold = Math.abs(targetGain) * 0.1;
if (gain >= targetGain + threshold) status = 'above';
else if (gain >= targetGain - threshold) status = 'near';
else status = 'below';
}
return { day: row.age, date: row.date, gain, targetGain, status };
}),
[rows, totalDays]
);
const hasNoData = chartData.length === 0;
const CustomLegend = () => (
<div className="mt-2 flex flex-wrap items-center justify-center gap-6 text-sm text-gray-600">
<div className="flex items-center gap-2">
<div className="h-3 w-8 rounded-sm bg-red-600" />
<span>Pertumbuhan Aktual</span>
</div>
<div className="flex items-center gap-2">
<div className="h-3 w-8 rounded-sm bg-blue-600" />
<span>Target Pertumbuhan</span>
</div>
</div>
);
const CustomTooltip = ({
active,
payload,
}: {
active?: boolean;
payload?: Array<{ payload: ChartPoint }>;
}) => {
if (!active || !payload?.length) return null;
const point = payload[0]?.payload;
if (!point || point.gain == null) return null;
const hasTarget = point.targetGain != null;
const gainDiff = hasTarget ? point.gain - point.targetGain! : null;
return (
<div className="bg-white p-4 border border-gray-300 rounded shadow-lg">
<p className="font-bold text-gray-800">Hari ke-{point.day}</p>
<p className="text-sm text-gray-600">{formatDateId(point.date)}</p>
<div className="mt-2 space-y-1 text-sm">
<p>
<span className="font-semibold text-red-600">Pertumbuhan Aktual:</span>{' '}
{formatWeight(point.gain, { stripTrailingZeros: true })}
</p>
{hasTarget ? (
<>
<p>
<span className="font-semibold text-blue-600">Target Pertumbuhan:</span>{' '}
{formatWeight(point.targetGain!, { stripTrailingZeros: true })}
</p>
<p>
<span className="font-semibold">Selisih:</span>{' '}
<span className={gainDiff! >= 0 ? 'text-green-600' : 'text-orange-600'}>
{gainDiff! >= 0 ? '+' : ''}
{formatWeight(Math.abs(gainDiff!), { stripTrailingZeros: true })}
</span>
</p>
<p>
<span className="font-semibold">Status:</span>{' '}
<span
className={
point.status === 'above'
? 'text-green-600'
: point.status === 'near'
? 'text-yellow-600'
: 'text-red-600'
}
>
{point.status === 'above'
? 'Di Atas Target'
: point.status === 'near'
? 'Mendekati Target'
: 'Di Bawah Target'}
</span>
</p>
</>
) : (
<p>
<span className="font-semibold text-gray-500">Target:</span>{' '}
<span className="text-gray-400">Belum ada data standar</span>
</p>
)}
</div>
</div>
);
};
return (
<div className="bg-white p-6 rounded-xl shadow-sm">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Grafik Kenaikan Berat Harian</h2>
</div>
<div className="h-80">
{hasNoData ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<p className="text-gray-500 text-lg font-medium">Tidak ada data tersedia</p>
<p className="text-gray-400 text-sm mt-2">Data pertumbuhan belum tersedia</p>
</div>
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={chartData} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" />
<XAxis
dataKey="day"
tick={{ fill: '#6b7280', fontSize: 12 }}
label={{ value: 'Umur (hari)', position: 'insideBottom', offset: -5 }}
/>
<YAxis
tick={{ fill: '#6b7280', fontSize: 12 }}
tickFormatter={(value) => formatWeight(value, { stripTrailingZeros: true })}
/>
<Legend content={<CustomLegend />} />
<Tooltip content={<CustomTooltip />} />
<ReferenceLine y={0} stroke="#9ca3af" strokeWidth={1} />
<Bar dataKey="gain" fill="#dc2626" radius={[8, 8, 0, 0]} name="Pertumbuhan Aktual" />
<Bar
dataKey="targetGain"
fill="#2563eb"
radius={[8, 8, 0, 0]}
name="Target Pertumbuhan"
/>
<Line
type="monotone"
dataKey="gain"
stroke="#dc2626"
strokeWidth={2}
dot={false}
activeDot={{ r: 5 }}
connectNulls
/>
<Line
type="monotone"
dataKey="targetGain"
stroke="#2563eb"
strokeWidth={2}
dot={false}
activeDot={{ r: 5 }}
connectNulls
/>
</ComposedChart>
</ResponsiveContainer>
)}
</div>
</div>
);
};
export default DailyWeightGainChart;