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