feat(prezentacja): wykres deklinacji i oś antyscji (LOG-07, kosmogram etap 6)

Ostatni etap rysowania kosmogramu: wizualizacje pochodne z aspektów
pozazodiakalnych (LOG-07). Do tej pory paralele i antyscja były tylko w tabelach —
teraz widać je na rzut oka.

WYKRES DEKLINACJI (`render_declination`):
- Pionowa skala deklinacji z równikiem (0°) i zwrotnikami (±ε, kreskowane) —
  ε bierzemy z `obliquity` w wyniku, więc granica jest dokładna dla daty.
- Obiekty na osi X ułożone wg POSORTOWANEJ deklinacji, więc paralele (ta sama
  wysokość) lądują obok siebie. Łączniki: paralela zielona (jak koniunkcja),
  kontrparalela czerwona (jak opozycja) — te same barwy co linie na kole, grubość
  wg orbu. Dymek z nazwą zjawiska i orbem.
- Strefa poza zwrotnikami cieniowana; obiekt OOB (out-of-bounds) w kolorze
  wyróżnienia + „OOB" w dymku. Od razu widać ciała o skrajnej deklinacji.

OŚ ANTYSCJI (`render_antiscia`):
- Ekliptyka rozwinięta w poziomą oś ze znakami; pionowo zaznaczona oś przesileń
  (0° Raka/Koziorożca) — lustro antyscji — i oś równonocy (0° Barana/Wagi) dla
  kontrantyscji. Pary połączone łukiem (zielony antyscja / czerwony kontrantyscja),
  z dymkiem. Bez par oś i obiekty i tak coś mówią.

Oba w obu motywach (screen + print), więc gotowe też do PDF-a. Pokazują się na
/chart pod odpowiednimi tabelami LOG-07. Testy: +13 (etap 6). Prezentacja: 170.

Domyka etapy kosmogramu (PRE-12): 1 szkielet, 2 obiekty, 3 aspekty, 4 dopracowanie,
5 aspektarian, 6 deklinacja/antyscja.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 17:17:35 +02:00
committed by gitea
parent 998c83b26e
commit adce568729
5 changed files with 298 additions and 0 deletions
@@ -429,3 +429,117 @@ def test_aspectarian_print_theme_has_no_css_vars():
svg = chartwheel.render_aspectarian(_chart_with_aspects(), theme="print")
assert "var(--" not in svg
assert "#a83232" in svg # square napięty w wariancie druku
# ─────────────── etap 6: deklinacja i antyscja (LOG-07) ───────────────────
def _chart_with_declination():
chart = _chart_with_objects() # Sun/Moon/Venus/Mercury
chart["obliquity"] = 23.44
decs = {"Sun": 15.0, "Moon": 15.2, "Venus": -15.0, "Mercury": 25.5}
for p in chart["positions"]:
p["declination"] = decs[p["name"]]
if p["name"] == "Mercury":
p["out_of_bounds"] = True # 25.5° > ε=23.44 → OOB
chart["parallels"] = [
{"obj1": "Sun", "obj2": "Moon", "type": "parallel",
"orb": 0.2, "allowed": 1.0, "dec1": 15.0, "dec2": 15.2},
{"obj1": "Sun", "obj2": "Venus", "type": "contraparallel",
"orb": 0.0, "allowed": 1.0, "dec1": 15.0, "dec2": -15.0},
]
return chart
def test_declination_is_well_formed_xml():
svg = chartwheel.render_declination(_chart_with_declination())
svg = (svg.replace("var(--line)", "#000").replace("var(--accent)", "#000")
.replace("var(--muted)", "#000").replace("var(--ink)", "#000"))
assert minidom.parseString(svg).documentElement.tagName == "svg"
def test_declination_empty_for_fewer_than_two():
chart = _chart_with_declination()
chart["positions"] = chart["positions"][:1]
assert chartwheel.render_declination(chart) == ""
def test_declination_draws_equator_and_tropics():
"""Równik (0°) i dwie linie zwrotników (kreskowane) muszą być."""
svg = chartwheel.render_declination(_chart_with_declination())
assert ">0°<" in svg
assert svg.count("stroke-dasharray") >= 2 # ±ε kreskowane
def test_declination_object_glyphs_present():
svg = chartwheel.render_declination(_chart_with_declination())
for g in ("", "", "", ""):
assert g in svg
def test_declination_oob_object_is_highlighted():
"""Merkury poza zwrotnikami (OOB) w kolorze wyróżnienia + z „OOB" w dymku."""
svg = chartwheel.render_declination(_chart_with_declination())
assert "#ff9b6a" in svg # kolor retro/OOB (motyw ekranowy)
assert "OOB" in svg
def test_declination_parallel_and_contraparallel_colors():
"""Paralela jak koniunkcja (zielony), kontrparalela jak opozycja (czerwony)."""
svg = chartwheel.render_declination(_chart_with_declination())
assert "#8fae7a" in svg # parallel → zielony
assert "#cf6a6a" in svg # contraparallel → czerwony
assert "paralela" in svg # dymek
def test_declination_print_theme_has_no_css_vars():
svg = chartwheel.render_declination(_chart_with_declination(), theme="print")
assert "var(--" not in svg
def _chart_with_antiscia():
chart = _chart_with_objects()
chart["antiscia"] = [
{"obj1": "Sun", "obj2": "Moon", "type": "antiscion", "orb": 0.3, "allowed": 1.0},
{"obj1": "Venus", "obj2": "Mercury", "type": "contra_antiscion", "orb": 0.5, "allowed": 1.0},
]
return chart
def test_antiscia_is_well_formed_xml():
svg = chartwheel.render_antiscia(_chart_with_antiscia())
svg = (svg.replace("var(--line)", "#000").replace("var(--accent)", "#000")
.replace("var(--muted)", "#000").replace("var(--ink)", "#000"))
assert minidom.parseString(svg).documentElement.tagName == "svg"
def test_antiscia_empty_for_fewer_than_two():
chart = _chart_with_antiscia()
chart["positions"] = chart["positions"][:1]
assert chartwheel.render_antiscia(chart) == ""
def test_antiscia_draws_reflection_axes():
"""Oś przesileń (i równonocy) — linie kreskowane."""
svg = chartwheel.render_antiscia(_chart_with_antiscia())
assert svg.count("stroke-dasharray") >= 2
def test_antiscia_pair_colors_and_tooltip():
"""Antyscja jak koniunkcja (zielony łuk), kontrantyscja jak opozycja (czerwony)."""
svg = chartwheel.render_antiscia(_chart_with_antiscia())
assert "#8fae7a" in svg # antiscion → zielony
assert "#cf6a6a" in svg # contra_antiscion → czerwony
assert "antyscja" in svg and "<path" in svg
def test_antiscia_renders_axis_even_without_pairs():
"""Bez par (ciasny orb) oś i obiekty i tak coś mówią."""
chart = _chart_with_objects()
chart["antiscia"] = []
svg = chartwheel.render_antiscia(chart)
assert svg and "stroke-dasharray" in svg and "" in svg
def test_antiscia_print_theme_has_no_css_vars():
svg = chartwheel.render_antiscia(_chart_with_antiscia(), theme="print")
assert "var(--" not in svg