Recherche an der Quelle statt an der Dokumentation. Zwei Ergebnisse: 1. Carbon regelt Datenvisualisierung nur teilweise. Farben, Gitter, Achsen, Tooltip und Tickanzahl sind verbindlich festgelegt — Paddings, Margins und Dashboard-Raster dagegen NICHT. Die Carbon-Seiten zu Chart-Anatomie und Dashboards tragen ausdrücklich den Hinweis "this guidance is a work in progress" und enthalten keine Zahlenwerte. Unsere Abstandswerte stammen deshalb aus der Carbon-Spacing-Skala und der Planung mit dem Branchenpartner, nicht aus der Data-Viz-Norm. Das steht jetzt so in DESIGN-SYSTEM.md §0. 2. Eine echte Abweichung gefunden: Der Tooltip ist seit Carbon v11 hell (--cds-layer-02 #ffffff auf --cds-text-primary #161616, abgesetzt allein durch den Schatten). Unser Theme führte den dunklen v10-Stand (#393939) und berief sich dabei auf das ausgelieferte Stylesheet — die Angabe war veraltet. Korrigiert. Neu: - docs/carbon-referenz.json — kanonische Werte mit Quelle, Version und Prüfdatum, gezogen aus @carbon/charts 1.23.0 styles.min.css und configuration.ts. Bewusste Abweichungen (Balkenbreite 48 statt 16, feste Kategoriefarben statt n-Serien-Paletten) sind dort begründet. - scripts/verify-carbon.mjs + npm run verify:carbon — gleicht chartTheme.ts gegen die Referenz ab und bricht bei Drift ab. - docs/PRUEFFRAGEN.md — Prüffragen für die Gestaltungsabnahme mit nachprüfbarem Ja/Nein, plus Leseliste (FT Visual Vocabulary, IBCS, Cleveland & McGill, Few, Datawrapper Academy). Die 14er-Palette wurde gegen die CSS-Variablen --cds-charts-14-1-* geprüft und stimmt exakt; ebenso Gitter #e0e0e0, Achse #8d8d8d und Achsentext #525252. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
11 KiB
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 (Light Theme) übernommen — nicht aus der Dokumentation
abgeschrieben. Alles Folgende ist in
features/dashboard/utils/chartTheme.ts als Code hinterlegt.
0. Was Carbon regelt — und was nicht
Wichtig für jeden, der hier etwas anzweifelt: Carbon regelt Datenvisualisierung nur teilweise. Die Grenze verläuft so:
| Frage | Regelt Carbon? | Wo steht die Antwort |
|---|---|---|
| Farbpalette, Reihenfolge, Statusfarben | ja, verbindlich | ausgeliefertes Stylesheet, CSS-Variablen --cds-charts-14-1-* |
| Gitter-, Achsen- und Textfarben | ja | dieselben Quellen |
| Tooltip-Aufbau | ja | .cds--cc--tooltip im Stylesheet |
| Tickanzahl je Achse | ja | configuration.ts (y: 5, x: 15) |
| Wann welcher Diagrammtyp | teilweise, qualitativ | Carbon „Simple/Complex charts" |
| Padding, Margins, Kartenabstände | nein | siehe unten |
| Dashboard-Raster, Kartenhöhen | nein, nur Prinzipien | siehe unten |
Die Carbon-Seiten zu Chart-Anatomie und Dashboards tragen ausdrücklich den Hinweis „this guidance is a work in progress" und enthalten keine Zahlenwerte für Abstände. Wer dort nach Paddings sucht, findet Prinzipien („create a clear visual hierarchy", „limit the number of metrics") — richtig, aber nicht nachprüfbar.
Die Abstandswerte in diesem Dokument stammen deshalb nicht von Carbon, sondern aus zwei anderen Quellen:
- der Carbon-Spacing-Skala und dem 2x-Grid des allgemeinen Design-Systems
(4/8/12/16/24/32/48…) — dieselbe Skala, aus der auch Davids
INTERIM-UI-DESIGN-STANDARDS.mdim Zielsystem schöpft; - der eigenen Planung mit dem Branchenpartner (
UI_UX_GUIDELINES.md).
Beides ist konsistent zueinander. Es ist aber ehrlicher zu sagen: Bei Farben und Achsen folgen wir einer Norm, bei Abständen einer begründeten Hauskonvention.
Nachprüfbar gemacht: npm run verify:carbon gleicht chartTheme.ts gegen
docs/carbon-referenz.json ab — Palette, neutrale Werte, Tooltip, Achsenraster.
Die Referenzdatei nennt Quelle, Version und Prüfdatum. Bewusste Abweichungen
(Balkenbreite, feste Kategoriefarben) sind dort begründet hinterlegt und werden
als Hinweis ausgegeben, nicht als Fehler.
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), Werte aus
.cds--cc--tooltip im ausgelieferten Stylesheet:
- Fläche
#ffffff(--cds-layer-02), Text#161616(--cds-text-primary) - abgesetzt allein durch den Schatten
0 1px 6px 0 rgba(0,0,0,.2)— kein Rahmen - Label und Reihenname 12 px in
#525252, Wert fett in Tabellenziffern - kein Doppelpunkt zwischen Name und Wert, maximal 270 px breit
- Farbmarker je Reihe, 10 × 10 px
Achtung bei älteren Quellen: Bis Carbon v10 war der Tooltip dunkel
(#393939 auf weißem Text). Sehr viele Artikel und Screenshots zeigen noch
diesen Stand. Maßgeblich ist das ausgelieferte Stylesheet, nicht die Bildersuche.
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:
: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.