feat: implement aggregated and per-account balance visualization in statistics page with interactive chart options
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Layers, SplitSquareHorizontal } from "lucide-react";
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
@@ -9,29 +12,75 @@ import {
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Legend,
|
||||
} from "recharts";
|
||||
import type { Account } from "@/lib/types";
|
||||
|
||||
interface BalanceChartData {
|
||||
interface BalanceChartDataPoint {
|
||||
date: string;
|
||||
solde: number;
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
interface BalanceLineChartProps {
|
||||
data: BalanceChartData[];
|
||||
aggregatedData: BalanceChartDataPoint[];
|
||||
perAccountData: BalanceChartDataPoint[];
|
||||
accounts: Account[];
|
||||
formatCurrency: (amount: number) => string;
|
||||
}
|
||||
|
||||
const ACCOUNT_COLORS = [
|
||||
"#6366f1", // indigo
|
||||
"#22c55e", // green
|
||||
"#f59e0b", // amber
|
||||
"#ef4444", // red
|
||||
"#8b5cf6", // violet
|
||||
"#06b6d4", // cyan
|
||||
"#ec4899", // pink
|
||||
"#84cc16", // lime
|
||||
];
|
||||
|
||||
export function BalanceLineChart({
|
||||
data,
|
||||
aggregatedData,
|
||||
perAccountData,
|
||||
accounts,
|
||||
formatCurrency,
|
||||
}: BalanceLineChartProps) {
|
||||
const [mode, setMode] = useState<"aggregated" | "split">("aggregated");
|
||||
const [hoveredAccount, setHoveredAccount] = useState<string | null>(null);
|
||||
|
||||
const data = mode === "aggregated" ? aggregatedData : perAccountData;
|
||||
const hasData = data.length > 0;
|
||||
|
||||
const getLineOpacity = (accountId: string) => {
|
||||
if (!hoveredAccount) return 1;
|
||||
return hoveredAccount === accountId ? 1 : 0.15;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle>Évolution du solde</CardTitle>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant={mode === "aggregated" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setMode("aggregated")}
|
||||
title="Tous les comptes agrégés"
|
||||
>
|
||||
<Layers className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={mode === "split" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setMode("split")}
|
||||
title="Par compte"
|
||||
>
|
||||
<SplitSquareHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.length > 0 ? (
|
||||
{hasData ? (
|
||||
<div className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data}>
|
||||
@@ -50,13 +99,68 @@ export function BalanceLineChart({
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="solde"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
{mode === "aggregated" ? (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="solde"
|
||||
name="Solde total"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
) : (
|
||||
accounts.map((account, index) => (
|
||||
<Line
|
||||
key={account.id}
|
||||
type="monotone"
|
||||
dataKey={account.id}
|
||||
name={account.name}
|
||||
stroke={ACCOUNT_COLORS[index % ACCOUNT_COLORS.length]}
|
||||
strokeWidth={hoveredAccount === account.id ? 3 : 2}
|
||||
strokeOpacity={getLineOpacity(account.id)}
|
||||
dot={false}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{mode === "split" && (
|
||||
<Legend
|
||||
content={({ payload }) => (
|
||||
<div
|
||||
className="flex flex-wrap justify-center gap-x-4 gap-y-1 mt-2"
|
||||
onMouseLeave={() => setHoveredAccount(null)}
|
||||
>
|
||||
{payload?.map((entry, index) => {
|
||||
const accountId = entry.dataKey as string;
|
||||
const isHovered = hoveredAccount === accountId;
|
||||
const isDimmed =
|
||||
hoveredAccount && hoveredAccount !== accountId;
|
||||
return (
|
||||
<div
|
||||
key={`legend-${index}`}
|
||||
className="flex items-center gap-1.5 text-sm cursor-pointer transition-opacity"
|
||||
style={{
|
||||
opacity: isDimmed ? 0.3 : 1,
|
||||
}}
|
||||
onMouseEnter={() => setHoveredAccount(accountId)}
|
||||
>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: entry.color,
|
||||
transform: isHovered ? "scale(1.2)" : "scale(1)",
|
||||
transition: "transform 0.15s",
|
||||
}}
|
||||
/>
|
||||
<span className="text-foreground">
|
||||
{entry.value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -69,4 +173,3 @@ export function BalanceLineChart({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user