Files
Dashboard/docs/DESIGN-SYSTEM.md
T
dejanandClaude Opus 5 b8bf987d0e 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
2026-08-29 17:25:20 +02:00

249 lines
8.9 KiB
Markdown
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.
# 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.