kerykeion 4.26.2__py3-none-any.whl → 5.0.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kerykeion might be problematic. Click here for more details.

Files changed (49) hide show
  1. kerykeion/__init__.py +9 -7
  2. kerykeion/aspects/aspects_utils.py +14 -8
  3. kerykeion/aspects/natal_aspects.py +26 -17
  4. kerykeion/aspects/synastry_aspects.py +32 -15
  5. kerykeion/aspects/transits_time_range.py +2 -2
  6. kerykeion/astrological_subject_factory.py +1132 -0
  7. kerykeion/charts/charts_utils.py +676 -146
  8. kerykeion/charts/draw_planets.py +9 -8
  9. kerykeion/charts/draw_planets_v2.py +639 -0
  10. kerykeion/charts/kerykeion_chart_svg.py +1334 -601
  11. kerykeion/charts/templates/chart.xml +184 -78
  12. kerykeion/charts/templates/wheel_only.xml +13 -12
  13. kerykeion/charts/themes/classic.css +91 -76
  14. kerykeion/charts/themes/dark-high-contrast.css +129 -107
  15. kerykeion/charts/themes/dark.css +130 -107
  16. kerykeion/charts/themes/light.css +130 -103
  17. kerykeion/charts/themes/strawberry.css +143 -0
  18. kerykeion/composite_subject_factory.py +26 -43
  19. kerykeion/ephemeris_data.py +6 -10
  20. kerykeion/house_comparison/__init__.py +3 -0
  21. kerykeion/house_comparison/house_comparison_factory.py +70 -0
  22. kerykeion/house_comparison/house_comparison_models.py +38 -0
  23. kerykeion/house_comparison/house_comparison_utils.py +98 -0
  24. kerykeion/kr_types/chart_types.py +13 -5
  25. kerykeion/kr_types/kr_literals.py +34 -6
  26. kerykeion/kr_types/kr_models.py +122 -160
  27. kerykeion/kr_types/settings_models.py +107 -143
  28. kerykeion/planetary_return_factory.py +299 -0
  29. kerykeion/{relationship_score/relationship_score_factory.py → relationship_score_factory.py} +10 -13
  30. kerykeion/report.py +4 -4
  31. kerykeion/settings/config_constants.py +35 -6
  32. kerykeion/settings/kerykeion_settings.py +1 -0
  33. kerykeion/settings/kr.config.json +1301 -1255
  34. kerykeion/settings/legacy/__init__.py +0 -0
  35. kerykeion/settings/legacy/legacy_celestial_points_settings.py +299 -0
  36. kerykeion/settings/legacy/legacy_chart_aspects_settings.py +71 -0
  37. kerykeion/settings/legacy/legacy_color_settings.py +42 -0
  38. kerykeion/transits_time_range.py +13 -9
  39. kerykeion/utilities.py +228 -31
  40. {kerykeion-4.26.2.dist-info → kerykeion-5.0.0a2.dist-info}/METADATA +119 -107
  41. kerykeion-5.0.0a2.dist-info/RECORD +54 -0
  42. {kerykeion-4.26.2.dist-info → kerykeion-5.0.0a2.dist-info}/WHEEL +1 -1
  43. kerykeion/astrological_subject.py +0 -841
  44. kerykeion/relationship_score/__init__.py +0 -2
  45. kerykeion/relationship_score/relationship_score.py +0 -175
  46. kerykeion-4.26.2.dist-info/LICENSE +0 -661
  47. kerykeion-4.26.2.dist-info/RECORD +0 -46
  48. /LICENSE → /kerykeion-5.0.0a2.dist-info/LICENSE +0 -0
  49. {kerykeion-4.26.2.dist-info → kerykeion-5.0.0a2.dist-info}/entry_points.txt +0 -0
@@ -1,9 +1,12 @@
1
1
  import math
2
2
  import datetime
3
3
  from kerykeion.kr_types import KerykeionException, ChartType
4
- from typing import Union, Literal
5
- from kerykeion.kr_types.kr_models import AspectModel, KerykeionPointModel
6
- from kerykeion.kr_types.settings_models import KerykeionLanguageCelestialPointModel, KerykeionSettingsAspectModel
4
+ from kerykeion.kr_types.kr_literals import AstrologicalPoint
5
+ from typing import Union, Literal, TYPE_CHECKING
6
+ from kerykeion.kr_types.kr_models import AspectModel, KerykeionPointModel, CompositeSubjectModel, PlanetReturnModel, AstrologicalSubjectModel
7
+ from kerykeion.kr_types.settings_models import KerykeionLanguageCelestialPointModel, KerykeionSettingsCelestialPointModel
8
+ from kerykeion.house_comparison import HouseComparisonModel
9
+
7
10
 
8
11
 
9
12
  def get_decoded_kerykeion_celestial_point_name(input_planet_name: str, celestial_point_language: KerykeionLanguageCelestialPointModel) -> str:
@@ -186,7 +189,7 @@ def draw_zodiac_slice(
186
189
  # pie slices
187
190
  offset = 360 - seventh_house_degree_ut
188
191
  # check transit
189
- if chart_type == "Transit" or chart_type == "Synastry":
192
+ if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
190
193
  dropin: Union[int, float] = 0
191
194
  else:
192
195
  dropin = c1
@@ -195,7 +198,7 @@ def draw_zodiac_slice(
195
198
  # symbols
196
199
  offset = offset + 15
197
200
  # check transit
198
- if chart_type == "Transit" or chart_type == "Synastry":
201
+ if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
199
202
  dropin = 54
200
203
  else:
201
204
  dropin = 18 + c1
@@ -289,6 +292,7 @@ def draw_aspect_line(
289
292
  f"</g>"
290
293
  )
291
294
 
295
+
292
296
  def convert_decimal_to_degree_string(dec: float, format_type: Literal["1", "2", "3"] = "3") -> str:
293
297
  """
294
298
  Converts a decimal float to a degrees string in the specified format.
@@ -415,7 +419,7 @@ def draw_first_circle(
415
419
  Returns:
416
420
  str: The SVG path of the first circle.
417
421
  """
418
- if chart_type == "Synastry" or chart_type == "Transit":
422
+ if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "Return":
419
423
  return f'<circle cx="{r}" cy="{r}" r="{r - 36}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.4;" />'
420
424
  else:
421
425
  if c1 is None:
@@ -443,7 +447,7 @@ def draw_second_circle(
443
447
  str: The SVG path of the second circle.
444
448
  """
445
449
 
446
- if chart_type == "Synastry" or chart_type == "Transit":
450
+ if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "Return":
447
451
  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
452
 
449
453
  else:
@@ -473,7 +477,7 @@ def draw_third_circle(
473
477
  Returns:
474
478
  - str: The SVG element as a string.
475
479
  """
476
- if chart_type in {"Synastry", "Transit"}:
480
+ if chart_type in {"Synastry", "Transit", "Return"}:
477
481
  # For Synastry and Transit charts, use a fixed radius adjustment of 160
478
482
  return f'<circle cx="{radius}" cy="{radius}" r="{radius - 160}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
479
483
 
@@ -485,7 +489,7 @@ def draw_aspect_grid(
485
489
  stroke_color: str,
486
490
  available_planets: list,
487
491
  aspects: list,
488
- x_start: int = 380,
492
+ x_start: int = 510,
489
493
  y_start: int = 468,
490
494
  ) -> str:
491
495
  """
@@ -506,7 +510,7 @@ def draw_aspect_grid(
506
510
  box_size = 14
507
511
 
508
512
  # Filter active planets
509
- active_planets = [planet for planet in available_planets if planet.is_active]
513
+ active_planets = [planet for planet in available_planets if planet["is_active"]]
510
514
 
511
515
  # Reverse the list of active planets for the first iteration
512
516
  reversed_planets = active_planets[::-1]
@@ -580,7 +584,7 @@ def draw_houses_cusps_and_text_number(
580
584
 
581
585
  for i in range(xr):
582
586
  # Determine offsets based on chart type
583
- dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry"] else (c3, c1, False)
587
+ dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry", "Return"] else (c3, c1, False)
584
588
 
585
589
  # Calculate the offset for the current house cusp
586
590
  offset = (int(first_subject_houses_list[int(xr / 2)].abs_pos) / -1) + int(first_subject_houses_list[i].abs_pos)
@@ -602,7 +606,7 @@ def draw_houses_cusps_and_text_number(
602
606
  i, standard_house_cusp_color
603
607
  )
604
608
 
605
- if chart_type in ["Transit", "Synastry"]:
609
+ if chart_type in ["Transit", "Synastry", "Return"]:
606
610
  if second_subject_houses_list is None or transit_house_cusp_color is None:
607
611
  raise KerykeionException("second_subject_houses_list_ut or transit_house_cusp_color is None")
608
612
 
@@ -637,7 +641,7 @@ def draw_houses_cusps_and_text_number(
637
641
  path += f"</g>"
638
642
 
639
643
  # Adjust dropin based on chart type
640
- dropin = {"Transit": 84, "Synastry": 84, "ExternalNatal": 100}.get(chart_type, 48)
644
+ dropin = {"Transit": 84, "Synastry": 84, "Return": 84, "ExternalNatal": 100}.get(chart_type, 48)
641
645
  xtext = sliceToX(0, (r - dropin), text_offset) + dropin
642
646
  ytext = sliceToY(0, (r - dropin), text_offset) + dropin
643
647
 
@@ -658,7 +662,12 @@ def draw_transit_aspect_list(
658
662
  grid_title: str,
659
663
  aspects_list: Union[list[AspectModel], list[dict]],
660
664
  celestial_point_language: Union[KerykeionLanguageCelestialPointModel, dict],
661
- aspects_settings: Union[KerykeionSettingsAspectModel, dict],
665
+ aspects_settings: dict,
666
+ *,
667
+ aspects_per_column: int = 14,
668
+ column_width: int = 100,
669
+ line_height: int = 14,
670
+ max_columns: int = 6
662
671
  ) -> str:
663
672
  """
664
673
  Generates the SVG output for the aspect transit grid.
@@ -666,8 +675,12 @@ def draw_transit_aspect_list(
666
675
  Parameters:
667
676
  - grid_title: Title of the grid.
668
677
  - aspects_list: List of aspects.
669
- - planets_labels: Dictionary containing the planet labels.
678
+ - celestial_point_language: Dictionary containing the celestial point language data.
670
679
  - aspects_settings: Dictionary containing the aspect settings.
680
+ - aspects_per_column: Number of aspects to display per column (default: 14).
681
+ - column_width: Width in pixels for each column (default: 100).
682
+ - line_height: Height in pixels for each line (default: 14).
683
+ - max_columns: Maximum number of columns before vertical adjustment (default: 6).
671
684
 
672
685
  Returns:
673
686
  - A string containing the SVG path data for the aspect transit grid.
@@ -676,65 +689,52 @@ def draw_transit_aspect_list(
676
689
  if isinstance(celestial_point_language, dict):
677
690
  celestial_point_language = KerykeionLanguageCelestialPointModel(**celestial_point_language)
678
691
 
679
- if isinstance(aspects_settings, dict):
680
- aspects_settings = KerykeionSettingsAspectModel(**aspects_settings)
681
-
682
692
  # 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
693
+ if aspects_list and isinstance(aspects_list[0], dict):
694
+ aspects_list = [AspectModel(**aspect) for aspect in aspects_list] # type: ignore
685
695
 
686
- line = 0
687
- nl = 0
688
696
  inner_path = ""
697
+
689
698
  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
699
+ # Calculate which column this aspect belongs in
700
+ current_column = i // aspects_per_column
701
+
702
+ # Calculate horizontal position based on column
703
+ horizontal_position = current_column * column_width
704
+
705
+ # Calculate vertical position within the column
706
+ current_line = i % aspects_per_column
707
+ vertical_position = current_line * line_height
708
+
709
+ # Special handling for many aspects - if we exceed max_columns
710
+ if current_column >= max_columns:
711
+ # Calculate how many aspects will overflow beyond the max columns
712
+ overflow_aspects = len(aspects_list) - (aspects_per_column * max_columns)
713
+ if overflow_aspects > 0:
714
+ # Adjust the starting vertical position to move text up
715
+ vertical_position = vertical_position - (overflow_aspects * line_height)
716
+
717
+ inner_path += f'<g transform="translate({horizontal_position},{vertical_position})">'
718
+
719
+ # First planet symbol
720
+ inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspect["p1"]]["name"]}" />'
721
+
722
+ # Aspect symbol
723
+ aspect_name = aspect["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
727
728
  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'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspect["p2"]]["name"]}" />'
729
730
  inner_path += f"</g>"
730
731
 
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
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(aspect["orbit"])}</text>'
734
+
734
735
  inner_path += f"</g>"
735
- line = line + 14
736
736
 
737
- out = '<g transform="translate(526,273)">'
737
+ out = '<g transform="translate(565,273)">'
738
738
  out += f'<text y="-15" x="0" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 14px;">{grid_title}:</text>'
739
739
  out += inner_path
740
740
  out += '</g>'
@@ -804,31 +804,28 @@ def calculate_moon_phase_chart_params(
804
804
  "lunar_phase_rotate": lunar_phase_rotate,
805
805
  }
806
806
 
807
- def draw_house_grid(
807
+
808
+ def draw_main_house_grid(
808
809
  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
810
  house_cusp_generale_name_label: str = "Cusp",
811
+ text_color: str = "#000000",
812
+ x_position: int = 720,
813
+ y_position: int = 30,
813
814
  ) -> str:
814
815
  """
815
- Generate SVG code for a grid of astrological houses.
816
+ Generate SVG code for a grid of astrological houses for the main subject.
816
817
 
817
818
  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.
819
+ - main_subject_houses_list (list[KerykeionPointModel]): List of houses for the main subject.
820
+ - house_cusp_generale_name_label (str): Label for the house cusp. Defaults to "Cusp".
821
+ - text_color (str): Color of the text. Defaults to "#000000".
822
+ - x_position (int): X position for the grid. Defaults to 720.
823
+ - y_position (int): Y position for the grid. Defaults to 30.
823
824
 
824
825
  Returns:
825
826
  - str: The SVG code for the grid of houses.
826
827
  """
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)">'
828
+ svg_output = f'<g transform="translate({x_position},{y_position})">'
832
829
 
833
830
  line_increment = 10
834
831
  for i, house in enumerate(main_subject_houses_list):
@@ -843,40 +840,57 @@ def draw_house_grid(
843
840
  line_increment += 14
844
841
 
845
842
  svg_output += "</g>"
843
+ return svg_output
846
844
 
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
845
 
863
- svg_output += "</g>"
846
+ def draw_secondary_house_grid(
847
+ secondary_subject_houses_list: list[KerykeionPointModel],
848
+ house_cusp_generale_name_label: str = "Cusp",
849
+ text_color: str = "#000000",
850
+ x_position: int = 970,
851
+ y_position: int = 30,
852
+ ) -> str:
853
+ """
854
+ Generate SVG code for a grid of astrological houses for the secondary subject.
855
+
856
+ Parameters:
857
+ - secondary_subject_houses_list (list[KerykeionPointModel]): List of houses for the secondary subject.
858
+ - house_cusp_generale_name_label (str): Label for the house cusp. Defaults to "Cusp".
859
+ - text_color (str): Color of the text. Defaults to "#000000".
860
+ - x_position (int): X position for the grid. Defaults to 970.
861
+ - y_position (int): Y position for the grid. Defaults to 30.
862
+
863
+ Returns:
864
+ - str: The SVG code for the grid of houses.
865
+ """
866
+ svg_output = f'<g transform="translate({x_position},{y_position})">'
867
+
868
+ line_increment = 10
869
+ for i, house in enumerate(secondary_subject_houses_list):
870
+ cusp_number = f"&#160;&#160;{i + 1}" if i < 9 else str(i + 1)
871
+ svg_output += (
872
+ f'<g transform="translate(0,{line_increment})">'
873
+ f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
874
+ f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
875
+ f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
876
+ f'</g>'
877
+ )
878
+ line_increment += 14
864
879
 
880
+ svg_output += "</g>"
865
881
  return svg_output
866
882
 
867
883
 
868
- def draw_planet_grid(
884
+ def draw_main_planet_grid(
869
885
  planets_and_houses_grid_title: str,
870
886
  subject_name: str,
871
887
  available_kerykeion_celestial_points: list[KerykeionPointModel],
872
888
  chart_type: ChartType,
873
889
  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
890
  text_color: str = "#000000",
877
891
  ) -> str:
878
892
  """
879
- Draws the planet grid for the given celestial points and chart type.
893
+ Draws the planet grid for the main subject.
880
894
 
881
895
  Args:
882
896
  planets_and_houses_grid_title (str): Title of the grid.
@@ -884,33 +898,47 @@ def draw_planet_grid(
884
898
  available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the subject.
885
899
  chart_type (ChartType): Type of the chart.
886
900
  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
901
  text_color (str, optional): Color of the text. Defaults to "#000000".
890
902
 
891
903
  Returns:
892
- str: The SVG output for the planet grid.
904
+ str: The SVG output for the main planet grid.
893
905
  """
894
906
  line_height = 10
895
907
  offset = 0
896
908
  offset_between_lines = 14
909
+ svg_output = ""
897
910
 
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
- )
911
+ if chart_type == "Synastry":
912
+ svg_output += (
913
+ f'<g transform="translate(620, 15)">' # Added the 620,30 offset (adjusted for -15)
914
+ f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
915
+ f'</g>'
916
+ )
917
+ elif chart_type == "Transit":
918
+ svg_output += (
919
+ f'<g transform="translate(620, 15)">' # Added the 620,30 offset (adjusted for -15)
920
+ f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
921
+ f'</g>'
922
+ )
923
+ elif chart_type == "Return":
924
+ svg_output += (
925
+ f'<g transform="translate(620, 15)">' # Added the 620,30 offset (adjusted for -15)
926
+ f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
927
+ f'</g>'
928
+ )
929
+ else:
930
+ svg_output += ""
903
931
 
904
932
  end_of_line = "</g>"
905
933
 
906
934
  for i, planet in enumerate(available_kerykeion_celestial_points):
907
- if i == 27:
935
+ if i == 22:
908
936
  line_height = 10
909
- offset = -120
937
+ offset = -125
910
938
 
911
939
  decoded_name = get_decoded_kerykeion_celestial_point_name(planet["name"], celestial_point_language)
912
940
  svg_output += (
913
- f'<g transform="translate({offset},{line_height})">'
941
+ f'<g transform="translate({620 + offset},{30 + line_height})">' # Added the 620,30 offset
914
942
  f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{decoded_name}</text>'
915
943
  f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{planet["name"]}" /></g>'
916
944
  f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(planet["position"])}</text>'
@@ -923,45 +951,70 @@ def draw_planet_grid(
923
951
  svg_output += end_of_line
924
952
  line_height += offset_between_lines
925
953
 
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")
954
+ return svg_output
929
955
 
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
956
 
941
- svg_output += end_of_line
957
+ def draw_secondary_planet_grid(
958
+ planets_and_houses_grid_title: str,
959
+ second_subject_name: str,
960
+ second_subject_available_kerykeion_celestial_points: list[KerykeionPointModel],
961
+ chart_type: ChartType,
962
+ celestial_point_language: KerykeionLanguageCelestialPointModel,
963
+ text_color: str = "#000000",
964
+ ) -> str:
965
+ """
966
+ Draws the planet grid for the secondary subject in Transit, Synastry, or Return charts.
942
967
 
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
- )
968
+ Args:
969
+ planets_and_houses_grid_title (str): Title of the grid.
970
+ second_subject_name (str): Name of the second subject.
971
+ second_subject_available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the second subject.
972
+ chart_type (ChartType): Type of the chart.
973
+ celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points.
974
+ text_color (str, optional): Color of the text. Defaults to "#000000".
959
975
 
960
- if t_planet["retrograde"]:
961
- svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
976
+ Returns:
977
+ str: The SVG output for the secondary planet grid.
978
+ """
979
+ svg_output = ""
980
+ end_of_line = "</g>"
962
981
 
963
- svg_output += end_of_line
964
- second_line_height += offset_between_lines
982
+ if chart_type == "Transit":
983
+ svg_output += (
984
+ f'<g transform="translate(820, 15)">' # 620+200, 30-15
985
+ f'<text style="fill:{text_color}; font-size: 14px;">{second_subject_name}</text>'
986
+ )
987
+ elif chart_type == "Return":
988
+ svg_output += (
989
+ f'<g transform="translate(870, 15)">' # 620+250, 30-15
990
+ f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}</text>'
991
+ )
992
+ else:
993
+ svg_output += (
994
+ f'<g transform="translate(870, 15)">' # 620+250, 30-15
995
+ f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}</text>'
996
+ )
997
+
998
+ svg_output += end_of_line
999
+
1000
+ line_height = 10
1001
+ offset = 250
1002
+
1003
+ for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
1004
+ second_decoded_name = get_decoded_kerykeion_celestial_point_name(t_planet["name"], celestial_point_language)
1005
+ svg_output += (
1006
+ f'<g transform="translate({620 + offset},{30 + line_height})">' # Added the 620,30 offset
1007
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{second_decoded_name}</text>'
1008
+ f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
1009
+ f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(t_planet["position"])}</text>'
1010
+ f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{t_planet["sign"]}" /></g>'
1011
+ )
1012
+
1013
+ if t_planet["retrograde"]:
1014
+ svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
1015
+
1016
+ svg_output += end_of_line
1017
+ line_height += 14 # Using fixed offset_between_lines value
965
1018
 
966
1019
  return svg_output
967
1020
 
@@ -994,7 +1047,7 @@ def draw_transit_aspect_grid(
994
1047
  y_start = y_indent
995
1048
 
996
1049
  # Filter active planets
997
- active_planets = [planet for planet in available_planets if planet.is_active]
1050
+ active_planets = [planet for planet in available_planets if planet["is_active"]]
998
1051
 
999
1052
  # Reverse the list of active planets for the first iteration
1000
1053
  reversed_planets = active_planets[::-1]
@@ -1040,3 +1093,480 @@ def draw_transit_aspect_grid(
1040
1093
  svg_output += f'<use x="{x_aspect - box_size + 1}" y="{y_aspect + 1}" xlink:href="#orb{aspect["aspect_degrees"]}" />'
1041
1094
 
1042
1095
  return svg_output
1096
+
1097
+
1098
+ def format_location_string(location: str, max_length: int = 35) -> str:
1099
+ """
1100
+ Format a location string to ensure it fits within a specified maximum length.
1101
+
1102
+ If the location is longer than max_length, it attempts to shorten by using only
1103
+ the first and last parts separated by commas. If still too long, it truncates
1104
+ and adds ellipsis.
1105
+
1106
+ Args:
1107
+ location: The original location string
1108
+ max_length: Maximum allowed length for the output string (default: 35)
1109
+
1110
+ Returns:
1111
+ Formatted location string that fits within max_length
1112
+ """
1113
+ if len(location) > max_length:
1114
+ split_location = location.split(",")
1115
+ if len(split_location) > 1:
1116
+ shortened = split_location[0] + ", " + split_location[-1]
1117
+ if len(shortened) > max_length:
1118
+ return shortened[:max_length] + "..."
1119
+ return shortened
1120
+ else:
1121
+ return location[:max_length] + "..."
1122
+ return location
1123
+
1124
+
1125
+ def format_datetime_with_timezone(iso_datetime_string: str) -> str:
1126
+ """
1127
+ Format an ISO datetime string with a custom format that includes properly formatted timezone.
1128
+
1129
+ Args:
1130
+ iso_datetime_string: ISO formatted datetime string
1131
+
1132
+ Returns:
1133
+ Formatted datetime string with properly formatted timezone offset (HH:MM)
1134
+ """
1135
+ dt = datetime.datetime.fromisoformat(iso_datetime_string)
1136
+ custom_format = dt.strftime('%Y-%m-%d %H:%M [%z]')
1137
+ custom_format = custom_format[:-3] + ':' + custom_format[-3:]
1138
+
1139
+ return custom_format
1140
+
1141
+
1142
+ def calculate_element_points(
1143
+ planets_settings: list[KerykeionSettingsCelestialPointModel],
1144
+ celestial_points_names: list[str],
1145
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
1146
+ ):
1147
+ """
1148
+ Calculate elemental point totals based on planetary positions.
1149
+
1150
+ Args:
1151
+ planets_settings (list): List of planet configuration dictionaries
1152
+ celestial_points_names (list): List of celestial point names to process
1153
+ subject: Astrological subject with get() method for accessing planet data
1154
+
1155
+ Returns:
1156
+ dict: Dictionary with element point totals for 'fire', 'earth', 'air', and 'water'
1157
+ """
1158
+ ZODIAC = (
1159
+ {"name": "Ari", "element": "fire"},
1160
+ {"name": "Tau", "element": "earth"},
1161
+ {"name": "Gem", "element": "air"},
1162
+ {"name": "Can", "element": "water"},
1163
+ {"name": "Leo", "element": "fire"},
1164
+ {"name": "Vir", "element": "earth"},
1165
+ {"name": "Lib", "element": "air"},
1166
+ {"name": "Sco", "element": "water"},
1167
+ {"name": "Sag", "element": "fire"},
1168
+ {"name": "Cap", "element": "earth"},
1169
+ {"name": "Aqu", "element": "air"},
1170
+ {"name": "Pis", "element": "water"},
1171
+ )
1172
+
1173
+ # Initialize element point totals
1174
+ element_totals = {
1175
+ "fire": 0.0,
1176
+ "earth": 0.0,
1177
+ "air": 0.0,
1178
+ "water": 0.0
1179
+ }
1180
+
1181
+ # Make list of the points sign
1182
+ points_sign = [subject.get(planet).sign_num for planet in celestial_points_names]
1183
+
1184
+ for i in range(len(planets_settings)):
1185
+ # Add points to appropriate element
1186
+ element = ZODIAC[points_sign[i]]["element"]
1187
+ element_totals[element] += planets_settings[i]["element_points"]
1188
+
1189
+ return element_totals
1190
+
1191
+
1192
+ def calculate_synastry_element_points(
1193
+ planets_settings: list[KerykeionSettingsCelestialPointModel],
1194
+ celestial_points_names: list[str],
1195
+ subject1: AstrologicalSubjectModel,
1196
+ subject2: AstrologicalSubjectModel,
1197
+ ):
1198
+ """
1199
+ Calculate elemental point totals for both subjects in a synastry chart.
1200
+
1201
+ Args:
1202
+ planets_settings (list): List of planet configuration dictionaries
1203
+ celestial_points_names (list): List of celestial point names to process
1204
+ subject1: First astrological subject with get() method for accessing planet data
1205
+ subject2: Second astrological subject with get() method for accessing planet data
1206
+
1207
+ Returns:
1208
+ dict: Dictionary with element point totals as percentages, where the sum equals 100%
1209
+ """
1210
+ ZODIAC = (
1211
+ {"name": "Ari", "element": "fire"},
1212
+ {"name": "Tau", "element": "earth"},
1213
+ {"name": "Gem", "element": "air"},
1214
+ {"name": "Can", "element": "water"},
1215
+ {"name": "Leo", "element": "fire"},
1216
+ {"name": "Vir", "element": "earth"},
1217
+ {"name": "Lib", "element": "air"},
1218
+ {"name": "Sco", "element": "water"},
1219
+ {"name": "Sag", "element": "fire"},
1220
+ {"name": "Cap", "element": "earth"},
1221
+ {"name": "Aqu", "element": "air"},
1222
+ {"name": "Pis", "element": "water"},
1223
+ )
1224
+
1225
+ # Initialize combined element point totals
1226
+ combined_totals = {
1227
+ "fire": 0.0,
1228
+ "earth": 0.0,
1229
+ "air": 0.0,
1230
+ "water": 0.0
1231
+ }
1232
+
1233
+ # Make list of the points sign for both subjects
1234
+ subject1_points_sign = [subject1.get(planet).sign_num for planet in celestial_points_names]
1235
+ subject2_points_sign = [subject2.get(planet).sign_num for planet in celestial_points_names]
1236
+
1237
+ # Calculate element points for subject 1
1238
+ for i in range(len(planets_settings)):
1239
+ # Add points to appropriate element
1240
+ element1 = ZODIAC[subject1_points_sign[i]]["element"]
1241
+ combined_totals[element1] += planets_settings[i]["element_points"]
1242
+
1243
+ # Calculate element points for subject 2
1244
+ for i in range(len(planets_settings)):
1245
+ # Add points to appropriate element
1246
+ element2 = ZODIAC[subject2_points_sign[i]]["element"]
1247
+ combined_totals[element2] += planets_settings[i]["element_points"]
1248
+
1249
+ # Calculate total points across all elements
1250
+ total_points = sum(combined_totals.values())
1251
+
1252
+ # Convert to percentages (total = 100%)
1253
+ if total_points > 0:
1254
+ for element in combined_totals:
1255
+ combined_totals[element] = (combined_totals[element] / total_points) * 100.0
1256
+
1257
+ return combined_totals
1258
+
1259
+
1260
+ def draw_house_comparison_grid(
1261
+ house_comparison: "HouseComparisonModel",
1262
+ celestial_point_language: KerykeionLanguageCelestialPointModel,
1263
+ active_points: list[AstrologicalPoint],
1264
+ *,
1265
+ points_owner_subject_number: Literal[1, 2] = 1,
1266
+ text_color: str = "var(--kerykeion-color-neutral-content)",
1267
+ house_position_comparison_label: str = "House Position Comparison",
1268
+ return_point_label: str = "Return Point",
1269
+ return_label: str = "Return",
1270
+ radix_label: str = "Radix",
1271
+ x_position: int = 1030,
1272
+ y_position: int = 0,
1273
+ ) -> str:
1274
+ """
1275
+ Generate SVG code for displaying a comparison of points across houses between two charts.
1276
+
1277
+ Parameters:
1278
+ - house_comparison ("HouseComparisonModel"): Model containing house comparison data,
1279
+ including first_subject_name, second_subject_name, and points in houses.
1280
+ - celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points
1281
+ - active_celestial_points (list[KerykeionPointModel]): List of active celestial points to display
1282
+ - text_color (str): Color for the text elements
1283
+
1284
+ Returns:
1285
+ - str: SVG code for the house comparison grid.
1286
+ """
1287
+ if points_owner_subject_number == 1:
1288
+ comparison_data = house_comparison.first_points_in_second_houses
1289
+ else:
1290
+ comparison_data = house_comparison.second_points_in_first_houses
1291
+
1292
+ svg_output = f'<g transform="translate({x_position},{y_position})">'
1293
+
1294
+ # Add title
1295
+ svg_output += f'<text text-anchor="start" x="0" y="-15" style="fill:{text_color}; font-size: 14px;">{house_position_comparison_label}</text>'
1296
+
1297
+ # Add column headers
1298
+ line_increment = 10
1299
+ svg_output += (
1300
+ f'<g transform="translate(0,{line_increment})">'
1301
+ f'<text text-anchor="start" x="0" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{return_point_label}</text>'
1302
+ f'<text text-anchor="start" x="77" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{return_label}</text>'
1303
+ f'<text text-anchor="start" x="132" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{radix_label}</text>'
1304
+ f'</g>'
1305
+ )
1306
+ line_increment += 15
1307
+
1308
+ # Create a dictionary to store all points by name for combined display
1309
+ all_points_by_name = {}
1310
+
1311
+ for point in comparison_data:
1312
+ # Only process points that are active
1313
+ if point.point_name in active_points and point.point_name not in all_points_by_name:
1314
+ all_points_by_name[point.point_name] = {
1315
+ "name": point.point_name,
1316
+ "secondary_house": point.projected_house_number,
1317
+ "native_house": point.point_owner_house_number
1318
+ }
1319
+
1320
+ # Display all points organized by name
1321
+ for name, point_data in all_points_by_name.items():
1322
+ native_house = point_data.get("native_house", "-")
1323
+ secondary_house = point_data.get("secondary_house", "-")
1324
+
1325
+ svg_output += (
1326
+ f'<g transform="translate(0,{line_increment})">'
1327
+ f'<g transform="translate(0,-9)"><use transform="scale(0.4)" xlink:href="#{name}" /></g>'
1328
+ f'<text text-anchor="start" x="15" style="fill:{text_color}; font-size: 10px;">{get_decoded_kerykeion_celestial_point_name(name, celestial_point_language)}</text>'
1329
+ f'<text text-anchor="start" x="90" style="fill:{text_color}; font-size: 10px;">{native_house}</text>'
1330
+ f'<text text-anchor="start" x="140" style="fill:{text_color}; font-size: 10px;">{secondary_house}</text>'
1331
+ f'</g>'
1332
+ )
1333
+ line_increment += 12
1334
+
1335
+ svg_output += "</g>"
1336
+
1337
+ return svg_output
1338
+
1339
+
1340
+ def draw_single_house_comparison_grid(
1341
+ house_comparison: "HouseComparisonModel",
1342
+ celestial_point_language: KerykeionLanguageCelestialPointModel,
1343
+ active_points: list[AstrologicalPoint],
1344
+ *,
1345
+ points_owner_subject_number: Literal[1, 2] = 1,
1346
+ text_color: str = "var(--kerykeion-color-neutral-content)",
1347
+ house_position_comparison_label: str = "House Position Comparison",
1348
+ return_point_label: str = "Return Point",
1349
+ natal_house_label: str = "Natal House",
1350
+ x_position: int = 1030,
1351
+ y_position: int = 0,
1352
+ ) -> str:
1353
+ """
1354
+ Generate SVG code for displaying celestial points and their house positions.
1355
+
1356
+ Parameters:
1357
+ - house_comparison ("HouseComparisonModel"): Model containing house comparison data,
1358
+ including first_subject_name, second_subject_name, and points in houses.
1359
+ - celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points
1360
+ - active_points (list[AstrologicalPoint]): List of active celestial points to display
1361
+ - points_owner_subject_number (Literal[1, 2]): Which subject's points to display (1 for first, 2 for second)
1362
+ - text_color (str): Color for the text elements
1363
+ - house_position_comparison_label (str): Label for the house position comparison grid
1364
+ - return_point_label (str): Label for the return point column
1365
+ - house_position_label (str): Label for the house position column
1366
+ - x_position (int): X position for the grid
1367
+ - y_position (int): Y position for the grid
1368
+
1369
+ Returns:
1370
+ - str: SVG code for the house position grid.
1371
+ """
1372
+ if points_owner_subject_number == 1:
1373
+ comparison_data = house_comparison.first_points_in_second_houses
1374
+ else:
1375
+ comparison_data = house_comparison.second_points_in_first_houses
1376
+
1377
+ svg_output = f'<g transform="translate({x_position},{y_position})">'
1378
+
1379
+ # Add title
1380
+ svg_output += f'<text text-anchor="start" x="0" y="-15" style="fill:{text_color}; font-size: 14px;">{house_position_comparison_label}</text>'
1381
+
1382
+ # Add column headers
1383
+ line_increment = 10
1384
+ svg_output += (
1385
+ f'<g transform="translate(0,{line_increment})">'
1386
+ f'<text text-anchor="start" x="0" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{return_point_label}</text>'
1387
+ f'<text text-anchor="start" x="77" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{natal_house_label}</text>'
1388
+ f'</g>'
1389
+ )
1390
+ line_increment += 15
1391
+
1392
+ # Create a dictionary to store all points by name for combined display
1393
+ all_points_by_name = {}
1394
+
1395
+ for point in comparison_data:
1396
+ # Only process points that are active
1397
+ if point.point_name in active_points and point.point_name not in all_points_by_name:
1398
+ all_points_by_name[point.point_name] = {
1399
+ "name": point.point_name,
1400
+ "house": point.projected_house_number
1401
+ }
1402
+
1403
+ # Display all points organized by name
1404
+ for name, point_data in all_points_by_name.items():
1405
+ house = point_data.get("house", "-")
1406
+
1407
+ svg_output += (
1408
+ f'<g transform="translate(0,{line_increment})">'
1409
+ f'<g transform="translate(0,-9)"><use transform="scale(0.4)" xlink:href="#{name}" /></g>'
1410
+ f'<text text-anchor="start" x="15" style="fill:{text_color}; font-size: 10px;">{get_decoded_kerykeion_celestial_point_name(name, celestial_point_language)}</text>'
1411
+ f'<text text-anchor="start" x="90" style="fill:{text_color}; font-size: 10px;">{house}</text>'
1412
+ f'</g>'
1413
+ )
1414
+ line_increment += 12
1415
+
1416
+ svg_output += "</g>"
1417
+
1418
+ return svg_output
1419
+
1420
+
1421
+ def makeLunarPhase(degrees_between_sun_and_moon: float, latitude: float) -> str:
1422
+ """
1423
+ Generate SVG representation of lunar phase.
1424
+
1425
+ Parameters:
1426
+ - degrees_between_sun_and_moon (float): Angle between sun and moon in degrees
1427
+ - latitude (float): Observer's latitude for correct orientation
1428
+
1429
+ Returns:
1430
+ - str: SVG representation of lunar phase
1431
+ """
1432
+ # Calculate parameters for the lunar phase visualization
1433
+ params = calculate_moon_phase_chart_params(degrees_between_sun_and_moon, latitude)
1434
+
1435
+ # Extract the calculated values
1436
+ lunar_phase_circle_center_x = params["circle_center_x"]
1437
+ lunar_phase_circle_radius = params["circle_radius"]
1438
+ lunar_phase_rotate = params["lunar_phase_rotate"]
1439
+
1440
+ # Generate the SVG for the lunar phase
1441
+ svg = (
1442
+ f'<g transform="rotate({lunar_phase_rotate} 20 10)">\n'
1443
+ f' <defs>\n'
1444
+ f' <clipPath id="moonPhaseCutOffCircle">\n'
1445
+ f' <circle cx="20" cy="10" r="10" />\n'
1446
+ f' </clipPath>\n'
1447
+ f' </defs>\n'
1448
+ f' <circle cx="20" cy="10" r="10" style="fill: var(--kerykeion-chart-color-lunar-phase-0)" />\n'
1449
+ f' <circle cx="{lunar_phase_circle_center_x}" cy="10" r="{lunar_phase_circle_radius}" style="fill: var(--kerykeion-chart-color-lunar-phase-1)" clip-path="url(#moonPhaseCutOffCircle)" />\n'
1450
+ f' <circle cx="20" cy="10" r="10" style="fill: none; stroke: var(--kerykeion-chart-color-lunar-phase-0); stroke-width: 0.5px; stroke-opacity: 0.5" />\n'
1451
+ f'</g>'
1452
+ )
1453
+
1454
+ return svg
1455
+
1456
+
1457
+ def calculate_quality_points(
1458
+ planets_settings: list[KerykeionSettingsCelestialPointModel],
1459
+ celestial_points_names: list[str],
1460
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
1461
+ ):
1462
+ """
1463
+ Calculate quality point totals based on planetary positions.
1464
+
1465
+ Args:
1466
+ planets_settings (list): List of planet configuration dictionaries
1467
+ celestial_points_names (list): List of celestial point names to process
1468
+ subject: Astrological subject with get() method for accessing planet data
1469
+ planet_in_zodiac_extra_points (int): Extra points awarded for planets in their home sign
1470
+
1471
+ Returns:
1472
+ dict: Dictionary with quality point totals for 'cardinal', 'fixed', and 'mutable'
1473
+ """
1474
+ ZODIAC = (
1475
+ {"name": "Ari", "quality": "cardinal"},
1476
+ {"name": "Tau", "quality": "fixed"},
1477
+ {"name": "Gem", "quality": "mutable"},
1478
+ {"name": "Can", "quality": "cardinal"},
1479
+ {"name": "Leo", "quality": "fixed"},
1480
+ {"name": "Vir", "quality": "mutable"},
1481
+ {"name": "Lib", "quality": "cardinal"},
1482
+ {"name": "Sco", "quality": "fixed"},
1483
+ {"name": "Sag", "quality": "mutable"},
1484
+ {"name": "Cap", "quality": "cardinal"},
1485
+ {"name": "Aqu", "quality": "fixed"},
1486
+ {"name": "Pis", "quality": "mutable"},
1487
+ )
1488
+
1489
+ # Initialize quality point totals
1490
+ quality_totals = {
1491
+ "cardinal": 0.0,
1492
+ "fixed": 0.0,
1493
+ "mutable": 0.0
1494
+ }
1495
+
1496
+ # Make list of the points sign
1497
+ points_sign = [subject.get(planet).sign_num for planet in celestial_points_names]
1498
+
1499
+ for i in range(len(planets_settings)):
1500
+ # Add points to appropriate quality
1501
+ quality = ZODIAC[points_sign[i]]["quality"]
1502
+ quality_totals[quality] += planets_settings[i]["element_points"]
1503
+
1504
+ return quality_totals
1505
+
1506
+
1507
+ def calculate_synastry_quality_points(
1508
+ planets_settings: list[KerykeionSettingsCelestialPointModel],
1509
+ celestial_points_names: list[str],
1510
+ subject1: AstrologicalSubjectModel,
1511
+ subject2: AstrologicalSubjectModel,
1512
+ ):
1513
+ """
1514
+ Calculate quality point totals for both subjects in a synastry chart.
1515
+
1516
+ Args:
1517
+ planets_settings (list): List of planet configuration dictionaries
1518
+ celestial_points_names (list): List of celestial point names to process
1519
+ subject1: First astrological subject with get() method for accessing planet data
1520
+ subject2: Second astrological subject with get() method for accessing planet data
1521
+
1522
+ Returns:
1523
+ dict: Dictionary with quality point totals as percentages, where the sum equals 100%
1524
+ """
1525
+ ZODIAC = (
1526
+ {"name": "Ari", "quality": "cardinal"},
1527
+ {"name": "Tau", "quality": "fixed"},
1528
+ {"name": "Gem", "quality": "mutable"},
1529
+ {"name": "Can", "quality": "cardinal"},
1530
+ {"name": "Leo", "quality": "fixed"},
1531
+ {"name": "Vir", "quality": "mutable"},
1532
+ {"name": "Lib", "quality": "cardinal"},
1533
+ {"name": "Sco", "quality": "fixed"},
1534
+ {"name": "Sag", "quality": "mutable"},
1535
+ {"name": "Cap", "quality": "cardinal"},
1536
+ {"name": "Aqu", "quality": "fixed"},
1537
+ {"name": "Pis", "quality": "mutable"},
1538
+ )
1539
+
1540
+ # Initialize combined quality point totals
1541
+ combined_totals = {
1542
+ "cardinal": 0.0,
1543
+ "fixed": 0.0,
1544
+ "mutable": 0.0
1545
+ }
1546
+
1547
+ # Make list of the points sign for both subjects
1548
+ subject1_points_sign = [subject1.get(planet).sign_num for planet in celestial_points_names]
1549
+ subject2_points_sign = [subject2.get(planet).sign_num for planet in celestial_points_names]
1550
+
1551
+ # Calculate quality points for subject 1
1552
+ for i in range(len(planets_settings)):
1553
+ # Add points to appropriate quality
1554
+ quality1 = ZODIAC[subject1_points_sign[i]]["quality"]
1555
+ combined_totals[quality1] += planets_settings[i]["element_points"]
1556
+
1557
+ # Calculate quality points for subject 2
1558
+ for i in range(len(planets_settings)):
1559
+ # Add points to appropriate quality
1560
+ quality2 = ZODIAC[subject2_points_sign[i]]["quality"]
1561
+ combined_totals[quality2] += planets_settings[i]["element_points"]
1562
+
1563
+ # Calculate total points across all qualities
1564
+ total_points = sum(combined_totals.values())
1565
+
1566
+ # Convert to percentages (total = 100%)
1567
+ if total_points > 0:
1568
+ for quality in combined_totals:
1569
+ combined_totals[quality] = (combined_totals[quality] / total_points) * 100.0
1570
+
1571
+ return combined_totals
1572
+