kerykeion 5.0.0a12__py3-none-any.whl → 5.0.0b2__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} +688 -440
- kerykeion/charts/charts_utils.py +157 -94
- kerykeion/charts/draw_planets.py +38 -28
- kerykeion/charts/templates/aspect_grid_only.xml +188 -17
- kerykeion/charts/templates/chart.xml +153 -47
- 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 +367 -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 +132 -42
- 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.0b2.dist-info}/METADATA +507 -120
- kerykeion-5.0.0b2.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.0b2.dist-info}/WHEEL +0 -0
- {kerykeion-5.0.0a12.dist-info → kerykeion-5.0.0b2.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,75 @@ 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
|
|
163
|
+
|
|
164
|
+
_BASE_VERTICAL_OFFSETS = {
|
|
165
|
+
"wheel": 50,
|
|
166
|
+
"grid": 0,
|
|
167
|
+
"aspect_grid": 50,
|
|
168
|
+
"aspect_list": 50,
|
|
169
|
+
"title": 0,
|
|
170
|
+
"elements": 0,
|
|
171
|
+
"qualities": 0,
|
|
172
|
+
"lunar_phase": 518,
|
|
173
|
+
"bottom_left": 0,
|
|
174
|
+
}
|
|
175
|
+
_MAX_TOP_SHIFT = 80
|
|
176
|
+
_TOP_SHIFT_FACTOR = 2
|
|
177
|
+
_ROW_HEIGHT = 8
|
|
160
178
|
|
|
161
179
|
_BASIC_CHART_VIEWBOX = f"0 0 {_DEFAULT_NATAL_WIDTH} {_DEFAULT_HEIGHT}"
|
|
162
180
|
_WIDE_CHART_VIEWBOX = f"0 0 {_DEFAULT_FULL_WIDTH} 546.0"
|
|
@@ -167,9 +185,6 @@ class KerykeionChartSVG:
|
|
|
167
185
|
first_obj: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel]
|
|
168
186
|
second_obj: Union[AstrologicalSubjectModel, PlanetReturnModel, None]
|
|
169
187
|
chart_type: ChartType
|
|
170
|
-
new_output_directory: Union[Path, None]
|
|
171
|
-
new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
|
|
172
|
-
output_directory: Path
|
|
173
188
|
new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
|
|
174
189
|
theme: Union[KerykeionChartTheme, None]
|
|
175
190
|
double_chart_aspect_grid_type: Literal["list", "table"]
|
|
@@ -177,6 +192,8 @@ class KerykeionChartSVG:
|
|
|
177
192
|
active_points: List[AstrologicalPoint]
|
|
178
193
|
active_aspects: List[ActiveAspect]
|
|
179
194
|
transparent_background: bool
|
|
195
|
+
external_view: bool
|
|
196
|
+
custom_title: Union[str, None]
|
|
180
197
|
|
|
181
198
|
# Internal properties
|
|
182
199
|
fire: float
|
|
@@ -189,8 +206,8 @@ class KerykeionChartSVG:
|
|
|
189
206
|
width: Union[float, int]
|
|
190
207
|
language_settings: dict
|
|
191
208
|
chart_colors_settings: dict
|
|
192
|
-
planets_settings: dict
|
|
193
|
-
aspects_settings: dict
|
|
209
|
+
planets_settings: list[dict[Any, Any]]
|
|
210
|
+
aspects_settings: list[dict[Any, Any]]
|
|
194
211
|
available_planets_setting: List[KerykeionSettingsCelestialPointModel]
|
|
195
212
|
height: float
|
|
196
213
|
location: str
|
|
@@ -200,34 +217,28 @@ class KerykeionChartSVG:
|
|
|
200
217
|
|
|
201
218
|
def __init__(
|
|
202
219
|
self,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
second_obj: Union[AstrologicalSubjectModel, PlanetReturnModel, None] = None,
|
|
206
|
-
new_output_directory: Union[str, None] = None,
|
|
220
|
+
chart_data: "ChartDataModel",
|
|
221
|
+
*,
|
|
207
222
|
new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None,
|
|
208
223
|
theme: Union[KerykeionChartTheme, None] = "classic",
|
|
209
224
|
double_chart_aspect_grid_type: Literal["list", "table"] = "list",
|
|
210
225
|
chart_language: KerykeionChartLanguage = "EN",
|
|
211
|
-
|
|
212
|
-
active_aspects: list[ActiveAspect]= DEFAULT_ACTIVE_ASPECTS,
|
|
213
|
-
*,
|
|
226
|
+
external_view: bool = False,
|
|
214
227
|
transparent_background: bool = False,
|
|
215
228
|
colors_settings: dict = DEFAULT_CHART_COLORS,
|
|
216
229
|
celestial_points_settings: list[dict] = DEFAULT_CELESTIAL_POINTS_SETTINGS,
|
|
217
230
|
aspects_settings: list[dict] = DEFAULT_CHART_ASPECTS_SETTINGS,
|
|
231
|
+
custom_title: Union[str, None] = None,
|
|
232
|
+
auto_size: bool = True,
|
|
233
|
+
padding: int = 20,
|
|
218
234
|
):
|
|
219
235
|
"""
|
|
220
|
-
Initialize the chart
|
|
236
|
+
Initialize the chart visualizer with pre-computed chart data.
|
|
221
237
|
|
|
222
238
|
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.
|
|
239
|
+
chart_data (ChartDataModel):
|
|
240
|
+
Pre-computed chart data from ChartDataFactory containing all subjects,
|
|
241
|
+
aspects, element/quality distributions, and other analytical data.
|
|
231
242
|
new_settings_file (Path, dict, or KerykeionSettingsModel, optional):
|
|
232
243
|
Custom settings source for chart colors, fonts, and aspects.
|
|
233
244
|
theme (KerykeionChartTheme or None, optional):
|
|
@@ -236,99 +247,91 @@ class KerykeionChartSVG:
|
|
|
236
247
|
Layout style for double-chart aspect grids ('list' or 'table').
|
|
237
248
|
chart_language (KerykeionChartLanguage, optional):
|
|
238
249
|
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.
|
|
250
|
+
external_view (bool, optional):
|
|
251
|
+
Whether to use external visualization (planets on outer ring) for single-subject charts. Defaults to False.
|
|
243
252
|
transparent_background (bool, optional):
|
|
244
253
|
Whether to use a transparent background instead of the theme color. Defaults to False.
|
|
254
|
+
custom_title (str or None, optional):
|
|
255
|
+
Custom title for the chart. If None, the default title will be used based on chart type. Defaults to None.
|
|
245
256
|
"""
|
|
246
257
|
# --------------------
|
|
247
258
|
# COMMON INITIALIZATION
|
|
248
259
|
# --------------------
|
|
249
|
-
home_directory = Path.home()
|
|
250
260
|
self.new_settings_file = new_settings_file
|
|
251
261
|
self.chart_language = chart_language
|
|
252
|
-
self.active_aspects = active_aspects
|
|
253
|
-
self.chart_type = chart_type
|
|
254
262
|
self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
|
|
255
263
|
self.transparent_background = transparent_background
|
|
264
|
+
self.external_view = external_view
|
|
256
265
|
self.chart_colors_settings = colors_settings
|
|
257
266
|
self.planets_settings = celestial_points_settings
|
|
258
267
|
self.aspects_settings = aspects_settings
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
self.
|
|
268
|
+
self.custom_title = custom_title
|
|
269
|
+
self.auto_size = auto_size
|
|
270
|
+
self._padding = padding
|
|
271
|
+
self._vertical_offsets: dict[str, int] = self._BASE_VERTICAL_OFFSETS.copy()
|
|
272
|
+
|
|
273
|
+
# Extract data from ChartDataModel
|
|
274
|
+
self.chart_data = chart_data
|
|
275
|
+
self.chart_type = chart_data.chart_type
|
|
276
|
+
self.active_points = chart_data.active_points
|
|
277
|
+
self.active_aspects = chart_data.active_aspects
|
|
278
|
+
|
|
279
|
+
# Extract subjects based on chart type
|
|
280
|
+
if chart_data.chart_type in ["Natal", "Composite", "SingleReturnChart"]:
|
|
281
|
+
# SingleChartDataModel
|
|
282
|
+
self.first_obj = getattr(chart_data, 'subject')
|
|
283
|
+
self.second_obj = None
|
|
284
|
+
|
|
285
|
+
else: # DualChartDataModel for Transit, Synastry, DualReturnChart
|
|
286
|
+
self.first_obj = getattr(chart_data, 'first_subject')
|
|
287
|
+
self.second_obj = getattr(chart_data, 'second_subject')
|
|
279
288
|
|
|
280
289
|
# Load settings
|
|
281
290
|
self.parse_json_settings(new_settings_file)
|
|
282
291
|
|
|
283
|
-
# Primary subject
|
|
284
|
-
self.first_obj = first_obj
|
|
285
|
-
|
|
286
292
|
# Default radius for all charts
|
|
287
293
|
self.main_radius = 240
|
|
288
294
|
|
|
289
|
-
# Configure available planets
|
|
295
|
+
# Configure available planets from chart data
|
|
290
296
|
self.available_planets_setting = []
|
|
291
297
|
for body in self.planets_settings:
|
|
292
298
|
if body["name"] in self.active_points:
|
|
293
299
|
body["is_active"] = True
|
|
294
|
-
self.available_planets_setting.append(body)
|
|
300
|
+
self.available_planets_setting.append(body) # type: ignore[arg-type]
|
|
301
|
+
|
|
302
|
+
active_points_count = len(self.available_planets_setting)
|
|
303
|
+
if active_points_count > 24:
|
|
304
|
+
logging.warning(
|
|
305
|
+
"ChartDrawer detected %s active celestial points; rendering may look crowded beyond 24.",
|
|
306
|
+
active_points_count,
|
|
307
|
+
)
|
|
295
308
|
|
|
296
309
|
# Set available celestial points
|
|
297
310
|
available_celestial_points_names = [body["name"].lower() for body in self.available_planets_setting]
|
|
298
311
|
self.available_kerykeion_celestial_points = []
|
|
299
312
|
for body in available_celestial_points_names:
|
|
300
|
-
|
|
313
|
+
if hasattr(self.first_obj, body):
|
|
314
|
+
self.available_kerykeion_celestial_points.append(self.first_obj.get(body)) # type: ignore[arg-type]
|
|
301
315
|
|
|
302
316
|
# ------------------------
|
|
303
|
-
# CHART TYPE SPECIFIC SETUP
|
|
317
|
+
# CHART TYPE SPECIFIC SETUP FROM CHART DATA
|
|
304
318
|
# ------------------------
|
|
305
319
|
|
|
306
|
-
if self.chart_type
|
|
307
|
-
# --- NATAL
|
|
308
|
-
|
|
309
|
-
# Validate Subject
|
|
310
|
-
if not isinstance(self.first_obj, AstrologicalSubjectModel):
|
|
311
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
320
|
+
if self.chart_type == "Natal":
|
|
321
|
+
# --- NATAL CHART SETUP ---
|
|
312
322
|
|
|
313
|
-
#
|
|
314
|
-
|
|
315
|
-
self.first_obj,
|
|
316
|
-
active_points=self.active_points,
|
|
317
|
-
active_aspects=active_aspects,
|
|
318
|
-
)
|
|
319
|
-
self.aspects_list = aspects_instance.relevant_aspects
|
|
323
|
+
# Extract aspects from pre-computed chart data
|
|
324
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
320
325
|
|
|
321
326
|
# Screen size
|
|
322
327
|
self.height = self._DEFAULT_HEIGHT
|
|
323
328
|
self.width = self._DEFAULT_NATAL_WIDTH
|
|
324
329
|
|
|
325
|
-
#
|
|
326
|
-
self.location = self.
|
|
327
|
-
self.geolat = self.first_obj.lat
|
|
328
|
-
self.geolon = self.first_obj.lng
|
|
330
|
+
# Get location and coordinates
|
|
331
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
329
332
|
|
|
330
|
-
# Circle radii
|
|
331
|
-
if self.
|
|
333
|
+
# Circle radii - depends on external_view
|
|
334
|
+
if self.external_view:
|
|
332
335
|
self.first_circle_radius = 56
|
|
333
336
|
self.second_circle_radius = 92
|
|
334
337
|
self.third_circle_radius = 112
|
|
@@ -340,21 +343,15 @@ class KerykeionChartSVG:
|
|
|
340
343
|
elif self.chart_type == "Composite":
|
|
341
344
|
# --- COMPOSITE CHART SETUP ---
|
|
342
345
|
|
|
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
|
|
346
|
+
# Extract aspects from pre-computed chart data
|
|
347
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
349
348
|
|
|
350
349
|
# Screen size
|
|
351
350
|
self.height = self._DEFAULT_HEIGHT
|
|
352
351
|
self.width = self._DEFAULT_NATAL_WIDTH
|
|
353
352
|
|
|
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
|
|
353
|
+
# Get location and coordinates
|
|
354
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
358
355
|
|
|
359
356
|
# Circle radii
|
|
360
357
|
self.first_circle_radius = 0
|
|
@@ -364,25 +361,8 @@ class KerykeionChartSVG:
|
|
|
364
361
|
elif self.chart_type == "Transit":
|
|
365
362
|
# --- TRANSIT CHART SETUP ---
|
|
366
363
|
|
|
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
|
|
364
|
+
# Extract aspects from pre-computed chart data
|
|
365
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
386
366
|
|
|
387
367
|
# Secondary subject available points
|
|
388
368
|
self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
|
|
@@ -394,11 +374,8 @@ class KerykeionChartSVG:
|
|
|
394
374
|
else:
|
|
395
375
|
self.width = self._DEFAULT_FULL_WIDTH
|
|
396
376
|
|
|
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"]
|
|
377
|
+
# Get location and coordinates
|
|
378
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
402
379
|
|
|
403
380
|
# Circle radii
|
|
404
381
|
self.first_circle_radius = 0
|
|
@@ -408,25 +385,8 @@ class KerykeionChartSVG:
|
|
|
408
385
|
elif self.chart_type == "Synastry":
|
|
409
386
|
# --- SYNASTRY CHART SETUP ---
|
|
410
387
|
|
|
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
|
|
388
|
+
# Extract aspects from pre-computed chart data
|
|
389
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
430
390
|
|
|
431
391
|
# Secondary subject available points
|
|
432
392
|
self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
|
|
@@ -435,38 +395,19 @@ class KerykeionChartSVG:
|
|
|
435
395
|
self.height = self._DEFAULT_HEIGHT
|
|
436
396
|
self.width = self._DEFAULT_FULL_WIDTH
|
|
437
397
|
|
|
438
|
-
#
|
|
439
|
-
self.location = self.
|
|
440
|
-
self.geolat = self.first_obj.lat
|
|
441
|
-
self.geolon = self.first_obj.lng
|
|
398
|
+
# Get location and coordinates
|
|
399
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
442
400
|
|
|
443
401
|
# Circle radii
|
|
444
402
|
self.first_circle_radius = 0
|
|
445
403
|
self.second_circle_radius = 36
|
|
446
404
|
self.third_circle_radius = 120
|
|
447
405
|
|
|
448
|
-
elif self.chart_type == "
|
|
406
|
+
elif self.chart_type == "DualReturnChart":
|
|
449
407
|
# --- RETURN CHART SETUP ---
|
|
450
408
|
|
|
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
|
|
409
|
+
# Extract aspects from pre-computed chart data
|
|
410
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
470
411
|
|
|
471
412
|
# Secondary subject available points
|
|
472
413
|
self.t_available_kerykeion_celestial_points = self.available_kerykeion_celestial_points
|
|
@@ -475,93 +416,45 @@ class KerykeionChartSVG:
|
|
|
475
416
|
self.height = self._DEFAULT_HEIGHT
|
|
476
417
|
self.width = self._DEFAULT_ULTRA_WIDE_WIDTH
|
|
477
418
|
|
|
478
|
-
#
|
|
479
|
-
self.location = self.
|
|
480
|
-
self.geolat = self.first_obj.lat
|
|
481
|
-
self.geolon = self.first_obj.lng
|
|
419
|
+
# Get location and coordinates
|
|
420
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
482
421
|
|
|
483
422
|
# Circle radii
|
|
484
423
|
self.first_circle_radius = 0
|
|
485
424
|
self.second_circle_radius = 36
|
|
486
425
|
self.third_circle_radius = 120
|
|
487
426
|
|
|
488
|
-
elif self.chart_type == "
|
|
489
|
-
# ---
|
|
427
|
+
elif self.chart_type == "SingleReturnChart":
|
|
428
|
+
# --- SINGLE WHEEL RETURN CHART SETUP ---
|
|
490
429
|
|
|
491
|
-
#
|
|
492
|
-
|
|
493
|
-
raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
|
|
494
|
-
|
|
495
|
-
# Calculate aspects
|
|
496
|
-
aspects_instance = AspectsFactory.single_chart_aspects(
|
|
497
|
-
self.first_obj,
|
|
498
|
-
active_points=self.active_points,
|
|
499
|
-
active_aspects=active_aspects,
|
|
500
|
-
)
|
|
501
|
-
self.aspects_list = aspects_instance.relevant_aspects
|
|
430
|
+
# Extract aspects from pre-computed chart data
|
|
431
|
+
self.aspects_list = chart_data.aspects.relevant_aspects
|
|
502
432
|
|
|
503
433
|
# Screen size
|
|
504
434
|
self.height = self._DEFAULT_HEIGHT
|
|
505
435
|
self.width = self._DEFAULT_NATAL_WIDTH
|
|
506
436
|
|
|
507
|
-
#
|
|
508
|
-
self.location = self.
|
|
509
|
-
self.geolat = self.first_obj.lat
|
|
510
|
-
self.geolon = self.first_obj.lng
|
|
437
|
+
# Get location and coordinates
|
|
438
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
511
439
|
|
|
512
440
|
# 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
|
|
441
|
+
self.first_circle_radius = 0
|
|
442
|
+
self.second_circle_radius = 36
|
|
443
|
+
self.third_circle_radius = 120
|
|
521
444
|
|
|
522
445
|
# --------------------
|
|
523
|
-
# FINAL COMMON SETUP
|
|
446
|
+
# FINAL COMMON SETUP FROM CHART DATA
|
|
524
447
|
# --------------------
|
|
525
448
|
|
|
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
|
-
)
|
|
449
|
+
# Extract pre-computed element and quality distributions
|
|
450
|
+
self.fire = chart_data.element_distribution.fire
|
|
451
|
+
self.earth = chart_data.element_distribution.earth
|
|
452
|
+
self.air = chart_data.element_distribution.air
|
|
453
|
+
self.water = chart_data.element_distribution.water
|
|
541
454
|
|
|
542
|
-
self.
|
|
543
|
-
self.
|
|
544
|
-
self.
|
|
545
|
-
self.water = element_totals["water"]
|
|
546
|
-
|
|
547
|
-
# Calculate qualities points
|
|
548
|
-
if self.chart_type == "Synastry":
|
|
549
|
-
qualities_totals = calculate_synastry_quality_points(
|
|
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"]
|
|
455
|
+
self.cardinal = chart_data.quality_distribution.cardinal
|
|
456
|
+
self.fixed = chart_data.quality_distribution.fixed
|
|
457
|
+
self.mutable = chart_data.quality_distribution.mutable
|
|
565
458
|
|
|
566
459
|
# Set up theme
|
|
567
460
|
if theme not in get_args(KerykeionChartTheme) and theme is not None:
|
|
@@ -569,6 +462,215 @@ class KerykeionChartSVG:
|
|
|
569
462
|
|
|
570
463
|
self.set_up_theme(theme)
|
|
571
464
|
|
|
465
|
+
# Optionally expand width dynamically to fit content
|
|
466
|
+
if self.auto_size:
|
|
467
|
+
try:
|
|
468
|
+
required_width = self._estimate_required_width_full()
|
|
469
|
+
if required_width > self.width:
|
|
470
|
+
self.width = required_width
|
|
471
|
+
except Exception as e:
|
|
472
|
+
# Keep default on any unexpected issue; do not break rendering
|
|
473
|
+
logging.debug(f"Auto-size width calculation failed: {e}")
|
|
474
|
+
|
|
475
|
+
self._apply_dynamic_height_adjustment()
|
|
476
|
+
|
|
477
|
+
def _count_active_planets(self) -> int:
|
|
478
|
+
"""Return number of active celestial points in the current chart."""
|
|
479
|
+
return len([p for p in self.available_planets_setting if p.get("is_active")])
|
|
480
|
+
|
|
481
|
+
def _apply_dynamic_height_adjustment(self) -> None:
|
|
482
|
+
"""Adjust chart height and vertical offsets based on active points."""
|
|
483
|
+
active_points_count = self._count_active_planets()
|
|
484
|
+
|
|
485
|
+
offsets = self._BASE_VERTICAL_OFFSETS.copy()
|
|
486
|
+
|
|
487
|
+
minimum_height = self._DEFAULT_HEIGHT
|
|
488
|
+
if active_points_count <= 20:
|
|
489
|
+
self.height = max(self.height, minimum_height)
|
|
490
|
+
self._vertical_offsets = offsets
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
extra_points = active_points_count - 20
|
|
494
|
+
extra_height = extra_points * self._ROW_HEIGHT
|
|
495
|
+
|
|
496
|
+
self.height = max(self.height, minimum_height + extra_height)
|
|
497
|
+
|
|
498
|
+
delta_height = max(self.height - minimum_height, 0)
|
|
499
|
+
|
|
500
|
+
# Anchor wheel, aspect grid/list, and lunar phase to the bottom
|
|
501
|
+
offsets["wheel"] += delta_height
|
|
502
|
+
offsets["aspect_grid"] += delta_height
|
|
503
|
+
offsets["aspect_list"] += delta_height
|
|
504
|
+
offsets["lunar_phase"] += delta_height
|
|
505
|
+
offsets["bottom_left"] += delta_height
|
|
506
|
+
|
|
507
|
+
# Smooth top offsets to keep breathing room near the title and grids
|
|
508
|
+
shift = min(extra_points * self._TOP_SHIFT_FACTOR, self._MAX_TOP_SHIFT)
|
|
509
|
+
top_shift = shift // 2
|
|
510
|
+
|
|
511
|
+
offsets["grid"] += shift
|
|
512
|
+
offsets["title"] += top_shift
|
|
513
|
+
offsets["elements"] += top_shift
|
|
514
|
+
offsets["qualities"] += top_shift
|
|
515
|
+
|
|
516
|
+
self._vertical_offsets = offsets
|
|
517
|
+
|
|
518
|
+
def _dynamic_viewbox(self) -> str:
|
|
519
|
+
"""Return the viewBox string based on current width/height."""
|
|
520
|
+
return f"0 0 {int(self.width)} {int(self.height)}"
|
|
521
|
+
|
|
522
|
+
def _wheel_only_viewbox(self, margin: int = 20) -> str:
|
|
523
|
+
"""Return a tight viewBox for the wheel-only template.
|
|
524
|
+
|
|
525
|
+
The wheel is drawn inside a group translated by (100, 50) and has
|
|
526
|
+
diameter 2 * main_radius. We add a small margin around it.
|
|
527
|
+
"""
|
|
528
|
+
left = 100 - margin
|
|
529
|
+
top = 50 - margin
|
|
530
|
+
width = (2 * self.main_radius) + (2 * margin)
|
|
531
|
+
height = (2 * self.main_radius) + (2 * margin)
|
|
532
|
+
return f"{left} {top} {width} {height}"
|
|
533
|
+
|
|
534
|
+
def _grid_only_viewbox(self, margin: int = 10) -> str:
|
|
535
|
+
"""Compute a tight viewBox for the Aspect Grid Only SVG.
|
|
536
|
+
|
|
537
|
+
The grid is rendered using fixed origins and box size:
|
|
538
|
+
- For Transit/Synastry/DualReturn charts, `draw_transit_aspect_grid`
|
|
539
|
+
uses `x_indent=50`, `y_indent=250`, `box_size=14` and draws:
|
|
540
|
+
• a header row to the right of `x_indent`
|
|
541
|
+
• a left header column at `x_indent - box_size`
|
|
542
|
+
• an NxN grid of cells above `y_indent`
|
|
543
|
+
|
|
544
|
+
- For Natal/Composite/SingleReturn charts, `draw_aspect_grid` uses
|
|
545
|
+
`x_start=50`, `y_start=250`, `box_size=14` and draws a triangular grid
|
|
546
|
+
that extends to the right (x) and upwards (y).
|
|
547
|
+
|
|
548
|
+
This function mirrors that geometry to return a snug viewBox around the
|
|
549
|
+
content, with a small configurable `margin`.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
margin: Extra pixels to add on each side of the computed bounds.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
A string "minX minY width height" suitable for the SVG `viewBox`.
|
|
556
|
+
"""
|
|
557
|
+
# Must match defaults used in the renderers
|
|
558
|
+
x0 = 50
|
|
559
|
+
y0 = 250
|
|
560
|
+
box = 14
|
|
561
|
+
|
|
562
|
+
n = max(len([p for p in self.available_planets_setting if p.get("is_active")]), 1)
|
|
563
|
+
|
|
564
|
+
if self.chart_type in ("Transit", "Synastry", "DualReturnChart"):
|
|
565
|
+
# Full N×N grid
|
|
566
|
+
left = (x0 - box) - margin
|
|
567
|
+
top = (y0 - box * n) - margin
|
|
568
|
+
right = (x0 + box * n) + margin
|
|
569
|
+
bottom = (y0 + box) + margin
|
|
570
|
+
else:
|
|
571
|
+
# Triangular grid (no extra left column)
|
|
572
|
+
left = x0 - margin
|
|
573
|
+
top = (y0 - box * n) - margin
|
|
574
|
+
right = (x0 + box * n) + margin
|
|
575
|
+
bottom = (y0 + box) + margin
|
|
576
|
+
|
|
577
|
+
width = max(1, int(right - left))
|
|
578
|
+
height = max(1, int(bottom - top))
|
|
579
|
+
|
|
580
|
+
return f"{int(left)} {int(top)} {width} {height}"
|
|
581
|
+
|
|
582
|
+
def _estimate_required_width_full(self) -> int:
|
|
583
|
+
"""Estimate minimal width to contain all rendered groups for the full chart.
|
|
584
|
+
|
|
585
|
+
The calculation is heuristic and mirrors the default x positions used in
|
|
586
|
+
the SVG templates and drawing utilities. We keep a conservative padding.
|
|
587
|
+
"""
|
|
588
|
+
# Wheel footprint (translate(100,50) + diameter of 2*radius)
|
|
589
|
+
wheel_right = 100 + (2 * self.main_radius)
|
|
590
|
+
extents = [wheel_right]
|
|
591
|
+
|
|
592
|
+
n_active = max(self._count_active_planets(), 1)
|
|
593
|
+
|
|
594
|
+
# Common grids present on many chart types
|
|
595
|
+
main_planet_grid_right = 645 + 80
|
|
596
|
+
main_houses_grid_right = 750 + 120
|
|
597
|
+
extents.extend([main_planet_grid_right, main_houses_grid_right])
|
|
598
|
+
|
|
599
|
+
if self.chart_type in ("Natal", "Composite", "SingleReturnChart"):
|
|
600
|
+
# Triangular aspect grid at x_start=540, width ~ 14 * n_active
|
|
601
|
+
aspect_grid_right = 560 + 14 * n_active
|
|
602
|
+
extents.append(aspect_grid_right)
|
|
603
|
+
|
|
604
|
+
if self.chart_type in ("Transit", "Synastry", "DualReturnChart"):
|
|
605
|
+
# Double-chart aspects placement
|
|
606
|
+
if self.double_chart_aspect_grid_type == "list":
|
|
607
|
+
# Columnar list placed at translate(565,273), ~100-110px per column, 14 aspects per column
|
|
608
|
+
aspects_per_column = 14
|
|
609
|
+
total_aspects = len(self.aspects_list) if hasattr(self, "aspects_list") else 0
|
|
610
|
+
columns = max((total_aspects + aspects_per_column - 1) // aspects_per_column, 1)
|
|
611
|
+
# Respect the max columns cap used in rendering: DualReturn=7, others=6
|
|
612
|
+
max_cols_cap = 7
|
|
613
|
+
columns = min(columns, max_cols_cap)
|
|
614
|
+
aspect_list_right = 565 + (columns * 110)
|
|
615
|
+
extents.append(aspect_list_right)
|
|
616
|
+
else:
|
|
617
|
+
# Grid table placed with x_indent ~550, width ~ 14px per cell across n_active+1
|
|
618
|
+
aspect_grid_table_right = 550 + (14 * (n_active + 1))
|
|
619
|
+
extents.append(aspect_grid_table_right)
|
|
620
|
+
|
|
621
|
+
# Secondary grids
|
|
622
|
+
secondary_planet_grid_right = 910 + 80
|
|
623
|
+
extents.append(secondary_planet_grid_right)
|
|
624
|
+
|
|
625
|
+
if self.chart_type == "Synastry":
|
|
626
|
+
# Secondary houses grid default x ~ 1015
|
|
627
|
+
secondary_houses_grid_right = 1015 + 120
|
|
628
|
+
extents.append(secondary_houses_grid_right)
|
|
629
|
+
|
|
630
|
+
if self.chart_type == "Transit":
|
|
631
|
+
# House comparison grid at x ~ 1030
|
|
632
|
+
house_comparison_grid_right = 1030 + 180
|
|
633
|
+
extents.append(house_comparison_grid_right)
|
|
634
|
+
|
|
635
|
+
if self.chart_type == "DualReturnChart":
|
|
636
|
+
# House comparison grid at x ~ 1030
|
|
637
|
+
house_comparison_grid_right = 1030 + 320
|
|
638
|
+
extents.append(house_comparison_grid_right)
|
|
639
|
+
|
|
640
|
+
# Conservative safety padding
|
|
641
|
+
return int(max(extents) + self._padding)
|
|
642
|
+
|
|
643
|
+
def _get_location_info(self) -> tuple[str, float, float]:
|
|
644
|
+
"""
|
|
645
|
+
Determine location information based on chart type and subjects.
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
tuple: (location_name, latitude, longitude)
|
|
649
|
+
"""
|
|
650
|
+
if self.chart_type == "Composite":
|
|
651
|
+
# For composite charts, use average location of the two composite subjects
|
|
652
|
+
if isinstance(self.first_obj, CompositeSubjectModel):
|
|
653
|
+
location_name = ""
|
|
654
|
+
latitude = (self.first_obj.first_subject.lat + self.first_obj.second_subject.lat) / 2
|
|
655
|
+
longitude = (self.first_obj.first_subject.lng + self.first_obj.second_subject.lng) / 2
|
|
656
|
+
else:
|
|
657
|
+
# Fallback to first subject location
|
|
658
|
+
location_name = self.first_obj.city or "Unknown"
|
|
659
|
+
latitude = self.first_obj.lat or 0.0
|
|
660
|
+
longitude = self.first_obj.lng or 0.0
|
|
661
|
+
elif self.chart_type in ["Transit", "DualReturnChart"] and self.second_obj:
|
|
662
|
+
# Use location from the second subject (transit/return)
|
|
663
|
+
location_name = self.second_obj.city or "Unknown"
|
|
664
|
+
latitude = self.second_obj.lat or 0.0
|
|
665
|
+
longitude = self.second_obj.lng or 0.0
|
|
666
|
+
else:
|
|
667
|
+
# Use location from the first subject
|
|
668
|
+
location_name = self.first_obj.city or "Unknown"
|
|
669
|
+
latitude = self.first_obj.lat or 0.0
|
|
670
|
+
longitude = self.first_obj.lng or 0.0
|
|
671
|
+
|
|
672
|
+
return location_name, latitude, longitude
|
|
673
|
+
|
|
572
674
|
def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None:
|
|
573
675
|
"""
|
|
574
676
|
Load and apply a CSS theme for the chart visualization.
|
|
@@ -585,16 +687,6 @@ class KerykeionChartSVG:
|
|
|
585
687
|
with open(theme_dir / f"{theme}.css", "r") as f:
|
|
586
688
|
self.color_style_tag = f.read()
|
|
587
689
|
|
|
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
690
|
def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None:
|
|
599
691
|
"""
|
|
600
692
|
Load and parse chart configuration settings.
|
|
@@ -682,7 +774,89 @@ class KerykeionChartSVG:
|
|
|
682
774
|
)
|
|
683
775
|
return out
|
|
684
776
|
|
|
685
|
-
def
|
|
777
|
+
def _truncate_name(self, name: str, max_length: int = 50) -> str:
|
|
778
|
+
"""
|
|
779
|
+
Truncate a name if it's too long, preserving readability.
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
name (str): The name to truncate
|
|
783
|
+
max_length (int): Maximum allowed length
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
str: Truncated name with ellipsis if needed
|
|
787
|
+
"""
|
|
788
|
+
if len(name) <= max_length:
|
|
789
|
+
return name
|
|
790
|
+
return name[:max_length-1] + "…"
|
|
791
|
+
|
|
792
|
+
def _get_chart_title(self) -> str:
|
|
793
|
+
"""
|
|
794
|
+
Generate the chart title based on chart type and custom title settings.
|
|
795
|
+
|
|
796
|
+
If a custom title is provided, it will be used. Otherwise, generates the
|
|
797
|
+
appropriate default title based on the chart type and subjects.
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
str: The chart title to display (max ~40 characters).
|
|
801
|
+
"""
|
|
802
|
+
# If custom title is provided, use it
|
|
803
|
+
if self.custom_title is not None:
|
|
804
|
+
return self.custom_title
|
|
805
|
+
|
|
806
|
+
# Generate default title based on chart type
|
|
807
|
+
if self.chart_type == "Natal":
|
|
808
|
+
natal_label = self.language_settings.get("birth_chart", "Natal")
|
|
809
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
810
|
+
return f'{truncated_name} - {natal_label}'
|
|
811
|
+
|
|
812
|
+
elif self.chart_type == "Composite":
|
|
813
|
+
composite_label = self.language_settings.get("composite_chart", "Composite")
|
|
814
|
+
and_word = self.language_settings.get("and_word", "&")
|
|
815
|
+
name1 = self._truncate_name(self.first_obj.first_subject.name) # type: ignore
|
|
816
|
+
name2 = self._truncate_name(self.first_obj.second_subject.name) # type: ignore
|
|
817
|
+
return f"{composite_label}: {name1} {and_word} {name2}"
|
|
818
|
+
|
|
819
|
+
elif self.chart_type == "Transit":
|
|
820
|
+
transit_label = self.language_settings.get("transits", "Transits")
|
|
821
|
+
from datetime import datetime
|
|
822
|
+
date_obj = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime) # type: ignore
|
|
823
|
+
date_str = date_obj.strftime("%d/%m/%y")
|
|
824
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
825
|
+
return f"{truncated_name} - {transit_label} {date_str}"
|
|
826
|
+
|
|
827
|
+
elif self.chart_type == "Synastry":
|
|
828
|
+
synastry_label = self.language_settings.get("synastry_chart", "Synastry")
|
|
829
|
+
and_word = self.language_settings.get("and_word", "&")
|
|
830
|
+
name1 = self._truncate_name(self.first_obj.name)
|
|
831
|
+
name2 = self._truncate_name(self.second_obj.name) # type: ignore
|
|
832
|
+
return f"{synastry_label}: {name1} {and_word} {name2}"
|
|
833
|
+
|
|
834
|
+
elif self.chart_type == "DualReturnChart":
|
|
835
|
+
from datetime import datetime
|
|
836
|
+
year = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime).year # type: ignore
|
|
837
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
838
|
+
if self.second_obj is not None and isinstance(self.second_obj, PlanetReturnModel) and self.second_obj.return_type == "Solar":
|
|
839
|
+
solar_label = self.language_settings.get("solar_return", "Solar")
|
|
840
|
+
return f"{truncated_name} - {solar_label} {year}"
|
|
841
|
+
else:
|
|
842
|
+
lunar_label = self.language_settings.get("lunar_return", "Lunar")
|
|
843
|
+
return f"{truncated_name} - {lunar_label} {year}"
|
|
844
|
+
|
|
845
|
+
elif self.chart_type == "SingleReturnChart":
|
|
846
|
+
from datetime import datetime
|
|
847
|
+
year = datetime.fromisoformat(self.first_obj.iso_formatted_local_datetime).year # type: ignore
|
|
848
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
849
|
+
if isinstance(self.first_obj, PlanetReturnModel) and self.first_obj.return_type == "Solar":
|
|
850
|
+
solar_label = self.language_settings.get("solar_return", "Solar")
|
|
851
|
+
return f"{truncated_name} - {solar_label} {year}"
|
|
852
|
+
else:
|
|
853
|
+
lunar_label = self.language_settings.get("lunar_return", "Lunar")
|
|
854
|
+
return f"{truncated_name} - {lunar_label} {year}"
|
|
855
|
+
|
|
856
|
+
# Fallback for unknown chart types
|
|
857
|
+
return self._truncate_name(self.first_obj.name)
|
|
858
|
+
|
|
859
|
+
def _create_template_dictionary(self) -> ChartTemplateModel:
|
|
686
860
|
"""
|
|
687
861
|
Assemble chart data and rendering instructions into a template dictionary.
|
|
688
862
|
|
|
@@ -690,7 +864,7 @@ class KerykeionChartSVG:
|
|
|
690
864
|
chart type and subjects.
|
|
691
865
|
|
|
692
866
|
Returns:
|
|
693
|
-
|
|
867
|
+
ChartTemplateModel: Populated structure of template variables.
|
|
694
868
|
"""
|
|
695
869
|
# Initialize template dictionary
|
|
696
870
|
template_dict: dict = {}
|
|
@@ -704,9 +878,19 @@ class KerykeionChartSVG:
|
|
|
704
878
|
template_dict["chart_height"] = self.height
|
|
705
879
|
template_dict["chart_width"] = self.width
|
|
706
880
|
|
|
881
|
+
offsets = self._vertical_offsets
|
|
882
|
+
template_dict["full_wheel_translate_y"] = offsets["wheel"]
|
|
883
|
+
template_dict["houses_and_planets_translate_y"] = offsets["grid"]
|
|
884
|
+
template_dict["aspect_grid_translate_y"] = offsets["aspect_grid"]
|
|
885
|
+
template_dict["aspect_list_translate_y"] = offsets["aspect_list"]
|
|
886
|
+
template_dict["title_translate_y"] = offsets["title"]
|
|
887
|
+
template_dict["elements_translate_y"] = offsets["elements"]
|
|
888
|
+
template_dict["qualities_translate_y"] = offsets["qualities"]
|
|
889
|
+
template_dict["lunar_phase_translate_y"] = offsets["lunar_phase"]
|
|
890
|
+
template_dict["bottom_left_translate_y"] = offsets["bottom_left"]
|
|
891
|
+
|
|
707
892
|
# Set paper colors
|
|
708
893
|
template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"]
|
|
709
|
-
template_dict["paper_color_1"] = self.chart_colors_settings["paper_1"]
|
|
710
894
|
|
|
711
895
|
# Set background color based on transparent_background setting
|
|
712
896
|
if self.transparent_background:
|
|
@@ -714,7 +898,12 @@ class KerykeionChartSVG:
|
|
|
714
898
|
else:
|
|
715
899
|
template_dict["background_color"] = self.chart_colors_settings["paper_1"]
|
|
716
900
|
|
|
717
|
-
# Set planet colors
|
|
901
|
+
# Set planet colors - initialize all possible colors first with defaults
|
|
902
|
+
default_color = "#000000" # Default black color for unused planets
|
|
903
|
+
for i in range(42): # Support all 42 celestial points (0-41)
|
|
904
|
+
template_dict[f"planets_color_{i}"] = default_color
|
|
905
|
+
|
|
906
|
+
# Override with actual colors from settings
|
|
718
907
|
for planet in self.planets_settings:
|
|
719
908
|
planet_id = planet["id"]
|
|
720
909
|
template_dict[f"planets_color_{planet_id}"] = planet["color"]
|
|
@@ -732,10 +921,12 @@ class KerykeionChartSVG:
|
|
|
732
921
|
|
|
733
922
|
# Calculate element percentages
|
|
734
923
|
total_elements = self.fire + self.water + self.earth + self.air
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
924
|
+
element_values = {"fire": self.fire, "earth": self.earth, "air": self.air, "water": self.water}
|
|
925
|
+
element_percentages = distribute_percentages_to_100(element_values) if total_elements > 0 else {"fire": 0, "earth": 0, "air": 0, "water": 0}
|
|
926
|
+
fire_percentage = element_percentages["fire"]
|
|
927
|
+
earth_percentage = element_percentages["earth"]
|
|
928
|
+
air_percentage = element_percentages["air"]
|
|
929
|
+
water_percentage = element_percentages["water"]
|
|
739
930
|
|
|
740
931
|
# Element Percentages
|
|
741
932
|
template_dict["elements_string"] = f"{self.language_settings.get('elements', 'Elements')}:"
|
|
@@ -747,9 +938,11 @@ class KerykeionChartSVG:
|
|
|
747
938
|
|
|
748
939
|
# Qualities Percentages
|
|
749
940
|
total_qualities = self.cardinal + self.fixed + self.mutable
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
941
|
+
quality_values = {"cardinal": self.cardinal, "fixed": self.fixed, "mutable": self.mutable}
|
|
942
|
+
quality_percentages = distribute_percentages_to_100(quality_values) if total_qualities > 0 else {"cardinal": 0, "fixed": 0, "mutable": 0}
|
|
943
|
+
cardinal_percentage = quality_percentages["cardinal"]
|
|
944
|
+
fixed_percentage = quality_percentages["fixed"]
|
|
945
|
+
mutable_percentage = quality_percentages["mutable"]
|
|
753
946
|
|
|
754
947
|
template_dict["qualities_string"] = f"{self.language_settings.get('qualities', 'Qualities')}:"
|
|
755
948
|
template_dict["cardinal_string"] = f"{self.language_settings.get('cardinal', 'Cardinal')} {cardinal_percentage}%"
|
|
@@ -759,13 +952,16 @@ class KerykeionChartSVG:
|
|
|
759
952
|
# Get houses list for main subject
|
|
760
953
|
first_subject_houses_list = get_houses_list(self.first_obj)
|
|
761
954
|
|
|
955
|
+
# Chart title
|
|
956
|
+
template_dict["stringTitle"] = self._get_chart_title()
|
|
957
|
+
|
|
762
958
|
# ------------------------------- #
|
|
763
959
|
# CHART TYPE SPECIFIC SETTINGS #
|
|
764
960
|
# ------------------------------- #
|
|
765
961
|
|
|
766
|
-
if self.chart_type
|
|
767
|
-
# Set viewbox
|
|
768
|
-
template_dict["viewbox"] = self.
|
|
962
|
+
if self.chart_type == "Natal":
|
|
963
|
+
# Set viewbox dynamically
|
|
964
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
769
965
|
|
|
770
966
|
# Rings and circles
|
|
771
967
|
template_dict["transitRing"] = ""
|
|
@@ -810,9 +1006,6 @@ class KerykeionChartSVG:
|
|
|
810
1006
|
)
|
|
811
1007
|
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
812
1008
|
|
|
813
|
-
# Chart title
|
|
814
|
-
template_dict["stringTitle"] = f'{self.first_obj.name} - {self.language_settings.get("birth_chart", "Birth Chart")}'
|
|
815
|
-
|
|
816
1009
|
# Top left section
|
|
817
1010
|
latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
|
|
818
1011
|
longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
|
|
@@ -822,7 +1015,8 @@ class KerykeionChartSVG:
|
|
|
822
1015
|
template_dict["top_left_2"] = f"{self.language_settings['latitude']}: {latitude_string}"
|
|
823
1016
|
template_dict["top_left_3"] = f"{self.language_settings['longitude']}: {longitude_string}"
|
|
824
1017
|
template_dict["top_left_4"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
|
|
825
|
-
|
|
1018
|
+
localized_weekday = self.language_settings.get('weekdays', {}).get(self.first_obj.day_of_week, self.first_obj.day_of_week) # type: ignore
|
|
1019
|
+
template_dict["top_left_5"] = f"{self.language_settings.get('day_of_week', 'Day of Week')}: {localized_weekday}" # type: ignore
|
|
826
1020
|
|
|
827
1021
|
# Bottom left section
|
|
828
1022
|
if self.first_obj.zodiac_type == "Tropic":
|
|
@@ -870,6 +1064,7 @@ class KerykeionChartSVG:
|
|
|
870
1064
|
c1=self.first_circle_radius,
|
|
871
1065
|
c3=self.third_circle_radius,
|
|
872
1066
|
chart_type=self.chart_type,
|
|
1067
|
+
external_view=self.external_view,
|
|
873
1068
|
)
|
|
874
1069
|
|
|
875
1070
|
template_dict["makePlanets"] = draw_planets(
|
|
@@ -880,6 +1075,7 @@ class KerykeionChartSVG:
|
|
|
880
1075
|
third_circle_radius=self.third_circle_radius,
|
|
881
1076
|
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
882
1077
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1078
|
+
external_view=self.external_view,
|
|
883
1079
|
)
|
|
884
1080
|
|
|
885
1081
|
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
@@ -894,8 +1090,8 @@ class KerykeionChartSVG:
|
|
|
894
1090
|
template_dict["makeHouseComparisonGrid"] = ""
|
|
895
1091
|
|
|
896
1092
|
elif self.chart_type == "Composite":
|
|
897
|
-
# Set viewbox
|
|
898
|
-
template_dict["viewbox"] = self.
|
|
1093
|
+
# Set viewbox dynamically
|
|
1094
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
899
1095
|
|
|
900
1096
|
# Rings and circles
|
|
901
1097
|
template_dict["transitRing"] = ""
|
|
@@ -940,9 +1136,6 @@ class KerykeionChartSVG:
|
|
|
940
1136
|
)
|
|
941
1137
|
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
942
1138
|
|
|
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
1139
|
# Top left section
|
|
947
1140
|
# First subject
|
|
948
1141
|
latitude = convert_latitude_coordinate_to_string(
|
|
@@ -1014,6 +1207,7 @@ class KerykeionChartSVG:
|
|
|
1014
1207
|
c1=self.first_circle_radius,
|
|
1015
1208
|
c3=self.third_circle_radius,
|
|
1016
1209
|
chart_type=self.chart_type,
|
|
1210
|
+
external_view=self.external_view,
|
|
1017
1211
|
)
|
|
1018
1212
|
|
|
1019
1213
|
template_dict["makePlanets"] = draw_planets(
|
|
@@ -1024,6 +1218,7 @@ class KerykeionChartSVG:
|
|
|
1024
1218
|
third_circle_radius=self.third_circle_radius,
|
|
1025
1219
|
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
1026
1220
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1221
|
+
external_view=self.external_view,
|
|
1027
1222
|
)
|
|
1028
1223
|
|
|
1029
1224
|
subject_name = f"{self.first_obj.first_subject.name} {self.language_settings['and_word']} {self.first_obj.second_subject.name}" # type: ignore
|
|
@@ -1054,11 +1249,8 @@ class KerykeionChartSVG:
|
|
|
1054
1249
|
template_dict["fixed_string"] = ""
|
|
1055
1250
|
template_dict["mutable_string"] = ""
|
|
1056
1251
|
|
|
1057
|
-
# Set viewbox
|
|
1058
|
-
|
|
1059
|
-
template_dict["viewbox"] = self._TRANSIT_CHART_WITH_TABLE_VIWBOX
|
|
1060
|
-
else:
|
|
1061
|
-
template_dict["viewbox"] = self._WIDE_CHART_VIEWBOX
|
|
1252
|
+
# Set viewbox dynamically
|
|
1253
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1062
1254
|
|
|
1063
1255
|
# Get houses list for secondary subject
|
|
1064
1256
|
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
@@ -1098,22 +1290,19 @@ class KerykeionChartSVG:
|
|
|
1098
1290
|
if self.double_chart_aspect_grid_type == "list":
|
|
1099
1291
|
title = f'{self.first_obj.name} - {self.language_settings.get("transit_aspects", "Transit Aspects")}'
|
|
1100
1292
|
template_dict["makeAspectGrid"] = ""
|
|
1101
|
-
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings)
|
|
1293
|
+
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
1294
|
else:
|
|
1103
1295
|
template_dict["makeAspectGrid"] = ""
|
|
1104
1296
|
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
|
|
1105
1297
|
self.chart_colors_settings["paper_0"],
|
|
1106
1298
|
self.available_planets_setting,
|
|
1107
1299
|
self.aspects_list,
|
|
1108
|
-
|
|
1109
|
-
|
|
1300
|
+
600,
|
|
1301
|
+
520,
|
|
1110
1302
|
)
|
|
1111
1303
|
|
|
1112
1304
|
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1113
1305
|
|
|
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
1306
|
# Top left section
|
|
1118
1307
|
latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
|
|
1119
1308
|
longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
|
|
@@ -1176,6 +1365,7 @@ class KerykeionChartSVG:
|
|
|
1176
1365
|
c1=self.first_circle_radius,
|
|
1177
1366
|
c3=self.third_circle_radius,
|
|
1178
1367
|
chart_type=self.chart_type,
|
|
1368
|
+
external_view=self.external_view,
|
|
1179
1369
|
second_subject_houses_list=second_subject_houses_list,
|
|
1180
1370
|
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
1181
1371
|
)
|
|
@@ -1189,6 +1379,7 @@ class KerykeionChartSVG:
|
|
|
1189
1379
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1190
1380
|
chart_type=self.chart_type,
|
|
1191
1381
|
third_circle_radius=self.third_circle_radius,
|
|
1382
|
+
external_view=self.external_view,
|
|
1192
1383
|
)
|
|
1193
1384
|
|
|
1194
1385
|
# Planet grids
|
|
@@ -1214,8 +1405,8 @@ class KerykeionChartSVG:
|
|
|
1214
1405
|
|
|
1215
1406
|
# House comparison grid
|
|
1216
1407
|
house_comparison_factory = HouseComparisonFactory(
|
|
1217
|
-
first_subject=self.first_obj,
|
|
1218
|
-
second_subject=self.second_obj,
|
|
1408
|
+
first_subject=self.first_obj, # type: ignore[arg-type]
|
|
1409
|
+
second_subject=self.second_obj, # type: ignore[arg-type]
|
|
1219
1410
|
active_points=self.active_points,
|
|
1220
1411
|
)
|
|
1221
1412
|
house_comparison = house_comparison_factory.get_house_comparison()
|
|
@@ -1228,12 +1419,12 @@ class KerykeionChartSVG:
|
|
|
1228
1419
|
house_position_comparison_label=self.language_settings.get("house_position_comparison", "House Position Comparison"),
|
|
1229
1420
|
return_point_label=self.language_settings.get("transit_point", "Transit Point"),
|
|
1230
1421
|
natal_house_label=self.language_settings.get("house_position", "Natal House"),
|
|
1231
|
-
x_position=
|
|
1422
|
+
x_position=980,
|
|
1232
1423
|
)
|
|
1233
1424
|
|
|
1234
1425
|
elif self.chart_type == "Synastry":
|
|
1235
|
-
# Set viewbox
|
|
1236
|
-
template_dict["viewbox"] = self.
|
|
1426
|
+
# Set viewbox dynamically
|
|
1427
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1237
1428
|
|
|
1238
1429
|
# Get houses list for secondary subject
|
|
1239
1430
|
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
@@ -1273,10 +1464,10 @@ class KerykeionChartSVG:
|
|
|
1273
1464
|
if self.double_chart_aspect_grid_type == "list":
|
|
1274
1465
|
template_dict["makeAspectGrid"] = ""
|
|
1275
1466
|
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')}",
|
|
1467
|
+
f"{self.first_obj.name} - {self.second_obj.name} {self.language_settings.get('synastry_aspects', 'Synastry Aspects')}", # type: ignore[union-attr]
|
|
1277
1468
|
self.aspects_list,
|
|
1278
|
-
self.planets_settings,
|
|
1279
|
-
self.aspects_settings
|
|
1469
|
+
self.planets_settings, # type: ignore[arg-type]
|
|
1470
|
+
self.aspects_settings # type: ignore[arg-type]
|
|
1280
1471
|
)
|
|
1281
1472
|
else:
|
|
1282
1473
|
template_dict["makeAspectGrid"] = ""
|
|
@@ -1290,9 +1481,6 @@ class KerykeionChartSVG:
|
|
|
1290
1481
|
|
|
1291
1482
|
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1292
1483
|
|
|
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
1484
|
# Top left section
|
|
1297
1485
|
template_dict["top_left_0"] = f"{self.first_obj.name}:"
|
|
1298
1486
|
template_dict["top_left_1"] = f"{self.first_obj.city}, {self.first_obj.nation}" # type: ignore
|
|
@@ -1343,6 +1531,7 @@ class KerykeionChartSVG:
|
|
|
1343
1531
|
c1=self.first_circle_radius,
|
|
1344
1532
|
c3=self.third_circle_radius,
|
|
1345
1533
|
chart_type=self.chart_type,
|
|
1534
|
+
external_view=self.external_view,
|
|
1346
1535
|
second_subject_houses_list=second_subject_houses_list,
|
|
1347
1536
|
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
1348
1537
|
)
|
|
@@ -1356,6 +1545,7 @@ class KerykeionChartSVG:
|
|
|
1356
1545
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1357
1546
|
chart_type=self.chart_type,
|
|
1358
1547
|
third_circle_radius=self.third_circle_radius,
|
|
1548
|
+
external_view=self.external_view,
|
|
1359
1549
|
)
|
|
1360
1550
|
|
|
1361
1551
|
# Planet grid
|
|
@@ -1377,9 +1567,9 @@ class KerykeionChartSVG:
|
|
|
1377
1567
|
)
|
|
1378
1568
|
template_dict["makeHouseComparisonGrid"] = ""
|
|
1379
1569
|
|
|
1380
|
-
elif self.chart_type == "
|
|
1381
|
-
# Set viewbox
|
|
1382
|
-
template_dict["viewbox"] = self.
|
|
1570
|
+
elif self.chart_type == "DualReturnChart":
|
|
1571
|
+
# Set viewbox dynamically
|
|
1572
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1383
1573
|
|
|
1384
1574
|
# Get houses list for secondary subject
|
|
1385
1575
|
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
@@ -1419,7 +1609,7 @@ class KerykeionChartSVG:
|
|
|
1419
1609
|
if self.double_chart_aspect_grid_type == "list":
|
|
1420
1610
|
title = self.language_settings.get("return_aspects", "Natal to Return Aspects")
|
|
1421
1611
|
template_dict["makeAspectGrid"] = ""
|
|
1422
|
-
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings, max_columns=7)
|
|
1612
|
+
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
1613
|
else:
|
|
1424
1614
|
template_dict["makeAspectGrid"] = ""
|
|
1425
1615
|
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
|
|
@@ -1432,12 +1622,6 @@ class KerykeionChartSVG:
|
|
|
1432
1622
|
|
|
1433
1623
|
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1434
1624
|
|
|
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
1625
|
|
|
1442
1626
|
# Top left section
|
|
1443
1627
|
# Subject
|
|
@@ -1509,6 +1693,7 @@ class KerykeionChartSVG:
|
|
|
1509
1693
|
c1=self.first_circle_radius,
|
|
1510
1694
|
c3=self.third_circle_radius,
|
|
1511
1695
|
chart_type=self.chart_type,
|
|
1696
|
+
external_view=self.external_view,
|
|
1512
1697
|
second_subject_houses_list=second_subject_houses_list,
|
|
1513
1698
|
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
1514
1699
|
)
|
|
@@ -1522,6 +1707,7 @@ class KerykeionChartSVG:
|
|
|
1522
1707
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1523
1708
|
chart_type=self.chart_type,
|
|
1524
1709
|
third_circle_radius=self.third_circle_radius,
|
|
1710
|
+
external_view=self.external_view,
|
|
1525
1711
|
)
|
|
1526
1712
|
|
|
1527
1713
|
# Planet grid
|
|
@@ -1549,8 +1735,8 @@ class KerykeionChartSVG:
|
|
|
1549
1735
|
)
|
|
1550
1736
|
|
|
1551
1737
|
house_comparison_factory = HouseComparisonFactory(
|
|
1552
|
-
first_subject=self.first_obj,
|
|
1553
|
-
second_subject=self.second_obj,
|
|
1738
|
+
first_subject=self.first_obj, # type: ignore[arg-type]
|
|
1739
|
+
second_subject=self.second_obj, # type: ignore[arg-type]
|
|
1554
1740
|
active_points=self.active_points,
|
|
1555
1741
|
)
|
|
1556
1742
|
house_comparison = house_comparison_factory.get_house_comparison()
|
|
@@ -1562,13 +1748,13 @@ class KerykeionChartSVG:
|
|
|
1562
1748
|
points_owner_subject_number=2, # The second subject is the Solar Return
|
|
1563
1749
|
house_position_comparison_label=self.language_settings.get("house_position_comparison", "House Position Comparison"),
|
|
1564
1750
|
return_point_label=self.language_settings.get("return_point", "Return Point"),
|
|
1565
|
-
return_label=self.language_settings.get("Return", "
|
|
1751
|
+
return_label=self.language_settings.get("Return", "DualReturnChart"),
|
|
1566
1752
|
radix_label=self.language_settings.get("Natal", "Natal"),
|
|
1567
1753
|
)
|
|
1568
1754
|
|
|
1569
|
-
elif self.chart_type == "
|
|
1570
|
-
# Set viewbox
|
|
1571
|
-
template_dict["viewbox"] = self.
|
|
1755
|
+
elif self.chart_type == "SingleReturnChart":
|
|
1756
|
+
# Set viewbox dynamically
|
|
1757
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1572
1758
|
|
|
1573
1759
|
# Rings and circles
|
|
1574
1760
|
template_dict["transitRing"] = ""
|
|
@@ -1613,9 +1799,6 @@ class KerykeionChartSVG:
|
|
|
1613
1799
|
)
|
|
1614
1800
|
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
1615
1801
|
|
|
1616
|
-
# Chart title
|
|
1617
|
-
template_dict["stringTitle"] = self.first_obj.name
|
|
1618
|
-
|
|
1619
1802
|
# Top left section
|
|
1620
1803
|
latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings["north"], self.language_settings["south"])
|
|
1621
1804
|
longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings["east"], self.language_settings["west"])
|
|
@@ -1677,6 +1860,7 @@ class KerykeionChartSVG:
|
|
|
1677
1860
|
c1=self.first_circle_radius,
|
|
1678
1861
|
c3=self.third_circle_radius,
|
|
1679
1862
|
chart_type=self.chart_type,
|
|
1863
|
+
external_view=self.external_view,
|
|
1680
1864
|
)
|
|
1681
1865
|
|
|
1682
1866
|
template_dict["makePlanets"] = draw_planets(
|
|
@@ -1687,6 +1871,7 @@ class KerykeionChartSVG:
|
|
|
1687
1871
|
third_circle_radius=self.third_circle_radius,
|
|
1688
1872
|
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
1689
1873
|
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1874
|
+
external_view=self.external_view,
|
|
1690
1875
|
)
|
|
1691
1876
|
|
|
1692
1877
|
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
@@ -1700,9 +1885,9 @@ class KerykeionChartSVG:
|
|
|
1700
1885
|
template_dict["makeSecondaryPlanetGrid"] = ""
|
|
1701
1886
|
template_dict["makeHouseComparisonGrid"] = ""
|
|
1702
1887
|
|
|
1703
|
-
return
|
|
1888
|
+
return ChartTemplateModel(**template_dict)
|
|
1704
1889
|
|
|
1705
|
-
def
|
|
1890
|
+
def generate_svg_string(self, minify: bool = False, remove_css_variables=False) -> str:
|
|
1706
1891
|
"""
|
|
1707
1892
|
Render the full chart SVG as a string.
|
|
1708
1893
|
|
|
@@ -1723,11 +1908,11 @@ class KerykeionChartSVG:
|
|
|
1723
1908
|
|
|
1724
1909
|
# read template
|
|
1725
1910
|
with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f:
|
|
1726
|
-
template = Template(f.read()).substitute(td)
|
|
1911
|
+
template = Template(f.read()).substitute(td.model_dump())
|
|
1727
1912
|
|
|
1728
1913
|
# return filename
|
|
1729
1914
|
|
|
1730
|
-
logging.debug(f"Template dictionary
|
|
1915
|
+
logging.debug(f"Template dictionary has {len(td.model_dump())} fields")
|
|
1731
1916
|
|
|
1732
1917
|
self._create_template_dictionary()
|
|
1733
1918
|
|
|
@@ -1742,36 +1927,50 @@ class KerykeionChartSVG:
|
|
|
1742
1927
|
|
|
1743
1928
|
return template
|
|
1744
1929
|
|
|
1745
|
-
def
|
|
1930
|
+
def save_svg(self, output_path: Union[str, Path, None] = None, filename: Union[str, None] = None, minify: bool = False, remove_css_variables=False):
|
|
1746
1931
|
"""
|
|
1747
1932
|
Generate and save the full chart SVG to disk.
|
|
1748
1933
|
|
|
1749
|
-
Calls
|
|
1750
|
-
"{subject.name} - {chart_type} Chart.svg" in the output directory.
|
|
1934
|
+
Calls generate_svg_string to render the SVG, then writes a file named
|
|
1935
|
+
"{subject.name} - {chart_type} Chart.svg" in the specified output directory.
|
|
1751
1936
|
|
|
1752
1937
|
Args:
|
|
1753
|
-
|
|
1754
|
-
|
|
1938
|
+
output_path (str, Path, or None): Directory path where the SVG file will be saved.
|
|
1939
|
+
If None, defaults to the user's home directory.
|
|
1940
|
+
filename (str or None): Custom filename for the SVG file (without extension).
|
|
1941
|
+
If None, uses the default pattern: "{subject.name} - {chart_type} Chart".
|
|
1942
|
+
minify (bool): Pass-through to generate_svg_string for compact output.
|
|
1943
|
+
remove_css_variables (bool): Pass-through to generate_svg_string to embed CSS variables.
|
|
1755
1944
|
|
|
1756
1945
|
Returns:
|
|
1757
1946
|
None
|
|
1758
1947
|
"""
|
|
1759
1948
|
|
|
1760
|
-
self.template = self.
|
|
1949
|
+
self.template = self.generate_svg_string(minify, remove_css_variables)
|
|
1950
|
+
|
|
1951
|
+
# Convert output_path to Path object, default to home directory
|
|
1952
|
+
output_directory = Path(output_path) if output_path is not None else Path.home()
|
|
1761
1953
|
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
chartname = self.output_directory / f"{self.first_obj.name} - {self.chart_type} Chart - Solar Return.svg"
|
|
1954
|
+
# Determine filename
|
|
1955
|
+
if filename is not None:
|
|
1956
|
+
chartname = output_directory / f"{filename}.svg"
|
|
1766
1957
|
else:
|
|
1767
|
-
|
|
1958
|
+
# Use default filename pattern
|
|
1959
|
+
chart_type_for_filename = self.chart_type
|
|
1960
|
+
|
|
1961
|
+
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":
|
|
1962
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Lunar Return.svg"
|
|
1963
|
+
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":
|
|
1964
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Solar Return.svg"
|
|
1965
|
+
else:
|
|
1966
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart.svg"
|
|
1768
1967
|
|
|
1769
1968
|
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
1770
1969
|
output_file.write(self.template)
|
|
1771
1970
|
|
|
1772
1971
|
print(f"SVG Generated Correctly in: {chartname}")
|
|
1773
1972
|
|
|
1774
|
-
def
|
|
1973
|
+
def generate_wheel_only_svg_string(self, minify: bool = False, remove_css_variables=False):
|
|
1775
1974
|
"""
|
|
1776
1975
|
Render the wheel-only chart SVG as a string.
|
|
1777
1976
|
|
|
@@ -1795,7 +1994,9 @@ class KerykeionChartSVG:
|
|
|
1795
1994
|
template = f.read()
|
|
1796
1995
|
|
|
1797
1996
|
template_dict = self._create_template_dictionary()
|
|
1798
|
-
|
|
1997
|
+
# Use a compact viewBox specific for the wheel-only rendering
|
|
1998
|
+
wheel_viewbox = self._wheel_only_viewbox()
|
|
1999
|
+
template = Template(template).substitute({**template_dict.model_dump(), "viewbox": wheel_viewbox})
|
|
1799
2000
|
|
|
1800
2001
|
if remove_css_variables:
|
|
1801
2002
|
template = inline_css_variables_in_svg(template)
|
|
@@ -1808,30 +2009,44 @@ class KerykeionChartSVG:
|
|
|
1808
2009
|
|
|
1809
2010
|
return template
|
|
1810
2011
|
|
|
1811
|
-
def
|
|
2012
|
+
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
2013
|
"""
|
|
1813
2014
|
Generate and save wheel-only chart SVG to disk.
|
|
1814
2015
|
|
|
1815
|
-
Calls
|
|
1816
|
-
"{subject.name} - {chart_type} Chart - Wheel Only.svg" in the output directory.
|
|
2016
|
+
Calls generate_wheel_only_svg_string and writes a file named
|
|
2017
|
+
"{subject.name} - {chart_type} Chart - Wheel Only.svg" in the specified output directory.
|
|
1817
2018
|
|
|
1818
2019
|
Args:
|
|
1819
|
-
|
|
1820
|
-
|
|
2020
|
+
output_path (str, Path, or None): Directory path where the SVG file will be saved.
|
|
2021
|
+
If None, defaults to the user's home directory.
|
|
2022
|
+
filename (str or None): Custom filename for the SVG file (without extension).
|
|
2023
|
+
If None, uses the default pattern: "{subject.name} - {chart_type} Chart - Wheel Only".
|
|
2024
|
+
minify (bool): Pass-through to generate_wheel_only_svg_string for compact output.
|
|
2025
|
+
remove_css_variables (bool): Pass-through to generate_wheel_only_svg_string to embed CSS variables.
|
|
1821
2026
|
|
|
1822
2027
|
Returns:
|
|
1823
2028
|
None
|
|
1824
2029
|
"""
|
|
1825
2030
|
|
|
1826
|
-
template = self.
|
|
1827
|
-
|
|
2031
|
+
template = self.generate_wheel_only_svg_string(minify, remove_css_variables)
|
|
2032
|
+
|
|
2033
|
+
# Convert output_path to Path object, default to home directory
|
|
2034
|
+
output_directory = Path(output_path) if output_path is not None else Path.home()
|
|
2035
|
+
|
|
2036
|
+
# Determine filename
|
|
2037
|
+
if filename is not None:
|
|
2038
|
+
chartname = output_directory / f"{filename}.svg"
|
|
2039
|
+
else:
|
|
2040
|
+
# Use default filename pattern
|
|
2041
|
+
chart_type_for_filename = "ExternalNatal" if self.external_view and self.chart_type == "Natal" else self.chart_type
|
|
2042
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Wheel Only.svg"
|
|
1828
2043
|
|
|
1829
2044
|
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
1830
2045
|
output_file.write(template)
|
|
1831
2046
|
|
|
1832
2047
|
print(f"SVG Generated Correctly in: {chartname}")
|
|
1833
2048
|
|
|
1834
|
-
def
|
|
2049
|
+
def generate_aspect_grid_only_svg_string(self, minify: bool = False, remove_css_variables=False):
|
|
1835
2050
|
"""
|
|
1836
2051
|
Render the aspect-grid-only chart SVG as a string.
|
|
1837
2052
|
|
|
@@ -1856,7 +2071,7 @@ class KerykeionChartSVG:
|
|
|
1856
2071
|
|
|
1857
2072
|
template_dict = self._create_template_dictionary()
|
|
1858
2073
|
|
|
1859
|
-
if self.chart_type in ["Transit", "Synastry", "
|
|
2074
|
+
if self.chart_type in ["Transit", "Synastry", "DualReturnChart"]:
|
|
1860
2075
|
aspects_grid = draw_transit_aspect_grid(
|
|
1861
2076
|
self.chart_colors_settings["paper_0"],
|
|
1862
2077
|
self.available_planets_setting,
|
|
@@ -1871,7 +2086,10 @@ class KerykeionChartSVG:
|
|
|
1871
2086
|
y_start=250,
|
|
1872
2087
|
)
|
|
1873
2088
|
|
|
1874
|
-
|
|
2089
|
+
# Use a compact, known-good viewBox that frames the grid
|
|
2090
|
+
viewbox_override = self._grid_only_viewbox()
|
|
2091
|
+
|
|
2092
|
+
template = Template(template).substitute({**template_dict.model_dump(), "makeAspectGrid": aspects_grid, "viewbox": viewbox_override})
|
|
1875
2093
|
|
|
1876
2094
|
if remove_css_variables:
|
|
1877
2095
|
template = inline_css_variables_in_svg(template)
|
|
@@ -1884,42 +2102,55 @@ class KerykeionChartSVG:
|
|
|
1884
2102
|
|
|
1885
2103
|
return template
|
|
1886
2104
|
|
|
1887
|
-
def
|
|
2105
|
+
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
2106
|
"""
|
|
1889
2107
|
Generate and save aspect-grid-only chart SVG to disk.
|
|
1890
2108
|
|
|
1891
|
-
Calls
|
|
1892
|
-
"{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the output directory.
|
|
2109
|
+
Calls generate_aspect_grid_only_svg_string and writes a file named
|
|
2110
|
+
"{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the specified output directory.
|
|
1893
2111
|
|
|
1894
2112
|
Args:
|
|
1895
|
-
|
|
1896
|
-
|
|
2113
|
+
output_path (str, Path, or None): Directory path where the SVG file will be saved.
|
|
2114
|
+
If None, defaults to the user's home directory.
|
|
2115
|
+
filename (str or None): Custom filename for the SVG file (without extension).
|
|
2116
|
+
If None, uses the default pattern: "{subject.name} - {chart_type} Chart - Aspect Grid Only".
|
|
2117
|
+
minify (bool): Pass-through to generate_aspect_grid_only_svg_string for compact output.
|
|
2118
|
+
remove_css_variables (bool): Pass-through to generate_aspect_grid_only_svg_string to embed CSS variables.
|
|
1897
2119
|
|
|
1898
2120
|
Returns:
|
|
1899
2121
|
None
|
|
1900
2122
|
"""
|
|
1901
2123
|
|
|
1902
|
-
template = self.
|
|
1903
|
-
|
|
2124
|
+
template = self.generate_aspect_grid_only_svg_string(minify, remove_css_variables)
|
|
2125
|
+
|
|
2126
|
+
# Convert output_path to Path object, default to home directory
|
|
2127
|
+
output_directory = Path(output_path) if output_path is not None else Path.home()
|
|
2128
|
+
|
|
2129
|
+
# Determine filename
|
|
2130
|
+
if filename is not None:
|
|
2131
|
+
chartname = output_directory / f"{filename}.svg"
|
|
2132
|
+
else:
|
|
2133
|
+
# Use default filename pattern
|
|
2134
|
+
chart_type_for_filename = "ExternalNatal" if self.external_view and self.chart_type == "Natal" else self.chart_type
|
|
2135
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Aspect Grid Only.svg"
|
|
1904
2136
|
|
|
1905
2137
|
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
1906
2138
|
output_file.write(template)
|
|
1907
2139
|
|
|
1908
2140
|
print(f"SVG Generated Correctly in: {chartname}")
|
|
1909
2141
|
|
|
1910
|
-
|
|
1911
2142
|
if __name__ == "__main__":
|
|
1912
2143
|
from kerykeion.utilities import setup_logging
|
|
1913
2144
|
from kerykeion.planetary_return_factory import PlanetaryReturnFactory
|
|
1914
2145
|
from kerykeion.astrological_subject_factory import AstrologicalSubjectFactory
|
|
2146
|
+
from kerykeion.chart_data_factory import ChartDataFactory
|
|
2147
|
+
from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
|
|
1915
2148
|
|
|
1916
|
-
ACTIVE_PLANETS: list[AstrologicalPoint] =
|
|
1917
|
-
|
|
1918
|
-
]
|
|
1919
|
-
|
|
2149
|
+
ACTIVE_PLANETS: list[AstrologicalPoint] = DEFAULT_ACTIVE_POINTS
|
|
2150
|
+
# ACTIVE_PLANETS: list[AstrologicalPoint] = ALL_ACTIVE_POINTS
|
|
1920
2151
|
setup_logging(level="info")
|
|
1921
2152
|
|
|
1922
|
-
subject = AstrologicalSubjectFactory.from_birth_data("John Lennon", 1940, 10, 9, 18, 30, "Liverpool", "GB")
|
|
2153
|
+
subject = AstrologicalSubjectFactory.from_birth_data("John Lennon", 1940, 10, 9, 18, 30, "Liverpool", "GB", active_points=ACTIVE_PLANETS)
|
|
1923
2154
|
|
|
1924
2155
|
return_factory = PlanetaryReturnFactory(
|
|
1925
2156
|
subject,
|
|
@@ -1932,101 +2163,118 @@ if __name__ == "__main__":
|
|
|
1932
2163
|
)
|
|
1933
2164
|
|
|
1934
2165
|
###
|
|
1935
|
-
## Birth Chart
|
|
1936
|
-
|
|
1937
|
-
|
|
2166
|
+
## Birth Chart - NEW APPROACH with ChartDataFactory
|
|
2167
|
+
birth_chart_data = ChartDataFactory.create_natal_chart_data(
|
|
2168
|
+
subject,
|
|
2169
|
+
active_points=ACTIVE_PLANETS,
|
|
2170
|
+
)
|
|
2171
|
+
birth_chart = ChartDrawer(
|
|
2172
|
+
chart_data=birth_chart_data,
|
|
1938
2173
|
chart_language="IT",
|
|
1939
2174
|
theme="strawberry",
|
|
1940
|
-
active_points=ACTIVE_PLANETS,
|
|
1941
2175
|
)
|
|
1942
|
-
birth_chart.
|
|
2176
|
+
birth_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1943
2177
|
|
|
1944
2178
|
###
|
|
1945
|
-
## Solar Return Chart
|
|
2179
|
+
## Solar Return Chart - NEW APPROACH with ChartDataFactory
|
|
1946
2180
|
solar_return = return_factory.next_return_from_iso_formatted_time(
|
|
1947
2181
|
"2025-01-09T18:30:00+01:00", # UTC+1
|
|
1948
2182
|
return_type="Solar",
|
|
1949
2183
|
)
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2184
|
+
solar_return_chart_data = ChartDataFactory.create_return_chart_data(
|
|
2185
|
+
subject,
|
|
2186
|
+
solar_return,
|
|
2187
|
+
active_points=ACTIVE_PLANETS,
|
|
2188
|
+
)
|
|
2189
|
+
solar_return_chart = ChartDrawer(
|
|
2190
|
+
chart_data=solar_return_chart_data,
|
|
1953
2191
|
chart_language="IT",
|
|
1954
2192
|
theme="classic",
|
|
1955
|
-
active_points=ACTIVE_PLANETS,
|
|
1956
2193
|
)
|
|
1957
2194
|
|
|
1958
|
-
solar_return_chart.
|
|
2195
|
+
solar_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1959
2196
|
|
|
1960
2197
|
###
|
|
1961
|
-
## Single wheel return
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2198
|
+
## Single wheel return - NEW APPROACH with ChartDataFactory
|
|
2199
|
+
single_wheel_return_chart_data = ChartDataFactory.create_single_wheel_return_chart_data(
|
|
2200
|
+
solar_return,
|
|
2201
|
+
active_points=ACTIVE_PLANETS,
|
|
2202
|
+
)
|
|
2203
|
+
single_wheel_return_chart = ChartDrawer(
|
|
2204
|
+
chart_data=single_wheel_return_chart_data,
|
|
1966
2205
|
chart_language="IT",
|
|
1967
2206
|
theme="dark",
|
|
1968
|
-
active_points=ACTIVE_PLANETS,
|
|
1969
2207
|
)
|
|
1970
2208
|
|
|
1971
|
-
single_wheel_return_chart.
|
|
2209
|
+
single_wheel_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1972
2210
|
|
|
1973
2211
|
###
|
|
1974
|
-
## Lunar return
|
|
2212
|
+
## Lunar return - NEW APPROACH with ChartDataFactory
|
|
1975
2213
|
lunar_return = return_factory.next_return_from_iso_formatted_time(
|
|
1976
2214
|
"2025-01-09T18:30:00+01:00", # UTC+1
|
|
1977
2215
|
return_type="Lunar",
|
|
1978
2216
|
)
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2217
|
+
lunar_return_chart_data = ChartDataFactory.create_return_chart_data(
|
|
2218
|
+
subject,
|
|
2219
|
+
lunar_return,
|
|
2220
|
+
active_points=ACTIVE_PLANETS,
|
|
2221
|
+
)
|
|
2222
|
+
lunar_return_chart = ChartDrawer(
|
|
2223
|
+
chart_data=lunar_return_chart_data,
|
|
1983
2224
|
chart_language="IT",
|
|
1984
2225
|
theme="dark",
|
|
1985
|
-
active_points=ACTIVE_PLANETS,
|
|
1986
2226
|
)
|
|
1987
|
-
lunar_return_chart.
|
|
2227
|
+
lunar_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
1988
2228
|
|
|
1989
2229
|
###
|
|
1990
|
-
## Transit Chart
|
|
2230
|
+
## Transit Chart - NEW APPROACH with ChartDataFactory
|
|
1991
2231
|
transit = AstrologicalSubjectFactory.from_iso_utc_time(
|
|
1992
2232
|
"Transit",
|
|
1993
2233
|
"2021-10-04T18:30:00+01:00",
|
|
1994
2234
|
)
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2235
|
+
transit_chart_data = ChartDataFactory.create_transit_chart_data(
|
|
2236
|
+
subject,
|
|
2237
|
+
transit,
|
|
2238
|
+
active_points=ACTIVE_PLANETS,
|
|
2239
|
+
)
|
|
2240
|
+
transit_chart = ChartDrawer(
|
|
2241
|
+
chart_data=transit_chart_data,
|
|
1999
2242
|
chart_language="IT",
|
|
2000
2243
|
theme="dark",
|
|
2001
|
-
active_points=ACTIVE_PLANETS
|
|
2002
2244
|
)
|
|
2003
|
-
transit_chart.
|
|
2245
|
+
transit_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2004
2246
|
|
|
2005
2247
|
###
|
|
2006
|
-
## Synastry Chart
|
|
2248
|
+
## Synastry Chart - NEW APPROACH with ChartDataFactory
|
|
2007
2249
|
second_subject = AstrologicalSubjectFactory.from_birth_data("Yoko Ono", 1933, 2, 18, 18, 30, "Tokyo", "JP")
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2250
|
+
synastry_chart_data = ChartDataFactory.create_synastry_chart_data(
|
|
2251
|
+
subject,
|
|
2252
|
+
second_subject,
|
|
2253
|
+
active_points=ACTIVE_PLANETS,
|
|
2254
|
+
)
|
|
2255
|
+
synastry_chart = ChartDrawer(
|
|
2256
|
+
chart_data=synastry_chart_data,
|
|
2012
2257
|
chart_language="IT",
|
|
2013
2258
|
theme="dark",
|
|
2014
|
-
active_points=ACTIVE_PLANETS
|
|
2015
2259
|
)
|
|
2016
|
-
synastry_chart.
|
|
2260
|
+
synastry_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2017
2261
|
|
|
2018
2262
|
##
|
|
2019
|
-
# Transit Chart with Grid
|
|
2263
|
+
# Transit Chart with Grid - NEW APPROACH with ChartDataFactory
|
|
2020
2264
|
subject.name = "Grid"
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2265
|
+
transit_chart_with_grid_data = ChartDataFactory.create_transit_chart_data(
|
|
2266
|
+
subject,
|
|
2267
|
+
transit,
|
|
2268
|
+
active_points=ACTIVE_PLANETS,
|
|
2269
|
+
)
|
|
2270
|
+
transit_chart_with_grid = ChartDrawer(
|
|
2271
|
+
chart_data=transit_chart_with_grid_data,
|
|
2025
2272
|
chart_language="IT",
|
|
2026
2273
|
theme="dark",
|
|
2027
|
-
active_points=ACTIVE_PLANETS,
|
|
2028
2274
|
double_chart_aspect_grid_type="table"
|
|
2029
2275
|
)
|
|
2030
|
-
transit_chart_with_grid.
|
|
2031
|
-
transit_chart_with_grid.
|
|
2032
|
-
transit_chart_with_grid.
|
|
2276
|
+
transit_chart_with_grid.save_svg() # minify=True, remove_css_variables=True)
|
|
2277
|
+
transit_chart_with_grid.save_aspect_grid_only_svg_file()
|
|
2278
|
+
transit_chart_with_grid.save_wheel_only_svg_file()
|
|
2279
|
+
|
|
2280
|
+
print("✅ All chart examples completed using ChartDataFactory + ChartDrawer architecture!")
|