kerykeion 5.0.0a9__py3-none-any.whl → 5.1.8__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.

Files changed (79) hide show
  1. kerykeion/__init__.py +50 -9
  2. kerykeion/aspects/__init__.py +5 -2
  3. kerykeion/aspects/aspects_factory.py +568 -0
  4. kerykeion/aspects/aspects_utils.py +78 -11
  5. kerykeion/astrological_subject_factory.py +1032 -275
  6. kerykeion/backword.py +820 -0
  7. kerykeion/chart_data_factory.py +552 -0
  8. kerykeion/charts/chart_drawer.py +2661 -0
  9. kerykeion/charts/charts_utils.py +652 -399
  10. kerykeion/charts/draw_planets.py +603 -353
  11. kerykeion/charts/templates/aspect_grid_only.xml +326 -198
  12. kerykeion/charts/templates/chart.xml +306 -256
  13. kerykeion/charts/templates/wheel_only.xml +330 -200
  14. kerykeion/charts/themes/black-and-white.css +148 -0
  15. kerykeion/charts/themes/classic.css +11 -0
  16. kerykeion/charts/themes/dark-high-contrast.css +11 -0
  17. kerykeion/charts/themes/dark.css +11 -0
  18. kerykeion/charts/themes/light.css +11 -0
  19. kerykeion/charts/themes/strawberry.css +10 -0
  20. kerykeion/composite_subject_factory.py +232 -13
  21. kerykeion/ephemeris_data_factory.py +443 -0
  22. kerykeion/fetch_geonames.py +78 -21
  23. kerykeion/house_comparison/__init__.py +4 -1
  24. kerykeion/house_comparison/house_comparison_factory.py +52 -19
  25. kerykeion/house_comparison/house_comparison_utils.py +37 -9
  26. kerykeion/kr_types/__init__.py +66 -6
  27. kerykeion/kr_types/chart_template_model.py +20 -0
  28. kerykeion/kr_types/kerykeion_exception.py +15 -9
  29. kerykeion/kr_types/kr_literals.py +14 -160
  30. kerykeion/kr_types/kr_models.py +14 -291
  31. kerykeion/kr_types/settings_models.py +15 -167
  32. kerykeion/planetary_return_factory.py +545 -40
  33. kerykeion/relationship_score_factory.py +137 -63
  34. kerykeion/report.py +749 -64
  35. kerykeion/schemas/__init__.py +106 -0
  36. kerykeion/schemas/chart_template_model.py +367 -0
  37. kerykeion/schemas/kerykeion_exception.py +20 -0
  38. kerykeion/schemas/kr_literals.py +181 -0
  39. kerykeion/schemas/kr_models.py +603 -0
  40. kerykeion/schemas/settings_models.py +188 -0
  41. kerykeion/settings/__init__.py +20 -1
  42. kerykeion/settings/chart_defaults.py +444 -0
  43. kerykeion/settings/config_constants.py +88 -12
  44. kerykeion/settings/kerykeion_settings.py +32 -75
  45. kerykeion/settings/translation_strings.py +1499 -0
  46. kerykeion/settings/translations.py +74 -0
  47. kerykeion/sweph/ast136/s136108s.se1 +0 -0
  48. kerykeion/sweph/ast136/s136199s.se1 +0 -0
  49. kerykeion/sweph/ast136/s136472s.se1 +0 -0
  50. kerykeion/sweph/ast28/se28978s.se1 +0 -0
  51. kerykeion/sweph/ast50/se50000s.se1 +0 -0
  52. kerykeion/sweph/ast90/se90377s.se1 +0 -0
  53. kerykeion/sweph/ast90/se90482s.se1 +0 -0
  54. kerykeion/sweph/sefstars.txt +1602 -0
  55. kerykeion/transits_time_range_factory.py +302 -0
  56. kerykeion/utilities.py +289 -204
  57. kerykeion-5.1.8.dist-info/METADATA +1793 -0
  58. kerykeion-5.1.8.dist-info/RECORD +63 -0
  59. kerykeion/aspects/natal_aspects.py +0 -181
  60. kerykeion/aspects/synastry_aspects.py +0 -141
  61. kerykeion/aspects/transits_time_range.py +0 -41
  62. kerykeion/charts/draw_planets_v2.py +0 -649
  63. kerykeion/charts/draw_planets_v3.py +0 -679
  64. kerykeion/charts/kerykeion_chart_svg.py +0 -2038
  65. kerykeion/enums.py +0 -57
  66. kerykeion/ephemeris_data.py +0 -238
  67. kerykeion/house_comparison/house_comparison_models.py +0 -38
  68. kerykeion/kr_types/chart_types.py +0 -106
  69. kerykeion/settings/kr.config.json +0 -1304
  70. kerykeion/settings/legacy/__init__.py +0 -0
  71. kerykeion/settings/legacy/legacy_celestial_points_settings.py +0 -299
  72. kerykeion/settings/legacy/legacy_chart_aspects_settings.py +0 -71
  73. kerykeion/settings/legacy/legacy_color_settings.py +0 -42
  74. kerykeion/transits_time_range.py +0 -128
  75. kerykeion-5.0.0a9.dist-info/METADATA +0 -636
  76. kerykeion-5.0.0a9.dist-info/RECORD +0 -55
  77. kerykeion-5.0.0a9.dist-info/entry_points.txt +0 -2
  78. {kerykeion-5.0.0a9.dist-info → kerykeion-5.1.8.dist-info}/WHEEL +0 -0
  79. {kerykeion-5.0.0a9.dist-info → kerykeion-5.1.8.dist-info}/licenses/LICENSE +0 -0
@@ -1,679 +0,0 @@
1
- """
2
- TODO: Not stable at all, check it very well before using it!
3
- """
4
-
5
- from dataclasses import dataclass
6
- from enum import Enum
7
- from typing import Union, Optional, List, Dict, Tuple, get_args
8
- import logging
9
-
10
- from kerykeion.charts.charts_utils import degreeDiff, sliceToX, sliceToY, convert_decimal_to_degree_string
11
- from kerykeion.kr_types import KerykeionException, ChartType, KerykeionPointModel
12
- from kerykeion.kr_types.settings_models import KerykeionSettingsCelestialPointModel
13
- from kerykeion.kr_types.kr_literals import Houses
14
-
15
-
16
- class ChartRadius(Enum):
17
- """Standard radius values for different chart elements."""
18
- ANGLE_TRANSIT = 76
19
- ANGLE_NATAL = 40
20
- ANGLE_EXTERNAL_NATAL = 30
21
- PLANET_PRIMARY = 94
22
- PLANET_ALTERNATE = 74
23
- PLANET_TRANSIT_PRIMARY = 130
24
- PLANET_TRANSIT_ALTERNATE = 110
25
- PLANET_EXTERNAL_PRIMARY = 84
26
- PLANET_EXTERNAL_ALTERNATE = 64
27
-
28
-
29
- class ChartConstants:
30
- """Configuration constants for chart drawing."""
31
- PLANET_GROUPING_THRESHOLD = 3.4
32
- SECONDARY_GROUPING_THRESHOLD = 2.5
33
- ANGLE_INDEX_RANGE = (22, 27) # ASC, MC, DSC, IC indices
34
- SYMBOL_SCALE_FACTOR = 0.8
35
- SYMBOL_SIZE = 12
36
- MAX_DEGREE_DISTANCE = 360.0
37
-
38
- # Line styling
39
- LINE_OPACITY_PRIMARY = 0.3
40
- LINE_OPACITY_SECONDARY = 0.5
41
- LINE_WIDTH_THIN = 1
42
- LINE_WIDTH_THICK = 2
43
-
44
- # Text styling
45
- TEXT_SIZE = 10
46
-
47
-
48
- @dataclass
49
- class PointPosition:
50
- """Represents a celestial point's position and distance information."""
51
- index: int
52
- position_index: int
53
- distance_to_prev: float
54
- distance_to_next: float
55
- label: str
56
-
57
-
58
- @dataclass
59
- class ChartConfiguration:
60
- """Configuration for chart drawing parameters."""
61
- radius: float
62
- third_circle_radius: float
63
- first_house_degree: float
64
- seventh_house_degree: float
65
- chart_type: ChartType
66
- scale_factor: float = 1.0
67
-
68
- def __post_init__(self):
69
- """Set scale factor based on chart type."""
70
- if self.chart_type in ["Transit", "Synastry", "Return", "ExternalNatal"]:
71
- self.scale_factor = ChartConstants.SYMBOL_SCALE_FACTOR
72
-
73
-
74
- class CelestialPointGrouper:
75
- """Handles grouping of celestial points that are close together."""
76
-
77
- def __init__(self, threshold: float = ChartConstants.PLANET_GROUPING_THRESHOLD):
78
- self.threshold = threshold
79
-
80
- def create_position_mapping(
81
- self,
82
- celestial_points: List[KerykeionPointModel],
83
- settings: List[KerykeionSettingsCelestialPointModel]
84
- ) -> Tuple[Dict[float, int], List[float]]:
85
- """Create mapping from absolute positions to point indices."""
86
- position_index_map = {}
87
- for i, point in enumerate(celestial_points):
88
- position_index_map[point.abs_pos] = i
89
- logging.debug(f"Point {settings[i]['label']}: index {i}, degree {point.abs_pos}")
90
-
91
- return position_index_map, sorted(position_index_map.keys())
92
-
93
- def calculate_distances(
94
- self,
95
- sorted_positions: List[float],
96
- position_index_map: Dict[float, int],
97
- abs_positions: List[float]
98
- ) -> List[PointPosition]:
99
- """Calculate distances between adjacent points."""
100
- point_positions = []
101
-
102
- for position_idx, abs_position in enumerate(sorted_positions):
103
- point_idx = position_index_map[abs_position]
104
-
105
- if len(sorted_positions) == 1:
106
- # Single point case
107
- distance_to_prev = distance_to_next = ChartConstants.MAX_DEGREE_DISTANCE
108
- else:
109
- prev_idx, next_idx = self._get_adjacent_indices(
110
- position_idx, sorted_positions, position_index_map
111
- )
112
- distance_to_prev = degreeDiff(abs_positions[prev_idx], abs_positions[point_idx])
113
- distance_to_next = degreeDiff(abs_positions[next_idx], abs_positions[point_idx])
114
-
115
- point_positions.append(PointPosition(
116
- index=point_idx,
117
- position_index=position_idx,
118
- distance_to_prev=distance_to_prev,
119
- distance_to_next=distance_to_next,
120
- label=f"point_{point_idx}" # Will be updated by caller
121
- ))
122
-
123
- return point_positions
124
-
125
- def _get_adjacent_indices(
126
- self,
127
- position_idx: int,
128
- sorted_positions: List[float],
129
- position_index_map: Dict[float, int]
130
- ) -> Tuple[int, int]:
131
- """Get indices of previous and next points."""
132
- total_positions = len(sorted_positions)
133
-
134
- if position_idx == 0:
135
- prev_position = sorted_positions[-1]
136
- next_position = sorted_positions[1]
137
- elif position_idx == total_positions - 1:
138
- prev_position = sorted_positions[position_idx - 1]
139
- next_position = sorted_positions[0]
140
- else:
141
- prev_position = sorted_positions[position_idx - 1]
142
- next_position = sorted_positions[position_idx + 1]
143
-
144
- return position_index_map[prev_position], position_index_map[next_position]
145
-
146
- def identify_groups(self, point_positions: List[PointPosition]) -> List[List[PointPosition]]:
147
- """Identify groups of points that are close together."""
148
- groups = []
149
- current_group = []
150
-
151
- for point_pos in point_positions:
152
- if point_pos.distance_to_next < self.threshold:
153
- current_group.append(point_pos)
154
- else:
155
- if current_group:
156
- current_group.append(point_pos)
157
- groups.append(current_group)
158
- current_group = []
159
-
160
- # Handle case where last group wraps around
161
- if current_group and groups and point_positions[0] in groups[0]:
162
- groups[0] = current_group + groups[0]
163
- elif current_group:
164
- groups.append(current_group)
165
-
166
- return groups
167
-
168
-
169
- class PositionAdjuster:
170
- """Calculates position adjustments to prevent overlapping points."""
171
-
172
- def __init__(self, threshold: float = ChartConstants.PLANET_GROUPING_THRESHOLD):
173
- self.threshold = threshold
174
-
175
- def calculate_adjustments(
176
- self,
177
- groups: List[List[PointPosition]],
178
- total_points: int
179
- ) -> List[float]:
180
- """Calculate position adjustments for all points."""
181
- adjustments = [0.0] * total_points
182
-
183
- for group in groups:
184
- if len(group) == 2:
185
- self._handle_two_point_group(group, adjustments)
186
- elif len(group) >= 3:
187
- self._handle_multi_point_group(group, adjustments)
188
-
189
- return adjustments
190
-
191
- def _handle_two_point_group(
192
- self,
193
- group: List[PointPosition],
194
- adjustments: List[float]
195
- ) -> None:
196
- """Handle positioning for a group of two points."""
197
- point_a, point_b = group[0], group[1]
198
-
199
- # Check available space around the group
200
- if (point_a.distance_to_prev > 2 * self.threshold and
201
- point_b.distance_to_next > 2 * self.threshold):
202
- # Both points have room
203
- offset = (self.threshold - point_a.distance_to_next) / 2
204
- adjustments[point_a.position_index] = -offset
205
- adjustments[point_b.position_index] = +offset
206
- elif point_a.distance_to_prev > 2 * self.threshold:
207
- # Only first point has room
208
- adjustments[point_a.position_index] = -self.threshold
209
- elif point_b.distance_to_next > 2 * self.threshold:
210
- # Only second point has room
211
- adjustments[point_b.position_index] = +self.threshold
212
-
213
- def _handle_multi_point_group(
214
- self,
215
- group: List[PointPosition],
216
- adjustments: List[float]
217
- ) -> None:
218
- """Handle positioning for groups of three or more points."""
219
- group_size = len(group)
220
-
221
- # Calculate available and needed space
222
- available_space = group[0].distance_to_prev
223
- for point in group:
224
- available_space += point.distance_to_next
225
-
226
- needed_space = 3 * self.threshold + 1.2 * (group_size - 1) * self.threshold
227
-
228
- if available_space > needed_space:
229
- # Distribute points evenly
230
- spacing = 1.2 * self.threshold
231
- start_offset = (available_space - needed_space) / 2
232
-
233
- for i, point in enumerate(group):
234
- adjustments[point.position_index] = start_offset + i * spacing - group[0].distance_to_prev
235
-
236
-
237
- class RadiusCalculator:
238
- """Calculates appropriate radius for different point types and chart types."""
239
-
240
- @staticmethod
241
- def get_point_radius(
242
- point_idx: int,
243
- chart_type: ChartType,
244
- is_alternate: bool = False
245
- ) -> int:
246
- """Get radius for a celestial point based on its type and chart context."""
247
- is_angle = ChartConstants.ANGLE_INDEX_RANGE[0] < point_idx < ChartConstants.ANGLE_INDEX_RANGE[1]
248
-
249
- if chart_type in ["Transit", "Synastry", "Return"]:
250
- if is_angle:
251
- return ChartRadius.ANGLE_TRANSIT.value
252
- return (ChartRadius.PLANET_TRANSIT_ALTERNATE.value if is_alternate
253
- else ChartRadius.PLANET_TRANSIT_PRIMARY.value)
254
-
255
- elif chart_type == "ExternalNatal":
256
- if is_angle:
257
- return ChartRadius.ANGLE_EXTERNAL_NATAL.value
258
- return (ChartRadius.PLANET_EXTERNAL_ALTERNATE.value if is_alternate
259
- else ChartRadius.PLANET_EXTERNAL_PRIMARY.value)
260
-
261
- else: # Natal chart
262
- if is_angle:
263
- return ChartRadius.ANGLE_NATAL.value
264
- return (ChartRadius.PLANET_ALTERNATE.value if is_alternate
265
- else ChartRadius.PLANET_PRIMARY.value)
266
-
267
-
268
- class SVGRenderer:
269
- """Handles SVG generation for celestial points."""
270
-
271
- def __init__(self, config: ChartConfiguration):
272
- self.config = config
273
-
274
- def calculate_offset(self, point_degree: float, adjustment: float = 0) -> float:
275
- """Calculate the angular offset for positioning a point."""
276
- return (-self.config.seventh_house_degree) + point_degree + adjustment
277
-
278
- def generate_point_svg(
279
- self,
280
- point: KerykeionPointModel,
281
- x: float,
282
- y: float,
283
- point_name: str
284
- ) -> str:
285
- """Generate SVG element for a celestial point."""
286
- scale = self.config.scale_factor
287
- transform_offset = ChartConstants.SYMBOL_SIZE * scale
288
-
289
- svg_parts = [
290
- f'<g kr:node="ChartPoint" kr:house="{point.house}" kr:sign="{point.sign}" ',
291
- f'kr:slug="{point.name}" transform="translate(-{transform_offset},-{transform_offset}) scale({scale})">',
292
- f'<use x="{x / scale}" y="{y / scale}" xlink:href="#{point_name}" />',
293
- '</g>'
294
- ]
295
-
296
- return ''.join(svg_parts)
297
-
298
- def draw_external_natal_lines(
299
- self,
300
- point_radius: float,
301
- true_offset: float,
302
- adjusted_offset: float,
303
- color: str
304
- ) -> str:
305
- """Draw connecting lines for ExternalNatal chart type."""
306
- lines = []
307
-
308
- # First line segment
309
- x1 = sliceToX(0, self.config.radius - self.config.third_circle_radius, true_offset) + self.config.third_circle_radius
310
- y1 = sliceToY(0, self.config.radius - self.config.third_circle_radius, true_offset) + self.config.third_circle_radius
311
- x2 = sliceToX(0, self.config.radius - point_radius - 30, true_offset) + point_radius + 30
312
- y2 = sliceToY(0, self.config.radius - point_radius - 30, true_offset) + point_radius + 30
313
-
314
- lines.append(f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
315
- f'style="stroke-width:{ChartConstants.LINE_WIDTH_THIN}px;stroke:{color};'
316
- f'stroke-opacity:{ChartConstants.LINE_OPACITY_PRIMARY};"/>')
317
-
318
- # Second line segment
319
- x3 = sliceToX(0, self.config.radius - point_radius - 10, adjusted_offset) + point_radius + 10
320
- y3 = sliceToY(0, self.config.radius - point_radius - 10, adjusted_offset) + point_radius + 10
321
-
322
- lines.append(f'<line x1="{x2}" y1="{y2}" x2="{x3}" y2="{y3}" '
323
- f'style="stroke-width:{ChartConstants.LINE_WIDTH_THIN}px;stroke:{color};'
324
- f'stroke-opacity:{ChartConstants.LINE_OPACITY_SECONDARY};"/>')
325
-
326
- return '\n'.join(lines) + '\n'
327
-
328
-
329
- class SecondaryPointsRenderer:
330
- """Handles rendering of secondary points (transit, synastry, return)."""
331
-
332
- def __init__(self, config: ChartConfiguration):
333
- self.config = config
334
- self.grouper = CelestialPointGrouper(ChartConstants.SECONDARY_GROUPING_THRESHOLD)
335
-
336
- def draw_secondary_points(
337
- self,
338
- points_abs_positions: List[float],
339
- points_rel_positions: List[float],
340
- points_settings: List[KerykeionSettingsCelestialPointModel],
341
- exclude_points: List[str]
342
- ) -> str:
343
- """Draw all secondary celestial points."""
344
- if not points_abs_positions:
345
- return ""
346
-
347
- # Filter out excluded points
348
- filtered_indices = [
349
- i for i, setting in enumerate(points_settings)
350
- if self.config.chart_type != "Transit" or setting["name"] not in exclude_points
351
- ]
352
-
353
- if not filtered_indices:
354
- return ""
355
-
356
- # Calculate position adjustments for grouping
357
- adjustments = self._calculate_secondary_adjustments(
358
- filtered_indices, points_abs_positions, points_settings
359
- )
360
-
361
- # Render each secondary point
362
- output_parts = []
363
- alternate_position = False
364
-
365
- for idx in filtered_indices:
366
- point_svg = self._render_single_secondary_point(
367
- idx, points_abs_positions, points_rel_positions,
368
- points_settings, adjustments, alternate_position
369
- )
370
- output_parts.append(point_svg)
371
- alternate_position = not alternate_position
372
-
373
- return ''.join(output_parts)
374
-
375
- def _calculate_secondary_adjustments(
376
- self,
377
- indices: List[int],
378
- positions: List[float],
379
- settings: List[KerykeionSettingsCelestialPointModel]
380
- ) -> Dict[int, float]:
381
- """Calculate position adjustments for secondary points."""
382
- # Create position mapping for filtered indices
383
- position_map = {positions[i]: i for i in indices}
384
- sorted_positions = sorted(position_map.keys())
385
-
386
- # Find groups
387
- groups = []
388
- current_group = []
389
-
390
- for i, pos in enumerate(sorted_positions):
391
- point_idx = position_map[pos]
392
- next_pos = sorted_positions[(i + 1) % len(sorted_positions)]
393
- next_idx = position_map[next_pos]
394
-
395
- distance = degreeDiff(positions[point_idx], positions[next_idx])
396
-
397
- if distance <= self.grouper.threshold:
398
- if not current_group:
399
- current_group = [point_idx]
400
- current_group.append(next_idx)
401
- else:
402
- if current_group:
403
- groups.append(current_group)
404
- current_group = []
405
-
406
- if current_group:
407
- groups.append(current_group)
408
-
409
- # Calculate adjustments
410
- adjustments = {i: 0.0 for i in indices}
411
-
412
- for group in groups:
413
- if len(group) == 2:
414
- adjustments[group[0]] = -1.0
415
- adjustments[group[1]] = 1.0
416
- elif len(group) == 3:
417
- adjustments[group[0]] = -1.5
418
- adjustments[group[1]] = 0.0
419
- adjustments[group[2]] = 1.5
420
- elif len(group) >= 4:
421
- for j, point_idx in enumerate(group):
422
- adjustments[point_idx] = -2.0 + j * (4.0 / (len(group) - 1))
423
-
424
- return adjustments
425
-
426
- def _render_single_secondary_point(
427
- self,
428
- point_idx: int,
429
- abs_positions: List[float],
430
- rel_positions: List[float],
431
- settings: List[KerykeionSettingsCelestialPointModel],
432
- adjustments: Dict[int, float],
433
- is_alternate: bool
434
- ) -> str:
435
- """Render a single secondary point with symbol, line, and degree text."""
436
- # Determine radius
437
- is_angle = ChartConstants.ANGLE_INDEX_RANGE[0] < point_idx < ChartConstants.ANGLE_INDEX_RANGE[1]
438
- point_radius = 9 if is_angle else (18 if is_alternate else 26)
439
-
440
- # Calculate position
441
- point_offset = self._calculate_secondary_offset(abs_positions[point_idx])
442
-
443
- # Generate SVG components
444
- symbol_svg = self._generate_secondary_symbol(point_idx, point_radius, point_offset, settings)
445
- line_svg = self._generate_secondary_line(point_idx, point_offset, settings)
446
- text_svg = self._generate_secondary_text(
447
- point_idx, abs_positions, rel_positions, settings, adjustments, point_offset
448
- )
449
-
450
- return symbol_svg + line_svg + text_svg
451
-
452
- def _calculate_secondary_offset(self, abs_position: float) -> float:
453
- """Calculate offset for secondary point positioning."""
454
- zero_point = 360 - self.config.seventh_house_degree
455
- offset = zero_point + abs_position
456
- return offset - 360 if offset > 360 else offset
457
-
458
- def _generate_secondary_symbol(
459
- self, point_idx: int, radius: int, offset: float,
460
- settings: List[KerykeionSettingsCelestialPointModel]
461
- ) -> str:
462
- """Generate SVG for secondary point symbol."""
463
- x = sliceToX(0, self.config.radius - radius, offset) + radius
464
- y = sliceToY(0, self.config.radius - radius, offset) + radius
465
-
466
- return (f'<g class="transit-planet-name" transform="translate(-6,-6)">'
467
- f'<g transform="scale(0.5)">'
468
- f'<use x="{x*2}" y="{y*2}" xlink:href="#{settings[point_idx]["name"]}" />'
469
- f'</g></g>')
470
-
471
- def _generate_secondary_line(
472
- self, point_idx: int, offset: float,
473
- settings: List[KerykeionSettingsCelestialPointModel]
474
- ) -> str:
475
- """Generate connecting line for secondary point."""
476
- x1 = sliceToX(0, self.config.radius + 3, offset) - 3
477
- y1 = sliceToY(0, self.config.radius + 3, offset) - 3
478
- x2 = sliceToX(0, self.config.radius - 3, offset) + 3
479
- y2 = sliceToY(0, self.config.radius - 3, offset) + 3
480
-
481
- color = settings[point_idx]["color"]
482
- return (f'<line class="transit-planet-line" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
483
- f'style="stroke: {color}; stroke-width: {ChartConstants.LINE_WIDTH_THIN}px; '
484
- f'stroke-opacity:.8;"/>')
485
-
486
- def _generate_secondary_text(
487
- self,
488
- point_idx: int,
489
- abs_positions: List[float],
490
- rel_positions: List[float],
491
- settings: List[KerykeionSettingsCelestialPointModel],
492
- adjustments: Dict[int, float],
493
- point_offset: float
494
- ) -> str:
495
- """Generate degree text for secondary point."""
496
- # Calculate rotation and text anchor
497
- rotation = self.config.first_house_degree - abs_positions[point_idx]
498
- text_anchor = "end"
499
-
500
- # Adjust for readability
501
- if -270 < rotation < -90:
502
- rotation += 180.0
503
- text_anchor = "start"
504
- elif 90 < rotation < 270:
505
- rotation -= 180.0
506
- text_anchor = "start"
507
-
508
- # Position text
509
- x_offset = 1 if text_anchor == "end" else -1
510
- adjusted_offset = point_offset + adjustments[point_idx]
511
- text_radius = -3.0
512
-
513
- deg_x = sliceToX(0, self.config.radius - text_radius, adjusted_offset + x_offset) + text_radius
514
- deg_y = sliceToY(0, self.config.radius - text_radius, adjusted_offset + x_offset) + text_radius
515
-
516
- # Format degree text
517
- degree_text = convert_decimal_to_degree_string(rel_positions[point_idx], format_type="1")
518
- color = settings[point_idx]["color"]
519
-
520
- return (f'<g transform="translate({deg_x},{deg_y})">'
521
- f'<text transform="rotate({rotation})" text-anchor="{text_anchor}" '
522
- f'style="fill: {color}; font-size: {ChartConstants.TEXT_SIZE}px;">{degree_text}</text>'
523
- f'</g>')
524
-
525
-
526
- def _validate_chart_inputs(
527
- chart_type: ChartType,
528
- secondary_points: Optional[List[KerykeionPointModel]]
529
- ) -> None:
530
- """Validate that required secondary points are provided for chart types that need them."""
531
- if _requires_secondary_points(chart_type) and secondary_points is None:
532
- raise KerykeionException(f"Secondary celestial points are required for {chart_type} charts")
533
-
534
-
535
- def _requires_secondary_points(chart_type: ChartType) -> bool:
536
- """Check if chart type requires secondary celestial points."""
537
- return chart_type in ["Transit", "Synastry", "Return"]
538
-
539
-
540
- def _draw_main_points(
541
- celestial_points: List[KerykeionPointModel],
542
- settings: List[KerykeionSettingsCelestialPointModel],
543
- config: ChartConfiguration,
544
- grouper: CelestialPointGrouper,
545
- adjuster: PositionAdjuster,
546
- renderer: SVGRenderer
547
- ) -> str:
548
- """Draw the main celestial points with proper grouping and positioning."""
549
- # Create position mapping and calculate distances
550
- position_map, sorted_positions = grouper.create_position_mapping(celestial_points, settings)
551
- abs_positions = [p.abs_pos for p in celestial_points]
552
-
553
- point_positions = grouper.calculate_distances(sorted_positions, position_map, abs_positions)
554
-
555
- # Update labels
556
- for point_pos in point_positions:
557
- point_pos.label = settings[point_pos.index]["label"]
558
-
559
- # Identify groups and calculate adjustments
560
- groups = grouper.identify_groups(point_positions)
561
- adjustments = adjuster.calculate_adjustments(groups, len(settings))
562
-
563
- # Draw each point
564
- output_parts = []
565
-
566
- for position_idx, abs_position in enumerate(sorted_positions):
567
- point_idx = position_map[abs_position]
568
-
569
- # Calculate positioning
570
- point_radius = RadiusCalculator.get_point_radius(
571
- point_idx, config.chart_type, bool(position_idx % 2)
572
- )
573
-
574
- adjusted_offset = renderer.calculate_offset(
575
- abs_positions[point_idx], adjustments[position_idx]
576
- )
577
-
578
- # Calculate coordinates
579
- point_x = sliceToX(0, config.radius - point_radius, adjusted_offset) + point_radius
580
- point_y = sliceToY(0, config.radius - point_radius, adjusted_offset) + point_radius
581
-
582
- # Draw external natal lines if needed
583
- if config.chart_type == "ExternalNatal":
584
- true_offset = renderer.calculate_offset(abs_positions[point_idx])
585
- line_svg = renderer.draw_external_natal_lines(
586
- point_radius, true_offset, adjusted_offset, settings[point_idx]["color"]
587
- )
588
- output_parts.append(line_svg)
589
-
590
- # Generate point SVG
591
- point_svg = renderer.generate_point_svg(
592
- celestial_points[point_idx], point_x, point_y, settings[point_idx]["name"]
593
- )
594
- output_parts.append(point_svg)
595
-
596
- return ''.join(output_parts)
597
-
598
-
599
- def draw_planets_v2(
600
- radius: Union[int, float],
601
- available_kerykeion_celestial_points: List[KerykeionPointModel],
602
- available_planets_setting: List[KerykeionSettingsCelestialPointModel],
603
- third_circle_radius: Union[int, float],
604
- main_subject_first_house_degree_ut: Union[int, float],
605
- main_subject_seventh_house_degree_ut: Union[int, float],
606
- chart_type: ChartType,
607
- second_subject_available_kerykeion_celestial_points: Optional[List[KerykeionPointModel]] = None,
608
- ) -> str:
609
- """
610
- Draw celestial points on an astrological chart.
611
-
612
- This is the main entry point for drawing planets and other celestial points
613
- on astrological charts. It handles positioning, grouping, and overlap resolution.
614
-
615
- Args:
616
- radius: Chart radius in pixels
617
- available_kerykeion_celestial_points: Main subject's celestial points
618
- available_planets_setting: Settings for celestial points
619
- third_circle_radius: Radius of the third circle
620
- main_subject_first_house_degree_ut: First house degree
621
- main_subject_seventh_house_degree_ut: Seventh house degree
622
- chart_type: Type of chart being drawn
623
- second_subject_available_kerykeion_celestial_points: Secondary subject's points
624
-
625
- Returns:
626
- SVG string for the celestial points
627
-
628
- Raises:
629
- KerykeionException: If secondary points are required but not provided
630
- """
631
- # Validate inputs
632
- _validate_chart_inputs(chart_type, second_subject_available_kerykeion_celestial_points)
633
-
634
- # Create configuration
635
- config = ChartConfiguration(
636
- radius=float(radius),
637
- third_circle_radius=float(third_circle_radius),
638
- first_house_degree=float(main_subject_first_house_degree_ut),
639
- seventh_house_degree=float(main_subject_seventh_house_degree_ut),
640
- chart_type=chart_type
641
- )
642
-
643
- # Initialize components
644
- grouper = CelestialPointGrouper()
645
- adjuster = PositionAdjuster()
646
- renderer = SVGRenderer(config)
647
-
648
- # Process main celestial points
649
- output_parts = []
650
-
651
- if available_kerykeion_celestial_points:
652
- main_svg = _draw_main_points(
653
- available_kerykeion_celestial_points,
654
- available_planets_setting,
655
- config,
656
- grouper,
657
- adjuster,
658
- renderer
659
- )
660
- output_parts.append(main_svg)
661
-
662
- # Process secondary points if needed
663
- if _requires_secondary_points(chart_type) and second_subject_available_kerykeion_celestial_points:
664
- secondary_renderer = SecondaryPointsRenderer(config)
665
-
666
- secondary_abs_positions = [p.abs_pos for p in second_subject_available_kerykeion_celestial_points]
667
- secondary_rel_positions = [p.position for p in second_subject_available_kerykeion_celestial_points]
668
- exclude_points = list(get_args(Houses)) if chart_type == "Transit" else []
669
-
670
- secondary_svg = secondary_renderer.draw_secondary_points(
671
- secondary_abs_positions,
672
- secondary_rel_positions,
673
- available_planets_setting,
674
- exclude_points
675
- )
676
- output_parts.append(secondary_svg)
677
-
678
- return ''.join(output_parts)
679
-