kerykeion 5.0.0a12__py3-none-any.whl → 5.0.0b2__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 (51) hide show
  1. kerykeion/__init__.py +30 -6
  2. kerykeion/aspects/aspects_factory.py +40 -24
  3. kerykeion/aspects/aspects_utils.py +75 -6
  4. kerykeion/astrological_subject_factory.py +377 -226
  5. kerykeion/backword.py +680 -0
  6. kerykeion/chart_data_factory.py +484 -0
  7. kerykeion/charts/{kerykeion_chart_svg.py → chart_drawer.py} +688 -440
  8. kerykeion/charts/charts_utils.py +157 -94
  9. kerykeion/charts/draw_planets.py +38 -28
  10. kerykeion/charts/templates/aspect_grid_only.xml +188 -17
  11. kerykeion/charts/templates/chart.xml +153 -47
  12. kerykeion/charts/templates/wheel_only.xml +195 -24
  13. kerykeion/charts/themes/classic.css +11 -0
  14. kerykeion/charts/themes/dark-high-contrast.css +11 -0
  15. kerykeion/charts/themes/dark.css +11 -0
  16. kerykeion/charts/themes/light.css +11 -0
  17. kerykeion/charts/themes/strawberry.css +10 -0
  18. kerykeion/composite_subject_factory.py +4 -4
  19. kerykeion/ephemeris_data_factory.py +12 -9
  20. kerykeion/house_comparison/__init__.py +0 -3
  21. kerykeion/house_comparison/house_comparison_factory.py +3 -3
  22. kerykeion/house_comparison/house_comparison_utils.py +3 -4
  23. kerykeion/planetary_return_factory.py +8 -4
  24. kerykeion/relationship_score_factory.py +3 -3
  25. kerykeion/report.py +748 -67
  26. kerykeion/{kr_types → schemas}/__init__.py +44 -4
  27. kerykeion/schemas/chart_template_model.py +367 -0
  28. kerykeion/{kr_types → schemas}/kr_literals.py +7 -3
  29. kerykeion/{kr_types → schemas}/kr_models.py +220 -11
  30. kerykeion/{kr_types → schemas}/settings_models.py +7 -7
  31. kerykeion/settings/config_constants.py +75 -8
  32. kerykeion/settings/kerykeion_settings.py +1 -1
  33. kerykeion/settings/kr.config.json +132 -42
  34. kerykeion/settings/legacy/legacy_celestial_points_settings.py +8 -8
  35. kerykeion/sweph/ast136/s136108s.se1 +0 -0
  36. kerykeion/sweph/ast136/s136199s.se1 +0 -0
  37. kerykeion/sweph/ast136/s136472s.se1 +0 -0
  38. kerykeion/sweph/ast28/se28978s.se1 +0 -0
  39. kerykeion/sweph/ast50/se50000s.se1 +0 -0
  40. kerykeion/sweph/ast90/se90377s.se1 +0 -0
  41. kerykeion/sweph/ast90/se90482s.se1 +0 -0
  42. kerykeion/transits_time_range_factory.py +7 -7
  43. kerykeion/utilities.py +61 -38
  44. {kerykeion-5.0.0a12.dist-info → kerykeion-5.0.0b2.dist-info}/METADATA +507 -120
  45. kerykeion-5.0.0b2.dist-info/RECORD +58 -0
  46. kerykeion/house_comparison/house_comparison_models.py +0 -76
  47. kerykeion/kr_types/chart_types.py +0 -106
  48. kerykeion-5.0.0a12.dist-info/RECORD +0 -50
  49. /kerykeion/{kr_types → schemas}/kerykeion_exception.py +0 -0
  50. {kerykeion-5.0.0a12.dist-info → kerykeion-5.0.0b2.dist-info}/WHEEL +0 -0
  51. {kerykeion-5.0.0a12.dist-info → kerykeion-5.0.0b2.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,35 @@
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
8
+
9
+
10
+ _SECOND_COLUMN_THRESHOLD = 20
11
+ _THIRD_COLUMN_THRESHOLD = 28
12
+ _FOURTH_COLUMN_THRESHOLD = 36
13
+ _GRID_COLUMN_WIDTH = 125
14
+
15
+
16
+ def _planet_grid_layout_position(index: int) -> tuple[int, int]:
17
+ """Return horizontal offset and row index for planet grids."""
18
+ if index < _SECOND_COLUMN_THRESHOLD:
19
+ column = 0
20
+ row = index
21
+ elif index < _THIRD_COLUMN_THRESHOLD:
22
+ column = 1
23
+ row = index - _SECOND_COLUMN_THRESHOLD
24
+ elif index < _FOURTH_COLUMN_THRESHOLD:
25
+ column = 2
26
+ row = index - _THIRD_COLUMN_THRESHOLD
27
+ else:
28
+ column = 3
29
+ row = index - _FOURTH_COLUMN_THRESHOLD
30
+
31
+ offset = -(_GRID_COLUMN_WIDTH * column)
32
+ return offset, row
9
33
 
10
34
 
11
35
 
@@ -189,7 +213,7 @@ def draw_zodiac_slice(
189
213
  # pie slices
190
214
  offset = 360 - seventh_house_degree_ut
191
215
  # check transit
192
- if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
216
+ if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "DualReturnChart":
193
217
  dropin: Union[int, float] = 0
194
218
  else:
195
219
  dropin = c1
@@ -198,7 +222,7 @@ def draw_zodiac_slice(
198
222
  # symbols
199
223
  offset = offset + 15
200
224
  # check transit
201
- if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
225
+ if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "DualReturnChart":
202
226
  dropin = 54
203
227
  else:
204
228
  dropin = 18 + c1
@@ -287,7 +311,7 @@ def draw_aspect_line(
287
311
  y2 = sliceToY(0, ar, second_offset) + (r - ar)
288
312
 
289
313
  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"]}">'
314
+ 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
315
  f'<line class="aspect" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {color}; stroke-width: 1; stroke-opacity: .9;"/>'
292
316
  f"</g>"
293
317
  )
@@ -419,7 +443,7 @@ def draw_first_circle(
419
443
  Returns:
420
444
  str: The SVG path of the first circle.
421
445
  """
422
- if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "Return":
446
+ if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "DualReturnChart":
423
447
  return f'<circle cx="{r}" cy="{r}" r="{r - 36}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.4;" />'
424
448
  else:
425
449
  if c1 is None:
@@ -462,7 +486,7 @@ def draw_second_circle(
462
486
  str: The SVG path of the second circle.
463
487
  """
464
488
 
465
- if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "Return":
489
+ if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "DualReturnChart":
466
490
  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
491
 
468
492
  else:
@@ -492,7 +516,7 @@ def draw_third_circle(
492
516
  Returns:
493
517
  - str: The SVG element as a string.
494
518
  """
495
- if chart_type in {"Synastry", "Transit", "Return"}:
519
+ if chart_type in {"Synastry", "Transit", "DualReturnChart"}:
496
520
  # For Synastry and Transit charts, use a fixed radius adjustment of 160
497
521
  return f'<circle cx="{radius}" cy="{radius}" r="{radius - 160}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
498
522
 
@@ -572,6 +596,7 @@ def draw_houses_cusps_and_text_number(
572
596
  chart_type: ChartType,
573
597
  second_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
574
598
  transit_house_cusp_color: Union[str, None] = None,
599
+ external_view: bool = False,
575
600
  ) -> str:
576
601
  """
577
602
  Draws the houses cusps and text numbers for a given chart type.
@@ -589,9 +614,10 @@ def draw_houses_cusps_and_text_number(
589
614
  - chart_type: Type of the chart (e.g., Transit, Synastry).
590
615
  - second_subject_houses_list: List of house for the second subject (optional).
591
616
  - transit_house_cusp_color: Color for transit house cusps (optional).
617
+ - external_view: Whether to use external view mode for positioning (optional).
592
618
 
593
619
  Returns:
594
- - A string containing the SVG path for the houses cusps and text numbers.
620
+ - A string containing SVG elements for house cusps and numbers.
595
621
  """
596
622
 
597
623
  path = ""
@@ -599,7 +625,7 @@ def draw_houses_cusps_and_text_number(
599
625
 
600
626
  for i in range(xr):
601
627
  # 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)
628
+ dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry", "DualReturnChart"] else (c3, c1, False)
603
629
 
604
630
  # Calculate the offset for the current house cusp
605
631
  offset = (int(first_subject_houses_list[int(xr / 2)].abs_pos) / -1) + int(first_subject_houses_list[i].abs_pos)
@@ -621,7 +647,7 @@ def draw_houses_cusps_and_text_number(
621
647
  i, standard_house_cusp_color
622
648
  )
623
649
 
624
- if chart_type in ["Transit", "Synastry", "Return"]:
650
+ if chart_type in ["Transit", "Synastry", "DualReturnChart"]:
625
651
  if second_subject_houses_list is None or transit_house_cusp_color is None:
626
652
  raise KerykeionException("second_subject_houses_list_ut or transit_house_cusp_color is None")
627
653
 
@@ -651,17 +677,21 @@ def draw_houses_cusps_and_text_number(
651
677
 
652
678
  # Add the house cusp line for the second subject
653
679
  stroke_opacity = "0" if chart_type == "Transit" else ".3"
654
- path += '<g kr:node="Cusp">'
680
+ 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
681
  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
682
  path += "</g>"
657
683
 
658
- # Adjust dropin based on chart type
659
- dropin = {"Transit": 84, "Synastry": 84, "Return": 84, "ExternalNatal": 100}.get(chart_type, 48)
684
+ # Adjust dropin based on chart type and external view
685
+ dropin_map = {"Transit": 84, "Synastry": 84, "DualReturnChart": 84}
686
+ if external_view:
687
+ dropin = 100
688
+ else:
689
+ dropin = dropin_map.get(chart_type, 48)
660
690
  xtext = sliceToX(0, (r - dropin), text_offset) + dropin
661
691
  ytext = sliceToY(0, (r - dropin), text_offset) + dropin
662
692
 
663
693
  # Add the house cusp line for the first subject
664
- path += '<g kr:node="Cusp">'
694
+ 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
695
  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
696
  path += "</g>"
667
697
 
@@ -708,9 +738,12 @@ def draw_transit_aspect_list(
708
738
  if aspects_list and isinstance(aspects_list[0], dict):
709
739
  aspects_list = [AspectModel(**aspect) for aspect in aspects_list] # type: ignore
710
740
 
741
+ # Type narrowing: at this point aspects_list contains AspectModel instances
742
+ typed_aspects_list: list[AspectModel] = aspects_list # type: ignore
743
+
711
744
  inner_path = ""
712
745
 
713
- for i, aspect in enumerate(aspects_list):
746
+ for i, aspect in enumerate(typed_aspects_list):
714
747
  # Calculate which column this aspect belongs in
715
748
  current_column = i // aspects_per_column
716
749
 
@@ -722,12 +755,22 @@ def draw_transit_aspect_list(
722
755
  vertical_position = current_line * line_height
723
756
 
724
757
  # Special handling for many aspects - if we exceed max_columns
758
+ # Bottom-align the overflow columns so the list starts from the bottom
725
759
  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)
760
+ overflow_total = len(aspects_list) - (aspects_per_column * max_columns)
761
+ if overflow_total > 0:
762
+ # Index within the overflow sequence (beyond the first row of columns)
763
+ overflow_index = i - (aspects_per_column * max_columns)
764
+ # Which overflow column we are in (0-based)
765
+ overflow_col_idx = overflow_index // aspects_per_column
766
+ # How many items go into this overflow column
767
+ items_in_this_column = min(
768
+ aspects_per_column,
769
+ max(0, overflow_total - (overflow_col_idx * aspects_per_column)),
770
+ )
771
+ # Compute extra top offset (in lines) to bottom-align this column
772
+ top_offset_lines = max(0, aspects_per_column - items_in_this_column)
773
+ vertical_position = (top_offset_lines + (i % aspects_per_column)) * line_height
731
774
 
732
775
  inner_path += f'<g transform="translate({horizontal_position},{vertical_position})">'
733
776
 
@@ -824,7 +867,7 @@ def draw_main_house_grid(
824
867
  main_subject_houses_list: list[KerykeionPointModel],
825
868
  house_cusp_generale_name_label: str = "Cusp",
826
869
  text_color: str = "#000000",
827
- x_position: int = 720,
870
+ x_position: int = 750,
828
871
  y_position: int = 30,
829
872
  ) -> str:
830
873
  """
@@ -862,7 +905,7 @@ def draw_secondary_house_grid(
862
905
  secondary_subject_houses_list: list[KerykeionPointModel],
863
906
  house_cusp_generale_name_label: str = "Cusp",
864
907
  text_color: str = "#000000",
865
- x_position: int = 970,
908
+ x_position: int = 1015,
866
909
  y_position: int = 30,
867
910
  ) -> str:
868
911
  """
@@ -903,57 +946,58 @@ def draw_main_planet_grid(
903
946
  chart_type: ChartType,
904
947
  celestial_point_language: KerykeionLanguageCelestialPointModel,
905
948
  text_color: str = "#000000",
949
+ x_position: int = 645,
950
+ y_position: int = 0,
906
951
  ) -> str:
907
952
  """
908
- Draws the planet grid for the main subject.
953
+ Draw the planet grid (main subject) and optional title.
954
+
955
+ The entire output is wrapped in a single SVG group `<g>` so the
956
+ whole block can be repositioned by changing the group transform.
909
957
 
910
958
  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".
959
+ planets_and_houses_grid_title: Title prefix to show for eligible chart types.
960
+ subject_name: Subject name to append to the title.
961
+ available_kerykeion_celestial_points: Celestial points to render in the grid.
962
+ chart_type: Chart type identifier (Literal string).
963
+ celestial_point_language: Language model for celestial point decoding.
964
+ text_color: Text color for labels (default: "#000000").
965
+ x_position: X translation applied to the outer `<g>` (default: 620).
966
+ y_position: Y translation applied to the outer `<g>` (default: 0).
917
967
 
918
968
  Returns:
919
- str: The SVG output for the main planet grid.
969
+ SVG string for the main planet grid wrapped in a `<g>`.
920
970
  """
921
- line_height = 10
922
- offset = 0
923
- offset_between_lines = 14
924
- svg_output = ""
971
+ # Layout constants (kept identical to previous behavior)
972
+ BASE_Y = 30
973
+ HEADER_Y = 15 # Title baseline inside the wrapper
974
+ LINE_START = 10
975
+ LINE_STEP = 14
925
976
 
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":
977
+ # Wrap everything inside a single group so position can be changed once
978
+ svg_output = f'<g transform="translate({x_position},{y_position})">'
979
+
980
+ # Add title only for specific chart types
981
+ if chart_type in ("Synastry", "Transit", "DualReturnChart"):
939
982
  svg_output += (
940
- f'<g transform="translate(620, 15)">' # Added the 620,30 offset (adjusted for -15)
983
+ f'<g transform="translate(0, {HEADER_Y})">'
941
984
  f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
942
985
  f'</g>'
943
986
  )
944
- else:
945
- svg_output += ""
946
987
 
947
988
  end_of_line = "</g>"
948
989
 
949
990
  for i, planet in enumerate(available_kerykeion_celestial_points):
950
- if i == 22:
951
- line_height = 10
952
- offset = -125
991
+ offset, row_index = _planet_grid_layout_position(i)
992
+ line_height = LINE_START + (row_index * LINE_STEP)
993
+
994
+ decoded_name = get_decoded_kerykeion_celestial_point_name(
995
+ planet["name"],
996
+ celestial_point_language,
997
+ )
953
998
 
954
- decoded_name = get_decoded_kerykeion_celestial_point_name(planet["name"], celestial_point_language)
955
999
  svg_output += (
956
- f'<g transform="translate({620 + offset},{30 + line_height})">' # Added the 620,30 offset
1000
+ f'<g transform="translate({offset},{BASE_Y + line_height})">'
957
1001
  f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{decoded_name}</text>'
958
1002
  f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{planet["name"]}" /></g>'
959
1003
  f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(planet["position"])}</text>'
@@ -964,7 +1008,9 @@ def draw_main_planet_grid(
964
1008
  svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
965
1009
 
966
1010
  svg_output += end_of_line
967
- line_height += offset_between_lines
1011
+
1012
+ # Close the wrapper group
1013
+ svg_output += "</g>"
968
1014
 
969
1015
  return svg_output
970
1016
 
@@ -976,49 +1022,64 @@ def draw_secondary_planet_grid(
976
1022
  chart_type: ChartType,
977
1023
  celestial_point_language: KerykeionLanguageCelestialPointModel,
978
1024
  text_color: str = "#000000",
1025
+ x_position: int = 910,
1026
+ y_position: int = 0,
979
1027
  ) -> str:
980
1028
  """
981
- Draws the planet grid for the secondary subject in Transit, Synastry, or Return charts.
1029
+ Draw the planet grid for the secondary subject and its title.
1030
+
1031
+ The entire output is wrapped in a single SVG group `<g>` so the
1032
+ whole block can be repositioned by changing the group transform.
982
1033
 
983
1034
  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".
1035
+ planets_and_houses_grid_title: Title prefix (used except for Transit charts).
1036
+ second_subject_name: Name of the secondary subject.
1037
+ second_subject_available_kerykeion_celestial_points: Celestial points to render for the secondary subject.
1038
+ chart_type: Chart type identifier (Literal string).
1039
+ celestial_point_language: Language model for celestial point decoding.
1040
+ text_color: Text color for labels (default: "#000000").
1041
+ x_position: X translation applied to the outer `<g>` (default: 870).
1042
+ y_position: Y translation applied to the outer `<g>` (default: 0).
990
1043
 
991
1044
  Returns:
992
- str: The SVG output for the secondary planet grid.
1045
+ SVG string for the secondary planet grid wrapped in a `<g>`.
993
1046
  """
994
- svg_output = ""
995
- end_of_line = "</g>"
1047
+ # Layout constants
1048
+ BASE_Y = 30
1049
+ HEADER_Y = 15
1050
+ LINE_START = 10
1051
+ LINE_STEP = 14
996
1052
 
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
- )
1053
+ # Open wrapper group
1054
+ svg_output = f'<g transform="translate({x_position},{y_position})">'
1012
1055
 
1013
- svg_output += end_of_line
1056
+ # Title content and its relative x offset
1057
+ header_text = (
1058
+ second_subject_name if chart_type == "Transit"
1059
+ else f"{planets_and_houses_grid_title} {second_subject_name}"
1060
+ )
1061
+ header_x_offset = -50 if chart_type == "Transit" else 0
1014
1062
 
1015
- line_height = 10
1016
- offset = 250
1063
+ svg_output += (
1064
+ f'<g transform="translate({header_x_offset}, {HEADER_Y})">'
1065
+ f'<text style="fill:{text_color}; font-size: 14px;">{header_text}</text>'
1066
+ f'</g>'
1067
+ )
1068
+
1069
+ # Grid rows
1070
+ line_height = LINE_START
1071
+ end_of_line = "</g>"
1017
1072
 
1018
1073
  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)
1074
+ offset, row_index = _planet_grid_layout_position(i)
1075
+ line_height = LINE_START + (row_index * LINE_STEP)
1076
+
1077
+ second_decoded_name = get_decoded_kerykeion_celestial_point_name(
1078
+ t_planet["name"],
1079
+ celestial_point_language,
1080
+ )
1020
1081
  svg_output += (
1021
- f'<g transform="translate({620 + offset},{30 + line_height})">' # Added the 620,30 offset
1082
+ f'<g transform="translate({offset},{BASE_Y + line_height})">'
1022
1083
  f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{second_decoded_name}</text>'
1023
1084
  f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
1024
1085
  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 +1090,10 @@ def draw_secondary_planet_grid(
1029
1090
  svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
1030
1091
 
1031
1092
  svg_output += end_of_line
1032
- line_height += 14 # Using fixed offset_between_lines value
1093
+
1094
+
1095
+ # Close wrapper group
1096
+ svg_output += "</g>"
1033
1097
 
1034
1098
  return svg_output
1035
1099
 
@@ -1281,9 +1345,9 @@ def draw_house_comparison_grid(
1281
1345
  text_color: str = "var(--kerykeion-color-neutral-content)",
1282
1346
  house_position_comparison_label: str = "House Position Comparison",
1283
1347
  return_point_label: str = "Return Point",
1284
- return_label: str = "Return",
1348
+ return_label: str = "DualReturnChart",
1285
1349
  radix_label: str = "Radix",
1286
- x_position: int = 1030,
1350
+ x_position: int = 1100,
1287
1351
  y_position: int = 0,
1288
1352
  ) -> str:
1289
1353
  """
@@ -1584,4 +1648,3 @@ def calculate_synastry_quality_points(
1584
1648
  combined_totals[quality] = (combined_totals[quality] / total_points) * 100.0
1585
1649
 
1586
1650
  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]