Files
Dashboard/features/dashboard/components/charts/Donut.tsx
T
dejanandClaude Opus 5 6a0b36ddcc Geviertstriche aus Text, Kommentaren und Doku entfernen
467 Stellen. Nicht durch einen anderen Strich ersetzt, sondern durch das
Satzzeichen, das an der jeweiligen Stelle richtig ist: Doppelpunkt, wo eine
Erlaeuterung folgt; Komma, wo der Satz weiterlaeuft; Punkt, wo zwei
Hauptsaetze aneinanderstiessen. Ein Halbgeviertstrich waere nur derselbe
Griff mit kuerzerem Strich gewesen.

Regelbasiert ersetzt und danach nachgeprueft, weil eine Regel allein nicht
reicht. Gefunden und einzeln korrigiert:

- Eingeschobene Saetze mit Strichpaar. Nur der oeffnende Strich passte ins
  Muster, der schliessende blieb als "—," stehen. Drei Stellen, jetzt mit
  Klammern oder als eigener Satz.

- Doppelte Doppelpunkte, wo beide Striche eines Paars zu Doppelpunkten wurden
  ("Globale Regeln: Schrift, Groessen, Abstaende: kommen aus HiveGrid").
  Sieben Stellen in Doku und Oberflaeche, jetzt in Klammern.

- Pfade, denen ein Komma statt eines Doppelpunkts folgte.

- Ausgerichtete Kommentartabellen, in denen der Strich eine Spalte war.

Nicht angefasst: drei Geviertstriche, die keine Prosa sind, sondern
Tabellenplatzhalter fuer "kein Wert" (Trichter, Auftraege, Kunden). Dort ist
der Strich die Angabe selbst; ihn zu entfernen liesse leere Zellen zurueck.

tsc, lint, build, verify:carbon und verify:layout laufen sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-31 20:51:19 +02:00

125 lines
4.8 KiB
TypeScript

"use client";
import React from "react";
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
import { ChartTooltip } from "./ChartTooltip";
import { ChartLeer } from "./ChartLeer";
import { DONUT, farbeFuer, gruppiereRest } from "../../utils/chartTheme";
/**
* Donut für Anteile.
*
* Regeln aus der Planung + Carbon:
* - maximal 5 Segmente, Rest als "Sonstige" (gruppiereRest)
* - größtes Segment startet oben, dann im Uhrzeigersinn absteigend
* - Gesamtwert steht in der Mitte, sonst muss man die Segmente addieren
* - Legende rechts mit Wert: Carbon: „positioned to the right of the chart
* when space is plentiful". Ist der Platz nicht reichlich, rutscht sie unter
* den Ring, statt Namen und Werte ineinanderlaufen zu lassen. Gesteuert über
* eine Container-Query, weil dieselbe Karte mal ein Drittel und mal die halbe
* Seitenbreite einnimmt, der Viewport weiß davon nichts.
* - Klick auf einen Legendeneintrag hebt das Segment hervor
*/
export function Donut({
daten,
formatWert,
hoehe = 220,
mitteLabel = "Gesamt",
maxSegmente,
/**
* Format der Ringmitte. Getrennt vom Legendenformat, weil die Mitte eine
* Schlagzahl ist: Steht dieselbe Summe als Kennzahl auf der Seite, muss sie
* gleich geschrieben sein: "285,3 T€" neben "285.318 €" liest sich wie
* zwei verschiedene Zahlen.
*/
formatMitte,
}: {
daten: { name: string; value: number }[];
formatWert: (wert: number) => string;
hoehe?: number;
mitteLabel?: string;
maxSegmente?: number;
formatMitte?: (wert: number) => string;
}) {
const [aktiv, setAktiv] = React.useState<string | null>(null);
const segmente = gruppiereRest(daten.filter((d) => d.value > 0), maxSegmente);
if (!segmente.length) return <ChartLeer hoehe={hoehe} />;
const gesamt = segmente.reduce((summe, s) => summe + s.value, 0);
return (
<div className="@container">
<div className="flex flex-col items-center gap-4 @md:grid @md:grid-cols-[168px_minmax(0,1fr)] @md:items-center @md:gap-5">
<div className="relative w-full max-w-[220px] @md:max-w-none" style={{ height: hoehe }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={segmente}
dataKey="value"
nameKey="name"
innerRadius={DONUT.innenRadius}
outerRadius={DONUT.aussenRadius}
paddingAngle={DONUT.segmentAbstand}
startAngle={90}
endAngle={-270}
stroke="#ffffff"
strokeWidth={2}
isAnimationActive={false}
>
{segmente.map((segment, index) => (
<Cell
key={segment.name}
fill={farbeFuer(segment.name, index)}
opacity={aktiv && aktiv !== segment.name ? 0.35 : 1}
/>
))}
</Pie>
<Tooltip content={(props) => <ChartTooltip {...props} formatWert={formatWert} />} />
</PieChart>
</ResponsiveContainer>
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
<span className="text-[11px] uppercase tracking-[0.06em] text-text-neutral-tertiary">
{mitteLabel}
</span>
<span className="text-[15px] font-semibold tabular-nums text-text-neutral-primary">
{(formatMitte ?? formatWert)(gesamt)}
</span>
</div>
</div>
<ul className="flex w-full flex-col gap-1.5">
{segmente.map((segment, index) => {
const anteil = gesamt > 0 ? (segment.value / gesamt) * 100 : 0;
return (
<li key={segment.name}>
<button
onMouseEnter={() => setAktiv(segment.name)}
onMouseLeave={() => setAktiv(null)}
onClick={() => setAktiv((wert) => (wert === segment.name ? null : segment.name))}
className="flex w-full items-start gap-2 rounded-sm px-1 py-1 text-left hover:bg-neutral-50"
>
<span
className="mt-1 h-2.5 w-2.5 flex-none rounded-sm"
style={{ background: farbeFuer(segment.name, index) }}
/>
<span className="min-w-0 grow break-words text-[12px] leading-snug text-text-neutral-secondary">
{segment.name}
</span>
<span className="flex-none whitespace-nowrap text-[12px] tabular-nums text-text-neutral-primary">
<span className="font-semibold">{formatWert(segment.value)}</span>
<span className="ml-1.5 text-text-neutral-tertiary">
{anteil.toFixed(0)} %
</span>
</span>
</button>
</li>
);
})}
</ul>
</div>
</div>
);
}