kerykeion 4.14.4__py3-none-any.whl → 4.14.5__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.

@@ -2,10 +2,41 @@ import math
2
2
  import datetime
3
3
  from kerykeion.kr_types import KerykeionException, ChartType
4
4
  from typing import Union
5
- from kerykeion.kr_types.kr_models import AspectModel
5
+ from kerykeion.kr_types.kr_models import AspectModel, KerykeionPointModel
6
6
  from kerykeion.kr_types.settings_models import KerykeionLanguageCelestialPointModel, KerykeionSettingsAspectModel
7
7
 
8
8
 
9
+ def get_decoded_kerykeion_celestial_point_name(input_planet_name: str, celestial_point_language: KerykeionLanguageCelestialPointModel) -> str:
10
+ """
11
+ Decode the given celestial point name based on the provided language model.
12
+
13
+ Args:
14
+ input_planet_name (str): The name of the celestial point to decode.
15
+ celestial_point_language (KerykeionLanguageCelestialPointModel): The language model containing celestial point names.
16
+
17
+ Returns:
18
+ str: The decoded celestial point name.
19
+ """
20
+
21
+ # Dictionary for special house names
22
+ special_house_names = {
23
+ "First_House": "Asc",
24
+ "Seventh_House": "Dsc",
25
+ "Tenth_House": "Mc",
26
+ "Fourth_House": "Ic"
27
+ }
28
+
29
+ # Get the language model keys
30
+ language_keys = celestial_point_language.model_dump().keys()
31
+
32
+ # Check if the input planet name exists in the language model
33
+ if input_planet_name in language_keys:
34
+ return celestial_point_language[input_planet_name]
35
+
36
+ # Return the special house name if it exists, otherwise return an empty string
37
+ return special_house_names.get(input_planet_name, "")
38
+
39
+
9
40
  def decHourJoin(inH: int, inM: int, inS: int) -> float:
10
41
  """Join hour, minutes, seconds, timezone integer to hour float.
11
42
 
@@ -132,10 +163,10 @@ def draw_zodiac_slice(
132
163
  - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
133
164
  - num (int): The number of the sign. Note: In OpenAstro it did refer to self.zodiac,
134
165
  which is a list of the signs in order, starting with Aries. Eg:
135
- {"name": "aries", "element": "fire"}
166
+ {"name": "Ari", "element": "fire"}
136
167
  - r (Union[int, float]): The value of r.
137
168
  - style (str): The CSS inline style.
138
- - type (str): The type ?. In OpenAstro, it was the symbol of the sign. Eg: "aries".
169
+ - type (str): The type ?. In OpenAstro, it was the symbol of the sign. Eg: "Ari".
139
170
  self.zodiac[i]["name"]
140
171
 
141
172
  Returns:
@@ -807,3 +838,164 @@ def draw_moon_phase(
807
838
  f' <circle cx="20" cy="10" r="10" style="fill: none; stroke: {lunar_phase_outline_color}; stroke-width: 0.5px; stroke-opacity: 0.5" />'
808
839
  f"</g>"
809
840
  )
841
+
842
+
843
+ def draw_house_grid(
844
+ main_subject_houses_list: list[KerykeionPointModel],
845
+ chart_type: ChartType,
846
+ secondary_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
847
+ text_color: str = "#000000",
848
+ house_cusp_generale_name_label: str = "Cusp",
849
+ ) -> str:
850
+ """
851
+ Generate SVG code for a grid of astrological houses.
852
+
853
+ Parameters:
854
+ - main_houses (list[KerykeionPointModel]): List of houses for the main subject.
855
+ - chart_type (ChartType): Type of the chart (e.g., Synastry, Transit).
856
+ - secondary_houses (list[KerykeionPointModel], optional): List of houses for the secondary subject.
857
+ - text_color (str): Color of the text.
858
+ - cusp_label (str): Label for the house cusp.
859
+
860
+ Returns:
861
+ - str: The SVG code for the grid of houses.
862
+ """
863
+
864
+ if chart_type in ["Synastry", "Transit"] and secondary_subject_houses_list is None:
865
+ raise KerykeionException("secondary_houses is None")
866
+
867
+ svg_output = '<g transform="translate(610,-20)">'
868
+
869
+ line_increment = 10
870
+ for i, house in enumerate(main_subject_houses_list):
871
+ cusp_number = f"&#160;&#160;{i + 1}" if i < 9 else str(i + 1)
872
+ svg_output += (
873
+ f'<g transform="translate(0,{line_increment})">'
874
+ f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
875
+ f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
876
+ f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
877
+ f'</g>'
878
+ )
879
+ line_increment += 14
880
+
881
+ svg_output += "</g>"
882
+
883
+ if chart_type == "Synastry":
884
+ svg_output += '<!-- Synastry Houses -->'
885
+ svg_output += '<g transform="translate(850, -20)">'
886
+ line_increment = 10
887
+
888
+ for i, house in enumerate(secondary_subject_houses_list):
889
+ cusp_number = f"&#160;&#160;{i + 1}" if i < 9 else str(i + 1)
890
+ svg_output += (
891
+ f'<g transform="translate(0,{line_increment})">'
892
+ f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
893
+ f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
894
+ f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
895
+ f'</g>'
896
+ )
897
+ line_increment += 14
898
+
899
+ svg_output += "</g>"
900
+
901
+ return svg_output
902
+
903
+
904
+ def draw_planet_grid(
905
+ planets_and_houses_grid_title: str,
906
+ subject_name: str,
907
+ available_kerykeion_celestial_points: list[KerykeionPointModel],
908
+ chart_type: ChartType,
909
+ celestial_point_language: KerykeionLanguageCelestialPointModel,
910
+ second_subject_name: str = None,
911
+ second_subject_available_kerykeion_celestial_points: list[KerykeionPointModel] = None,
912
+ text_color: str = "#000000",
913
+ ) -> str:
914
+ """
915
+ Draws the planet grid for the given celestial points and chart type.
916
+
917
+ Args:
918
+ planets_and_houses_grid_title (str): Title of the grid.
919
+ subject_name (str): Name of the subject.
920
+ available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the subject.
921
+ chart_type (ChartType): Type of the chart.
922
+ celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points.
923
+ second_subject_name (str, optional): Name of the second subject. Defaults to None.
924
+ second_subject_available_kerykeion_celestial_points (list[KerykeionPointModel], optional): List of celestial points for the second subject. Defaults to None.
925
+ text_color (str, optional): Color of the text. Defaults to "#000000".
926
+
927
+ Returns:
928
+ str: The SVG output for the planet grid.
929
+ """
930
+ line_height = 10
931
+ offset = 0
932
+ offset_between_lines = 14
933
+
934
+ svg_output = (
935
+ f'<g transform="translate(510,-20)">'
936
+ f'<g transform="translate(140, -15)">'
937
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}:</text>'
938
+ f'</g>'
939
+ )
940
+
941
+ end_of_line = "</g>"
942
+
943
+ for i, planet in enumerate(available_kerykeion_celestial_points):
944
+ if i == 27:
945
+ line_height = 10
946
+ offset = -120
947
+
948
+ decoded_name = get_decoded_kerykeion_celestial_point_name(planet["name"], celestial_point_language)
949
+ svg_output += (
950
+ f'<g transform="translate({offset},{line_height})">'
951
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{decoded_name}</text>'
952
+ f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{planet["name"]}" /></g>'
953
+ f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(planet["position"])}</text>'
954
+ f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{planet["sign"]}" /></g>'
955
+ )
956
+
957
+ if planet["retrograde"]:
958
+ svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
959
+
960
+ svg_output += end_of_line
961
+ line_height += offset_between_lines
962
+
963
+ if chart_type in ["Transit", "Synastry"]:
964
+ if chart_type == "Transit":
965
+ svg_output += (
966
+ f'<g transform="translate(320, -15)">'
967
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{second_subject_name}:</text>'
968
+ )
969
+ else:
970
+ svg_output += (
971
+ f'<g transform="translate(380, -15)">'
972
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}:</text>'
973
+ )
974
+
975
+ svg_output += end_of_line
976
+
977
+ second_line_height = 10
978
+ second_offset = 250
979
+
980
+ for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
981
+ if i == 27:
982
+ second_line_height = 10
983
+ second_offset = -120
984
+
985
+ second_decoded_name = get_decoded_kerykeion_celestial_point_name(t_planet["name"], celestial_point_language)
986
+ svg_output += (
987
+ f'<g transform="translate({second_offset},{second_line_height})">'
988
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{second_decoded_name}</text>'
989
+ f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
990
+ f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(t_planet["position"])}</text>'
991
+ f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{t_planet["sign"]}" /></g>'
992
+ )
993
+
994
+ if t_planet["retrograde"]:
995
+ svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
996
+
997
+ svg_output += end_of_line
998
+ second_line_height += offset_between_lines
999
+
1000
+ svg_output += end_of_line
1001
+ return svg_output
@@ -0,0 +1,406 @@
1
+ from kerykeion.charts.charts_utils import degreeDiff, sliceToX, sliceToY, convert_decimal_to_degree_string
2
+ from kerykeion.kr_types import KerykeionException, ChartType, KerykeionPointModel
3
+ from kerykeion.kr_types.settings_models import KerykeionSettingsCelestialPointModel
4
+ from kerykeion.kr_types.kr_literals import Houses
5
+ import logging
6
+ from typing import Union, get_args
7
+
8
+
9
+
10
+
11
+ def draw_planets(
12
+ radius: Union[int, float],
13
+ available_kerykeion_celestial_points: list[KerykeionPointModel],
14
+ available_planets_setting: list[KerykeionSettingsCelestialPointModel],
15
+ third_circle_radius: Union[int, float],
16
+ main_subject_first_house_degree_ut: Union[int, float],
17
+ main_subject_seventh_house_degree_ut: Union[int, float],
18
+ chart_type: ChartType,
19
+ second_subject_available_kerykeion_celestial_points: Union[list[KerykeionPointModel], None] = None,
20
+ ):
21
+ """
22
+ Draws the planets on a chart based on the provided parameters.
23
+
24
+ Args:
25
+ radius (int): The radius of the chart.
26
+ available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the main subject.
27
+ available_planets_setting (list[KerykeionSettingsCelestialPointModel]): Settings for the celestial points.
28
+ third_circle_radius (Union[int, float]): Radius of the third circle.
29
+ main_subject_first_house_degree_ut (Union[int, float]): Degree of the first house for the main subject.
30
+ main_subject_seventh_house_degree_ut (Union[int, float]): Degree of the seventh house for the main subject.
31
+ chart_type (ChartType): Type of the chart (e.g., "Transit", "Synastry").
32
+ second_subject_available_kerykeion_celestial_points (Union[list[KerykeionPointModel], None], optional):
33
+ List of celestial points for the second subject, required for "Transit" or "Synastry" charts. Defaults to None.
34
+
35
+ Raises:
36
+ KerykeionException: If the second subject is required but not provided.
37
+
38
+ Returns:
39
+ str: SVG output for the chart with the planets drawn.
40
+ """
41
+ TRANSIT_RING_EXCLUDE_POINTS_NAMES = get_args(Houses)
42
+
43
+ if chart_type == "Transit" or chart_type == "Synastry":
44
+ if second_subject_available_kerykeion_celestial_points is None:
45
+ raise KerykeionException("Second subject is required for Transit or Synastry charts")
46
+
47
+ # Make a list for the absolute degrees of the points of the graphic.
48
+ points_deg_ut = []
49
+ for planet in available_kerykeion_celestial_points:
50
+ points_deg_ut.append(planet.abs_pos)
51
+
52
+ # Make a list of the relative degrees of the points in the graphic.
53
+ points_deg = []
54
+ for planet in available_kerykeion_celestial_points:
55
+ points_deg.append(planet.position)
56
+
57
+ if chart_type == "Transit" or chart_type == "Synastry":
58
+ # Make a list for the absolute degrees of the points of the graphic.
59
+ t_points_deg_ut = []
60
+ for planet in second_subject_available_kerykeion_celestial_points:
61
+ t_points_deg_ut.append(planet.abs_pos)
62
+
63
+ # Make a list of the relative degrees of the points in the graphic.
64
+ t_points_deg = []
65
+ for planet in second_subject_available_kerykeion_celestial_points:
66
+ t_points_deg.append(planet.position)
67
+
68
+ planets_degut = {}
69
+ diff = range(len(available_planets_setting))
70
+
71
+ for i in range(len(available_planets_setting)):
72
+ # list of planets sorted by degree
73
+ logging.debug(f"planet: {i}, degree: {points_deg_ut[i]}")
74
+ planets_degut[points_deg_ut[i]] = i
75
+
76
+ """
77
+ FIXME: The planets_degut is a dictionary like:
78
+ {planet_degree: planet_index}
79
+ It should be replaced bu points_deg_ut
80
+ print(points_deg_ut)
81
+ print(planets_degut)
82
+ """
83
+
84
+ output = ""
85
+ keys = list(planets_degut.keys())
86
+ keys.sort()
87
+ switch = 0
88
+
89
+ planets_degrouped = {}
90
+ groups = []
91
+ planets_by_pos = list(range(len(planets_degut)))
92
+ planet_drange = 3.4
93
+ # get groups closely together
94
+ group_open = False
95
+ for e in range(len(keys)):
96
+ i = planets_degut[keys[e]]
97
+ # get distances between planets
98
+ if e == 0:
99
+ prev = points_deg_ut[planets_degut[keys[-1]]]
100
+ next = points_deg_ut[planets_degut[keys[1]]]
101
+ elif e == (len(keys) - 1):
102
+ prev = points_deg_ut[planets_degut[keys[e - 1]]]
103
+ next = points_deg_ut[planets_degut[keys[0]]]
104
+ else:
105
+ prev = points_deg_ut[planets_degut[keys[e - 1]]]
106
+ next = points_deg_ut[planets_degut[keys[e + 1]]]
107
+ diffa = degreeDiff(prev, points_deg_ut[i])
108
+ diffb = degreeDiff(next, points_deg_ut[i])
109
+ planets_by_pos[e] = [i, diffa, diffb]
110
+
111
+ logging.debug(f'{available_planets_setting[i]["label"]}, {diffa}, {diffb}')
112
+
113
+ if diffb < planet_drange:
114
+ if group_open:
115
+ groups[-1].append([e, diffa, diffb, available_planets_setting[planets_degut[keys[e]]]["label"]])
116
+ else:
117
+ group_open = True
118
+ groups.append([])
119
+ groups[-1].append([e, diffa, diffb, available_planets_setting[planets_degut[keys[e]]]["label"]])
120
+ else:
121
+ if group_open:
122
+ groups[-1].append([e, diffa, diffb, available_planets_setting[planets_degut[keys[e]]]["label"]])
123
+ group_open = False
124
+
125
+ def zero(x):
126
+ return 0
127
+
128
+ planets_delta = list(map(zero, range(len(available_planets_setting))))
129
+
130
+ # print groups
131
+ # print planets_by_pos
132
+ for a in range(len(groups)):
133
+ # Two grouped planets
134
+ if len(groups[a]) == 2:
135
+ next_to_a = groups[a][0][0] - 1
136
+ if groups[a][1][0] == (len(planets_by_pos) - 1):
137
+ next_to_b = 0
138
+ else:
139
+ next_to_b = groups[a][1][0] + 1
140
+ # if both planets have room
141
+ if (groups[a][0][1] > (2 * planet_drange)) & (groups[a][1][2] > (2 * planet_drange)):
142
+ planets_delta[groups[a][0][0]] = -(planet_drange - groups[a][0][2]) / 2
143
+ planets_delta[groups[a][1][0]] = +(planet_drange - groups[a][0][2]) / 2
144
+ # if planet a has room
145
+ elif groups[a][0][1] > (2 * planet_drange):
146
+ planets_delta[groups[a][0][0]] = -planet_drange
147
+ # if planet b has room
148
+ elif groups[a][1][2] > (2 * planet_drange):
149
+ planets_delta[groups[a][1][0]] = +planet_drange
150
+
151
+ # if planets next to a and b have room move them
152
+ elif (planets_by_pos[next_to_a][1] > (2.4 * planet_drange)) & (
153
+ planets_by_pos[next_to_b][2] > (2.4 * planet_drange)
154
+ ):
155
+ planets_delta[(next_to_a)] = groups[a][0][1] - planet_drange * 2
156
+ planets_delta[groups[a][0][0]] = -planet_drange * 0.5
157
+ planets_delta[next_to_b] = -(groups[a][1][2] - planet_drange * 2)
158
+ planets_delta[groups[a][1][0]] = +planet_drange * 0.5
159
+
160
+ # if planet next to a has room move them
161
+ elif planets_by_pos[next_to_a][1] > (2 * planet_drange):
162
+ planets_delta[(next_to_a)] = groups[a][0][1] - planet_drange * 2.5
163
+ planets_delta[groups[a][0][0]] = -planet_drange * 1.2
164
+
165
+ # if planet next to b has room move them
166
+ elif planets_by_pos[next_to_b][2] > (2 * planet_drange):
167
+ planets_delta[next_to_b] = -(groups[a][1][2] - planet_drange * 2.5)
168
+ planets_delta[groups[a][1][0]] = +planet_drange * 1.2
169
+
170
+ # Three grouped planets or more
171
+ xl = len(groups[a])
172
+ if xl >= 3:
173
+ available = groups[a][0][1]
174
+ for f in range(xl):
175
+ available += groups[a][f][2]
176
+ need = (3 * planet_drange) + (1.2 * (xl - 1) * planet_drange)
177
+ leftover = available - need
178
+ xa = groups[a][0][1]
179
+ xb = groups[a][(xl - 1)][2]
180
+
181
+ # center
182
+ if (xa > (need * 0.5)) & (xb > (need * 0.5)):
183
+ startA = xa - (need * 0.5)
184
+ # position relative to next planets
185
+ else:
186
+ startA = (leftover / (xa + xb)) * xa
187
+ startB = (leftover / (xa + xb)) * xb
188
+
189
+ if available > need:
190
+ planets_delta[groups[a][0][0]] = startA - groups[a][0][1] + (1.5 * planet_drange)
191
+ for f in range(xl - 1):
192
+ planets_delta[groups[a][(f + 1)][0]] = (
193
+ 1.2 * planet_drange + planets_delta[groups[a][f][0]] - groups[a][f][2]
194
+ )
195
+
196
+ for e in range(len(keys)):
197
+ i = planets_degut[keys[e]]
198
+
199
+ # coordinates
200
+ if chart_type == "Transit" or chart_type == "Synastry":
201
+ if 22 < i < 27:
202
+ rplanet = 76
203
+ elif switch == 1:
204
+ rplanet = 110
205
+ switch = 0
206
+ else:
207
+ rplanet = 130
208
+ switch = 1
209
+ else:
210
+ # if 22 < i < 27 it is asc,mc,dsc,ic (angles of chart)
211
+ # put on special line (rplanet is range from outer ring)
212
+ amin, bmin, cmin = 0, 0, 0
213
+ if chart_type == "ExternalNatal":
214
+ amin = 74 - 10
215
+ bmin = 94 - 10
216
+ cmin = 40 - 10
217
+
218
+ if 22 < i < 27:
219
+ rplanet = 40 - cmin
220
+ elif switch == 1:
221
+ rplanet = 74 - amin
222
+ switch = 0
223
+ else:
224
+ rplanet = 94 - bmin
225
+ switch = 1
226
+
227
+ rtext = 45
228
+
229
+ offset = (int(main_subject_seventh_house_degree_ut) / -1) + int(points_deg_ut[i] + planets_delta[e])
230
+ trueoffset = (int(main_subject_seventh_house_degree_ut) / -1) + int(points_deg_ut[i])
231
+
232
+ planet_x = sliceToX(0, (radius - rplanet), offset) + rplanet
233
+ planet_y = sliceToY(0, (radius - rplanet), offset) + rplanet
234
+ if chart_type == "Transit" or chart_type == "Synastry":
235
+ scale = 0.8
236
+
237
+ elif chart_type == "ExternalNatal":
238
+ scale = 0.8
239
+ # line1
240
+ x1 = sliceToX(0, (radius - third_circle_radius), trueoffset) + third_circle_radius
241
+ y1 = sliceToY(0, (radius - third_circle_radius), trueoffset) + third_circle_radius
242
+ x2 = sliceToX(0, (radius - rplanet - 30), trueoffset) + rplanet + 30
243
+ y2 = sliceToY(0, (radius - rplanet - 30), trueoffset) + rplanet + 30
244
+ color = available_planets_setting[i]["color"]
245
+ output += (
246
+ '<line x1="%s" y1="%s" x2="%s" y2="%s" style="stroke-width:1px;stroke:%s;stroke-opacity:.3;"/>\n'
247
+ % (x1, y1, x2, y2, color)
248
+ )
249
+ # line2
250
+ x1 = sliceToX(0, (radius - rplanet - 30), trueoffset) + rplanet + 30
251
+ y1 = sliceToY(0, (radius - rplanet - 30), trueoffset) + rplanet + 30
252
+ x2 = sliceToX(0, (radius - rplanet - 10), offset) + rplanet + 10
253
+ y2 = sliceToY(0, (radius - rplanet - 10), offset) + rplanet + 10
254
+ output += (
255
+ '<line x1="%s" y1="%s" x2="%s" y2="%s" style="stroke-width:1px;stroke:%s;stroke-opacity:.5;"/>\n'
256
+ % (x1, y1, x2, y2, color)
257
+ )
258
+
259
+ else:
260
+ scale = 1
261
+
262
+ planet_details = available_kerykeion_celestial_points[i]
263
+
264
+ 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})">'
265
+ output += f'<use x="{planet_x * (1/scale)}" y="{planet_y * (1/scale)}" xlink:href="#{available_planets_setting[i]["name"]}" />'
266
+ output += f"</g>"
267
+
268
+ # make transit degut and display planets
269
+ if chart_type == "Transit" or chart_type == "Synastry":
270
+ group_offset = {}
271
+ t_planets_degut = {}
272
+ list_range = len(available_planets_setting)
273
+
274
+ for i in range(list_range):
275
+ if chart_type == "Transit" and available_planets_setting[i]["name"] in TRANSIT_RING_EXCLUDE_POINTS_NAMES:
276
+ continue
277
+
278
+ group_offset[i] = 0
279
+ t_planets_degut[t_points_deg_ut[i]] = i
280
+
281
+ t_keys = list(t_planets_degut.keys())
282
+ t_keys.sort()
283
+
284
+ # grab closely grouped planets
285
+ groups = []
286
+ in_group = False
287
+ for e in range(len(t_keys)):
288
+ i_a = t_planets_degut[t_keys[e]]
289
+ if e == (len(t_keys) - 1):
290
+ i_b = t_planets_degut[t_keys[0]]
291
+ else:
292
+ i_b = t_planets_degut[t_keys[e + 1]]
293
+
294
+ a = t_points_deg_ut[i_a]
295
+ b = t_points_deg_ut[i_b]
296
+ diff = degreeDiff(a, b)
297
+ if diff <= 2.5:
298
+ if in_group:
299
+ groups[-1].append(i_b)
300
+ else:
301
+ groups.append([i_a])
302
+ groups[-1].append(i_b)
303
+ in_group = True
304
+ else:
305
+ in_group = False
306
+ # loop groups and set degrees display adjustment
307
+ for i in range(len(groups)):
308
+ if len(groups[i]) == 2:
309
+ group_offset[groups[i][0]] = -1.0
310
+ group_offset[groups[i][1]] = 1.0
311
+ elif len(groups[i]) == 3:
312
+ group_offset[groups[i][0]] = -1.5
313
+ group_offset[groups[i][1]] = 0
314
+ group_offset[groups[i][2]] = 1.5
315
+ elif len(groups[i]) == 4:
316
+ group_offset[groups[i][0]] = -2.0
317
+ group_offset[groups[i][1]] = -1.0
318
+ group_offset[groups[i][2]] = 1.0
319
+ group_offset[groups[i][3]] = 2.0
320
+
321
+ switch = 0
322
+
323
+ # Transit planets loop
324
+ for e in range(len(t_keys)):
325
+ if chart_type == "Transit" and available_planets_setting[e]["name"] in TRANSIT_RING_EXCLUDE_POINTS_NAMES:
326
+ continue
327
+
328
+ i = t_planets_degut[t_keys[e]]
329
+
330
+ if 22 < i < 27:
331
+ rplanet = 9
332
+ elif switch == 1:
333
+ rplanet = 18
334
+ switch = 0
335
+ else:
336
+ rplanet = 26
337
+ switch = 1
338
+
339
+ # Transit planet name
340
+ zeropoint = 360 - main_subject_seventh_house_degree_ut
341
+ t_offset = zeropoint + t_points_deg_ut[i]
342
+ if t_offset > 360:
343
+ t_offset = t_offset - 360
344
+ planet_x = sliceToX(0, (radius - rplanet), t_offset) + rplanet
345
+ planet_y = sliceToY(0, (radius - rplanet), t_offset) + rplanet
346
+ 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>'
347
+
348
+ # Transit planet line
349
+ x1 = sliceToX(0, radius + 3, t_offset) - 3
350
+ y1 = sliceToY(0, radius + 3, t_offset) - 3
351
+ x2 = sliceToX(0, radius - 3, t_offset) + 3
352
+ y2 = sliceToY(0, radius - 3, t_offset) + 3
353
+ 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;"/>'
354
+
355
+ # transit planet degree text
356
+ rotate = main_subject_first_house_degree_ut - t_points_deg_ut[i]
357
+ textanchor = "end"
358
+ t_offset += group_offset[i]
359
+ rtext = -3.0
360
+
361
+ if -90 > rotate > -270:
362
+ rotate = rotate + 180.0
363
+ textanchor = "start"
364
+ if 270 > rotate > 90:
365
+ rotate = rotate - 180.0
366
+ textanchor = "start"
367
+
368
+ if textanchor == "end":
369
+ xo = 1
370
+ else:
371
+ xo = -1
372
+ deg_x = sliceToX(0, (radius - rtext), t_offset + xo) + rtext
373
+ deg_y = sliceToY(0, (radius - rtext), t_offset + xo) + rtext
374
+ degree = int(t_offset)
375
+ output += f'<g transform="translate({deg_x},{deg_y})">'
376
+ output += f'<text transform="rotate({rotate})" text-anchor="{textanchor}'
377
+ output += f'" style="fill: {available_planets_setting[i]["color"]}; font-size: 10px;">{convert_decimal_to_degree_string(t_points_deg[i], type="1")}'
378
+ output += "</text></g>"
379
+
380
+ # check transit
381
+ if chart_type == "Transit" or chart_type == "Synastry":
382
+ dropin = 36
383
+ else:
384
+ dropin = 0
385
+
386
+ # planet line
387
+ x1 = sliceToX(0, radius - (dropin + 3), offset) + (dropin + 3)
388
+ y1 = sliceToY(0, radius - (dropin + 3), offset) + (dropin + 3)
389
+ x2 = sliceToX(0, (radius - (dropin - 3)), offset) + (dropin - 3)
390
+ y2 = sliceToY(0, (radius - (dropin - 3)), offset) + (dropin - 3)
391
+
392
+ output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {available_planets_setting[i]["color"]}; stroke-width: 2px; stroke-opacity:.6;"/>'
393
+
394
+ # check transit
395
+ if chart_type == "Transit" or chart_type == "Synastry":
396
+ dropin = 160
397
+ else:
398
+ dropin = 120
399
+
400
+ x1 = sliceToX(0, radius - dropin, offset) + dropin
401
+ y1 = sliceToY(0, radius - dropin, offset) + dropin
402
+ x2 = sliceToX(0, (radius - (dropin - 3)), offset) + (dropin - 3)
403
+ y2 = sliceToY(0, (radius - (dropin - 3)), offset) + (dropin - 3)
404
+ output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {available_planets_setting[i]["color"]}; stroke-width: 2px; stroke-opacity:.6;"/>'
405
+
406
+ return output