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,13 +1,9 @@
1
- # type: ignore
2
- # TODO: Legacy original method extracted as function. The V2 is a heavy refactor of this code. If it's safe, delete this.
3
-
4
1
  from kerykeion.charts.charts_utils import degreeDiff, sliceToX, sliceToY, convert_decimal_to_degree_string
5
- from kerykeion.kr_types import KerykeionException, ChartType, KerykeionPointModel
6
- from kerykeion.kr_types.settings_models import KerykeionSettingsCelestialPointModel
7
- from kerykeion.kr_types.kr_literals import Houses
2
+ from kerykeion.schemas import KerykeionException, ChartType, KerykeionPointModel
3
+ from kerykeion.schemas.settings_models import KerykeionSettingsCelestialPointModel
4
+ from kerykeion.schemas.kr_literals import Houses
8
5
  import logging
9
- from typing import Union, get_args
10
-
6
+ from typing import Union, get_args, List, Optional
11
7
 
12
8
 
13
9
  def draw_planets(
@@ -19,390 +15,644 @@ def draw_planets(
19
15
  main_subject_seventh_house_degree_ut: Union[int, float],
20
16
  chart_type: ChartType,
21
17
  second_subject_available_kerykeion_celestial_points: Union[list[KerykeionPointModel], None] = None,
22
- ):
18
+ external_view: bool = False,
19
+ ) -> str:
23
20
  """
24
- Draws the planets on a chart based on the provided parameters.
21
+ Draws the planets on an astrological chart based on the provided parameters.
22
+
23
+ This function calculates positions, handles overlap of celestial points, and draws SVG
24
+ elements for each planet/point on the chart. It supports different chart types including
25
+ natal charts, transits, synastry, and returns. For single-subject charts (Natal), it
26
+ can render planets in external view mode using the external_view parameter.
25
27
 
26
28
  Args:
27
- radius (int): The radius of the chart.
29
+ radius (Union[int, float]): The radius of the chart in pixels.
28
30
  available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the main subject.
29
31
  available_planets_setting (list[KerykeionSettingsCelestialPointModel]): Settings for the celestial points.
30
- third_circle_radius (Union[int, float]): Radius of the third circle.
32
+ third_circle_radius (Union[int, float]): Radius of the third circle in the chart.
31
33
  main_subject_first_house_degree_ut (Union[int, float]): Degree of the first house for the main subject.
32
34
  main_subject_seventh_house_degree_ut (Union[int, float]): Degree of the seventh house for the main subject.
33
- chart_type (ChartType): Type of the chart (e.g., "Transit", "Synastry").
35
+ chart_type (ChartType): Type of the chart (e.g., "Transit", "Synastry", "DualReturnChart", "Natal").
34
36
  second_subject_available_kerykeion_celestial_points (Union[list[KerykeionPointModel], None], optional):
35
- List of celestial points for the second subject, required for "Transit" or "Synastry" charts. Defaults to None.
37
+ List of celestial points for the second subject, required for "Transit", "Synastry", or "Return" charts.
38
+ Defaults to None.
39
+ external_view (bool, optional):
40
+ Whether to render planets in external view mode (planets on outer ring with connecting lines).
41
+ Only applicable for single-subject charts. Defaults to False.
36
42
 
37
43
  Raises:
38
- KerykeionException: If the second subject is required but not provided.
44
+ KerykeionException: If secondary celestial points are required but not provided.
39
45
 
40
46
  Returns:
41
47
  str: SVG output for the chart with the planets drawn.
42
48
  """
43
- TRANSIT_RING_EXCLUDE_POINTS_NAMES = get_args(Houses)
49
+ # Constants and initialization
50
+ PLANET_GROUPING_THRESHOLD = 3.4 # Distance threshold to consider planets as grouped
51
+ TRANSIT_RING_EXCLUDE_POINTS_NAMES: List[str] = list(get_args(Houses))
52
+ output = ""
44
53
 
54
+ # -----------------------------------------------------------
55
+ # 1. Validate inputs and prepare data
56
+ # -----------------------------------------------------------
57
+ if chart_type == "Transit" and second_subject_available_kerykeion_celestial_points is None:
58
+ raise KerykeionException("Secondary celestial points are required for Transit charts")
59
+ elif chart_type == "Synastry" and second_subject_available_kerykeion_celestial_points is None:
60
+ raise KerykeionException("Secondary celestial points are required for Synastry charts")
61
+ elif chart_type == "Return" and second_subject_available_kerykeion_celestial_points is None:
62
+ raise KerykeionException("Secondary celestial points are required for Return charts")
63
+
64
+ # Extract absolute and relative positions for main celestial points
65
+ main_points_abs_positions = [planet.abs_pos for planet in available_kerykeion_celestial_points]
66
+ [planet.position for planet in available_kerykeion_celestial_points]
67
+
68
+ # Extract absolute and relative positions for secondary celestial points if needed
69
+ secondary_points_abs_positions = []
70
+ secondary_points_rel_positions = []
45
71
  if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
46
- if second_subject_available_kerykeion_celestial_points is None:
47
- raise KerykeionException("Second subject is required for Transit or Synastry charts")
72
+ if second_subject_available_kerykeion_celestial_points is not None:
73
+ secondary_points_abs_positions = [
74
+ planet.abs_pos for planet in second_subject_available_kerykeion_celestial_points
75
+ ]
76
+ secondary_points_rel_positions = [
77
+ planet.position for planet in second_subject_available_kerykeion_celestial_points
78
+ ]
79
+
80
+ # -----------------------------------------------------------
81
+ # 2. Create position lookup dictionary for main celestial points
82
+ # -----------------------------------------------------------
83
+ # Map absolute degree to index in the settings array
84
+ position_index_map = {}
85
+ for i in range(len(available_planets_setting)):
86
+ position_index_map[main_points_abs_positions[i]] = i
87
+ logging.debug(f"Planet index: {i}, degree: {main_points_abs_positions[i]}")
88
+
89
+ # Sort positions for ordered processing
90
+ sorted_positions = sorted(position_index_map.keys())
91
+
92
+ # -----------------------------------------------------------
93
+ # 3. Identify groups of celestial points that are close to each other
94
+ # -----------------------------------------------------------
95
+ point_groups: List[List[List[Union[int, float, str]]]] = []
96
+ is_group_open = False
97
+ planets_by_position: List[Optional[List[Union[int, float]]]] = [None] * len(position_index_map)
98
+
99
+ # Process each celestial point to find groups
100
+ for position_idx, abs_position in enumerate(sorted_positions):
101
+ point_idx = position_index_map[abs_position]
102
+
103
+ # Find previous and next point positions for distance calculations
104
+ # Handle special case when there's only one planet
105
+ if len(sorted_positions) == 1:
106
+ # With only one planet, there are no adjacent planets
107
+ prev_position = main_points_abs_positions[point_idx]
108
+ next_position = main_points_abs_positions[point_idx]
109
+ elif position_idx == 0:
110
+ prev_position = main_points_abs_positions[position_index_map[sorted_positions[-1]]]
111
+ next_position = main_points_abs_positions[position_index_map[sorted_positions[1]]]
112
+ elif position_idx == len(sorted_positions) - 1:
113
+ prev_position = main_points_abs_positions[position_index_map[sorted_positions[position_idx - 1]]]
114
+ next_position = main_points_abs_positions[position_index_map[sorted_positions[0]]]
115
+ else:
116
+ prev_position = main_points_abs_positions[position_index_map[sorted_positions[position_idx - 1]]]
117
+ next_position = main_points_abs_positions[position_index_map[sorted_positions[position_idx + 1]]]
118
+
119
+ # Calculate distance to adjacent points
120
+ # When there's only one planet, set distances to a large value to prevent grouping
121
+ if len(sorted_positions) == 1:
122
+ distance_to_prev = 360.0 # Maximum possible distance
123
+ distance_to_next = 360.0 # Maximum possible distance
124
+ else:
125
+ distance_to_prev = degreeDiff(prev_position, main_points_abs_positions[point_idx])
126
+ distance_to_next = degreeDiff(next_position, main_points_abs_positions[point_idx])
127
+
128
+ # Store position and distance information
129
+ planets_by_position[position_idx] = [point_idx, distance_to_prev, distance_to_next]
48
130
 
49
- # Make a list for the absolute degrees of the points of the graphic.
50
- points_deg_ut = []
51
- for planet in available_kerykeion_celestial_points:
52
- points_deg_ut.append(planet.abs_pos)
131
+ label = available_planets_setting[point_idx]["label"]
132
+ logging.debug(f"{label}, distance_to_prev: {distance_to_prev}, distance_to_next: {distance_to_next}")
133
+
134
+ # Group points that are close to each other
135
+ if distance_to_next < PLANET_GROUPING_THRESHOLD:
136
+ point_data = [position_idx, distance_to_prev, distance_to_next, label]
137
+ if is_group_open:
138
+ point_groups[-1].append(point_data)
139
+ else:
140
+ is_group_open = True
141
+ point_groups.append([point_data])
142
+ else:
143
+ if is_group_open:
144
+ point_data = [position_idx, distance_to_prev, distance_to_next, label]
145
+ point_groups[-1].append(point_data)
146
+ is_group_open = False
147
+
148
+ # -----------------------------------------------------------
149
+ # 4. Calculate position adjustments to avoid overlapping
150
+ # -----------------------------------------------------------
151
+ position_adjustments: List[float] = [0.0] * len(available_planets_setting)
152
+
153
+ # Process each group to calculate position adjustments
154
+ for group in point_groups:
155
+ group_size = len(group)
156
+
157
+ # Handle groups of two celestial points
158
+ if group_size == 2:
159
+ _handle_two_point_group(group, planets_by_position, position_adjustments, PLANET_GROUPING_THRESHOLD)
160
+
161
+ # Handle groups of three or more celestial points
162
+ elif group_size >= 3:
163
+ _handle_multi_point_group(group, position_adjustments, PLANET_GROUPING_THRESHOLD)
164
+
165
+ # -----------------------------------------------------------
166
+ # 5. Draw main celestial points
167
+ # -----------------------------------------------------------
168
+ adjusted_offset = 0.0 # Initialize for use outside loop
169
+ for position_idx, abs_position in enumerate(sorted_positions):
170
+ point_idx = position_index_map[abs_position]
171
+
172
+ # Determine radius based on chart type and point type
173
+ point_radius = _determine_point_radius(point_idx, chart_type, bool(position_idx % 2), external_view)
174
+
175
+ # Calculate position offset for the point
176
+ adjusted_offset = _calculate_point_offset(
177
+ main_subject_seventh_house_degree_ut,
178
+ main_points_abs_positions[point_idx],
179
+ position_adjustments[position_idx],
180
+ )
181
+
182
+ # Calculate true position without adjustment (used for connecting lines)
183
+ true_offset = _calculate_point_offset(
184
+ main_subject_seventh_house_degree_ut,
185
+ main_points_abs_positions[point_idx],
186
+ 0
187
+ )
188
+
189
+ # Calculate point coordinates
190
+ point_x = sliceToX(0, radius - point_radius, adjusted_offset) + point_radius
191
+ point_y = sliceToY(0, radius - point_radius, adjusted_offset) + point_radius
192
+
193
+ # Determine scale factor based on chart type
194
+ scale_factor = 1.0
195
+ if chart_type == "Transit":
196
+ scale_factor = 0.8
197
+ elif chart_type == "Synastry":
198
+ scale_factor = 0.8
199
+ elif chart_type == "Return":
200
+ scale_factor = 0.8
201
+ elif external_view:
202
+ scale_factor = 0.8
203
+
204
+ # Draw connecting lines for external view
205
+ if external_view:
206
+ output = _draw_external_natal_lines(
207
+ output,
208
+ radius,
209
+ third_circle_radius,
210
+ point_radius,
211
+ true_offset,
212
+ adjusted_offset,
213
+ available_planets_setting[point_idx]["color"],
214
+ )
53
215
 
54
- # Make a list of the relative degrees of the points in the graphic.
55
- points_deg = []
56
- for planet in available_kerykeion_celestial_points:
57
- points_deg.append(planet.position)
216
+ # Draw the celestial point SVG element
217
+ point_details = available_kerykeion_celestial_points[point_idx]
218
+ output += _generate_point_svg(
219
+ point_details, point_x, point_y, scale_factor, available_planets_setting[point_idx]["name"]
220
+ )
58
221
 
222
+ # -----------------------------------------------------------
223
+ # 6. Draw transit/secondary celestial points
224
+ # -----------------------------------------------------------
59
225
  if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
60
- # Make a list for the absolute degrees of the points of the graphic.
61
- t_points_deg_ut = []
62
- for planet in second_subject_available_kerykeion_celestial_points:
63
- t_points_deg_ut.append(planet.abs_pos)
226
+ output = _draw_secondary_points(
227
+ output,
228
+ radius,
229
+ main_subject_first_house_degree_ut,
230
+ main_subject_seventh_house_degree_ut,
231
+ secondary_points_abs_positions,
232
+ secondary_points_rel_positions,
233
+ available_planets_setting,
234
+ chart_type,
235
+ TRANSIT_RING_EXCLUDE_POINTS_NAMES,
236
+ adjusted_offset,
237
+ )
64
238
 
65
- # Make a list of the relative degrees of the points in the graphic.
66
- t_points_deg = []
67
- for planet in second_subject_available_kerykeion_celestial_points:
68
- t_points_deg.append(planet.position)
239
+ return output
69
240
 
70
- planets_degut = {}
71
- diff = range(len(available_planets_setting))
72
241
 
73
- for i in range(len(available_planets_setting)):
74
- # list of planets sorted by degree
75
- logging.debug(f"planet: {i}, degree: {points_deg_ut[i]}")
76
- planets_degut[points_deg_ut[i]] = i
242
+ def _handle_two_point_group(
243
+ group: list, planets_by_position: list, position_adjustments: list, threshold: float
244
+ ) -> None:
245
+ """
246
+ Handle positioning for a group of two celestial points that are close to each other.
247
+
248
+ Adjusts positions to prevent overlapping by calculating appropriate offsets
249
+ based on available space around the points.
77
250
 
251
+ Args:
252
+ group (list): A list containing data about two closely positioned points.
253
+ planets_by_position (list): A list with data about all planets positions.
254
+ position_adjustments (list): The list to store calculated position adjustments.
255
+ threshold (float): The minimum distance threshold for considering points as grouped.
78
256
  """
79
- FIXME: The planets_degut is a dictionary like:
80
- {planet_degree: planet_index}
81
- It should be replaced bu points_deg_ut
82
- print(points_deg_ut)
83
- print(planets_degut)
257
+ next_to_a = group[0][0] - 1
258
+ next_to_b = 0 if group[1][0] == (len(planets_by_position) - 1) else group[1][0] + 1
259
+
260
+ # If both points have room
261
+ if (group[0][1] > (2 * threshold)) and (group[1][2] > (2 * threshold)):
262
+ position_adjustments[group[0][0]] = -(threshold - group[0][2]) / 2
263
+ position_adjustments[group[1][0]] = +(threshold - group[0][2]) / 2
264
+
265
+ # If only first point has room
266
+ elif group[0][1] > (2 * threshold):
267
+ position_adjustments[group[0][0]] = -threshold
268
+
269
+ # If only second point has room
270
+ elif group[1][2] > (2 * threshold):
271
+ position_adjustments[group[1][0]] = +threshold
272
+
273
+ # If points adjacent to group have room
274
+ elif (planets_by_position[next_to_a][1] > (2.4 * threshold)) and (planets_by_position[next_to_b][2] > (2.4 * threshold)):
275
+ position_adjustments[next_to_a] = group[0][1] - threshold * 2
276
+ position_adjustments[group[0][0]] = -threshold * 0.5
277
+ position_adjustments[next_to_b] = -(group[1][2] - threshold * 2)
278
+ position_adjustments[group[1][0]] = +threshold * 0.5
279
+
280
+ # If only point adjacent to first has room
281
+ elif planets_by_position[next_to_a][1] > (2 * threshold):
282
+ position_adjustments[next_to_a] = group[0][1] - threshold * 2.5
283
+ position_adjustments[group[0][0]] = -threshold * 1.2
284
+
285
+ # If only point adjacent to second has room
286
+ elif planets_by_position[next_to_b][2] > (2 * threshold):
287
+ position_adjustments[next_to_b] = -(group[1][2] - threshold * 2.5)
288
+ position_adjustments[group[1][0]] = +threshold * 1.2
289
+
290
+
291
+ def _handle_multi_point_group(group: list, position_adjustments: list, threshold: float) -> None:
84
292
  """
293
+ Handle positioning for a group of three or more celestial points that are close to each other.
85
294
 
86
- output = ""
87
- keys = list(planets_degut.keys())
88
- keys.sort()
89
- switch = 0
90
-
91
- planets_degrouped = {}
92
- groups = []
93
- planets_by_pos = list(range(len(planets_degut)))
94
- planet_drange = 3.4
95
- # get groups closely together
96
- group_open = False
97
- for e in range(len(keys)):
98
- i = planets_degut[keys[e]]
99
- # get distances between planets
100
- if e == 0:
101
- prev = points_deg_ut[planets_degut[keys[-1]]]
102
- next = points_deg_ut[planets_degut[keys[1]]]
103
- elif e == (len(keys) - 1):
104
- prev = points_deg_ut[planets_degut[keys[e - 1]]]
105
- next = points_deg_ut[planets_degut[keys[0]]]
106
- else:
107
- prev = points_deg_ut[planets_degut[keys[e - 1]]]
108
- next = points_deg_ut[planets_degut[keys[e + 1]]]
109
- diffa = degreeDiff(prev, points_deg_ut[i])
110
- diffb = degreeDiff(next, points_deg_ut[i])
111
- planets_by_pos[e] = [i, diffa, diffb]
295
+ Distributes points evenly within the available space to prevent overlapping.
296
+
297
+ Args:
298
+ group (list): A list containing data about grouped points.
299
+ position_adjustments (list): The list to store calculated position adjustments.
300
+ threshold (float): The minimum distance threshold for considering points as grouped.
301
+ """
302
+ group_size = len(group)
303
+
304
+ # Calculate available space
305
+ available_space = group[0][1] # Distance before first point
306
+ for i in range(group_size):
307
+ available_space += group[i][2] # Add distance after each point
308
+
309
+ # Calculate needed space
310
+ needed_space = (3 * threshold) + (1.2 * (group_size - 1) * threshold)
311
+ leftover_space = available_space - needed_space
312
+
313
+ # Get spacing before first and after last point
314
+ space_before_first = group[0][1]
315
+ space_after_last = group[group_size - 1][2]
316
+
317
+ # Position points based on available space
318
+ if (space_before_first > (needed_space * 0.5)) and (space_after_last > (needed_space * 0.5)):
319
+ # Center the group
320
+ start_position = space_before_first - (needed_space * 0.5)
321
+ else:
322
+ # Distribute leftover space proportionally
323
+ start_position = (leftover_space / (space_before_first + space_after_last)) * space_before_first
324
+
325
+ # Apply positions if there's enough space
326
+ if available_space > needed_space:
327
+ position_adjustments[group[0][0]] = start_position - group[0][1] + (1.5 * threshold)
328
+
329
+ # Position each subsequent point relative to the previous one
330
+ for i in range(group_size - 1):
331
+ position_adjustments[group[i + 1][0]] = 1.2 * threshold + position_adjustments[group[i][0]] - group[i][2]
332
+
333
+
334
+ def _determine_point_radius(
335
+ point_idx: int,
336
+ chart_type: str,
337
+ is_alternate_position: bool,
338
+ external_view: bool = False
339
+ ) -> int:
340
+ """
341
+ Determine the radius for placing a celestial point based on its type and chart type.
112
342
 
113
- logging.debug(f'{available_planets_setting[i]["label"]}, {diffa}, {diffb}')
343
+ Args:
344
+ point_idx (int): Index of the celestial point.
345
+ chart_type (str): Type of the chart.
346
+ is_alternate_position (bool): Whether to use alternate positioning.
347
+ external_view (bool): Whether external view is enabled.
114
348
 
115
- if diffb < planet_drange:
116
- if group_open:
117
- groups[-1].append([e, diffa, diffb, available_planets_setting[planets_degut[keys[e]]]["label"]])
118
- else:
119
- group_open = True
120
- groups.append([])
121
- groups[-1].append([e, diffa, diffb, available_planets_setting[planets_degut[keys[e]]]["label"]])
349
+ Returns:
350
+ int: Radius value for the point.
351
+ """
352
+ # Check if point is an angle of the chart (ASC, MC, DSC, IC)
353
+ is_chart_angle = 22 < point_idx < 27
354
+
355
+ if chart_type == "Transit":
356
+ if is_chart_angle:
357
+ return 76
122
358
  else:
123
- if group_open:
124
- groups[-1].append([e, diffa, diffb, available_planets_setting[planets_degut[keys[e]]]["label"]])
125
- group_open = False
126
-
127
- def zero(x):
128
- return 0
129
-
130
- planets_delta = list(map(zero, range(len(available_planets_setting))))
131
-
132
- # print groups
133
- # print planets_by_pos
134
- for a in range(len(groups)):
135
- # Two grouped planets
136
- if len(groups[a]) == 2:
137
- next_to_a = groups[a][0][0] - 1
138
- if groups[a][1][0] == (len(planets_by_pos) - 1):
139
- next_to_b = 0
140
- else:
141
- next_to_b = groups[a][1][0] + 1
142
- # if both planets have room
143
- if (groups[a][0][1] > (2 * planet_drange)) & (groups[a][1][2] > (2 * planet_drange)):
144
- planets_delta[groups[a][0][0]] = -(planet_drange - groups[a][0][2]) / 2
145
- planets_delta[groups[a][1][0]] = +(planet_drange - groups[a][0][2]) / 2
146
- # if planet a has room
147
- elif groups[a][0][1] > (2 * planet_drange):
148
- planets_delta[groups[a][0][0]] = -planet_drange
149
- # if planet b has room
150
- elif groups[a][1][2] > (2 * planet_drange):
151
- planets_delta[groups[a][1][0]] = +planet_drange
152
-
153
- # if planets next to a and b have room move them
154
- elif (planets_by_pos[next_to_a][1] > (2.4 * planet_drange)) & (
155
- planets_by_pos[next_to_b][2] > (2.4 * planet_drange)
156
- ):
157
- planets_delta[(next_to_a)] = groups[a][0][1] - planet_drange * 2
158
- planets_delta[groups[a][0][0]] = -planet_drange * 0.5
159
- planets_delta[next_to_b] = -(groups[a][1][2] - planet_drange * 2)
160
- planets_delta[groups[a][1][0]] = +planet_drange * 0.5
161
-
162
- # if planet next to a has room move them
163
- elif planets_by_pos[next_to_a][1] > (2 * planet_drange):
164
- planets_delta[(next_to_a)] = groups[a][0][1] - planet_drange * 2.5
165
- planets_delta[groups[a][0][0]] = -planet_drange * 1.2
166
-
167
- # if planet next to b has room move them
168
- elif planets_by_pos[next_to_b][2] > (2 * planet_drange):
169
- planets_delta[next_to_b] = -(groups[a][1][2] - planet_drange * 2.5)
170
- planets_delta[groups[a][1][0]] = +planet_drange * 1.2
171
-
172
- # Three grouped planets or more
173
- xl = len(groups[a])
174
- if xl >= 3:
175
- available = groups[a][0][1]
176
- for f in range(xl):
177
- available += groups[a][f][2]
178
- need = (3 * planet_drange) + (1.2 * (xl - 1) * planet_drange)
179
- leftover = available - need
180
- xa = groups[a][0][1]
181
- xb = groups[a][(xl - 1)][2]
182
-
183
- # center
184
- if (xa > (need * 0.5)) & (xb > (need * 0.5)):
185
- startA = xa - (need * 0.5)
186
- # position relative to next planets
187
- else:
188
- startA = (leftover / (xa + xb)) * xa
189
- startB = (leftover / (xa + xb)) * xb
190
-
191
- if available > need:
192
- planets_delta[groups[a][0][0]] = startA - groups[a][0][1] + (1.5 * planet_drange)
193
- for f in range(xl - 1):
194
- planets_delta[groups[a][(f + 1)][0]] = (
195
- 1.2 * planet_drange + planets_delta[groups[a][f][0]] - groups[a][f][2]
196
- )
197
-
198
- for e in range(len(keys)):
199
- i = planets_degut[keys[e]]
200
-
201
- # coordinates
202
- if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
203
- if 22 < i < 27:
204
- rplanet = 76
205
- elif switch == 1:
206
- rplanet = 110
207
- switch = 0
208
- else:
209
- rplanet = 130
210
- switch = 1
359
+ return 110 if is_alternate_position else 130
360
+ elif chart_type == "Synastry":
361
+ if is_chart_angle:
362
+ return 76
211
363
  else:
212
- # if 22 < i < 27 it is asc,mc,dsc,ic (angles of chart)
213
- # put on special line (rplanet is range from outer ring)
214
- amin, bmin, cmin = 0, 0, 0
215
- if chart_type == "ExternalNatal":
216
- amin = 74 - 10
217
- bmin = 94 - 10
218
- cmin = 40 - 10
219
-
220
- if 22 < i < 27:
221
- rplanet = 40 - cmin
222
- elif switch == 1:
223
- rplanet = 74 - amin
224
- switch = 0
225
- else:
226
- rplanet = 94 - bmin
227
- switch = 1
228
-
229
- rtext = 45
230
-
231
- offset = (int(main_subject_seventh_house_degree_ut) / -1) + int(points_deg_ut[i] + planets_delta[e])
232
- trueoffset = (int(main_subject_seventh_house_degree_ut) / -1) + int(points_deg_ut[i])
233
-
234
- planet_x = sliceToX(0, (radius - rplanet), offset) + rplanet
235
- planet_y = sliceToY(0, (radius - rplanet), offset) + rplanet
236
- if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
237
- scale = 0.8
238
-
239
- elif chart_type == "ExternalNatal":
240
- scale = 0.8
241
- # line1
242
- x1 = sliceToX(0, (radius - third_circle_radius), trueoffset) + third_circle_radius
243
- y1 = sliceToY(0, (radius - third_circle_radius), trueoffset) + third_circle_radius
244
- x2 = sliceToX(0, (radius - rplanet - 30), trueoffset) + rplanet + 30
245
- y2 = sliceToY(0, (radius - rplanet - 30), trueoffset) + rplanet + 30
246
- color = available_planets_setting[i]["color"]
247
- output += (
248
- '<line x1="%s" y1="%s" x2="%s" y2="%s" style="stroke-width:1px;stroke:%s;stroke-opacity:.3;"/>\n'
249
- % (x1, y1, x2, y2, color)
250
- )
251
- # line2
252
- x1 = sliceToX(0, (radius - rplanet - 30), trueoffset) + rplanet + 30
253
- y1 = sliceToY(0, (radius - rplanet - 30), trueoffset) + rplanet + 30
254
- x2 = sliceToX(0, (radius - rplanet - 10), offset) + rplanet + 10
255
- y2 = sliceToY(0, (radius - rplanet - 10), offset) + rplanet + 10
256
- output += (
257
- '<line x1="%s" y1="%s" x2="%s" y2="%s" style="stroke-width:1px;stroke:%s;stroke-opacity:.5;"/>\n'
258
- % (x1, y1, x2, y2, color)
259
- )
260
-
364
+ return 110 if is_alternate_position else 130
365
+ elif chart_type == "Return":
366
+ if is_chart_angle:
367
+ return 76
368
+ else:
369
+ return 110 if is_alternate_position else 130
370
+ else:
371
+ # Default natal chart and external view handling
372
+ # if 22 < point_idx < 27 it is asc,mc,dsc,ic (angles of chart)
373
+ amin, bmin, cmin = 0, 0, 0
374
+ if external_view:
375
+ amin = 74 - 10
376
+ bmin = 94 - 10
377
+ cmin = 40 - 10
378
+
379
+ if is_chart_angle:
380
+ return 40 - cmin
381
+ elif is_alternate_position:
382
+ return 74 - amin
261
383
  else:
262
- scale = 1
384
+ return 94 - bmin
263
385
 
264
- planet_details = available_kerykeion_celestial_points[i]
265
386
 
266
- output += f'<g kr:node="ChartPoint" kr:house="{planet_details["house"]}" kr:sign="{planet_details["sign"]}" kr:slug="{planet_details["name"]}" transform="translate(-{12 * scale},-{12 * scale}) scale({scale})">'
267
- output += f'<use x="{planet_x * (1/scale)}" y="{planet_y * (1/scale)}" xlink:href="#{available_planets_setting[i]["name"]}" />'
268
- output += f"</g>"
387
+ def _calculate_point_offset(
388
+ seventh_house_degree: Union[int, float], point_degree: Union[int, float], adjustment: Union[int, float]
389
+ ) -> float:
390
+ """
391
+ Calculate the offset position of a celestial point on the chart.
269
392
 
270
- # make transit degut and display planets
271
- if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
272
- group_offset = {}
273
- t_planets_degut = {}
274
- list_range = len(available_planets_setting)
275
-
276
- for i in range(list_range):
277
- if chart_type == "Transit" and available_planets_setting[i]["name"] in TRANSIT_RING_EXCLUDE_POINTS_NAMES:
278
- continue
279
-
280
- group_offset[i] = 0
281
- t_planets_degut[t_points_deg_ut[i]] = i
282
-
283
- t_keys = list(t_planets_degut.keys())
284
- t_keys.sort()
285
-
286
- # grab closely grouped planets
287
- groups = []
288
- in_group = False
289
- for e in range(len(t_keys)):
290
- i_a = t_planets_degut[t_keys[e]]
291
- if e == (len(t_keys) - 1):
292
- i_b = t_planets_degut[t_keys[0]]
293
- else:
294
- i_b = t_planets_degut[t_keys[e + 1]]
295
-
296
- a = t_points_deg_ut[i_a]
297
- b = t_points_deg_ut[i_b]
298
- diff = degreeDiff(a, b)
299
- if diff <= 2.5:
300
- if in_group:
301
- groups[-1].append(i_b)
302
- else:
303
- groups.append([i_a])
304
- groups[-1].append(i_b)
305
- in_group = True
306
- else:
307
- in_group = False
308
- # loop groups and set degrees display adjustment
309
- for i in range(len(groups)):
310
- if len(groups[i]) == 2:
311
- group_offset[groups[i][0]] = -1.0
312
- group_offset[groups[i][1]] = 1.0
313
- elif len(groups[i]) == 3:
314
- group_offset[groups[i][0]] = -1.5
315
- group_offset[groups[i][1]] = 0
316
- group_offset[groups[i][2]] = 1.5
317
- elif len(groups[i]) == 4:
318
- group_offset[groups[i][0]] = -2.0
319
- group_offset[groups[i][1]] = -1.0
320
- group_offset[groups[i][2]] = 1.0
321
- group_offset[groups[i][3]] = 2.0
322
-
323
- switch = 0
324
-
325
- # Transit planets loop
326
- for e in range(len(t_keys)):
327
- if chart_type == "Transit" and available_planets_setting[e]["name"] in TRANSIT_RING_EXCLUDE_POINTS_NAMES:
328
- continue
329
-
330
- i = t_planets_degut[t_keys[e]]
331
-
332
- if 22 < i < 27:
333
- rplanet = 9
334
- elif switch == 1:
335
- rplanet = 18
336
- switch = 0
337
- else:
338
- rplanet = 26
339
- switch = 1
340
-
341
- # Transit planet name
342
- zeropoint = 360 - main_subject_seventh_house_degree_ut
343
- t_offset = zeropoint + t_points_deg_ut[i]
344
- if t_offset > 360:
345
- t_offset = t_offset - 360
346
- planet_x = sliceToX(0, (radius - rplanet), t_offset) + rplanet
347
- planet_y = sliceToY(0, (radius - rplanet), t_offset) + rplanet
348
- output += f'<g class="transit-planet-name" transform="translate(-6,-6)"><g transform="scale(0.5)"><use x="{planet_x*2}" y="{planet_y*2}" xlink:href="#{available_planets_setting[i]["name"]}" /></g></g>'
349
-
350
- # Transit planet line
351
- x1 = sliceToX(0, radius + 3, t_offset) - 3
352
- y1 = sliceToY(0, radius + 3, t_offset) - 3
353
- x2 = sliceToX(0, radius - 3, t_offset) + 3
354
- y2 = sliceToY(0, radius - 3, t_offset) + 3
355
- output += f'<line class="transit-planet-line" x1="{str(x1)}" y1="{str(y1)}" x2="{str(x2)}" y2="{str(y2)}" style="stroke: {available_planets_setting[i]["color"]}; stroke-width: 1px; stroke-opacity:.8;"/>'
356
-
357
- # transit planet degree text
358
- rotate = main_subject_first_house_degree_ut - t_points_deg_ut[i]
359
- textanchor = "end"
360
- t_offset += group_offset[i]
361
- rtext = -3.0
362
-
363
- if -90 > rotate > -270:
364
- rotate = rotate + 180.0
365
- textanchor = "start"
366
- if 270 > rotate > 90:
367
- rotate = rotate - 180.0
368
- textanchor = "start"
369
-
370
- if textanchor == "end":
371
- xo = 1
372
- else:
373
- xo = -1
374
- deg_x = sliceToX(0, (radius - rtext), t_offset + xo) + rtext
375
- deg_y = sliceToY(0, (radius - rtext), t_offset + xo) + rtext
376
- degree = int(t_offset)
377
- output += f'<g transform="translate({deg_x},{deg_y})">'
378
- output += f'<text transform="rotate({rotate})" text-anchor="{textanchor}'
379
- output += f'" style="fill: {available_planets_setting[i]["color"]}; font-size: 10px;">{convert_decimal_to_degree_string(t_points_deg[i], format_type="1")}'
380
- output += "</text></g>"
381
-
382
- # check transit
383
- if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
384
- dropin = 36
385
- else:
386
- dropin = 0
393
+ Args:
394
+ seventh_house_degree (Union[int, float]): Degree of the seventh house.
395
+ point_degree (Union[int, float]): Degree of the celestial point.
396
+ adjustment (Union[int, float]): Adjustment value to prevent overlapping.
397
+
398
+ Returns:
399
+ float: The calculated offset position.
400
+ """
401
+ return (int(seventh_house_degree) / -1) + int(point_degree + adjustment)
402
+
403
+
404
+ def _draw_external_natal_lines(
405
+ output: str,
406
+ radius: Union[int, float],
407
+ third_circle_radius: Union[int, float],
408
+ point_radius: Union[int, float],
409
+ true_offset: Union[int, float],
410
+ adjusted_offset: Union[int, float],
411
+ color: str,
412
+ ) -> str:
413
+ """
414
+ Draw connecting lines for external view charts.
415
+
416
+ Creates two line segments: one from the circle to the original position,
417
+ and another from the original position to the adjusted position.
418
+
419
+ Args:
420
+ output (str): The SVG output string to append to.
421
+ radius (Union[int, float]): Chart radius.
422
+ third_circle_radius (Union[int, float]): Radius of the third circle.
423
+ point_radius (Union[int, float]): Radius of the celestial point.
424
+ true_offset (Union[int, float]): True position offset.
425
+ adjusted_offset (Union[int, float]): Adjusted position offset.
426
+ color (str): Line color.
427
+
428
+ Returns:
429
+ str: Updated SVG output with added line elements.
430
+ """
431
+ # First line - from circle to outer position
432
+ x1 = sliceToX(0, radius - third_circle_radius, true_offset) + third_circle_radius
433
+ y1 = sliceToY(0, radius - third_circle_radius, true_offset) + third_circle_radius
434
+ x2 = sliceToX(0, radius - point_radius - 30, true_offset) + point_radius + 30
435
+ y2 = sliceToY(0, radius - point_radius - 30, true_offset) + point_radius + 30
436
+ output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke-width:1px;stroke:{color};stroke-opacity:.3;"/>\n'
437
+
438
+ # Second line - from outer position to adjusted position
439
+ x1 = x2
440
+ y1 = y2
441
+ x2 = sliceToX(0, radius - point_radius - 10, adjusted_offset) + point_radius + 10
442
+ y2 = sliceToY(0, radius - point_radius - 10, adjusted_offset) + point_radius + 10
443
+ output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke-width:1px;stroke:{color};stroke-opacity:.5;"/>\n'
444
+
445
+ return output
387
446
 
388
- # planet line
389
- x1 = sliceToX(0, radius - (dropin + 3), offset) + (dropin + 3)
390
- y1 = sliceToY(0, radius - (dropin + 3), offset) + (dropin + 3)
391
- x2 = sliceToX(0, (radius - (dropin - 3)), offset) + (dropin - 3)
392
- y2 = sliceToY(0, (radius - (dropin - 3)), offset) + (dropin - 3)
393
447
 
394
- output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {available_planets_setting[i]["color"]}; stroke-width: 2px; stroke-opacity:.6;"/>'
448
+ def _generate_point_svg(point_details: KerykeionPointModel, x: float, y: float, scale: float, point_name: str) -> str:
449
+ """
450
+ Generate the SVG element for a celestial point.
451
+
452
+ Args:
453
+ point_details (KerykeionPointModel): Details about the celestial point.
454
+ x (float): X-coordinate for the point.
455
+ y (float): Y-coordinate for the point.
456
+ scale (float): Scale factor for the point.
457
+ point_name (str): Name of the celestial point.
458
+
459
+ Returns:
460
+ str: SVG element for the celestial point.
461
+ """
462
+ svg = f'<g kr:node="ChartPoint" kr:house="{point_details["house"]}" kr:sign="{point_details["sign"]}" kr:absoluteposition="{point_details["abs_pos"]}" kr:signposition="{point_details["position"]}" '
463
+ svg += f'kr:slug="{point_details["name"]}" transform="translate(-{12 * scale},-{12 * scale}) scale({scale})">'
464
+ svg += f'<use x="{x * (1/scale)}" y="{y * (1/scale)}" xlink:href="#{point_name}" />'
465
+ svg += "</g>"
466
+ return svg
467
+
468
+
469
+ def _draw_secondary_points(
470
+ output: str,
471
+ radius: Union[int, float],
472
+ first_house_degree: Union[int, float],
473
+ seventh_house_degree: Union[int, float],
474
+ points_abs_positions: list[Union[int, float]],
475
+ points_rel_positions: list[Union[int, float]],
476
+ points_settings: list[KerykeionSettingsCelestialPointModel],
477
+ chart_type: str,
478
+ exclude_points: list[str],
479
+ main_offset: float,
480
+ ) -> str:
481
+ """
482
+ Draw secondary celestial points (transit/synastry/return) on the chart.
483
+
484
+ Args:
485
+ output (str): Current SVG output to append to.
486
+ radius (Union[int, float]): Chart radius.
487
+ first_house_degree (Union[int, float]): Degree of the first house.
488
+ seventh_house_degree (Union[int, float]): Degree of the seventh house.
489
+ points_abs_positions (list[Union[int, float]]): Absolute positions of points.
490
+ points_rel_positions (list[Union[int, float]]): Relative positions of points.
491
+ points_settings (list[KerykeionSettingsCelestialPointModel]): Settings for points.
492
+ chart_type (str): Type of chart.
493
+ exclude_points (list[str]): List of point names to exclude.
494
+ main_offset (float): Offset position for the main point.
395
495
 
396
- # check transit
397
- if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
398
- dropin = 160
496
+ Returns:
497
+ str: Updated SVG output with added secondary points.
498
+ """
499
+ # Initialize position adjustments for grouped points
500
+ position_adjustments: dict[int, float] = {i: 0.0 for i in range(len(points_settings))}
501
+
502
+ # Map absolute position to point index
503
+ position_index_map = {}
504
+ for i in range(len(points_settings)):
505
+ if chart_type == "Transit" and points_settings[i]["name"] in exclude_points:
506
+ continue
507
+ position_index_map[points_abs_positions[i]] = i
508
+
509
+ # Sort positions
510
+ sorted_positions = sorted(position_index_map.keys())
511
+
512
+ # Find groups of points that are close to each other
513
+ point_groups: List[List[int]] = []
514
+ in_group = False
515
+
516
+ for pos_idx, abs_position in enumerate(sorted_positions):
517
+ point_a_idx = position_index_map[abs_position]
518
+
519
+ # Get next point
520
+ if pos_idx == len(sorted_positions) - 1:
521
+ point_b_idx = position_index_map[sorted_positions[0]]
399
522
  else:
400
- dropin = 120
523
+ point_b_idx = position_index_map[sorted_positions[pos_idx + 1]]
524
+
525
+ # Check distance between points
526
+ position_a = points_abs_positions[point_a_idx]
527
+ position_b = points_abs_positions[point_b_idx]
528
+ distance = degreeDiff(position_a, position_b)
401
529
 
402
- x1 = sliceToX(0, radius - dropin, offset) + dropin
403
- y1 = sliceToY(0, radius - dropin, offset) + dropin
404
- x2 = sliceToX(0, (radius - (dropin - 3)), offset) + (dropin - 3)
405
- y2 = sliceToY(0, (radius - (dropin - 3)), offset) + (dropin - 3)
406
- output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {available_planets_setting[i]["color"]}; stroke-width: 2px; stroke-opacity:.6;"/>'
530
+ # Group points that are close
531
+ if distance <= 2.5:
532
+ if in_group:
533
+ point_groups[-1].append(point_b_idx)
534
+ else:
535
+ point_groups.append([point_a_idx])
536
+ point_groups[-1].append(point_b_idx)
537
+ in_group = True
538
+ else:
539
+ in_group = False
540
+
541
+ # Set position adjustments for grouped points
542
+ for group in point_groups:
543
+ if len(group) == 2:
544
+ position_adjustments[group[0]] = -1.0
545
+ position_adjustments[group[1]] = 1.0
546
+ elif len(group) == 3:
547
+ position_adjustments[group[0]] = -1.5
548
+ position_adjustments[group[1]] = 0.0
549
+ position_adjustments[group[2]] = 1.5
550
+ elif len(group) == 4:
551
+ position_adjustments[group[0]] = -2.0
552
+ position_adjustments[group[1]] = -1.0
553
+ position_adjustments[group[2]] = 1.0
554
+ position_adjustments[group[3]] = 2.0
555
+
556
+ # Draw each secondary point
557
+ alternate_position = False
558
+ point_idx = 0 # Initialize for use outside loop
559
+
560
+ for pos_idx, abs_position in enumerate(sorted_positions):
561
+ point_idx = position_index_map[abs_position]
562
+
563
+ if chart_type == "Transit" and points_settings[point_idx]["name"] in exclude_points:
564
+ continue
565
+
566
+ # Determine radius based on point type
567
+ if 22 < point_idx < 27: # Chart angles
568
+ point_radius = 9
569
+ elif alternate_position:
570
+ point_radius = 18
571
+ alternate_position = False
572
+ else:
573
+ point_radius = 26
574
+ alternate_position = True
575
+
576
+ # Calculate position
577
+ zero_point = 360 - seventh_house_degree
578
+ point_offset = zero_point + points_abs_positions[point_idx]
579
+ if point_offset > 360:
580
+ point_offset -= 360
581
+
582
+ # Draw point symbol
583
+ point_x = sliceToX(0, radius - point_radius, point_offset) + point_radius
584
+ point_y = sliceToY(0, radius - point_radius, point_offset) + point_radius
585
+ output += '<g class="transit-planet-name" transform="translate(-6,-6)"><g transform="scale(0.5)">'
586
+ output += f'<use x="{point_x*2}" y="{point_y*2}" xlink:href="#{points_settings[point_idx]["name"]}" /></g></g>'
587
+
588
+ # Draw connecting line
589
+ x1 = sliceToX(0, radius + 3, point_offset) - 3
590
+ y1 = sliceToY(0, radius + 3, point_offset) - 3
591
+ x2 = sliceToX(0, radius - 3, point_offset) + 3
592
+ y2 = sliceToY(0, radius - 3, point_offset) + 3
593
+ output += f'<line class="transit-planet-line" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
594
+ output += f'style="stroke: {points_settings[point_idx]["color"]}; stroke-width: 1px; stroke-opacity:.8;"/>'
595
+
596
+ # Draw degree text with rotation
597
+ rotation = first_house_degree - points_abs_positions[point_idx]
598
+ text_anchor = "end"
599
+
600
+ # Adjust text rotation and anchor for readability
601
+ if -90 > rotation > -270:
602
+ rotation += 180.0
603
+ text_anchor = "start"
604
+ if 270 > rotation > 90:
605
+ rotation -= 180.0
606
+ text_anchor = "start"
607
+
608
+ # Position the degree text
609
+ x_offset = 1 if text_anchor == "end" else -1
610
+ adjusted_point_offset = point_offset + position_adjustments[point_idx]
611
+ text_radius = -3.0
612
+
613
+ deg_x = sliceToX(0, radius - text_radius, adjusted_point_offset + x_offset) + text_radius
614
+ deg_y = sliceToY(0, radius - text_radius, adjusted_point_offset + x_offset) + text_radius
615
+
616
+ # Format and output the degree text
617
+ degree_text = convert_decimal_to_degree_string(points_rel_positions[point_idx], format_type="1")
618
+ output += f'<g transform="translate({deg_x},{deg_y})">'
619
+ output += f'<text transform="rotate({rotation})" text-anchor="{text_anchor}" '
620
+ output += f'style="fill: {points_settings[point_idx]["color"]}; font-size: 10px;">{degree_text}</text></g>'
621
+
622
+ # Draw connecting lines for the main point
623
+ dropin = 0
624
+ if chart_type == "Transit":
625
+ dropin = 36
626
+ elif chart_type == "Synastry":
627
+ dropin = 36
628
+ elif chart_type == "Return":
629
+ dropin = 36
630
+
631
+ # First connecting line segment
632
+ x1 = sliceToX(0, radius - (dropin + 3), main_offset) + (dropin + 3)
633
+ y1 = sliceToY(0, radius - (dropin + 3), main_offset) + (dropin + 3)
634
+ x2 = sliceToX(0, radius - (dropin - 3), main_offset) + (dropin - 3)
635
+ y2 = sliceToY(0, radius - (dropin - 3), main_offset) + (dropin - 3)
636
+
637
+ point_color = points_settings[point_idx]["color"]
638
+ output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
639
+ output += f'style="stroke: {point_color}; stroke-width: 2px; stroke-opacity:.6;"/>'
640
+
641
+ # Second connecting line segment
642
+ dropin = 120
643
+ if chart_type == "Transit":
644
+ dropin = 160
645
+ elif chart_type == "Synastry":
646
+ dropin = 160
647
+ elif chart_type == "Return":
648
+ dropin = 160
649
+
650
+ x1 = sliceToX(0, radius - dropin, main_offset) + dropin
651
+ y1 = sliceToY(0, radius - dropin, main_offset) + dropin
652
+ x2 = sliceToX(0, radius - (dropin - 3), main_offset) + (dropin - 3)
653
+ y2 = sliceToY(0, radius - (dropin - 3), main_offset) + (dropin - 3)
654
+
655
+ output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
656
+ output += f'style="stroke: {point_color}; stroke-width: 2px; stroke-opacity:.6;"/>'
407
657
 
408
658
  return output