# 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.