# kerykeion.charts.charts_utils

View Source

       1import math
       2import datetime
       3from kerykeion.kr_types import KerykeionException, ChartType
       4from typing import Union, Literal
       5from kerykeion.kr_types.kr_models import AspectModel, KerykeionPointModel
       6from kerykeion.kr_types.settings_models import KerykeionLanguageCelestialPointModel, KerykeionSettingsAspectModel
       7
       8
       9def 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
      22    # Get the language model keys
      23    language_keys = celestial_point_language.model_dump().keys()
      24
      25    # Check if the input planet name exists in the language model
      26    if input_planet_name in language_keys:
      27        return celestial_point_language[input_planet_name]
      28    else:
      29        raise KerykeionException(f"Celestial point {input_planet_name} not found in language model.")
      30
      31
      32def decHourJoin(inH: int, inM: int, inS: int) -> float:
      33    """Join hour, minutes, seconds, timezone integer to hour float.
      34
      35    Args:
      36        - inH (int): hour
      37        - inM (int): minutes
      38        - inS (int): seconds
      39    Returns:
      40        float: hour in float format
      41    """
      42
      43    dh = float(inH)
      44    dm = float(inM) / 60
      45    ds = float(inS) / 3600
      46    output = dh + dm + ds
      47    return output
      48
      49
      50def degreeDiff(a: Union[int, float], b: Union[int, float]) -> float:
      51    """Calculate the smallest difference between two angles in degrees.
      52
      53    Args:
      54        a (int | float): first angle in degrees
      55        b (int | float): second angle in degrees
      56
      57    Returns:
      58        float: smallest difference between a and b (0 to 180 degrees)
      59    """
      60    diff = math.fmod(abs(a - b), 360)  # Assicura che il valore sia in [0, 360)
      61    return min(diff, 360 - diff)  # Prende l'angolo più piccolo tra i due possibili
      62
      63
      64def degreeSum(a: Union[int, float], b: Union[int, float]) -> float:
      65    """Calculate the sum of two angles in degrees, normalized to [0, 360).
      66
      67    Args:
      68        a (int | float): first angle in degrees
      69        b (int | float): second angle in degrees
      70
      71    Returns:
      72        float: normalized sum of a and b in the range [0, 360)
      73    """
      74    return math.fmod(a + b, 360) if (a + b) % 360 != 0 else 0.0
      75
      76
      77def normalizeDegree(angle: Union[int, float]) -> float:
      78    """Normalize an angle to the range [0, 360).
      79
      80    Args:
      81        angle (int | float): The input angle in degrees.
      82
      83    Returns:
      84        float: The normalized angle in the range [0, 360).
      85    """
      86    return angle % 360 if angle % 360 != 0 else 0.0
      87
      88
      89def offsetToTz(datetime_offset: Union[datetime.timedelta, None]) -> float:
      90    """Convert datetime offset to float in hours.
      91
      92    Args:
      93        - datetime_offset (datetime.timedelta): datetime offset
      94
      95    Returns:
      96        - float: offset in hours
      97    """
      98
      99    if datetime_offset is None:
     100        raise KerykeionException("datetime_offset is None")
     101
     102    # days to hours
     103    dh = float(datetime_offset.days * 24)
     104    # seconds to hours
     105    sh = float(datetime_offset.seconds / 3600.0)
     106    # total hours
     107    output = dh + sh
     108    return output
     109
     110
     111def sliceToX(slice: Union[int, float], radius: Union[int, float], offset: Union[int, float]) -> float:
     112    """Calculates the x-coordinate of a point on a circle based on the slice, radius, and offset.
     113
     114    Args:
     115        - slice (int | float): Represents the
     116            slice of the circle to calculate the x-coordinate for.
     117            It must be  between 0 and 11 (inclusive).
     118        - radius (int | float): Represents the radius of the circle.
     119        - offset (int | float): Represents the offset in degrees.
     120            It must be between 0 and 360 (inclusive).
     121
     122    Returns:
     123        float: The x-coordinate of the point on the circle.
     124
     125    Example:
     126        >>> import math
     127        >>> sliceToX(3, 5, 45)
     128        2.5000000000000018
     129    """
     130
     131    plus = (math.pi * offset) / 180
     132    radial = ((math.pi / 6) * slice) + plus
     133    return radius * (math.cos(radial) + 1)
     134
     135
     136def sliceToY(slice: Union[int, float], r: Union[int, float], offset: Union[int, float]) -> float:
     137    """Calculates the y-coordinate of a point on a circle based on the slice, radius, and offset.
     138
     139    Args:
     140        - slice (int | float): Represents the slice of the circle to calculate
     141            the y-coordinate for. It must be between 0 and 11 (inclusive).
     142        - r (int | float): Represents the radius of the circle.
     143        - offset (int | float): Represents the offset in degrees.
     144            It must be between 0 and 360 (inclusive).
     145
     146    Returns:
     147        float: The y-coordinate of the point on the circle.
     148
     149    Example:
     150        >>> import math
     151        >>> __sliceToY(3, 5, 45)
     152        -4.330127018922194
     153    """
     154    plus = (math.pi * offset) / 180
     155    radial = ((math.pi / 6) * slice) + plus
     156    return r * ((math.sin(radial) / -1) + 1)
     157
     158
     159def draw_zodiac_slice(
     160    c1: Union[int, float],
     161    chart_type: ChartType,
     162    seventh_house_degree_ut: Union[int, float],
     163    num: int,
     164    r: Union[int, float],
     165    style: str,
     166    type: str,
     167) -> str:
     168    """Draws a zodiac slice based on the given parameters.
     169
     170    Args:
     171        - c1 (Union[int, float]): The value of c1.
     172        - chart_type (ChartType): The type of chart.
     173        - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
     174        - num (int): The number of the sign. Note: In OpenAstro it did refer to self.zodiac,
     175            which is a list of the signs in order, starting with Aries. Eg:
     176            {"name": "Ari", "element": "fire"}
     177        - r (Union[int, float]): The value of r.
     178        - style (str): The CSS inline style.
     179        - type (str): The type ?. In OpenAstro, it was the symbol of the sign. Eg: "Ari".
     180            self.zodiac[i]["name"]
     181
     182    Returns:
     183        - str: The zodiac slice and symbol as an SVG path.
     184    """
     185
     186    # pie slices
     187    offset = 360 - seventh_house_degree_ut
     188    # check transit
     189    if chart_type == "Transit" or chart_type == "Synastry":
     190        dropin: Union[int, float] = 0
     191    else:
     192        dropin = c1
     193    slice = f'<path d="M{str(r)},{str(r)} L{str(dropin + sliceToX(num, r - dropin, offset))},{str(dropin + sliceToY(num, r - dropin, offset))} A{str(r - dropin)},{str(r - dropin)} 0 0,0 {str(dropin + sliceToX(num + 1, r - dropin, offset))},{str(dropin + sliceToY(num + 1, r - dropin, offset))} z" style="{style}"/>'
     194
     195    # symbols
     196    offset = offset + 15
     197    # check transit
     198    if chart_type == "Transit" or chart_type == "Synastry":
     199        dropin = 54
     200    else:
     201        dropin = 18 + c1
     202    sign = f'<g transform="translate(-16,-16)"><use x="{str(dropin + sliceToX(num, r - dropin, offset))}" y="{str(dropin + sliceToY(num, r - dropin, offset))}" xlink:href="#{type}" /></g>'
     203
     204    return slice + "" + sign
     205
     206
     207def convert_latitude_coordinate_to_string(coord: Union[int, float], north_label: str, south_label: str) -> str:
     208    """Converts a floating point latitude to string with
     209    degree, minutes and seconds and the appropriate sign
     210    (north or south). Eg. 52.1234567 -> 52°7'25" N
     211
     212    Args:
     213        - coord (float | int): latitude in floating or integer format
     214        - north_label (str): String label for north
     215        - south_label (str): String label for south
     216    Returns:
     217        - str: latitude in string format with degree, minutes,
     218        seconds and sign (N/S)
     219    """
     220
     221    sign = north_label
     222    if coord < 0.0:
     223        sign = south_label
     224        coord = abs(coord)
     225    deg = int(coord)
     226    min = int((float(coord) - deg) * 60)
     227    sec = int(round(float(((float(coord) - deg) * 60) - min) * 60.0))
     228    return f"{deg}°{min}'{sec}\" {sign}"
     229
     230
     231def convert_longitude_coordinate_to_string(coord: Union[int, float], east_label: str, west_label: str) -> str:
     232    """Converts a floating point longitude to string with
     233    degree, minutes and seconds and the appropriate sign
     234    (east or west). Eg. 52.1234567 -> 52°7'25" E
     235
     236    Args:
     237        - coord (float|int): longitude in floating point format
     238        - east_label (str): String label for east
     239        - west_label (str): String label for west
     240    Returns:
     241        str: longitude in string format with degree, minutes,
     242            seconds and sign (E/W)
     243    """
     244
     245    sign = east_label
     246    if coord < 0.0:
     247        sign = west_label
     248        coord = abs(coord)
     249    deg = int(coord)
     250    min = int((float(coord) - deg) * 60)
     251    sec = int(round(float(((float(coord) - deg) * 60) - min) * 60.0))
     252    return f"{deg}°{min}'{sec}\" {sign}"
     253
     254
     255def draw_aspect_line(
     256    r: Union[int, float],
     257    ar: Union[int, float],
     258    aspect: Union[AspectModel, dict],
     259    color: str,
     260    seventh_house_degree_ut: Union[int, float],
     261) -> str:
     262    """Draws svg aspects: ring, aspect ring, degreeA degreeB
     263
     264    Args:
     265        - r (Union[int, float]): The value of r.
     266        - ar (Union[int, float]): The value of ar.
     267        - aspect_dict (dict): The aspect dictionary.
     268        - color (str): The color of the aspect.
     269        - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
     270
     271    Returns:
     272        str: The SVG line element as a string.
     273    """
     274
     275    if isinstance(aspect, dict):
     276        aspect = AspectModel(**aspect)
     277
     278    first_offset = (int(seventh_house_degree_ut) / -1) + int(aspect["p1_abs_pos"])
     279    x1 = sliceToX(0, ar, first_offset) + (r - ar)
     280    y1 = sliceToY(0, ar, first_offset) + (r - ar)
     281
     282    second_offset = (int(seventh_house_degree_ut) / -1) + int(aspect["p2_abs_pos"])
     283    x2 = sliceToX(0, ar, second_offset) + (r - ar)
     284    y2 = sliceToY(0, ar, second_offset) + (r - ar)
     285
     286    return (
     287        f'<g kr:node="Aspect" kr:aspectname="{aspect["aspect"]}" kr:to="{aspect["p1_name"]}" kr:tooriginaldegrees="{aspect["p1_abs_pos"]}" kr:from="{aspect["p2_name"]}" kr:fromoriginaldegrees="{aspect["p2_abs_pos"]}">'
     288        f'<line class="aspect" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {color}; stroke-width: 1; stroke-opacity: .9;"/>'
     289        f"</g>"
     290    )
     291
     292def convert_decimal_to_degree_string(dec: float, format_type: Literal["1", "2", "3"] = "3") -> str:
     293    """
     294    Converts a decimal float to a degrees string in the specified format.
     295
     296    Args:
     297        dec (float): The decimal float to convert.
     298        format_type (str): The format type:
     299            - "1": a°
     300            - "2": a°b'
     301            - "3": a°b'c" (default)
     302
     303    Returns:
     304        str: The degrees string in the specified format.
     305    """
     306    # Ensure the input is a float
     307    dec = float(dec)
     308
     309    # Calculate degrees, minutes, and seconds
     310    degrees = int(dec)
     311    minutes = int((dec - degrees) * 60)
     312    seconds = int(round((dec - degrees - minutes / 60) * 3600))
     313
     314    # Format the output based on the specified type
     315    if format_type == "1":
     316        return f"{degrees}°"
     317    elif format_type == "2":
     318        return f"{degrees}°{minutes:02d}'"
     319    elif format_type == "3":
     320        return f"{degrees}°{minutes:02d}'{seconds:02d}\""
     321
     322
     323def draw_transit_ring_degree_steps(r: Union[int, float], seventh_house_degree_ut: Union[int, float]) -> str:
     324    """Draws the transit ring degree steps.
     325
     326    Args:
     327        - r (Union[int, float]): The value of r.
     328        - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
     329
     330    Returns:
     331        str: The SVG path of the transit ring degree steps.
     332    """
     333
     334    out = '<g id="transitRingDegreeSteps">'
     335    for i in range(72):
     336        offset = float(i * 5) - seventh_house_degree_ut
     337        if offset < 0:
     338            offset = offset + 360.0
     339        elif offset > 360:
     340            offset = offset - 360.0
     341        x1 = sliceToX(0, r, offset)
     342        y1 = sliceToY(0, r, offset)
     343        x2 = sliceToX(0, r + 2, offset) - 2
     344        y2 = sliceToY(0, r + 2, offset) - 2
     345        out += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: #F00; stroke-width: 1px; stroke-opacity:.9;"/>'
     346    out += "</g>"
     347
     348    return out
     349
     350
     351def draw_degree_ring(
     352    r: Union[int, float], c1: Union[int, float], seventh_house_degree_ut: Union[int, float], stroke_color: str
     353) -> str:
     354    """Draws the degree ring.
     355
     356    Args:
     357        - r (Union[int, float]): The value of r.
     358        - c1 (Union[int, float]): The value of c1.
     359        - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
     360        - stroke_color (str): The color of the stroke.
     361
     362    Returns:
     363        str: The SVG path of the degree ring.
     364    """
     365    out = '<g id="degreeRing">'
     366    for i in range(72):
     367        offset = float(i * 5) - seventh_house_degree_ut
     368        if offset < 0:
     369            offset = offset + 360.0
     370        elif offset > 360:
     371            offset = offset - 360.0
     372        x1 = sliceToX(0, r - c1, offset) + c1
     373        y1 = sliceToY(0, r - c1, offset) + c1
     374        x2 = sliceToX(0, r + 2 - c1, offset) - 2 + c1
     375        y2 = sliceToY(0, r + 2 - c1, offset) - 2 + c1
     376
     377        out += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.9;"/>'
     378    out += "</g>"
     379
     380    return out
     381
     382
     383def draw_transit_ring(r: Union[int, float], paper_1_color: str, zodiac_transit_ring_3_color: str) -> str:
     384    """
     385    Draws the transit ring.
     386
     387    Args:
     388        - r (Union[int, float]): The value of r.
     389        - paper_1_color (str): The color of paper 1.
     390        - zodiac_transit_ring_3_color (str): The color of the zodiac transit ring
     391
     392    Returns:
     393        str: The SVG path of the transit ring.
     394    """
     395    radius_offset = 18
     396
     397    out = f'<circle cx="{r}" cy="{r}" r="{r - radius_offset}" style="fill: none; stroke: {paper_1_color}; stroke-width: 36px; stroke-opacity: .4;"/>'
     398    out += f'<circle cx="{r}" cy="{r}" r="{r}" style="fill: none; stroke: {zodiac_transit_ring_3_color}; stroke-width: 1px; stroke-opacity: .6;"/>'
     399
     400    return out
     401
     402
     403def draw_first_circle(
     404    r: Union[int, float], stroke_color: str, chart_type: ChartType, c1: Union[int, float, None] = None
     405) -> str:
     406    """
     407    Draws the first circle.
     408
     409    Args:
     410        - r (Union[int, float]): The value of r.
     411        - color (str): The color of the circle.
     412        - chart_type (ChartType): The type of chart.
     413        - c1 (Union[int, float]): The value of c1.
     414
     415    Returns:
     416        str: The SVG path of the first circle.
     417    """
     418    if chart_type == "Synastry" or chart_type == "Transit":
     419        return f'<circle cx="{r}" cy="{r}" r="{r - 36}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.4;" />'
     420    else:
     421        if c1 is None:
     422            raise KerykeionException("c1 is None")
     423
     424        return (
     425            f'<circle cx="{r}" cy="{r}" r="{r - c1}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; " />'
     426        )
     427
     428
     429def draw_second_circle(
     430    r: Union[int, float], stroke_color: str, fill_color: str, chart_type: ChartType, c2: Union[int, float, None] = None
     431) -> str:
     432    """
     433    Draws the second circle.
     434
     435    Args:
     436        - r (Union[int, float]): The value of r.
     437        - stroke_color (str): The color of the stroke.
     438        - fill_color (str): The color of the fill.
     439        - chart_type (ChartType): The type of chart.
     440        - c2 (Union[int, float]): The value of c2.
     441
     442    Returns:
     443        str: The SVG path of the second circle.
     444    """
     445
     446    if chart_type == "Synastry" or chart_type == "Transit":
     447        return f'<circle cx="{r}" cy="{r}" r="{r - 72}" style="fill: {fill_color}; fill-opacity:.4; stroke: {stroke_color}; stroke-opacity:.4; stroke-width: 1px" />'
     448
     449    else:
     450        if c2 is None:
     451            raise KerykeionException("c2 is None")
     452
     453        return f'<circle cx="{r}" cy="{r}" r="{r - c2}" style="fill: {fill_color}; fill-opacity:.2; stroke: {stroke_color}; stroke-opacity:.4; stroke-width: 1px" />'
     454
     455
     456def draw_third_circle(
     457    radius: Union[int, float],
     458    stroke_color: str,
     459    fill_color: str,
     460    chart_type: ChartType,
     461    c3: Union[int, float]
     462) -> str:
     463    """
     464    Draws the third circle in an SVG chart.
     465
     466    Parameters:
     467    - radius (Union[int, float]): The radius of the circle.
     468    - stroke_color (str): The stroke color of the circle.
     469    - fill_color (str): The fill color of the circle.
     470    - chart_type (ChartType): The type of the chart.
     471    - c3 (Union[int, float, None], optional): The radius adjustment for non-Synastry and non-Transit charts.
     472
     473    Returns:
     474    - str: The SVG element as a string.
     475    """
     476    if chart_type in {"Synastry", "Transit"}:
     477        # For Synastry and Transit charts, use a fixed radius adjustment of 160
     478        return f'<circle cx="{radius}" cy="{radius}" r="{radius - 160}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
     479
     480    else:
     481        return f'<circle cx="{radius}" cy="{radius}" r="{radius - c3}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
     482
     483
     484def draw_aspect_grid(
     485        stroke_color: str,
     486        available_planets: list,
     487        aspects: list,
     488        x_start: int = 380,
     489        y_start: int = 468,
     490    ) -> str:
     491    """
     492    Draws the aspect grid for the given planets and aspects.
     493
     494    Args:
     495        stroke_color (str): The color of the stroke.
     496        available_planets (list): List of all planets. Only planets with "is_active" set to True will be used.
     497        aspects (list): List of aspects.
     498        x_start (int): The x-coordinate starting point.
     499        y_start (int): The y-coordinate starting point.
     500
     501    Returns:
     502        str: SVG string representing the aspect grid.
     503    """
     504    svg_output = ""
     505    style = f"stroke:{stroke_color}; stroke-width: 1px; stroke-width: 0.5px; fill:none"
     506    box_size = 14
     507
     508    # Filter active planets
     509    active_planets = [planet for planet in available_planets if planet.is_active]
     510
     511    # Reverse the list of active planets for the first iteration
     512    reversed_planets = active_planets[::-1]
     513
     514    for index, planet_a in enumerate(reversed_planets):
     515        # Draw the grid box for the planet
     516        svg_output += f'<rect kr:node="AspectsGridRect" x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
     517        svg_output += f'<use transform="scale(0.4)" x="{(x_start + 2) * 2.5}" y="{(y_start + 1) * 2.5}" xlink:href="#{planet_a["name"]}" />'
     518
     519        # Update the starting coordinates for the next box
     520        x_start += box_size
     521        y_start -= box_size
     522
     523        # Coordinates for the aspect symbols
     524        x_aspect = x_start
     525        y_aspect = y_start + box_size
     526
     527        # Iterate over the remaining planets
     528        for planet_b in reversed_planets[index + 1:]:
     529            # Draw the grid box for the aspect
     530            svg_output += f'<rect kr:node="AspectsGridRect" x="{x_aspect}" y="{y_aspect}" width="{box_size}" height="{box_size}" style="{style}"/>'
     531            x_aspect += box_size
     532
     533            # Check for aspects between the planets
     534            for aspect in aspects:
     535                if (aspect["p1"] == planet_a["id"] and aspect["p2"] == planet_b["id"]) or (
     536                    aspect["p1"] == planet_b["id"] and aspect["p2"] == planet_a["id"]
     537                ):
     538                    svg_output += f'<use  x="{x_aspect - box_size + 1}" y="{y_aspect + 1}" xlink:href="#orb{aspect["aspect_degrees"]}" />'
     539
     540    return svg_output
     541
     542
     543def draw_houses_cusps_and_text_number(
     544    r: Union[int, float],
     545    first_subject_houses_list: list[KerykeionPointModel],
     546    standard_house_cusp_color: str,
     547    first_house_color: str,
     548    tenth_house_color: str,
     549    seventh_house_color: str,
     550    fourth_house_color: str,
     551    c1: Union[int, float],
     552    c3: Union[int, float],
     553    chart_type: ChartType,
     554    second_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
     555    transit_house_cusp_color: Union[str, None] = None,
     556) -> str:
     557    """
     558    Draws the houses cusps and text numbers for a given chart type.
     559
     560    Parameters:
     561    - r: Radius of the chart.
     562    - first_subject_houses_list: List of house for the first subject.
     563    - standard_house_cusp_color: Default color for house cusps.
     564    - first_house_color: Color for the first house cusp.
     565    - tenth_house_color: Color for the tenth house cusp.
     566    - seventh_house_color: Color for the seventh house cusp.
     567    - fourth_house_color: Color for the fourth house cusp.
     568    - c1: Offset for the first subject.
     569    - c3: Offset for the third subject.
     570    - chart_type: Type of the chart (e.g., Transit, Synastry).
     571    - second_subject_houses_list: List of house for the second subject (optional).
     572    - transit_house_cusp_color: Color for transit house cusps (optional).
     573
     574    Returns:
     575    - A string containing the SVG path for the houses cusps and text numbers.
     576    """
     577
     578    path = ""
     579    xr = 12
     580
     581    for i in range(xr):
     582        # Determine offsets based on chart type
     583        dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry"] else (c3, c1, False)
     584
     585        # Calculate the offset for the current house cusp
     586        offset = (int(first_subject_houses_list[int(xr / 2)].abs_pos) / -1) + int(first_subject_houses_list[i].abs_pos)
     587
     588        # Calculate the coordinates for the house cusp lines
     589        x1 = sliceToX(0, (r - dropin), offset) + dropin
     590        y1 = sliceToY(0, (r - dropin), offset) + dropin
     591        x2 = sliceToX(0, r - roff, offset) + roff
     592        y2 = sliceToY(0, r - roff, offset) + roff
     593
     594        # Calculate the text offset for the house number
     595        next_index = (i + 1) % xr
     596        text_offset = offset + int(
     597            degreeDiff(first_subject_houses_list[next_index].abs_pos, first_subject_houses_list[i].abs_pos) / 2
     598        )
     599
     600        # Determine the line color based on the house index
     601        linecolor = {0: first_house_color, 9: tenth_house_color, 6: seventh_house_color, 3: fourth_house_color}.get(
     602            i, standard_house_cusp_color
     603        )
     604
     605        if chart_type in ["Transit", "Synastry"]:
     606            if second_subject_houses_list is None or transit_house_cusp_color is None:
     607                raise KerykeionException("second_subject_houses_list_ut or transit_house_cusp_color is None")
     608
     609            # Calculate the offset for the second subject's house cusp
     610            zeropoint = 360 - first_subject_houses_list[6].abs_pos
     611            t_offset = (zeropoint + second_subject_houses_list[i].abs_pos) % 360
     612
     613            # Calculate the coordinates for the second subject's house cusp lines
     614            t_x1 = sliceToX(0, (r - t_roff), t_offset) + t_roff
     615            t_y1 = sliceToY(0, (r - t_roff), t_offset) + t_roff
     616            t_x2 = sliceToX(0, r, t_offset)
     617            t_y2 = sliceToY(0, r, t_offset)
     618
     619            # Calculate the text offset for the second subject's house number
     620            t_text_offset = t_offset + int(
     621                degreeDiff(second_subject_houses_list[next_index].abs_pos, second_subject_houses_list[i].abs_pos) / 2
     622            )
     623            t_linecolor = linecolor if i in [0, 9, 6, 3] else transit_house_cusp_color
     624            xtext = sliceToX(0, (r - 8), t_text_offset) + 8
     625            ytext = sliceToY(0, (r - 8), t_text_offset) + 8
     626
     627            # Add the house number text for the second subject
     628            fill_opacity = "0" if chart_type == "Transit" else ".4"
     629            path += f'<g kr:node="HouseNumber">'
     630            path += f'<text style="fill: var(--kerykeion-chart-color-house-number); fill-opacity: {fill_opacity}; font-size: 14px"><tspan x="{xtext - 3}" y="{ytext + 3}">{i + 1}</tspan></text>'
     631            path += f"</g>"
     632
     633            # Add the house cusp line for the second subject
     634            stroke_opacity = "0" if chart_type == "Transit" else ".3"
     635            path += f'<g kr:node="Cusp">'
     636            path += f"<line x1='{t_x1}' y1='{t_y1}' x2='{t_x2}' y2='{t_y2}' style='stroke: {t_linecolor}; stroke-width: 1px; stroke-opacity:{stroke_opacity};'/>"
     637            path += f"</g>"
     638
     639        # Adjust dropin based on chart type
     640        dropin = {"Transit": 84, "Synastry": 84, "ExternalNatal": 100}.get(chart_type, 48)
     641        xtext = sliceToX(0, (r - dropin), text_offset) + dropin
     642        ytext = sliceToY(0, (r - dropin), text_offset) + dropin
     643
     644        # Add the house cusp line for the first subject
     645        path += f'<g kr:node="Cusp">'
     646        path += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {linecolor}; stroke-width: 1px; stroke-dasharray:3,2; stroke-opacity:.4;"/>'
     647        path += f"</g>"
     648
     649        # Add the house number text for the first subject
     650        path += f'<g kr:node="HouseNumber">'
     651        path += f'<text style="fill: var(--kerykeion-chart-color-house-number); fill-opacity: .6; font-size: 14px"><tspan x="{xtext - 3}" y="{ytext + 3}">{i + 1}</tspan></text>'
     652        path += f"</g>"
     653
     654    return path
     655
     656
     657def draw_transit_aspect_list(
     658    grid_title: str,
     659    aspects_list: Union[list[AspectModel], list[dict]],
     660    celestial_point_language: Union[KerykeionLanguageCelestialPointModel, dict],
     661    aspects_settings: Union[KerykeionSettingsAspectModel, dict],
     662) -> str:
     663    """
     664    Generates the SVG output for the aspect transit grid.
     665
     666    Parameters:
     667    - grid_title: Title of the grid.
     668    - aspects_list: List of aspects.
     669    - planets_labels: Dictionary containing the planet labels.
     670    - aspects_settings: Dictionary containing the aspect settings.
     671
     672    Returns:
     673    - A string containing the SVG path data for the aspect transit grid.
     674    """
     675
     676    if isinstance(celestial_point_language, dict):
     677        celestial_point_language = KerykeionLanguageCelestialPointModel(**celestial_point_language)
     678
     679    if isinstance(aspects_settings, dict):
     680        aspects_settings = KerykeionSettingsAspectModel(**aspects_settings)
     681
     682    # If not instance of AspectModel, convert to AspectModel
     683    if isinstance(aspects_list[0], dict):
     684        aspects_list = [AspectModel(**aspect) for aspect in aspects_list] # type: ignore
     685
     686    line = 0
     687    nl = 0
     688    inner_path = ""
     689    for i, aspect in enumerate(aspects_list):
     690        # Adjust the vertical position for every 12 aspects
     691        if i == 14:
     692            nl = 100
     693            line = 0
     694
     695        elif i == 28:
     696            nl = 200
     697            line = 0
     698
     699        elif i == 42:
     700            nl = 300
     701            line = 0
     702
     703        elif i == 56:
     704            nl = 400
     705            line = 0
     706
     707        elif i == 70:
     708            nl = 500
     709            # When there are more than 60 aspects, the text is moved up
     710            if len(aspects_list) > 84:
     711                line = -1 * (len(aspects_list) - 84) * 14
     712            else:
     713                line = 0
     714
     715        inner_path += f'<g transform="translate({nl},{line})">'
     716
     717        # first planet symbol
     718        inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspects_list[i]["p1"]]["name"]}" />'
     719
     720        # aspect symbol
     721        # TODO: Remove the "degree" element EVERYWHERE!
     722        aspect_name = aspects_list[i]["aspect"]
     723        id_value = next((a["degree"] for a in aspects_settings if a["name"] == aspect_name), None) # type: ignore
     724        inner_path += f'<use  x="15" y="0" xlink:href="#orb{id_value}" />'
     725
     726        # second planet symbol
     727        inner_path += f'<g transform="translate(30,0)">'
     728        inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspects_list[i]["p2"]]["name"]}" />'
     729        inner_path += f"</g>"
     730
     731        # difference in degrees
     732        inner_path += f'<text y="8" x="45" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 10px;">{convert_decimal_to_degree_string(aspects_list[i]["orbit"])}</text>'
     733        # line
     734        inner_path += f"</g>"
     735        line = line + 14
     736
     737    out = '<g transform="translate(526,273)">'
     738    out += f'<text y="-15" x="0" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 14px;">{grid_title}:</text>'
     739    out += inner_path
     740    out += '</g>'
     741
     742    return out
     743
     744
     745def calculate_moon_phase_chart_params(
     746    degrees_between_sun_and_moon: float,
     747    latitude: float
     748) -> dict:
     749    """
     750    Calculate the parameters for the moon phase chart.
     751
     752    Parameters:
     753    - degrees_between_sun_and_moon (float): The degrees between the sun and the moon.
     754    - latitude (float): The latitude for rotation calculation.
     755
     756    Returns:
     757    - dict: The moon phase chart parameters.
     758    """
     759    deg = degrees_between_sun_and_moon
     760
     761    # Initialize variables for lunar phase properties
     762    circle_center_x = None
     763    circle_radius = None
     764
     765    # Determine lunar phase properties based on the degree
     766    if deg < 90.0:
     767        max_radius = deg
     768        if deg > 80.0:
     769            max_radius = max_radius * max_radius
     770        circle_center_x = 20.0 + (deg / 90.0) * (max_radius + 10.0)
     771        circle_radius = 10.0 + (deg / 90.0) * max_radius
     772
     773    elif deg < 180.0:
     774        max_radius = 180.0 - deg
     775        if deg < 100.0:
     776            max_radius = max_radius * max_radius
     777        circle_center_x = 20.0 + ((deg - 90.0) / 90.0 * (max_radius + 10.0)) - (max_radius + 10.0)
     778        circle_radius = 10.0 + max_radius - ((deg - 90.0) / 90.0 * max_radius)
     779
     780    elif deg < 270.0:
     781        max_radius = deg - 180.0
     782        if deg > 260.0:
     783            max_radius = max_radius * max_radius
     784        circle_center_x = 20.0 + ((deg - 180.0) / 90.0 * (max_radius + 10.0))
     785        circle_radius = 10.0 + ((deg - 180.0) / 90.0 * max_radius)
     786
     787    elif deg < 361.0:
     788        max_radius = 360.0 - deg
     789        if deg < 280.0:
     790            max_radius = max_radius * max_radius
     791        circle_center_x = 20.0 + ((deg - 270.0) / 90.0 * (max_radius + 10.0)) - (max_radius + 10.0)
     792        circle_radius = 10.0 + max_radius - ((deg - 270.0) / 90.0 * max_radius)
     793
     794    else:
     795        raise KerykeionException(f"Invalid degree value: {deg}")
     796
     797
     798    # Calculate rotation based on latitude
     799    lunar_phase_rotate = -90.0 - latitude
     800
     801    return {
     802        "circle_center_x": circle_center_x,
     803        "circle_radius": circle_radius,
     804        "lunar_phase_rotate": lunar_phase_rotate,
     805    }
     806
     807def draw_house_grid(
     808        main_subject_houses_list: list[KerykeionPointModel],
     809        chart_type: ChartType,
     810        secondary_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
     811        text_color: str = "#000000",
     812        house_cusp_generale_name_label: str = "Cusp",
     813    ) -> str:
     814    """
     815    Generate SVG code for a grid of astrological houses.
     816
     817    Parameters:
     818    - main_houses (list[KerykeionPointModel]): List of houses for the main subject.
     819    - chart_type (ChartType): Type of the chart (e.g., Synastry, Transit).
     820    - secondary_houses (list[KerykeionPointModel], optional): List of houses for the secondary subject.
     821    - text_color (str): Color of the text.
     822    - cusp_label (str): Label for the house cusp.
     823
     824    Returns:
     825    - str: The SVG code for the grid of houses.
     826    """
     827
     828    if chart_type in ["Synastry", "Transit"] and secondary_subject_houses_list is None:
     829        raise KerykeionException("secondary_houses is None")
     830
     831    svg_output = '<g transform="translate(650,-20)">'
     832
     833    line_increment = 10
     834    for i, house in enumerate(main_subject_houses_list):
     835        cusp_number = f"&#160;&#160;{i + 1}" if i < 9 else str(i + 1)
     836        svg_output += (
     837            f'<g transform="translate(0,{line_increment})">'
     838            f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
     839            f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
     840            f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
     841            f'</g>'
     842        )
     843        line_increment += 14
     844
     845    svg_output += "</g>"
     846
     847    if chart_type == "Synastry":
     848        svg_output += '<!-- Synastry Houses -->'
     849        svg_output += '<g transform="translate(910, -20)">'
     850        line_increment = 10
     851
     852        for i, house in enumerate(secondary_subject_houses_list): # type: ignore
     853            cusp_number = f"&#160;&#160;{i + 1}" if i < 9 else str(i + 1)
     854            svg_output += (
     855                f'<g transform="translate(0,{line_increment})">'
     856                f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
     857                f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
     858                f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
     859                f'</g>'
     860            )
     861            line_increment += 14
     862
     863        svg_output += "</g>"
     864
     865    return svg_output
     866
     867
     868def draw_planet_grid(
     869        planets_and_houses_grid_title: str,
     870        subject_name: str,
     871        available_kerykeion_celestial_points: list[KerykeionPointModel],
     872        chart_type: ChartType,
     873        celestial_point_language: KerykeionLanguageCelestialPointModel,
     874        second_subject_name: Union[str, None] = None,
     875        second_subject_available_kerykeion_celestial_points: Union[list[KerykeionPointModel], None] = None,
     876        text_color: str = "#000000",
     877    ) -> str:
     878    """
     879    Draws the planet grid for the given celestial points and chart type.
     880
     881    Args:
     882        planets_and_houses_grid_title (str): Title of the grid.
     883        subject_name (str): Name of the subject.
     884        available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the subject.
     885        chart_type (ChartType): Type of the chart.
     886        celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points.
     887        second_subject_name (str, optional): Name of the second subject. Defaults to None.
     888        second_subject_available_kerykeion_celestial_points (list[KerykeionPointModel], optional): List of celestial points for the second subject. Defaults to None.
     889        text_color (str, optional): Color of the text. Defaults to "#000000".
     890
     891    Returns:
     892        str: The SVG output for the planet grid.
     893    """
     894    line_height = 10
     895    offset = 0
     896    offset_between_lines = 14
     897
     898    svg_output = (
     899        f'<g transform="translate(175, -15)">'
     900        f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}:</text>'
     901        f'</g>'
     902    )
     903
     904    end_of_line = "</g>"
     905
     906    for i, planet in enumerate(available_kerykeion_celestial_points):
     907        if i == 27:
     908            line_height = 10
     909            offset = -120
     910
     911        decoded_name = get_decoded_kerykeion_celestial_point_name(planet["name"], celestial_point_language)
     912        svg_output += (
     913            f'<g transform="translate({offset},{line_height})">'
     914            f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{decoded_name}</text>'
     915            f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{planet["name"]}" /></g>'
     916            f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(planet["position"])}</text>'
     917            f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{planet["sign"]}" /></g>'
     918        )
     919
     920        if planet["retrograde"]:
     921            svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
     922
     923        svg_output += end_of_line
     924        line_height += offset_between_lines
     925
     926    if chart_type in ["Transit", "Synastry"]:
     927        if second_subject_available_kerykeion_celestial_points is None:
     928            raise KerykeionException("second_subject_available_kerykeion_celestial_points is None")
     929
     930        if chart_type == "Transit":
     931            svg_output += (
     932                f'<g transform="translate(320, -15)">'
     933                f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{second_subject_name}:</text>'
     934            )
     935        else:
     936            svg_output += (
     937                f'<g transform="translate(380, -15)">'
     938                f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}:</text>'
     939            )
     940
     941        svg_output += end_of_line
     942
     943        second_line_height = 10
     944        second_offset = 250
     945
     946        for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
     947            if i == 27:
     948                second_line_height = 10
     949                second_offset = -120
     950
     951            second_decoded_name = get_decoded_kerykeion_celestial_point_name(t_planet["name"], celestial_point_language)
     952            svg_output += (
     953                f'<g transform="translate({second_offset},{second_line_height})">'
     954                f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{second_decoded_name}</text>'
     955                f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
     956                f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(t_planet["position"])}</text>'
     957                f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{t_planet["sign"]}" /></g>'
     958            )
     959
     960            if t_planet["retrograde"]:
     961                svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
     962
     963            svg_output += end_of_line
     964            second_line_height += offset_between_lines
     965
     966    return svg_output
     967
     968
     969def draw_transit_aspect_grid(
     970        stroke_color: str,
     971        available_planets: list,
     972        aspects: list,
     973        x_indent: int = 50,
     974        y_indent: int = 250,
     975        box_size: int = 14
     976    ) -> str:
     977    """
     978    Draws the aspect grid for the given planets and aspects. The default args value are specific for a stand alone
     979    aspect grid.
     980
     981    Args:
     982        stroke_color (str): The color of the stroke.
     983        available_planets (list): List of all planets. Only planets with "is_active" set to True will be used.
     984        aspects (list): List of aspects.
     985        x_indent (int): The initial x-coordinate starting point.
     986        y_indent (int): The initial y-coordinate starting point.
     987
     988    Returns:
     989        str: SVG string representing the aspect grid.
     990    """
     991    svg_output = ""
     992    style = f"stroke:{stroke_color}; stroke-width: 1px; stroke-width: 0.5px; fill:none"
     993    x_start = x_indent
     994    y_start = y_indent
     995
     996    # Filter active planets
     997    active_planets = [planet for planet in available_planets if planet.is_active]
     998
     999    # Reverse the list of active planets for the first iteration
    1000    reversed_planets = active_planets[::-1]
    1001    for index, planet_a in enumerate(reversed_planets):
    1002        # Draw the grid box for the planet
    1003        svg_output += f'<rect x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
    1004        svg_output += f'<use transform="scale(0.4)" x="{(x_start + 2) * 2.5}" y="{(y_start + 1) * 2.5}" xlink:href="#{planet_a["name"]}" />'
    1005        x_start += box_size
    1006
    1007    x_start = x_indent - box_size
    1008    y_start = y_indent - box_size
    1009
    1010    for index, planet_a in enumerate(reversed_planets):
    1011        # Draw the grid box for the planet
    1012        svg_output += f'<rect x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
    1013        svg_output += f'<use transform="scale(0.4)" x="{(x_start + 2) * 2.5}" y="{(y_start + 1) * 2.5}" xlink:href="#{planet_a["name"]}" />'
    1014        y_start -= box_size
    1015
    1016    x_start = x_indent
    1017    y_start = y_indent
    1018    y_start = y_start - box_size
    1019
    1020    for index, planet_a in enumerate(reversed_planets):
    1021        # Draw the grid box for the planet
    1022        svg_output += f'<rect x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
    1023
    1024        # Update the starting coordinates for the next box
    1025        y_start -= box_size
    1026
    1027        # Coordinates for the aspect symbols
    1028        x_aspect = x_start
    1029        y_aspect = y_start + box_size
    1030
    1031        # Iterate over the remaining planets
    1032        for planet_b in reversed_planets:
    1033            # Draw the grid box for the aspect
    1034            svg_output += f'<rect x="{x_aspect}" y="{y_aspect}" width="{box_size}" height="{box_size}" style="{style}"/>'
    1035            x_aspect += box_size
    1036
    1037            # Check for aspects between the planets
    1038            for aspect in aspects:
    1039                if (aspect["p1"] == planet_a["id"] and aspect["p2"] == planet_b["id"]):
    1040                    svg_output += f'<use  x="{x_aspect - box_size + 1}" y="{y_aspect + 1}" xlink:href="#orb{aspect["aspect_degrees"]}" />'
    1041
    1042    return svg_output

def get_decoded_kerykeion_celestial_point_name( input_planet_name: str, celestial_point_language: kerykeion.kr_types.settings_models.KerykeionLanguageCelestialPointModel) -> str: View Source

    10def get_decoded_kerykeion_celestial_point_name(input_planet_name: str, celestial_point_language: KerykeionLanguageCelestialPointModel) -> str:
    11    """
    12    Decode the given celestial point name based on the provided language model.
    13
    14    Args:
    15        input_planet_name (str): The name of the celestial point to decode.
    16        celestial_point_language (KerykeionLanguageCelestialPointModel): The language model containing celestial point names.
    17
    18    Returns:
    19        str: The decoded celestial point name.
    20    """
    21
    22
    23    # Get the language model keys
    24    language_keys = celestial_point_language.model_dump().keys()
    25
    26    # Check if the input planet name exists in the language model
    27    if input_planet_name in language_keys:
    28        return celestial_point_language[input_planet_name]
    29    else:
    30        raise KerykeionException(f"Celestial point {input_planet_name} not found in language model.")

Decode the given celestial point name based on the provided language model.

Args: input_planet_name (str): The name of the celestial point to decode. celestial_point_language (KerykeionLanguageCelestialPointModel): The language model containing celestial point names.

Returns: str: The decoded celestial point name.

def decHourJoin(inH: int, inM: int, inS: int) -> float: View Source

    33def decHourJoin(inH: int, inM: int, inS: int) -> float:
    34    """Join hour, minutes, seconds, timezone integer to hour float.
    35
    36    Args:
    37        - inH (int): hour
    38        - inM (int): minutes
    39        - inS (int): seconds
    40    Returns:
    41        float: hour in float format
    42    """
    43
    44    dh = float(inH)
    45    dm = float(inM) / 60
    46    ds = float(inS) / 3600
    47    output = dh + dm + ds
    48    return output

Join hour, minutes, seconds, timezone integer to hour float.

Args: - inH (int): hour - inM (int): minutes - inS (int): seconds Returns: float: hour in float format

def degreeDiff(a: Union[int, float], b: Union[int, float]) -> float: View Source

    51def degreeDiff(a: Union[int, float], b: Union[int, float]) -> float:
    52    """Calculate the smallest difference between two angles in degrees.
    53
    54    Args:
    55        a (int | float): first angle in degrees
    56        b (int | float): second angle in degrees
    57
    58    Returns:
    59        float: smallest difference between a and b (0 to 180 degrees)
    60    """
    61    diff = math.fmod(abs(a - b), 360)  # Assicura che il valore sia in [0, 360)
    62    return min(diff, 360 - diff)  # Prende l'angolo più piccolo tra i due possibili

Calculate the smallest difference between two angles in degrees.

Args: a (int | float): first angle in degrees b (int | float): second angle in degrees

Returns: float: smallest difference between a and b (0 to 180 degrees)

def degreeSum(a: Union[int, float], b: Union[int, float]) -> float: View Source

    65def degreeSum(a: Union[int, float], b: Union[int, float]) -> float:
    66    """Calculate the sum of two angles in degrees, normalized to [0, 360).
    67
    68    Args:
    69        a (int | float): first angle in degrees
    70        b (int | float): second angle in degrees
    71
    72    Returns:
    73        float: normalized sum of a and b in the range [0, 360)
    74    """
    75    return math.fmod(a + b, 360) if (a + b) % 360 != 0 else 0.0

Calculate the sum of two angles in degrees, normalized to [0, 360).

Args: a (int | float): first angle in degrees b (int | float): second angle in degrees

Returns: float: normalized sum of a and b in the range [0, 360)

def normalizeDegree(angle: Union[int, float]) -> float: View Source

    78def normalizeDegree(angle: Union[int, float]) -> float:
    79    """Normalize an angle to the range [0, 360).
    80
    81    Args:
    82        angle (int | float): The input angle in degrees.
    83
    84    Returns:
    85        float: The normalized angle in the range [0, 360).
    86    """
    87    return angle % 360 if angle % 360 != 0 else 0.0

Normalize an angle to the range [0, 360).

Args: angle (int | float): The input angle in degrees.

Returns: float: The normalized angle in the range [0, 360).

def offsetToTz(datetime_offset: Optional[datetime.timedelta]) -> float: View Source

     90def offsetToTz(datetime_offset: Union[datetime.timedelta, None]) -> float:
     91    """Convert datetime offset to float in hours.
     92
     93    Args:
     94        - datetime_offset (datetime.timedelta): datetime offset
     95
     96    Returns:
     97        - float: offset in hours
     98    """
     99
    100    if datetime_offset is None:
    101        raise KerykeionException("datetime_offset is None")
    102
    103    # days to hours
    104    dh = float(datetime_offset.days * 24)
    105    # seconds to hours
    106    sh = float(datetime_offset.seconds / 3600.0)
    107    # total hours
    108    output = dh + sh
    109    return output

Convert datetime offset to float in hours.

Args: - datetime_offset (datetime.timedelta): datetime offset

Returns: - float: offset in hours

def sliceToX( slice: Union[int, float], radius: Union[int, float], offset: Union[int, float]) -> float: View Source

    112def sliceToX(slice: Union[int, float], radius: Union[int, float], offset: Union[int, float]) -> float:
    113    """Calculates the x-coordinate of a point on a circle based on the slice, radius, and offset.
    114
    115    Args:
    116        - slice (int | float): Represents the
    117            slice of the circle to calculate the x-coordinate for.
    118            It must be  between 0 and 11 (inclusive).
    119        - radius (int | float): Represents the radius of the circle.
    120        - offset (int | float): Represents the offset in degrees.
    121            It must be between 0 and 360 (inclusive).
    122
    123    Returns:
    124        float: The x-coordinate of the point on the circle.
    125
    126    Example:
    127        >>> import math
    128        >>> sliceToX(3, 5, 45)
    129        2.5000000000000018
    130    """
    131
    132    plus = (math.pi * offset) / 180
    133    radial = ((math.pi / 6) * slice) + plus
    134    return radius * (math.cos(radial) + 1)

Calculates the x-coordinate of a point on a circle based on the slice, radius, and offset.

Args: - slice (int | float): Represents the slice of the circle to calculate the x-coordinate for. It must be between 0 and 11 (inclusive). - radius (int | float): Represents the radius of the circle. - offset (int | float): Represents the offset in degrees. It must be between 0 and 360 (inclusive).

Returns: float: The x-coordinate of the point on the circle.

Example:

> > > import math sliceToX(3, 5, 45) 2.5000000000000018

def sliceToY( slice: Union[int, float], r: Union[int, float], offset: Union[int, float]) -> float: View Source

    137def sliceToY(slice: Union[int, float], r: Union[int, float], offset: Union[int, float]) -> float:
    138    """Calculates the y-coordinate of a point on a circle based on the slice, radius, and offset.
    139
    140    Args:
    141        - slice (int | float): Represents the slice of the circle to calculate
    142            the y-coordinate for. It must be between 0 and 11 (inclusive).
    143        - r (int | float): Represents the radius of the circle.
    144        - offset (int | float): Represents the offset in degrees.
    145            It must be between 0 and 360 (inclusive).
    146
    147    Returns:
    148        float: The y-coordinate of the point on the circle.
    149
    150    Example:
    151        >>> import math
    152        >>> __sliceToY(3, 5, 45)
    153        -4.330127018922194
    154    """
    155    plus = (math.pi * offset) / 180
    156    radial = ((math.pi / 6) * slice) + plus
    157    return r * ((math.sin(radial) / -1) + 1)

Calculates the y-coordinate of a point on a circle based on the slice, radius, and offset.

Args: - slice (int | float): Represents the slice of the circle to calculate the y-coordinate for. It must be between 0 and 11 (inclusive). - r (int | float): Represents the radius of the circle. - offset (int | float): Represents the offset in degrees. It must be between 0 and 360 (inclusive).

Returns: float: The y-coordinate of the point on the circle.

Example:

> > > import math __sliceToY(3, 5, 45) -4.330127018922194

def draw_zodiac_slice( c1: Union[int, float], chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit', 'Composite'], seventh_house_degree_ut: Union[int, float], num: int, r: Union[int, float], style: str, type: str) -> str: View Source

    160def draw_zodiac_slice(
    161    c1: Union[int, float],
    162    chart_type: ChartType,
    163    seventh_house_degree_ut: Union[int, float],
    164    num: int,
    165    r: Union[int, float],
    166    style: str,
    167    type: str,
    168) -> str:
    169    """Draws a zodiac slice based on the given parameters.
    170
    171    Args:
    172        - c1 (Union[int, float]): The value of c1.
    173        - chart_type (ChartType): The type of chart.
    174        - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
    175        - num (int): The number of the sign. Note: In OpenAstro it did refer to self.zodiac,
    176            which is a list of the signs in order, starting with Aries. Eg:
    177            {"name": "Ari", "element": "fire"}
    178        - r (Union[int, float]): The value of r.
    179        - style (str): The CSS inline style.
    180        - type (str): The type ?. In OpenAstro, it was the symbol of the sign. Eg: "Ari".
    181            self.zodiac[i]["name"]
    182
    183    Returns:
    184        - str: The zodiac slice and symbol as an SVG path.
    185    """
    186
    187    # pie slices
    188    offset = 360 - seventh_house_degree_ut
    189    # check transit
    190    if chart_type == "Transit" or chart_type == "Synastry":
    191        dropin: Union[int, float] = 0
    192    else:
    193        dropin = c1
    194    slice = f'<path d="M{str(r)},{str(r)} L{str(dropin + sliceToX(num, r - dropin, offset))},{str(dropin + sliceToY(num, r - dropin, offset))} A{str(r - dropin)},{str(r - dropin)} 0 0,0 {str(dropin + sliceToX(num + 1, r - dropin, offset))},{str(dropin + sliceToY(num + 1, r - dropin, offset))} z" style="{style}"/>'
    195
    196    # symbols
    197    offset = offset + 15
    198    # check transit
    199    if chart_type == "Transit" or chart_type == "Synastry":
    200        dropin = 54
    201    else:
    202        dropin = 18 + c1
    203    sign = f'<g transform="translate(-16,-16)"><use x="{str(dropin + sliceToX(num, r - dropin, offset))}" y="{str(dropin + sliceToY(num, r - dropin, offset))}" xlink:href="#{type}" /></g>'
    204
    205    return slice + "" + sign

Draws a zodiac slice based on the given parameters.

Args: - c1 (Union[int, float]): The value of c1. - chart_type (ChartType): The type of chart. - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house. - num (int): The number of the sign. Note: In OpenAstro it did refer to self.zodiac, which is a list of the signs in order, starting with Aries. Eg: {"name": "Ari", "element": "fire"} - r (Union[int, float]): The value of r. - style (str): The CSS inline style. - type (str): The type ?. In OpenAstro, it was the symbol of the sign. Eg: "Ari". self.zodiac[i]["name"]

Returns: - str: The zodiac slice and symbol as an SVG path.

def convert_latitude_coordinate_to_string(coord: Union[int, float], north_label: str, south_label: str) -> str: View Source

    208def convert_latitude_coordinate_to_string(coord: Union[int, float], north_label: str, south_label: str) -> str:
    209    """Converts a floating point latitude to string with
    210    degree, minutes and seconds and the appropriate sign
    211    (north or south). Eg. 52.1234567 -> 52°7'25" N
    212
    213    Args:
    214        - coord (float | int): latitude in floating or integer format
    215        - north_label (str): String label for north
    216        - south_label (str): String label for south
    217    Returns:
    218        - str: latitude in string format with degree, minutes,
    219        seconds and sign (N/S)
    220    """
    221
    222    sign = north_label
    223    if coord < 0.0:
    224        sign = south_label
    225        coord = abs(coord)
    226    deg = int(coord)
    227    min = int((float(coord) - deg) * 60)
    228    sec = int(round(float(((float(coord) - deg) * 60) - min) * 60.0))
    229    return f"{deg}°{min}'{sec}\" {sign}"

Converts a floating point latitude to string with degree, minutes and seconds and the appropriate sign (north or south). Eg. 52.1234567 -> 52°7'25" N

Args: - coord (float | int): latitude in floating or integer format - north_label (str): String label for north - south_label (str): String label for south Returns: - str: latitude in string format with degree, minutes, seconds and sign (N/S)

def convert_longitude_coordinate_to_string(coord: Union[int, float], east_label: str, west_label: str) -> str: View Source

    232def convert_longitude_coordinate_to_string(coord: Union[int, float], east_label: str, west_label: str) -> str:
    233    """Converts a floating point longitude to string with
    234    degree, minutes and seconds and the appropriate sign
    235    (east or west). Eg. 52.1234567 -> 52°7'25" E
    236
    237    Args:
    238        - coord (float|int): longitude in floating point format
    239        - east_label (str): String label for east
    240        - west_label (str): String label for west
    241    Returns:
    242        str: longitude in string format with degree, minutes,
    243            seconds and sign (E/W)
    244    """
    245
    246    sign = east_label
    247    if coord < 0.0:
    248        sign = west_label
    249        coord = abs(coord)
    250    deg = int(coord)
    251    min = int((float(coord) - deg) * 60)
    252    sec = int(round(float(((float(coord) - deg) * 60) - min) * 60.0))
    253    return f"{deg}°{min}'{sec}\" {sign}"

Converts a floating point longitude to string with degree, minutes and seconds and the appropriate sign (east or west). Eg. 52.1234567 -> 52°7'25" E

Args: - coord (float|int): longitude in floating point format - east_label (str): String label for east - west_label (str): String label for west Returns: str: longitude in string format with degree, minutes, seconds and sign (E/W)

def draw_aspect_line( r: Union[int, float], ar: Union[int, float], aspect: Union[kerykeion.kr_types.kr_models.AspectModel, dict], color: str, seventh_house_degree_ut: Union[int, float]) -> str: View Source

    256def draw_aspect_line(
    257    r: Union[int, float],
    258    ar: Union[int, float],
    259    aspect: Union[AspectModel, dict],
    260    color: str,
    261    seventh_house_degree_ut: Union[int, float],
    262) -> str:
    263    """Draws svg aspects: ring, aspect ring, degreeA degreeB
    264
    265    Args:
    266        - r (Union[int, float]): The value of r.
    267        - ar (Union[int, float]): The value of ar.
    268        - aspect_dict (dict): The aspect dictionary.
    269        - color (str): The color of the aspect.
    270        - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
    271
    272    Returns:
    273        str: The SVG line element as a string.
    274    """
    275
    276    if isinstance(aspect, dict):
    277        aspect = AspectModel(**aspect)
    278
    279    first_offset = (int(seventh_house_degree_ut) / -1) + int(aspect["p1_abs_pos"])
    280    x1 = sliceToX(0, ar, first_offset) + (r - ar)
    281    y1 = sliceToY(0, ar, first_offset) + (r - ar)
    282
    283    second_offset = (int(seventh_house_degree_ut) / -1) + int(aspect["p2_abs_pos"])
    284    x2 = sliceToX(0, ar, second_offset) + (r - ar)
    285    y2 = sliceToY(0, ar, second_offset) + (r - ar)
    286
    287    return (
    288        f'<g kr:node="Aspect" kr:aspectname="{aspect["aspect"]}" kr:to="{aspect["p1_name"]}" kr:tooriginaldegrees="{aspect["p1_abs_pos"]}" kr:from="{aspect["p2_name"]}" kr:fromoriginaldegrees="{aspect["p2_abs_pos"]}">'
    289        f'<line class="aspect" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {color}; stroke-width: 1; stroke-opacity: .9;"/>'
    290        f"</g>"
    291    )

Draws svg aspects: ring, aspect ring, degreeA degreeB

Args: - r (Union[int, float]): The value of r. - ar (Union[int, float]): The value of ar. - aspect_dict (dict): The aspect dictionary. - color (str): The color of the aspect. - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.

Returns: str: The SVG line element as a string.

def convert_decimal_to_degree_string(dec: float, format_type: Literal['1', '2', '3'] = '3') -> str: View Source

    293def convert_decimal_to_degree_string(dec: float, format_type: Literal["1", "2", "3"] = "3") -> str:
    294    """
    295    Converts a decimal float to a degrees string in the specified format.
    296
    297    Args:
    298        dec (float): The decimal float to convert.
    299        format_type (str): The format type:
    300            - "1": a°
    301            - "2": a°b'
    302            - "3": a°b'c" (default)
    303
    304    Returns:
    305        str: The degrees string in the specified format.
    306    """
    307    # Ensure the input is a float
    308    dec = float(dec)
    309
    310    # Calculate degrees, minutes, and seconds
    311    degrees = int(dec)
    312    minutes = int((dec - degrees) * 60)
    313    seconds = int(round((dec - degrees - minutes / 60) * 3600))
    314
    315    # Format the output based on the specified type
    316    if format_type == "1":
    317        return f"{degrees}°"
    318    elif format_type == "2":
    319        return f"{degrees}°{minutes:02d}'"
    320    elif format_type == "3":
    321        return f"{degrees}°{minutes:02d}'{seconds:02d}\""

Converts a decimal float to a degrees string in the specified format.

Args: dec (float): The decimal float to convert. format_type (str): The format type: - "1": a° - "2": a°b' - "3": a°b'c" (default)

Returns: str: The degrees string in the specified format.

def draw_transit_ring_degree_steps(r: Union[int, float], seventh_house_degree_ut: Union[int, float]) -> str: View Source

    324def draw_transit_ring_degree_steps(r: Union[int, float], seventh_house_degree_ut: Union[int, float]) -> str:
    325    """Draws the transit ring degree steps.
    326
    327    Args:
    328        - r (Union[int, float]): The value of r.
    329        - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
    330
    331    Returns:
    332        str: The SVG path of the transit ring degree steps.
    333    """
    334
    335    out = '<g id="transitRingDegreeSteps">'
    336    for i in range(72):
    337        offset = float(i * 5) - seventh_house_degree_ut
    338        if offset < 0:
    339            offset = offset + 360.0
    340        elif offset > 360:
    341            offset = offset - 360.0
    342        x1 = sliceToX(0, r, offset)
    343        y1 = sliceToY(0, r, offset)
    344        x2 = sliceToX(0, r + 2, offset) - 2
    345        y2 = sliceToY(0, r + 2, offset) - 2
    346        out += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: #F00; stroke-width: 1px; stroke-opacity:.9;"/>'
    347    out += "</g>"
    348
    349    return out

Draws the transit ring degree steps.

Args: - r (Union[int, float]): The value of r. - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.

Returns: str: The SVG path of the transit ring degree steps.

def draw_degree_ring( r: Union[int, float], c1: Union[int, float], seventh_house_degree_ut: Union[int, float], stroke_color: str) -> str: View Source

    352def draw_degree_ring(
    353    r: Union[int, float], c1: Union[int, float], seventh_house_degree_ut: Union[int, float], stroke_color: str
    354) -> str:
    355    """Draws the degree ring.
    356
    357    Args:
    358        - r (Union[int, float]): The value of r.
    359        - c1 (Union[int, float]): The value of c1.
    360        - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
    361        - stroke_color (str): The color of the stroke.
    362
    363    Returns:
    364        str: The SVG path of the degree ring.
    365    """
    366    out = '<g id="degreeRing">'
    367    for i in range(72):
    368        offset = float(i * 5) - seventh_house_degree_ut
    369        if offset < 0:
    370            offset = offset + 360.0
    371        elif offset > 360:
    372            offset = offset - 360.0
    373        x1 = sliceToX(0, r - c1, offset) + c1
    374        y1 = sliceToY(0, r - c1, offset) + c1
    375        x2 = sliceToX(0, r + 2 - c1, offset) - 2 + c1
    376        y2 = sliceToY(0, r + 2 - c1, offset) - 2 + c1
    377
    378        out += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.9;"/>'
    379    out += "</g>"
    380
    381    return out

Draws the degree ring.

Args: - r (Union[int, float]): The value of r. - c1 (Union[int, float]): The value of c1. - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house. - stroke_color (str): The color of the stroke.

Returns: str: The SVG path of the degree ring.

def draw_transit_ring( r: Union[int, float], paper_1_color: str, zodiac_transit_ring_3_color: str) -> str: View Source

    384def draw_transit_ring(r: Union[int, float], paper_1_color: str, zodiac_transit_ring_3_color: str) -> str:
    385    """
    386    Draws the transit ring.
    387
    388    Args:
    389        - r (Union[int, float]): The value of r.
    390        - paper_1_color (str): The color of paper 1.
    391        - zodiac_transit_ring_3_color (str): The color of the zodiac transit ring
    392
    393    Returns:
    394        str: The SVG path of the transit ring.
    395    """
    396    radius_offset = 18
    397
    398    out = f'<circle cx="{r}" cy="{r}" r="{r - radius_offset}" style="fill: none; stroke: {paper_1_color}; stroke-width: 36px; stroke-opacity: .4;"/>'
    399    out += f'<circle cx="{r}" cy="{r}" r="{r}" style="fill: none; stroke: {zodiac_transit_ring_3_color}; stroke-width: 1px; stroke-opacity: .6;"/>'
    400
    401    return out

Draws the transit ring.

Args: - r (Union[int, float]): The value of r. - paper_1_color (str): The color of paper 1. - zodiac_transit_ring_3_color (str): The color of the zodiac transit ring

Returns: str: The SVG path of the transit ring.

def draw_first_circle( r: Union[int, float], stroke_color: str, chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit', 'Composite'], c1: Union[int, float, NoneType] = None) -> str: View Source

    404def draw_first_circle(
    405    r: Union[int, float], stroke_color: str, chart_type: ChartType, c1: Union[int, float, None] = None
    406) -> str:
    407    """
    408    Draws the first circle.
    409
    410    Args:
    411        - r (Union[int, float]): The value of r.
    412        - color (str): The color of the circle.
    413        - chart_type (ChartType): The type of chart.
    414        - c1 (Union[int, float]): The value of c1.
    415
    416    Returns:
    417        str: The SVG path of the first circle.
    418    """
    419    if chart_type == "Synastry" or chart_type == "Transit":
    420        return f'<circle cx="{r}" cy="{r}" r="{r - 36}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.4;" />'
    421    else:
    422        if c1 is None:
    423            raise KerykeionException("c1 is None")
    424
    425        return (
    426            f'<circle cx="{r}" cy="{r}" r="{r - c1}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; " />'
    427        )

Draws the first circle.

Args: - r (Union[int, float]): The value of r. - color (str): The color of the circle. - chart_type (ChartType): The type of chart. - c1 (Union[int, float]): The value of c1.

Returns: str: The SVG path of the first circle.

def draw_second_circle( r: Union[int, float], stroke_color: str, fill_color: str, chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit', 'Composite'], c2: Union[int, float, NoneType] = None) -> str: View Source

    430def draw_second_circle(
    431    r: Union[int, float], stroke_color: str, fill_color: str, chart_type: ChartType, c2: Union[int, float, None] = None
    432) -> str:
    433    """
    434    Draws the second circle.
    435
    436    Args:
    437        - r (Union[int, float]): The value of r.
    438        - stroke_color (str): The color of the stroke.
    439        - fill_color (str): The color of the fill.
    440        - chart_type (ChartType): The type of chart.
    441        - c2 (Union[int, float]): The value of c2.
    442
    443    Returns:
    444        str: The SVG path of the second circle.
    445    """
    446
    447    if chart_type == "Synastry" or chart_type == "Transit":
    448        return f'<circle cx="{r}" cy="{r}" r="{r - 72}" style="fill: {fill_color}; fill-opacity:.4; stroke: {stroke_color}; stroke-opacity:.4; stroke-width: 1px" />'
    449
    450    else:
    451        if c2 is None:
    452            raise KerykeionException("c2 is None")
    453
    454        return f'<circle cx="{r}" cy="{r}" r="{r - c2}" style="fill: {fill_color}; fill-opacity:.2; stroke: {stroke_color}; stroke-opacity:.4; stroke-width: 1px" />'

Draws the second circle.

Args: - r (Union[int, float]): The value of r. - stroke_color (str): The color of the stroke. - fill_color (str): The color of the fill. - chart_type (ChartType): The type of chart. - c2 (Union[int, float]): The value of c2.

Returns: str: The SVG path of the second circle.

def draw_third_circle( radius: Union[int, float], stroke_color: str, fill_color: str, chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit', 'Composite'], c3: Union[int, float]) -> str: View Source

    457def draw_third_circle(
    458    radius: Union[int, float],
    459    stroke_color: str,
    460    fill_color: str,
    461    chart_type: ChartType,
    462    c3: Union[int, float]
    463) -> str:
    464    """
    465    Draws the third circle in an SVG chart.
    466
    467    Parameters:
    468    - radius (Union[int, float]): The radius of the circle.
    469    - stroke_color (str): The stroke color of the circle.
    470    - fill_color (str): The fill color of the circle.
    471    - chart_type (ChartType): The type of the chart.
    472    - c3 (Union[int, float, None], optional): The radius adjustment for non-Synastry and non-Transit charts.
    473
    474    Returns:
    475    - str: The SVG element as a string.
    476    """
    477    if chart_type in {"Synastry", "Transit"}:
    478        # For Synastry and Transit charts, use a fixed radius adjustment of 160
    479        return f'<circle cx="{radius}" cy="{radius}" r="{radius - 160}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
    480
    481    else:
    482        return f'<circle cx="{radius}" cy="{radius}" r="{radius - c3}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'

Draws the third circle in an SVG chart.

Parameters:

- radius (Union[int, float]): The radius of the circle.
- stroke_color (str): The stroke color of the circle.
- fill_color (str): The fill color of the circle.
- chart_type (ChartType): The type of the chart.
- c3 (Union[int, float, None], optional): The radius adjustment for non-Synastry and non-Transit charts.

Returns:

- str: The SVG element as a string.

def draw_aspect_grid( stroke_color: str, available_planets: list, aspects: list, x_start: int = 380, y_start: int = 468) -> str: View Source

    485def draw_aspect_grid(
    486        stroke_color: str,
    487        available_planets: list,
    488        aspects: list,
    489        x_start: int = 380,
    490        y_start: int = 468,
    491    ) -> str:
    492    """
    493    Draws the aspect grid for the given planets and aspects.
    494
    495    Args:
    496        stroke_color (str): The color of the stroke.
    497        available_planets (list): List of all planets. Only planets with "is_active" set to True will be used.
    498        aspects (list): List of aspects.
    499        x_start (int): The x-coordinate starting point.
    500        y_start (int): The y-coordinate starting point.
    501
    502    Returns:
    503        str: SVG string representing the aspect grid.
    504    """
    505    svg_output = ""
    506    style = f"stroke:{stroke_color}; stroke-width: 1px; stroke-width: 0.5px; fill:none"
    507    box_size = 14
    508
    509    # Filter active planets
    510    active_planets = [planet for planet in available_planets if planet.is_active]
    511
    512    # Reverse the list of active planets for the first iteration
    513    reversed_planets = active_planets[::-1]
    514
    515    for index, planet_a in enumerate(reversed_planets):
    516        # Draw the grid box for the planet
    517        svg_output += f'<rect kr:node="AspectsGridRect" x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
    518        svg_output += f'<use transform="scale(0.4)" x="{(x_start + 2) * 2.5}" y="{(y_start + 1) * 2.5}" xlink:href="#{planet_a["name"]}" />'
    519
    520        # Update the starting coordinates for the next box
    521        x_start += box_size
    522        y_start -= box_size
    523
    524        # Coordinates for the aspect symbols
    525        x_aspect = x_start
    526        y_aspect = y_start + box_size
    527
    528        # Iterate over the remaining planets
    529        for planet_b in reversed_planets[index + 1:]:
    530            # Draw the grid box for the aspect
    531            svg_output += f'<rect kr:node="AspectsGridRect" x="{x_aspect}" y="{y_aspect}" width="{box_size}" height="{box_size}" style="{style}"/>'
    532            x_aspect += box_size
    533
    534            # Check for aspects between the planets
    535            for aspect in aspects:
    536                if (aspect["p1"] == planet_a["id"] and aspect["p2"] == planet_b["id"]) or (
    537                    aspect["p1"] == planet_b["id"] and aspect["p2"] == planet_a["id"]
    538                ):
    539                    svg_output += f'<use  x="{x_aspect - box_size + 1}" y="{y_aspect + 1}" xlink:href="#orb{aspect["aspect_degrees"]}" />'
    540
    541    return svg_output

Draws the aspect grid for the given planets and aspects.

Args: stroke_color (str): The color of the stroke. available_planets (list): List of all planets. Only planets with "is_active" set to True will be used. aspects (list): List of aspects. x_start (int): The x-coordinate starting point. y_start (int): The y-coordinate starting point.

Returns: str: SVG string representing the aspect grid.

def draw_houses_cusps_and_text_number( r: Union[int, float], first_subject_houses_list: list[kerykeion.kr_types.kr_models.KerykeionPointModel], standard_house_cusp_color: str, first_house_color: str, tenth_house_color: str, seventh_house_color: str, fourth_house_color: str, c1: Union[int, float], c3: Union[int, float], chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit', 'Composite'], second_subject_houses_list: Optional[list[kerykeion.kr_types.kr_models.KerykeionPointModel]] = None, transit_house_cusp_color: Optional[str] = None) -> str: View Source

    544def draw_houses_cusps_and_text_number(
    545    r: Union[int, float],
    546    first_subject_houses_list: list[KerykeionPointModel],
    547    standard_house_cusp_color: str,
    548    first_house_color: str,
    549    tenth_house_color: str,
    550    seventh_house_color: str,
    551    fourth_house_color: str,
    552    c1: Union[int, float],
    553    c3: Union[int, float],
    554    chart_type: ChartType,
    555    second_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
    556    transit_house_cusp_color: Union[str, None] = None,
    557) -> str:
    558    """
    559    Draws the houses cusps and text numbers for a given chart type.
    560
    561    Parameters:
    562    - r: Radius of the chart.
    563    - first_subject_houses_list: List of house for the first subject.
    564    - standard_house_cusp_color: Default color for house cusps.
    565    - first_house_color: Color for the first house cusp.
    566    - tenth_house_color: Color for the tenth house cusp.
    567    - seventh_house_color: Color for the seventh house cusp.
    568    - fourth_house_color: Color for the fourth house cusp.
    569    - c1: Offset for the first subject.
    570    - c3: Offset for the third subject.
    571    - chart_type: Type of the chart (e.g., Transit, Synastry).
    572    - second_subject_houses_list: List of house for the second subject (optional).
    573    - transit_house_cusp_color: Color for transit house cusps (optional).
    574
    575    Returns:
    576    - A string containing the SVG path for the houses cusps and text numbers.
    577    """
    578
    579    path = ""
    580    xr = 12
    581
    582    for i in range(xr):
    583        # Determine offsets based on chart type
    584        dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry"] else (c3, c1, False)
    585
    586        # Calculate the offset for the current house cusp
    587        offset = (int(first_subject_houses_list[int(xr / 2)].abs_pos) / -1) + int(first_subject_houses_list[i].abs_pos)
    588
    589        # Calculate the coordinates for the house cusp lines
    590        x1 = sliceToX(0, (r - dropin), offset) + dropin
    591        y1 = sliceToY(0, (r - dropin), offset) + dropin
    592        x2 = sliceToX(0, r - roff, offset) + roff
    593        y2 = sliceToY(0, r - roff, offset) + roff
    594
    595        # Calculate the text offset for the house number
    596        next_index = (i + 1) % xr
    597        text_offset = offset + int(
    598            degreeDiff(first_subject_houses_list[next_index].abs_pos, first_subject_houses_list[i].abs_pos) / 2
    599        )
    600
    601        # Determine the line color based on the house index
    602        linecolor = {0: first_house_color, 9: tenth_house_color, 6: seventh_house_color, 3: fourth_house_color}.get(
    603            i, standard_house_cusp_color
    604        )
    605
    606        if chart_type in ["Transit", "Synastry"]:
    607            if second_subject_houses_list is None or transit_house_cusp_color is None:
    608                raise KerykeionException("second_subject_houses_list_ut or transit_house_cusp_color is None")
    609
    610            # Calculate the offset for the second subject's house cusp
    611            zeropoint = 360 - first_subject_houses_list[6].abs_pos
    612            t_offset = (zeropoint + second_subject_houses_list[i].abs_pos) % 360
    613
    614            # Calculate the coordinates for the second subject's house cusp lines
    615            t_x1 = sliceToX(0, (r - t_roff), t_offset) + t_roff
    616            t_y1 = sliceToY(0, (r - t_roff), t_offset) + t_roff
    617            t_x2 = sliceToX(0, r, t_offset)
    618            t_y2 = sliceToY(0, r, t_offset)
    619
    620            # Calculate the text offset for the second subject's house number
    621            t_text_offset = t_offset + int(
    622                degreeDiff(second_subject_houses_list[next_index].abs_pos, second_subject_houses_list[i].abs_pos) / 2
    623            )
    624            t_linecolor = linecolor if i in [0, 9, 6, 3] else transit_house_cusp_color
    625            xtext = sliceToX(0, (r - 8), t_text_offset) + 8
    626            ytext = sliceToY(0, (r - 8), t_text_offset) + 8
    627
    628            # Add the house number text for the second subject
    629            fill_opacity = "0" if chart_type == "Transit" else ".4"
    630            path += f'<g kr:node="HouseNumber">'
    631            path += f'<text style="fill: var(--kerykeion-chart-color-house-number); fill-opacity: {fill_opacity}; font-size: 14px"><tspan x="{xtext - 3}" y="{ytext + 3}">{i + 1}</tspan></text>'
    632            path += f"</g>"
    633
    634            # Add the house cusp line for the second subject
    635            stroke_opacity = "0" if chart_type == "Transit" else ".3"
    636            path += f'<g kr:node="Cusp">'
    637            path += f"<line x1='{t_x1}' y1='{t_y1}' x2='{t_x2}' y2='{t_y2}' style='stroke: {t_linecolor}; stroke-width: 1px; stroke-opacity:{stroke_opacity};'/>"
    638            path += f"</g>"
    639
    640        # Adjust dropin based on chart type
    641        dropin = {"Transit": 84, "Synastry": 84, "ExternalNatal": 100}.get(chart_type, 48)
    642        xtext = sliceToX(0, (r - dropin), text_offset) + dropin
    643        ytext = sliceToY(0, (r - dropin), text_offset) + dropin
    644
    645        # Add the house cusp line for the first subject
    646        path += f'<g kr:node="Cusp">'
    647        path += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {linecolor}; stroke-width: 1px; stroke-dasharray:3,2; stroke-opacity:.4;"/>'
    648        path += f"</g>"
    649
    650        # Add the house number text for the first subject
    651        path += f'<g kr:node="HouseNumber">'
    652        path += f'<text style="fill: var(--kerykeion-chart-color-house-number); fill-opacity: .6; font-size: 14px"><tspan x="{xtext - 3}" y="{ytext + 3}">{i + 1}</tspan></text>'
    653        path += f"</g>"
    654
    655    return path

Draws the houses cusps and text numbers for a given chart type.

Parameters:

- r: Radius of the chart.
- first_subject_houses_list: List of house for the first subject.
- standard_house_cusp_color: Default color for house cusps.
- first_house_color: Color for the first house cusp.
- tenth_house_color: Color for the tenth house cusp.
- seventh_house_color: Color for the seventh house cusp.
- fourth_house_color: Color for the fourth house cusp.
- c1: Offset for the first subject.
- c3: Offset for the third subject.
- chart_type: Type of the chart (e.g., Transit, Synastry).
- second_subject_houses_list: List of house for the second subject (optional).
- transit_house_cusp_color: Color for transit house cusps (optional).

Returns:

- A string containing the SVG path for the houses cusps and text numbers.

def draw_transit_aspect_list( grid_title: str, aspects_list: Union[list[kerykeion.kr_types.kr_models.AspectModel], list[dict]], celestial_point_language: Union[kerykeion.kr_types.settings_models.KerykeionLanguageCelestialPointModel, dict], aspects_settings: Union[kerykeion.kr_types.settings_models.KerykeionSettingsAspectModel, dict]) -> str: View Source

    658def draw_transit_aspect_list(
    659    grid_title: str,
    660    aspects_list: Union[list[AspectModel], list[dict]],
    661    celestial_point_language: Union[KerykeionLanguageCelestialPointModel, dict],
    662    aspects_settings: Union[KerykeionSettingsAspectModel, dict],
    663) -> str:
    664    """
    665    Generates the SVG output for the aspect transit grid.
    666
    667    Parameters:
    668    - grid_title: Title of the grid.
    669    - aspects_list: List of aspects.
    670    - planets_labels: Dictionary containing the planet labels.
    671    - aspects_settings: Dictionary containing the aspect settings.
    672
    673    Returns:
    674    - A string containing the SVG path data for the aspect transit grid.
    675    """
    676
    677    if isinstance(celestial_point_language, dict):
    678        celestial_point_language = KerykeionLanguageCelestialPointModel(**celestial_point_language)
    679
    680    if isinstance(aspects_settings, dict):
    681        aspects_settings = KerykeionSettingsAspectModel(**aspects_settings)
    682
    683    # If not instance of AspectModel, convert to AspectModel
    684    if isinstance(aspects_list[0], dict):
    685        aspects_list = [AspectModel(**aspect) for aspect in aspects_list] # type: ignore
    686
    687    line = 0
    688    nl = 0
    689    inner_path = ""
    690    for i, aspect in enumerate(aspects_list):
    691        # Adjust the vertical position for every 12 aspects
    692        if i == 14:
    693            nl = 100
    694            line = 0
    695
    696        elif i == 28:
    697            nl = 200
    698            line = 0
    699
    700        elif i == 42:
    701            nl = 300
    702            line = 0
    703
    704        elif i == 56:
    705            nl = 400
    706            line = 0
    707
    708        elif i == 70:
    709            nl = 500
    710            # When there are more than 60 aspects, the text is moved up
    711            if len(aspects_list) > 84:
    712                line = -1 * (len(aspects_list) - 84) * 14
    713            else:
    714                line = 0
    715
    716        inner_path += f'<g transform="translate({nl},{line})">'
    717
    718        # first planet symbol
    719        inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspects_list[i]["p1"]]["name"]}" />'
    720
    721        # aspect symbol
    722        # TODO: Remove the "degree" element EVERYWHERE!
    723        aspect_name = aspects_list[i]["aspect"]
    724        id_value = next((a["degree"] for a in aspects_settings if a["name"] == aspect_name), None) # type: ignore
    725        inner_path += f'<use  x="15" y="0" xlink:href="#orb{id_value}" />'
    726
    727        # second planet symbol
    728        inner_path += f'<g transform="translate(30,0)">'
    729        inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspects_list[i]["p2"]]["name"]}" />'
    730        inner_path += f"</g>"
    731
    732        # difference in degrees
    733        inner_path += f'<text y="8" x="45" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 10px;">{convert_decimal_to_degree_string(aspects_list[i]["orbit"])}</text>'
    734        # line
    735        inner_path += f"</g>"
    736        line = line + 14
    737
    738    out = '<g transform="translate(526,273)">'
    739    out += f'<text y="-15" x="0" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 14px;">{grid_title}:</text>'
    740    out += inner_path
    741    out += '</g>'
    742
    743    return out

Generates the SVG output for the aspect transit grid.

Parameters:

- grid_title: Title of the grid.
- aspects_list: List of aspects.
- planets_labels: Dictionary containing the planet labels.
- aspects_settings: Dictionary containing the aspect settings.

Returns:

- A string containing the SVG path data for the aspect transit grid.

def calculate_moon_phase_chart_params(degrees_between_sun_and_moon: float, latitude: float) -> dict: View Source

    746def calculate_moon_phase_chart_params(
    747    degrees_between_sun_and_moon: float,
    748    latitude: float
    749) -> dict:
    750    """
    751    Calculate the parameters for the moon phase chart.
    752
    753    Parameters:
    754    - degrees_between_sun_and_moon (float): The degrees between the sun and the moon.
    755    - latitude (float): The latitude for rotation calculation.
    756
    757    Returns:
    758    - dict: The moon phase chart parameters.
    759    """
    760    deg = degrees_between_sun_and_moon
    761
    762    # Initialize variables for lunar phase properties
    763    circle_center_x = None
    764    circle_radius = None
    765
    766    # Determine lunar phase properties based on the degree
    767    if deg < 90.0:
    768        max_radius = deg
    769        if deg > 80.0:
    770            max_radius = max_radius * max_radius
    771        circle_center_x = 20.0 + (deg / 90.0) * (max_radius + 10.0)
    772        circle_radius = 10.0 + (deg / 90.0) * max_radius
    773
    774    elif deg < 180.0:
    775        max_radius = 180.0 - deg
    776        if deg < 100.0:
    777            max_radius = max_radius * max_radius
    778        circle_center_x = 20.0 + ((deg - 90.0) / 90.0 * (max_radius + 10.0)) - (max_radius + 10.0)
    779        circle_radius = 10.0 + max_radius - ((deg - 90.0) / 90.0 * max_radius)
    780
    781    elif deg < 270.0:
    782        max_radius = deg - 180.0
    783        if deg > 260.0:
    784            max_radius = max_radius * max_radius
    785        circle_center_x = 20.0 + ((deg - 180.0) / 90.0 * (max_radius + 10.0))
    786        circle_radius = 10.0 + ((deg - 180.0) / 90.0 * max_radius)
    787
    788    elif deg < 361.0:
    789        max_radius = 360.0 - deg
    790        if deg < 280.0:
    791            max_radius = max_radius * max_radius
    792        circle_center_x = 20.0 + ((deg - 270.0) / 90.0 * (max_radius + 10.0)) - (max_radius + 10.0)
    793        circle_radius = 10.0 + max_radius - ((deg - 270.0) / 90.0 * max_radius)
    794
    795    else:
    796        raise KerykeionException(f"Invalid degree value: {deg}")
    797
    798
    799    # Calculate rotation based on latitude
    800    lunar_phase_rotate = -90.0 - latitude
    801
    802    return {
    803        "circle_center_x": circle_center_x,
    804        "circle_radius": circle_radius,
    805        "lunar_phase_rotate": lunar_phase_rotate,
    806    }

Calculate the parameters for the moon phase chart.

Parameters:

- degrees_between_sun_and_moon (float): The degrees between the sun and the moon.
- latitude (float): The latitude for rotation calculation.

Returns:

- dict: The moon phase chart parameters.

def draw_house_grid( main_subject_houses_list: list[kerykeion.kr_types.kr_models.KerykeionPointModel], chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit', 'Composite'], secondary_subject_houses_list: Optional[list[kerykeion.kr_types.kr_models.KerykeionPointModel]] = None, text_color: str = '#000000', house_cusp_generale_name_label: str = 'Cusp') -> str: View Source

    808def draw_house_grid(
    809        main_subject_houses_list: list[KerykeionPointModel],
    810        chart_type: ChartType,
    811        secondary_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
    812        text_color: str = "#000000",
    813        house_cusp_generale_name_label: str = "Cusp",
    814    ) -> str:
    815    """
    816    Generate SVG code for a grid of astrological houses.
    817
    818    Parameters:
    819    - main_houses (list[KerykeionPointModel]): List of houses for the main subject.
    820    - chart_type (ChartType): Type of the chart (e.g., Synastry, Transit).
    821    - secondary_houses (list[KerykeionPointModel], optional): List of houses for the secondary subject.
    822    - text_color (str): Color of the text.
    823    - cusp_label (str): Label for the house cusp.
    824
    825    Returns:
    826    - str: The SVG code for the grid of houses.
    827    """
    828
    829    if chart_type in ["Synastry", "Transit"] and secondary_subject_houses_list is None:
    830        raise KerykeionException("secondary_houses is None")
    831
    832    svg_output = '<g transform="translate(650,-20)">'
    833
    834    line_increment = 10
    835    for i, house in enumerate(main_subject_houses_list):
    836        cusp_number = f"&#160;&#160;{i + 1}" if i < 9 else str(i + 1)
    837        svg_output += (
    838            f'<g transform="translate(0,{line_increment})">'
    839            f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
    840            f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
    841            f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
    842            f'</g>'
    843        )
    844        line_increment += 14
    845
    846    svg_output += "</g>"
    847
    848    if chart_type == "Synastry":
    849        svg_output += '<!-- Synastry Houses -->'
    850        svg_output += '<g transform="translate(910, -20)">'
    851        line_increment = 10
    852
    853        for i, house in enumerate(secondary_subject_houses_list): # type: ignore
    854            cusp_number = f"&#160;&#160;{i + 1}" if i < 9 else str(i + 1)
    855            svg_output += (
    856                f'<g transform="translate(0,{line_increment})">'
    857                f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
    858                f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
    859                f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
    860                f'</g>'
    861            )
    862            line_increment += 14
    863
    864        svg_output += "</g>"
    865
    866    return svg_output

Generate SVG code for a grid of astrological houses.

Parameters:

- main_houses (list[KerykeionPointModel]): List of houses for the main subject.
- chart_type (ChartType): Type of the chart (e.g., Synastry, Transit).
- secondary_houses (list[KerykeionPointModel], optional): List of houses for the secondary subject.
- text_color (str): Color of the text.
- cusp_label (str): Label for the house cusp.

Returns:

- str: The SVG code for the grid of houses.

def draw_planet_grid( planets_and_houses_grid_title: str, subject_name: str, available_kerykeion_celestial_points: list[kerykeion.kr_types.kr_models.KerykeionPointModel], chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit', 'Composite'], celestial_point_language: kerykeion.kr_types.settings_models.KerykeionLanguageCelestialPointModel, second_subject_name: Optional[str] = None, second_subject_available_kerykeion_celestial_points: Optional[list[kerykeion.kr_types.kr_models.KerykeionPointModel]] = None, text_color: str = '#000000') -> str: View Source

    869def draw_planet_grid(
    870        planets_and_houses_grid_title: str,
    871        subject_name: str,
    872        available_kerykeion_celestial_points: list[KerykeionPointModel],
    873        chart_type: ChartType,
    874        celestial_point_language: KerykeionLanguageCelestialPointModel,
    875        second_subject_name: Union[str, None] = None,
    876        second_subject_available_kerykeion_celestial_points: Union[list[KerykeionPointModel], None] = None,
    877        text_color: str = "#000000",
    878    ) -> str:
    879    """
    880    Draws the planet grid for the given celestial points and chart type.
    881
    882    Args:
    883        planets_and_houses_grid_title (str): Title of the grid.
    884        subject_name (str): Name of the subject.
    885        available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the subject.
    886        chart_type (ChartType): Type of the chart.
    887        celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points.
    888        second_subject_name (str, optional): Name of the second subject. Defaults to None.
    889        second_subject_available_kerykeion_celestial_points (list[KerykeionPointModel], optional): List of celestial points for the second subject. Defaults to None.
    890        text_color (str, optional): Color of the text. Defaults to "#000000".
    891
    892    Returns:
    893        str: The SVG output for the planet grid.
    894    """
    895    line_height = 10
    896    offset = 0
    897    offset_between_lines = 14
    898
    899    svg_output = (
    900        f'<g transform="translate(175, -15)">'
    901        f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}:</text>'
    902        f'</g>'
    903    )
    904
    905    end_of_line = "</g>"
    906
    907    for i, planet in enumerate(available_kerykeion_celestial_points):
    908        if i == 27:
    909            line_height = 10
    910            offset = -120
    911
    912        decoded_name = get_decoded_kerykeion_celestial_point_name(planet["name"], celestial_point_language)
    913        svg_output += (
    914            f'<g transform="translate({offset},{line_height})">'
    915            f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{decoded_name}</text>'
    916            f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{planet["name"]}" /></g>'
    917            f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(planet["position"])}</text>'
    918            f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{planet["sign"]}" /></g>'
    919        )
    920
    921        if planet["retrograde"]:
    922            svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
    923
    924        svg_output += end_of_line
    925        line_height += offset_between_lines
    926
    927    if chart_type in ["Transit", "Synastry"]:
    928        if second_subject_available_kerykeion_celestial_points is None:
    929            raise KerykeionException("second_subject_available_kerykeion_celestial_points is None")
    930
    931        if chart_type == "Transit":
    932            svg_output += (
    933                f'<g transform="translate(320, -15)">'
    934                f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{second_subject_name}:</text>'
    935            )
    936        else:
    937            svg_output += (
    938                f'<g transform="translate(380, -15)">'
    939                f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}:</text>'
    940            )
    941
    942        svg_output += end_of_line
    943
    944        second_line_height = 10
    945        second_offset = 250
    946
    947        for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
    948            if i == 27:
    949                second_line_height = 10
    950                second_offset = -120
    951
    952            second_decoded_name = get_decoded_kerykeion_celestial_point_name(t_planet["name"], celestial_point_language)
    953            svg_output += (
    954                f'<g transform="translate({second_offset},{second_line_height})">'
    955                f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{second_decoded_name}</text>'
    956                f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
    957                f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(t_planet["position"])}</text>'
    958                f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{t_planet["sign"]}" /></g>'
    959            )
    960
    961            if t_planet["retrograde"]:
    962                svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
    963
    964            svg_output += end_of_line
    965            second_line_height += offset_between_lines
    966
    967    return svg_output

Draws the planet grid for the given celestial points and chart type.

Args: planets_and_houses_grid_title (str): Title of the grid. subject_name (str): Name of the subject. available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the subject. chart_type (ChartType): Type of the chart. celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points. second_subject_name (str, optional): Name of the second subject. Defaults to None. second_subject_available_kerykeion_celestial_points (list[KerykeionPointModel], optional): List of celestial points for the second subject. Defaults to None. text_color (str, optional): Color of the text. Defaults to "#000000".

Returns: str: The SVG output for the planet grid.

def draw_transit_aspect_grid( stroke_color: str, available_planets: list, aspects: list, x_indent: int = 50, y_indent: int = 250, box_size: int = 14) -> str: View Source

     970def draw_transit_aspect_grid(
     971        stroke_color: str,
     972        available_planets: list,
     973        aspects: list,
     974        x_indent: int = 50,
     975        y_indent: int = 250,
     976        box_size: int = 14
     977    ) -> str:
     978    """
     979    Draws the aspect grid for the given planets and aspects. The default args value are specific for a stand alone
     980    aspect grid.
     981
     982    Args:
     983        stroke_color (str): The color of the stroke.
     984        available_planets (list): List of all planets. Only planets with "is_active" set to True will be used.
     985        aspects (list): List of aspects.
     986        x_indent (int): The initial x-coordinate starting point.
     987        y_indent (int): The initial y-coordinate starting point.
     988
     989    Returns:
     990        str: SVG string representing the aspect grid.
     991    """
     992    svg_output = ""
     993    style = f"stroke:{stroke_color}; stroke-width: 1px; stroke-width: 0.5px; fill:none"
     994    x_start = x_indent
     995    y_start = y_indent
     996
     997    # Filter active planets
     998    active_planets = [planet for planet in available_planets if planet.is_active]
     999
    1000    # Reverse the list of active planets for the first iteration
    1001    reversed_planets = active_planets[::-1]
    1002    for index, planet_a in enumerate(reversed_planets):
    1003        # Draw the grid box for the planet
    1004        svg_output += f'<rect x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
    1005        svg_output += f'<use transform="scale(0.4)" x="{(x_start + 2) * 2.5}" y="{(y_start + 1) * 2.5}" xlink:href="#{planet_a["name"]}" />'
    1006        x_start += box_size
    1007
    1008    x_start = x_indent - box_size
    1009    y_start = y_indent - box_size
    1010
    1011    for index, planet_a in enumerate(reversed_planets):
    1012        # Draw the grid box for the planet
    1013        svg_output += f'<rect x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
    1014        svg_output += f'<use transform="scale(0.4)" x="{(x_start + 2) * 2.5}" y="{(y_start + 1) * 2.5}" xlink:href="#{planet_a["name"]}" />'
    1015        y_start -= box_size
    1016
    1017    x_start = x_indent
    1018    y_start = y_indent
    1019    y_start = y_start - box_size
    1020
    1021    for index, planet_a in enumerate(reversed_planets):
    1022        # Draw the grid box for the planet
    1023        svg_output += f'<rect x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
    1024
    1025        # Update the starting coordinates for the next box
    1026        y_start -= box_size
    1027
    1028        # Coordinates for the aspect symbols
    1029        x_aspect = x_start
    1030        y_aspect = y_start + box_size
    1031
    1032        # Iterate over the remaining planets
    1033        for planet_b in reversed_planets:
    1034            # Draw the grid box for the aspect
    1035            svg_output += f'<rect x="{x_aspect}" y="{y_aspect}" width="{box_size}" height="{box_size}" style="{style}"/>'
    1036            x_aspect += box_size
    1037
    1038            # Check for aspects between the planets
    1039            for aspect in aspects:
    1040                if (aspect["p1"] == planet_a["id"] and aspect["p2"] == planet_b["id"]):
    1041                    svg_output += f'<use  x="{x_aspect - box_size + 1}" y="{y_aspect + 1}" xlink:href="#orb{aspect["aspect_degrees"]}" />'
    1042
    1043    return svg_output

Draws the aspect grid for the given planets and aspects. The default args value are specific for a stand alone aspect grid.

Args: stroke_color (str): The color of the stroke. available_planets (list): List of all planets. Only planets with "is_active" set to True will be used. aspects (list): List of aspects. x_indent (int): The initial x-coordinate starting point. y_indent (int): The initial y-coordinate starting point.

Returns: str: SVG string representing the aspect grid.