Files
2026-09-29 16:16:49 +02:00

82 lines
3.1 KiB
Python

"""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