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