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>
This commit is contained in:
Flo
2026-05-07 14:22:53 +02:00
parent 0f10f8507f
commit 9a958c0051
46 changed files with 10056 additions and 1 deletions
+197
View File
@@ -0,0 +1,197 @@
# Design Brief — ms-haushalt
A single-page editorial app for navigating the City of Münster budget. Output of `/impeccable shape`. Intended to be handed to `/impeccable craft` or any implementation skill.
## 1. Feature Summary
A print-rooted, editorial single-page app that lets a Münster resident or journalist navigate the city's budget as a living treemap. Both money flows — revenues coming in, expenses going out — are first-class. A time slider runs from 2008 (historical actuals) through 2028 (planning projection). Clicking any tile semantically zooms into its components. Each tile carries data-derived context plus, where authored, short editorial summaries pulled from the source PDFs. The deliverable is a static-feeling site that reads like a Sunday-paper data feature.
## 2. Primary User Action
**Understand the budget as two flows: where money comes from, and where it goes.** The visitor leaves with intuition for the rough shape of both sides — and, on click, a clear sense of what dominates a particular Produktbereich.
## 3. Design Direction
Refer to `.impeccable.md` for the full design context. Specific to this feature:
* **Editorial, not exploratory.** This is not "a tool for power users to slice data." It's a guided document that happens to be interactive. Defaults must be opinionated: the page on first paint should already say something specific about the 2026/2027 draft.
* **Treemap as protagonist.** Every other element — type, side panel, time slider, controls — is in service to the treemap. Nothing else gets to be visually loud.
* **Two flows, not two charts.** The page does not show two competing treemaps. It shows one canvas that the visitor pivots between *Aufwendungen* (expenses) and *Erträge* (revenues) via a confident toggle that's part of the headline area, not a hidden control.
* **Time as scrubbable narrative.** The slider is not a filter — it's a storytelling device. Default position lands on the latest plan year; scrubbing reveals continuity (15+ years of actuals) and projection (3 forward years). Tile sizes tween smoothly; tile colors track delta vs. previous slider position.
## 4. Layout Strategy
**Asymmetric magazine spread, single scroll.**
```text
┌────────────────────────────────────────────────────────────────┐
│ WO MÜNSTER │
│ SEIN GELD Aufwendungen │ Erträge │
│ AUSGIBT. ───────────────────── │
│ ───── │
│ Stadt Münster · Haushaltsentwurf 2026/2027 │
│ │
│ ┌────────────────────────────────┐ ┌──────────────────────┐ │
│ │ │ │ DETAIL │ │
│ │ │ │ ───── │ │
│ │ T R E E M A P │ │ Selected node name │ │
│ │ (lead canvas) │ │ → 312,4 Mio. € │ │
│ │ │ │ 24 % der Ausgaben │ │
│ │ │ │ │ │
│ │ │ │ Veränderung ggü. 2024 │ │
│ │ │ │ Sparkline 20082028 │ │
│ │ │ │ │ │
│ │ │ │ Editorial-Note │ │
│ └────────────────────────────────┘ │ (sourced or written) │ │
│ │ │ │
│ ◄──────●────────────────────────► │ ↳ Quelle: Band 1, S.… │ │
│ 2008 2028 └──────────────────────┘ │
│ │
│ Wie liest man das? · Methodik · Datenquelle │
└────────────────────────────────────────────────────────────────┘
```
* **Headline area** (left-anchored, asymmetric): a vertical-stacked display headline in a strong serif/slab. Period after the headline is set as a typographic accent. Subhead in a smaller, refined body face. **Aufwendungen | Erträge** toggle sits to the right of the headline at top, set as a typographic switch (active state = solid, inactive = thin underline), not as buttons.
* **Treemap canvas**: occupies ~2/3 of horizontal space below the headline. Asymmetric — the panel is *not* centered. Tiles are flat fields of color; numbers and labels are typeset *on* the tile, not floated above it.
* **Detail panel**: ~1/3 width, right side. Hairline rule separates it from the canvas. No card, no shadow, no border-stripe — just typography and white space. Typeset like a magazine sidebar: small caps eyebrow, headline, body, footnote.
* **Time slider**: lives below the canvas, full editorial width. The slider track is a typographic timeline — years marked in small caps, with subtle marks distinguishing actuals (filled) from plan/projection (hollow).
* **Bottom matter**: small-caps links to "Wie liest man das?" / methodology / data source. Set like a magazine's footer.
**Mobile spread:**
* Headline collapses to single column, toggle drops below subhead.
* Treemap takes full width but reflows: at the deepest level it becomes a vertical proportional list (rectangles stacked, height = share). This preserves the "shape of the budget" reading on a phone where small-area tiles become unreadable.
* Detail panel becomes a bottom sheet, summoned by tapping a tile. Drag to expand/dismiss.
* Time slider sits above the bottom sheet, pinned.
## 5. Key States
* **First paint (default):** *Aufwendungen* selected, slider at 2026/2027, no tile selected. Detail panel shows totals + a one-paragraph editorial lead about the draft (the "this is the news" beat).
* **Hover (desktop) / focus:** tile lifts slightly via opacity/contrast change (no scale, no shadow). Detail panel previews that tile's figures without commitment. Cursor: zoom-in.
* **Selected (drilled in):** clicked tile's children fill the canvas; siblings have animated offscreen; breadcrumb appears above canvas (`Gesamt Soziale Leistungen Hilfen für Asylbewerber`). Esc / breadcrumb-click zooms back out.
* **Compare mode (slider scrubbing):** tile sizes and positions tween smoothly; tile fill briefly desaturates and is overlaid with a delta encoding (subtle directional tint — warmer if growing, cooler if shrinking, calibrated quietly, never traffic-light red/green).
* **Empty / missing data** (a Produktgruppe has no value for the chosen year, common for old years before reorganizations): tile becomes a hatched/textured placeholder with the label preserved; detail panel explains "Keine Daten für dieses Jahr — diese Produktgruppe wurde XYZ neu strukturiert." (text TBD by data inspection)
* **Loading:** the treemap layout itself is the loader — tiles materialize from a single block via opacity stagger, ~600ms, ease-out-quart. No spinner.
* **Error / data fetch failure:** quiet inline notice in the detail panel — "Daten konnten nicht geladen werden." with a retry link. No modal, no toast.
* **No-JS / progressive enhancement:** show the totals as a static typeset table and a link to the source CSV/XLSX. The Astro architecture supports this naturally.
## 6. Interaction Model
* **Drill: semantic zoom.** Click a tile → CSS transform-based animation expands its rect to the canvas bounds while siblings translate offscreen (opacity to 0). On arrival, children fade in within the new bounds. Reverse on zoom-out. ~450ms, ease-out-quart. Transform-only — no width/height animation. Reduced-motion: crossfade replacement at 150ms.
* **Toggle Aufwendungen ↔ Erträge:** typographic switch in the headline. Treemap re-tiles; tiles tween from current rects to new rects. The toggle is also a keyboard shortcut (e.g., `1`/`2` or `←`/`→`).
* **Time slider:** drag to scrub, click to jump. Snaps to year ticks. Tiles tween rect + fill on each step. Slider is keyboard-operable (arrow keys). Selected node persists across years; if a Produktgruppe didn't exist in the scrubbed-to year, its tile shows the empty/hatched state without losing selection.
* **Hover preview** (desktop only): tile lifts; detail panel updates in place, but doesn't lock until clicked. Cursor changes to indicate zoom is available.
* **Deep linking & sharing:** URL captures `{flow, year, drill-path}` (e.g., `/aufwendungen/2026/soziale-leistungen/hilfen-asyl`). Browser back/forward navigates the drill stack. Sharing a link lands the recipient on the same scene.
* **Source links:** every detail panel includes "Quelle: Band 1, S. ###" linking to the PDF (anchored where possible). For values that span data files (e.g., 2025 appears in two plans), a "Welcher Wert?" footnote explains which plan is canonical.
## 7. Content Requirements
**Display headline** (one of these or similar — to author):
* "WO MÜNSTER SEIN GELD AUSGIBT." (when Aufwendungen)
* "WORAUS MÜNSTERS HAUSHALT BESTEHT." (when Erträge)
* Typeset in two or three vertically-stacked lines, period as typographic accent.
**Subhead:** "Stadt Münster · Haushaltsentwurf 2026/2027 · interaktiv erkundet"
**Editorial lead** (~80120 words, hand-written): the "this is the news" paragraph for the 2026/2027 draft. Replaces the empty detail panel on first paint.
**Per-node content (data-derived, automatic for all 87+ nodes):**
* Display name (from CSV)
* Current year value (formatted German: `312,4 Mio. €`)
* Share of parent and share of total
* Year-over-year change (absolute and %)
* Sparkline 20082028 (where data exists)
* Top 3 sub-items by value (when applicable)
**Per-node editorial note (optional, hand-written or LLM-summarized from PDFs):**
* 13 sentences pointing at what's interesting about this node
* Stored as `content/notes/{produktbereich-slug}/{produktgruppe-slug}.md` with frontmatter
* Build-time fallback: short LLM summary derived from the relevant section of Band 1/2
**Microcopy:**
* Toggle: `Aufwendungen` | `Erträge`
* Breadcrumb separator: ``
* Empty data: `Keine Daten · Produktgruppe in diesem Jahr nicht ausgewiesen`
* Source link: `↳ Quelle: …`
* Methodik footer link: `Wie liest man das? · Methodik · Datenquelle: opendata.stadt-muenster.de`
**Number formatting:**
* German formatting throughout (`.` thousands, `,` decimal)
* Auto-scale at the tile level: `Mio. €` / `Mrd. €` for compactness; full euros only in detail panel
* Revenues displayed as positive numbers (the source CSV's negative sign convention is normalized away from the user's view)
## 8. Recommended References
For implementation, prioritize:
* `reference/spatial-design.md` — asymmetric magazine layouts, fluid grids, container queries for the side panel
* `reference/typography.md` — display-vs-body pairing, OpenType features, fluid clamp scales
* `reference/motion-design.md` — semantic-zoom timing, slider-tween orchestration, reduced-motion fallbacks
* `reference/color-and-contrast.md` — OKLCH-driven warm-paper palette, treemap fill scales that survive both small and large tiles
* `reference/responsive-design.md` — the mobile reflow from treemap to vertical proportional stack
## 9. Open Questions
These should be resolved during implementation, not in the brief:
1. **Display font choice.** The brief calls for "strong serif or slab with editorial-print rooting." We will go with Söhne Breit, and Söhne Mono for numbers.
2. **Canonical-year resolution.** When the same year appears in multiple plan files (e.g., 2025 is in the 2024 plan and the 2025 plan, with different values), the most recent plan that contains it is the slider's source of truth, with a footnote when overlapping plans disagree by >5 %.
3. **2026/2027 machine-readable data.** Currently only PDF. Implementation needs a one-time tabula-py / camelot extract from the PDF Band 1.
4. **LLM summary pipeline.** The hybrid editorial model needs a build-time job that extracts per-Produktbereich sections from Band 1/2 PDFs and summarizes them. Open: which model, prompt, and review gate before the summaries are exposed to readers? Flag-each-summary-as-AI-generated should be the default until reviewed.
5. **Color encoding — decided: two-hue system, one per flow.** *Aufwendungen* and *Erträge* each get their own hue family, constructed in OKLCH on the warm-paper background. Within a flow, tiles vary in lightness (and a touch of chroma) to encode value — bigger tiles sit darker/more saturated, smaller tiles lighter. The two hues should be far enough apart on the OKLCH wheel to read as a clear pivot when toggling, but both must hold their own against the off-white surface. Recommended exploration: a deep purple for *Aufwendungen* (money leaving) and an orange for *Erträge* (money coming in) — to be auditioned in OKLCH. Anti-pattern: red/green (traffic-light) or any pairing that signals "good/bad," since neither flow is morally loaded. The delta-while-scrubbing encoding (warmer = growing, cooler = shrinking) operates *within* the active hue's family, never as a second color overlay.
6. **Hosting / deploy target.** Static-first via Astro simply placed via sftp on an uberspace account. domain: faz.ms/haushalt/ Analytics Matomo without cookies
7. **20082022 actuals integration.** The Jahresabschluss file has fewer columns and slightly different spelling (`allgemeine` vs `algemeine`). The data layer must reconcile both into one long-format table.