money: day-by-day forecast with real payment dates

The wealth forecast now runs per day instead of per month, so it shows the
same sawtooth as the actual history:

- recurring items hit on the day the bank history shows (most common day of
  the last four months, clamped to month end), else their start date
- "every N weeks" items (Basic-Fit, haircut) run on an exact N-day cadence
  from the last charge; yearly items on their renewal date
- salary and its extras (holiday pay in May, 13th month in November, bonus,
  settlement) land on the payday from history (contract 25th as fallback),
  moved to the Friday before a weekend; toeslagen move to the Monday after
- variable budgets are spread per day, the vacation budget over Jul-Aug
- excess above the buffer is invested the day after payday; returns
  compound daily
- long horizons stay fast (~35 ms for 100 years): the chart thins each
  pixel bucket to its low and high
This commit is contained in:
2026-10-26 01:13:43 +00:00
parent 5d732fc750
commit 807a6b3104
8 changed files with 1163 additions and 248 deletions

View File

@@ -1,8 +1,15 @@
<script lang="ts">
import { timeGrid, yearLabels, type ChartPoint, type ChartSeries, type GridMode } from './charts';
import {
decimateSeries,
timeGrid,
yearLabels,
type ChartPoint,
type ChartSeries,
type GridMode
} from './charts';
let {
series,
series: input,
title,
unit = '€',
marker,
@@ -24,6 +31,8 @@
let compact = $derived(width < 520);
let height = $derived(compact ? 240 : 280);
let plot = $derived({ left: compact ? 52 : 72, right: 14, top: 18, bottom: 30 });
// Daily projections over decades: keep each pixel column's low and high so spikes stay.
let series = $derived(decimateSeries(input, width - plot.left - plot.right));
let values = $derived(series.flatMap((item) => item.points.map((point) => point.value)));
let times = $derived(series.flatMap((item) => item.points.map((point) => time(point.date))));
let scale = $derived(niceScale(Math.min(0, ...values), Math.max(0, ...values)));