kerykeion.charts.draw_planets

  1# type: ignore
  2
  3from kerykeion.charts.charts_utils import degreeDiff, sliceToX, sliceToY, convert_decimal_to_degree_string
  4from kerykeion.kr_types import KerykeionException, ChartType, KerykeionPointModel
  5from kerykeion.kr_types.settings_models import KerykeionSettingsCelestialPointModel
  6from kerykeion.kr_types.kr_literals import Houses
  7import logging
  8from typing import Union, get_args
  9
 10
 11
 12def draw_planets(
 13    radius: Union[int, float],
 14    available_kerykeion_celestial_points: list[KerykeionPointModel],
 15    available_planets_setting: list[KerykeionSettingsCelestialPointModel],
 16    third_circle_radius: Union[int, float],
 17    main_subject_first_house_degree_ut: Union[int, float],
 18    main_subject_seventh_house_degree_ut: Union[int, float],
 19    chart_type: ChartType,
 20    second_subject_available_kerykeion_celestial_points: Union[list[KerykeionPointModel], None] = None,
 21):
 22    """
 23    Draws the planets on a chart based on the provided parameters.
 24
 25    Args:
 26        radius (int): The radius of the chart.
 27        available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the main subject.
 28        available_planets_setting (list[KerykeionSettingsCelestialPointModel]): Settings for the celestial points.
 29        third_circle_radius (Union[int, float]): Radius of the third circle.
 30        main_subject_first_house_degree_ut (Union[int, float]): Degree of the first house for the main subject.
 31        main_subject_seventh_house_degree_ut (Union[int, float]): Degree of the seventh house for the main subject.
 32        chart_type (ChartType): Type of the chart (e.g., "Transit", "Synastry").
 33        second_subject_available_kerykeion_celestial_points (Union[list[KerykeionPointModel], None], optional): 
 34            List of celestial points for the second subject, required for "Transit" or "Synastry" charts. Defaults to None.
 35
 36    Raises:
 37        KerykeionException: If the second subject is required but not provided.
 38
 39    Returns:
 40        str: SVG output for the chart with the planets drawn.
 41    """
 42    TRANSIT_RING_EXCLUDE_POINTS_NAMES = get_args(Houses)
 43
 44    if chart_type == "Transit" or chart_type == "Synastry":
 45        if second_subject_available_kerykeion_celestial_points is None:
 46            raise KerykeionException("Second subject is required for Transit or Synastry charts")
 47
 48    # Make a list for the absolute degrees of the points of the graphic.
 49    points_deg_ut = []
 50    for planet in available_kerykeion_celestial_points:
 51        points_deg_ut.append(planet.abs_pos)
 52
 53    # Make a list of the relative degrees of the points in the graphic.
 54    points_deg = []
 55    for planet in available_kerykeion_celestial_points:
 56        points_deg.append(planet.position)
 57
 58    if chart_type == "Transit" or chart_type == "Synastry":
 59        # Make a list for the absolute degrees of the points of the graphic.
 60        t_points_deg_ut = []
 61        for planet in second_subject_available_kerykeion_celestial_points:
 62            t_points_deg_ut.append(planet.abs_pos)
 63
 64        # Make a list of the relative degrees of the points in the graphic.
 65        t_points_deg = []
 66        for planet in second_subject_available_kerykeion_celestial_points:
 67            t_points_deg.append(planet.position)
 68
 69    planets_degut = {}
 70    diff = range(len(available_planets_setting))
 71
 72    for i in range(len(available_planets_setting)):
 73        # list of planets sorted by degree
 74        logging.debug(f"planet: {i}, degree: {points_deg_ut[i]}")
 75        planets_degut[points_deg_ut[i]] = i
 76
 77    """
 78    FIXME: The planets_degut is a dictionary like:
 79    {planet_degree: planet_index}
 80    It should be replaced bu points_deg_ut
 81    print(points_deg_ut)
 82    print(planets_degut)
 83    """
 84
 85    output = ""
 86    keys = list(planets_degut.keys())
 87    keys.sort()
 88    switch = 0
 89
 90    planets_degrouped = {}
 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]
111
112        logging.debug(f'{available_planets_setting[i]["label"]}, {diffa}, {diffb}')
113
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"]])
121        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                startB = (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":
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
210        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
227
228        rtext = 45
229
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])
232
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":
236            scale = 0.8
237
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            )
259
260        else:
261            scale = 1
262
263        planet_details = available_kerykeion_celestial_points[i]
264
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 += f"</g>"
268
269    # make transit degut and display planets
270    if chart_type == "Transit" or chart_type == "Synastry":
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            degree = 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":
383            dropin = 36
384        else:
385            dropin = 0
386
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)
392
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
395        # check transit
396        if chart_type == "Transit" or chart_type == "Synastry":
397            dropin = 160
398        else:
399            dropin = 120
400
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;"/>'
406
407    return output
def draw_planets( radius: Union[int, float], available_kerykeion_celestial_points: list[kerykeion.kr_types.kr_models.KerykeionPointModel], available_planets_setting: list[kerykeion.kr_types.settings_models.KerykeionSettingsCelestialPointModel], third_circle_radius: Union[int, float], main_subject_first_house_degree_ut: Union[int, float], main_subject_seventh_house_degree_ut: Union[int, float], chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit', 'Composite'], second_subject_available_kerykeion_celestial_points: Optional[list[kerykeion.kr_types.kr_models.KerykeionPointModel]] = None):
 13def draw_planets(
 14    radius: Union[int, float],
 15    available_kerykeion_celestial_points: list[KerykeionPointModel],
 16    available_planets_setting: list[KerykeionSettingsCelestialPointModel],
 17    third_circle_radius: Union[int, float],
 18    main_subject_first_house_degree_ut: Union[int, float],
 19    main_subject_seventh_house_degree_ut: Union[int, float],
 20    chart_type: ChartType,
 21    second_subject_available_kerykeion_celestial_points: Union[list[KerykeionPointModel], None] = None,
 22):
 23    """
 24    Draws the planets on a chart based on the provided parameters.
 25
 26    Args:
 27        radius (int): The radius of the chart.
 28        available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the main subject.
 29        available_planets_setting (list[KerykeionSettingsCelestialPointModel]): Settings for the celestial points.
 30        third_circle_radius (Union[int, float]): Radius of the third circle.
 31        main_subject_first_house_degree_ut (Union[int, float]): Degree of the first house for the main subject.
 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").
 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.
 36
 37    Raises:
 38        KerykeionException: If the second subject is required but not provided.
 39
 40    Returns:
 41        str: SVG output for the chart with the planets drawn.
 42    """
 43    TRANSIT_RING_EXCLUDE_POINTS_NAMES = get_args(Houses)
 44
 45    if chart_type == "Transit" or chart_type == "Synastry":
 46        if second_subject_available_kerykeion_celestial_points is None:
 47            raise KerykeionException("Second subject is required for Transit or Synastry charts")
 48
 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)
 53
 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)
 58
 59    if chart_type == "Transit" or chart_type == "Synastry":
 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)
 64
 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
 70    planets_degut = {}
 71    diff = range(len(available_planets_setting))
 72
 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
 77
 78    """
 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)
 84    """
 85
 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]
112
113        logging.debug(f'{available_planets_setting[i]["label"]}, {diffa}, {diffb}')
114
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"]])
122        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":
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
211        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":
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
261        else:
262            scale = 1
263
264        planet_details = available_kerykeion_celestial_points[i]
265
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>"
269
270    # make transit degut and display planets
271    if chart_type == "Transit" or chart_type == "Synastry":
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":
384            dropin = 36
385        else:
386            dropin = 0
387
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
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;"/>'
395
396        # check transit
397        if chart_type == "Transit" or chart_type == "Synastry":
398            dropin = 160
399        else:
400            dropin = 120
401
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;"/>'
407
408    return output

Draws the planets on a chart based on the provided parameters.

Args: radius (int): The radius of the chart. available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the main subject. available_planets_setting (list[KerykeionSettingsCelestialPointModel]): Settings for the celestial points. third_circle_radius (Union[int, float]): Radius of the third circle. main_subject_first_house_degree_ut (Union[int, float]): Degree of the first house for the main subject. main_subject_seventh_house_degree_ut (Union[int, float]): Degree of the seventh house for the main subject. chart_type (ChartType): Type of the chart (e.g., "Transit", "Synastry"). second_subject_available_kerykeion_celestial_points (Union[list[KerykeionPointModel], None], optional): List of celestial points for the second subject, required for "Transit" or "Synastry" charts. Defaults to None.

Raises: KerykeionException: If the second subject is required but not provided.

Returns: str: SVG output for the chart with the planets drawn.