kerykeion 4.26.3__py3-none-any.whl → 5.0.0__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 +54 -11
- kerykeion/aspects/__init__.py +5 -2
- kerykeion/aspects/aspects_factory.py +569 -0
- kerykeion/aspects/aspects_utils.py +81 -8
- kerykeion/astrological_subject_factory.py +1897 -0
- kerykeion/backword.py +773 -0
- kerykeion/chart_data_factory.py +549 -0
- kerykeion/charts/chart_drawer.py +2601 -0
- kerykeion/charts/charts_utils.py +948 -177
- kerykeion/charts/draw_planets.py +602 -351
- kerykeion/charts/templates/aspect_grid_only.xml +328 -202
- kerykeion/charts/templates/chart.xml +432 -272
- kerykeion/charts/templates/wheel_only.xml +350 -214
- kerykeion/charts/themes/black-and-white.css +148 -0
- kerykeion/charts/themes/classic.css +107 -76
- kerykeion/charts/themes/dark-high-contrast.css +145 -107
- kerykeion/charts/themes/dark.css +146 -107
- kerykeion/charts/themes/light.css +146 -103
- kerykeion/charts/themes/strawberry.css +158 -0
- kerykeion/composite_subject_factory.py +253 -51
- kerykeion/ephemeris_data_factory.py +434 -0
- kerykeion/fetch_geonames.py +27 -8
- kerykeion/house_comparison/__init__.py +6 -0
- kerykeion/house_comparison/house_comparison_factory.py +103 -0
- kerykeion/house_comparison/house_comparison_utils.py +126 -0
- 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 -132
- kerykeion/kr_types/kr_models.py +14 -318
- kerykeion/kr_types/settings_models.py +15 -203
- kerykeion/planetary_return_factory.py +805 -0
- kerykeion/relationship_score_factory.py +301 -0
- kerykeion/report.py +751 -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 +605 -0
- kerykeion/schemas/settings_models.py +180 -0
- kerykeion/settings/__init__.py +20 -1
- kerykeion/settings/chart_defaults.py +444 -0
- kerykeion/settings/config_constants.py +117 -12
- kerykeion/settings/kerykeion_settings.py +31 -73
- kerykeion/settings/translation_strings.py +1479 -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 +393 -114
- kerykeion-5.0.0.dist-info/METADATA +1176 -0
- kerykeion-5.0.0.dist-info/RECORD +63 -0
- {kerykeion-4.26.3.dist-info → kerykeion-5.0.0.dist-info}/WHEEL +1 -1
- kerykeion/aspects/natal_aspects.py +0 -172
- kerykeion/aspects/synastry_aspects.py +0 -124
- kerykeion/aspects/transits_time_range.py +0 -41
- kerykeion/astrological_subject.py +0 -841
- kerykeion/charts/kerykeion_chart_svg.py +0 -1219
- kerykeion/enums.py +0 -57
- kerykeion/ephemeris_data.py +0 -242
- kerykeion/kr_types/chart_types.py +0 -95
- kerykeion/relationship_score/__init__.py +0 -2
- kerykeion/relationship_score/relationship_score.py +0 -175
- kerykeion/relationship_score/relationship_score_factory.py +0 -230
- kerykeion/settings/kr.config.json +0 -1258
- kerykeion/transits_time_range.py +0 -124
- kerykeion-4.26.3.dist-info/METADATA +0 -634
- kerykeion-4.26.3.dist-info/RECORD +0 -45
- kerykeion-4.26.3.dist-info/entry_points.txt +0 -3
- {kerykeion-4.26.3.dist-info → kerykeion-5.0.0.dist-info/licenses}/LICENSE +0 -0
kerykeion/charts/charts_utils.py
CHANGED
|
@@ -1,9 +1,230 @@
|
|
|
1
1
|
import math
|
|
2
2
|
import datetime
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
from kerykeion.
|
|
6
|
-
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
|
|
227
|
+
|
|
7
228
|
|
|
8
229
|
|
|
9
230
|
def get_decoded_kerykeion_celestial_point_name(input_planet_name: str, celestial_point_language: KerykeionLanguageCelestialPointModel) -> str:
|
|
@@ -186,7 +407,7 @@ def draw_zodiac_slice(
|
|
|
186
407
|
# pie slices
|
|
187
408
|
offset = 360 - seventh_house_degree_ut
|
|
188
409
|
# check transit
|
|
189
|
-
if chart_type == "Transit" or chart_type == "Synastry":
|
|
410
|
+
if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "DualReturnChart":
|
|
190
411
|
dropin: Union[int, float] = 0
|
|
191
412
|
else:
|
|
192
413
|
dropin = c1
|
|
@@ -195,7 +416,7 @@ def draw_zodiac_slice(
|
|
|
195
416
|
# symbols
|
|
196
417
|
offset = offset + 15
|
|
197
418
|
# check transit
|
|
198
|
-
if chart_type == "Transit" or chart_type == "Synastry":
|
|
419
|
+
if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "DualReturnChart":
|
|
199
420
|
dropin = 54
|
|
200
421
|
else:
|
|
201
422
|
dropin = 18 + c1
|
|
@@ -284,11 +505,12 @@ def draw_aspect_line(
|
|
|
284
505
|
y2 = sliceToY(0, ar, second_offset) + (r - ar)
|
|
285
506
|
|
|
286
507
|
return (
|
|
287
|
-
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"]}">'
|
|
288
509
|
f'<line class="aspect" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {color}; stroke-width: 1; stroke-opacity: .9;"/>'
|
|
289
510
|
f"</g>"
|
|
290
511
|
)
|
|
291
512
|
|
|
513
|
+
|
|
292
514
|
def convert_decimal_to_degree_string(dec: float, format_type: Literal["1", "2", "3"] = "3") -> str:
|
|
293
515
|
"""
|
|
294
516
|
Converts a decimal float to a degrees string in the specified format.
|
|
@@ -415,7 +637,7 @@ def draw_first_circle(
|
|
|
415
637
|
Returns:
|
|
416
638
|
str: The SVG path of the first circle.
|
|
417
639
|
"""
|
|
418
|
-
if chart_type == "Synastry" or chart_type == "Transit":
|
|
640
|
+
if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "DualReturnChart":
|
|
419
641
|
return f'<circle cx="{r}" cy="{r}" r="{r - 36}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.4;" />'
|
|
420
642
|
else:
|
|
421
643
|
if c1 is None:
|
|
@@ -426,6 +648,21 @@ def draw_first_circle(
|
|
|
426
648
|
)
|
|
427
649
|
|
|
428
650
|
|
|
651
|
+
def draw_background_circle(r: Union[int, float], stroke_color: str, fill_color: str) -> str:
|
|
652
|
+
"""
|
|
653
|
+
Draws the background circle.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
- r (Union[int, float]): The value of r.
|
|
657
|
+
- stroke_color (str): The color of the stroke.
|
|
658
|
+
- fill_color (str): The color of the fill.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
str: The SVG path of the background circle.
|
|
662
|
+
"""
|
|
663
|
+
return f'<circle cx="{r}" cy="{r}" r="{r}" style="fill: {fill_color}; stroke: {stroke_color}; stroke-width: 1px;" />'
|
|
664
|
+
|
|
665
|
+
|
|
429
666
|
def draw_second_circle(
|
|
430
667
|
r: Union[int, float], stroke_color: str, fill_color: str, chart_type: ChartType, c2: Union[int, float, None] = None
|
|
431
668
|
) -> str:
|
|
@@ -443,7 +680,7 @@ def draw_second_circle(
|
|
|
443
680
|
str: The SVG path of the second circle.
|
|
444
681
|
"""
|
|
445
682
|
|
|
446
|
-
if chart_type == "Synastry" or chart_type == "Transit":
|
|
683
|
+
if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "DualReturnChart":
|
|
447
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" />'
|
|
448
685
|
|
|
449
686
|
else:
|
|
@@ -473,7 +710,7 @@ def draw_third_circle(
|
|
|
473
710
|
Returns:
|
|
474
711
|
- str: The SVG element as a string.
|
|
475
712
|
"""
|
|
476
|
-
if chart_type in {"Synastry", "Transit"}:
|
|
713
|
+
if chart_type in {"Synastry", "Transit", "DualReturnChart"}:
|
|
477
714
|
# For Synastry and Transit charts, use a fixed radius adjustment of 160
|
|
478
715
|
return f'<circle cx="{radius}" cy="{radius}" r="{radius - 160}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
|
|
479
716
|
|
|
@@ -485,7 +722,7 @@ def draw_aspect_grid(
|
|
|
485
722
|
stroke_color: str,
|
|
486
723
|
available_planets: list,
|
|
487
724
|
aspects: list,
|
|
488
|
-
x_start: int =
|
|
725
|
+
x_start: int = 510,
|
|
489
726
|
y_start: int = 468,
|
|
490
727
|
) -> str:
|
|
491
728
|
"""
|
|
@@ -506,7 +743,7 @@ def draw_aspect_grid(
|
|
|
506
743
|
box_size = 14
|
|
507
744
|
|
|
508
745
|
# Filter active planets
|
|
509
|
-
active_planets = [planet for planet in available_planets if planet
|
|
746
|
+
active_planets = [planet for planet in available_planets if planet["is_active"]]
|
|
510
747
|
|
|
511
748
|
# Reverse the list of active planets for the first iteration
|
|
512
749
|
reversed_planets = active_planets[::-1]
|
|
@@ -553,6 +790,7 @@ def draw_houses_cusps_and_text_number(
|
|
|
553
790
|
chart_type: ChartType,
|
|
554
791
|
second_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
|
|
555
792
|
transit_house_cusp_color: Union[str, None] = None,
|
|
793
|
+
external_view: bool = False,
|
|
556
794
|
) -> str:
|
|
557
795
|
"""
|
|
558
796
|
Draws the houses cusps and text numbers for a given chart type.
|
|
@@ -570,9 +808,10 @@ def draw_houses_cusps_and_text_number(
|
|
|
570
808
|
- chart_type: Type of the chart (e.g., Transit, Synastry).
|
|
571
809
|
- second_subject_houses_list: List of house for the second subject (optional).
|
|
572
810
|
- transit_house_cusp_color: Color for transit house cusps (optional).
|
|
811
|
+
- external_view: Whether to use external view mode for positioning (optional).
|
|
573
812
|
|
|
574
813
|
Returns:
|
|
575
|
-
- A string containing
|
|
814
|
+
- A string containing SVG elements for house cusps and numbers.
|
|
576
815
|
"""
|
|
577
816
|
|
|
578
817
|
path = ""
|
|
@@ -580,7 +819,7 @@ def draw_houses_cusps_and_text_number(
|
|
|
580
819
|
|
|
581
820
|
for i in range(xr):
|
|
582
821
|
# Determine offsets based on chart type
|
|
583
|
-
dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry"] else (c3, c1, False)
|
|
822
|
+
dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry", "DualReturnChart"] else (c3, c1, False)
|
|
584
823
|
|
|
585
824
|
# Calculate the offset for the current house cusp
|
|
586
825
|
offset = (int(first_subject_houses_list[int(xr / 2)].abs_pos) / -1) + int(first_subject_houses_list[i].abs_pos)
|
|
@@ -602,7 +841,7 @@ def draw_houses_cusps_and_text_number(
|
|
|
602
841
|
i, standard_house_cusp_color
|
|
603
842
|
)
|
|
604
843
|
|
|
605
|
-
if chart_type in ["Transit", "Synastry"]:
|
|
844
|
+
if chart_type in ["Transit", "Synastry", "DualReturnChart"]:
|
|
606
845
|
if second_subject_houses_list is None or transit_house_cusp_color is None:
|
|
607
846
|
raise KerykeionException("second_subject_houses_list_ut or transit_house_cusp_color is None")
|
|
608
847
|
|
|
@@ -626,30 +865,34 @@ def draw_houses_cusps_and_text_number(
|
|
|
626
865
|
|
|
627
866
|
# Add the house number text for the second subject
|
|
628
867
|
fill_opacity = "0" if chart_type == "Transit" else ".4"
|
|
629
|
-
path +=
|
|
868
|
+
path += '<g kr:node="HouseNumber">'
|
|
630
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>'
|
|
631
|
-
path +=
|
|
870
|
+
path += "</g>"
|
|
632
871
|
|
|
633
872
|
# Add the house cusp line for the second subject
|
|
634
873
|
stroke_opacity = "0" if chart_type == "Transit" else ".3"
|
|
635
|
-
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}">'
|
|
636
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};'/>"
|
|
637
|
-
path +=
|
|
876
|
+
path += "</g>"
|
|
638
877
|
|
|
639
|
-
# Adjust dropin based on chart type
|
|
640
|
-
|
|
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)
|
|
641
884
|
xtext = sliceToX(0, (r - dropin), text_offset) + dropin
|
|
642
885
|
ytext = sliceToY(0, (r - dropin), text_offset) + dropin
|
|
643
886
|
|
|
644
887
|
# Add the house cusp line for the first subject
|
|
645
|
-
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}">'
|
|
646
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;"/>'
|
|
647
|
-
path +=
|
|
890
|
+
path += "</g>"
|
|
648
891
|
|
|
649
892
|
# Add the house number text for the first subject
|
|
650
|
-
path +=
|
|
893
|
+
path += '<g kr:node="HouseNumber">'
|
|
651
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>'
|
|
652
|
-
path +=
|
|
895
|
+
path += "</g>"
|
|
653
896
|
|
|
654
897
|
return path
|
|
655
898
|
|
|
@@ -658,7 +901,13 @@ def draw_transit_aspect_list(
|
|
|
658
901
|
grid_title: str,
|
|
659
902
|
aspects_list: Union[list[AspectModel], list[dict]],
|
|
660
903
|
celestial_point_language: Union[KerykeionLanguageCelestialPointModel, dict],
|
|
661
|
-
aspects_settings:
|
|
904
|
+
aspects_settings: dict,
|
|
905
|
+
*,
|
|
906
|
+
aspects_per_column: int = 14,
|
|
907
|
+
column_width: int = 100,
|
|
908
|
+
line_height: int = 14,
|
|
909
|
+
max_columns: int = 6,
|
|
910
|
+
chart_height: Optional[int] = None,
|
|
662
911
|
) -> str:
|
|
663
912
|
"""
|
|
664
913
|
Generates the SVG output for the aspect transit grid.
|
|
@@ -666,8 +915,14 @@ def draw_transit_aspect_list(
|
|
|
666
915
|
Parameters:
|
|
667
916
|
- grid_title: Title of the grid.
|
|
668
917
|
- aspects_list: List of aspects.
|
|
669
|
-
-
|
|
918
|
+
- celestial_point_language: Dictionary containing the celestial point language data.
|
|
670
919
|
- aspects_settings: Dictionary containing the aspect settings.
|
|
920
|
+
- aspects_per_column: Number of aspects to display per column (default: 14).
|
|
921
|
+
- column_width: Width in pixels for each column (default: 100).
|
|
922
|
+
- line_height: Height in pixels for each line (default: 14).
|
|
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).
|
|
671
926
|
|
|
672
927
|
Returns:
|
|
673
928
|
- A string containing the SVG path data for the aspect transit grid.
|
|
@@ -676,68 +931,93 @@ def draw_transit_aspect_list(
|
|
|
676
931
|
if isinstance(celestial_point_language, dict):
|
|
677
932
|
celestial_point_language = KerykeionLanguageCelestialPointModel(**celestial_point_language)
|
|
678
933
|
|
|
679
|
-
if isinstance(aspects_settings, dict):
|
|
680
|
-
aspects_settings = KerykeionSettingsAspectModel(**aspects_settings)
|
|
681
|
-
|
|
682
934
|
# If not instance of AspectModel, convert to AspectModel
|
|
683
|
-
if isinstance(aspects_list[0], dict):
|
|
684
|
-
aspects_list = [AspectModel(**aspect) for aspect in aspects_list]
|
|
935
|
+
if aspects_list and isinstance(aspects_list[0], dict):
|
|
936
|
+
aspects_list = [AspectModel(**aspect) for aspect in aspects_list] # type: ignore
|
|
937
|
+
|
|
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
|
|
685
952
|
|
|
686
|
-
line = 0
|
|
687
|
-
nl = 0
|
|
688
953
|
inner_path = ""
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
954
|
+
|
|
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})">'
|
|
738
1018
|
out += f'<text y="-15" x="0" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 14px;">{grid_title}:</text>'
|
|
739
1019
|
out += inner_path
|
|
740
|
-
out +=
|
|
1020
|
+
out += "</g>"
|
|
741
1021
|
|
|
742
1022
|
return out
|
|
743
1023
|
|
|
@@ -804,31 +1084,28 @@ def calculate_moon_phase_chart_params(
|
|
|
804
1084
|
"lunar_phase_rotate": lunar_phase_rotate,
|
|
805
1085
|
}
|
|
806
1086
|
|
|
807
|
-
|
|
1087
|
+
|
|
1088
|
+
def draw_main_house_grid(
|
|
808
1089
|
main_subject_houses_list: list[KerykeionPointModel],
|
|
809
|
-
chart_type: ChartType,
|
|
810
|
-
secondary_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
|
|
811
|
-
text_color: str = "#000000",
|
|
812
1090
|
house_cusp_generale_name_label: str = "Cusp",
|
|
1091
|
+
text_color: str = "#000000",
|
|
1092
|
+
x_position: int = 750,
|
|
1093
|
+
y_position: int = 30,
|
|
813
1094
|
) -> str:
|
|
814
1095
|
"""
|
|
815
|
-
Generate SVG code for a grid of astrological houses.
|
|
1096
|
+
Generate SVG code for a grid of astrological houses for the main subject.
|
|
816
1097
|
|
|
817
1098
|
Parameters:
|
|
818
|
-
-
|
|
819
|
-
-
|
|
820
|
-
-
|
|
821
|
-
-
|
|
822
|
-
-
|
|
1099
|
+
- main_subject_houses_list (list[KerykeionPointModel]): List of houses for the main subject.
|
|
1100
|
+
- house_cusp_generale_name_label (str): Label for the house cusp. Defaults to "Cusp".
|
|
1101
|
+
- text_color (str): Color of the text. Defaults to "#000000".
|
|
1102
|
+
- x_position (int): X position for the grid. Defaults to 720.
|
|
1103
|
+
- y_position (int): Y position for the grid. Defaults to 30.
|
|
823
1104
|
|
|
824
1105
|
Returns:
|
|
825
1106
|
- str: The SVG code for the grid of houses.
|
|
826
1107
|
"""
|
|
827
|
-
|
|
828
|
-
if chart_type in ["Synastry", "Transit"] and secondary_subject_houses_list is None:
|
|
829
|
-
raise KerykeionException("secondary_houses is None")
|
|
830
|
-
|
|
831
|
-
svg_output = '<g transform="translate(650,-20)">'
|
|
1108
|
+
svg_output = f'<g transform="translate({x_position},{y_position})">'
|
|
832
1109
|
|
|
833
1110
|
line_increment = 10
|
|
834
1111
|
for i, house in enumerate(main_subject_houses_list):
|
|
@@ -843,74 +1120,108 @@ def draw_house_grid(
|
|
|
843
1120
|
line_increment += 14
|
|
844
1121
|
|
|
845
1122
|
svg_output += "</g>"
|
|
1123
|
+
return svg_output
|
|
846
1124
|
|
|
847
|
-
if chart_type == "Synastry":
|
|
848
|
-
svg_output += '<!-- Synastry Houses -->'
|
|
849
|
-
svg_output += '<g transform="translate(910, -20)">'
|
|
850
|
-
line_increment = 10
|
|
851
|
-
|
|
852
|
-
for i, house in enumerate(secondary_subject_houses_list): # type: ignore
|
|
853
|
-
cusp_number = f"  {i + 1}" if i < 9 else str(i + 1)
|
|
854
|
-
svg_output += (
|
|
855
|
-
f'<g transform="translate(0,{line_increment})">'
|
|
856
|
-
f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
|
|
857
|
-
f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
|
|
858
|
-
f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
|
|
859
|
-
f'</g>'
|
|
860
|
-
)
|
|
861
|
-
line_increment += 14
|
|
862
1125
|
|
|
863
|
-
|
|
1126
|
+
def draw_secondary_house_grid(
|
|
1127
|
+
secondary_subject_houses_list: list[KerykeionPointModel],
|
|
1128
|
+
house_cusp_generale_name_label: str = "Cusp",
|
|
1129
|
+
text_color: str = "#000000",
|
|
1130
|
+
x_position: int = 1015,
|
|
1131
|
+
y_position: int = 30,
|
|
1132
|
+
) -> str:
|
|
1133
|
+
"""
|
|
1134
|
+
Generate SVG code for a grid of astrological houses for the secondary subject.
|
|
1135
|
+
|
|
1136
|
+
Parameters:
|
|
1137
|
+
- secondary_subject_houses_list (list[KerykeionPointModel]): List of houses for the secondary subject.
|
|
1138
|
+
- house_cusp_generale_name_label (str): Label for the house cusp. Defaults to "Cusp".
|
|
1139
|
+
- text_color (str): Color of the text. Defaults to "#000000".
|
|
1140
|
+
- x_position (int): X position for the grid. Defaults to 970.
|
|
1141
|
+
- y_position (int): Y position for the grid. Defaults to 30.
|
|
864
1142
|
|
|
1143
|
+
Returns:
|
|
1144
|
+
- str: The SVG code for the grid of houses.
|
|
1145
|
+
"""
|
|
1146
|
+
svg_output = f'<g transform="translate({x_position},{y_position})">'
|
|
1147
|
+
|
|
1148
|
+
line_increment = 10
|
|
1149
|
+
for i, house in enumerate(secondary_subject_houses_list):
|
|
1150
|
+
cusp_number = f"  {i + 1}" if i < 9 else str(i + 1)
|
|
1151
|
+
svg_output += (
|
|
1152
|
+
f'<g transform="translate(0,{line_increment})">'
|
|
1153
|
+
f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
|
|
1154
|
+
f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
|
|
1155
|
+
f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
|
|
1156
|
+
f'</g>'
|
|
1157
|
+
)
|
|
1158
|
+
line_increment += 14
|
|
1159
|
+
|
|
1160
|
+
svg_output += "</g>"
|
|
865
1161
|
return svg_output
|
|
866
1162
|
|
|
867
1163
|
|
|
868
|
-
def
|
|
1164
|
+
def draw_main_planet_grid(
|
|
869
1165
|
planets_and_houses_grid_title: str,
|
|
870
1166
|
subject_name: str,
|
|
871
1167
|
available_kerykeion_celestial_points: list[KerykeionPointModel],
|
|
872
1168
|
chart_type: ChartType,
|
|
873
1169
|
celestial_point_language: KerykeionLanguageCelestialPointModel,
|
|
874
|
-
second_subject_name: Union[str, None] = None,
|
|
875
|
-
second_subject_available_kerykeion_celestial_points: Union[list[KerykeionPointModel], None] = None,
|
|
876
1170
|
text_color: str = "#000000",
|
|
1171
|
+
x_position: int = 645,
|
|
1172
|
+
y_position: int = 0,
|
|
877
1173
|
) -> str:
|
|
878
1174
|
"""
|
|
879
|
-
|
|
1175
|
+
Draw the planet grid (main subject) and optional title.
|
|
1176
|
+
|
|
1177
|
+
The entire output is wrapped in a single SVG group `<g>` so the
|
|
1178
|
+
whole block can be repositioned by changing the group transform.
|
|
880
1179
|
|
|
881
1180
|
Args:
|
|
882
|
-
planets_and_houses_grid_title
|
|
883
|
-
subject_name
|
|
884
|
-
available_kerykeion_celestial_points
|
|
885
|
-
chart_type
|
|
886
|
-
celestial_point_language
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1181
|
+
planets_and_houses_grid_title: Title prefix to show for eligible chart types.
|
|
1182
|
+
subject_name: Subject name to append to the title.
|
|
1183
|
+
available_kerykeion_celestial_points: Celestial points to render in the grid.
|
|
1184
|
+
chart_type: Chart type identifier (Literal string).
|
|
1185
|
+
celestial_point_language: Language model for celestial point decoding.
|
|
1186
|
+
text_color: Text color for labels (default: "#000000").
|
|
1187
|
+
x_position: X translation applied to the outer `<g>` (default: 620).
|
|
1188
|
+
y_position: Y translation applied to the outer `<g>` (default: 0).
|
|
890
1189
|
|
|
891
1190
|
Returns:
|
|
892
|
-
|
|
1191
|
+
SVG string for the main planet grid wrapped in a `<g>`.
|
|
893
1192
|
"""
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1193
|
+
# Layout constants (kept identical to previous behavior)
|
|
1194
|
+
BASE_Y = 30
|
|
1195
|
+
HEADER_Y = 15 # Title baseline inside the wrapper
|
|
1196
|
+
LINE_START = 10
|
|
1197
|
+
LINE_STEP = 14
|
|
897
1198
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
)
|
|
1199
|
+
# Wrap everything inside a single group so position can be changed once
|
|
1200
|
+
svg_output = f'<g transform="translate({x_position},{y_position})">'
|
|
1201
|
+
|
|
1202
|
+
# Add title only for specific chart types
|
|
1203
|
+
if chart_type in ("Synastry", "Transit", "DualReturnChart"):
|
|
1204
|
+
svg_output += (
|
|
1205
|
+
f'<g transform="translate(0, {HEADER_Y})">'
|
|
1206
|
+
f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
|
|
1207
|
+
f'</g>'
|
|
1208
|
+
)
|
|
903
1209
|
|
|
904
1210
|
end_of_line = "</g>"
|
|
905
1211
|
|
|
1212
|
+
column_thresholds = _select_planet_grid_thresholds(chart_type)
|
|
1213
|
+
|
|
906
1214
|
for i, planet in enumerate(available_kerykeion_celestial_points):
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1215
|
+
offset, row_index = _planet_grid_layout_position(i, column_thresholds)
|
|
1216
|
+
line_height = LINE_START + (row_index * LINE_STEP)
|
|
1217
|
+
|
|
1218
|
+
decoded_name = get_decoded_kerykeion_celestial_point_name(
|
|
1219
|
+
planet["name"],
|
|
1220
|
+
celestial_point_language,
|
|
1221
|
+
)
|
|
910
1222
|
|
|
911
|
-
decoded_name = get_decoded_kerykeion_celestial_point_name(planet["name"], celestial_point_language)
|
|
912
1223
|
svg_output += (
|
|
913
|
-
f'<g transform="translate({offset},{line_height})">'
|
|
1224
|
+
f'<g transform="translate({offset},{BASE_Y + line_height})">'
|
|
914
1225
|
f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{decoded_name}</text>'
|
|
915
1226
|
f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{planet["name"]}" /></g>'
|
|
916
1227
|
f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(planet["position"])}</text>'
|
|
@@ -921,47 +1232,94 @@ def draw_planet_grid(
|
|
|
921
1232
|
svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
|
|
922
1233
|
|
|
923
1234
|
svg_output += end_of_line
|
|
924
|
-
line_height += offset_between_lines
|
|
925
1235
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
raise KerykeionException("second_subject_available_kerykeion_celestial_points is None")
|
|
1236
|
+
# Close the wrapper group
|
|
1237
|
+
svg_output += "</g>"
|
|
929
1238
|
|
|
930
|
-
|
|
931
|
-
svg_output += (
|
|
932
|
-
f'<g transform="translate(320, -15)">'
|
|
933
|
-
f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{second_subject_name}:</text>'
|
|
934
|
-
)
|
|
935
|
-
else:
|
|
936
|
-
svg_output += (
|
|
937
|
-
f'<g transform="translate(380, -15)">'
|
|
938
|
-
f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}:</text>'
|
|
939
|
-
)
|
|
1239
|
+
return svg_output
|
|
940
1240
|
|
|
941
|
-
svg_output += end_of_line
|
|
942
1241
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
|
|
956
|
-
f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(t_planet["position"])}</text>'
|
|
957
|
-
f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{t_planet["sign"]}" /></g>'
|
|
958
|
-
)
|
|
1242
|
+
def draw_secondary_planet_grid(
|
|
1243
|
+
planets_and_houses_grid_title: str,
|
|
1244
|
+
second_subject_name: str,
|
|
1245
|
+
second_subject_available_kerykeion_celestial_points: list[KerykeionPointModel],
|
|
1246
|
+
chart_type: ChartType,
|
|
1247
|
+
celestial_point_language: KerykeionLanguageCelestialPointModel,
|
|
1248
|
+
text_color: str = "#000000",
|
|
1249
|
+
x_position: int = 910,
|
|
1250
|
+
y_position: int = 0,
|
|
1251
|
+
) -> str:
|
|
1252
|
+
"""
|
|
1253
|
+
Draw the planet grid for the secondary subject and its title.
|
|
959
1254
|
|
|
960
|
-
|
|
961
|
-
|
|
1255
|
+
The entire output is wrapped in a single SVG group `<g>` so the
|
|
1256
|
+
whole block can be repositioned by changing the group transform.
|
|
962
1257
|
|
|
963
|
-
|
|
964
|
-
|
|
1258
|
+
Args:
|
|
1259
|
+
planets_and_houses_grid_title: Title prefix (used except for Transit charts).
|
|
1260
|
+
second_subject_name: Name of the secondary subject.
|
|
1261
|
+
second_subject_available_kerykeion_celestial_points: Celestial points to render for the secondary subject.
|
|
1262
|
+
chart_type: Chart type identifier (Literal string).
|
|
1263
|
+
celestial_point_language: Language model for celestial point decoding.
|
|
1264
|
+
text_color: Text color for labels (default: "#000000").
|
|
1265
|
+
x_position: X translation applied to the outer `<g>` (default: 870).
|
|
1266
|
+
y_position: Y translation applied to the outer `<g>` (default: 0).
|
|
1267
|
+
|
|
1268
|
+
Returns:
|
|
1269
|
+
SVG string for the secondary planet grid wrapped in a `<g>`.
|
|
1270
|
+
"""
|
|
1271
|
+
# Layout constants
|
|
1272
|
+
BASE_Y = 30
|
|
1273
|
+
HEADER_Y = 15
|
|
1274
|
+
LINE_START = 10
|
|
1275
|
+
LINE_STEP = 14
|
|
1276
|
+
|
|
1277
|
+
# Open wrapper group
|
|
1278
|
+
svg_output = f'<g transform="translate({x_position},{y_position})">'
|
|
1279
|
+
|
|
1280
|
+
# Title content and its relative x offset
|
|
1281
|
+
header_text = (
|
|
1282
|
+
second_subject_name if chart_type == "Transit"
|
|
1283
|
+
else f"{planets_and_houses_grid_title} {second_subject_name}"
|
|
1284
|
+
)
|
|
1285
|
+
header_x_offset = -50 if chart_type == "Transit" else 0
|
|
1286
|
+
|
|
1287
|
+
svg_output += (
|
|
1288
|
+
f'<g transform="translate({header_x_offset}, {HEADER_Y})">'
|
|
1289
|
+
f'<text style="fill:{text_color}; font-size: 14px;">{header_text}</text>'
|
|
1290
|
+
f'</g>'
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
# Grid rows
|
|
1294
|
+
line_height = LINE_START
|
|
1295
|
+
end_of_line = "</g>"
|
|
1296
|
+
|
|
1297
|
+
column_thresholds = _select_planet_grid_thresholds(chart_type)
|
|
1298
|
+
|
|
1299
|
+
for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
|
|
1300
|
+
offset, row_index = _planet_grid_layout_position(i, column_thresholds)
|
|
1301
|
+
line_height = LINE_START + (row_index * LINE_STEP)
|
|
1302
|
+
|
|
1303
|
+
second_decoded_name = get_decoded_kerykeion_celestial_point_name(
|
|
1304
|
+
t_planet["name"],
|
|
1305
|
+
celestial_point_language,
|
|
1306
|
+
)
|
|
1307
|
+
svg_output += (
|
|
1308
|
+
f'<g transform="translate({offset},{BASE_Y + line_height})">'
|
|
1309
|
+
f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{second_decoded_name}</text>'
|
|
1310
|
+
f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
|
|
1311
|
+
f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(t_planet["position"])}</text>'
|
|
1312
|
+
f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{t_planet["sign"]}" /></g>'
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
if t_planet["retrograde"]:
|
|
1316
|
+
svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
|
|
1317
|
+
|
|
1318
|
+
svg_output += end_of_line
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
# Close wrapper group
|
|
1322
|
+
svg_output += "</g>"
|
|
965
1323
|
|
|
966
1324
|
return svg_output
|
|
967
1325
|
|
|
@@ -994,7 +1352,7 @@ def draw_transit_aspect_grid(
|
|
|
994
1352
|
y_start = y_indent
|
|
995
1353
|
|
|
996
1354
|
# Filter active planets
|
|
997
|
-
active_planets = [planet for planet in available_planets if planet
|
|
1355
|
+
active_planets = [planet for planet in available_planets if planet["is_active"]]
|
|
998
1356
|
|
|
999
1357
|
# Reverse the list of active planets for the first iteration
|
|
1000
1358
|
reversed_planets = active_planets[::-1]
|
|
@@ -1040,3 +1398,416 @@ def draw_transit_aspect_grid(
|
|
|
1040
1398
|
svg_output += f'<use x="{x_aspect - box_size + 1}" y="{y_aspect + 1}" xlink:href="#orb{aspect["aspect_degrees"]}" />'
|
|
1041
1399
|
|
|
1042
1400
|
return svg_output
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
def format_location_string(location: str, max_length: int = 35) -> str:
|
|
1404
|
+
"""
|
|
1405
|
+
Format a location string to ensure it fits within a specified maximum length.
|
|
1406
|
+
|
|
1407
|
+
If the location is longer than max_length, it attempts to shorten by using only
|
|
1408
|
+
the first and last parts separated by commas. If still too long, it truncates
|
|
1409
|
+
and adds ellipsis.
|
|
1410
|
+
|
|
1411
|
+
Args:
|
|
1412
|
+
location: The original location string
|
|
1413
|
+
max_length: Maximum allowed length for the output string (default: 35)
|
|
1414
|
+
|
|
1415
|
+
Returns:
|
|
1416
|
+
Formatted location string that fits within max_length
|
|
1417
|
+
"""
|
|
1418
|
+
if len(location) > max_length:
|
|
1419
|
+
split_location = location.split(",")
|
|
1420
|
+
if len(split_location) > 1:
|
|
1421
|
+
shortened = split_location[0] + ", " + split_location[-1]
|
|
1422
|
+
if len(shortened) > max_length:
|
|
1423
|
+
return shortened[:max_length] + "..."
|
|
1424
|
+
return shortened
|
|
1425
|
+
else:
|
|
1426
|
+
return location[:max_length] + "..."
|
|
1427
|
+
return location
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def format_datetime_with_timezone(iso_datetime_string: str) -> str:
|
|
1431
|
+
"""
|
|
1432
|
+
Format an ISO datetime string with a custom format that includes properly formatted timezone.
|
|
1433
|
+
|
|
1434
|
+
Args:
|
|
1435
|
+
iso_datetime_string: ISO formatted datetime string
|
|
1436
|
+
|
|
1437
|
+
Returns:
|
|
1438
|
+
Formatted datetime string with properly formatted timezone offset (HH:MM)
|
|
1439
|
+
"""
|
|
1440
|
+
dt = datetime.datetime.fromisoformat(iso_datetime_string)
|
|
1441
|
+
custom_format = dt.strftime('%Y-%m-%d %H:%M [%z]')
|
|
1442
|
+
custom_format = custom_format[:-3] + ':' + custom_format[-3:]
|
|
1443
|
+
|
|
1444
|
+
return custom_format
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def calculate_element_points(
|
|
1448
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1449
|
+
celestial_points_names: Sequence[str],
|
|
1450
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
1451
|
+
*,
|
|
1452
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1453
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1454
|
+
) -> dict[str, float]:
|
|
1455
|
+
"""
|
|
1456
|
+
Calculate elemental totals for a subject using the selected strategy.
|
|
1457
|
+
|
|
1458
|
+
Args:
|
|
1459
|
+
planets_settings: Planet configuration list (kept for API compatibility).
|
|
1460
|
+
celestial_points_names: Celestial point names to include.
|
|
1461
|
+
subject: Astrological subject with planetary data.
|
|
1462
|
+
method: Calculation method (pure_count or weighted). Defaults to weighted.
|
|
1463
|
+
custom_weights: Optional overrides for point weights keyed by name.
|
|
1464
|
+
|
|
1465
|
+
Returns:
|
|
1466
|
+
Dictionary mapping each element to its accumulated total.
|
|
1467
|
+
"""
|
|
1468
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1469
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1470
|
+
|
|
1471
|
+
return _calculate_distribution_for_subject(
|
|
1472
|
+
subject,
|
|
1473
|
+
normalized_names,
|
|
1474
|
+
_SIGN_TO_ELEMENT,
|
|
1475
|
+
_ELEMENT_KEYS,
|
|
1476
|
+
weight_lookup,
|
|
1477
|
+
fallback_weight,
|
|
1478
|
+
)
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
def calculate_synastry_element_points(
|
|
1482
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1483
|
+
celestial_points_names: Sequence[str],
|
|
1484
|
+
subject1: AstrologicalSubjectModel,
|
|
1485
|
+
subject2: AstrologicalSubjectModel,
|
|
1486
|
+
*,
|
|
1487
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1488
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1489
|
+
) -> dict[str, float]:
|
|
1490
|
+
"""
|
|
1491
|
+
Calculate combined element percentages for a synastry chart.
|
|
1492
|
+
|
|
1493
|
+
Args:
|
|
1494
|
+
planets_settings: Planet configuration list (unused but preserved).
|
|
1495
|
+
celestial_points_names: Celestial point names to process.
|
|
1496
|
+
subject1: First astrological subject.
|
|
1497
|
+
subject2: Second astrological subject.
|
|
1498
|
+
method: Calculation strategy (pure_count or weighted).
|
|
1499
|
+
custom_weights: Optional overrides for point weights.
|
|
1500
|
+
|
|
1501
|
+
Returns:
|
|
1502
|
+
Dictionary with element percentages summing to 100.
|
|
1503
|
+
"""
|
|
1504
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1505
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1506
|
+
|
|
1507
|
+
subject1_totals = _calculate_distribution_for_subject(
|
|
1508
|
+
subject1,
|
|
1509
|
+
normalized_names,
|
|
1510
|
+
_SIGN_TO_ELEMENT,
|
|
1511
|
+
_ELEMENT_KEYS,
|
|
1512
|
+
weight_lookup,
|
|
1513
|
+
fallback_weight,
|
|
1514
|
+
)
|
|
1515
|
+
subject2_totals = _calculate_distribution_for_subject(
|
|
1516
|
+
subject2,
|
|
1517
|
+
normalized_names,
|
|
1518
|
+
_SIGN_TO_ELEMENT,
|
|
1519
|
+
_ELEMENT_KEYS,
|
|
1520
|
+
weight_lookup,
|
|
1521
|
+
fallback_weight,
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
combined_totals = {key: subject1_totals[key] + subject2_totals[key] for key in _ELEMENT_KEYS}
|
|
1525
|
+
total_points = sum(combined_totals.values())
|
|
1526
|
+
|
|
1527
|
+
if total_points == 0:
|
|
1528
|
+
return {key: 0.0 for key in _ELEMENT_KEYS}
|
|
1529
|
+
|
|
1530
|
+
return {key: (combined_totals[key] / total_points) * 100.0 for key in _ELEMENT_KEYS}
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
def draw_house_comparison_grid(
|
|
1534
|
+
house_comparison: "HouseComparisonModel",
|
|
1535
|
+
celestial_point_language: KerykeionLanguageCelestialPointModel,
|
|
1536
|
+
active_points: list[AstrologicalPoint],
|
|
1537
|
+
*,
|
|
1538
|
+
points_owner_subject_number: Literal[1, 2] = 1,
|
|
1539
|
+
text_color: str = "var(--kerykeion-color-neutral-content)",
|
|
1540
|
+
house_position_comparison_label: str = "House Position Comparison",
|
|
1541
|
+
return_point_label: str = "Return Point",
|
|
1542
|
+
return_label: str = "DualReturnChart",
|
|
1543
|
+
radix_label: str = "Radix",
|
|
1544
|
+
x_position: int = 1100,
|
|
1545
|
+
y_position: int = 0,
|
|
1546
|
+
) -> str:
|
|
1547
|
+
"""
|
|
1548
|
+
Generate SVG code for displaying a comparison of points across houses between two charts.
|
|
1549
|
+
|
|
1550
|
+
Parameters:
|
|
1551
|
+
- house_comparison ("HouseComparisonModel"): Model containing house comparison data,
|
|
1552
|
+
including first_subject_name, second_subject_name, and points in houses.
|
|
1553
|
+
- celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points
|
|
1554
|
+
- active_celestial_points (list[KerykeionPointModel]): List of active celestial points to display
|
|
1555
|
+
- text_color (str): Color for the text elements
|
|
1556
|
+
|
|
1557
|
+
Returns:
|
|
1558
|
+
- str: SVG code for the house comparison grid.
|
|
1559
|
+
"""
|
|
1560
|
+
if points_owner_subject_number == 1:
|
|
1561
|
+
comparison_data = house_comparison.first_points_in_second_houses
|
|
1562
|
+
else:
|
|
1563
|
+
comparison_data = house_comparison.second_points_in_first_houses
|
|
1564
|
+
|
|
1565
|
+
svg_output = f'<g transform="translate({x_position},{y_position})">'
|
|
1566
|
+
|
|
1567
|
+
# Add title
|
|
1568
|
+
svg_output += f'<text text-anchor="start" x="0" y="-15" style="fill:{text_color}; font-size: 14px;">{house_position_comparison_label}</text>'
|
|
1569
|
+
|
|
1570
|
+
# Add column headers
|
|
1571
|
+
line_increment = 10
|
|
1572
|
+
svg_output += (
|
|
1573
|
+
f'<g transform="translate(0,{line_increment})">'
|
|
1574
|
+
f'<text text-anchor="start" x="0" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{return_point_label}</text>'
|
|
1575
|
+
f'<text text-anchor="start" x="77" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{return_label}</text>'
|
|
1576
|
+
f'<text text-anchor="start" x="132" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{radix_label}</text>'
|
|
1577
|
+
f'</g>'
|
|
1578
|
+
)
|
|
1579
|
+
line_increment += 15
|
|
1580
|
+
|
|
1581
|
+
# Create a dictionary to store all points by name for combined display
|
|
1582
|
+
all_points_by_name = {}
|
|
1583
|
+
|
|
1584
|
+
for point in comparison_data:
|
|
1585
|
+
# Only process points that are active
|
|
1586
|
+
if point.point_name in active_points and point.point_name not in all_points_by_name:
|
|
1587
|
+
all_points_by_name[point.point_name] = {
|
|
1588
|
+
"name": point.point_name,
|
|
1589
|
+
"secondary_house": point.projected_house_number,
|
|
1590
|
+
"native_house": point.point_owner_house_number
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
# Display all points organized by name
|
|
1594
|
+
for name, point_data in all_points_by_name.items():
|
|
1595
|
+
native_house = point_data.get("native_house", "-")
|
|
1596
|
+
secondary_house = point_data.get("secondary_house", "-")
|
|
1597
|
+
|
|
1598
|
+
svg_output += (
|
|
1599
|
+
f'<g transform="translate(0,{line_increment})">'
|
|
1600
|
+
f'<g transform="translate(0,-9)"><use transform="scale(0.4)" xlink:href="#{name}" /></g>'
|
|
1601
|
+
f'<text text-anchor="start" x="15" style="fill:{text_color}; font-size: 10px;">{get_decoded_kerykeion_celestial_point_name(name, celestial_point_language)}</text>'
|
|
1602
|
+
f'<text text-anchor="start" x="90" style="fill:{text_color}; font-size: 10px;">{native_house}</text>'
|
|
1603
|
+
f'<text text-anchor="start" x="140" style="fill:{text_color}; font-size: 10px;">{secondary_house}</text>'
|
|
1604
|
+
f'</g>'
|
|
1605
|
+
)
|
|
1606
|
+
line_increment += 12
|
|
1607
|
+
|
|
1608
|
+
svg_output += "</g>"
|
|
1609
|
+
|
|
1610
|
+
return svg_output
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
def draw_single_house_comparison_grid(
|
|
1614
|
+
house_comparison: "HouseComparisonModel",
|
|
1615
|
+
celestial_point_language: KerykeionLanguageCelestialPointModel,
|
|
1616
|
+
active_points: list[AstrologicalPoint],
|
|
1617
|
+
*,
|
|
1618
|
+
points_owner_subject_number: Literal[1, 2] = 1,
|
|
1619
|
+
text_color: str = "var(--kerykeion-color-neutral-content)",
|
|
1620
|
+
house_position_comparison_label: str = "House Position Comparison",
|
|
1621
|
+
return_point_label: str = "Return Point",
|
|
1622
|
+
natal_house_label: str = "Natal House",
|
|
1623
|
+
x_position: int = 1030,
|
|
1624
|
+
y_position: int = 0,
|
|
1625
|
+
) -> str:
|
|
1626
|
+
"""
|
|
1627
|
+
Generate SVG code for displaying celestial points and their house positions.
|
|
1628
|
+
|
|
1629
|
+
Parameters:
|
|
1630
|
+
- house_comparison ("HouseComparisonModel"): Model containing house comparison data,
|
|
1631
|
+
including first_subject_name, second_subject_name, and points in houses.
|
|
1632
|
+
- celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points
|
|
1633
|
+
- active_points (list[AstrologicalPoint]): List of active celestial points to display
|
|
1634
|
+
- points_owner_subject_number (Literal[1, 2]): Which subject's points to display (1 for first, 2 for second)
|
|
1635
|
+
- text_color (str): Color for the text elements
|
|
1636
|
+
- house_position_comparison_label (str): Label for the house position comparison grid
|
|
1637
|
+
- return_point_label (str): Label for the return point column
|
|
1638
|
+
- house_position_label (str): Label for the house position column
|
|
1639
|
+
- x_position (int): X position for the grid
|
|
1640
|
+
- y_position (int): Y position for the grid
|
|
1641
|
+
|
|
1642
|
+
Returns:
|
|
1643
|
+
- str: SVG code for the house position grid.
|
|
1644
|
+
"""
|
|
1645
|
+
if points_owner_subject_number == 1:
|
|
1646
|
+
comparison_data = house_comparison.first_points_in_second_houses
|
|
1647
|
+
else:
|
|
1648
|
+
comparison_data = house_comparison.second_points_in_first_houses
|
|
1649
|
+
|
|
1650
|
+
svg_output = f'<g transform="translate({x_position},{y_position})">'
|
|
1651
|
+
|
|
1652
|
+
# Add title
|
|
1653
|
+
svg_output += f'<text text-anchor="start" x="0" y="-15" style="fill:{text_color}; font-size: 14px;">{house_position_comparison_label}</text>'
|
|
1654
|
+
|
|
1655
|
+
# Add column headers
|
|
1656
|
+
line_increment = 10
|
|
1657
|
+
svg_output += (
|
|
1658
|
+
f'<g transform="translate(0,{line_increment})">'
|
|
1659
|
+
f'<text text-anchor="start" x="0" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{return_point_label}</text>'
|
|
1660
|
+
f'<text text-anchor="start" x="77" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{natal_house_label}</text>'
|
|
1661
|
+
f'</g>'
|
|
1662
|
+
)
|
|
1663
|
+
line_increment += 15
|
|
1664
|
+
|
|
1665
|
+
# Create a dictionary to store all points by name for combined display
|
|
1666
|
+
all_points_by_name = {}
|
|
1667
|
+
|
|
1668
|
+
for point in comparison_data:
|
|
1669
|
+
# Only process points that are active
|
|
1670
|
+
if point.point_name in active_points and point.point_name not in all_points_by_name:
|
|
1671
|
+
all_points_by_name[point.point_name] = {
|
|
1672
|
+
"name": point.point_name,
|
|
1673
|
+
"house": point.projected_house_number
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
# Display all points organized by name
|
|
1677
|
+
for name, point_data in all_points_by_name.items():
|
|
1678
|
+
house = point_data.get("house", "-")
|
|
1679
|
+
|
|
1680
|
+
svg_output += (
|
|
1681
|
+
f'<g transform="translate(0,{line_increment})">'
|
|
1682
|
+
f'<g transform="translate(0,-9)"><use transform="scale(0.4)" xlink:href="#{name}" /></g>'
|
|
1683
|
+
f'<text text-anchor="start" x="15" style="fill:{text_color}; font-size: 10px;">{get_decoded_kerykeion_celestial_point_name(name, celestial_point_language)}</text>'
|
|
1684
|
+
f'<text text-anchor="start" x="90" style="fill:{text_color}; font-size: 10px;">{house}</text>'
|
|
1685
|
+
f'</g>'
|
|
1686
|
+
)
|
|
1687
|
+
line_increment += 12
|
|
1688
|
+
|
|
1689
|
+
svg_output += "</g>"
|
|
1690
|
+
|
|
1691
|
+
return svg_output
|
|
1692
|
+
|
|
1693
|
+
|
|
1694
|
+
def makeLunarPhase(degrees_between_sun_and_moon: float, latitude: float) -> str:
|
|
1695
|
+
"""
|
|
1696
|
+
Generate SVG representation of lunar phase.
|
|
1697
|
+
|
|
1698
|
+
Parameters:
|
|
1699
|
+
- degrees_between_sun_and_moon (float): Angle between sun and moon in degrees
|
|
1700
|
+
- latitude (float): Observer's latitude for correct orientation
|
|
1701
|
+
|
|
1702
|
+
Returns:
|
|
1703
|
+
- str: SVG representation of lunar phase
|
|
1704
|
+
"""
|
|
1705
|
+
# Calculate parameters for the lunar phase visualization
|
|
1706
|
+
params = calculate_moon_phase_chart_params(degrees_between_sun_and_moon, latitude)
|
|
1707
|
+
|
|
1708
|
+
# Extract the calculated values
|
|
1709
|
+
lunar_phase_circle_center_x = params["circle_center_x"]
|
|
1710
|
+
lunar_phase_circle_radius = params["circle_radius"]
|
|
1711
|
+
lunar_phase_rotate = params["lunar_phase_rotate"]
|
|
1712
|
+
|
|
1713
|
+
# Generate the SVG for the lunar phase
|
|
1714
|
+
svg = (
|
|
1715
|
+
f'<g transform="rotate({lunar_phase_rotate} 20 10)">\n'
|
|
1716
|
+
f' <defs>\n'
|
|
1717
|
+
f' <clipPath id="moonPhaseCutOffCircle">\n'
|
|
1718
|
+
f' <circle cx="20" cy="10" r="10" />\n'
|
|
1719
|
+
f' </clipPath>\n'
|
|
1720
|
+
f' </defs>\n'
|
|
1721
|
+
f' <circle cx="20" cy="10" r="10" style="fill: var(--kerykeion-chart-color-lunar-phase-0)" />\n'
|
|
1722
|
+
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'
|
|
1723
|
+
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'
|
|
1724
|
+
f'</g>'
|
|
1725
|
+
)
|
|
1726
|
+
|
|
1727
|
+
return svg
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def calculate_quality_points(
|
|
1731
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1732
|
+
celestial_points_names: Sequence[str],
|
|
1733
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
1734
|
+
*,
|
|
1735
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1736
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1737
|
+
) -> dict[str, float]:
|
|
1738
|
+
"""
|
|
1739
|
+
Calculate modality totals for a subject using the selected strategy.
|
|
1740
|
+
|
|
1741
|
+
Args:
|
|
1742
|
+
planets_settings: Planet configuration list (kept for API compatibility).
|
|
1743
|
+
celestial_points_names: Celestial point names to include.
|
|
1744
|
+
subject: Astrological subject with planetary data.
|
|
1745
|
+
method: Calculation method (pure_count or weighted). Defaults to weighted.
|
|
1746
|
+
custom_weights: Optional overrides for point weights keyed by name.
|
|
1747
|
+
|
|
1748
|
+
Returns:
|
|
1749
|
+
Dictionary mapping each modality to its accumulated total.
|
|
1750
|
+
"""
|
|
1751
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1752
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1753
|
+
|
|
1754
|
+
return _calculate_distribution_for_subject(
|
|
1755
|
+
subject,
|
|
1756
|
+
normalized_names,
|
|
1757
|
+
_SIGN_TO_QUALITY,
|
|
1758
|
+
_QUALITY_KEYS,
|
|
1759
|
+
weight_lookup,
|
|
1760
|
+
fallback_weight,
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
def calculate_synastry_quality_points(
|
|
1765
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1766
|
+
celestial_points_names: Sequence[str],
|
|
1767
|
+
subject1: AstrologicalSubjectModel,
|
|
1768
|
+
subject2: AstrologicalSubjectModel,
|
|
1769
|
+
*,
|
|
1770
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1771
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1772
|
+
) -> dict[str, float]:
|
|
1773
|
+
"""
|
|
1774
|
+
Calculate combined modality percentages for a synastry chart.
|
|
1775
|
+
|
|
1776
|
+
Args:
|
|
1777
|
+
planets_settings: Planet configuration list (unused but preserved).
|
|
1778
|
+
celestial_points_names: Celestial point names to process.
|
|
1779
|
+
subject1: First astrological subject.
|
|
1780
|
+
subject2: Second astrological subject.
|
|
1781
|
+
method: Calculation strategy (pure_count or weighted).
|
|
1782
|
+
custom_weights: Optional overrides for point weights.
|
|
1783
|
+
|
|
1784
|
+
Returns:
|
|
1785
|
+
Dictionary with modality percentages summing to 100.
|
|
1786
|
+
"""
|
|
1787
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1788
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1789
|
+
|
|
1790
|
+
subject1_totals = _calculate_distribution_for_subject(
|
|
1791
|
+
subject1,
|
|
1792
|
+
normalized_names,
|
|
1793
|
+
_SIGN_TO_QUALITY,
|
|
1794
|
+
_QUALITY_KEYS,
|
|
1795
|
+
weight_lookup,
|
|
1796
|
+
fallback_weight,
|
|
1797
|
+
)
|
|
1798
|
+
subject2_totals = _calculate_distribution_for_subject(
|
|
1799
|
+
subject2,
|
|
1800
|
+
normalized_names,
|
|
1801
|
+
_SIGN_TO_QUALITY,
|
|
1802
|
+
_QUALITY_KEYS,
|
|
1803
|
+
weight_lookup,
|
|
1804
|
+
fallback_weight,
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
combined_totals = {key: subject1_totals[key] + subject2_totals[key] for key in _QUALITY_KEYS}
|
|
1808
|
+
total_points = sum(combined_totals.values())
|
|
1809
|
+
|
|
1810
|
+
if total_points == 0:
|
|
1811
|
+
return {key: 0.0 for key in _QUALITY_KEYS}
|
|
1812
|
+
|
|
1813
|
+
return {key: (combined_totals[key] / total_points) * 100.0 for key in _QUALITY_KEYS}
|