import React from 'react'; import { Area, CartesianGrid, ComposedChart, Legend, Line, ReferenceArea, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts'; /** One plotted line: which field in the row, how to name it, what colour. */ export interface InsightSeries { field: string; label: string; color: string; /** Draw as a dashed reference line — for a target or standard, not a measurement. */ dashed?: boolean; } /** * Shade the distance between a measurement and the standard it is judged against. * * Recharts has no between-two-series area, so the band is two stacked areas: a * transparent base up to the lower of the pair, then the gap itself. */ export interface InsightGapSpec { actualField: string; targetField: string; } export interface InsightTrendChartProps { /** FULL history — the analysed window is highlighted, not isolated. */ rows: Array>; series: InsightSeries[]; /** Inclusive day range the insight text actually covers. */ highlight?: { firstDay: number; lastDay: number } | null; unit?: string; title?: string; emptyLabel?: string; gap?: InsightGapSpec | null; } const dayOf = (row: Record): number | null => { for (const key of ['hari', 'day']) { const value = row[key]; if (typeof value === 'number' && Number.isFinite(value)) return value; } return null; }; const numOrNull = (value: unknown) => typeof value === 'number' && Number.isFinite(value) ? value : null; /** * Trend behind an insight, with the analysed period marked. */ const InsightTrendChart: React.FC = ({ rows, series, highlight, unit, title, emptyLabel = 'Belum ada data untuk digambarkan.', gap = null, }) => { const data = (Array.isArray(rows) ? rows : []) .map((row) => { const day = dayOf(row); if (day === null) return null; const point: Record = { hari: day }; for (const s of series) point[s.field] = numOrNull(row[s.field]); if (gap) { const actual = numOrNull(row[gap.actualField]); const target = numOrNull(row[gap.targetField]); point.bandBase = actual !== null && target !== null ? Math.min(actual, target) : null; point.bandSize = actual !== null && target !== null ? Math.abs(actual - target) : null; } return point; }) .filter((point): point is Record & { hari: number } => point !== null) .sort((a, b) => a.hari - b.hari); const drawn = series.filter((s) => data.some((point) => point[s.field] !== null)); if (data.length === 0 || drawn.length === 0) { return (
{emptyLabel}
); } const days = data.map((point) => point.hari); const step = Math.max(1, Math.ceil(days.length / 10)); const ticks = days.filter((_, i) => i % step === 0 || i === days.length - 1); const band = highlight && Number.isFinite(highlight.firstDay) && Number.isFinite(highlight.lastDay) ? highlight : null; const bandFrom = band ? band.firstDay === band.lastDay ? band.firstDay - 0.4 : band.firstDay : 0; const bandTo = band ? (band.firstDay === band.lastDay ? band.lastDay + 0.4 : band.lastDay) : 0; const gapFill = (() => { if (!gap) return null; const last = [...data] .reverse() .find((point) => point[gap.actualField] !== null && point[gap.targetField] !== null); const actual = last?.[gap.actualField]; const target = last?.[gap.targetField]; if (typeof actual !== 'number' || typeof target !== 'number' || target === 0) { return 'rgba(107, 114, 128, 0.12)'; } const deviation = ((actual - target) / target) * 100; if (deviation >= -5) return 'rgba(12, 163, 12, 0.12)'; if (deviation >= -15) return 'rgba(250, 178, 25, 0.18)'; return 'rgba(208, 59, 59, 0.14)'; })(); return (
{title &&

{title}

} {band && (

Area berbayang = periode yang dibahas insight {band.firstDay === band.lastDay ? ` (hari ke-${band.lastDay})` : ` (hari ke-${band.firstDay} s/d ke-${band.lastDay})`}

)} {band && ( )} `H${value}`} /> Number(value).toLocaleString('id-ID')} /> `Hari ke-${value}`} formatter={(value, name) => { const label = String(name ?? ''); if (label === 'bandBase' || label === 'bandSize') return null; if (value == null || Array.isArray(value)) return null; return [`${Number(value).toLocaleString('id-ID')}${unit ? ` ${unit}` : ''}`, label]; }} /> {drawn.length > 1 && } {gap && gapFill && ( <> )} {drawn.map((s) => ( ))}
); }; export default InsightTrendChart;