kerykeion 4.24.7__py3-none-any.whl → 4.25.1__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.

kerykeion/__init__.py CHANGED
@@ -16,3 +16,4 @@ from .report import Report
16
16
  from .settings import KerykeionSettingsModel, get_settings
17
17
  from .enums import Planets, Aspects, Signs
18
18
  from .ephemeris_data import EphemerisDataFactory
19
+ from .composite_subject_factory import CompositeSubjectFactory
@@ -5,6 +5,7 @@
5
5
 
6
6
  from pathlib import Path
7
7
  from kerykeion import AstrologicalSubject
8
+ from kerykeion.kr_types import CompositeSubjectModel
8
9
  import logging
9
10
  from typing import Union, List
10
11
  from kerykeion.settings.kerykeion_settings import get_settings
@@ -32,7 +33,7 @@ class NatalAspects:
32
33
  Generates an object with all the aspects of a birthcart.
33
34
  """
34
35
 
35
- user: Union[AstrologicalSubject, AstrologicalSubjectModel]
36
+ user: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel]
36
37
  new_settings_file: Union[Path, KerykeionSettingsModel, dict, None] = None
37
38
  active_points: List[Union[AxialCusps, Planet]] = field(default_factory=lambda: DEFAULT_ACTIVE_POINTS)
38
39
  active_aspects: List[ActiveAspect] = field(default_factory=lambda: DEFAULT_ACTIVE_ASPECTS)
@@ -31,9 +31,8 @@ from kerykeion.utilities import (
31
31
  get_number_from_name,
32
32
  get_kerykeion_point_from_degree,
33
33
  get_planet_house,
34
- get_moon_emoji_from_phase_int,
35
- get_moon_phase_name_from_phase_int,
36
- check_and_adjust_polar_latitude
34
+ check_and_adjust_polar_latitude,
35
+ calculate_moon_phase
37
36
  )
38
37
  from pathlib import Path
39
38
  from typing import Union, get_args
@@ -389,9 +388,15 @@ class AstrologicalSubject:
389
388
  self.julian_day = float(swe.julday(utc_object.year, utc_object.month, utc_object.day, utc_float_hour_with_minutes))
390
389
  # <--- UTC, julian day and local time setup
391
390
 
391
+ # Planets and Houses setup
392
392
  self._initialize_houses()
393
393
  self._initialize_planets()
394
- self._initialize_moon_phase()
394
+
395
+ # Lunar Phase
396
+ self.lunar_phase = calculate_moon_phase(
397
+ self.moon.abs_pos,
398
+ self.sun.abs_pos
399
+ )
395
400
 
396
401
  # Deprecated properties
397
402
  self.utc_time
@@ -667,49 +672,6 @@ class AstrologicalSubject:
667
672
  self.medium_coeli.retrograde = False
668
673
  self.imum_coeli.retrograde = False
669
674
 
670
- def _initialize_moon_phase(self) -> None:
671
- """
672
- Calculate and initialize the lunar phase based on the positions of the moon and sun.
673
-
674
- This function calculates the degrees between the moon and the sun, determines the moon phase
675
- and sun phase, and initializes the lunar phase model with the calculated values.
676
- """
677
- # Initialize moon_phase and sun_phase to None in case of an error
678
- moon_phase, sun_phase = None, None
679
-
680
- # Calculate the anti-clockwise degrees between the sun and moon
681
- moon, sun = self.moon.abs_pos, self.sun.abs_pos
682
- degrees_between = (moon - sun) % 360
683
-
684
- # Calculate the moon phase (1-28) based on the degrees between the sun and moon
685
- step = 360.0 / 28.0
686
- moon_phase = int(degrees_between // step) + 1
687
-
688
- # Define the sun phase steps
689
- sunstep = [
690
- 0, 30, 40, 50, 60, 70, 80, 90, 120, 130, 140, 150, 160, 170, 180,
691
- 210, 220, 230, 240, 250, 260, 270, 300, 310, 320, 330, 340, 350
692
- ]
693
-
694
- # Calculate the sun phase (1-28) based on the degrees between the sun and moon
695
- for x in range(len(sunstep)):
696
- low = sunstep[x]
697
- high = sunstep[x + 1] if x < len(sunstep) - 1 else 360
698
- if low <= degrees_between < high:
699
- sun_phase = x + 1
700
- break
701
-
702
- # Create a dictionary with the lunar phase information
703
- lunar_phase_dictionary = {
704
- "degrees_between_s_m": degrees_between,
705
- "moon_phase": moon_phase,
706
- "sun_phase": sun_phase,
707
- "moon_emoji": get_moon_emoji_from_phase_int(moon_phase),
708
- "moon_phase_name": get_moon_phase_name_from_phase_int(moon_phase)
709
- }
710
-
711
- # Initialize the lunar phase model with the calculated values
712
- self.lunar_phase = LunarPhaseModel(**lunar_phase_dictionary)
713
675
 
714
676
  def json(self, dump=False, destination_folder: Union[str, None] = None, indent: Union[int, None] = None) -> str:
715
677
  """
@@ -28,6 +28,7 @@ def get_decoded_kerykeion_celestial_point_name(input_planet_name: str, celestial
28
28
  else:
29
29
  raise KerykeionException(f"Celestial point {input_planet_name} not found in language model.")
30
30
 
31
+
31
32
  def decHourJoin(inH: int, inM: int, inS: int) -> float:
32
33
  """Join hour, minutes, seconds, timezone integer to hour float.
33
34
 
@@ -47,24 +48,42 @@ def decHourJoin(inH: int, inM: int, inS: int) -> float:
47
48
 
48
49
 
49
50
  def degreeDiff(a: Union[int, float], b: Union[int, float]) -> float:
50
- """Calculate the difference between two degrees.
51
+ """Calculate the smallest difference between two angles in degrees.
51
52
 
52
53
  Args:
53
- - a (int | float): first degree
54
- - b (int | float): second degree
54
+ a (int | float): first angle in degrees
55
+ b (int | float): second angle in degrees
55
56
 
56
57
  Returns:
57
- float: difference between a and b
58
+ float: smallest difference between a and b (0 to 180 degrees)
58
59
  """
60
+ diff = math.fmod(abs(a - b), 360) # Assicura che il valore sia in [0, 360)
61
+ return min(diff, 360 - diff) # Prende l'angolo più piccolo tra i due possibili
62
+
63
+
64
+ def degreeSum(a: Union[int, float], b: Union[int, float]) -> float:
65
+ """Calculate the sum of two angles in degrees, normalized to [0, 360).
66
+
67
+ Args:
68
+ a (int | float): first angle in degrees
69
+ b (int | float): second angle in degrees
70
+
71
+ Returns:
72
+ float: normalized sum of a and b in the range [0, 360)
73
+ """
74
+ return math.fmod(a + b, 360) if (a + b) % 360 != 0 else 0.0
59
75
 
60
- out = float()
61
- if a > b:
62
- out = a - b
63
- if a < b:
64
- out = b - a
65
- if out > 180.0:
66
- out = 360.0 - out
67
- return out
76
+
77
+ def normalizeDegree(angle: Union[int, float]) -> float:
78
+ """Normalize an angle to the range [0, 360).
79
+
80
+ Args:
81
+ angle (int | float): The input angle in degrees.
82
+
83
+ Returns:
84
+ float: The normalized angle in the range [0, 360).
85
+ """
86
+ return angle % 360 if angle % 360 != 0 else 0.0
68
87
 
69
88
 
70
89
  def offsetToTz(datetime_offset: Union[datetime.timedelta, None]) -> float:
@@ -270,49 +289,6 @@ def draw_aspect_line(
270
289
  f"</g>"
271
290
  )
272
291
 
273
-
274
- def draw_elements_percentages(
275
- fire_label: str,
276
- fire_points: float,
277
- earth_label: str,
278
- earth_points: float,
279
- air_label: str,
280
- air_points: float,
281
- water_label: str,
282
- water_points: float,
283
- ) -> str:
284
- """Draw the elements grid.
285
-
286
- Args:
287
- - fire_label (str): Label for fire
288
- - fire_points (float): Points for fire
289
- - earth_label (str): Label for earth
290
- - earth_points (float): Points for earth
291
- - air_label (str): Label for air
292
- - air_points (float): Points for air
293
- - water_label (str): Label for water
294
- - water_points (float): Points for water
295
-
296
- Returns:
297
- str: The SVG elements grid as a string.
298
- """
299
- total = fire_points + earth_points + air_points + water_points
300
-
301
- fire_percentage = int(round(100 * fire_points / total))
302
- earth_percentage = int(round(100 * earth_points / total))
303
- air_percentage = int(round(100 * air_points / total))
304
- water_percentage = int(round(100 * water_points / total))
305
-
306
- return (
307
- f'<g transform="translate(-30,79)">'
308
- f'<text y="0" style="fill: var(--kerykeion-chart-color-fire-percentage); font-size: 10px;">{fire_label} {str(fire_percentage)}%</text>'
309
- f'<text y="12" style="fill: var(--kerykeion-chart-color-earth-percentage); font-size: 10px;">{earth_label} {str(earth_percentage)}%</text>'
310
- f'<text y="24" style="fill: var(--kerykeion-chart-color-air-percentage); font-size: 10px;">{air_label} {str(air_percentage)}%</text>'
311
- f'<text y="36" style="fill: var(--kerykeion-chart-color-water-percentage); font-size: 10px;">{water_label} {str(water_percentage)}%</text>'
312
- f"</g>"
313
- )
314
-
315
-
316
292
  def convert_decimal_to_degree_string(dec: float, format_type: Literal["1", "2", "3"] = "3") -> str:
317
293
  """
318
294
  Converts a decimal float to a degrees string in the specified format.
@@ -766,22 +742,19 @@ def draw_transit_aspect_list(
766
742
  return out
767
743
 
768
744
 
769
- def draw_moon_phase(
745
+ def calculate_moon_phase_chart_params(
770
746
  degrees_between_sun_and_moon: float,
771
747
  latitude: float
772
- ) -> str:
748
+ ) -> dict:
773
749
  """
774
- Draws the moon phase based on the degrees between the sun and the moon.
750
+ Calculate the parameters for the moon phase chart.
775
751
 
776
752
  Parameters:
777
753
  - degrees_between_sun_and_moon (float): The degrees between the sun and the moon.
778
754
  - latitude (float): The latitude for rotation calculation.
779
- - lunar_phase_outline_color (str): The color for the lunar phase outline.
780
- - dark_color (str): The color for the dark part of the moon.
781
- - light_color (str): The color for the light part of the moon.
782
755
 
783
756
  Returns:
784
- - str: The SVG element as a string.
757
+ - dict: The moon phase chart parameters.
785
758
  """
786
759
  deg = degrees_between_sun_and_moon
787
760
 
@@ -825,20 +798,11 @@ def draw_moon_phase(
825
798
  # Calculate rotation based on latitude
826
799
  lunar_phase_rotate = -90.0 - latitude
827
800
 
828
- # Return the SVG element as a string
829
- return (
830
- f'<g transform="rotate({lunar_phase_rotate} 20 10)">'
831
- f' <defs>'
832
- f' <clipPath id="moonPhaseCutOffCircle">'
833
- f' <circle cx="20" cy="10" r="10" />'
834
- f' </clipPath>'
835
- f' </defs>'
836
- f' <circle cx="20" cy="10" r="10" style="fill: var(--kerykeion-chart-color-lunar-phase-0)" />'
837
- f' <circle cx="{circle_center_x}" cy="10" r="{circle_radius}" style="fill: var(--kerykeion-chart-color-lunar-phase-1)" clip-path="url(#moonPhaseCutOffCircle)" />'
838
- 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" />'
839
- f'</g>'
840
- )
841
-
801
+ return {
802
+ "circle_center_x": circle_center_x,
803
+ "circle_radius": circle_radius,
804
+ "lunar_phase_rotate": lunar_phase_rotate,
805
+ }
842
806
 
843
807
  def draw_house_grid(
844
808
  main_subject_houses_list: list[KerykeionPointModel],
@@ -14,7 +14,7 @@ from kerykeion.aspects.natal_aspects import NatalAspects
14
14
  from kerykeion.astrological_subject import AstrologicalSubject
15
15
  from kerykeion.kr_types import KerykeionException, ChartType, KerykeionPointModel, Sign, ActiveAspect
16
16
  from kerykeion.kr_types import ChartTemplateDictionary
17
- from kerykeion.kr_types.kr_models import AstrologicalSubjectModel
17
+ from kerykeion.kr_types.kr_models import AstrologicalSubjectModel, CompositeSubjectModel
18
18
  from kerykeion.kr_types.settings_models import KerykeionSettingsCelestialPointModel, KerykeionSettingsModel
19
19
  from kerykeion.kr_types.kr_literals import KerykeionChartTheme, KerykeionChartLanguage, AxialCusps, Planet
20
20
  from kerykeion.charts.charts_utils import (
@@ -22,7 +22,6 @@ from kerykeion.charts.charts_utils import (
22
22
  convert_latitude_coordinate_to_string,
23
23
  convert_longitude_coordinate_to_string,
24
24
  draw_aspect_line,
25
- draw_elements_percentages,
26
25
  draw_transit_ring_degree_steps,
27
26
  draw_degree_ring,
28
27
  draw_transit_ring,
@@ -33,7 +32,7 @@ from kerykeion.charts.charts_utils import (
33
32
  draw_houses_cusps_and_text_number,
34
33
  draw_transit_aspect_list,
35
34
  draw_transit_aspect_grid,
36
- draw_moon_phase,
35
+ calculate_moon_phase_chart_params,
37
36
  draw_house_grid,
38
37
  draw_planet_grid,
39
38
  )
@@ -113,7 +112,7 @@ class KerykeionChartSVG:
113
112
  chart_colors_settings: dict
114
113
  planets_settings: dict
115
114
  aspects_settings: dict
116
- user: Union[AstrologicalSubject, AstrologicalSubjectModel]
115
+ user: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel]
117
116
  available_planets_setting: List[KerykeionSettingsCelestialPointModel]
118
117
  height: float
119
118
  location: str
@@ -123,7 +122,7 @@ class KerykeionChartSVG:
123
122
 
124
123
  def __init__(
125
124
  self,
126
- first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel],
125
+ first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel],
127
126
  chart_type: ChartType = "Natal",
128
127
  second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] = None,
129
128
  new_output_directory: Union[str, None] = None,
@@ -178,7 +177,7 @@ class KerykeionChartSVG:
178
177
  )
179
178
  self.aspects_list = natal_aspects_instance.relevant_aspects
180
179
 
181
- if self.chart_type == "Transit" or self.chart_type == "Synastry":
180
+ elif self.chart_type == "Transit" or self.chart_type == "Synastry":
182
181
  if not second_obj:
183
182
  raise KerykeionException("Second object is required for Transit or Synastry charts.")
184
183
 
@@ -198,6 +197,12 @@ class KerykeionChartSVG:
198
197
  for body in available_celestial_points_names:
199
198
  self.t_available_kerykeion_celestial_points.append(self.t_user.get(body))
200
199
 
200
+ elif self.chart_type == "Composite":
201
+ if not isinstance(first_obj, CompositeSubjectModel):
202
+ raise KerykeionException("First object must be a CompositeSubjectModel instance.")
203
+
204
+ self.aspects_list = NatalAspects(self.user, new_settings_file=self.new_settings_file, active_points=active_points).relevant_aspects
205
+
201
206
  # Double chart aspect grid type
202
207
  self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
203
208
 
@@ -210,12 +215,20 @@ class KerykeionChartSVG:
210
215
  else:
211
216
  self.width = self._DEFAULT_NATAL_WIDTH
212
217
 
213
- # default location
214
- self.location = self.user.city
215
- self.geolat = self.user.lat
216
- self.geolon = self.user.lng
217
-
218
- if self.chart_type == "Transit":
218
+ if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]:
219
+ self.location = self.user.city
220
+ self.geolat = self.user.lat
221
+ self.geolon = self.user.lng
222
+
223
+ elif self.chart_type == "Composite":
224
+ self.location = ""
225
+ self.geolat = (self.user.first_subject.lat + self.user.second_subject.lat) / 2
226
+ self.geolon = (self.user.first_subject.lng + self.user.second_subject.lng) / 2
227
+
228
+ elif self.chart_type in ["Transit"]:
229
+ self.location = self.t_user.city
230
+ self.geolat = self.t_user.lat
231
+ self.geolon = self.t_user.lng
219
232
  self.t_name = self.language_settings["transit_name"]
220
233
 
221
234
  # Default radius for the chart
@@ -393,7 +406,7 @@ class KerykeionChartSVG:
393
406
  ChartTemplateDictionary: A dictionary with template data for the chart.
394
407
  """
395
408
  # Initialize template dictionary
396
- template_dict: ChartTemplateDictionary = dict() # type: ignore
409
+ template_dict: dict = {}
397
410
 
398
411
  # Set the color style tag
399
412
  template_dict["color_style_tag"] = self.color_style_tag
@@ -402,11 +415,8 @@ class KerykeionChartSVG:
402
415
  template_dict["chart_height"] = self.height
403
416
  template_dict["chart_width"] = self.width
404
417
 
405
- # Set chart name
406
- template_dict["stringName"] = f"{self.user.name}:" if self.chart_type in ["Synastry", "Transit"] else f'{self.language_settings["info"]}:'
407
-
408
418
  # Set viewbox based on chart type
409
- if self.chart_type in ["Natal", "ExternalNatal"]:
419
+ if self.chart_type in ["Natal", "ExternalNatal", "Composite"]:
410
420
  template_dict['viewbox'] = self._BASIC_CHART_VIEWBOX
411
421
  elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit":
412
422
  template_dict['viewbox'] = self._TRANSIT_CHART_WITH_TABLE_VIWBOX
@@ -442,58 +452,85 @@ class KerykeionChartSVG:
442
452
  template_dict["stringTitle"] = f"{self.user.name} {self.language_settings['and_word']} {self.t_user.name}"
443
453
  elif self.chart_type == "Transit":
444
454
  template_dict["stringTitle"] = f"{self.language_settings['transits']} {self.t_user.day}/{self.t_user.month}/{self.t_user.year}"
445
- else:
455
+ elif self.chart_type in ["Natal", "ExternalNatal"]:
446
456
  template_dict["stringTitle"] = self.user.name
457
+ elif self.chart_type == "Composite":
458
+ template_dict["stringTitle"] = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}"
447
459
 
460
+ # Zodiac Type Info
448
461
  if self.user.zodiac_type == 'Tropic':
449
- zodiac_info = "Tropical Zodiac"
450
-
462
+ zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}"
451
463
  else:
452
464
  mode_const = "SIDM_" + self.user.sidereal_mode # type: ignore
453
465
  mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
454
- zodiac_info = f"Ayanamsa: {mode_name}"
466
+ zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
455
467
 
456
- template_dict["bottomLeft0"] = f"{self.user.houses_system_name.capitalize()} Houses"
457
- template_dict["bottomLeft1"] = zodiac_info
468
+ template_dict["bottom_left_0"] = f"{self.language_settings.get('houses_system_' + self.user.houses_system_identifier, self.user.houses_system_name)} {self.language_settings.get('houses', 'Houses')}"
469
+ template_dict["bottom_left_1"] = zodiac_info
458
470
 
459
471
  if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]:
460
- template_dict["bottomLeft2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")} {self.language_settings.get("day", "Day").lower()}: {self.user.lunar_phase.get("moon_phase", "")}'
461
- template_dict["bottomLeft3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.user.lunar_phase.moon_phase_name}'
462
- template_dict["bottomLeft4"] = f'{self.user.perspective_type}'
463
- else:
464
- template_dict["bottomLeft2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get("day", "Day")} {self.t_user.lunar_phase.get("moon_phase", "")}'
465
- template_dict["bottomLeft3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.t_user.lunar_phase.moon_phase_name}'
466
- template_dict["bottomLeft4"] = f'{self.t_user.perspective_type}'
472
+ template_dict["bottom_left_2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")} {self.language_settings.get("day", "Day").lower()}: {self.user.lunar_phase.get("moon_phase", "")}'
473
+ template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get(self.user.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.user.lunar_phase.moon_phase_name)}'
474
+ template_dict["bottom_left_4"] = f'{self.language_settings.get(self.user.perspective_type.lower().replace(" ", "_"), self.user.perspective_type)}'
475
+ elif self.chart_type == "Transit":
476
+ template_dict["bottom_left_2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get("day", "Day")} {self.t_user.lunar_phase.get("moon_phase", "")}'
477
+ template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.t_user.lunar_phase.moon_phase_name}'
478
+ template_dict["bottom_left_4"] = f'{self.language_settings.get(self.t_user.perspective_type.lower().replace(" ", "_"), self.t_user.perspective_type)}'
479
+ elif self.chart_type == "Composite":
480
+ template_dict["bottom_left_2"] = f'{self.user.first_subject.perspective_type}'
481
+ template_dict["bottom_left_3"] = f'{self.language_settings.get("composite_chart", "Composite Chart")} - {self.language_settings.get("midpoints", "Midpoints")}'
482
+ template_dict["bottom_left_4"] = ""
467
483
 
468
484
  # Draw moon phase
469
- template_dict['moon_phase'] = draw_moon_phase(
485
+ moon_phase_dict = calculate_moon_phase_chart_params(
470
486
  self.user.lunar_phase["degrees_between_s_m"],
471
487
  self.geolat
472
488
  )
473
489
 
490
+ template_dict["lunar_phase_rotate"] = moon_phase_dict["lunar_phase_rotate"]
491
+ template_dict["lunar_phase_circle_center_x"] = moon_phase_dict["circle_center_x"]
492
+ template_dict["lunar_phase_circle_radius"] = moon_phase_dict["circle_radius"]
493
+
494
+ if self.chart_type == "Composite":
495
+ template_dict["top_left_1"] = f"{datetime.fromisoformat(self.user.first_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}"
474
496
  # Set location string
475
- if len(self.location) > 35:
497
+ elif len(self.location) > 35:
476
498
  split_location = self.location.split(",")
477
499
  if len(split_location) > 1:
478
- template_dict["stringLocation"] = split_location[0] + ", " + split_location[-1]
479
- if len(template_dict["stringLocation"]) > 35:
480
- template_dict["stringLocation"] = template_dict["stringLocation"][:35] + "..."
500
+ template_dict["top_left_1"] = split_location[0] + ", " + split_location[-1]
501
+ if len(template_dict["top_left_1"]) > 35:
502
+ template_dict["top_left_1"] = template_dict["top_left_1"][:35] + "..."
481
503
  else:
482
- template_dict["stringLocation"] = self.location[:35] + "..."
504
+ template_dict["top_left_1"] = self.location[:35] + "..."
483
505
  else:
484
- template_dict["stringLocation"] = self.location
506
+ template_dict["top_left_1"] = self.location
507
+
508
+ # Set chart name
509
+ if self.chart_type in ["Synastry", "Transit"]:
510
+ template_dict["top_left_0"] = f"{self.user.name}:"
511
+ elif self.chart_type in ["Natal", "ExternalNatal"]:
512
+ template_dict["top_left_0"] = f'{self.language_settings["info"]}:'
513
+ elif self.chart_type == "Composite":
514
+ template_dict["top_left_0"] = f'{self.user.first_subject.name}'
485
515
 
486
516
  # Set additional information for Synastry chart type
487
517
  if self.chart_type == "Synastry":
488
- template_dict["stringLat"] = f"{self.t_user.name}: "
489
- template_dict["stringLon"] = self.t_user.city
490
- template_dict["stringPosition"] = f"{self.t_user.year}-{self.t_user.month}-{self.t_user.day} {self.t_user.hour:02d}:{self.t_user.minute:02d}"
518
+ template_dict["top_left_3"] = f"{self.t_user.name}: "
519
+ template_dict["top_left_4"] = self.t_user.city
520
+ template_dict["top_left_5"] = f"{self.t_user.year}-{self.t_user.month}-{self.t_user.day} {self.t_user.hour:02d}:{self.t_user.minute:02d}"
521
+ elif self.chart_type == "Composite":
522
+ template_dict["top_left_3"] = self.user.second_subject.name
523
+ template_dict["top_left_4"] = f"{datetime.fromisoformat(self.user.second_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}"
524
+ latitude_string = convert_latitude_coordinate_to_string(self.user.second_subject.lat, self.language_settings['north_letter'], self.language_settings['south_letter'])
525
+ longitude_string = convert_longitude_coordinate_to_string(self.user.second_subject.lng, self.language_settings['east_letter'], self.language_settings['west_letter'])
526
+ template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}"
491
527
  else:
492
528
  latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings['north'], self.language_settings['south'])
493
529
  longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings['east'], self.language_settings['west'])
494
- template_dict["stringLat"] = f"{self.language_settings['latitude']}: {latitude_string}"
495
- template_dict["stringLon"] = f"{self.language_settings['longitude']}: {longitude_string}"
496
- template_dict["stringPosition"] = f"{self.language_settings['type']}: {self.chart_type}"
530
+ template_dict["top_left_3"] = f"{self.language_settings['latitude']}: {latitude_string}"
531
+ template_dict["top_left_4"] = f"{self.language_settings['longitude']}: {longitude_string}"
532
+ template_dict["top_left_5"] = f"{self.language_settings['type']}: {self.language_settings.get(self.chart_type, self.chart_type)}"
533
+
497
534
 
498
535
  # Set paper colors
499
536
  template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"]
@@ -589,16 +626,17 @@ class KerykeionChartSVG:
589
626
  )
590
627
 
591
628
  # Draw elements percentages
592
- template_dict["elements_percentages"] = draw_elements_percentages(
593
- self.language_settings['fire'],
594
- self.fire,
595
- self.language_settings['earth'],
596
- self.earth,
597
- self.language_settings['air'],
598
- self.air,
599
- self.language_settings['water'],
600
- self.water,
601
- )
629
+ total = self.fire + self.water + self.earth + self.air
630
+
631
+ fire_percentage = int(round(100 * self.fire / total))
632
+ earth_percentage = int(round(100 * self.earth / total))
633
+ air_percentage = int(round(100 * self.air / total))
634
+ water_percentage = int(round(100 * self.water / total))
635
+
636
+ template_dict["fire_string"] = f"{self.language_settings['fire']} {fire_percentage}%"
637
+ template_dict["earth_string"] = f"{self.language_settings['earth']} {earth_percentage}%"
638
+ template_dict["air_string"] = f"{self.language_settings['air']} {air_percentage}%"
639
+ template_dict["water_string"] = f"{self.language_settings['water']} {water_percentage}%"
602
640
 
603
641
  # Draw planet grid
604
642
  if self.chart_type in ["Transit", "Synastry"]:
@@ -606,6 +644,7 @@ class KerykeionChartSVG:
606
644
  second_subject_table_name = self.language_settings["transit_name"]
607
645
  else:
608
646
  second_subject_table_name = self.t_user.name
647
+
609
648
  template_dict["makePlanetGrid"] = draw_planet_grid(
610
649
  planets_and_houses_grid_title=self.language_settings["planets_and_house"],
611
650
  subject_name=self.user.name,
@@ -617,9 +656,14 @@ class KerykeionChartSVG:
617
656
  second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
618
657
  )
619
658
  else:
659
+ if self.chart_type == "Composite":
660
+ subject_name = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}"
661
+ else:
662
+ subject_name = self.user.name
663
+
620
664
  template_dict["makePlanetGrid"] = draw_planet_grid(
621
665
  planets_and_houses_grid_title=self.language_settings["planets_and_house"],
622
- subject_name=self.user.name,
666
+ subject_name=subject_name,
623
667
  available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
624
668
  chart_type=self.chart_type,
625
669
  text_color=self.chart_colors_settings["paper_0"],
@@ -627,12 +671,18 @@ class KerykeionChartSVG:
627
671
  )
628
672
 
629
673
  # Set date time string
630
- dt = datetime.fromisoformat(self.user.iso_formatted_local_datetime)
631
- custom_format = dt.strftime('%Y-%m-%d %H:%M [%z]')
632
- custom_format = custom_format[:-3] + ':' + custom_format[-3:]
633
- template_dict["stringDateTime"] = f"{custom_format}"
674
+ if self.chart_type in ["Composite"]:
675
+ # First Subject Latitude and Longitude
676
+ latitude = convert_latitude_coordinate_to_string(self.user.first_subject.lat, self.language_settings["north_letter"], self.language_settings["south_letter"])
677
+ longitude = convert_longitude_coordinate_to_string(self.user.first_subject.lng, self.language_settings["east_letter"], self.language_settings["west_letter"])
678
+ template_dict["top_left_2"] = f"{latitude} {longitude}"
679
+ else:
680
+ dt = datetime.fromisoformat(self.user.iso_formatted_local_datetime)
681
+ custom_format = dt.strftime('%Y-%m-%d %H:%M [%z]')
682
+ custom_format = custom_format[:-3] + ':' + custom_format[-3:]
683
+ template_dict["top_left_2"] = f"{custom_format}"
634
684
 
635
- return template_dict
685
+ return ChartTemplateDictionary(**template_dict)
636
686
 
637
687
  def makeTemplate(self, minify: bool = False) -> str:
638
688
  """Creates the template for the SVG file"""
@@ -734,6 +784,7 @@ class KerykeionChartSVG:
734
784
 
735
785
  if __name__ == "__main__":
736
786
  from kerykeion.utilities import setup_logging
787
+ from kerykeion.composite_subject_factory import CompositeSubjectFactory
737
788
  setup_logging(level="debug")
738
789
 
739
790
  first = AstrologicalSubject("John Lennon", 1940, 10, 9, 18, 30, "Liverpool", "GB")
@@ -952,3 +1003,12 @@ if __name__ == "__main__":
952
1003
  kanye_west_subject = AstrologicalSubject("Kanye", 1977, 6, 8, 8, 45, "Atlanta", "US")
953
1004
  kanye_west_chart = KerykeionChartSVG(kanye_west_subject)
954
1005
  kanye_west_chart.makeSVG()
1006
+
1007
+ # Composite Chart
1008
+ angelina = AstrologicalSubject("Angelina Jolie", 1975, 6, 4, 9, 9, "Los Angeles", "US", lng=-118.15, lat=34.03, tz_str="America/Los_Angeles")
1009
+ brad = AstrologicalSubject("Brad Pitt", 1963, 12, 18, 6, 31, "Shawnee", "US", lng=-96.56, lat=35.20, tz_str="America/Chicago")
1010
+
1011
+ composite_subject_factory = CompositeSubjectFactory(angelina, brad)
1012
+ composite_subject_model = composite_subject_factory.get_midpoint_composite_subject_model()
1013
+ composite_chart = KerykeionChartSVG(composite_subject_model, "Composite")
1014
+ composite_chart.makeSVG()
@@ -26,22 +26,31 @@ OpenAstro.org -->
26
26
  <rect class="background-rectangle" x="0" y="0" width="$chart_width"
27
27
  height="$chart_height" style="fill: $paper_color_1" />
28
28
  <text x="20" y="22" style="fill: $paper_color_0; font-size: 24px">$stringTitle</text>
29
- <text x="20" y="50" style="fill: $paper_color_0; font-size: 11px">$stringName</text>
30
- <text x="20" y="62" style="fill: $paper_color_0; font-size: 11px">$stringLocation</text>
31
- <text x="20" y="74" style="fill: $paper_color_0; font-size: 11px">$stringDateTime</text>
32
- <text x="20" y="86" style="fill: $paper_color_0; font-size: 11px">$stringLat</text>
33
- <text x="20" y="98" style="fill: $paper_color_0; font-size: 11px">$stringLon</text>
34
- <text x="20" y="110" style="fill: $paper_color_0; font-size: 11px">$stringPosition</text>
35
- <text x="20" y="452" style="fill: $paper_color_0; font-size: 10px">$bottomLeft0</text>
36
- <text x="20" y="466" style="fill: $paper_color_0; font-size: 10px">$bottomLeft1</text>
37
- <text x="20" y="480" style="fill: $paper_color_0; font-size: 10px">$bottomLeft2</text>
38
- <text x="20" y="494" style="fill: $paper_color_0; font-size: 10px">$bottomLeft3</text>
39
- <text x="20" y="508" style="fill: $paper_color_0; font-size: 10px">$bottomLeft4</text>
29
+ <text x="20" y="50" style="fill: $paper_color_0; font-size: 10px">$top_left_0</text>
30
+ <text x="20" y="62" style="fill: $paper_color_0; font-size: 10px">$top_left_1</text>
31
+ <text x="20" y="74" style="fill: $paper_color_0; font-size: 10px">$top_left_2</text>
32
+ <text x="20" y="86" style="fill: $paper_color_0; font-size: 10px">$top_left_3</text>
33
+ <text x="20" y="98" style="fill: $paper_color_0; font-size: 10px">$top_left_4</text>
34
+ <text x="20" y="110" style="fill: $paper_color_0; font-size: 10px">$top_left_5</text>
35
+ <text x="20" y="452" style="fill: $paper_color_0; font-size: 10px">$bottom_left_0</text>
36
+ <text x="20" y="466" style="fill: $paper_color_0; font-size: 10px">$bottom_left_1</text>
37
+ <text x="20" y="480" style="fill: $paper_color_0; font-size: 10px">$bottom_left_2</text>
38
+ <text x="20" y="494" style="fill: $paper_color_0; font-size: 10px">$bottom_left_3</text>
39
+ <text x="20" y="508" style="fill: $paper_color_0; font-size: 10px">$bottom_left_4</text>
40
40
  </g>
41
41
 
42
42
  <!-- Lunar Phase -->
43
43
  <g kr:node="Lunar_Phase" transform="translate(20,518)">
44
- $moon_phase
44
+ <g transform="rotate($lunar_phase_rotate 20 10)">
45
+ <defs>
46
+ <clipPath id="moonPhaseCutOffCircle">
47
+ <circle cx="20" cy="10" r="10" />
48
+ </clipPath>
49
+ </defs>
50
+ <circle cx="20" cy="10" r="10" style="fill: var(--kerykeion-chart-color-lunar-phase-0)" />
51
+ <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)" />
52
+ <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" />
53
+ </g>
45
54
  </g>
46
55
 
47
56
  <g kr:node="Main_Content" transform="translate(50,50)">
@@ -100,7 +109,12 @@ OpenAstro.org -->
100
109
 
101
110
  <!-- Elements -->
102
111
  <g kr:node="Elements_Percentages">
103
- $elements_percentages
112
+ <g transform="translate(-30,79)">
113
+ <text y="0" style="fill: var(--kerykeion-chart-color-fire-percentage); font-size: 10px;">$fire_string</text>
114
+ <text y="12" style="fill: var(--kerykeion-chart-color-earth-percentage); font-size: 10px;">$earth_string</text>
115
+ <text y="24" style="fill: var(--kerykeion-chart-color-air-percentage); font-size: 10px;">$air_string</text>
116
+ <text y="36" style="fill: var(--kerykeion-chart-color-water-percentage); font-size: 10px;">$water_string</text>
117
+ </g>"
104
118
  </g>
105
119
 
106
120
  <!-- Houses_And_Planets_Grid -->