/** * Ein Kreisdiagramm. * * **Serverseitig gerendertes SVG, kein Skript.** Eine Diagrammbibliothek wäre * hier das Teuerste auf der Seite; die Rechnung ist ein Dutzend Zeilen * Trigonometrie, und das Ergebnis lädt ohne ein Byte JavaScript. * * Die Farben kommen **nicht** aus dem Stufensystem. Die sechs * Klassifizierungsfarben sind das einzige Buntsystem der Anwendung und * bedeuten dort etwas Bestimmtes; ein Tortenstück in Oxidrot würde als * „streng geheim" gelesen. Die Segmente sind deshalb Abstufungen von * `--steel`, von kräftig nach blass. * * Die Beschriftung steht daneben als Liste mit Zahlen, nicht nur als Farbe im * Kreis: Farbe allein darf keine Angabe tragen, siehe docs/10-design-system.md. * Damit ist das Diagramm der Schmuck und die Liste die Auskunft. */ export type Slice = { label: string; value: number } const GROESSE = 160 const RADIUS = 70 export function PieChart({ slices, emptyLabel, totalLabel, }: { slices: Slice[] emptyLabel: string /** „12 Fälle" — steht in der Mitte und sagt, worauf sich die Anteile beziehen. */ totalLabel: (total: number) => string }) { const gesamt = slices.reduce((summe, stueck) => summe + stueck.value, 0) if (gesamt === 0) return

{emptyLabel}

let begonnen = 0 const segmente = slices.map((stueck, index) => { const anteil = stueck.value / gesamt const start = begonnen begonnen += anteil return { ...stueck, anteil, farbe: farbeFuer(index, slices.length), pfad: kreisAusschnitt(start, begonnen), } }) return (
{segmente.length === 1 ? ( // Ein einziger Anteil ist ein voller Kreis. Als Pfad gezeichnet // würde er zu einer Linie zusammenfallen. ) : ( segmente.map((segment) => ( )) )}
{totalLabel(gesamt)}
{segmente.map((segment) => (
{segment.label}
{segment.value} · {Math.round(segment.anteil * 100)}%
))}
) } /** * Abstufungen von `--steel`, gleichmässig verteilt. Bei einem einzigen * Segment die volle Farbe, sonst von kräftig nach blass — so bleibt die * Reihenfolge der Liste auch im Kreis ablesbar. */ function farbeFuer(index: number, anzahl: number): string { if (anzahl === 1) return 'var(--color-accent)' const anteil = 100 - Math.round((index / (anzahl - 1)) * 65) return `color-mix(in srgb, var(--color-accent) ${anteil}%, var(--color-panel))` } /** Ein Kreisausschnitt von `start` bis `ende`, beide als Anteil von 0 bis 1. */ function kreisAusschnitt(start: number, ende: number): string { const mitte = GROESSE / 2 const [x1, y1] = punkt(start) const [x2, y2] = punkt(ende) const grosserBogen = ende - start > 0.5 ? 1 : 0 return `M ${mitte} ${mitte} L ${x1} ${y1} A ${RADIUS} ${RADIUS} 0 ${grosserBogen} 1 ${x2} ${y2} Z` } function punkt(anteil: number): [number, number] { // Bei zwölf Uhr anfangen, im Uhrzeigersinn. const winkel = anteil * 2 * Math.PI - Math.PI / 2 const mitte = GROESSE / 2 return [mitte + RADIUS * Math.cos(winkel), mitte + RADIUS * Math.sin(winkel)] }