whisper-money/resources/js/lib/sankey-utils.ts

77 lines
2.3 KiB
TypeScript

import { SankeyCategory } from '@/hooks/use-cashflow-data';
export interface GroupedCategory {
categories: SankeyCategory[];
total: number;
}
export interface GroupedCategoryResult {
main: SankeyCategory[];
other: GroupedCategory | null;
}
/**
* Groups small categories (below threshold) into an "Other" category
*
* @param categories - Array of categories to potentially group
* @param total - Total amount for calculating percentages
* @param threshold - Percentage threshold (e.g., 0.05 for 5%)
* @returns Object with main categories to display and grouped "other" categories
*/
export function groupSmallCategories(
categories: SankeyCategory[],
total: number,
threshold: number = 0.03,
): GroupedCategoryResult {
// If no total or empty categories, return as-is
if (total === 0 || categories.length === 0) {
return { main: categories, other: null };
}
// Sort by amount descending
const sortedCategories = [...categories].sort(
(a, b) => b.amount - a.amount,
);
// If we have 5 or fewer categories total, don't group
if (sortedCategories.length <= 5) {
return { main: sortedCategories, other: null };
}
const thresholdAmount = total * threshold;
const mainCategories: SankeyCategory[] = [];
const otherCategories: SankeyCategory[] = [];
for (const category of sortedCategories) {
// Keep categories that are:
// 1. Above threshold amount, OR
// 2. In the top 3 (ensure minimum visibility)
if (category.amount >= thresholdAmount || mainCategories.length < 3) {
mainCategories.push(category);
} else {
otherCategories.push(category);
}
}
// Only create "Other" group if:
// 1. We have at least 2 categories to group
// 2. We're showing at least 3 main categories
if (otherCategories.length >= 2 && mainCategories.length >= 3) {
const otherTotal = otherCategories.reduce(
(sum, cat) => sum + cat.amount,
0,
);
return {
main: mainCategories,
other: {
categories: otherCategories,
total: otherTotal,
},
};
}
// Don't group - return all categories as main
return { main: sortedCategories, other: null };
}