add icons to lists and solve money tracker bugs
All checks were successful
Test and deploy staging / staging (push) Successful in 2m30s
All checks were successful
Test and deploy staging / staging (push) Successful in 2m30s
This commit is contained in:
@@ -4,13 +4,21 @@ export type ChartSeries = {
|
||||
color: string;
|
||||
points: ChartPoint[];
|
||||
emphasis?: boolean;
|
||||
/** Points after this date are drawn dashed (projected). */
|
||||
dashedFrom?: string;
|
||||
};
|
||||
|
||||
const palette = ['#0070f3', '#e5484d', '#46a758', '#ad7f00', '#8e4ec6', '#0091ad', '#e56910'];
|
||||
|
||||
/** The account's chosen color, or a palette color based on its position in the list. */
|
||||
export function accountColor(account: { color?: string | null }, index: number): string {
|
||||
return account.color || palette[index % palette.length];
|
||||
}
|
||||
|
||||
export function balanceSeries(
|
||||
accounts: { id: number; name: string; openingBalanceCents: number }[],
|
||||
accounts: { id: number; name: string; openingBalanceCents: number; color?: string | null }[],
|
||||
transactions: { accountId: number; date: string; amountCents: number }[]
|
||||
): ChartSeries[] {
|
||||
const palette = ['#0070f3', '#e5484d', '#46a758', '#ad7f00', '#8e4ec6', '#0091ad', '#e56910'];
|
||||
const ordered = [...transactions].sort((a, b) => a.date.localeCompare(b.date));
|
||||
const balances = new Map(accounts.map((account) => [account.id, account.openingBalanceCents]));
|
||||
const accountPoints = new Map(accounts.map((account) => [account.id, [] as ChartPoint[]]));
|
||||
@@ -34,7 +42,7 @@ export function balanceSeries(
|
||||
return [
|
||||
...accounts.map((account, index) => ({
|
||||
label: account.name,
|
||||
color: palette[index % palette.length],
|
||||
color: accountColor(account, index),
|
||||
points: accountPoints.get(account.id)!
|
||||
})),
|
||||
{ label: 'All accounts', color: 'var(--site-muted)', points: combinedPoints }
|
||||
@@ -163,7 +171,7 @@ export function wealthProjection(
|
||||
} else spendable += value;
|
||||
}
|
||||
}
|
||||
if (validInvestmentId !== null && index > 0) {
|
||||
if (index > 0) {
|
||||
const growth = Math.round(indexBalance * monthlyGrowth);
|
||||
invested += growth;
|
||||
indexBalance += growth;
|
||||
@@ -194,6 +202,82 @@ export function wealthProjection(
|
||||
];
|
||||
}
|
||||
|
||||
export type TimelineView = 'total' | 'split' | 'accounts';
|
||||
|
||||
/**
|
||||
* Joins actual history (solid, through today) with the wealth projection (dashed, after
|
||||
* today) so both read as one continuous line per measure.
|
||||
*/
|
||||
export function wealthTimeline(
|
||||
accounts: {
|
||||
id: number;
|
||||
name: string;
|
||||
kind: string;
|
||||
openingBalanceCents: number;
|
||||
color?: string | null;
|
||||
}[],
|
||||
actualTransactions: { accountId: number; date: string; amountCents: number }[],
|
||||
projection: ChartSeries[],
|
||||
today: string,
|
||||
view: TimelineView
|
||||
): ChartSeries[] {
|
||||
const history = balanceSeries(accounts, actualTransactions);
|
||||
const accountLines = history.slice(0, accounts.length);
|
||||
const dates = history.at(-1)!.points.map((point) => point.date);
|
||||
const sumKind = (investment: boolean) =>
|
||||
dates.map((date, index) => ({
|
||||
date,
|
||||
value: accounts.reduce(
|
||||
(sum, account, accountIndex) =>
|
||||
(account.kind === 'investment') === investment
|
||||
? sum + accountLines[accountIndex].points[index].value
|
||||
: sum,
|
||||
0
|
||||
)
|
||||
}));
|
||||
const untilToday = (points: ChartPoint[]) => {
|
||||
const last = points.at(-1);
|
||||
return last && last.date < today ? [...points, { date: today, value: last.value }] : points;
|
||||
};
|
||||
const join = (actual: ChartPoint[], projected: ChartPoint[]) => {
|
||||
const solid = untilToday(actual);
|
||||
const cutoff = solid.at(-1)?.date ?? '';
|
||||
return [...solid, ...projected.filter((point) => point.date > cutoff)];
|
||||
};
|
||||
const [totalProjection, spendableProjection, investedProjection] = projection;
|
||||
const total: ChartSeries = {
|
||||
label: 'Total',
|
||||
color: 'var(--site-text)',
|
||||
emphasis: true,
|
||||
dashedFrom: today,
|
||||
points: join(history.at(-1)!.points, totalProjection?.points ?? [])
|
||||
};
|
||||
if (view === 'accounts')
|
||||
return [
|
||||
...accountLines
|
||||
.filter((line) => line.points.some((point) => point.value !== 0))
|
||||
.map((line) => ({ ...line, points: untilToday(line.points) })),
|
||||
total
|
||||
];
|
||||
if (view === 'split')
|
||||
return [
|
||||
total,
|
||||
{
|
||||
label: 'Spendable',
|
||||
color: '#46a758',
|
||||
dashedFrom: today,
|
||||
points: join(sumKind(false), spendableProjection?.points ?? [])
|
||||
},
|
||||
{
|
||||
label: 'Invested',
|
||||
color: '#8e4ec6',
|
||||
dashedFrom: today,
|
||||
points: join(sumKind(true), investedProjection?.points ?? [])
|
||||
}
|
||||
];
|
||||
return [total];
|
||||
}
|
||||
|
||||
export function utilityEstimate(
|
||||
readings: { date: string; reading: string; kind: string }[],
|
||||
kind: string,
|
||||
|
||||
Reference in New Issue
Block a user