kerykeion.charts.charts_utils

   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:
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:
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:
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:
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:
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:
 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
 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.