"""Tortendiagramm-Segmente (Ausbaustufe 13), rein. Beträge und Anteile bleiben Decimal; nur die Geometrie (Winkel, SVG- Koordinaten im Einheitskreis) rechnet mit float. Start 12 Uhr, im Uhrzeigersinn (SVG-y zeigt nach unten). Große Segmente (> 50 %) werden als zwei Teilbögen durch Mittelpunkt gezogen, um Rundungsfehler zu vermeiden. """ import math from dataclasses import dataclass from decimal import ROUND_HALF_UP, Decimal UEBRIGE = "Übrige" MIN_SHARE = Decimal("0.03") MAX_NAMED = 8 PCT = Decimal("0.1") @dataclass(frozen=True) class Slice: label: str value: Decimal share: Decimal # Prozent der Tortenbasis, eine Nachkommastelle path: str | None # SVG-d; None = Vollkreis (einziges Segment) color: str # "s1".."s8", "uebrige", "frei" merged: bool members: tuple[str, ...] start: float # Anteil 0..1 end: float def _point(fraction: float) -> tuple[float, float]: angle = 2 * math.pi * fraction - math.pi / 2 return math.cos(angle), math.sin(angle) def _arc(start: float, end: float) -> str: x0, y0 = _point(start) x1, y1 = _point(end) if end - start > 0.5: # Großes Segment: zwei Teilbögen durch Mittelpunkt, um Rundungsfehler zu vermeiden mid = (start + end) / 2 xm, ym = _point(mid) return f"M 0 0 L {x0:.4f} {y0:.4f} A 1 1 0 0 1 {xm:.4f} {ym:.4f} A 1 1 0 0 1 {x1:.4f} {y1:.4f} Z" else: return f"M 0 0 L {x0:.4f} {y0:.4f} A 1 1 0 0 1 {x1:.4f} {y1:.4f} Z" def pie_slices(values: list[tuple[str, Decimal]], rest: Decimal | None = None, rest_label: str = "", min_share: Decimal = MIN_SHARE, max_named: int = MAX_NAMED) -> list[Slice]: items = sorted(((l, v) for l, v in values if v > 0), key=lambda lv: (-lv[1], lv[0])) rest = rest if rest is not None and rest > 0 else None total = sum((v for _, v in items), Decimal("0")) + (rest or Decimal("0")) if total <= 0: return [] named: list[tuple[str, Decimal]] = [] small: list[tuple[str, Decimal]] = [] for label, value in items: if len(named) < max_named and value / total >= min_share: named.append((label, value)) else: small.append((label, value)) parts = [(l, v, f"s{i}", False, ()) for i, (l, v) in enumerate(named, 1)] if len(small) == 1: parts.append((small[0][0], small[0][1], "uebrige", False, ())) elif small: parts.append((UEBRIGE, sum((v for _, v in small), Decimal("0")), "uebrige", True, tuple(l for l, _ in small))) if rest is not None: parts.append((rest_label, rest, "frei", False, ())) slices: list[Slice] = [] acc = Decimal("0") for label, value, color, merged, members in parts: start = float(acc / total) acc += value end = float(acc / total) slices.append(Slice(label, value, (value * 100 / total).quantize(PCT, ROUND_HALF_UP), None if len(parts) == 1 else _arc(start, end), color, merged, members, start, end)) return slices