feat: Engine pie – Tortensegmente mit Übrige-Regel und SVG-Pfaden (A13-T3)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 16:13:11 +02:00
co-authored by Claude Opus 5.5
parent 7555d8e6a4
commit 125d370497
2 changed files with 151 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
from decimal import Decimal
import pytest
from app.engine.pie import UEBRIGE, pie_slices
D = Decimal
def test_leer_und_nullwerte():
assert pie_slices([]) == []
assert pie_slices([("A", D("0")), ("B", D("-5"))]) == []
def test_segmente_lueckenlos_und_anteile():
sl = pie_slices([("C", D("20")), ("A", D("50")), ("B", D("30"))])
assert [s.label for s in sl] == ["A", "B", "C"]
assert [s.color for s in sl] == ["s1", "s2", "s3"]
assert [s.share for s in sl] == [D("50.0"), D("30.0"), D("20.0")]
assert sl[0].start == 0.0 and sl[-1].end == pytest.approx(1.0)
for a, b in zip(sl, sl[1:]):
assert a.end == pytest.approx(b.start)
assert sl[0].path.startswith("M 0 0 L 0.0000 -1.0000 A 1 1 0 0 1 ")
def test_large_arc_flag():
a, b = pie_slices([("A", D("70")), ("B", D("30"))])
assert " A 1 1 0 1 1 " in a.path
assert " A 1 1 0 0 1 " in b.path
def test_einzelsegment_ist_vollkreis():
(s,) = pie_slices([("A", D("12.34"))])
assert s.path is None and s.share == D("100.0") and s.color == "s1"
def test_kleine_werte_zu_uebrige():
sl = pie_slices([("A", D("95")), ("B", D("2")), ("C", D("2")), ("D", D("1"))])
assert [s.label for s in sl] == ["A", UEBRIGE]
u = sl[1]
assert (u.value, u.color, u.merged, u.members) == (D("5"), "uebrige", True, ("B", "C", "D"))
def test_einzelner_kleiner_wert_behaelt_namen():
sl = pie_slices([("A", D("98")), ("B", D("2"))])
assert [(s.label, s.color, s.merged) for s in sl] == [("A", "s1", False), ("B", "uebrige", False)]
def test_hoechstens_acht_benannte_segmente():
sl = pie_slices([(f"K{i}", D("10")) for i in range(10)], rest=D("5"),
rest_label="Frei verfügbar")
assert [s.color for s in sl] == ["s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8",
"uebrige", "frei"]
assert sl[-2].merged and sl[-2].members == ("K8", "K9")
def test_acht_kategorien_ohne_uebrige():
sl = pie_slices([(f"K{i}", D("10")) for i in range(8)])
assert [s.color for s in sl] == [f"s{i}" for i in range(1, 9)]
assert not any(s.merged for s in sl)
def test_rest_wird_nie_zusammengefasst():
sl = pie_slices([("A", D("99.5"))], rest=D("0.5"), rest_label="Frei verfügbar")
assert [(s.label, s.color, s.merged) for s in sl] == [("A", "s1", False), ("Frei verfügbar", "frei", False)]
assert sl[1].share == D("0.5")
def test_nur_rest_ist_vollkreis():
(s,) = pie_slices([], rest=D("100"), rest_label="Frei verfügbar")
assert (s.label, s.color, s.path) == ("Frei verfügbar", "frei", None)
def test_rest_none_oder_null_ignoriert():
assert len(pie_slices([("A", D("1"))], rest=None)) == 1
assert len(pie_slices([("A", D("1"))], rest=D("0"), rest_label="X")) == 1