mirror of
https://github.com/dat515-2025/Group-8.git
synced 2026-03-22 06:57:47 +01:00
104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
// src/CategoryPieCharts.tsx (renamed from CategoryPieChart.tsx)
|
|
import { useMemo } from 'react';
|
|
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
|
import { type Transaction, type Category } from '../api';
|
|
|
|
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#AF19FF', '#FF4242', '#8884d8', '#82ca9d'];
|
|
|
|
// Helper component for a single pie chart
|
|
function SinglePieChart({ data, title }: { data: { name: string; value: number }[]; title: string }) {
|
|
if (data.length === 0) {
|
|
return (
|
|
<div style={{ flex: 1, textAlign: 'center' }}>
|
|
<h4>{title}</h4>
|
|
<div>No data to display.</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ flex: 1 }}>
|
|
<h4>{title}</h4>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<PieChart>
|
|
<Pie
|
|
data={data}
|
|
cx="50%"
|
|
cy="50%"
|
|
labelLine={false}
|
|
outerRadius={80}
|
|
fill="#8884d8"
|
|
dataKey="value"
|
|
nameKey="name"
|
|
label={(props: any) => `${props.name} ${(props.percent * 100).toFixed(0)}%`}
|
|
>
|
|
{data.map((_entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip formatter={(value) => new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' }).format(value as number)} />
|
|
<Legend />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
export default function CategoryPieCharts({ transactions, categories }: { transactions: Transaction[], categories: Category[] }) {
|
|
|
|
// Calculate expenses data
|
|
const expensesData = useMemo(() => {
|
|
const spendingMap = new Map<number, number>();
|
|
|
|
transactions.forEach(tx => {
|
|
// Expenses are typically negative amounts in your system
|
|
if (tx.amount < 0 && tx.category_ids.length > 0) {
|
|
tx.category_ids.forEach(catId => {
|
|
// Use absolute value for display on chart
|
|
spendingMap.set(catId, (spendingMap.get(catId) || 0) + Math.abs(tx.amount));
|
|
});
|
|
}
|
|
});
|
|
|
|
return Array.from(spendingMap.entries())
|
|
.map(([categoryId, total]) => ({
|
|
name: categories.find(c => c.id === categoryId)?.name || `Category #${categoryId}`,
|
|
value: total,
|
|
}))
|
|
.sort((a, b) => b.value - a.value); // Sort descending
|
|
}, [transactions, categories]);
|
|
|
|
// Calculate earnings data
|
|
const earningsData = useMemo(() => {
|
|
const incomeMap = new Map<number, number>();
|
|
|
|
transactions.forEach(tx => {
|
|
// Earnings are typically positive amounts in your system
|
|
if (tx.amount > 0 && tx.category_ids.length > 0) {
|
|
tx.category_ids.forEach(catId => {
|
|
incomeMap.set(catId, (incomeMap.get(catId) || 0) + tx.amount);
|
|
});
|
|
}
|
|
});
|
|
|
|
return Array.from(incomeMap.entries())
|
|
.map(([categoryId, total]) => ({
|
|
name: categories.find(c => c.id === categoryId)?.name || `Category #${categoryId}`,
|
|
value: total,
|
|
}))
|
|
.sort((a, b) => b.value - a.value); // Sort descending
|
|
}, [transactions, categories]);
|
|
|
|
|
|
return (
|
|
<div className="pie-grid" >
|
|
<div className="pie-card">
|
|
<SinglePieChart data={expensesData} title="Expenses by Category" />
|
|
</div>
|
|
<div className="pie-card">
|
|
<SinglePieChart data={earningsData} title="Earnings by Category" />
|
|
</div>
|
|
</div>
|
|
);
|
|
} |