Files
astrololo/services/presentation/tests/test_chartwheel.py
T
gitea adce568729 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>
2026-07-28 18:27:40 +00:00

546 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Kosmogram — koło horoskopowe jako SVG (PRE-12).
Sprawdzamy, że SVG jest POPRAWNY (parsuje się jako XML — inaczej wstawiony do
strony rozjedzie render) i że niesie to, co ma nieść: pierścień 12 znaków, cztery
osie, numery domów i obiekty. Geometria (Asc po lewej) oraz rozsuwanie ciasnych
skupisk sprawdzone liczbowo, nie na oko.
"""
import pytest
import xml.dom.minidom as minidom
from app import chartwheel
def _sample_chart(asc=128.9, mc=5.0):
"""Minimalny wynik z warstwy logiki: osie + cuspy (whole sign) + pierścień znaków."""
signs = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
glyphs = ["♈", "♉", "♊", "♋", "♌", "♍", "♎", "♏", "♐", "♑", "♒", "♓"]
asc_sign = int(asc // 30)
cusps = [{"house": i + 1, "sign": signs[(asc_sign + i) % 12],
"decimal": (asc_sign * 30 + i * 30) % 360, "sign_glyph": glyphs[(asc_sign + i) % 12]}
for i in range(12)]
return {
"angles": {
"Asc": {"name": "Asc", "decimal": asc, "sign": signs[asc_sign]},
"MC": {"name": "MC", "decimal": mc},
"Dsc": {"name": "Dsc", "decimal": (asc + 180) % 360},
"IC": {"name": "IC", "decimal": (mc + 180) % 360},
},
"cusps": cusps,
"sign_glyphs": [{"sign": s, "glyph": g} for s, g in zip(signs, glyphs)],
}
def _rendered():
return chartwheel.render(_sample_chart())
def test_output_is_well_formed_xml():
"""Wstawiamy surowy SVG do strony — musi być poprawnym XML, bo inaczej
zepsuje cały dokument."""
svg = _rendered().replace("var(--line)", "#000").replace(
"var(--accent)", "#000").replace("var(--muted)", "#000")
doc = minidom.parseString(svg) # rzuci wyjątkiem, gdy błędny
assert doc.documentElement.tagName == "svg"
def test_has_twelve_sign_glyphs():
assert _rendered().count('class="glyph"') == 12
def test_has_all_four_axis_labels():
svg = _rendered()
for label in (">AC<", ">DC<", ">MC<", ">IC<"):
assert label in svg
def test_has_house_numbers_one_to_twelve():
svg = _rendered()
for h in range(1, 13):
assert f">{h}<" in svg
def test_ascendant_is_on_the_left():
"""Konwencja: Asc po lewej. Sprawdzamy oś ACDC liczbowo — punkt Asc musi mieć
x wyraźnie mniejszy od środka (220), a Dsc większy."""
chart = _sample_chart(asc=128.9)
asc = chart["angles"]["Asc"]["decimal"]
ax, ay = chartwheel._pt(asc, asc, chartwheel.R_ZOD)
dx, dy = chartwheel._pt(asc + 180, asc, chartwheel.R_ZOD)
assert ax < chartwheel._CX - 100, "Asc powinien być po lewej"
assert dx > chartwheel._CX + 100, "Dsc powinien być po prawej"
assert abs(ay - chartwheel._CY) < 1e-6, "oś AscDsc jest pozioma"
def test_longitude_increases_counterclockwise():
"""Asc+90° (II dom) ląduje na DOLE koła (y > środek, bo w SVG y rośnie w dół)."""
asc = 100.0
_, y = chartwheel._pt(asc + 90, asc, chartwheel.R_ZOD)
assert y > chartwheel._CY + 100
def test_empty_when_no_angles():
"""Silnik bez osi/domów (brak sidereal) → brak koła, nie wyjątek."""
assert chartwheel.render({"positions": []}) == ""
assert chartwheel.render({"angles": {}, "cusps": [], "sign_glyphs": []}) == ""
# ───────────────────────── rozsuwanie nakładających się obiektów ─────────
# Prawdziwe długości z horoskopu referencyjnego (1984-04-30, Kraków), w kolejności
# DEFAULT_OBJECTS. Merkury i Wenus dzieli 0,41°, Wenus i Księżyc 3,68° — bez
# rozsuwania glify rysują się jeden na drugim.
_REAL = [40.210, 31.449, 27.355, 27.767, 234.520, 282.962, 223.306,
252.811, 271.220, 210.477, 68.158, 248.158, 345.667]
def _min_gap(angles):
s = sorted(a % 360.0 for a in angles)
return min((s[(i + 1) % len(s)] - s[i]) % 360.0 for i in range(len(s)))
def test_spread_separates_real_chart_cluster():
"""REGRESJA: przy liczeniu odstępu modulo 360 przesunięcie, które przerzucało
obiekt ZA sąsiada, dawało lukę ~359,9° zamiast ujemnej — algorytm uznawał, że
jest luzem, i kończył z Wenus dokładnie na Księżycu (0,3 px od siebie)."""
out = chartwheel.spread(_REAL)
assert _min_gap(out) >= chartwheel.MIN_SEP - 1e-6
def test_spread_preserves_order():
out = chartwheel.spread(_REAL)
by_new = [i for i, _ in sorted(enumerate(out), key=lambda p: p[1])]
by_old = [i for i, _ in sorted(enumerate(_REAL), key=lambda p: p[1] % 360.0)]
assert by_new == by_old
def test_spread_leaves_roomy_objects_untouched():
"""Obiekty, które nikomu nie wchodzą w drogę, mają zostać na swoim miejscu —
wykres ma kłamać jak najmniej."""
out = chartwheel.spread(_REAL)
node = _REAL.index(68.158) # Węzeł Płn. — 28° od najbliższego
assert out[node] == pytest.approx(_REAL[node], abs=1e-6)
def test_spread_handles_wrap_around_zero():
"""Skupisko na styku 0/360° też musi się rozsunąć."""
out = chartwheel.spread([358.0, 359.0, 1.0, 2.0])
assert _min_gap(out) >= chartwheel.MIN_SEP - 1e-6
def test_spread_falls_back_to_even_layout_when_impossible():
"""Gdy obiektów jest tyle, że min. odstęp się nie zmieści — rozkładamy równo,
zamiast kręcić się w pętli."""
many = [10.0] * 60 # 60 × 8° = 480° > 360°
out = chartwheel.spread(many)
assert _min_gap(out) == pytest.approx(360.0 / 60, abs=1e-6)
def test_spread_single_object_is_identity():
assert chartwheel.spread([42.0]) == [42.0]
# ─────────────────────────────── obiekty na kole ─────────────────────────
def _chart_with_objects():
chart = _sample_chart()
chart["positions"] = [
{"name": "Sun", "decimal": 40.21, "glyph": "☉", "direction": "D"},
{"name": "Moon", "decimal": 31.45, "glyph": "☽", "direction": "D"},
{"name": "Venus", "decimal": 27.77, "glyph": "♀", "direction": "D"},
{"name": "Mercury", "decimal": 27.36, "glyph": "☿", "direction": "Rx"},
]
return chart
def test_all_objects_are_drawn():
svg = chartwheel.render(_chart_with_objects())
for glyph in ("☉", "☽", "♀", "☿"):
assert glyph in svg
def test_degree_in_sign_is_labelled():
"""Stopień W ZNAKU (nie długość absolutna): Słońce 40,21° → 10 Byka."""
svg = chartwheel.render(_chart_with_objects())
assert ">10<" in svg # Słońce
assert ">1<" in svg # Księżyc (31,45 → 1 Byka)
def test_retrograde_is_marked():
svg = chartwheel.render(_chart_with_objects())
assert "℞" in svg
def test_no_retrograde_marker_when_all_direct():
chart = _chart_with_objects()
for p in chart["positions"]:
p["direction"] = "D"
assert "℞" not in chartwheel.render(chart)
def test_objects_without_longitude_are_skipped():
"""Niekompletny obiekt nie może wywalić całego rysunku."""
chart = _chart_with_objects()
chart["positions"].append({"name": "Broken", "glyph": "?"})
svg = chartwheel.render(chart)
assert svg and "☉" in svg
def test_empty_when_cusps_lack_longitude():
"""Starsze wyniki bez `decimal` w cuspach — degradujemy do pustego, nie sypiemy."""
chart = _sample_chart()
for c in chart["cusps"]:
del c["decimal"]
assert chartwheel.render(chart) == ""
# ───────────────────────────── linie aspektów ────────────────────────────
def _chart_with_aspects():
chart = _chart_with_objects()
chart["positions"].append({"name": "Mars", "decimal": 220.0, "glyph": "♂", "direction": "D"})
chart["aspects"] = [
{"obj1": "Sun", "obj2": "Mars", "aspect": "trine", "orb": 0.2, "allowed": 8.0},
{"obj1": "Moon", "obj2": "Mars", "aspect": "square", "orb": 5.0, "allowed": 8.0},
{"obj1": "Mercury", "obj2": "Venus", "aspect": "conjunction", "orb": 0.4, "allowed": 10.0},
]
return chart
def test_aspect_lines_are_drawn():
"""Każdy aspekt, którego oba obiekty mają pozycję, dostaje linię — porównujemy
TEN SAM wykres z aspektami i bez, żeby różnica była wyłącznie aspektami."""
chart = _chart_with_aspects()
with_asp = chartwheel.render(chart).count("<line")
chart_no = _chart_with_aspects()
chart_no["aspects"] = []
without = chartwheel.render(chart_no).count("<line")
assert with_asp - without == 3
def test_aspect_color_encodes_type():
"""Niebieski = harmonijny, czerwony = napięty, zielony = koniunkcja."""
svg = chartwheel.render(_chart_with_aspects())
assert "#6aa6c9" in svg # trine — harmonijny (niebieski)
assert "#cf6a6a" in svg # square — napięty (czerwony)
assert "#8fae7a" in svg # conjunction — zielony
def test_tight_aspect_is_bold():
"""Poniżej 1° linia pogrubiona (2.0) — wprost z wymagania. Szeroki aspekt
(square, orb 5°) NIE jest pogrubiony."""
svg = chartwheel.render(_chart_with_aspects())
assert 'stroke-width="2.0"' in svg # SunMars trine 0.2° i MercuryVenus 0.4°
# dwa ciasne aspekty → dwie pogrubione linie
assert svg.count('stroke-width="2.0"') == 2
def test_aspect_uses_true_position_not_spread():
"""Linia aspektu łączy PRAWDZIWE pozycje. Mercury(27.36) i Venus(0.41° od
siebie) są rozsuwane jako glify, ale linia koniunkcji ma iść między ich
faktycznymi punktami na piaście."""
chart = _chart_with_aspects()
asc = chart["angles"]["Asc"]["decimal"]
# punkt Wenus na piaście z prawdziwej długości
vx, vy = chartwheel._pt(27.77, asc, chartwheel.R_HUB)
svg = chartwheel.render(chart)
assert f'x1="{vx:.2f}"' in svg or f'x2="{vx:.2f}"' in svg
def test_aspect_to_missing_object_is_skipped():
"""Aspekt do obiektu bez pozycji nie może wywalić rysunku."""
chart = _chart_with_aspects()
chart["aspects"].append(
{"obj1": "Sun", "obj2": "Chiron", "aspect": "trine", "orb": 1.0, "allowed": 8.0})
svg = chartwheel.render(chart)
assert svg and "☉" in svg
def test_no_aspect_lines_without_aspects():
chart = _chart_with_objects() # bez klucza 'aspects'
a = chartwheel.render(chart).count("<line")
chart["aspects"] = []
assert chartwheel.render(chart).count("<line") == a
def test_print_theme_aspect_colors_have_no_css_vars():
"""Motyw druku (PDF) — konkretne kolory aspektów, bez zmiennych CSS."""
svg = chartwheel.render(_chart_with_aspects(), theme="print")
assert "var(--" not in svg
assert "#a83232" in svg # square napięty w wariancie druku
# ─────────────────────────── etap 4: dopracowanie ────────────────────────
def test_objects_have_hover_tooltips():
"""Najechanie na glif pokazuje nazwę, pozycję, dom i retrogradację (natywny
<title> w SVG — bez JS-a)."""
chart = _chart_with_objects()
for p in chart["positions"]: # fixture jest minimalny; dodaj etykiety
p["in_sign"] = "Tau 10°"
p["house"] = 9
svg = chartwheel.render(chart)
assert "<title>" in svg
assert "Sun · Tau 10° · dom 9" in svg # nazwa + pozycja + dom w dymku
def test_retrograde_object_says_so_in_tooltip():
svg = chartwheel.render(_chart_with_objects()) # Mercury jest Rx
assert "retrogradacja" in svg
def test_aspect_lines_have_polish_tooltips():
svg = chartwheel.render(_chart_with_aspects())
assert "trygon" in svg # SunMars trine
assert "kwadratura" in svg # MoonMars square
assert "orb" in svg
def test_part_of_fortune_is_drawn():
"""Fortuna (⊗) na kole — jedyny Lot ze standardowym glifem."""
chart = _chart_with_objects()
chart["lots"] = [
{"name": "Fortune", "longitude": 120.14, "glyph": "⊗", "in_sign": "Leo 0°", "house": 1},
{"name": "Spirit", "longitude": 137.66, "glyph": None, "in_sign": "Leo 17°"},
]
svg = chartwheel.render(chart)
assert "⊗" in svg
assert "Fortune · " in svg # dymek Lota
def test_lots_without_glyph_are_skipped():
"""Loty bez standardowego symbolu (Spirit, Eros…) nie zaśmiecają koła."""
chart = _chart_with_objects()
chart["lots"] = [{"name": "Spirit", "longitude": 137.66, "glyph": None}]
before = chartwheel.render(chart).count("<text")
chart["lots"] = []
after = chartwheel.render(chart).count("<text")
assert before == after # Spirit nic nie dorysował
def test_cusp_degrees_shown_for_quadrant_hidden_for_whole_sign():
"""Whole sign: cuspy na 0° znaku → pomijamy zbędne zera. System kwadratowy:
stopnie realnie coś mówią, więc je pokazujemy."""
whole = _sample_chart() # cuspy na 0/30/60…
assert chartwheel.render(whole).count('font-size="6.5"') == 0
quad = _sample_chart()
for i, c in enumerate(quad["cusps"]): # przesuń cuspy poza granice znaków
c["decimal"] = (c["decimal"] + 12.5) % 360.0
assert chartwheel.render(quad).count('font-size="6.5"') == 12
def test_print_theme_survives_stage4_features():
"""Motyw druku (PDF) nadal bez zmiennych CSS mimo Lotów, dymków i cuspów."""
chart = _chart_with_aspects()
chart["lots"] = [{"name": "Fortune", "longitude": 120.0, "glyph": "⊗", "in_sign": "Leo 0°"}]
svg = chartwheel.render(chart, theme="print")
assert "var(--" not in svg
assert "⊗" in svg and "<title>" in svg
# ─────────────────────────── etap 5: aspektarian (PRE-18) ─────────────────
def test_aspectarian_is_well_formed_xml():
"""Aspektarian też wstawiamy surowo do strony — musi być poprawnym XML."""
svg = chartwheel.render_aspectarian(_chart_with_aspects())
svg = (svg.replace("var(--line)", "#000").replace("var(--accent)", "#000")
.replace("var(--muted)", "#000").replace("var(--ink)", "#000"))
doc = minidom.parseString(svg)
assert doc.documentElement.tagName == "svg"
def test_aspectarian_triangular_grid_has_right_cell_count():
"""N obiektów → przekątna (N komórek) + dolny-lewy trójkąt (N·(N1)/2).
Fixture ma 5 obiektów: 5 + 10 = 15 komórek."""
svg = chartwheel.render_aspectarian(_chart_with_aspects())
assert svg.count("<rect") == 15
def test_aspectarian_puts_object_glyphs_on_diagonal():
"""Każdy obiekt raz — na przekątnej."""
svg = chartwheel.render_aspectarian(_chart_with_aspects())
for glyph in ("☉", "☽", "♀", "☿", "♂"):
assert glyph in svg
def test_aspectarian_shows_aspect_glyphs():
"""Aspekty z fixture: trygon (△), kwadratura (□), koniunkcja (☌)."""
svg = chartwheel.render_aspectarian(_chart_with_aspects())
assert "△" in svg and "□" in svg and "☌" in svg
def test_aspectarian_colors_match_the_wheel():
"""Te same barwy co linie na kole: niebieski/czerwony/zielony."""
svg = chartwheel.render_aspectarian(_chart_with_aspects())
assert "#6aa6c9" in svg # trine — harmonijny
assert "#cf6a6a" in svg # square — napięty
assert "#8fae7a" in svg # conjunction — zielony
def test_aspectarian_tight_aspect_is_bold():
"""Orb < 1° → glif pogrubiony (jak pogrubiona linia na kole). Dwa ciasne
aspekty w fixture (SunMars 0.2°, MercuryVenus 0.4°) → dwa pogrubienia."""
svg = chartwheel.render_aspectarian(_chart_with_aspects())
assert svg.count('font-weight="bold"') == 2
def test_aspectarian_cells_have_tooltips():
svg = chartwheel.render_aspectarian(_chart_with_aspects())
assert "<title>" in svg
assert "trygon" in svg and "orb" in svg
def test_aspectarian_tooltip_shows_applying_separating():
"""Gdy aspekt niesie kierunek (A/S), dymek to mówi po polsku."""
chart = _chart_with_aspects()
chart["aspects"][0]["as"] = "A" # SunMars aplikacyjny
svg = chartwheel.render_aspectarian(chart)
assert "aplikacyjny" in svg
def test_aspectarian_empty_for_fewer_than_two_objects():
chart = _chart_with_objects()
chart["positions"] = chart["positions"][:1]
assert chartwheel.render_aspectarian(chart) == ""
def test_aspectarian_renders_grid_even_without_aspects():
"""Pusta siatka też niesie informację (które pary NIE mają aspektu) —
rysujemy przekątną z obiektami, bez glifów aspektów."""
chart = _chart_with_objects()
chart["aspects"] = []
svg = chartwheel.render_aspectarian(chart)
assert svg and "☉" in svg
for asp_glyph in ("△", "□", "☌", "⚹", "☍"):
assert asp_glyph not in svg
def test_aspectarian_object_without_glyph_is_safe():
"""Niekompletny obiekt (bez glifu) nie wywala siatki."""
chart = _chart_with_aspects()
chart["positions"].append({"name": "Chiron", "decimal": 100.0}) # brak glyph
svg = chartwheel.render_aspectarian(chart)
assert svg and "☉" in svg
def test_aspectarian_print_theme_has_no_css_vars():
"""Motyw druku (PDF) — konkretne kolory, żaden `var(--…)`."""
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