diff --git a/src/components/balance/BalanceEvolutionChart.tsx b/src/components/balance/BalanceEvolutionChart.tsx index f9908eb..0be8774 100644 --- a/src/components/balance/BalanceEvolutionChart.tsx +++ b/src/components/balance/BalanceEvolutionChart.tsx @@ -22,12 +22,14 @@ import { Tooltip, ResponsiveContainer, Legend, + ReferenceLine, } from "recharts"; import type { SnapshotTotalPoint, SnapshotCategoryBreakdownPoint, } from "../../services/balance.service"; import type { BalanceChartMode } from "../../hooks/useBalanceOverview"; +import type { BalanceAccountTransferWithTransaction } from "../../shared/types"; // Stable palette for the stacked-by-category areas. Indexed deterministically // by category sort order so the colour assignment stays consistent across @@ -51,6 +53,13 @@ export interface BalanceEvolutionChartProps { byCategory: SnapshotCategoryBreakdownPoint[]; /** Map category_key → translated label so the legend reads naturally. */ categoryLabels?: Record; + /** + * Issue #142 — every linked transfer in the visible range. Rendered as + * vertical `` markers on the X axis: green for `in` + * (capital added), red for `out` (capital removed). The label tooltip + * shows the underlying transaction date + description. + */ + transferMarkers?: BalanceAccountTransferWithTransaction[]; } export default function BalanceEvolutionChart({ @@ -58,6 +67,7 @@ export default function BalanceEvolutionChart({ totals, byCategory, categoryLabels = {}, + transferMarkers = [], }: BalanceEvolutionChartProps) { const { t, i18n } = useTranslation(); @@ -114,6 +124,31 @@ export default function BalanceEvolutionChart({ const isEmpty = mode === "line" ? lineData.length === 0 : stackedData.length === 0; + // Filter transfer markers to dates that are actually rendered on the X + // axis (categorical scale ignores unknown ticks). We don't aggregate or + // dedupe — the user can have several transfers on the same day across + // accounts; ReferenceLine tolerates duplicates fine. + const xAxisDates = useMemo(() => { + const dates = new Set(); + if (mode === "line") { + for (const p of lineData) dates.add(p.snapshot_date); + } else { + for (const p of stackedData) dates.add(p.snapshot_date as string); + } + return dates; + }, [mode, lineData, stackedData]); + + const renderableMarkers = useMemo( + () => + transferMarkers + .filter((m) => xAxisDates.has(m.transaction_date)) + // Sort so 'in' (green) draws before 'out' (red) for stable z-order. + .sort((a, b) => + a.direction === b.direction ? 0 : a.direction === "in" ? -1 : 1 + ), + [transferMarkers, xAxisDates] + ); + if (isEmpty) { return (
@@ -168,6 +203,28 @@ export default function BalanceEvolutionChart({ dot={{ r: 3 }} activeDot={{ r: 5 }} /> + {renderableMarkers.map((m) => ( + + ))} ) : ( ))} + {renderableMarkers.map((m) => ( + + ))} )}