kerykeion.charts.kerykeion_chart_svg
This is part of Kerykeion (C) 2024 Giacomo Battaglia
1# -*- coding: utf-8 -*- 2""" 3 This is part of Kerykeion (C) 2024 Giacomo Battaglia 4""" 5 6 7import logging 8 9from kerykeion.settings.kerykeion_settings import get_settings 10from kerykeion.aspects.synastry_aspects import SynastryAspects 11from kerykeion.aspects.natal_aspects import NatalAspects 12from kerykeion.astrological_subject import AstrologicalSubject 13from kerykeion.kr_types import KerykeionException, ChartType 14from kerykeion.kr_types import ChartTemplateDictionary 15from kerykeion.kr_types.settings_models import KerykeionSettingsCelestialPointModel 16from kerykeion.charts.charts_utils import ( 17 degreeDiff, 18 sliceToX, 19 sliceToY, 20 draw_zodiac_slice, 21 convert_latitude_coordinate_to_string, 22 convert_longitude_coordinate_to_string, 23 draw_aspect_line, 24 draw_elements_percentages, 25 convert_decimal_to_degree_string, 26 draw_transit_ring_degree_steps, 27 draw_degree_ring, 28 draw_transit_ring, 29 draw_first_circle, 30 draw_second_circle 31) 32from pathlib import Path 33from scour.scour import scourString 34from string import Template 35from typing import Union, List 36from datetime import datetime 37 38 39 40class KerykeionChartSVG: 41 """ 42 Creates the instance that can generate the chart with the 43 function makeSVG(). 44 45 Parameters: 46 - first_obj: First kerykeion object 47 - chart_type: Natal, ExternalNatal, Transit, Synastry (Default: Type="Natal"). 48 - second_obj: Second kerykeion object (Not required if type is Natal) 49 - new_output_directory: Set the output directory (default: home directory). 50 - new_settings_file: Set the settings file (default: kr.config.json). 51 In the settings file you can set the language, colors, planets, aspects, etc. 52 """ 53 54 # Constants 55 _DEFAULT_HEIGHT = 546.0 56 _DEFAULT_FULL_WIDTH = 1200 57 _DEFAULT_NATAL_WIDTH = 772.2 58 59 # Set at init 60 first_obj: AstrologicalSubject 61 second_obj: Union[AstrologicalSubject, None] 62 chart_type: ChartType 63 new_output_directory: Union[Path, None] 64 new_settings_file: Union[Path, None] 65 output_directory: Path 66 67 # Internal properties 68 fire: float 69 earth: float 70 air: float 71 water: float 72 c1: float 73 c2: float 74 c3: float 75 homedir: Path 76 xml_svg: Path 77 width: Union[float, int] 78 language_settings: dict 79 chart_colors_settings: dict 80 planets_settings: dict 81 aspects_settings: dict 82 planet_in_zodiac_extra_points: int 83 chart_settings: dict 84 user: AstrologicalSubject 85 available_planets_setting: List[KerykeionSettingsCelestialPointModel] 86 transit_ring_exclude_points_names: List[str] 87 points_deg_ut: list 88 points_deg: list 89 points_sign: list 90 points_retrograde: list 91 houses_sign_graph: list 92 t_points_deg_ut: list 93 t_points_deg: list 94 t_points_sign: list 95 t_points_retrograde: list 96 t_houses_sign_graph: list 97 height: float 98 location: str 99 geolat: float 100 geolon: float 101 zoom: int 102 zodiac: tuple 103 template: str 104 105 def __init__( 106 self, 107 first_obj: AstrologicalSubject, 108 chart_type: ChartType = "Natal", 109 second_obj: Union[AstrologicalSubject, None] = None, 110 new_output_directory: Union[str, None] = None, 111 new_settings_file: Union[Path, None] = None, 112 ): 113 # Directories: 114 DATA_DIR = Path(__file__).parent 115 self.homedir = Path.home() 116 self.new_settings_file = new_settings_file 117 118 if new_output_directory: 119 self.output_directory = Path(new_output_directory) 120 else: 121 self.output_directory = self.homedir 122 123 self.xml_svg = DATA_DIR / "templates/chart.xml" 124 125 self.parse_json_settings(new_settings_file) 126 self.chart_type = chart_type 127 128 # Kerykeion instance 129 self.user = first_obj 130 131 self.available_planets_setting = [] 132 for body in self.planets_settings: 133 if body['is_active'] == False: 134 continue 135 136 self.available_planets_setting.append(body) 137 138 # House cusp points are excluded from the transit ring. 139 self.transit_ring_exclude_points_names = [ 140 "First_House", 141 "Second_House", 142 "Third_House", 143 "Fourth_House", 144 "Fifth_House", 145 "Sixth_House", 146 "Seventh_House", 147 "Eighth_House", 148 "Ninth_House", 149 "Tenth_House", 150 "Eleventh_House", 151 "Twelfth_House" 152 ] 153 154 # Available bodies 155 available_celestial_points = [] 156 for body in self.available_planets_setting: 157 available_celestial_points.append(body["name"].lower()) 158 159 # Make a list for the absolute degrees of the points of the graphic. 160 self.points_deg_ut = [] 161 for planet in available_celestial_points: 162 self.points_deg_ut.append(self.user.get(planet).abs_pos) 163 164 # Make a list of the relative degrees of the points in the graphic. 165 self.points_deg = [] 166 for planet in available_celestial_points: 167 self.points_deg.append(self.user.get(planet).position) 168 169 # Make list of the points sign 170 self.points_sign = [] 171 for planet in available_celestial_points: 172 self.points_sign.append(self.user.get(planet).sign_num) 173 174 # Make a list of points if they are retrograde or not. 175 self.points_retrograde = [] 176 for planet in available_celestial_points: 177 self.points_retrograde.append(self.user.get(planet).retrograde) 178 179 # Makes the sign number list. 180 181 self.houses_sign_graph = [] 182 for h in self.user.houses_list: 183 self.houses_sign_graph.append(h["sign_num"]) 184 185 if self.chart_type == "Natal" or self.chart_type == "ExternalNatal": 186 natal_aspects_instance = NatalAspects(self.user, new_settings_file=self.new_settings_file) 187 self.aspects_list = natal_aspects_instance.relevant_aspects 188 189 # TODO: If not second should exit 190 if self.chart_type == "Transit" or self.chart_type == "Synastry": 191 if not second_obj: 192 raise KerykeionException("Second object is required for Transit or Synastry charts.") 193 194 # Kerykeion instance 195 self.t_user = second_obj 196 197 # Make a list for the absolute degrees of the points of the graphic. 198 self.t_points_deg_ut = [] 199 for planet in available_celestial_points: 200 self.t_points_deg_ut.append(self.t_user.get(planet).abs_pos) 201 202 # Make a list of the relative degrees of the points in the graphic. 203 self.t_points_deg = [] 204 for planet in available_celestial_points: 205 self.t_points_deg.append(self.t_user.get(planet).position) 206 207 # Make list of the poits sign. 208 self.t_points_sign = [] 209 for planet in available_celestial_points: 210 self.t_points_sign.append(self.t_user.get(planet).sign_num) 211 212 # Make a list of poits if they are retrograde or not. 213 self.t_points_retrograde = [] 214 for planet in available_celestial_points: 215 self.t_points_retrograde.append(self.t_user.get(planet).retrograde) 216 217 self.t_houses_sign_graph = [] 218 for h in self.t_user.houses_list: 219 self.t_houses_sign_graph.append(h["sign_num"]) 220 221 # screen size 222 self.height = self._DEFAULT_HEIGHT 223 if self.chart_type == "Synastry" or self.chart_type == "Transit": 224 self.width = self._DEFAULT_FULL_WIDTH 225 else: 226 self.width = self._DEFAULT_NATAL_WIDTH 227 228 # default location 229 self.location = self.user.city 230 self.geolat = self.user.lat 231 self.geolon = self.user.lng 232 233 logging.info(f"{self.user.name} birth location: {self.location}, {self.geolat}, {self.geolon}") 234 235 if self.chart_type == "Transit": 236 self.t_name = self.language_settings["transit_name"] 237 238 # configuration 239 # ZOOM 1 = 100% 240 self.zoom = 1 241 242 self.zodiac = ( 243 {"name": "aries", "element": "fire"}, 244 {"name": "taurus", "element": "earth"}, 245 {"name": "gemini", "element": "air"}, 246 {"name": "cancer", "element": "water"}, 247 {"name": "leo", "element": "fire"}, 248 {"name": "virgo", "element": "earth"}, 249 {"name": "libra", "element": "air"}, 250 {"name": "scorpio", "element": "water"}, 251 {"name": "sagittarius", "element": "fire"}, 252 {"name": "capricorn", "element": "earth"}, 253 {"name": "aquarius", "element": "air"}, 254 {"name": "pisces", "element": "water"}, 255 ) 256 257 self.template = None 258 259 def set_output_directory(self, dir_path: Path) -> None: 260 """ 261 Sets the output direcotry and returns it's path. 262 """ 263 self.output_directory = dir_path 264 logging.info(f"Output direcotry set to: {self.output_directory}") 265 266 def parse_json_settings(self, settings_file): 267 """ 268 Parse the settings file. 269 """ 270 settings = get_settings(settings_file) 271 272 language = settings["general_settings"]["language"] 273 self.language_settings = settings["language_settings"].get(language, "EN") 274 self.chart_colors_settings = settings["chart_colors"] 275 self.planets_settings = settings["celestial_points"] 276 self.aspects_settings = settings["aspects"] 277 self.planet_in_zodiac_extra_points = settings["general_settings"]["planet_in_zodiac_extra_points"] 278 self.chart_settings = settings["chart_settings"] 279 280 def _draw_zodiac_circle_slices(self, r): 281 """ 282 Generate the SVG string representing the zodiac circle 283 with the 12 slices for each zodiac sign. 284 285 Args: 286 r (float): The radius of the zodiac slices. 287 288 Returns: 289 str: The SVG string representing the zodiac circle. 290 """ 291 292 output = "" 293 for i, zodiac_element in enumerate(self.zodiac): 294 output += draw_zodiac_slice( 295 c1=self.c1, 296 chart_type=self.chart_type, 297 seventh_house_degree_ut=self.user.houses_degree_ut[6], 298 num=i, 299 r=r, 300 style=f'fill:{self.chart_colors_settings[f"zodiac_bg_{i}"]}; fill-opacity: 0.5;', 301 type=zodiac_element["name"], 302 ) 303 304 return output 305 306 def _makeHouses(self, r): 307 path = "" 308 309 xr = 12 310 for i in range(xr): 311 # check transit 312 if self.chart_type == "Transit" or self.chart_type == "Synastry": 313 dropin = 160 314 roff = 72 315 t_roff = 36 316 else: 317 dropin = self.c3 318 roff = self.c1 319 320 # offset is negative desc houses_degree_ut[6] 321 offset = (int(self.user.houses_degree_ut[int(xr / 2)]) / -1) + int(self.user.houses_degree_ut[i]) 322 x1 = sliceToX(0, (r - dropin), offset) + dropin 323 y1 = sliceToY(0, (r - dropin), offset) + dropin 324 x2 = sliceToX(0, r - roff, offset) + roff 325 y2 = sliceToY(0, r - roff, offset) + roff 326 327 if i < (xr - 1): 328 text_offset = offset + int(degreeDiff(self.user.houses_degree_ut[(i + 1)], self.user.houses_degree_ut[i]) / 2) 329 else: 330 text_offset = offset + int(degreeDiff(self.user.houses_degree_ut[0], self.user.houses_degree_ut[(xr - 1)]) / 2) 331 332 # mc, asc, dsc, ic 333 if i == 0: 334 linecolor = self.planets_settings[12]["color"] 335 elif i == 9: 336 linecolor = self.planets_settings[13]["color"] 337 elif i == 6: 338 linecolor = self.planets_settings[14]["color"] 339 elif i == 3: 340 linecolor = self.planets_settings[15]["color"] 341 else: 342 linecolor = self.chart_colors_settings["houses_radix_line"] 343 344 # Transit houses lines. 345 if self.chart_type == "Transit" or self.chart_type == "Synastry": 346 # Degrees for point zero. 347 348 zeropoint = 360 - self.user.houses_degree_ut[6] 349 t_offset = zeropoint + self.t_user.houses_degree_ut[i] 350 if t_offset > 360: 351 t_offset = t_offset - 360 352 t_x1 = sliceToX(0, (r - t_roff), t_offset) + t_roff 353 t_y1 = sliceToY(0, (r - t_roff), t_offset) + t_roff 354 t_x2 = sliceToX(0, r, t_offset) 355 t_y2 = sliceToY(0, r, t_offset) 356 if i < 11: 357 t_text_offset = t_offset + int(degreeDiff(self.t_user.houses_degree_ut[(i + 1)], self.t_user.houses_degree_ut[i]) / 2) 358 else: 359 t_text_offset = t_offset + int(degreeDiff(self.t_user.houses_degree_ut[0], self.t_user.houses_degree_ut[11]) / 2) 360 # linecolor 361 if i == 0 or i == 9 or i == 6 or i == 3: 362 t_linecolor = linecolor 363 else: 364 t_linecolor = self.chart_colors_settings["houses_transit_line"] 365 xtext = sliceToX(0, (r - 8), t_text_offset) + 8 366 ytext = sliceToY(0, (r - 8), t_text_offset) + 8 367 368 if self.chart_type == "Transit": 369 path = path + '<text style="fill: #00f; fill-opacity: 0; font-size: 14px"><tspan x="' + str(xtext - 3) + '" y="' + str(ytext + 3) + '">' + str(i + 1) + "</tspan></text>" 370 path = f"{path}<line x1='{str(t_x1)}' y1='{str(t_y1)}' x2='{str(t_x2)}' y2='{str(t_y2)}' style='stroke: {t_linecolor}; stroke-width: 2px; stroke-opacity:0;'/>" 371 372 else: 373 path = path + '<text style="fill: #00f; fill-opacity: .4; font-size: 14px"><tspan x="' + str(xtext - 3) + '" y="' + str(ytext + 3) + '">' + str(i + 1) + "</tspan></text>" 374 path = f"{path}<line x1='{str(t_x1)}' y1='{str(t_y1)}' x2='{str(t_x2)}' y2='{str(t_y2)}' style='stroke: {t_linecolor}; stroke-width: 2px; stroke-opacity:.3;'/>" 375 376 377 # if transit 378 if self.chart_type == "Transit" or self.chart_type == "Synastry": 379 dropin = 84 380 elif self.chart_type == "ExternalNatal": 381 dropin = 100 382 # Natal 383 else: 384 dropin = 48 385 386 xtext = sliceToX(0, (r - dropin), text_offset) + dropin # was 132 387 ytext = sliceToY(0, (r - dropin), text_offset) + dropin # was 132 388 path = f'{path}<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {linecolor}; stroke-width: 2px; stroke-dasharray:3,2; stroke-opacity:.4;"/>' 389 path = path + '<text style="fill: #f00; fill-opacity: .6; font-size: 14px"><tspan x="' + str(xtext - 3) + '" y="' + str(ytext + 3) + '">' + str(i + 1) + "</tspan></text>" 390 391 return path 392 393 def _calculate_elements_points_from_planets(self): 394 """ 395 Calculate chart element points from a planet. 396 """ 397 398 for i in range(len(self.available_planets_setting)): 399 # element: get extra points if planet is in own zodiac sign. 400 related_zodiac_signs = self.available_planets_setting[i]["related_zodiac_signs"] 401 cz = self.points_sign[i] 402 extra_points = 0 403 if related_zodiac_signs != []: 404 for e in range(len(related_zodiac_signs)): 405 if int(related_zodiac_signs[e]) == int(cz): 406 extra_points = self.planet_in_zodiac_extra_points 407 408 ele = self.zodiac[self.points_sign[i]]["element"] 409 if ele == "fire": 410 self.fire = self.fire + self.available_planets_setting[i]["element_points"] + extra_points 411 412 elif ele == "earth": 413 self.earth = self.earth + self.available_planets_setting[i]["element_points"] + extra_points 414 415 elif ele == "air": 416 self.air = self.air + self.available_planets_setting[i]["element_points"] + extra_points 417 418 elif ele == "water": 419 self.water = self.water + self.available_planets_setting[i]["element_points"] + extra_points 420 421 def _make_planets(self, r): 422 planets_degut = {} 423 diff = range(len(self.available_planets_setting)) 424 425 for i in range(len(self.available_planets_setting)): 426 # list of planets sorted by degree 427 logging.debug(f"planet: {i}, degree: {self.points_deg_ut[i]}") 428 planets_degut[self.points_deg_ut[i]] = i 429 430 """ 431 FIXME: The planets_degut is a dictionary like: 432 {planet_degree: planet_index} 433 It should be replaced bu points_deg_ut 434 print(self.points_deg_ut) 435 print(planets_degut) 436 """ 437 438 output = "" 439 keys = list(planets_degut.keys()) 440 keys.sort() 441 switch = 0 442 443 planets_degrouped = {} 444 groups = [] 445 planets_by_pos = list(range(len(planets_degut))) 446 planet_drange = 3.4 447 # get groups closely together 448 group_open = False 449 for e in range(len(keys)): 450 i = planets_degut[keys[e]] 451 # get distances between planets 452 if e == 0: 453 prev = self.points_deg_ut[planets_degut[keys[-1]]] 454 next = self.points_deg_ut[planets_degut[keys[1]]] 455 elif e == (len(keys) - 1): 456 prev = self.points_deg_ut[planets_degut[keys[e - 1]]] 457 next = self.points_deg_ut[planets_degut[keys[0]]] 458 else: 459 prev = self.points_deg_ut[planets_degut[keys[e - 1]]] 460 next = self.points_deg_ut[planets_degut[keys[e + 1]]] 461 diffa = degreeDiff(prev, self.points_deg_ut[i]) 462 diffb = degreeDiff(next, self.points_deg_ut[i]) 463 planets_by_pos[e] = [i, diffa, diffb] 464 465 logging.debug(f'{self.available_planets_setting[i]["label"]}, {diffa}, {diffb}') 466 467 if diffb < planet_drange: 468 if group_open: 469 groups[-1].append([e, diffa, diffb, self.available_planets_setting[planets_degut[keys[e]]]["label"]]) 470 else: 471 group_open = True 472 groups.append([]) 473 groups[-1].append([e, diffa, diffb, self.available_planets_setting[planets_degut[keys[e]]]["label"]]) 474 else: 475 if group_open: 476 groups[-1].append([e, diffa, diffb, self.available_planets_setting[planets_degut[keys[e]]]["label"]]) 477 group_open = False 478 479 def zero(x): 480 return 0 481 482 planets_delta = list(map(zero, range(len(self.available_planets_setting)))) 483 484 # print groups 485 # print planets_by_pos 486 for a in range(len(groups)): 487 # Two grouped planets 488 if len(groups[a]) == 2: 489 next_to_a = groups[a][0][0] - 1 490 if groups[a][1][0] == (len(planets_by_pos) - 1): 491 next_to_b = 0 492 else: 493 next_to_b = groups[a][1][0] + 1 494 # if both planets have room 495 if (groups[a][0][1] > (2 * planet_drange)) & (groups[a][1][2] > (2 * planet_drange)): 496 planets_delta[groups[a][0][0]] = -(planet_drange - groups[a][0][2]) / 2 497 planets_delta[groups[a][1][0]] = +(planet_drange - groups[a][0][2]) / 2 498 # if planet a has room 499 elif groups[a][0][1] > (2 * planet_drange): 500 planets_delta[groups[a][0][0]] = -planet_drange 501 # if planet b has room 502 elif groups[a][1][2] > (2 * planet_drange): 503 planets_delta[groups[a][1][0]] = +planet_drange 504 505 # if planets next to a and b have room move them 506 elif (planets_by_pos[next_to_a][1] > (2.4 * planet_drange)) & (planets_by_pos[next_to_b][2] > (2.4 * planet_drange)): 507 planets_delta[(next_to_a)] = groups[a][0][1] - planet_drange * 2 508 planets_delta[groups[a][0][0]] = -planet_drange * 0.5 509 planets_delta[next_to_b] = -(groups[a][1][2] - planet_drange * 2) 510 planets_delta[groups[a][1][0]] = +planet_drange * 0.5 511 512 # if planet next to a has room move them 513 elif planets_by_pos[next_to_a][1] > (2 * planet_drange): 514 planets_delta[(next_to_a)] = groups[a][0][1] - planet_drange * 2.5 515 planets_delta[groups[a][0][0]] = -planet_drange * 1.2 516 517 # if planet next to b has room move them 518 elif planets_by_pos[next_to_b][2] > (2 * planet_drange): 519 planets_delta[next_to_b] = -(groups[a][1][2] - planet_drange * 2.5) 520 planets_delta[groups[a][1][0]] = +planet_drange * 1.2 521 522 # Three grouped planets or more 523 xl = len(groups[a]) 524 if xl >= 3: 525 available = groups[a][0][1] 526 for f in range(xl): 527 available += groups[a][f][2] 528 need = (3 * planet_drange) + (1.2 * (xl - 1) * planet_drange) 529 leftover = available - need 530 xa = groups[a][0][1] 531 xb = groups[a][(xl - 1)][2] 532 533 # center 534 if (xa > (need * 0.5)) & (xb > (need * 0.5)): 535 startA = xa - (need * 0.5) 536 # position relative to next planets 537 else: 538 startA = (leftover / (xa + xb)) * xa 539 startB = (leftover / (xa + xb)) * xb 540 541 if available > need: 542 planets_delta[groups[a][0][0]] = startA - groups[a][0][1] + (1.5 * planet_drange) 543 for f in range(xl - 1): 544 planets_delta[groups[a][(f + 1)][0]] = 1.2 * planet_drange + planets_delta[groups[a][f][0]] - groups[a][f][2] 545 546 for e in range(len(keys)): 547 i = planets_degut[keys[e]] 548 549 # coordinates 550 if self.chart_type == "Transit" or self.chart_type == "Synastry": 551 if 22 < i < 27: 552 rplanet = 76 553 elif switch == 1: 554 rplanet = 110 555 switch = 0 556 else: 557 rplanet = 130 558 switch = 1 559 else: 560 # if 22 < i < 27 it is asc,mc,dsc,ic (angles of chart) 561 # put on special line (rplanet is range from outer ring) 562 amin, bmin, cmin = 0, 0, 0 563 if self.chart_type == "ExternalNatal": 564 amin = 74 - 10 565 bmin = 94 - 10 566 cmin = 40 - 10 567 568 if 22 < i < 27: 569 rplanet = 40 - cmin 570 elif switch == 1: 571 rplanet = 74 - amin 572 switch = 0 573 else: 574 rplanet = 94 - bmin 575 switch = 1 576 577 rtext = 45 578 579 offset = (int(self.user.houses_degree_ut[6]) / -1) + int(self.points_deg_ut[i] + planets_delta[e]) 580 trueoffset = (int(self.user.houses_degree_ut[6]) / -1) + int(self.points_deg_ut[i]) 581 582 planet_x = sliceToX(0, (r - rplanet), offset) + rplanet 583 planet_y = sliceToY(0, (r - rplanet), offset) + rplanet 584 if self.chart_type == "Transit" or self.chart_type == "Synastry": 585 scale = 0.8 586 587 elif self.chart_type == "ExternalNatal": 588 scale = 0.8 589 # line1 590 x1 = sliceToX(0, (r - self.c3), trueoffset) + self.c3 591 y1 = sliceToY(0, (r - self.c3), trueoffset) + self.c3 592 x2 = sliceToX(0, (r - rplanet - 30), trueoffset) + rplanet + 30 593 y2 = sliceToY(0, (r - rplanet - 30), trueoffset) + rplanet + 30 594 color = self.available_planets_setting[i]["color"] 595 output += ( 596 '<line x1="%s" y1="%s" x2="%s" y2="%s" style="stroke-width:1px;stroke:%s;stroke-opacity:.3;"/>\n' 597 % (x1, y1, x2, y2, color) 598 ) 599 # line2 600 x1 = sliceToX(0, (r - rplanet - 30), trueoffset) + rplanet + 30 601 y1 = sliceToY(0, (r - rplanet - 30), trueoffset) + rplanet + 30 602 x2 = sliceToX(0, (r - rplanet - 10), offset) + rplanet + 10 603 y2 = sliceToY(0, (r - rplanet - 10), offset) + rplanet + 10 604 output += ( 605 '<line x1="%s" y1="%s" x2="%s" y2="%s" style="stroke-width:1px;stroke:%s;stroke-opacity:.5;"/>\n' 606 % (x1, y1, x2, y2, color) 607 ) 608 609 else: 610 scale = 1 611 # output planet 612 output += f'<g transform="translate(-{12 * scale},-{12 * scale})"><g transform="scale({scale})"><use x="{planet_x * (1/scale)}" y="{planet_y * (1/scale)}" xlink:href="#{self.available_planets_setting[i]["name"]}" /></g></g>' 613 614 # make transit degut and display planets 615 if self.chart_type == "Transit" or self.chart_type == "Synastry": 616 group_offset = {} 617 t_planets_degut = {} 618 list_range = len(self.available_planets_setting) 619 620 for i in range(list_range): 621 if self.chart_type == "Transit" and self.available_planets_setting[i]['name'] in self.transit_ring_exclude_points_names: 622 continue 623 624 group_offset[i] = 0 625 t_planets_degut[self.t_points_deg_ut[i]] = i 626 627 t_keys = list(t_planets_degut.keys()) 628 t_keys.sort() 629 630 # grab closely grouped planets 631 groups = [] 632 in_group = False 633 for e in range(len(t_keys)): 634 i_a = t_planets_degut[t_keys[e]] 635 if e == (len(t_keys) - 1): 636 i_b = t_planets_degut[t_keys[0]] 637 else: 638 i_b = t_planets_degut[t_keys[e + 1]] 639 640 a = self.t_points_deg_ut[i_a] 641 b = self.t_points_deg_ut[i_b] 642 diff = degreeDiff(a, b) 643 if diff <= 2.5: 644 if in_group: 645 groups[-1].append(i_b) 646 else: 647 groups.append([i_a]) 648 groups[-1].append(i_b) 649 in_group = True 650 else: 651 in_group = False 652 # loop groups and set degrees display adjustment 653 for i in range(len(groups)): 654 if len(groups[i]) == 2: 655 group_offset[groups[i][0]] = -1.0 656 group_offset[groups[i][1]] = 1.0 657 elif len(groups[i]) == 3: 658 group_offset[groups[i][0]] = -1.5 659 group_offset[groups[i][1]] = 0 660 group_offset[groups[i][2]] = 1.5 661 elif len(groups[i]) == 4: 662 group_offset[groups[i][0]] = -2.0 663 group_offset[groups[i][1]] = -1.0 664 group_offset[groups[i][2]] = 1.0 665 group_offset[groups[i][3]] = 2.0 666 667 switch = 0 668 669 # Transit planets loop 670 for e in range(len(t_keys)): 671 if self.chart_type == "Transit" and self.available_planets_setting[e]["name"] in self.transit_ring_exclude_points_names: 672 continue 673 674 i = t_planets_degut[t_keys[e]] 675 676 if 22 < i < 27: 677 rplanet = 9 678 elif switch == 1: 679 rplanet = 18 680 switch = 0 681 else: 682 rplanet = 26 683 switch = 1 684 685 # Transit planet name 686 zeropoint = 360 - self.user.houses_degree_ut[6] 687 t_offset = zeropoint + self.t_points_deg_ut[i] 688 if t_offset > 360: 689 t_offset = t_offset - 360 690 planet_x = sliceToX(0, (r - rplanet), t_offset) + rplanet 691 planet_y = sliceToY(0, (r - rplanet), t_offset) + rplanet 692 output += f'<g class="transit-planet-name" transform="translate(-6,-6)"><g transform="scale(0.5)"><use x="{planet_x*2}" y="{planet_y*2}" xlink:href="#{self.available_planets_setting[i]["name"]}" /></g></g>' 693 694 # Transit planet line 695 x1 = sliceToX(0, r + 3, t_offset) - 3 696 y1 = sliceToY(0, r + 3, t_offset) - 3 697 x2 = sliceToX(0, r - 3, t_offset) + 3 698 y2 = sliceToY(0, r - 3, t_offset) + 3 699 output += f'<line class="transit-planet-line" x1="{str(x1)}" y1="{str(y1)}" x2="{str(x2)}" y2="{str(y2)}" style="stroke: {self.available_planets_setting[i]["color"]}; stroke-width: 1px; stroke-opacity:.8;"/>' 700 701 # transit planet degree text 702 rotate = self.user.houses_degree_ut[0] - self.t_points_deg_ut[i] 703 textanchor = "end" 704 t_offset += group_offset[i] 705 rtext = -3.0 706 707 if -90 > rotate > -270: 708 rotate = rotate + 180.0 709 textanchor = "start" 710 if 270 > rotate > 90: 711 rotate = rotate - 180.0 712 textanchor = "start" 713 714 if textanchor == "end": 715 xo = 1 716 else: 717 xo = -1 718 deg_x = sliceToX(0, (r - rtext), t_offset + xo) + rtext 719 deg_y = sliceToY(0, (r - rtext), t_offset + xo) + rtext 720 degree = int(t_offset) 721 output += f'<g transform="translate({deg_x},{deg_y})">' 722 output += f'<text transform="rotate({rotate})" text-anchor="{textanchor}' 723 output += f'" style="fill: {self.available_planets_setting[i]["color"]}; font-size: 10px;">{convert_decimal_to_degree_string(self.t_points_deg[i], type="1")}' 724 output += "</text></g>" 725 726 # check transit 727 if self.chart_type == "Transit" or self.chart_type == "Synastry": 728 dropin = 36 729 else: 730 dropin = 0 731 732 # planet line 733 x1 = sliceToX(0, r - (dropin + 3), offset) + (dropin + 3) 734 y1 = sliceToY(0, r - (dropin + 3), offset) + (dropin + 3) 735 x2 = sliceToX(0, (r - (dropin - 3)), offset) + (dropin - 3) 736 y2 = sliceToY(0, (r - (dropin - 3)), offset) + (dropin - 3) 737 738 output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {self.available_planets_setting[i]["color"]}; stroke-width: 2px; stroke-opacity:.6;"/>' 739 740 # check transit 741 if self.chart_type == "Transit" or self.chart_type == "Synastry": 742 dropin = 160 743 else: 744 dropin = 120 745 746 x1 = sliceToX(0, r - dropin, offset) + dropin 747 y1 = sliceToY(0, r - dropin, offset) + dropin 748 x2 = sliceToX(0, (r - (dropin - 3)), offset) + (dropin - 3) 749 y2 = sliceToY(0, (r - (dropin - 3)), offset) + (dropin - 3) 750 output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {self.available_planets_setting[i]["color"]}; stroke-width: 2px; stroke-opacity:.6;"/>' 751 752 return output 753 754 def _makePatterns(self): 755 """ 756 * Stellium: At least four planets linked together in a series of continuous conjunctions. 757 * Grand trine: Three trine aspects together. 758 * Grand cross: Two pairs of opposing planets squared to each other. 759 * T-Square: Two planets in opposition squared to a third. 760 * Yod: Two qunicunxes together joined by a sextile. 761 """ 762 conj = {} # 0 763 opp = {} # 10 764 sq = {} # 5 765 tr = {} # 6 766 qc = {} # 9 767 sext = {} # 3 768 for i in range(len(self.available_planets_setting)): 769 a = self.points_deg_ut[i] 770 qc[i] = {} 771 sext[i] = {} 772 opp[i] = {} 773 sq[i] = {} 774 tr[i] = {} 775 conj[i] = {} 776 # skip some points 777 n = self.available_planets_setting[i]["name"] 778 if n == "earth" or n == "True_Node" or n == "osc. apogee" or n == "intp. apogee" or n == "intp. perigee": 779 continue 780 if n == "Dsc" or n == "Ic": 781 continue 782 for j in range(len(self.available_planets_setting)): 783 # skip some points 784 n = self.available_planets_setting[j]["name"] 785 if n == "earth" or n == "True_Node" or n == "osc. apogee" or n == "intp. apogee" or n == "intp. perigee": 786 continue 787 if n == "Dsc" or n == "Ic": 788 continue 789 b = self.points_deg_ut[j] 790 delta = float(degreeDiff(a, b)) 791 # check for opposition 792 xa = float(self.aspects_settings[10]["degree"]) - float(self.aspects_settings[10]["orb"]) 793 xb = float(self.aspects_settings[10]["degree"]) + float(self.aspects_settings[10]["orb"]) 794 if xa <= delta <= xb: 795 opp[i][j] = True 796 # check for conjunction 797 xa = float(self.aspects_settings[0]["degree"]) - float(self.aspects_settings[0]["orb"]) 798 xb = float(self.aspects_settings[0]["degree"]) + float(self.aspects_settings[0]["orb"]) 799 if xa <= delta <= xb: 800 conj[i][j] = True 801 # check for squares 802 xa = float(self.aspects_settings[5]["degree"]) - float(self.aspects_settings[5]["orb"]) 803 xb = float(self.aspects_settings[5]["degree"]) + float(self.aspects_settings[5]["orb"]) 804 if xa <= delta <= xb: 805 sq[i][j] = True 806 # check for qunicunxes 807 xa = float(self.aspects_settings[9]["degree"]) - float(self.aspects_settings[9]["orb"]) 808 xb = float(self.aspects_settings[9]["degree"]) + float(self.aspects_settings[9]["orb"]) 809 if xa <= delta <= xb: 810 qc[i][j] = True 811 # check for sextiles 812 xa = float(self.aspects_settings[3]["degree"]) - float(self.aspects_settings[3]["orb"]) 813 xb = float(self.aspects_settings[3]["degree"]) + float(self.aspects_settings[3]["orb"]) 814 if xa <= delta <= xb: 815 sext[i][j] = True 816 817 yot = {} 818 # check for double qunicunxes 819 for k, v in qc.items(): 820 if len(qc[k]) >= 2: 821 # check for sextile 822 for l, w in qc[k].items(): 823 for m, x in qc[k].items(): 824 if m in sext[l]: 825 if l > m: 826 yot["%s,%s,%s" % (k, m, l)] = [k, m, l] 827 else: 828 yot["%s,%s,%s" % (k, l, m)] = [k, l, m] 829 tsquare = {} 830 # check for opposition 831 for k, v in opp.items(): 832 if len(opp[k]) >= 1: 833 # check for square 834 for l, w in opp[k].items(): 835 for a, b in sq.items(): 836 if k in sq[a] and l in sq[a]: 837 logging.debug(f"Got tsquare {a} {k} {l}") 838 if k > l: 839 tsquare[f"{a},{l},{k}"] = f"{self.available_planets_setting[a]['label']} => {self.available_planets_setting[l]['label']}, {self.available_planets_setting[k]['label']}" 840 841 else: 842 tsquare[f"{a},{k},{l}"] = f"{self.available_planets_setting[a]['label']} => {self.available_planets_setting[k]['label']}, {self.available_planets_setting[l]['label']}" 843 844 stellium = {} 845 # check for 4 continuous conjunctions 846 for k, v in conj.items(): 847 if len(conj[k]) >= 1: 848 # first conjunction 849 for l, m in conj[k].items(): 850 if len(conj[l]) >= 1: 851 for n, o in conj[l].items(): 852 # skip 1st conj 853 if n == k: 854 continue 855 if len(conj[n]) >= 1: 856 # third conjunction 857 for p, q in conj[n].items(): 858 # skip first and second conj 859 if p == k or p == n: 860 continue 861 if len(conj[p]) >= 1: 862 # fourth conjunction 863 for r, s in conj[p].items(): 864 # skip conj 1,2,3 865 if r == k or r == n or r == p: 866 continue 867 868 l = [k, n, p, r] 869 l.sort() 870 stellium["%s %s %s %s" % (l[0], l[1], l[2], l[3])] = "%s %s %s %s" % ( 871 self.available_planets_setting[l[0]]["label"], 872 self.available_planets_setting[l[1]]["label"], 873 self.available_planets_setting[l[2]]["label"], 874 self.available_planets_setting[l[3]]["label"], 875 ) 876 # print yots 877 out = '<g transform="translate(-30,380)">' 878 if len(yot) >= 1: 879 y = 0 880 for k, v in yot.items(): 881 out += f'<text y="{y}" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 12px;">{"Yot"}</text>' 882 883 # first planet symbol 884 out += f'<g transform="translate(20,{y})">' 885 out += f'<use transform="scale(0.4)" x="0" y="-20" xlink:href="#{self.available_planets_setting[yot[k][0]]["name"]}" /></g>' 886 887 # second planet symbol 888 out += f'<g transform="translate(30,{y})">' 889 out += f'<use transform="scale(0.4)" x="0" y="-20" xlink:href="#{self.available_planets_setting[yot[k][1]]["name"]}" /></g>' 890 891 # third planet symbol 892 out += f'<g transform="translate(40,{y})">' 893 out += f'<use transform="scale(0.4)" x="0" y="-20" xlink:href="#{self.available_planets_setting[yot[k][2]]["name"]}" /></g>' 894 895 y = y + 14 896 # finalize 897 out += "</g>" 898 # return out 899 return "" 900 901 # Aspect and aspect grid functions for natal type charts. 902 def _makeAspects(self, r, ar): 903 out = "" 904 for element in self.aspects_list: 905 out += draw_aspect_line( 906 r=r, 907 ar=ar, 908 degA=element["p1_abs_pos"], 909 degB=element["p2_abs_pos"], 910 color=self.aspects_settings[element["aid"]]["color"], 911 seventh_house_degree_ut=self.user.seventh_house.abs_pos 912 ) 913 914 return out 915 916 def _makeAspectGrid(self, r): 917 out = "" 918 style = "stroke:%s; stroke-width: 1px; stroke-opacity:.6; fill:none" % (self.chart_colors_settings["paper_0"]) 919 xindent = 380 920 yindent = 468 921 box = 14 922 revr = list(range(len(self.available_planets_setting))) 923 revr.reverse() 924 counter = 0 925 for a in revr: 926 counter += 1 927 out += f'<rect x="{xindent}" y="{yindent}" width="{box}" height="{box}" style="{style}"/>' 928 out += f'<use transform="scale(0.4)" x="{(xindent+2)*2.5}" y="{(yindent+1)*2.5}" xlink:href="#{self.available_planets_setting[a]["name"]}" />' 929 930 xindent = xindent + box 931 yindent = yindent - box 932 revr2 = list(range(a)) 933 revr2.reverse() 934 xorb = xindent 935 yorb = yindent + box 936 for b in revr2: 937 out += f'<rect x="{xorb}" y="{yorb}" width="{box}" height="{box}" style="{style}"/>' 938 939 xorb = xorb + box 940 for element in self.aspects_list: 941 if (element["p1"] == a and element["p2"] == b) or (element["p1"] == b and element["p2"] == a): 942 out += f'<use x="{xorb-box+1}" y="{yorb+1}" xlink:href="#orb{element["aspect_degrees"]}" />' 943 944 return out 945 946 # Aspect and aspect grid functions for transit type charts 947 def _makeAspectsTransit(self, r, ar): 948 out = "" 949 950 self.aspects_list = SynastryAspects(self.user, self.t_user, new_settings_file=self.new_settings_file).relevant_aspects 951 952 for element in self.aspects_list: 953 out += draw_aspect_line( 954 r=r, 955 ar=ar, 956 degA=element["p1_abs_pos"], 957 degB=element["p2_abs_pos"], 958 color=self.aspects_settings[element["aid"]]["color"], 959 seventh_house_degree_ut=self.user.seventh_house.abs_pos 960 ) 961 962 return out 963 964 def _makeAspectTransitGrid(self, r): 965 out = '<g transform="translate(500,310)">' 966 out += f'<text y="-15" x="0" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 14px;">{self.language_settings["aspects"]}:</text>' 967 968 line = 0 969 nl = 0 970 971 for i in range(len(self.aspects_list)): 972 if i == 12: 973 nl = 100 974 975 line = 0 976 977 elif i == 24: 978 nl = 200 979 980 line = 0 981 982 elif i == 36: 983 nl = 300 984 985 line = 0 986 987 elif i == 48: 988 nl = 400 989 990 # When there are more than 60 aspects, the text is moved up 991 if len(self.aspects_list) > 60: 992 line = -1 * (len(self.aspects_list) - 60) * 14 993 else: 994 line = 0 995 996 out += f'<g transform="translate({nl},{line})">' 997 998 # first planet symbol 999 out += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{self.planets_settings[self.aspects_list[i]["p1"]]["name"]}" />' 1000 1001 # aspect symbol 1002 out += f'<use x="15" y="0" xlink:href="#orb{self.aspects_settings[self.aspects_list[i]["aid"]]["degree"]}" />' 1003 1004 # second planet symbol 1005 out += '<g transform="translate(30,0)">' 1006 out += '<use transform="scale(0.4)" x="0" y="3" xlink:href="#%s" />' % (self.planets_settings[self.aspects_list[i]["p2"]]["name"]) 1007 1008 out += "</g>" 1009 # difference in degrees 1010 out += f'<text y="8" x="45" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{convert_decimal_to_degree_string(self.aspects_list[i]["orbit"])}</text>' 1011 # line 1012 out += "</g>" 1013 line = line + 14 1014 out += "</g>" 1015 return out 1016 1017 def _makePlanetGrid(self): 1018 li = 10 1019 offset = 0 1020 1021 out = '<g transform="translate(510,-20)">' 1022 out += '<g transform="translate(140, -15)">' 1023 out += f'<text text-anchor="end" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 14px;">{self.language_settings["planets_and_house"]} {self.user.name}:</text>' 1024 out += "</g>" 1025 1026 end_of_line = None 1027 for i in range(len(self.available_planets_setting)): 1028 offset_between_lines = 14 1029 end_of_line = "</g>" 1030 1031 # Guarda qui !! 1032 if i == 27: 1033 li = 10 1034 offset = -120 1035 1036 # start of line 1037 out += f'<g transform="translate({offset},{li})">' 1038 1039 # planet text 1040 out += f'<text text-anchor="end" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{self.language_settings["celestial_points"][self.available_planets_setting[i]["label"]]}</text>' 1041 1042 # planet symbol 1043 out += f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{self.available_planets_setting[i]["name"]}" /></g>' 1044 1045 # planet degree 1046 out += f'<text text-anchor="start" x="19" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{convert_decimal_to_degree_string(self.points_deg[i])}</text>' 1047 1048 # zodiac 1049 out += f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{self.zodiac[self.points_sign[i]]["name"]}" /></g>' 1050 1051 # planet retrograde 1052 if self.points_retrograde[i]: 1053 out += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>' 1054 1055 # end of line 1056 out += end_of_line 1057 1058 li = li + offset_between_lines 1059 1060 if self.chart_type == "Transit" or self.chart_type == "Synastry": 1061 if self.chart_type == "Transit": 1062 out += '<g transform="translate(320, -15)">' 1063 out += f'<text text-anchor="end" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 14px;">{self.t_name}:</text>' 1064 else: 1065 out += '<g transform="translate(380, -15)">' 1066 out += f'<text text-anchor="end" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 14px;">{self.language_settings["planets_and_house"]} {self.t_user.name}:</text>' 1067 1068 out += end_of_line 1069 1070 t_li = 10 1071 t_offset = 250 1072 1073 for i in range(len(self.available_planets_setting)): 1074 if i == 27: 1075 t_li = 10 1076 t_offset = -120 1077 1078 # start of line 1079 out += f'<g transform="translate({t_offset},{t_li})">' 1080 1081 # planet text 1082 out += f'<text text-anchor="end" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{self.language_settings["celestial_points"][self.available_planets_setting[i]["label"]]}</text>' 1083 # planet symbol 1084 out += f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{self.available_planets_setting[i]["name"]}" /></g>' 1085 # planet degree 1086 out += f'<text text-anchor="start" x="19" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{convert_decimal_to_degree_string(self.t_points_deg[i])}</text>' 1087 # zodiac 1088 out += f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{self.zodiac[self.t_points_sign[i]]["name"]}" /></g>' 1089 1090 # planet retrograde 1091 if self.t_points_retrograde[i]: 1092 out += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>' 1093 1094 # end of line 1095 out += end_of_line 1096 1097 t_li = t_li + offset_between_lines 1098 1099 if end_of_line is None: 1100 raise KerykeionException("End of line not found") 1101 1102 out += end_of_line 1103 return out 1104 1105 def _draw_house_grid(self): 1106 """ 1107 Generate SVG code for a grid of astrological houses. 1108 1109 Returns: 1110 str: The SVG code for the grid of houses. 1111 """ 1112 out = '<g transform="translate(610,-20)">' 1113 1114 li = 10 1115 for i in range(12): 1116 if i < 9: 1117 cusp = "  " + str(i + 1) 1118 else: 1119 cusp = str(i + 1) 1120 out += f'<g transform="translate(0,{li})">' 1121 out += f'<text text-anchor="end" x="40" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{self.language_settings["cusp"]} {cusp}:</text>' 1122 out += f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{self.zodiac[self.houses_sign_graph[i]]["name"]}" /></g>' 1123 out += f'<text x="53" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;"> {convert_decimal_to_degree_string(self.user.houses_list[i]["position"])}</text>' 1124 out += "</g>" 1125 li = li + 14 1126 1127 out += "</g>" 1128 1129 if self.chart_type == "Synastry": 1130 out += '<!-- Synastry Houses -->' 1131 out += '<g transform="translate(850, -20)">' 1132 li = 10 1133 1134 for i in range(12): 1135 if i < 9: 1136 cusp = "  " + str(i + 1) 1137 else: 1138 cusp = str(i + 1) 1139 out += '<g transform="translate(0,' + str(li) + ')">' 1140 out += f'<text text-anchor="end" x="40" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{self.language_settings["cusp"]} {cusp}:</text>' 1141 out += f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{self.zodiac[self.t_houses_sign_graph[i]]["name"]}" /></g>' 1142 out += f'<text x="53" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;"> {convert_decimal_to_degree_string(self.t_user.houses_list[i]["position"])}</text>' 1143 out += "</g>" 1144 li = li + 14 1145 out += "</g>" 1146 1147 return out 1148 1149 def _createTemplateDictionary(self) -> ChartTemplateDictionary: 1150 # self.chart_type = "Transit" 1151 # empty element points 1152 self.fire = 0.0 1153 self.earth = 0.0 1154 self.air = 0.0 1155 self.water = 0.0 1156 1157 # Calculate the elements points 1158 self._calculate_elements_points_from_planets() 1159 1160 # Viewbox and sizing 1161 svgHeight = "100%" 1162 svgWidth = "100%" 1163 rotate = "0" 1164 1165 # To increase the size of the chart, change the viewbox 1166 if self.chart_type == "Natal" or self.chart_type == "ExternalNatal": 1167 viewbox = self.chart_settings["basic_chart_viewBox"] 1168 else: 1169 viewbox = self.chart_settings["wide_chart_viewBox"] 1170 1171 # template dictionary 1172 td: ChartTemplateDictionary = dict() # type: ignore 1173 r = 240 1174 1175 if self.chart_type == "ExternalNatal": 1176 self.c1 = 56 1177 self.c2 = 92 1178 self.c3 = 112 1179 else: 1180 self.c1 = 0 1181 self.c2 = 36 1182 self.c3 = 120 1183 1184 # transit 1185 if self.chart_type == "Transit" or self.chart_type == "Synastry": 1186 td["transitRing"] = draw_transit_ring(r, self.chart_colors_settings["paper_1"], self.chart_colors_settings["zodiac_transit_ring_3"]) 1187 td["degreeRing"] = draw_transit_ring_degree_steps(r, self.user.seventh_house.abs_pos) 1188 1189 # circles 1190 td["first_circle"] = draw_first_circle(r, self.chart_colors_settings["zodiac_transit_ring_2"], self.chart_type) 1191 td["second_circle"] = draw_second_circle(r, self.chart_colors_settings['zodiac_transit_ring_1'], self.chart_colors_settings['paper_1'], self.chart_type) 1192 1193 td["c3"] = 'cx="' + str(r) + '" cy="' + str(r) + '" r="' + str(r - 160) + '"' 1194 td["c3style"] = f"fill: {self.chart_colors_settings['paper_1']}; fill-opacity:.8; stroke: {self.chart_colors_settings['zodiac_transit_ring_0']}; stroke-width: 1px" 1195 1196 td["makeAspects"] = self._makeAspectsTransit(r, (r - 160)) 1197 td["makeAspectGrid"] = self._makeAspectTransitGrid(r) 1198 td["makePatterns"] = "" 1199 else: 1200 td["transitRing"] = "" 1201 td["degreeRing"] = draw_degree_ring(r, self.c1, self.user.seventh_house.abs_pos, self.chart_colors_settings["paper_0"]) 1202 1203 td['first_circle'] = draw_first_circle(r, self.chart_colors_settings["zodiac_radix_ring_2"], self.chart_type, self.c1) 1204 1205 td["second_circle"] = draw_second_circle(r, self.chart_colors_settings["zodiac_radix_ring_1"], self.chart_colors_settings["paper_1"], self.chart_type, self.c2) 1206 1207 td["c3"] = f'cx="{r}" cy="{r}" r="{r - self.c3}"' 1208 td["c3style"] = f'fill: {self.chart_colors_settings["paper_1"]}; fill-opacity:.8; stroke: {self.chart_colors_settings["zodiac_radix_ring_0"]}; stroke-width: 1px' 1209 1210 td["makeAspects"] = self._makeAspects(r, (r - self.c3)) 1211 td["makeAspectGrid"] = self._makeAspectGrid(r) 1212 td["makePatterns"] = self._makePatterns() 1213 1214 td["chart_height"] = self.height 1215 td["chart_width"] = self.width 1216 td["circleX"] = str(0) 1217 td["circleY"] = str(0) 1218 td["svgWidth"] = str(svgWidth) 1219 td["svgHeight"] = str(svgHeight) 1220 td["viewbox"] = viewbox 1221 1222 # Chart Title 1223 if self.chart_type == "Synastry": 1224 td["stringTitle"] = f"{self.user.name} {self.language_settings['and_word']} {self.t_user.name}" 1225 1226 elif self.chart_type == "Transit": 1227 td["stringTitle"] = f"{self.language_settings['transits']} {self.t_user.day}/{self.t_user.month}/{self.t_user.year}" 1228 1229 else: 1230 td["stringTitle"] = self.user.name 1231 1232 # Chart Name 1233 if self.chart_type == "Synastry" or self.chart_type == "Transit": 1234 td["stringName"] = f"{self.user.name}:" 1235 else: 1236 td["stringName"] = f'{self.language_settings["info"]}:' 1237 1238 # Bottom Left Corner 1239 if self.chart_type == "Natal" or self.chart_type == "ExternalNatal" or self.chart_type == "Synastry": 1240 td["bottomLeft0"] = f"{self.user.zodiac_type if self.user.zodiac_type == 'Tropic' else self.user.zodiac_type + ' ' + self.user.sidereal_mode}" 1241 td["bottomLeft1"] = f"{self.user.houses_system_name}" 1242 td["bottomLeft2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get("day", "Day")} {self.user.lunar_phase.get("moon_phase", "")}' 1243 td["bottomLeft3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.user.lunar_phase.moon_phase_name}' 1244 td["bottomLeft4"] = f'{self.user.perspective_type}' 1245 1246 else: 1247 td["bottomLeft0"] = f"{self.user.zodiac_type if self.user.zodiac_type == 'Tropic' else self.user.zodiac_type + ' ' + self.user.sidereal_mode}" 1248 td["bottomLeft1"] = f"{self.user.houses_system_name}" 1249 td["bottomLeft2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get("day", "Day")} {self.t_user.lunar_phase.get("moon_phase", "")}' 1250 td["bottomLeft3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.t_user.lunar_phase.moon_phase_name}' 1251 td["bottomLeft4"] = f'{self.t_user.perspective_type}' 1252 1253 # lunar phase 1254 deg = self.user.lunar_phase["degrees_between_s_m"] 1255 1256 lffg = None 1257 lfbg = None 1258 lfcx = None 1259 lfr = None 1260 1261 if deg < 90.0: 1262 maxr = deg 1263 if deg > 80.0: 1264 maxr = maxr * maxr 1265 lfcx = 20.0 + (deg / 90.0) * (maxr + 10.0) 1266 lfr = 10.0 + (deg / 90.0) * maxr 1267 lffg = self.chart_colors_settings["lunar_phase_0"] 1268 lfbg = self.chart_colors_settings["lunar_phase_1"] 1269 1270 elif deg < 180.0: 1271 maxr = 180.0 - deg 1272 if deg < 100.0: 1273 maxr = maxr * maxr 1274 lfcx = 20.0 + ((deg - 90.0) / 90.0 * (maxr + 10.0)) - (maxr + 10.0) 1275 lfr = 10.0 + maxr - ((deg - 90.0) / 90.0 * maxr) 1276 lffg = self.chart_colors_settings["lunar_phase_1"] 1277 lfbg = self.chart_colors_settings["lunar_phase_0"] 1278 1279 elif deg < 270.0: 1280 maxr = deg - 180.0 1281 if deg > 260.0: 1282 maxr = maxr * maxr 1283 lfcx = 20.0 + ((deg - 180.0) / 90.0 * (maxr + 10.0)) 1284 lfr = 10.0 + ((deg - 180.0) / 90.0 * maxr) 1285 lffg, lfbg = self.chart_colors_settings["lunar_phase_1"], self.chart_colors_settings["lunar_phase_0"] 1286 1287 elif deg < 361: 1288 maxr = 360.0 - deg 1289 if deg < 280.0: 1290 maxr = maxr * maxr 1291 lfcx = 20.0 + ((deg - 270.0) / 90.0 * (maxr + 10.0)) - (maxr + 10.0) 1292 lfr = 10.0 + maxr - ((deg - 270.0) / 90.0 * maxr) 1293 lffg, lfbg = self.chart_colors_settings["lunar_phase_0"], self.chart_colors_settings["lunar_phase_1"] 1294 1295 if lffg is None or lfbg is None or lfcx is None or lfr is None: 1296 raise KerykeionException("Lunar phase error") 1297 1298 td["lunar_phase_fg"] = lffg 1299 td["lunar_phase_bg"] = lfbg 1300 td["lunar_phase_cx"] = lfcx 1301 td["lunar_phase_r"] = lfr 1302 td["lunar_phase_outline"] = self.chart_colors_settings["lunar_phase_2"] 1303 1304 # rotation based on latitude 1305 td["lunar_phase_rotate"] = -90.0 - self.geolat 1306 1307 # stringlocation 1308 if len(self.location) > 35: 1309 split = self.location.split(",") 1310 if len(split) > 1: 1311 td["stringLocation"] = split[0] + ", " + split[-1] 1312 if len(td["stringLocation"]) > 35: 1313 td["stringLocation"] = td["stringLocation"][:35] + "..." 1314 else: 1315 td["stringLocation"] = self.location[:35] + "..." 1316 else: 1317 td["stringLocation"] = self.location 1318 1319 if self.chart_type == "Synastry": 1320 td["stringLat"] = f"{self.t_user.name}: " 1321 td["stringLon"] = self.t_user.city 1322 td["stringPosition"] = f"{self.t_user.year}-{self.t_user.month}-{self.t_user.day} {self.t_user.hour:02d}:{self.t_user.minute:02d}" 1323 1324 else: 1325 latitude_string = convert_latitude_coordinate_to_string( 1326 self.geolat, 1327 self.language_settings['north'], 1328 self.language_settings['south'] 1329 ) 1330 longitude_string = convert_longitude_coordinate_to_string( 1331 self.geolon, 1332 self.language_settings['east'], 1333 self.language_settings['west'] 1334 ) 1335 1336 td["stringLat"] = f"{self.language_settings['latitude']}: {latitude_string}" 1337 td["stringLon"] = f"{self.language_settings['longitude']}: {longitude_string}" 1338 td["stringPosition"] = f"{self.language_settings['type']}: {self.chart_type}" 1339 1340 # paper_color_X 1341 td["paper_color_0"] = self.chart_colors_settings["paper_0"] 1342 td["paper_color_1"] = self.chart_colors_settings["paper_1"] 1343 1344 # planets_color_X 1345 for i in range(len(self.planets_settings)): 1346 planet_id = self.planets_settings[i]["id"] 1347 td[f"planets_color_{planet_id}"] = self.planets_settings[i]["color"] 1348 1349 # zodiac_color_X 1350 for i in range(12): 1351 td[f"zodiac_color_{i}"] = self.chart_colors_settings[f"zodiac_icon_{i}"] 1352 1353 # orb_color_X 1354 for i in range(len(self.aspects_settings)): 1355 td[f"orb_color_{self.aspects_settings[i]['degree']}"] = self.aspects_settings[i]['color'] 1356 1357 # config 1358 td["cfgZoom"] = str(self.zoom) 1359 td["cfgRotate"] = rotate 1360 1361 # --- 1362 # Drawing Functions 1363 #--- 1364 1365 td["makeZodiac"] = self._draw_zodiac_circle_slices(r) 1366 td["makeHousesGrid"] = self._draw_house_grid() 1367 # TODO: Add the rest of the functions 1368 td["makeHouses"] = self._makeHouses(r) 1369 td["makePlanets"] = self._make_planets(r) 1370 td["elements_percentages"] = draw_elements_percentages( 1371 self.language_settings['fire'], 1372 self.fire, 1373 self.language_settings['earth'], 1374 self.earth, 1375 self.language_settings['air'], 1376 self.air, 1377 self.language_settings['water'], 1378 self.water, 1379 ) 1380 td["makePlanetGrid"] = self._makePlanetGrid() 1381 1382 # Date time String 1383 dt = datetime.fromisoformat(self.user.iso_formatted_local_datetime) 1384 custom_format = dt.strftime('%Y-%-m-%-d %H:%M [%z]') # Note the use of '-' to remove leading zeros 1385 custom_format = custom_format[:-3] + ':' + custom_format[-3:] 1386 td["stringDateTime"] = f"{custom_format}" 1387 1388 return td 1389 1390 def makeTemplate(self, minify: bool = False) -> str: 1391 """Creates the template for the SVG file""" 1392 td = self._createTemplateDictionary() 1393 1394 # read template 1395 with open(self.xml_svg, "r", encoding="utf-8", errors="ignore") as f: 1396 template = Template(f.read()).substitute(td) 1397 1398 # return filename 1399 1400 logging.debug(f"Template dictionary keys: {td.keys()}") 1401 1402 self._createTemplateDictionary() 1403 1404 if minify: 1405 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 1406 1407 else: 1408 template = template.replace('"', "'") 1409 1410 return template 1411 1412 def makeSVG(self, minify: bool = False): 1413 """Prints out the SVG file in the specifide folder""" 1414 1415 if not (self.template): 1416 self.template = self.makeTemplate(minify) 1417 1418 self.chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg" 1419 1420 with open(self.chartname, "w", encoding="utf-8", errors="ignore") as output_file: 1421 output_file.write(self.template) 1422 1423 logging.info(f"SVG Generated Correctly in: {self.chartname}") 1424 1425 1426if __name__ == "__main__": 1427 from kerykeion.utilities import setup_logging 1428 setup_logging(level="debug") 1429 1430 first = AstrologicalSubject("John Lennon", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1431 second = AstrologicalSubject("Paul McCartney", 1942, 6, 18, 15, 30, "Liverpool", "GB") 1432 1433 # Internal Natal Chart 1434 internal_natal_chart = KerykeionChartSVG(first) 1435 internal_natal_chart.makeSVG() 1436 1437 # External Natal Chart 1438 external_natal_chart = KerykeionChartSVG(first, "ExternalNatal", second) 1439 external_natal_chart.makeSVG() 1440 1441 # Synastry Chart 1442 synastry_chart = KerykeionChartSVG(first, "Synastry", second) 1443 synastry_chart.makeSVG() 1444 1445 # Transits Chart 1446 transits_chart = KerykeionChartSVG(first, "Transit", second) 1447 transits_chart.makeSVG() 1448 1449 # Sidereal Birth Chart (Lahiri) 1450 sidereal_subject = AstrologicalSubject("John Lennon Lahiri", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="LAHIRI") 1451 sidereal_chart = KerykeionChartSVG(sidereal_subject) 1452 sidereal_chart.makeSVG() 1453 1454 # Sidereal Birth Chart (Fagan-Bradley) 1455 sidereal_subject = AstrologicalSubject("John Lennon Fagan-Bradley", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="FAGAN_BRADLEY") 1456 sidereal_chart = KerykeionChartSVG(sidereal_subject) 1457 sidereal_chart.makeSVG() 1458 1459 # Sidereal Birth Chart (DeLuce) 1460 sidereal_subject = AstrologicalSubject("John Lennon DeLuce", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="DELUCE") 1461 sidereal_chart = KerykeionChartSVG(sidereal_subject) 1462 sidereal_chart.makeSVG() 1463 1464 # Sidereal Birth Chart (J2000) 1465 sidereal_subject = AstrologicalSubject("John Lennon J2000", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="J2000") 1466 sidereal_chart = KerykeionChartSVG(sidereal_subject) 1467 sidereal_chart.makeSVG() 1468 1469 # House System Morinus 1470 morinus_house_subject = AstrologicalSubject("John Lennon - House System Morinus", 1940, 10, 9, 18, 30, "Liverpool", "GB", houses_system_identifier="M") 1471 morinus_house_chart = KerykeionChartSVG(morinus_house_subject) 1472 morinus_house_chart.makeSVG() 1473 1474 ## To check all the available house systems uncomment the following code: 1475 # from kerykeion.kr_types import HousesSystemIdentifier 1476 # from typing import get_args 1477 # for i in get_args(HousesSystemIdentifier): 1478 # alternatives_house_subject = AstrologicalSubject(f"John Lennon - House System {i}", 1940, 10, 9, 18, 30, "Liverpool", "GB", houses_system=i) 1479 # alternatives_house_chart = KerykeionChartSVG(alternatives_house_subject) 1480 # alternatives_house_chart.makeSVG() 1481 1482 # With True Geocentric Perspective 1483 true_geocentric_subject = AstrologicalSubject("John Lennon - True Geocentric", 1940, 10, 9, 18, 30, "Liverpool", "GB", perspective_type="True Geocentric") 1484 true_geocentric_chart = KerykeionChartSVG(true_geocentric_subject) 1485 true_geocentric_chart.makeSVG() 1486 1487 # With Heliocentric Perspective 1488 heliocentric_subject = AstrologicalSubject("John Lennon - Heliocentric", 1940, 10, 9, 18, 30, "Liverpool", "GB", perspective_type="Heliocentric") 1489 heliocentric_chart = KerykeionChartSVG(heliocentric_subject) 1490 heliocentric_chart.makeSVG() 1491 1492 # With Topocentric Perspective 1493 topocentric_subject = AstrologicalSubject("John Lennon - Topocentric", 1940, 10, 9, 18, 30, "Liverpool", "GB", perspective_type="Topocentric") 1494 topocentric_chart = KerykeionChartSVG(topocentric_subject) 1495 topocentric_chart.makeSVG()
class
KerykeionChartSVG:
41class KerykeionChartSVG: 42 """ 43 Creates the instance that can generate the chart with the 44 function makeSVG(). 45 46 Parameters: 47 - first_obj: First kerykeion object 48 - chart_type: Natal, ExternalNatal, Transit, Synastry (Default: Type="Natal"). 49 - second_obj: Second kerykeion object (Not required if type is Natal) 50 - new_output_directory: Set the output directory (default: home directory). 51 - new_settings_file: Set the settings file (default: kr.config.json). 52 In the settings file you can set the language, colors, planets, aspects, etc. 53 """ 54 55 # Constants 56 _DEFAULT_HEIGHT = 546.0 57 _DEFAULT_FULL_WIDTH = 1200 58 _DEFAULT_NATAL_WIDTH = 772.2 59 60 # Set at init 61 first_obj: AstrologicalSubject 62 second_obj: Union[AstrologicalSubject, None] 63 chart_type: ChartType 64 new_output_directory: Union[Path, None] 65 new_settings_file: Union[Path, None] 66 output_directory: Path 67 68 # Internal properties 69 fire: float 70 earth: float 71 air: float 72 water: float 73 c1: float 74 c2: float 75 c3: float 76 homedir: Path 77 xml_svg: Path 78 width: Union[float, int] 79 language_settings: dict 80 chart_colors_settings: dict 81 planets_settings: dict 82 aspects_settings: dict 83 planet_in_zodiac_extra_points: int 84 chart_settings: dict 85 user: AstrologicalSubject 86 available_planets_setting: List[KerykeionSettingsCelestialPointModel] 87 transit_ring_exclude_points_names: List[str] 88 points_deg_ut: list 89 points_deg: list 90 points_sign: list 91 points_retrograde: list 92 houses_sign_graph: list 93 t_points_deg_ut: list 94 t_points_deg: list 95 t_points_sign: list 96 t_points_retrograde: list 97 t_houses_sign_graph: list 98 height: float 99 location: str 100 geolat: float 101 geolon: float 102 zoom: int 103 zodiac: tuple 104 template: str 105 106 def __init__( 107 self, 108 first_obj: AstrologicalSubject, 109 chart_type: ChartType = "Natal", 110 second_obj: Union[AstrologicalSubject, None] = None, 111 new_output_directory: Union[str, None] = None, 112 new_settings_file: Union[Path, None] = None, 113 ): 114 # Directories: 115 DATA_DIR = Path(__file__).parent 116 self.homedir = Path.home() 117 self.new_settings_file = new_settings_file 118 119 if new_output_directory: 120 self.output_directory = Path(new_output_directory) 121 else: 122 self.output_directory = self.homedir 123 124 self.xml_svg = DATA_DIR / "templates/chart.xml" 125 126 self.parse_json_settings(new_settings_file) 127 self.chart_type = chart_type 128 129 # Kerykeion instance 130 self.user = first_obj 131 132 self.available_planets_setting = [] 133 for body in self.planets_settings: 134 if body['is_active'] == False: 135 continue 136 137 self.available_planets_setting.append(body) 138 139 # House cusp points are excluded from the transit ring. 140 self.transit_ring_exclude_points_names = [ 141 "First_House", 142 "Second_House", 143 "Third_House", 144 "Fourth_House", 145 "Fifth_House", 146 "Sixth_House", 147 "Seventh_House", 148 "Eighth_House", 149 "Ninth_House", 150 "Tenth_House", 151 "Eleventh_House", 152 "Twelfth_House" 153 ] 154 155 # Available bodies 156 available_celestial_points = [] 157 for body in self.available_planets_setting: 158 available_celestial_points.append(body["name"].lower()) 159 160 # Make a list for the absolute degrees of the points of the graphic. 161 self.points_deg_ut = [] 162 for planet in available_celestial_points: 163 self.points_deg_ut.append(self.user.get(planet).abs_pos) 164 165 # Make a list of the relative degrees of the points in the graphic. 166 self.points_deg = [] 167 for planet in available_celestial_points: 168 self.points_deg.append(self.user.get(planet).position) 169 170 # Make list of the points sign 171 self.points_sign = [] 172 for planet in available_celestial_points: 173 self.points_sign.append(self.user.get(planet).sign_num) 174 175 # Make a list of points if they are retrograde or not. 176 self.points_retrograde = [] 177 for planet in available_celestial_points: 178 self.points_retrograde.append(self.user.get(planet).retrograde) 179 180 # Makes the sign number list. 181 182 self.houses_sign_graph = [] 183 for h in self.user.houses_list: 184 self.houses_sign_graph.append(h["sign_num"]) 185 186 if self.chart_type == "Natal" or self.chart_type == "ExternalNatal": 187 natal_aspects_instance = NatalAspects(self.user, new_settings_file=self.new_settings_file) 188 self.aspects_list = natal_aspects_instance.relevant_aspects 189 190 # TODO: If not second should exit 191 if self.chart_type == "Transit" or self.chart_type == "Synastry": 192 if not second_obj: 193 raise KerykeionException("Second object is required for Transit or Synastry charts.") 194 195 # Kerykeion instance 196 self.t_user = second_obj 197 198 # Make a list for the absolute degrees of the points of the graphic. 199 self.t_points_deg_ut = [] 200 for planet in available_celestial_points: 201 self.t_points_deg_ut.append(self.t_user.get(planet).abs_pos) 202 203 # Make a list of the relative degrees of the points in the graphic. 204 self.t_points_deg = [] 205 for planet in available_celestial_points: 206 self.t_points_deg.append(self.t_user.get(planet).position) 207 208 # Make list of the poits sign. 209 self.t_points_sign = [] 210 for planet in available_celestial_points: 211 self.t_points_sign.append(self.t_user.get(planet).sign_num) 212 213 # Make a list of poits if they are retrograde or not. 214 self.t_points_retrograde = [] 215 for planet in available_celestial_points: 216 self.t_points_retrograde.append(self.t_user.get(planet).retrograde) 217 218 self.t_houses_sign_graph = [] 219 for h in self.t_user.houses_list: 220 self.t_houses_sign_graph.append(h["sign_num"]) 221 222 # screen size 223 self.height = self._DEFAULT_HEIGHT 224 if self.chart_type == "Synastry" or self.chart_type == "Transit": 225 self.width = self._DEFAULT_FULL_WIDTH 226 else: 227 self.width = self._DEFAULT_NATAL_WIDTH 228 229 # default location 230 self.location = self.user.city 231 self.geolat = self.user.lat 232 self.geolon = self.user.lng 233 234 logging.info(f"{self.user.name} birth location: {self.location}, {self.geolat}, {self.geolon}") 235 236 if self.chart_type == "Transit": 237 self.t_name = self.language_settings["transit_name"] 238 239 # configuration 240 # ZOOM 1 = 100% 241 self.zoom = 1 242 243 self.zodiac = ( 244 {"name": "aries", "element": "fire"}, 245 {"name": "taurus", "element": "earth"}, 246 {"name": "gemini", "element": "air"}, 247 {"name": "cancer", "element": "water"}, 248 {"name": "leo", "element": "fire"}, 249 {"name": "virgo", "element": "earth"}, 250 {"name": "libra", "element": "air"}, 251 {"name": "scorpio", "element": "water"}, 252 {"name": "sagittarius", "element": "fire"}, 253 {"name": "capricorn", "element": "earth"}, 254 {"name": "aquarius", "element": "air"}, 255 {"name": "pisces", "element": "water"}, 256 ) 257 258 self.template = None 259 260 def set_output_directory(self, dir_path: Path) -> None: 261 """ 262 Sets the output direcotry and returns it's path. 263 """ 264 self.output_directory = dir_path 265 logging.info(f"Output direcotry set to: {self.output_directory}") 266 267 def parse_json_settings(self, settings_file): 268 """ 269 Parse the settings file. 270 """ 271 settings = get_settings(settings_file) 272 273 language = settings["general_settings"]["language"] 274 self.language_settings = settings["language_settings"].get(language, "EN") 275 self.chart_colors_settings = settings["chart_colors"] 276 self.planets_settings = settings["celestial_points"] 277 self.aspects_settings = settings["aspects"] 278 self.planet_in_zodiac_extra_points = settings["general_settings"]["planet_in_zodiac_extra_points"] 279 self.chart_settings = settings["chart_settings"] 280 281 def _draw_zodiac_circle_slices(self, r): 282 """ 283 Generate the SVG string representing the zodiac circle 284 with the 12 slices for each zodiac sign. 285 286 Args: 287 r (float): The radius of the zodiac slices. 288 289 Returns: 290 str: The SVG string representing the zodiac circle. 291 """ 292 293 output = "" 294 for i, zodiac_element in enumerate(self.zodiac): 295 output += draw_zodiac_slice( 296 c1=self.c1, 297 chart_type=self.chart_type, 298 seventh_house_degree_ut=self.user.houses_degree_ut[6], 299 num=i, 300 r=r, 301 style=f'fill:{self.chart_colors_settings[f"zodiac_bg_{i}"]}; fill-opacity: 0.5;', 302 type=zodiac_element["name"], 303 ) 304 305 return output 306 307 def _makeHouses(self, r): 308 path = "" 309 310 xr = 12 311 for i in range(xr): 312 # check transit 313 if self.chart_type == "Transit" or self.chart_type == "Synastry": 314 dropin = 160 315 roff = 72 316 t_roff = 36 317 else: 318 dropin = self.c3 319 roff = self.c1 320 321 # offset is negative desc houses_degree_ut[6] 322 offset = (int(self.user.houses_degree_ut[int(xr / 2)]) / -1) + int(self.user.houses_degree_ut[i]) 323 x1 = sliceToX(0, (r - dropin), offset) + dropin 324 y1 = sliceToY(0, (r - dropin), offset) + dropin 325 x2 = sliceToX(0, r - roff, offset) + roff 326 y2 = sliceToY(0, r - roff, offset) + roff 327 328 if i < (xr - 1): 329 text_offset = offset + int(degreeDiff(self.user.houses_degree_ut[(i + 1)], self.user.houses_degree_ut[i]) / 2) 330 else: 331 text_offset = offset + int(degreeDiff(self.user.houses_degree_ut[0], self.user.houses_degree_ut[(xr - 1)]) / 2) 332 333 # mc, asc, dsc, ic 334 if i == 0: 335 linecolor = self.planets_settings[12]["color"] 336 elif i == 9: 337 linecolor = self.planets_settings[13]["color"] 338 elif i == 6: 339 linecolor = self.planets_settings[14]["color"] 340 elif i == 3: 341 linecolor = self.planets_settings[15]["color"] 342 else: 343 linecolor = self.chart_colors_settings["houses_radix_line"] 344 345 # Transit houses lines. 346 if self.chart_type == "Transit" or self.chart_type == "Synastry": 347 # Degrees for point zero. 348 349 zeropoint = 360 - self.user.houses_degree_ut[6] 350 t_offset = zeropoint + self.t_user.houses_degree_ut[i] 351 if t_offset > 360: 352 t_offset = t_offset - 360 353 t_x1 = sliceToX(0, (r - t_roff), t_offset) + t_roff 354 t_y1 = sliceToY(0, (r - t_roff), t_offset) + t_roff 355 t_x2 = sliceToX(0, r, t_offset) 356 t_y2 = sliceToY(0, r, t_offset) 357 if i < 11: 358 t_text_offset = t_offset + int(degreeDiff(self.t_user.houses_degree_ut[(i + 1)], self.t_user.houses_degree_ut[i]) / 2) 359 else: 360 t_text_offset = t_offset + int(degreeDiff(self.t_user.houses_degree_ut[0], self.t_user.houses_degree_ut[11]) / 2) 361 # linecolor 362 if i == 0 or i == 9 or i == 6 or i == 3: 363 t_linecolor = linecolor 364 else: 365 t_linecolor = self.chart_colors_settings["houses_transit_line"] 366 xtext = sliceToX(0, (r - 8), t_text_offset) + 8 367 ytext = sliceToY(0, (r - 8), t_text_offset) + 8 368 369 if self.chart_type == "Transit": 370 path = path + '<text style="fill: #00f; fill-opacity: 0; font-size: 14px"><tspan x="' + str(xtext - 3) + '" y="' + str(ytext + 3) + '">' + str(i + 1) + "</tspan></text>" 371 path = f"{path}<line x1='{str(t_x1)}' y1='{str(t_y1)}' x2='{str(t_x2)}' y2='{str(t_y2)}' style='stroke: {t_linecolor}; stroke-width: 2px; stroke-opacity:0;'/>" 372 373 else: 374 path = path + '<text style="fill: #00f; fill-opacity: .4; font-size: 14px"><tspan x="' + str(xtext - 3) + '" y="' + str(ytext + 3) + '">' + str(i + 1) + "</tspan></text>" 375 path = f"{path}<line x1='{str(t_x1)}' y1='{str(t_y1)}' x2='{str(t_x2)}' y2='{str(t_y2)}' style='stroke: {t_linecolor}; stroke-width: 2px; stroke-opacity:.3;'/>" 376 377 378 # if transit 379 if self.chart_type == "Transit" or self.chart_type == "Synastry": 380 dropin = 84 381 elif self.chart_type == "ExternalNatal": 382 dropin = 100 383 # Natal 384 else: 385 dropin = 48 386 387 xtext = sliceToX(0, (r - dropin), text_offset) + dropin # was 132 388 ytext = sliceToY(0, (r - dropin), text_offset) + dropin # was 132 389 path = f'{path}<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {linecolor}; stroke-width: 2px; stroke-dasharray:3,2; stroke-opacity:.4;"/>' 390 path = path + '<text style="fill: #f00; fill-opacity: .6; font-size: 14px"><tspan x="' + str(xtext - 3) + '" y="' + str(ytext + 3) + '">' + str(i + 1) + "</tspan></text>" 391 392 return path 393 394 def _calculate_elements_points_from_planets(self): 395 """ 396 Calculate chart element points from a planet. 397 """ 398 399 for i in range(len(self.available_planets_setting)): 400 # element: get extra points if planet is in own zodiac sign. 401 related_zodiac_signs = self.available_planets_setting[i]["related_zodiac_signs"] 402 cz = self.points_sign[i] 403 extra_points = 0 404 if related_zodiac_signs != []: 405 for e in range(len(related_zodiac_signs)): 406 if int(related_zodiac_signs[e]) == int(cz): 407 extra_points = self.planet_in_zodiac_extra_points 408 409 ele = self.zodiac[self.points_sign[i]]["element"] 410 if ele == "fire": 411 self.fire = self.fire + self.available_planets_setting[i]["element_points"] + extra_points 412 413 elif ele == "earth": 414 self.earth = self.earth + self.available_planets_setting[i]["element_points"] + extra_points 415 416 elif ele == "air": 417 self.air = self.air + self.available_planets_setting[i]["element_points"] + extra_points 418 419 elif ele == "water": 420 self.water = self.water + self.available_planets_setting[i]["element_points"] + extra_points 421 422 def _make_planets(self, r): 423 planets_degut = {} 424 diff = range(len(self.available_planets_setting)) 425 426 for i in range(len(self.available_planets_setting)): 427 # list of planets sorted by degree 428 logging.debug(f"planet: {i}, degree: {self.points_deg_ut[i]}") 429 planets_degut[self.points_deg_ut[i]] = i 430 431 """ 432 FIXME: The planets_degut is a dictionary like: 433 {planet_degree: planet_index} 434 It should be replaced bu points_deg_ut 435 print(self.points_deg_ut) 436 print(planets_degut) 437 """ 438 439 output = "" 440 keys = list(planets_degut.keys()) 441 keys.sort() 442 switch = 0 443 444 planets_degrouped = {} 445 groups = [] 446 planets_by_pos = list(range(len(planets_degut))) 447 planet_drange = 3.4 448 # get groups closely together 449 group_open = False 450 for e in range(len(keys)): 451 i = planets_degut[keys[e]] 452 # get distances between planets 453 if e == 0: 454 prev = self.points_deg_ut[planets_degut[keys[-1]]] 455 next = self.points_deg_ut[planets_degut[keys[1]]] 456 elif e == (len(keys) - 1): 457 prev = self.points_deg_ut[planets_degut[keys[e - 1]]] 458 next = self.points_deg_ut[planets_degut[keys[0]]] 459 else: 460 prev = self.points_deg_ut[planets_degut[keys[e - 1]]] 461 next = self.points_deg_ut[planets_degut[keys[e + 1]]] 462 diffa = degreeDiff(prev, self.points_deg_ut[i]) 463 diffb = degreeDiff(next, self.points_deg_ut[i]) 464 planets_by_pos[e] = [i, diffa, diffb] 465 466 logging.debug(f'{self.available_planets_setting[i]["label"]}, {diffa}, {diffb}') 467 468 if diffb < planet_drange: 469 if group_open: 470 groups[-1].append([e, diffa, diffb, self.available_planets_setting[planets_degut[keys[e]]]["label"]]) 471 else: 472 group_open = True 473 groups.append([]) 474 groups[-1].append([e, diffa, diffb, self.available_planets_setting[planets_degut[keys[e]]]["label"]]) 475 else: 476 if group_open: 477 groups[-1].append([e, diffa, diffb, self.available_planets_setting[planets_degut[keys[e]]]["label"]]) 478 group_open = False 479 480 def zero(x): 481 return 0 482 483 planets_delta = list(map(zero, range(len(self.available_planets_setting)))) 484 485 # print groups 486 # print planets_by_pos 487 for a in range(len(groups)): 488 # Two grouped planets 489 if len(groups[a]) == 2: 490 next_to_a = groups[a][0][0] - 1 491 if groups[a][1][0] == (len(planets_by_pos) - 1): 492 next_to_b = 0 493 else: 494 next_to_b = groups[a][1][0] + 1 495 # if both planets have room 496 if (groups[a][0][1] > (2 * planet_drange)) & (groups[a][1][2] > (2 * planet_drange)): 497 planets_delta[groups[a][0][0]] = -(planet_drange - groups[a][0][2]) / 2 498 planets_delta[groups[a][1][0]] = +(planet_drange - groups[a][0][2]) / 2 499 # if planet a has room 500 elif groups[a][0][1] > (2 * planet_drange): 501 planets_delta[groups[a][0][0]] = -planet_drange 502 # if planet b has room 503 elif groups[a][1][2] > (2 * planet_drange): 504 planets_delta[groups[a][1][0]] = +planet_drange 505 506 # if planets next to a and b have room move them 507 elif (planets_by_pos[next_to_a][1] > (2.4 * planet_drange)) & (planets_by_pos[next_to_b][2] > (2.4 * planet_drange)): 508 planets_delta[(next_to_a)] = groups[a][0][1] - planet_drange * 2 509 planets_delta[groups[a][0][0]] = -planet_drange * 0.5 510 planets_delta[next_to_b] = -(groups[a][1][2] - planet_drange * 2) 511 planets_delta[groups[a][1][0]] = +planet_drange * 0.5 512 513 # if planet next to a has room move them 514 elif planets_by_pos[next_to_a][1] > (2 * planet_drange): 515 planets_delta[(next_to_a)] = groups[a][0][1] - planet_drange * 2.5 516 planets_delta[groups[a][0][0]] = -planet_drange * 1.2 517 518 # if planet next to b has room move them 519 elif planets_by_pos[next_to_b][2] > (2 * planet_drange): 520 planets_delta[next_to_b] = -(groups[a][1][2] - planet_drange * 2.5) 521 planets_delta[groups[a][1][0]] = +planet_drange * 1.2 522 523 # Three grouped planets or more 524 xl = len(groups[a]) 525 if xl >= 3: 526 available = groups[a][0][1] 527 for f in range(xl): 528 available += groups[a][f][2] 529 need = (3 * planet_drange) + (1.2 * (xl - 1) * planet_drange) 530 leftover = available - need 531 xa = groups[a][0][1] 532 xb = groups[a][(xl - 1)][2] 533 534 # center 535 if (xa > (need * 0.5)) & (xb > (need * 0.5)): 536 startA = xa - (need * 0.5) 537 # position relative to next planets 538 else: 539 startA = (leftover / (xa + xb)) * xa 540 startB = (leftover / (xa + xb)) * xb 541 542 if available > need: 543 planets_delta[groups[a][0][0]] = startA - groups[a][0][1] + (1.5 * planet_drange) 544 for f in range(xl - 1): 545 planets_delta[groups[a][(f + 1)][0]] = 1.2 * planet_drange + planets_delta[groups[a][f][0]] - groups[a][f][2] 546 547 for e in range(len(keys)): 548 i = planets_degut[keys[e]] 549 550 # coordinates 551 if self.chart_type == "Transit" or self.chart_type == "Synastry": 552 if 22 < i < 27: 553 rplanet = 76 554 elif switch == 1: 555 rplanet = 110 556 switch = 0 557 else: 558 rplanet = 130 559 switch = 1 560 else: 561 # if 22 < i < 27 it is asc,mc,dsc,ic (angles of chart) 562 # put on special line (rplanet is range from outer ring) 563 amin, bmin, cmin = 0, 0, 0 564 if self.chart_type == "ExternalNatal": 565 amin = 74 - 10 566 bmin = 94 - 10 567 cmin = 40 - 10 568 569 if 22 < i < 27: 570 rplanet = 40 - cmin 571 elif switch == 1: 572 rplanet = 74 - amin 573 switch = 0 574 else: 575 rplanet = 94 - bmin 576 switch = 1 577 578 rtext = 45 579 580 offset = (int(self.user.houses_degree_ut[6]) / -1) + int(self.points_deg_ut[i] + planets_delta[e]) 581 trueoffset = (int(self.user.houses_degree_ut[6]) / -1) + int(self.points_deg_ut[i]) 582 583 planet_x = sliceToX(0, (r - rplanet), offset) + rplanet 584 planet_y = sliceToY(0, (r - rplanet), offset) + rplanet 585 if self.chart_type == "Transit" or self.chart_type == "Synastry": 586 scale = 0.8 587 588 elif self.chart_type == "ExternalNatal": 589 scale = 0.8 590 # line1 591 x1 = sliceToX(0, (r - self.c3), trueoffset) + self.c3 592 y1 = sliceToY(0, (r - self.c3), trueoffset) + self.c3 593 x2 = sliceToX(0, (r - rplanet - 30), trueoffset) + rplanet + 30 594 y2 = sliceToY(0, (r - rplanet - 30), trueoffset) + rplanet + 30 595 color = self.available_planets_setting[i]["color"] 596 output += ( 597 '<line x1="%s" y1="%s" x2="%s" y2="%s" style="stroke-width:1px;stroke:%s;stroke-opacity:.3;"/>\n' 598 % (x1, y1, x2, y2, color) 599 ) 600 # line2 601 x1 = sliceToX(0, (r - rplanet - 30), trueoffset) + rplanet + 30 602 y1 = sliceToY(0, (r - rplanet - 30), trueoffset) + rplanet + 30 603 x2 = sliceToX(0, (r - rplanet - 10), offset) + rplanet + 10 604 y2 = sliceToY(0, (r - rplanet - 10), offset) + rplanet + 10 605 output += ( 606 '<line x1="%s" y1="%s" x2="%s" y2="%s" style="stroke-width:1px;stroke:%s;stroke-opacity:.5;"/>\n' 607 % (x1, y1, x2, y2, color) 608 ) 609 610 else: 611 scale = 1 612 # output planet 613 output += f'<g transform="translate(-{12 * scale},-{12 * scale})"><g transform="scale({scale})"><use x="{planet_x * (1/scale)}" y="{planet_y * (1/scale)}" xlink:href="#{self.available_planets_setting[i]["name"]}" /></g></g>' 614 615 # make transit degut and display planets 616 if self.chart_type == "Transit" or self.chart_type == "Synastry": 617 group_offset = {} 618 t_planets_degut = {} 619 list_range = len(self.available_planets_setting) 620 621 for i in range(list_range): 622 if self.chart_type == "Transit" and self.available_planets_setting[i]['name'] in self.transit_ring_exclude_points_names: 623 continue 624 625 group_offset[i] = 0 626 t_planets_degut[self.t_points_deg_ut[i]] = i 627 628 t_keys = list(t_planets_degut.keys()) 629 t_keys.sort() 630 631 # grab closely grouped planets 632 groups = [] 633 in_group = False 634 for e in range(len(t_keys)): 635 i_a = t_planets_degut[t_keys[e]] 636 if e == (len(t_keys) - 1): 637 i_b = t_planets_degut[t_keys[0]] 638 else: 639 i_b = t_planets_degut[t_keys[e + 1]] 640 641 a = self.t_points_deg_ut[i_a] 642 b = self.t_points_deg_ut[i_b] 643 diff = degreeDiff(a, b) 644 if diff <= 2.5: 645 if in_group: 646 groups[-1].append(i_b) 647 else: 648 groups.append([i_a]) 649 groups[-1].append(i_b) 650 in_group = True 651 else: 652 in_group = False 653 # loop groups and set degrees display adjustment 654 for i in range(len(groups)): 655 if len(groups[i]) == 2: 656 group_offset[groups[i][0]] = -1.0 657 group_offset[groups[i][1]] = 1.0 658 elif len(groups[i]) == 3: 659 group_offset[groups[i][0]] = -1.5 660 group_offset[groups[i][1]] = 0 661 group_offset[groups[i][2]] = 1.5 662 elif len(groups[i]) == 4: 663 group_offset[groups[i][0]] = -2.0 664 group_offset[groups[i][1]] = -1.0 665 group_offset[groups[i][2]] = 1.0 666 group_offset[groups[i][3]] = 2.0 667 668 switch = 0 669 670 # Transit planets loop 671 for e in range(len(t_keys)): 672 if self.chart_type == "Transit" and self.available_planets_setting[e]["name"] in self.transit_ring_exclude_points_names: 673 continue 674 675 i = t_planets_degut[t_keys[e]] 676 677 if 22 < i < 27: 678 rplanet = 9 679 elif switch == 1: 680 rplanet = 18 681 switch = 0 682 else: 683 rplanet = 26 684 switch = 1 685 686 # Transit planet name 687 zeropoint = 360 - self.user.houses_degree_ut[6] 688 t_offset = zeropoint + self.t_points_deg_ut[i] 689 if t_offset > 360: 690 t_offset = t_offset - 360 691 planet_x = sliceToX(0, (r - rplanet), t_offset) + rplanet 692 planet_y = sliceToY(0, (r - rplanet), t_offset) + rplanet 693 output += f'<g class="transit-planet-name" transform="translate(-6,-6)"><g transform="scale(0.5)"><use x="{planet_x*2}" y="{planet_y*2}" xlink:href="#{self.available_planets_setting[i]["name"]}" /></g></g>' 694 695 # Transit planet line 696 x1 = sliceToX(0, r + 3, t_offset) - 3 697 y1 = sliceToY(0, r + 3, t_offset) - 3 698 x2 = sliceToX(0, r - 3, t_offset) + 3 699 y2 = sliceToY(0, r - 3, t_offset) + 3 700 output += f'<line class="transit-planet-line" x1="{str(x1)}" y1="{str(y1)}" x2="{str(x2)}" y2="{str(y2)}" style="stroke: {self.available_planets_setting[i]["color"]}; stroke-width: 1px; stroke-opacity:.8;"/>' 701 702 # transit planet degree text 703 rotate = self.user.houses_degree_ut[0] - self.t_points_deg_ut[i] 704 textanchor = "end" 705 t_offset += group_offset[i] 706 rtext = -3.0 707 708 if -90 > rotate > -270: 709 rotate = rotate + 180.0 710 textanchor = "start" 711 if 270 > rotate > 90: 712 rotate = rotate - 180.0 713 textanchor = "start" 714 715 if textanchor == "end": 716 xo = 1 717 else: 718 xo = -1 719 deg_x = sliceToX(0, (r - rtext), t_offset + xo) + rtext 720 deg_y = sliceToY(0, (r - rtext), t_offset + xo) + rtext 721 degree = int(t_offset) 722 output += f'<g transform="translate({deg_x},{deg_y})">' 723 output += f'<text transform="rotate({rotate})" text-anchor="{textanchor}' 724 output += f'" style="fill: {self.available_planets_setting[i]["color"]}; font-size: 10px;">{convert_decimal_to_degree_string(self.t_points_deg[i], type="1")}' 725 output += "</text></g>" 726 727 # check transit 728 if self.chart_type == "Transit" or self.chart_type == "Synastry": 729 dropin = 36 730 else: 731 dropin = 0 732 733 # planet line 734 x1 = sliceToX(0, r - (dropin + 3), offset) + (dropin + 3) 735 y1 = sliceToY(0, r - (dropin + 3), offset) + (dropin + 3) 736 x2 = sliceToX(0, (r - (dropin - 3)), offset) + (dropin - 3) 737 y2 = sliceToY(0, (r - (dropin - 3)), offset) + (dropin - 3) 738 739 output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {self.available_planets_setting[i]["color"]}; stroke-width: 2px; stroke-opacity:.6;"/>' 740 741 # check transit 742 if self.chart_type == "Transit" or self.chart_type == "Synastry": 743 dropin = 160 744 else: 745 dropin = 120 746 747 x1 = sliceToX(0, r - dropin, offset) + dropin 748 y1 = sliceToY(0, r - dropin, offset) + dropin 749 x2 = sliceToX(0, (r - (dropin - 3)), offset) + (dropin - 3) 750 y2 = sliceToY(0, (r - (dropin - 3)), offset) + (dropin - 3) 751 output += f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: {self.available_planets_setting[i]["color"]}; stroke-width: 2px; stroke-opacity:.6;"/>' 752 753 return output 754 755 def _makePatterns(self): 756 """ 757 * Stellium: At least four planets linked together in a series of continuous conjunctions. 758 * Grand trine: Three trine aspects together. 759 * Grand cross: Two pairs of opposing planets squared to each other. 760 * T-Square: Two planets in opposition squared to a third. 761 * Yod: Two qunicunxes together joined by a sextile. 762 """ 763 conj = {} # 0 764 opp = {} # 10 765 sq = {} # 5 766 tr = {} # 6 767 qc = {} # 9 768 sext = {} # 3 769 for i in range(len(self.available_planets_setting)): 770 a = self.points_deg_ut[i] 771 qc[i] = {} 772 sext[i] = {} 773 opp[i] = {} 774 sq[i] = {} 775 tr[i] = {} 776 conj[i] = {} 777 # skip some points 778 n = self.available_planets_setting[i]["name"] 779 if n == "earth" or n == "True_Node" or n == "osc. apogee" or n == "intp. apogee" or n == "intp. perigee": 780 continue 781 if n == "Dsc" or n == "Ic": 782 continue 783 for j in range(len(self.available_planets_setting)): 784 # skip some points 785 n = self.available_planets_setting[j]["name"] 786 if n == "earth" or n == "True_Node" or n == "osc. apogee" or n == "intp. apogee" or n == "intp. perigee": 787 continue 788 if n == "Dsc" or n == "Ic": 789 continue 790 b = self.points_deg_ut[j] 791 delta = float(degreeDiff(a, b)) 792 # check for opposition 793 xa = float(self.aspects_settings[10]["degree"]) - float(self.aspects_settings[10]["orb"]) 794 xb = float(self.aspects_settings[10]["degree"]) + float(self.aspects_settings[10]["orb"]) 795 if xa <= delta <= xb: 796 opp[i][j] = True 797 # check for conjunction 798 xa = float(self.aspects_settings[0]["degree"]) - float(self.aspects_settings[0]["orb"]) 799 xb = float(self.aspects_settings[0]["degree"]) + float(self.aspects_settings[0]["orb"]) 800 if xa <= delta <= xb: 801 conj[i][j] = True 802 # check for squares 803 xa = float(self.aspects_settings[5]["degree"]) - float(self.aspects_settings[5]["orb"]) 804 xb = float(self.aspects_settings[5]["degree"]) + float(self.aspects_settings[5]["orb"]) 805 if xa <= delta <= xb: 806 sq[i][j] = True 807 # check for qunicunxes 808 xa = float(self.aspects_settings[9]["degree"]) - float(self.aspects_settings[9]["orb"]) 809 xb = float(self.aspects_settings[9]["degree"]) + float(self.aspects_settings[9]["orb"]) 810 if xa <= delta <= xb: 811 qc[i][j] = True 812 # check for sextiles 813 xa = float(self.aspects_settings[3]["degree"]) - float(self.aspects_settings[3]["orb"]) 814 xb = float(self.aspects_settings[3]["degree"]) + float(self.aspects_settings[3]["orb"]) 815 if xa <= delta <= xb: 816 sext[i][j] = True 817 818 yot = {} 819 # check for double qunicunxes 820 for k, v in qc.items(): 821 if len(qc[k]) >= 2: 822 # check for sextile 823 for l, w in qc[k].items(): 824 for m, x in qc[k].items(): 825 if m in sext[l]: 826 if l > m: 827 yot["%s,%s,%s" % (k, m, l)] = [k, m, l] 828 else: 829 yot["%s,%s,%s" % (k, l, m)] = [k, l, m] 830 tsquare = {} 831 # check for opposition 832 for k, v in opp.items(): 833 if len(opp[k]) >= 1: 834 # check for square 835 for l, w in opp[k].items(): 836 for a, b in sq.items(): 837 if k in sq[a] and l in sq[a]: 838 logging.debug(f"Got tsquare {a} {k} {l}") 839 if k > l: 840 tsquare[f"{a},{l},{k}"] = f"{self.available_planets_setting[a]['label']} => {self.available_planets_setting[l]['label']}, {self.available_planets_setting[k]['label']}" 841 842 else: 843 tsquare[f"{a},{k},{l}"] = f"{self.available_planets_setting[a]['label']} => {self.available_planets_setting[k]['label']}, {self.available_planets_setting[l]['label']}" 844 845 stellium = {} 846 # check for 4 continuous conjunctions 847 for k, v in conj.items(): 848 if len(conj[k]) >= 1: 849 # first conjunction 850 for l, m in conj[k].items(): 851 if len(conj[l]) >= 1: 852 for n, o in conj[l].items(): 853 # skip 1st conj 854 if n == k: 855 continue 856 if len(conj[n]) >= 1: 857 # third conjunction 858 for p, q in conj[n].items(): 859 # skip first and second conj 860 if p == k or p == n: 861 continue 862 if len(conj[p]) >= 1: 863 # fourth conjunction 864 for r, s in conj[p].items(): 865 # skip conj 1,2,3 866 if r == k or r == n or r == p: 867 continue 868 869 l = [k, n, p, r] 870 l.sort() 871 stellium["%s %s %s %s" % (l[0], l[1], l[2], l[3])] = "%s %s %s %s" % ( 872 self.available_planets_setting[l[0]]["label"], 873 self.available_planets_setting[l[1]]["label"], 874 self.available_planets_setting[l[2]]["label"], 875 self.available_planets_setting[l[3]]["label"], 876 ) 877 # print yots 878 out = '<g transform="translate(-30,380)">' 879 if len(yot) >= 1: 880 y = 0 881 for k, v in yot.items(): 882 out += f'<text y="{y}" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 12px;">{"Yot"}</text>' 883 884 # first planet symbol 885 out += f'<g transform="translate(20,{y})">' 886 out += f'<use transform="scale(0.4)" x="0" y="-20" xlink:href="#{self.available_planets_setting[yot[k][0]]["name"]}" /></g>' 887 888 # second planet symbol 889 out += f'<g transform="translate(30,{y})">' 890 out += f'<use transform="scale(0.4)" x="0" y="-20" xlink:href="#{self.available_planets_setting[yot[k][1]]["name"]}" /></g>' 891 892 # third planet symbol 893 out += f'<g transform="translate(40,{y})">' 894 out += f'<use transform="scale(0.4)" x="0" y="-20" xlink:href="#{self.available_planets_setting[yot[k][2]]["name"]}" /></g>' 895 896 y = y + 14 897 # finalize 898 out += "</g>" 899 # return out 900 return "" 901 902 # Aspect and aspect grid functions for natal type charts. 903 def _makeAspects(self, r, ar): 904 out = "" 905 for element in self.aspects_list: 906 out += draw_aspect_line( 907 r=r, 908 ar=ar, 909 degA=element["p1_abs_pos"], 910 degB=element["p2_abs_pos"], 911 color=self.aspects_settings[element["aid"]]["color"], 912 seventh_house_degree_ut=self.user.seventh_house.abs_pos 913 ) 914 915 return out 916 917 def _makeAspectGrid(self, r): 918 out = "" 919 style = "stroke:%s; stroke-width: 1px; stroke-opacity:.6; fill:none" % (self.chart_colors_settings["paper_0"]) 920 xindent = 380 921 yindent = 468 922 box = 14 923 revr = list(range(len(self.available_planets_setting))) 924 revr.reverse() 925 counter = 0 926 for a in revr: 927 counter += 1 928 out += f'<rect x="{xindent}" y="{yindent}" width="{box}" height="{box}" style="{style}"/>' 929 out += f'<use transform="scale(0.4)" x="{(xindent+2)*2.5}" y="{(yindent+1)*2.5}" xlink:href="#{self.available_planets_setting[a]["name"]}" />' 930 931 xindent = xindent + box 932 yindent = yindent - box 933 revr2 = list(range(a)) 934 revr2.reverse() 935 xorb = xindent 936 yorb = yindent + box 937 for b in revr2: 938 out += f'<rect x="{xorb}" y="{yorb}" width="{box}" height="{box}" style="{style}"/>' 939 940 xorb = xorb + box 941 for element in self.aspects_list: 942 if (element["p1"] == a and element["p2"] == b) or (element["p1"] == b and element["p2"] == a): 943 out += f'<use x="{xorb-box+1}" y="{yorb+1}" xlink:href="#orb{element["aspect_degrees"]}" />' 944 945 return out 946 947 # Aspect and aspect grid functions for transit type charts 948 def _makeAspectsTransit(self, r, ar): 949 out = "" 950 951 self.aspects_list = SynastryAspects(self.user, self.t_user, new_settings_file=self.new_settings_file).relevant_aspects 952 953 for element in self.aspects_list: 954 out += draw_aspect_line( 955 r=r, 956 ar=ar, 957 degA=element["p1_abs_pos"], 958 degB=element["p2_abs_pos"], 959 color=self.aspects_settings[element["aid"]]["color"], 960 seventh_house_degree_ut=self.user.seventh_house.abs_pos 961 ) 962 963 return out 964 965 def _makeAspectTransitGrid(self, r): 966 out = '<g transform="translate(500,310)">' 967 out += f'<text y="-15" x="0" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 14px;">{self.language_settings["aspects"]}:</text>' 968 969 line = 0 970 nl = 0 971 972 for i in range(len(self.aspects_list)): 973 if i == 12: 974 nl = 100 975 976 line = 0 977 978 elif i == 24: 979 nl = 200 980 981 line = 0 982 983 elif i == 36: 984 nl = 300 985 986 line = 0 987 988 elif i == 48: 989 nl = 400 990 991 # When there are more than 60 aspects, the text is moved up 992 if len(self.aspects_list) > 60: 993 line = -1 * (len(self.aspects_list) - 60) * 14 994 else: 995 line = 0 996 997 out += f'<g transform="translate({nl},{line})">' 998 999 # first planet symbol 1000 out += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{self.planets_settings[self.aspects_list[i]["p1"]]["name"]}" />' 1001 1002 # aspect symbol 1003 out += f'<use x="15" y="0" xlink:href="#orb{self.aspects_settings[self.aspects_list[i]["aid"]]["degree"]}" />' 1004 1005 # second planet symbol 1006 out += '<g transform="translate(30,0)">' 1007 out += '<use transform="scale(0.4)" x="0" y="3" xlink:href="#%s" />' % (self.planets_settings[self.aspects_list[i]["p2"]]["name"]) 1008 1009 out += "</g>" 1010 # difference in degrees 1011 out += f'<text y="8" x="45" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{convert_decimal_to_degree_string(self.aspects_list[i]["orbit"])}</text>' 1012 # line 1013 out += "</g>" 1014 line = line + 14 1015 out += "</g>" 1016 return out 1017 1018 def _makePlanetGrid(self): 1019 li = 10 1020 offset = 0 1021 1022 out = '<g transform="translate(510,-20)">' 1023 out += '<g transform="translate(140, -15)">' 1024 out += f'<text text-anchor="end" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 14px;">{self.language_settings["planets_and_house"]} {self.user.name}:</text>' 1025 out += "</g>" 1026 1027 end_of_line = None 1028 for i in range(len(self.available_planets_setting)): 1029 offset_between_lines = 14 1030 end_of_line = "</g>" 1031 1032 # Guarda qui !! 1033 if i == 27: 1034 li = 10 1035 offset = -120 1036 1037 # start of line 1038 out += f'<g transform="translate({offset},{li})">' 1039 1040 # planet text 1041 out += f'<text text-anchor="end" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{self.language_settings["celestial_points"][self.available_planets_setting[i]["label"]]}</text>' 1042 1043 # planet symbol 1044 out += f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{self.available_planets_setting[i]["name"]}" /></g>' 1045 1046 # planet degree 1047 out += f'<text text-anchor="start" x="19" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{convert_decimal_to_degree_string(self.points_deg[i])}</text>' 1048 1049 # zodiac 1050 out += f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{self.zodiac[self.points_sign[i]]["name"]}" /></g>' 1051 1052 # planet retrograde 1053 if self.points_retrograde[i]: 1054 out += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>' 1055 1056 # end of line 1057 out += end_of_line 1058 1059 li = li + offset_between_lines 1060 1061 if self.chart_type == "Transit" or self.chart_type == "Synastry": 1062 if self.chart_type == "Transit": 1063 out += '<g transform="translate(320, -15)">' 1064 out += f'<text text-anchor="end" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 14px;">{self.t_name}:</text>' 1065 else: 1066 out += '<g transform="translate(380, -15)">' 1067 out += f'<text text-anchor="end" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 14px;">{self.language_settings["planets_and_house"]} {self.t_user.name}:</text>' 1068 1069 out += end_of_line 1070 1071 t_li = 10 1072 t_offset = 250 1073 1074 for i in range(len(self.available_planets_setting)): 1075 if i == 27: 1076 t_li = 10 1077 t_offset = -120 1078 1079 # start of line 1080 out += f'<g transform="translate({t_offset},{t_li})">' 1081 1082 # planet text 1083 out += f'<text text-anchor="end" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{self.language_settings["celestial_points"][self.available_planets_setting[i]["label"]]}</text>' 1084 # planet symbol 1085 out += f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{self.available_planets_setting[i]["name"]}" /></g>' 1086 # planet degree 1087 out += f'<text text-anchor="start" x="19" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{convert_decimal_to_degree_string(self.t_points_deg[i])}</text>' 1088 # zodiac 1089 out += f'<g transform="translate(60,-8)"><use transform="scale(0.3)" xlink:href="#{self.zodiac[self.t_points_sign[i]]["name"]}" /></g>' 1090 1091 # planet retrograde 1092 if self.t_points_retrograde[i]: 1093 out += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>' 1094 1095 # end of line 1096 out += end_of_line 1097 1098 t_li = t_li + offset_between_lines 1099 1100 if end_of_line is None: 1101 raise KerykeionException("End of line not found") 1102 1103 out += end_of_line 1104 return out 1105 1106 def _draw_house_grid(self): 1107 """ 1108 Generate SVG code for a grid of astrological houses. 1109 1110 Returns: 1111 str: The SVG code for the grid of houses. 1112 """ 1113 out = '<g transform="translate(610,-20)">' 1114 1115 li = 10 1116 for i in range(12): 1117 if i < 9: 1118 cusp = "  " + str(i + 1) 1119 else: 1120 cusp = str(i + 1) 1121 out += f'<g transform="translate(0,{li})">' 1122 out += f'<text text-anchor="end" x="40" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{self.language_settings["cusp"]} {cusp}:</text>' 1123 out += f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{self.zodiac[self.houses_sign_graph[i]]["name"]}" /></g>' 1124 out += f'<text x="53" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;"> {convert_decimal_to_degree_string(self.user.houses_list[i]["position"])}</text>' 1125 out += "</g>" 1126 li = li + 14 1127 1128 out += "</g>" 1129 1130 if self.chart_type == "Synastry": 1131 out += '<!-- Synastry Houses -->' 1132 out += '<g transform="translate(850, -20)">' 1133 li = 10 1134 1135 for i in range(12): 1136 if i < 9: 1137 cusp = "  " + str(i + 1) 1138 else: 1139 cusp = str(i + 1) 1140 out += '<g transform="translate(0,' + str(li) + ')">' 1141 out += f'<text text-anchor="end" x="40" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;">{self.language_settings["cusp"]} {cusp}:</text>' 1142 out += f'<g transform="translate(40,-8)"><use transform="scale(0.3)" xlink:href="#{self.zodiac[self.t_houses_sign_graph[i]]["name"]}" /></g>' 1143 out += f'<text x="53" style="fill:{self.chart_colors_settings["paper_0"]}; font-size: 10px;"> {convert_decimal_to_degree_string(self.t_user.houses_list[i]["position"])}</text>' 1144 out += "</g>" 1145 li = li + 14 1146 out += "</g>" 1147 1148 return out 1149 1150 def _createTemplateDictionary(self) -> ChartTemplateDictionary: 1151 # self.chart_type = "Transit" 1152 # empty element points 1153 self.fire = 0.0 1154 self.earth = 0.0 1155 self.air = 0.0 1156 self.water = 0.0 1157 1158 # Calculate the elements points 1159 self._calculate_elements_points_from_planets() 1160 1161 # Viewbox and sizing 1162 svgHeight = "100%" 1163 svgWidth = "100%" 1164 rotate = "0" 1165 1166 # To increase the size of the chart, change the viewbox 1167 if self.chart_type == "Natal" or self.chart_type == "ExternalNatal": 1168 viewbox = self.chart_settings["basic_chart_viewBox"] 1169 else: 1170 viewbox = self.chart_settings["wide_chart_viewBox"] 1171 1172 # template dictionary 1173 td: ChartTemplateDictionary = dict() # type: ignore 1174 r = 240 1175 1176 if self.chart_type == "ExternalNatal": 1177 self.c1 = 56 1178 self.c2 = 92 1179 self.c3 = 112 1180 else: 1181 self.c1 = 0 1182 self.c2 = 36 1183 self.c3 = 120 1184 1185 # transit 1186 if self.chart_type == "Transit" or self.chart_type == "Synastry": 1187 td["transitRing"] = draw_transit_ring(r, self.chart_colors_settings["paper_1"], self.chart_colors_settings["zodiac_transit_ring_3"]) 1188 td["degreeRing"] = draw_transit_ring_degree_steps(r, self.user.seventh_house.abs_pos) 1189 1190 # circles 1191 td["first_circle"] = draw_first_circle(r, self.chart_colors_settings["zodiac_transit_ring_2"], self.chart_type) 1192 td["second_circle"] = draw_second_circle(r, self.chart_colors_settings['zodiac_transit_ring_1'], self.chart_colors_settings['paper_1'], self.chart_type) 1193 1194 td["c3"] = 'cx="' + str(r) + '" cy="' + str(r) + '" r="' + str(r - 160) + '"' 1195 td["c3style"] = f"fill: {self.chart_colors_settings['paper_1']}; fill-opacity:.8; stroke: {self.chart_colors_settings['zodiac_transit_ring_0']}; stroke-width: 1px" 1196 1197 td["makeAspects"] = self._makeAspectsTransit(r, (r - 160)) 1198 td["makeAspectGrid"] = self._makeAspectTransitGrid(r) 1199 td["makePatterns"] = "" 1200 else: 1201 td["transitRing"] = "" 1202 td["degreeRing"] = draw_degree_ring(r, self.c1, self.user.seventh_house.abs_pos, self.chart_colors_settings["paper_0"]) 1203 1204 td['first_circle'] = draw_first_circle(r, self.chart_colors_settings["zodiac_radix_ring_2"], self.chart_type, self.c1) 1205 1206 td["second_circle"] = draw_second_circle(r, self.chart_colors_settings["zodiac_radix_ring_1"], self.chart_colors_settings["paper_1"], self.chart_type, self.c2) 1207 1208 td["c3"] = f'cx="{r}" cy="{r}" r="{r - self.c3}"' 1209 td["c3style"] = f'fill: {self.chart_colors_settings["paper_1"]}; fill-opacity:.8; stroke: {self.chart_colors_settings["zodiac_radix_ring_0"]}; stroke-width: 1px' 1210 1211 td["makeAspects"] = self._makeAspects(r, (r - self.c3)) 1212 td["makeAspectGrid"] = self._makeAspectGrid(r) 1213 td["makePatterns"] = self._makePatterns() 1214 1215 td["chart_height"] = self.height 1216 td["chart_width"] = self.width 1217 td["circleX"] = str(0) 1218 td["circleY"] = str(0) 1219 td["svgWidth"] = str(svgWidth) 1220 td["svgHeight"] = str(svgHeight) 1221 td["viewbox"] = viewbox 1222 1223 # Chart Title 1224 if self.chart_type == "Synastry": 1225 td["stringTitle"] = f"{self.user.name} {self.language_settings['and_word']} {self.t_user.name}" 1226 1227 elif self.chart_type == "Transit": 1228 td["stringTitle"] = f"{self.language_settings['transits']} {self.t_user.day}/{self.t_user.month}/{self.t_user.year}" 1229 1230 else: 1231 td["stringTitle"] = self.user.name 1232 1233 # Chart Name 1234 if self.chart_type == "Synastry" or self.chart_type == "Transit": 1235 td["stringName"] = f"{self.user.name}:" 1236 else: 1237 td["stringName"] = f'{self.language_settings["info"]}:' 1238 1239 # Bottom Left Corner 1240 if self.chart_type == "Natal" or self.chart_type == "ExternalNatal" or self.chart_type == "Synastry": 1241 td["bottomLeft0"] = f"{self.user.zodiac_type if self.user.zodiac_type == 'Tropic' else self.user.zodiac_type + ' ' + self.user.sidereal_mode}" 1242 td["bottomLeft1"] = f"{self.user.houses_system_name}" 1243 td["bottomLeft2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get("day", "Day")} {self.user.lunar_phase.get("moon_phase", "")}' 1244 td["bottomLeft3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.user.lunar_phase.moon_phase_name}' 1245 td["bottomLeft4"] = f'{self.user.perspective_type}' 1246 1247 else: 1248 td["bottomLeft0"] = f"{self.user.zodiac_type if self.user.zodiac_type == 'Tropic' else self.user.zodiac_type + ' ' + self.user.sidereal_mode}" 1249 td["bottomLeft1"] = f"{self.user.houses_system_name}" 1250 td["bottomLeft2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get("day", "Day")} {self.t_user.lunar_phase.get("moon_phase", "")}' 1251 td["bottomLeft3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.t_user.lunar_phase.moon_phase_name}' 1252 td["bottomLeft4"] = f'{self.t_user.perspective_type}' 1253 1254 # lunar phase 1255 deg = self.user.lunar_phase["degrees_between_s_m"] 1256 1257 lffg = None 1258 lfbg = None 1259 lfcx = None 1260 lfr = None 1261 1262 if deg < 90.0: 1263 maxr = deg 1264 if deg > 80.0: 1265 maxr = maxr * maxr 1266 lfcx = 20.0 + (deg / 90.0) * (maxr + 10.0) 1267 lfr = 10.0 + (deg / 90.0) * maxr 1268 lffg = self.chart_colors_settings["lunar_phase_0"] 1269 lfbg = self.chart_colors_settings["lunar_phase_1"] 1270 1271 elif deg < 180.0: 1272 maxr = 180.0 - deg 1273 if deg < 100.0: 1274 maxr = maxr * maxr 1275 lfcx = 20.0 + ((deg - 90.0) / 90.0 * (maxr + 10.0)) - (maxr + 10.0) 1276 lfr = 10.0 + maxr - ((deg - 90.0) / 90.0 * maxr) 1277 lffg = self.chart_colors_settings["lunar_phase_1"] 1278 lfbg = self.chart_colors_settings["lunar_phase_0"] 1279 1280 elif deg < 270.0: 1281 maxr = deg - 180.0 1282 if deg > 260.0: 1283 maxr = maxr * maxr 1284 lfcx = 20.0 + ((deg - 180.0) / 90.0 * (maxr + 10.0)) 1285 lfr = 10.0 + ((deg - 180.0) / 90.0 * maxr) 1286 lffg, lfbg = self.chart_colors_settings["lunar_phase_1"], self.chart_colors_settings["lunar_phase_0"] 1287 1288 elif deg < 361: 1289 maxr = 360.0 - deg 1290 if deg < 280.0: 1291 maxr = maxr * maxr 1292 lfcx = 20.0 + ((deg - 270.0) / 90.0 * (maxr + 10.0)) - (maxr + 10.0) 1293 lfr = 10.0 + maxr - ((deg - 270.0) / 90.0 * maxr) 1294 lffg, lfbg = self.chart_colors_settings["lunar_phase_0"], self.chart_colors_settings["lunar_phase_1"] 1295 1296 if lffg is None or lfbg is None or lfcx is None or lfr is None: 1297 raise KerykeionException("Lunar phase error") 1298 1299 td["lunar_phase_fg"] = lffg 1300 td["lunar_phase_bg"] = lfbg 1301 td["lunar_phase_cx"] = lfcx 1302 td["lunar_phase_r"] = lfr 1303 td["lunar_phase_outline"] = self.chart_colors_settings["lunar_phase_2"] 1304 1305 # rotation based on latitude 1306 td["lunar_phase_rotate"] = -90.0 - self.geolat 1307 1308 # stringlocation 1309 if len(self.location) > 35: 1310 split = self.location.split(",") 1311 if len(split) > 1: 1312 td["stringLocation"] = split[0] + ", " + split[-1] 1313 if len(td["stringLocation"]) > 35: 1314 td["stringLocation"] = td["stringLocation"][:35] + "..." 1315 else: 1316 td["stringLocation"] = self.location[:35] + "..." 1317 else: 1318 td["stringLocation"] = self.location 1319 1320 if self.chart_type == "Synastry": 1321 td["stringLat"] = f"{self.t_user.name}: " 1322 td["stringLon"] = self.t_user.city 1323 td["stringPosition"] = f"{self.t_user.year}-{self.t_user.month}-{self.t_user.day} {self.t_user.hour:02d}:{self.t_user.minute:02d}" 1324 1325 else: 1326 latitude_string = convert_latitude_coordinate_to_string( 1327 self.geolat, 1328 self.language_settings['north'], 1329 self.language_settings['south'] 1330 ) 1331 longitude_string = convert_longitude_coordinate_to_string( 1332 self.geolon, 1333 self.language_settings['east'], 1334 self.language_settings['west'] 1335 ) 1336 1337 td["stringLat"] = f"{self.language_settings['latitude']}: {latitude_string}" 1338 td["stringLon"] = f"{self.language_settings['longitude']}: {longitude_string}" 1339 td["stringPosition"] = f"{self.language_settings['type']}: {self.chart_type}" 1340 1341 # paper_color_X 1342 td["paper_color_0"] = self.chart_colors_settings["paper_0"] 1343 td["paper_color_1"] = self.chart_colors_settings["paper_1"] 1344 1345 # planets_color_X 1346 for i in range(len(self.planets_settings)): 1347 planet_id = self.planets_settings[i]["id"] 1348 td[f"planets_color_{planet_id}"] = self.planets_settings[i]["color"] 1349 1350 # zodiac_color_X 1351 for i in range(12): 1352 td[f"zodiac_color_{i}"] = self.chart_colors_settings[f"zodiac_icon_{i}"] 1353 1354 # orb_color_X 1355 for i in range(len(self.aspects_settings)): 1356 td[f"orb_color_{self.aspects_settings[i]['degree']}"] = self.aspects_settings[i]['color'] 1357 1358 # config 1359 td["cfgZoom"] = str(self.zoom) 1360 td["cfgRotate"] = rotate 1361 1362 # --- 1363 # Drawing Functions 1364 #--- 1365 1366 td["makeZodiac"] = self._draw_zodiac_circle_slices(r) 1367 td["makeHousesGrid"] = self._draw_house_grid() 1368 # TODO: Add the rest of the functions 1369 td["makeHouses"] = self._makeHouses(r) 1370 td["makePlanets"] = self._make_planets(r) 1371 td["elements_percentages"] = draw_elements_percentages( 1372 self.language_settings['fire'], 1373 self.fire, 1374 self.language_settings['earth'], 1375 self.earth, 1376 self.language_settings['air'], 1377 self.air, 1378 self.language_settings['water'], 1379 self.water, 1380 ) 1381 td["makePlanetGrid"] = self._makePlanetGrid() 1382 1383 # Date time String 1384 dt = datetime.fromisoformat(self.user.iso_formatted_local_datetime) 1385 custom_format = dt.strftime('%Y-%-m-%-d %H:%M [%z]') # Note the use of '-' to remove leading zeros 1386 custom_format = custom_format[:-3] + ':' + custom_format[-3:] 1387 td["stringDateTime"] = f"{custom_format}" 1388 1389 return td 1390 1391 def makeTemplate(self, minify: bool = False) -> str: 1392 """Creates the template for the SVG file""" 1393 td = self._createTemplateDictionary() 1394 1395 # read template 1396 with open(self.xml_svg, "r", encoding="utf-8", errors="ignore") as f: 1397 template = Template(f.read()).substitute(td) 1398 1399 # return filename 1400 1401 logging.debug(f"Template dictionary keys: {td.keys()}") 1402 1403 self._createTemplateDictionary() 1404 1405 if minify: 1406 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 1407 1408 else: 1409 template = template.replace('"', "'") 1410 1411 return template 1412 1413 def makeSVG(self, minify: bool = False): 1414 """Prints out the SVG file in the specifide folder""" 1415 1416 if not (self.template): 1417 self.template = self.makeTemplate(minify) 1418 1419 self.chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg" 1420 1421 with open(self.chartname, "w", encoding="utf-8", errors="ignore") as output_file: 1422 output_file.write(self.template) 1423 1424 logging.info(f"SVG Generated Correctly in: {self.chartname}")
Creates the instance that can generate the chart with the function makeSVG().
Parameters: - first_obj: First kerykeion object - chart_type: Natal, ExternalNatal, Transit, Synastry (Default: Type="Natal"). - second_obj: Second kerykeion object (Not required if type is Natal) - new_output_directory: Set the output directory (default: home directory). - new_settings_file: Set the settings file (default: kr.config.json). In the settings file you can set the language, colors, planets, aspects, etc.
KerykeionChartSVG( first_obj: kerykeion.astrological_subject.AstrologicalSubject, chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit'] = 'Natal', second_obj: Optional[kerykeion.astrological_subject.AstrologicalSubject] = None, new_output_directory: Optional[str] = None, new_settings_file: Optional[pathlib.Path] = None)
106 def __init__( 107 self, 108 first_obj: AstrologicalSubject, 109 chart_type: ChartType = "Natal", 110 second_obj: Union[AstrologicalSubject, None] = None, 111 new_output_directory: Union[str, None] = None, 112 new_settings_file: Union[Path, None] = None, 113 ): 114 # Directories: 115 DATA_DIR = Path(__file__).parent 116 self.homedir = Path.home() 117 self.new_settings_file = new_settings_file 118 119 if new_output_directory: 120 self.output_directory = Path(new_output_directory) 121 else: 122 self.output_directory = self.homedir 123 124 self.xml_svg = DATA_DIR / "templates/chart.xml" 125 126 self.parse_json_settings(new_settings_file) 127 self.chart_type = chart_type 128 129 # Kerykeion instance 130 self.user = first_obj 131 132 self.available_planets_setting = [] 133 for body in self.planets_settings: 134 if body['is_active'] == False: 135 continue 136 137 self.available_planets_setting.append(body) 138 139 # House cusp points are excluded from the transit ring. 140 self.transit_ring_exclude_points_names = [ 141 "First_House", 142 "Second_House", 143 "Third_House", 144 "Fourth_House", 145 "Fifth_House", 146 "Sixth_House", 147 "Seventh_House", 148 "Eighth_House", 149 "Ninth_House", 150 "Tenth_House", 151 "Eleventh_House", 152 "Twelfth_House" 153 ] 154 155 # Available bodies 156 available_celestial_points = [] 157 for body in self.available_planets_setting: 158 available_celestial_points.append(body["name"].lower()) 159 160 # Make a list for the absolute degrees of the points of the graphic. 161 self.points_deg_ut = [] 162 for planet in available_celestial_points: 163 self.points_deg_ut.append(self.user.get(planet).abs_pos) 164 165 # Make a list of the relative degrees of the points in the graphic. 166 self.points_deg = [] 167 for planet in available_celestial_points: 168 self.points_deg.append(self.user.get(planet).position) 169 170 # Make list of the points sign 171 self.points_sign = [] 172 for planet in available_celestial_points: 173 self.points_sign.append(self.user.get(planet).sign_num) 174 175 # Make a list of points if they are retrograde or not. 176 self.points_retrograde = [] 177 for planet in available_celestial_points: 178 self.points_retrograde.append(self.user.get(planet).retrograde) 179 180 # Makes the sign number list. 181 182 self.houses_sign_graph = [] 183 for h in self.user.houses_list: 184 self.houses_sign_graph.append(h["sign_num"]) 185 186 if self.chart_type == "Natal" or self.chart_type == "ExternalNatal": 187 natal_aspects_instance = NatalAspects(self.user, new_settings_file=self.new_settings_file) 188 self.aspects_list = natal_aspects_instance.relevant_aspects 189 190 # TODO: If not second should exit 191 if self.chart_type == "Transit" or self.chart_type == "Synastry": 192 if not second_obj: 193 raise KerykeionException("Second object is required for Transit or Synastry charts.") 194 195 # Kerykeion instance 196 self.t_user = second_obj 197 198 # Make a list for the absolute degrees of the points of the graphic. 199 self.t_points_deg_ut = [] 200 for planet in available_celestial_points: 201 self.t_points_deg_ut.append(self.t_user.get(planet).abs_pos) 202 203 # Make a list of the relative degrees of the points in the graphic. 204 self.t_points_deg = [] 205 for planet in available_celestial_points: 206 self.t_points_deg.append(self.t_user.get(planet).position) 207 208 # Make list of the poits sign. 209 self.t_points_sign = [] 210 for planet in available_celestial_points: 211 self.t_points_sign.append(self.t_user.get(planet).sign_num) 212 213 # Make a list of poits if they are retrograde or not. 214 self.t_points_retrograde = [] 215 for planet in available_celestial_points: 216 self.t_points_retrograde.append(self.t_user.get(planet).retrograde) 217 218 self.t_houses_sign_graph = [] 219 for h in self.t_user.houses_list: 220 self.t_houses_sign_graph.append(h["sign_num"]) 221 222 # screen size 223 self.height = self._DEFAULT_HEIGHT 224 if self.chart_type == "Synastry" or self.chart_type == "Transit": 225 self.width = self._DEFAULT_FULL_WIDTH 226 else: 227 self.width = self._DEFAULT_NATAL_WIDTH 228 229 # default location 230 self.location = self.user.city 231 self.geolat = self.user.lat 232 self.geolon = self.user.lng 233 234 logging.info(f"{self.user.name} birth location: {self.location}, {self.geolat}, {self.geolon}") 235 236 if self.chart_type == "Transit": 237 self.t_name = self.language_settings["transit_name"] 238 239 # configuration 240 # ZOOM 1 = 100% 241 self.zoom = 1 242 243 self.zodiac = ( 244 {"name": "aries", "element": "fire"}, 245 {"name": "taurus", "element": "earth"}, 246 {"name": "gemini", "element": "air"}, 247 {"name": "cancer", "element": "water"}, 248 {"name": "leo", "element": "fire"}, 249 {"name": "virgo", "element": "earth"}, 250 {"name": "libra", "element": "air"}, 251 {"name": "scorpio", "element": "water"}, 252 {"name": "sagittarius", "element": "fire"}, 253 {"name": "capricorn", "element": "earth"}, 254 {"name": "aquarius", "element": "air"}, 255 {"name": "pisces", "element": "water"}, 256 ) 257 258 self.template = None
second_obj: Optional[kerykeion.astrological_subject.AstrologicalSubject]
available_planets_setting: List[kerykeion.kr_types.settings_models.KerykeionSettingsCelestialPointModel]
def
set_output_directory(self, dir_path: pathlib.Path) -> None:
260 def set_output_directory(self, dir_path: Path) -> None: 261 """ 262 Sets the output direcotry and returns it's path. 263 """ 264 self.output_directory = dir_path 265 logging.info(f"Output direcotry set to: {self.output_directory}")
Sets the output direcotry and returns it's path.
def
parse_json_settings(self, settings_file):
267 def parse_json_settings(self, settings_file): 268 """ 269 Parse the settings file. 270 """ 271 settings = get_settings(settings_file) 272 273 language = settings["general_settings"]["language"] 274 self.language_settings = settings["language_settings"].get(language, "EN") 275 self.chart_colors_settings = settings["chart_colors"] 276 self.planets_settings = settings["celestial_points"] 277 self.aspects_settings = settings["aspects"] 278 self.planet_in_zodiac_extra_points = settings["general_settings"]["planet_in_zodiac_extra_points"] 279 self.chart_settings = settings["chart_settings"]
Parse the settings file.
def
makeTemplate(self, minify: bool = False) -> str:
1391 def makeTemplate(self, minify: bool = False) -> str: 1392 """Creates the template for the SVG file""" 1393 td = self._createTemplateDictionary() 1394 1395 # read template 1396 with open(self.xml_svg, "r", encoding="utf-8", errors="ignore") as f: 1397 template = Template(f.read()).substitute(td) 1398 1399 # return filename 1400 1401 logging.debug(f"Template dictionary keys: {td.keys()}") 1402 1403 self._createTemplateDictionary() 1404 1405 if minify: 1406 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 1407 1408 else: 1409 template = template.replace('"', "'") 1410 1411 return template
Creates the template for the SVG file
def
makeSVG(self, minify: bool = False):
1413 def makeSVG(self, minify: bool = False): 1414 """Prints out the SVG file in the specifide folder""" 1415 1416 if not (self.template): 1417 self.template = self.makeTemplate(minify) 1418 1419 self.chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg" 1420 1421 with open(self.chartname, "w", encoding="utf-8", errors="ignore") as output_file: 1422 output_file.write(self.template) 1423 1424 logging.info(f"SVG Generated Correctly in: {self.chartname}")
Prints out the SVG file in the specifide folder