feat(dashboard): Analytics-Dashboard als Gestaltungsvorlage zur Übergabe

UI/UX-Prototyp der Analytics-Dashboards in HiveGrid, fachlich auf einen
SHK-/Handwerksbetrieb zugeschnitten. Ziel ist die Übergabe an die Entwicklung,
nicht der Produktivbetrieb.

Inhalt: sechs Seiten (Übersicht, Finanzen, Aufträge, Kunden, Team, Komponenten),
26 KPIs, neun Chart-Bausteine, Filtersystem mit seitenabhängiger Relevanz und ein
deterministischer Demo-Datensatz (420 Kunden, 1.900 Aufträge, Stichtag 30.07.2026).

Drei Quellen sind zusammengeführt: die fachliche Planung mit dem Branchenpartner
(Januar 2026), die Subframe-Komponentenbibliothek aus dem HiveGrid-Pitch und
IBM Carbon Data Visualization für die Diagrammregeln. Herkunft und bewusste
Abweichungen stehen in docs/HERKUNFT.md.

Dokumentation: README, HANDOVER (was echte Logik braucht), docs/DESIGN-SYSTEM,
KPI-KATALOG, DATENMODELL, NAVIGATION, HERKUNFT, ZIELSYSTEM (Einbauort in
HiveGrid Cloud) und LEITPLANKEN (welche Cloud-Screens als Vorbild taugen).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-29 17:25:20 +02:00
commit b8bf987d0e
250 files changed
+33153

No files matched your search

+248
View File
@@ -0,0 +1,248 @@
# Design-System: Datenvisualisierung
Grundlage ist **IBM Carbon Data Visualization**, ergänzt um die Spacing- und
Zahlenregeln aus der eigenen Planung (`UI_UX_GUIDELINES.md`, Januar 2026).
Die konkreten Farb- und Linienwerte sind aus dem ausgelieferten Stylesheet von
`@carbon/charts` (`dist/styles.css`, Light Theme) übernommen — nicht aus der
Dokumentation abgeschrieben. Alles Folgende ist in
`features/dashboard/utils/chartTheme.ts` als Code hinterlegt.
---
## 1. Farben
### Kategoriale Palette (14 Farben, Reihenfolge verbindlich)
```
1 #6929c4 2 #1192e8 3 #005d5d 4 #9f1853 5 #fa4d56
6 #520408 7 #198038 8 #002d9c 9 #ee5396 10 #b28600
11 #009d9a 12 #012749 13 #8a3800 14 #a56eff
```
Die Reihenfolge ist auf Unterscheidbarkeit optimiert — auch bei
Farbfehlsichtigkeit und in Graustufen. Farben deshalb **immer von vorne
vergeben**, nie nach Geschmack auswählen und nie umsortieren.
**Maximal fünf Kategorien je Chart.** Alles darüber fasst
`gruppiereRest()` zu „Sonstige" zusammen. Ein Ring mit zwölf Segmenten ist
keine Information mehr, sondern Dekoration.
### Statusfarben (nur für Aussagen, nie für Kategorien)
| Bedeutung | Wert |
|---|---|
| positiv | `#198038` |
| Warnung | `#b28600` |
| negativ | `#da1e28` |
| neutral | `#8d8d8d` |
| Information | `#1192e8` |
Kategorie- und Statusfarben nie im selben Chart mischen — eine grüne Kategorie
neben einem grünen Positiv-Status ist nicht mehr eindeutig lesbar.
**Rangfolgen einfarbig.** Top-Kunden oder Aufträge je Mitarbeiter sind keine
Kategorien: Dort trägt die Farbe keine Information, und eine bunte Palette
erzeugt nur Unruhe. Palette nur, wenn die Farbe etwas bedeutet.
### Feste Farbzuordnung
Wiederkehrende Kategorien behalten ihre Farbe über alle Charts und Seiten
hinweg (`GEWERK_FARBEN`, `STATUS_KATEGORIE_FARBEN` in `chartTheme.ts`). Sonst
ist Heizung im Ring lila und im Balkendiagramm blau, und jede Legende muss neu
gelesen werden.
| Gewerk | Farbe | | Status | Farbe |
|---|---|---|---|---|
| Heizung | `#6929c4` | | Verloren / Überfällig | `#fa4d56` / `#da1e28` |
| Sanitär | `#1192e8` | | In Arbeit / Bezahlt | `#198038` |
| Klima & Lüftung | `#005d5d` | | Beauftragt | `#009d9a` |
| Photovoltaik | `#9f1853` | | Angebot | `#6929c4` |
| Elektro | `#198038` | | Entwurf / Sonstige | `#8d8d8d` |
Bei Statuswerten trägt die Farbe Bedeutung: „Verloren" und „Überfällig" sind
rot, „Bezahlt" grün. Eine zufällige Palettenfarbe für „Verloren" würde die
Aussage der Grafik verwässern.
### Geordnete Dimensionen
Altersklassen, Risikostufen und Ähnliches sind **keine Kategorien**, sondern eine
Reihenfolge mit wachsendem Risiko. Dafür gilt die vierstufige Risikorampe
(`RISIKO_RAMPE` in `chartTheme.ts`) — durchgehend Statusfarben, keine
Palettenfarbe dazwischen:
| Stufe | Wert | Bedeutung |
|---|---|---|
| 1 | `#198038` | im Zahlungsziel |
| 2 | `#b28600` | beobachten |
| 3 | `#ba4e00` | mahnen |
| 4 | `#da1e28` | Abschreibungsrisiko |
### Einzelserien
| Rolle | Wert |
|---|---|
| Hauptreihe (Umsatzverlauf o. Ä.) | `#1192e8` |
| Vergleichsreihe (Vorjahr) | `#8d8d8d`, gestrichelt |
Die Vergleichsreihe ist bewusst neutral: Sie soll einordnen, nicht konkurrieren.
---
## 2. Achsen und Gitter
| Element | Wert | Begründung |
|---|---|---|
| Gitterlinien | `#e0e0e0`, **nur horizontal** | Vertikale Linien zerschneiden den Verlauf |
| Achsenlinie | `#8d8d8d`, 1 px | Nur an der X-Achse; die Y-Achse braucht keine |
| Tick-Marks | keine | Die Beschriftung reicht als Orientierung |
| Beschriftung | 12 px, `#525252` | Carbon-Standard |
| Y-Achsen-Breite | 58 px | Platz für „245 T€" ohne Umbruch |
Bei mehr als 12 Datenpunkten wird nur jede n-te Beschriftung gezeigt, sonst
überlappen sie (`MAX_ACHSENLABELS` in `chartTheme.ts` — eine Konstante für
alle Diagrammtypen).
**Achsenwerte sind rund.** Recharts leitet Ticks aus `max/(n-1)` ab und rundet
den Rohschritt auf; bei 219 T€ ergibt das 0/55/110/165/220. Krumme Achsenwerte
kosten den Leser bei jedem Blick eine Rechenoperation. `nettesRaster()` erzwingt
deshalb Schrittweiten aus der 1-2-2,5-5-Reihe.
**Die Einheit gehört zur Achse, nicht zum Wert.** `achsenFormat(max)` wählt sie
einmal aus dem Achsenmaximum. Sonst steht „0 €" neben „10 T€" und die Skala
wechselt mitten im Lesen die Einheit; die Nulllinie trägt gar keine Einheit.
---
## 3. Tooltip
Einheitlich für alle Charts (`components/charts/ChartTooltip.tsx`):
- Fläche `#393939`, Text `#ffffff`
- Label 12 px mit 80 % Deckkraft, Wert 13 px fett in Tabellenziffern
- kein Rahmen, kein Doppelpunkt zwischen Name und Wert
- Farbmarker je Reihe, 10 × 10 px
Ein gemeinsamer Tooltip verhindert, dass jedes Chart eigene Regeln entwickelt.
---
## 4. Balken, Flächen, Ringe
| Element | Regel |
|---|---|
| Balkenbreite | maximal 48 px |
| Balkenabstand | mindestens halbe Balkenbreite (`barCategoryGap: 28 %`) |
| Balkenecken | 2 px, nur an der Werteseite |
| Linienstärke Fläche | 2 px, Verlaufsfüllung 24 % → 2 % |
| Interpolation | **linear** — eine Monotone-Kurve erfindet Zwischenwerte zwischen zwei Messpunkten |
| Punkte | nur beim Hovern, Radius 4, weißer Rand |
| Ring innen/außen | 62 % / 88 %, 2° Segmentabstand |
| Ringmitte | Gesamtwert — sonst muss man die Segmente addieren |
| Ringlegende | rechts, einzeilig, mit Wert und Prozentanteil |
Ringsegmente starten oben (12 Uhr) mit dem größten Wert und laufen im
Uhrzeigersinn absteigend. Auch „Sonstige" wird nach Größe einsortiert, nicht
ans Ende gehängt.
**Ein Ring braucht ein echtes Ganzes.** Beträgt ein Segment über 80 %, trägt die
Grafik nichts mehr bei — dann ist die Bezugsgröße falsch gewählt. Beispiel: Der
Rechnungsbestand aus zwei Jahren gegen den heutigen Forderungsstand ergibt
92 % „Bezahlt". Richtig ist der offene Bestand allein; dann zeigt der Ring, wie
viel davon bereits überfällig ist.
### Werte in einem engen Band: Punkt statt Balken
Auslastung liegt betrieblich zwischen etwa 70 und 100 %. Ein Balken **muss** bei
Null beginnen, sonst verzerrt seine Länge — und dann sind 74 % und 96 % optisch
fast gleich lang. Ein Punkt kodiert über die **Position**, nicht über die Länge,
und darf deshalb einen ausgeschnittenen Bereich zeigen.
Der Zielkorridor liegt als graues Band hinter den Punkten, die 100-%-Marke als
beschriftete Linie, darunter eine Achse. Damit erklärt sich die Grafik ohne
Ampel-Legende — und tote Legendeneinträge („Luft frei", wenn niemand darunter
liegt) entfallen.
---
## 5. Abstände (8-px-System)
| Bereich | Wert |
|---|---|
| Seitenrand | 24 px |
| Abstand zwischen Karten | 16 px |
| Innenabstand Karte | 16 px |
| Kopf zu Inhalt in der Karte | 12 px |
| Label zu Wert in der KPI-Karte | 4 px |
| Icon zu Text | 8 px |
**Grundregel: innen ≤ außen.** Ist der Innenabstand größer als der Abstand
zwischen den Karten, zerfällt das Raster optisch.
---
## 6. Typografie
| Rolle | Größe | Gewicht |
|---|---|---|
| Seitentitel | 22 px | 600 |
| KPI-Wert | 28 px | 600 |
| Kartentitel | 14 px | 600 |
| Fließtext / Tabelle | 13 px | 400 |
| Label, Achsen, Legende | 12 px | 400–500 |
| Kleinstschrift (Meta) | 11 px | 400 |
Eine Schriftfamilie (Inter). Zahlen immer in Tabellenziffern
(`font-variant-numeric: tabular-nums`, global gesetzt), damit Spalten
untereinander stehen.
---
## 7. Zahlenformate (deutsch)
| Fall | Ausgabe |
|---|---|
| Währung, KPI | `238.412 €` |
| Währung, kompakt (Achsen, Balken) | `8,4 T€` · `245 T€` · `1,25 Mio. €` |
| Prozent | `58,2 %` |
| Tage / Stunden | `34,2 Tage` · `7,5 h` |
| Datum | `30.07.2026`, kurz `30.07.`, Monat `Jul 26` |
Punkt als Tausendertrenner, Komma als Dezimaltrenner. Innerhalb einer Ansicht
immer dasselbe Format — nicht in einer Zeile `245.000 €` und in der nächsten
`245 T€`.
---
## 8. Zustände
Jedes Chart braucht einen **Leerzustand** — Filter können jede Ansicht leer
laufen lassen. `ChartLeer` zeigt dann eine gestrichelte Fläche mit Hinweistext
statt eines leeren Rechtecks.
Hover-Zustände: Karten erhalten einen stärkeren Schatten, Balken und
Ringsegmente werden hervorgehoben (die übrigen auf 35–45 % Deckkraft).
Tabellenzeilen bekommen **keinen** Hover, solange sie nicht anklickbar sind —
ein Hover ohne Ziel verspricht eine Interaktion, die es nicht gibt.
## 9. Fokus und Kontrast
**Fokus** ist eine einzige Regel in `app/globals.css` statt `focus:`-Klassen an
jedem Element:
```css
:where(a, button, [role="button"], input, select, textarea, [tabindex]):focus-visible {
outline: none;
box-shadow: var(--shadow-focus);
border-radius: var(--radius-sm);
}
```
`:focus-visible` zeigt den Ring nur bei Tastaturbedienung. Overlays (Filter-
Dropdown, Karten-Menü) schließen mit **Escape** und geben den Fokus an den
auslösenden Knopf zurück.
**Kontrast:** `--color-text-neutral-tertiary` liegt auf `neutral-700`
(5,15:1 auf Weiß), nicht auf `neutral-600` (3,68:1 — unter WCAG AA). Das
entspricht dem a11y-Override im Zielsystem. `text-neutral-subtle` ist ein
Placeholder-Token und für lesbaren Text nicht geeignet.