kerykeion 4.12.3__py3-none-any.whl → 4.18.0__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 (36) hide show
  1. kerykeion/__init__.py +3 -1
  2. kerykeion/aspects/aspects_utils.py +40 -123
  3. kerykeion/aspects/natal_aspects.py +34 -25
  4. kerykeion/aspects/synastry_aspects.py +34 -28
  5. kerykeion/astrological_subject.py +199 -196
  6. kerykeion/charts/charts_utils.py +701 -62
  7. kerykeion/charts/draw_planets.py +407 -0
  8. kerykeion/charts/kerykeion_chart_svg.py +534 -1140
  9. kerykeion/charts/templates/aspect_grid_only.xml +452 -0
  10. kerykeion/charts/templates/chart.xml +88 -70
  11. kerykeion/charts/templates/wheel_only.xml +499 -0
  12. kerykeion/charts/themes/classic.css +82 -0
  13. kerykeion/charts/themes/dark-high-contrast.css +121 -0
  14. kerykeion/charts/themes/dark.css +121 -0
  15. kerykeion/charts/themes/light.css +117 -0
  16. kerykeion/enums.py +1 -0
  17. kerykeion/ephemeris_data.py +178 -0
  18. kerykeion/fetch_geonames.py +2 -3
  19. kerykeion/kr_types/chart_types.py +6 -16
  20. kerykeion/kr_types/kr_literals.py +12 -3
  21. kerykeion/kr_types/kr_models.py +77 -32
  22. kerykeion/kr_types/settings_models.py +4 -10
  23. kerykeion/relationship_score/__init__.py +2 -0
  24. kerykeion/relationship_score/relationship_score.py +175 -0
  25. kerykeion/relationship_score/relationship_score_factory.py +275 -0
  26. kerykeion/report.py +6 -3
  27. kerykeion/settings/kerykeion_settings.py +6 -1
  28. kerykeion/settings/kr.config.json +256 -102
  29. kerykeion/utilities.py +122 -217
  30. {kerykeion-4.12.3.dist-info → kerykeion-4.18.0.dist-info}/METADATA +40 -10
  31. kerykeion-4.18.0.dist-info/RECORD +42 -0
  32. kerykeion/relationship_score.py +0 -205
  33. kerykeion-4.12.3.dist-info/RECORD +0 -32
  34. {kerykeion-4.12.3.dist-info → kerykeion-4.18.0.dist-info}/LICENSE +0 -0
  35. {kerykeion-4.12.3.dist-info → kerykeion-4.18.0.dist-info}/WHEEL +0 -0
  36. {kerykeion-4.12.3.dist-info → kerykeion-4.18.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,40 @@
1
1
  import math
2
2
  import datetime
3
3
  from kerykeion.kr_types import KerykeionException, ChartType
4
- from typing import Union
4
+ from typing import Union, Literal
5
+ from kerykeion.kr_types.kr_models import AspectModel, KerykeionPointModel
6
+ from kerykeion.kr_types.settings_models import KerykeionLanguageCelestialPointModel, KerykeionSettingsAspectModel
7
+
8
+
9
+ def get_decoded_kerykeion_celestial_point_name(input_planet_name: str, celestial_point_language: KerykeionLanguageCelestialPointModel) -> str:
10
+ """
11
+ Decode the given celestial point name based on the provided language model.
12
+
13
+ Args:
14
+ input_planet_name (str): The name of the celestial point to decode.
15
+ celestial_point_language (KerykeionLanguageCelestialPointModel): The language model containing celestial point names.
16
+
17
+ Returns:
18
+ str: The decoded celestial point name.
19
+ """
20
+
21
+ # Dictionary for special house names
22
+ special_house_names = {
23
+ "First_House": "Asc",
24
+ "Seventh_House": "Dsc",
25
+ "Tenth_House": "Mc",
26
+ "Fourth_House": "Ic"
27
+ }
28
+
29
+ # Get the language model keys
30
+ language_keys = celestial_point_language.model_dump().keys()
31
+
32
+ # Check if the input planet name exists in the language model
33
+ if input_planet_name in language_keys:
34
+ return celestial_point_language[input_planet_name]
35
+
36
+ # Return the special house name if it exists, otherwise return an empty string
37
+ return special_house_names.get(input_planet_name, "")
5
38
 
6
39
 
7
40
  def decHourJoin(inH: int, inM: int, inS: int) -> float:
@@ -130,10 +163,10 @@ def draw_zodiac_slice(
130
163
  - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
131
164
  - num (int): The number of the sign. Note: In OpenAstro it did refer to self.zodiac,
132
165
  which is a list of the signs in order, starting with Aries. Eg:
133
- {"name": "aries", "element": "fire"}
166
+ {"name": "Ari", "element": "fire"}
134
167
  - r (Union[int, float]): The value of r.
135
168
  - style (str): The CSS inline style.
136
- - type (str): The type ?. In OpenAstro, it was the symbol of the sign. Eg: "aries".
169
+ - type (str): The type ?. In OpenAstro, it was the symbol of the sign. Eg: "Ari".
137
170
  self.zodiac[i]["name"]
138
171
 
139
172
  Returns:
@@ -144,7 +177,7 @@ def draw_zodiac_slice(
144
177
  offset = 360 - seventh_house_degree_ut
145
178
  # check transit
146
179
  if chart_type == "Transit" or chart_type == "Synastry":
147
- dropin = 0
180
+ dropin: Union[int, float] = 0
148
181
  else:
149
182
  dropin = c1
150
183
  slice = f'<path d="M{str(r)},{str(r)} L{str(dropin + sliceToX(num, r - dropin, offset))},{str(dropin + sliceToY(num, r - dropin, offset))} A{str(r - dropin)},{str(r - dropin)} 0 0,0 {str(dropin + sliceToX(num + 1, r - dropin, offset))},{str(dropin + sliceToY(num + 1, r - dropin, offset))} z" style="{style}"/>'
@@ -212,8 +245,7 @@ def convert_longitude_coordinate_to_string(coord: Union[int, float], east_label:
212
245
  def draw_aspect_line(
213
246
  r: Union[int, float],
214
247
  ar: Union[int, float],
215
- degA: Union[int, float],
216
- degB: Union[int, float],
248
+ aspect: Union[AspectModel, dict],
217
249
  color: str,
218
250
  seventh_house_degree_ut: Union[int, float],
219
251
  ) -> str:
@@ -222,8 +254,7 @@ def draw_aspect_line(
222
254
  Args:
223
255
  - r (Union[int, float]): The value of r.
224
256
  - ar (Union[int, float]): The value of ar.
225
- - degA (Union[int, float]): The degree of A.
226
- - degB (Union[int, float]): The degree of B.
257
+ - aspect_dict (dict): The aspect dictionary.
227
258
  - color (str): The color of the aspect.
228
259
  - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
229
260
 
@@ -231,17 +262,22 @@ def draw_aspect_line(
231
262
  str: The SVG line element as a string.
232
263
  """
233
264
 
234
- first_offset = (int(seventh_house_degree_ut) / -1) + int(degA)
265
+ if isinstance(aspect, dict):
266
+ aspect = AspectModel(**aspect)
267
+
268
+ first_offset = (int(seventh_house_degree_ut) / -1) + int(aspect["p1_abs_pos"])
235
269
  x1 = sliceToX(0, ar, first_offset) + (r - ar)
236
270
  y1 = sliceToY(0, ar, first_offset) + (r - ar)
237
271
 
238
- second_offset = (int(seventh_house_degree_ut) / -1) + int(degB)
272
+ second_offset = (int(seventh_house_degree_ut) / -1) + int(aspect["p2_abs_pos"])
239
273
  x2 = sliceToX(0, ar, second_offset) + (r - ar)
240
274
  y2 = sliceToY(0, ar, second_offset) + (r - ar)
241
275
 
242
- out = f'<line class="aspect" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {color}; stroke-width: 1; stroke-opacity: .9;"/>'
243
-
244
- return out
276
+ return (
277
+ 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"]}">'
278
+ f'<line class="aspect" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {color}; stroke-width: 1; stroke-opacity: .9;"/>'
279
+ f"</g>"
280
+ )
245
281
 
246
282
 
247
283
  def draw_elements_percentages(
@@ -276,48 +312,45 @@ def draw_elements_percentages(
276
312
  air_percentage = int(round(100 * air_points / total))
277
313
  water_percentage = int(round(100 * water_points / total))
278
314
 
279
- out = '<g transform="translate(-30,79)">'
280
- out += f'<text y="0" style="fill:#ff6600; font-size: 10px;">{fire_label} {str(fire_percentage)}%</text>'
281
- out += f'<text y="12" style="fill:#6a2d04; font-size: 10px;">{earth_label} {str(earth_percentage)}%</text>'
282
- out += f'<text y="24" style="fill:#6f76d1; font-size: 10px;">{air_label} {str(air_percentage)}%</text>'
283
- out += f'<text y="36" style="fill:#630e73; font-size: 10px;">{water_label} {str(water_percentage)}%</text>'
284
- out += "</g>"
285
-
286
- return out
315
+ return (
316
+ f'<g transform="translate(-30,79)">'
317
+ f'<text y="0" style="fill: var(--kerykeion-chart-color-fire-percentage); font-size: 10px;">{fire_label} {str(fire_percentage)}%</text>'
318
+ f'<text y="12" style="fill: var(--kerykeion-chart-color-earth-percentage); font-size: 10px;">{earth_label} {str(earth_percentage)}%</text>'
319
+ f'<text y="24" style="fill: var(--kerykeion-chart-color-air-percentage); font-size: 10px;">{air_label} {str(air_percentage)}%</text>'
320
+ f'<text y="36" style="fill: var(--kerykeion-chart-color-water-percentage); font-size: 10px;">{water_label} {str(water_percentage)}%</text>'
321
+ f"</g>"
322
+ )
287
323
 
288
324
 
289
- def convert_decimal_to_degree_string(dec: float, type="3") -> str:
325
+ def convert_decimal_to_degree_string(dec: float, format_type: Literal["1", "2", "3"] = "3") -> str:
290
326
  """
291
- Coverts decimal float to degrees in format a°b'c".
327
+ Converts a decimal float to a degrees string in the specified format.
292
328
 
293
329
  Args:
294
- - dec (float): decimal float
295
- - type (str): type of format:
296
- - 1: a°
297
- - 2: a°b'
298
- - 3: a°b'c"
330
+ dec (float): The decimal float to convert.
331
+ format_type (str): The format type:
332
+ - "1": a°
333
+ - "2": a°b'
334
+ - "3": a°b'c" (default)
299
335
 
300
336
  Returns:
301
- str: degrees in format a°b'c"
337
+ str: The degrees string in the specified format.
302
338
  """
303
-
339
+ # Ensure the input is a float
304
340
  dec = float(dec)
305
- a = int(dec)
306
- a_new = (dec - float(a)) * 60.0
307
- b_rounded = int(round(a_new))
308
- b = int(a_new)
309
- c = int(round((a_new - float(b)) * 60.0))
310
-
311
- if type == "3":
312
- out = f"{a:02d}&#176;{b:02d}&#39;{c:02d}&#34;"
313
- elif type == "2":
314
- out = f"{a:02d}&#176;{b_rounded:02d}&#39;"
315
- elif type == "1":
316
- out = f"{a:02d}&#176;"
317
- else:
318
- raise KerykeionException(f"Wrong type: {type}, it must be 1, 2 or 3.")
319
341
 
320
- return str(out)
342
+ # Calculate degrees, minutes, and seconds
343
+ degrees = int(dec)
344
+ minutes = int((dec - degrees) * 60)
345
+ seconds = int(round((dec - degrees - minutes / 60) * 3600))
346
+
347
+ # Format the output based on the specified type
348
+ if format_type == "1":
349
+ return f"{degrees}°"
350
+ elif format_type == "2":
351
+ return f"{degrees}°{minutes:02d}'"
352
+ elif format_type == "3":
353
+ return f"{degrees}°{minutes:02d}'{seconds:02d}\""
321
354
 
322
355
 
323
356
  def draw_transit_ring_degree_steps(r: Union[int, float], seventh_house_degree_ut: Union[int, float]) -> str:
@@ -344,19 +377,21 @@ def draw_transit_ring_degree_steps(r: Union[int, float], seventh_house_degree_ut
344
377
  y2 = sliceToY(0, r + 2, offset) - 2
345
378
  out += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: #F00; stroke-width: 1px; stroke-opacity:.9;"/>'
346
379
  out += "</g>"
347
-
380
+
348
381
  return out
349
382
 
350
383
 
351
- def draw_degree_ring(r: Union[int, float], c1: Union[int, float], seventh_house_degree_ut: Union[int, float], stroke_color: str) -> str:
384
+ def draw_degree_ring(
385
+ r: Union[int, float], c1: Union[int, float], seventh_house_degree_ut: Union[int, float], stroke_color: str
386
+ ) -> str:
352
387
  """Draws the degree ring.
353
-
388
+
354
389
  Args:
355
390
  - r (Union[int, float]): The value of r.
356
391
  - c1 (Union[int, float]): The value of c1.
357
392
  - seventh_house_degree_ut (Union[int, float]): The degree of the seventh house.
358
393
  - stroke_color (str): The color of the stroke.
359
-
394
+
360
395
  Returns:
361
396
  str: The SVG path of the degree ring.
362
397
  """
@@ -374,18 +409,19 @@ def draw_degree_ring(r: Union[int, float], c1: Union[int, float], seventh_house_
374
409
 
375
410
  out += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.9;"/>'
376
411
  out += "</g>"
377
-
412
+
378
413
  return out
379
414
 
415
+
380
416
  def draw_transit_ring(r: Union[int, float], paper_1_color: str, zodiac_transit_ring_3_color: str) -> str:
381
417
  """
382
418
  Draws the transit ring.
383
-
419
+
384
420
  Args:
385
421
  - r (Union[int, float]): The value of r.
386
422
  - paper_1_color (str): The color of paper 1.
387
423
  - zodiac_transit_ring_3_color (str): The color of the zodiac transit ring
388
-
424
+
389
425
  Returns:
390
426
  str: The SVG path of the transit ring.
391
427
  """
@@ -397,16 +433,18 @@ def draw_transit_ring(r: Union[int, float], paper_1_color: str, zodiac_transit_r
397
433
  return out
398
434
 
399
435
 
400
- def draw_first_circle(r: Union[int, float], stroke_color: str, chart_type: ChartType, c1: Union[int, float, None] = None) -> str:
436
+ def draw_first_circle(
437
+ r: Union[int, float], stroke_color: str, chart_type: ChartType, c1: Union[int, float, None] = None
438
+ ) -> str:
401
439
  """
402
440
  Draws the first circle.
403
-
441
+
404
442
  Args:
405
443
  - r (Union[int, float]): The value of r.
406
444
  - color (str): The color of the circle.
407
445
  - chart_type (ChartType): The type of chart.
408
446
  - c1 (Union[int, float]): The value of c1.
409
-
447
+
410
448
  Returns:
411
449
  str: The SVG path of the first circle.
412
450
  """
@@ -416,29 +454,630 @@ def draw_first_circle(r: Union[int, float], stroke_color: str, chart_type: Chart
416
454
  if c1 is None:
417
455
  raise KerykeionException("c1 is None")
418
456
 
419
- return f'<circle cx="{r}" cy="{r}" r="{r - c1}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; " />'
457
+ return (
458
+ f'<circle cx="{r}" cy="{r}" r="{r - c1}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; " />'
459
+ )
420
460
 
421
461
 
422
- def draw_second_circle(r: Union[int, float], stroke_color: str, fill_color: str, chart_type: ChartType, c2: Union[int, float, None] = None) -> str:
462
+ def draw_second_circle(
463
+ r: Union[int, float], stroke_color: str, fill_color: str, chart_type: ChartType, c2: Union[int, float, None] = None
464
+ ) -> str:
423
465
  """
424
466
  Draws the second circle.
425
-
467
+
426
468
  Args:
427
469
  - r (Union[int, float]): The value of r.
428
470
  - stroke_color (str): The color of the stroke.
429
471
  - fill_color (str): The color of the fill.
430
472
  - chart_type (ChartType): The type of chart.
431
473
  - c2 (Union[int, float]): The value of c2.
432
-
474
+
433
475
  Returns:
434
476
  str: The SVG path of the second circle.
435
477
  """
436
-
478
+
437
479
  if chart_type == "Synastry" or chart_type == "Transit":
438
480
  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" />'
439
-
481
+
440
482
  else:
441
483
  if c2 is None:
442
484
  raise KerykeionException("c2 is None")
443
485
 
444
- return f'<circle cx="{r}" cy="{r}" r="{r - c2}" style="fill: {fill_color}; fill-opacity:.2; stroke: {stroke_color}; stroke-opacity:.4; stroke-width: 1px" />'
486
+ return f'<circle cx="{r}" cy="{r}" r="{r - c2}" style="fill: {fill_color}; fill-opacity:.2; stroke: {stroke_color}; stroke-opacity:.4; stroke-width: 1px" />'
487
+
488
+
489
+ def draw_third_circle(
490
+ radius: Union[int, float],
491
+ stroke_color: str,
492
+ fill_color: str,
493
+ chart_type: ChartType,
494
+ c3: Union[int, float]
495
+ ) -> str:
496
+ """
497
+ Draws the third circle in an SVG chart.
498
+
499
+ Parameters:
500
+ - radius (Union[int, float]): The radius of the circle.
501
+ - stroke_color (str): The stroke color of the circle.
502
+ - fill_color (str): The fill color of the circle.
503
+ - chart_type (ChartType): The type of the chart.
504
+ - c3 (Union[int, float, None], optional): The radius adjustment for non-Synastry and non-Transit charts.
505
+
506
+ Returns:
507
+ - str: The SVG element as a string.
508
+ """
509
+ if chart_type in {"Synastry", "Transit"}:
510
+ # For Synastry and Transit charts, use a fixed radius adjustment of 160
511
+ return f'<circle cx="{radius}" cy="{radius}" r="{radius - 160}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
512
+
513
+ else:
514
+ return f'<circle cx="{radius}" cy="{radius}" r="{radius - c3}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
515
+
516
+
517
+ def draw_aspect_grid(
518
+ stroke_color: str,
519
+ available_planets: list,
520
+ aspects: list,
521
+ x_start: int = 380,
522
+ y_start: int = 468,
523
+ ) -> str:
524
+ """
525
+ Draws the aspect grid for the given planets and aspects.
526
+
527
+ Args:
528
+ stroke_color (str): The color of the stroke.
529
+ available_planets (list): List of all planets. Only planets with "is_active" set to True will be used.
530
+ aspects (list): List of aspects.
531
+ x_start (int): The x-coordinate starting point.
532
+ y_start (int): The y-coordinate starting point.
533
+
534
+ Returns:
535
+ str: SVG string representing the aspect grid.
536
+ """
537
+ svg_output = ""
538
+ style = f"stroke:{stroke_color}; stroke-width: 1px; fill:none"
539
+ box_size = 14
540
+
541
+ # Filter active planets
542
+ active_planets = [planet for planet in available_planets if planet.is_active]
543
+
544
+ # Reverse the list of active planets for the first iteration
545
+ reversed_planets = active_planets[::-1]
546
+
547
+ for index, planet_a in enumerate(reversed_planets):
548
+ # Draw the grid box for the planet
549
+ svg_output += f'<rect x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
550
+ svg_output += f'<use transform="scale(0.4)" x="{(x_start + 2) * 2.5}" y="{(y_start + 1) * 2.5}" xlink:href="#{planet_a["name"]}" />'
551
+
552
+ # Update the starting coordinates for the next box
553
+ x_start += box_size
554
+ y_start -= box_size
555
+
556
+ # Coordinates for the aspect symbols
557
+ x_aspect = x_start
558
+ y_aspect = y_start + box_size
559
+
560
+ # Iterate over the remaining planets
561
+ for planet_b in reversed_planets[index + 1:]:
562
+ # Draw the grid box for the aspect
563
+ svg_output += f'<rect x="{x_aspect}" y="{y_aspect}" width="{box_size}" height="{box_size}" style="{style}"/>'
564
+ x_aspect += box_size
565
+
566
+ # Check for aspects between the planets
567
+ for aspect in aspects:
568
+ if (aspect["p1"] == planet_a["id"] and aspect["p2"] == planet_b["id"]) or (
569
+ aspect["p1"] == planet_b["id"] and aspect["p2"] == planet_a["id"]
570
+ ):
571
+ svg_output += f'<use x="{x_aspect - box_size + 1}" y="{y_aspect + 1}" xlink:href="#orb{aspect["aspect_degrees"]}" />'
572
+
573
+ return svg_output
574
+
575
+
576
+ def draw_houses_cusps_and_text_number(
577
+ r: Union[int, float],
578
+ first_subject_houses_list: list[KerykeionPointModel],
579
+ standard_house_cusp_color: str,
580
+ first_house_color: str,
581
+ tenth_house_color: str,
582
+ seventh_house_color: str,
583
+ fourth_house_color: str,
584
+ c1: Union[int, float],
585
+ c3: Union[int, float],
586
+ chart_type: ChartType,
587
+ second_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
588
+ transit_house_cusp_color: Union[str, None] = None,
589
+ ) -> str:
590
+ """
591
+ Draws the houses cusps and text numbers for a given chart type.
592
+
593
+ Parameters:
594
+ - r: Radius of the chart.
595
+ - first_subject_houses_list: List of house for the first subject.
596
+ - standard_house_cusp_color: Default color for house cusps.
597
+ - first_house_color: Color for the first house cusp.
598
+ - tenth_house_color: Color for the tenth house cusp.
599
+ - seventh_house_color: Color for the seventh house cusp.
600
+ - fourth_house_color: Color for the fourth house cusp.
601
+ - c1: Offset for the first subject.
602
+ - c3: Offset for the third subject.
603
+ - chart_type: Type of the chart (e.g., Transit, Synastry).
604
+ - second_subject_houses_list: List of house for the second subject (optional).
605
+ - transit_house_cusp_color: Color for transit house cusps (optional).
606
+
607
+ Returns:
608
+ - A string containing the SVG path for the houses cusps and text numbers.
609
+ """
610
+
611
+ path = ""
612
+ xr = 12
613
+
614
+ for i in range(xr):
615
+ # Determine offsets based on chart type
616
+ dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry"] else (c3, c1, False)
617
+
618
+ # Calculate the offset for the current house cusp
619
+ offset = (int(first_subject_houses_list[int(xr / 2)].abs_pos) / -1) + int(first_subject_houses_list[i].abs_pos)
620
+
621
+ # Calculate the coordinates for the house cusp lines
622
+ x1 = sliceToX(0, (r - dropin), offset) + dropin
623
+ y1 = sliceToY(0, (r - dropin), offset) + dropin
624
+ x2 = sliceToX(0, r - roff, offset) + roff
625
+ y2 = sliceToY(0, r - roff, offset) + roff
626
+
627
+ # Calculate the text offset for the house number
628
+ next_index = (i + 1) % xr
629
+ text_offset = offset + int(
630
+ degreeDiff(first_subject_houses_list[next_index].abs_pos, first_subject_houses_list[i].abs_pos) / 2
631
+ )
632
+
633
+ # Determine the line color based on the house index
634
+ linecolor = {0: first_house_color, 9: tenth_house_color, 6: seventh_house_color, 3: fourth_house_color}.get(
635
+ i, standard_house_cusp_color
636
+ )
637
+
638
+ if chart_type in ["Transit", "Synastry"]:
639
+ if second_subject_houses_list is None or transit_house_cusp_color is None:
640
+ raise KerykeionException("second_subject_houses_list_ut or transit_house_cusp_color is None")
641
+
642
+ # Calculate the offset for the second subject's house cusp
643
+ zeropoint = 360 - first_subject_houses_list[6].abs_pos
644
+ t_offset = (zeropoint + second_subject_houses_list[i].abs_pos) % 360
645
+
646
+ # Calculate the coordinates for the second subject's house cusp lines
647
+ t_x1 = sliceToX(0, (r - t_roff), t_offset) + t_roff
648
+ t_y1 = sliceToY(0, (r - t_roff), t_offset) + t_roff
649
+ t_x2 = sliceToX(0, r, t_offset)
650
+ t_y2 = sliceToY(0, r, t_offset)
651
+
652
+ # Calculate the text offset for the second subject's house number
653
+ t_text_offset = t_offset + int(
654
+ degreeDiff(second_subject_houses_list[next_index].abs_pos, second_subject_houses_list[i].abs_pos) / 2
655
+ )
656
+ t_linecolor = linecolor if i in [0, 9, 6, 3] else transit_house_cusp_color
657
+ xtext = sliceToX(0, (r - 8), t_text_offset) + 8
658
+ ytext = sliceToY(0, (r - 8), t_text_offset) + 8
659
+
660
+ # Add the house number text for the second subject
661
+ fill_opacity = "0" if chart_type == "Transit" else ".4"
662
+ path += f'<g kr:node="HouseNumber">'
663
+ path += f'<text style="fill: var(--kerykeion-chart-color-house-number); fill-opacity: {fill_opacity}; font-size: 14px"><tspan x="{xtext - 3}" y="{ytext + 3}">{i + 1}</tspan></text>'
664
+ path += f"</g>"
665
+
666
+ # Add the house cusp line for the second subject
667
+ stroke_opacity = "0" if chart_type == "Transit" else ".3"
668
+ path += f'<g kr:node="Cusp">'
669
+ 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};'/>"
670
+ path += f"</g>"
671
+
672
+ # Adjust dropin based on chart type
673
+ dropin = {"Transit": 84, "Synastry": 84, "ExternalNatal": 100}.get(chart_type, 48)
674
+ xtext = sliceToX(0, (r - dropin), text_offset) + dropin
675
+ ytext = sliceToY(0, (r - dropin), text_offset) + dropin
676
+
677
+ # Add the house cusp line for the first subject
678
+ path += f'<g kr:node="Cusp">'
679
+ path += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {linecolor}; stroke-width: 1px; stroke-dasharray:3,2; stroke-opacity:.4;"/>'
680
+ path += f"</g>"
681
+
682
+ # Add the house number text for the first subject
683
+ path += f'<g kr:node="HouseNumber">'
684
+ path += f'<text style="fill: var(--kerykeion-chart-color-house-number); fill-opacity: .6; font-size: 14px"><tspan x="{xtext - 3}" y="{ytext + 3}">{i + 1}</tspan></text>'
685
+ path += f"</g>"
686
+
687
+ return path
688
+
689
+
690
+ def draw_transit_aspect_list(
691
+ grid_title: str,
692
+ aspects_list: Union[list[AspectModel], list[dict]],
693
+ celestial_point_language: Union[KerykeionLanguageCelestialPointModel, dict],
694
+ aspects_settings: Union[KerykeionSettingsAspectModel, dict],
695
+ ) -> str:
696
+ """
697
+ Generates the SVG output for the aspect transit grid.
698
+
699
+ Parameters:
700
+ - grid_title: Title of the grid.
701
+ - aspects_list: List of aspects.
702
+ - planets_labels: Dictionary containing the planet labels.
703
+ - aspects_settings: Dictionary containing the aspect settings.
704
+
705
+ Returns:
706
+ - A string containing the SVG path data for the aspect transit grid.
707
+ """
708
+
709
+ if isinstance(celestial_point_language, dict):
710
+ celestial_point_language = KerykeionLanguageCelestialPointModel(**celestial_point_language)
711
+
712
+ if isinstance(aspects_settings, dict):
713
+ aspects_settings = KerykeionSettingsAspectModel(**aspects_settings)
714
+
715
+ # If not instance of AspectModel, convert to AspectModel
716
+ if isinstance(aspects_list[0], dict):
717
+ aspects_list = [AspectModel(**aspect) for aspect in aspects_list] # type: ignore
718
+
719
+ line = 0
720
+ nl = 0
721
+ inner_path = ""
722
+ scale = 1
723
+ for i, aspect in enumerate(aspects_list):
724
+ # Adjust the vertical position for every 12 aspects
725
+ if i == 12:
726
+ nl = 100
727
+ line = 0
728
+
729
+ elif i == 24:
730
+ nl = 200
731
+ line = 0
732
+
733
+ elif i == 36:
734
+ nl = 300
735
+ line = 0
736
+
737
+ elif i == 48:
738
+ nl = 400
739
+ # When there are more than 60 aspects, the text is moved up
740
+ if len(aspects_list) > 60:
741
+ line = -1 * (len(aspects_list) - 60) * 14
742
+ else:
743
+ line = 0
744
+
745
+ inner_path += f'<g transform="translate({nl},{line})">'
746
+
747
+ # first planet symbol
748
+ inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspects_list[i]["p1"]]["name"]}" />'
749
+
750
+ # aspect symbol
751
+ inner_path += f'<use x="15" y="0" xlink:href="#orb{aspects_settings[aspects_list[i]["aid"]]["degree"]}" />'
752
+
753
+ # second planet symbol
754
+ inner_path += f'<g transform="translate(30,0)">'
755
+ inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspects_list[i]["p2"]]["name"]}" />'
756
+ inner_path += f"</g>"
757
+
758
+ # difference in degrees
759
+ inner_path += f'<text y="8" x="45" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 10px;">{convert_decimal_to_degree_string(aspects_list[i]["orbit"])}</text>'
760
+ # line
761
+ inner_path += f"</g>"
762
+ line = line + 14
763
+
764
+ out = f'<g style="transform: translate(47%, 59%) scale({scale})">'
765
+ out += f'<text y="-15" x="0" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 14px;">{grid_title}:</text>'
766
+ out += inner_path
767
+ out += "</g>"
768
+
769
+ return out
770
+
771
+
772
+ def draw_moon_phase(
773
+ degrees_between_sun_and_moon: float,
774
+ latitude: float
775
+ ) -> str:
776
+ """
777
+ Draws the moon phase based on the degrees between the sun and the moon.
778
+
779
+ Parameters:
780
+ - degrees_between_sun_and_moon (float): The degrees between the sun and the moon.
781
+ - latitude (float): The latitude for rotation calculation.
782
+ - lunar_phase_outline_color (str): The color for the lunar phase outline.
783
+ - dark_color (str): The color for the dark part of the moon.
784
+ - light_color (str): The color for the light part of the moon.
785
+
786
+ Returns:
787
+ - str: The SVG element as a string.
788
+ """
789
+ deg = degrees_between_sun_and_moon
790
+
791
+ # Initialize variables for lunar phase properties
792
+ circle_center_x = None
793
+ circle_radius = None
794
+
795
+ # Determine lunar phase properties based on the degree
796
+ if deg < 90.0:
797
+ max_radius = deg
798
+ if deg > 80.0:
799
+ max_radius = max_radius * max_radius
800
+ circle_center_x = 20.0 + (deg / 90.0) * (max_radius + 10.0)
801
+ circle_radius = 10.0 + (deg / 90.0) * max_radius
802
+
803
+ elif deg < 180.0:
804
+ max_radius = 180.0 - deg
805
+ if deg < 100.0:
806
+ max_radius = max_radius * max_radius
807
+ circle_center_x = 20.0 + ((deg - 90.0) / 90.0 * (max_radius + 10.0)) - (max_radius + 10.0)
808
+ circle_radius = 10.0 + max_radius - ((deg - 90.0) / 90.0 * max_radius)
809
+
810
+ elif deg < 270.0:
811
+ max_radius = deg - 180.0
812
+ if deg > 260.0:
813
+ max_radius = max_radius * max_radius
814
+ circle_center_x = 20.0 + ((deg - 180.0) / 90.0 * (max_radius + 10.0))
815
+ circle_radius = 10.0 + ((deg - 180.0) / 90.0 * max_radius)
816
+
817
+ elif deg < 361.0:
818
+ max_radius = 360.0 - deg
819
+ if deg < 280.0:
820
+ max_radius = max_radius * max_radius
821
+ circle_center_x = 20.0 + ((deg - 270.0) / 90.0 * (max_radius + 10.0)) - (max_radius + 10.0)
822
+ circle_radius = 10.0 + max_radius - ((deg - 270.0) / 90.0 * max_radius)
823
+
824
+ else:
825
+ raise KerykeionException(f"Invalid degree value: {deg}")
826
+
827
+
828
+ # Calculate rotation based on latitude
829
+ lunar_phase_rotate = -90.0 - latitude
830
+
831
+ # Return the SVG element as a string
832
+ return (
833
+ f'<g transform="rotate({lunar_phase_rotate} 20 10)">'
834
+ f' <defs>'
835
+ f' <clipPath id="moonPhaseCutOffCircle">'
836
+ f' <circle cx="20" cy="10" r="10" />'
837
+ f' </clipPath>'
838
+ f' </defs>'
839
+ f' <circle cx="20" cy="10" r="10" style="fill: var(--kerykeion-chart-color-lunar-phase-0)" />'
840
+ f' <circle cx="{circle_center_x}" cy="10" r="{circle_radius}" style="fill: var(--kerykeion-chart-color-lunar-phase-1)" clip-path="url(#moonPhaseCutOffCircle)" />'
841
+ 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" />'
842
+ f'</g>'
843
+ )
844
+
845
+
846
+ def draw_house_grid(
847
+ main_subject_houses_list: list[KerykeionPointModel],
848
+ chart_type: ChartType,
849
+ secondary_subject_houses_list: Union[list[KerykeionPointModel], None] = None,
850
+ text_color: str = "#000000",
851
+ house_cusp_generale_name_label: str = "Cusp",
852
+ ) -> str:
853
+ """
854
+ Generate SVG code for a grid of astrological houses.
855
+
856
+ Parameters:
857
+ - main_houses (list[KerykeionPointModel]): List of houses for the main subject.
858
+ - chart_type (ChartType): Type of the chart (e.g., Synastry, Transit).
859
+ - secondary_houses (list[KerykeionPointModel], optional): List of houses for the secondary subject.
860
+ - text_color (str): Color of the text.
861
+ - cusp_label (str): Label for the house cusp.
862
+
863
+ Returns:
864
+ - str: The SVG code for the grid of houses.
865
+ """
866
+
867
+ if chart_type in ["Synastry", "Transit"] and secondary_subject_houses_list is None:
868
+ raise KerykeionException("secondary_houses is None")
869
+
870
+ svg_output = '<g transform="translate(610,-20)">'
871
+
872
+ line_increment = 10
873
+ for i, house in enumerate(main_subject_houses_list):
874
+ cusp_number = f"&#160;&#160;{i + 1}" if i < 9 else str(i + 1)
875
+ svg_output += (
876
+ f'<g transform="translate(0,{line_increment})">'
877
+ f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
878
+ f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
879
+ f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
880
+ f'</g>'
881
+ )
882
+ line_increment += 14
883
+
884
+ svg_output += "</g>"
885
+
886
+ if chart_type == "Synastry":
887
+ svg_output += '<!-- Synastry Houses -->'
888
+ svg_output += '<g transform="translate(850, -20)">'
889
+ line_increment = 10
890
+
891
+ for i, house in enumerate(secondary_subject_houses_list): # type: ignore
892
+ cusp_number = f"&#160;&#160;{i + 1}" if i < 9 else str(i + 1)
893
+ svg_output += (
894
+ f'<g transform="translate(0,{line_increment})">'
895
+ f'<text text-anchor="end" x="40" style="fill:{text_color}; font-size: 10px;">{house_cusp_generale_name_label} {cusp_number}:</text>'
896
+ f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{house["sign"]}" /></g>'
897
+ f'<text x="53" style="fill:{text_color}; font-size: 10px;"> {convert_decimal_to_degree_string(house["position"])}</text>'
898
+ f'</g>'
899
+ )
900
+ line_increment += 14
901
+
902
+ svg_output += "</g>"
903
+
904
+ return svg_output
905
+
906
+
907
+ def draw_planet_grid(
908
+ planets_and_houses_grid_title: str,
909
+ subject_name: str,
910
+ available_kerykeion_celestial_points: list[KerykeionPointModel],
911
+ chart_type: ChartType,
912
+ celestial_point_language: KerykeionLanguageCelestialPointModel,
913
+ second_subject_name: Union[str, None] = None,
914
+ second_subject_available_kerykeion_celestial_points: Union[list[KerykeionPointModel], None] = None,
915
+ text_color: str = "#000000",
916
+ ) -> str:
917
+ """
918
+ Draws the planet grid for the given celestial points and chart type.
919
+
920
+ Args:
921
+ planets_and_houses_grid_title (str): Title of the grid.
922
+ subject_name (str): Name of the subject.
923
+ available_kerykeion_celestial_points (list[KerykeionPointModel]): List of celestial points for the subject.
924
+ chart_type (ChartType): Type of the chart.
925
+ celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points.
926
+ second_subject_name (str, optional): Name of the second subject. Defaults to None.
927
+ second_subject_available_kerykeion_celestial_points (list[KerykeionPointModel], optional): List of celestial points for the second subject. Defaults to None.
928
+ text_color (str, optional): Color of the text. Defaults to "#000000".
929
+
930
+ Returns:
931
+ str: The SVG output for the planet grid.
932
+ """
933
+ line_height = 10
934
+ offset = 0
935
+ offset_between_lines = 14
936
+
937
+ svg_output = (
938
+ f'<g transform="translate(510,-20)">'
939
+ f'<g transform="translate(140, -15)">'
940
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}:</text>'
941
+ f'</g>'
942
+ )
943
+
944
+ end_of_line = "</g>"
945
+
946
+ for i, planet in enumerate(available_kerykeion_celestial_points):
947
+ if i == 27:
948
+ line_height = 10
949
+ offset = -120
950
+
951
+ decoded_name = get_decoded_kerykeion_celestial_point_name(planet["name"], celestial_point_language)
952
+ svg_output += (
953
+ f'<g transform="translate({offset},{line_height})">'
954
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{decoded_name}</text>'
955
+ f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{planet["name"]}" /></g>'
956
+ f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(planet["position"])}</text>'
957
+ f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{planet["sign"]}" /></g>'
958
+ )
959
+
960
+ if planet["retrograde"]:
961
+ svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
962
+
963
+ svg_output += end_of_line
964
+ line_height += offset_between_lines
965
+
966
+ if chart_type in ["Transit", "Synastry"]:
967
+ if second_subject_available_kerykeion_celestial_points is None:
968
+ raise KerykeionException("second_subject_available_kerykeion_celestial_points is None")
969
+
970
+ if chart_type == "Transit":
971
+ svg_output += (
972
+ f'<g transform="translate(320, -15)">'
973
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{second_subject_name}:</text>'
974
+ )
975
+ else:
976
+ svg_output += (
977
+ f'<g transform="translate(380, -15)">'
978
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}:</text>'
979
+ )
980
+
981
+ svg_output += end_of_line
982
+
983
+ second_line_height = 10
984
+ second_offset = 250
985
+
986
+ for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
987
+ if i == 27:
988
+ second_line_height = 10
989
+ second_offset = -120
990
+
991
+ second_decoded_name = get_decoded_kerykeion_celestial_point_name(t_planet["name"], celestial_point_language)
992
+ svg_output += (
993
+ f'<g transform="translate({second_offset},{second_line_height})">'
994
+ f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{second_decoded_name}</text>'
995
+ f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
996
+ f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(t_planet["position"])}</text>'
997
+ f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{t_planet["sign"]}" /></g>'
998
+ )
999
+
1000
+ if t_planet["retrograde"]:
1001
+ svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
1002
+
1003
+ svg_output += end_of_line
1004
+ second_line_height += offset_between_lines
1005
+
1006
+ svg_output += end_of_line
1007
+ return svg_output
1008
+
1009
+
1010
+ def draw_transit_aspect_grid(
1011
+ stroke_color: str,
1012
+ available_planets: list,
1013
+ aspects: list,
1014
+ x_indent: int = 50,
1015
+ y_indent: int = 250,
1016
+ box_size: int = 14
1017
+ ) -> str:
1018
+ """
1019
+ Draws the aspect grid for the given planets and aspects. The default args value are specific for a stand alone
1020
+ aspect grid.
1021
+
1022
+ Args:
1023
+ stroke_color (str): The color of the stroke.
1024
+ available_planets (list): List of all planets. Only planets with "is_active" set to True will be used.
1025
+ aspects (list): List of aspects.
1026
+ x_indent (int): The initial x-coordinate starting point.
1027
+ y_indent (int): The initial y-coordinate starting point.
1028
+
1029
+ Returns:
1030
+ str: SVG string representing the aspect grid.
1031
+ """
1032
+ svg_output = ""
1033
+ style = f"stroke:{stroke_color}; stroke-width: 1px; fill:none"
1034
+ x_start = x_indent
1035
+ y_start = y_indent
1036
+
1037
+ # Filter active planets
1038
+ active_planets = [planet for planet in available_planets if planet.is_active]
1039
+
1040
+ # Reverse the list of active planets for the first iteration
1041
+ reversed_planets = active_planets[::-1]
1042
+ for index, planet_a in enumerate(reversed_planets):
1043
+ # Draw the grid box for the planet
1044
+ svg_output += f'<rect x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
1045
+ svg_output += f'<use transform="scale(0.4)" x="{(x_start + 2) * 2.5}" y="{(y_start + 1) * 2.5}" xlink:href="#{planet_a["name"]}" />'
1046
+ x_start += box_size
1047
+
1048
+ x_start = x_indent - box_size
1049
+ y_start = y_indent - box_size
1050
+
1051
+ for index, planet_a in enumerate(reversed_planets):
1052
+ # Draw the grid box for the planet
1053
+ svg_output += f'<rect x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
1054
+ svg_output += f'<use transform="scale(0.4)" x="{(x_start + 2) * 2.5}" y="{(y_start + 1) * 2.5}" xlink:href="#{planet_a["name"]}" />'
1055
+ y_start -= box_size
1056
+
1057
+ x_start = x_indent
1058
+ y_start = y_indent
1059
+ y_start = y_start - box_size
1060
+
1061
+ for index, planet_a in enumerate(reversed_planets):
1062
+ # Draw the grid box for the planet
1063
+ svg_output += f'<rect x="{x_start}" y="{y_start}" width="{box_size}" height="{box_size}" style="{style}"/>'
1064
+
1065
+ # Update the starting coordinates for the next box
1066
+ y_start -= box_size
1067
+
1068
+ # Coordinates for the aspect symbols
1069
+ x_aspect = x_start
1070
+ y_aspect = y_start + box_size
1071
+
1072
+ # Iterate over the remaining planets
1073
+ for planet_b in reversed_planets:
1074
+ # Draw the grid box for the aspect
1075
+ svg_output += f'<rect x="{x_aspect}" y="{y_aspect}" width="{box_size}" height="{box_size}" style="{style}"/>'
1076
+ x_aspect += box_size
1077
+
1078
+ # Check for aspects between the planets
1079
+ for aspect in aspects:
1080
+ if (aspect["p1"] == planet_a["id"] and aspect["p2"] == planet_b["id"]):
1081
+ svg_output += f'<use x="{x_aspect - box_size + 1}" y="{y_aspect + 1}" xlink:href="#orb{aspect["aspect_degrees"]}" />'
1082
+
1083
+ return svg_output