kerykeion 5.0.0a11__py3-none-any.whl → 5.0.0b1__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 (56) hide show
  1. kerykeion/__init__.py +32 -9
  2. kerykeion/aspects/__init__.py +2 -4
  3. kerykeion/aspects/aspects_factory.py +530 -0
  4. kerykeion/aspects/aspects_utils.py +75 -6
  5. kerykeion/astrological_subject_factory.py +380 -229
  6. kerykeion/backword.py +680 -0
  7. kerykeion/chart_data_factory.py +484 -0
  8. kerykeion/charts/{kerykeion_chart_svg.py → chart_drawer.py} +612 -439
  9. kerykeion/charts/charts_utils.py +135 -94
  10. kerykeion/charts/draw_planets.py +38 -28
  11. kerykeion/charts/templates/aspect_grid_only.xml +188 -17
  12. kerykeion/charts/templates/chart.xml +104 -28
  13. kerykeion/charts/templates/wheel_only.xml +195 -24
  14. kerykeion/charts/themes/classic.css +11 -0
  15. kerykeion/charts/themes/dark-high-contrast.css +11 -0
  16. kerykeion/charts/themes/dark.css +11 -0
  17. kerykeion/charts/themes/light.css +11 -0
  18. kerykeion/charts/themes/strawberry.css +10 -0
  19. kerykeion/composite_subject_factory.py +4 -4
  20. kerykeion/ephemeris_data_factory.py +12 -9
  21. kerykeion/house_comparison/__init__.py +0 -3
  22. kerykeion/house_comparison/house_comparison_factory.py +51 -18
  23. kerykeion/house_comparison/house_comparison_utils.py +37 -8
  24. kerykeion/planetary_return_factory.py +8 -4
  25. kerykeion/relationship_score_factory.py +5 -5
  26. kerykeion/report.py +748 -67
  27. kerykeion/{kr_types → schemas}/__init__.py +44 -4
  28. kerykeion/schemas/chart_template_model.py +340 -0
  29. kerykeion/{kr_types → schemas}/kr_literals.py +7 -3
  30. kerykeion/{kr_types → schemas}/kr_models.py +247 -21
  31. kerykeion/{kr_types → schemas}/settings_models.py +7 -7
  32. kerykeion/settings/config_constants.py +75 -8
  33. kerykeion/settings/kerykeion_settings.py +1 -1
  34. kerykeion/settings/kr.config.json +130 -40
  35. kerykeion/settings/legacy/legacy_celestial_points_settings.py +8 -8
  36. kerykeion/sweph/ast136/s136108s.se1 +0 -0
  37. kerykeion/sweph/ast136/s136199s.se1 +0 -0
  38. kerykeion/sweph/ast136/s136472s.se1 +0 -0
  39. kerykeion/sweph/ast28/se28978s.se1 +0 -0
  40. kerykeion/sweph/ast50/se50000s.se1 +0 -0
  41. kerykeion/sweph/ast90/se90377s.se1 +0 -0
  42. kerykeion/sweph/ast90/se90482s.se1 +0 -0
  43. kerykeion/sweph/sefstars.txt +1602 -0
  44. kerykeion/transits_time_range_factory.py +11 -11
  45. kerykeion/utilities.py +61 -38
  46. kerykeion-5.0.0b1.dist-info/METADATA +1055 -0
  47. kerykeion-5.0.0b1.dist-info/RECORD +58 -0
  48. kerykeion/aspects/natal_aspects_factory.py +0 -235
  49. kerykeion/aspects/synastry_aspects_factory.py +0 -275
  50. kerykeion/house_comparison/house_comparison_models.py +0 -38
  51. kerykeion/kr_types/chart_types.py +0 -106
  52. kerykeion-5.0.0a11.dist-info/METADATA +0 -641
  53. kerykeion-5.0.0a11.dist-info/RECORD +0 -50
  54. /kerykeion/{kr_types → schemas}/kerykeion_exception.py +0 -0
  55. {kerykeion-5.0.0a11.dist-info → kerykeion-5.0.0b1.dist-info}/WHEEL +0 -0
  56. {kerykeion-5.0.0a11.dist-info → kerykeion-5.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,10 @@
1
1
  import math
2
2
  import datetime
3
- from kerykeion.kr_types import KerykeionException, ChartType
4
- from kerykeion.kr_types.kr_literals import AstrologicalPoint
3
+ from kerykeion.schemas import KerykeionException, ChartType
4
+ from kerykeion.schemas.kr_literals import AstrologicalPoint
5
5
  from typing import Union, Literal
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
6
+ from kerykeion.schemas.kr_models import AspectModel, KerykeionPointModel, CompositeSubjectModel, PlanetReturnModel, AstrologicalSubjectModel, HouseComparisonModel
7
+ from kerykeion.schemas.settings_models import KerykeionLanguageCelestialPointModel, KerykeionSettingsCelestialPointModel
9
8
 
10
9
 
11
10
 
@@ -189,7 +188,7 @@ def draw_zodiac_slice(
189
188
  # pie slices
190
189
  offset = 360 - seventh_house_degree_ut
191
190
  # check transit
192
- if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
191
+ if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "DualReturnChart":
193
192
  dropin: Union[int, float] = 0
194
193
  else:
195
194
  dropin = c1
@@ -198,7 +197,7 @@ def draw_zodiac_slice(
198
197
  # symbols
199
198
  offset = offset + 15
200
199
  # check transit
201
- if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
200
+ if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "DualReturnChart":
202
201
  dropin = 54
203
202
  else:
204
203
  dropin = 18 + c1
@@ -287,7 +286,7 @@ def draw_aspect_line(
287
286
  y2 = sliceToY(0, ar, second_offset) + (r - ar)
288
287
 
289
288
  return (
290
- 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'<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"]}" kr:orb="{aspect["orbit"]}" kr:aspectdegrees="{aspect["aspect_degrees"]}" kr:planetsdiff="{aspect["diff"]}" kr:aspectmovement="{aspect["aspect_movement"]}">'
291
290
  f'<line class="aspect" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {color}; stroke-width: 1; stroke-opacity: .9;"/>'
292
291
  f"</g>"
293
292
  )
@@ -419,7 +418,7 @@ def draw_first_circle(
419
418
  Returns:
420
419
  str: The SVG path of the first circle.
421
420
  """
422
- if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "Return":
421
+ if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "DualReturnChart":
423
422
  return f'<circle cx="{r}" cy="{r}" r="{r - 36}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.4;" />'
424
423
  else:
425
424
  if c1 is None:
@@ -462,7 +461,7 @@ def draw_second_circle(
462
461
  str: The SVG path of the second circle.
463
462
  """
464
463
 
465
- if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "Return":
464
+ if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "DualReturnChart":
466
465
  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" />'
467
466
 
468
467
  else:
@@ -492,7 +491,7 @@ def draw_third_circle(
492
491
  Returns:
493
492
  - str: The SVG element as a string.
494
493
  """
495
- if chart_type in {"Synastry", "Transit", "Return"}:
494
+ if chart_type in {"Synastry", "Transit", "DualReturnChart"}:
496
495
  # For Synastry and Transit charts, use a fixed radius adjustment of 160
497
496
  return f'<circle cx="{radius}" cy="{radius}" r="{radius - 160}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
498
497
 
@@ -572,6 +571,7 @@ def draw_houses_cusps_and_text_number(
572
571
  chart_type: ChartType,
573
572
  second_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
574
573
  transit_house_cusp_color: Union[str, None] = None,
574
+ external_view: bool = False,
575
575
  ) -> str:
576
576
  """
577
577
  Draws the houses cusps and text numbers for a given chart type.
@@ -589,9 +589,10 @@ def draw_houses_cusps_and_text_number(
589
589
  - chart_type: Type of the chart (e.g., Transit, Synastry).
590
590
  - second_subject_houses_list: List of house for the second subject (optional).
591
591
  - transit_house_cusp_color: Color for transit house cusps (optional).
592
+ - external_view: Whether to use external view mode for positioning (optional).
592
593
 
593
594
  Returns:
594
- - A string containing the SVG path for the houses cusps and text numbers.
595
+ - A string containing SVG elements for house cusps and numbers.
595
596
  """
596
597
 
597
598
  path = ""
@@ -599,7 +600,7 @@ def draw_houses_cusps_and_text_number(
599
600
 
600
601
  for i in range(xr):
601
602
  # Determine offsets based on chart type
602
- dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry", "Return"] else (c3, c1, False)
603
+ dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry", "DualReturnChart"] else (c3, c1, False)
603
604
 
604
605
  # Calculate the offset for the current house cusp
605
606
  offset = (int(first_subject_houses_list[int(xr / 2)].abs_pos) / -1) + int(first_subject_houses_list[i].abs_pos)
@@ -621,7 +622,7 @@ def draw_houses_cusps_and_text_number(
621
622
  i, standard_house_cusp_color
622
623
  )
623
624
 
624
- if chart_type in ["Transit", "Synastry", "Return"]:
625
+ if chart_type in ["Transit", "Synastry", "DualReturnChart"]:
625
626
  if second_subject_houses_list is None or transit_house_cusp_color is None:
626
627
  raise KerykeionException("second_subject_houses_list_ut or transit_house_cusp_color is None")
627
628
 
@@ -651,17 +652,21 @@ def draw_houses_cusps_and_text_number(
651
652
 
652
653
  # Add the house cusp line for the second subject
653
654
  stroke_opacity = "0" if chart_type == "Transit" else ".3"
654
- path += '<g kr:node="Cusp">'
655
+ path += f'<g kr:node="Cusp" kr:absoluteposition="{second_subject_houses_list[i].abs_pos}" kr:signposition="{second_subject_houses_list[i].position}" kr:sing="{second_subject_houses_list[i].sign}" kr:slug="{second_subject_houses_list[i].name}">'
655
656
  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};'/>"
656
657
  path += "</g>"
657
658
 
658
- # Adjust dropin based on chart type
659
- dropin = {"Transit": 84, "Synastry": 84, "Return": 84, "ExternalNatal": 100}.get(chart_type, 48)
659
+ # Adjust dropin based on chart type and external view
660
+ dropin_map = {"Transit": 84, "Synastry": 84, "DualReturnChart": 84}
661
+ if external_view:
662
+ dropin = 100
663
+ else:
664
+ dropin = dropin_map.get(chart_type, 48)
660
665
  xtext = sliceToX(0, (r - dropin), text_offset) + dropin
661
666
  ytext = sliceToY(0, (r - dropin), text_offset) + dropin
662
667
 
663
668
  # Add the house cusp line for the first subject
664
- path += '<g kr:node="Cusp">'
669
+ path += f'<g kr:node="Cusp" kr:absoluteposition="{first_subject_houses_list[i].abs_pos}" kr:signposition="{first_subject_houses_list[i].position}" kr:sing="{first_subject_houses_list[i].sign}" kr:slug="{first_subject_houses_list[i].name}">'
665
670
  path += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {linecolor}; stroke-width: 1px; stroke-dasharray:3,2; stroke-opacity:.4;"/>'
666
671
  path += "</g>"
667
672
 
@@ -708,9 +713,12 @@ def draw_transit_aspect_list(
708
713
  if aspects_list and isinstance(aspects_list[0], dict):
709
714
  aspects_list = [AspectModel(**aspect) for aspect in aspects_list] # type: ignore
710
715
 
716
+ # Type narrowing: at this point aspects_list contains AspectModel instances
717
+ typed_aspects_list: list[AspectModel] = aspects_list # type: ignore
718
+
711
719
  inner_path = ""
712
720
 
713
- for i, aspect in enumerate(aspects_list):
721
+ for i, aspect in enumerate(typed_aspects_list):
714
722
  # Calculate which column this aspect belongs in
715
723
  current_column = i // aspects_per_column
716
724
 
@@ -722,12 +730,22 @@ def draw_transit_aspect_list(
722
730
  vertical_position = current_line * line_height
723
731
 
724
732
  # Special handling for many aspects - if we exceed max_columns
733
+ # Bottom-align the overflow columns so the list starts from the bottom
725
734
  if current_column >= max_columns:
726
- # Calculate how many aspects will overflow beyond the max columns
727
- overflow_aspects = len(aspects_list) - (aspects_per_column * max_columns)
728
- if overflow_aspects > 0:
729
- # Adjust the starting vertical position to move text up
730
- vertical_position = vertical_position - (overflow_aspects * line_height)
735
+ overflow_total = len(aspects_list) - (aspects_per_column * max_columns)
736
+ if overflow_total > 0:
737
+ # Index within the overflow sequence (beyond the first row of columns)
738
+ overflow_index = i - (aspects_per_column * max_columns)
739
+ # Which overflow column we are in (0-based)
740
+ overflow_col_idx = overflow_index // aspects_per_column
741
+ # How many items go into this overflow column
742
+ items_in_this_column = min(
743
+ aspects_per_column,
744
+ max(0, overflow_total - (overflow_col_idx * aspects_per_column)),
745
+ )
746
+ # Compute extra top offset (in lines) to bottom-align this column
747
+ top_offset_lines = max(0, aspects_per_column - items_in_this_column)
748
+ vertical_position = (top_offset_lines + (i % aspects_per_column)) * line_height
731
749
 
732
750
  inner_path += f'<g transform="translate({horizontal_position},{vertical_position})">'
733
751
 
@@ -824,7 +842,7 @@ def draw_main_house_grid(
824
842
  main_subject_houses_list: list[KerykeionPointModel],
825
843
  house_cusp_generale_name_label: str = "Cusp",
826
844
  text_color: str = "#000000",
827
- x_position: int = 720,
845
+ x_position: int = 750,
828
846
  y_position: int = 30,
829
847
  ) -> str:
830
848
  """
@@ -862,7 +880,7 @@ def draw_secondary_house_grid(
862
880
  secondary_subject_houses_list: list[KerykeionPointModel],
863
881
  house_cusp_generale_name_label: str = "Cusp",
864
882
  text_color: str = "#000000",
865
- x_position: int = 970,
883
+ x_position: int = 1015,
866
884
  y_position: int = 30,
867
885
  ) -> str:
868
886
  """
@@ -903,57 +921,63 @@ def draw_main_planet_grid(
903
921
  chart_type: ChartType,
904
922
  celestial_point_language: KerykeionLanguageCelestialPointModel,
905
923
  text_color: str = "#000000",
924
+ x_position: int = 645,
925
+ y_position: int = 0,
906
926
  ) -> str:
907
927
  """
908
- Draws the planet grid for the main subject.
928
+ Draw the planet grid (main subject) and optional title.
929
+
930
+ The entire output is wrapped in a single SVG group `<g>` so the
931
+ whole block can be repositioned by changing the group transform.
909
932
 
910
933
  Args:
911
- planets_and_houses_grid_title (str): Title of the grid.
912
- subject_name (str): Name of the subject.
913
- available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the subject.
914
- chart_type (ChartType): Type of the chart.
915
- celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points.
916
- text_color (str, optional): Color of the text. Defaults to "#000000".
934
+ planets_and_houses_grid_title: Title prefix to show for eligible chart types.
935
+ subject_name: Subject name to append to the title.
936
+ available_kerykeion_celestial_points: Celestial points to render in the grid.
937
+ chart_type: Chart type identifier (Literal string).
938
+ celestial_point_language: Language model for celestial point decoding.
939
+ text_color: Text color for labels (default: "#000000").
940
+ x_position: X translation applied to the outer `<g>` (default: 620).
941
+ y_position: Y translation applied to the outer `<g>` (default: 0).
917
942
 
918
943
  Returns:
919
- str: The SVG output for the main planet grid.
944
+ SVG string for the main planet grid wrapped in a `<g>`.
920
945
  """
921
- line_height = 10
922
- offset = 0
923
- offset_between_lines = 14
924
- svg_output = ""
946
+ # Layout constants (kept identical to previous behavior)
947
+ BASE_Y = 30
948
+ HEADER_Y = 15 # Title baseline inside the wrapper
949
+ LINE_START = 10
950
+ LINE_STEP = 14
925
951
 
926
- if chart_type == "Synastry":
927
- svg_output += (
928
- f'<g transform="translate(620, 15)">' # Added the 620,30 offset (adjusted for -15)
929
- f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
930
- f'</g>'
931
- )
932
- elif chart_type == "Transit":
933
- svg_output += (
934
- f'<g transform="translate(620, 15)">' # Added the 620,30 offset (adjusted for -15)
935
- f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
936
- f'</g>'
937
- )
938
- elif chart_type == "Return":
952
+ # Wrap everything inside a single group so position can be changed once
953
+ svg_output = f'<g transform="translate({x_position},{y_position})">'
954
+
955
+ # Add title only for specific chart types
956
+ if chart_type in ("Synastry", "Transit", "DualReturnChart"):
939
957
  svg_output += (
940
- f'<g transform="translate(620, 15)">' # Added the 620,30 offset (adjusted for -15)
958
+ f'<g transform="translate(0, {HEADER_Y})">'
941
959
  f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
942
960
  f'</g>'
943
961
  )
944
- else:
945
- svg_output += ""
962
+
963
+ line_height = LINE_START
964
+ offset = 0
946
965
 
947
966
  end_of_line = "</g>"
948
967
 
949
968
  for i, planet in enumerate(available_kerykeion_celestial_points):
950
- if i == 22:
951
- line_height = 10
969
+ # Start a second column at item 23 (index 22)
970
+ if i == 20:
971
+ line_height = LINE_START
952
972
  offset = -125
953
973
 
954
- decoded_name = get_decoded_kerykeion_celestial_point_name(planet["name"], celestial_point_language)
974
+ decoded_name = get_decoded_kerykeion_celestial_point_name(
975
+ planet["name"],
976
+ celestial_point_language,
977
+ )
978
+
955
979
  svg_output += (
956
- f'<g transform="translate({620 + offset},{30 + line_height})">' # Added the 620,30 offset
980
+ f'<g transform="translate({offset},{BASE_Y + line_height})">'
957
981
  f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{decoded_name}</text>'
958
982
  f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{planet["name"]}" /></g>'
959
983
  f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(planet["position"])}</text>'
@@ -964,7 +988,10 @@ def draw_main_planet_grid(
964
988
  svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
965
989
 
966
990
  svg_output += end_of_line
967
- line_height += offset_between_lines
991
+ line_height += LINE_STEP
992
+
993
+ # Close the wrapper group
994
+ svg_output += "</g>"
968
995
 
969
996
  return svg_output
970
997
 
@@ -976,49 +1003,61 @@ def draw_secondary_planet_grid(
976
1003
  chart_type: ChartType,
977
1004
  celestial_point_language: KerykeionLanguageCelestialPointModel,
978
1005
  text_color: str = "#000000",
1006
+ x_position: int = 910,
1007
+ y_position: int = 0,
979
1008
  ) -> str:
980
1009
  """
981
- Draws the planet grid for the secondary subject in Transit, Synastry, or Return charts.
1010
+ Draw the planet grid for the secondary subject and its title.
1011
+
1012
+ The entire output is wrapped in a single SVG group `<g>` so the
1013
+ whole block can be repositioned by changing the group transform.
982
1014
 
983
1015
  Args:
984
- planets_and_houses_grid_title (str): Title of the grid.
985
- second_subject_name (str): Name of the second subject.
986
- second_subject_available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the second subject.
987
- chart_type (ChartType): Type of the chart.
988
- celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points.
989
- text_color (str, optional): Color of the text. Defaults to "#000000".
1016
+ planets_and_houses_grid_title: Title prefix (used except for Transit charts).
1017
+ second_subject_name: Name of the secondary subject.
1018
+ second_subject_available_kerykeion_celestial_points: Celestial points to render for the secondary subject.
1019
+ chart_type: Chart type identifier (Literal string).
1020
+ celestial_point_language: Language model for celestial point decoding.
1021
+ text_color: Text color for labels (default: "#000000").
1022
+ x_position: X translation applied to the outer `<g>` (default: 870).
1023
+ y_position: Y translation applied to the outer `<g>` (default: 0).
990
1024
 
991
1025
  Returns:
992
- str: The SVG output for the secondary planet grid.
1026
+ SVG string for the secondary planet grid wrapped in a `<g>`.
993
1027
  """
994
- svg_output = ""
995
- end_of_line = "</g>"
1028
+ # Layout constants
1029
+ BASE_Y = 30
1030
+ HEADER_Y = 15
1031
+ LINE_START = 10
1032
+ LINE_STEP = 14
996
1033
 
997
- if chart_type == "Transit":
998
- svg_output += (
999
- f'<g transform="translate(820, 15)">' # 620+200, 30-15
1000
- f'<text style="fill:{text_color}; font-size: 14px;">{second_subject_name}</text>'
1001
- )
1002
- elif chart_type == "Return":
1003
- svg_output += (
1004
- f'<g transform="translate(870, 15)">' # 620+250, 30-15
1005
- f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}</text>'
1006
- )
1007
- else:
1008
- svg_output += (
1009
- f'<g transform="translate(870, 15)">' # 620+250, 30-15
1010
- f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}</text>'
1011
- )
1034
+ # Open wrapper group
1035
+ svg_output = f'<g transform="translate({x_position},{y_position})">'
1012
1036
 
1013
- svg_output += end_of_line
1037
+ # Title content and its relative x offset
1038
+ header_text = (
1039
+ second_subject_name if chart_type == "Transit"
1040
+ else f"{planets_and_houses_grid_title} {second_subject_name}"
1041
+ )
1042
+ header_x_offset = -50 if chart_type == "Transit" else 0
1043
+
1044
+ svg_output += (
1045
+ f'<g transform="translate({header_x_offset}, {HEADER_Y})">'
1046
+ f'<text style="fill:{text_color}; font-size: 14px;">{header_text}</text>'
1047
+ f'</g>'
1048
+ )
1014
1049
 
1015
- line_height = 10
1016
- offset = 250
1050
+ # Grid rows
1051
+ line_height = LINE_START
1052
+ end_of_line = "</g>"
1017
1053
 
1018
- for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
1019
- second_decoded_name = get_decoded_kerykeion_celestial_point_name(t_planet["name"], celestial_point_language)
1054
+ for t_planet in second_subject_available_kerykeion_celestial_points:
1055
+ second_decoded_name = get_decoded_kerykeion_celestial_point_name(
1056
+ t_planet["name"],
1057
+ celestial_point_language,
1058
+ )
1020
1059
  svg_output += (
1021
- f'<g transform="translate({620 + offset},{30 + line_height})">' # Added the 620,30 offset
1060
+ f'<g transform="translate(0,{BASE_Y + line_height})">'
1022
1061
  f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{second_decoded_name}</text>'
1023
1062
  f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
1024
1063
  f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(t_planet["position"])}</text>'
@@ -1029,7 +1068,10 @@ def draw_secondary_planet_grid(
1029
1068
  svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
1030
1069
 
1031
1070
  svg_output += end_of_line
1032
- line_height += 14 # Using fixed offset_between_lines value
1071
+ line_height += LINE_STEP
1072
+
1073
+ # Close wrapper group
1074
+ svg_output += "</g>"
1033
1075
 
1034
1076
  return svg_output
1035
1077
 
@@ -1281,9 +1323,9 @@ def draw_house_comparison_grid(
1281
1323
  text_color: str = "var(--kerykeion-color-neutral-content)",
1282
1324
  house_position_comparison_label: str = "House Position Comparison",
1283
1325
  return_point_label: str = "Return Point",
1284
- return_label: str = "Return",
1326
+ return_label: str = "DualReturnChart",
1285
1327
  radix_label: str = "Radix",
1286
- x_position: int = 1030,
1328
+ x_position: int = 1100,
1287
1329
  y_position: int = 0,
1288
1330
  ) -> str:
1289
1331
  """
@@ -1584,4 +1626,3 @@ def calculate_synastry_quality_points(
1584
1626
  combined_totals[quality] = (combined_totals[quality] / total_points) * 100.0
1585
1627
 
1586
1628
  return combined_totals
1587
-
@@ -1,9 +1,9 @@
1
1
  from kerykeion.charts.charts_utils import degreeDiff, sliceToX, sliceToY, convert_decimal_to_degree_string
2
- from kerykeion.kr_types import KerykeionException, ChartType, KerykeionPointModel
3
- from kerykeion.kr_types.settings_models import KerykeionSettingsCelestialPointModel
4
- from kerykeion.kr_types.kr_literals import Houses
2
+ from kerykeion.schemas import KerykeionException, ChartType, KerykeionPointModel
3
+ from kerykeion.schemas.settings_models import KerykeionSettingsCelestialPointModel
4
+ from kerykeion.schemas.kr_literals import Houses
5
5
  import logging
6
- from typing import Union, get_args
6
+ from typing import Union, get_args, List, Optional
7
7
 
8
8
 
9
9
  def draw_planets(
@@ -15,13 +15,15 @@ def draw_planets(
15
15
  main_subject_seventh_house_degree_ut: Union[int, float],
16
16
  chart_type: ChartType,
17
17
  second_subject_available_kerykeion_celestial_points: Union[list[KerykeionPointModel], None] = None,
18
+ external_view: bool = False,
18
19
  ) -> str:
19
20
  """
20
21
  Draws the planets on an astrological chart based on the provided parameters.
21
22
 
22
23
  This function calculates positions, handles overlap of celestial points, and draws SVG
23
24
  elements for each planet/point on the chart. It supports different chart types including
24
- natal charts, transits, synastry, and returns.
25
+ natal charts, transits, synastry, and returns. For single-subject charts (Natal), it
26
+ can render planets in external view mode using the external_view parameter.
25
27
 
26
28
  Args:
27
29
  radius (Union[int, float]): The radius of the chart in pixels.
@@ -30,10 +32,13 @@ def draw_planets(
30
32
  third_circle_radius (Union[int, float]): Radius of the third circle in the chart.
31
33
  main_subject_first_house_degree_ut (Union[int, float]): Degree of the first house for the main subject.
32
34
  main_subject_seventh_house_degree_ut (Union[int, float]): Degree of the seventh house for the main subject.
33
- chart_type (ChartType): Type of the chart (e.g., "Transit", "Synastry", "Return", "ExternalNatal").
35
+ chart_type (ChartType): Type of the chart (e.g., "Transit", "Synastry", "DualReturnChart", "Natal").
34
36
  second_subject_available_kerykeion_celestial_points (Union[list[KerykeionPointModel], None], optional):
35
37
  List of celestial points for the second subject, required for "Transit", "Synastry", or "Return" charts.
36
38
  Defaults to None.
39
+ external_view (bool, optional):
40
+ Whether to render planets in external view mode (planets on outer ring with connecting lines).
41
+ Only applicable for single-subject charts. Defaults to False.
37
42
 
38
43
  Raises:
39
44
  KerykeionException: If secondary celestial points are required but not provided.
@@ -43,7 +48,7 @@ def draw_planets(
43
48
  """
44
49
  # Constants and initialization
45
50
  PLANET_GROUPING_THRESHOLD = 3.4 # Distance threshold to consider planets as grouped
46
- TRANSIT_RING_EXCLUDE_POINTS_NAMES = get_args(Houses)
51
+ TRANSIT_RING_EXCLUDE_POINTS_NAMES: List[str] = list(get_args(Houses))
47
52
  output = ""
48
53
 
49
54
  # -----------------------------------------------------------
@@ -64,12 +69,13 @@ def draw_planets(
64
69
  secondary_points_abs_positions = []
65
70
  secondary_points_rel_positions = []
66
71
  if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
67
- secondary_points_abs_positions = [
68
- planet.abs_pos for planet in second_subject_available_kerykeion_celestial_points
69
- ]
70
- secondary_points_rel_positions = [
71
- planet.position for planet in second_subject_available_kerykeion_celestial_points
72
- ]
72
+ if second_subject_available_kerykeion_celestial_points is not None:
73
+ secondary_points_abs_positions = [
74
+ planet.abs_pos for planet in second_subject_available_kerykeion_celestial_points
75
+ ]
76
+ secondary_points_rel_positions = [
77
+ planet.position for planet in second_subject_available_kerykeion_celestial_points
78
+ ]
73
79
 
74
80
  # -----------------------------------------------------------
75
81
  # 2. Create position lookup dictionary for main celestial points
@@ -86,9 +92,9 @@ def draw_planets(
86
92
  # -----------------------------------------------------------
87
93
  # 3. Identify groups of celestial points that are close to each other
88
94
  # -----------------------------------------------------------
89
- point_groups = []
95
+ point_groups: List[List[List[Union[int, float, str]]]] = []
90
96
  is_group_open = False
91
- planets_by_position = [None] * len(position_index_map)
97
+ planets_by_position: List[Optional[List[Union[int, float]]]] = [None] * len(position_index_map)
92
98
 
93
99
  # Process each celestial point to find groups
94
100
  for position_idx, abs_position in enumerate(sorted_positions):
@@ -142,7 +148,7 @@ def draw_planets(
142
148
  # -----------------------------------------------------------
143
149
  # 4. Calculate position adjustments to avoid overlapping
144
150
  # -----------------------------------------------------------
145
- position_adjustments = [0] * len(available_planets_setting)
151
+ position_adjustments: List[float] = [0.0] * len(available_planets_setting)
146
152
 
147
153
  # Process each group to calculate position adjustments
148
154
  for group in point_groups:
@@ -159,11 +165,12 @@ def draw_planets(
159
165
  # -----------------------------------------------------------
160
166
  # 5. Draw main celestial points
161
167
  # -----------------------------------------------------------
168
+ adjusted_offset = 0.0 # Initialize for use outside loop
162
169
  for position_idx, abs_position in enumerate(sorted_positions):
163
170
  point_idx = position_index_map[abs_position]
164
171
 
165
172
  # Determine radius based on chart type and point type
166
- point_radius = _determine_point_radius(point_idx, chart_type, bool(position_idx % 2))
173
+ point_radius = _determine_point_radius(point_idx, chart_type, bool(position_idx % 2), external_view)
167
174
 
168
175
  # Calculate position offset for the point
169
176
  adjusted_offset = _calculate_point_offset(
@@ -191,11 +198,11 @@ def draw_planets(
191
198
  scale_factor = 0.8
192
199
  elif chart_type == "Return":
193
200
  scale_factor = 0.8
194
- elif chart_type == "ExternalNatal":
201
+ elif external_view:
195
202
  scale_factor = 0.8
196
203
 
197
- # Draw connecting lines for ExternalNatal chart type
198
- if chart_type == "ExternalNatal":
204
+ # Draw connecting lines for external view
205
+ if external_view:
199
206
  output = _draw_external_natal_lines(
200
207
  output,
201
208
  radius,
@@ -327,7 +334,8 @@ def _handle_multi_point_group(group: list, position_adjustments: list, threshold
327
334
  def _determine_point_radius(
328
335
  point_idx: int,
329
336
  chart_type: str,
330
- is_alternate_position: bool
337
+ is_alternate_position: bool,
338
+ external_view: bool = False
331
339
  ) -> int:
332
340
  """
333
341
  Determine the radius for placing a celestial point based on its type and chart type.
@@ -336,6 +344,7 @@ def _determine_point_radius(
336
344
  point_idx (int): Index of the celestial point.
337
345
  chart_type (str): Type of the chart.
338
346
  is_alternate_position (bool): Whether to use alternate positioning.
347
+ external_view (bool): Whether external view is enabled.
339
348
 
340
349
  Returns:
341
350
  int: Radius value for the point.
@@ -359,10 +368,10 @@ def _determine_point_radius(
359
368
  else:
360
369
  return 110 if is_alternate_position else 130
361
370
  else:
362
- # Default natal chart and ExternalNatal handling
371
+ # Default natal chart and external view handling
363
372
  # if 22 < point_idx < 27 it is asc,mc,dsc,ic (angles of chart)
364
373
  amin, bmin, cmin = 0, 0, 0
365
- if chart_type == "ExternalNatal":
374
+ if external_view:
366
375
  amin = 74 - 10
367
376
  bmin = 94 - 10
368
377
  cmin = 40 - 10
@@ -402,7 +411,7 @@ def _draw_external_natal_lines(
402
411
  color: str,
403
412
  ) -> str:
404
413
  """
405
- Draw connecting lines for the ExternalNatal chart type.
414
+ Draw connecting lines for external view charts.
406
415
 
407
416
  Creates two line segments: one from the circle to the original position,
408
417
  and another from the original position to the adjusted position.
@@ -450,7 +459,7 @@ def _generate_point_svg(point_details: KerykeionPointModel, x: float, y: float,
450
459
  Returns:
451
460
  str: SVG element for the celestial point.
452
461
  """
453
- svg = f'<g kr:node="ChartPoint" kr:house="{point_details["house"]}" kr:sign="{point_details["sign"]}" '
462
+ svg = f'<g kr:node="ChartPoint" kr:house="{point_details["house"]}" kr:sign="{point_details["sign"]}" kr:absoluteposition="{point_details["abs_pos"]}" kr:signposition="{point_details["position"]}" '
454
463
  svg += f'kr:slug="{point_details["name"]}" transform="translate(-{12 * scale},-{12 * scale}) scale({scale})">'
455
464
  svg += f'<use x="{x * (1/scale)}" y="{y * (1/scale)}" xlink:href="#{point_name}" />'
456
465
  svg += "</g>"
@@ -488,7 +497,7 @@ def _draw_secondary_points(
488
497
  str: Updated SVG output with added secondary points.
489
498
  """
490
499
  # Initialize position adjustments for grouped points
491
- position_adjustments = {i: 0 for i in range(len(points_settings))}
500
+ position_adjustments: dict[int, float] = {i: 0.0 for i in range(len(points_settings))}
492
501
 
493
502
  # Map absolute position to point index
494
503
  position_index_map = {}
@@ -501,7 +510,7 @@ def _draw_secondary_points(
501
510
  sorted_positions = sorted(position_index_map.keys())
502
511
 
503
512
  # Find groups of points that are close to each other
504
- point_groups = []
513
+ point_groups: List[List[int]] = []
505
514
  in_group = False
506
515
 
507
516
  for pos_idx, abs_position in enumerate(sorted_positions):
@@ -536,7 +545,7 @@ def _draw_secondary_points(
536
545
  position_adjustments[group[1]] = 1.0
537
546
  elif len(group) == 3:
538
547
  position_adjustments[group[0]] = -1.5
539
- position_adjustments[group[1]] = 0
548
+ position_adjustments[group[1]] = 0.0
540
549
  position_adjustments[group[2]] = 1.5
541
550
  elif len(group) == 4:
542
551
  position_adjustments[group[0]] = -2.0
@@ -546,6 +555,7 @@ def _draw_secondary_points(
546
555
 
547
556
  # Draw each secondary point
548
557
  alternate_position = False
558
+ point_idx = 0 # Initialize for use outside loop
549
559
 
550
560
  for pos_idx, abs_position in enumerate(sorted_positions):
551
561
  point_idx = position_index_map[abs_position]