# kerykeion.charts.draw_planets

View Source

      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): View Source

     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.