Zeitachse duennt sich selbst aus, statt Monatsnamen zusammenzudruecken

Zwoelf Monatsnamen brauchen rund 340 px. In einer Drittelkarte bei 1400 px
sind knapp 300 da — "Aug", "Sep" und "Okt" standen ohne Luecke aneinander und
lasen sich als ein Wort.

Die Ursache war eine Schwelle, die nur die Datenmenge kannte: Erst ab mehr als
zwoelf Punkten wurde ueberhaupt ausgeduennt. Genau zwoelf Monate lagen darunter
und wurden alle gezeichnet, egal wie schmal die Karte war.

Jetzt zwei Stufen, und die zweite haengt an der Breite statt an der Menge:
Ueber zwoelf Werten weiterhin fest jede n-te Beschriftung — das ist eine
Entscheidung ueber die Reihe und darf nicht vom Bildschirm abhaengen. Darunter
entscheidet Recharts zur Laufzeit (preserveStartEnd mit minTickGap) und laesst
weg, was nicht passt, haelt dabei aber die erste und letzte Beschriftung; ohne
die waere der Zeitraum nicht mehr ablesbar.

Ergebnis: bei 1920 px stehen alle zwoelf Monate mit Luft, bei 1400 px zeigt
dieselbe Karte Jan, Maer, Mai, Jul, Sep, Dez. Diagramme mit wenigen Punkten
(fuenf Kalenderwochen im Auftragseingang) bleiben unveraendert vollstaendig.

Die Regel liegt jetzt als achsenIntervall() in chartTheme.ts, weil sie zwei
Diagramme betrifft. Der Zeitverlauf hatte den Fall schon behoben, das
Saeulendiagramm nicht — genau so laeuft so etwas auseinander.

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-31 20:36:03 +02:00
1 parent 84ce322a19
commit e998231fed
3 files changed
+53 -25

No files matched your search

@@ -16,11 +16,12 @@ import { ChartLeer } from "./ChartLeer";
import {
CHART,
MARGIN,
MAX_ACHSENLABELS,
MIN_TICK_ABSTAND,
PRIMAER_FARBE,
VERGLEICH_FARBE,
achseX,
achseY,
achsenIntervall,
nettesRaster,
} from "../../utils/chartTheme";
import type { ZeitreihePunkt } from "../../data/types";
@@ -66,25 +67,8 @@ export function AreaVerlauf({
const hatWerte = daten.some((d) => d.value > 0 || (d.vergleich ?? 0) > 0);
if (!daten.length || !hatWerte) return <ChartLeer hoehe={hoehe} />;
/**
* Beschriftungen der Zeitachse ausdünnen — zweistufig, weil zwei
* verschiedene Dinge sie zum Kollidieren bringen.
*
* Viele Punkte: Bei mehr als zwölf Werten wird fest jede n-te Beschriftung
* gezeigt. Das ist eine Entscheidung über die Lesbarkeit der Reihe und darf
* nicht von der Kartenbreite abhängen, sonst zeigt dieselbe Reihe auf zwei
* Bildschirmen verschiedene Stützstellen.
*
* Wenige Punkte in einer schmalen Karte: Fünf Kalenderwochen brauchen rund
* 200 px. Eine halbe Kachel bei 768 px hat 150 px — dort liefen "KW 27" und
* "KW 28" ineinander. `preserveStartEnd` mit `minTickGap` überlässt Recharts
* das Weglassen und hält dabei die erste und letzte Beschriftung, damit der
* Zeitraum ablesbar bleibt.
*/
const vieleWerte = daten.length > MAX_ACHSENLABELS;
const intervall = vieleWerte
? Math.ceil(daten.length / MAX_ACHSENLABELS) - 1
: ("preserveStartEnd" as const);
// Ausdünnen der Zeitachse: siehe achsenIntervall() in chartTheme.ts.
const intervall = achsenIntervall(daten.length);
const maximum = Math.max(
...daten.map((d) => Math.max(d.value, zeigeVergleich ? (d.vergleich ?? 0) : 0))
@@ -103,7 +87,12 @@ export function AreaVerlauf({
</defs>
<CartesianGrid stroke={CHART.grid} vertical={false} />
<XAxis dataKey="label" interval={intervall} minTickGap={12} {...achseX} />
<XAxis
dataKey="label"
interval={intervall}
minTickGap={MIN_TICK_ABSTAND}
{...achseX}
/>
<YAxis
domain={raster.domain}
ticks={raster.ticks}
@@ -8,7 +8,8 @@ import {
BALKEN,
CHART,
MARGIN,
MAX_ACHSENLABELS,
MIN_TICK_ABSTAND,
achsenIntervall,
PRIMAER_FARBE,
achseX,
achseY,
@@ -47,8 +48,9 @@ export function BalkenVertikal({
const punkte = daten.map((d) => ({ label: d.label ?? d.name ?? "", value: d.value }));
if (!punkte.length || !punkte.some((p) => p.value > 0)) return <ChartLeer hoehe={hoehe} />;
const intervall =
punkte.length > MAX_ACHSENLABELS ? Math.ceil(punkte.length / MAX_ACHSENLABELS) - 1 : 0;
// Ausdünnen der Zeitachse: siehe achsenIntervall() in chartTheme.ts. Zwölf
// Monatsnamen standen in einer Drittelkarte ohne Lücke aneinander.
const intervall = achsenIntervall(punkte.length);
// Ganzzahlreihen brauchen ganzzahlige Ticks — sonst steht "1,5 Vorgänge".
const maximum = Math.max(...punkte.map((p) => p.value));
@@ -69,7 +71,12 @@ export function BalkenVertikal({
onMouseLeave={() => setAktiv(null)}
>
<CartesianGrid stroke={CHART.grid} vertical={false} />
<XAxis dataKey="label" interval={intervall} {...achseX} />
<XAxis
dataKey="label"
interval={intervall}
minTickGap={MIN_TICK_ABSTAND}
{...achseX}
/>
<YAxis
tickFormatter={formatAchse ?? formatWert}
allowDecimals={!nurGanzzahlen}
+32
View File
@@ -267,6 +267,38 @@ export function nettesRaster(
*/
export const MAX_ACHSENLABELS = 12;
/**
* Mindestabstand zwischen zwei Beschriftungen der Zeitachse, in Pixeln.
*
* 12 px entspricht Carbons `$spacing-04`. Weniger ist keine Frage des
* Geschmacks: Bei rund 8 px beruehren sich "Nov" und "Dez" bereits, und die
* Achse liest sich als ein einziges Wort.
*/
export const MIN_TICK_ABSTAND = 12;
/**
* Wie stark die Zeitachse ihre Beschriftungen ausduennt.
*
* Zwei Stufen, weil zwei verschiedene Dinge sie zum Kollidieren bringen:
*
* **Viele Punkte.** Ueber MAX_ACHSENLABELS Werten wird fest jede n-te
* Beschriftung gezeigt. Das ist eine Entscheidung ueber die Lesbarkeit der
* Reihe und darf nicht von der Kartenbreite abhaengen — sonst zeigt dieselbe
* Reihe auf zwei Bildschirmen verschiedene Stuetzstellen.
*
* **Wenige Punkte in einer schmalen Karte.** Zwoelf Monatsnamen brauchen rund
* 340 px. Eine Drittelkarte hat bei 1400 px knapp 300 — dort standen "Aug",
* "Sep" und "Okt" ohne Luecke aneinander. Diese Faelle haengen an der Breite
* und nicht an der Datenmenge, deshalb entscheidet sie Recharts zur Laufzeit:
* `preserveStartEnd` laesst weg, was nicht passt, und haelt dabei die erste
* und letzte Beschriftung — ohne die waere der Zeitraum nicht mehr ablesbar.
*/
export function achsenIntervall(anzahlPunkte: number): number | "preserveStartEnd" {
return anzahlPunkte > MAX_ACHSENLABELS
? Math.ceil(anzahlPunkte / MAX_ACHSENLABELS) - 1
: "preserveStartEnd";
}
/** Farbe nach Index, rotiert sicher innerhalb der Palette. */
export function farbe(index: number): string {
return KATEGORIE_FARBEN[index % KATEGORIE_FARBEN.length];