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
@@ -0,0 +1,484 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Chart Data Factory Module
4
+
5
+ This module provides factory classes for creating comprehensive chart data models that include
6
+ all the pure data from astrological charts, including subjects, aspects, house comparisons,
7
+ and other analytical data without the visual rendering components.
8
+
9
+ This is designed to be the "pure data" counterpart to ChartDrawer, providing structured
10
+ access to all chart information for API consumption, data analysis, or other programmatic uses.
11
+
12
+ Key Features:
13
+ - Comprehensive chart data including subjects and aspects
14
+ - House comparison analysis for dual charts
15
+ - Element and quality distributions
16
+ - Relationship scoring for synastry charts
17
+ - Flexible point and aspect filtering
18
+ - Support for all chart types (Natal, Transit, Synastry, Composite, Return)
19
+
20
+ Classes:
21
+ ElementDistributionModel: Model for element distribution analysis
22
+ QualityDistributionModel: Model for quality distribution analysis
23
+ SingleChartDataModel: Model for single-subject chart data
24
+ DualChartDataModel: Model for dual-subject chart data
25
+ ChartDataFactory: Factory for creating chart data models
26
+
27
+ Author: Giacomo Battaglia
28
+ Copyright: (C) 2025 Kerykeion Project
29
+ License: AGPL-3.0
30
+ """
31
+
32
+ from typing import Union, Optional, List, Literal, cast
33
+
34
+ from kerykeion.aspects import AspectsFactory
35
+ from kerykeion.house_comparison.house_comparison_factory import HouseComparisonFactory
36
+ from kerykeion.relationship_score_factory import RelationshipScoreFactory
37
+ from kerykeion.schemas import (
38
+ KerykeionException,
39
+ ChartType,
40
+ ActiveAspect
41
+ )
42
+ from kerykeion.schemas.kr_models import (
43
+ AstrologicalSubjectModel,
44
+ CompositeSubjectModel,
45
+ PlanetReturnModel,
46
+ SingleChartAspectsModel,
47
+ DualChartAspectsModel,
48
+ ElementDistributionModel,
49
+ QualityDistributionModel,
50
+ SingleChartDataModel,
51
+ DualChartDataModel,
52
+ ChartDataModel
53
+ )
54
+ from kerykeion.schemas.settings_models import KerykeionSettingsCelestialPointModel
55
+ from kerykeion.schemas.kr_literals import (
56
+ AstrologicalPoint,
57
+ )
58
+ from kerykeion.utilities import find_common_active_points, distribute_percentages_to_100
59
+ from kerykeion.settings.config_constants import DEFAULT_ACTIVE_ASPECTS
60
+ from kerykeion.settings.legacy.legacy_celestial_points_settings import DEFAULT_CELESTIAL_POINTS_SETTINGS
61
+ from kerykeion.charts.charts_utils import (
62
+ calculate_element_points,
63
+ calculate_quality_points,
64
+ calculate_synastry_element_points,
65
+ calculate_synastry_quality_points,
66
+ )
67
+
68
+
69
+
70
+ class ChartDataFactory:
71
+ """
72
+ Factory class for creating comprehensive chart data models.
73
+
74
+ This factory creates ChartDataModel instances containing all the pure data
75
+ from astrological charts, including subjects, aspects, house comparisons,
76
+ and analytical metrics. It provides the structured data equivalent of
77
+ ChartDrawer's visual output.
78
+
79
+ The factory handles all chart types and automatically includes relevant
80
+ analyses based on chart type (e.g., house comparison for dual charts,
81
+ relationship scoring for synastry charts).
82
+
83
+ Example:
84
+ >>> # Create natal chart data
85
+ >>> john = AstrologicalSubjectFactory.from_birth_data("John", 1990, 1, 1, 12, 0, "London", "GB")
86
+ >>> natal_data = ChartDataFactory.create_chart_data("Natal", john)
87
+ >>> print(f"Elements: Fire {natal_data.element_distribution.fire_percentage}%")
88
+ >>>
89
+ >>> # Create synastry chart data
90
+ >>> jane = AstrologicalSubjectFactory.from_birth_data("Jane", 1992, 6, 15, 14, 30, "Paris", "FR")
91
+ >>> synastry_data = ChartDataFactory.create_chart_data("Synastry", john, jane)
92
+ >>> print(f"Relationship score: {synastry_data.relationship_score.score_value}")
93
+ """
94
+
95
+ @staticmethod
96
+ def create_chart_data(
97
+ chart_type: ChartType,
98
+ first_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
99
+ second_subject: Optional[Union[AstrologicalSubjectModel, PlanetReturnModel]] = None,
100
+ active_points: Optional[List[AstrologicalPoint]] = None,
101
+ active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
102
+ include_house_comparison: bool = True,
103
+ include_relationship_score: bool = True,
104
+ ) -> ChartDataModel:
105
+ """
106
+ Create comprehensive chart data for the specified chart type.
107
+
108
+ Args:
109
+ chart_type: Type of chart to create data for
110
+ first_subject: Primary astrological subject
111
+ second_subject: Secondary subject (required for dual charts)
112
+ active_points: Points to include in calculations (defaults to first_subject.active_points)
113
+ active_aspects: Aspect types and orbs to use
114
+ include_house_comparison: Whether to include house comparison for dual charts
115
+ include_relationship_score: Whether to include relationship scoring for synastry
116
+
117
+ Returns:
118
+ ChartDataModel: Comprehensive chart data model
119
+
120
+ Raises:
121
+ KerykeionException: If chart type requirements are not met
122
+ """
123
+
124
+ # Validate chart type requirements
125
+ if chart_type in ["Transit", "Synastry", "DualReturnChart"] and not second_subject:
126
+ raise KerykeionException(f"Second subject is required for {chart_type} charts.")
127
+
128
+ if chart_type == "Composite" and not isinstance(first_subject, CompositeSubjectModel):
129
+ raise KerykeionException("First subject must be a CompositeSubjectModel for Composite charts.")
130
+
131
+ if chart_type == "Return" and not isinstance(second_subject, PlanetReturnModel):
132
+ raise KerykeionException("Second subject must be a PlanetReturnModel for Return charts.")
133
+
134
+ if chart_type == "SingleReturnChart" and not isinstance(first_subject, PlanetReturnModel):
135
+ raise KerykeionException("First subject must be a PlanetReturnModel for SingleReturnChart charts.")
136
+
137
+ # Determine active points
138
+ if not active_points:
139
+ effective_active_points = first_subject.active_points
140
+ else:
141
+ effective_active_points = find_common_active_points(
142
+ active_points,
143
+ first_subject.active_points
144
+ )
145
+
146
+ # For dual charts, further filter by second subject's active points
147
+ if second_subject:
148
+ effective_active_points = find_common_active_points(
149
+ effective_active_points,
150
+ second_subject.active_points
151
+ )
152
+
153
+ # Calculate aspects based on chart type
154
+ aspects: Union[SingleChartAspectsModel, DualChartAspectsModel]
155
+ if chart_type in ["Natal", "Composite", "SingleReturnChart"]:
156
+ # Single chart aspects
157
+ aspects = AspectsFactory.single_chart_aspects(
158
+ first_subject,
159
+ active_points=effective_active_points,
160
+ active_aspects=active_aspects,
161
+ )
162
+ else:
163
+ # Dual chart aspects - second_subject is guaranteed to exist here due to validation above
164
+ if second_subject is None:
165
+ raise KerykeionException(f"Second subject is required for {chart_type} charts.")
166
+ aspects = AspectsFactory.dual_chart_aspects(
167
+ first_subject,
168
+ second_subject,
169
+ active_points=effective_active_points,
170
+ active_aspects=active_aspects,
171
+ )
172
+
173
+ # Calculate house comparison for dual charts
174
+ house_comparison = None
175
+ if second_subject and include_house_comparison and chart_type in ["Transit", "Synastry", "DualReturnChart"]:
176
+ if isinstance(first_subject, AstrologicalSubjectModel) and isinstance(second_subject, (AstrologicalSubjectModel, PlanetReturnModel)):
177
+ house_comparison_factory = HouseComparisonFactory(
178
+ first_subject,
179
+ second_subject,
180
+ active_points=effective_active_points
181
+ )
182
+ house_comparison = house_comparison_factory.get_house_comparison()
183
+
184
+ # Calculate relationship score for synastry
185
+ relationship_score = None
186
+ if chart_type == "Synastry" and include_relationship_score and second_subject:
187
+ if isinstance(first_subject, AstrologicalSubjectModel) and isinstance(second_subject, AstrologicalSubjectModel):
188
+ relationship_score_factory = RelationshipScoreFactory(
189
+ first_subject,
190
+ second_subject
191
+ )
192
+ relationship_score = relationship_score_factory.get_relationship_score()
193
+
194
+ # Calculate element and quality distributions
195
+ celestial_points_settings = DEFAULT_CELESTIAL_POINTS_SETTINGS
196
+ available_planets_setting_dicts: list[dict[str, object]] = []
197
+ for body in celestial_points_settings:
198
+ if body["name"] in effective_active_points:
199
+ body["is_active"] = True
200
+ available_planets_setting_dicts.append(body)
201
+
202
+ # Convert to models for type safety
203
+ available_planets_setting: list[KerykeionSettingsCelestialPointModel] = [
204
+ KerykeionSettingsCelestialPointModel(**body) for body in available_planets_setting_dicts # type: ignore
205
+ ]
206
+
207
+ celestial_points_names = [body.name.lower() for body in available_planets_setting]
208
+
209
+ if chart_type == "Synastry" and second_subject:
210
+ # Calculate combined element/quality points for synastry
211
+ # Type narrowing: ensure both subjects are AstrologicalSubjectModel for synastry
212
+ if isinstance(first_subject, AstrologicalSubjectModel) and isinstance(second_subject, AstrologicalSubjectModel):
213
+ element_totals = calculate_synastry_element_points(
214
+ available_planets_setting,
215
+ celestial_points_names,
216
+ first_subject,
217
+ second_subject,
218
+ )
219
+ quality_totals = calculate_synastry_quality_points(
220
+ available_planets_setting,
221
+ celestial_points_names,
222
+ first_subject,
223
+ second_subject,
224
+ )
225
+ else:
226
+ # Fallback to single chart calculation for incompatible types
227
+ element_totals = calculate_element_points(
228
+ available_planets_setting,
229
+ celestial_points_names,
230
+ first_subject,
231
+ )
232
+ quality_totals = calculate_quality_points(
233
+ available_planets_setting,
234
+ celestial_points_names,
235
+ first_subject,
236
+ )
237
+ else:
238
+ # Calculate element/quality points for single chart
239
+ element_totals = calculate_element_points(
240
+ available_planets_setting,
241
+ celestial_points_names,
242
+ first_subject,
243
+ )
244
+ quality_totals = calculate_quality_points(
245
+ available_planets_setting,
246
+ celestial_points_names,
247
+ first_subject,
248
+ )
249
+
250
+ # Calculate percentages
251
+ total_elements = element_totals["fire"] + element_totals["water"] + element_totals["earth"] + element_totals["air"]
252
+ element_percentages = distribute_percentages_to_100(element_totals) if total_elements > 0 else {"fire": 0, "earth": 0, "air": 0, "water": 0}
253
+ element_distribution = ElementDistributionModel(
254
+ fire=element_totals["fire"],
255
+ earth=element_totals["earth"],
256
+ air=element_totals["air"],
257
+ water=element_totals["water"],
258
+ fire_percentage=element_percentages["fire"],
259
+ earth_percentage=element_percentages["earth"],
260
+ air_percentage=element_percentages["air"],
261
+ water_percentage=element_percentages["water"],
262
+ )
263
+
264
+ total_qualities = quality_totals["cardinal"] + quality_totals["fixed"] + quality_totals["mutable"]
265
+ quality_percentages = distribute_percentages_to_100(quality_totals) if total_qualities > 0 else {"cardinal": 0, "fixed": 0, "mutable": 0}
266
+ quality_distribution = QualityDistributionModel(
267
+ cardinal=quality_totals["cardinal"],
268
+ fixed=quality_totals["fixed"],
269
+ mutable=quality_totals["mutable"],
270
+ cardinal_percentage=quality_percentages["cardinal"],
271
+ fixed_percentage=quality_percentages["fixed"],
272
+ mutable_percentage=quality_percentages["mutable"],
273
+ )
274
+
275
+ # Create and return the appropriate chart data model
276
+ if chart_type in ["Natal", "Composite", "SingleReturnChart"]:
277
+ # Single chart data model - cast types since they're already validated
278
+ return SingleChartDataModel(
279
+ chart_type=cast(Literal["Natal", "Composite", "SingleReturnChart"], chart_type),
280
+ subject=first_subject,
281
+ aspects=cast(SingleChartAspectsModel, aspects),
282
+ element_distribution=element_distribution,
283
+ quality_distribution=quality_distribution,
284
+ active_points=effective_active_points,
285
+ active_aspects=active_aspects,
286
+ )
287
+ else:
288
+ # Dual chart data model - cast types since they're already validated
289
+ if second_subject is None:
290
+ raise KerykeionException(f"Second subject is required for {chart_type} charts.")
291
+ return DualChartDataModel(
292
+ chart_type=cast(Literal["Transit", "Synastry", "DualReturnChart"], chart_type),
293
+ first_subject=first_subject,
294
+ second_subject=second_subject,
295
+ aspects=cast(DualChartAspectsModel, aspects),
296
+ house_comparison=house_comparison,
297
+ relationship_score=relationship_score,
298
+ element_distribution=element_distribution,
299
+ quality_distribution=quality_distribution,
300
+ active_points=effective_active_points,
301
+ active_aspects=active_aspects,
302
+ )
303
+
304
+ @staticmethod
305
+ def create_natal_chart_data(
306
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
307
+ active_points: Optional[List[AstrologicalPoint]] = None,
308
+ active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
309
+ ) -> ChartDataModel:
310
+ """
311
+ Convenience method for creating natal chart data.
312
+
313
+ Args:
314
+ subject: Astrological subject
315
+ active_points: Points to include in calculations
316
+ active_aspects: Aspect types and orbs to use
317
+
318
+ Returns:
319
+ ChartDataModel: Natal chart data
320
+ """
321
+ return ChartDataFactory.create_chart_data(
322
+ first_subject=subject,
323
+ chart_type="Natal",
324
+ active_points=active_points,
325
+ active_aspects=active_aspects,
326
+ )
327
+
328
+ @staticmethod
329
+ def create_synastry_chart_data(
330
+ first_subject: AstrologicalSubjectModel,
331
+ second_subject: AstrologicalSubjectModel,
332
+ active_points: Optional[List[AstrologicalPoint]] = None,
333
+ active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
334
+ include_house_comparison: bool = True,
335
+ include_relationship_score: bool = True,
336
+ ) -> ChartDataModel:
337
+ """
338
+ Convenience method for creating synastry chart data.
339
+
340
+ Args:
341
+ first_subject: First astrological subject
342
+ second_subject: Second astrological subject
343
+ active_points: Points to include in calculations
344
+ active_aspects: Aspect types and orbs to use
345
+ include_house_comparison: Whether to include house comparison
346
+ include_relationship_score: Whether to include relationship scoring
347
+
348
+ Returns:
349
+ ChartDataModel: Synastry chart data
350
+ """
351
+ return ChartDataFactory.create_chart_data(
352
+ first_subject=first_subject,
353
+ chart_type="Synastry",
354
+ second_subject=second_subject,
355
+ active_points=active_points,
356
+ active_aspects=active_aspects,
357
+ include_house_comparison=include_house_comparison,
358
+ include_relationship_score=include_relationship_score,
359
+ )
360
+
361
+ @staticmethod
362
+ def create_transit_chart_data(
363
+ natal_subject: AstrologicalSubjectModel,
364
+ transit_subject: AstrologicalSubjectModel,
365
+ active_points: Optional[List[AstrologicalPoint]] = None,
366
+ active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
367
+ include_house_comparison: bool = True,
368
+ ) -> ChartDataModel:
369
+ """
370
+ Convenience method for creating transit chart data.
371
+
372
+ Args:
373
+ natal_subject: Natal astrological subject
374
+ transit_subject: Transit astrological subject
375
+ active_points: Points to include in calculations
376
+ active_aspects: Aspect types and orbs to use
377
+ include_house_comparison: Whether to include house comparison
378
+
379
+ Returns:
380
+ ChartDataModel: Transit chart data
381
+ """
382
+ return ChartDataFactory.create_chart_data(
383
+ first_subject=natal_subject,
384
+ chart_type="Transit",
385
+ second_subject=transit_subject,
386
+ active_points=active_points,
387
+ active_aspects=active_aspects,
388
+ include_house_comparison=include_house_comparison,
389
+ )
390
+
391
+ @staticmethod
392
+ def create_composite_chart_data(
393
+ composite_subject: CompositeSubjectModel,
394
+ active_points: Optional[List[AstrologicalPoint]] = None,
395
+ active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
396
+ ) -> ChartDataModel:
397
+ """
398
+ Convenience method for creating composite chart data.
399
+
400
+ Args:
401
+ composite_subject: Composite astrological subject
402
+ active_points: Points to include in calculations
403
+ active_aspects: Aspect types and orbs to use
404
+
405
+ Returns:
406
+ ChartDataModel: Composite chart data
407
+ """
408
+ return ChartDataFactory.create_chart_data(
409
+ first_subject=composite_subject,
410
+ chart_type="Composite",
411
+ active_points=active_points,
412
+ active_aspects=active_aspects,
413
+ )
414
+
415
+ @staticmethod
416
+ def create_return_chart_data(
417
+ natal_subject: AstrologicalSubjectModel,
418
+ return_subject: PlanetReturnModel,
419
+ active_points: Optional[List[AstrologicalPoint]] = None,
420
+ active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
421
+ include_house_comparison: bool = True,
422
+ ) -> ChartDataModel:
423
+ """
424
+ Convenience method for creating planetary return chart data.
425
+
426
+ Args:
427
+ natal_subject: Natal astrological subject
428
+ return_subject: Planetary return subject
429
+ active_points: Points to include in calculations
430
+ active_aspects: Aspect types and orbs to use
431
+ include_house_comparison: Whether to include house comparison
432
+
433
+ Returns:
434
+ ChartDataModel: Return chart data
435
+ """
436
+ return ChartDataFactory.create_chart_data(
437
+ first_subject=natal_subject,
438
+ chart_type="DualReturnChart",
439
+ second_subject=return_subject,
440
+ active_points=active_points,
441
+ active_aspects=active_aspects,
442
+ include_house_comparison=include_house_comparison,
443
+ )
444
+
445
+ @staticmethod
446
+ def create_single_wheel_return_chart_data(
447
+ return_subject: PlanetReturnModel,
448
+ active_points: Optional[List[AstrologicalPoint]] = None,
449
+ active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
450
+ ) -> ChartDataModel:
451
+ """
452
+ Convenience method for creating single wheel planetary return chart data.
453
+
454
+ Args:
455
+ return_subject: Planetary return subject
456
+ active_points: Points to include in calculations
457
+ active_aspects: Aspect types and orbs to use
458
+
459
+ Returns:
460
+ ChartDataModel: Single wheel return chart data
461
+ """
462
+ return ChartDataFactory.create_chart_data(
463
+ first_subject=return_subject,
464
+ chart_type="SingleReturnChart",
465
+ active_points=active_points,
466
+ active_aspects=active_aspects,
467
+ )
468
+
469
+
470
+ if __name__ == "__main__":
471
+ # Example usage
472
+ from kerykeion.astrological_subject_factory import AstrologicalSubjectFactory
473
+
474
+ # Create a natal chart data
475
+ subject = AstrologicalSubjectFactory.from_current_time(name="Test Subject")
476
+ natal_data = ChartDataFactory.create_natal_chart_data(subject)
477
+
478
+ print(f"Chart Type: {natal_data.chart_type}")
479
+ print(f"Active Points: {len(natal_data.active_points)}")
480
+ print(f"Aspects: {len(natal_data.aspects.relevant_aspects)}")
481
+ print(f"Fire: {natal_data.element_distribution.fire_percentage}%")
482
+ print(f"Earth: {natal_data.element_distribution.earth_percentage}%")
483
+ print(f"Air: {natal_data.element_distribution.air_percentage}%")
484
+ print(f"Water: {natal_data.element_distribution.water_percentage}%")