kerykeion.charts.kerykeion_chart_svg
This is part of Kerykeion (C) 2025 Giacomo Battaglia
1# -*- coding: utf-8 -*- 2""" 3 This is part of Kerykeion (C) 2025 Giacomo Battaglia 4""" 5 6 7import logging 8import swisseph as swe 9from typing import get_args 10 11from kerykeion.settings.kerykeion_settings import get_settings 12from kerykeion.aspects.synastry_aspects import SynastryAspects 13from kerykeion.aspects.natal_aspects import NatalAspects 14from kerykeion.astrological_subject import AstrologicalSubject 15from kerykeion.kr_types import KerykeionException, ChartType, KerykeionPointModel, Sign, ActiveAspect 16from kerykeion.kr_types import ChartTemplateDictionary 17from kerykeion.kr_types.kr_models import AstrologicalSubjectModel, CompositeSubjectModel 18from kerykeion.kr_types.settings_models import KerykeionSettingsCelestialPointModel, KerykeionSettingsModel 19from kerykeion.kr_types.kr_literals import KerykeionChartTheme, KerykeionChartLanguage, AxialCusps, Planet 20from kerykeion.charts.charts_utils import ( 21 draw_zodiac_slice, 22 convert_latitude_coordinate_to_string, 23 convert_longitude_coordinate_to_string, 24 draw_aspect_line, 25 draw_transit_ring_degree_steps, 26 draw_degree_ring, 27 draw_transit_ring, 28 draw_first_circle, 29 draw_second_circle, 30 draw_third_circle, 31 draw_aspect_grid, 32 draw_houses_cusps_and_text_number, 33 draw_transit_aspect_list, 34 draw_transit_aspect_grid, 35 calculate_moon_phase_chart_params, 36 draw_house_grid, 37 draw_planet_grid, 38) 39from kerykeion.charts.draw_planets import draw_planets # type: ignore 40from kerykeion.utilities import get_houses_list, inline_css_variables_in_svg 41from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS, DEFAULT_ACTIVE_ASPECTS 42from pathlib import Path 43from scour.scour import scourString 44from string import Template 45from typing import Union, List, Literal 46from datetime import datetime 47 48class KerykeionChartSVG: 49 """ 50 KerykeionChartSVG generates astrological chart visualizations as SVG files. 51 52 This class supports creating full chart SVGs, wheel-only SVGs, and aspect-grid-only SVGs 53 for various chart types including Natal, ExternalNatal, Transit, Synastry, and Composite. 54 Charts are rendered using XML templates and drawing utilities, with customizable themes, 55 language, active points, and aspects. 56 The rendered SVGs can be saved to a specified output directory or, by default, to the user's home directory. 57 58 NOTE: 59 The generated SVG files are optimized for web use, opening in browsers. If you want to 60 use them in other applications, you might need to adjust the SVG settings or styles. 61 62 Args: 63 first_obj (AstrologicalSubject | AstrologicalSubjectModel | CompositeSubjectModel): 64 The primary astrological subject for the chart. 65 chart_type (ChartType, optional): 66 The type of chart to generate ('Natal', 'ExternalNatal', 'Transit', 'Synastry', 'Composite'). 67 Defaults to 'Natal'. 68 second_obj (AstrologicalSubject | AstrologicalSubjectModel, optional): 69 The secondary subject for Transit or Synastry charts. Not required for Natal or Composite. 70 new_output_directory (str | Path, optional): 71 Directory to write generated SVG files. Defaults to the user's home directory. 72 new_settings_file (Path | dict | KerykeionSettingsModel, optional): 73 Path or settings object to override default chart configuration (colors, fonts, aspects). 74 theme (KerykeionChartTheme, optional): 75 CSS theme for the chart. If None, no default styles are applied. Defaults to 'classic'. 76 double_chart_aspect_grid_type (Literal['list', 'table'], optional): 77 Specifies rendering style for double-chart aspect grids. Defaults to 'list'. 78 chart_language (KerykeionChartLanguage, optional): 79 Language code for chart labels. Defaults to 'EN'. 80 active_points (list[Planet | AxialCusps], optional): 81 List of celestial points and angles to include. Defaults to DEFAULT_ACTIVE_POINTS. 82 Example: 83 ["Sun", "Moon", "Mercury", "Venus"] 84 85 active_aspects (list[ActiveAspect], optional): 86 List of aspects (name and orb) to calculate. Defaults to DEFAULT_ACTIVE_ASPECTS. 87 Example: 88 [ 89 {"name": "conjunction", "orb": 10}, 90 {"name": "opposition", "orb": 10}, 91 {"name": "trine", "orb": 8}, 92 {"name": "sextile", "orb": 6}, 93 {"name": "square", "orb": 5}, 94 {"name": "quintile", "orb": 1}, 95 ] 96 97 Public Methods: 98 makeTemplate(minify=False, remove_css_variables=False) -> str: 99 Render the full chart SVG as a string without writing to disk. Use `minify=True` 100 to remove whitespace and quotes, and `remove_css_variables=True` to embed CSS vars. 101 102 makeSVG(minify=False, remove_css_variables=False) -> None: 103 Generate and write the full chart SVG file to the output directory. 104 Filenames follow the pattern: 105 '{subject.name} - {chart_type} Chart.svg'. 106 107 makeWheelOnlyTemplate(minify=False, remove_css_variables=False) -> str: 108 Render only the chart wheel (no aspect grid) as an SVG string. 109 110 makeWheelOnlySVG(minify=False, remove_css_variables=False) -> None: 111 Generate and write the wheel-only SVG file: 112 '{subject.name} - {chart_type} Chart - Wheel Only.svg'. 113 114 makeAspectGridOnlyTemplate(minify=False, remove_css_variables=False) -> str: 115 Render only the aspect grid as an SVG string. 116 117 makeAspectGridOnlySVG(minify=False, remove_css_variables=False) -> None: 118 Generate and write the aspect-grid-only SVG file: 119 '{subject.name} - {chart_type} Chart - Aspect Grid Only.svg'. 120 """ 121 122 # Constants 123 _BASIC_CHART_VIEWBOX = "0 0 820 550.0" 124 _WIDE_CHART_VIEWBOX = "0 0 1200 546.0" 125 _TRANSIT_CHART_WITH_TABLE_VIWBOX = "0 0 960 546.0" 126 127 _DEFAULT_HEIGHT = 550 128 _DEFAULT_FULL_WIDTH = 1200 129 _DEFAULT_NATAL_WIDTH = 820 130 _DEFAULT_FULL_WIDTH_WITH_TABLE = 960 131 _PLANET_IN_ZODIAC_EXTRA_POINTS = 10 132 133 # Set at init 134 first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel] 135 second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] 136 chart_type: ChartType 137 new_output_directory: Union[Path, None] 138 new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] 139 output_directory: Path 140 new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] 141 theme: Union[KerykeionChartTheme, None] 142 double_chart_aspect_grid_type: Literal["list", "table"] 143 chart_language: KerykeionChartLanguage 144 active_points: List[Union[Planet, AxialCusps]] 145 active_aspects: List[ActiveAspect] 146 147 # Internal properties 148 fire: float 149 earth: float 150 air: float 151 water: float 152 first_circle_radius: float 153 second_circle_radius: float 154 third_circle_radius: float 155 width: Union[float, int] 156 language_settings: dict 157 chart_colors_settings: dict 158 planets_settings: dict 159 aspects_settings: dict 160 user: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel] 161 available_planets_setting: List[KerykeionSettingsCelestialPointModel] 162 height: float 163 location: str 164 geolat: float 165 geolon: float 166 template: str 167 168 def __init__( 169 self, 170 first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel], 171 chart_type: ChartType = "Natal", 172 second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] = None, 173 new_output_directory: Union[str, None] = None, 174 new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None, 175 theme: Union[KerykeionChartTheme, None] = "classic", 176 double_chart_aspect_grid_type: Literal["list", "table"] = "list", 177 chart_language: KerykeionChartLanguage = "EN", 178 active_points: List[Union[Planet, AxialCusps]] = DEFAULT_ACTIVE_POINTS, 179 active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS, 180 ): 181 """ 182 Initialize the chart generator with subject data and configuration options. 183 184 Args: 185 first_obj (AstrologicalSubject, AstrologicalSubjectModel, or CompositeSubjectModel): 186 Primary astrological subject instance. 187 chart_type (ChartType, optional): 188 Type of chart to generate (e.g., 'Natal', 'Transit'). 189 second_obj (AstrologicalSubject or AstrologicalSubjectModel, optional): 190 Secondary subject for Transit or Synastry charts. 191 new_output_directory (str or Path, optional): 192 Base directory to save generated SVG files. 193 new_settings_file (Path, dict, or KerykeionSettingsModel, optional): 194 Custom settings source for chart colors, fonts, and aspects. 195 theme (KerykeionChartTheme or None, optional): 196 CSS theme to apply; None for default styling. 197 double_chart_aspect_grid_type (Literal['list','table'], optional): 198 Layout style for double-chart aspect grids ('list' or 'table'). 199 chart_language (KerykeionChartLanguage, optional): 200 Language code for chart labels (e.g., 'EN', 'IT'). 201 active_points (List[Planet or AxialCusps], optional): 202 Celestial points to include in the chart visualization. 203 active_aspects (List[ActiveAspect], optional): 204 Aspects to calculate, each defined by name and orb. 205 """ 206 home_directory = Path.home() 207 self.new_settings_file = new_settings_file 208 self.chart_language = chart_language 209 self.active_points = active_points 210 self.active_aspects = active_aspects 211 212 if new_output_directory: 213 self.output_directory = Path(new_output_directory) 214 else: 215 self.output_directory = home_directory 216 217 self.parse_json_settings(new_settings_file) 218 self.chart_type = chart_type 219 220 # Kerykeion instance 221 self.user = first_obj 222 223 self.available_planets_setting = [] 224 for body in self.planets_settings: 225 if body["name"] not in active_points: 226 continue 227 else: 228 body["is_active"] = True 229 230 self.available_planets_setting.append(body) 231 232 # Available bodies 233 available_celestial_points_names = [] 234 for body in self.available_planets_setting: 235 available_celestial_points_names.append(body["name"].lower()) 236 237 self.available_kerykeion_celestial_points: list[KerykeionPointModel] = [] 238 for body in available_celestial_points_names: 239 self.available_kerykeion_celestial_points.append(self.user.get(body)) 240 241 # Makes the sign number list. 242 if self.chart_type == "Natal" or self.chart_type == "ExternalNatal": 243 natal_aspects_instance = NatalAspects( 244 self.user, new_settings_file=self.new_settings_file, 245 active_points=active_points, 246 active_aspects=active_aspects, 247 ) 248 self.aspects_list = natal_aspects_instance.relevant_aspects 249 250 elif self.chart_type == "Transit" or self.chart_type == "Synastry": 251 if not second_obj: 252 raise KerykeionException("Second object is required for Transit or Synastry charts.") 253 254 # Kerykeion instance 255 self.t_user = second_obj 256 257 # Aspects 258 if self.chart_type == "Transit": 259 synastry_aspects_instance = SynastryAspects( 260 self.t_user, 261 self.user, 262 new_settings_file=self.new_settings_file, 263 active_points=active_points, 264 active_aspects=active_aspects, 265 ) 266 267 else: 268 synastry_aspects_instance = SynastryAspects( 269 self.user, 270 self.t_user, 271 new_settings_file=self.new_settings_file, 272 active_points=active_points, 273 active_aspects=active_aspects, 274 ) 275 276 self.aspects_list = synastry_aspects_instance.relevant_aspects 277 278 self.t_available_kerykeion_celestial_points = [] 279 for body in available_celestial_points_names: 280 self.t_available_kerykeion_celestial_points.append(self.t_user.get(body)) 281 282 elif self.chart_type == "Composite": 283 if not isinstance(first_obj, CompositeSubjectModel): 284 raise KerykeionException("First object must be a CompositeSubjectModel instance.") 285 286 self.aspects_list = NatalAspects(self.user, new_settings_file=self.new_settings_file, active_points=active_points).relevant_aspects 287 288 # Double chart aspect grid type 289 self.double_chart_aspect_grid_type = double_chart_aspect_grid_type 290 291 # screen size 292 self.height = self._DEFAULT_HEIGHT 293 if self.chart_type == "Synastry" or self.chart_type == "Transit": 294 self.width = self._DEFAULT_FULL_WIDTH 295 elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit": 296 self.width = self._DEFAULT_FULL_WIDTH_WITH_TABLE 297 else: 298 self.width = self._DEFAULT_NATAL_WIDTH 299 300 if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]: 301 self.location = self.user.city 302 self.geolat = self.user.lat 303 self.geolon = self.user.lng 304 305 elif self.chart_type == "Composite": 306 self.location = "" 307 self.geolat = (self.user.first_subject.lat + self.user.second_subject.lat) / 2 308 self.geolon = (self.user.first_subject.lng + self.user.second_subject.lng) / 2 309 310 elif self.chart_type in ["Transit"]: 311 self.location = self.t_user.city 312 self.geolat = self.t_user.lat 313 self.geolon = self.t_user.lng 314 self.t_name = self.language_settings["transit_name"] 315 316 # Default radius for the chart 317 self.main_radius = 240 318 319 # Set circle radii based on chart type 320 if self.chart_type == "ExternalNatal": 321 self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 56, 92, 112 322 else: 323 self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 0, 36, 120 324 325 # Initialize element points 326 self.fire = 0.0 327 self.earth = 0.0 328 self.air = 0.0 329 self.water = 0.0 330 331 # Calculate element points from planets 332 self._calculate_elements_points_from_planets() 333 334 # Set up theme 335 if theme not in get_args(KerykeionChartTheme) and theme is not None: 336 raise KerykeionException(f"Theme {theme} is not available. Set None for default theme.") 337 338 self.set_up_theme(theme) 339 340 def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None: 341 """ 342 Load and apply a CSS theme for the chart visualization. 343 344 Args: 345 theme (KerykeionChartTheme or None): Name of the theme to apply. If None, no CSS is applied. 346 """ 347 if theme is None: 348 self.color_style_tag = "" 349 return 350 351 theme_dir = Path(__file__).parent / "themes" 352 353 with open(theme_dir / f"{theme}.css", "r") as f: 354 self.color_style_tag = f.read() 355 356 def set_output_directory(self, dir_path: Path) -> None: 357 """ 358 Set the directory where generated SVG files will be saved. 359 360 Args: 361 dir_path (Path): Target directory for SVG output. 362 """ 363 self.output_directory = dir_path 364 logging.info(f"Output direcotry set to: {self.output_directory}") 365 366 def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None: 367 """ 368 Load and parse chart configuration settings. 369 370 Args: 371 settings_file_or_dict (Path, dict, or KerykeionSettingsModel): 372 Source for custom chart settings. 373 """ 374 settings = get_settings(settings_file_or_dict) 375 376 self.language_settings = settings["language_settings"][self.chart_language] 377 self.chart_colors_settings = settings["chart_colors"] 378 self.planets_settings = settings["celestial_points"] 379 self.aspects_settings = settings["aspects"] 380 381 def _draw_zodiac_circle_slices(self, r): 382 """ 383 Draw zodiac circle slices for each sign. 384 385 Args: 386 r (float): Outer radius of the zodiac ring. 387 388 Returns: 389 str: Concatenated SVG elements for zodiac slices. 390 """ 391 sings = get_args(Sign) 392 output = "" 393 for i, sing in enumerate(sings): 394 output += draw_zodiac_slice( 395 c1=self.first_circle_radius, 396 chart_type=self.chart_type, 397 seventh_house_degree_ut=self.user.seventh_house.abs_pos, 398 num=i, 399 r=r, 400 style=f'fill:{self.chart_colors_settings[f"zodiac_bg_{i}"]}; fill-opacity: 0.5;', 401 type=sing, 402 ) 403 404 return output 405 406 def _calculate_elements_points_from_planets(self): 407 """ 408 Compute elemental point totals based on active planetary positions. 409 410 Iterates over each active planet to determine its zodiac element and adds extra points 411 if the planet is in a related sign. Updates self.fire, self.earth, self.air, and self.water. 412 413 Returns: 414 None 415 """ 416 417 ZODIAC = ( 418 {"name": "Ari", "element": "fire"}, 419 {"name": "Tau", "element": "earth"}, 420 {"name": "Gem", "element": "air"}, 421 {"name": "Can", "element": "water"}, 422 {"name": "Leo", "element": "fire"}, 423 {"name": "Vir", "element": "earth"}, 424 {"name": "Lib", "element": "air"}, 425 {"name": "Sco", "element": "water"}, 426 {"name": "Sag", "element": "fire"}, 427 {"name": "Cap", "element": "earth"}, 428 {"name": "Aqu", "element": "air"}, 429 {"name": "Pis", "element": "water"}, 430 ) 431 432 # Available bodies 433 available_celestial_points_names = [] 434 for body in self.available_planets_setting: 435 available_celestial_points_names.append(body["name"].lower()) 436 437 # Make list of the points sign 438 points_sign = [] 439 for planet in available_celestial_points_names: 440 points_sign.append(self.user.get(planet).sign_num) 441 442 for i in range(len(self.available_planets_setting)): 443 # element: get extra points if planet is in own zodiac sign. 444 related_zodiac_signs = self.available_planets_setting[i]["related_zodiac_signs"] 445 cz = points_sign[i] 446 extra_points = 0 447 if related_zodiac_signs != []: 448 for e in range(len(related_zodiac_signs)): 449 if int(related_zodiac_signs[e]) == int(cz): 450 extra_points = self._PLANET_IN_ZODIAC_EXTRA_POINTS 451 452 ele = ZODIAC[points_sign[i]]["element"] 453 if ele == "fire": 454 self.fire = self.fire + self.available_planets_setting[i]["element_points"] + extra_points 455 456 elif ele == "earth": 457 self.earth = self.earth + self.available_planets_setting[i]["element_points"] + extra_points 458 459 elif ele == "air": 460 self.air = self.air + self.available_planets_setting[i]["element_points"] + extra_points 461 462 elif ele == "water": 463 self.water = self.water + self.available_planets_setting[i]["element_points"] + extra_points 464 465 def _draw_all_aspects_lines(self, r, ar): 466 """ 467 Render SVG lines for all aspects in the chart. 468 469 Args: 470 r (float): Radius at which aspect lines originate. 471 ar (float): Radius at which aspect lines terminate. 472 473 Returns: 474 str: SVG markup for all aspect lines. 475 """ 476 out = "" 477 for aspect in self.aspects_list: 478 aspect_name = aspect["aspect"] 479 aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None) 480 if aspect_color: 481 out += draw_aspect_line( 482 r=r, 483 ar=ar, 484 aspect=aspect, 485 color=aspect_color, 486 seventh_house_degree_ut=self.user.seventh_house.abs_pos 487 ) 488 return out 489 490 def _draw_all_transit_aspects_lines(self, r, ar): 491 """ 492 Render SVG lines for all transit aspects in the chart. 493 494 Args: 495 r (float): Radius at which transit aspect lines originate. 496 ar (float): Radius at which transit aspect lines terminate. 497 498 Returns: 499 str: SVG markup for all transit aspect lines. 500 """ 501 out = "" 502 for aspect in self.aspects_list: 503 aspect_name = aspect["aspect"] 504 aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None) 505 if aspect_color: 506 out += draw_aspect_line( 507 r=r, 508 ar=ar, 509 aspect=aspect, 510 color=aspect_color, 511 seventh_house_degree_ut=self.user.seventh_house.abs_pos 512 ) 513 return out 514 515 def _create_template_dictionary(self) -> ChartTemplateDictionary: 516 """ 517 Assemble chart data and rendering instructions into a template dictionary. 518 519 Gathers styling, dimensions, and SVG fragments for chart components based on 520 chart type and subjects. 521 522 Returns: 523 ChartTemplateDictionary: Populated structure of template variables. 524 """ 525 # Initialize template dictionary 526 template_dict: dict = {} 527 528 # Set the color style tag 529 template_dict["color_style_tag"] = self.color_style_tag 530 531 # Set chart dimensions 532 template_dict["chart_height"] = self.height 533 template_dict["chart_width"] = self.width 534 535 # Set viewbox based on chart type 536 if self.chart_type in ["Natal", "ExternalNatal", "Composite"]: 537 template_dict['viewbox'] = self._BASIC_CHART_VIEWBOX 538 elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit": 539 template_dict['viewbox'] = self._TRANSIT_CHART_WITH_TABLE_VIWBOX 540 else: 541 template_dict['viewbox'] = self._WIDE_CHART_VIEWBOX 542 543 # Generate rings and circles based on chart type 544 if self.chart_type in ["Transit", "Synastry"]: 545 template_dict["transitRing"] = draw_transit_ring(self.main_radius, self.chart_colors_settings["paper_1"], self.chart_colors_settings["zodiac_transit_ring_3"]) 546 template_dict["degreeRing"] = draw_transit_ring_degree_steps(self.main_radius, self.user.seventh_house.abs_pos) 547 template_dict["first_circle"] = draw_first_circle(self.main_radius, self.chart_colors_settings["zodiac_transit_ring_2"], self.chart_type) 548 template_dict["second_circle"] = draw_second_circle(self.main_radius, self.chart_colors_settings['zodiac_transit_ring_1'], self.chart_colors_settings['paper_1'], self.chart_type) 549 template_dict['third_circle'] = draw_third_circle(self.main_radius, self.chart_colors_settings['zodiac_transit_ring_0'], self.chart_colors_settings['paper_1'], self.chart_type, self.third_circle_radius) 550 551 if self.double_chart_aspect_grid_type == "list": 552 title = "" 553 if self.chart_type == "Synastry": 554 title = self.language_settings.get("couple_aspects", "Couple Aspects") 555 else: 556 title = self.language_settings.get("transit_aspects", "Transit Aspects") 557 558 template_dict["makeAspectGrid"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings) 559 else: 560 template_dict["makeAspectGrid"] = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, 550, 450) 561 562 template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160) 563 else: 564 template_dict["transitRing"] = "" 565 template_dict["degreeRing"] = draw_degree_ring(self.main_radius, self.first_circle_radius, self.user.seventh_house.abs_pos, self.chart_colors_settings["paper_0"]) 566 template_dict['first_circle'] = draw_first_circle(self.main_radius, self.chart_colors_settings["zodiac_radix_ring_2"], self.chart_type, self.first_circle_radius) 567 template_dict["second_circle"] = draw_second_circle(self.main_radius, self.chart_colors_settings["zodiac_radix_ring_1"], self.chart_colors_settings["paper_1"], self.chart_type, self.second_circle_radius) 568 template_dict['third_circle'] = draw_third_circle(self.main_radius, self.chart_colors_settings["zodiac_radix_ring_0"], self.chart_colors_settings["paper_1"], self.chart_type, self.third_circle_radius) 569 template_dict["makeAspectGrid"] = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list) 570 571 template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius) 572 573 # Set chart title 574 if self.chart_type == "Synastry": 575 template_dict["stringTitle"] = f"{self.user.name} {self.language_settings['and_word']} {self.t_user.name}" 576 elif self.chart_type == "Transit": 577 template_dict["stringTitle"] = f"{self.language_settings['transits']} {self.t_user.day}/{self.t_user.month}/{self.t_user.year}" 578 elif self.chart_type in ["Natal", "ExternalNatal"]: 579 template_dict["stringTitle"] = self.user.name 580 elif self.chart_type == "Composite": 581 template_dict["stringTitle"] = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}" 582 583 # Zodiac Type Info 584 if self.user.zodiac_type == 'Tropic': 585 zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}" 586 else: 587 mode_const = "SIDM_" + self.user.sidereal_mode # type: ignore 588 mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const)) 589 zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}" 590 591 template_dict["bottom_left_0"] = f"{self.language_settings.get('houses_system_' + self.user.houses_system_identifier, self.user.houses_system_name)} {self.language_settings.get('houses', 'Houses')}" 592 template_dict["bottom_left_1"] = zodiac_info 593 594 if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]: 595 template_dict["bottom_left_2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")} {self.language_settings.get("day", "Day").lower()}: {self.user.lunar_phase.get("moon_phase", "")}' 596 template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get(self.user.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.user.lunar_phase.moon_phase_name)}' 597 template_dict["bottom_left_4"] = f'{self.language_settings.get(self.user.perspective_type.lower().replace(" ", "_"), self.user.perspective_type)}' 598 elif self.chart_type == "Transit": 599 template_dict["bottom_left_2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get("day", "Day")} {self.t_user.lunar_phase.get("moon_phase", "")}' 600 template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.t_user.lunar_phase.moon_phase_name}' 601 template_dict["bottom_left_4"] = f'{self.language_settings.get(self.t_user.perspective_type.lower().replace(" ", "_"), self.t_user.perspective_type)}' 602 elif self.chart_type == "Composite": 603 template_dict["bottom_left_2"] = f'{self.user.first_subject.perspective_type}' 604 template_dict["bottom_left_3"] = f'{self.language_settings.get("composite_chart", "Composite Chart")} - {self.language_settings.get("midpoints", "Midpoints")}' 605 template_dict["bottom_left_4"] = "" 606 607 # Draw moon phase 608 moon_phase_dict = calculate_moon_phase_chart_params( 609 self.user.lunar_phase["degrees_between_s_m"], 610 self.geolat 611 ) 612 613 template_dict["lunar_phase_rotate"] = moon_phase_dict["lunar_phase_rotate"] 614 template_dict["lunar_phase_circle_center_x"] = moon_phase_dict["circle_center_x"] 615 template_dict["lunar_phase_circle_radius"] = moon_phase_dict["circle_radius"] 616 617 if self.chart_type == "Composite": 618 template_dict["top_left_1"] = f"{datetime.fromisoformat(self.user.first_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}" 619 # Set location string 620 elif len(self.location) > 35: 621 split_location = self.location.split(",") 622 if len(split_location) > 1: 623 template_dict["top_left_1"] = split_location[0] + ", " + split_location[-1] 624 if len(template_dict["top_left_1"]) > 35: 625 template_dict["top_left_1"] = template_dict["top_left_1"][:35] + "..." 626 else: 627 template_dict["top_left_1"] = self.location[:35] + "..." 628 else: 629 template_dict["top_left_1"] = self.location 630 631 # Set chart name 632 if self.chart_type in ["Synastry", "Transit"]: 633 template_dict["top_left_0"] = f"{self.user.name}:" 634 elif self.chart_type in ["Natal", "ExternalNatal"]: 635 template_dict["top_left_0"] = f'{self.language_settings["info"]}:' 636 elif self.chart_type == "Composite": 637 template_dict["top_left_0"] = f'{self.user.first_subject.name}' 638 639 # Set additional information for Synastry chart type 640 if self.chart_type == "Synastry": 641 template_dict["top_left_3"] = f"{self.t_user.name}: " 642 template_dict["top_left_4"] = self.t_user.city 643 template_dict["top_left_5"] = f"{self.t_user.year}-{self.t_user.month}-{self.t_user.day} {self.t_user.hour:02d}:{self.t_user.minute:02d}" 644 elif self.chart_type == "Composite": 645 template_dict["top_left_3"] = self.user.second_subject.name 646 template_dict["top_left_4"] = f"{datetime.fromisoformat(self.user.second_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}" 647 latitude_string = convert_latitude_coordinate_to_string(self.user.second_subject.lat, self.language_settings['north_letter'], self.language_settings['south_letter']) 648 longitude_string = convert_longitude_coordinate_to_string(self.user.second_subject.lng, self.language_settings['east_letter'], self.language_settings['west_letter']) 649 template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}" 650 else: 651 latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings['north'], self.language_settings['south']) 652 longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings['east'], self.language_settings['west']) 653 template_dict["top_left_3"] = f"{self.language_settings['latitude']}: {latitude_string}" 654 template_dict["top_left_4"] = f"{self.language_settings['longitude']}: {longitude_string}" 655 template_dict["top_left_5"] = f"{self.language_settings['type']}: {self.language_settings.get(self.chart_type, self.chart_type)}" 656 657 658 # Set paper colors 659 template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"] 660 template_dict["paper_color_1"] = self.chart_colors_settings["paper_1"] 661 662 # Set planet colors 663 for planet in self.planets_settings: 664 planet_id = planet["id"] 665 template_dict[f"planets_color_{planet_id}"] = planet["color"] # type: ignore 666 667 # Set zodiac colors 668 for i in range(12): 669 template_dict[f"zodiac_color_{i}"] = self.chart_colors_settings[f"zodiac_icon_{i}"] # type: ignore 670 671 # Set orb colors 672 for aspect in self.aspects_settings: 673 template_dict[f"orb_color_{aspect['degree']}"] = aspect['color'] # type: ignore 674 675 # Drawing functions 676 template_dict["makeZodiac"] = self._draw_zodiac_circle_slices(self.main_radius) 677 678 first_subject_houses_list = get_houses_list(self.user) 679 680 # Draw houses grid and cusps 681 if self.chart_type in ["Transit", "Synastry"]: 682 second_subject_houses_list = get_houses_list(self.t_user) 683 684 template_dict["makeHousesGrid"] = draw_house_grid( 685 main_subject_houses_list=first_subject_houses_list, 686 secondary_subject_houses_list=second_subject_houses_list, 687 chart_type=self.chart_type, 688 text_color=self.chart_colors_settings["paper_0"], 689 house_cusp_generale_name_label=self.language_settings["cusp"] 690 ) 691 692 template_dict["makeHouses"] = draw_houses_cusps_and_text_number( 693 r=self.main_radius, 694 first_subject_houses_list=first_subject_houses_list, 695 standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"], 696 first_house_color=self.planets_settings[12]["color"], 697 tenth_house_color=self.planets_settings[13]["color"], 698 seventh_house_color=self.planets_settings[14]["color"], 699 fourth_house_color=self.planets_settings[15]["color"], 700 c1=self.first_circle_radius, 701 c3=self.third_circle_radius, 702 chart_type=self.chart_type, 703 second_subject_houses_list=second_subject_houses_list, 704 transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"], 705 ) 706 707 else: 708 template_dict["makeHousesGrid"] = draw_house_grid( 709 main_subject_houses_list=first_subject_houses_list, 710 chart_type=self.chart_type, 711 text_color=self.chart_colors_settings["paper_0"], 712 house_cusp_generale_name_label=self.language_settings["cusp"] 713 ) 714 715 template_dict["makeHouses"] = draw_houses_cusps_and_text_number( 716 r=self.main_radius, 717 first_subject_houses_list=first_subject_houses_list, 718 standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"], 719 first_house_color=self.planets_settings[12]["color"], 720 tenth_house_color=self.planets_settings[13]["color"], 721 seventh_house_color=self.planets_settings[14]["color"], 722 fourth_house_color=self.planets_settings[15]["color"], 723 c1=self.first_circle_radius, 724 c3=self.third_circle_radius, 725 chart_type=self.chart_type, 726 ) 727 728 # Draw planets 729 if self.chart_type in ["Transit", "Synastry"]: 730 template_dict["makePlanets"] = draw_planets( 731 available_kerykeion_celestial_points=self.available_kerykeion_celestial_points, 732 available_planets_setting=self.available_planets_setting, 733 second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points, 734 radius=self.main_radius, 735 main_subject_first_house_degree_ut=self.user.first_house.abs_pos, 736 main_subject_seventh_house_degree_ut=self.user.seventh_house.abs_pos, 737 chart_type=self.chart_type, 738 third_circle_radius=self.third_circle_radius, 739 ) 740 else: 741 template_dict["makePlanets"] = draw_planets( 742 available_planets_setting=self.available_planets_setting, 743 chart_type=self.chart_type, 744 radius=self.main_radius, 745 available_kerykeion_celestial_points=self.available_kerykeion_celestial_points, 746 third_circle_radius=self.third_circle_radius, 747 main_subject_first_house_degree_ut=self.user.first_house.abs_pos, 748 main_subject_seventh_house_degree_ut=self.user.seventh_house.abs_pos 749 ) 750 751 # Draw elements percentages 752 total = self.fire + self.water + self.earth + self.air 753 754 fire_percentage = int(round(100 * self.fire / total)) 755 earth_percentage = int(round(100 * self.earth / total)) 756 air_percentage = int(round(100 * self.air / total)) 757 water_percentage = int(round(100 * self.water / total)) 758 759 template_dict["fire_string"] = f"{self.language_settings['fire']} {fire_percentage}%" 760 template_dict["earth_string"] = f"{self.language_settings['earth']} {earth_percentage}%" 761 template_dict["air_string"] = f"{self.language_settings['air']} {air_percentage}%" 762 template_dict["water_string"] = f"{self.language_settings['water']} {water_percentage}%" 763 764 # Draw planet grid 765 if self.chart_type in ["Transit", "Synastry"]: 766 if self.chart_type == "Transit": 767 second_subject_table_name = self.language_settings["transit_name"] 768 else: 769 second_subject_table_name = self.t_user.name 770 771 template_dict["makePlanetGrid"] = draw_planet_grid( 772 planets_and_houses_grid_title=self.language_settings["planets_and_house"], 773 subject_name=self.user.name, 774 available_kerykeion_celestial_points=self.available_kerykeion_celestial_points, 775 chart_type=self.chart_type, 776 text_color=self.chart_colors_settings["paper_0"], 777 celestial_point_language=self.language_settings["celestial_points"], 778 second_subject_name=second_subject_table_name, 779 second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points, 780 ) 781 else: 782 if self.chart_type == "Composite": 783 subject_name = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}" 784 else: 785 subject_name = self.user.name 786 787 template_dict["makePlanetGrid"] = draw_planet_grid( 788 planets_and_houses_grid_title=self.language_settings["planets_and_house"], 789 subject_name=subject_name, 790 available_kerykeion_celestial_points=self.available_kerykeion_celestial_points, 791 chart_type=self.chart_type, 792 text_color=self.chart_colors_settings["paper_0"], 793 celestial_point_language=self.language_settings["celestial_points"], 794 ) 795 796 # Set date time string 797 if self.chart_type in ["Composite"]: 798 # First Subject Latitude and Longitude 799 latitude = convert_latitude_coordinate_to_string(self.user.first_subject.lat, self.language_settings["north_letter"], self.language_settings["south_letter"]) 800 longitude = convert_longitude_coordinate_to_string(self.user.first_subject.lng, self.language_settings["east_letter"], self.language_settings["west_letter"]) 801 template_dict["top_left_2"] = f"{latitude} {longitude}" 802 else: 803 dt = datetime.fromisoformat(self.user.iso_formatted_local_datetime) 804 custom_format = dt.strftime('%Y-%m-%d %H:%M [%z]') 805 custom_format = custom_format[:-3] + ':' + custom_format[-3:] 806 template_dict["top_left_2"] = f"{custom_format}" 807 808 return ChartTemplateDictionary(**template_dict) 809 810 def makeTemplate(self, minify: bool = False, remove_css_variables = False) -> str: 811 """ 812 Render the full chart SVG as a string. 813 814 Reads the XML template, substitutes variables, and optionally inlines CSS 815 variables and minifies the output. 816 817 Args: 818 minify (bool): Remove whitespace and quotes for compactness. 819 remove_css_variables (bool): Embed CSS variable definitions. 820 821 Returns: 822 str: SVG markup as a string. 823 """ 824 td = self._create_template_dictionary() 825 826 DATA_DIR = Path(__file__).parent 827 xml_svg = DATA_DIR / "templates" / "chart.xml" 828 829 # read template 830 with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f: 831 template = Template(f.read()).substitute(td) 832 833 # return filename 834 835 logging.debug(f"Template dictionary keys: {td.keys()}") 836 837 self._create_template_dictionary() 838 839 if remove_css_variables: 840 template = inline_css_variables_in_svg(template) 841 842 if minify: 843 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 844 845 else: 846 template = template.replace('"', "'") 847 848 return template 849 850 def makeSVG(self, minify: bool = False, remove_css_variables = False): 851 """ 852 Generate and save the full chart SVG to disk. 853 854 Calls makeTemplate to render the SVG, then writes a file named 855 "{subject.name} - {chart_type} Chart.svg" in the output directory. 856 857 Args: 858 minify (bool): Pass-through to makeTemplate for compact output. 859 remove_css_variables (bool): Pass-through to makeTemplate to embed CSS variables. 860 861 Returns: 862 None 863 """ 864 865 self.template = self.makeTemplate(minify, remove_css_variables) 866 867 chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg" 868 869 with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file: 870 output_file.write(self.template) 871 872 print(f"SVG Generated Correctly in: {chartname}") 873 874 def makeWheelOnlyTemplate(self, minify: bool = False, remove_css_variables = False): 875 """ 876 Render the wheel-only chart SVG as a string. 877 878 Reads the wheel-only XML template, substitutes chart data, and applies optional 879 CSS inlining and minification. 880 881 Args: 882 minify (bool): Remove whitespace and quotes for compactness. 883 remove_css_variables (bool): Embed CSS variable definitions. 884 885 Returns: 886 str: SVG markup for the chart wheel only. 887 """ 888 889 with open(Path(__file__).parent / "templates" / "wheel_only.xml", "r", encoding="utf-8", errors="ignore") as f: 890 template = f.read() 891 892 template_dict = self._create_template_dictionary() 893 template = Template(template).substitute(template_dict) 894 895 if remove_css_variables: 896 template = inline_css_variables_in_svg(template) 897 898 if minify: 899 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 900 901 else: 902 template = template.replace('"', "'") 903 904 return template 905 906 def makeWheelOnlySVG(self, minify: bool = False, remove_css_variables = False): 907 """ 908 Generate and save wheel-only chart SVG to disk. 909 910 Calls makeWheelOnlyTemplate and writes a file named 911 "{subject.name} - {chart_type} Chart - Wheel Only.svg" in the output directory. 912 913 Args: 914 minify (bool): Pass-through to makeWheelOnlyTemplate for compact output. 915 remove_css_variables (bool): Pass-through to makeWheelOnlyTemplate to embed CSS variables. 916 917 Returns: 918 None 919 """ 920 921 template = self.makeWheelOnlyTemplate(minify, remove_css_variables) 922 chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Wheel Only.svg" 923 924 with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file: 925 output_file.write(template) 926 927 print(f"SVG Generated Correctly in: {chartname}") 928 929 def makeAspectGridOnlyTemplate(self, minify: bool = False, remove_css_variables = False): 930 """ 931 Render the aspect-grid-only chart SVG as a string. 932 933 Reads the aspect-grid XML template, generates the aspect grid based on chart type, 934 and applies optional CSS inlining and minification. 935 936 Args: 937 minify (bool): Remove whitespace and quotes for compactness. 938 remove_css_variables (bool): Embed CSS variable definitions. 939 940 Returns: 941 str: SVG markup for the aspect grid only. 942 """ 943 944 with open(Path(__file__).parent / "templates" / "aspect_grid_only.xml", "r", encoding="utf-8", errors="ignore") as f: 945 template = f.read() 946 947 template_dict = self._create_template_dictionary() 948 949 if self.chart_type in ["Transit", "Synastry"]: 950 aspects_grid = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list) 951 else: 952 aspects_grid = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, x_start=50, y_start=250) 953 954 template = Template(template).substitute({**template_dict, "makeAspectGrid": aspects_grid}) 955 956 if remove_css_variables: 957 template = inline_css_variables_in_svg(template) 958 959 if minify: 960 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 961 962 else: 963 template = template.replace('"', "'") 964 965 return template 966 967 def makeAspectGridOnlySVG(self, minify: bool = False, remove_css_variables = False): 968 """ 969 Generate and save aspect-grid-only chart SVG to disk. 970 971 Calls makeAspectGridOnlyTemplate and writes a file named 972 "{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the output directory. 973 974 Args: 975 minify (bool): Pass-through to makeAspectGridOnlyTemplate for compact output. 976 remove_css_variables (bool): Pass-through to makeAspectGridOnlyTemplate to embed CSS variables. 977 978 Returns: 979 None 980 """ 981 982 template = self.makeAspectGridOnlyTemplate(minify, remove_css_variables) 983 chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Aspect Grid Only.svg" 984 985 with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file: 986 output_file.write(template) 987 988 print(f"SVG Generated Correctly in: {chartname}") 989 990if __name__ == "__main__": 991 from kerykeion.utilities import setup_logging 992 from kerykeion.composite_subject_factory import CompositeSubjectFactory 993 setup_logging(level="debug") 994 995 first = AstrologicalSubject("John Lennon", 1940, 10, 9, 18, 30, "Liverpool", "GB") 996 second = AstrologicalSubject("Paul McCartney", 1942, 6, 18, 15, 30, "Liverpool", "GB") 997 998 # Internal Natal Chart 999 internal_natal_chart = KerykeionChartSVG(first) 1000 internal_natal_chart.makeSVG() 1001 1002 # External Natal Chart 1003 external_natal_chart = KerykeionChartSVG(first, "ExternalNatal", second) 1004 external_natal_chart.makeSVG() 1005 1006 # Synastry Chart 1007 synastry_chart = KerykeionChartSVG(first, "Synastry", second) 1008 synastry_chart.makeSVG() 1009 1010 # Transits Chart 1011 transits_chart = KerykeionChartSVG(first, "Transit", second) 1012 transits_chart.makeSVG() 1013 1014 # Sidereal Birth Chart (Lahiri) 1015 sidereal_subject = AstrologicalSubject("John Lennon Lahiri", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="LAHIRI") 1016 sidereal_chart = KerykeionChartSVG(sidereal_subject) 1017 sidereal_chart.makeSVG() 1018 1019 # Sidereal Birth Chart (Fagan-Bradley) 1020 sidereal_subject = AstrologicalSubject("John Lennon Fagan-Bradley", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="FAGAN_BRADLEY") 1021 sidereal_chart = KerykeionChartSVG(sidereal_subject) 1022 sidereal_chart.makeSVG() 1023 1024 # Sidereal Birth Chart (DeLuce) 1025 sidereal_subject = AstrologicalSubject("John Lennon DeLuce", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="DELUCE") 1026 sidereal_chart = KerykeionChartSVG(sidereal_subject) 1027 sidereal_chart.makeSVG() 1028 1029 # Sidereal Birth Chart (J2000) 1030 sidereal_subject = AstrologicalSubject("John Lennon J2000", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="J2000") 1031 sidereal_chart = KerykeionChartSVG(sidereal_subject) 1032 sidereal_chart.makeSVG() 1033 1034 # House System Morinus 1035 morinus_house_subject = AstrologicalSubject("John Lennon - House System Morinus", 1940, 10, 9, 18, 30, "Liverpool", "GB", houses_system_identifier="M") 1036 morinus_house_chart = KerykeionChartSVG(morinus_house_subject) 1037 morinus_house_chart.makeSVG() 1038 1039 ## To check all the available house systems uncomment the following code: 1040 # from kerykeion.kr_types import HousesSystemIdentifier 1041 # from typing import get_args 1042 # for i in get_args(HousesSystemIdentifier): 1043 # alternatives_house_subject = AstrologicalSubject(f"John Lennon - House System {i}", 1940, 10, 9, 18, 30, "Liverpool", "GB", houses_system=i) 1044 # alternatives_house_chart = KerykeionChartSVG(alternatives_house_subject) 1045 # alternatives_house_chart.makeSVG() 1046 1047 # With True Geocentric Perspective 1048 true_geocentric_subject = AstrologicalSubject("John Lennon - True Geocentric", 1940, 10, 9, 18, 30, "Liverpool", "GB", perspective_type="True Geocentric") 1049 true_geocentric_chart = KerykeionChartSVG(true_geocentric_subject) 1050 true_geocentric_chart.makeSVG() 1051 1052 # With Heliocentric Perspective 1053 heliocentric_subject = AstrologicalSubject("John Lennon - Heliocentric", 1940, 10, 9, 18, 30, "Liverpool", "GB", perspective_type="Heliocentric") 1054 heliocentric_chart = KerykeionChartSVG(heliocentric_subject) 1055 heliocentric_chart.makeSVG() 1056 1057 # With Topocentric Perspective 1058 topocentric_subject = AstrologicalSubject("John Lennon - Topocentric", 1940, 10, 9, 18, 30, "Liverpool", "GB", perspective_type="Topocentric") 1059 topocentric_chart = KerykeionChartSVG(topocentric_subject) 1060 topocentric_chart.makeSVG() 1061 1062 # Minified SVG 1063 minified_subject = AstrologicalSubject("John Lennon - Minified", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1064 minified_chart = KerykeionChartSVG(minified_subject) 1065 minified_chart.makeSVG(minify=True) 1066 1067 # Dark Theme Natal Chart 1068 dark_theme_subject = AstrologicalSubject("John Lennon - Dark Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1069 dark_theme_natal_chart = KerykeionChartSVG(dark_theme_subject, theme="dark") 1070 dark_theme_natal_chart.makeSVG() 1071 1072 # Dark High Contrast Theme Natal Chart 1073 dark_high_contrast_theme_subject = AstrologicalSubject("John Lennon - Dark High Contrast Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1074 dark_high_contrast_theme_natal_chart = KerykeionChartSVG(dark_high_contrast_theme_subject, theme="dark-high-contrast") 1075 dark_high_contrast_theme_natal_chart.makeSVG() 1076 1077 # Light Theme Natal Chart 1078 light_theme_subject = AstrologicalSubject("John Lennon - Light Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1079 light_theme_natal_chart = KerykeionChartSVG(light_theme_subject, theme="light") 1080 light_theme_natal_chart.makeSVG() 1081 1082 # Dark Theme External Natal Chart 1083 dark_theme_external_subject = AstrologicalSubject("John Lennon - Dark Theme External", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1084 dark_theme_external_chart = KerykeionChartSVG(dark_theme_external_subject, "ExternalNatal", second, theme="dark") 1085 dark_theme_external_chart.makeSVG() 1086 1087 # Dark Theme Synastry Chart 1088 dark_theme_synastry_subject = AstrologicalSubject("John Lennon - DTS", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1089 dark_theme_synastry_chart = KerykeionChartSVG(dark_theme_synastry_subject, "Synastry", second, theme="dark") 1090 dark_theme_synastry_chart.makeSVG() 1091 1092 # Wheel Natal Only Chart 1093 wheel_only_subject = AstrologicalSubject("John Lennon - Wheel Only", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1094 wheel_only_chart = KerykeionChartSVG(wheel_only_subject) 1095 wheel_only_chart.makeWheelOnlySVG() 1096 1097 # Wheel External Natal Only Chart 1098 wheel_external_subject = AstrologicalSubject("John Lennon - Wheel External Only", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1099 wheel_external_chart = KerykeionChartSVG(wheel_external_subject, "ExternalNatal", second) 1100 wheel_external_chart.makeWheelOnlySVG() 1101 1102 # Wheel Synastry Only Chart 1103 wheel_synastry_subject = AstrologicalSubject("John Lennon - Wheel Synastry Only", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1104 wheel_synastry_chart = KerykeionChartSVG(wheel_synastry_subject, "Synastry", second) 1105 wheel_synastry_chart.makeWheelOnlySVG() 1106 1107 # Wheel Transit Only Chart 1108 wheel_transit_subject = AstrologicalSubject("John Lennon - Wheel Transit Only", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1109 wheel_transit_chart = KerykeionChartSVG(wheel_transit_subject, "Transit", second) 1110 wheel_transit_chart.makeWheelOnlySVG() 1111 1112 # Wheel Sidereal Birth Chart (Lahiri) Dark Theme 1113 sidereal_dark_subject = AstrologicalSubject("John Lennon Lahiri - Dark Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="LAHIRI") 1114 sidereal_dark_chart = KerykeionChartSVG(sidereal_dark_subject, theme="dark") 1115 sidereal_dark_chart.makeWheelOnlySVG() 1116 1117 # Wheel Sidereal Birth Chart (Fagan-Bradley) Light Theme 1118 sidereal_light_subject = AstrologicalSubject("John Lennon Fagan-Bradley - Light Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="FAGAN_BRADLEY") 1119 sidereal_light_chart = KerykeionChartSVG(sidereal_light_subject, theme="light") 1120 sidereal_light_chart.makeWheelOnlySVG() 1121 1122 # Aspect Grid Only Natal Chart 1123 aspect_grid_only_subject = AstrologicalSubject("John Lennon - Aspect Grid Only", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1124 aspect_grid_only_chart = KerykeionChartSVG(aspect_grid_only_subject) 1125 aspect_grid_only_chart.makeAspectGridOnlySVG() 1126 1127 # Aspect Grid Only Dark Theme Natal Chart 1128 aspect_grid_dark_subject = AstrologicalSubject("John Lennon - Aspect Grid Dark Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1129 aspect_grid_dark_chart = KerykeionChartSVG(aspect_grid_dark_subject, theme="dark") 1130 aspect_grid_dark_chart.makeAspectGridOnlySVG() 1131 1132 # Aspect Grid Only Light Theme Natal Chart 1133 aspect_grid_light_subject = AstrologicalSubject("John Lennon - Aspect Grid Light Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1134 aspect_grid_light_chart = KerykeionChartSVG(aspect_grid_light_subject, theme="light") 1135 aspect_grid_light_chart.makeAspectGridOnlySVG() 1136 1137 # Synastry Chart Aspect Grid Only 1138 aspect_grid_synastry_subject = AstrologicalSubject("John Lennon - Aspect Grid Synastry", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1139 aspect_grid_synastry_chart = KerykeionChartSVG(aspect_grid_synastry_subject, "Synastry", second) 1140 aspect_grid_synastry_chart.makeAspectGridOnlySVG() 1141 1142 # Transit Chart Aspect Grid Only 1143 aspect_grid_transit_subject = AstrologicalSubject("John Lennon - Aspect Grid Transit", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1144 aspect_grid_transit_chart = KerykeionChartSVG(aspect_grid_transit_subject, "Transit", second) 1145 aspect_grid_transit_chart.makeAspectGridOnlySVG() 1146 1147 # Synastry Chart Aspect Grid Only Dark Theme 1148 aspect_grid_dark_synastry_subject = AstrologicalSubject("John Lennon - Aspect Grid Dark Synastry", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1149 aspect_grid_dark_synastry_chart = KerykeionChartSVG(aspect_grid_dark_synastry_subject, "Synastry", second, theme="dark") 1150 aspect_grid_dark_synastry_chart.makeAspectGridOnlySVG() 1151 1152 # Synastry Chart With draw_transit_aspect_list table 1153 synastry_chart_with_table_list_subject = AstrologicalSubject("John Lennon - SCTWL", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1154 synastry_chart_with_table_list = KerykeionChartSVG(synastry_chart_with_table_list_subject, "Synastry", second, double_chart_aspect_grid_type="list", theme="dark") 1155 synastry_chart_with_table_list.makeSVG() 1156 1157 # Transit Chart With draw_transit_aspect_grid table 1158 transit_chart_with_table_grid_subject = AstrologicalSubject("John Lennon - TCWTG", 1940, 10, 9, 18, 30, "Liverpool", "GB") 1159 transit_chart_with_table_grid = KerykeionChartSVG(transit_chart_with_table_grid_subject, "Transit", second, double_chart_aspect_grid_type="table", theme="dark") 1160 transit_chart_with_table_grid.makeSVG() 1161 1162 # Chines Language Chart 1163 chinese_subject = AstrologicalSubject("Hua Chenyu", 1990, 2, 7, 12, 0, "Hunan", "CN") 1164 chinese_chart = KerykeionChartSVG(chinese_subject, chart_language="CN") 1165 chinese_chart.makeSVG() 1166 1167 # French Language Chart 1168 french_subject = AstrologicalSubject("Jeanne Moreau", 1928, 1, 23, 10, 0, "Paris", "FR") 1169 french_chart = KerykeionChartSVG(french_subject, chart_language="FR") 1170 french_chart.makeSVG() 1171 1172 # Spanish Language Chart 1173 spanish_subject = AstrologicalSubject("Antonio Banderas", 1960, 8, 10, 12, 0, "Malaga", "ES") 1174 spanish_chart = KerykeionChartSVG(spanish_subject, chart_language="ES") 1175 spanish_chart.makeSVG() 1176 1177 # Portuguese Language Chart 1178 portuguese_subject = AstrologicalSubject("Cristiano Ronaldo", 1985, 2, 5, 5, 25, "Funchal", "PT") 1179 portuguese_chart = KerykeionChartSVG(portuguese_subject, chart_language="PT") 1180 portuguese_chart.makeSVG() 1181 1182 # Italian Language Chart 1183 italian_subject = AstrologicalSubject("Sophia Loren", 1934, 9, 20, 2, 0, "Rome", "IT") 1184 italian_chart = KerykeionChartSVG(italian_subject, chart_language="IT") 1185 italian_chart.makeSVG() 1186 1187 # Russian Language Chart 1188 russian_subject = AstrologicalSubject("Mikhail Bulgakov", 1891, 5, 15, 12, 0, "Kiev", "UA") 1189 russian_chart = KerykeionChartSVG(russian_subject, chart_language="RU") 1190 russian_chart.makeSVG() 1191 1192 # Turkish Language Chart 1193 turkish_subject = AstrologicalSubject("Mehmet Oz", 1960, 6, 11, 12, 0, "Istanbul", "TR") 1194 turkish_chart = KerykeionChartSVG(turkish_subject, chart_language="TR") 1195 turkish_chart.makeSVG() 1196 1197 # German Language Chart 1198 german_subject = AstrologicalSubject("Albert Einstein", 1879, 3, 14, 11, 30, "Ulm", "DE") 1199 german_chart = KerykeionChartSVG(german_subject, chart_language="DE") 1200 german_chart.makeSVG() 1201 1202 # Hindi Language Chart 1203 hindi_subject = AstrologicalSubject("Amitabh Bachchan", 1942, 10, 11, 4, 0, "Allahabad", "IN") 1204 hindi_chart = KerykeionChartSVG(hindi_subject, chart_language="HI") 1205 hindi_chart.makeSVG() 1206 1207 # Kanye West Natal Chart 1208 kanye_west_subject = AstrologicalSubject("Kanye", 1977, 6, 8, 8, 45, "Atlanta", "US") 1209 kanye_west_chart = KerykeionChartSVG(kanye_west_subject) 1210 kanye_west_chart.makeSVG() 1211 1212 # Composite Chart 1213 angelina = AstrologicalSubject("Angelina Jolie", 1975, 6, 4, 9, 9, "Los Angeles", "US", lng=-118.15, lat=34.03, tz_str="America/Los_Angeles") 1214 brad = AstrologicalSubject("Brad Pitt", 1963, 12, 18, 6, 31, "Shawnee", "US", lng=-96.56, lat=35.20, tz_str="America/Chicago") 1215 1216 composite_subject_factory = CompositeSubjectFactory(angelina, brad) 1217 composite_subject_model = composite_subject_factory.get_midpoint_composite_subject_model() 1218 composite_chart = KerykeionChartSVG(composite_subject_model, "Composite") 1219 composite_chart.makeSVG()
49class KerykeionChartSVG: 50 """ 51 KerykeionChartSVG generates astrological chart visualizations as SVG files. 52 53 This class supports creating full chart SVGs, wheel-only SVGs, and aspect-grid-only SVGs 54 for various chart types including Natal, ExternalNatal, Transit, Synastry, and Composite. 55 Charts are rendered using XML templates and drawing utilities, with customizable themes, 56 language, active points, and aspects. 57 The rendered SVGs can be saved to a specified output directory or, by default, to the user's home directory. 58 59 NOTE: 60 The generated SVG files are optimized for web use, opening in browsers. If you want to 61 use them in other applications, you might need to adjust the SVG settings or styles. 62 63 Args: 64 first_obj (AstrologicalSubject | AstrologicalSubjectModel | CompositeSubjectModel): 65 The primary astrological subject for the chart. 66 chart_type (ChartType, optional): 67 The type of chart to generate ('Natal', 'ExternalNatal', 'Transit', 'Synastry', 'Composite'). 68 Defaults to 'Natal'. 69 second_obj (AstrologicalSubject | AstrologicalSubjectModel, optional): 70 The secondary subject for Transit or Synastry charts. Not required for Natal or Composite. 71 new_output_directory (str | Path, optional): 72 Directory to write generated SVG files. Defaults to the user's home directory. 73 new_settings_file (Path | dict | KerykeionSettingsModel, optional): 74 Path or settings object to override default chart configuration (colors, fonts, aspects). 75 theme (KerykeionChartTheme, optional): 76 CSS theme for the chart. If None, no default styles are applied. Defaults to 'classic'. 77 double_chart_aspect_grid_type (Literal['list', 'table'], optional): 78 Specifies rendering style for double-chart aspect grids. Defaults to 'list'. 79 chart_language (KerykeionChartLanguage, optional): 80 Language code for chart labels. Defaults to 'EN'. 81 active_points (list[Planet | AxialCusps], optional): 82 List of celestial points and angles to include. Defaults to DEFAULT_ACTIVE_POINTS. 83 Example: 84 ["Sun", "Moon", "Mercury", "Venus"] 85 86 active_aspects (list[ActiveAspect], optional): 87 List of aspects (name and orb) to calculate. Defaults to DEFAULT_ACTIVE_ASPECTS. 88 Example: 89 [ 90 {"name": "conjunction", "orb": 10}, 91 {"name": "opposition", "orb": 10}, 92 {"name": "trine", "orb": 8}, 93 {"name": "sextile", "orb": 6}, 94 {"name": "square", "orb": 5}, 95 {"name": "quintile", "orb": 1}, 96 ] 97 98 Public Methods: 99 makeTemplate(minify=False, remove_css_variables=False) -> str: 100 Render the full chart SVG as a string without writing to disk. Use `minify=True` 101 to remove whitespace and quotes, and `remove_css_variables=True` to embed CSS vars. 102 103 makeSVG(minify=False, remove_css_variables=False) -> None: 104 Generate and write the full chart SVG file to the output directory. 105 Filenames follow the pattern: 106 '{subject.name} - {chart_type} Chart.svg'. 107 108 makeWheelOnlyTemplate(minify=False, remove_css_variables=False) -> str: 109 Render only the chart wheel (no aspect grid) as an SVG string. 110 111 makeWheelOnlySVG(minify=False, remove_css_variables=False) -> None: 112 Generate and write the wheel-only SVG file: 113 '{subject.name} - {chart_type} Chart - Wheel Only.svg'. 114 115 makeAspectGridOnlyTemplate(minify=False, remove_css_variables=False) -> str: 116 Render only the aspect grid as an SVG string. 117 118 makeAspectGridOnlySVG(minify=False, remove_css_variables=False) -> None: 119 Generate and write the aspect-grid-only SVG file: 120 '{subject.name} - {chart_type} Chart - Aspect Grid Only.svg'. 121 """ 122 123 # Constants 124 _BASIC_CHART_VIEWBOX = "0 0 820 550.0" 125 _WIDE_CHART_VIEWBOX = "0 0 1200 546.0" 126 _TRANSIT_CHART_WITH_TABLE_VIWBOX = "0 0 960 546.0" 127 128 _DEFAULT_HEIGHT = 550 129 _DEFAULT_FULL_WIDTH = 1200 130 _DEFAULT_NATAL_WIDTH = 820 131 _DEFAULT_FULL_WIDTH_WITH_TABLE = 960 132 _PLANET_IN_ZODIAC_EXTRA_POINTS = 10 133 134 # Set at init 135 first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel] 136 second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] 137 chart_type: ChartType 138 new_output_directory: Union[Path, None] 139 new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] 140 output_directory: Path 141 new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] 142 theme: Union[KerykeionChartTheme, None] 143 double_chart_aspect_grid_type: Literal["list", "table"] 144 chart_language: KerykeionChartLanguage 145 active_points: List[Union[Planet, AxialCusps]] 146 active_aspects: List[ActiveAspect] 147 148 # Internal properties 149 fire: float 150 earth: float 151 air: float 152 water: float 153 first_circle_radius: float 154 second_circle_radius: float 155 third_circle_radius: float 156 width: Union[float, int] 157 language_settings: dict 158 chart_colors_settings: dict 159 planets_settings: dict 160 aspects_settings: dict 161 user: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel] 162 available_planets_setting: List[KerykeionSettingsCelestialPointModel] 163 height: float 164 location: str 165 geolat: float 166 geolon: float 167 template: str 168 169 def __init__( 170 self, 171 first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel], 172 chart_type: ChartType = "Natal", 173 second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] = None, 174 new_output_directory: Union[str, None] = None, 175 new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None, 176 theme: Union[KerykeionChartTheme, None] = "classic", 177 double_chart_aspect_grid_type: Literal["list", "table"] = "list", 178 chart_language: KerykeionChartLanguage = "EN", 179 active_points: List[Union[Planet, AxialCusps]] = DEFAULT_ACTIVE_POINTS, 180 active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS, 181 ): 182 """ 183 Initialize the chart generator with subject data and configuration options. 184 185 Args: 186 first_obj (AstrologicalSubject, AstrologicalSubjectModel, or CompositeSubjectModel): 187 Primary astrological subject instance. 188 chart_type (ChartType, optional): 189 Type of chart to generate (e.g., 'Natal', 'Transit'). 190 second_obj (AstrologicalSubject or AstrologicalSubjectModel, optional): 191 Secondary subject for Transit or Synastry charts. 192 new_output_directory (str or Path, optional): 193 Base directory to save generated SVG files. 194 new_settings_file (Path, dict, or KerykeionSettingsModel, optional): 195 Custom settings source for chart colors, fonts, and aspects. 196 theme (KerykeionChartTheme or None, optional): 197 CSS theme to apply; None for default styling. 198 double_chart_aspect_grid_type (Literal['list','table'], optional): 199 Layout style for double-chart aspect grids ('list' or 'table'). 200 chart_language (KerykeionChartLanguage, optional): 201 Language code for chart labels (e.g., 'EN', 'IT'). 202 active_points (List[Planet or AxialCusps], optional): 203 Celestial points to include in the chart visualization. 204 active_aspects (List[ActiveAspect], optional): 205 Aspects to calculate, each defined by name and orb. 206 """ 207 home_directory = Path.home() 208 self.new_settings_file = new_settings_file 209 self.chart_language = chart_language 210 self.active_points = active_points 211 self.active_aspects = active_aspects 212 213 if new_output_directory: 214 self.output_directory = Path(new_output_directory) 215 else: 216 self.output_directory = home_directory 217 218 self.parse_json_settings(new_settings_file) 219 self.chart_type = chart_type 220 221 # Kerykeion instance 222 self.user = first_obj 223 224 self.available_planets_setting = [] 225 for body in self.planets_settings: 226 if body["name"] not in active_points: 227 continue 228 else: 229 body["is_active"] = True 230 231 self.available_planets_setting.append(body) 232 233 # Available bodies 234 available_celestial_points_names = [] 235 for body in self.available_planets_setting: 236 available_celestial_points_names.append(body["name"].lower()) 237 238 self.available_kerykeion_celestial_points: list[KerykeionPointModel] = [] 239 for body in available_celestial_points_names: 240 self.available_kerykeion_celestial_points.append(self.user.get(body)) 241 242 # Makes the sign number list. 243 if self.chart_type == "Natal" or self.chart_type == "ExternalNatal": 244 natal_aspects_instance = NatalAspects( 245 self.user, new_settings_file=self.new_settings_file, 246 active_points=active_points, 247 active_aspects=active_aspects, 248 ) 249 self.aspects_list = natal_aspects_instance.relevant_aspects 250 251 elif self.chart_type == "Transit" or self.chart_type == "Synastry": 252 if not second_obj: 253 raise KerykeionException("Second object is required for Transit or Synastry charts.") 254 255 # Kerykeion instance 256 self.t_user = second_obj 257 258 # Aspects 259 if self.chart_type == "Transit": 260 synastry_aspects_instance = SynastryAspects( 261 self.t_user, 262 self.user, 263 new_settings_file=self.new_settings_file, 264 active_points=active_points, 265 active_aspects=active_aspects, 266 ) 267 268 else: 269 synastry_aspects_instance = SynastryAspects( 270 self.user, 271 self.t_user, 272 new_settings_file=self.new_settings_file, 273 active_points=active_points, 274 active_aspects=active_aspects, 275 ) 276 277 self.aspects_list = synastry_aspects_instance.relevant_aspects 278 279 self.t_available_kerykeion_celestial_points = [] 280 for body in available_celestial_points_names: 281 self.t_available_kerykeion_celestial_points.append(self.t_user.get(body)) 282 283 elif self.chart_type == "Composite": 284 if not isinstance(first_obj, CompositeSubjectModel): 285 raise KerykeionException("First object must be a CompositeSubjectModel instance.") 286 287 self.aspects_list = NatalAspects(self.user, new_settings_file=self.new_settings_file, active_points=active_points).relevant_aspects 288 289 # Double chart aspect grid type 290 self.double_chart_aspect_grid_type = double_chart_aspect_grid_type 291 292 # screen size 293 self.height = self._DEFAULT_HEIGHT 294 if self.chart_type == "Synastry" or self.chart_type == "Transit": 295 self.width = self._DEFAULT_FULL_WIDTH 296 elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit": 297 self.width = self._DEFAULT_FULL_WIDTH_WITH_TABLE 298 else: 299 self.width = self._DEFAULT_NATAL_WIDTH 300 301 if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]: 302 self.location = self.user.city 303 self.geolat = self.user.lat 304 self.geolon = self.user.lng 305 306 elif self.chart_type == "Composite": 307 self.location = "" 308 self.geolat = (self.user.first_subject.lat + self.user.second_subject.lat) / 2 309 self.geolon = (self.user.first_subject.lng + self.user.second_subject.lng) / 2 310 311 elif self.chart_type in ["Transit"]: 312 self.location = self.t_user.city 313 self.geolat = self.t_user.lat 314 self.geolon = self.t_user.lng 315 self.t_name = self.language_settings["transit_name"] 316 317 # Default radius for the chart 318 self.main_radius = 240 319 320 # Set circle radii based on chart type 321 if self.chart_type == "ExternalNatal": 322 self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 56, 92, 112 323 else: 324 self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 0, 36, 120 325 326 # Initialize element points 327 self.fire = 0.0 328 self.earth = 0.0 329 self.air = 0.0 330 self.water = 0.0 331 332 # Calculate element points from planets 333 self._calculate_elements_points_from_planets() 334 335 # Set up theme 336 if theme not in get_args(KerykeionChartTheme) and theme is not None: 337 raise KerykeionException(f"Theme {theme} is not available. Set None for default theme.") 338 339 self.set_up_theme(theme) 340 341 def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None: 342 """ 343 Load and apply a CSS theme for the chart visualization. 344 345 Args: 346 theme (KerykeionChartTheme or None): Name of the theme to apply. If None, no CSS is applied. 347 """ 348 if theme is None: 349 self.color_style_tag = "" 350 return 351 352 theme_dir = Path(__file__).parent / "themes" 353 354 with open(theme_dir / f"{theme}.css", "r") as f: 355 self.color_style_tag = f.read() 356 357 def set_output_directory(self, dir_path: Path) -> None: 358 """ 359 Set the directory where generated SVG files will be saved. 360 361 Args: 362 dir_path (Path): Target directory for SVG output. 363 """ 364 self.output_directory = dir_path 365 logging.info(f"Output direcotry set to: {self.output_directory}") 366 367 def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None: 368 """ 369 Load and parse chart configuration settings. 370 371 Args: 372 settings_file_or_dict (Path, dict, or KerykeionSettingsModel): 373 Source for custom chart settings. 374 """ 375 settings = get_settings(settings_file_or_dict) 376 377 self.language_settings = settings["language_settings"][self.chart_language] 378 self.chart_colors_settings = settings["chart_colors"] 379 self.planets_settings = settings["celestial_points"] 380 self.aspects_settings = settings["aspects"] 381 382 def _draw_zodiac_circle_slices(self, r): 383 """ 384 Draw zodiac circle slices for each sign. 385 386 Args: 387 r (float): Outer radius of the zodiac ring. 388 389 Returns: 390 str: Concatenated SVG elements for zodiac slices. 391 """ 392 sings = get_args(Sign) 393 output = "" 394 for i, sing in enumerate(sings): 395 output += draw_zodiac_slice( 396 c1=self.first_circle_radius, 397 chart_type=self.chart_type, 398 seventh_house_degree_ut=self.user.seventh_house.abs_pos, 399 num=i, 400 r=r, 401 style=f'fill:{self.chart_colors_settings[f"zodiac_bg_{i}"]}; fill-opacity: 0.5;', 402 type=sing, 403 ) 404 405 return output 406 407 def _calculate_elements_points_from_planets(self): 408 """ 409 Compute elemental point totals based on active planetary positions. 410 411 Iterates over each active planet to determine its zodiac element and adds extra points 412 if the planet is in a related sign. Updates self.fire, self.earth, self.air, and self.water. 413 414 Returns: 415 None 416 """ 417 418 ZODIAC = ( 419 {"name": "Ari", "element": "fire"}, 420 {"name": "Tau", "element": "earth"}, 421 {"name": "Gem", "element": "air"}, 422 {"name": "Can", "element": "water"}, 423 {"name": "Leo", "element": "fire"}, 424 {"name": "Vir", "element": "earth"}, 425 {"name": "Lib", "element": "air"}, 426 {"name": "Sco", "element": "water"}, 427 {"name": "Sag", "element": "fire"}, 428 {"name": "Cap", "element": "earth"}, 429 {"name": "Aqu", "element": "air"}, 430 {"name": "Pis", "element": "water"}, 431 ) 432 433 # Available bodies 434 available_celestial_points_names = [] 435 for body in self.available_planets_setting: 436 available_celestial_points_names.append(body["name"].lower()) 437 438 # Make list of the points sign 439 points_sign = [] 440 for planet in available_celestial_points_names: 441 points_sign.append(self.user.get(planet).sign_num) 442 443 for i in range(len(self.available_planets_setting)): 444 # element: get extra points if planet is in own zodiac sign. 445 related_zodiac_signs = self.available_planets_setting[i]["related_zodiac_signs"] 446 cz = points_sign[i] 447 extra_points = 0 448 if related_zodiac_signs != []: 449 for e in range(len(related_zodiac_signs)): 450 if int(related_zodiac_signs[e]) == int(cz): 451 extra_points = self._PLANET_IN_ZODIAC_EXTRA_POINTS 452 453 ele = ZODIAC[points_sign[i]]["element"] 454 if ele == "fire": 455 self.fire = self.fire + self.available_planets_setting[i]["element_points"] + extra_points 456 457 elif ele == "earth": 458 self.earth = self.earth + self.available_planets_setting[i]["element_points"] + extra_points 459 460 elif ele == "air": 461 self.air = self.air + self.available_planets_setting[i]["element_points"] + extra_points 462 463 elif ele == "water": 464 self.water = self.water + self.available_planets_setting[i]["element_points"] + extra_points 465 466 def _draw_all_aspects_lines(self, r, ar): 467 """ 468 Render SVG lines for all aspects in the chart. 469 470 Args: 471 r (float): Radius at which aspect lines originate. 472 ar (float): Radius at which aspect lines terminate. 473 474 Returns: 475 str: SVG markup for all aspect lines. 476 """ 477 out = "" 478 for aspect in self.aspects_list: 479 aspect_name = aspect["aspect"] 480 aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None) 481 if aspect_color: 482 out += draw_aspect_line( 483 r=r, 484 ar=ar, 485 aspect=aspect, 486 color=aspect_color, 487 seventh_house_degree_ut=self.user.seventh_house.abs_pos 488 ) 489 return out 490 491 def _draw_all_transit_aspects_lines(self, r, ar): 492 """ 493 Render SVG lines for all transit aspects in the chart. 494 495 Args: 496 r (float): Radius at which transit aspect lines originate. 497 ar (float): Radius at which transit aspect lines terminate. 498 499 Returns: 500 str: SVG markup for all transit aspect lines. 501 """ 502 out = "" 503 for aspect in self.aspects_list: 504 aspect_name = aspect["aspect"] 505 aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None) 506 if aspect_color: 507 out += draw_aspect_line( 508 r=r, 509 ar=ar, 510 aspect=aspect, 511 color=aspect_color, 512 seventh_house_degree_ut=self.user.seventh_house.abs_pos 513 ) 514 return out 515 516 def _create_template_dictionary(self) -> ChartTemplateDictionary: 517 """ 518 Assemble chart data and rendering instructions into a template dictionary. 519 520 Gathers styling, dimensions, and SVG fragments for chart components based on 521 chart type and subjects. 522 523 Returns: 524 ChartTemplateDictionary: Populated structure of template variables. 525 """ 526 # Initialize template dictionary 527 template_dict: dict = {} 528 529 # Set the color style tag 530 template_dict["color_style_tag"] = self.color_style_tag 531 532 # Set chart dimensions 533 template_dict["chart_height"] = self.height 534 template_dict["chart_width"] = self.width 535 536 # Set viewbox based on chart type 537 if self.chart_type in ["Natal", "ExternalNatal", "Composite"]: 538 template_dict['viewbox'] = self._BASIC_CHART_VIEWBOX 539 elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit": 540 template_dict['viewbox'] = self._TRANSIT_CHART_WITH_TABLE_VIWBOX 541 else: 542 template_dict['viewbox'] = self._WIDE_CHART_VIEWBOX 543 544 # Generate rings and circles based on chart type 545 if self.chart_type in ["Transit", "Synastry"]: 546 template_dict["transitRing"] = draw_transit_ring(self.main_radius, self.chart_colors_settings["paper_1"], self.chart_colors_settings["zodiac_transit_ring_3"]) 547 template_dict["degreeRing"] = draw_transit_ring_degree_steps(self.main_radius, self.user.seventh_house.abs_pos) 548 template_dict["first_circle"] = draw_first_circle(self.main_radius, self.chart_colors_settings["zodiac_transit_ring_2"], self.chart_type) 549 template_dict["second_circle"] = draw_second_circle(self.main_radius, self.chart_colors_settings['zodiac_transit_ring_1'], self.chart_colors_settings['paper_1'], self.chart_type) 550 template_dict['third_circle'] = draw_third_circle(self.main_radius, self.chart_colors_settings['zodiac_transit_ring_0'], self.chart_colors_settings['paper_1'], self.chart_type, self.third_circle_radius) 551 552 if self.double_chart_aspect_grid_type == "list": 553 title = "" 554 if self.chart_type == "Synastry": 555 title = self.language_settings.get("couple_aspects", "Couple Aspects") 556 else: 557 title = self.language_settings.get("transit_aspects", "Transit Aspects") 558 559 template_dict["makeAspectGrid"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings) 560 else: 561 template_dict["makeAspectGrid"] = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, 550, 450) 562 563 template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160) 564 else: 565 template_dict["transitRing"] = "" 566 template_dict["degreeRing"] = draw_degree_ring(self.main_radius, self.first_circle_radius, self.user.seventh_house.abs_pos, self.chart_colors_settings["paper_0"]) 567 template_dict['first_circle'] = draw_first_circle(self.main_radius, self.chart_colors_settings["zodiac_radix_ring_2"], self.chart_type, self.first_circle_radius) 568 template_dict["second_circle"] = draw_second_circle(self.main_radius, self.chart_colors_settings["zodiac_radix_ring_1"], self.chart_colors_settings["paper_1"], self.chart_type, self.second_circle_radius) 569 template_dict['third_circle'] = draw_third_circle(self.main_radius, self.chart_colors_settings["zodiac_radix_ring_0"], self.chart_colors_settings["paper_1"], self.chart_type, self.third_circle_radius) 570 template_dict["makeAspectGrid"] = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list) 571 572 template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius) 573 574 # Set chart title 575 if self.chart_type == "Synastry": 576 template_dict["stringTitle"] = f"{self.user.name} {self.language_settings['and_word']} {self.t_user.name}" 577 elif self.chart_type == "Transit": 578 template_dict["stringTitle"] = f"{self.language_settings['transits']} {self.t_user.day}/{self.t_user.month}/{self.t_user.year}" 579 elif self.chart_type in ["Natal", "ExternalNatal"]: 580 template_dict["stringTitle"] = self.user.name 581 elif self.chart_type == "Composite": 582 template_dict["stringTitle"] = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}" 583 584 # Zodiac Type Info 585 if self.user.zodiac_type == 'Tropic': 586 zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}" 587 else: 588 mode_const = "SIDM_" + self.user.sidereal_mode # type: ignore 589 mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const)) 590 zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}" 591 592 template_dict["bottom_left_0"] = f"{self.language_settings.get('houses_system_' + self.user.houses_system_identifier, self.user.houses_system_name)} {self.language_settings.get('houses', 'Houses')}" 593 template_dict["bottom_left_1"] = zodiac_info 594 595 if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]: 596 template_dict["bottom_left_2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")} {self.language_settings.get("day", "Day").lower()}: {self.user.lunar_phase.get("moon_phase", "")}' 597 template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get(self.user.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.user.lunar_phase.moon_phase_name)}' 598 template_dict["bottom_left_4"] = f'{self.language_settings.get(self.user.perspective_type.lower().replace(" ", "_"), self.user.perspective_type)}' 599 elif self.chart_type == "Transit": 600 template_dict["bottom_left_2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get("day", "Day")} {self.t_user.lunar_phase.get("moon_phase", "")}' 601 template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.t_user.lunar_phase.moon_phase_name}' 602 template_dict["bottom_left_4"] = f'{self.language_settings.get(self.t_user.perspective_type.lower().replace(" ", "_"), self.t_user.perspective_type)}' 603 elif self.chart_type == "Composite": 604 template_dict["bottom_left_2"] = f'{self.user.first_subject.perspective_type}' 605 template_dict["bottom_left_3"] = f'{self.language_settings.get("composite_chart", "Composite Chart")} - {self.language_settings.get("midpoints", "Midpoints")}' 606 template_dict["bottom_left_4"] = "" 607 608 # Draw moon phase 609 moon_phase_dict = calculate_moon_phase_chart_params( 610 self.user.lunar_phase["degrees_between_s_m"], 611 self.geolat 612 ) 613 614 template_dict["lunar_phase_rotate"] = moon_phase_dict["lunar_phase_rotate"] 615 template_dict["lunar_phase_circle_center_x"] = moon_phase_dict["circle_center_x"] 616 template_dict["lunar_phase_circle_radius"] = moon_phase_dict["circle_radius"] 617 618 if self.chart_type == "Composite": 619 template_dict["top_left_1"] = f"{datetime.fromisoformat(self.user.first_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}" 620 # Set location string 621 elif len(self.location) > 35: 622 split_location = self.location.split(",") 623 if len(split_location) > 1: 624 template_dict["top_left_1"] = split_location[0] + ", " + split_location[-1] 625 if len(template_dict["top_left_1"]) > 35: 626 template_dict["top_left_1"] = template_dict["top_left_1"][:35] + "..." 627 else: 628 template_dict["top_left_1"] = self.location[:35] + "..." 629 else: 630 template_dict["top_left_1"] = self.location 631 632 # Set chart name 633 if self.chart_type in ["Synastry", "Transit"]: 634 template_dict["top_left_0"] = f"{self.user.name}:" 635 elif self.chart_type in ["Natal", "ExternalNatal"]: 636 template_dict["top_left_0"] = f'{self.language_settings["info"]}:' 637 elif self.chart_type == "Composite": 638 template_dict["top_left_0"] = f'{self.user.first_subject.name}' 639 640 # Set additional information for Synastry chart type 641 if self.chart_type == "Synastry": 642 template_dict["top_left_3"] = f"{self.t_user.name}: " 643 template_dict["top_left_4"] = self.t_user.city 644 template_dict["top_left_5"] = f"{self.t_user.year}-{self.t_user.month}-{self.t_user.day} {self.t_user.hour:02d}:{self.t_user.minute:02d}" 645 elif self.chart_type == "Composite": 646 template_dict["top_left_3"] = self.user.second_subject.name 647 template_dict["top_left_4"] = f"{datetime.fromisoformat(self.user.second_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}" 648 latitude_string = convert_latitude_coordinate_to_string(self.user.second_subject.lat, self.language_settings['north_letter'], self.language_settings['south_letter']) 649 longitude_string = convert_longitude_coordinate_to_string(self.user.second_subject.lng, self.language_settings['east_letter'], self.language_settings['west_letter']) 650 template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}" 651 else: 652 latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings['north'], self.language_settings['south']) 653 longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings['east'], self.language_settings['west']) 654 template_dict["top_left_3"] = f"{self.language_settings['latitude']}: {latitude_string}" 655 template_dict["top_left_4"] = f"{self.language_settings['longitude']}: {longitude_string}" 656 template_dict["top_left_5"] = f"{self.language_settings['type']}: {self.language_settings.get(self.chart_type, self.chart_type)}" 657 658 659 # Set paper colors 660 template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"] 661 template_dict["paper_color_1"] = self.chart_colors_settings["paper_1"] 662 663 # Set planet colors 664 for planet in self.planets_settings: 665 planet_id = planet["id"] 666 template_dict[f"planets_color_{planet_id}"] = planet["color"] # type: ignore 667 668 # Set zodiac colors 669 for i in range(12): 670 template_dict[f"zodiac_color_{i}"] = self.chart_colors_settings[f"zodiac_icon_{i}"] # type: ignore 671 672 # Set orb colors 673 for aspect in self.aspects_settings: 674 template_dict[f"orb_color_{aspect['degree']}"] = aspect['color'] # type: ignore 675 676 # Drawing functions 677 template_dict["makeZodiac"] = self._draw_zodiac_circle_slices(self.main_radius) 678 679 first_subject_houses_list = get_houses_list(self.user) 680 681 # Draw houses grid and cusps 682 if self.chart_type in ["Transit", "Synastry"]: 683 second_subject_houses_list = get_houses_list(self.t_user) 684 685 template_dict["makeHousesGrid"] = draw_house_grid( 686 main_subject_houses_list=first_subject_houses_list, 687 secondary_subject_houses_list=second_subject_houses_list, 688 chart_type=self.chart_type, 689 text_color=self.chart_colors_settings["paper_0"], 690 house_cusp_generale_name_label=self.language_settings["cusp"] 691 ) 692 693 template_dict["makeHouses"] = draw_houses_cusps_and_text_number( 694 r=self.main_radius, 695 first_subject_houses_list=first_subject_houses_list, 696 standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"], 697 first_house_color=self.planets_settings[12]["color"], 698 tenth_house_color=self.planets_settings[13]["color"], 699 seventh_house_color=self.planets_settings[14]["color"], 700 fourth_house_color=self.planets_settings[15]["color"], 701 c1=self.first_circle_radius, 702 c3=self.third_circle_radius, 703 chart_type=self.chart_type, 704 second_subject_houses_list=second_subject_houses_list, 705 transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"], 706 ) 707 708 else: 709 template_dict["makeHousesGrid"] = draw_house_grid( 710 main_subject_houses_list=first_subject_houses_list, 711 chart_type=self.chart_type, 712 text_color=self.chart_colors_settings["paper_0"], 713 house_cusp_generale_name_label=self.language_settings["cusp"] 714 ) 715 716 template_dict["makeHouses"] = draw_houses_cusps_and_text_number( 717 r=self.main_radius, 718 first_subject_houses_list=first_subject_houses_list, 719 standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"], 720 first_house_color=self.planets_settings[12]["color"], 721 tenth_house_color=self.planets_settings[13]["color"], 722 seventh_house_color=self.planets_settings[14]["color"], 723 fourth_house_color=self.planets_settings[15]["color"], 724 c1=self.first_circle_radius, 725 c3=self.third_circle_radius, 726 chart_type=self.chart_type, 727 ) 728 729 # Draw planets 730 if self.chart_type in ["Transit", "Synastry"]: 731 template_dict["makePlanets"] = draw_planets( 732 available_kerykeion_celestial_points=self.available_kerykeion_celestial_points, 733 available_planets_setting=self.available_planets_setting, 734 second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points, 735 radius=self.main_radius, 736 main_subject_first_house_degree_ut=self.user.first_house.abs_pos, 737 main_subject_seventh_house_degree_ut=self.user.seventh_house.abs_pos, 738 chart_type=self.chart_type, 739 third_circle_radius=self.third_circle_radius, 740 ) 741 else: 742 template_dict["makePlanets"] = draw_planets( 743 available_planets_setting=self.available_planets_setting, 744 chart_type=self.chart_type, 745 radius=self.main_radius, 746 available_kerykeion_celestial_points=self.available_kerykeion_celestial_points, 747 third_circle_radius=self.third_circle_radius, 748 main_subject_first_house_degree_ut=self.user.first_house.abs_pos, 749 main_subject_seventh_house_degree_ut=self.user.seventh_house.abs_pos 750 ) 751 752 # Draw elements percentages 753 total = self.fire + self.water + self.earth + self.air 754 755 fire_percentage = int(round(100 * self.fire / total)) 756 earth_percentage = int(round(100 * self.earth / total)) 757 air_percentage = int(round(100 * self.air / total)) 758 water_percentage = int(round(100 * self.water / total)) 759 760 template_dict["fire_string"] = f"{self.language_settings['fire']} {fire_percentage}%" 761 template_dict["earth_string"] = f"{self.language_settings['earth']} {earth_percentage}%" 762 template_dict["air_string"] = f"{self.language_settings['air']} {air_percentage}%" 763 template_dict["water_string"] = f"{self.language_settings['water']} {water_percentage}%" 764 765 # Draw planet grid 766 if self.chart_type in ["Transit", "Synastry"]: 767 if self.chart_type == "Transit": 768 second_subject_table_name = self.language_settings["transit_name"] 769 else: 770 second_subject_table_name = self.t_user.name 771 772 template_dict["makePlanetGrid"] = draw_planet_grid( 773 planets_and_houses_grid_title=self.language_settings["planets_and_house"], 774 subject_name=self.user.name, 775 available_kerykeion_celestial_points=self.available_kerykeion_celestial_points, 776 chart_type=self.chart_type, 777 text_color=self.chart_colors_settings["paper_0"], 778 celestial_point_language=self.language_settings["celestial_points"], 779 second_subject_name=second_subject_table_name, 780 second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points, 781 ) 782 else: 783 if self.chart_type == "Composite": 784 subject_name = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}" 785 else: 786 subject_name = self.user.name 787 788 template_dict["makePlanetGrid"] = draw_planet_grid( 789 planets_and_houses_grid_title=self.language_settings["planets_and_house"], 790 subject_name=subject_name, 791 available_kerykeion_celestial_points=self.available_kerykeion_celestial_points, 792 chart_type=self.chart_type, 793 text_color=self.chart_colors_settings["paper_0"], 794 celestial_point_language=self.language_settings["celestial_points"], 795 ) 796 797 # Set date time string 798 if self.chart_type in ["Composite"]: 799 # First Subject Latitude and Longitude 800 latitude = convert_latitude_coordinate_to_string(self.user.first_subject.lat, self.language_settings["north_letter"], self.language_settings["south_letter"]) 801 longitude = convert_longitude_coordinate_to_string(self.user.first_subject.lng, self.language_settings["east_letter"], self.language_settings["west_letter"]) 802 template_dict["top_left_2"] = f"{latitude} {longitude}" 803 else: 804 dt = datetime.fromisoformat(self.user.iso_formatted_local_datetime) 805 custom_format = dt.strftime('%Y-%m-%d %H:%M [%z]') 806 custom_format = custom_format[:-3] + ':' + custom_format[-3:] 807 template_dict["top_left_2"] = f"{custom_format}" 808 809 return ChartTemplateDictionary(**template_dict) 810 811 def makeTemplate(self, minify: bool = False, remove_css_variables = False) -> str: 812 """ 813 Render the full chart SVG as a string. 814 815 Reads the XML template, substitutes variables, and optionally inlines CSS 816 variables and minifies the output. 817 818 Args: 819 minify (bool): Remove whitespace and quotes for compactness. 820 remove_css_variables (bool): Embed CSS variable definitions. 821 822 Returns: 823 str: SVG markup as a string. 824 """ 825 td = self._create_template_dictionary() 826 827 DATA_DIR = Path(__file__).parent 828 xml_svg = DATA_DIR / "templates" / "chart.xml" 829 830 # read template 831 with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f: 832 template = Template(f.read()).substitute(td) 833 834 # return filename 835 836 logging.debug(f"Template dictionary keys: {td.keys()}") 837 838 self._create_template_dictionary() 839 840 if remove_css_variables: 841 template = inline_css_variables_in_svg(template) 842 843 if minify: 844 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 845 846 else: 847 template = template.replace('"', "'") 848 849 return template 850 851 def makeSVG(self, minify: bool = False, remove_css_variables = False): 852 """ 853 Generate and save the full chart SVG to disk. 854 855 Calls makeTemplate to render the SVG, then writes a file named 856 "{subject.name} - {chart_type} Chart.svg" in the output directory. 857 858 Args: 859 minify (bool): Pass-through to makeTemplate for compact output. 860 remove_css_variables (bool): Pass-through to makeTemplate to embed CSS variables. 861 862 Returns: 863 None 864 """ 865 866 self.template = self.makeTemplate(minify, remove_css_variables) 867 868 chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg" 869 870 with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file: 871 output_file.write(self.template) 872 873 print(f"SVG Generated Correctly in: {chartname}") 874 875 def makeWheelOnlyTemplate(self, minify: bool = False, remove_css_variables = False): 876 """ 877 Render the wheel-only chart SVG as a string. 878 879 Reads the wheel-only XML template, substitutes chart data, and applies optional 880 CSS inlining and minification. 881 882 Args: 883 minify (bool): Remove whitespace and quotes for compactness. 884 remove_css_variables (bool): Embed CSS variable definitions. 885 886 Returns: 887 str: SVG markup for the chart wheel only. 888 """ 889 890 with open(Path(__file__).parent / "templates" / "wheel_only.xml", "r", encoding="utf-8", errors="ignore") as f: 891 template = f.read() 892 893 template_dict = self._create_template_dictionary() 894 template = Template(template).substitute(template_dict) 895 896 if remove_css_variables: 897 template = inline_css_variables_in_svg(template) 898 899 if minify: 900 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 901 902 else: 903 template = template.replace('"', "'") 904 905 return template 906 907 def makeWheelOnlySVG(self, minify: bool = False, remove_css_variables = False): 908 """ 909 Generate and save wheel-only chart SVG to disk. 910 911 Calls makeWheelOnlyTemplate and writes a file named 912 "{subject.name} - {chart_type} Chart - Wheel Only.svg" in the output directory. 913 914 Args: 915 minify (bool): Pass-through to makeWheelOnlyTemplate for compact output. 916 remove_css_variables (bool): Pass-through to makeWheelOnlyTemplate to embed CSS variables. 917 918 Returns: 919 None 920 """ 921 922 template = self.makeWheelOnlyTemplate(minify, remove_css_variables) 923 chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Wheel Only.svg" 924 925 with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file: 926 output_file.write(template) 927 928 print(f"SVG Generated Correctly in: {chartname}") 929 930 def makeAspectGridOnlyTemplate(self, minify: bool = False, remove_css_variables = False): 931 """ 932 Render the aspect-grid-only chart SVG as a string. 933 934 Reads the aspect-grid XML template, generates the aspect grid based on chart type, 935 and applies optional CSS inlining and minification. 936 937 Args: 938 minify (bool): Remove whitespace and quotes for compactness. 939 remove_css_variables (bool): Embed CSS variable definitions. 940 941 Returns: 942 str: SVG markup for the aspect grid only. 943 """ 944 945 with open(Path(__file__).parent / "templates" / "aspect_grid_only.xml", "r", encoding="utf-8", errors="ignore") as f: 946 template = f.read() 947 948 template_dict = self._create_template_dictionary() 949 950 if self.chart_type in ["Transit", "Synastry"]: 951 aspects_grid = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list) 952 else: 953 aspects_grid = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, x_start=50, y_start=250) 954 955 template = Template(template).substitute({**template_dict, "makeAspectGrid": aspects_grid}) 956 957 if remove_css_variables: 958 template = inline_css_variables_in_svg(template) 959 960 if minify: 961 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 962 963 else: 964 template = template.replace('"', "'") 965 966 return template 967 968 def makeAspectGridOnlySVG(self, minify: bool = False, remove_css_variables = False): 969 """ 970 Generate and save aspect-grid-only chart SVG to disk. 971 972 Calls makeAspectGridOnlyTemplate and writes a file named 973 "{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the output directory. 974 975 Args: 976 minify (bool): Pass-through to makeAspectGridOnlyTemplate for compact output. 977 remove_css_variables (bool): Pass-through to makeAspectGridOnlyTemplate to embed CSS variables. 978 979 Returns: 980 None 981 """ 982 983 template = self.makeAspectGridOnlyTemplate(minify, remove_css_variables) 984 chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Aspect Grid Only.svg" 985 986 with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file: 987 output_file.write(template) 988 989 print(f"SVG Generated Correctly in: {chartname}")
KerykeionChartSVG generates astrological chart visualizations as SVG files.
This class supports creating full chart SVGs, wheel-only SVGs, and aspect-grid-only SVGs for various chart types including Natal, ExternalNatal, Transit, Synastry, and Composite. Charts are rendered using XML templates and drawing utilities, with customizable themes, language, active points, and aspects. The rendered SVGs can be saved to a specified output directory or, by default, to the user's home directory.
NOTE: The generated SVG files are optimized for web use, opening in browsers. If you want to use them in other applications, you might need to adjust the SVG settings or styles.
Args: first_obj (AstrologicalSubject | AstrologicalSubjectModel | CompositeSubjectModel): The primary astrological subject for the chart. chart_type (ChartType, optional): The type of chart to generate ('Natal', 'ExternalNatal', 'Transit', 'Synastry', 'Composite'). Defaults to 'Natal'. second_obj (AstrologicalSubject | AstrologicalSubjectModel, optional): The secondary subject for Transit or Synastry charts. Not required for Natal or Composite. new_output_directory (str | Path, optional): Directory to write generated SVG files. Defaults to the user's home directory. new_settings_file (Path | dict | KerykeionSettingsModel, optional): Path or settings object to override default chart configuration (colors, fonts, aspects). theme (KerykeionChartTheme, optional): CSS theme for the chart. If None, no default styles are applied. Defaults to 'classic'. double_chart_aspect_grid_type (Literal['list', 'table'], optional): Specifies rendering style for double-chart aspect grids. Defaults to 'list'. chart_language (KerykeionChartLanguage, optional): Language code for chart labels. Defaults to 'EN'. active_points (list[Planet | AxialCusps], optional): List of celestial points and angles to include. Defaults to DEFAULT_ACTIVE_POINTS. Example: ["Sun", "Moon", "Mercury", "Venus"]
active_aspects (list[ActiveAspect], optional):
List of aspects (name and orb) to calculate. Defaults to DEFAULT_ACTIVE_ASPECTS.
Example:
[
{"name": "conjunction", "orb": 10},
{"name": "opposition", "orb": 10},
{"name": "trine", "orb": 8},
{"name": "sextile", "orb": 6},
{"name": "square", "orb": 5},
{"name": "quintile", "orb": 1},
]
Public Methods:
makeTemplate(minify=False, remove_css_variables=False) -> str:
Render the full chart SVG as a string without writing to disk. Use minify=True
to remove whitespace and quotes, and remove_css_variables=True
to embed CSS vars.
makeSVG(minify=False, remove_css_variables=False) -> None:
Generate and write the full chart SVG file to the output directory.
Filenames follow the pattern:
'{subject.name} - {chart_type} Chart.svg'.
makeWheelOnlyTemplate(minify=False, remove_css_variables=False) -> str:
Render only the chart wheel (no aspect grid) as an SVG string.
makeWheelOnlySVG(minify=False, remove_css_variables=False) -> None:
Generate and write the wheel-only SVG file:
'{subject.name} - {chart_type} Chart - Wheel Only.svg'.
makeAspectGridOnlyTemplate(minify=False, remove_css_variables=False) -> str:
Render only the aspect grid as an SVG string.
makeAspectGridOnlySVG(minify=False, remove_css_variables=False) -> None:
Generate and write the aspect-grid-only SVG file:
'{subject.name} - {chart_type} Chart - Aspect Grid Only.svg'.
169 def __init__( 170 self, 171 first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel], 172 chart_type: ChartType = "Natal", 173 second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] = None, 174 new_output_directory: Union[str, None] = None, 175 new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None, 176 theme: Union[KerykeionChartTheme, None] = "classic", 177 double_chart_aspect_grid_type: Literal["list", "table"] = "list", 178 chart_language: KerykeionChartLanguage = "EN", 179 active_points: List[Union[Planet, AxialCusps]] = DEFAULT_ACTIVE_POINTS, 180 active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS, 181 ): 182 """ 183 Initialize the chart generator with subject data and configuration options. 184 185 Args: 186 first_obj (AstrologicalSubject, AstrologicalSubjectModel, or CompositeSubjectModel): 187 Primary astrological subject instance. 188 chart_type (ChartType, optional): 189 Type of chart to generate (e.g., 'Natal', 'Transit'). 190 second_obj (AstrologicalSubject or AstrologicalSubjectModel, optional): 191 Secondary subject for Transit or Synastry charts. 192 new_output_directory (str or Path, optional): 193 Base directory to save generated SVG files. 194 new_settings_file (Path, dict, or KerykeionSettingsModel, optional): 195 Custom settings source for chart colors, fonts, and aspects. 196 theme (KerykeionChartTheme or None, optional): 197 CSS theme to apply; None for default styling. 198 double_chart_aspect_grid_type (Literal['list','table'], optional): 199 Layout style for double-chart aspect grids ('list' or 'table'). 200 chart_language (KerykeionChartLanguage, optional): 201 Language code for chart labels (e.g., 'EN', 'IT'). 202 active_points (List[Planet or AxialCusps], optional): 203 Celestial points to include in the chart visualization. 204 active_aspects (List[ActiveAspect], optional): 205 Aspects to calculate, each defined by name and orb. 206 """ 207 home_directory = Path.home() 208 self.new_settings_file = new_settings_file 209 self.chart_language = chart_language 210 self.active_points = active_points 211 self.active_aspects = active_aspects 212 213 if new_output_directory: 214 self.output_directory = Path(new_output_directory) 215 else: 216 self.output_directory = home_directory 217 218 self.parse_json_settings(new_settings_file) 219 self.chart_type = chart_type 220 221 # Kerykeion instance 222 self.user = first_obj 223 224 self.available_planets_setting = [] 225 for body in self.planets_settings: 226 if body["name"] not in active_points: 227 continue 228 else: 229 body["is_active"] = True 230 231 self.available_planets_setting.append(body) 232 233 # Available bodies 234 available_celestial_points_names = [] 235 for body in self.available_planets_setting: 236 available_celestial_points_names.append(body["name"].lower()) 237 238 self.available_kerykeion_celestial_points: list[KerykeionPointModel] = [] 239 for body in available_celestial_points_names: 240 self.available_kerykeion_celestial_points.append(self.user.get(body)) 241 242 # Makes the sign number list. 243 if self.chart_type == "Natal" or self.chart_type == "ExternalNatal": 244 natal_aspects_instance = NatalAspects( 245 self.user, new_settings_file=self.new_settings_file, 246 active_points=active_points, 247 active_aspects=active_aspects, 248 ) 249 self.aspects_list = natal_aspects_instance.relevant_aspects 250 251 elif self.chart_type == "Transit" or self.chart_type == "Synastry": 252 if not second_obj: 253 raise KerykeionException("Second object is required for Transit or Synastry charts.") 254 255 # Kerykeion instance 256 self.t_user = second_obj 257 258 # Aspects 259 if self.chart_type == "Transit": 260 synastry_aspects_instance = SynastryAspects( 261 self.t_user, 262 self.user, 263 new_settings_file=self.new_settings_file, 264 active_points=active_points, 265 active_aspects=active_aspects, 266 ) 267 268 else: 269 synastry_aspects_instance = SynastryAspects( 270 self.user, 271 self.t_user, 272 new_settings_file=self.new_settings_file, 273 active_points=active_points, 274 active_aspects=active_aspects, 275 ) 276 277 self.aspects_list = synastry_aspects_instance.relevant_aspects 278 279 self.t_available_kerykeion_celestial_points = [] 280 for body in available_celestial_points_names: 281 self.t_available_kerykeion_celestial_points.append(self.t_user.get(body)) 282 283 elif self.chart_type == "Composite": 284 if not isinstance(first_obj, CompositeSubjectModel): 285 raise KerykeionException("First object must be a CompositeSubjectModel instance.") 286 287 self.aspects_list = NatalAspects(self.user, new_settings_file=self.new_settings_file, active_points=active_points).relevant_aspects 288 289 # Double chart aspect grid type 290 self.double_chart_aspect_grid_type = double_chart_aspect_grid_type 291 292 # screen size 293 self.height = self._DEFAULT_HEIGHT 294 if self.chart_type == "Synastry" or self.chart_type == "Transit": 295 self.width = self._DEFAULT_FULL_WIDTH 296 elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit": 297 self.width = self._DEFAULT_FULL_WIDTH_WITH_TABLE 298 else: 299 self.width = self._DEFAULT_NATAL_WIDTH 300 301 if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]: 302 self.location = self.user.city 303 self.geolat = self.user.lat 304 self.geolon = self.user.lng 305 306 elif self.chart_type == "Composite": 307 self.location = "" 308 self.geolat = (self.user.first_subject.lat + self.user.second_subject.lat) / 2 309 self.geolon = (self.user.first_subject.lng + self.user.second_subject.lng) / 2 310 311 elif self.chart_type in ["Transit"]: 312 self.location = self.t_user.city 313 self.geolat = self.t_user.lat 314 self.geolon = self.t_user.lng 315 self.t_name = self.language_settings["transit_name"] 316 317 # Default radius for the chart 318 self.main_radius = 240 319 320 # Set circle radii based on chart type 321 if self.chart_type == "ExternalNatal": 322 self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 56, 92, 112 323 else: 324 self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 0, 36, 120 325 326 # Initialize element points 327 self.fire = 0.0 328 self.earth = 0.0 329 self.air = 0.0 330 self.water = 0.0 331 332 # Calculate element points from planets 333 self._calculate_elements_points_from_planets() 334 335 # Set up theme 336 if theme not in get_args(KerykeionChartTheme) and theme is not None: 337 raise KerykeionException(f"Theme {theme} is not available. Set None for default theme.") 338 339 self.set_up_theme(theme)
Initialize the chart generator with subject data and configuration options.
Args: first_obj (AstrologicalSubject, AstrologicalSubjectModel, or CompositeSubjectModel): Primary astrological subject instance. chart_type (ChartType, optional): Type of chart to generate (e.g., 'Natal', 'Transit'). second_obj (AstrologicalSubject or AstrologicalSubjectModel, optional): Secondary subject for Transit or Synastry charts. new_output_directory (str or Path, optional): Base directory to save generated SVG files. new_settings_file (Path, dict, or KerykeionSettingsModel, optional): Custom settings source for chart colors, fonts, and aspects. theme (KerykeionChartTheme or None, optional): CSS theme to apply; None for default styling. double_chart_aspect_grid_type (Literal['list','table'], optional): Layout style for double-chart aspect grids ('list' or 'table'). chart_language (KerykeionChartLanguage, optional): Language code for chart labels (e.g., 'EN', 'IT'). active_points (List[Planet or AxialCusps], optional): Celestial points to include in the chart visualization. active_aspects (List[ActiveAspect], optional): Aspects to calculate, each defined by name and orb.
341 def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None: 342 """ 343 Load and apply a CSS theme for the chart visualization. 344 345 Args: 346 theme (KerykeionChartTheme or None): Name of the theme to apply. If None, no CSS is applied. 347 """ 348 if theme is None: 349 self.color_style_tag = "" 350 return 351 352 theme_dir = Path(__file__).parent / "themes" 353 354 with open(theme_dir / f"{theme}.css", "r") as f: 355 self.color_style_tag = f.read()
Load and apply a CSS theme for the chart visualization.
Args: theme (KerykeionChartTheme or None): Name of the theme to apply. If None, no CSS is applied.
357 def set_output_directory(self, dir_path: Path) -> None: 358 """ 359 Set the directory where generated SVG files will be saved. 360 361 Args: 362 dir_path (Path): Target directory for SVG output. 363 """ 364 self.output_directory = dir_path 365 logging.info(f"Output direcotry set to: {self.output_directory}")
Set the directory where generated SVG files will be saved.
Args: dir_path (Path): Target directory for SVG output.
367 def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None: 368 """ 369 Load and parse chart configuration settings. 370 371 Args: 372 settings_file_or_dict (Path, dict, or KerykeionSettingsModel): 373 Source for custom chart settings. 374 """ 375 settings = get_settings(settings_file_or_dict) 376 377 self.language_settings = settings["language_settings"][self.chart_language] 378 self.chart_colors_settings = settings["chart_colors"] 379 self.planets_settings = settings["celestial_points"] 380 self.aspects_settings = settings["aspects"]
Load and parse chart configuration settings.
Args: settings_file_or_dict (Path, dict, or KerykeionSettingsModel): Source for custom chart settings.
811 def makeTemplate(self, minify: bool = False, remove_css_variables = False) -> str: 812 """ 813 Render the full chart SVG as a string. 814 815 Reads the XML template, substitutes variables, and optionally inlines CSS 816 variables and minifies the output. 817 818 Args: 819 minify (bool): Remove whitespace and quotes for compactness. 820 remove_css_variables (bool): Embed CSS variable definitions. 821 822 Returns: 823 str: SVG markup as a string. 824 """ 825 td = self._create_template_dictionary() 826 827 DATA_DIR = Path(__file__).parent 828 xml_svg = DATA_DIR / "templates" / "chart.xml" 829 830 # read template 831 with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f: 832 template = Template(f.read()).substitute(td) 833 834 # return filename 835 836 logging.debug(f"Template dictionary keys: {td.keys()}") 837 838 self._create_template_dictionary() 839 840 if remove_css_variables: 841 template = inline_css_variables_in_svg(template) 842 843 if minify: 844 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 845 846 else: 847 template = template.replace('"', "'") 848 849 return template
Render the full chart SVG as a string.
Reads the XML template, substitutes variables, and optionally inlines CSS variables and minifies the output.
Args: minify (bool): Remove whitespace and quotes for compactness. remove_css_variables (bool): Embed CSS variable definitions.
Returns: str: SVG markup as a string.
851 def makeSVG(self, minify: bool = False, remove_css_variables = False): 852 """ 853 Generate and save the full chart SVG to disk. 854 855 Calls makeTemplate to render the SVG, then writes a file named 856 "{subject.name} - {chart_type} Chart.svg" in the output directory. 857 858 Args: 859 minify (bool): Pass-through to makeTemplate for compact output. 860 remove_css_variables (bool): Pass-through to makeTemplate to embed CSS variables. 861 862 Returns: 863 None 864 """ 865 866 self.template = self.makeTemplate(minify, remove_css_variables) 867 868 chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg" 869 870 with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file: 871 output_file.write(self.template) 872 873 print(f"SVG Generated Correctly in: {chartname}")
Generate and save the full chart SVG to disk.
Calls makeTemplate to render the SVG, then writes a file named "{subject.name} - {chart_type} Chart.svg" in the output directory.
Args: minify (bool): Pass-through to makeTemplate for compact output. remove_css_variables (bool): Pass-through to makeTemplate to embed CSS variables.
Returns: None
875 def makeWheelOnlyTemplate(self, minify: bool = False, remove_css_variables = False): 876 """ 877 Render the wheel-only chart SVG as a string. 878 879 Reads the wheel-only XML template, substitutes chart data, and applies optional 880 CSS inlining and minification. 881 882 Args: 883 minify (bool): Remove whitespace and quotes for compactness. 884 remove_css_variables (bool): Embed CSS variable definitions. 885 886 Returns: 887 str: SVG markup for the chart wheel only. 888 """ 889 890 with open(Path(__file__).parent / "templates" / "wheel_only.xml", "r", encoding="utf-8", errors="ignore") as f: 891 template = f.read() 892 893 template_dict = self._create_template_dictionary() 894 template = Template(template).substitute(template_dict) 895 896 if remove_css_variables: 897 template = inline_css_variables_in_svg(template) 898 899 if minify: 900 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 901 902 else: 903 template = template.replace('"', "'") 904 905 return template
Render the wheel-only chart SVG as a string.
Reads the wheel-only XML template, substitutes chart data, and applies optional CSS inlining and minification.
Args: minify (bool): Remove whitespace and quotes for compactness. remove_css_variables (bool): Embed CSS variable definitions.
Returns: str: SVG markup for the chart wheel only.
907 def makeWheelOnlySVG(self, minify: bool = False, remove_css_variables = False): 908 """ 909 Generate and save wheel-only chart SVG to disk. 910 911 Calls makeWheelOnlyTemplate and writes a file named 912 "{subject.name} - {chart_type} Chart - Wheel Only.svg" in the output directory. 913 914 Args: 915 minify (bool): Pass-through to makeWheelOnlyTemplate for compact output. 916 remove_css_variables (bool): Pass-through to makeWheelOnlyTemplate to embed CSS variables. 917 918 Returns: 919 None 920 """ 921 922 template = self.makeWheelOnlyTemplate(minify, remove_css_variables) 923 chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Wheel Only.svg" 924 925 with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file: 926 output_file.write(template) 927 928 print(f"SVG Generated Correctly in: {chartname}")
Generate and save wheel-only chart SVG to disk.
Calls makeWheelOnlyTemplate and writes a file named "{subject.name} - {chart_type} Chart - Wheel Only.svg" in the output directory.
Args: minify (bool): Pass-through to makeWheelOnlyTemplate for compact output. remove_css_variables (bool): Pass-through to makeWheelOnlyTemplate to embed CSS variables.
Returns: None
930 def makeAspectGridOnlyTemplate(self, minify: bool = False, remove_css_variables = False): 931 """ 932 Render the aspect-grid-only chart SVG as a string. 933 934 Reads the aspect-grid XML template, generates the aspect grid based on chart type, 935 and applies optional CSS inlining and minification. 936 937 Args: 938 minify (bool): Remove whitespace and quotes for compactness. 939 remove_css_variables (bool): Embed CSS variable definitions. 940 941 Returns: 942 str: SVG markup for the aspect grid only. 943 """ 944 945 with open(Path(__file__).parent / "templates" / "aspect_grid_only.xml", "r", encoding="utf-8", errors="ignore") as f: 946 template = f.read() 947 948 template_dict = self._create_template_dictionary() 949 950 if self.chart_type in ["Transit", "Synastry"]: 951 aspects_grid = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list) 952 else: 953 aspects_grid = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, x_start=50, y_start=250) 954 955 template = Template(template).substitute({**template_dict, "makeAspectGrid": aspects_grid}) 956 957 if remove_css_variables: 958 template = inline_css_variables_in_svg(template) 959 960 if minify: 961 template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace(" ", "").replace(" ", "") 962 963 else: 964 template = template.replace('"', "'") 965 966 return template
Render the aspect-grid-only chart SVG as a string.
Reads the aspect-grid XML template, generates the aspect grid based on chart type, and applies optional CSS inlining and minification.
Args: minify (bool): Remove whitespace and quotes for compactness. remove_css_variables (bool): Embed CSS variable definitions.
Returns: str: SVG markup for the aspect grid only.
968 def makeAspectGridOnlySVG(self, minify: bool = False, remove_css_variables = False): 969 """ 970 Generate and save aspect-grid-only chart SVG to disk. 971 972 Calls makeAspectGridOnlyTemplate and writes a file named 973 "{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the output directory. 974 975 Args: 976 minify (bool): Pass-through to makeAspectGridOnlyTemplate for compact output. 977 remove_css_variables (bool): Pass-through to makeAspectGridOnlyTemplate to embed CSS variables. 978 979 Returns: 980 None 981 """ 982 983 template = self.makeAspectGridOnlyTemplate(minify, remove_css_variables) 984 chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Aspect Grid Only.svg" 985 986 with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file: 987 output_file.write(template) 988 989 print(f"SVG Generated Correctly in: {chartname}")
Generate and save aspect-grid-only chart SVG to disk.
Calls makeAspectGridOnlyTemplate and writes a file named "{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the output directory.
Args: minify (bool): Pass-through to makeAspectGridOnlyTemplate for compact output. remove_css_variables (bool): Pass-through to makeAspectGridOnlyTemplate to embed CSS variables.
Returns: None