kerykeion 5.0.0a11__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 +32 -9
- kerykeion/aspects/__init__.py +2 -4
- kerykeion/aspects/aspects_factory.py +530 -0
- kerykeion/aspects/aspects_utils.py +75 -6
- kerykeion/astrological_subject_factory.py +380 -229
- kerykeion/backword.py +680 -0
- kerykeion/chart_data_factory.py +484 -0
- kerykeion/charts/{kerykeion_chart_svg.py → chart_drawer.py} +612 -439
- 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 +51 -18
- kerykeion/house_comparison/house_comparison_utils.py +37 -8
- kerykeion/planetary_return_factory.py +8 -4
- kerykeion/relationship_score_factory.py +5 -5
- 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 +247 -21
- 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/sweph/sefstars.txt +1602 -0
- kerykeion/transits_time_range_factory.py +11 -11
- kerykeion/utilities.py +61 -38
- kerykeion-5.0.0b1.dist-info/METADATA +1055 -0
- kerykeion-5.0.0b1.dist-info/RECORD +58 -0
- kerykeion/aspects/natal_aspects_factory.py +0 -235
- kerykeion/aspects/synastry_aspects_factory.py +0 -275
- kerykeion/house_comparison/house_comparison_models.py +0 -38
- kerykeion/kr_types/chart_types.py +0 -106
- kerykeion-5.0.0a11.dist-info/METADATA +0 -641
- kerykeion-5.0.0a11.dist-info/RECORD +0 -50
- /kerykeion/{kr_types → schemas}/kerykeion_exception.py +0 -0
- {kerykeion-5.0.0a11.dist-info → kerykeion-5.0.0b1.dist-info}/WHEEL +0 -0
- {kerykeion-5.0.0a11.dist-info → kerykeion-5.0.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,34 +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.synastry_aspects_factory import SynastryAspectsFactory
|
|
13
|
-
from kerykeion.aspects.natal_aspects_factory import NatalAspectsFactory
|
|
14
15
|
from kerykeion.house_comparison.house_comparison_factory import HouseComparisonFactory
|
|
15
|
-
from kerykeion.
|
|
16
|
+
from kerykeion.schemas import (
|
|
16
17
|
KerykeionException,
|
|
17
18
|
ChartType,
|
|
18
19
|
Sign,
|
|
19
20
|
ActiveAspect,
|
|
20
21
|
)
|
|
21
|
-
from kerykeion.
|
|
22
|
-
from kerykeion.
|
|
22
|
+
from kerykeion.schemas import ChartTemplateModel
|
|
23
|
+
from kerykeion.schemas.kr_models import (
|
|
23
24
|
AstrologicalSubjectModel,
|
|
24
25
|
CompositeSubjectModel,
|
|
25
26
|
PlanetReturnModel,
|
|
26
27
|
)
|
|
27
|
-
from kerykeion.
|
|
28
|
+
from kerykeion.schemas.settings_models import (
|
|
28
29
|
KerykeionSettingsCelestialPointModel,
|
|
29
30
|
KerykeionSettingsModel,
|
|
30
31
|
)
|
|
31
|
-
from kerykeion.
|
|
32
|
+
from kerykeion.schemas.kr_literals import (
|
|
32
33
|
KerykeionChartTheme,
|
|
33
34
|
KerykeionChartLanguage,
|
|
34
35
|
AstrologicalPoint,
|
|
35
36
|
)
|
|
36
|
-
from kerykeion.utilities import find_common_active_points
|
|
37
37
|
from kerykeion.charts.charts_utils import (
|
|
38
38
|
draw_zodiac_slice,
|
|
39
39
|
convert_latitude_coordinate_to_string,
|
|
@@ -58,15 +58,10 @@ from kerykeion.charts.charts_utils import (
|
|
|
58
58
|
draw_main_planet_grid,
|
|
59
59
|
draw_secondary_planet_grid,
|
|
60
60
|
format_location_string,
|
|
61
|
-
format_datetime_with_timezone
|
|
62
|
-
calculate_element_points,
|
|
63
|
-
calculate_synastry_element_points,
|
|
64
|
-
calculate_quality_points,
|
|
65
|
-
calculate_synastry_quality_points
|
|
61
|
+
format_datetime_with_timezone
|
|
66
62
|
)
|
|
67
63
|
from kerykeion.charts.draw_planets import draw_planets
|
|
68
|
-
from kerykeion.utilities import get_houses_list, inline_css_variables_in_svg
|
|
69
|
-
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
|
|
70
65
|
from kerykeion.settings.legacy.legacy_color_settings import DEFAULT_CHART_COLORS
|
|
71
66
|
from kerykeion.settings.legacy.legacy_celestial_points_settings import DEFAULT_CELESTIAL_POINTS_SETTINGS
|
|
72
67
|
from kerykeion.settings.legacy.legacy_chart_aspects_settings import DEFAULT_CHART_ASPECTS_SETTINGS
|
|
@@ -77,30 +72,32 @@ from typing import List, Literal
|
|
|
77
72
|
from datetime import datetime
|
|
78
73
|
|
|
79
74
|
|
|
80
|
-
class
|
|
75
|
+
class ChartDrawer:
|
|
81
76
|
"""
|
|
82
|
-
|
|
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.
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
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.
|
|
86
86
|
Charts are rendered using XML templates and drawing utilities, with customizable themes,
|
|
87
|
-
language,
|
|
88
|
-
|
|
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.
|
|
89
91
|
|
|
90
92
|
NOTE:
|
|
91
93
|
The generated SVG files are optimized for web use, opening in browsers. If you want to
|
|
92
94
|
use them in other applications, you might need to adjust the SVG settings or styles.
|
|
93
95
|
|
|
94
96
|
Args:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Defaults to 'Natal'.
|
|
100
|
-
second_obj (AstrologicalSubject | AstrologicalSubjectModel, optional):
|
|
101
|
-
The secondary subject for Transit or Synastry charts. Not required for Natal or Composite.
|
|
102
|
-
new_output_directory (str | Path, optional):
|
|
103
|
-
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.
|
|
104
101
|
new_settings_file (Path | dict | KerykeionSettingsModel, optional):
|
|
105
102
|
Path or settings object to override default chart configuration (colors, fonts, aspects).
|
|
106
103
|
theme (KerykeionChartTheme, optional):
|
|
@@ -109,55 +106,60 @@ class KerykeionChartSVG:
|
|
|
109
106
|
Specifies rendering style for double-chart aspect grids. Defaults to 'list'.
|
|
110
107
|
chart_language (KerykeionChartLanguage, optional):
|
|
111
108
|
Language code for chart labels. Defaults to 'EN'.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Example:
|
|
115
|
-
["Sun", "Moon", "Mercury", "Venus"]
|
|
116
|
-
|
|
117
|
-
active_aspects (list[ActiveAspect], optional):
|
|
118
|
-
List of aspects (name and orb) to calculate. Defaults to DEFAULT_ACTIVE_ASPECTS.
|
|
119
|
-
Example:
|
|
120
|
-
[
|
|
121
|
-
{"name": "conjunction", "orb": 10},
|
|
122
|
-
{"name": "opposition", "orb": 10},
|
|
123
|
-
{"name": "trine", "orb": 8},
|
|
124
|
-
{"name": "sextile", "orb": 6},
|
|
125
|
-
{"name": "square", "orb": 5},
|
|
126
|
-
{"name": "quintile", "orb": 1},
|
|
127
|
-
]
|
|
109
|
+
transparent_background (bool, optional):
|
|
110
|
+
Whether to use a transparent background instead of the theme color. Defaults to False.
|
|
128
111
|
|
|
129
112
|
Public Methods:
|
|
130
113
|
makeTemplate(minify=False, remove_css_variables=False) -> str:
|
|
131
114
|
Render the full chart SVG as a string without writing to disk. Use `minify=True`
|
|
132
115
|
to remove whitespace and quotes, and `remove_css_variables=True` to embed CSS vars.
|
|
133
116
|
|
|
134
|
-
|
|
135
|
-
Generate and write the full chart SVG file to the
|
|
136
|
-
|
|
137
|
-
'{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'.
|
|
138
121
|
|
|
139
122
|
makeWheelOnlyTemplate(minify=False, remove_css_variables=False) -> str:
|
|
140
123
|
Render only the chart wheel (no aspect grid) as an SVG string.
|
|
141
124
|
|
|
142
|
-
|
|
143
|
-
Generate and write the wheel-only SVG file
|
|
144
|
-
|
|
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'.
|
|
145
129
|
|
|
146
130
|
makeAspectGridOnlyTemplate(minify=False, remove_css_variables=False) -> str:
|
|
147
131
|
Render only the aspect grid as an SVG string.
|
|
148
132
|
|
|
149
|
-
|
|
150
|
-
Generate and write the aspect-grid-only SVG file
|
|
151
|
-
|
|
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")
|
|
152
154
|
"""
|
|
153
155
|
|
|
154
156
|
# Constants
|
|
155
157
|
|
|
156
158
|
_DEFAULT_HEIGHT = 550
|
|
157
|
-
_DEFAULT_FULL_WIDTH =
|
|
159
|
+
_DEFAULT_FULL_WIDTH = 1250
|
|
158
160
|
_DEFAULT_NATAL_WIDTH = 870
|
|
159
|
-
_DEFAULT_FULL_WIDTH_WITH_TABLE =
|
|
160
|
-
_DEFAULT_ULTRA_WIDE_WIDTH =
|
|
161
|
+
_DEFAULT_FULL_WIDTH_WITH_TABLE = 1250
|
|
162
|
+
_DEFAULT_ULTRA_WIDE_WIDTH = 1320
|
|
161
163
|
|
|
162
164
|
_BASIC_CHART_VIEWBOX = f"0 0 {_DEFAULT_NATAL_WIDTH} {_DEFAULT_HEIGHT}"
|
|
163
165
|
_WIDE_CHART_VIEWBOX = f"0 0 {_DEFAULT_FULL_WIDTH} 546.0"
|
|
@@ -168,9 +170,6 @@ class KerykeionChartSVG:
|
|
|
168
170
|
first_obj: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel]
|
|
169
171
|
second_obj: Union[AstrologicalSubjectModel, PlanetReturnModel, None]
|
|
170
172
|
chart_type: ChartType
|
|
171
|
-
new_output_directory: Union[Path, None]
|
|
172
|
-
new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
|
|
173
|
-
output_directory: Path
|
|
174
173
|
new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
|
|
175
174
|
theme: Union[KerykeionChartTheme, None]
|
|
176
175
|
double_chart_aspect_grid_type: Literal["list", "table"]
|
|
@@ -178,6 +177,8 @@ class KerykeionChartSVG:
|
|
|
178
177
|
active_points: List[AstrologicalPoint]
|
|
179
178
|
active_aspects: List[ActiveAspect]
|
|
180
179
|
transparent_background: bool
|
|
180
|
+
external_view: bool
|
|
181
|
+
custom_title: Union[str, None]
|
|
181
182
|
|
|
182
183
|
# Internal properties
|
|
183
184
|
fire: float
|
|
@@ -190,8 +191,8 @@ class KerykeionChartSVG:
|
|
|
190
191
|
width: Union[float, int]
|
|
191
192
|
language_settings: dict
|
|
192
193
|
chart_colors_settings: dict
|
|
193
|
-
planets_settings: dict
|
|
194
|
-
aspects_settings: dict
|
|
194
|
+
planets_settings: list[dict[Any, Any]]
|
|
195
|
+
aspects_settings: list[dict[Any, Any]]
|
|
195
196
|
available_planets_setting: List[KerykeionSettingsCelestialPointModel]
|
|
196
197
|
height: float
|
|
197
198
|
location: str
|
|
@@ -201,34 +202,28 @@ class KerykeionChartSVG:
|
|
|
201
202
|
|
|
202
203
|
def __init__(
|
|
203
204
|
self,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
second_obj: Union[AstrologicalSubjectModel, PlanetReturnModel, None] = None,
|
|
207
|
-
new_output_directory: Union[str, None] = None,
|
|
205
|
+
chart_data: "ChartDataModel",
|
|
206
|
+
*,
|
|
208
207
|
new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None,
|
|
209
208
|
theme: Union[KerykeionChartTheme, None] = "classic",
|
|
210
209
|
double_chart_aspect_grid_type: Literal["list", "table"] = "list",
|
|
211
210
|
chart_language: KerykeionChartLanguage = "EN",
|
|
212
|
-
|
|
213
|
-
active_aspects: list[ActiveAspect]= DEFAULT_ACTIVE_ASPECTS,
|
|
214
|
-
*,
|
|
211
|
+
external_view: bool = False,
|
|
215
212
|
transparent_background: bool = False,
|
|
216
213
|
colors_settings: dict = DEFAULT_CHART_COLORS,
|
|
217
214
|
celestial_points_settings: list[dict] = DEFAULT_CELESTIAL_POINTS_SETTINGS,
|
|
218
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,
|
|
219
219
|
):
|
|
220
220
|
"""
|
|
221
|
-
Initialize the chart
|
|
221
|
+
Initialize the chart visualizer with pre-computed chart data.
|
|
222
222
|
|
|
223
223
|
Args:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
Type of chart to generate (e.g., 'Natal', 'Transit').
|
|
228
|
-
second_obj (AstrologicalSubject, optional):
|
|
229
|
-
Secondary subject for Transit or Synastry charts.
|
|
230
|
-
new_output_directory (str or Path, optional):
|
|
231
|
-
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.
|
|
232
227
|
new_settings_file (Path, dict, or KerykeionSettingsModel, optional):
|
|
233
228
|
Custom settings source for chart colors, fonts, and aspects.
|
|
234
229
|
theme (KerykeionChartTheme or None, optional):
|
|
@@ -237,99 +232,82 @@ class KerykeionChartSVG:
|
|
|
237
232
|
Layout style for double-chart aspect grids ('list' or 'table').
|
|
238
233
|
chart_language (KerykeionChartLanguage, optional):
|
|
239
234
|
Language code for chart labels (e.g., 'EN', 'IT').
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
active_aspects (List[ActiveAspect], optional):
|
|
243
|
-
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.
|
|
244
237
|
transparent_background (bool, optional):
|
|
245
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.
|
|
246
241
|
"""
|
|
247
242
|
# --------------------
|
|
248
243
|
# COMMON INITIALIZATION
|
|
249
244
|
# --------------------
|
|
250
|
-
home_directory = Path.home()
|
|
251
245
|
self.new_settings_file = new_settings_file
|
|
252
246
|
self.chart_language = chart_language
|
|
253
|
-
self.active_aspects = active_aspects
|
|
254
|
-
self.chart_type = chart_type
|
|
255
247
|
self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
|
|
256
248
|
self.transparent_background = transparent_background
|
|
249
|
+
self.external_view = external_view
|
|
257
250
|
self.chart_colors_settings = colors_settings
|
|
258
251
|
self.planets_settings = celestial_points_settings
|
|
259
252
|
self.aspects_settings = aspects_settings
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
#
|
|
276
|
-
|
|
277
|
-
self.
|
|
278
|
-
else:
|
|
279
|
-
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')
|
|
280
271
|
|
|
281
272
|
# Load settings
|
|
282
273
|
self.parse_json_settings(new_settings_file)
|
|
283
274
|
|
|
284
|
-
# Primary subject
|
|
285
|
-
self.first_obj = first_obj
|
|
286
|
-
|
|
287
275
|
# Default radius for all charts
|
|
288
276
|
self.main_radius = 240
|
|
289
277
|
|
|
290
|
-
# Configure available planets
|
|
278
|
+
# Configure available planets from chart data
|
|
291
279
|
self.available_planets_setting = []
|
|
292
280
|
for body in self.planets_settings:
|
|
293
281
|
if body["name"] in self.active_points:
|
|
294
282
|
body["is_active"] = True
|
|
295
|
-
self.available_planets_setting.append(body)
|
|
283
|
+
self.available_planets_setting.append(body) # type: ignore[arg-type]
|
|
296
284
|
|
|
297
285
|
# Set available celestial points
|
|
298
286
|
available_celestial_points_names = [body["name"].lower() for body in self.available_planets_setting]
|
|
299
287
|
self.available_kerykeion_celestial_points = []
|
|
300
288
|
for body in available_celestial_points_names:
|
|
301
|
-
|
|
289
|
+
if hasattr(self.first_obj, body):
|
|
290
|
+
self.available_kerykeion_celestial_points.append(self.first_obj.get(body)) # type: ignore[arg-type]
|
|
302
291
|
|
|
303
292
|
# ------------------------
|
|
304
|
-
# CHART TYPE SPECIFIC SETUP
|
|
293
|
+
# CHART TYPE SPECIFIC SETUP FROM CHART DATA
|
|
305
294
|
# ------------------------
|
|
306
295
|
|
|
307
|
-
if self.chart_type
|
|
308
|
-
# --- NATAL
|
|
296
|
+
if self.chart_type == "Natal":
|
|
297
|
+
# --- NATAL CHART SETUP ---
|
|
309
298
|
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
313
|
-
|
|
314
|
-
# Calculate aspects
|
|
315
|
-
natal_aspects_instance = NatalAspectsFactory.from_subject(
|
|
316
|
-
self.first_obj,
|
|
317
|
-
active_points=self.active_points,
|
|
318
|
-
active_aspects=active_aspects,
|
|
319
|
-
)
|
|
320
|
-
self.aspects_list = natal_aspects_instance.relevant_aspects
|
|
299
|
+
# Extract aspects from pre-computed chart data
|
|
300
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
321
301
|
|
|
322
302
|
# Screen size
|
|
323
303
|
self.height = self._DEFAULT_HEIGHT
|
|
324
304
|
self.width = self._DEFAULT_NATAL_WIDTH
|
|
325
305
|
|
|
326
|
-
#
|
|
327
|
-
self.location = self.
|
|
328
|
-
self.geolat = self.first_obj.lat
|
|
329
|
-
self.geolon = self.first_obj.lng
|
|
306
|
+
# Get location and coordinates
|
|
307
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
330
308
|
|
|
331
|
-
# Circle radii
|
|
332
|
-
if self.
|
|
309
|
+
# Circle radii - depends on external_view
|
|
310
|
+
if self.external_view:
|
|
333
311
|
self.first_circle_radius = 56
|
|
334
312
|
self.second_circle_radius = 92
|
|
335
313
|
self.third_circle_radius = 112
|
|
@@ -341,21 +319,15 @@ class KerykeionChartSVG:
|
|
|
341
319
|
elif self.chart_type == "Composite":
|
|
342
320
|
# --- COMPOSITE CHART SETUP ---
|
|
343
321
|
|
|
344
|
-
#
|
|
345
|
-
|
|
346
|
-
raise KerykeionException("First object must be a CompositeSubjectModel instance.")
|
|
347
|
-
|
|
348
|
-
# Calculate aspects
|
|
349
|
-
self.aspects_list = NatalAspectsFactory.from_subject(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
|
|
350
324
|
|
|
351
325
|
# Screen size
|
|
352
326
|
self.height = self._DEFAULT_HEIGHT
|
|
353
327
|
self.width = self._DEFAULT_NATAL_WIDTH
|
|
354
328
|
|
|
355
|
-
#
|
|
356
|
-
self.location =
|
|
357
|
-
self.geolat = (self.first_obj.first_subject.lat + self.first_obj.second_subject.lat) / 2
|
|
358
|
-
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()
|
|
359
331
|
|
|
360
332
|
# Circle radii
|
|
361
333
|
self.first_circle_radius = 0
|
|
@@ -365,25 +337,8 @@ class KerykeionChartSVG:
|
|
|
365
337
|
elif self.chart_type == "Transit":
|
|
366
338
|
# --- TRANSIT CHART SETUP ---
|
|
367
339
|
|
|
368
|
-
#
|
|
369
|
-
|
|
370
|
-
raise KerykeionException("Second object is required for Transit charts.")
|
|
371
|
-
if not isinstance(self.first_obj, AstrologicalSubjectModel):
|
|
372
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
373
|
-
if not isinstance(second_obj, AstrologicalSubjectModel):
|
|
374
|
-
raise KerykeionException("Second object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
375
|
-
|
|
376
|
-
# Secondary subject setup
|
|
377
|
-
self.second_obj = second_obj
|
|
378
|
-
|
|
379
|
-
# Calculate aspects (transit to natal)
|
|
380
|
-
synastry_aspects_instance = SynastryAspectsFactory.from_subjects(
|
|
381
|
-
self.first_obj,
|
|
382
|
-
self.second_obj,
|
|
383
|
-
active_points=self.active_points,
|
|
384
|
-
active_aspects=active_aspects,
|
|
385
|
-
)
|
|
386
|
-
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
|
|
387
342
|
|
|
388
343
|
# Secondary subject available points
|
|
389
344
|
self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
|
|
@@ -395,11 +350,8 @@ class KerykeionChartSVG:
|
|
|
395
350
|
else:
|
|
396
351
|
self.width = self._DEFAULT_FULL_WIDTH
|
|
397
352
|
|
|
398
|
-
#
|
|
399
|
-
self.location = self.
|
|
400
|
-
self.geolat = self.second_obj.lat
|
|
401
|
-
self.geolon = self.second_obj.lng
|
|
402
|
-
self.t_name = self.language_settings["transit_name"]
|
|
353
|
+
# Get location and coordinates
|
|
354
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
403
355
|
|
|
404
356
|
# Circle radii
|
|
405
357
|
self.first_circle_radius = 0
|
|
@@ -409,25 +361,8 @@ class KerykeionChartSVG:
|
|
|
409
361
|
elif self.chart_type == "Synastry":
|
|
410
362
|
# --- SYNASTRY CHART SETUP ---
|
|
411
363
|
|
|
412
|
-
#
|
|
413
|
-
|
|
414
|
-
raise KerykeionException("Second object is required for Synastry charts.")
|
|
415
|
-
if not isinstance(self.first_obj, AstrologicalSubjectModel):
|
|
416
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
417
|
-
if not isinstance(second_obj, AstrologicalSubjectModel):
|
|
418
|
-
raise KerykeionException("Second object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
419
|
-
|
|
420
|
-
# Secondary subject setup
|
|
421
|
-
self.second_obj = second_obj
|
|
422
|
-
|
|
423
|
-
# Calculate aspects (natal to partner)
|
|
424
|
-
synastry_aspects_instance = SynastryAspectsFactory.from_subjects(
|
|
425
|
-
self.first_obj,
|
|
426
|
-
self.second_obj,
|
|
427
|
-
active_points=self.active_points,
|
|
428
|
-
active_aspects=active_aspects,
|
|
429
|
-
)
|
|
430
|
-
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
|
|
431
366
|
|
|
432
367
|
# Secondary subject available points
|
|
433
368
|
self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
|
|
@@ -436,38 +371,19 @@ class KerykeionChartSVG:
|
|
|
436
371
|
self.height = self._DEFAULT_HEIGHT
|
|
437
372
|
self.width = self._DEFAULT_FULL_WIDTH
|
|
438
373
|
|
|
439
|
-
#
|
|
440
|
-
self.location = self.
|
|
441
|
-
self.geolat = self.first_obj.lat
|
|
442
|
-
self.geolon = self.first_obj.lng
|
|
374
|
+
# Get location and coordinates
|
|
375
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
443
376
|
|
|
444
377
|
# Circle radii
|
|
445
378
|
self.first_circle_radius = 0
|
|
446
379
|
self.second_circle_radius = 36
|
|
447
380
|
self.third_circle_radius = 120
|
|
448
381
|
|
|
449
|
-
elif self.chart_type == "
|
|
382
|
+
elif self.chart_type == "DualReturnChart":
|
|
450
383
|
# --- RETURN CHART SETUP ---
|
|
451
384
|
|
|
452
|
-
#
|
|
453
|
-
|
|
454
|
-
raise KerykeionException("Second object is required for Return charts.")
|
|
455
|
-
if not isinstance(self.first_obj, AstrologicalSubjectModel):
|
|
456
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
457
|
-
if not isinstance(second_obj, PlanetReturnModel):
|
|
458
|
-
raise KerykeionException("Second object must be a PlanetReturnModel instance.")
|
|
459
|
-
|
|
460
|
-
# Secondary subject setup
|
|
461
|
-
self.second_obj = second_obj
|
|
462
|
-
|
|
463
|
-
# Calculate aspects (natal to return)
|
|
464
|
-
synastry_aspects_instance = SynastryAspectsFactory.from_subjects(
|
|
465
|
-
self.first_obj,
|
|
466
|
-
self.second_obj,
|
|
467
|
-
active_points=self.active_points,
|
|
468
|
-
active_aspects=active_aspects,
|
|
469
|
-
)
|
|
470
|
-
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
|
|
471
387
|
|
|
472
388
|
# Secondary subject available points
|
|
473
389
|
self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
|
|
@@ -476,93 +392,45 @@ class KerykeionChartSVG:
|
|
|
476
392
|
self.height = self._DEFAULT_HEIGHT
|
|
477
393
|
self.width = self._DEFAULT_ULTRA_WIDE_WIDTH
|
|
478
394
|
|
|
479
|
-
#
|
|
480
|
-
self.location = self.
|
|
481
|
-
self.geolat = self.first_obj.lat
|
|
482
|
-
self.geolon = self.first_obj.lng
|
|
395
|
+
# Get location and coordinates
|
|
396
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
483
397
|
|
|
484
398
|
# Circle radii
|
|
485
399
|
self.first_circle_radius = 0
|
|
486
400
|
self.second_circle_radius = 36
|
|
487
401
|
self.third_circle_radius = 120
|
|
488
402
|
|
|
489
|
-
elif self.chart_type == "
|
|
490
|
-
# ---
|
|
491
|
-
|
|
492
|
-
# Validate Subject
|
|
493
|
-
if not isinstance(self.first_obj, PlanetReturnModel):
|
|
494
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
403
|
+
elif self.chart_type == "SingleReturnChart":
|
|
404
|
+
# --- SINGLE WHEEL RETURN CHART SETUP ---
|
|
495
405
|
|
|
496
|
-
#
|
|
497
|
-
|
|
498
|
-
self.first_obj,
|
|
499
|
-
active_points=self.active_points,
|
|
500
|
-
active_aspects=active_aspects,
|
|
501
|
-
)
|
|
502
|
-
self.aspects_list = natal_aspects_instance.relevant_aspects
|
|
406
|
+
# Extract aspects from pre-computed chart data
|
|
407
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
503
408
|
|
|
504
409
|
# Screen size
|
|
505
410
|
self.height = self._DEFAULT_HEIGHT
|
|
506
411
|
self.width = self._DEFAULT_NATAL_WIDTH
|
|
507
412
|
|
|
508
|
-
#
|
|
509
|
-
self.location = self.
|
|
510
|
-
self.geolat = self.first_obj.lat
|
|
511
|
-
self.geolon = self.first_obj.lng
|
|
413
|
+
# Get location and coordinates
|
|
414
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
512
415
|
|
|
513
416
|
# Circle radii
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
self.third_circle_radius = 112
|
|
518
|
-
else:
|
|
519
|
-
self.first_circle_radius = 0
|
|
520
|
-
self.second_circle_radius = 36
|
|
521
|
-
self.third_circle_radius = 120
|
|
417
|
+
self.first_circle_radius = 0
|
|
418
|
+
self.second_circle_radius = 36
|
|
419
|
+
self.third_circle_radius = 120
|
|
522
420
|
|
|
523
421
|
# --------------------
|
|
524
|
-
# FINAL COMMON SETUP
|
|
422
|
+
# FINAL COMMON SETUP FROM CHART DATA
|
|
525
423
|
# --------------------
|
|
526
424
|
|
|
527
|
-
#
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
celestial_points_names,
|
|
533
|
-
self.first_obj,
|
|
534
|
-
self.second_obj,
|
|
535
|
-
)
|
|
536
|
-
else:
|
|
537
|
-
element_totals = calculate_element_points(
|
|
538
|
-
self.available_planets_setting,
|
|
539
|
-
celestial_points_names,
|
|
540
|
-
self.first_obj,
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
self.fire = element_totals["fire"]
|
|
544
|
-
self.earth = element_totals["earth"]
|
|
545
|
-
self.air = element_totals["air"]
|
|
546
|
-
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
|
|
547
430
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
self.available_planets_setting,
|
|
552
|
-
celestial_points_names,
|
|
553
|
-
self.first_obj,
|
|
554
|
-
self.second_obj,
|
|
555
|
-
)
|
|
556
|
-
else:
|
|
557
|
-
qualities_totals = calculate_quality_points(
|
|
558
|
-
self.available_planets_setting,
|
|
559
|
-
celestial_points_names,
|
|
560
|
-
self.first_obj,
|
|
561
|
-
)
|
|
562
|
-
|
|
563
|
-
self.cardinal = qualities_totals["cardinal"]
|
|
564
|
-
self.fixed = qualities_totals["fixed"]
|
|
565
|
-
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
|
|
566
434
|
|
|
567
435
|
# Set up theme
|
|
568
436
|
if theme not in get_args(KerykeionChartTheme) and theme is not None:
|
|
@@ -570,6 +438,176 @@ class KerykeionChartSVG:
|
|
|
570
438
|
|
|
571
439
|
self.set_up_theme(theme)
|
|
572
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
|
+
|
|
573
611
|
def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None:
|
|
574
612
|
"""
|
|
575
613
|
Load and apply a CSS theme for the chart visualization.
|
|
@@ -586,16 +624,6 @@ class KerykeionChartSVG:
|
|
|
586
624
|
with open(theme_dir / f"{theme}.css", "r") as f:
|
|
587
625
|
self.color_style_tag = f.read()
|
|
588
626
|
|
|
589
|
-
def set_output_directory(self, dir_path: Path) -> None:
|
|
590
|
-
"""
|
|
591
|
-
Set the directory where generated SVG files will be saved.
|
|
592
|
-
|
|
593
|
-
Args:
|
|
594
|
-
dir_path (Path): Target directory for SVG output.
|
|
595
|
-
"""
|
|
596
|
-
self.output_directory = dir_path
|
|
597
|
-
logging.info(f"Output directory set to: {self.output_directory}")
|
|
598
|
-
|
|
599
627
|
def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None:
|
|
600
628
|
"""
|
|
601
629
|
Load and parse chart configuration settings.
|
|
@@ -683,7 +711,89 @@ class KerykeionChartSVG:
|
|
|
683
711
|
)
|
|
684
712
|
return out
|
|
685
713
|
|
|
686
|
-
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:
|
|
687
797
|
"""
|
|
688
798
|
Assemble chart data and rendering instructions into a template dictionary.
|
|
689
799
|
|
|
@@ -691,7 +801,7 @@ class KerykeionChartSVG:
|
|
|
691
801
|
chart type and subjects.
|
|
692
802
|
|
|
693
803
|
Returns:
|
|
694
|
-
|
|
804
|
+
ChartTemplateModel: Populated structure of template variables.
|
|
695
805
|
"""
|
|
696
806
|
# Initialize template dictionary
|
|
697
807
|
template_dict: dict = {}
|
|
@@ -707,7 +817,6 @@ class KerykeionChartSVG:
|
|
|
707
817
|
|
|
708
818
|
# Set paper colors
|
|
709
819
|
template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"]
|
|
710
|
-
template_dict["paper_color_1"] = self.chart_colors_settings["paper_1"]
|
|
711
820
|
|
|
712
821
|
# Set background color based on transparent_background setting
|
|
713
822
|
if self.transparent_background:
|
|
@@ -715,7 +824,12 @@ class KerykeionChartSVG:
|
|
|
715
824
|
else:
|
|
716
825
|
template_dict["background_color"] = self.chart_colors_settings["paper_1"]
|
|
717
826
|
|
|
718
|
-
# 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
|
|
719
833
|
for planet in self.planets_settings:
|
|
720
834
|
planet_id = planet["id"]
|
|
721
835
|
template_dict[f"planets_color_{planet_id}"] = planet["color"]
|
|
@@ -733,10 +847,12 @@ class KerykeionChartSVG:
|
|
|
733
847
|
|
|
734
848
|
# Calculate element percentages
|
|
735
849
|
total_elements = self.fire + self.water + self.earth + self.air
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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"]
|
|
740
856
|
|
|
741
857
|
# Element Percentages
|
|
742
858
|
template_dict["elements_string"] = f"{self.language_settings.get('elements', 'Elements')}:"
|
|
@@ -748,9 +864,11 @@ class KerykeionChartSVG:
|
|
|
748
864
|
|
|
749
865
|
# Qualities Percentages
|
|
750
866
|
total_qualities = self.cardinal + self.fixed + self.mutable
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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"]
|
|
754
872
|
|
|
755
873
|
template_dict["qualities_string"] = f"{self.language_settings.get('qualities', 'Qualities')}:"
|
|
756
874
|
template_dict["cardinal_string"] = f"{self.language_settings.get('cardinal', 'Cardinal')} {cardinal_percentage}%"
|
|
@@ -760,13 +878,16 @@ class KerykeionChartSVG:
|
|
|
760
878
|
# Get houses list for main subject
|
|
761
879
|
first_subject_houses_list = get_houses_list(self.first_obj)
|
|
762
880
|
|
|
881
|
+
# Chart title
|
|
882
|
+
template_dict["stringTitle"] = self._get_chart_title()
|
|
883
|
+
|
|
763
884
|
# ------------------------------- #
|
|
764
885
|
# CHART TYPE SPECIFIC SETTINGS #
|
|
765
886
|
# ------------------------------- #
|
|
766
887
|
|
|
767
|
-
if self.chart_type
|
|
768
|
-
# Set viewbox
|
|
769
|
-
template_dict["viewbox"] = self.
|
|
888
|
+
if self.chart_type == "Natal":
|
|
889
|
+
# Set viewbox dynamically
|
|
890
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
770
891
|
|
|
771
892
|
# Rings and circles
|
|
772
893
|
template_dict["transitRing"] = ""
|
|
@@ -811,9 +932,6 @@ class KerykeionChartSVG:
|
|
|
811
932
|
)
|
|
812
933
|
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
813
934
|
|
|
814
|
-
# Chart title
|
|
815
|
-
template_dict["stringTitle"] = f'{self.first_obj.name} - {self.language_settings.get("birth_chart", "Birth Chart")}'
|
|
816
|
-
|
|
817
935
|
# Top left section
|
|
818
936
|
latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
|
|
819
937
|
longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
|
|
@@ -823,7 +941,8 @@ class KerykeionChartSVG:
|
|
|
823
941
|
template_dict["top_left_2"] = f"{self.language_settings['latitude']}: {latitude_string}"
|
|
824
942
|
template_dict["top_left_3"] = f"{self.language_settings['longitude']}: {longitude_string}"
|
|
825
943
|
template_dict["top_left_4"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
|
|
826
|
-
|
|
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
|
|
827
946
|
|
|
828
947
|
# Bottom left section
|
|
829
948
|
if self.first_obj.zodiac_type == "Tropic":
|
|
@@ -871,6 +990,7 @@ class KerykeionChartSVG:
|
|
|
871
990
|
c1=self.first_circle_radius,
|
|
872
991
|
c3=self.third_circle_radius,
|
|
873
992
|
chart_type=self.chart_type,
|
|
993
|
+
external_view=self.external_view,
|
|
874
994
|
)
|
|
875
995
|
|
|
876
996
|
template_dict["makePlanets"] = draw_planets(
|
|
@@ -881,6 +1001,7 @@ class KerykeionChartSVG:
|
|
|
881
1001
|
third_circle_radius=self.third_circle_radius,
|
|
882
1002
|
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
883
1003
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1004
|
+
external_view=self.external_view,
|
|
884
1005
|
)
|
|
885
1006
|
|
|
886
1007
|
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
@@ -895,8 +1016,8 @@ class KerykeionChartSVG:
|
|
|
895
1016
|
template_dict["makeHouseComparisonGrid"] = ""
|
|
896
1017
|
|
|
897
1018
|
elif self.chart_type == "Composite":
|
|
898
|
-
# Set viewbox
|
|
899
|
-
template_dict["viewbox"] = self.
|
|
1019
|
+
# Set viewbox dynamically
|
|
1020
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
900
1021
|
|
|
901
1022
|
# Rings and circles
|
|
902
1023
|
template_dict["transitRing"] = ""
|
|
@@ -941,9 +1062,6 @@ class KerykeionChartSVG:
|
|
|
941
1062
|
)
|
|
942
1063
|
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
943
1064
|
|
|
944
|
-
# Chart title
|
|
945
|
-
template_dict["stringTitle"] = f"{self.first_obj.first_subject.name} {self.language_settings['and_word']} {self.first_obj.second_subject.name}" # type: ignore
|
|
946
|
-
|
|
947
1065
|
# Top left section
|
|
948
1066
|
# First subject
|
|
949
1067
|
latitude = convert_latitude_coordinate_to_string(
|
|
@@ -1015,6 +1133,7 @@ class KerykeionChartSVG:
|
|
|
1015
1133
|
c1=self.first_circle_radius,
|
|
1016
1134
|
c3=self.third_circle_radius,
|
|
1017
1135
|
chart_type=self.chart_type,
|
|
1136
|
+
external_view=self.external_view,
|
|
1018
1137
|
)
|
|
1019
1138
|
|
|
1020
1139
|
template_dict["makePlanets"] = draw_planets(
|
|
@@ -1025,6 +1144,7 @@ class KerykeionChartSVG:
|
|
|
1025
1144
|
third_circle_radius=self.third_circle_radius,
|
|
1026
1145
|
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
1027
1146
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1147
|
+
external_view=self.external_view,
|
|
1028
1148
|
)
|
|
1029
1149
|
|
|
1030
1150
|
subject_name = f"{self.first_obj.first_subject.name} {self.language_settings['and_word']} {self.first_obj.second_subject.name}" # type: ignore
|
|
@@ -1055,11 +1175,8 @@ class KerykeionChartSVG:
|
|
|
1055
1175
|
template_dict["fixed_string"] = ""
|
|
1056
1176
|
template_dict["mutable_string"] = ""
|
|
1057
1177
|
|
|
1058
|
-
# Set viewbox
|
|
1059
|
-
|
|
1060
|
-
template_dict["viewbox"] = self._TRANSIT_CHART_WITH_TABLE_VIWBOX
|
|
1061
|
-
else:
|
|
1062
|
-
template_dict["viewbox"] = self._WIDE_CHART_VIEWBOX
|
|
1178
|
+
# Set viewbox dynamically
|
|
1179
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1063
1180
|
|
|
1064
1181
|
# Get houses list for secondary subject
|
|
1065
1182
|
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
@@ -1099,7 +1216,7 @@ class KerykeionChartSVG:
|
|
|
1099
1216
|
if self.double_chart_aspect_grid_type == "list":
|
|
1100
1217
|
title = f'{self.first_obj.name} - {self.language_settings.get("transit_aspects", "Transit Aspects")}'
|
|
1101
1218
|
template_dict["makeAspectGrid"] = ""
|
|
1102
|
-
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]
|
|
1103
1220
|
else:
|
|
1104
1221
|
template_dict["makeAspectGrid"] = ""
|
|
1105
1222
|
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
|
|
@@ -1112,9 +1229,6 @@ class KerykeionChartSVG:
|
|
|
1112
1229
|
|
|
1113
1230
|
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1114
1231
|
|
|
1115
|
-
# Chart title
|
|
1116
|
-
template_dict["stringTitle"] = f"{self.language_settings['transits']} {format_datetime_with_timezone(self.second_obj.iso_formatted_local_datetime)}" # type: ignore
|
|
1117
|
-
|
|
1118
1232
|
# Top left section
|
|
1119
1233
|
latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
|
|
1120
1234
|
longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
|
|
@@ -1177,6 +1291,7 @@ class KerykeionChartSVG:
|
|
|
1177
1291
|
c1=self.first_circle_radius,
|
|
1178
1292
|
c3=self.third_circle_radius,
|
|
1179
1293
|
chart_type=self.chart_type,
|
|
1294
|
+
external_view=self.external_view,
|
|
1180
1295
|
second_subject_houses_list=second_subject_houses_list,
|
|
1181
1296
|
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
1182
1297
|
)
|
|
@@ -1190,6 +1305,7 @@ class KerykeionChartSVG:
|
|
|
1190
1305
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1191
1306
|
chart_type=self.chart_type,
|
|
1192
1307
|
third_circle_radius=self.third_circle_radius,
|
|
1308
|
+
external_view=self.external_view,
|
|
1193
1309
|
)
|
|
1194
1310
|
|
|
1195
1311
|
# Planet grids
|
|
@@ -1215,8 +1331,8 @@ class KerykeionChartSVG:
|
|
|
1215
1331
|
|
|
1216
1332
|
# House comparison grid
|
|
1217
1333
|
house_comparison_factory = HouseComparisonFactory(
|
|
1218
|
-
first_subject=self.first_obj,
|
|
1219
|
-
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]
|
|
1220
1336
|
active_points=self.active_points,
|
|
1221
1337
|
)
|
|
1222
1338
|
house_comparison = house_comparison_factory.get_house_comparison()
|
|
@@ -1229,12 +1345,12 @@ class KerykeionChartSVG:
|
|
|
1229
1345
|
house_position_comparison_label=self.language_settings.get("house_position_comparison", "House Position Comparison"),
|
|
1230
1346
|
return_point_label=self.language_settings.get("transit_point", "Transit Point"),
|
|
1231
1347
|
natal_house_label=self.language_settings.get("house_position", "Natal House"),
|
|
1232
|
-
x_position=
|
|
1348
|
+
x_position=980,
|
|
1233
1349
|
)
|
|
1234
1350
|
|
|
1235
1351
|
elif self.chart_type == "Synastry":
|
|
1236
|
-
# Set viewbox
|
|
1237
|
-
template_dict["viewbox"] = self.
|
|
1352
|
+
# Set viewbox dynamically
|
|
1353
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1238
1354
|
|
|
1239
1355
|
# Get houses list for secondary subject
|
|
1240
1356
|
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
@@ -1274,10 +1390,10 @@ class KerykeionChartSVG:
|
|
|
1274
1390
|
if self.double_chart_aspect_grid_type == "list":
|
|
1275
1391
|
template_dict["makeAspectGrid"] = ""
|
|
1276
1392
|
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(
|
|
1277
|
-
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]
|
|
1278
1394
|
self.aspects_list,
|
|
1279
|
-
self.planets_settings,
|
|
1280
|
-
self.aspects_settings
|
|
1395
|
+
self.planets_settings, # type: ignore[arg-type]
|
|
1396
|
+
self.aspects_settings # type: ignore[arg-type]
|
|
1281
1397
|
)
|
|
1282
1398
|
else:
|
|
1283
1399
|
template_dict["makeAspectGrid"] = ""
|
|
@@ -1291,9 +1407,6 @@ class KerykeionChartSVG:
|
|
|
1291
1407
|
|
|
1292
1408
|
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1293
1409
|
|
|
1294
|
-
# Chart title
|
|
1295
|
-
template_dict["stringTitle"] = f"{self.first_obj.name} {self.language_settings['and_word']} {self.second_obj.name}" # type: ignore
|
|
1296
|
-
|
|
1297
1410
|
# Top left section
|
|
1298
1411
|
template_dict["top_left_0"] = f"{self.first_obj.name}:"
|
|
1299
1412
|
template_dict["top_left_1"] = f"{self.first_obj.city}, {self.first_obj.nation}" # type: ignore
|
|
@@ -1344,6 +1457,7 @@ class KerykeionChartSVG:
|
|
|
1344
1457
|
c1=self.first_circle_radius,
|
|
1345
1458
|
c3=self.third_circle_radius,
|
|
1346
1459
|
chart_type=self.chart_type,
|
|
1460
|
+
external_view=self.external_view,
|
|
1347
1461
|
second_subject_houses_list=second_subject_houses_list,
|
|
1348
1462
|
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
1349
1463
|
)
|
|
@@ -1357,6 +1471,7 @@ class KerykeionChartSVG:
|
|
|
1357
1471
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1358
1472
|
chart_type=self.chart_type,
|
|
1359
1473
|
third_circle_radius=self.third_circle_radius,
|
|
1474
|
+
external_view=self.external_view,
|
|
1360
1475
|
)
|
|
1361
1476
|
|
|
1362
1477
|
# Planet grid
|
|
@@ -1378,9 +1493,9 @@ class KerykeionChartSVG:
|
|
|
1378
1493
|
)
|
|
1379
1494
|
template_dict["makeHouseComparisonGrid"] = ""
|
|
1380
1495
|
|
|
1381
|
-
elif self.chart_type == "
|
|
1382
|
-
# Set viewbox
|
|
1383
|
-
template_dict["viewbox"] = self.
|
|
1496
|
+
elif self.chart_type == "DualReturnChart":
|
|
1497
|
+
# Set viewbox dynamically
|
|
1498
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1384
1499
|
|
|
1385
1500
|
# Get houses list for secondary subject
|
|
1386
1501
|
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
@@ -1420,7 +1535,7 @@ class KerykeionChartSVG:
|
|
|
1420
1535
|
if self.double_chart_aspect_grid_type == "list":
|
|
1421
1536
|
title = self.language_settings.get("return_aspects", "Natal to Return Aspects")
|
|
1422
1537
|
template_dict["makeAspectGrid"] = ""
|
|
1423
|
-
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]
|
|
1424
1539
|
else:
|
|
1425
1540
|
template_dict["makeAspectGrid"] = ""
|
|
1426
1541
|
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
|
|
@@ -1433,12 +1548,6 @@ class KerykeionChartSVG:
|
|
|
1433
1548
|
|
|
1434
1549
|
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1435
1550
|
|
|
1436
|
-
# Chart title
|
|
1437
|
-
if self.second_obj is not None and hasattr(self.second_obj, 'return_type') and self.second_obj.return_type == "Solar":
|
|
1438
|
-
template_dict["stringTitle"] = f"{self.first_obj.name} - {self.language_settings.get('solar_return', 'Solar Return')}"
|
|
1439
|
-
else:
|
|
1440
|
-
template_dict["stringTitle"] = f"{self.first_obj.name} - {self.language_settings.get('lunar_return', 'Lunar Return')}"
|
|
1441
|
-
|
|
1442
1551
|
|
|
1443
1552
|
# Top left section
|
|
1444
1553
|
# Subject
|
|
@@ -1510,6 +1619,7 @@ class KerykeionChartSVG:
|
|
|
1510
1619
|
c1=self.first_circle_radius,
|
|
1511
1620
|
c3=self.third_circle_radius,
|
|
1512
1621
|
chart_type=self.chart_type,
|
|
1622
|
+
external_view=self.external_view,
|
|
1513
1623
|
second_subject_houses_list=second_subject_houses_list,
|
|
1514
1624
|
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
1515
1625
|
)
|
|
@@ -1523,6 +1633,7 @@ class KerykeionChartSVG:
|
|
|
1523
1633
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1524
1634
|
chart_type=self.chart_type,
|
|
1525
1635
|
third_circle_radius=self.third_circle_radius,
|
|
1636
|
+
external_view=self.external_view,
|
|
1526
1637
|
)
|
|
1527
1638
|
|
|
1528
1639
|
# Planet grid
|
|
@@ -1550,8 +1661,8 @@ class KerykeionChartSVG:
|
|
|
1550
1661
|
)
|
|
1551
1662
|
|
|
1552
1663
|
house_comparison_factory = HouseComparisonFactory(
|
|
1553
|
-
first_subject=self.first_obj,
|
|
1554
|
-
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]
|
|
1555
1666
|
active_points=self.active_points,
|
|
1556
1667
|
)
|
|
1557
1668
|
house_comparison = house_comparison_factory.get_house_comparison()
|
|
@@ -1563,13 +1674,13 @@ class KerykeionChartSVG:
|
|
|
1563
1674
|
points_owner_subject_number=2, # The second subject is the Solar Return
|
|
1564
1675
|
house_position_comparison_label=self.language_settings.get("house_position_comparison", "House Position Comparison"),
|
|
1565
1676
|
return_point_label=self.language_settings.get("return_point", "Return Point"),
|
|
1566
|
-
return_label=self.language_settings.get("Return", "
|
|
1677
|
+
return_label=self.language_settings.get("Return", "DualReturnChart"),
|
|
1567
1678
|
radix_label=self.language_settings.get("Natal", "Natal"),
|
|
1568
1679
|
)
|
|
1569
1680
|
|
|
1570
|
-
elif self.chart_type == "
|
|
1571
|
-
# Set viewbox
|
|
1572
|
-
template_dict["viewbox"] = self.
|
|
1681
|
+
elif self.chart_type == "SingleReturnChart":
|
|
1682
|
+
# Set viewbox dynamically
|
|
1683
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1573
1684
|
|
|
1574
1685
|
# Rings and circles
|
|
1575
1686
|
template_dict["transitRing"] = ""
|
|
@@ -1614,9 +1725,6 @@ class KerykeionChartSVG:
|
|
|
1614
1725
|
)
|
|
1615
1726
|
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
1616
1727
|
|
|
1617
|
-
# Chart title
|
|
1618
|
-
template_dict["stringTitle"] = self.first_obj.name
|
|
1619
|
-
|
|
1620
1728
|
# Top left section
|
|
1621
1729
|
latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
|
|
1622
1730
|
longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
|
|
@@ -1678,6 +1786,7 @@ class KerykeionChartSVG:
|
|
|
1678
1786
|
c1=self.first_circle_radius,
|
|
1679
1787
|
c3=self.third_circle_radius,
|
|
1680
1788
|
chart_type=self.chart_type,
|
|
1789
|
+
external_view=self.external_view,
|
|
1681
1790
|
)
|
|
1682
1791
|
|
|
1683
1792
|
template_dict["makePlanets"] = draw_planets(
|
|
@@ -1688,6 +1797,7 @@ class KerykeionChartSVG:
|
|
|
1688
1797
|
third_circle_radius=self.third_circle_radius,
|
|
1689
1798
|
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
1690
1799
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1800
|
+
external_view=self.external_view,
|
|
1691
1801
|
)
|
|
1692
1802
|
|
|
1693
1803
|
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
@@ -1701,9 +1811,9 @@ class KerykeionChartSVG:
|
|
|
1701
1811
|
template_dict["makeSecondaryPlanetGrid"] = ""
|
|
1702
1812
|
template_dict["makeHouseComparisonGrid"] = ""
|
|
1703
1813
|
|
|
1704
|
-
return
|
|
1814
|
+
return ChartTemplateModel(**template_dict)
|
|
1705
1815
|
|
|
1706
|
-
def
|
|
1816
|
+
def generate_svg_string(self, minify: bool = False, remove_css_variables=False) -> str:
|
|
1707
1817
|
"""
|
|
1708
1818
|
Render the full chart SVG as a string.
|
|
1709
1819
|
|
|
@@ -1724,11 +1834,11 @@ class KerykeionChartSVG:
|
|
|
1724
1834
|
|
|
1725
1835
|
# read template
|
|
1726
1836
|
with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f:
|
|
1727
|
-
template = Template(f.read()).substitute(td)
|
|
1837
|
+
template = Template(f.read()).substitute(td.model_dump())
|
|
1728
1838
|
|
|
1729
1839
|
# return filename
|
|
1730
1840
|
|
|
1731
|
-
logging.debug(f"Template dictionary
|
|
1841
|
+
logging.debug(f"Template dictionary has {len(td.model_dump())} fields")
|
|
1732
1842
|
|
|
1733
1843
|
self._create_template_dictionary()
|
|
1734
1844
|
|
|
@@ -1743,36 +1853,50 @@ class KerykeionChartSVG:
|
|
|
1743
1853
|
|
|
1744
1854
|
return template
|
|
1745
1855
|
|
|
1746
|
-
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):
|
|
1747
1857
|
"""
|
|
1748
1858
|
Generate and save the full chart SVG to disk.
|
|
1749
1859
|
|
|
1750
|
-
Calls
|
|
1751
|
-
"{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.
|
|
1752
1862
|
|
|
1753
1863
|
Args:
|
|
1754
|
-
|
|
1755
|
-
|
|
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.
|
|
1756
1870
|
|
|
1757
1871
|
Returns:
|
|
1758
1872
|
None
|
|
1759
1873
|
"""
|
|
1760
1874
|
|
|
1761
|
-
self.template = self.
|
|
1875
|
+
self.template = self.generate_svg_string(minify, remove_css_variables)
|
|
1762
1876
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
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"
|
|
1767
1883
|
else:
|
|
1768
|
-
|
|
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"
|
|
1769
1893
|
|
|
1770
1894
|
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
1771
1895
|
output_file.write(self.template)
|
|
1772
1896
|
|
|
1773
1897
|
print(f"SVG Generated Correctly in: {chartname}")
|
|
1774
1898
|
|
|
1775
|
-
def
|
|
1899
|
+
def generate_wheel_only_svg_string(self, minify: bool = False, remove_css_variables=False):
|
|
1776
1900
|
"""
|
|
1777
1901
|
Render the wheel-only chart SVG as a string.
|
|
1778
1902
|
|
|
@@ -1796,7 +1920,9 @@ class KerykeionChartSVG:
|
|
|
1796
1920
|
template = f.read()
|
|
1797
1921
|
|
|
1798
1922
|
template_dict = self._create_template_dictionary()
|
|
1799
|
-
|
|
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})
|
|
1800
1926
|
|
|
1801
1927
|
if remove_css_variables:
|
|
1802
1928
|
template = inline_css_variables_in_svg(template)
|
|
@@ -1809,30 +1935,44 @@ class KerykeionChartSVG:
|
|
|
1809
1935
|
|
|
1810
1936
|
return template
|
|
1811
1937
|
|
|
1812
|
-
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):
|
|
1813
1939
|
"""
|
|
1814
1940
|
Generate and save wheel-only chart SVG to disk.
|
|
1815
1941
|
|
|
1816
|
-
Calls
|
|
1817
|
-
"{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.
|
|
1818
1944
|
|
|
1819
1945
|
Args:
|
|
1820
|
-
|
|
1821
|
-
|
|
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.
|
|
1822
1952
|
|
|
1823
1953
|
Returns:
|
|
1824
1954
|
None
|
|
1825
1955
|
"""
|
|
1826
1956
|
|
|
1827
|
-
template = self.
|
|
1828
|
-
|
|
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"
|
|
1829
1969
|
|
|
1830
1970
|
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
1831
1971
|
output_file.write(template)
|
|
1832
1972
|
|
|
1833
1973
|
print(f"SVG Generated Correctly in: {chartname}")
|
|
1834
1974
|
|
|
1835
|
-
def
|
|
1975
|
+
def generate_aspect_grid_only_svg_string(self, minify: bool = False, remove_css_variables=False):
|
|
1836
1976
|
"""
|
|
1837
1977
|
Render the aspect-grid-only chart SVG as a string.
|
|
1838
1978
|
|
|
@@ -1857,7 +1997,7 @@ class KerykeionChartSVG:
|
|
|
1857
1997
|
|
|
1858
1998
|
template_dict = self._create_template_dictionary()
|
|
1859
1999
|
|
|
1860
|
-
if self.chart_type in ["Transit", "Synastry", "
|
|
2000
|
+
if self.chart_type in ["Transit", "Synastry", "DualReturnChart"]:
|
|
1861
2001
|
aspects_grid = draw_transit_aspect_grid(
|
|
1862
2002
|
self.chart_colors_settings["paper_0"],
|
|
1863
2003
|
self.available_planets_setting,
|
|
@@ -1872,7 +2012,10 @@ class KerykeionChartSVG:
|
|
|
1872
2012
|
y_start=250,
|
|
1873
2013
|
)
|
|
1874
2014
|
|
|
1875
|
-
|
|
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})
|
|
1876
2019
|
|
|
1877
2020
|
if remove_css_variables:
|
|
1878
2021
|
template = inline_css_variables_in_svg(template)
|
|
@@ -1885,42 +2028,55 @@ class KerykeionChartSVG:
|
|
|
1885
2028
|
|
|
1886
2029
|
return template
|
|
1887
2030
|
|
|
1888
|
-
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):
|
|
1889
2032
|
"""
|
|
1890
2033
|
Generate and save aspect-grid-only chart SVG to disk.
|
|
1891
2034
|
|
|
1892
|
-
Calls
|
|
1893
|
-
"{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.
|
|
1894
2037
|
|
|
1895
2038
|
Args:
|
|
1896
|
-
|
|
1897
|
-
|
|
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.
|
|
1898
2045
|
|
|
1899
2046
|
Returns:
|
|
1900
2047
|
None
|
|
1901
2048
|
"""
|
|
1902
2049
|
|
|
1903
|
-
template = self.
|
|
1904
|
-
|
|
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"
|
|
1905
2062
|
|
|
1906
2063
|
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
1907
2064
|
output_file.write(template)
|
|
1908
2065
|
|
|
1909
2066
|
print(f"SVG Generated Correctly in: {chartname}")
|
|
1910
2067
|
|
|
1911
|
-
|
|
1912
2068
|
if __name__ == "__main__":
|
|
1913
2069
|
from kerykeion.utilities import setup_logging
|
|
1914
2070
|
from kerykeion.planetary_return_factory import PlanetaryReturnFactory
|
|
1915
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
|
|
1916
2074
|
|
|
1917
|
-
ACTIVE_PLANETS: list[AstrologicalPoint] =
|
|
1918
|
-
|
|
1919
|
-
]
|
|
1920
|
-
|
|
2075
|
+
ACTIVE_PLANETS: list[AstrologicalPoint] = DEFAULT_ACTIVE_POINTS
|
|
2076
|
+
# ACTIVE_PLANETS: list[AstrologicalPoint] = ALL_ACTIVE_POINTS
|
|
1921
2077
|
setup_logging(level="info")
|
|
1922
2078
|
|
|
1923
|
-
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)
|
|
1924
2080
|
|
|
1925
2081
|
return_factory = PlanetaryReturnFactory(
|
|
1926
2082
|
subject,
|
|
@@ -1933,101 +2089,118 @@ if __name__ == "__main__":
|
|
|
1933
2089
|
)
|
|
1934
2090
|
|
|
1935
2091
|
###
|
|
1936
|
-
## Birth Chart
|
|
1937
|
-
|
|
1938
|
-
|
|
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,
|
|
1939
2099
|
chart_language="IT",
|
|
1940
2100
|
theme="strawberry",
|
|
1941
|
-
active_points=ACTIVE_PLANETS,
|
|
1942
2101
|
)
|
|
1943
|
-
birth_chart.
|
|
2102
|
+
birth_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1944
2103
|
|
|
1945
2104
|
###
|
|
1946
|
-
## Solar Return Chart
|
|
2105
|
+
## Solar Return Chart - NEW APPROACH with ChartDataFactory
|
|
1947
2106
|
solar_return = return_factory.next_return_from_iso_formatted_time(
|
|
1948
2107
|
"2025-01-09T18:30:00+01:00", # UTC+1
|
|
1949
2108
|
return_type="Solar",
|
|
1950
2109
|
)
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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,
|
|
1954
2117
|
chart_language="IT",
|
|
1955
2118
|
theme="classic",
|
|
1956
|
-
active_points=ACTIVE_PLANETS,
|
|
1957
2119
|
)
|
|
1958
2120
|
|
|
1959
|
-
solar_return_chart.
|
|
2121
|
+
solar_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1960
2122
|
|
|
1961
2123
|
###
|
|
1962
|
-
## Single wheel return
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
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,
|
|
1967
2131
|
chart_language="IT",
|
|
1968
2132
|
theme="dark",
|
|
1969
|
-
active_points=ACTIVE_PLANETS,
|
|
1970
2133
|
)
|
|
1971
2134
|
|
|
1972
|
-
single_wheel_return_chart.
|
|
2135
|
+
single_wheel_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1973
2136
|
|
|
1974
2137
|
###
|
|
1975
|
-
## Lunar return
|
|
2138
|
+
## Lunar return - NEW APPROACH with ChartDataFactory
|
|
1976
2139
|
lunar_return = return_factory.next_return_from_iso_formatted_time(
|
|
1977
2140
|
"2025-01-09T18:30:00+01:00", # UTC+1
|
|
1978
2141
|
return_type="Lunar",
|
|
1979
2142
|
)
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
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,
|
|
1984
2150
|
chart_language="IT",
|
|
1985
2151
|
theme="dark",
|
|
1986
|
-
active_points=ACTIVE_PLANETS,
|
|
1987
2152
|
)
|
|
1988
|
-
lunar_return_chart.
|
|
2153
|
+
lunar_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1989
2154
|
|
|
1990
2155
|
###
|
|
1991
|
-
## Transit Chart
|
|
2156
|
+
## Transit Chart - NEW APPROACH with ChartDataFactory
|
|
1992
2157
|
transit = AstrologicalSubjectFactory.from_iso_utc_time(
|
|
1993
2158
|
"Transit",
|
|
1994
2159
|
"2021-10-04T18:30:00+01:00",
|
|
1995
2160
|
)
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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,
|
|
2000
2168
|
chart_language="IT",
|
|
2001
2169
|
theme="dark",
|
|
2002
|
-
active_points=ACTIVE_PLANETS
|
|
2003
2170
|
)
|
|
2004
|
-
transit_chart.
|
|
2171
|
+
transit_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2005
2172
|
|
|
2006
2173
|
###
|
|
2007
|
-
## Synastry Chart
|
|
2174
|
+
## Synastry Chart - NEW APPROACH with ChartDataFactory
|
|
2008
2175
|
second_subject = AstrologicalSubjectFactory.from_birth_data("Yoko Ono", 1933, 2, 18, 18, 30, "Tokyo", "JP")
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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,
|
|
2013
2183
|
chart_language="IT",
|
|
2014
2184
|
theme="dark",
|
|
2015
|
-
active_points=ACTIVE_PLANETS
|
|
2016
2185
|
)
|
|
2017
|
-
synastry_chart.
|
|
2186
|
+
synastry_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2018
2187
|
|
|
2019
2188
|
##
|
|
2020
|
-
# Transit Chart with Grid
|
|
2189
|
+
# Transit Chart with Grid - NEW APPROACH with ChartDataFactory
|
|
2021
2190
|
subject.name = "Grid"
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
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,
|
|
2026
2198
|
chart_language="IT",
|
|
2027
2199
|
theme="dark",
|
|
2028
|
-
active_points=ACTIVE_PLANETS,
|
|
2029
2200
|
double_chart_aspect_grid_type="table"
|
|
2030
2201
|
)
|
|
2031
|
-
transit_chart_with_grid.
|
|
2032
|
-
transit_chart_with_grid.
|
|
2033
|
-
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!")
|