Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
76 lines
2.7 KiB
Python
76 lines
2.7 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).
|
|
"""
|
|
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)
|
|
large = 1 if end - start > 0.5 else 0
|
|
return f"M 0 0 L {x0:.4f} {y0:.4f} A 1 1 0 {large} 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
|