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
  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    Creates the instance that can generate the chart with the
  51    function makeSVG().
  52
  53    Parameters:
  54        - first_obj: First kerykeion object
  55        - chart_type: Natal, ExternalNatal, Transit, Synastry (Default: Type="Natal").
  56        - second_obj: Second kerykeion object (Not required if type is Natal)
  57        - new_output_directory: Set the output directory (default: home directory).
  58        - new_settings_file: Set the settings file (default: kr.config.json).
  59            In the settings file you can set the language, colors, planets, aspects, etc.
  60        - theme: Set the theme for the chart (default: classic). If None the <style> tag will be empty.
  61            That's useful if you want to use your own CSS file customizing the value of the default theme variables.
  62        - double_chart_aspect_grid_type: Set the type of the aspect grid for the double chart (transit or synastry). (Default: list.)
  63        - chart_language: Set the language for the chart (default: EN).
  64        - active_points: Set the active points for the chart (default: DEFAULT_ACTIVE_POINTS). Example:
  65            ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", "True_Node", "True_South_Node", "Ascendant", "Medium_Coeli", "Descendant", "Imum_Coeli"]
  66        - active_aspects: Set the active aspects for the chart (default: DEFAULT_ACTIVE_ASPECTS). Example:
  67            [
  68                {"name": "conjunction", "orb": 10},
  69                {"name": "opposition", "orb": 10},
  70                {"name": "trine", "orb": 8},
  71                {"name": "sextile", "orb": 6},
  72                {"name": "square", "orb": 5},
  73                {"name": "quintile", "orb": 1},
  74            ]
  75    """
  76
  77    # Constants
  78    _BASIC_CHART_VIEWBOX = "0 0 820 550.0"
  79    _WIDE_CHART_VIEWBOX = "0 0 1200 546.0"
  80    _TRANSIT_CHART_WITH_TABLE_VIWBOX = "0 0 960 546.0"
  81
  82    _DEFAULT_HEIGHT = 550
  83    _DEFAULT_FULL_WIDTH = 1200
  84    _DEFAULT_NATAL_WIDTH = 820
  85    _DEFAULT_FULL_WIDTH_WITH_TABLE = 960
  86    _PLANET_IN_ZODIAC_EXTRA_POINTS = 10
  87
  88    # Set at init
  89    first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel]
  90    second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None]
  91    chart_type: ChartType
  92    new_output_directory: Union[Path, None]
  93    new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
  94    output_directory: Path
  95    new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
  96    theme: Union[KerykeionChartTheme, None]
  97    double_chart_aspect_grid_type: Literal["list", "table"]
  98    chart_language: KerykeionChartLanguage
  99    active_points: List[Union[Planet, AxialCusps]]
 100    active_aspects: List[ActiveAspect]
 101
 102    # Internal properties
 103    fire: float
 104    earth: float
 105    air: float
 106    water: float
 107    first_circle_radius: float
 108    second_circle_radius: float
 109    third_circle_radius: float
 110    width: Union[float, int]
 111    language_settings: dict
 112    chart_colors_settings: dict
 113    planets_settings: dict
 114    aspects_settings: dict
 115    user: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel]
 116    available_planets_setting: List[KerykeionSettingsCelestialPointModel]
 117    height: float
 118    location: str
 119    geolat: float
 120    geolon: float
 121    template: str
 122
 123    def __init__(
 124        self,
 125        first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel],
 126        chart_type: ChartType = "Natal",
 127        second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] = None,
 128        new_output_directory: Union[str, None] = None,
 129        new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None,
 130        theme: Union[KerykeionChartTheme, None] = "classic",
 131        double_chart_aspect_grid_type: Literal["list", "table"] = "list",
 132        chart_language: KerykeionChartLanguage = "EN",
 133        active_points: List[Union[Planet, AxialCusps]] = DEFAULT_ACTIVE_POINTS,
 134        active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
 135    ):
 136        home_directory = Path.home()
 137        self.new_settings_file = new_settings_file
 138        self.chart_language = chart_language
 139        self.active_points = active_points
 140        self.active_aspects = active_aspects
 141
 142        if new_output_directory:
 143            self.output_directory = Path(new_output_directory)
 144        else:
 145            self.output_directory = home_directory
 146
 147        self.parse_json_settings(new_settings_file)
 148        self.chart_type = chart_type
 149
 150        # Kerykeion instance
 151        self.user = first_obj
 152
 153        self.available_planets_setting = []
 154        for body in self.planets_settings:
 155            if body["name"] not in active_points:
 156                continue
 157            else:
 158                body["is_active"] = True
 159
 160            self.available_planets_setting.append(body)
 161
 162        # Available bodies
 163        available_celestial_points_names = []
 164        for body in self.available_planets_setting:
 165            available_celestial_points_names.append(body["name"].lower())
 166
 167        self.available_kerykeion_celestial_points: list[KerykeionPointModel] = []
 168        for body in available_celestial_points_names:
 169            self.available_kerykeion_celestial_points.append(self.user.get(body))
 170
 171        # Makes the sign number list.
 172        if self.chart_type == "Natal" or self.chart_type == "ExternalNatal":
 173            natal_aspects_instance = NatalAspects(
 174                self.user, new_settings_file=self.new_settings_file,
 175                active_points=active_points,
 176                active_aspects=active_aspects,
 177            )
 178            self.aspects_list = natal_aspects_instance.relevant_aspects
 179
 180        elif self.chart_type == "Transit" or self.chart_type == "Synastry":
 181            if not second_obj:
 182                raise KerykeionException("Second object is required for Transit or Synastry charts.")
 183
 184            # Kerykeion instance
 185            self.t_user = second_obj
 186
 187            # Aspects
 188            if self.chart_type == "Transit":
 189                synastry_aspects_instance = SynastryAspects(
 190                    self.t_user,
 191                    self.user,
 192                    new_settings_file=self.new_settings_file,
 193                    active_points=active_points,
 194                    active_aspects=active_aspects,
 195                )
 196
 197            else:
 198                synastry_aspects_instance = SynastryAspects(
 199                    self.user,
 200                    self.t_user,
 201                    new_settings_file=self.new_settings_file,
 202                    active_points=active_points,
 203                    active_aspects=active_aspects,
 204                )
 205
 206            self.aspects_list = synastry_aspects_instance.relevant_aspects
 207
 208            self.t_available_kerykeion_celestial_points = []
 209            for body in available_celestial_points_names:
 210                self.t_available_kerykeion_celestial_points.append(self.t_user.get(body))
 211
 212        elif self.chart_type == "Composite":
 213            if not isinstance(first_obj, CompositeSubjectModel):
 214                raise KerykeionException("First object must be a CompositeSubjectModel instance.")
 215
 216            self.aspects_list = NatalAspects(self.user, new_settings_file=self.new_settings_file, active_points=active_points).relevant_aspects
 217
 218        # Double chart aspect grid type
 219        self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
 220
 221        # screen size
 222        self.height = self._DEFAULT_HEIGHT
 223        if self.chart_type == "Synastry" or self.chart_type == "Transit":
 224            self.width = self._DEFAULT_FULL_WIDTH
 225        elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit":
 226            self.width = self._DEFAULT_FULL_WIDTH_WITH_TABLE
 227        else:
 228            self.width = self._DEFAULT_NATAL_WIDTH
 229
 230        if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]:
 231            self.location = self.user.city
 232            self.geolat = self.user.lat
 233            self.geolon =  self.user.lng
 234
 235        elif self.chart_type == "Composite":
 236            self.location = ""
 237            self.geolat = (self.user.first_subject.lat + self.user.second_subject.lat) / 2
 238            self.geolon = (self.user.first_subject.lng + self.user.second_subject.lng) / 2
 239
 240        elif self.chart_type in ["Transit"]:
 241            self.location = self.t_user.city
 242            self.geolat = self.t_user.lat
 243            self.geolon = self.t_user.lng
 244            self.t_name = self.language_settings["transit_name"]
 245
 246        # Default radius for the chart
 247        self.main_radius = 240
 248
 249        # Set circle radii based on chart type
 250        if self.chart_type == "ExternalNatal":
 251            self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 56, 92, 112
 252        else:
 253            self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 0, 36, 120
 254
 255        # Initialize element points
 256        self.fire = 0.0
 257        self.earth = 0.0
 258        self.air = 0.0
 259        self.water = 0.0
 260
 261        # Calculate element points from planets
 262        self._calculate_elements_points_from_planets()
 263
 264        # Set up theme
 265        if theme not in get_args(KerykeionChartTheme) and theme is not None:
 266            raise KerykeionException(f"Theme {theme} is not available. Set None for default theme.")
 267
 268        self.set_up_theme(theme)
 269
 270    def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None:
 271        """
 272        Set the theme for the chart.
 273        """
 274        if theme is None:
 275            self.color_style_tag = ""
 276            return
 277
 278        theme_dir = Path(__file__).parent / "themes"
 279
 280        with open(theme_dir / f"{theme}.css", "r") as f:
 281            self.color_style_tag = f.read()
 282
 283    def set_output_directory(self, dir_path: Path) -> None:
 284        """
 285        Sets the output direcotry and returns it's path.
 286        """
 287        self.output_directory = dir_path
 288        logging.info(f"Output direcotry set to: {self.output_directory}")
 289
 290    def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None:
 291        """
 292        Parse the settings file.
 293        """
 294        settings = get_settings(settings_file_or_dict)
 295
 296        self.language_settings = settings["language_settings"][self.chart_language]
 297        self.chart_colors_settings = settings["chart_colors"]
 298        self.planets_settings = settings["celestial_points"]
 299        self.aspects_settings = settings["aspects"]
 300
 301    def _draw_zodiac_circle_slices(self, r):
 302        """
 303        Generate the SVG string representing the zodiac circle
 304        with the 12 slices for each zodiac sign.
 305
 306        Args:
 307            r (float): The radius of the zodiac slices.
 308
 309        Returns:
 310            str: The SVG string representing the zodiac circle.
 311        """
 312        sings = get_args(Sign)
 313        output = ""
 314        for i, sing in enumerate(sings):
 315            output += draw_zodiac_slice(
 316                c1=self.first_circle_radius,
 317                chart_type=self.chart_type,
 318                seventh_house_degree_ut=self.user.seventh_house.abs_pos,
 319                num=i,
 320                r=r,
 321                style=f'fill:{self.chart_colors_settings[f"zodiac_bg_{i}"]}; fill-opacity: 0.5;',
 322                type=sing,
 323            )
 324
 325        return output
 326
 327    def _calculate_elements_points_from_planets(self):
 328        """
 329        Calculate chart element points from a planet.
 330        TODO: Refactor this method.
 331        Should be completely rewritten. Maybe even part of the AstrologicalSubject class.
 332        The points should include just the standard way of calculating the elements points.
 333        """
 334
 335        ZODIAC = (
 336            {"name": "Ari", "element": "fire"},
 337            {"name": "Tau", "element": "earth"},
 338            {"name": "Gem", "element": "air"},
 339            {"name": "Can", "element": "water"},
 340            {"name": "Leo", "element": "fire"},
 341            {"name": "Vir", "element": "earth"},
 342            {"name": "Lib", "element": "air"},
 343            {"name": "Sco", "element": "water"},
 344            {"name": "Sag", "element": "fire"},
 345            {"name": "Cap", "element": "earth"},
 346            {"name": "Aqu", "element": "air"},
 347            {"name": "Pis", "element": "water"},
 348        )
 349
 350        # Available bodies
 351        available_celestial_points_names = []
 352        for body in self.available_planets_setting:
 353            available_celestial_points_names.append(body["name"].lower())
 354
 355        # Make list of the points sign
 356        points_sign = []
 357        for planet in available_celestial_points_names:
 358            points_sign.append(self.user.get(planet).sign_num)
 359
 360        for i in range(len(self.available_planets_setting)):
 361            # element: get extra points if planet is in own zodiac sign.
 362            related_zodiac_signs = self.available_planets_setting[i]["related_zodiac_signs"]
 363            cz = points_sign[i]
 364            extra_points = 0
 365            if related_zodiac_signs != []:
 366                for e in range(len(related_zodiac_signs)):
 367                    if int(related_zodiac_signs[e]) == int(cz):
 368                        extra_points = self._PLANET_IN_ZODIAC_EXTRA_POINTS
 369
 370            ele = ZODIAC[points_sign[i]]["element"]
 371            if ele == "fire":
 372                self.fire = self.fire + self.available_planets_setting[i]["element_points"] + extra_points
 373
 374            elif ele == "earth":
 375                self.earth = self.earth + self.available_planets_setting[i]["element_points"] + extra_points
 376
 377            elif ele == "air":
 378                self.air = self.air + self.available_planets_setting[i]["element_points"] + extra_points
 379
 380            elif ele == "water":
 381                self.water = self.water + self.available_planets_setting[i]["element_points"] + extra_points
 382
 383    def _draw_all_aspects_lines(self, r, ar):
 384        out = ""
 385        for aspect in self.aspects_list:
 386            aspect_name = aspect["aspect"]
 387            aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None)
 388            if aspect_color:
 389                out += draw_aspect_line(
 390                    r=r,
 391                    ar=ar,
 392                    aspect=aspect,
 393                    color=aspect_color,
 394                    seventh_house_degree_ut=self.user.seventh_house.abs_pos
 395                )
 396        return out
 397
 398    def _draw_all_transit_aspects_lines(self, r, ar):
 399        out = ""
 400        for aspect in self.aspects_list:
 401            aspect_name = aspect["aspect"]
 402            aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None)
 403            if aspect_color:
 404                out += draw_aspect_line(
 405                    r=r,
 406                    ar=ar,
 407                    aspect=aspect,
 408                    color=aspect_color,
 409                    seventh_house_degree_ut=self.user.seventh_house.abs_pos
 410                )
 411        return out
 412
 413    def _create_template_dictionary(self) -> ChartTemplateDictionary:
 414        """
 415        Create a dictionary containing the template data for generating an astrological chart.
 416
 417        Returns:
 418            ChartTemplateDictionary: A dictionary with template data for the chart.
 419        """
 420        # Initialize template dictionary
 421        template_dict: dict = {}
 422
 423        # Set the color style tag
 424        template_dict["color_style_tag"] = self.color_style_tag
 425
 426        # Set chart dimensions
 427        template_dict["chart_height"] = self.height
 428        template_dict["chart_width"] = self.width
 429
 430        # Set viewbox based on chart type
 431        if self.chart_type in ["Natal", "ExternalNatal", "Composite"]:
 432            template_dict['viewbox'] = self._BASIC_CHART_VIEWBOX
 433        elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit":
 434            template_dict['viewbox'] = self._TRANSIT_CHART_WITH_TABLE_VIWBOX
 435        else:
 436            template_dict['viewbox'] = self._WIDE_CHART_VIEWBOX
 437
 438        # Generate rings and circles based on chart type
 439        if self.chart_type in ["Transit", "Synastry"]:
 440            template_dict["transitRing"] = draw_transit_ring(self.main_radius, self.chart_colors_settings["paper_1"], self.chart_colors_settings["zodiac_transit_ring_3"])
 441            template_dict["degreeRing"] = draw_transit_ring_degree_steps(self.main_radius, self.user.seventh_house.abs_pos)
 442            template_dict["first_circle"] = draw_first_circle(self.main_radius, self.chart_colors_settings["zodiac_transit_ring_2"], self.chart_type)
 443            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)
 444            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)
 445
 446            if self.double_chart_aspect_grid_type == "list":
 447                title = ""
 448                if self.chart_type == "Synastry":
 449                    title = self.language_settings.get("couple_aspects", "Couple Aspects")
 450                else:
 451                    title = self.language_settings.get("transit_aspects", "Transit Aspects")
 452
 453                template_dict["makeAspectGrid"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings)
 454            else:
 455                template_dict["makeAspectGrid"] = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, 550, 450)
 456
 457            template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
 458        else:
 459            template_dict["transitRing"] = ""
 460            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"])
 461            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)
 462            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)
 463            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)
 464            template_dict["makeAspectGrid"] = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list)
 465
 466            template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
 467
 468        # Set chart title
 469        if self.chart_type == "Synastry":
 470            template_dict["stringTitle"] = f"{self.user.name} {self.language_settings['and_word']} {self.t_user.name}"
 471        elif self.chart_type == "Transit":
 472            template_dict["stringTitle"] = f"{self.language_settings['transits']} {self.t_user.day}/{self.t_user.month}/{self.t_user.year}"
 473        elif self.chart_type in ["Natal", "ExternalNatal"]:
 474            template_dict["stringTitle"] = self.user.name
 475        elif self.chart_type == "Composite":
 476            template_dict["stringTitle"] = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}"
 477
 478        # Zodiac Type Info
 479        if self.user.zodiac_type == 'Tropic':
 480            zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}"
 481        else:
 482            mode_const = "SIDM_" + self.user.sidereal_mode # type: ignore
 483            mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
 484            zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
 485
 486        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')}"
 487        template_dict["bottom_left_1"] = zodiac_info
 488
 489        if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]:
 490            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", "")}'
 491            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)}'
 492            template_dict["bottom_left_4"] = f'{self.language_settings.get(self.user.perspective_type.lower().replace(" ", "_"), self.user.perspective_type)}'
 493        elif self.chart_type == "Transit":
 494            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", "")}'
 495            template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.t_user.lunar_phase.moon_phase_name}'
 496            template_dict["bottom_left_4"] = f'{self.language_settings.get(self.t_user.perspective_type.lower().replace(" ", "_"), self.t_user.perspective_type)}'
 497        elif self.chart_type == "Composite":
 498            template_dict["bottom_left_2"] = f'{self.user.first_subject.perspective_type}'
 499            template_dict["bottom_left_3"] = f'{self.language_settings.get("composite_chart", "Composite Chart")} - {self.language_settings.get("midpoints", "Midpoints")}'
 500            template_dict["bottom_left_4"] = ""
 501
 502        # Draw moon phase
 503        moon_phase_dict = calculate_moon_phase_chart_params(
 504            self.user.lunar_phase["degrees_between_s_m"],
 505            self.geolat
 506        )
 507
 508        template_dict["lunar_phase_rotate"] = moon_phase_dict["lunar_phase_rotate"]
 509        template_dict["lunar_phase_circle_center_x"] = moon_phase_dict["circle_center_x"]
 510        template_dict["lunar_phase_circle_radius"] = moon_phase_dict["circle_radius"]
 511
 512        if self.chart_type == "Composite":
 513            template_dict["top_left_1"] = f"{datetime.fromisoformat(self.user.first_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}"
 514        # Set location string
 515        elif len(self.location) > 35:
 516            split_location = self.location.split(",")
 517            if len(split_location) > 1:
 518                template_dict["top_left_1"] = split_location[0] + ", " + split_location[-1]
 519                if len(template_dict["top_left_1"]) > 35:
 520                    template_dict["top_left_1"] = template_dict["top_left_1"][:35] + "..."
 521            else:
 522                template_dict["top_left_1"] = self.location[:35] + "..."
 523        else:
 524            template_dict["top_left_1"] = self.location
 525
 526        # Set chart name
 527        if self.chart_type in ["Synastry", "Transit"]:
 528            template_dict["top_left_0"] = f"{self.user.name}:"
 529        elif self.chart_type in ["Natal", "ExternalNatal"]:
 530            template_dict["top_left_0"] = f'{self.language_settings["info"]}:'
 531        elif self.chart_type == "Composite":
 532            template_dict["top_left_0"] = f'{self.user.first_subject.name}'
 533
 534        # Set additional information for Synastry chart type
 535        if self.chart_type == "Synastry":
 536            template_dict["top_left_3"] = f"{self.t_user.name}: "
 537            template_dict["top_left_4"] = self.t_user.city
 538            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}"
 539        elif self.chart_type == "Composite":
 540            template_dict["top_left_3"] = self.user.second_subject.name
 541            template_dict["top_left_4"] = f"{datetime.fromisoformat(self.user.second_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}"
 542            latitude_string = convert_latitude_coordinate_to_string(self.user.second_subject.lat, self.language_settings['north_letter'], self.language_settings['south_letter'])
 543            longitude_string = convert_longitude_coordinate_to_string(self.user.second_subject.lng, self.language_settings['east_letter'], self.language_settings['west_letter'])
 544            template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}"
 545        else:
 546            latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings['north'], self.language_settings['south'])
 547            longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings['east'], self.language_settings['west'])
 548            template_dict["top_left_3"] = f"{self.language_settings['latitude']}: {latitude_string}"
 549            template_dict["top_left_4"] = f"{self.language_settings['longitude']}: {longitude_string}"
 550            template_dict["top_left_5"] = f"{self.language_settings['type']}: {self.language_settings.get(self.chart_type, self.chart_type)}"
 551
 552
 553        # Set paper colors
 554        template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"]
 555        template_dict["paper_color_1"] = self.chart_colors_settings["paper_1"]
 556
 557        # Set planet colors
 558        for planet in self.planets_settings:
 559            planet_id = planet["id"]
 560            template_dict[f"planets_color_{planet_id}"] = planet["color"] # type: ignore
 561
 562        # Set zodiac colors
 563        for i in range(12):
 564            template_dict[f"zodiac_color_{i}"] = self.chart_colors_settings[f"zodiac_icon_{i}"] # type: ignore
 565
 566        # Set orb colors
 567        for aspect in self.aspects_settings:
 568            template_dict[f"orb_color_{aspect['degree']}"] = aspect['color'] # type: ignore
 569
 570        # Drawing functions
 571        template_dict["makeZodiac"] = self._draw_zodiac_circle_slices(self.main_radius)
 572
 573        first_subject_houses_list = get_houses_list(self.user)
 574
 575        # Draw houses grid and cusps
 576        if self.chart_type in ["Transit", "Synastry"]:
 577            second_subject_houses_list = get_houses_list(self.t_user)
 578
 579            template_dict["makeHousesGrid"] = draw_house_grid(
 580                main_subject_houses_list=first_subject_houses_list,
 581                secondary_subject_houses_list=second_subject_houses_list,
 582                chart_type=self.chart_type,
 583                text_color=self.chart_colors_settings["paper_0"],
 584                house_cusp_generale_name_label=self.language_settings["cusp"]
 585            )
 586
 587            template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
 588                r=self.main_radius,
 589                first_subject_houses_list=first_subject_houses_list,
 590                standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
 591                first_house_color=self.planets_settings[12]["color"],
 592                tenth_house_color=self.planets_settings[13]["color"],
 593                seventh_house_color=self.planets_settings[14]["color"],
 594                fourth_house_color=self.planets_settings[15]["color"],
 595                c1=self.first_circle_radius,
 596                c3=self.third_circle_radius,
 597                chart_type=self.chart_type,
 598                second_subject_houses_list=second_subject_houses_list,
 599                transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
 600            )
 601
 602        else:
 603            template_dict["makeHousesGrid"] = draw_house_grid(
 604                main_subject_houses_list=first_subject_houses_list,
 605                chart_type=self.chart_type,
 606                text_color=self.chart_colors_settings["paper_0"],
 607                house_cusp_generale_name_label=self.language_settings["cusp"]
 608            )
 609
 610            template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
 611                r=self.main_radius,
 612                first_subject_houses_list=first_subject_houses_list,
 613                standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
 614                first_house_color=self.planets_settings[12]["color"],
 615                tenth_house_color=self.planets_settings[13]["color"],
 616                seventh_house_color=self.planets_settings[14]["color"],
 617                fourth_house_color=self.planets_settings[15]["color"],
 618                c1=self.first_circle_radius,
 619                c3=self.third_circle_radius,
 620                chart_type=self.chart_type,
 621            )
 622
 623        # Draw planets
 624        if self.chart_type in ["Transit", "Synastry"]:
 625            template_dict["makePlanets"] = draw_planets(
 626                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
 627                available_planets_setting=self.available_planets_setting,
 628                second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
 629                radius=self.main_radius,
 630                main_subject_first_house_degree_ut=self.user.first_house.abs_pos,
 631                main_subject_seventh_house_degree_ut=self.user.seventh_house.abs_pos,
 632                chart_type=self.chart_type,
 633                third_circle_radius=self.third_circle_radius,
 634            )
 635        else:
 636            template_dict["makePlanets"] = draw_planets(
 637                available_planets_setting=self.available_planets_setting,
 638                chart_type=self.chart_type,
 639                radius=self.main_radius,
 640                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
 641                third_circle_radius=self.third_circle_radius,
 642                main_subject_first_house_degree_ut=self.user.first_house.abs_pos,
 643                main_subject_seventh_house_degree_ut=self.user.seventh_house.abs_pos
 644            )
 645
 646        # Draw elements percentages
 647        total = self.fire + self.water + self.earth + self.air
 648
 649        fire_percentage = int(round(100 * self.fire / total))
 650        earth_percentage = int(round(100 * self.earth / total))
 651        air_percentage = int(round(100 * self.air / total))
 652        water_percentage = int(round(100 * self.water / total))
 653
 654        template_dict["fire_string"] = f"{self.language_settings['fire']} {fire_percentage}%"
 655        template_dict["earth_string"] = f"{self.language_settings['earth']} {earth_percentage}%"
 656        template_dict["air_string"] = f"{self.language_settings['air']} {air_percentage}%"
 657        template_dict["water_string"] = f"{self.language_settings['water']} {water_percentage}%"
 658
 659        # Draw planet grid
 660        if self.chart_type in ["Transit", "Synastry"]:
 661            if self.chart_type == "Transit":
 662                second_subject_table_name = self.language_settings["transit_name"]
 663            else:
 664                second_subject_table_name = self.t_user.name
 665
 666            template_dict["makePlanetGrid"] = draw_planet_grid(
 667                planets_and_houses_grid_title=self.language_settings["planets_and_house"],
 668                subject_name=self.user.name,
 669                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
 670                chart_type=self.chart_type,
 671                text_color=self.chart_colors_settings["paper_0"],
 672                celestial_point_language=self.language_settings["celestial_points"],
 673                second_subject_name=second_subject_table_name,
 674                second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
 675            )
 676        else:
 677            if self.chart_type == "Composite":
 678                subject_name = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}"
 679            else:
 680                subject_name = self.user.name
 681
 682            template_dict["makePlanetGrid"] = draw_planet_grid(
 683                planets_and_houses_grid_title=self.language_settings["planets_and_house"],
 684                subject_name=subject_name,
 685                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
 686                chart_type=self.chart_type,
 687                text_color=self.chart_colors_settings["paper_0"],
 688                celestial_point_language=self.language_settings["celestial_points"],
 689            )
 690
 691        # Set date time string
 692        if self.chart_type in ["Composite"]:
 693            # First Subject Latitude and Longitude
 694            latitude = convert_latitude_coordinate_to_string(self.user.first_subject.lat, self.language_settings["north_letter"], self.language_settings["south_letter"])
 695            longitude = convert_longitude_coordinate_to_string(self.user.first_subject.lng, self.language_settings["east_letter"], self.language_settings["west_letter"])
 696            template_dict["top_left_2"] = f"{latitude} {longitude}"
 697        else:
 698            dt = datetime.fromisoformat(self.user.iso_formatted_local_datetime)
 699            custom_format = dt.strftime('%Y-%m-%d %H:%M [%z]')
 700            custom_format = custom_format[:-3] + ':' + custom_format[-3:]
 701            template_dict["top_left_2"] = f"{custom_format}"
 702
 703        return ChartTemplateDictionary(**template_dict)
 704
 705    def makeTemplate(self, minify: bool = False) -> str:
 706        """Creates the template for the SVG file"""
 707        td = self._create_template_dictionary()
 708
 709        DATA_DIR = Path(__file__).parent
 710        xml_svg = DATA_DIR / "templates" / "chart.xml"
 711
 712        # read template
 713        with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f:
 714            template = Template(f.read()).substitute(td)
 715
 716        # return filename
 717
 718        logging.debug(f"Template dictionary keys: {td.keys()}")
 719
 720        self._create_template_dictionary()
 721
 722        if minify:
 723            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
 724
 725        else:
 726            template = template.replace('"', "'")
 727
 728        return template
 729
 730    def makeSVG(self, minify: bool = False):
 731        """Prints out the SVG file in the specified folder"""
 732
 733        if not hasattr(self, "template"):
 734            self.template = self.makeTemplate(minify)
 735
 736        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg"
 737
 738        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
 739            output_file.write(self.template)
 740
 741        print(f"SVG Generated Correctly in: {chartname}")
 742    def makeWheelOnlyTemplate(self, minify: bool = False):
 743        """Creates the template for the SVG file with only the wheel"""
 744
 745        with open(Path(__file__).parent / "templates" / "wheel_only.xml", "r", encoding="utf-8", errors="ignore") as f:
 746            template = f.read()
 747
 748        template_dict = self._create_template_dictionary()
 749        template = Template(template).substitute(template_dict)
 750
 751        if minify:
 752            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
 753
 754        else:
 755            template = template.replace('"', "'")
 756
 757        return template
 758
 759    def makeWheelOnlySVG(self, minify: bool = False):
 760        """Prints out the SVG file in the specified folder with only the wheel"""
 761
 762        template = self.makeWheelOnlyTemplate(minify)
 763        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Wheel Only.svg"
 764
 765        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
 766            output_file.write(template)
 767
 768        print(f"SVG Generated Correctly in: {chartname}")
 769    def makeAspectGridOnlyTemplate(self, minify: bool = False):
 770        """Creates the template for the SVG file with only the aspect grid"""
 771
 772        with open(Path(__file__).parent / "templates" / "aspect_grid_only.xml", "r", encoding="utf-8", errors="ignore") as f:
 773            template = f.read()
 774
 775        template_dict = self._create_template_dictionary()
 776
 777        if self.chart_type in ["Transit", "Synastry"]:
 778            aspects_grid = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list)
 779        else:
 780            aspects_grid = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, x_start=50, y_start=250)
 781
 782        template = Template(template).substitute({**template_dict, "makeAspectGrid": aspects_grid})
 783
 784        if minify:
 785            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
 786
 787        else:
 788            template = template.replace('"', "'")
 789
 790        return template
 791
 792    def makeAspectGridOnlySVG(self, minify: bool = False):
 793        """Prints out the SVG file in the specified folder with only the aspect grid"""
 794
 795        template = self.makeAspectGridOnlyTemplate(minify)
 796        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Aspect Grid Only.svg"
 797
 798        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
 799            output_file.write(template)
 800
 801        print(f"SVG Generated Correctly in: {chartname}")
 802
 803if __name__ == "__main__":
 804    from kerykeion.utilities import setup_logging
 805    from kerykeion.composite_subject_factory import CompositeSubjectFactory
 806    setup_logging(level="debug")
 807
 808    first = AstrologicalSubject("John Lennon", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 809    second = AstrologicalSubject("Paul McCartney", 1942, 6, 18, 15, 30, "Liverpool", "GB")
 810
 811    # Internal Natal Chart
 812    internal_natal_chart = KerykeionChartSVG(first)
 813    internal_natal_chart.makeSVG()
 814
 815    # External Natal Chart
 816    external_natal_chart = KerykeionChartSVG(first, "ExternalNatal", second)
 817    external_natal_chart.makeSVG()
 818
 819    # Synastry Chart
 820    synastry_chart = KerykeionChartSVG(first, "Synastry", second)
 821    synastry_chart.makeSVG()
 822
 823    # Transits Chart
 824    transits_chart = KerykeionChartSVG(first, "Transit", second)
 825    transits_chart.makeSVG()
 826
 827    # Sidereal Birth Chart (Lahiri)
 828    sidereal_subject = AstrologicalSubject("John Lennon Lahiri", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="LAHIRI")
 829    sidereal_chart = KerykeionChartSVG(sidereal_subject)
 830    sidereal_chart.makeSVG()
 831
 832    # Sidereal Birth Chart (Fagan-Bradley)
 833    sidereal_subject = AstrologicalSubject("John Lennon Fagan-Bradley", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="FAGAN_BRADLEY")
 834    sidereal_chart = KerykeionChartSVG(sidereal_subject)
 835    sidereal_chart.makeSVG()
 836
 837    # Sidereal Birth Chart (DeLuce)
 838    sidereal_subject = AstrologicalSubject("John Lennon DeLuce", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="DELUCE")
 839    sidereal_chart = KerykeionChartSVG(sidereal_subject)
 840    sidereal_chart.makeSVG()
 841
 842    # Sidereal Birth Chart (J2000)
 843    sidereal_subject = AstrologicalSubject("John Lennon J2000", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="J2000")
 844    sidereal_chart = KerykeionChartSVG(sidereal_subject)
 845    sidereal_chart.makeSVG()
 846
 847    # House System Morinus
 848    morinus_house_subject = AstrologicalSubject("John Lennon - House System Morinus", 1940, 10, 9, 18, 30, "Liverpool", "GB", houses_system_identifier="M")
 849    morinus_house_chart = KerykeionChartSVG(morinus_house_subject)
 850    morinus_house_chart.makeSVG()
 851
 852    ## To check all the available house systems uncomment the following code:
 853    # from kerykeion.kr_types import HousesSystemIdentifier
 854    # from typing import get_args
 855    # for i in get_args(HousesSystemIdentifier):
 856    #     alternatives_house_subject = AstrologicalSubject(f"John Lennon - House System {i}", 1940, 10, 9, 18, 30, "Liverpool", "GB", houses_system=i)
 857    #     alternatives_house_chart = KerykeionChartSVG(alternatives_house_subject)
 858    #     alternatives_house_chart.makeSVG()
 859
 860    # With True Geocentric Perspective
 861    true_geocentric_subject = AstrologicalSubject("John Lennon - True Geocentric", 1940, 10, 9, 18, 30, "Liverpool", "GB", perspective_type="True Geocentric")
 862    true_geocentric_chart = KerykeionChartSVG(true_geocentric_subject)
 863    true_geocentric_chart.makeSVG()
 864
 865    # With Heliocentric Perspective
 866    heliocentric_subject = AstrologicalSubject("John Lennon - Heliocentric", 1940, 10, 9, 18, 30, "Liverpool", "GB", perspective_type="Heliocentric")
 867    heliocentric_chart = KerykeionChartSVG(heliocentric_subject)
 868    heliocentric_chart.makeSVG()
 869
 870    # With Topocentric Perspective
 871    topocentric_subject = AstrologicalSubject("John Lennon - Topocentric", 1940, 10, 9, 18, 30, "Liverpool", "GB", perspective_type="Topocentric")
 872    topocentric_chart = KerykeionChartSVG(topocentric_subject)
 873    topocentric_chart.makeSVG()
 874
 875    # Minified SVG
 876    minified_subject = AstrologicalSubject("John Lennon - Minified", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 877    minified_chart = KerykeionChartSVG(minified_subject)
 878    minified_chart.makeSVG(minify=True)
 879
 880    # Dark Theme Natal Chart
 881    dark_theme_subject = AstrologicalSubject("John Lennon - Dark Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 882    dark_theme_natal_chart = KerykeionChartSVG(dark_theme_subject, theme="dark")
 883    dark_theme_natal_chart.makeSVG()
 884
 885    # Dark High Contrast Theme Natal Chart
 886    dark_high_contrast_theme_subject = AstrologicalSubject("John Lennon - Dark High Contrast Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 887    dark_high_contrast_theme_natal_chart = KerykeionChartSVG(dark_high_contrast_theme_subject, theme="dark-high-contrast")
 888    dark_high_contrast_theme_natal_chart.makeSVG()
 889
 890    # Light Theme Natal Chart
 891    light_theme_subject = AstrologicalSubject("John Lennon - Light Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 892    light_theme_natal_chart = KerykeionChartSVG(light_theme_subject, theme="light")
 893    light_theme_natal_chart.makeSVG()
 894
 895    # Dark Theme External Natal Chart
 896    dark_theme_external_subject = AstrologicalSubject("John Lennon - Dark Theme External", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 897    dark_theme_external_chart = KerykeionChartSVG(dark_theme_external_subject, "ExternalNatal", second, theme="dark")
 898    dark_theme_external_chart.makeSVG()
 899
 900    # Dark Theme Synastry Chart
 901    dark_theme_synastry_subject = AstrologicalSubject("John Lennon - DTS", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 902    dark_theme_synastry_chart = KerykeionChartSVG(dark_theme_synastry_subject, "Synastry", second, theme="dark")
 903    dark_theme_synastry_chart.makeSVG()
 904
 905    # Wheel Natal Only Chart
 906    wheel_only_subject = AstrologicalSubject("John Lennon - Wheel Only", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 907    wheel_only_chart = KerykeionChartSVG(wheel_only_subject)
 908    wheel_only_chart.makeWheelOnlySVG()
 909
 910    # Wheel External Natal Only Chart
 911    wheel_external_subject = AstrologicalSubject("John Lennon - Wheel External Only", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 912    wheel_external_chart = KerykeionChartSVG(wheel_external_subject, "ExternalNatal", second)
 913    wheel_external_chart.makeWheelOnlySVG()
 914
 915    # Wheel Synastry Only Chart
 916    wheel_synastry_subject = AstrologicalSubject("John Lennon - Wheel Synastry Only", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 917    wheel_synastry_chart = KerykeionChartSVG(wheel_synastry_subject, "Synastry", second)
 918    wheel_synastry_chart.makeWheelOnlySVG()
 919
 920    # Wheel Transit Only Chart
 921    wheel_transit_subject = AstrologicalSubject("John Lennon - Wheel Transit Only", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 922    wheel_transit_chart = KerykeionChartSVG(wheel_transit_subject, "Transit", second)
 923    wheel_transit_chart.makeWheelOnlySVG()
 924
 925    # Wheel Sidereal Birth Chart (Lahiri) Dark Theme
 926    sidereal_dark_subject = AstrologicalSubject("John Lennon Lahiri - Dark Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="LAHIRI")
 927    sidereal_dark_chart = KerykeionChartSVG(sidereal_dark_subject, theme="dark")
 928    sidereal_dark_chart.makeWheelOnlySVG()
 929
 930    # Wheel Sidereal Birth Chart (Fagan-Bradley) Light Theme
 931    sidereal_light_subject = AstrologicalSubject("John Lennon Fagan-Bradley - Light Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB", zodiac_type="Sidereal", sidereal_mode="FAGAN_BRADLEY")
 932    sidereal_light_chart = KerykeionChartSVG(sidereal_light_subject, theme="light")
 933    sidereal_light_chart.makeWheelOnlySVG()
 934
 935    # Aspect Grid Only Natal Chart
 936    aspect_grid_only_subject = AstrologicalSubject("John Lennon - Aspect Grid Only", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 937    aspect_grid_only_chart = KerykeionChartSVG(aspect_grid_only_subject)
 938    aspect_grid_only_chart.makeAspectGridOnlySVG()
 939
 940    # Aspect Grid Only Dark Theme Natal Chart
 941    aspect_grid_dark_subject = AstrologicalSubject("John Lennon - Aspect Grid Dark Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 942    aspect_grid_dark_chart = KerykeionChartSVG(aspect_grid_dark_subject, theme="dark")
 943    aspect_grid_dark_chart.makeAspectGridOnlySVG()
 944
 945    # Aspect Grid Only Light Theme Natal Chart
 946    aspect_grid_light_subject = AstrologicalSubject("John Lennon - Aspect Grid Light Theme", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 947    aspect_grid_light_chart = KerykeionChartSVG(aspect_grid_light_subject, theme="light")
 948    aspect_grid_light_chart.makeAspectGridOnlySVG()
 949
 950    # Synastry Chart Aspect Grid Only
 951    aspect_grid_synastry_subject = AstrologicalSubject("John Lennon - Aspect Grid Synastry", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 952    aspect_grid_synastry_chart = KerykeionChartSVG(aspect_grid_synastry_subject, "Synastry", second)
 953    aspect_grid_synastry_chart.makeAspectGridOnlySVG()
 954
 955    # Transit Chart Aspect Grid Only
 956    aspect_grid_transit_subject = AstrologicalSubject("John Lennon - Aspect Grid Transit", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 957    aspect_grid_transit_chart = KerykeionChartSVG(aspect_grid_transit_subject, "Transit", second)
 958    aspect_grid_transit_chart.makeAspectGridOnlySVG()
 959
 960    # Synastry Chart Aspect Grid Only Dark Theme
 961    aspect_grid_dark_synastry_subject = AstrologicalSubject("John Lennon - Aspect Grid Dark Synastry", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 962    aspect_grid_dark_synastry_chart = KerykeionChartSVG(aspect_grid_dark_synastry_subject, "Synastry", second, theme="dark")
 963    aspect_grid_dark_synastry_chart.makeAspectGridOnlySVG()
 964
 965    # Synastry Chart With draw_transit_aspect_list table
 966    synastry_chart_with_table_list_subject = AstrologicalSubject("John Lennon - SCTWL", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 967    synastry_chart_with_table_list = KerykeionChartSVG(synastry_chart_with_table_list_subject, "Synastry", second, double_chart_aspect_grid_type="list", theme="dark")
 968    synastry_chart_with_table_list.makeSVG()
 969
 970    # Transit Chart With draw_transit_aspect_grid table
 971    transit_chart_with_table_grid_subject = AstrologicalSubject("John Lennon - TCWTG", 1940, 10, 9, 18, 30, "Liverpool", "GB")
 972    transit_chart_with_table_grid = KerykeionChartSVG(transit_chart_with_table_grid_subject, "Transit", second, double_chart_aspect_grid_type="table", theme="dark")
 973    transit_chart_with_table_grid.makeSVG()
 974
 975    # Chines Language Chart
 976    chinese_subject = AstrologicalSubject("Hua Chenyu", 1990, 2, 7, 12, 0, "Hunan", "CN")
 977    chinese_chart = KerykeionChartSVG(chinese_subject, chart_language="CN")
 978    chinese_chart.makeSVG()
 979
 980    # French Language Chart
 981    french_subject = AstrologicalSubject("Jeanne Moreau", 1928, 1, 23, 10, 0, "Paris", "FR")
 982    french_chart = KerykeionChartSVG(french_subject, chart_language="FR")
 983    french_chart.makeSVG()
 984
 985    # Spanish Language Chart
 986    spanish_subject = AstrologicalSubject("Antonio Banderas", 1960, 8, 10, 12, 0, "Malaga", "ES")
 987    spanish_chart = KerykeionChartSVG(spanish_subject, chart_language="ES")
 988    spanish_chart.makeSVG()
 989
 990    # Portuguese Language Chart
 991    portuguese_subject = AstrologicalSubject("Cristiano Ronaldo", 1985, 2, 5, 5, 25, "Funchal", "PT")
 992    portuguese_chart = KerykeionChartSVG(portuguese_subject, chart_language="PT")
 993    portuguese_chart.makeSVG()
 994
 995    # Italian Language Chart
 996    italian_subject = AstrologicalSubject("Sophia Loren", 1934, 9, 20, 2, 0, "Rome", "IT")
 997    italian_chart = KerykeionChartSVG(italian_subject, chart_language="IT")
 998    italian_chart.makeSVG()
 999
1000    # Russian Language Chart
1001    russian_subject = AstrologicalSubject("Mikhail Bulgakov", 1891, 5, 15, 12, 0, "Kiev", "UA")
1002    russian_chart = KerykeionChartSVG(russian_subject, chart_language="RU")
1003    russian_chart.makeSVG()
1004
1005    # Turkish Language Chart
1006    turkish_subject = AstrologicalSubject("Mehmet Oz", 1960, 6, 11, 12, 0, "Istanbul", "TR")
1007    turkish_chart = KerykeionChartSVG(turkish_subject, chart_language="TR")
1008    turkish_chart.makeSVG()
1009
1010    # German Language Chart
1011    german_subject = AstrologicalSubject("Albert Einstein", 1879, 3, 14, 11, 30, "Ulm", "DE")
1012    german_chart = KerykeionChartSVG(german_subject, chart_language="DE")
1013    german_chart.makeSVG()
1014
1015    # Hindi Language Chart
1016    hindi_subject = AstrologicalSubject("Amitabh Bachchan", 1942, 10, 11, 4, 0, "Allahabad", "IN")
1017    hindi_chart = KerykeionChartSVG(hindi_subject, chart_language="HI")
1018    hindi_chart.makeSVG()
1019
1020    # Kanye West Natal Chart
1021    kanye_west_subject = AstrologicalSubject("Kanye", 1977, 6, 8, 8, 45, "Atlanta", "US")
1022    kanye_west_chart = KerykeionChartSVG(kanye_west_subject)
1023    kanye_west_chart.makeSVG()
1024
1025    # Composite Chart
1026    angelina = AstrologicalSubject("Angelina Jolie", 1975, 6, 4, 9, 9, "Los Angeles", "US", lng=-118.15, lat=34.03, tz_str="America/Los_Angeles")
1027    brad = AstrologicalSubject("Brad Pitt", 1963, 12, 18, 6, 31, "Shawnee", "US", lng=-96.56, lat=35.20, tz_str="America/Chicago")
1028
1029    composite_subject_factory = CompositeSubjectFactory(angelina, brad)
1030    composite_subject_model = composite_subject_factory.get_midpoint_composite_subject_model()
1031    composite_chart = KerykeionChartSVG(composite_subject_model, "Composite")
1032    composite_chart.makeSVG()
class KerykeionChartSVG:
 49class KerykeionChartSVG:
 50    """
 51    Creates the instance that can generate the chart with the
 52    function makeSVG().
 53
 54    Parameters:
 55        - first_obj: First kerykeion object
 56        - chart_type: Natal, ExternalNatal, Transit, Synastry (Default: Type="Natal").
 57        - second_obj: Second kerykeion object (Not required if type is Natal)
 58        - new_output_directory: Set the output directory (default: home directory).
 59        - new_settings_file: Set the settings file (default: kr.config.json).
 60            In the settings file you can set the language, colors, planets, aspects, etc.
 61        - theme: Set the theme for the chart (default: classic). If None the <style> tag will be empty.
 62            That's useful if you want to use your own CSS file customizing the value of the default theme variables.
 63        - double_chart_aspect_grid_type: Set the type of the aspect grid for the double chart (transit or synastry). (Default: list.)
 64        - chart_language: Set the language for the chart (default: EN).
 65        - active_points: Set the active points for the chart (default: DEFAULT_ACTIVE_POINTS). Example:
 66            ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", "True_Node", "True_South_Node", "Ascendant", "Medium_Coeli", "Descendant", "Imum_Coeli"]
 67        - active_aspects: Set the active aspects for the chart (default: DEFAULT_ACTIVE_ASPECTS). Example:
 68            [
 69                {"name": "conjunction", "orb": 10},
 70                {"name": "opposition", "orb": 10},
 71                {"name": "trine", "orb": 8},
 72                {"name": "sextile", "orb": 6},
 73                {"name": "square", "orb": 5},
 74                {"name": "quintile", "orb": 1},
 75            ]
 76    """
 77
 78    # Constants
 79    _BASIC_CHART_VIEWBOX = "0 0 820 550.0"
 80    _WIDE_CHART_VIEWBOX = "0 0 1200 546.0"
 81    _TRANSIT_CHART_WITH_TABLE_VIWBOX = "0 0 960 546.0"
 82
 83    _DEFAULT_HEIGHT = 550
 84    _DEFAULT_FULL_WIDTH = 1200
 85    _DEFAULT_NATAL_WIDTH = 820
 86    _DEFAULT_FULL_WIDTH_WITH_TABLE = 960
 87    _PLANET_IN_ZODIAC_EXTRA_POINTS = 10
 88
 89    # Set at init
 90    first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel]
 91    second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None]
 92    chart_type: ChartType
 93    new_output_directory: Union[Path, None]
 94    new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
 95    output_directory: Path
 96    new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
 97    theme: Union[KerykeionChartTheme, None]
 98    double_chart_aspect_grid_type: Literal["list", "table"]
 99    chart_language: KerykeionChartLanguage
100    active_points: List[Union[Planet, AxialCusps]]
101    active_aspects: List[ActiveAspect]
102
103    # Internal properties
104    fire: float
105    earth: float
106    air: float
107    water: float
108    first_circle_radius: float
109    second_circle_radius: float
110    third_circle_radius: float
111    width: Union[float, int]
112    language_settings: dict
113    chart_colors_settings: dict
114    planets_settings: dict
115    aspects_settings: dict
116    user: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel]
117    available_planets_setting: List[KerykeionSettingsCelestialPointModel]
118    height: float
119    location: str
120    geolat: float
121    geolon: float
122    template: str
123
124    def __init__(
125        self,
126        first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel],
127        chart_type: ChartType = "Natal",
128        second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] = None,
129        new_output_directory: Union[str, None] = None,
130        new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None,
131        theme: Union[KerykeionChartTheme, None] = "classic",
132        double_chart_aspect_grid_type: Literal["list", "table"] = "list",
133        chart_language: KerykeionChartLanguage = "EN",
134        active_points: List[Union[Planet, AxialCusps]] = DEFAULT_ACTIVE_POINTS,
135        active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
136    ):
137        home_directory = Path.home()
138        self.new_settings_file = new_settings_file
139        self.chart_language = chart_language
140        self.active_points = active_points
141        self.active_aspects = active_aspects
142
143        if new_output_directory:
144            self.output_directory = Path(new_output_directory)
145        else:
146            self.output_directory = home_directory
147
148        self.parse_json_settings(new_settings_file)
149        self.chart_type = chart_type
150
151        # Kerykeion instance
152        self.user = first_obj
153
154        self.available_planets_setting = []
155        for body in self.planets_settings:
156            if body["name"] not in active_points:
157                continue
158            else:
159                body["is_active"] = True
160
161            self.available_planets_setting.append(body)
162
163        # Available bodies
164        available_celestial_points_names = []
165        for body in self.available_planets_setting:
166            available_celestial_points_names.append(body["name"].lower())
167
168        self.available_kerykeion_celestial_points: list[KerykeionPointModel] = []
169        for body in available_celestial_points_names:
170            self.available_kerykeion_celestial_points.append(self.user.get(body))
171
172        # Makes the sign number list.
173        if self.chart_type == "Natal" or self.chart_type == "ExternalNatal":
174            natal_aspects_instance = NatalAspects(
175                self.user, new_settings_file=self.new_settings_file,
176                active_points=active_points,
177                active_aspects=active_aspects,
178            )
179            self.aspects_list = natal_aspects_instance.relevant_aspects
180
181        elif self.chart_type == "Transit" or self.chart_type == "Synastry":
182            if not second_obj:
183                raise KerykeionException("Second object is required for Transit or Synastry charts.")
184
185            # Kerykeion instance
186            self.t_user = second_obj
187
188            # Aspects
189            if self.chart_type == "Transit":
190                synastry_aspects_instance = SynastryAspects(
191                    self.t_user,
192                    self.user,
193                    new_settings_file=self.new_settings_file,
194                    active_points=active_points,
195                    active_aspects=active_aspects,
196                )
197
198            else:
199                synastry_aspects_instance = SynastryAspects(
200                    self.user,
201                    self.t_user,
202                    new_settings_file=self.new_settings_file,
203                    active_points=active_points,
204                    active_aspects=active_aspects,
205                )
206
207            self.aspects_list = synastry_aspects_instance.relevant_aspects
208
209            self.t_available_kerykeion_celestial_points = []
210            for body in available_celestial_points_names:
211                self.t_available_kerykeion_celestial_points.append(self.t_user.get(body))
212
213        elif self.chart_type == "Composite":
214            if not isinstance(first_obj, CompositeSubjectModel):
215                raise KerykeionException("First object must be a CompositeSubjectModel instance.")
216
217            self.aspects_list = NatalAspects(self.user, new_settings_file=self.new_settings_file, active_points=active_points).relevant_aspects
218
219        # Double chart aspect grid type
220        self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
221
222        # screen size
223        self.height = self._DEFAULT_HEIGHT
224        if self.chart_type == "Synastry" or self.chart_type == "Transit":
225            self.width = self._DEFAULT_FULL_WIDTH
226        elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit":
227            self.width = self._DEFAULT_FULL_WIDTH_WITH_TABLE
228        else:
229            self.width = self._DEFAULT_NATAL_WIDTH
230
231        if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]:
232            self.location = self.user.city
233            self.geolat = self.user.lat
234            self.geolon =  self.user.lng
235
236        elif self.chart_type == "Composite":
237            self.location = ""
238            self.geolat = (self.user.first_subject.lat + self.user.second_subject.lat) / 2
239            self.geolon = (self.user.first_subject.lng + self.user.second_subject.lng) / 2
240
241        elif self.chart_type in ["Transit"]:
242            self.location = self.t_user.city
243            self.geolat = self.t_user.lat
244            self.geolon = self.t_user.lng
245            self.t_name = self.language_settings["transit_name"]
246
247        # Default radius for the chart
248        self.main_radius = 240
249
250        # Set circle radii based on chart type
251        if self.chart_type == "ExternalNatal":
252            self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 56, 92, 112
253        else:
254            self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 0, 36, 120
255
256        # Initialize element points
257        self.fire = 0.0
258        self.earth = 0.0
259        self.air = 0.0
260        self.water = 0.0
261
262        # Calculate element points from planets
263        self._calculate_elements_points_from_planets()
264
265        # Set up theme
266        if theme not in get_args(KerykeionChartTheme) and theme is not None:
267            raise KerykeionException(f"Theme {theme} is not available. Set None for default theme.")
268
269        self.set_up_theme(theme)
270
271    def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None:
272        """
273        Set the theme for the chart.
274        """
275        if theme is None:
276            self.color_style_tag = ""
277            return
278
279        theme_dir = Path(__file__).parent / "themes"
280
281        with open(theme_dir / f"{theme}.css", "r") as f:
282            self.color_style_tag = f.read()
283
284    def set_output_directory(self, dir_path: Path) -> None:
285        """
286        Sets the output direcotry and returns it's path.
287        """
288        self.output_directory = dir_path
289        logging.info(f"Output direcotry set to: {self.output_directory}")
290
291    def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None:
292        """
293        Parse the settings file.
294        """
295        settings = get_settings(settings_file_or_dict)
296
297        self.language_settings = settings["language_settings"][self.chart_language]
298        self.chart_colors_settings = settings["chart_colors"]
299        self.planets_settings = settings["celestial_points"]
300        self.aspects_settings = settings["aspects"]
301
302    def _draw_zodiac_circle_slices(self, r):
303        """
304        Generate the SVG string representing the zodiac circle
305        with the 12 slices for each zodiac sign.
306
307        Args:
308            r (float): The radius of the zodiac slices.
309
310        Returns:
311            str: The SVG string representing the zodiac circle.
312        """
313        sings = get_args(Sign)
314        output = ""
315        for i, sing in enumerate(sings):
316            output += draw_zodiac_slice(
317                c1=self.first_circle_radius,
318                chart_type=self.chart_type,
319                seventh_house_degree_ut=self.user.seventh_house.abs_pos,
320                num=i,
321                r=r,
322                style=f'fill:{self.chart_colors_settings[f"zodiac_bg_{i}"]}; fill-opacity: 0.5;',
323                type=sing,
324            )
325
326        return output
327
328    def _calculate_elements_points_from_planets(self):
329        """
330        Calculate chart element points from a planet.
331        TODO: Refactor this method.
332        Should be completely rewritten. Maybe even part of the AstrologicalSubject class.
333        The points should include just the standard way of calculating the elements points.
334        """
335
336        ZODIAC = (
337            {"name": "Ari", "element": "fire"},
338            {"name": "Tau", "element": "earth"},
339            {"name": "Gem", "element": "air"},
340            {"name": "Can", "element": "water"},
341            {"name": "Leo", "element": "fire"},
342            {"name": "Vir", "element": "earth"},
343            {"name": "Lib", "element": "air"},
344            {"name": "Sco", "element": "water"},
345            {"name": "Sag", "element": "fire"},
346            {"name": "Cap", "element": "earth"},
347            {"name": "Aqu", "element": "air"},
348            {"name": "Pis", "element": "water"},
349        )
350
351        # Available bodies
352        available_celestial_points_names = []
353        for body in self.available_planets_setting:
354            available_celestial_points_names.append(body["name"].lower())
355
356        # Make list of the points sign
357        points_sign = []
358        for planet in available_celestial_points_names:
359            points_sign.append(self.user.get(planet).sign_num)
360
361        for i in range(len(self.available_planets_setting)):
362            # element: get extra points if planet is in own zodiac sign.
363            related_zodiac_signs = self.available_planets_setting[i]["related_zodiac_signs"]
364            cz = points_sign[i]
365            extra_points = 0
366            if related_zodiac_signs != []:
367                for e in range(len(related_zodiac_signs)):
368                    if int(related_zodiac_signs[e]) == int(cz):
369                        extra_points = self._PLANET_IN_ZODIAC_EXTRA_POINTS
370
371            ele = ZODIAC[points_sign[i]]["element"]
372            if ele == "fire":
373                self.fire = self.fire + self.available_planets_setting[i]["element_points"] + extra_points
374
375            elif ele == "earth":
376                self.earth = self.earth + self.available_planets_setting[i]["element_points"] + extra_points
377
378            elif ele == "air":
379                self.air = self.air + self.available_planets_setting[i]["element_points"] + extra_points
380
381            elif ele == "water":
382                self.water = self.water + self.available_planets_setting[i]["element_points"] + extra_points
383
384    def _draw_all_aspects_lines(self, r, ar):
385        out = ""
386        for aspect in self.aspects_list:
387            aspect_name = aspect["aspect"]
388            aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None)
389            if aspect_color:
390                out += draw_aspect_line(
391                    r=r,
392                    ar=ar,
393                    aspect=aspect,
394                    color=aspect_color,
395                    seventh_house_degree_ut=self.user.seventh_house.abs_pos
396                )
397        return out
398
399    def _draw_all_transit_aspects_lines(self, r, ar):
400        out = ""
401        for aspect in self.aspects_list:
402            aspect_name = aspect["aspect"]
403            aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None)
404            if aspect_color:
405                out += draw_aspect_line(
406                    r=r,
407                    ar=ar,
408                    aspect=aspect,
409                    color=aspect_color,
410                    seventh_house_degree_ut=self.user.seventh_house.abs_pos
411                )
412        return out
413
414    def _create_template_dictionary(self) -> ChartTemplateDictionary:
415        """
416        Create a dictionary containing the template data for generating an astrological chart.
417
418        Returns:
419            ChartTemplateDictionary: A dictionary with template data for the chart.
420        """
421        # Initialize template dictionary
422        template_dict: dict = {}
423
424        # Set the color style tag
425        template_dict["color_style_tag"] = self.color_style_tag
426
427        # Set chart dimensions
428        template_dict["chart_height"] = self.height
429        template_dict["chart_width"] = self.width
430
431        # Set viewbox based on chart type
432        if self.chart_type in ["Natal", "ExternalNatal", "Composite"]:
433            template_dict['viewbox'] = self._BASIC_CHART_VIEWBOX
434        elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit":
435            template_dict['viewbox'] = self._TRANSIT_CHART_WITH_TABLE_VIWBOX
436        else:
437            template_dict['viewbox'] = self._WIDE_CHART_VIEWBOX
438
439        # Generate rings and circles based on chart type
440        if self.chart_type in ["Transit", "Synastry"]:
441            template_dict["transitRing"] = draw_transit_ring(self.main_radius, self.chart_colors_settings["paper_1"], self.chart_colors_settings["zodiac_transit_ring_3"])
442            template_dict["degreeRing"] = draw_transit_ring_degree_steps(self.main_radius, self.user.seventh_house.abs_pos)
443            template_dict["first_circle"] = draw_first_circle(self.main_radius, self.chart_colors_settings["zodiac_transit_ring_2"], self.chart_type)
444            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)
445            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)
446
447            if self.double_chart_aspect_grid_type == "list":
448                title = ""
449                if self.chart_type == "Synastry":
450                    title = self.language_settings.get("couple_aspects", "Couple Aspects")
451                else:
452                    title = self.language_settings.get("transit_aspects", "Transit Aspects")
453
454                template_dict["makeAspectGrid"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings)
455            else:
456                template_dict["makeAspectGrid"] = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, 550, 450)
457
458            template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
459        else:
460            template_dict["transitRing"] = ""
461            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"])
462            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)
463            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)
464            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)
465            template_dict["makeAspectGrid"] = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list)
466
467            template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
468
469        # Set chart title
470        if self.chart_type == "Synastry":
471            template_dict["stringTitle"] = f"{self.user.name} {self.language_settings['and_word']} {self.t_user.name}"
472        elif self.chart_type == "Transit":
473            template_dict["stringTitle"] = f"{self.language_settings['transits']} {self.t_user.day}/{self.t_user.month}/{self.t_user.year}"
474        elif self.chart_type in ["Natal", "ExternalNatal"]:
475            template_dict["stringTitle"] = self.user.name
476        elif self.chart_type == "Composite":
477            template_dict["stringTitle"] = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}"
478
479        # Zodiac Type Info
480        if self.user.zodiac_type == 'Tropic':
481            zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}"
482        else:
483            mode_const = "SIDM_" + self.user.sidereal_mode # type: ignore
484            mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
485            zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
486
487        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')}"
488        template_dict["bottom_left_1"] = zodiac_info
489
490        if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]:
491            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", "")}'
492            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)}'
493            template_dict["bottom_left_4"] = f'{self.language_settings.get(self.user.perspective_type.lower().replace(" ", "_"), self.user.perspective_type)}'
494        elif self.chart_type == "Transit":
495            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", "")}'
496            template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.t_user.lunar_phase.moon_phase_name}'
497            template_dict["bottom_left_4"] = f'{self.language_settings.get(self.t_user.perspective_type.lower().replace(" ", "_"), self.t_user.perspective_type)}'
498        elif self.chart_type == "Composite":
499            template_dict["bottom_left_2"] = f'{self.user.first_subject.perspective_type}'
500            template_dict["bottom_left_3"] = f'{self.language_settings.get("composite_chart", "Composite Chart")} - {self.language_settings.get("midpoints", "Midpoints")}'
501            template_dict["bottom_left_4"] = ""
502
503        # Draw moon phase
504        moon_phase_dict = calculate_moon_phase_chart_params(
505            self.user.lunar_phase["degrees_between_s_m"],
506            self.geolat
507        )
508
509        template_dict["lunar_phase_rotate"] = moon_phase_dict["lunar_phase_rotate"]
510        template_dict["lunar_phase_circle_center_x"] = moon_phase_dict["circle_center_x"]
511        template_dict["lunar_phase_circle_radius"] = moon_phase_dict["circle_radius"]
512
513        if self.chart_type == "Composite":
514            template_dict["top_left_1"] = f"{datetime.fromisoformat(self.user.first_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}"
515        # Set location string
516        elif len(self.location) > 35:
517            split_location = self.location.split(",")
518            if len(split_location) > 1:
519                template_dict["top_left_1"] = split_location[0] + ", " + split_location[-1]
520                if len(template_dict["top_left_1"]) > 35:
521                    template_dict["top_left_1"] = template_dict["top_left_1"][:35] + "..."
522            else:
523                template_dict["top_left_1"] = self.location[:35] + "..."
524        else:
525            template_dict["top_left_1"] = self.location
526
527        # Set chart name
528        if self.chart_type in ["Synastry", "Transit"]:
529            template_dict["top_left_0"] = f"{self.user.name}:"
530        elif self.chart_type in ["Natal", "ExternalNatal"]:
531            template_dict["top_left_0"] = f'{self.language_settings["info"]}:'
532        elif self.chart_type == "Composite":
533            template_dict["top_left_0"] = f'{self.user.first_subject.name}'
534
535        # Set additional information for Synastry chart type
536        if self.chart_type == "Synastry":
537            template_dict["top_left_3"] = f"{self.t_user.name}: "
538            template_dict["top_left_4"] = self.t_user.city
539            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}"
540        elif self.chart_type == "Composite":
541            template_dict["top_left_3"] = self.user.second_subject.name
542            template_dict["top_left_4"] = f"{datetime.fromisoformat(self.user.second_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}"
543            latitude_string = convert_latitude_coordinate_to_string(self.user.second_subject.lat, self.language_settings['north_letter'], self.language_settings['south_letter'])
544            longitude_string = convert_longitude_coordinate_to_string(self.user.second_subject.lng, self.language_settings['east_letter'], self.language_settings['west_letter'])
545            template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}"
546        else:
547            latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings['north'], self.language_settings['south'])
548            longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings['east'], self.language_settings['west'])
549            template_dict["top_left_3"] = f"{self.language_settings['latitude']}: {latitude_string}"
550            template_dict["top_left_4"] = f"{self.language_settings['longitude']}: {longitude_string}"
551            template_dict["top_left_5"] = f"{self.language_settings['type']}: {self.language_settings.get(self.chart_type, self.chart_type)}"
552
553
554        # Set paper colors
555        template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"]
556        template_dict["paper_color_1"] = self.chart_colors_settings["paper_1"]
557
558        # Set planet colors
559        for planet in self.planets_settings:
560            planet_id = planet["id"]
561            template_dict[f"planets_color_{planet_id}"] = planet["color"] # type: ignore
562
563        # Set zodiac colors
564        for i in range(12):
565            template_dict[f"zodiac_color_{i}"] = self.chart_colors_settings[f"zodiac_icon_{i}"] # type: ignore
566
567        # Set orb colors
568        for aspect in self.aspects_settings:
569            template_dict[f"orb_color_{aspect['degree']}"] = aspect['color'] # type: ignore
570
571        # Drawing functions
572        template_dict["makeZodiac"] = self._draw_zodiac_circle_slices(self.main_radius)
573
574        first_subject_houses_list = get_houses_list(self.user)
575
576        # Draw houses grid and cusps
577        if self.chart_type in ["Transit", "Synastry"]:
578            second_subject_houses_list = get_houses_list(self.t_user)
579
580            template_dict["makeHousesGrid"] = draw_house_grid(
581                main_subject_houses_list=first_subject_houses_list,
582                secondary_subject_houses_list=second_subject_houses_list,
583                chart_type=self.chart_type,
584                text_color=self.chart_colors_settings["paper_0"],
585                house_cusp_generale_name_label=self.language_settings["cusp"]
586            )
587
588            template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
589                r=self.main_radius,
590                first_subject_houses_list=first_subject_houses_list,
591                standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
592                first_house_color=self.planets_settings[12]["color"],
593                tenth_house_color=self.planets_settings[13]["color"],
594                seventh_house_color=self.planets_settings[14]["color"],
595                fourth_house_color=self.planets_settings[15]["color"],
596                c1=self.first_circle_radius,
597                c3=self.third_circle_radius,
598                chart_type=self.chart_type,
599                second_subject_houses_list=second_subject_houses_list,
600                transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
601            )
602
603        else:
604            template_dict["makeHousesGrid"] = draw_house_grid(
605                main_subject_houses_list=first_subject_houses_list,
606                chart_type=self.chart_type,
607                text_color=self.chart_colors_settings["paper_0"],
608                house_cusp_generale_name_label=self.language_settings["cusp"]
609            )
610
611            template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
612                r=self.main_radius,
613                first_subject_houses_list=first_subject_houses_list,
614                standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
615                first_house_color=self.planets_settings[12]["color"],
616                tenth_house_color=self.planets_settings[13]["color"],
617                seventh_house_color=self.planets_settings[14]["color"],
618                fourth_house_color=self.planets_settings[15]["color"],
619                c1=self.first_circle_radius,
620                c3=self.third_circle_radius,
621                chart_type=self.chart_type,
622            )
623
624        # Draw planets
625        if self.chart_type in ["Transit", "Synastry"]:
626            template_dict["makePlanets"] = draw_planets(
627                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
628                available_planets_setting=self.available_planets_setting,
629                second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
630                radius=self.main_radius,
631                main_subject_first_house_degree_ut=self.user.first_house.abs_pos,
632                main_subject_seventh_house_degree_ut=self.user.seventh_house.abs_pos,
633                chart_type=self.chart_type,
634                third_circle_radius=self.third_circle_radius,
635            )
636        else:
637            template_dict["makePlanets"] = draw_planets(
638                available_planets_setting=self.available_planets_setting,
639                chart_type=self.chart_type,
640                radius=self.main_radius,
641                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
642                third_circle_radius=self.third_circle_radius,
643                main_subject_first_house_degree_ut=self.user.first_house.abs_pos,
644                main_subject_seventh_house_degree_ut=self.user.seventh_house.abs_pos
645            )
646
647        # Draw elements percentages
648        total = self.fire + self.water + self.earth + self.air
649
650        fire_percentage = int(round(100 * self.fire / total))
651        earth_percentage = int(round(100 * self.earth / total))
652        air_percentage = int(round(100 * self.air / total))
653        water_percentage = int(round(100 * self.water / total))
654
655        template_dict["fire_string"] = f"{self.language_settings['fire']} {fire_percentage}%"
656        template_dict["earth_string"] = f"{self.language_settings['earth']} {earth_percentage}%"
657        template_dict["air_string"] = f"{self.language_settings['air']} {air_percentage}%"
658        template_dict["water_string"] = f"{self.language_settings['water']} {water_percentage}%"
659
660        # Draw planet grid
661        if self.chart_type in ["Transit", "Synastry"]:
662            if self.chart_type == "Transit":
663                second_subject_table_name = self.language_settings["transit_name"]
664            else:
665                second_subject_table_name = self.t_user.name
666
667            template_dict["makePlanetGrid"] = draw_planet_grid(
668                planets_and_houses_grid_title=self.language_settings["planets_and_house"],
669                subject_name=self.user.name,
670                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
671                chart_type=self.chart_type,
672                text_color=self.chart_colors_settings["paper_0"],
673                celestial_point_language=self.language_settings["celestial_points"],
674                second_subject_name=second_subject_table_name,
675                second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
676            )
677        else:
678            if self.chart_type == "Composite":
679                subject_name = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}"
680            else:
681                subject_name = self.user.name
682
683            template_dict["makePlanetGrid"] = draw_planet_grid(
684                planets_and_houses_grid_title=self.language_settings["planets_and_house"],
685                subject_name=subject_name,
686                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
687                chart_type=self.chart_type,
688                text_color=self.chart_colors_settings["paper_0"],
689                celestial_point_language=self.language_settings["celestial_points"],
690            )
691
692        # Set date time string
693        if self.chart_type in ["Composite"]:
694            # First Subject Latitude and Longitude
695            latitude = convert_latitude_coordinate_to_string(self.user.first_subject.lat, self.language_settings["north_letter"], self.language_settings["south_letter"])
696            longitude = convert_longitude_coordinate_to_string(self.user.first_subject.lng, self.language_settings["east_letter"], self.language_settings["west_letter"])
697            template_dict["top_left_2"] = f"{latitude} {longitude}"
698        else:
699            dt = datetime.fromisoformat(self.user.iso_formatted_local_datetime)
700            custom_format = dt.strftime('%Y-%m-%d %H:%M [%z]')
701            custom_format = custom_format[:-3] + ':' + custom_format[-3:]
702            template_dict["top_left_2"] = f"{custom_format}"
703
704        return ChartTemplateDictionary(**template_dict)
705
706    def makeTemplate(self, minify: bool = False) -> str:
707        """Creates the template for the SVG file"""
708        td = self._create_template_dictionary()
709
710        DATA_DIR = Path(__file__).parent
711        xml_svg = DATA_DIR / "templates" / "chart.xml"
712
713        # read template
714        with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f:
715            template = Template(f.read()).substitute(td)
716
717        # return filename
718
719        logging.debug(f"Template dictionary keys: {td.keys()}")
720
721        self._create_template_dictionary()
722
723        if minify:
724            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
725
726        else:
727            template = template.replace('"', "'")
728
729        return template
730
731    def makeSVG(self, minify: bool = False):
732        """Prints out the SVG file in the specified folder"""
733
734        if not hasattr(self, "template"):
735            self.template = self.makeTemplate(minify)
736
737        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg"
738
739        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
740            output_file.write(self.template)
741
742        print(f"SVG Generated Correctly in: {chartname}")
743    def makeWheelOnlyTemplate(self, minify: bool = False):
744        """Creates the template for the SVG file with only the wheel"""
745
746        with open(Path(__file__).parent / "templates" / "wheel_only.xml", "r", encoding="utf-8", errors="ignore") as f:
747            template = f.read()
748
749        template_dict = self._create_template_dictionary()
750        template = Template(template).substitute(template_dict)
751
752        if minify:
753            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
754
755        else:
756            template = template.replace('"', "'")
757
758        return template
759
760    def makeWheelOnlySVG(self, minify: bool = False):
761        """Prints out the SVG file in the specified folder with only the wheel"""
762
763        template = self.makeWheelOnlyTemplate(minify)
764        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Wheel Only.svg"
765
766        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
767            output_file.write(template)
768
769        print(f"SVG Generated Correctly in: {chartname}")
770    def makeAspectGridOnlyTemplate(self, minify: bool = False):
771        """Creates the template for the SVG file with only the aspect grid"""
772
773        with open(Path(__file__).parent / "templates" / "aspect_grid_only.xml", "r", encoding="utf-8", errors="ignore") as f:
774            template = f.read()
775
776        template_dict = self._create_template_dictionary()
777
778        if self.chart_type in ["Transit", "Synastry"]:
779            aspects_grid = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list)
780        else:
781            aspects_grid = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, x_start=50, y_start=250)
782
783        template = Template(template).substitute({**template_dict, "makeAspectGrid": aspects_grid})
784
785        if minify:
786            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
787
788        else:
789            template = template.replace('"', "'")
790
791        return template
792
793    def makeAspectGridOnlySVG(self, minify: bool = False):
794        """Prints out the SVG file in the specified folder with only the aspect grid"""
795
796        template = self.makeAspectGridOnlyTemplate(minify)
797        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Aspect Grid Only.svg"
798
799        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
800            output_file.write(template)
801
802        print(f"SVG Generated Correctly in: {chartname}")

Creates the instance that can generate the chart with the function makeSVG().

Parameters: - first_obj: First kerykeion object - chart_type: Natal, ExternalNatal, Transit, Synastry (Default: Type="Natal"). - second_obj: Second kerykeion object (Not required if type is Natal) - new_output_directory: Set the output directory (default: home directory). - new_settings_file: Set the settings file (default: kr.config.json). In the settings file you can set the language, colors, planets, aspects, etc. - theme: Set the theme for the chart (default: classic). If None the