kerykeion 3.1.1__py3-none-any.whl → 5.1.9__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 +58 -141
- kerykeion/aspects/__init__.py +14 -0
- kerykeion/aspects/aspects_factory.py +568 -0
- kerykeion/aspects/aspects_utils.py +164 -0
- kerykeion/astrological_subject_factory.py +1901 -0
- kerykeion/backword.py +820 -0
- kerykeion/chart_data_factory.py +552 -0
- kerykeion/charts/__init__.py +5 -0
- kerykeion/charts/chart_drawer.py +2794 -0
- kerykeion/charts/charts_utils.py +1840 -0
- kerykeion/charts/draw_planets.py +658 -0
- kerykeion/charts/templates/aspect_grid_only.xml +596 -0
- kerykeion/charts/templates/chart.xml +741 -0
- kerykeion/charts/templates/wheel_only.xml +653 -0
- kerykeion/charts/themes/black-and-white.css +148 -0
- kerykeion/charts/themes/classic.css +113 -0
- kerykeion/charts/themes/dark-high-contrast.css +159 -0
- kerykeion/charts/themes/dark.css +160 -0
- kerykeion/charts/themes/light.css +160 -0
- kerykeion/charts/themes/strawberry.css +158 -0
- kerykeion/composite_subject_factory.py +408 -0
- kerykeion/ephemeris_data_factory.py +443 -0
- kerykeion/fetch_geonames.py +105 -61
- kerykeion/house_comparison/__init__.py +6 -0
- kerykeion/house_comparison/house_comparison_factory.py +103 -0
- kerykeion/house_comparison/house_comparison_utils.py +126 -0
- kerykeion/kr_types/__init__.py +70 -0
- kerykeion/kr_types/chart_template_model.py +20 -0
- kerykeion/kr_types/kerykeion_exception.py +20 -0
- kerykeion/kr_types/kr_literals.py +20 -0
- kerykeion/kr_types/kr_models.py +20 -0
- kerykeion/kr_types/settings_models.py +20 -0
- kerykeion/planetary_return_factory.py +805 -0
- kerykeion/relationship_score_factory.py +301 -0
- kerykeion/report.py +779 -0
- kerykeion/schemas/__init__.py +106 -0
- kerykeion/schemas/chart_template_model.py +367 -0
- kerykeion/schemas/kerykeion_exception.py +20 -0
- kerykeion/schemas/kr_literals.py +181 -0
- kerykeion/schemas/kr_models.py +603 -0
- kerykeion/schemas/settings_models.py +188 -0
- kerykeion/settings/__init__.py +20 -0
- kerykeion/settings/chart_defaults.py +444 -0
- kerykeion/settings/config_constants.py +152 -0
- kerykeion/settings/kerykeion_settings.py +51 -0
- kerykeion/settings/translation_strings.py +1499 -0
- kerykeion/settings/translations.py +74 -0
- kerykeion/sweph/README.md +3 -0
- kerykeion/sweph/ast136/s136108s.se1 +0 -0
- kerykeion/sweph/ast136/s136199s.se1 +0 -0
- kerykeion/sweph/ast136/s136472s.se1 +0 -0
- kerykeion/sweph/ast28/se28978s.se1 +0 -0
- kerykeion/sweph/ast50/se50000s.se1 +0 -0
- kerykeion/sweph/ast90/se90377s.se1 +0 -0
- kerykeion/sweph/ast90/se90482s.se1 +0 -0
- kerykeion/sweph/seas_18.se1 +0 -0
- kerykeion/sweph/sefstars.txt +1602 -0
- kerykeion/transits_time_range_factory.py +302 -0
- kerykeion/utilities.py +762 -130
- kerykeion-5.1.9.dist-info/METADATA +1793 -0
- kerykeion-5.1.9.dist-info/RECORD +63 -0
- {kerykeion-3.1.1.dist-info → kerykeion-5.1.9.dist-info}/WHEEL +1 -2
- kerykeion-5.1.9.dist-info/licenses/LICENSE +661 -0
- kerykeion/aspects.py +0 -331
- kerykeion/charts/charts_svg.py +0 -1607
- kerykeion/charts/templates/basic.xml +0 -285
- kerykeion/charts/templates/extended.xml +0 -294
- kerykeion/kr.config.json +0 -464
- kerykeion/main.py +0 -595
- kerykeion/print_all_data.py +0 -44
- kerykeion/relationship_score.py +0 -219
- kerykeion/types.py +0 -190
- kerykeion-3.1.1.dist-info/METADATA +0 -204
- kerykeion-3.1.1.dist-info/RECORD +0 -17
- kerykeion-3.1.1.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,2794 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
This is part of Kerykeion (C) 2025 Giacomo Battaglia
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from math import ceil
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from string import Template
|
|
13
|
+
from typing import Any, Mapping, Optional, Sequence, Union, get_args
|
|
14
|
+
|
|
15
|
+
import swisseph as swe
|
|
16
|
+
from scour.scour import scourString
|
|
17
|
+
|
|
18
|
+
from kerykeion.house_comparison.house_comparison_factory import HouseComparisonFactory
|
|
19
|
+
from kerykeion.schemas import (
|
|
20
|
+
KerykeionException,
|
|
21
|
+
ChartType,
|
|
22
|
+
Sign,
|
|
23
|
+
ActiveAspect,
|
|
24
|
+
KerykeionPointModel,
|
|
25
|
+
)
|
|
26
|
+
from kerykeion.schemas import ChartTemplateModel
|
|
27
|
+
from kerykeion.schemas.kr_models import (
|
|
28
|
+
AstrologicalSubjectModel,
|
|
29
|
+
CompositeSubjectModel,
|
|
30
|
+
PlanetReturnModel,
|
|
31
|
+
)
|
|
32
|
+
from kerykeion.schemas.settings_models import (
|
|
33
|
+
KerykeionSettingsCelestialPointModel,
|
|
34
|
+
KerykeionLanguageModel,
|
|
35
|
+
)
|
|
36
|
+
from kerykeion.schemas.kr_literals import (
|
|
37
|
+
KerykeionChartTheme,
|
|
38
|
+
KerykeionChartLanguage,
|
|
39
|
+
AstrologicalPoint,
|
|
40
|
+
)
|
|
41
|
+
from kerykeion.schemas.kr_models import ChartDataModel
|
|
42
|
+
from kerykeion.settings import LANGUAGE_SETTINGS
|
|
43
|
+
from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
|
|
44
|
+
from kerykeion.settings.translations import get_translations, load_language_settings
|
|
45
|
+
from kerykeion.charts.charts_utils import (
|
|
46
|
+
draw_zodiac_slice,
|
|
47
|
+
convert_latitude_coordinate_to_string,
|
|
48
|
+
convert_longitude_coordinate_to_string,
|
|
49
|
+
draw_aspect_line,
|
|
50
|
+
draw_transit_ring_degree_steps,
|
|
51
|
+
draw_degree_ring,
|
|
52
|
+
draw_transit_ring,
|
|
53
|
+
draw_background_circle,
|
|
54
|
+
draw_first_circle,
|
|
55
|
+
draw_house_comparison_grid,
|
|
56
|
+
draw_second_circle,
|
|
57
|
+
draw_third_circle,
|
|
58
|
+
draw_aspect_grid,
|
|
59
|
+
draw_houses_cusps_and_text_number,
|
|
60
|
+
draw_transit_aspect_list,
|
|
61
|
+
draw_transit_aspect_grid,
|
|
62
|
+
draw_single_house_comparison_grid,
|
|
63
|
+
makeLunarPhase,
|
|
64
|
+
draw_main_house_grid,
|
|
65
|
+
draw_secondary_house_grid,
|
|
66
|
+
draw_main_planet_grid,
|
|
67
|
+
draw_secondary_planet_grid,
|
|
68
|
+
format_location_string,
|
|
69
|
+
format_datetime_with_timezone
|
|
70
|
+
)
|
|
71
|
+
from kerykeion.charts.draw_planets import draw_planets
|
|
72
|
+
from kerykeion.utilities import get_houses_list, inline_css_variables_in_svg, distribute_percentages_to_100
|
|
73
|
+
from kerykeion.settings.chart_defaults import (
|
|
74
|
+
DEFAULT_CHART_COLORS,
|
|
75
|
+
DEFAULT_CELESTIAL_POINTS_SETTINGS,
|
|
76
|
+
DEFAULT_CHART_ASPECTS_SETTINGS,
|
|
77
|
+
)
|
|
78
|
+
from typing import List, Literal
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
logger = logging.getLogger(__name__)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ChartDrawer:
|
|
85
|
+
"""
|
|
86
|
+
ChartDrawer generates astrological chart visualizations as SVG files from pre-computed chart data.
|
|
87
|
+
|
|
88
|
+
This class is designed for pure visualization and requires chart data to be pre-computed using
|
|
89
|
+
ChartDataFactory. This separation ensures clean architecture where ChartDataFactory handles
|
|
90
|
+
all calculations (aspects, element/quality distributions, subjects) while ChartDrawer focuses
|
|
91
|
+
solely on rendering SVG visualizations.
|
|
92
|
+
|
|
93
|
+
ChartDrawer supports creating full chart SVGs, wheel-only SVGs, and aspect-grid-only SVGs
|
|
94
|
+
for various chart types including Natal, Transit, Synastry, and Composite.
|
|
95
|
+
Charts are rendered using XML templates and drawing utilities, with customizable themes,
|
|
96
|
+
language, and visual settings.
|
|
97
|
+
|
|
98
|
+
The generated SVG files are optimized for web use and can be saved to any specified
|
|
99
|
+
destination path using the save_svg method.
|
|
100
|
+
|
|
101
|
+
NOTE:
|
|
102
|
+
The generated SVG files are optimized for web use, opening in browsers. If you want to
|
|
103
|
+
use them in other applications, you might need to adjust the SVG settings or styles.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
chart_data (ChartDataModel):
|
|
107
|
+
Pre-computed chart data from ChartDataFactory containing all subjects, aspects,
|
|
108
|
+
element/quality distributions, and other analytical data. This is the ONLY source
|
|
109
|
+
of chart information - no calculations are performed by ChartDrawer.
|
|
110
|
+
theme (KerykeionChartTheme, optional):
|
|
111
|
+
CSS theme for the chart. If None, no default styles are applied. Defaults to 'classic'.
|
|
112
|
+
double_chart_aspect_grid_type (Literal['list', 'table'], optional):
|
|
113
|
+
Specifies rendering style for double-chart aspect grids. Defaults to 'list'.
|
|
114
|
+
chart_language (KerykeionChartLanguage, optional):
|
|
115
|
+
Language code for chart labels. Defaults to 'EN'.
|
|
116
|
+
language_pack (dict | None, optional):
|
|
117
|
+
Additional translations merged over the bundled defaults for the
|
|
118
|
+
selected language. Useful to introduce new languages or override
|
|
119
|
+
existing labels.
|
|
120
|
+
transparent_background (bool, optional):
|
|
121
|
+
Whether to use a transparent background instead of the theme color. Defaults to False.
|
|
122
|
+
|
|
123
|
+
Public Methods:
|
|
124
|
+
makeTemplate(minify=False, remove_css_variables=False) -> str:
|
|
125
|
+
Render the full chart SVG as a string without writing to disk. Use `minify=True`
|
|
126
|
+
to remove whitespace and quotes, and `remove_css_variables=True` to embed CSS vars.
|
|
127
|
+
|
|
128
|
+
save_svg(output_path=None, filename=None, minify=False, remove_css_variables=False) -> None:
|
|
129
|
+
Generate and write the full chart SVG file to the specified path.
|
|
130
|
+
If output_path is None, saves to the user's home directory.
|
|
131
|
+
If filename is None, uses default pattern: '{subject.name} - {chart_type} Chart.svg'.
|
|
132
|
+
|
|
133
|
+
makeWheelOnlyTemplate(minify=False, remove_css_variables=False) -> str:
|
|
134
|
+
Render only the chart wheel (no aspect grid) as an SVG string.
|
|
135
|
+
|
|
136
|
+
save_wheel_only_svg_file(output_path=None, filename=None, minify=False, remove_css_variables=False) -> None:
|
|
137
|
+
Generate and write the wheel-only SVG file to the specified path.
|
|
138
|
+
If output_path is None, saves to the user's home directory.
|
|
139
|
+
If filename is None, uses default pattern: '{subject.name} - {chart_type} Chart - Wheel Only.svg'.
|
|
140
|
+
|
|
141
|
+
makeAspectGridOnlyTemplate(minify=False, remove_css_variables=False) -> str:
|
|
142
|
+
Render only the aspect grid as an SVG string.
|
|
143
|
+
|
|
144
|
+
save_aspect_grid_only_svg_file(output_path=None, filename=None, minify=False, remove_css_variables=False) -> None:
|
|
145
|
+
Generate and write the aspect-grid-only SVG file to the specified path.
|
|
146
|
+
If output_path is None, saves to the user's home directory.
|
|
147
|
+
If filename is None, uses default pattern: '{subject.name} - {chart_type} Chart - Aspect Grid Only.svg'.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
>>> from kerykeion.astrological_subject_factory import AstrologicalSubjectFactory
|
|
151
|
+
>>> from kerykeion.chart_data_factory import ChartDataFactory
|
|
152
|
+
>>> from kerykeion.charts.chart_drawer import ChartDrawer
|
|
153
|
+
>>>
|
|
154
|
+
>>> # Step 1: Create subject
|
|
155
|
+
>>> subject = AstrologicalSubjectFactory.from_birth_data("John", 1990, 1, 1, 12, 0, "London", "GB")
|
|
156
|
+
>>>
|
|
157
|
+
>>> # Step 2: Pre-compute chart data
|
|
158
|
+
>>> chart_data = ChartDataFactory.create_natal_chart_data(subject)
|
|
159
|
+
>>>
|
|
160
|
+
>>> # Step 3: Create visualization
|
|
161
|
+
>>> chart_drawer = ChartDrawer(chart_data=chart_data, theme="classic")
|
|
162
|
+
>>> chart_drawer.save_svg() # Saves to home directory with default filename
|
|
163
|
+
>>> # Or specify custom path and filename:
|
|
164
|
+
>>> chart_drawer.save_svg("/path/to/output/directory", "my_custom_chart")
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
# Constants
|
|
168
|
+
|
|
169
|
+
_DEFAULT_HEIGHT = 550
|
|
170
|
+
_DEFAULT_FULL_WIDTH = 1250
|
|
171
|
+
_DEFAULT_SYNASTRY_WIDTH = 1570
|
|
172
|
+
_DEFAULT_NATAL_WIDTH = 870
|
|
173
|
+
_DEFAULT_FULL_WIDTH_WITH_TABLE = 1250
|
|
174
|
+
_DEFAULT_ULTRA_WIDE_WIDTH = 1320
|
|
175
|
+
|
|
176
|
+
_VERTICAL_PADDING_TOP = 15
|
|
177
|
+
_VERTICAL_PADDING_BOTTOM = 15
|
|
178
|
+
_TITLE_SPACING = 8
|
|
179
|
+
|
|
180
|
+
_ASPECT_LIST_ASPECTS_PER_COLUMN = 14
|
|
181
|
+
_ASPECT_LIST_COLUMN_WIDTH = 105
|
|
182
|
+
|
|
183
|
+
_BASE_VERTICAL_OFFSETS = {
|
|
184
|
+
"wheel": 50,
|
|
185
|
+
"grid": 0,
|
|
186
|
+
"aspect_grid": 50,
|
|
187
|
+
"aspect_list": 50,
|
|
188
|
+
"title": 0,
|
|
189
|
+
"elements": 0,
|
|
190
|
+
"qualities": 0,
|
|
191
|
+
"lunar_phase": 518,
|
|
192
|
+
"bottom_left": 0,
|
|
193
|
+
}
|
|
194
|
+
_MAX_TOP_SHIFT = 80
|
|
195
|
+
_TOP_SHIFT_FACTOR = 2
|
|
196
|
+
_ROW_HEIGHT = 8
|
|
197
|
+
|
|
198
|
+
_BASIC_CHART_VIEWBOX = f"0 0 {_DEFAULT_NATAL_WIDTH} {_DEFAULT_HEIGHT}"
|
|
199
|
+
_WIDE_CHART_VIEWBOX = f"0 0 {_DEFAULT_FULL_WIDTH} 546.0"
|
|
200
|
+
_ULTRA_WIDE_CHART_VIEWBOX = f"0 0 {_DEFAULT_ULTRA_WIDE_WIDTH} 546.0"
|
|
201
|
+
_TRANSIT_CHART_WITH_TABLE_VIWBOX = f"0 0 {_DEFAULT_FULL_WIDTH_WITH_TABLE} 546.0"
|
|
202
|
+
|
|
203
|
+
# Set at init
|
|
204
|
+
first_obj: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel]
|
|
205
|
+
second_obj: Union[AstrologicalSubjectModel, PlanetReturnModel, None]
|
|
206
|
+
chart_type: ChartType
|
|
207
|
+
theme: Union[KerykeionChartTheme, None]
|
|
208
|
+
double_chart_aspect_grid_type: Literal["list", "table"]
|
|
209
|
+
chart_language: KerykeionChartLanguage
|
|
210
|
+
active_points: List[AstrologicalPoint]
|
|
211
|
+
active_aspects: List[ActiveAspect]
|
|
212
|
+
transparent_background: bool
|
|
213
|
+
external_view: bool
|
|
214
|
+
custom_title: Union[str, None]
|
|
215
|
+
_language_model: KerykeionLanguageModel
|
|
216
|
+
_fallback_language_model: KerykeionLanguageModel
|
|
217
|
+
|
|
218
|
+
# Internal properties
|
|
219
|
+
fire: float
|
|
220
|
+
earth: float
|
|
221
|
+
air: float
|
|
222
|
+
water: float
|
|
223
|
+
first_circle_radius: float
|
|
224
|
+
second_circle_radius: float
|
|
225
|
+
third_circle_radius: float
|
|
226
|
+
width: Union[float, int]
|
|
227
|
+
language_settings: dict
|
|
228
|
+
chart_colors_settings: dict
|
|
229
|
+
planets_settings: list[dict[Any, Any]]
|
|
230
|
+
aspects_settings: list[dict[Any, Any]]
|
|
231
|
+
available_planets_setting: List[KerykeionSettingsCelestialPointModel]
|
|
232
|
+
height: float
|
|
233
|
+
location: str
|
|
234
|
+
geolat: float
|
|
235
|
+
geolon: float
|
|
236
|
+
template: str
|
|
237
|
+
|
|
238
|
+
def __init__(
|
|
239
|
+
self,
|
|
240
|
+
chart_data: "ChartDataModel",
|
|
241
|
+
*,
|
|
242
|
+
theme: Union[KerykeionChartTheme, None] = "classic",
|
|
243
|
+
double_chart_aspect_grid_type: Literal["list", "table"] = "list",
|
|
244
|
+
chart_language: KerykeionChartLanguage = "EN",
|
|
245
|
+
language_pack: Optional[Mapping[str, Any]] = None,
|
|
246
|
+
external_view: bool = False,
|
|
247
|
+
transparent_background: bool = False,
|
|
248
|
+
colors_settings: dict = DEFAULT_CHART_COLORS,
|
|
249
|
+
celestial_points_settings: list[dict] = DEFAULT_CELESTIAL_POINTS_SETTINGS,
|
|
250
|
+
aspects_settings: list[dict] = DEFAULT_CHART_ASPECTS_SETTINGS,
|
|
251
|
+
custom_title: Union[str, None] = None,
|
|
252
|
+
auto_size: bool = True,
|
|
253
|
+
padding: int = 20,
|
|
254
|
+
):
|
|
255
|
+
"""
|
|
256
|
+
Initialize the chart visualizer with pre-computed chart data.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
chart_data (ChartDataModel):
|
|
260
|
+
Pre-computed chart data from ChartDataFactory containing all subjects,
|
|
261
|
+
aspects, element/quality distributions, and other analytical data.
|
|
262
|
+
theme (KerykeionChartTheme or None, optional):
|
|
263
|
+
CSS theme to apply; None for default styling.
|
|
264
|
+
double_chart_aspect_grid_type (Literal['list','table'], optional):
|
|
265
|
+
Layout style for double-chart aspect grids ('list' or 'table').
|
|
266
|
+
chart_language (KerykeionChartLanguage, optional):
|
|
267
|
+
Language code for chart labels (e.g., 'EN', 'IT').
|
|
268
|
+
language_pack (dict | None, optional):
|
|
269
|
+
Additional translations merged over the bundled defaults for the
|
|
270
|
+
selected language. Useful to introduce new languages or override
|
|
271
|
+
existing labels.
|
|
272
|
+
external_view (bool, optional):
|
|
273
|
+
Whether to use external visualization (planets on outer ring) for single-subject charts. Defaults to False.
|
|
274
|
+
transparent_background (bool, optional):
|
|
275
|
+
Whether to use a transparent background instead of the theme color. Defaults to False.
|
|
276
|
+
custom_title (str or None, optional):
|
|
277
|
+
Custom title for the chart. If None, the default title will be used based on chart type. Defaults to None.
|
|
278
|
+
"""
|
|
279
|
+
# --------------------
|
|
280
|
+
# COMMON INITIALIZATION
|
|
281
|
+
# --------------------
|
|
282
|
+
self.chart_language = chart_language
|
|
283
|
+
self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
|
|
284
|
+
self.transparent_background = transparent_background
|
|
285
|
+
self.external_view = external_view
|
|
286
|
+
self.chart_colors_settings = deepcopy(colors_settings)
|
|
287
|
+
self.planets_settings = [dict(body) for body in celestial_points_settings]
|
|
288
|
+
self.aspects_settings = [dict(aspect) for aspect in aspects_settings]
|
|
289
|
+
self.custom_title = custom_title
|
|
290
|
+
self.auto_size = auto_size
|
|
291
|
+
self._padding = padding
|
|
292
|
+
self._vertical_offsets: dict[str, int] = self._BASE_VERTICAL_OFFSETS.copy()
|
|
293
|
+
|
|
294
|
+
# Extract data from ChartDataModel
|
|
295
|
+
self.chart_data = chart_data
|
|
296
|
+
self.chart_type = chart_data.chart_type
|
|
297
|
+
self.active_points = chart_data.active_points
|
|
298
|
+
self.active_aspects = chart_data.active_aspects
|
|
299
|
+
|
|
300
|
+
# Extract subjects based on chart type
|
|
301
|
+
if chart_data.chart_type in ["Natal", "Composite", "SingleReturnChart"]:
|
|
302
|
+
# SingleChartDataModel
|
|
303
|
+
self.first_obj = getattr(chart_data, 'subject')
|
|
304
|
+
self.second_obj = None
|
|
305
|
+
|
|
306
|
+
else: # DualChartDataModel for Transit, Synastry, DualReturnChart
|
|
307
|
+
self.first_obj = getattr(chart_data, 'first_subject')
|
|
308
|
+
self.second_obj = getattr(chart_data, 'second_subject')
|
|
309
|
+
|
|
310
|
+
# Load settings
|
|
311
|
+
self._load_language_settings(language_pack)
|
|
312
|
+
|
|
313
|
+
# Default radius for all charts
|
|
314
|
+
self.main_radius = 240
|
|
315
|
+
|
|
316
|
+
# Configure available planets from chart data
|
|
317
|
+
self.available_planets_setting = []
|
|
318
|
+
for body in self.planets_settings:
|
|
319
|
+
if body["name"] in self.active_points:
|
|
320
|
+
body["is_active"] = True
|
|
321
|
+
self.available_planets_setting.append(body) # type: ignore[arg-type]
|
|
322
|
+
|
|
323
|
+
active_points_count = len(self.available_planets_setting)
|
|
324
|
+
if active_points_count > 24:
|
|
325
|
+
logger.warning(
|
|
326
|
+
"ChartDrawer detected %s active celestial points; rendering may look crowded beyond 24.",
|
|
327
|
+
active_points_count,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Set available celestial points
|
|
331
|
+
available_celestial_points_names = [body["name"].lower() for body in self.available_planets_setting]
|
|
332
|
+
self.available_kerykeion_celestial_points = self._collect_subject_points(
|
|
333
|
+
self.first_obj,
|
|
334
|
+
available_celestial_points_names,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Collect secondary subject points for dual charts using the same active set
|
|
338
|
+
self.t_available_kerykeion_celestial_points: list[KerykeionPointModel] = []
|
|
339
|
+
if self.second_obj is not None:
|
|
340
|
+
self.t_available_kerykeion_celestial_points = self._collect_subject_points(
|
|
341
|
+
self.second_obj,
|
|
342
|
+
available_celestial_points_names,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# ------------------------
|
|
346
|
+
# CHART TYPE SPECIFIC SETUP FROM CHART DATA
|
|
347
|
+
# ------------------------
|
|
348
|
+
|
|
349
|
+
if self.chart_type == "Natal":
|
|
350
|
+
# --- NATAL CHART SETUP ---
|
|
351
|
+
|
|
352
|
+
# Extract aspects from pre-computed chart data
|
|
353
|
+
self.aspects_list = chart_data.aspects
|
|
354
|
+
|
|
355
|
+
# Screen size
|
|
356
|
+
self.height = self._DEFAULT_HEIGHT
|
|
357
|
+
self.width = self._DEFAULT_NATAL_WIDTH
|
|
358
|
+
|
|
359
|
+
# Get location and coordinates
|
|
360
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
361
|
+
|
|
362
|
+
# Circle radii - depends on external_view
|
|
363
|
+
if self.external_view:
|
|
364
|
+
self.first_circle_radius = 56
|
|
365
|
+
self.second_circle_radius = 92
|
|
366
|
+
self.third_circle_radius = 112
|
|
367
|
+
else:
|
|
368
|
+
self.first_circle_radius = 0
|
|
369
|
+
self.second_circle_radius = 36
|
|
370
|
+
self.third_circle_radius = 120
|
|
371
|
+
|
|
372
|
+
elif self.chart_type == "Composite":
|
|
373
|
+
# --- COMPOSITE CHART SETUP ---
|
|
374
|
+
|
|
375
|
+
# Extract aspects from pre-computed chart data
|
|
376
|
+
self.aspects_list = chart_data.aspects
|
|
377
|
+
|
|
378
|
+
# Screen size
|
|
379
|
+
self.height = self._DEFAULT_HEIGHT
|
|
380
|
+
self.width = self._DEFAULT_NATAL_WIDTH
|
|
381
|
+
|
|
382
|
+
# Get location and coordinates
|
|
383
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
384
|
+
|
|
385
|
+
# Circle radii
|
|
386
|
+
self.first_circle_radius = 0
|
|
387
|
+
self.second_circle_radius = 36
|
|
388
|
+
self.third_circle_radius = 120
|
|
389
|
+
|
|
390
|
+
elif self.chart_type == "Transit":
|
|
391
|
+
# --- TRANSIT CHART SETUP ---
|
|
392
|
+
|
|
393
|
+
# Extract aspects from pre-computed chart data
|
|
394
|
+
self.aspects_list = chart_data.aspects
|
|
395
|
+
|
|
396
|
+
# Screen size
|
|
397
|
+
self.height = self._DEFAULT_HEIGHT
|
|
398
|
+
if self.double_chart_aspect_grid_type == "table":
|
|
399
|
+
self.width = self._DEFAULT_FULL_WIDTH_WITH_TABLE
|
|
400
|
+
else:
|
|
401
|
+
self.width = self._DEFAULT_FULL_WIDTH
|
|
402
|
+
|
|
403
|
+
# Get location and coordinates
|
|
404
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
405
|
+
|
|
406
|
+
# Circle radii
|
|
407
|
+
self.first_circle_radius = 0
|
|
408
|
+
self.second_circle_radius = 36
|
|
409
|
+
self.third_circle_radius = 120
|
|
410
|
+
|
|
411
|
+
elif self.chart_type == "Synastry":
|
|
412
|
+
# --- SYNASTRY CHART SETUP ---
|
|
413
|
+
|
|
414
|
+
# Extract aspects from pre-computed chart data
|
|
415
|
+
self.aspects_list = chart_data.aspects
|
|
416
|
+
|
|
417
|
+
# Screen size
|
|
418
|
+
self.height = self._DEFAULT_HEIGHT
|
|
419
|
+
self.width = self._DEFAULT_SYNASTRY_WIDTH
|
|
420
|
+
|
|
421
|
+
# Get location and coordinates
|
|
422
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
423
|
+
|
|
424
|
+
# Circle radii
|
|
425
|
+
self.first_circle_radius = 0
|
|
426
|
+
self.second_circle_radius = 36
|
|
427
|
+
self.third_circle_radius = 120
|
|
428
|
+
|
|
429
|
+
elif self.chart_type == "DualReturnChart":
|
|
430
|
+
# --- RETURN CHART SETUP ---
|
|
431
|
+
|
|
432
|
+
# Extract aspects from pre-computed chart data
|
|
433
|
+
self.aspects_list = chart_data.aspects
|
|
434
|
+
|
|
435
|
+
# Screen size
|
|
436
|
+
self.height = self._DEFAULT_HEIGHT
|
|
437
|
+
self.width = self._DEFAULT_ULTRA_WIDE_WIDTH
|
|
438
|
+
|
|
439
|
+
# Get location and coordinates
|
|
440
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
441
|
+
|
|
442
|
+
# Circle radii
|
|
443
|
+
self.first_circle_radius = 0
|
|
444
|
+
self.second_circle_radius = 36
|
|
445
|
+
self.third_circle_radius = 120
|
|
446
|
+
|
|
447
|
+
elif self.chart_type == "SingleReturnChart":
|
|
448
|
+
# --- SINGLE WHEEL RETURN CHART SETUP ---
|
|
449
|
+
|
|
450
|
+
# Extract aspects from pre-computed chart data
|
|
451
|
+
self.aspects_list = chart_data.aspects
|
|
452
|
+
|
|
453
|
+
# Screen size
|
|
454
|
+
self.height = self._DEFAULT_HEIGHT
|
|
455
|
+
self.width = self._DEFAULT_NATAL_WIDTH
|
|
456
|
+
|
|
457
|
+
# Get location and coordinates
|
|
458
|
+
self.location, self.geolat, self.geolon = self._get_location_info()
|
|
459
|
+
|
|
460
|
+
# Circle radii
|
|
461
|
+
self.first_circle_radius = 0
|
|
462
|
+
self.second_circle_radius = 36
|
|
463
|
+
self.third_circle_radius = 120
|
|
464
|
+
|
|
465
|
+
# --------------------
|
|
466
|
+
# FINAL COMMON SETUP FROM CHART DATA
|
|
467
|
+
# --------------------
|
|
468
|
+
|
|
469
|
+
# Extract pre-computed element and quality distributions
|
|
470
|
+
self.fire = chart_data.element_distribution.fire
|
|
471
|
+
self.earth = chart_data.element_distribution.earth
|
|
472
|
+
self.air = chart_data.element_distribution.air
|
|
473
|
+
self.water = chart_data.element_distribution.water
|
|
474
|
+
|
|
475
|
+
self.cardinal = chart_data.quality_distribution.cardinal
|
|
476
|
+
self.fixed = chart_data.quality_distribution.fixed
|
|
477
|
+
self.mutable = chart_data.quality_distribution.mutable
|
|
478
|
+
|
|
479
|
+
# Set up theme
|
|
480
|
+
if theme not in get_args(KerykeionChartTheme) and theme is not None:
|
|
481
|
+
raise KerykeionException(f"Theme {theme} is not available. Set None for default theme.")
|
|
482
|
+
|
|
483
|
+
self.set_up_theme(theme)
|
|
484
|
+
|
|
485
|
+
self._apply_dynamic_height_adjustment()
|
|
486
|
+
self._adjust_height_for_extended_aspect_columns()
|
|
487
|
+
# Reconcile width with the updated layout once height adjustments are known.
|
|
488
|
+
if self.auto_size:
|
|
489
|
+
self._update_width_to_content()
|
|
490
|
+
|
|
491
|
+
def _count_active_planets(self) -> int:
|
|
492
|
+
"""Return number of active celestial points in the current chart."""
|
|
493
|
+
return len([p for p in self.available_planets_setting if p.get("is_active")])
|
|
494
|
+
|
|
495
|
+
def _apply_dynamic_height_adjustment(self) -> None:
|
|
496
|
+
"""Adjust chart height and vertical offsets based on active points."""
|
|
497
|
+
active_points_count = self._count_active_planets()
|
|
498
|
+
|
|
499
|
+
offsets = self._BASE_VERTICAL_OFFSETS.copy()
|
|
500
|
+
|
|
501
|
+
minimum_height = self._DEFAULT_HEIGHT
|
|
502
|
+
|
|
503
|
+
if self.chart_type == "Synastry":
|
|
504
|
+
self._apply_synastry_height_adjustment(
|
|
505
|
+
active_points_count=active_points_count,
|
|
506
|
+
offsets=offsets,
|
|
507
|
+
minimum_height=minimum_height,
|
|
508
|
+
)
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
if active_points_count <= 20:
|
|
512
|
+
self.height = max(self.height, minimum_height)
|
|
513
|
+
self._vertical_offsets = offsets
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
extra_points = active_points_count - 20
|
|
517
|
+
extra_height = extra_points * self._ROW_HEIGHT
|
|
518
|
+
|
|
519
|
+
self.height = max(self.height, minimum_height + extra_height)
|
|
520
|
+
|
|
521
|
+
delta_height = max(self.height - minimum_height, 0)
|
|
522
|
+
|
|
523
|
+
# Anchor wheel, aspect grid/list, and lunar phase to the bottom
|
|
524
|
+
offsets["wheel"] += delta_height
|
|
525
|
+
offsets["aspect_grid"] += delta_height
|
|
526
|
+
offsets["aspect_list"] += delta_height
|
|
527
|
+
offsets["lunar_phase"] += delta_height
|
|
528
|
+
offsets["bottom_left"] += delta_height
|
|
529
|
+
|
|
530
|
+
# Smooth top offsets to keep breathing room near the title and grids
|
|
531
|
+
shift = min(extra_points * self._TOP_SHIFT_FACTOR, self._MAX_TOP_SHIFT)
|
|
532
|
+
top_shift = shift // 2
|
|
533
|
+
|
|
534
|
+
offsets["grid"] += shift
|
|
535
|
+
offsets["title"] += top_shift
|
|
536
|
+
offsets["elements"] += top_shift
|
|
537
|
+
offsets["qualities"] += top_shift
|
|
538
|
+
|
|
539
|
+
self._vertical_offsets = offsets
|
|
540
|
+
|
|
541
|
+
def _adjust_height_for_extended_aspect_columns(self) -> None:
|
|
542
|
+
"""Ensure tall aspect columns fit within the SVG for double-chart lists."""
|
|
543
|
+
if self.double_chart_aspect_grid_type != "list":
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
if self.chart_type not in ("Synastry", "Transit", "DualReturnChart"):
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
total_aspects = len(self.aspects_list) if hasattr(self, "aspects_list") else 0
|
|
550
|
+
if total_aspects == 0:
|
|
551
|
+
return
|
|
552
|
+
|
|
553
|
+
aspects_per_column = 14
|
|
554
|
+
extended_column_start = 11 # Zero-based column index where tall columns begin
|
|
555
|
+
base_capacity = aspects_per_column * extended_column_start
|
|
556
|
+
|
|
557
|
+
if total_aspects <= base_capacity:
|
|
558
|
+
return
|
|
559
|
+
|
|
560
|
+
translate_y = 273
|
|
561
|
+
bottom_padding = 40
|
|
562
|
+
title_clearance = 18
|
|
563
|
+
line_height = 14
|
|
564
|
+
baseline_index = aspects_per_column - 1
|
|
565
|
+
top_limit_index = ceil((-translate_y + title_clearance) / line_height)
|
|
566
|
+
max_capacity_by_top = baseline_index - top_limit_index + 1
|
|
567
|
+
|
|
568
|
+
if max_capacity_by_top <= aspects_per_column:
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
target_capacity = max_capacity_by_top
|
|
572
|
+
required_available_height = target_capacity * line_height
|
|
573
|
+
required_height = translate_y + bottom_padding + required_available_height
|
|
574
|
+
|
|
575
|
+
if required_height <= self.height:
|
|
576
|
+
return
|
|
577
|
+
|
|
578
|
+
delta = required_height - self.height
|
|
579
|
+
self.height = required_height
|
|
580
|
+
|
|
581
|
+
offsets = self._vertical_offsets
|
|
582
|
+
# Keep bottom-anchored groups aligned after changing the overall height.
|
|
583
|
+
offsets["wheel"] += delta
|
|
584
|
+
offsets["aspect_grid"] += delta
|
|
585
|
+
offsets["aspect_list"] += delta
|
|
586
|
+
offsets["lunar_phase"] += delta
|
|
587
|
+
offsets["bottom_left"] += delta
|
|
588
|
+
self._vertical_offsets = offsets
|
|
589
|
+
|
|
590
|
+
def _apply_synastry_height_adjustment(
|
|
591
|
+
self,
|
|
592
|
+
*,
|
|
593
|
+
active_points_count: int,
|
|
594
|
+
offsets: dict[str, int],
|
|
595
|
+
minimum_height: int,
|
|
596
|
+
) -> None:
|
|
597
|
+
"""Specialised dynamic height handling for Synastry charts.
|
|
598
|
+
|
|
599
|
+
With the planet grids locked to a single column, every additional active
|
|
600
|
+
point extends multiple tables vertically (planets, houses, comparisons).
|
|
601
|
+
We therefore scale the height using the actual line spacing used by those
|
|
602
|
+
tables (≈14px) and keep the bottom anchored elements aligned.
|
|
603
|
+
"""
|
|
604
|
+
base_rows = 14 # Up to 16 active points fit without extra height
|
|
605
|
+
extra_rows = max(active_points_count - base_rows, 0)
|
|
606
|
+
|
|
607
|
+
synastry_row_height = 15
|
|
608
|
+
comparison_padding_per_row = 4 # Keeps house comparison grids within view.
|
|
609
|
+
extra_height = extra_rows * (synastry_row_height + comparison_padding_per_row)
|
|
610
|
+
|
|
611
|
+
self.height = max(self.height, minimum_height + extra_height)
|
|
612
|
+
|
|
613
|
+
delta_height = max(self.height - minimum_height, 0)
|
|
614
|
+
|
|
615
|
+
offsets["wheel"] += delta_height
|
|
616
|
+
offsets["aspect_grid"] += delta_height
|
|
617
|
+
offsets["aspect_list"] += delta_height
|
|
618
|
+
offsets["lunar_phase"] += delta_height
|
|
619
|
+
offsets["bottom_left"] += delta_height
|
|
620
|
+
|
|
621
|
+
row_height_ratio = synastry_row_height / max(self._ROW_HEIGHT, 1)
|
|
622
|
+
synastry_top_shift_factor = max(
|
|
623
|
+
self._TOP_SHIFT_FACTOR,
|
|
624
|
+
int(ceil(self._TOP_SHIFT_FACTOR * row_height_ratio)),
|
|
625
|
+
)
|
|
626
|
+
shift = min(extra_rows * synastry_top_shift_factor, self._MAX_TOP_SHIFT)
|
|
627
|
+
|
|
628
|
+
base_grid_padding = 36
|
|
629
|
+
grid_padding_per_row = 6
|
|
630
|
+
base_header_padding = 12
|
|
631
|
+
header_padding_per_row = 4
|
|
632
|
+
min_title_to_grid_gap = 36
|
|
633
|
+
|
|
634
|
+
grid_shift = shift + base_grid_padding + (extra_rows * grid_padding_per_row)
|
|
635
|
+
grid_shift = min(grid_shift, shift + self._MAX_TOP_SHIFT)
|
|
636
|
+
|
|
637
|
+
top_shift = (shift // 2) + base_header_padding + (extra_rows * header_padding_per_row)
|
|
638
|
+
|
|
639
|
+
max_allowed_shift = shift + self._MAX_TOP_SHIFT
|
|
640
|
+
missing_gap = min_title_to_grid_gap - (grid_shift - top_shift)
|
|
641
|
+
grid_shift = min(grid_shift + missing_gap, max_allowed_shift)
|
|
642
|
+
if grid_shift - top_shift < min_title_to_grid_gap:
|
|
643
|
+
top_shift = max(0, grid_shift - min_title_to_grid_gap)
|
|
644
|
+
|
|
645
|
+
offsets["grid"] += grid_shift
|
|
646
|
+
offsets["title"] += top_shift
|
|
647
|
+
offsets["elements"] += top_shift
|
|
648
|
+
offsets["qualities"] += top_shift
|
|
649
|
+
|
|
650
|
+
self._vertical_offsets = offsets
|
|
651
|
+
|
|
652
|
+
def _collect_subject_points(
|
|
653
|
+
self,
|
|
654
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
655
|
+
point_attribute_names: list[str],
|
|
656
|
+
) -> list[KerykeionPointModel]:
|
|
657
|
+
"""Collect ordered active celestial points for a subject."""
|
|
658
|
+
|
|
659
|
+
collected: list[KerykeionPointModel] = []
|
|
660
|
+
|
|
661
|
+
for raw_name in point_attribute_names:
|
|
662
|
+
attr_name = raw_name if hasattr(subject, raw_name) else raw_name.lower()
|
|
663
|
+
point = getattr(subject, attr_name, None)
|
|
664
|
+
if point is None:
|
|
665
|
+
continue
|
|
666
|
+
collected.append(point)
|
|
667
|
+
|
|
668
|
+
return collected
|
|
669
|
+
|
|
670
|
+
def _dynamic_viewbox(self) -> str:
|
|
671
|
+
"""Return the viewBox string based on current width/height with vertical padding."""
|
|
672
|
+
min_y = -self._VERTICAL_PADDING_TOP
|
|
673
|
+
viewbox_height = int(self.height) + self._VERTICAL_PADDING_TOP + self._VERTICAL_PADDING_BOTTOM
|
|
674
|
+
return f"0 {min_y} {int(self.width)} {viewbox_height}"
|
|
675
|
+
|
|
676
|
+
def _wheel_only_viewbox(self, margin: int = 20) -> str:
|
|
677
|
+
"""Return a tight viewBox for the wheel-only template.
|
|
678
|
+
|
|
679
|
+
The wheel is drawn inside a group translated by (100, 50) and has
|
|
680
|
+
diameter 2 * main_radius. We add a small margin around it.
|
|
681
|
+
"""
|
|
682
|
+
left = 100 - margin
|
|
683
|
+
top = 50 - margin
|
|
684
|
+
width = (2 * self.main_radius) + (2 * margin)
|
|
685
|
+
height = (2 * self.main_radius) + (2 * margin)
|
|
686
|
+
return f"{left} {top} {width} {height}"
|
|
687
|
+
|
|
688
|
+
def _grid_only_viewbox(self, margin: int = 10) -> str:
|
|
689
|
+
"""Compute a tight viewBox for the Aspect Grid Only SVG.
|
|
690
|
+
|
|
691
|
+
The grid is rendered using fixed origins and box size:
|
|
692
|
+
- For Transit/Synastry/DualReturn charts, `draw_transit_aspect_grid`
|
|
693
|
+
uses `x_indent=50`, `y_indent=250`, `box_size=14` and draws:
|
|
694
|
+
• a header row to the right of `x_indent`
|
|
695
|
+
• a left header column at `x_indent - box_size`
|
|
696
|
+
• an NxN grid of cells above `y_indent`
|
|
697
|
+
|
|
698
|
+
- For Natal/Composite/SingleReturn charts, `draw_aspect_grid` uses
|
|
699
|
+
`x_start=50`, `y_start=250`, `box_size=14` and draws a triangular grid
|
|
700
|
+
that extends to the right (x) and upwards (y).
|
|
701
|
+
|
|
702
|
+
This function mirrors that geometry to return a snug viewBox around the
|
|
703
|
+
content, with a small configurable `margin`.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
margin: Extra pixels to add on each side of the computed bounds.
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
A string "minX minY width height" suitable for the SVG `viewBox`.
|
|
710
|
+
"""
|
|
711
|
+
# Must match defaults used in the renderers
|
|
712
|
+
x0 = 50
|
|
713
|
+
y0 = 250
|
|
714
|
+
box = 14
|
|
715
|
+
|
|
716
|
+
n = max(len([p for p in self.available_planets_setting if p.get("is_active")]), 1)
|
|
717
|
+
|
|
718
|
+
if self.chart_type in ("Transit", "Synastry", "DualReturnChart"):
|
|
719
|
+
# Full N×N grid
|
|
720
|
+
left = (x0 - box) - margin
|
|
721
|
+
top = (y0 - box * n) - margin
|
|
722
|
+
right = (x0 + box * n) + margin
|
|
723
|
+
bottom = (y0 + box) + margin
|
|
724
|
+
else:
|
|
725
|
+
# Triangular grid (no extra left column)
|
|
726
|
+
left = x0 - margin
|
|
727
|
+
top = (y0 - box * n) - margin
|
|
728
|
+
right = (x0 + box * n) + margin
|
|
729
|
+
bottom = (y0 + box) + margin
|
|
730
|
+
|
|
731
|
+
width = max(1, int(right - left))
|
|
732
|
+
height = max(1, int(bottom - top))
|
|
733
|
+
|
|
734
|
+
return f"{int(left)} {int(top)} {width} {height}"
|
|
735
|
+
|
|
736
|
+
def _estimate_required_width_full(self) -> int:
|
|
737
|
+
"""Estimate minimal width to contain all rendered groups for the full chart.
|
|
738
|
+
|
|
739
|
+
The calculation is heuristic and mirrors the default x positions used in
|
|
740
|
+
the SVG templates and drawing utilities. We keep a conservative padding.
|
|
741
|
+
"""
|
|
742
|
+
# Wheel footprint (translate(100,50) + diameter of 2*radius)
|
|
743
|
+
wheel_right = 100 + (2 * self.main_radius)
|
|
744
|
+
extents = [wheel_right]
|
|
745
|
+
|
|
746
|
+
n_active = max(self._count_active_planets(), 1)
|
|
747
|
+
|
|
748
|
+
# Common grids present on many chart types
|
|
749
|
+
main_planet_grid_right = 645 + 80
|
|
750
|
+
main_houses_grid_right = 750 + 120
|
|
751
|
+
extents.extend([main_planet_grid_right, main_houses_grid_right])
|
|
752
|
+
|
|
753
|
+
if self.chart_type in ("Natal", "Composite", "SingleReturnChart"):
|
|
754
|
+
# Triangular aspect grid at x_start=540, width ~ 14 * n_active
|
|
755
|
+
aspect_grid_right = 560 + 14 * n_active
|
|
756
|
+
extents.append(aspect_grid_right)
|
|
757
|
+
|
|
758
|
+
if self.chart_type in ("Transit", "Synastry", "DualReturnChart"):
|
|
759
|
+
# Double-chart aspects placement
|
|
760
|
+
if self.double_chart_aspect_grid_type == "list":
|
|
761
|
+
total_aspects = len(self.aspects_list) if hasattr(self, "aspects_list") else 0
|
|
762
|
+
columns = self._calculate_double_chart_aspect_columns(total_aspects, self.height)
|
|
763
|
+
columns = max(columns, 1)
|
|
764
|
+
aspect_list_right = 565 + (columns * self._ASPECT_LIST_COLUMN_WIDTH)
|
|
765
|
+
extents.append(aspect_list_right)
|
|
766
|
+
else:
|
|
767
|
+
# Grid table placed with x_indent ~550, width ~ 14px per cell across n_active+1
|
|
768
|
+
aspect_grid_table_right = 550 + (14 * (n_active + 1))
|
|
769
|
+
extents.append(aspect_grid_table_right)
|
|
770
|
+
|
|
771
|
+
# Secondary grids
|
|
772
|
+
secondary_planet_grid_right = 910 + 80
|
|
773
|
+
extents.append(secondary_planet_grid_right)
|
|
774
|
+
|
|
775
|
+
if self.chart_type == "Synastry":
|
|
776
|
+
# Secondary houses grid default x ~ 1015
|
|
777
|
+
secondary_houses_grid_right = 1015 + 120
|
|
778
|
+
extents.append(secondary_houses_grid_right)
|
|
779
|
+
if self.second_obj is not None:
|
|
780
|
+
point_column_label = self._translate("point", "Point")
|
|
781
|
+
first_subject_label = self._truncate_name(self.first_obj.name, 8, "…", True) # type: ignore[union-attr]
|
|
782
|
+
second_subject_label = self._truncate_name(self.second_obj.name, 8, "…", True) # type: ignore[union-attr]
|
|
783
|
+
|
|
784
|
+
first_columns = [
|
|
785
|
+
f"{first_subject_label} {point_column_label}",
|
|
786
|
+
first_subject_label,
|
|
787
|
+
second_subject_label,
|
|
788
|
+
]
|
|
789
|
+
second_columns = [
|
|
790
|
+
f"{second_subject_label} {point_column_label}",
|
|
791
|
+
second_subject_label,
|
|
792
|
+
first_subject_label,
|
|
793
|
+
]
|
|
794
|
+
|
|
795
|
+
first_grid_width = self._estimate_house_comparison_grid_width(
|
|
796
|
+
column_labels=first_columns,
|
|
797
|
+
include_radix_column=True,
|
|
798
|
+
include_title=True,
|
|
799
|
+
)
|
|
800
|
+
second_grid_width = self._estimate_house_comparison_grid_width(
|
|
801
|
+
column_labels=second_columns,
|
|
802
|
+
include_radix_column=True,
|
|
803
|
+
include_title=False,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
first_house_comparison_grid_right = 1090 + first_grid_width
|
|
807
|
+
second_house_comparison_grid_right = 1290 + second_grid_width
|
|
808
|
+
extents.extend([first_house_comparison_grid_right, second_house_comparison_grid_right])
|
|
809
|
+
|
|
810
|
+
if self.chart_type == "Transit":
|
|
811
|
+
# House comparison grid at x ~ 1030
|
|
812
|
+
transit_columns = [
|
|
813
|
+
self._translate("transit_point", "Transit Point"),
|
|
814
|
+
self._translate("house_position", "Natal House"),
|
|
815
|
+
]
|
|
816
|
+
transit_grid_width = self._estimate_house_comparison_grid_width(
|
|
817
|
+
column_labels=transit_columns,
|
|
818
|
+
include_radix_column=False,
|
|
819
|
+
include_title=True,
|
|
820
|
+
minimum_width=170.0,
|
|
821
|
+
)
|
|
822
|
+
house_comparison_grid_right = 980 + transit_grid_width
|
|
823
|
+
extents.append(house_comparison_grid_right)
|
|
824
|
+
|
|
825
|
+
if self.chart_type == "DualReturnChart":
|
|
826
|
+
# House comparison grid translated to x ~ 1100
|
|
827
|
+
dual_return_columns = [
|
|
828
|
+
self._translate("return_point", "Return Point"),
|
|
829
|
+
self._translate("Return", "DualReturnChart"),
|
|
830
|
+
self._translate("Natal", "Natal"),
|
|
831
|
+
]
|
|
832
|
+
dual_return_grid_width = self._estimate_house_comparison_grid_width(
|
|
833
|
+
column_labels=dual_return_columns,
|
|
834
|
+
include_radix_column=True,
|
|
835
|
+
include_title=True,
|
|
836
|
+
)
|
|
837
|
+
house_comparison_grid_right = 1100 + dual_return_grid_width
|
|
838
|
+
extents.append(house_comparison_grid_right)
|
|
839
|
+
|
|
840
|
+
# Conservative safety padding
|
|
841
|
+
return int(max(extents) + self._padding)
|
|
842
|
+
|
|
843
|
+
def _calculate_double_chart_aspect_columns(
|
|
844
|
+
self,
|
|
845
|
+
total_aspects: int,
|
|
846
|
+
chart_height: Optional[int],
|
|
847
|
+
) -> int:
|
|
848
|
+
"""Return how many columns the double-chart aspect list needs.
|
|
849
|
+
|
|
850
|
+
The first 11 columns follow the legacy 14-rows layout. Starting from the
|
|
851
|
+
12th column we can fit more rows thanks to the taller chart height that
|
|
852
|
+
gets computed earlier, so we re-use the same capacity as the SVG builder.
|
|
853
|
+
"""
|
|
854
|
+
if total_aspects <= 0:
|
|
855
|
+
return 0
|
|
856
|
+
|
|
857
|
+
per_column = self._ASPECT_LIST_ASPECTS_PER_COLUMN
|
|
858
|
+
extended_start = 10 # 0-based index where tall columns begin
|
|
859
|
+
base_capacity = per_column * extended_start
|
|
860
|
+
|
|
861
|
+
full_height_capacity = self._calculate_full_height_column_capacity(chart_height)
|
|
862
|
+
|
|
863
|
+
if total_aspects <= base_capacity:
|
|
864
|
+
return ceil(total_aspects / per_column)
|
|
865
|
+
|
|
866
|
+
remaining = max(total_aspects - base_capacity, 0)
|
|
867
|
+
extra_columns = ceil(remaining / full_height_capacity) if remaining > 0 else 0
|
|
868
|
+
return extended_start + extra_columns
|
|
869
|
+
|
|
870
|
+
def _calculate_full_height_column_capacity(
|
|
871
|
+
self,
|
|
872
|
+
chart_height: Optional[int],
|
|
873
|
+
) -> int:
|
|
874
|
+
"""Compute the row capacity for columns that use the tall layout."""
|
|
875
|
+
per_column = self._ASPECT_LIST_ASPECTS_PER_COLUMN
|
|
876
|
+
|
|
877
|
+
if chart_height is None:
|
|
878
|
+
return per_column
|
|
879
|
+
|
|
880
|
+
translate_y = 273
|
|
881
|
+
bottom_padding = 40
|
|
882
|
+
title_clearance = 18
|
|
883
|
+
line_height = 14
|
|
884
|
+
baseline_index = per_column - 1
|
|
885
|
+
top_limit_index = ceil((-translate_y + title_clearance) / line_height)
|
|
886
|
+
max_capacity_by_top = baseline_index - top_limit_index + 1
|
|
887
|
+
|
|
888
|
+
available_height = max(chart_height - translate_y - bottom_padding, line_height)
|
|
889
|
+
allowed_capacity = max(per_column, int(available_height // line_height))
|
|
890
|
+
|
|
891
|
+
# Respect both the physical height of the SVG and the visual limit
|
|
892
|
+
# imposed by the title area.
|
|
893
|
+
return max(per_column, min(allowed_capacity, max_capacity_by_top))
|
|
894
|
+
|
|
895
|
+
@staticmethod
|
|
896
|
+
def _estimate_text_width(text: str, font_size: int) -> float:
|
|
897
|
+
"""Very rough text width estimation in pixels based on font size."""
|
|
898
|
+
if not text:
|
|
899
|
+
return 0.0
|
|
900
|
+
average_char_width = float(font_size)
|
|
901
|
+
return max(float(font_size), len(text) * average_char_width)
|
|
902
|
+
|
|
903
|
+
def _get_active_point_display_names(self) -> list[str]:
|
|
904
|
+
"""Return localized labels for the currently active celestial points."""
|
|
905
|
+
language_map = {}
|
|
906
|
+
fallback_map = {}
|
|
907
|
+
|
|
908
|
+
if hasattr(self, "_language_model"):
|
|
909
|
+
language_map = self._language_model.celestial_points.model_dump()
|
|
910
|
+
if hasattr(self, "_fallback_language_model"):
|
|
911
|
+
fallback_map = self._fallback_language_model.celestial_points.model_dump()
|
|
912
|
+
|
|
913
|
+
display_names: list[str] = []
|
|
914
|
+
for point in self.active_points:
|
|
915
|
+
key = str(point)
|
|
916
|
+
label = language_map.get(key) or fallback_map.get(key) or key
|
|
917
|
+
display_names.append(str(label))
|
|
918
|
+
return display_names
|
|
919
|
+
|
|
920
|
+
def _estimate_house_comparison_grid_width(
|
|
921
|
+
self,
|
|
922
|
+
*,
|
|
923
|
+
column_labels: Sequence[str],
|
|
924
|
+
include_radix_column: bool,
|
|
925
|
+
include_title: bool,
|
|
926
|
+
minimum_width: float = 250.0,
|
|
927
|
+
) -> int:
|
|
928
|
+
"""
|
|
929
|
+
Approximate the rendered width for a house comparison grid in the current locale.
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
column_labels: Ordered labels for the header row.
|
|
933
|
+
include_radix_column: Whether a third numeric column is rendered.
|
|
934
|
+
include_title: Include the localized title in the width estimation.
|
|
935
|
+
minimum_width: Absolute lower bound to prevent extreme shrinking.
|
|
936
|
+
"""
|
|
937
|
+
font_size_body = 10
|
|
938
|
+
font_size_title = 14
|
|
939
|
+
minimum_grid_width = float(minimum_width)
|
|
940
|
+
|
|
941
|
+
active_names = self._get_active_point_display_names()
|
|
942
|
+
max_name_width = max(
|
|
943
|
+
(self._estimate_text_width(name, font_size_body) for name in active_names),
|
|
944
|
+
default=self._estimate_text_width("Sun", font_size_body),
|
|
945
|
+
)
|
|
946
|
+
width_candidates: list[float] = []
|
|
947
|
+
|
|
948
|
+
name_start = 15
|
|
949
|
+
width_candidates.append(name_start + max_name_width)
|
|
950
|
+
|
|
951
|
+
value_offsets = [90]
|
|
952
|
+
if include_radix_column:
|
|
953
|
+
value_offsets.append(140)
|
|
954
|
+
value_samples = ("12", "-", "0")
|
|
955
|
+
max_value_width = max((self._estimate_text_width(sample, font_size_body) for sample in value_samples))
|
|
956
|
+
for offset in value_offsets:
|
|
957
|
+
width_candidates.append(offset + max_value_width)
|
|
958
|
+
|
|
959
|
+
header_offsets = [0, 77]
|
|
960
|
+
if include_radix_column:
|
|
961
|
+
header_offsets.append(132)
|
|
962
|
+
for idx, offset in enumerate(header_offsets):
|
|
963
|
+
label = column_labels[idx] if idx < len(column_labels) else ""
|
|
964
|
+
if not label:
|
|
965
|
+
continue
|
|
966
|
+
width_candidates.append(offset + self._estimate_text_width(label, font_size_body))
|
|
967
|
+
|
|
968
|
+
if include_title:
|
|
969
|
+
title_label = self._translate("house_position_comparison", "House Position Comparison")
|
|
970
|
+
width_candidates.append(self._estimate_text_width(title_label, font_size_title))
|
|
971
|
+
|
|
972
|
+
grid_width = max(width_candidates, default=minimum_grid_width)
|
|
973
|
+
return int(max(grid_width, minimum_grid_width))
|
|
974
|
+
|
|
975
|
+
def _minimum_width_for_chart_type(self) -> int:
|
|
976
|
+
"""Baseline width to avoid compressing core groups too tightly."""
|
|
977
|
+
wheel_right = 100 + (2 * self.main_radius)
|
|
978
|
+
baseline = wheel_right + self._padding
|
|
979
|
+
|
|
980
|
+
if self.chart_type in ("Natal", "Composite", "SingleReturnChart"):
|
|
981
|
+
return max(int(baseline), self._DEFAULT_NATAL_WIDTH)
|
|
982
|
+
if self.chart_type == "Synastry":
|
|
983
|
+
return max(int(baseline), self._DEFAULT_SYNASTRY_WIDTH // 2)
|
|
984
|
+
if self.chart_type == "DualReturnChart":
|
|
985
|
+
return max(int(baseline), self._DEFAULT_ULTRA_WIDE_WIDTH // 2)
|
|
986
|
+
if self.chart_type == "Transit":
|
|
987
|
+
return max(int(baseline), 450)
|
|
988
|
+
return max(int(baseline), self._DEFAULT_NATAL_WIDTH)
|
|
989
|
+
|
|
990
|
+
def _update_width_to_content(self) -> None:
|
|
991
|
+
"""Resize the chart width so the farthest element fits comfortably."""
|
|
992
|
+
try:
|
|
993
|
+
required_width = self._estimate_required_width_full()
|
|
994
|
+
except Exception as e:
|
|
995
|
+
logger.debug("Auto-size width calculation failed: %s", e)
|
|
996
|
+
return
|
|
997
|
+
|
|
998
|
+
minimum_width = self._minimum_width_for_chart_type()
|
|
999
|
+
self.width = max(required_width, minimum_width)
|
|
1000
|
+
|
|
1001
|
+
def _get_location_info(self) -> tuple[str, float, float]:
|
|
1002
|
+
"""
|
|
1003
|
+
Determine location information based on chart type and subjects.
|
|
1004
|
+
|
|
1005
|
+
Returns:
|
|
1006
|
+
tuple: (location_name, latitude, longitude)
|
|
1007
|
+
"""
|
|
1008
|
+
if self.chart_type == "Composite":
|
|
1009
|
+
# For composite charts, use average location of the two composite subjects
|
|
1010
|
+
if isinstance(self.first_obj, CompositeSubjectModel):
|
|
1011
|
+
location_name = ""
|
|
1012
|
+
latitude = (self.first_obj.first_subject.lat + self.first_obj.second_subject.lat) / 2
|
|
1013
|
+
longitude = (self.first_obj.first_subject.lng + self.first_obj.second_subject.lng) / 2
|
|
1014
|
+
else:
|
|
1015
|
+
# Fallback to first subject location
|
|
1016
|
+
location_name = self.first_obj.city or "Unknown"
|
|
1017
|
+
latitude = self.first_obj.lat or 0.0
|
|
1018
|
+
longitude = self.first_obj.lng or 0.0
|
|
1019
|
+
elif self.chart_type in ["Transit", "DualReturnChart"] and self.second_obj:
|
|
1020
|
+
# Use location from the second subject (transit/return)
|
|
1021
|
+
location_name = self.second_obj.city or "Unknown"
|
|
1022
|
+
latitude = self.second_obj.lat or 0.0
|
|
1023
|
+
longitude = self.second_obj.lng or 0.0
|
|
1024
|
+
else:
|
|
1025
|
+
# Use location from the first subject
|
|
1026
|
+
location_name = self.first_obj.city or "Unknown"
|
|
1027
|
+
latitude = self.first_obj.lat or 0.0
|
|
1028
|
+
longitude = self.first_obj.lng or 0.0
|
|
1029
|
+
|
|
1030
|
+
return location_name, latitude, longitude
|
|
1031
|
+
|
|
1032
|
+
def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None:
|
|
1033
|
+
"""
|
|
1034
|
+
Load and apply a CSS theme for the chart visualization.
|
|
1035
|
+
|
|
1036
|
+
Args:
|
|
1037
|
+
theme (KerykeionChartTheme or None): Name of the theme to apply. If None, no CSS is applied.
|
|
1038
|
+
"""
|
|
1039
|
+
if theme is None:
|
|
1040
|
+
self.color_style_tag = ""
|
|
1041
|
+
return
|
|
1042
|
+
|
|
1043
|
+
theme_dir = Path(__file__).parent / "themes"
|
|
1044
|
+
|
|
1045
|
+
with open(theme_dir / f"{theme}.css", "r") as f:
|
|
1046
|
+
self.color_style_tag = f.read()
|
|
1047
|
+
|
|
1048
|
+
def _load_language_settings(
|
|
1049
|
+
self,
|
|
1050
|
+
language_pack: Optional[Mapping[str, Any]],
|
|
1051
|
+
) -> None:
|
|
1052
|
+
"""Resolve language models for the requested chart language."""
|
|
1053
|
+
overrides = {self.chart_language: dict(language_pack)} if language_pack else None
|
|
1054
|
+
languages = load_language_settings(overrides)
|
|
1055
|
+
|
|
1056
|
+
fallback_data = languages.get("EN")
|
|
1057
|
+
if fallback_data is None:
|
|
1058
|
+
raise KerykeionException("English translations are missing from LANGUAGE_SETTINGS.")
|
|
1059
|
+
|
|
1060
|
+
base_data = languages.get(self.chart_language, fallback_data)
|
|
1061
|
+
selected_model = KerykeionLanguageModel(**base_data)
|
|
1062
|
+
fallback_model = KerykeionLanguageModel(**fallback_data)
|
|
1063
|
+
|
|
1064
|
+
self._fallback_language_model = fallback_model
|
|
1065
|
+
self._language_model = selected_model
|
|
1066
|
+
self._fallback_language_dict = fallback_model.model_dump()
|
|
1067
|
+
self._language_dict = selected_model.model_dump()
|
|
1068
|
+
self.language_settings = self._language_dict # Backward compatibility
|
|
1069
|
+
|
|
1070
|
+
def _translate(self, key: str, default: Any) -> Any:
|
|
1071
|
+
fallback_value = get_translations(key, default, language_dict=self._fallback_language_dict)
|
|
1072
|
+
return get_translations(key, fallback_value, language_dict=self._language_dict)
|
|
1073
|
+
|
|
1074
|
+
def _draw_zodiac_circle_slices(self, r):
|
|
1075
|
+
"""
|
|
1076
|
+
Draw zodiac circle slices for each sign.
|
|
1077
|
+
|
|
1078
|
+
Args:
|
|
1079
|
+
r (float): Outer radius of the zodiac ring.
|
|
1080
|
+
|
|
1081
|
+
Returns:
|
|
1082
|
+
str: Concatenated SVG elements for zodiac slices.
|
|
1083
|
+
"""
|
|
1084
|
+
sings = get_args(Sign)
|
|
1085
|
+
output = ""
|
|
1086
|
+
for i, sing in enumerate(sings):
|
|
1087
|
+
output += draw_zodiac_slice(
|
|
1088
|
+
c1=self.first_circle_radius,
|
|
1089
|
+
chart_type=self.chart_type,
|
|
1090
|
+
seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1091
|
+
num=i,
|
|
1092
|
+
r=r,
|
|
1093
|
+
style=f'fill:{self.chart_colors_settings[f"zodiac_bg_{i}"]}; fill-opacity: 0.5;',
|
|
1094
|
+
type=sing,
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
return output
|
|
1098
|
+
|
|
1099
|
+
def _draw_all_aspects_lines(self, r, ar):
|
|
1100
|
+
"""
|
|
1101
|
+
Render SVG lines for all aspects in the chart.
|
|
1102
|
+
|
|
1103
|
+
Args:
|
|
1104
|
+
r (float): Radius at which aspect lines originate.
|
|
1105
|
+
ar (float): Radius at which aspect lines terminate.
|
|
1106
|
+
|
|
1107
|
+
Returns:
|
|
1108
|
+
str: SVG markup for all aspect lines.
|
|
1109
|
+
"""
|
|
1110
|
+
out = ""
|
|
1111
|
+
for aspect in self.aspects_list:
|
|
1112
|
+
aspect_name = aspect["aspect"]
|
|
1113
|
+
aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None)
|
|
1114
|
+
if aspect_color:
|
|
1115
|
+
out += draw_aspect_line(
|
|
1116
|
+
r=r,
|
|
1117
|
+
ar=ar,
|
|
1118
|
+
aspect=aspect,
|
|
1119
|
+
color=aspect_color,
|
|
1120
|
+
seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1121
|
+
)
|
|
1122
|
+
return out
|
|
1123
|
+
|
|
1124
|
+
def _draw_all_transit_aspects_lines(self, r, ar):
|
|
1125
|
+
"""
|
|
1126
|
+
Render SVG lines for all transit aspects in the chart.
|
|
1127
|
+
|
|
1128
|
+
Args:
|
|
1129
|
+
r (float): Radius at which transit aspect lines originate.
|
|
1130
|
+
ar (float): Radius at which transit aspect lines terminate.
|
|
1131
|
+
|
|
1132
|
+
Returns:
|
|
1133
|
+
str: SVG markup for all transit aspect lines.
|
|
1134
|
+
"""
|
|
1135
|
+
out = ""
|
|
1136
|
+
for aspect in self.aspects_list:
|
|
1137
|
+
aspect_name = aspect["aspect"]
|
|
1138
|
+
aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None)
|
|
1139
|
+
if aspect_color:
|
|
1140
|
+
out += draw_aspect_line(
|
|
1141
|
+
r=r,
|
|
1142
|
+
ar=ar,
|
|
1143
|
+
aspect=aspect,
|
|
1144
|
+
color=aspect_color,
|
|
1145
|
+
seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1146
|
+
)
|
|
1147
|
+
return out
|
|
1148
|
+
|
|
1149
|
+
def _truncate_name(self, name: str, max_length: int = 50, ellipsis_symbol: str = "…", truncate_at_space: bool = False) -> str:
|
|
1150
|
+
"""
|
|
1151
|
+
Truncate a name if it's too long, preserving readability.
|
|
1152
|
+
|
|
1153
|
+
Args:
|
|
1154
|
+
name (str): The name to truncate
|
|
1155
|
+
max_length (int): Maximum allowed length
|
|
1156
|
+
|
|
1157
|
+
Returns:
|
|
1158
|
+
str: Truncated name with ellipsis if needed
|
|
1159
|
+
"""
|
|
1160
|
+
if truncate_at_space:
|
|
1161
|
+
name = name.split(" ")[0]
|
|
1162
|
+
|
|
1163
|
+
if len(name) <= max_length:
|
|
1164
|
+
return name
|
|
1165
|
+
|
|
1166
|
+
return name[:max_length-1] + ellipsis_symbol
|
|
1167
|
+
|
|
1168
|
+
def _get_chart_title(self) -> str:
|
|
1169
|
+
"""
|
|
1170
|
+
Generate the chart title based on chart type and custom title settings.
|
|
1171
|
+
|
|
1172
|
+
If a custom title is provided, it will be used. Otherwise, generates the
|
|
1173
|
+
appropriate default title based on the chart type and subjects.
|
|
1174
|
+
|
|
1175
|
+
Returns:
|
|
1176
|
+
str: The chart title to display (max ~40 characters).
|
|
1177
|
+
"""
|
|
1178
|
+
# If custom title is provided, use it
|
|
1179
|
+
if self.custom_title is not None:
|
|
1180
|
+
return self.custom_title
|
|
1181
|
+
|
|
1182
|
+
# Generate default title based on chart type
|
|
1183
|
+
if self.chart_type == "Natal":
|
|
1184
|
+
natal_label = self._translate("birth_chart", "Natal")
|
|
1185
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
1186
|
+
return f'{truncated_name} - {natal_label}'
|
|
1187
|
+
|
|
1188
|
+
elif self.chart_type == "Composite":
|
|
1189
|
+
composite_label = self._translate("composite_chart", "Composite")
|
|
1190
|
+
and_word = self._translate("and_word", "&")
|
|
1191
|
+
name1 = self._truncate_name(self.first_obj.first_subject.name) # type: ignore
|
|
1192
|
+
name2 = self._truncate_name(self.first_obj.second_subject.name) # type: ignore
|
|
1193
|
+
return f"{composite_label}: {name1} {and_word} {name2}"
|
|
1194
|
+
|
|
1195
|
+
elif self.chart_type == "Transit":
|
|
1196
|
+
transit_label = self._translate("transits", "Transits")
|
|
1197
|
+
date_obj = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime) # type: ignore
|
|
1198
|
+
date_str = date_obj.strftime("%d/%m/%y")
|
|
1199
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
1200
|
+
return f"{truncated_name} - {transit_label} {date_str}"
|
|
1201
|
+
|
|
1202
|
+
elif self.chart_type == "Synastry":
|
|
1203
|
+
synastry_label = self._translate("synastry_chart", "Synastry")
|
|
1204
|
+
and_word = self._translate("and_word", "&")
|
|
1205
|
+
name1 = self._truncate_name(self.first_obj.name)
|
|
1206
|
+
name2 = self._truncate_name(self.second_obj.name) # type: ignore
|
|
1207
|
+
return f"{synastry_label}: {name1} {and_word} {name2}"
|
|
1208
|
+
|
|
1209
|
+
elif self.chart_type == "DualReturnChart":
|
|
1210
|
+
return_datetime = datetime.fromisoformat(self.second_obj.iso_formatted_local_datetime) # type: ignore
|
|
1211
|
+
year = return_datetime.year
|
|
1212
|
+
month_year = return_datetime.strftime("%m/%Y")
|
|
1213
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
1214
|
+
if self.second_obj is not None and isinstance(self.second_obj, PlanetReturnModel) and self.second_obj.return_type == "Solar":
|
|
1215
|
+
solar_label = self._translate("solar_return", "Solar")
|
|
1216
|
+
return f"{truncated_name} - {solar_label} {year}"
|
|
1217
|
+
else:
|
|
1218
|
+
lunar_label = self._translate("lunar_return", "Lunar")
|
|
1219
|
+
return f"{truncated_name} - {lunar_label} {month_year}"
|
|
1220
|
+
|
|
1221
|
+
elif self.chart_type == "SingleReturnChart":
|
|
1222
|
+
return_datetime = datetime.fromisoformat(self.first_obj.iso_formatted_local_datetime) # type: ignore
|
|
1223
|
+
year = return_datetime.year
|
|
1224
|
+
month_year = return_datetime.strftime("%m/%Y")
|
|
1225
|
+
truncated_name = self._truncate_name(self.first_obj.name)
|
|
1226
|
+
if isinstance(self.first_obj, PlanetReturnModel) and self.first_obj.return_type == "Solar":
|
|
1227
|
+
solar_label = self._translate("solar_return", "Solar")
|
|
1228
|
+
return f"{truncated_name} - {solar_label} {year}"
|
|
1229
|
+
else:
|
|
1230
|
+
lunar_label = self._translate("lunar_return", "Lunar")
|
|
1231
|
+
return f"{truncated_name} - {lunar_label} {month_year}"
|
|
1232
|
+
|
|
1233
|
+
# Fallback for unknown chart types
|
|
1234
|
+
return self._truncate_name(self.first_obj.name)
|
|
1235
|
+
|
|
1236
|
+
def _create_template_dictionary(self) -> ChartTemplateModel:
|
|
1237
|
+
"""
|
|
1238
|
+
Assemble chart data and rendering instructions into a template dictionary.
|
|
1239
|
+
|
|
1240
|
+
Gathers styling, dimensions, and SVG fragments for chart components based on
|
|
1241
|
+
chart type and subjects.
|
|
1242
|
+
|
|
1243
|
+
Returns:
|
|
1244
|
+
ChartTemplateModel: Populated structure of template variables.
|
|
1245
|
+
"""
|
|
1246
|
+
# Initialize template dictionary
|
|
1247
|
+
template_dict: dict = {}
|
|
1248
|
+
|
|
1249
|
+
# -------------------------------------#
|
|
1250
|
+
# COMMON SETTINGS FOR ALL CHART TYPES #
|
|
1251
|
+
# -------------------------------------#
|
|
1252
|
+
|
|
1253
|
+
# Set the color style tag and basic dimensions
|
|
1254
|
+
template_dict["color_style_tag"] = self.color_style_tag
|
|
1255
|
+
template_dict["chart_height"] = self.height
|
|
1256
|
+
template_dict["chart_width"] = self.width
|
|
1257
|
+
|
|
1258
|
+
offsets = self._vertical_offsets
|
|
1259
|
+
template_dict["full_wheel_translate_y"] = offsets["wheel"]
|
|
1260
|
+
template_dict["houses_and_planets_translate_y"] = offsets["grid"]
|
|
1261
|
+
template_dict["aspect_grid_translate_y"] = offsets["aspect_grid"]
|
|
1262
|
+
template_dict["aspect_list_translate_y"] = offsets["aspect_list"]
|
|
1263
|
+
template_dict["title_translate_y"] = offsets["title"]
|
|
1264
|
+
template_dict["elements_translate_y"] = offsets["elements"]
|
|
1265
|
+
template_dict["qualities_translate_y"] = offsets["qualities"]
|
|
1266
|
+
template_dict["lunar_phase_translate_y"] = offsets["lunar_phase"]
|
|
1267
|
+
template_dict["bottom_left_translate_y"] = offsets["bottom_left"]
|
|
1268
|
+
|
|
1269
|
+
# Set paper colors
|
|
1270
|
+
template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"]
|
|
1271
|
+
|
|
1272
|
+
# Set background color based on transparent_background setting
|
|
1273
|
+
if self.transparent_background:
|
|
1274
|
+
template_dict["background_color"] = "transparent"
|
|
1275
|
+
else:
|
|
1276
|
+
template_dict["background_color"] = self.chart_colors_settings["paper_1"]
|
|
1277
|
+
|
|
1278
|
+
# Set planet colors - initialize all possible colors first with defaults
|
|
1279
|
+
default_color = "#000000" # Default black color for unused planets
|
|
1280
|
+
for i in range(42): # Support all 42 celestial points (0-41)
|
|
1281
|
+
template_dict[f"planets_color_{i}"] = default_color
|
|
1282
|
+
|
|
1283
|
+
# Override with actual colors from settings
|
|
1284
|
+
for planet in self.planets_settings:
|
|
1285
|
+
planet_id = planet["id"]
|
|
1286
|
+
template_dict[f"planets_color_{planet_id}"] = planet["color"]
|
|
1287
|
+
|
|
1288
|
+
# Set zodiac colors
|
|
1289
|
+
for i in range(12):
|
|
1290
|
+
template_dict[f"zodiac_color_{i}"] = self.chart_colors_settings[f"zodiac_icon_{i}"]
|
|
1291
|
+
|
|
1292
|
+
# Set orb colors
|
|
1293
|
+
for aspect in self.aspects_settings:
|
|
1294
|
+
template_dict[f"orb_color_{aspect['degree']}"] = aspect["color"]
|
|
1295
|
+
|
|
1296
|
+
# Draw zodiac circle slices
|
|
1297
|
+
template_dict["makeZodiac"] = self._draw_zodiac_circle_slices(self.main_radius)
|
|
1298
|
+
|
|
1299
|
+
# Calculate element percentages
|
|
1300
|
+
total_elements = self.fire + self.water + self.earth + self.air
|
|
1301
|
+
element_values = {"fire": self.fire, "earth": self.earth, "air": self.air, "water": self.water}
|
|
1302
|
+
element_percentages = distribute_percentages_to_100(element_values) if total_elements > 0 else {"fire": 0, "earth": 0, "air": 0, "water": 0}
|
|
1303
|
+
fire_percentage = element_percentages["fire"]
|
|
1304
|
+
earth_percentage = element_percentages["earth"]
|
|
1305
|
+
air_percentage = element_percentages["air"]
|
|
1306
|
+
water_percentage = element_percentages["water"]
|
|
1307
|
+
|
|
1308
|
+
# Element Percentages
|
|
1309
|
+
template_dict["elements_string"] = f"{self._translate('elements', 'Elements')}:"
|
|
1310
|
+
template_dict["fire_string"] = f"{self._translate('fire', 'Fire')} {fire_percentage}%"
|
|
1311
|
+
template_dict["earth_string"] = f"{self._translate('earth', 'Earth')} {earth_percentage}%"
|
|
1312
|
+
template_dict["air_string"] = f"{self._translate('air', 'Air')} {air_percentage}%"
|
|
1313
|
+
template_dict["water_string"] = f"{self._translate('water', 'Water')} {water_percentage}%"
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
# Qualities Percentages
|
|
1317
|
+
total_qualities = self.cardinal + self.fixed + self.mutable
|
|
1318
|
+
quality_values = {"cardinal": self.cardinal, "fixed": self.fixed, "mutable": self.mutable}
|
|
1319
|
+
quality_percentages = distribute_percentages_to_100(quality_values) if total_qualities > 0 else {"cardinal": 0, "fixed": 0, "mutable": 0}
|
|
1320
|
+
cardinal_percentage = quality_percentages["cardinal"]
|
|
1321
|
+
fixed_percentage = quality_percentages["fixed"]
|
|
1322
|
+
mutable_percentage = quality_percentages["mutable"]
|
|
1323
|
+
|
|
1324
|
+
template_dict["qualities_string"] = f"{self._translate('qualities', 'Qualities')}:"
|
|
1325
|
+
template_dict["cardinal_string"] = f"{self._translate('cardinal', 'Cardinal')} {cardinal_percentage}%"
|
|
1326
|
+
template_dict["fixed_string"] = f"{self._translate('fixed', 'Fixed')} {fixed_percentage}%"
|
|
1327
|
+
template_dict["mutable_string"] = f"{self._translate('mutable', 'Mutable')} {mutable_percentage}%"
|
|
1328
|
+
|
|
1329
|
+
# Get houses list for main subject
|
|
1330
|
+
first_subject_houses_list = get_houses_list(self.first_obj)
|
|
1331
|
+
|
|
1332
|
+
# Chart title
|
|
1333
|
+
template_dict["stringTitle"] = self._get_chart_title()
|
|
1334
|
+
|
|
1335
|
+
# ------------------------------- #
|
|
1336
|
+
# CHART TYPE SPECIFIC SETTINGS #
|
|
1337
|
+
# ------------------------------- #
|
|
1338
|
+
|
|
1339
|
+
if self.chart_type == "Natal":
|
|
1340
|
+
# Set viewbox dynamically
|
|
1341
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1342
|
+
|
|
1343
|
+
# Rings and circles
|
|
1344
|
+
template_dict["transitRing"] = ""
|
|
1345
|
+
template_dict["degreeRing"] = draw_degree_ring(
|
|
1346
|
+
self.main_radius,
|
|
1347
|
+
self.first_circle_radius,
|
|
1348
|
+
self.first_obj.seventh_house.abs_pos,
|
|
1349
|
+
self.chart_colors_settings["paper_0"],
|
|
1350
|
+
)
|
|
1351
|
+
template_dict["background_circle"] = draw_background_circle(
|
|
1352
|
+
self.main_radius,
|
|
1353
|
+
self.chart_colors_settings["paper_1"],
|
|
1354
|
+
self.chart_colors_settings["paper_1"],
|
|
1355
|
+
)
|
|
1356
|
+
template_dict["first_circle"] = draw_first_circle(
|
|
1357
|
+
self.main_radius,
|
|
1358
|
+
self.chart_colors_settings["zodiac_radix_ring_2"],
|
|
1359
|
+
self.chart_type,
|
|
1360
|
+
self.first_circle_radius,
|
|
1361
|
+
)
|
|
1362
|
+
template_dict["second_circle"] = draw_second_circle(
|
|
1363
|
+
self.main_radius,
|
|
1364
|
+
self.chart_colors_settings["zodiac_radix_ring_1"],
|
|
1365
|
+
self.chart_colors_settings["paper_1"],
|
|
1366
|
+
self.chart_type,
|
|
1367
|
+
self.second_circle_radius,
|
|
1368
|
+
)
|
|
1369
|
+
template_dict["third_circle"] = draw_third_circle(
|
|
1370
|
+
self.main_radius,
|
|
1371
|
+
self.chart_colors_settings["zodiac_radix_ring_0"],
|
|
1372
|
+
self.chart_colors_settings["paper_1"],
|
|
1373
|
+
self.chart_type,
|
|
1374
|
+
self.third_circle_radius,
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
# Aspects
|
|
1378
|
+
template_dict["makeDoubleChartAspectList"] = ""
|
|
1379
|
+
template_dict["makeAspectGrid"] = draw_aspect_grid(
|
|
1380
|
+
self.chart_colors_settings["paper_0"],
|
|
1381
|
+
self.available_planets_setting,
|
|
1382
|
+
self.aspects_list,
|
|
1383
|
+
)
|
|
1384
|
+
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
1385
|
+
|
|
1386
|
+
# Top left section
|
|
1387
|
+
latitude_string = convert_latitude_coordinate_to_string(
|
|
1388
|
+
self.geolat,
|
|
1389
|
+
self._translate("north", "North"),
|
|
1390
|
+
self._translate("south", "South"),
|
|
1391
|
+
)
|
|
1392
|
+
longitude_string = convert_longitude_coordinate_to_string(
|
|
1393
|
+
self.geolon,
|
|
1394
|
+
self._translate("east", "East"),
|
|
1395
|
+
self._translate("west", "West"),
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
template_dict["top_left_0"] = f'{self._translate("location", "Location")}:'
|
|
1399
|
+
template_dict["top_left_1"] = f"{self.first_obj.city}, {self.first_obj.nation}"
|
|
1400
|
+
template_dict["top_left_2"] = f"{self._translate('latitude', 'Latitude')}: {latitude_string}"
|
|
1401
|
+
template_dict["top_left_3"] = f"{self._translate('longitude', 'Longitude')}: {longitude_string}"
|
|
1402
|
+
template_dict["top_left_4"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
|
|
1403
|
+
localized_weekday = self._translate(
|
|
1404
|
+
f"weekdays.{self.first_obj.day_of_week}",
|
|
1405
|
+
self.first_obj.day_of_week, # type: ignore[arg-type]
|
|
1406
|
+
)
|
|
1407
|
+
template_dict["top_left_5"] = f"{self._translate('day_of_week', 'Day of Week')}: {localized_weekday}" # type: ignore
|
|
1408
|
+
|
|
1409
|
+
# Bottom left section
|
|
1410
|
+
if self.first_obj.zodiac_type == "Tropical":
|
|
1411
|
+
zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
|
|
1412
|
+
else:
|
|
1413
|
+
mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
|
|
1414
|
+
mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
|
|
1415
|
+
zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
|
|
1416
|
+
|
|
1417
|
+
template_dict["bottom_left_0"] = zodiac_info
|
|
1418
|
+
template_dict["bottom_left_1"] = (
|
|
1419
|
+
f"{self._translate('domification', 'Domification')}: "
|
|
1420
|
+
f"{self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)}"
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
# Lunar phase information (optional)
|
|
1424
|
+
if self.first_obj.lunar_phase is not None:
|
|
1425
|
+
template_dict["bottom_left_2"] = (
|
|
1426
|
+
f'{self._translate("lunation_day", "Lunation Day")}: '
|
|
1427
|
+
f'{self.first_obj.lunar_phase.get("moon_phase", "")}'
|
|
1428
|
+
)
|
|
1429
|
+
template_dict["bottom_left_3"] = (
|
|
1430
|
+
f'{self._translate("lunar_phase", "Lunar Phase")}: '
|
|
1431
|
+
f'{self._translate(self.first_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.first_obj.lunar_phase.moon_phase_name)}'
|
|
1432
|
+
)
|
|
1433
|
+
else:
|
|
1434
|
+
template_dict["bottom_left_2"] = ""
|
|
1435
|
+
template_dict["bottom_left_3"] = ""
|
|
1436
|
+
|
|
1437
|
+
template_dict["bottom_left_4"] = (
|
|
1438
|
+
f'{self._translate("perspective_type", "Perspective")}: '
|
|
1439
|
+
f'{self._translate(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
# Moon phase section calculations
|
|
1443
|
+
if self.first_obj.lunar_phase is not None:
|
|
1444
|
+
template_dict["makeLunarPhase"] = makeLunarPhase(self.first_obj.lunar_phase["degrees_between_s_m"], self.geolat)
|
|
1445
|
+
else:
|
|
1446
|
+
template_dict["makeLunarPhase"] = ""
|
|
1447
|
+
|
|
1448
|
+
# Houses and planet drawing
|
|
1449
|
+
template_dict["makeMainHousesGrid"] = draw_main_house_grid(
|
|
1450
|
+
main_subject_houses_list=first_subject_houses_list,
|
|
1451
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
1452
|
+
house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
|
|
1453
|
+
)
|
|
1454
|
+
template_dict["makeSecondaryHousesGrid"] = ""
|
|
1455
|
+
|
|
1456
|
+
template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
|
|
1457
|
+
r=self.main_radius,
|
|
1458
|
+
first_subject_houses_list=first_subject_houses_list,
|
|
1459
|
+
standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
|
|
1460
|
+
first_house_color=self.planets_settings[12]["color"],
|
|
1461
|
+
tenth_house_color=self.planets_settings[13]["color"],
|
|
1462
|
+
seventh_house_color=self.planets_settings[14]["color"],
|
|
1463
|
+
fourth_house_color=self.planets_settings[15]["color"],
|
|
1464
|
+
c1=self.first_circle_radius,
|
|
1465
|
+
c3=self.third_circle_radius,
|
|
1466
|
+
chart_type=self.chart_type,
|
|
1467
|
+
external_view=self.external_view,
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
template_dict["makePlanets"] = draw_planets(
|
|
1471
|
+
available_planets_setting=self.available_planets_setting,
|
|
1472
|
+
chart_type=self.chart_type,
|
|
1473
|
+
radius=self.main_radius,
|
|
1474
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
1475
|
+
third_circle_radius=self.third_circle_radius,
|
|
1476
|
+
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
1477
|
+
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1478
|
+
external_view=self.external_view,
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
1482
|
+
planets_and_houses_grid_title=self._translate("planets_and_house", "Points for"),
|
|
1483
|
+
subject_name=self.first_obj.name,
|
|
1484
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
1485
|
+
chart_type=self.chart_type,
|
|
1486
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
1487
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
1488
|
+
)
|
|
1489
|
+
template_dict["makeSecondaryPlanetGrid"] = ""
|
|
1490
|
+
template_dict["makeHouseComparisonGrid"] = ""
|
|
1491
|
+
|
|
1492
|
+
elif self.chart_type == "Composite":
|
|
1493
|
+
# Set viewbox dynamically
|
|
1494
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1495
|
+
|
|
1496
|
+
# Rings and circles
|
|
1497
|
+
template_dict["transitRing"] = ""
|
|
1498
|
+
template_dict["degreeRing"] = draw_degree_ring(
|
|
1499
|
+
self.main_radius,
|
|
1500
|
+
self.first_circle_radius,
|
|
1501
|
+
self.first_obj.seventh_house.abs_pos,
|
|
1502
|
+
self.chart_colors_settings["paper_0"],
|
|
1503
|
+
)
|
|
1504
|
+
template_dict["background_circle"] = draw_background_circle(
|
|
1505
|
+
self.main_radius,
|
|
1506
|
+
self.chart_colors_settings["paper_1"],
|
|
1507
|
+
self.chart_colors_settings["paper_1"],
|
|
1508
|
+
)
|
|
1509
|
+
template_dict["first_circle"] = draw_first_circle(
|
|
1510
|
+
self.main_radius,
|
|
1511
|
+
self.chart_colors_settings["zodiac_radix_ring_2"],
|
|
1512
|
+
self.chart_type,
|
|
1513
|
+
self.first_circle_radius,
|
|
1514
|
+
)
|
|
1515
|
+
template_dict["second_circle"] = draw_second_circle(
|
|
1516
|
+
self.main_radius,
|
|
1517
|
+
self.chart_colors_settings["zodiac_radix_ring_1"],
|
|
1518
|
+
self.chart_colors_settings["paper_1"],
|
|
1519
|
+
self.chart_type,
|
|
1520
|
+
self.second_circle_radius,
|
|
1521
|
+
)
|
|
1522
|
+
template_dict["third_circle"] = draw_third_circle(
|
|
1523
|
+
self.main_radius,
|
|
1524
|
+
self.chart_colors_settings["zodiac_radix_ring_0"],
|
|
1525
|
+
self.chart_colors_settings["paper_1"],
|
|
1526
|
+
self.chart_type,
|
|
1527
|
+
self.third_circle_radius,
|
|
1528
|
+
)
|
|
1529
|
+
|
|
1530
|
+
# Aspects
|
|
1531
|
+
template_dict["makeDoubleChartAspectList"] = ""
|
|
1532
|
+
template_dict["makeAspectGrid"] = draw_aspect_grid(
|
|
1533
|
+
self.chart_colors_settings["paper_0"],
|
|
1534
|
+
self.available_planets_setting,
|
|
1535
|
+
self.aspects_list,
|
|
1536
|
+
)
|
|
1537
|
+
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
1538
|
+
|
|
1539
|
+
# Top left section
|
|
1540
|
+
# First subject
|
|
1541
|
+
latitude = convert_latitude_coordinate_to_string(
|
|
1542
|
+
self.first_obj.first_subject.lat, # type: ignore
|
|
1543
|
+
self._translate("north_letter", "N"),
|
|
1544
|
+
self._translate("south_letter", "S"),
|
|
1545
|
+
)
|
|
1546
|
+
longitude = convert_longitude_coordinate_to_string(
|
|
1547
|
+
self.first_obj.first_subject.lng, # type: ignore
|
|
1548
|
+
self._translate("east_letter", "E"),
|
|
1549
|
+
self._translate("west_letter", "W"),
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
# Second subject
|
|
1553
|
+
latitude_string = convert_latitude_coordinate_to_string(
|
|
1554
|
+
self.first_obj.second_subject.lat, # type: ignore
|
|
1555
|
+
self._translate("north_letter", "N"),
|
|
1556
|
+
self._translate("south_letter", "S"),
|
|
1557
|
+
)
|
|
1558
|
+
longitude_string = convert_longitude_coordinate_to_string(
|
|
1559
|
+
self.first_obj.second_subject.lng, # type: ignore
|
|
1560
|
+
self._translate("east_letter", "E"),
|
|
1561
|
+
self._translate("west_letter", "W"),
|
|
1562
|
+
)
|
|
1563
|
+
|
|
1564
|
+
template_dict["top_left_0"] = f"{self.first_obj.first_subject.name}" # type: ignore
|
|
1565
|
+
template_dict["top_left_1"] = f"{datetime.fromisoformat(self.first_obj.first_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}" # type: ignore
|
|
1566
|
+
template_dict["top_left_2"] = f"{latitude} {longitude}"
|
|
1567
|
+
template_dict["top_left_3"] = self.first_obj.second_subject.name # type: ignore
|
|
1568
|
+
template_dict["top_left_4"] = f"{datetime.fromisoformat(self.first_obj.second_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}" # type: ignore
|
|
1569
|
+
template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}"
|
|
1570
|
+
|
|
1571
|
+
# Bottom left section
|
|
1572
|
+
if self.first_obj.zodiac_type == "Tropical":
|
|
1573
|
+
zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
|
|
1574
|
+
else:
|
|
1575
|
+
mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
|
|
1576
|
+
mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
|
|
1577
|
+
zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
|
|
1578
|
+
|
|
1579
|
+
template_dict["bottom_left_0"] = zodiac_info
|
|
1580
|
+
template_dict["bottom_left_1"] = f"{self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)} {self._translate('houses', 'Houses')}"
|
|
1581
|
+
template_dict["bottom_left_2"] = f'{self._translate("perspective_type", "Perspective")}: {self.first_obj.first_subject.perspective_type}' # type: ignore
|
|
1582
|
+
template_dict["bottom_left_3"] = f'{self._translate("composite_chart", "Composite Chart")} - {self._translate("midpoints", "Midpoints")}'
|
|
1583
|
+
template_dict["bottom_left_4"] = ""
|
|
1584
|
+
|
|
1585
|
+
# Moon phase section calculations
|
|
1586
|
+
if self.first_obj.lunar_phase is not None:
|
|
1587
|
+
template_dict["makeLunarPhase"] = makeLunarPhase(self.first_obj.lunar_phase["degrees_between_s_m"], self.geolat)
|
|
1588
|
+
else:
|
|
1589
|
+
template_dict["makeLunarPhase"] = ""
|
|
1590
|
+
|
|
1591
|
+
# Houses and planet drawing
|
|
1592
|
+
template_dict["makeMainHousesGrid"] = draw_main_house_grid(
|
|
1593
|
+
main_subject_houses_list=first_subject_houses_list,
|
|
1594
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
1595
|
+
house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
|
|
1596
|
+
)
|
|
1597
|
+
template_dict["makeSecondaryHousesGrid"] = ""
|
|
1598
|
+
|
|
1599
|
+
template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
|
|
1600
|
+
r=self.main_radius,
|
|
1601
|
+
first_subject_houses_list=first_subject_houses_list,
|
|
1602
|
+
standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
|
|
1603
|
+
first_house_color=self.planets_settings[12]["color"],
|
|
1604
|
+
tenth_house_color=self.planets_settings[13]["color"],
|
|
1605
|
+
seventh_house_color=self.planets_settings[14]["color"],
|
|
1606
|
+
fourth_house_color=self.planets_settings[15]["color"],
|
|
1607
|
+
c1=self.first_circle_radius,
|
|
1608
|
+
c3=self.third_circle_radius,
|
|
1609
|
+
chart_type=self.chart_type,
|
|
1610
|
+
external_view=self.external_view,
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
template_dict["makePlanets"] = draw_planets(
|
|
1614
|
+
available_planets_setting=self.available_planets_setting,
|
|
1615
|
+
chart_type=self.chart_type,
|
|
1616
|
+
radius=self.main_radius,
|
|
1617
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
1618
|
+
third_circle_radius=self.third_circle_radius,
|
|
1619
|
+
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
1620
|
+
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1621
|
+
external_view=self.external_view,
|
|
1622
|
+
)
|
|
1623
|
+
|
|
1624
|
+
subject_name = (
|
|
1625
|
+
f"{self.first_obj.first_subject.name}"
|
|
1626
|
+
f" {self._translate('and_word', '&')} "
|
|
1627
|
+
f"{self.first_obj.second_subject.name}"
|
|
1628
|
+
) # type: ignore
|
|
1629
|
+
|
|
1630
|
+
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
1631
|
+
planets_and_houses_grid_title=self._translate("planets_and_house", "Points for"),
|
|
1632
|
+
subject_name=subject_name,
|
|
1633
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
1634
|
+
chart_type=self.chart_type,
|
|
1635
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
1636
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
1637
|
+
)
|
|
1638
|
+
template_dict["makeSecondaryPlanetGrid"] = ""
|
|
1639
|
+
template_dict["makeHouseComparisonGrid"] = ""
|
|
1640
|
+
|
|
1641
|
+
elif self.chart_type == "Transit":
|
|
1642
|
+
|
|
1643
|
+
# Transit has no Element Percentages
|
|
1644
|
+
template_dict["elements_string"] = ""
|
|
1645
|
+
template_dict["fire_string"] = ""
|
|
1646
|
+
template_dict["earth_string"] = ""
|
|
1647
|
+
template_dict["air_string"] = ""
|
|
1648
|
+
template_dict["water_string"] = ""
|
|
1649
|
+
|
|
1650
|
+
# Transit has no Qualities Percentages
|
|
1651
|
+
template_dict["qualities_string"] = ""
|
|
1652
|
+
template_dict["cardinal_string"] = ""
|
|
1653
|
+
template_dict["fixed_string"] = ""
|
|
1654
|
+
template_dict["mutable_string"] = ""
|
|
1655
|
+
|
|
1656
|
+
# Set viewbox dynamically
|
|
1657
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1658
|
+
|
|
1659
|
+
# Get houses list for secondary subject
|
|
1660
|
+
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
1661
|
+
|
|
1662
|
+
# Rings and circles
|
|
1663
|
+
template_dict["transitRing"] = draw_transit_ring(
|
|
1664
|
+
self.main_radius,
|
|
1665
|
+
self.chart_colors_settings["paper_1"],
|
|
1666
|
+
self.chart_colors_settings["zodiac_transit_ring_3"],
|
|
1667
|
+
)
|
|
1668
|
+
template_dict["degreeRing"] = draw_transit_ring_degree_steps(self.main_radius, self.first_obj.seventh_house.abs_pos)
|
|
1669
|
+
template_dict["background_circle"] = draw_background_circle(
|
|
1670
|
+
self.main_radius,
|
|
1671
|
+
self.chart_colors_settings["paper_1"],
|
|
1672
|
+
self.chart_colors_settings["paper_1"],
|
|
1673
|
+
)
|
|
1674
|
+
template_dict["first_circle"] = draw_first_circle(
|
|
1675
|
+
self.main_radius,
|
|
1676
|
+
self.chart_colors_settings["zodiac_transit_ring_2"],
|
|
1677
|
+
self.chart_type,
|
|
1678
|
+
)
|
|
1679
|
+
template_dict["second_circle"] = draw_second_circle(
|
|
1680
|
+
self.main_radius,
|
|
1681
|
+
self.chart_colors_settings["zodiac_transit_ring_1"],
|
|
1682
|
+
self.chart_colors_settings["paper_1"],
|
|
1683
|
+
self.chart_type,
|
|
1684
|
+
)
|
|
1685
|
+
template_dict["third_circle"] = draw_third_circle(
|
|
1686
|
+
self.main_radius,
|
|
1687
|
+
self.chart_colors_settings["zodiac_transit_ring_0"],
|
|
1688
|
+
self.chart_colors_settings["paper_1"],
|
|
1689
|
+
self.chart_type,
|
|
1690
|
+
self.third_circle_radius,
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
# Aspects
|
|
1694
|
+
if self.double_chart_aspect_grid_type == "list":
|
|
1695
|
+
title = f'{self.first_obj.name} - {self._translate("transit_aspects", "Transit Aspects")}'
|
|
1696
|
+
template_dict["makeAspectGrid"] = ""
|
|
1697
|
+
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(
|
|
1698
|
+
title,
|
|
1699
|
+
self.aspects_list,
|
|
1700
|
+
self.planets_settings,
|
|
1701
|
+
self.aspects_settings,
|
|
1702
|
+
chart_height=self.height,
|
|
1703
|
+
) # type: ignore[arg-type]
|
|
1704
|
+
else:
|
|
1705
|
+
template_dict["makeAspectGrid"] = ""
|
|
1706
|
+
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
|
|
1707
|
+
self.chart_colors_settings["paper_0"],
|
|
1708
|
+
self.available_planets_setting,
|
|
1709
|
+
self.aspects_list,
|
|
1710
|
+
600,
|
|
1711
|
+
520,
|
|
1712
|
+
)
|
|
1713
|
+
|
|
1714
|
+
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1715
|
+
|
|
1716
|
+
# Top left section (clear separation of Natal vs Transit details)
|
|
1717
|
+
natal_latitude_string = (
|
|
1718
|
+
convert_latitude_coordinate_to_string(
|
|
1719
|
+
self.first_obj.lat, # type: ignore[arg-type]
|
|
1720
|
+
self._translate("north_letter", "N"),
|
|
1721
|
+
self._translate("south_letter", "S"),
|
|
1722
|
+
)
|
|
1723
|
+
if getattr(self.first_obj, "lat", None) is not None
|
|
1724
|
+
else ""
|
|
1725
|
+
)
|
|
1726
|
+
natal_longitude_string = (
|
|
1727
|
+
convert_longitude_coordinate_to_string(
|
|
1728
|
+
self.first_obj.lng, # type: ignore[arg-type]
|
|
1729
|
+
self._translate("east_letter", "E"),
|
|
1730
|
+
self._translate("west_letter", "W"),
|
|
1731
|
+
)
|
|
1732
|
+
if getattr(self.first_obj, "lng", None) is not None
|
|
1733
|
+
else ""
|
|
1734
|
+
)
|
|
1735
|
+
|
|
1736
|
+
transit_latitude_string = ""
|
|
1737
|
+
transit_longitude_string = ""
|
|
1738
|
+
if self.second_obj is not None:
|
|
1739
|
+
if getattr(self.second_obj, "lat", None) is not None:
|
|
1740
|
+
transit_latitude_string = convert_latitude_coordinate_to_string(
|
|
1741
|
+
self.second_obj.lat, # type: ignore[arg-type]
|
|
1742
|
+
self._translate("north_letter", "N"),
|
|
1743
|
+
self._translate("south_letter", "S"),
|
|
1744
|
+
)
|
|
1745
|
+
if getattr(self.second_obj, "lng", None) is not None:
|
|
1746
|
+
transit_longitude_string = convert_longitude_coordinate_to_string(
|
|
1747
|
+
self.second_obj.lng, # type: ignore[arg-type]
|
|
1748
|
+
self._translate("east_letter", "E"),
|
|
1749
|
+
self._translate("west_letter", "W"),
|
|
1750
|
+
)
|
|
1751
|
+
|
|
1752
|
+
natal_dt = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore[arg-type]
|
|
1753
|
+
natal_place = f"{format_location_string(self.first_obj.city)}, {self.first_obj.nation}" # type: ignore[arg-type]
|
|
1754
|
+
transit_dt = (
|
|
1755
|
+
format_datetime_with_timezone(self.second_obj.iso_formatted_local_datetime) # type: ignore[arg-type]
|
|
1756
|
+
if self.second_obj is not None and getattr(self.second_obj, "iso_formatted_local_datetime", None) is not None
|
|
1757
|
+
else ""
|
|
1758
|
+
)
|
|
1759
|
+
transit_place = (
|
|
1760
|
+
f"{format_location_string(self.second_obj.city)}, {self.second_obj.nation}" # type: ignore[arg-type]
|
|
1761
|
+
if self.second_obj is not None
|
|
1762
|
+
else ""
|
|
1763
|
+
)
|
|
1764
|
+
|
|
1765
|
+
template_dict["top_left_0"] = f"{self._translate('chart_info_natal_label', 'Natal')}: {natal_dt}"
|
|
1766
|
+
template_dict["top_left_1"] = natal_place
|
|
1767
|
+
template_dict["top_left_2"] = f"{natal_latitude_string} · {natal_longitude_string}"
|
|
1768
|
+
template_dict["top_left_3"] = f"{self._translate('chart_info_transit_label', 'Transit')}: {transit_dt}"
|
|
1769
|
+
template_dict["top_left_4"] = transit_place
|
|
1770
|
+
template_dict["top_left_5"] = f"{transit_latitude_string} · {transit_longitude_string}"
|
|
1771
|
+
|
|
1772
|
+
# Bottom left section
|
|
1773
|
+
if self.first_obj.zodiac_type == "Tropical":
|
|
1774
|
+
zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
|
|
1775
|
+
else:
|
|
1776
|
+
mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
|
|
1777
|
+
mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
|
|
1778
|
+
zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
|
|
1779
|
+
|
|
1780
|
+
template_dict["bottom_left_0"] = zodiac_info
|
|
1781
|
+
template_dict["bottom_left_1"] = f"{self._translate('domification', 'Domification')}: {self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)}"
|
|
1782
|
+
|
|
1783
|
+
# Lunar phase information from second object (Transit) (optional)
|
|
1784
|
+
if self.second_obj is not None and hasattr(self.second_obj, 'lunar_phase') and self.second_obj.lunar_phase is not None:
|
|
1785
|
+
template_dict["bottom_left_3"] = (
|
|
1786
|
+
f"{self._translate('Transit', 'Transit')} "
|
|
1787
|
+
f"{self._translate('lunation_day', 'Lunation Day')}: "
|
|
1788
|
+
f"{self.second_obj.lunar_phase.get('moon_phase', '')}"
|
|
1789
|
+
) # type: ignore
|
|
1790
|
+
template_dict["bottom_left_4"] = (
|
|
1791
|
+
f"{self._translate('Transit', 'Transit')} "
|
|
1792
|
+
f"{self._translate('lunar_phase', 'Lunar Phase')}: "
|
|
1793
|
+
f"{self._translate(self.second_obj.lunar_phase.moon_phase_name.lower().replace(' ', '_'), self.second_obj.lunar_phase.moon_phase_name)}"
|
|
1794
|
+
)
|
|
1795
|
+
else:
|
|
1796
|
+
template_dict["bottom_left_3"] = ""
|
|
1797
|
+
template_dict["bottom_left_4"] = ""
|
|
1798
|
+
|
|
1799
|
+
template_dict["bottom_left_2"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.second_obj.perspective_type.lower().replace(" ", "_"), self.second_obj.perspective_type)}' # type: ignore
|
|
1800
|
+
|
|
1801
|
+
# Moon phase section calculations - use transit subject data only
|
|
1802
|
+
if self.second_obj is not None and getattr(self.second_obj, "lunar_phase", None):
|
|
1803
|
+
template_dict["makeLunarPhase"] = makeLunarPhase(self.second_obj.lunar_phase["degrees_between_s_m"], self.geolat)
|
|
1804
|
+
else:
|
|
1805
|
+
template_dict["makeLunarPhase"] = ""
|
|
1806
|
+
|
|
1807
|
+
# Houses and planet drawing
|
|
1808
|
+
template_dict["makeMainHousesGrid"] = draw_main_house_grid(
|
|
1809
|
+
main_subject_houses_list=first_subject_houses_list,
|
|
1810
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
1811
|
+
house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
|
|
1812
|
+
)
|
|
1813
|
+
# template_dict["makeSecondaryHousesGrid"] = draw_secondary_house_grid(
|
|
1814
|
+
# secondary_subject_houses_list=second_subject_houses_list,
|
|
1815
|
+
# text_color=self.chart_colors_settings["paper_0"],
|
|
1816
|
+
# house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
|
|
1817
|
+
# )
|
|
1818
|
+
template_dict["makeSecondaryHousesGrid"] = ""
|
|
1819
|
+
|
|
1820
|
+
template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
|
|
1821
|
+
r=self.main_radius,
|
|
1822
|
+
first_subject_houses_list=first_subject_houses_list,
|
|
1823
|
+
standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
|
|
1824
|
+
first_house_color=self.planets_settings[12]["color"],
|
|
1825
|
+
tenth_house_color=self.planets_settings[13]["color"],
|
|
1826
|
+
seventh_house_color=self.planets_settings[14]["color"],
|
|
1827
|
+
fourth_house_color=self.planets_settings[15]["color"],
|
|
1828
|
+
c1=self.first_circle_radius,
|
|
1829
|
+
c3=self.third_circle_radius,
|
|
1830
|
+
chart_type=self.chart_type,
|
|
1831
|
+
external_view=self.external_view,
|
|
1832
|
+
second_subject_houses_list=second_subject_houses_list,
|
|
1833
|
+
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
1834
|
+
)
|
|
1835
|
+
|
|
1836
|
+
template_dict["makePlanets"] = draw_planets(
|
|
1837
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
1838
|
+
available_planets_setting=self.available_planets_setting,
|
|
1839
|
+
second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
|
|
1840
|
+
radius=self.main_radius,
|
|
1841
|
+
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
1842
|
+
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
1843
|
+
chart_type=self.chart_type,
|
|
1844
|
+
third_circle_radius=self.third_circle_radius,
|
|
1845
|
+
external_view=self.external_view,
|
|
1846
|
+
)
|
|
1847
|
+
|
|
1848
|
+
# Planet grids
|
|
1849
|
+
first_name_label = self._truncate_name(self.first_obj.name)
|
|
1850
|
+
transit_label = self._translate("transit", "Transit")
|
|
1851
|
+
first_return_grid_title = f"{first_name_label} ({self._translate('inner_wheel', 'Inner Wheel')})"
|
|
1852
|
+
second_return_grid_title = f"{transit_label} ({self._translate('outer_wheel', 'Outer Wheel')})"
|
|
1853
|
+
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
1854
|
+
planets_and_houses_grid_title="",
|
|
1855
|
+
subject_name=first_return_grid_title,
|
|
1856
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
1857
|
+
chart_type=self.chart_type,
|
|
1858
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
1859
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
1860
|
+
)
|
|
1861
|
+
|
|
1862
|
+
template_dict["makeSecondaryPlanetGrid"] = draw_secondary_planet_grid(
|
|
1863
|
+
planets_and_houses_grid_title="",
|
|
1864
|
+
second_subject_name=second_return_grid_title,
|
|
1865
|
+
second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
|
|
1866
|
+
chart_type=self.chart_type,
|
|
1867
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
1868
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
1869
|
+
)
|
|
1870
|
+
|
|
1871
|
+
# House comparison grid
|
|
1872
|
+
house_comparison_factory = HouseComparisonFactory(
|
|
1873
|
+
first_subject=self.first_obj, # type: ignore[arg-type]
|
|
1874
|
+
second_subject=self.second_obj, # type: ignore[arg-type]
|
|
1875
|
+
active_points=self.active_points,
|
|
1876
|
+
)
|
|
1877
|
+
house_comparison = house_comparison_factory.get_house_comparison()
|
|
1878
|
+
|
|
1879
|
+
template_dict["makeHouseComparisonGrid"] = draw_single_house_comparison_grid(
|
|
1880
|
+
house_comparison,
|
|
1881
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
1882
|
+
active_points=self.active_points,
|
|
1883
|
+
points_owner_subject_number=2, # The second subject is the Transit
|
|
1884
|
+
house_position_comparison_label=self._translate("house_position_comparison", "House Position Comparison"),
|
|
1885
|
+
return_point_label=self._translate("transit_point", "Transit Point"),
|
|
1886
|
+
natal_house_label=self._translate("house_position", "Natal House"),
|
|
1887
|
+
x_position=980,
|
|
1888
|
+
)
|
|
1889
|
+
|
|
1890
|
+
elif self.chart_type == "Synastry":
|
|
1891
|
+
# Set viewbox dynamically
|
|
1892
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
1893
|
+
|
|
1894
|
+
# Get houses list for secondary subject
|
|
1895
|
+
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
1896
|
+
|
|
1897
|
+
# Rings and circles
|
|
1898
|
+
template_dict["transitRing"] = draw_transit_ring(
|
|
1899
|
+
self.main_radius,
|
|
1900
|
+
self.chart_colors_settings["paper_1"],
|
|
1901
|
+
self.chart_colors_settings["zodiac_transit_ring_3"],
|
|
1902
|
+
)
|
|
1903
|
+
template_dict["degreeRing"] = draw_transit_ring_degree_steps(self.main_radius, self.first_obj.seventh_house.abs_pos)
|
|
1904
|
+
template_dict["background_circle"] = draw_background_circle(
|
|
1905
|
+
self.main_radius,
|
|
1906
|
+
self.chart_colors_settings["paper_1"],
|
|
1907
|
+
self.chart_colors_settings["paper_1"],
|
|
1908
|
+
)
|
|
1909
|
+
template_dict["first_circle"] = draw_first_circle(
|
|
1910
|
+
self.main_radius,
|
|
1911
|
+
self.chart_colors_settings["zodiac_transit_ring_2"],
|
|
1912
|
+
self.chart_type,
|
|
1913
|
+
)
|
|
1914
|
+
template_dict["second_circle"] = draw_second_circle(
|
|
1915
|
+
self.main_radius,
|
|
1916
|
+
self.chart_colors_settings["zodiac_transit_ring_1"],
|
|
1917
|
+
self.chart_colors_settings["paper_1"],
|
|
1918
|
+
self.chart_type,
|
|
1919
|
+
)
|
|
1920
|
+
template_dict["third_circle"] = draw_third_circle(
|
|
1921
|
+
self.main_radius,
|
|
1922
|
+
self.chart_colors_settings["zodiac_transit_ring_0"],
|
|
1923
|
+
self.chart_colors_settings["paper_1"],
|
|
1924
|
+
self.chart_type,
|
|
1925
|
+
self.third_circle_radius,
|
|
1926
|
+
)
|
|
1927
|
+
|
|
1928
|
+
# Aspects
|
|
1929
|
+
if self.double_chart_aspect_grid_type == "list":
|
|
1930
|
+
template_dict["makeAspectGrid"] = ""
|
|
1931
|
+
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(
|
|
1932
|
+
f"{self.first_obj.name} - {self.second_obj.name} {self._translate('synastry_aspects', 'Synastry Aspects')}", # type: ignore[union-attr]
|
|
1933
|
+
self.aspects_list,
|
|
1934
|
+
self.planets_settings, # type: ignore[arg-type]
|
|
1935
|
+
self.aspects_settings, # type: ignore[arg-type]
|
|
1936
|
+
chart_height=self.height,
|
|
1937
|
+
)
|
|
1938
|
+
else:
|
|
1939
|
+
template_dict["makeAspectGrid"] = ""
|
|
1940
|
+
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
|
|
1941
|
+
self.chart_colors_settings["paper_0"],
|
|
1942
|
+
self.available_planets_setting,
|
|
1943
|
+
self.aspects_list,
|
|
1944
|
+
550,
|
|
1945
|
+
450,
|
|
1946
|
+
)
|
|
1947
|
+
|
|
1948
|
+
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
1949
|
+
|
|
1950
|
+
# Top left section
|
|
1951
|
+
template_dict["top_left_0"] = f"{self.first_obj.name}:"
|
|
1952
|
+
template_dict["top_left_1"] = f"{self.first_obj.city}, {self.first_obj.nation}" # type: ignore
|
|
1953
|
+
template_dict["top_left_2"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
|
|
1954
|
+
template_dict["top_left_3"] = f"{self.second_obj.name}: " # type: ignore
|
|
1955
|
+
template_dict["top_left_4"] = f"{self.second_obj.city}, {self.second_obj.nation}" # type: ignore
|
|
1956
|
+
template_dict["top_left_5"] = format_datetime_with_timezone(self.second_obj.iso_formatted_local_datetime) # type: ignore
|
|
1957
|
+
|
|
1958
|
+
# Bottom left section
|
|
1959
|
+
if self.first_obj.zodiac_type == "Tropical":
|
|
1960
|
+
zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
|
|
1961
|
+
else:
|
|
1962
|
+
mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
|
|
1963
|
+
mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
|
|
1964
|
+
zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
|
|
1965
|
+
|
|
1966
|
+
template_dict["bottom_left_0"] = ""
|
|
1967
|
+
# FIXME!
|
|
1968
|
+
template_dict["bottom_left_1"] = "" # f"Compatibility Score: {16}/44" # type: ignore
|
|
1969
|
+
template_dict["bottom_left_2"] = zodiac_info
|
|
1970
|
+
template_dict["bottom_left_3"] = f"{self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)} {self._translate('houses', 'Houses')}"
|
|
1971
|
+
template_dict["bottom_left_4"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
|
|
1972
|
+
|
|
1973
|
+
# Moon phase section calculations
|
|
1974
|
+
template_dict["makeLunarPhase"] = ""
|
|
1975
|
+
|
|
1976
|
+
# Houses and planet drawing
|
|
1977
|
+
template_dict["makeMainHousesGrid"] = draw_main_house_grid(
|
|
1978
|
+
main_subject_houses_list=first_subject_houses_list,
|
|
1979
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
1980
|
+
house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
|
|
1981
|
+
)
|
|
1982
|
+
|
|
1983
|
+
template_dict["makeSecondaryHousesGrid"] = draw_secondary_house_grid(
|
|
1984
|
+
secondary_subject_houses_list=second_subject_houses_list,
|
|
1985
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
1986
|
+
house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
|
|
1987
|
+
)
|
|
1988
|
+
|
|
1989
|
+
template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
|
|
1990
|
+
r=self.main_radius,
|
|
1991
|
+
first_subject_houses_list=first_subject_houses_list,
|
|
1992
|
+
standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
|
|
1993
|
+
first_house_color=self.planets_settings[12]["color"],
|
|
1994
|
+
tenth_house_color=self.planets_settings[13]["color"],
|
|
1995
|
+
seventh_house_color=self.planets_settings[14]["color"],
|
|
1996
|
+
fourth_house_color=self.planets_settings[15]["color"],
|
|
1997
|
+
c1=self.first_circle_radius,
|
|
1998
|
+
c3=self.third_circle_radius,
|
|
1999
|
+
chart_type=self.chart_type,
|
|
2000
|
+
external_view=self.external_view,
|
|
2001
|
+
second_subject_houses_list=second_subject_houses_list,
|
|
2002
|
+
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
2003
|
+
)
|
|
2004
|
+
|
|
2005
|
+
template_dict["makePlanets"] = draw_planets(
|
|
2006
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
2007
|
+
available_planets_setting=self.available_planets_setting,
|
|
2008
|
+
second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
|
|
2009
|
+
radius=self.main_radius,
|
|
2010
|
+
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
2011
|
+
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
2012
|
+
chart_type=self.chart_type,
|
|
2013
|
+
third_circle_radius=self.third_circle_radius,
|
|
2014
|
+
external_view=self.external_view,
|
|
2015
|
+
)
|
|
2016
|
+
|
|
2017
|
+
# Planet grid
|
|
2018
|
+
first_name_label = self._truncate_name(self.first_obj.name, 18, "…") # type: ignore[union-attr]
|
|
2019
|
+
second_name_label = self._truncate_name(self.second_obj.name, 18, "…") # type: ignore[union-attr]
|
|
2020
|
+
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
2021
|
+
planets_and_houses_grid_title="",
|
|
2022
|
+
subject_name=f"{first_name_label} ({self._translate('inner_wheel', 'Inner Wheel')})",
|
|
2023
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
2024
|
+
chart_type=self.chart_type,
|
|
2025
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
2026
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
2027
|
+
)
|
|
2028
|
+
template_dict["makeSecondaryPlanetGrid"] = draw_secondary_planet_grid(
|
|
2029
|
+
planets_and_houses_grid_title="",
|
|
2030
|
+
second_subject_name= f"{second_name_label} ({self._translate('outer_wheel', 'Outer Wheel')})", # type: ignore
|
|
2031
|
+
second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
|
|
2032
|
+
chart_type=self.chart_type,
|
|
2033
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
2034
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
2035
|
+
)
|
|
2036
|
+
house_comparison_factory = HouseComparisonFactory(
|
|
2037
|
+
first_subject=self.first_obj, # type: ignore[arg-type]
|
|
2038
|
+
second_subject=self.second_obj, # type: ignore[arg-type]
|
|
2039
|
+
active_points=self.active_points,
|
|
2040
|
+
)
|
|
2041
|
+
house_comparison = house_comparison_factory.get_house_comparison()
|
|
2042
|
+
|
|
2043
|
+
first_subject_label = self._truncate_name(self.first_obj.name, 8, "…", True) # type: ignore[union-attr]
|
|
2044
|
+
second_subject_label = self._truncate_name(self.second_obj.name, 8, "…", True) # type: ignore[union-attr]
|
|
2045
|
+
point_column_label = self._translate("point", "Point")
|
|
2046
|
+
comparison_label = self._translate("house_position_comparison", "House Position Comparison")
|
|
2047
|
+
|
|
2048
|
+
first_subject_grid = draw_house_comparison_grid(
|
|
2049
|
+
house_comparison,
|
|
2050
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
2051
|
+
active_points=self.active_points,
|
|
2052
|
+
points_owner_subject_number=1,
|
|
2053
|
+
house_position_comparison_label=comparison_label,
|
|
2054
|
+
return_point_label=first_subject_label + " " + point_column_label,
|
|
2055
|
+
return_label=first_subject_label,
|
|
2056
|
+
radix_label=second_subject_label,
|
|
2057
|
+
x_position=1090,
|
|
2058
|
+
y_position=0,
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
second_subject_grid = draw_house_comparison_grid(
|
|
2062
|
+
house_comparison,
|
|
2063
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
2064
|
+
active_points=self.active_points,
|
|
2065
|
+
points_owner_subject_number=2,
|
|
2066
|
+
house_position_comparison_label="",
|
|
2067
|
+
return_point_label=second_subject_label + " " + point_column_label,
|
|
2068
|
+
return_label=second_subject_label,
|
|
2069
|
+
radix_label=first_subject_label,
|
|
2070
|
+
x_position=1290,
|
|
2071
|
+
y_position=0,
|
|
2072
|
+
)
|
|
2073
|
+
|
|
2074
|
+
template_dict["makeHouseComparisonGrid"] = first_subject_grid + second_subject_grid
|
|
2075
|
+
|
|
2076
|
+
elif self.chart_type == "DualReturnChart":
|
|
2077
|
+
# Set viewbox dynamically
|
|
2078
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
2079
|
+
|
|
2080
|
+
# Get houses list for secondary subject
|
|
2081
|
+
second_subject_houses_list = get_houses_list(self.second_obj) # type: ignore
|
|
2082
|
+
|
|
2083
|
+
# Rings and circles
|
|
2084
|
+
template_dict["transitRing"] = draw_transit_ring(
|
|
2085
|
+
self.main_radius,
|
|
2086
|
+
self.chart_colors_settings["paper_1"],
|
|
2087
|
+
self.chart_colors_settings["zodiac_transit_ring_3"],
|
|
2088
|
+
)
|
|
2089
|
+
template_dict["degreeRing"] = draw_transit_ring_degree_steps(self.main_radius, self.first_obj.seventh_house.abs_pos)
|
|
2090
|
+
template_dict["background_circle"] = draw_background_circle(
|
|
2091
|
+
self.main_radius,
|
|
2092
|
+
self.chart_colors_settings["paper_1"],
|
|
2093
|
+
self.chart_colors_settings["paper_1"],
|
|
2094
|
+
)
|
|
2095
|
+
template_dict["first_circle"] = draw_first_circle(
|
|
2096
|
+
self.main_radius,
|
|
2097
|
+
self.chart_colors_settings["zodiac_transit_ring_2"],
|
|
2098
|
+
self.chart_type,
|
|
2099
|
+
)
|
|
2100
|
+
template_dict["second_circle"] = draw_second_circle(
|
|
2101
|
+
self.main_radius,
|
|
2102
|
+
self.chart_colors_settings["zodiac_transit_ring_1"],
|
|
2103
|
+
self.chart_colors_settings["paper_1"],
|
|
2104
|
+
self.chart_type,
|
|
2105
|
+
)
|
|
2106
|
+
template_dict["third_circle"] = draw_third_circle(
|
|
2107
|
+
self.main_radius,
|
|
2108
|
+
self.chart_colors_settings["zodiac_transit_ring_0"],
|
|
2109
|
+
self.chart_colors_settings["paper_1"],
|
|
2110
|
+
self.chart_type,
|
|
2111
|
+
self.third_circle_radius,
|
|
2112
|
+
)
|
|
2113
|
+
|
|
2114
|
+
# Aspects
|
|
2115
|
+
if self.double_chart_aspect_grid_type == "list":
|
|
2116
|
+
title = self._translate("return_aspects", "Natal to Return Aspects")
|
|
2117
|
+
template_dict["makeAspectGrid"] = ""
|
|
2118
|
+
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_list(
|
|
2119
|
+
title,
|
|
2120
|
+
self.aspects_list,
|
|
2121
|
+
self.planets_settings,
|
|
2122
|
+
self.aspects_settings,
|
|
2123
|
+
max_columns=7,
|
|
2124
|
+
chart_height=self.height,
|
|
2125
|
+
) # type: ignore[arg-type]
|
|
2126
|
+
else:
|
|
2127
|
+
template_dict["makeAspectGrid"] = ""
|
|
2128
|
+
template_dict["makeDoubleChartAspectList"] = draw_transit_aspect_grid(
|
|
2129
|
+
self.chart_colors_settings["paper_0"],
|
|
2130
|
+
self.available_planets_setting,
|
|
2131
|
+
self.aspects_list,
|
|
2132
|
+
550,
|
|
2133
|
+
450,
|
|
2134
|
+
)
|
|
2135
|
+
|
|
2136
|
+
template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
|
|
2137
|
+
|
|
2138
|
+
|
|
2139
|
+
# Top left section
|
|
2140
|
+
# Subject
|
|
2141
|
+
latitude_string = convert_latitude_coordinate_to_string(self.first_obj.lat, self._translate("north", "North"), self._translate("south", "South")) # type: ignore
|
|
2142
|
+
longitude_string = convert_longitude_coordinate_to_string(self.first_obj.lng, self._translate("east", "East"), self._translate("west", "West")) # type: ignore
|
|
2143
|
+
|
|
2144
|
+
# Return
|
|
2145
|
+
return_latitude_string = convert_latitude_coordinate_to_string(self.second_obj.lat, self._translate("north", "North"), self._translate("south", "South")) # type: ignore
|
|
2146
|
+
return_longitude_string = convert_longitude_coordinate_to_string(self.second_obj.lng, self._translate("east", "East"), self._translate("west", "West")) # type: ignore
|
|
2147
|
+
|
|
2148
|
+
if self.second_obj is not None and hasattr(self.second_obj, 'return_type') and self.second_obj.return_type == "Solar":
|
|
2149
|
+
template_dict["top_left_0"] = f"{self._translate('solar_return', 'Solar Return')}:"
|
|
2150
|
+
else:
|
|
2151
|
+
template_dict["top_left_0"] = f"{self._translate('lunar_return', 'Lunar Return')}:"
|
|
2152
|
+
template_dict["top_left_1"] = format_datetime_with_timezone(self.second_obj.iso_formatted_local_datetime) # type: ignore
|
|
2153
|
+
template_dict["top_left_2"] = f"{return_latitude_string} / {return_longitude_string}"
|
|
2154
|
+
template_dict["top_left_3"] = f"{self.first_obj.name}"
|
|
2155
|
+
template_dict["top_left_4"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
|
|
2156
|
+
template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}"
|
|
2157
|
+
|
|
2158
|
+
# Bottom left section
|
|
2159
|
+
if self.first_obj.zodiac_type == "Tropical":
|
|
2160
|
+
zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
|
|
2161
|
+
else:
|
|
2162
|
+
mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
|
|
2163
|
+
mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
|
|
2164
|
+
zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
|
|
2165
|
+
|
|
2166
|
+
template_dict["bottom_left_0"] = zodiac_info
|
|
2167
|
+
template_dict["bottom_left_1"] = f"{self._translate('domification', 'Domification')}: {self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)}"
|
|
2168
|
+
|
|
2169
|
+
# Lunar phase information (optional)
|
|
2170
|
+
if self.first_obj.lunar_phase is not None:
|
|
2171
|
+
template_dict["bottom_left_2"] = f'{self._translate("lunation_day", "Lunation Day")}: {self.first_obj.lunar_phase.get("moon_phase", "")}'
|
|
2172
|
+
template_dict["bottom_left_3"] = f'{self._translate("lunar_phase", "Lunar Phase")}: {self._translate(self.first_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.first_obj.lunar_phase.moon_phase_name)}'
|
|
2173
|
+
else:
|
|
2174
|
+
template_dict["bottom_left_2"] = ""
|
|
2175
|
+
template_dict["bottom_left_3"] = ""
|
|
2176
|
+
|
|
2177
|
+
template_dict["bottom_left_4"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
|
|
2178
|
+
|
|
2179
|
+
# Moon phase section calculations
|
|
2180
|
+
if self.first_obj.lunar_phase is not None:
|
|
2181
|
+
template_dict["makeLunarPhase"] = makeLunarPhase(self.first_obj.lunar_phase["degrees_between_s_m"], self.geolat)
|
|
2182
|
+
else:
|
|
2183
|
+
template_dict["makeLunarPhase"] = ""
|
|
2184
|
+
|
|
2185
|
+
# Houses and planet drawing
|
|
2186
|
+
template_dict["makeMainHousesGrid"] = draw_main_house_grid(
|
|
2187
|
+
main_subject_houses_list=first_subject_houses_list,
|
|
2188
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
2189
|
+
house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
|
|
2190
|
+
)
|
|
2191
|
+
|
|
2192
|
+
template_dict["makeSecondaryHousesGrid"] = draw_secondary_house_grid(
|
|
2193
|
+
secondary_subject_houses_list=second_subject_houses_list,
|
|
2194
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
2195
|
+
house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
|
|
2196
|
+
)
|
|
2197
|
+
|
|
2198
|
+
template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
|
|
2199
|
+
r=self.main_radius,
|
|
2200
|
+
first_subject_houses_list=first_subject_houses_list,
|
|
2201
|
+
standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
|
|
2202
|
+
first_house_color=self.planets_settings[12]["color"],
|
|
2203
|
+
tenth_house_color=self.planets_settings[13]["color"],
|
|
2204
|
+
seventh_house_color=self.planets_settings[14]["color"],
|
|
2205
|
+
fourth_house_color=self.planets_settings[15]["color"],
|
|
2206
|
+
c1=self.first_circle_radius,
|
|
2207
|
+
c3=self.third_circle_radius,
|
|
2208
|
+
chart_type=self.chart_type,
|
|
2209
|
+
external_view=self.external_view,
|
|
2210
|
+
second_subject_houses_list=second_subject_houses_list,
|
|
2211
|
+
transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
|
|
2212
|
+
)
|
|
2213
|
+
|
|
2214
|
+
template_dict["makePlanets"] = draw_planets(
|
|
2215
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
2216
|
+
available_planets_setting=self.available_planets_setting,
|
|
2217
|
+
second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
|
|
2218
|
+
radius=self.main_radius,
|
|
2219
|
+
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
2220
|
+
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
2221
|
+
chart_type=self.chart_type,
|
|
2222
|
+
third_circle_radius=self.third_circle_radius,
|
|
2223
|
+
external_view=self.external_view,
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2226
|
+
# Planet grid
|
|
2227
|
+
first_name_label = self._truncate_name(self.first_obj.name)
|
|
2228
|
+
if self.second_obj is not None and hasattr(self.second_obj, 'return_type') and self.second_obj.return_type == "Solar":
|
|
2229
|
+
first_return_grid_title = f"{first_name_label} ({self._translate('inner_wheel', 'Inner Wheel')})"
|
|
2230
|
+
second_return_grid_title = f"{self._translate('solar_return', 'Solar Return')} ({self._translate('outer_wheel', 'Outer Wheel')})"
|
|
2231
|
+
else:
|
|
2232
|
+
first_return_grid_title = f"{first_name_label} ({self._translate('inner_wheel', 'Inner Wheel')})"
|
|
2233
|
+
second_return_grid_title = f'{self._translate("lunar_return", "Lunar Return")} ({self._translate("outer_wheel", "Outer Wheel")})'
|
|
2234
|
+
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
2235
|
+
planets_and_houses_grid_title="",
|
|
2236
|
+
subject_name=first_return_grid_title,
|
|
2237
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
2238
|
+
chart_type=self.chart_type,
|
|
2239
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
2240
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
2241
|
+
)
|
|
2242
|
+
template_dict["makeSecondaryPlanetGrid"] = draw_secondary_planet_grid(
|
|
2243
|
+
planets_and_houses_grid_title="",
|
|
2244
|
+
second_subject_name=second_return_grid_title,
|
|
2245
|
+
second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
|
|
2246
|
+
chart_type=self.chart_type,
|
|
2247
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
2248
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
2249
|
+
)
|
|
2250
|
+
|
|
2251
|
+
house_comparison_factory = HouseComparisonFactory(
|
|
2252
|
+
first_subject=self.first_obj, # type: ignore[arg-type]
|
|
2253
|
+
second_subject=self.second_obj, # type: ignore[arg-type]
|
|
2254
|
+
active_points=self.active_points,
|
|
2255
|
+
)
|
|
2256
|
+
house_comparison = house_comparison_factory.get_house_comparison()
|
|
2257
|
+
|
|
2258
|
+
template_dict["makeHouseComparisonGrid"] = draw_house_comparison_grid(
|
|
2259
|
+
house_comparison,
|
|
2260
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
2261
|
+
active_points=self.active_points,
|
|
2262
|
+
points_owner_subject_number=2, # The second subject is the Solar Return
|
|
2263
|
+
house_position_comparison_label=self._translate("house_position_comparison", "House Position Comparison"),
|
|
2264
|
+
return_point_label=self._translate("return_point", "Return Point"),
|
|
2265
|
+
return_label=self._translate("Return", "DualReturnChart"),
|
|
2266
|
+
radix_label=self._translate("Natal", "Natal"),
|
|
2267
|
+
)
|
|
2268
|
+
|
|
2269
|
+
elif self.chart_type == "SingleReturnChart":
|
|
2270
|
+
# Set viewbox dynamically
|
|
2271
|
+
template_dict["viewbox"] = self._dynamic_viewbox()
|
|
2272
|
+
|
|
2273
|
+
# Rings and circles
|
|
2274
|
+
template_dict["transitRing"] = ""
|
|
2275
|
+
template_dict["degreeRing"] = draw_degree_ring(
|
|
2276
|
+
self.main_radius,
|
|
2277
|
+
self.first_circle_radius,
|
|
2278
|
+
self.first_obj.seventh_house.abs_pos,
|
|
2279
|
+
self.chart_colors_settings["paper_0"],
|
|
2280
|
+
)
|
|
2281
|
+
template_dict["background_circle"] = draw_background_circle(
|
|
2282
|
+
self.main_radius,
|
|
2283
|
+
self.chart_colors_settings["paper_1"],
|
|
2284
|
+
self.chart_colors_settings["paper_1"],
|
|
2285
|
+
)
|
|
2286
|
+
template_dict["first_circle"] = draw_first_circle(
|
|
2287
|
+
self.main_radius,
|
|
2288
|
+
self.chart_colors_settings["zodiac_radix_ring_2"],
|
|
2289
|
+
self.chart_type,
|
|
2290
|
+
self.first_circle_radius,
|
|
2291
|
+
)
|
|
2292
|
+
template_dict["second_circle"] = draw_second_circle(
|
|
2293
|
+
self.main_radius,
|
|
2294
|
+
self.chart_colors_settings["zodiac_radix_ring_1"],
|
|
2295
|
+
self.chart_colors_settings["paper_1"],
|
|
2296
|
+
self.chart_type,
|
|
2297
|
+
self.second_circle_radius,
|
|
2298
|
+
)
|
|
2299
|
+
template_dict["third_circle"] = draw_third_circle(
|
|
2300
|
+
self.main_radius,
|
|
2301
|
+
self.chart_colors_settings["zodiac_radix_ring_0"],
|
|
2302
|
+
self.chart_colors_settings["paper_1"],
|
|
2303
|
+
self.chart_type,
|
|
2304
|
+
self.third_circle_radius,
|
|
2305
|
+
)
|
|
2306
|
+
|
|
2307
|
+
# Aspects
|
|
2308
|
+
template_dict["makeDoubleChartAspectList"] = ""
|
|
2309
|
+
template_dict["makeAspectGrid"] = draw_aspect_grid(
|
|
2310
|
+
self.chart_colors_settings["paper_0"],
|
|
2311
|
+
self.available_planets_setting,
|
|
2312
|
+
self.aspects_list,
|
|
2313
|
+
)
|
|
2314
|
+
template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
|
|
2315
|
+
|
|
2316
|
+
# Top left section
|
|
2317
|
+
latitude_string = convert_latitude_coordinate_to_string(self.geolat, self._translate("north", "North"), self._translate("south", "South"))
|
|
2318
|
+
longitude_string = convert_longitude_coordinate_to_string(self.geolon, self._translate("east", "East"), self._translate("west", "West"))
|
|
2319
|
+
|
|
2320
|
+
template_dict["top_left_0"] = f'{self._translate("info", "Info")}:'
|
|
2321
|
+
template_dict["top_left_1"] = format_datetime_with_timezone(self.first_obj.iso_formatted_local_datetime) # type: ignore
|
|
2322
|
+
template_dict["top_left_2"] = f"{self.first_obj.city}, {self.first_obj.nation}"
|
|
2323
|
+
template_dict["top_left_3"] = f"{self._translate('latitude', 'Latitude')}: {latitude_string}"
|
|
2324
|
+
template_dict["top_left_4"] = f"{self._translate('longitude', 'Longitude')}: {longitude_string}"
|
|
2325
|
+
|
|
2326
|
+
if hasattr(self.first_obj, 'return_type') and self.first_obj.return_type == "Solar":
|
|
2327
|
+
template_dict["top_left_5"] = f"{self._translate('type', 'Type')}: {self._translate('solar_return', 'Solar Return')}"
|
|
2328
|
+
else:
|
|
2329
|
+
template_dict["top_left_5"] = f"{self._translate('type', 'Type')}: {self._translate('lunar_return', 'Lunar Return')}"
|
|
2330
|
+
|
|
2331
|
+
# Bottom left section
|
|
2332
|
+
if self.first_obj.zodiac_type == "Tropical":
|
|
2333
|
+
zodiac_info = f"{self._translate('zodiac', 'Zodiac')}: {self._translate('tropical', 'Tropical')}"
|
|
2334
|
+
else:
|
|
2335
|
+
mode_const = "SIDM_" + self.first_obj.sidereal_mode # type: ignore
|
|
2336
|
+
mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
|
|
2337
|
+
zodiac_info = f"{self._translate('ayanamsa', 'Ayanamsa')}: {mode_name}"
|
|
2338
|
+
|
|
2339
|
+
template_dict["bottom_left_0"] = zodiac_info
|
|
2340
|
+
template_dict["bottom_left_1"] = f"{self._translate('houses_system_' + self.first_obj.houses_system_identifier, self.first_obj.houses_system_name)} {self._translate('houses', 'Houses')}"
|
|
2341
|
+
|
|
2342
|
+
# Lunar phase information (optional)
|
|
2343
|
+
if self.first_obj.lunar_phase is not None:
|
|
2344
|
+
template_dict["bottom_left_2"] = f'{self._translate("lunation_day", "Lunation Day")}: {self.first_obj.lunar_phase.get("moon_phase", "")}'
|
|
2345
|
+
template_dict["bottom_left_3"] = f'{self._translate("lunar_phase", "Lunar Phase")}: {self._translate(self.first_obj.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.first_obj.lunar_phase.moon_phase_name)}'
|
|
2346
|
+
else:
|
|
2347
|
+
template_dict["bottom_left_2"] = ""
|
|
2348
|
+
template_dict["bottom_left_3"] = ""
|
|
2349
|
+
|
|
2350
|
+
template_dict["bottom_left_4"] = f'{self._translate("perspective_type", "Perspective")}: {self._translate(self.first_obj.perspective_type.lower().replace(" ", "_"), self.first_obj.perspective_type)}'
|
|
2351
|
+
|
|
2352
|
+
# Moon phase section calculations
|
|
2353
|
+
if self.first_obj.lunar_phase is not None:
|
|
2354
|
+
template_dict["makeLunarPhase"] = makeLunarPhase(self.first_obj.lunar_phase["degrees_between_s_m"], self.geolat)
|
|
2355
|
+
else:
|
|
2356
|
+
template_dict["makeLunarPhase"] = ""
|
|
2357
|
+
|
|
2358
|
+
# Houses and planet drawing
|
|
2359
|
+
template_dict["makeMainHousesGrid"] = draw_main_house_grid(
|
|
2360
|
+
main_subject_houses_list=first_subject_houses_list,
|
|
2361
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
2362
|
+
house_cusp_generale_name_label=self._translate("cusp", "Cusp"),
|
|
2363
|
+
)
|
|
2364
|
+
template_dict["makeSecondaryHousesGrid"] = ""
|
|
2365
|
+
|
|
2366
|
+
template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
|
|
2367
|
+
r=self.main_radius,
|
|
2368
|
+
first_subject_houses_list=first_subject_houses_list,
|
|
2369
|
+
standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
|
|
2370
|
+
first_house_color=self.planets_settings[12]["color"],
|
|
2371
|
+
tenth_house_color=self.planets_settings[13]["color"],
|
|
2372
|
+
seventh_house_color=self.planets_settings[14]["color"],
|
|
2373
|
+
fourth_house_color=self.planets_settings[15]["color"],
|
|
2374
|
+
c1=self.first_circle_radius,
|
|
2375
|
+
c3=self.third_circle_radius,
|
|
2376
|
+
chart_type=self.chart_type,
|
|
2377
|
+
external_view=self.external_view,
|
|
2378
|
+
)
|
|
2379
|
+
|
|
2380
|
+
template_dict["makePlanets"] = draw_planets(
|
|
2381
|
+
available_planets_setting=self.available_planets_setting,
|
|
2382
|
+
chart_type=self.chart_type,
|
|
2383
|
+
radius=self.main_radius,
|
|
2384
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
2385
|
+
third_circle_radius=self.third_circle_radius,
|
|
2386
|
+
main_subject_first_house_degree_ut=self.first_obj.first_house.abs_pos,
|
|
2387
|
+
main_subject_seventh_house_degree_ut=self.first_obj.seventh_house.abs_pos,
|
|
2388
|
+
external_view=self.external_view,
|
|
2389
|
+
)
|
|
2390
|
+
|
|
2391
|
+
template_dict["makeMainPlanetGrid"] = draw_main_planet_grid(
|
|
2392
|
+
planets_and_houses_grid_title=self._translate("planets_and_house", "Points for"),
|
|
2393
|
+
subject_name=self.first_obj.name,
|
|
2394
|
+
available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
|
|
2395
|
+
chart_type=self.chart_type,
|
|
2396
|
+
text_color=self.chart_colors_settings["paper_0"],
|
|
2397
|
+
celestial_point_language=self._language_model.celestial_points,
|
|
2398
|
+
)
|
|
2399
|
+
template_dict["makeSecondaryPlanetGrid"] = ""
|
|
2400
|
+
template_dict["makeHouseComparisonGrid"] = ""
|
|
2401
|
+
|
|
2402
|
+
return ChartTemplateModel(**template_dict)
|
|
2403
|
+
|
|
2404
|
+
def generate_svg_string(self, minify: bool = False, remove_css_variables=False) -> str:
|
|
2405
|
+
"""
|
|
2406
|
+
Render the full chart SVG as a string.
|
|
2407
|
+
|
|
2408
|
+
Reads the XML template, substitutes variables, and optionally inlines CSS
|
|
2409
|
+
variables and minifies the output.
|
|
2410
|
+
|
|
2411
|
+
Args:
|
|
2412
|
+
minify (bool): Remove whitespace and quotes for compactness.
|
|
2413
|
+
remove_css_variables (bool): Embed CSS variable definitions.
|
|
2414
|
+
|
|
2415
|
+
Returns:
|
|
2416
|
+
str: SVG markup as a string.
|
|
2417
|
+
"""
|
|
2418
|
+
td = self._create_template_dictionary()
|
|
2419
|
+
|
|
2420
|
+
DATA_DIR = Path(__file__).parent
|
|
2421
|
+
xml_svg = DATA_DIR / "templates" / "chart.xml"
|
|
2422
|
+
|
|
2423
|
+
# read template
|
|
2424
|
+
with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f:
|
|
2425
|
+
template = Template(f.read()).substitute(td.model_dump())
|
|
2426
|
+
|
|
2427
|
+
# return filename
|
|
2428
|
+
|
|
2429
|
+
logger.debug("Template dictionary includes %s fields", len(td.model_dump()))
|
|
2430
|
+
|
|
2431
|
+
self._create_template_dictionary()
|
|
2432
|
+
|
|
2433
|
+
if remove_css_variables:
|
|
2434
|
+
template = inline_css_variables_in_svg(template)
|
|
2435
|
+
|
|
2436
|
+
if minify:
|
|
2437
|
+
template = scourString(template).replace('"', "'").replace("\n", "").replace("\t", "").replace(" ", "").replace(" ", "")
|
|
2438
|
+
|
|
2439
|
+
else:
|
|
2440
|
+
template = template.replace('"', "'")
|
|
2441
|
+
|
|
2442
|
+
return template
|
|
2443
|
+
|
|
2444
|
+
def save_svg(self, output_path: Union[str, Path, None] = None, filename: Union[str, None] = None, minify: bool = False, remove_css_variables=False):
|
|
2445
|
+
"""
|
|
2446
|
+
Generate and save the full chart SVG to disk.
|
|
2447
|
+
|
|
2448
|
+
Calls generate_svg_string to render the SVG, then writes a file named
|
|
2449
|
+
"{subject.name} - {chart_type} Chart.svg" in the specified output directory.
|
|
2450
|
+
|
|
2451
|
+
Args:
|
|
2452
|
+
output_path (str, Path, or None): Directory path where the SVG file will be saved.
|
|
2453
|
+
If None, defaults to the user's home directory.
|
|
2454
|
+
filename (str or None): Custom filename for the SVG file (without extension).
|
|
2455
|
+
If None, uses the default pattern: "{subject.name} - {chart_type} Chart".
|
|
2456
|
+
minify (bool): Pass-through to generate_svg_string for compact output.
|
|
2457
|
+
remove_css_variables (bool): Pass-through to generate_svg_string to embed CSS variables.
|
|
2458
|
+
|
|
2459
|
+
Returns:
|
|
2460
|
+
None
|
|
2461
|
+
"""
|
|
2462
|
+
|
|
2463
|
+
self.template = self.generate_svg_string(minify, remove_css_variables)
|
|
2464
|
+
|
|
2465
|
+
# Convert output_path to Path object, default to home directory
|
|
2466
|
+
output_directory = Path(output_path) if output_path is not None else Path.home()
|
|
2467
|
+
|
|
2468
|
+
# Determine filename
|
|
2469
|
+
if filename is not None:
|
|
2470
|
+
chartname = output_directory / f"{filename}.svg"
|
|
2471
|
+
else:
|
|
2472
|
+
# Use default filename pattern
|
|
2473
|
+
chart_type_for_filename = self.chart_type
|
|
2474
|
+
|
|
2475
|
+
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":
|
|
2476
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Lunar Return.svg"
|
|
2477
|
+
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":
|
|
2478
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Solar Return.svg"
|
|
2479
|
+
else:
|
|
2480
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart.svg"
|
|
2481
|
+
|
|
2482
|
+
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
2483
|
+
output_file.write(self.template)
|
|
2484
|
+
|
|
2485
|
+
print(f"SVG Generated Correctly in: {chartname}")
|
|
2486
|
+
|
|
2487
|
+
def generate_wheel_only_svg_string(self, minify: bool = False, remove_css_variables=False):
|
|
2488
|
+
"""
|
|
2489
|
+
Render the wheel-only chart SVG as a string.
|
|
2490
|
+
|
|
2491
|
+
Reads the wheel-only XML template, substitutes chart data, and applies optional
|
|
2492
|
+
CSS inlining and minification.
|
|
2493
|
+
|
|
2494
|
+
Args:
|
|
2495
|
+
minify (bool): Remove whitespace and quotes for compactness.
|
|
2496
|
+
remove_css_variables (bool): Embed CSS variable definitions.
|
|
2497
|
+
|
|
2498
|
+
Returns:
|
|
2499
|
+
str: SVG markup for the chart wheel only.
|
|
2500
|
+
"""
|
|
2501
|
+
|
|
2502
|
+
with open(
|
|
2503
|
+
Path(__file__).parent / "templates" / "wheel_only.xml",
|
|
2504
|
+
"r",
|
|
2505
|
+
encoding="utf-8",
|
|
2506
|
+
errors="ignore",
|
|
2507
|
+
) as f:
|
|
2508
|
+
template = f.read()
|
|
2509
|
+
|
|
2510
|
+
template_dict = self._create_template_dictionary()
|
|
2511
|
+
# Use a compact viewBox specific for the wheel-only rendering
|
|
2512
|
+
wheel_viewbox = self._wheel_only_viewbox()
|
|
2513
|
+
template = Template(template).substitute({**template_dict.model_dump(), "viewbox": wheel_viewbox})
|
|
2514
|
+
|
|
2515
|
+
if remove_css_variables:
|
|
2516
|
+
template = inline_css_variables_in_svg(template)
|
|
2517
|
+
|
|
2518
|
+
if minify:
|
|
2519
|
+
template = scourString(template).replace('"', "'").replace("\n", "").replace("\t", "").replace(" ", "").replace(" ", "")
|
|
2520
|
+
|
|
2521
|
+
else:
|
|
2522
|
+
template = template.replace('"', "'")
|
|
2523
|
+
|
|
2524
|
+
return template
|
|
2525
|
+
|
|
2526
|
+
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):
|
|
2527
|
+
"""
|
|
2528
|
+
Generate and save wheel-only chart SVG to disk.
|
|
2529
|
+
|
|
2530
|
+
Calls generate_wheel_only_svg_string and writes a file named
|
|
2531
|
+
"{subject.name} - {chart_type} Chart - Wheel Only.svg" in the specified output directory.
|
|
2532
|
+
|
|
2533
|
+
Args:
|
|
2534
|
+
output_path (str, Path, or None): Directory path where the SVG file will be saved.
|
|
2535
|
+
If None, defaults to the user's home directory.
|
|
2536
|
+
filename (str or None): Custom filename for the SVG file (without extension).
|
|
2537
|
+
If None, uses the default pattern: "{subject.name} - {chart_type} Chart - Wheel Only".
|
|
2538
|
+
minify (bool): Pass-through to generate_wheel_only_svg_string for compact output.
|
|
2539
|
+
remove_css_variables (bool): Pass-through to generate_wheel_only_svg_string to embed CSS variables.
|
|
2540
|
+
|
|
2541
|
+
Returns:
|
|
2542
|
+
None
|
|
2543
|
+
"""
|
|
2544
|
+
|
|
2545
|
+
template = self.generate_wheel_only_svg_string(minify, remove_css_variables)
|
|
2546
|
+
|
|
2547
|
+
# Convert output_path to Path object, default to home directory
|
|
2548
|
+
output_directory = Path(output_path) if output_path is not None else Path.home()
|
|
2549
|
+
|
|
2550
|
+
# Determine filename
|
|
2551
|
+
if filename is not None:
|
|
2552
|
+
chartname = output_directory / f"{filename}.svg"
|
|
2553
|
+
else:
|
|
2554
|
+
# Use default filename pattern
|
|
2555
|
+
chart_type_for_filename = "ExternalNatal" if self.external_view and self.chart_type == "Natal" else self.chart_type
|
|
2556
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Wheel Only.svg"
|
|
2557
|
+
|
|
2558
|
+
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
2559
|
+
output_file.write(template)
|
|
2560
|
+
|
|
2561
|
+
print(f"SVG Generated Correctly in: {chartname}")
|
|
2562
|
+
|
|
2563
|
+
def generate_aspect_grid_only_svg_string(self, minify: bool = False, remove_css_variables=False):
|
|
2564
|
+
"""
|
|
2565
|
+
Render the aspect-grid-only chart SVG as a string.
|
|
2566
|
+
|
|
2567
|
+
Reads the aspect-grid XML template, generates the aspect grid based on chart type,
|
|
2568
|
+
and applies optional CSS inlining and minification.
|
|
2569
|
+
|
|
2570
|
+
Args:
|
|
2571
|
+
minify (bool): Remove whitespace and quotes for compactness.
|
|
2572
|
+
remove_css_variables (bool): Embed CSS variable definitions.
|
|
2573
|
+
|
|
2574
|
+
Returns:
|
|
2575
|
+
str: SVG markup for the aspect grid only.
|
|
2576
|
+
"""
|
|
2577
|
+
|
|
2578
|
+
with open(
|
|
2579
|
+
Path(__file__).parent / "templates" / "aspect_grid_only.xml",
|
|
2580
|
+
"r",
|
|
2581
|
+
encoding="utf-8",
|
|
2582
|
+
errors="ignore",
|
|
2583
|
+
) as f:
|
|
2584
|
+
template = f.read()
|
|
2585
|
+
|
|
2586
|
+
template_dict = self._create_template_dictionary()
|
|
2587
|
+
|
|
2588
|
+
if self.chart_type in ["Transit", "Synastry", "DualReturnChart"]:
|
|
2589
|
+
aspects_grid = draw_transit_aspect_grid(
|
|
2590
|
+
self.chart_colors_settings["paper_0"],
|
|
2591
|
+
self.available_planets_setting,
|
|
2592
|
+
self.aspects_list,
|
|
2593
|
+
)
|
|
2594
|
+
else:
|
|
2595
|
+
aspects_grid = draw_aspect_grid(
|
|
2596
|
+
self.chart_colors_settings["paper_0"],
|
|
2597
|
+
self.available_planets_setting,
|
|
2598
|
+
self.aspects_list,
|
|
2599
|
+
x_start=50,
|
|
2600
|
+
y_start=250,
|
|
2601
|
+
)
|
|
2602
|
+
|
|
2603
|
+
# Use a compact, known-good viewBox that frames the grid
|
|
2604
|
+
viewbox_override = self._grid_only_viewbox()
|
|
2605
|
+
|
|
2606
|
+
template = Template(template).substitute({**template_dict.model_dump(), "makeAspectGrid": aspects_grid, "viewbox": viewbox_override})
|
|
2607
|
+
|
|
2608
|
+
if remove_css_variables:
|
|
2609
|
+
template = inline_css_variables_in_svg(template)
|
|
2610
|
+
|
|
2611
|
+
if minify:
|
|
2612
|
+
template = scourString(template).replace('"', "'").replace("\n", "").replace("\t", "").replace(" ", "").replace(" ", "")
|
|
2613
|
+
|
|
2614
|
+
else:
|
|
2615
|
+
template = template.replace('"', "'")
|
|
2616
|
+
|
|
2617
|
+
return template
|
|
2618
|
+
|
|
2619
|
+
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):
|
|
2620
|
+
"""
|
|
2621
|
+
Generate and save aspect-grid-only chart SVG to disk.
|
|
2622
|
+
|
|
2623
|
+
Calls generate_aspect_grid_only_svg_string and writes a file named
|
|
2624
|
+
"{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the specified output directory.
|
|
2625
|
+
|
|
2626
|
+
Args:
|
|
2627
|
+
output_path (str, Path, or None): Directory path where the SVG file will be saved.
|
|
2628
|
+
If None, defaults to the user's home directory.
|
|
2629
|
+
filename (str or None): Custom filename for the SVG file (without extension).
|
|
2630
|
+
If None, uses the default pattern: "{subject.name} - {chart_type} Chart - Aspect Grid Only".
|
|
2631
|
+
minify (bool): Pass-through to generate_aspect_grid_only_svg_string for compact output.
|
|
2632
|
+
remove_css_variables (bool): Pass-through to generate_aspect_grid_only_svg_string to embed CSS variables.
|
|
2633
|
+
|
|
2634
|
+
Returns:
|
|
2635
|
+
None
|
|
2636
|
+
"""
|
|
2637
|
+
|
|
2638
|
+
template = self.generate_aspect_grid_only_svg_string(minify, remove_css_variables)
|
|
2639
|
+
|
|
2640
|
+
# Convert output_path to Path object, default to home directory
|
|
2641
|
+
output_directory = Path(output_path) if output_path is not None else Path.home()
|
|
2642
|
+
|
|
2643
|
+
# Determine filename
|
|
2644
|
+
if filename is not None:
|
|
2645
|
+
chartname = output_directory / f"{filename}.svg"
|
|
2646
|
+
else:
|
|
2647
|
+
# Use default filename pattern
|
|
2648
|
+
chart_type_for_filename = "ExternalNatal" if self.external_view and self.chart_type == "Natal" else self.chart_type
|
|
2649
|
+
chartname = output_directory / f"{self.first_obj.name} - {chart_type_for_filename} Chart - Aspect Grid Only.svg"
|
|
2650
|
+
|
|
2651
|
+
with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
|
|
2652
|
+
output_file.write(template)
|
|
2653
|
+
|
|
2654
|
+
print(f"SVG Generated Correctly in: {chartname}")
|
|
2655
|
+
|
|
2656
|
+
if __name__ == "__main__":
|
|
2657
|
+
from kerykeion.utilities import setup_logging
|
|
2658
|
+
from kerykeion.planetary_return_factory import PlanetaryReturnFactory
|
|
2659
|
+
from kerykeion.astrological_subject_factory import AstrologicalSubjectFactory
|
|
2660
|
+
from kerykeion.chart_data_factory import ChartDataFactory
|
|
2661
|
+
from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
|
|
2662
|
+
|
|
2663
|
+
ACTIVE_PLANETS: list[AstrologicalPoint] = DEFAULT_ACTIVE_POINTS
|
|
2664
|
+
# ACTIVE_PLANETS: list[AstrologicalPoint] = ALL_ACTIVE_POINTS
|
|
2665
|
+
setup_logging(level="info")
|
|
2666
|
+
|
|
2667
|
+
subject = AstrologicalSubjectFactory.from_birth_data("John Lennon", 1940, 10, 9, 18, 30, "Liverpool", "GB", active_points=ACTIVE_PLANETS)
|
|
2668
|
+
|
|
2669
|
+
return_factory = PlanetaryReturnFactory(
|
|
2670
|
+
subject,
|
|
2671
|
+
city="Los Angeles",
|
|
2672
|
+
nation="US",
|
|
2673
|
+
lng=-118.2437,
|
|
2674
|
+
lat=34.0522,
|
|
2675
|
+
tz_str="America/Los_Angeles",
|
|
2676
|
+
altitude=0
|
|
2677
|
+
)
|
|
2678
|
+
|
|
2679
|
+
###
|
|
2680
|
+
## Birth Chart - NEW APPROACH with ChartDataFactory
|
|
2681
|
+
birth_chart_data = ChartDataFactory.create_natal_chart_data(
|
|
2682
|
+
subject,
|
|
2683
|
+
active_points=ACTIVE_PLANETS,
|
|
2684
|
+
)
|
|
2685
|
+
birth_chart = ChartDrawer(
|
|
2686
|
+
chart_data=birth_chart_data,
|
|
2687
|
+
chart_language="IT",
|
|
2688
|
+
theme="strawberry",
|
|
2689
|
+
)
|
|
2690
|
+
birth_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2691
|
+
|
|
2692
|
+
###
|
|
2693
|
+
## Solar Return Chart - NEW APPROACH with ChartDataFactory
|
|
2694
|
+
solar_return = return_factory.next_return_from_iso_formatted_time(
|
|
2695
|
+
"2025-01-09T18:30:00+01:00", # UTC+1
|
|
2696
|
+
return_type="Solar",
|
|
2697
|
+
)
|
|
2698
|
+
solar_return_chart_data = ChartDataFactory.create_return_chart_data(
|
|
2699
|
+
subject,
|
|
2700
|
+
solar_return,
|
|
2701
|
+
active_points=ACTIVE_PLANETS,
|
|
2702
|
+
)
|
|
2703
|
+
solar_return_chart = ChartDrawer(
|
|
2704
|
+
chart_data=solar_return_chart_data,
|
|
2705
|
+
chart_language="IT",
|
|
2706
|
+
theme="classic",
|
|
2707
|
+
)
|
|
2708
|
+
|
|
2709
|
+
solar_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2710
|
+
|
|
2711
|
+
###
|
|
2712
|
+
## Single wheel return - NEW APPROACH with ChartDataFactory
|
|
2713
|
+
single_wheel_return_chart_data = ChartDataFactory.create_single_wheel_return_chart_data(
|
|
2714
|
+
solar_return,
|
|
2715
|
+
active_points=ACTIVE_PLANETS,
|
|
2716
|
+
)
|
|
2717
|
+
single_wheel_return_chart = ChartDrawer(
|
|
2718
|
+
chart_data=single_wheel_return_chart_data,
|
|
2719
|
+
chart_language="IT",
|
|
2720
|
+
theme="dark",
|
|
2721
|
+
)
|
|
2722
|
+
|
|
2723
|
+
single_wheel_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2724
|
+
|
|
2725
|
+
###
|
|
2726
|
+
## Lunar return - NEW APPROACH with ChartDataFactory
|
|
2727
|
+
lunar_return = return_factory.next_return_from_iso_formatted_time(
|
|
2728
|
+
"2025-01-09T18:30:00+01:00", # UTC+1
|
|
2729
|
+
return_type="Lunar",
|
|
2730
|
+
)
|
|
2731
|
+
lunar_return_chart_data = ChartDataFactory.create_return_chart_data(
|
|
2732
|
+
subject,
|
|
2733
|
+
lunar_return,
|
|
2734
|
+
active_points=ACTIVE_PLANETS,
|
|
2735
|
+
)
|
|
2736
|
+
lunar_return_chart = ChartDrawer(
|
|
2737
|
+
chart_data=lunar_return_chart_data,
|
|
2738
|
+
chart_language="IT",
|
|
2739
|
+
theme="dark",
|
|
2740
|
+
)
|
|
2741
|
+
lunar_return_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2742
|
+
|
|
2743
|
+
###
|
|
2744
|
+
## Transit Chart - NEW APPROACH with ChartDataFactory
|
|
2745
|
+
transit = AstrologicalSubjectFactory.from_iso_utc_time(
|
|
2746
|
+
"Transit",
|
|
2747
|
+
"2021-10-04T18:30:00+01:00",
|
|
2748
|
+
)
|
|
2749
|
+
transit_chart_data = ChartDataFactory.create_transit_chart_data(
|
|
2750
|
+
subject,
|
|
2751
|
+
transit,
|
|
2752
|
+
active_points=ACTIVE_PLANETS,
|
|
2753
|
+
)
|
|
2754
|
+
transit_chart = ChartDrawer(
|
|
2755
|
+
chart_data=transit_chart_data,
|
|
2756
|
+
chart_language="IT",
|
|
2757
|
+
theme="dark",
|
|
2758
|
+
)
|
|
2759
|
+
transit_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2760
|
+
|
|
2761
|
+
###
|
|
2762
|
+
## Synastry Chart - NEW APPROACH with ChartDataFactory
|
|
2763
|
+
second_subject = AstrologicalSubjectFactory.from_birth_data("Yoko Ono", 1933, 2, 18, 18, 30, "Tokyo", "JP")
|
|
2764
|
+
synastry_chart_data = ChartDataFactory.create_synastry_chart_data(
|
|
2765
|
+
subject,
|
|
2766
|
+
second_subject,
|
|
2767
|
+
active_points=ACTIVE_PLANETS,
|
|
2768
|
+
)
|
|
2769
|
+
synastry_chart = ChartDrawer(
|
|
2770
|
+
chart_data=synastry_chart_data,
|
|
2771
|
+
chart_language="IT",
|
|
2772
|
+
theme="dark",
|
|
2773
|
+
)
|
|
2774
|
+
synastry_chart.save_svg() # minify=True, remove_css_variables=True)
|
|
2775
|
+
|
|
2776
|
+
##
|
|
2777
|
+
# Transit Chart with Grid - NEW APPROACH with ChartDataFactory
|
|
2778
|
+
subject.name = "Grid"
|
|
2779
|
+
transit_chart_with_grid_data = ChartDataFactory.create_transit_chart_data(
|
|
2780
|
+
subject,
|
|
2781
|
+
transit,
|
|
2782
|
+
active_points=ACTIVE_PLANETS,
|
|
2783
|
+
)
|
|
2784
|
+
transit_chart_with_grid = ChartDrawer(
|
|
2785
|
+
chart_data=transit_chart_with_grid_data,
|
|
2786
|
+
chart_language="IT",
|
|
2787
|
+
theme="dark",
|
|
2788
|
+
double_chart_aspect_grid_type="table"
|
|
2789
|
+
)
|
|
2790
|
+
transit_chart_with_grid.save_svg() # minify=True, remove_css_variables=True)
|
|
2791
|
+
transit_chart_with_grid.save_aspect_grid_only_svg_file()
|
|
2792
|
+
transit_chart_with_grid.save_wheel_only_svg_file()
|
|
2793
|
+
|
|
2794
|
+
print("✅ All chart examples completed using ChartDataFactory + ChartDrawer architecture!")
|