Files
ms-haushalt/src/data/__check.ts
T
FloandClaude 9a958c0051 feat: initial Münster Haushalt icicle viewer
Editorial single-page viewer for the City of Münster's 2026/2027
budget draft, built as an Astro v6 SPA with a 4-level zoomable
icicle (Produktbereich → Produktgruppe → Category → Breakdown).

Highlights:
- Multi-flow data layer over the official open-data CSVs
  (Aufwendungen + Erträge, 2008–2028) with overlap reconciliation
  across plan years.
- Year slider as a 21-year mini-histogram of both flows;
  drag-to-scrub and click-to-jump, with bars morphing via CSS
  transitions on SVG geometry attributes.
- Vertically centred icicle with year-outline rectangles framing
  each year's relative budget size, à la Bostock's animated treemap.
- Headline "ausgibt / einnimmt" toggle; sidebar Aufwendungen/Erträge
  rows double as flow toggles. Active flow in Aufwendungen-purple /
  Erträge-orange (OKLCH).
- Click-to-zoom via path-keyed lookup with ZOOM_COL_BOUNDS that
  reallocate the depth axis per zoom state. Zoomed item moves to the
  sidebar; canvas shows its descendants only (no adjacent-block leaks).
- Sidebar shows path-specific Aufwendungen/Erträge/Saldo plus the
  source-PDF Beschreibung; Erläuterungen behind a collapsed details.
- Build-time PDF extraction (scripts/extract-pg-sections.mjs) parses
  68 Produktgruppen' Beschreibung + Erläuterungen sections from
  Band 1, including 10 cells of structured Mio.-€ breakdowns
  (Steuern, Transferaufwendungen, etc.) that drive the level-4 view.
- URL state sync for path, year, and flow via history.replaceState
  so any zoom is shareable.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 16:27:45 +02:00

62 lines
2.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Ad-hoc sanity check, not a real test runner.
// pnpm exec tsx src/data/__check.ts
import { loadBudget } from "./load.js";
function fmtEUR(n: number): string {
return new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
maximumFractionDigits: 0,
}).format(n);
}
const tree = await loadBudget();
console.log(`Years: ${tree.years.length} (${tree.years[0]}${tree.years.at(-1)})`);
console.log(`Produktbereiche: ${tree.produktbereiche.length}`);
console.log(
`Produktgruppen total: ${tree.produktbereiche.reduce(
(n, b) => n + b.produktgruppen.length,
0
)}`
);
console.log(`Overlap notes (>5% disagreement): ${tree.overlaps.length}`);
// Bucket overlaps by year to confirm they're concentrated in the expected
// triple-counted years (2025/26/27 appear in two plans each).
const byYear: Record<number, number> = {};
for (const o of tree.overlaps) byYear[o.year] = (byYear[o.year] ?? 0) + 1;
console.log("Overlaps by year:", byYear);
// Sample 3 overlaps to confirm they're plausible plan revisions, not bugs.
console.log("\nSample overlaps:");
for (const o of tree.overlaps.slice(0, 3)) {
console.log(
` ${o.produktbereich} > ${o.produktgruppe} · ${o.year} · ${o.category}`
);
for (const v of o.values) {
console.log(` ${v.source.padEnd(50)} ${fmtEUR(v.value)}`);
}
console.log(` Δ = ${(o.divergencePct * 100).toFixed(1)}%`);
}
for (const year of [2022, 2025, 2026, 2028]) {
const t = tree.totals[year];
if (!t) continue;
console.log(
`\n${year}: Aufw ${fmtEUR(t.aufwendungen.total)} · Ertr ${fmtEUR(
t.ertraege.total
)} · Saldo ${fmtEUR(t.ertraege.total - t.aufwendungen.total)}`
);
}
console.log("\nTop 5 Produktbereiche by Aufwand 2026:");
const ranked = [...tree.produktbereiche]
.map((b) => ({ name: b.name, aufwand: b.totals[2026]?.aufwendungen.total ?? 0 }))
.sort((a, b) => b.aufwand - a.aufwand)
.slice(0, 5);
for (const r of ranked) {
console.log(` ${r.name.padEnd(42)} ${fmtEUR(r.aufwand)}`);
}