kerykeion 5.0.0a9__py3-none-any.whl → 5.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kerykeion might be problematic. Click here for more details.
- kerykeion/__init__.py +50 -9
- kerykeion/aspects/__init__.py +5 -2
- kerykeion/aspects/aspects_factory.py +568 -0
- kerykeion/aspects/aspects_utils.py +78 -11
- kerykeion/astrological_subject_factory.py +1032 -275
- kerykeion/backword.py +820 -0
- kerykeion/chart_data_factory.py +552 -0
- kerykeion/charts/chart_drawer.py +2661 -0
- kerykeion/charts/charts_utils.py +652 -399
- kerykeion/charts/draw_planets.py +603 -353
- kerykeion/charts/templates/aspect_grid_only.xml +326 -198
- kerykeion/charts/templates/chart.xml +306 -256
- kerykeion/charts/templates/wheel_only.xml +330 -200
- kerykeion/charts/themes/black-and-white.css +148 -0
- kerykeion/charts/themes/classic.css +11 -0
- kerykeion/charts/themes/dark-high-contrast.css +11 -0
- kerykeion/charts/themes/dark.css +11 -0
- kerykeion/charts/themes/light.css +11 -0
- kerykeion/charts/themes/strawberry.css +10 -0
- kerykeion/composite_subject_factory.py +232 -13
- kerykeion/ephemeris_data_factory.py +443 -0
- kerykeion/fetch_geonames.py +78 -21
- kerykeion/house_comparison/__init__.py +4 -1
- kerykeion/house_comparison/house_comparison_factory.py +52 -19
- kerykeion/house_comparison/house_comparison_utils.py +37 -9
- kerykeion/kr_types/__init__.py +66 -6
- kerykeion/kr_types/chart_template_model.py +20 -0
- kerykeion/kr_types/kerykeion_exception.py +15 -9
- kerykeion/kr_types/kr_literals.py +14 -160
- kerykeion/kr_types/kr_models.py +14 -291
- kerykeion/kr_types/settings_models.py +15 -167
- kerykeion/planetary_return_factory.py +545 -40
- kerykeion/relationship_score_factory.py +137 -63
- kerykeion/report.py +749 -64
- kerykeion/schemas/__init__.py +106 -0
- kerykeion/schemas/chart_template_model.py +367 -0
- kerykeion/schemas/kerykeion_exception.py +20 -0
- kerykeion/schemas/kr_literals.py +181 -0
- kerykeion/schemas/kr_models.py +603 -0
- kerykeion/schemas/settings_models.py +188 -0
- kerykeion/settings/__init__.py +20 -1
- kerykeion/settings/chart_defaults.py +444 -0
- kerykeion/settings/config_constants.py +88 -12
- kerykeion/settings/kerykeion_settings.py +32 -75
- kerykeion/settings/translation_strings.py +1499 -0
- kerykeion/settings/translations.py +74 -0
- kerykeion/sweph/ast136/s136108s.se1 +0 -0
- kerykeion/sweph/ast136/s136199s.se1 +0 -0
- kerykeion/sweph/ast136/s136472s.se1 +0 -0
- kerykeion/sweph/ast28/se28978s.se1 +0 -0
- kerykeion/sweph/ast50/se50000s.se1 +0 -0
- kerykeion/sweph/ast90/se90377s.se1 +0 -0
- kerykeion/sweph/ast90/se90482s.se1 +0 -0
- kerykeion/sweph/sefstars.txt +1602 -0
- kerykeion/transits_time_range_factory.py +302 -0
- kerykeion/utilities.py +289 -204
- kerykeion-5.1.8.dist-info/METADATA +1793 -0
- kerykeion-5.1.8.dist-info/RECORD +63 -0
- kerykeion/aspects/natal_aspects.py +0 -181
- kerykeion/aspects/synastry_aspects.py +0 -141
- kerykeion/aspects/transits_time_range.py +0 -41
- kerykeion/charts/draw_planets_v2.py +0 -649
- kerykeion/charts/draw_planets_v3.py +0 -679
- kerykeion/charts/kerykeion_chart_svg.py +0 -2038
- kerykeion/enums.py +0 -57
- kerykeion/ephemeris_data.py +0 -238
- kerykeion/house_comparison/house_comparison_models.py +0 -38
- kerykeion/kr_types/chart_types.py +0 -106
- kerykeion/settings/kr.config.json +0 -1304
- kerykeion/settings/legacy/__init__.py +0 -0
- kerykeion/settings/legacy/legacy_celestial_points_settings.py +0 -299
- kerykeion/settings/legacy/legacy_chart_aspects_settings.py +0 -71
- kerykeion/settings/legacy/legacy_color_settings.py +0 -42
- kerykeion/transits_time_range.py +0 -128
- kerykeion-5.0.0a9.dist-info/METADATA +0 -636
- kerykeion-5.0.0a9.dist-info/RECORD +0 -55
- kerykeion-5.0.0a9.dist-info/entry_points.txt +0 -2
- {kerykeion-5.0.0a9.dist-info → kerykeion-5.1.8.dist-info}/WHEEL +0 -0
- {kerykeion-5.0.0a9.dist-info → kerykeion-5.1.8.dist-info}/licenses/LICENSE +0 -0
kerykeion/charts/charts_utils.py
CHANGED
|
@@ -1,11 +1,229 @@
|
|
|
1
1
|
import math
|
|
2
2
|
import datetime
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from kerykeion.
|
|
7
|
-
from kerykeion.
|
|
8
|
-
from kerykeion.
|
|
3
|
+
from typing import Mapping, Optional, Sequence, Union, Literal
|
|
4
|
+
|
|
5
|
+
from kerykeion.schemas import KerykeionException, ChartType
|
|
6
|
+
from kerykeion.schemas.kr_literals import AstrologicalPoint
|
|
7
|
+
from kerykeion.schemas.kr_models import AspectModel, KerykeionPointModel, CompositeSubjectModel, PlanetReturnModel, AstrologicalSubjectModel, HouseComparisonModel
|
|
8
|
+
from kerykeion.schemas.settings_models import KerykeionLanguageCelestialPointModel, KerykeionSettingsCelestialPointModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ElementQualityDistributionMethod = Literal["pure_count", "weighted"]
|
|
12
|
+
"""Supported strategies for calculating element and modality distributions."""
|
|
13
|
+
|
|
14
|
+
_SIGN_TO_ELEMENT: tuple[str, ...] = (
|
|
15
|
+
"fire", # Aries
|
|
16
|
+
"earth", # Taurus
|
|
17
|
+
"air", # Gemini
|
|
18
|
+
"water", # Cancer
|
|
19
|
+
"fire", # Leo
|
|
20
|
+
"earth", # Virgo
|
|
21
|
+
"air", # Libra
|
|
22
|
+
"water", # Scorpio
|
|
23
|
+
"fire", # Sagittarius
|
|
24
|
+
"earth", # Capricorn
|
|
25
|
+
"air", # Aquarius
|
|
26
|
+
"water", # Pisces
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_SIGN_TO_QUALITY: tuple[str, ...] = (
|
|
30
|
+
"cardinal", # Aries
|
|
31
|
+
"fixed", # Taurus
|
|
32
|
+
"mutable", # Gemini
|
|
33
|
+
"cardinal", # Cancer
|
|
34
|
+
"fixed", # Leo
|
|
35
|
+
"mutable", # Virgo
|
|
36
|
+
"cardinal", # Libra
|
|
37
|
+
"fixed", # Scorpio
|
|
38
|
+
"mutable", # Sagittarius
|
|
39
|
+
"cardinal", # Capricorn
|
|
40
|
+
"fixed", # Aquarius
|
|
41
|
+
"mutable", # Pisces
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_ELEMENT_KEYS: tuple[str, ...] = ("fire", "earth", "air", "water")
|
|
45
|
+
_QUALITY_KEYS: tuple[str, ...] = ("cardinal", "fixed", "mutable")
|
|
46
|
+
|
|
47
|
+
_DEFAULT_WEIGHTED_FALLBACK = 1.0
|
|
48
|
+
DEFAULT_WEIGHTED_POINT_WEIGHTS: dict[str, float] = {
|
|
49
|
+
# Core luminaries & angles
|
|
50
|
+
"sun": 2.0,
|
|
51
|
+
"moon": 2.0,
|
|
52
|
+
"ascendant": 2.0,
|
|
53
|
+
"medium_coeli": 1.5,
|
|
54
|
+
"descendant": 1.5,
|
|
55
|
+
"imum_coeli": 1.5,
|
|
56
|
+
"vertex": 0.8,
|
|
57
|
+
"anti_vertex": 0.8,
|
|
58
|
+
# Personal planets
|
|
59
|
+
"mercury": 1.5,
|
|
60
|
+
"venus": 1.5,
|
|
61
|
+
"mars": 1.5,
|
|
62
|
+
# Social planets
|
|
63
|
+
"jupiter": 1.0,
|
|
64
|
+
"saturn": 1.0,
|
|
65
|
+
# Outer/transpersonal
|
|
66
|
+
"uranus": 0.5,
|
|
67
|
+
"neptune": 0.5,
|
|
68
|
+
"pluto": 0.5,
|
|
69
|
+
# Lunar nodes (mean/true variants)
|
|
70
|
+
"mean_north_lunar_node": 0.5,
|
|
71
|
+
"true_north_lunar_node": 0.5,
|
|
72
|
+
"mean_south_lunar_node": 0.5,
|
|
73
|
+
"true_south_lunar_node": 0.5,
|
|
74
|
+
# Chiron, Lilith variants
|
|
75
|
+
"chiron": 0.6,
|
|
76
|
+
"mean_lilith": 0.5,
|
|
77
|
+
"true_lilith": 0.5,
|
|
78
|
+
# Asteroids / centaurs
|
|
79
|
+
"ceres": 0.5,
|
|
80
|
+
"pallas": 0.4,
|
|
81
|
+
"juno": 0.4,
|
|
82
|
+
"vesta": 0.4,
|
|
83
|
+
"pholus": 0.3,
|
|
84
|
+
# Dwarf planets & TNOs
|
|
85
|
+
"eris": 0.3,
|
|
86
|
+
"sedna": 0.3,
|
|
87
|
+
"haumea": 0.3,
|
|
88
|
+
"makemake": 0.3,
|
|
89
|
+
"ixion": 0.3,
|
|
90
|
+
"orcus": 0.3,
|
|
91
|
+
"quaoar": 0.3,
|
|
92
|
+
# Arabic Parts
|
|
93
|
+
"pars_fortunae": 0.8,
|
|
94
|
+
"pars_spiritus": 0.7,
|
|
95
|
+
"pars_amoris": 0.6,
|
|
96
|
+
"pars_fidei": 0.6,
|
|
97
|
+
# Fixed stars
|
|
98
|
+
"regulus": 0.2,
|
|
99
|
+
"spica": 0.2,
|
|
100
|
+
# Other
|
|
101
|
+
"earth": 0.3,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _prepare_weight_lookup(
|
|
106
|
+
method: ElementQualityDistributionMethod,
|
|
107
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
108
|
+
) -> tuple[dict[str, float], float]:
|
|
109
|
+
"""
|
|
110
|
+
Normalize and merge default weights with any custom overrides.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
method: Calculation strategy to use.
|
|
114
|
+
custom_weights: Optional mapping of point name (case-insensitive) to weight.
|
|
115
|
+
Supports special key "__default__" as fallback weight.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A tuple containing the weight lookup dictionary and fallback weight.
|
|
119
|
+
"""
|
|
120
|
+
normalized_custom = (
|
|
121
|
+
{key.lower(): float(value) for key, value in custom_weights.items()}
|
|
122
|
+
if custom_weights
|
|
123
|
+
else {}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if method == "weighted":
|
|
127
|
+
weight_lookup: dict[str, float] = dict(DEFAULT_WEIGHTED_POINT_WEIGHTS)
|
|
128
|
+
fallback_weight = _DEFAULT_WEIGHTED_FALLBACK
|
|
129
|
+
else:
|
|
130
|
+
weight_lookup = {}
|
|
131
|
+
fallback_weight = 1.0
|
|
132
|
+
|
|
133
|
+
fallback_weight = normalized_custom.get("__default__", fallback_weight)
|
|
134
|
+
|
|
135
|
+
for key, value in normalized_custom.items():
|
|
136
|
+
if key == "__default__":
|
|
137
|
+
continue
|
|
138
|
+
weight_lookup[key] = float(value)
|
|
139
|
+
|
|
140
|
+
return weight_lookup, fallback_weight
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _calculate_distribution_for_subject(
|
|
144
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
145
|
+
celestial_points_names: Sequence[str],
|
|
146
|
+
sign_to_group_map: Sequence[str],
|
|
147
|
+
group_keys: Sequence[str],
|
|
148
|
+
weight_lookup: Mapping[str, float],
|
|
149
|
+
fallback_weight: float,
|
|
150
|
+
) -> dict[str, float]:
|
|
151
|
+
"""
|
|
152
|
+
Accumulate distribution totals for a single subject.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
subject: Subject providing planetary positions.
|
|
156
|
+
celestial_points_names: Names of celestial points to consider (lowercase).
|
|
157
|
+
sign_to_group_map: Mapping from sign index to element/modality key.
|
|
158
|
+
group_keys: Iterable of expected keys for the resulting totals.
|
|
159
|
+
weight_lookup: Precomputed mapping of weights per point.
|
|
160
|
+
fallback_weight: Default weight if point missing in lookup.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary with accumulated totals keyed by element/modality.
|
|
164
|
+
"""
|
|
165
|
+
totals = {key: 0.0 for key in group_keys}
|
|
166
|
+
|
|
167
|
+
for point_name in celestial_points_names:
|
|
168
|
+
point = subject.get(point_name)
|
|
169
|
+
if point is None:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
sign_index = getattr(point, "sign_num", None)
|
|
173
|
+
if sign_index is None or not (0 <= sign_index < len(sign_to_group_map)):
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
group_key = sign_to_group_map[sign_index]
|
|
177
|
+
weight = weight_lookup.get(point_name, fallback_weight)
|
|
178
|
+
totals[group_key] += weight
|
|
179
|
+
|
|
180
|
+
return totals
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
_SECOND_COLUMN_THRESHOLD = 20
|
|
184
|
+
_THIRD_COLUMN_THRESHOLD = 28
|
|
185
|
+
_FOURTH_COLUMN_THRESHOLD = 36
|
|
186
|
+
|
|
187
|
+
_DOUBLE_CHART_TYPES: tuple[ChartType, ...] = ("Synastry", "Transit", "DualReturnChart")
|
|
188
|
+
_GRID_COLUMN_WIDTH = 125
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _select_planet_grid_thresholds(chart_type: ChartType) -> tuple[int, int, int]:
|
|
192
|
+
"""Return column thresholds for the planet grids based on chart type."""
|
|
193
|
+
if chart_type in _DOUBLE_CHART_TYPES:
|
|
194
|
+
return (
|
|
195
|
+
1_000_000, # effectively disable first column
|
|
196
|
+
1_000_008, # effectively disable second column
|
|
197
|
+
1_000_016, # effectively disable third column
|
|
198
|
+
)
|
|
199
|
+
return _SECOND_COLUMN_THRESHOLD, _THIRD_COLUMN_THRESHOLD, _FOURTH_COLUMN_THRESHOLD
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _planet_grid_layout_position(
|
|
203
|
+
index: int, thresholds: Optional[tuple[int, int, int]] = None
|
|
204
|
+
) -> tuple[int, int]:
|
|
205
|
+
"""Return horizontal offset and row index for planet grids."""
|
|
206
|
+
second_threshold, third_threshold, fourth_threshold = (
|
|
207
|
+
thresholds
|
|
208
|
+
if thresholds is not None
|
|
209
|
+
else (_SECOND_COLUMN_THRESHOLD, _THIRD_COLUMN_THRESHOLD, _FOURTH_COLUMN_THRESHOLD)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if index < second_threshold:
|
|
213
|
+
column = 0
|
|
214
|
+
row = index
|
|
215
|
+
elif index < third_threshold:
|
|
216
|
+
column = 1
|
|
217
|
+
row = index - second_threshold
|
|
218
|
+
elif index < fourth_threshold:
|
|
219
|
+
column = 2
|
|
220
|
+
row = index - third_threshold
|
|
221
|
+
else:
|
|
222
|
+
column = 3
|
|
223
|
+
row = index - fourth_threshold
|
|
224
|
+
|
|
225
|
+
offset = -(_GRID_COLUMN_WIDTH * column)
|
|
226
|
+
return offset, row
|
|
9
227
|
|
|
10
228
|
|
|
11
229
|
|
|
@@ -189,7 +407,7 @@ def draw_zodiac_slice(
|
|
|
189
407
|
# pie slices
|
|
190
408
|
offset = 360 - seventh_house_degree_ut
|
|
191
409
|
# check transit
|
|
192
|
-
if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "
|
|
410
|
+
if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "DualReturnChart":
|
|
193
411
|
dropin: Union[int, float] = 0
|
|
194
412
|
else:
|
|
195
413
|
dropin = c1
|
|
@@ -198,7 +416,7 @@ def draw_zodiac_slice(
|
|
|
198
416
|
# symbols
|
|
199
417
|
offset = offset + 15
|
|
200
418
|
# check transit
|
|
201
|
-
if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "
|
|
419
|
+
if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "DualReturnChart":
|
|
202
420
|
dropin = 54
|
|
203
421
|
else:
|
|
204
422
|
dropin = 18 + c1
|
|
@@ -287,7 +505,7 @@ def draw_aspect_line(
|
|
|
287
505
|
y2 = sliceToY(0, ar, second_offset) + (r - ar)
|
|
288
506
|
|
|
289
507
|
return (
|
|
290
|
-
f'<g kr:node="Aspect" kr:aspectname="{aspect["aspect"]}" kr:to="{aspect["p1_name"]}" kr:tooriginaldegrees="{aspect["p1_abs_pos"]}" kr:from="{aspect["p2_name"]}" kr:fromoriginaldegrees="{aspect["p2_abs_pos"]}">'
|
|
508
|
+
f'<g kr:node="Aspect" kr:aspectname="{aspect["aspect"]}" kr:to="{aspect["p1_name"]}" kr:tooriginaldegrees="{aspect["p1_abs_pos"]}" kr:from="{aspect["p2_name"]}" kr:fromoriginaldegrees="{aspect["p2_abs_pos"]}" kr:orb="{aspect["orbit"]}" kr:aspectdegrees="{aspect["aspect_degrees"]}" kr:planetsdiff="{aspect["diff"]}" kr:aspectmovement="{aspect["aspect_movement"]}">'
|
|
291
509
|
f'<line class="aspect" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {color}; stroke-width: 1; stroke-opacity: .9;"/>'
|
|
292
510
|
f"</g>"
|
|
293
511
|
)
|
|
@@ -419,7 +637,7 @@ def draw_first_circle(
|
|
|
419
637
|
Returns:
|
|
420
638
|
str: The SVG path of the first circle.
|
|
421
639
|
"""
|
|
422
|
-
if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "
|
|
640
|
+
if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "DualReturnChart":
|
|
423
641
|
return f'<circle cx="{r}" cy="{r}" r="{r - 36}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.4;" />'
|
|
424
642
|
else:
|
|
425
643
|
if c1 is None:
|
|
@@ -462,7 +680,7 @@ def draw_second_circle(
|
|
|
462
680
|
str: The SVG path of the second circle.
|
|
463
681
|
"""
|
|
464
682
|
|
|
465
|
-
if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "
|
|
683
|
+
if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "DualReturnChart":
|
|
466
684
|
return f'<circle cx="{r}" cy="{r}" r="{r - 72}" style="fill: {fill_color}; fill-opacity:.4; stroke: {stroke_color}; stroke-opacity:.4; stroke-width: 1px" />'
|
|
467
685
|
|
|
468
686
|
else:
|
|
@@ -492,7 +710,7 @@ def draw_third_circle(
|
|
|
492
710
|
Returns:
|
|
493
711
|
- str: The SVG element as a string.
|
|
494
712
|
"""
|
|
495
|
-
if chart_type in {"Synastry", "Transit", "
|
|
713
|
+
if chart_type in {"Synastry", "Transit", "DualReturnChart"}:
|
|
496
714
|
# For Synastry and Transit charts, use a fixed radius adjustment of 160
|
|
497
715
|
return f'<circle cx="{radius}" cy="{radius}" r="{radius - 160}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
|
|
498
716
|
|
|
@@ -572,6 +790,7 @@ def draw_houses_cusps_and_text_number(
|
|
|
572
790
|
chart_type: ChartType,
|
|
573
791
|
second_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
|
|
574
792
|
transit_house_cusp_color: Union[str, None] = None,
|
|
793
|
+
external_view: bool = False,
|
|
575
794
|
) -> str:
|
|
576
795
|
"""
|
|
577
796
|
Draws the houses cusps and text numbers for a given chart type.
|
|
@@ -589,9 +808,10 @@ def draw_houses_cusps_and_text_number(
|
|
|
589
808
|
- chart_type: Type of the chart (e.g., Transit, Synastry).
|
|
590
809
|
- second_subject_houses_list: List of house for the second subject (optional).
|
|
591
810
|
- transit_house_cusp_color: Color for transit house cusps (optional).
|
|
811
|
+
- external_view: Whether to use external view mode for positioning (optional).
|
|
592
812
|
|
|
593
813
|
Returns:
|
|
594
|
-
- A string containing
|
|
814
|
+
- A string containing SVG elements for house cusps and numbers.
|
|
595
815
|
"""
|
|
596
816
|
|
|
597
817
|
path = ""
|
|
@@ -599,7 +819,7 @@ def draw_houses_cusps_and_text_number(
|
|
|
599
819
|
|
|
600
820
|
for i in range(xr):
|
|
601
821
|
# Determine offsets based on chart type
|
|
602
|
-
dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry", "
|
|
822
|
+
dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry", "DualReturnChart"] else (c3, c1, False)
|
|
603
823
|
|
|
604
824
|
# Calculate the offset for the current house cusp
|
|
605
825
|
offset = (int(first_subject_houses_list[int(xr / 2)].abs_pos) / -1) + int(first_subject_houses_list[i].abs_pos)
|
|
@@ -621,7 +841,7 @@ def draw_houses_cusps_and_text_number(
|
|
|
621
841
|
i, standard_house_cusp_color
|
|
622
842
|
)
|
|
623
843
|
|
|
624
|
-
if chart_type in ["Transit", "Synastry", "
|
|
844
|
+
if chart_type in ["Transit", "Synastry", "DualReturnChart"]:
|
|
625
845
|
if second_subject_houses_list is None or transit_house_cusp_color is None:
|
|
626
846
|
raise KerykeionException("second_subject_houses_list_ut or transit_house_cusp_color is None")
|
|
627
847
|
|
|
@@ -645,30 +865,34 @@ def draw_houses_cusps_and_text_number(
|
|
|
645
865
|
|
|
646
866
|
# Add the house number text for the second subject
|
|
647
867
|
fill_opacity = "0" if chart_type == "Transit" else ".4"
|
|
648
|
-
path +=
|
|
868
|
+
path += '<g kr:node="HouseNumber">'
|
|
649
869
|
path += f'<text style="fill: var(--kerykeion-chart-color-house-number); fill-opacity: {fill_opacity}; font-size: 14px"><tspan x="{xtext - 3}" y="{ytext + 3}">{i + 1}</tspan></text>'
|
|
650
|
-
path +=
|
|
870
|
+
path += "</g>"
|
|
651
871
|
|
|
652
872
|
# Add the house cusp line for the second subject
|
|
653
873
|
stroke_opacity = "0" if chart_type == "Transit" else ".3"
|
|
654
|
-
path += f'<g kr:node="Cusp">'
|
|
874
|
+
path += f'<g kr:node="Cusp" kr:absoluteposition="{second_subject_houses_list[i].abs_pos}" kr:signposition="{second_subject_houses_list[i].position}" kr:sing="{second_subject_houses_list[i].sign}" kr:slug="{second_subject_houses_list[i].name}">'
|
|
655
875
|
path += f"<line x1='{t_x1}' y1='{t_y1}' x2='{t_x2}' y2='{t_y2}' style='stroke: {t_linecolor}; stroke-width: 1px; stroke-opacity:{stroke_opacity};'/>"
|
|
656
|
-
path +=
|
|
876
|
+
path += "</g>"
|
|
657
877
|
|
|
658
|
-
# Adjust dropin based on chart type
|
|
659
|
-
|
|
878
|
+
# Adjust dropin based on chart type and external view
|
|
879
|
+
dropin_map = {"Transit": 84, "Synastry": 84, "DualReturnChart": 84}
|
|
880
|
+
if external_view:
|
|
881
|
+
dropin = 100
|
|
882
|
+
else:
|
|
883
|
+
dropin = dropin_map.get(chart_type, 48)
|
|
660
884
|
xtext = sliceToX(0, (r - dropin), text_offset) + dropin
|
|
661
885
|
ytext = sliceToY(0, (r - dropin), text_offset) + dropin
|
|
662
886
|
|
|
663
887
|
# Add the house cusp line for the first subject
|
|
664
|
-
path += f'<g kr:node="Cusp">'
|
|
888
|
+
path += f'<g kr:node="Cusp" kr:absoluteposition="{first_subject_houses_list[i].abs_pos}" kr:signposition="{first_subject_houses_list[i].position}" kr:sing="{first_subject_houses_list[i].sign}" kr:slug="{first_subject_houses_list[i].name}">'
|
|
665
889
|
path += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {linecolor}; stroke-width: 1px; stroke-dasharray:3,2; stroke-opacity:.4;"/>'
|
|
666
|
-
path +=
|
|
890
|
+
path += "</g>"
|
|
667
891
|
|
|
668
892
|
# Add the house number text for the first subject
|
|
669
|
-
path +=
|
|
893
|
+
path += '<g kr:node="HouseNumber">'
|
|
670
894
|
path += f'<text style="fill: var(--kerykeion-chart-color-house-number); fill-opacity: .6; font-size: 14px"><tspan x="{xtext - 3}" y="{ytext + 3}">{i + 1}</tspan></text>'
|
|
671
|
-
path +=
|
|
895
|
+
path += "</g>"
|
|
672
896
|
|
|
673
897
|
return path
|
|
674
898
|
|
|
@@ -682,7 +906,8 @@ def draw_transit_aspect_list(
|
|
|
682
906
|
aspects_per_column: int = 14,
|
|
683
907
|
column_width: int = 100,
|
|
684
908
|
line_height: int = 14,
|
|
685
|
-
max_columns: int = 6
|
|
909
|
+
max_columns: int = 6,
|
|
910
|
+
chart_height: Optional[int] = None,
|
|
686
911
|
) -> str:
|
|
687
912
|
"""
|
|
688
913
|
Generates the SVG output for the aspect transit grid.
|
|
@@ -696,6 +921,8 @@ def draw_transit_aspect_list(
|
|
|
696
921
|
- column_width: Width in pixels for each column (default: 100).
|
|
697
922
|
- line_height: Height in pixels for each line (default: 14).
|
|
698
923
|
- max_columns: Maximum number of columns before vertical adjustment (default: 6).
|
|
924
|
+
- chart_height: Total chart height. When provided, columns from the 12th onward
|
|
925
|
+
leverage the taller layout capacity (default: None).
|
|
699
926
|
|
|
700
927
|
Returns:
|
|
701
928
|
- A string containing the SVG path data for the aspect transit grid.
|
|
@@ -708,115 +935,122 @@ def draw_transit_aspect_list(
|
|
|
708
935
|
if aspects_list and isinstance(aspects_list[0], dict):
|
|
709
936
|
aspects_list = [AspectModel(**aspect) for aspect in aspects_list] # type: ignore
|
|
710
937
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if current_column >= max_columns:
|
|
726
|
-
# Calculate how many aspects will overflow beyond the max columns
|
|
727
|
-
overflow_aspects = len(aspects_list) - (aspects_per_column * max_columns)
|
|
728
|
-
if overflow_aspects > 0:
|
|
729
|
-
# Adjust the starting vertical position to move text up
|
|
730
|
-
vertical_position = vertical_position - (overflow_aspects * line_height)
|
|
731
|
-
|
|
732
|
-
inner_path += f'<g transform="translate({horizontal_position},{vertical_position})">'
|
|
733
|
-
|
|
734
|
-
# First planet symbol
|
|
735
|
-
inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspect["p1"]]["name"]}" />'
|
|
938
|
+
# Type narrowing: at this point aspects_list contains AspectModel instances
|
|
939
|
+
typed_aspects_list: list[AspectModel] = aspects_list # type: ignore
|
|
940
|
+
|
|
941
|
+
translate_x = 565
|
|
942
|
+
translate_y = 273
|
|
943
|
+
title_clearance = 18
|
|
944
|
+
top_limit_y: float = -translate_y + title_clearance
|
|
945
|
+
bottom_padding = 40
|
|
946
|
+
baseline_index = aspects_per_column - 1
|
|
947
|
+
top_limit_index = math.ceil(top_limit_y / line_height)
|
|
948
|
+
# `top_limit_index` identifies the highest row index we can reach without
|
|
949
|
+
# touching the title block. Combined with the baseline index we know how many
|
|
950
|
+
# rows a "tall" column may contain.
|
|
951
|
+
max_capacity_by_top = baseline_index - top_limit_index + 1
|
|
736
952
|
|
|
737
|
-
|
|
738
|
-
aspect_name = aspect["aspect"]
|
|
739
|
-
id_value = next((a["degree"] for a in aspects_settings if a["name"] == aspect_name), None) # type: ignore
|
|
740
|
-
inner_path += f'<use x="15" y="0" xlink:href="#orb{id_value}" />'
|
|
741
|
-
|
|
742
|
-
# Second planet symbol
|
|
743
|
-
inner_path += f'<g transform="translate(30,0)">'
|
|
744
|
-
inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspect["p2"]]["name"]}" />'
|
|
745
|
-
inner_path += f"</g>"
|
|
746
|
-
|
|
747
|
-
# Difference in degrees
|
|
748
|
-
inner_path += f'<text y="8" x="45" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 10px;">{convert_decimal_to_degree_string(aspect["orbit"])}</text>'
|
|
749
|
-
|
|
750
|
-
inner_path += f"</g>"
|
|
953
|
+
inner_path = ""
|
|
751
954
|
|
|
752
|
-
|
|
955
|
+
full_height_column_index = 10 # 0-based index → 11th column onward
|
|
956
|
+
if chart_height is not None:
|
|
957
|
+
available_height = max(chart_height - translate_y - bottom_padding, line_height)
|
|
958
|
+
allowed_capacity = max(aspects_per_column, int(available_height // line_height))
|
|
959
|
+
full_height_capacity = max(aspects_per_column, min(allowed_capacity, max_capacity_by_top))
|
|
960
|
+
else:
|
|
961
|
+
full_height_capacity = aspects_per_column
|
|
962
|
+
|
|
963
|
+
# Bucket aspects into columns while respecting the capacity of each column.
|
|
964
|
+
columns: list[list[AspectModel]] = []
|
|
965
|
+
column_capacities: list[int] = []
|
|
966
|
+
|
|
967
|
+
for aspect in typed_aspects_list:
|
|
968
|
+
if not columns or len(columns[-1]) >= column_capacities[-1]:
|
|
969
|
+
new_col_index = len(columns)
|
|
970
|
+
capacity = aspects_per_column if new_col_index < full_height_column_index else full_height_capacity
|
|
971
|
+
capacity = max(capacity, 1)
|
|
972
|
+
columns.append([])
|
|
973
|
+
column_capacities.append(capacity)
|
|
974
|
+
columns[-1].append(aspect)
|
|
975
|
+
|
|
976
|
+
for col_idx, column in enumerate(columns):
|
|
977
|
+
capacity = column_capacities[col_idx]
|
|
978
|
+
horizontal_position = col_idx * column_width
|
|
979
|
+
column_len = len(column)
|
|
980
|
+
|
|
981
|
+
for row_idx, aspect in enumerate(column):
|
|
982
|
+
# Default top-aligned placement
|
|
983
|
+
vertical_position = row_idx * line_height
|
|
984
|
+
|
|
985
|
+
# Full-height columns reuse the shared baseline so every column
|
|
986
|
+
# finishes at the same vertical position and grows upwards.
|
|
987
|
+
if col_idx >= full_height_column_index:
|
|
988
|
+
vertical_index = baseline_index - (column_len - 1 - row_idx)
|
|
989
|
+
vertical_position = vertical_index * line_height
|
|
990
|
+
# Legacy overflow columns (before the 12th) keep the older behaviour:
|
|
991
|
+
# once we exceed the configured column count, bottom-align the content
|
|
992
|
+
# so the shorter columns do not look awkwardly padded at the top.
|
|
993
|
+
elif col_idx >= max_columns and capacity == aspects_per_column:
|
|
994
|
+
top_offset_lines = max(0, capacity - len(column))
|
|
995
|
+
vertical_position = (top_offset_lines + row_idx) * line_height
|
|
996
|
+
|
|
997
|
+
inner_path += f'<g transform="translate({horizontal_position},{vertical_position})">'
|
|
998
|
+
|
|
999
|
+
# First planet symbol
|
|
1000
|
+
inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspect["p1"]]["name"]}" />'
|
|
1001
|
+
|
|
1002
|
+
# Aspect symbol
|
|
1003
|
+
aspect_name = aspect["aspect"]
|
|
1004
|
+
id_value = next((a["degree"] for a in aspects_settings if a["name"] == aspect_name), None) # type: ignore
|
|
1005
|
+
inner_path += f'<use x="15" y="0" xlink:href="#orb{id_value}" />'
|
|
1006
|
+
|
|
1007
|
+
# Second planet symbol
|
|
1008
|
+
inner_path += '<g transform="translate(30,0)">'
|
|
1009
|
+
inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspect["p2"]]["name"]}" />'
|
|
1010
|
+
inner_path += "</g>"
|
|
1011
|
+
|
|
1012
|
+
# Difference in degrees
|
|
1013
|
+
inner_path += f'<text y="8" x="45" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 10px;">{convert_decimal_to_degree_string(aspect["orbit"])}</text>'
|
|
1014
|
+
|
|
1015
|
+
inner_path += "</g>"
|
|
1016
|
+
|
|
1017
|
+
out = f'<g transform="translate({translate_x},{translate_y})">'
|
|
753
1018
|
out += f'<text y="-15" x="0" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 14px;">{grid_title}:</text>'
|
|
754
1019
|
out += inner_path
|
|
755
|
-
out +=
|
|
1020
|
+
out += "</g>"
|
|
756
1021
|
|
|
757
1022
|
return out
|
|
758
1023
|
|
|
759
1024
|
|
|
760
1025
|
def calculate_moon_phase_chart_params(
|
|
761
|
-
degrees_between_sun_and_moon: float
|
|
762
|
-
latitude: float
|
|
1026
|
+
degrees_between_sun_and_moon: float
|
|
763
1027
|
) -> dict:
|
|
764
1028
|
"""
|
|
765
|
-
Calculate
|
|
1029
|
+
Calculate normalized parameters used by the moon phase icon.
|
|
766
1030
|
|
|
767
1031
|
Parameters:
|
|
768
|
-
- degrees_between_sun_and_moon (float): The
|
|
769
|
-
- latitude (float): The latitude for rotation calculation.
|
|
1032
|
+
- degrees_between_sun_and_moon (float): The elongation between the sun and moon.
|
|
770
1033
|
|
|
771
1034
|
Returns:
|
|
772
|
-
- dict:
|
|
773
|
-
"""
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
circle_radius = None
|
|
779
|
-
|
|
780
|
-
# Determine lunar phase properties based on the degree
|
|
781
|
-
if deg < 90.0:
|
|
782
|
-
max_radius = deg
|
|
783
|
-
if deg > 80.0:
|
|
784
|
-
max_radius = max_radius * max_radius
|
|
785
|
-
circle_center_x = 20.0 + (deg / 90.0) * (max_radius + 10.0)
|
|
786
|
-
circle_radius = 10.0 + (deg / 90.0) * max_radius
|
|
787
|
-
|
|
788
|
-
elif deg < 180.0:
|
|
789
|
-
max_radius = 180.0 - deg
|
|
790
|
-
if deg < 100.0:
|
|
791
|
-
max_radius = max_radius * max_radius
|
|
792
|
-
circle_center_x = 20.0 + ((deg - 90.0) / 90.0 * (max_radius + 10.0)) - (max_radius + 10.0)
|
|
793
|
-
circle_radius = 10.0 + max_radius - ((deg - 90.0) / 90.0 * max_radius)
|
|
794
|
-
|
|
795
|
-
elif deg < 270.0:
|
|
796
|
-
max_radius = deg - 180.0
|
|
797
|
-
if deg > 260.0:
|
|
798
|
-
max_radius = max_radius * max_radius
|
|
799
|
-
circle_center_x = 20.0 + ((deg - 180.0) / 90.0 * (max_radius + 10.0))
|
|
800
|
-
circle_radius = 10.0 + ((deg - 180.0) / 90.0 * max_radius)
|
|
801
|
-
|
|
802
|
-
elif deg < 361.0:
|
|
803
|
-
max_radius = 360.0 - deg
|
|
804
|
-
if deg < 280.0:
|
|
805
|
-
max_radius = max_radius * max_radius
|
|
806
|
-
circle_center_x = 20.0 + ((deg - 270.0) / 90.0 * (max_radius + 10.0)) - (max_radius + 10.0)
|
|
807
|
-
circle_radius = 10.0 + max_radius - ((deg - 270.0) / 90.0 * max_radius)
|
|
808
|
-
|
|
809
|
-
else:
|
|
810
|
-
raise KerykeionException(f"Invalid degree value: {deg}")
|
|
1035
|
+
- dict: Normalized phase data (angle, illuminated fraction, shadow ellipse radius).
|
|
1036
|
+
"""
|
|
1037
|
+
if not math.isfinite(degrees_between_sun_and_moon):
|
|
1038
|
+
raise KerykeionException(
|
|
1039
|
+
f"Invalid degree value: {degrees_between_sun_and_moon}"
|
|
1040
|
+
)
|
|
811
1041
|
|
|
1042
|
+
phase_angle = degrees_between_sun_and_moon % 360.0
|
|
1043
|
+
radians = math.radians(phase_angle)
|
|
1044
|
+
cosine = math.cos(radians)
|
|
1045
|
+
illuminated_fraction = (1.0 - cosine) / 2.0
|
|
812
1046
|
|
|
813
|
-
#
|
|
814
|
-
|
|
1047
|
+
# Guard against floating point spillover outside [0, 1].
|
|
1048
|
+
illuminated_fraction = max(0.0, min(1.0, illuminated_fraction))
|
|
815
1049
|
|
|
816
1050
|
return {
|
|
817
|
-
"
|
|
818
|
-
"
|
|
819
|
-
"
|
|
1051
|
+
"phase_angle": phase_angle,
|
|
1052
|
+
"illuminated_fraction": illuminated_fraction,
|
|
1053
|
+
"shadow_ellipse_rx": 10.0 * cosine,
|
|
820
1054
|
}
|
|
821
1055
|
|
|
822
1056
|
|
|
@@ -824,7 +1058,7 @@ def draw_main_house_grid(
|
|
|
824
1058
|
main_subject_houses_list: list[KerykeionPointModel],
|
|
825
1059
|
house_cusp_generale_name_label: str = "Cusp",
|
|
826
1060
|
text_color: str = "#000000",
|
|
827
|
-
x_position: int =
|
|
1061
|
+
x_position: int = 750,
|
|
828
1062
|
y_position: int = 30,
|
|
829
1063
|
) -> str:
|
|
830
1064
|
"""
|
|
@@ -862,7 +1096,7 @@ def draw_secondary_house_grid(
|
|
|
862
1096
|
secondary_subject_houses_list: list[KerykeionPointModel],
|
|
863
1097
|
house_cusp_generale_name_label: str = "Cusp",
|
|
864
1098
|
text_color: str = "#000000",
|
|
865
|
-
x_position: int =
|
|
1099
|
+
x_position: int = 1015,
|
|
866
1100
|
y_position: int = 30,
|
|
867
1101
|
) -> str:
|
|
868
1102
|
"""
|
|
@@ -903,57 +1137,60 @@ def draw_main_planet_grid(
|
|
|
903
1137
|
chart_type: ChartType,
|
|
904
1138
|
celestial_point_language: KerykeionLanguageCelestialPointModel,
|
|
905
1139
|
text_color: str = "#000000",
|
|
1140
|
+
x_position: int = 645,
|
|
1141
|
+
y_position: int = 0,
|
|
906
1142
|
) -> str:
|
|
907
1143
|
"""
|
|
908
|
-
|
|
1144
|
+
Draw the planet grid (main subject) and optional title.
|
|
1145
|
+
|
|
1146
|
+
The entire output is wrapped in a single SVG group `<g>` so the
|
|
1147
|
+
whole block can be repositioned by changing the group transform.
|
|
909
1148
|
|
|
910
1149
|
Args:
|
|
911
|
-
planets_and_houses_grid_title
|
|
912
|
-
subject_name
|
|
913
|
-
available_kerykeion_celestial_points
|
|
914
|
-
chart_type
|
|
915
|
-
celestial_point_language
|
|
916
|
-
text_color
|
|
1150
|
+
planets_and_houses_grid_title: Title prefix to show for eligible chart types.
|
|
1151
|
+
subject_name: Subject name to append to the title.
|
|
1152
|
+
available_kerykeion_celestial_points: Celestial points to render in the grid.
|
|
1153
|
+
chart_type: Chart type identifier (Literal string).
|
|
1154
|
+
celestial_point_language: Language model for celestial point decoding.
|
|
1155
|
+
text_color: Text color for labels (default: "#000000").
|
|
1156
|
+
x_position: X translation applied to the outer `<g>` (default: 620).
|
|
1157
|
+
y_position: Y translation applied to the outer `<g>` (default: 0).
|
|
917
1158
|
|
|
918
1159
|
Returns:
|
|
919
|
-
|
|
1160
|
+
SVG string for the main planet grid wrapped in a `<g>`.
|
|
920
1161
|
"""
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1162
|
+
# Layout constants (kept identical to previous behavior)
|
|
1163
|
+
BASE_Y = 30
|
|
1164
|
+
HEADER_Y = 15 # Title baseline inside the wrapper
|
|
1165
|
+
LINE_START = 10
|
|
1166
|
+
LINE_STEP = 14
|
|
925
1167
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
)
|
|
932
|
-
elif chart_type == "Transit":
|
|
933
|
-
svg_output += (
|
|
934
|
-
f'<g transform="translate(620, 15)">' # Added the 620,30 offset (adjusted for -15)
|
|
935
|
-
f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
|
|
936
|
-
f'</g>'
|
|
937
|
-
)
|
|
938
|
-
elif chart_type == "Return":
|
|
1168
|
+
# Wrap everything inside a single group so position can be changed once
|
|
1169
|
+
svg_output = f'<g transform="translate({x_position},{y_position})">'
|
|
1170
|
+
|
|
1171
|
+
# Add title only for specific chart types
|
|
1172
|
+
if chart_type in ("Synastry", "Transit", "DualReturnChart"):
|
|
939
1173
|
svg_output += (
|
|
940
|
-
f'<g transform="translate(
|
|
1174
|
+
f'<g transform="translate(0, {HEADER_Y})">'
|
|
941
1175
|
f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
|
|
942
1176
|
f'</g>'
|
|
943
1177
|
)
|
|
944
|
-
else:
|
|
945
|
-
svg_output += ""
|
|
946
1178
|
|
|
947
1179
|
end_of_line = "</g>"
|
|
948
1180
|
|
|
1181
|
+
column_thresholds = _select_planet_grid_thresholds(chart_type)
|
|
1182
|
+
|
|
949
1183
|
for i, planet in enumerate(available_kerykeion_celestial_points):
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1184
|
+
offset, row_index = _planet_grid_layout_position(i, column_thresholds)
|
|
1185
|
+
line_height = LINE_START + (row_index * LINE_STEP)
|
|
1186
|
+
|
|
1187
|
+
decoded_name = get_decoded_kerykeion_celestial_point_name(
|
|
1188
|
+
planet["name"],
|
|
1189
|
+
celestial_point_language,
|
|
1190
|
+
)
|
|
953
1191
|
|
|
954
|
-
decoded_name = get_decoded_kerykeion_celestial_point_name(planet["name"], celestial_point_language)
|
|
955
1192
|
svg_output += (
|
|
956
|
-
f'<g transform="translate({
|
|
1193
|
+
f'<g transform="translate({offset},{BASE_Y + line_height})">'
|
|
957
1194
|
f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{decoded_name}</text>'
|
|
958
1195
|
f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{planet["name"]}" /></g>'
|
|
959
1196
|
f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(planet["position"])}</text>'
|
|
@@ -964,7 +1201,9 @@ def draw_main_planet_grid(
|
|
|
964
1201
|
svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
|
|
965
1202
|
|
|
966
1203
|
svg_output += end_of_line
|
|
967
|
-
|
|
1204
|
+
|
|
1205
|
+
# Close the wrapper group
|
|
1206
|
+
svg_output += "</g>"
|
|
968
1207
|
|
|
969
1208
|
return svg_output
|
|
970
1209
|
|
|
@@ -976,49 +1215,66 @@ def draw_secondary_planet_grid(
|
|
|
976
1215
|
chart_type: ChartType,
|
|
977
1216
|
celestial_point_language: KerykeionLanguageCelestialPointModel,
|
|
978
1217
|
text_color: str = "#000000",
|
|
1218
|
+
x_position: int = 910,
|
|
1219
|
+
y_position: int = 0,
|
|
979
1220
|
) -> str:
|
|
980
1221
|
"""
|
|
981
|
-
|
|
1222
|
+
Draw the planet grid for the secondary subject and its title.
|
|
1223
|
+
|
|
1224
|
+
The entire output is wrapped in a single SVG group `<g>` so the
|
|
1225
|
+
whole block can be repositioned by changing the group transform.
|
|
982
1226
|
|
|
983
1227
|
Args:
|
|
984
|
-
planets_and_houses_grid_title
|
|
985
|
-
second_subject_name
|
|
986
|
-
second_subject_available_kerykeion_celestial_points
|
|
987
|
-
chart_type
|
|
988
|
-
celestial_point_language
|
|
989
|
-
text_color
|
|
1228
|
+
planets_and_houses_grid_title: Title prefix (used except for Transit charts).
|
|
1229
|
+
second_subject_name: Name of the secondary subject.
|
|
1230
|
+
second_subject_available_kerykeion_celestial_points: Celestial points to render for the secondary subject.
|
|
1231
|
+
chart_type: Chart type identifier (Literal string).
|
|
1232
|
+
celestial_point_language: Language model for celestial point decoding.
|
|
1233
|
+
text_color: Text color for labels (default: "#000000").
|
|
1234
|
+
x_position: X translation applied to the outer `<g>` (default: 870).
|
|
1235
|
+
y_position: Y translation applied to the outer `<g>` (default: 0).
|
|
990
1236
|
|
|
991
1237
|
Returns:
|
|
992
|
-
|
|
1238
|
+
SVG string for the secondary planet grid wrapped in a `<g>`.
|
|
993
1239
|
"""
|
|
994
|
-
|
|
995
|
-
|
|
1240
|
+
# Layout constants
|
|
1241
|
+
BASE_Y = 30
|
|
1242
|
+
HEADER_Y = 15
|
|
1243
|
+
LINE_START = 10
|
|
1244
|
+
LINE_STEP = 14
|
|
996
1245
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1246
|
+
# Open wrapper group
|
|
1247
|
+
svg_output = f'<g transform="translate({x_position},{y_position})">'
|
|
1248
|
+
|
|
1249
|
+
# Title content and its relative x offset
|
|
1250
|
+
header_text = (
|
|
1251
|
+
second_subject_name if chart_type == "Transit"
|
|
1252
|
+
else f"{planets_and_houses_grid_title} {second_subject_name}"
|
|
1253
|
+
)
|
|
1254
|
+
header_x_offset = -50 if chart_type == "Transit" else 0
|
|
1255
|
+
|
|
1256
|
+
svg_output += (
|
|
1257
|
+
f'<g transform="translate({header_x_offset}, {HEADER_Y})">'
|
|
1258
|
+
f'<text style="fill:{text_color}; font-size: 14px;">{header_text}</text>'
|
|
1259
|
+
f'</g>'
|
|
1260
|
+
)
|
|
1012
1261
|
|
|
1013
|
-
|
|
1262
|
+
# Grid rows
|
|
1263
|
+
line_height = LINE_START
|
|
1264
|
+
end_of_line = "</g>"
|
|
1014
1265
|
|
|
1015
|
-
|
|
1016
|
-
offset = 250
|
|
1266
|
+
column_thresholds = _select_planet_grid_thresholds(chart_type)
|
|
1017
1267
|
|
|
1018
1268
|
for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
|
|
1019
|
-
|
|
1269
|
+
offset, row_index = _planet_grid_layout_position(i, column_thresholds)
|
|
1270
|
+
line_height = LINE_START + (row_index * LINE_STEP)
|
|
1271
|
+
|
|
1272
|
+
second_decoded_name = get_decoded_kerykeion_celestial_point_name(
|
|
1273
|
+
t_planet["name"],
|
|
1274
|
+
celestial_point_language,
|
|
1275
|
+
)
|
|
1020
1276
|
svg_output += (
|
|
1021
|
-
f'<g transform="translate({
|
|
1277
|
+
f'<g transform="translate({offset},{BASE_Y + line_height})">'
|
|
1022
1278
|
f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{second_decoded_name}</text>'
|
|
1023
1279
|
f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
|
|
1024
1280
|
f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(t_planet["position"])}</text>'
|
|
@@ -1029,7 +1285,10 @@ def draw_secondary_planet_grid(
|
|
|
1029
1285
|
svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
|
|
1030
1286
|
|
|
1031
1287
|
svg_output += end_of_line
|
|
1032
|
-
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
# Close wrapper group
|
|
1291
|
+
svg_output += "</g>"
|
|
1033
1292
|
|
|
1034
1293
|
return svg_output
|
|
1035
1294
|
|
|
@@ -1155,121 +1414,89 @@ def format_datetime_with_timezone(iso_datetime_string: str) -> str:
|
|
|
1155
1414
|
|
|
1156
1415
|
|
|
1157
1416
|
def calculate_element_points(
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1417
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1418
|
+
celestial_points_names: Sequence[str],
|
|
1419
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
1420
|
+
*,
|
|
1421
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1422
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1423
|
+
) -> dict[str, float]:
|
|
1162
1424
|
"""
|
|
1163
|
-
Calculate elemental
|
|
1425
|
+
Calculate elemental totals for a subject using the selected strategy.
|
|
1164
1426
|
|
|
1165
1427
|
Args:
|
|
1166
|
-
planets_settings
|
|
1167
|
-
celestial_points_names
|
|
1168
|
-
subject: Astrological subject with
|
|
1428
|
+
planets_settings: Planet configuration list (kept for API compatibility).
|
|
1429
|
+
celestial_points_names: Celestial point names to include.
|
|
1430
|
+
subject: Astrological subject with planetary data.
|
|
1431
|
+
method: Calculation method (pure_count or weighted). Defaults to weighted.
|
|
1432
|
+
custom_weights: Optional overrides for point weights keyed by name.
|
|
1169
1433
|
|
|
1170
1434
|
Returns:
|
|
1171
|
-
|
|
1172
|
-
"""
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
{"name": "Cap", "element": "earth"},
|
|
1184
|
-
{"name": "Aqu", "element": "air"},
|
|
1185
|
-
{"name": "Pis", "element": "water"},
|
|
1435
|
+
Dictionary mapping each element to its accumulated total.
|
|
1436
|
+
"""
|
|
1437
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1438
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1439
|
+
|
|
1440
|
+
return _calculate_distribution_for_subject(
|
|
1441
|
+
subject,
|
|
1442
|
+
normalized_names,
|
|
1443
|
+
_SIGN_TO_ELEMENT,
|
|
1444
|
+
_ELEMENT_KEYS,
|
|
1445
|
+
weight_lookup,
|
|
1446
|
+
fallback_weight,
|
|
1186
1447
|
)
|
|
1187
1448
|
|
|
1188
|
-
# Initialize element point totals
|
|
1189
|
-
element_totals = {
|
|
1190
|
-
"fire": 0.0,
|
|
1191
|
-
"earth": 0.0,
|
|
1192
|
-
"air": 0.0,
|
|
1193
|
-
"water": 0.0
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
# Make list of the points sign
|
|
1197
|
-
points_sign = [subject.get(planet).sign_num for planet in celestial_points_names]
|
|
1198
|
-
|
|
1199
|
-
for i in range(len(planets_settings)):
|
|
1200
|
-
# Add points to appropriate element
|
|
1201
|
-
element = ZODIAC[points_sign[i]]["element"]
|
|
1202
|
-
element_totals[element] += planets_settings[i]["element_points"]
|
|
1203
|
-
|
|
1204
|
-
return element_totals
|
|
1205
|
-
|
|
1206
1449
|
|
|
1207
1450
|
def calculate_synastry_element_points(
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1451
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1452
|
+
celestial_points_names: Sequence[str],
|
|
1453
|
+
subject1: AstrologicalSubjectModel,
|
|
1454
|
+
subject2: AstrologicalSubjectModel,
|
|
1455
|
+
*,
|
|
1456
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1457
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1458
|
+
) -> dict[str, float]:
|
|
1213
1459
|
"""
|
|
1214
|
-
Calculate
|
|
1460
|
+
Calculate combined element percentages for a synastry chart.
|
|
1215
1461
|
|
|
1216
1462
|
Args:
|
|
1217
|
-
planets_settings
|
|
1218
|
-
celestial_points_names
|
|
1219
|
-
subject1: First astrological subject
|
|
1220
|
-
subject2: Second astrological subject
|
|
1463
|
+
planets_settings: Planet configuration list (unused but preserved).
|
|
1464
|
+
celestial_points_names: Celestial point names to process.
|
|
1465
|
+
subject1: First astrological subject.
|
|
1466
|
+
subject2: Second astrological subject.
|
|
1467
|
+
method: Calculation strategy (pure_count or weighted).
|
|
1468
|
+
custom_weights: Optional overrides for point weights.
|
|
1221
1469
|
|
|
1222
1470
|
Returns:
|
|
1223
|
-
|
|
1224
|
-
"""
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1471
|
+
Dictionary with element percentages summing to 100.
|
|
1472
|
+
"""
|
|
1473
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1474
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1475
|
+
|
|
1476
|
+
subject1_totals = _calculate_distribution_for_subject(
|
|
1477
|
+
subject1,
|
|
1478
|
+
normalized_names,
|
|
1479
|
+
_SIGN_TO_ELEMENT,
|
|
1480
|
+
_ELEMENT_KEYS,
|
|
1481
|
+
weight_lookup,
|
|
1482
|
+
fallback_weight,
|
|
1483
|
+
)
|
|
1484
|
+
subject2_totals = _calculate_distribution_for_subject(
|
|
1485
|
+
subject2,
|
|
1486
|
+
normalized_names,
|
|
1487
|
+
_SIGN_TO_ELEMENT,
|
|
1488
|
+
_ELEMENT_KEYS,
|
|
1489
|
+
weight_lookup,
|
|
1490
|
+
fallback_weight,
|
|
1238
1491
|
)
|
|
1239
1492
|
|
|
1240
|
-
|
|
1241
|
-
combined_totals = {
|
|
1242
|
-
"fire": 0.0,
|
|
1243
|
-
"earth": 0.0,
|
|
1244
|
-
"air": 0.0,
|
|
1245
|
-
"water": 0.0
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
# Make list of the points sign for both subjects
|
|
1249
|
-
subject1_points_sign = [subject1.get(planet).sign_num for planet in celestial_points_names]
|
|
1250
|
-
subject2_points_sign = [subject2.get(planet).sign_num for planet in celestial_points_names]
|
|
1251
|
-
|
|
1252
|
-
# Calculate element points for subject 1
|
|
1253
|
-
for i in range(len(planets_settings)):
|
|
1254
|
-
# Add points to appropriate element
|
|
1255
|
-
element1 = ZODIAC[subject1_points_sign[i]]["element"]
|
|
1256
|
-
combined_totals[element1] += planets_settings[i]["element_points"]
|
|
1257
|
-
|
|
1258
|
-
# Calculate element points for subject 2
|
|
1259
|
-
for i in range(len(planets_settings)):
|
|
1260
|
-
# Add points to appropriate element
|
|
1261
|
-
element2 = ZODIAC[subject2_points_sign[i]]["element"]
|
|
1262
|
-
combined_totals[element2] += planets_settings[i]["element_points"]
|
|
1263
|
-
|
|
1264
|
-
# Calculate total points across all elements
|
|
1493
|
+
combined_totals = {key: subject1_totals[key] + subject2_totals[key] for key in _ELEMENT_KEYS}
|
|
1265
1494
|
total_points = sum(combined_totals.values())
|
|
1266
1495
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
for element in combined_totals:
|
|
1270
|
-
combined_totals[element] = (combined_totals[element] / total_points) * 100.0
|
|
1496
|
+
if total_points == 0:
|
|
1497
|
+
return {key: 0.0 for key in _ELEMENT_KEYS}
|
|
1271
1498
|
|
|
1272
|
-
return combined_totals
|
|
1499
|
+
return {key: (combined_totals[key] / total_points) * 100.0 for key in _ELEMENT_KEYS}
|
|
1273
1500
|
|
|
1274
1501
|
|
|
1275
1502
|
def draw_house_comparison_grid(
|
|
@@ -1281,9 +1508,9 @@ def draw_house_comparison_grid(
|
|
|
1281
1508
|
text_color: str = "var(--kerykeion-color-neutral-content)",
|
|
1282
1509
|
house_position_comparison_label: str = "House Position Comparison",
|
|
1283
1510
|
return_point_label: str = "Return Point",
|
|
1284
|
-
return_label: str = "
|
|
1511
|
+
return_label: str = "DualReturnChart",
|
|
1285
1512
|
radix_label: str = "Radix",
|
|
1286
|
-
x_position: int =
|
|
1513
|
+
x_position: int = 1100,
|
|
1287
1514
|
y_position: int = 0,
|
|
1288
1515
|
) -> str:
|
|
1289
1516
|
"""
|
|
@@ -1439,149 +1666,175 @@ def makeLunarPhase(degrees_between_sun_and_moon: float, latitude: float) -> str:
|
|
|
1439
1666
|
|
|
1440
1667
|
Parameters:
|
|
1441
1668
|
- degrees_between_sun_and_moon (float): Angle between sun and moon in degrees
|
|
1442
|
-
- latitude (float): Observer's latitude for
|
|
1669
|
+
- latitude (float): Observer's latitude (no longer used, kept for backward compatibility)
|
|
1443
1670
|
|
|
1444
1671
|
Returns:
|
|
1445
1672
|
- str: SVG representation of lunar phase
|
|
1446
1673
|
"""
|
|
1447
|
-
|
|
1448
|
-
params = calculate_moon_phase_chart_params(degrees_between_sun_and_moon, latitude)
|
|
1449
|
-
|
|
1450
|
-
# Extract the calculated values
|
|
1451
|
-
lunar_phase_circle_center_x = params["circle_center_x"]
|
|
1452
|
-
lunar_phase_circle_radius = params["circle_radius"]
|
|
1453
|
-
lunar_phase_rotate = params["lunar_phase_rotate"]
|
|
1454
|
-
|
|
1455
|
-
# Generate the SVG for the lunar phase
|
|
1456
|
-
svg = (
|
|
1457
|
-
f'<g transform="rotate({lunar_phase_rotate} 20 10)">\n'
|
|
1458
|
-
f' <defs>\n'
|
|
1459
|
-
f' <clipPath id="moonPhaseCutOffCircle">\n'
|
|
1460
|
-
f' <circle cx="20" cy="10" r="10" />\n'
|
|
1461
|
-
f' </clipPath>\n'
|
|
1462
|
-
f' </defs>\n'
|
|
1463
|
-
f' <circle cx="20" cy="10" r="10" style="fill: var(--kerykeion-chart-color-lunar-phase-0)" />\n'
|
|
1464
|
-
f' <circle cx="{lunar_phase_circle_center_x}" cy="10" r="{lunar_phase_circle_radius}" style="fill: var(--kerykeion-chart-color-lunar-phase-1)" clip-path="url(#moonPhaseCutOffCircle)" />\n'
|
|
1465
|
-
f' <circle cx="20" cy="10" r="10" style="fill: none; stroke: var(--kerykeion-chart-color-lunar-phase-0); stroke-width: 0.5px; stroke-opacity: 0.5" />\n'
|
|
1466
|
-
f'</g>'
|
|
1467
|
-
)
|
|
1674
|
+
params = calculate_moon_phase_chart_params(degrees_between_sun_and_moon)
|
|
1468
1675
|
|
|
1469
|
-
|
|
1676
|
+
phase_angle = params["phase_angle"]
|
|
1677
|
+
illuminated_fraction = 1.0 - params["illuminated_fraction"]
|
|
1678
|
+
shadow_ellipse_rx = abs(params["shadow_ellipse_rx"])
|
|
1470
1679
|
|
|
1680
|
+
radius = 10.0
|
|
1681
|
+
center_x = 20.0
|
|
1682
|
+
center_y = 10.0
|
|
1471
1683
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
celestial_points_names: list[str],
|
|
1475
|
-
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
1476
|
-
):
|
|
1477
|
-
"""
|
|
1478
|
-
Calculate quality point totals based on planetary positions.
|
|
1684
|
+
bright_color = "var(--kerykeion-chart-color-lunar-phase-1)"
|
|
1685
|
+
shadow_color = "var(--kerykeion-chart-color-lunar-phase-0)"
|
|
1479
1686
|
|
|
1480
|
-
|
|
1481
|
-
planets_settings (list): List of planet configuration dictionaries
|
|
1482
|
-
celestial_points_names (list): List of celestial point names to process
|
|
1483
|
-
subject: Astrological subject with get() method for accessing planet data
|
|
1484
|
-
planet_in_zodiac_extra_points (int): Extra points awarded for planets in their home sign
|
|
1687
|
+
is_waxing = phase_angle < 180.0
|
|
1485
1688
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1689
|
+
if illuminated_fraction <= 1e-6:
|
|
1690
|
+
base_fill = shadow_color
|
|
1691
|
+
overlay_path = ""
|
|
1692
|
+
overlay_fill = ""
|
|
1693
|
+
elif 1.0 - illuminated_fraction <= 1e-6:
|
|
1694
|
+
base_fill = bright_color
|
|
1695
|
+
overlay_path = ""
|
|
1696
|
+
overlay_fill = ""
|
|
1697
|
+
else:
|
|
1698
|
+
is_lit_major = illuminated_fraction >= 0.5
|
|
1699
|
+
if is_lit_major:
|
|
1700
|
+
base_fill = bright_color
|
|
1701
|
+
overlay_fill = shadow_color
|
|
1702
|
+
overlay_side = "left" if is_waxing else "right"
|
|
1703
|
+
else:
|
|
1704
|
+
base_fill = shadow_color
|
|
1705
|
+
overlay_fill = bright_color
|
|
1706
|
+
overlay_side = "right" if is_waxing else "left"
|
|
1707
|
+
|
|
1708
|
+
# The illuminated limb is the orthographic projection of the lunar terminator;
|
|
1709
|
+
# it appears as an ellipse with vertical radius equal to the lunar radius and
|
|
1710
|
+
# horizontal radius scaled by |cos(phase)|.
|
|
1711
|
+
def build_lune_path(side: str, ellipse_rx: float) -> str:
|
|
1712
|
+
ellipse_rx = max(0.0, min(radius, ellipse_rx))
|
|
1713
|
+
top_y = center_y - radius
|
|
1714
|
+
bottom_y = center_y + radius
|
|
1715
|
+
circle_sweep = 1 if side == "right" else 0
|
|
1716
|
+
|
|
1717
|
+
if ellipse_rx <= 1e-6:
|
|
1718
|
+
return (
|
|
1719
|
+
f"M {center_x:.4f} {top_y:.4f}"
|
|
1720
|
+
f" A {radius:.4f} {radius:.4f} 0 0 {circle_sweep} {center_x:.4f} {bottom_y:.4f}"
|
|
1721
|
+
f" L {center_x:.4f} {top_y:.4f}"
|
|
1722
|
+
" Z"
|
|
1723
|
+
)
|
|
1724
|
+
|
|
1725
|
+
return (
|
|
1726
|
+
f"M {center_x:.4f} {top_y:.4f}"
|
|
1727
|
+
f" A {radius:.4f} {radius:.4f} 0 0 {circle_sweep} {center_x:.4f} {bottom_y:.4f}"
|
|
1728
|
+
f" A {ellipse_rx:.4f} {radius:.4f} 0 0 {circle_sweep} {center_x:.4f} {top_y:.4f}"
|
|
1729
|
+
" Z"
|
|
1730
|
+
)
|
|
1510
1731
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1732
|
+
overlay_path = build_lune_path(overlay_side, shadow_ellipse_rx)
|
|
1733
|
+
|
|
1734
|
+
svg_lines = [
|
|
1735
|
+
'<g transform="rotate(0 20 10)">',
|
|
1736
|
+
' <defs>',
|
|
1737
|
+
' <clipPath id="moonPhaseCutOffCircle">',
|
|
1738
|
+
' <circle cx="20" cy="10" r="10" />',
|
|
1739
|
+
' </clipPath>',
|
|
1740
|
+
' </defs>',
|
|
1741
|
+
f' <circle cx="20" cy="10" r="10" style="fill: {base_fill}" />',
|
|
1742
|
+
]
|
|
1743
|
+
|
|
1744
|
+
if overlay_path:
|
|
1745
|
+
svg_lines.append(
|
|
1746
|
+
f' <path d="{overlay_path}" style="fill: {overlay_fill}" clip-path="url(#moonPhaseCutOffCircle)" />'
|
|
1747
|
+
)
|
|
1513
1748
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1749
|
+
svg_lines.append(
|
|
1750
|
+
' <circle cx="20" cy="10" r="10" style="fill: none; stroke: var(--kerykeion-chart-color-lunar-phase-0); stroke-width: 0.5px; stroke-opacity: 0.5" />'
|
|
1751
|
+
)
|
|
1752
|
+
svg_lines.append('</g>')
|
|
1518
1753
|
|
|
1519
|
-
return
|
|
1754
|
+
return "\n".join(svg_lines)
|
|
1520
1755
|
|
|
1521
1756
|
|
|
1522
|
-
def
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1757
|
+
def calculate_quality_points(
|
|
1758
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1759
|
+
celestial_points_names: Sequence[str],
|
|
1760
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
1761
|
+
*,
|
|
1762
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1763
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1764
|
+
) -> dict[str, float]:
|
|
1528
1765
|
"""
|
|
1529
|
-
Calculate
|
|
1766
|
+
Calculate modality totals for a subject using the selected strategy.
|
|
1530
1767
|
|
|
1531
1768
|
Args:
|
|
1532
|
-
planets_settings
|
|
1533
|
-
celestial_points_names
|
|
1534
|
-
|
|
1535
|
-
|
|
1769
|
+
planets_settings: Planet configuration list (kept for API compatibility).
|
|
1770
|
+
celestial_points_names: Celestial point names to include.
|
|
1771
|
+
subject: Astrological subject with planetary data.
|
|
1772
|
+
method: Calculation method (pure_count or weighted). Defaults to weighted.
|
|
1773
|
+
custom_weights: Optional overrides for point weights keyed by name.
|
|
1536
1774
|
|
|
1537
1775
|
Returns:
|
|
1538
|
-
|
|
1539
|
-
"""
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
{"name": "Cap", "quality": "cardinal"},
|
|
1551
|
-
{"name": "Aqu", "quality": "fixed"},
|
|
1552
|
-
{"name": "Pis", "quality": "mutable"},
|
|
1776
|
+
Dictionary mapping each modality to its accumulated total.
|
|
1777
|
+
"""
|
|
1778
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1779
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1780
|
+
|
|
1781
|
+
return _calculate_distribution_for_subject(
|
|
1782
|
+
subject,
|
|
1783
|
+
normalized_names,
|
|
1784
|
+
_SIGN_TO_QUALITY,
|
|
1785
|
+
_QUALITY_KEYS,
|
|
1786
|
+
weight_lookup,
|
|
1787
|
+
fallback_weight,
|
|
1553
1788
|
)
|
|
1554
1789
|
|
|
1555
|
-
# Initialize combined quality point totals
|
|
1556
|
-
combined_totals = {
|
|
1557
|
-
"cardinal": 0.0,
|
|
1558
|
-
"fixed": 0.0,
|
|
1559
|
-
"mutable": 0.0
|
|
1560
|
-
}
|
|
1561
1790
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1791
|
+
def calculate_synastry_quality_points(
|
|
1792
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1793
|
+
celestial_points_names: Sequence[str],
|
|
1794
|
+
subject1: AstrologicalSubjectModel,
|
|
1795
|
+
subject2: AstrologicalSubjectModel,
|
|
1796
|
+
*,
|
|
1797
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1798
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1799
|
+
) -> dict[str, float]:
|
|
1800
|
+
"""
|
|
1801
|
+
Calculate combined modality percentages for a synastry chart.
|
|
1565
1802
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1803
|
+
Args:
|
|
1804
|
+
planets_settings: Planet configuration list (unused but preserved).
|
|
1805
|
+
celestial_points_names: Celestial point names to process.
|
|
1806
|
+
subject1: First astrological subject.
|
|
1807
|
+
subject2: Second astrological subject.
|
|
1808
|
+
method: Calculation strategy (pure_count or weighted).
|
|
1809
|
+
custom_weights: Optional overrides for point weights.
|
|
1571
1810
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1811
|
+
Returns:
|
|
1812
|
+
Dictionary with modality percentages summing to 100.
|
|
1813
|
+
"""
|
|
1814
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1815
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1816
|
+
|
|
1817
|
+
subject1_totals = _calculate_distribution_for_subject(
|
|
1818
|
+
subject1,
|
|
1819
|
+
normalized_names,
|
|
1820
|
+
_SIGN_TO_QUALITY,
|
|
1821
|
+
_QUALITY_KEYS,
|
|
1822
|
+
weight_lookup,
|
|
1823
|
+
fallback_weight,
|
|
1824
|
+
)
|
|
1825
|
+
subject2_totals = _calculate_distribution_for_subject(
|
|
1826
|
+
subject2,
|
|
1827
|
+
normalized_names,
|
|
1828
|
+
_SIGN_TO_QUALITY,
|
|
1829
|
+
_QUALITY_KEYS,
|
|
1830
|
+
weight_lookup,
|
|
1831
|
+
fallback_weight,
|
|
1832
|
+
)
|
|
1577
1833
|
|
|
1578
|
-
|
|
1834
|
+
combined_totals = {key: subject1_totals[key] + subject2_totals[key] for key in _QUALITY_KEYS}
|
|
1579
1835
|
total_points = sum(combined_totals.values())
|
|
1580
1836
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
for quality in combined_totals:
|
|
1584
|
-
combined_totals[quality] = (combined_totals[quality] / total_points) * 100.0
|
|
1585
|
-
|
|
1586
|
-
return combined_totals
|
|
1837
|
+
if total_points == 0:
|
|
1838
|
+
return {key: 0.0 for key in _QUALITY_KEYS}
|
|
1587
1839
|
|
|
1840
|
+
return {key: (combined_totals[key] / total_points) * 100.0 for key in _QUALITY_KEYS}
|