kerykeion 5.0.0a12__py3-none-any.whl → 5.0.0b1__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 +30 -6
- kerykeion/aspects/aspects_factory.py +40 -24
- kerykeion/aspects/aspects_utils.py +75 -6
- kerykeion/astrological_subject_factory.py +377 -226
- kerykeion/backword.py +680 -0
- kerykeion/chart_data_factory.py +484 -0
- kerykeion/charts/{kerykeion_chart_svg.py → chart_drawer.py} +612 -438
- kerykeion/charts/charts_utils.py +135 -94
- kerykeion/charts/draw_planets.py +38 -28
- kerykeion/charts/templates/aspect_grid_only.xml +188 -17
- kerykeion/charts/templates/chart.xml +104 -28
- kerykeion/charts/templates/wheel_only.xml +195 -24
- 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 +4 -4
- kerykeion/ephemeris_data_factory.py +12 -9
- kerykeion/house_comparison/__init__.py +0 -3
- kerykeion/house_comparison/house_comparison_factory.py +3 -3
- kerykeion/house_comparison/house_comparison_utils.py +3 -4
- kerykeion/planetary_return_factory.py +8 -4
- kerykeion/relationship_score_factory.py +3 -3
- kerykeion/report.py +748 -67
- kerykeion/{kr_types → schemas}/__init__.py +44 -4
- kerykeion/schemas/chart_template_model.py +340 -0
- kerykeion/{kr_types → schemas}/kr_literals.py +7 -3
- kerykeion/{kr_types → schemas}/kr_models.py +220 -11
- kerykeion/{kr_types → schemas}/settings_models.py +7 -7
- kerykeion/settings/config_constants.py +75 -8
- kerykeion/settings/kerykeion_settings.py +1 -1
- kerykeion/settings/kr.config.json +130 -40
- kerykeion/settings/legacy/legacy_celestial_points_settings.py +8 -8
- 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/transits_time_range_factory.py +7 -7
- kerykeion/utilities.py +61 -38
- {kerykeion-5.0.0a12.dist-info → kerykeion-5.0.0b1.dist-info}/METADATA +507 -120
- kerykeion-5.0.0b1.dist-info/RECORD +58 -0
- kerykeion/house_comparison/house_comparison_models.py +0 -76
- kerykeion/kr_types/chart_types.py +0 -106
- kerykeion-5.0.0a12.dist-info/RECORD +0 -50
- /kerykeion/{kr_types → schemas}/kerykeion_exception.py +0 -0
- {kerykeion-5.0.0a12.dist-info → kerykeion-5.0.0b1.dist-info}/WHEEL +0 -0
- {kerykeion-5.0.0a12.dist-info → kerykeion-5.0.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,33 +6,34 @@
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
import swisseph as swe
|
|
9
|
-
from typing import get_args, Union,
|
|
9
|
+
from typing import get_args, Union, Any
|
|
10
10
|
|
|
11
|
+
|
|
12
|
+
from kerykeion.schemas.kr_models import ChartDataModel
|
|
13
|
+
from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
|
|
11
14
|
from kerykeion.settings.kerykeion_settings import get_settings
|
|
12
|
-
from kerykeion.aspects import AspectsFactory
|
|
13
15
|
from kerykeion.house_comparison.house_comparison_factory import HouseComparisonFactory
|
|
14
|
-
from kerykeion.
|
|
16
|
+
from kerykeion.schemas import (
|
|
15
17
|
KerykeionException,
|
|
16
18
|
ChartType,
|
|
17
19
|
Sign,
|
|
18
20
|
ActiveAspect,
|
|
19
21
|
)
|
|
20
|
-
from kerykeion.
|
|
21
|
-
from kerykeion.
|
|
22
|
+
from kerykeion.schemas import ChartTemplateModel
|
|
23
|
+
from kerykeion.schemas.kr_models import (
|
|
22
24
|
AstrologicalSubjectModel,
|
|
23
25
|
CompositeSubjectModel,
|
|
24
26
|
PlanetReturnModel,
|
|
25
27
|
)
|
|
26
|
-
from kerykeion.
|
|
28
|
+
from kerykeion.schemas.settings_models import (
|
|
27
29
|
KerykeionSettingsCelestialPointModel,
|
|
28
30
|
KerykeionSettingsModel,
|
|
29
31
|
)
|
|
30
|
-
from kerykeion.
|
|
32
|
+
from kerykeion.schemas.kr_literals import (
|
|
31
33
|
KerykeionChartTheme,
|
|
32
34
|
KerykeionChartLanguage,
|
|
33
35
|
AstrologicalPoint,
|
|
34
36
|
)
|
|
35
|
-
from kerykeion.utilities import find_common_active_points
|
|
36
37
|
from kerykeion.charts.charts_utils import (
|
|
37
38
|
draw_zodiac_slice,
|
|
38
39
|
convert_latitude_coordinate_to_string,
|
|
@@ -57,15 +58,10 @@ from kerykeion.charts.charts_utils import (
|
|
|
57
58
|
draw_main_planet_grid,
|
|
58
59
|
draw_secondary_planet_grid,
|
|
59
60
|
format_location_string,
|
|
60
|
-
format_datetime_with_timezone
|
|
61
|
-
calculate_element_points,
|
|
62
|
-
calculate_synastry_element_points,
|
|
63
|
-
calculate_quality_points,
|
|
64
|
-
calculate_synastry_quality_points
|
|
61
|
+
format_datetime_with_timezone
|
|
65
62
|
)
|
|
66
63
|
from kerykeion.charts.draw_planets import draw_planets
|
|
67
|
-
from kerykeion.utilities import get_houses_list, inline_css_variables_in_svg
|
|
68
|
-
from kerykeion.settings.config_constants import DEFAULT_ACTIVE_ASPECTS
|
|
64
|
+
from kerykeion.utilities import get_houses_list, inline_css_variables_in_svg, distribute_percentages_to_100
|
|
69
65
|
from kerykeion.settings.legacy.legacy_color_settings import DEFAULT_CHART_COLORS
|
|
70
66
|
from kerykeion.settings.legacy.legacy_celestial_points_settings import DEFAULT_CELESTIAL_POINTS_SETTINGS
|
|
71
67
|
from kerykeion.settings.legacy.legacy_chart_aspects_settings import DEFAULT_CHART_ASPECTS_SETTINGS
|
|
@@ -76,30 +72,32 @@ from typing import List, Literal
|
|
|
76
72
|
from datetime import datetime
|
|
77
73
|
|
|
78
74
|
|
|
79
|
-
class
|
|
75
|
+
class ChartDrawer:
|
|
80
76
|
"""
|
|
81
|
-
|
|
77
|
+
ChartDrawer generates astrological chart visualizations as SVG files from pre-computed chart data.
|
|
78
|
+
|
|
79
|
+
This class is designed for pure visualization and requires chart data to be pre-computed using
|
|
80
|
+
ChartDataFactory. This separation ensures clean architecture where ChartDataFactory handles
|
|
81
|
+
all calculations (aspects, element/quality distributions, subjects) while ChartDrawer focuses
|
|
82
|
+
solely on rendering SVG visualizations.
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
for various chart types including Natal,
|
|
84
|
+
ChartDrawer supports creating full chart SVGs, wheel-only SVGs, and aspect-grid-only SVGs
|
|
85
|
+
for various chart types including Natal, Transit, Synastry, and Composite.
|
|
85
86
|
Charts are rendered using XML templates and drawing utilities, with customizable themes,
|
|
86
|
-
language,
|
|
87
|
-
|
|
87
|
+
language, and visual settings.
|
|
88
|
+
|
|
89
|
+
The generated SVG files are optimized for web use and can be saved to any specified
|
|
90
|
+
destination path using the save_svg method.
|
|
88
91
|
|
|
89
92
|
NOTE:
|
|
90
93
|
The generated SVG files are optimized for web use, opening in browsers. If you want to
|
|
91
94
|
use them in other applications, you might need to adjust the SVG settings or styles.
|
|
92
95
|
|
|
93
96
|
Args:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Defaults to 'Natal'.
|
|
99
|
-
second_obj (AstrologicalSubject | AstrologicalSubjectModel, optional):
|
|
100
|
-
The secondary subject for Transit or Synastry charts. Not required for Natal or Composite.
|
|
101
|
-
new_output_directory (str | Path, optional):
|
|
102
|
-
Directory to write generated SVG files. Defaults to the user's home directory.
|
|
97
|
+
chart_data (ChartDataModel):
|
|
98
|
+
Pre-computed chart data from ChartDataFactory containing all subjects, aspects,
|
|
99
|
+
element/quality distributions, and other analytical data. This is the ONLY source
|
|
100
|
+
of chart information - no calculations are performed by ChartDrawer.
|
|
103
101
|
new_settings_file (Path | dict | KerykeionSettingsModel, optional):
|
|
104
102
|
Path or settings object to override default chart configuration (colors, fonts, aspects).
|
|
105
103
|
theme (KerykeionChartTheme, optional):
|
|
@@ -108,55 +106,60 @@ class KerykeionChartSVG:
|
|
|
108
106
|
Specifies rendering style for double-chart aspect grids. Defaults to 'list'.
|
|
109
107
|
chart_language (KerykeionChartLanguage, optional):
|
|
110
108
|
Language code for chart labels. Defaults to 'EN'.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
Example:
|
|
114
|
-
["Sun", "Moon", "Mercury", "Venus"]
|
|
115
|
-
|
|
116
|
-
active_aspects (list[ActiveAspect], optional):
|
|
117
|
-
List of aspects (name and orb) to calculate. Defaults to DEFAULT_ACTIVE_ASPECTS.
|
|
118
|
-
Example:
|
|
119
|
-
[
|
|
120
|
-
{"name": "conjunction", "orb": 10},
|
|
121
|
-
{"name": "opposition", "orb": 10},
|
|
122
|
-
{"name": "trine", "orb": 8},
|
|
123
|
-
{"name": "sextile", "orb": 6},
|
|
124
|
-
{"name": "square", "orb": 5},
|
|
125
|
-
{"name": "quintile", "orb": 1},
|
|
126
|
-
]
|
|
109
|
+
transparent_background (bool, optional):
|
|
110
|
+
Whether to use a transparent background instead of the theme color. Defaults to False.
|
|
127
111
|
|
|
128
112
|
Public Methods:
|
|
129
113
|
makeTemplate(minify=False, remove_css_variables=False) -> str:
|
|
130
114
|
Render the full chart SVG as a string without writing to disk. Use `minify=True`
|
|
131
115
|
to remove whitespace and quotes, and `remove_css_variables=True` to embed CSS vars.
|
|
132
116
|
|
|
133
|
-
|
|
134
|
-
Generate and write the full chart SVG file to the
|
|
135
|
-
|
|
136
|
-
'{subject.name} - {chart_type} Chart.svg'.
|
|
117
|
+
save_svg(output_path=None, filename=None, minify=False, remove_css_variables=False) -> None:
|
|
118
|
+
Generate and write the full chart SVG file to the specified path.
|
|
119
|
+
If output_path is None, saves to the user's home directory.
|
|
120
|
+
If filename is None, uses default pattern: '{subject.name} - {chart_type} Chart.svg'.
|
|
137
121
|
|
|
138
122
|
makeWheelOnlyTemplate(minify=False, remove_css_variables=False) -> str:
|
|
139
123
|
Render only the chart wheel (no aspect grid) as an SVG string.
|
|
140
124
|
|
|
141
|
-
|
|
142
|
-
Generate and write the wheel-only SVG file
|
|
143
|
-
|
|
125
|
+
save_wheel_only_svg_file(output_path=None, filename=None, minify=False, remove_css_variables=False) -> None:
|
|
126
|
+
Generate and write the wheel-only SVG file to the specified path.
|
|
127
|
+
If output_path is None, saves to the user's home directory.
|
|
128
|
+
If filename is None, uses default pattern: '{subject.name} - {chart_type} Chart - Wheel Only.svg'.
|
|
144
129
|
|
|
145
130
|
makeAspectGridOnlyTemplate(minify=False, remove_css_variables=False) -> str:
|
|
146
131
|
Render only the aspect grid as an SVG string.
|
|
147
132
|
|
|
148
|
-
|
|
149
|
-
Generate and write the aspect-grid-only SVG file
|
|
150
|
-
|
|
133
|
+
save_aspect_grid_only_svg_file(output_path=None, filename=None, minify=False, remove_css_variables=False) -> None:
|
|
134
|
+
Generate and write the aspect-grid-only SVG file to the specified path.
|
|
135
|
+
If output_path is None, saves to the user's home directory.
|
|
136
|
+
If filename is None, uses default pattern: '{subject.name} - {chart_type} Chart - Aspect Grid Only.svg'.
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
>>> from kerykeion.astrological_subject_factory import AstrologicalSubjectFactory
|
|
140
|
+
>>> from kerykeion.chart_data_factory import ChartDataFactory
|
|
141
|
+
>>> from kerykeion.charts.chart_drawer import ChartDrawer
|
|
142
|
+
>>>
|
|
143
|
+
>>> # Step 1: Create subject
|
|
144
|
+
>>> subject = AstrologicalSubjectFactory.from_birth_data("John", 1990, 1, 1, 12, 0, "London", "GB")
|
|
145
|
+
>>>
|
|
146
|
+
>>> # Step 2: Pre-compute chart data
|
|
147
|
+
>>> chart_data = ChartDataFactory.create_natal_chart_data(subject)
|
|
148
|
+
>>>
|
|
149
|
+
>>> # Step 3: Create visualization
|
|
150
|
+
>>> chart_drawer = ChartDrawer(chart_data=chart_data, theme="classic")
|
|
151
|
+
>>> chart_drawer.save_svg() # Saves to home directory with default filename
|
|
152
|
+
>>> # Or specify custom path and filename:
|
|
153
|
+
>>> chart_drawer.save_svg("/path/to/output/directory", "my_custom_chart")
|
|
151
154
|
"""
|
|
152
155
|
|
|
153
156
|
# Constants
|
|
154
157
|
|
|
155
158
|
_DEFAULT_HEIGHT = 550
|
|
156
|
-
_DEFAULT_FULL_WIDTH =
|
|
159
|
+
_DEFAULT_FULL_WIDTH = 1250
|
|
157
160
|
_DEFAULT_NATAL_WIDTH = 870
|
|
158
|
-
_DEFAULT_FULL_WIDTH_WITH_TABLE =
|
|
159
|
-
_DEFAULT_ULTRA_WIDE_WIDTH =
|
|
161
|
+
_DEFAULT_FULL_WIDTH_WITH_TABLE = 1250
|
|
162
|
+
_DEFAULT_ULTRA_WIDE_WIDTH = 1320
|
|
160
163
|
|
|
161
164
|
_BASIC_CHART_VIEWBOX = f"0 0 {_DEFAULT_NATAL_WIDTH} {_DEFAULT_HEIGHT}"
|
|
162
165
|
_WIDE_CHART_VIEWBOX = f"0 0 {_DEFAULT_FULL_WIDTH} 546.0"
|
|
@@ -167,9 +170,6 @@ class KerykeionChartSVG:
|
|
|
167
170
|
first_obj: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel]
|
|
168
171
|
second_obj: Union[AstrologicalSubjectModel, PlanetReturnModel, None]
|
|
169
172
|
chart_type: ChartType
|
|
170
|
-
new_output_directory: Union[Path, None]
|
|
171
|
-
new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
|
|
172
|
-
output_directory: Path
|
|
173
173
|
new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
|
|
174
174
|
theme: Union[KerykeionChartTheme, None]
|
|
175
175
|
double_chart_aspect_grid_type: Literal["list", "table"]
|
|
@@ -177,6 +177,8 @@ class KerykeionChartSVG:
|
|
|
177
177
|
active_points: List[AstrologicalPoint]
|
|
178
178
|
active_aspects: List[ActiveAspect]
|
|
179
179
|
transparent_background: bool
|
|
180
|
+
external_view: bool
|
|
181
|
+
custom_title: Union[str, None]
|
|
180
182
|
|
|
181
183
|
# Internal properties
|
|
182
184
|
fire: float
|
|
@@ -189,8 +191,8 @@ class KerykeionChartSVG:
|
|
|
189
191
|
width: Union[float, int]
|
|
190
192
|
language_settings: dict
|
|
191
193
|
chart_colors_settings: dict
|
|
192
|
-
planets_settings: dict
|
|
193
|
-
aspects_settings: dict
|
|
194
|
+
planets_settings: list[dict[Any, Any]]
|
|
195
|
+
aspects_settings: list[dict[Any, Any]]
|
|
194
196
|
available_planets_setting: List[KerykeionSettingsCelestialPointModel]
|
|
195
197
|
height: float
|
|
196
198
|
location: str
|
|
@@ -200,34 +202,28 @@ class KerykeionChartSVG:
|
|
|
200
202
|
|
|
201
203
|
def __init__(
|
|
202
204
|
self,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
second_obj: Union[AstrologicalSubjectModel, PlanetReturnModel, None] = None,
|
|
206
|
-
new_output_directory: Union[str, None] = None,
|
|
205
|
+
chart_data: "ChartDataModel",
|
|
206
|
+
*,
|
|
207
207
|
new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None,
|
|
208
208
|
theme: Union[KerykeionChartTheme, None] = "classic",
|
|
209
209
|
double_chart_aspect_grid_type: Literal["list", "table"] = "list",
|
|
210
210
|
chart_language: KerykeionChartLanguage = "EN",
|
|
211
|
-
|
|
212
|
-
active_aspects: list[ActiveAspect]= DEFAULT_ACTIVE_ASPECTS,
|
|
213
|
-
*,
|
|
211
|
+
external_view: bool = False,
|
|
214
212
|
transparent_background: bool = False,
|
|
215
213
|
colors_settings: dict = DEFAULT_CHART_COLORS,
|
|
216
214
|
celestial_points_settings: list[dict] = DEFAULT_CELESTIAL_POINTS_SETTINGS,
|
|
217
215
|
aspects_settings: list[dict] = DEFAULT_CHART_ASPECTS_SETTINGS,
|
|
216
|
+
custom_title: Union[str, None] = None,
|
|
217
|
+
auto_size: bool = True,
|
|
218
|
+
padding: int = 20,
|
|
218
219
|
):
|
|
219
220
|
"""
|
|
220
|
-
Initialize the chart
|
|
221
|
+
Initialize the chart visualizer with pre-computed chart data.
|
|
221
222
|
|
|
222
223
|
Args:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
Type of chart to generate (e.g., 'Natal', 'Transit').
|
|
227
|
-
second_obj (AstrologicalSubject, optional):
|
|
228
|
-
Secondary subject for Transit or Synastry charts.
|
|
229
|
-
new_output_directory (str or Path, optional):
|
|
230
|
-
Base directory to save generated SVG files.
|
|
224
|
+
chart_data (ChartDataModel):
|
|
225
|
+
Pre-computed chart data from ChartDataFactory containing all subjects,
|
|
226
|
+
aspects, element/quality distributions, and other analytical data.
|
|
231
227
|
new_settings_file (Path, dict, or KerykeionSettingsModel, optional):
|
|
232
228
|
Custom settings source for chart colors, fonts, and aspects.
|
|
233
229
|
theme (KerykeionChartTheme or None, optional):
|
|
@@ -236,99 +232,82 @@ class KerykeionChartSVG:
|
|
|
236
232
|
Layout style for double-chart aspect grids ('list' or 'table').
|
|
237
233
|
chart_language (KerykeionChartLanguage, optional):
|
|
238
234
|
Language code for chart labels (e.g., 'EN', 'IT').
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
active_aspects (List[ActiveAspect], optional):
|
|
242
|
-
Aspects to calculate, each defined by name and orb.
|
|
235
|
+
external_view (bool, optional):
|
|
236
|
+
Whether to use external visualization (planets on outer ring) for single-subject charts. Defaults to False.
|
|
243
237
|
transparent_background (bool, optional):
|
|
244
238
|
Whether to use a transparent background instead of the theme color. Defaults to False.
|
|
239
|
+
custom_title (str or None, optional):
|
|
240
|
+
Custom title for the chart. If None, the default title will be used based on chart type. Defaults to None.
|
|
245
241
|
"""
|
|
246
242
|
# --------------------
|
|
247
243
|
# COMMON INITIALIZATION
|
|
248
244
|
# --------------------
|
|
249
|
-
home_directory = Path.home()
|
|
250
245
|
self.new_settings_file = new_settings_file
|
|
251
246
|
self.chart_language = chart_language
|
|
252
|
-
self.active_aspects = active_aspects
|
|
253
|
-
self.chart_type = chart_type
|
|
254
247
|
self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
|
|
255
248
|
self.transparent_background = transparent_background
|
|
249
|
+
self.external_view = external_view
|
|
256
250
|
self.chart_colors_settings = colors_settings
|
|
257
251
|
self.planets_settings = celestial_points_settings
|
|
258
252
|
self.aspects_settings = aspects_settings
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
self.
|
|
277
|
-
else:
|
|
278
|
-
self.output_directory = home_directory
|
|
253
|
+
self.custom_title = custom_title
|
|
254
|
+
self.auto_size = auto_size
|
|
255
|
+
self._padding = padding
|
|
256
|
+
|
|
257
|
+
# Extract data from ChartDataModel
|
|
258
|
+
self.chart_data = chart_data
|
|
259
|
+
self.chart_type = chart_data.chart_type
|
|
260
|
+
self.active_points = chart_data.active_points
|
|
261
|
+
self.active_aspects = chart_data.active_aspects
|
|
262
|
+
|
|
263
|
+
# Extract subjects based on chart type
|
|
264
|
+
if chart_data.chart_type in ["Natal", "Composite", "SingleReturnChart"]:
|
|
265
|
+
# SingleChartDataModel
|
|
266
|
+
self.first_obj = getattr(chart_data, 'subject')
|
|
267
|
+
self.second_obj = None
|
|
268
|
+
else: # DualChartDataModel for Transit, Synastry, DualReturnChart
|
|
269
|
+
self.first_obj = getattr(chart_data, 'first_subject')
|
|
270
|
+
self.second_obj = getattr(chart_data, 'second_subject')
|
|
279
271
|
|
|
280
272
|
# Load settings
|
|
281
273
|
self.parse_json_settings(new_settings_file)
|
|
282
274
|
|
|
283
|
-
# Primary subject
|
|
284
|
-
self.first_obj = first_obj
|
|
285
|
-
|
|
286
275
|
# Default radius for all charts
|
|
287
276
|
self.main_radius = 240
|
|
288
277
|
|
|
289
|
-
# Configure available planets
|
|
278
|
+
# Configure available planets from chart data
|
|
290
279
|
self.available_planets_setting = []
|
|
291
280
|
for body in self.planets_settings:
|
|
292
281
|
if body["name"] in self.active_points:
|
|
293
282
|
body["is_active"] = True
|
|
294
|
-
self.available_planets_setting.append(body)
|
|
283
|
+
self.available_planets_setting.append(body) # type: ignore[arg-type]
|
|
295
284
|
|
|
296
285
|
# Set available celestial points
|
|
297
286
|
available_celestial_points_names = [body["name"].lower() for body in self.available_planets_setting]
|
|
298
287
|
self.available_kerykeion_celestial_points = []
|
|
299
288
|
for body in available_celestial_points_names:
|
|
300
|
-
|
|
289
|
+
if hasattr(self.first_obj, body):
|
|
290
|
+
self.available_kerykeion_celestial_points.append(self.first_obj.get(body)) # type: ignore[arg-type]
|
|
301
291
|
|
|
302
292
|
# ------------------------
|
|
303
|
-
# CHART TYPE SPECIFIC SETUP
|
|
293
|
+
# CHART TYPE SPECIFIC SETUP FROM CHART DATA
|
|
304
294
|
# ------------------------
|
|
305
295
|
|
|
306
|
-
if self.chart_type
|
|
307
|
-
# --- NATAL
|
|
296
|
+
if self.chart_type == "Natal":
|
|
297
|
+
# --- NATAL CHART SETUP ---
|
|
308
298
|
|
|
309
|
-
#
|
|
310
|
-
|
|
311
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
312
|
-
|
|
313
|
-
# Calculate aspects
|
|
314
|
-
aspects_instance = AspectsFactory.single_chart_aspects(
|
|
315
|
-
self.first_obj,
|
|
316
|
-
active_points=self.active_points,
|
|
317
|
-
active_aspects=active_aspects,
|
|
318
|
-
)
|
|
319
|
-
self.aspects_list = aspects_instance.relevant_aspects
|
|
299
|
+
# Extract aspects from pre-computed chart data
|
|
300
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
320
301
|
|
|
321
302
|
# Screen size
|
|
322
303
|
self.height = self._DEFAULT_HEIGHT
|
|
323
304
|
self.width = self._DEFAULT_NATAL_WIDTH
|
|
324
305
|
|
|
325
|
-
#
|
|
326
|
-
self.location = self.
|
|
327
|
-
self.geolat = self.first_obj.lat
|
|
328
|
-
self.geolon = self.first_obj.lng
|
|
306
|
+
# Get location and coordinates
|
|
307
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
329
308
|
|
|
330
|
-
# Circle radii
|
|
331
|
-
if self.
|
|
309
|
+
# Circle radii - depends on external_view
|
|
310
|
+
if self.external_view:
|
|
332
311
|
self.first_circle_radius = 56
|
|
333
312
|
self.second_circle_radius = 92
|
|
334
313
|
self.third_circle_radius = 112
|
|
@@ -340,21 +319,15 @@ class KerykeionChartSVG:
|
|
|
340
319
|
elif self.chart_type == "Composite":
|
|
341
320
|
# --- COMPOSITE CHART SETUP ---
|
|
342
321
|
|
|
343
|
-
#
|
|
344
|
-
|
|
345
|
-
raise KerykeionException("First object must be a CompositeSubjectModel instance.")
|
|
346
|
-
|
|
347
|
-
# Calculate aspects
|
|
348
|
-
self.aspects_list = AspectsFactory.single_chart_aspects(self.first_obj, active_points=self.active_points).relevant_aspects
|
|
322
|
+
# Extract aspects from pre-computed chart data
|
|
323
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
349
324
|
|
|
350
325
|
# Screen size
|
|
351
326
|
self.height = self._DEFAULT_HEIGHT
|
|
352
327
|
self.width = self._DEFAULT_NATAL_WIDTH
|
|
353
328
|
|
|
354
|
-
#
|
|
355
|
-
self.location =
|
|
356
|
-
self.geolat = (self.first_obj.first_subject.lat + self.first_obj.second_subject.lat) / 2
|
|
357
|
-
self.geolon = (self.first_obj.first_subject.lng + self.first_obj.second_subject.lng) / 2
|
|
329
|
+
# Get location and coordinates
|
|
330
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
358
331
|
|
|
359
332
|
# Circle radii
|
|
360
333
|
self.first_circle_radius = 0
|
|
@@ -364,25 +337,8 @@ class KerykeionChartSVG:
|
|
|
364
337
|
elif self.chart_type == "Transit":
|
|
365
338
|
# --- TRANSIT CHART SETUP ---
|
|
366
339
|
|
|
367
|
-
#
|
|
368
|
-
|
|
369
|
-
raise KerykeionException("Second object is required for Transit charts.")
|
|
370
|
-
if not isinstance(self.first_obj, AstrologicalSubjectModel):
|
|
371
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
372
|
-
if not isinstance(second_obj, AstrologicalSubjectModel):
|
|
373
|
-
raise KerykeionException("Second object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
374
|
-
|
|
375
|
-
# Secondary subject setup
|
|
376
|
-
self.second_obj = second_obj
|
|
377
|
-
|
|
378
|
-
# Calculate aspects (transit to natal)
|
|
379
|
-
synastry_aspects_instance = AspectsFactory.dual_chart_aspects(
|
|
380
|
-
self.first_obj,
|
|
381
|
-
self.second_obj,
|
|
382
|
-
active_points=self.active_points,
|
|
383
|
-
active_aspects=active_aspects,
|
|
384
|
-
)
|
|
385
|
-
self.aspects_list = synastry_aspects_instance.relevant_aspects
|
|
340
|
+
# Extract aspects from pre-computed chart data
|
|
341
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
386
342
|
|
|
387
343
|
# Secondary subject available points
|
|
388
344
|
self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
|
|
@@ -394,11 +350,8 @@ class KerykeionChartSVG:
|
|
|
394
350
|
else:
|
|
395
351
|
self.width = self._DEFAULT_FULL_WIDTH
|
|
396
352
|
|
|
397
|
-
#
|
|
398
|
-
self.location = self.
|
|
399
|
-
self.geolat = self.second_obj.lat
|
|
400
|
-
self.geolon = self.second_obj.lng
|
|
401
|
-
self.t_name = self.language_settings["transit_name"]
|
|
353
|
+
# Get location and coordinates
|
|
354
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
402
355
|
|
|
403
356
|
# Circle radii
|
|
404
357
|
self.first_circle_radius = 0
|
|
@@ -408,25 +361,8 @@ class KerykeionChartSVG:
|
|
|
408
361
|
elif self.chart_type == "Synastry":
|
|
409
362
|
# --- SYNASTRY CHART SETUP ---
|
|
410
363
|
|
|
411
|
-
#
|
|
412
|
-
|
|
413
|
-
raise KerykeionException("Second object is required for Synastry charts.")
|
|
414
|
-
if not isinstance(self.first_obj, AstrologicalSubjectModel):
|
|
415
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
416
|
-
if not isinstance(second_obj, AstrologicalSubjectModel):
|
|
417
|
-
raise KerykeionException("Second object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
418
|
-
|
|
419
|
-
# Secondary subject setup
|
|
420
|
-
self.second_obj = second_obj
|
|
421
|
-
|
|
422
|
-
# Calculate aspects (natal to partner)
|
|
423
|
-
synastry_aspects_instance = AspectsFactory.dual_chart_aspects(
|
|
424
|
-
self.first_obj,
|
|
425
|
-
self.second_obj,
|
|
426
|
-
active_points=self.active_points,
|
|
427
|
-
active_aspects=active_aspects,
|
|
428
|
-
)
|
|
429
|
-
self.aspects_list = synastry_aspects_instance.relevant_aspects
|
|
364
|
+
# Extract aspects from pre-computed chart data
|
|
365
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
430
366
|
|
|
431
367
|
# Secondary subject available points
|
|
432
368
|
self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
|
|
@@ -435,38 +371,19 @@ class KerykeionChartSVG:
|
|
|
435
371
|
self.height = self._DEFAULT_HEIGHT
|
|
436
372
|
self.width = self._DEFAULT_FULL_WIDTH
|
|
437
373
|
|
|
438
|
-
#
|
|
439
|
-
self.location = self.
|
|
440
|
-
self.geolat = self.first_obj.lat
|
|
441
|
-
self.geolon = self.first_obj.lng
|
|
374
|
+
# Get location and coordinates
|
|
375
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
442
376
|
|
|
443
377
|
# Circle radii
|
|
444
378
|
self.first_circle_radius = 0
|
|
445
379
|
self.second_circle_radius = 36
|
|
446
380
|
self.third_circle_radius = 120
|
|
447
381
|
|
|
448
|
-
elif self.chart_type == "
|
|
382
|
+
elif self.chart_type == "DualReturnChart":
|
|
449
383
|
# --- RETURN CHART SETUP ---
|
|
450
384
|
|
|
451
|
-
#
|
|
452
|
-
|
|
453
|
-
raise KerykeionException("Second object is required for Return charts.")
|
|
454
|
-
if not isinstance(self.first_obj, AstrologicalSubjectModel):
|
|
455
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
456
|
-
if not isinstance(second_obj, PlanetReturnModel):
|
|
457
|
-
raise KerykeionException("Second object must be a PlanetReturnModel instance.")
|
|
458
|
-
|
|
459
|
-
# Secondary subject setup
|
|
460
|
-
self.second_obj = second_obj
|
|
461
|
-
|
|
462
|
-
# Calculate aspects (natal to return)
|
|
463
|
-
synastry_aspects_instance = AspectsFactory.dual_chart_aspects(
|
|
464
|
-
self.first_obj,
|
|
465
|
-
self.second_obj,
|
|
466
|
-
active_points=self.active_points,
|
|
467
|
-
active_aspects=active_aspects,
|
|
468
|
-
)
|
|
469
|
-
self.aspects_list = synastry_aspects_instance.relevant_aspects
|
|
385
|
+
# Extract aspects from pre-computed chart data
|
|
386
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
470
387
|
|
|
471
388
|
# Secondary subject available points
|
|
472
389
|
self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
|
|
@@ -475,93 +392,45 @@ class KerykeionChartSVG:
|
|
|
475
392
|
self.height = self._DEFAULT_HEIGHT
|
|
476
393
|
self.width = self._DEFAULT_ULTRA_WIDE_WIDTH
|
|
477
394
|
|
|
478
|
-
#
|
|
479
|
-
self.location = self.
|
|
480
|
-
self.geolat = self.first_obj.lat
|
|
481
|
-
self.geolon = self.first_obj.lng
|
|
395
|
+
# Get location and coordinates
|
|
396
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
482
397
|
|
|
483
398
|
# Circle radii
|
|
484
399
|
self.first_circle_radius = 0
|
|
485
400
|
self.second_circle_radius = 36
|
|
486
401
|
self.third_circle_radius = 120
|
|
487
402
|
|
|
488
|
-
elif self.chart_type == "
|
|
489
|
-
# ---
|
|
490
|
-
|
|
491
|
-
# Validate Subject
|
|
492
|
-
if not isinstance(self.first_obj, PlanetReturnModel):
|
|
493
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
403
|
+
elif self.chart_type == "SingleReturnChart":
|
|
404
|
+
# --- SINGLE WHEEL RETURN CHART SETUP ---
|
|
494
405
|
|
|
495
|
-
#
|
|
496
|
-
|
|
497
|
-
self.first_obj,
|
|
498
|
-
active_points=self.active_points,
|
|
499
|
-
active_aspects=active_aspects,
|
|
500
|
-
)
|
|
501
|
-
self.aspects_list = aspects_instance.relevant_aspects
|
|
406
|
+
# Extract aspects from pre-computed chart data
|
|
407
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
502
408
|
|
|
503
409
|
# Screen size
|
|
504
410
|
self.height = self._DEFAULT_HEIGHT
|
|
505
411
|
self.width = self._DEFAULT_NATAL_WIDTH
|
|
506
412
|
|
|
507
|
-
#
|
|
508
|
-
self.location = self.
|
|
509
|
-
self.geolat = self.first_obj.lat
|
|
510
|
-
self.geolon = self.first_obj.lng
|
|
413
|
+
# Get location and coordinates
|
|
414
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
511
415
|
|
|
512
416
|
# Circle radii
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
self.third_circle_radius = 112
|
|
517
|
-
else:
|
|
518
|
-
self.first_circle_radius = 0
|
|
519
|
-
self.second_circle_radius = 36
|
|
520
|
-
self.third_circle_radius = 120
|
|
417
|
+
self.first_circle_radius = 0
|
|
418
|
+
self.second_circle_radius = 36
|
|
419
|
+
self.third_circle_radius = 120
|
|
521
420
|
|
|
522
421
|
# --------------------
|
|
523
|
-
# FINAL COMMON SETUP
|
|
422
|
+
# FINAL COMMON SETUP FROM CHART DATA
|
|
524
423
|
# --------------------
|
|
525
424
|
|
|
526
|
-
#
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
celestial_points_names,
|
|
532
|
-
self.first_obj,
|
|
533
|
-
self.second_obj,
|
|
534
|
-
)
|
|
535
|
-
else:
|
|
536
|
-
element_totals = calculate_element_points(
|
|
537
|
-
self.available_planets_setting,
|
|
538
|
-
celestial_points_names,
|
|
539
|
-
self.first_obj,
|
|
540
|
-
)
|
|
541
|
-
|
|
542
|
-
self.fire = element_totals["fire"]
|
|
543
|
-
self.earth = element_totals["earth"]
|
|
544
|
-
self.air = element_totals["air"]
|
|
545
|
-
self.water = element_totals["water"]
|
|
425
|
+
# Extract pre-computed element and quality distributions
|
|
426
|
+
self.fire = chart_data.element_distribution.fire
|
|
427
|
+
self.earth = chart_data.element_distribution.earth
|
|
428
|
+
self.air = chart_data.element_distribution.air
|
|
429
|
+
self.water = chart_data.element_distribution.water
|
|
546
430
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
self.available_planets_setting,
|
|
551
|
-
celestial_points_names,
|
|
552
|
-
self.first_obj,
|
|
553
|
-
self.second_obj,
|
|
554
|
-
)
|
|
555
|
-
else:
|
|
556
|
-
qualities_totals = calculate_quality_points(
|
|
557
|
-
self.available_planets_setting,
|
|
558
|
-
celestial_points_names,
|
|
559
|
-
self.first_obj,
|
|
560
|
-
)
|
|
561
|
-
|
|
562
|
-
self.cardinal = qualities_totals["cardinal"]
|
|
563
|
-
self.fixed = qualities_totals["fixed"]
|
|
564
|
-
self.mutable = qualities_totals["mutable"]
|
|
431
|
+
self.cardinal = chart_data.quality_distribution.cardinal
|
|
432
|
+
self.fixed = chart_data.quality_distribution.fixed
|
|
433
|
+
self.mutable = chart_data.quality_distribution.mutable
|
|
565
434
|
|
|
566
435
|
# Set up theme
|
|
567
436
|
if theme not in get_args(KerykeionChartTheme) and theme is not None:
|
|
@@ -569,6 +438,176 @@ class KerykeionChartSVG:
|
|
|
569
438
|
|
|
570
439
|
self.set_up_theme(theme)
|
|
571
440
|
|
|
441
|
+
# Optionally expand width dynamically to fit content
|
|
442
|
+
if self.auto_size:
|
|
443
|
+
try:
|
|
444
|
+
required_width = self._estimate_required_width_full()
|
|
445
|
+
if required_width > self.width:
|
|
446
|
+
self.width = required_width
|
|
447
|
+
except Exception as e:
|
|
448
|
+
# Keep default on any unexpected issue; do not break rendering
|
|
449
|
+
logging.debug(f"Auto-size width calculation failed: {e}")
|
|
450
|
+
|
|
451
|
+
def _count_active_planets(self) -> int:
|
|
452
|
+
"""Return number of active celestial points in the current chart."""
|
|
453
|
+
return len([p for p in self.available_planets_setting if p.get("is_active")])
|
|
454
|
+
|
|
455
|
+
def _dynamic_viewbox(self) -> str:
|
|
456
|
+
"""Return the viewBox string based on current width/height."""
|
|
457
|
+
return f"0 0 {int(self.width)} {int(self.height)}"
|
|
458
|
+
|
|
459
|
+
def _wheel_only_viewbox(self, margin: int = 20) -> str:
|
|
460
|
+
"""Return a tight viewBox for the wheel-only template.
|
|
461
|
+
|
|
462
|
+
The wheel is drawn inside a group translated by (100, 50) and has
|
|
463
|
+
diameter 2 * main_radius. We add a small margin around it.
|
|
464
|
+
"""
|
|
465
|
+
left = 100 - margin
|
|
466
|
+
top = 50 - margin
|
|
467
|
+
width = (2 * self.main_radius) + (2 * margin)
|
|
468
|
+
height = (2 * self.main_radius) + (2 * margin)
|
|
469
|
+
return f"{left} {top} {width} {height}"
|
|
470
|
+
|
|
471
|
+
def _grid_only_viewbox(self, margin: int = 10) -> str:
|
|
472
|
+
"""Compute a tight viewBox for the Aspect Grid Only SVG.
|
|
473
|
+
|
|
474
|
+
The grid is rendered using fixed origins and box size:
|
|
475
|
+
- For Transit/Synastry/DualReturn charts, `draw_transit_aspect_grid`
|
|
476
|
+
uses `x_indent=50`, `y_indent=250`, `box_size=14` and draws:
|
|
477
|
+
• a header row to the right of `x_indent`
|
|
478
|
+
• a left header column at `x_indent - box_size`
|
|
479
|
+
• an N×N grid of cells above `y_indent`
|
|
480
|
+
|
|
481
|
+
- For Natal/Composite/SingleReturn charts, `draw_aspect_grid` uses
|
|
482
|
+
`x_start=50`, `y_start=250`, `box_size=14` and draws a triangular grid
|
|
483
|
+
that extends to the right (x) and upwards (y).
|
|
484
|
+
|
|
485
|
+
This function mirrors that geometry to return a snug viewBox around the
|
|
486
|
+
content, with a small configurable `margin`.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
margin: Extra pixels to add on each side of the computed bounds.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
A string "minX minY width height" suitable for the SVG `viewBox`.
|
|
493
|
+
"""
|
|
494
|
+
# Must match defaults used in the renderers
|
|
495
|
+
x0 = 50
|
|
496
|
+
y0 = 250
|
|
497
|
+
box = 14
|
|
498
|
+
|
|
499
|
+
n = max(len([p for p in self.available_planets_setting if p.get("is_active")]), 1)
|
|
500
|
+
|
|
501
|
+
if self.chart_type in ("Transit", "Synastry", "DualReturnChart"):
|
|
502
|
+
# Full N×N grid
|
|
503
|
+
left = (x0 - box) - margin
|
|
504
|
+
top = (y0 - box * n) - margin
|
|
505
|
+
right = (x0 + box * n) + margin
|
|
506
|
+
bottom = (y0 + box) + margin
|
|
507
|
+
else:
|
|
508
|
+
# Triangular grid (no extra left column)
|
|
509
|
+
left = x0 - margin
|
|
510
|
+
top = (y0 - box * n) - margin
|
|
511
|
+
right = (x0 + box * n) + margin
|
|
512
|
+
bottom = (y0 + box) + margin
|
|
513
|
+
|
|
514
|
+
width = max(1, int(right - left))
|
|
515
|
+
height = max(1, int(bottom - top))
|
|
516
|
+
|
|
517
|
+
return f"{int(left)} {int(top)} {width} {height}"
|
|
518
|
+
|
|
519
|
+
def _estimate_required_width_full(self) -> int:
|
|
520
|
+
"""Estimate minimal width to contain all rendered groups for the full chart.
|
|
521
|
+
|
|
522
|
+
The calculation is heuristic and mirrors the default x positions used in
|
|
523
|
+
the SVG templates and drawing utilities. We keep a conservative padding.
|
|
524
|
+
"""
|
|
525
|
+
# Wheel footprint (translate(100,50) + diameter of 2*radius)
|
|
526
|
+
wheel_right = 100 + (2 * self.main_radius)
|
|
527
|
+
extents = [wheel_right]
|
|
528
|
+
|
|
529
|
+
n_active = max(self._count_active_planets(), 1)
|
|
530
|
+
|
|
531
|
+
# Common grids present on many chart types
|
|
532
|
+
main_planet_grid_right = 645 + 80
|
|
533
|
+
main_houses_grid_right = 750 + 120
|
|
534
|
+
extents.extend([main_planet_grid_right, main_houses_grid_right])
|
|
535
|
+
|
|
536
|
+
if self.chart_type in ("Natal", "Composite", "SingleReturnChart"):
|
|
537
|
+
# Triangular aspect grid at x_start=510, width ~ 14 * n_active
|
|
538
|
+
aspect_grid_right = 510 + 14 * n_active
|
|
539
|
+
extents.append(aspect_grid_right)
|
|
540
|
+
|
|
541
|
+
if self.chart_type in ("Transit", "Synastry", "DualReturnChart"):
|
|
542
|
+
# Double-chart aspects placement
|
|
543
|
+
if self.double_chart_aspect_grid_type == "list":
|
|
544
|
+
# Columnar list placed at translate(565,273), ~100-110px per column, 14 aspects per column
|
|
545
|
+
aspects_per_column = 14
|
|
546
|
+
total_aspects = len(self.aspects_list) if hasattr(self, "aspects_list") else 0
|
|
547
|
+
columns = max((total_aspects + aspects_per_column - 1) // aspects_per_column, 1)
|
|
548
|
+
# Respect the max columns cap used in rendering: DualReturn=7, others=6
|
|
549
|
+
max_cols_cap = 7
|
|
550
|
+
columns = min(columns, max_cols_cap)
|
|
551
|
+
aspect_list_right = 565 + (columns * 110)
|
|
552
|
+
extents.append(aspect_list_right)
|
|
553
|
+
else:
|
|
554
|
+
# Grid table placed with x_indent ~550, width ~ 14px per cell across n_active+1
|
|
555
|
+
aspect_grid_table_right = 550 + (14 * (n_active + 1))
|
|
556
|
+
extents.append(aspect_grid_table_right)
|
|
557
|
+
|
|
558
|
+
# Secondary grids
|
|
559
|
+
secondary_planet_grid_right = 910 + 80
|
|
560
|
+
extents.append(secondary_planet_grid_right)
|
|
561
|
+
|
|
562
|
+
if self.chart_type == "Synastry":
|
|
563
|
+
# Secondary houses grid default x ~ 1015
|
|
564
|
+
secondary_houses_grid_right = 1015 + 120
|
|
565
|
+
extents.append(secondary_houses_grid_right)
|
|
566
|
+
|
|
567
|
+
if self.chart_type == "Transit":
|
|
568
|
+
# House comparison grid at x ~ 1030
|
|
569
|
+
house_comparison_grid_right = 1030 + 180
|
|
570
|
+
extents.append(house_comparison_grid_right)
|
|
571
|
+
|
|
572
|
+
if self.chart_type == "DualReturnChart":
|
|
573
|
+
# House comparison grid at x ~ 1030
|
|
574
|
+
house_comparison_grid_right = 1030 + 320
|
|
575
|
+
extents.append(house_comparison_grid_right)
|
|
576
|
+
|
|
577
|
+
# Conservative safety padding
|
|
578
|
+
return int(max(extents) + self._padding)
|
|
579
|
+
|
|
580
|
+
def _get_location_info(self) -> tuple[str, float, float]:
|
|
581
|
+
"""
|
|
582
|
+
Determine location information based on chart type and subjects.
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
tuple: (location_name, latitude, longitude)
|
|
586
|
+
"""
|
|
587
|
+
if self.chart_type == "Composite":
|
|
588
|
+
# For composite charts, use average location of the two composite subjects
|
|
589
|
+
if isinstance(self.first_obj, CompositeSubjectModel):
|
|
590
|
+
location_name = ""
|
|
591
|
+
latitude = (self.first_obj.first_subject.lat + self.first_obj.second_subject.lat) / 2
|
|
592
|
+
longitude = (self.first_obj.first_subject.lng + self.first_obj.second_subject.lng) / 2
|
|
593
|
+
else:
|
|
594
|
+
# Fallback to first subject location
|
|
595
|
+
location_name = self.first_obj.city or "Unknown"
|
|
596
|
+
latitude = self.first_obj.lat or 0.0
|
|
597
|
+
longitude = self.first_obj.lng or 0.0
|
|
598
|
+
elif self.chart_type in ["Transit", "DualReturnChart"] and self.second_obj:
|
|
599
|
+
# Use location from the second subject (transit/return)
|
|
600
|
+
location_name = self.second_obj.city or "Unknown"
|
|
601
|
+
latitude = self.second_obj.lat or 0.0
|
|
602
|
+
longitude = self.second_obj.lng or 0.0
|
|
603
|
+
else:
|
|
604
|
+
# Use location from the first subject
|
|
605
|
+
location_name = self.first_obj.city or "Unknown"
|
|
606
|
+
latitude = self.first_obj.lat or 0.0
|
|
607
|
+
longitude = self.first_obj.lng or 0.0
|
|
608
|
+
|
|
609
|
+
return location_name, latitude, longitude
|
|
610
|
+
|
|
572
611
|
def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None:
|
|
573
612
|
"""
|
|
574
613
|
Load and apply a CSS theme for the chart visualization.
|
|
@@ -585,16 +624,6 @@ class KerykeionChartSVG:
|
|
|
585
624
|
with open(theme_dir / f"{theme}.css", "r") as f:
|
|
586
625
|
self.color_style_tag = f.read()
|
|
587
626
|
|
|
588
|
-
def set_output_directory(self, dir_path: Path) -> None:
|
|
589
|
-
"""
|
|
590
|
-
Set the directory where generated SVG files will be saved.
|
|
591
|
-
|
|
592
|
-
Args:
|
|
593
|
-
dir_path (Path): Target directory for SVG output.
|
|
594
|
-
"""
|
|
595
|
-
self.output_directory = dir_path
|
|
596
|
-
logging.info(f"Output directory set to: {self.output_directory}")
|
|
597
|
-
|
|
598
627
|
def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None:
|
|
599
628
|
"""
|
|
600
629
|
Load and parse chart configuration settings.
|
|
@@ -682,7 +711,89 @@ class KerykeionChartSVG:
|
|
|
682
711
|
)
|
|
683
712
|
return out
|
|
684
713
|
|
|
685
|
-
def
|
|
714
|
+
def _truncate_name(self, name: str, max_length: int = 50) -> str:
|
|
715
|
+
"""
|
|
716
|
+
Truncate a name if it's too long, preserving readability.
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
name (str): The name to truncate
|
|
720
|
+
max_length (int): Maximum allowed length
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
str: Truncated name with ellipsis if needed
|
|
724
|
+
"""
|
|
725
|
+
if len(name) <= max_length:
|
|
726
|
+
return name
|
|
727
|
+
return name[:max_length-1] + "…"
|
|
728
|
+
|
|
729
|
+
def _get_chart_title(self) -> str:
|
|
730
|
+
"""
|
|
731
|
+
Generate the chart title based on chart type and custom title settings.
|
|
732
|
+
|
|
733
|
+
If a custom title is provided, it will be used. Otherwise, generates the
|
|
734
|
+
appropriate default title based on the chart type and subjects.
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
str: The chart title to display (max ~40 characters).
|
|
738
|
+
"""
|
|
739
|
+
# If custom title is provided, use it
|
|
740
|
+
if self.custom_title is not None:
|
|
741
|
+
return self.custom_title
|
|
742
|
+
|
|
743
|
+
# Generate default title based on chart type
|
|
744
|
+
if self.chart_type == "Natal":
|
|
745
|
+
natal_label = self.language_settings.get("birth_chart", "Natal")
|
|
746
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
747
|
+
return f'{truncated_name} - {natal_label}'
|
|
748
|
+
|
|
749
|
+
elif self.chart_type == "Composite":
|
|
750
|
+
composite_label = self.language_settings.get("composite_chart", "Composite")
|
|
751
|
+
and_word = self.language_settings.get("and_word", "&")
|
|
752
|
+
name1 = self._truncate_name(self.first_obj.first_subject.name) # type: ignore
|
|
753
|
+
name2 = self._truncate_name(self.first_obj.second_subject.name) # type: ignore
|
|
754
|
+
return f"{composite_label}: {name1} {and_word} {name2}"
|
|
755
|
+
|
|
756
|
+
elif self.chart_type == "Transit":
|
|
757
|
+
transit_label = self.language_settings.get("transits", "Transits")
|
|
758
|
+
from datetime import datetime
|
|
759
|
+
date_obj = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime) # type: ignore
|
|
760
|
+
date_str = date_obj.strftime("%d/%m/%y")
|
|
761
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
762
|
+
return f"{truncated_name} - {transit_label} {date_str}"
|
|
763
|
+
|
|
764
|
+
elif self.chart_type == "Synastry":
|
|
765
|
+
synastry_label = self.language_settings.get("synastry_chart", "Synastry")
|
|
766
|
+
and_word = self.language_settings.get("and_word", "&")
|
|
767
|
+
name1 = self._truncate_name(self.first_obj.name)
|
|
768
|
+
name2 = self._truncate_name(self.second_obj.name) # type: ignore
|
|
769
|
+
return f"{synastry_label}: {name1} {and_word} {name2}"
|
|
770
|
+
|
|
771
|
+
elif self.chart_type == "DualReturnChart":
|
|
772
|
+
from datetime import datetime
|
|
773
|
+
year = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime).year # type: ignore
|
|
774
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
775
|
+
if self.second_obj is not None and isinstance(self.second_obj, PlanetReturnModel) and self.second_obj.return_type == "Solar":
|
|
776
|
+
solar_label = self.language_settings.get("solar_return", "Solar")
|
|
777
|
+
return f"{truncated_name} - {solar_label} {year}"
|
|
778
|
+
else:
|
|
779
|
+
lunar_label = self.language_settings.get("lunar_return", "Lunar")
|
|
780
|
+
return f"{truncated_name} - {lunar_label} {year}"
|
|
781
|
+
|
|
782
|
+
elif self.chart_type == "SingleReturnChart":
|
|
783
|
+
from datetime import datetime
|
|
784
|
+
year = datetime.fromisoformat(self.first_obj.iso_formatted_local_datetime).year # type: ignore
|
|
785
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
786
|
+
if isinstance(self.first_obj, PlanetReturnModel) and self.first_obj.return_type == "Solar":
|
|
787
|
+
solar_label = self.language_settings.get("solar_return", "Solar")
|
|
788
|
+
return f"{truncated_name} - {solar_label} {year}"
|
|
789
|
+
else:
|
|
790
|
+
lunar_label = self.language_settings.get("lunar_return", "Lunar")
|
|
791
|
+
return f"{truncated_name} - {lunar_label} {year}"
|
|
792
|
+
|
|
793
|
+
# Fallback for unknown chart types
|
|
794
|
+
return self._truncate_name(self.first_obj.name)
|
|
795
|
+
|
|
796
|
+
def _create_template_dictionary(self) -> ChartTemplateModel:
|
|
686
797
|
"""
|
|
687
798
|
Assemble chart data and rendering instructions into a template dictionary.
|
|
688
799
|
|
|
@@ -690,7 +801,7 @@ class KerykeionChartSVG:
|
|
|
690
801
|
chart type and subjects.
|
|
691
802
|
|
|
692
803
|
Returns:
|
|
693
|
-
|
|
804
|
+
ChartTemplateModel: Populated structure of template variables.
|
|
694
805
|
"""
|
|
695
806
|
# Initialize template dictionary
|
|
696
807
|
template_dict: dict = {}
|
|
@@ -706,7 +817,6 @@ class KerykeionChartSVG:
|
|
|
706
817
|
|
|
707
818
|
# Set paper colors
|
|
708
819
|
template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"]
|
|
709
|
-
template_dict["paper_color_1"] = self.chart_colors_settings["paper_1"]
|
|
710
820
|
|
|
711
821
|
# Set background color based on transparent_background setting
|
|
712
822
|
if self.transparent_background:
|
|
@@ -714,7 +824,12 @@ class KerykeionChartSVG:
|
|
|
714
824
|
else:
|
|
715
825
|
template_dict["background_color"] = self.chart_colors_settings["paper_1"]
|
|
716
826
|
|
|
717
|
-
# Set planet colors
|
|
827
|
+
# Set planet colors - initialize all possible colors first with defaults
|
|
828
|
+
default_color = "#000000" # Default black color for unused planets
|
|
829
|
+
for i in range(42): # Support all 42 celestial points (0-41)
|
|
830
|
+
template_dict[f"planets_color_{i}"] = default_color
|
|
831
|
+
|
|
832
|
+
# Override with actual colors from settings
|
|
718
833
|
for planet in self.planets_settings:
|
|
719
834
|
planet_id = planet["id"]
|
|
720
835
|
template_dict[f"planets_color_{planet_id}"] = planet["color"]
|
|
@@ -732,10 +847,12 @@ class KerykeionChartSVG:
|
|
|
732
847
|
|
|
733
848
|
# Calculate element percentages
|
|
734
849
|
total_elements = self.fire + self.water + self.earth + self.air
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
850
|
+
element_values = {"fire": self.fire, "earth": self.earth, "air": self.air, "water": self.water}
|
|
851
|
+
element_percentages = distribute_percentages_to_100(element_values) if total_elements > 0 else {"fire": 0, "earth": 0, "air": 0, "water": 0}
|
|
852
|
+
fire_percentage = element_percentages["fire"]
|
|
853
|
+
earth_percentage = element_percentages["earth"]
|
|
854
|
+
air_percentage = element_percentages["air"]
|
|
855
|
+
water_percentage = element_percentages["water"]
|
|
739
856
|
|
|
740
857
|
# Element Percentages
|
|
741
858
|
template_dict["elements_string"] = f"{self.language_settings.get('elements', 'Elements')}:"
|
|
@@ -747,9 +864,11 @@ class KerykeionChartSVG:
|
|
|
747
864
|
|
|
748
865
|
# Qualities Percentages
|
|
749
866
|
total_qualities = self.cardinal + self.fixed + self.mutable
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
867
|
+
quality_values = {"cardinal": self.cardinal, "fixed": self.fixed, "mutable": self.mutable}
|
|
868
|
+
quality_percentages = distribute_percentages_to_100(quality_values) if total_qualities > 0 else {"cardinal": 0, "fixed": 0, "mutable": 0}
|
|
869
|
+
cardinal_percentage = quality_percentages["cardinal"]
|
|
870
|
+
fixed_percentage = quality_percentages["fixed"]
|
|
871
|
+
mutable_percentage = quality_percentages["mutable"]
|
|
753
872
|
|
|
754
873
|
template_dict["qualities_string"] = f"{self.language_settings.get('qualities', 'Qualities')}:"
|
|
755
874
|
template_dict["cardinal_string"] = f"{self.language_settings.get('cardinal', 'Cardinal')} {cardinal_percentage}%"
|
|
@@ -759,13 +878,16 @@ class KerykeionChartSVG:
|
|
|
759
878
|
# Get houses list for main subject
|
|
760
879
|
first_subject_houses_list = get_houses_list(self.first_obj)
|
|
761
880
|
|
|
881
|
+
# Chart title
|
|
882
|
+
template_dict["stringTitle"] = self._get_chart_title()
|
|
883
|
+
|
|
762
884
|
# ------------------------------- #
|
|
763
885
|
# CHART TYPE SPECIFIC SETTINGS #
|
|
764
886
|
# ------------------------------- #
|
|
765
887
|
|
|
766
|
-
if self.chart_type
|
|
767
|
-
# Set viewbox
|
|
768
|
-
template_dict["viewbox"] = self.
|
|
888
|
+
if self.chart_type == "Natal":
|
|
889
|
+
# Set viewbox dynamically
|
|
890
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
769
891
|
|
|
770
892
|
# Rings and circles
|
|
771
893
|
template_dict["transitRing"] = ""
|
|
@@ -810,9 +932,6 @@ class KerykeionChartSVG:
|
|
|
810
932
|
)
|
|
811
933
|
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
812
934
|
|
|
813
|
-
# Chart title
|
|
814
|
-
template_dict["stringTitle"] = f'{self.first_obj.name} - {self.language_settings.get("birth_chart", "Birth Chart")}'
|
|
815
|
-
|
|
816
935
|
# Top left section
|
|
817
936
|
latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
|
|
818
937
|
longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
|
|
@@ -822,7 +941,8 @@ class KerykeionChartSVG:
|
|
|
822
941
|
template_dict["top_left_2"] = f"{self.language_settings['latitude']}: {latitude_string}"
|
|
823
942
|
template_dict["top_left_3"] = f"{self.language_settings['longitude']}: {longitude_string}"
|
|
824
943
|
template_dict["top_left_4"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
|
|
825
|
-
|
|
944
|
+
localized_weekday = self.language_settings.get('weekdays', {}).get(self.first_obj.day_of_week, self.first_obj.day_of_week) # type: ignore
|
|
945
|
+
template_dict["top_left_5"] = f"{self.language_settings.get('day_of_week', 'Day of Week')}: {localized_weekday}" # type: ignore
|
|
826
946
|
|
|
827
947
|
# Bottom left section
|
|
828
948
|
if self.first_obj.zodiac_type == "Tropic":
|
|
@@ -870,6 +990,7 @@ class KerykeionChartSVG:
|
|
|
870
990
|
c1=self.first_circle_radius,
|
|
871
991
|
c3=self.third_circle_radius,
|
|
872
992
|
chart_type=self.chart_type,
|
|
993
|
+
external_view=self.external_view,
|
|
873
994
|
)
|
|
874
995
|
|
|
875
996
|
template_dict["makePlanets"] = draw_planets(
|
|
@@ -880,6 +1001,7 @@ class KerykeionChartSVG:
|
|
|
880
1001
|
third_circle_radius=self.third_circle_radius,
|
|
881
1002
|
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
882
1003
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1004
|
+
external_view=self.external_view,
|
|
883
1005
|
)
|
|
884
1006
|
|
|
885
1007
|
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
@@ -894,8 +1016,8 @@ class KerykeionChartSVG:
|
|
|
894
1016
|
template_dict["makeHouseComparisonGrid"] = ""
|
|
895
1017
|
|
|
896
1018
|
elif self.chart_type == "Composite":
|
|
897
|
-
# Set viewbox
|
|
898
|
-
template_dict["viewbox"] = self.
|
|
1019
|
+
# Set viewbox dynamically
|
|
1020
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
899
1021
|
|
|
900
1022
|
# Rings and circles
|
|
901
1023
|
template_dict["transitRing"] = ""
|
|
@@ -940,9 +1062,6 @@ class KerykeionChartSVG:
|
|
|
940
1062
|
)
|
|
941
1063
|
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
942
1064
|
|
|
943
|
-
# Chart title
|
|
944
|
-
template_dict["stringTitle"] = f"{self.first_obj.first_subject.name} {self.language_settings['and_word']} {self.first_obj.second_subject.name}" # type: ignore
|
|
945
|
-
|
|
946
1065
|
# Top left section
|
|
947
1066
|
# First subject
|
|
948
1067
|
latitude = convert_latitude_coordinate_to_string(
|
|
@@ -1014,6 +1133,7 @@ class KerykeionChartSVG:
|
|
|
1014
1133
|
c1=self.first_circle_radius,
|
|
1015
1134
|
c3=self.third_circle_radius,
|
|
1016
1135
|
chart_type=self.chart_type,
|
|
1136
|
+
external_view=self.external_view,
|
|
1017
1137
|
)
|
|
1018
1138
|
|
|
1019
1139
|
template_dict["makePlanets"] = draw_planets(
|
|
@@ -1024,6 +1144,7 @@ class KerykeionChartSVG:
|
|
|
1024
1144
|
third_circle_radius=self.third_circle_radius,
|
|
1025
1145
|
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
1026
1146
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1147
|
+
external_view=self.external_view,
|
|
1027
1148
|
)
|
|
1028
1149
|
|
|
1029
1150
|
subject_name = f"{self.first_obj.first_subject.name} {self.language_settings['and_word']} {self.first_obj.second_subject.name}" # type: ignore
|
|
@@ -1054,11 +1175,8 @@ class KerykeionChartSVG:
|
|
|
1054
1175
|
template_dict["fixed_string"] = ""
|
|
1055
1176
|
template_dict["mutable_string"] = ""
|
|
1056
1177
|
|
|
1057
|
-
# Set viewbox
|
|
1058
|
-
|
|
1059
|
-
template_dict["viewbox"] = self._TRANSIT_CHART_WITH_TABLE_VIWBOX
|
|
1060
|
-
else:
|
|
1061
|
-
template_dict["viewbox"] = self._WIDE_CHART_VIEWBOX
|
|
1178
|
+
# Set viewbox dynamically
|
|
1179
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1062
1180
|
|
|
1063
1181
|
# Get houses list for secondary subject
|
|
1064
1182
|
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
@@ -1098,7 +1216,7 @@ class KerykeionChartSVG:
|
|
|
1098
1216
|
if self.double_chart_aspect_grid_type == "list":
|
|
1099
1217
|
title = f'{self.first_obj.name} - {self.language_settings.get("transit_aspects", "Transit Aspects")}'
|
|
1100
1218
|
template_dict["makeAspectGrid"] = ""
|
|
1101
|
-
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings)
|
|
1219
|
+
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings) # type: ignore[arg-type] # type: ignore[arg-type]
|
|
1102
1220
|
else:
|
|
1103
1221
|
template_dict["makeAspectGrid"] = ""
|
|
1104
1222
|
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
|
|
@@ -1111,9 +1229,6 @@ class KerykeionChartSVG:
|
|
|
1111
1229
|
|
|
1112
1230
|
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1113
1231
|
|
|
1114
|
-
# Chart title
|
|
1115
|
-
template_dict["stringTitle"] = f"{self.language_settings['transits']} {format_datetime_with_timezone(self.second_obj.iso_formatted_local_datetime)}" # type: ignore
|
|
1116
|
-
|
|
1117
1232
|
# Top left section
|
|
1118
1233
|
latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
|
|
1119
1234
|
longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
|
|
@@ -1176,6 +1291,7 @@ class KerykeionChartSVG:
|
|
|
1176
1291
|
c1=self.first_circle_radius,
|
|
1177
1292
|
c3=self.third_circle_radius,
|
|
1178
1293
|
chart_type=self.chart_type,
|
|
1294
|
+
external_view=self.external_view,
|
|
1179
1295
|
second_subject_houses_list=second_subject_houses_list,
|
|
1180
1296
|
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
1181
1297
|
)
|
|
@@ -1189,6 +1305,7 @@ class KerykeionChartSVG:
|
|
|
1189
1305
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1190
1306
|
chart_type=self.chart_type,
|
|
1191
1307
|
third_circle_radius=self.third_circle_radius,
|
|
1308
|
+
external_view=self.external_view,
|
|
1192
1309
|
)
|
|
1193
1310
|
|
|
1194
1311
|
# Planet grids
|
|
@@ -1214,8 +1331,8 @@ class KerykeionChartSVG:
|
|
|
1214
1331
|
|
|
1215
1332
|
# House comparison grid
|
|
1216
1333
|
house_comparison_factory = HouseComparisonFactory(
|
|
1217
|
-
first_subject=self.first_obj,
|
|
1218
|
-
second_subject=self.second_obj,
|
|
1334
|
+
first_subject=self.first_obj, # type: ignore[arg-type]
|
|
1335
|
+
second_subject=self.second_obj, # type: ignore[arg-type]
|
|
1219
1336
|
active_points=self.active_points,
|
|
1220
1337
|
)
|
|
1221
1338
|
house_comparison = house_comparison_factory.get_house_comparison()
|
|
@@ -1228,12 +1345,12 @@ class KerykeionChartSVG:
|
|
|
1228
1345
|
house_position_comparison_label=self.language_settings.get("house_position_comparison", "House Position Comparison"),
|
|
1229
1346
|
return_point_label=self.language_settings.get("transit_point", "Transit Point"),
|
|
1230
1347
|
natal_house_label=self.language_settings.get("house_position", "Natal House"),
|
|
1231
|
-
x_position=
|
|
1348
|
+
x_position=980,
|
|
1232
1349
|
)
|
|
1233
1350
|
|
|
1234
1351
|
elif self.chart_type == "Synastry":
|
|
1235
|
-
# Set viewbox
|
|
1236
|
-
template_dict["viewbox"] = self.
|
|
1352
|
+
# Set viewbox dynamically
|
|
1353
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1237
1354
|
|
|
1238
1355
|
# Get houses list for secondary subject
|
|
1239
1356
|
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
@@ -1273,10 +1390,10 @@ class KerykeionChartSVG:
|
|
|
1273
1390
|
if self.double_chart_aspect_grid_type == "list":
|
|
1274
1391
|
template_dict["makeAspectGrid"] = ""
|
|
1275
1392
|
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(
|
|
1276
|
-
f"{self.first_obj.name} - {self.second_obj.name} {self.language_settings.get('synastry_aspects', 'Synastry Aspects')}",
|
|
1393
|
+
f"{self.first_obj.name} - {self.second_obj.name} {self.language_settings.get('synastry_aspects', 'Synastry Aspects')}", # type: ignore[union-attr]
|
|
1277
1394
|
self.aspects_list,
|
|
1278
|
-
self.planets_settings,
|
|
1279
|
-
self.aspects_settings
|
|
1395
|
+
self.planets_settings, # type: ignore[arg-type]
|
|
1396
|
+
self.aspects_settings # type: ignore[arg-type]
|
|
1280
1397
|
)
|
|
1281
1398
|
else:
|
|
1282
1399
|
template_dict["makeAspectGrid"] = ""
|
|
@@ -1290,9 +1407,6 @@ class KerykeionChartSVG:
|
|
|
1290
1407
|
|
|
1291
1408
|
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1292
1409
|
|
|
1293
|
-
# Chart title
|
|
1294
|
-
template_dict["stringTitle"] = f"{self.first_obj.name} {self.language_settings['and_word']} {self.second_obj.name}" # type: ignore
|
|
1295
|
-
|
|
1296
1410
|
# Top left section
|
|
1297
1411
|
template_dict["top_left_0"] = f"{self.first_obj.name}:"
|
|
1298
1412
|
template_dict["top_left_1"] = f"{self.first_obj.city}, {self.first_obj.nation}" # type: ignore
|
|
@@ -1343,6 +1457,7 @@ class KerykeionChartSVG:
|
|
|
1343
1457
|
c1=self.first_circle_radius,
|
|
1344
1458
|
c3=self.third_circle_radius,
|
|
1345
1459
|
chart_type=self.chart_type,
|
|
1460
|
+
external_view=self.external_view,
|
|
1346
1461
|
second_subject_houses_list=second_subject_houses_list,
|
|
1347
1462
|
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
1348
1463
|
)
|
|
@@ -1356,6 +1471,7 @@ class KerykeionChartSVG:
|
|
|
1356
1471
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1357
1472
|
chart_type=self.chart_type,
|
|
1358
1473
|
third_circle_radius=self.third_circle_radius,
|
|
1474
|
+
external_view=self.external_view,
|
|
1359
1475
|
)
|
|
1360
1476
|
|
|
1361
1477
|
# Planet grid
|
|
@@ -1377,9 +1493,9 @@ class KerykeionChartSVG:
|
|
|
1377
1493
|
)
|
|
1378
1494
|
template_dict["makeHouseComparisonGrid"] = ""
|
|
1379
1495
|
|
|
1380
|
-
elif self.chart_type == "
|
|
1381
|
-
# Set viewbox
|
|
1382
|
-
template_dict["viewbox"] = self.
|
|
1496
|
+
elif self.chart_type == "DualReturnChart":
|
|
1497
|
+
# Set viewbox dynamically
|
|
1498
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1383
1499
|
|
|
1384
1500
|
# Get houses list for secondary subject
|
|
1385
1501
|
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
@@ -1419,7 +1535,7 @@ class KerykeionChartSVG:
|
|
|
1419
1535
|
if self.double_chart_aspect_grid_type == "list":
|
|
1420
1536
|
title = self.language_settings.get("return_aspects", "Natal to Return Aspects")
|
|
1421
1537
|
template_dict["makeAspectGrid"] = ""
|
|
1422
|
-
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings, max_columns=7)
|
|
1538
|
+
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings, max_columns=7) # type: ignore[arg-type] # type: ignore[arg-type]
|
|
1423
1539
|
else:
|
|
1424
1540
|
template_dict["makeAspectGrid"] = ""
|
|
1425
1541
|
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
|
|
@@ -1432,12 +1548,6 @@ class KerykeionChartSVG:
|
|
|
1432
1548
|
|
|
1433
1549
|
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1434
1550
|
|
|
1435
|
-
# Chart title
|
|
1436
|
-
if self.second_obj is not None and hasattr(self.second_obj, 'return_type') and self.second_obj.return_type == "Solar":
|
|
1437
|
-
template_dict["stringTitle"] = f"{self.first_obj.name} - {self.language_settings.get('solar_return', 'Solar Return')}"
|
|
1438
|
-
else:
|
|
1439
|
-
template_dict["stringTitle"] = f"{self.first_obj.name} - {self.language_settings.get('lunar_return', 'Lunar Return')}"
|
|
1440
|
-
|
|
1441
1551
|
|
|
1442
1552
|
# Top left section
|
|
1443
1553
|
# Subject
|
|
@@ -1509,6 +1619,7 @@ class KerykeionChartSVG:
|
|
|
1509
1619
|
c1=self.first_circle_radius,
|
|
1510
1620
|
c3=self.third_circle_radius,
|
|
1511
1621
|
chart_type=self.chart_type,
|
|
1622
|
+
external_view=self.external_view,
|
|
1512
1623
|
second_subject_houses_list=second_subject_houses_list,
|
|
1513
1624
|
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
1514
1625
|
)
|
|
@@ -1522,6 +1633,7 @@ class KerykeionChartSVG:
|
|
|
1522
1633
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1523
1634
|
chart_type=self.chart_type,
|
|
1524
1635
|
third_circle_radius=self.third_circle_radius,
|
|
1636
|
+
external_view=self.external_view,
|
|
1525
1637
|
)
|
|
1526
1638
|
|
|
1527
1639
|
# Planet grid
|
|
@@ -1549,8 +1661,8 @@ class KerykeionChartSVG:
|
|
|
1549
1661
|
)
|
|
1550
1662
|
|
|
1551
1663
|
house_comparison_factory = HouseComparisonFactory(
|
|
1552
|
-
first_subject=self.first_obj,
|
|
1553
|
-
second_subject=self.second_obj,
|
|
1664
|
+
first_subject=self.first_obj, # type: ignore[arg-type]
|
|
1665
|
+
second_subject=self.second_obj, # type: ignore[arg-type]
|
|
1554
1666
|
active_points=self.active_points,
|
|
1555
1667
|
)
|
|
1556
1668
|
house_comparison = house_comparison_factory.get_house_comparison()
|
|
@@ -1562,13 +1674,13 @@ class KerykeionChartSVG:
|
|
|
1562
1674
|
points_owner_subject_number=2, # The second subject is the Solar Return
|
|
1563
1675
|
house_position_comparison_label=self.language_settings.get("house_position_comparison", "House Position Comparison"),
|
|
1564
1676
|
return_point_label=self.language_settings.get("return_point", "Return Point"),
|
|
1565
|
-
return_label=self.language_settings.get("Return", "
|
|
1677
|
+
return_label=self.language_settings.get("Return", "DualReturnChart"),
|
|
1566
1678
|
radix_label=self.language_settings.get("Natal", "Natal"),
|
|
1567
1679
|
)
|
|
1568
1680
|
|
|
1569
|
-
elif self.chart_type == "
|
|
1570
|
-
# Set viewbox
|
|
1571
|
-
template_dict["viewbox"] = self.
|
|
1681
|
+
elif self.chart_type == "SingleReturnChart":
|
|
1682
|
+
# Set viewbox dynamically
|
|
1683
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1572
1684
|
|
|
1573
1685
|
# Rings and circles
|
|
1574
1686
|
template_dict["transitRing"] = ""
|
|
@@ -1613,9 +1725,6 @@ class KerykeionChartSVG:
|
|
|
1613
1725
|
)
|
|
1614
1726
|
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
1615
1727
|
|
|
1616
|
-
# Chart title
|
|
1617
|
-
template_dict["stringTitle"] = self.first_obj.name
|
|
1618
|
-
|
|
1619
1728
|
# Top left section
|
|
1620
1729
|
latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
|
|
1621
1730
|
longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
|
|
@@ -1677,6 +1786,7 @@ class KerykeionChartSVG:
|
|
|
1677
1786
|
c1=self.first_circle_radius,
|
|
1678
1787
|
c3=self.third_circle_radius,
|
|
1679
1788
|
chart_type=self.chart_type,
|
|
1789
|
+
external_view=self.external_view,
|
|
1680
1790
|
)
|
|
1681
1791
|
|
|
1682
1792
|
template_dict["makePlanets"] = draw_planets(
|
|
@@ -1687,6 +1797,7 @@ class KerykeionChartSVG:
|
|
|
1687
1797
|
third_circle_radius=self.third_circle_radius,
|
|
1688
1798
|
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
1689
1799
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1800
|
+
external_view=self.external_view,
|
|
1690
1801
|
)
|
|
1691
1802
|
|
|
1692
1803
|
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
@@ -1700,9 +1811,9 @@ class KerykeionChartSVG:
|
|
|
1700
1811
|
template_dict["makeSecondaryPlanetGrid"] = ""
|
|
1701
1812
|
template_dict["makeHouseComparisonGrid"] = ""
|
|
1702
1813
|
|
|
1703
|
-
return
|
|
1814
|
+
return ChartTemplateModel(**template_dict)
|
|
1704
1815
|
|
|
1705
|
-
def
|
|
1816
|
+
def generate_svg_string(self, minify: bool = False, remove_css_variables=False) -> str:
|
|
1706
1817
|
"""
|
|
1707
1818
|
Render the full chart SVG as a string.
|
|
1708
1819
|
|
|
@@ -1723,11 +1834,11 @@ class KerykeionChartSVG:
|
|
|
1723
1834
|
|
|
1724
1835
|
# read template
|
|
1725
1836
|
with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f:
|
|
1726
|
-
template = Template(f.read()).substitute(td)
|
|
1837
|
+
template = Template(f.read()).substitute(td.model_dump())
|
|
1727
1838
|
|
|
1728
1839
|
# return filename
|
|
1729
1840
|
|
|
1730
|
-
logging.debug(f"Template dictionary
|
|
1841
|
+
logging.debug(f"Template dictionary has {len(td.model_dump())} fields")
|
|
1731
1842
|
|
|
1732
1843
|
self._create_template_dictionary()
|
|
1733
1844
|
|
|
@@ -1742,36 +1853,50 @@ class KerykeionChartSVG:
|
|
|
1742
1853
|
|
|
1743
1854
|
return template
|
|
1744
1855
|
|
|
1745
|
-
def
|
|
1856
|
+
def save_svg(self, output_path: Union[str, Path, None] = None, filename: Union[str, None] = None, minify: bool = False, remove_css_variables=False):
|
|
1746
1857
|
"""
|
|
1747
1858
|
Generate and save the full chart SVG to disk.
|
|
1748
1859
|
|
|
1749
|
-
Calls
|
|
1750
|
-
"{subject.name} - {chart_type} Chart.svg" in the output directory.
|
|
1860
|
+
Calls generate_svg_string to render the SVG, then writes a file named
|
|
1861
|
+
"{subject.name} - {chart_type} Chart.svg" in the specified output directory.
|
|
1751
1862
|
|
|
1752
1863
|
Args:
|
|
1753
|
-
|
|
1754
|
-
|
|
1864
|
+
output_path (str, Path, or None): Directory path where the SVG file will be saved.
|
|
1865
|
+
If None, defaults to the user's home directory.
|
|
1866
|
+
filename (str or None): Custom filename for the SVG file (without extension).
|
|
1867
|
+
If None, uses the default pattern: "{subject.name} - {chart_type} Chart".
|
|
1868
|
+
minify (bool): Pass-through to generate_svg_string for compact output.
|
|
1869
|
+
remove_css_variables (bool): Pass-through to generate_svg_string to embed CSS variables.
|
|
1755
1870
|
|
|
1756
1871
|
Returns:
|
|
1757
1872
|
None
|
|
1758
1873
|
"""
|
|
1759
1874
|
|
|
1760
|
-
self.template = self.
|
|
1875
|
+
self.template = self.generate_svg_string(minify, remove_css_variables)
|
|
1761
1876
|
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1877
|
+
# Convert output_path to Path object, default to home directory
|
|
1878
|
+
output_directory = Path(output_path) if output_path is not None else Path.home()
|
|
1879
|
+
|
|
1880
|
+
# Determine filename
|
|
1881
|
+
if filename is not None:
|
|
1882
|
+
chartname = output_directory / f"{filename}.svg"
|
|
1766
1883
|
else:
|
|
1767
|
-
|
|
1884
|
+
# Use default filename pattern
|
|
1885
|
+
chart_type_for_filename = self.chart_type
|
|
1886
|
+
|
|
1887
|
+
if self.chart_type == "DualReturnChart" and self.second_obj is not None and hasattr(self.second_obj, 'return_type') and self.second_obj.return_type == "Lunar":
|
|
1888
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Lunar Return.svg"
|
|
1889
|
+
elif self.chart_type == "DualReturnChart" and self.second_obj is not None and hasattr(self.second_obj, 'return_type') and self.second_obj.return_type == "Solar":
|
|
1890
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Solar Return.svg"
|
|
1891
|
+
else:
|
|
1892
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart.svg"
|
|
1768
1893
|
|
|
1769
1894
|
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
1770
1895
|
output_file.write(self.template)
|
|
1771
1896
|
|
|
1772
1897
|
print(f"SVG Generated Correctly in: {chartname}")
|
|
1773
1898
|
|
|
1774
|
-
def
|
|
1899
|
+
def generate_wheel_only_svg_string(self, minify: bool = False, remove_css_variables=False):
|
|
1775
1900
|
"""
|
|
1776
1901
|
Render the wheel-only chart SVG as a string.
|
|
1777
1902
|
|
|
@@ -1795,7 +1920,9 @@ class KerykeionChartSVG:
|
|
|
1795
1920
|
template = f.read()
|
|
1796
1921
|
|
|
1797
1922
|
template_dict = self._create_template_dictionary()
|
|
1798
|
-
|
|
1923
|
+
# Use a compact viewBox specific for the wheel-only rendering
|
|
1924
|
+
wheel_viewbox = self._wheel_only_viewbox()
|
|
1925
|
+
template = Template(template).substitute({**template_dict.model_dump(), "viewbox": wheel_viewbox})
|
|
1799
1926
|
|
|
1800
1927
|
if remove_css_variables:
|
|
1801
1928
|
template = inline_css_variables_in_svg(template)
|
|
@@ -1808,30 +1935,44 @@ class KerykeionChartSVG:
|
|
|
1808
1935
|
|
|
1809
1936
|
return template
|
|
1810
1937
|
|
|
1811
|
-
def
|
|
1938
|
+
def save_wheel_only_svg_file(self, output_path: Union[str, Path, None] = None, filename: Union[str, None] = None, minify: bool = False, remove_css_variables=False):
|
|
1812
1939
|
"""
|
|
1813
1940
|
Generate and save wheel-only chart SVG to disk.
|
|
1814
1941
|
|
|
1815
|
-
Calls
|
|
1816
|
-
"{subject.name} - {chart_type} Chart - Wheel Only.svg" in the output directory.
|
|
1942
|
+
Calls generate_wheel_only_svg_string and writes a file named
|
|
1943
|
+
"{subject.name} - {chart_type} Chart - Wheel Only.svg" in the specified output directory.
|
|
1817
1944
|
|
|
1818
1945
|
Args:
|
|
1819
|
-
|
|
1820
|
-
|
|
1946
|
+
output_path (str, Path, or None): Directory path where the SVG file will be saved.
|
|
1947
|
+
If None, defaults to the user's home directory.
|
|
1948
|
+
filename (str or None): Custom filename for the SVG file (without extension).
|
|
1949
|
+
If None, uses the default pattern: "{subject.name} - {chart_type} Chart - Wheel Only".
|
|
1950
|
+
minify (bool): Pass-through to generate_wheel_only_svg_string for compact output.
|
|
1951
|
+
remove_css_variables (bool): Pass-through to generate_wheel_only_svg_string to embed CSS variables.
|
|
1821
1952
|
|
|
1822
1953
|
Returns:
|
|
1823
1954
|
None
|
|
1824
1955
|
"""
|
|
1825
1956
|
|
|
1826
|
-
template = self.
|
|
1827
|
-
|
|
1957
|
+
template = self.generate_wheel_only_svg_string(minify, remove_css_variables)
|
|
1958
|
+
|
|
1959
|
+
# Convert output_path to Path object, default to home directory
|
|
1960
|
+
output_directory = Path(output_path) if output_path is not None else Path.home()
|
|
1961
|
+
|
|
1962
|
+
# Determine filename
|
|
1963
|
+
if filename is not None:
|
|
1964
|
+
chartname = output_directory / f"{filename}.svg"
|
|
1965
|
+
else:
|
|
1966
|
+
# Use default filename pattern
|
|
1967
|
+
chart_type_for_filename = "ExternalNatal" if self.external_view and self.chart_type == "Natal" else self.chart_type
|
|
1968
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Wheel Only.svg"
|
|
1828
1969
|
|
|
1829
1970
|
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
1830
1971
|
output_file.write(template)
|
|
1831
1972
|
|
|
1832
1973
|
print(f"SVG Generated Correctly in: {chartname}")
|
|
1833
1974
|
|
|
1834
|
-
def
|
|
1975
|
+
def generate_aspect_grid_only_svg_string(self, minify: bool = False, remove_css_variables=False):
|
|
1835
1976
|
"""
|
|
1836
1977
|
Render the aspect-grid-only chart SVG as a string.
|
|
1837
1978
|
|
|
@@ -1856,7 +1997,7 @@ class KerykeionChartSVG:
|
|
|
1856
1997
|
|
|
1857
1998
|
template_dict = self._create_template_dictionary()
|
|
1858
1999
|
|
|
1859
|
-
if self.chart_type in ["Transit", "Synastry", "
|
|
2000
|
+
if self.chart_type in ["Transit", "Synastry", "DualReturnChart"]:
|
|
1860
2001
|
aspects_grid = draw_transit_aspect_grid(
|
|
1861
2002
|
self.chart_colors_settings["paper_0"],
|
|
1862
2003
|
self.available_planets_setting,
|
|
@@ -1871,7 +2012,10 @@ class KerykeionChartSVG:
|
|
|
1871
2012
|
y_start=250,
|
|
1872
2013
|
)
|
|
1873
2014
|
|
|
1874
|
-
|
|
2015
|
+
# Use a compact, known-good viewBox that frames the grid
|
|
2016
|
+
viewbox_override = self._grid_only_viewbox()
|
|
2017
|
+
|
|
2018
|
+
template = Template(template).substitute({**template_dict.model_dump(), "makeAspectGrid": aspects_grid, "viewbox": viewbox_override})
|
|
1875
2019
|
|
|
1876
2020
|
if remove_css_variables:
|
|
1877
2021
|
template = inline_css_variables_in_svg(template)
|
|
@@ -1884,42 +2028,55 @@ class KerykeionChartSVG:
|
|
|
1884
2028
|
|
|
1885
2029
|
return template
|
|
1886
2030
|
|
|
1887
|
-
def
|
|
2031
|
+
def save_aspect_grid_only_svg_file(self, output_path: Union[str, Path, None] = None, filename: Union[str, None] = None, minify: bool = False, remove_css_variables=False):
|
|
1888
2032
|
"""
|
|
1889
2033
|
Generate and save aspect-grid-only chart SVG to disk.
|
|
1890
2034
|
|
|
1891
|
-
Calls
|
|
1892
|
-
"{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the output directory.
|
|
2035
|
+
Calls generate_aspect_grid_only_svg_string and writes a file named
|
|
2036
|
+
"{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the specified output directory.
|
|
1893
2037
|
|
|
1894
2038
|
Args:
|
|
1895
|
-
|
|
1896
|
-
|
|
2039
|
+
output_path (str, Path, or None): Directory path where the SVG file will be saved.
|
|
2040
|
+
If None, defaults to the user's home directory.
|
|
2041
|
+
filename (str or None): Custom filename for the SVG file (without extension).
|
|
2042
|
+
If None, uses the default pattern: "{subject.name} - {chart_type} Chart - Aspect Grid Only".
|
|
2043
|
+
minify (bool): Pass-through to generate_aspect_grid_only_svg_string for compact output.
|
|
2044
|
+
remove_css_variables (bool): Pass-through to generate_aspect_grid_only_svg_string to embed CSS variables.
|
|
1897
2045
|
|
|
1898
2046
|
Returns:
|
|
1899
2047
|
None
|
|
1900
2048
|
"""
|
|
1901
2049
|
|
|
1902
|
-
template = self.
|
|
1903
|
-
|
|
2050
|
+
template = self.generate_aspect_grid_only_svg_string(minify, remove_css_variables)
|
|
2051
|
+
|
|
2052
|
+
# Convert output_path to Path object, default to home directory
|
|
2053
|
+
output_directory = Path(output_path) if output_path is not None else Path.home()
|
|
2054
|
+
|
|
2055
|
+
# Determine filename
|
|
2056
|
+
if filename is not None:
|
|
2057
|
+
chartname = output_directory / f"{filename}.svg"
|
|
2058
|
+
else:
|
|
2059
|
+
# Use default filename pattern
|
|
2060
|
+
chart_type_for_filename = "ExternalNatal" if self.external_view and self.chart_type == "Natal" else self.chart_type
|
|
2061
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Aspect Grid Only.svg"
|
|
1904
2062
|
|
|
1905
2063
|
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
1906
2064
|
output_file.write(template)
|
|
1907
2065
|
|
|
1908
2066
|
print(f"SVG Generated Correctly in: {chartname}")
|
|
1909
2067
|
|
|
1910
|
-
|
|
1911
2068
|
if __name__ == "__main__":
|
|
1912
2069
|
from kerykeion.utilities import setup_logging
|
|
1913
2070
|
from kerykeion.planetary_return_factory import PlanetaryReturnFactory
|
|
1914
2071
|
from kerykeion.astrological_subject_factory import AstrologicalSubjectFactory
|
|
2072
|
+
from kerykeion.chart_data_factory import ChartDataFactory
|
|
2073
|
+
from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
|
|
1915
2074
|
|
|
1916
|
-
ACTIVE_PLANETS: list[AstrologicalPoint] =
|
|
1917
|
-
|
|
1918
|
-
]
|
|
1919
|
-
|
|
2075
|
+
ACTIVE_PLANETS: list[AstrologicalPoint] = DEFAULT_ACTIVE_POINTS
|
|
2076
|
+
# ACTIVE_PLANETS: list[AstrologicalPoint] = ALL_ACTIVE_POINTS
|
|
1920
2077
|
setup_logging(level="info")
|
|
1921
2078
|
|
|
1922
|
-
subject = AstrologicalSubjectFactory.from_birth_data("John Lennon", 1940, 10, 9, 18, 30, "Liverpool", "GB")
|
|
2079
|
+
subject = AstrologicalSubjectFactory.from_birth_data("John Lennon", 1940, 10, 9, 18, 30, "Liverpool", "GB", active_points=ACTIVE_PLANETS)
|
|
1923
2080
|
|
|
1924
2081
|
return_factory = PlanetaryReturnFactory(
|
|
1925
2082
|
subject,
|
|
@@ -1932,101 +2089,118 @@ if __name__ == "__main__":
|
|
|
1932
2089
|
)
|
|
1933
2090
|
|
|
1934
2091
|
###
|
|
1935
|
-
## Birth Chart
|
|
1936
|
-
|
|
1937
|
-
|
|
2092
|
+
## Birth Chart - NEW APPROACH with ChartDataFactory
|
|
2093
|
+
birth_chart_data = ChartDataFactory.create_natal_chart_data(
|
|
2094
|
+
subject,
|
|
2095
|
+
active_points=ACTIVE_PLANETS,
|
|
2096
|
+
)
|
|
2097
|
+
birth_chart = ChartDrawer(
|
|
2098
|
+
chart_data=birth_chart_data,
|
|
1938
2099
|
chart_language="IT",
|
|
1939
2100
|
theme="strawberry",
|
|
1940
|
-
active_points=ACTIVE_PLANETS,
|
|
1941
2101
|
)
|
|
1942
|
-
birth_chart.
|
|
2102
|
+
birth_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1943
2103
|
|
|
1944
2104
|
###
|
|
1945
|
-
## Solar Return Chart
|
|
2105
|
+
## Solar Return Chart - NEW APPROACH with ChartDataFactory
|
|
1946
2106
|
solar_return = return_factory.next_return_from_iso_formatted_time(
|
|
1947
2107
|
"2025-01-09T18:30:00+01:00", # UTC+1
|
|
1948
2108
|
return_type="Solar",
|
|
1949
2109
|
)
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2110
|
+
solar_return_chart_data = ChartDataFactory.create_return_chart_data(
|
|
2111
|
+
subject,
|
|
2112
|
+
solar_return,
|
|
2113
|
+
active_points=ACTIVE_PLANETS,
|
|
2114
|
+
)
|
|
2115
|
+
solar_return_chart = ChartDrawer(
|
|
2116
|
+
chart_data=solar_return_chart_data,
|
|
1953
2117
|
chart_language="IT",
|
|
1954
2118
|
theme="classic",
|
|
1955
|
-
active_points=ACTIVE_PLANETS,
|
|
1956
2119
|
)
|
|
1957
2120
|
|
|
1958
|
-
solar_return_chart.
|
|
2121
|
+
solar_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1959
2122
|
|
|
1960
2123
|
###
|
|
1961
|
-
## Single wheel return
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2124
|
+
## Single wheel return - NEW APPROACH with ChartDataFactory
|
|
2125
|
+
single_wheel_return_chart_data = ChartDataFactory.create_single_wheel_return_chart_data(
|
|
2126
|
+
solar_return,
|
|
2127
|
+
active_points=ACTIVE_PLANETS,
|
|
2128
|
+
)
|
|
2129
|
+
single_wheel_return_chart = ChartDrawer(
|
|
2130
|
+
chart_data=single_wheel_return_chart_data,
|
|
1966
2131
|
chart_language="IT",
|
|
1967
2132
|
theme="dark",
|
|
1968
|
-
active_points=ACTIVE_PLANETS,
|
|
1969
2133
|
)
|
|
1970
2134
|
|
|
1971
|
-
single_wheel_return_chart.
|
|
2135
|
+
single_wheel_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1972
2136
|
|
|
1973
2137
|
###
|
|
1974
|
-
## Lunar return
|
|
2138
|
+
## Lunar return - NEW APPROACH with ChartDataFactory
|
|
1975
2139
|
lunar_return = return_factory.next_return_from_iso_formatted_time(
|
|
1976
2140
|
"2025-01-09T18:30:00+01:00", # UTC+1
|
|
1977
2141
|
return_type="Lunar",
|
|
1978
2142
|
)
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2143
|
+
lunar_return_chart_data = ChartDataFactory.create_return_chart_data(
|
|
2144
|
+
subject,
|
|
2145
|
+
lunar_return,
|
|
2146
|
+
active_points=ACTIVE_PLANETS,
|
|
2147
|
+
)
|
|
2148
|
+
lunar_return_chart = ChartDrawer(
|
|
2149
|
+
chart_data=lunar_return_chart_data,
|
|
1983
2150
|
chart_language="IT",
|
|
1984
2151
|
theme="dark",
|
|
1985
|
-
active_points=ACTIVE_PLANETS,
|
|
1986
2152
|
)
|
|
1987
|
-
lunar_return_chart.
|
|
2153
|
+
lunar_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1988
2154
|
|
|
1989
2155
|
###
|
|
1990
|
-
## Transit Chart
|
|
2156
|
+
## Transit Chart - NEW APPROACH with ChartDataFactory
|
|
1991
2157
|
transit = AstrologicalSubjectFactory.from_iso_utc_time(
|
|
1992
2158
|
"Transit",
|
|
1993
2159
|
"2021-10-04T18:30:00+01:00",
|
|
1994
2160
|
)
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2161
|
+
transit_chart_data = ChartDataFactory.create_transit_chart_data(
|
|
2162
|
+
subject,
|
|
2163
|
+
transit,
|
|
2164
|
+
active_points=ACTIVE_PLANETS,
|
|
2165
|
+
)
|
|
2166
|
+
transit_chart = ChartDrawer(
|
|
2167
|
+
chart_data=transit_chart_data,
|
|
1999
2168
|
chart_language="IT",
|
|
2000
2169
|
theme="dark",
|
|
2001
|
-
active_points=ACTIVE_PLANETS
|
|
2002
2170
|
)
|
|
2003
|
-
transit_chart.
|
|
2171
|
+
transit_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2004
2172
|
|
|
2005
2173
|
###
|
|
2006
|
-
## Synastry Chart
|
|
2174
|
+
## Synastry Chart - NEW APPROACH with ChartDataFactory
|
|
2007
2175
|
second_subject = AstrologicalSubjectFactory.from_birth_data("Yoko Ono", 1933, 2, 18, 18, 30, "Tokyo", "JP")
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2176
|
+
synastry_chart_data = ChartDataFactory.create_synastry_chart_data(
|
|
2177
|
+
subject,
|
|
2178
|
+
second_subject,
|
|
2179
|
+
active_points=ACTIVE_PLANETS,
|
|
2180
|
+
)
|
|
2181
|
+
synastry_chart = ChartDrawer(
|
|
2182
|
+
chart_data=synastry_chart_data,
|
|
2012
2183
|
chart_language="IT",
|
|
2013
2184
|
theme="dark",
|
|
2014
|
-
active_points=ACTIVE_PLANETS
|
|
2015
2185
|
)
|
|
2016
|
-
synastry_chart.
|
|
2186
|
+
synastry_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2017
2187
|
|
|
2018
2188
|
##
|
|
2019
|
-
# Transit Chart with Grid
|
|
2189
|
+
# Transit Chart with Grid - NEW APPROACH with ChartDataFactory
|
|
2020
2190
|
subject.name = "Grid"
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2191
|
+
transit_chart_with_grid_data = ChartDataFactory.create_transit_chart_data(
|
|
2192
|
+
subject,
|
|
2193
|
+
transit,
|
|
2194
|
+
active_points=ACTIVE_PLANETS,
|
|
2195
|
+
)
|
|
2196
|
+
transit_chart_with_grid = ChartDrawer(
|
|
2197
|
+
chart_data=transit_chart_with_grid_data,
|
|
2025
2198
|
chart_language="IT",
|
|
2026
2199
|
theme="dark",
|
|
2027
|
-
active_points=ACTIVE_PLANETS,
|
|
2028
2200
|
double_chart_aspect_grid_type="table"
|
|
2029
2201
|
)
|
|
2030
|
-
transit_chart_with_grid.
|
|
2031
|
-
transit_chart_with_grid.
|
|
2032
|
-
transit_chart_with_grid.
|
|
2202
|
+
transit_chart_with_grid.save_svg() # minify=True, remove_css_variables=True)
|
|
2203
|
+
transit_chart_with_grid.save_aspect_grid_only_svg_file()
|
|
2204
|
+
transit_chart_with_grid.save_wheel_only_svg_file()
|
|
2205
|
+
|
|
2206
|
+
print("✅ All chart examples completed using ChartDataFactory + ChartDrawer architecture!")
|