# kerykeion.charts.kerykeion_chart_svg

This is part of Kerykeion (C) 2025 Giacomo Battaglia

View Source

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

class KerykeionChartSVG: View Source

     49class KerykeionChartSVG:
     50    """
     51    KerykeionChartSVG generates astrological chart visualizations as SVG files.
     52
     53    This class supports creating full chart SVGs, wheel-only SVGs, and aspect-grid-only SVGs
     54    for various chart types including Natal, ExternalNatal, Transit, Synastry, and Composite.
     55    Charts are rendered using XML templates and drawing utilities, with customizable themes,
     56    language, active points, and aspects.
     57    The rendered SVGs can be saved to a specified output directory or, by default, to the user's home directory.
     58
     59    NOTE:
     60        The generated SVG files are optimized for web use, opening in browsers. If you want to
     61        use them in other applications, you might need to adjust the SVG settings or styles.
     62
     63    Args:
     64        first_obj (AstrologicalSubject | AstrologicalSubjectModel | CompositeSubjectModel):
     65            The primary astrological subject for the chart.
     66        chart_type (ChartType, optional):
     67            The type of chart to generate ('Natal', 'ExternalNatal', 'Transit', 'Synastry', 'Composite').
     68            Defaults to 'Natal'.
     69        second_obj (AstrologicalSubject | AstrologicalSubjectModel, optional):
     70            The secondary subject for Transit or Synastry charts. Not required for Natal or Composite.
     71        new_output_directory (str | Path, optional):
     72            Directory to write generated SVG files. Defaults to the user's home directory.
     73        new_settings_file (Path | dict | KerykeionSettingsModel, optional):
     74            Path or settings object to override default chart configuration (colors, fonts, aspects).
     75        theme (KerykeionChartTheme, optional):
     76            CSS theme for the chart. If None, no default styles are applied. Defaults to 'classic'.
     77        double_chart_aspect_grid_type (Literal['list', 'table'], optional):
     78            Specifies rendering style for double-chart aspect grids. Defaults to 'list'.
     79        chart_language (KerykeionChartLanguage, optional):
     80            Language code for chart labels. Defaults to 'EN'.
     81        active_points (list[Planet | AxialCusps], optional):
     82            List of celestial points and angles to include. Defaults to DEFAULT_ACTIVE_POINTS.
     83            Example:
     84            ["Sun", "Moon", "Mercury", "Venus"]
     85
     86        active_aspects (list[ActiveAspect], optional):
     87            List of aspects (name and orb) to calculate. Defaults to DEFAULT_ACTIVE_ASPECTS.
     88            Example:
     89            [
     90                {"name": "conjunction", "orb": 10},
     91                {"name": "opposition", "orb": 10},
     92                {"name": "trine", "orb": 8},
     93                {"name": "sextile", "orb": 6},
     94                {"name": "square", "orb": 5},
     95                {"name": "quintile", "orb": 1},
     96            ]
     97
     98    Public Methods:
     99        makeTemplate(minify=False, remove_css_variables=False) -> str:
    100            Render the full chart SVG as a string without writing to disk. Use `minify=True`
    101            to remove whitespace and quotes, and `remove_css_variables=True` to embed CSS vars.
    102
    103        makeSVG(minify=False, remove_css_variables=False) -> None:
    104            Generate and write the full chart SVG file to the output directory.
    105            Filenames follow the pattern:
    106            '{subject.name} - {chart_type} Chart.svg'.
    107
    108        makeWheelOnlyTemplate(minify=False, remove_css_variables=False) -> str:
    109            Render only the chart wheel (no aspect grid) as an SVG string.
    110
    111        makeWheelOnlySVG(minify=False, remove_css_variables=False) -> None:
    112            Generate and write the wheel-only SVG file:
    113            '{subject.name} - {chart_type} Chart - Wheel Only.svg'.
    114
    115        makeAspectGridOnlyTemplate(minify=False, remove_css_variables=False) -> str:
    116            Render only the aspect grid as an SVG string.
    117
    118        makeAspectGridOnlySVG(minify=False, remove_css_variables=False) -> None:
    119            Generate and write the aspect-grid-only SVG file:
    120            '{subject.name} - {chart_type} Chart - Aspect Grid Only.svg'.
    121    """
    122
    123    # Constants
    124    _BASIC_CHART_VIEWBOX = "0 0 820 550.0"
    125    _WIDE_CHART_VIEWBOX = "0 0 1200 546.0"
    126    _TRANSIT_CHART_WITH_TABLE_VIWBOX = "0 0 960 546.0"
    127
    128    _DEFAULT_HEIGHT = 550
    129    _DEFAULT_FULL_WIDTH = 1200
    130    _DEFAULT_NATAL_WIDTH = 820
    131    _DEFAULT_FULL_WIDTH_WITH_TABLE = 960
    132    _PLANET_IN_ZODIAC_EXTRA_POINTS = 10
    133
    134    # Set at init
    135    first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel]
    136    second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None]
    137    chart_type: ChartType
    138    new_output_directory: Union[Path, None]
    139    new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
    140    output_directory: Path
    141    new_settings_file: Union[Path, None, KerykeionSettingsModel, dict]
    142    theme: Union[KerykeionChartTheme, None]
    143    double_chart_aspect_grid_type: Literal["list", "table"]
    144    chart_language: KerykeionChartLanguage
    145    active_points: List[Union[Planet, AxialCusps]]
    146    active_aspects: List[ActiveAspect]
    147
    148    # Internal properties
    149    fire: float
    150    earth: float
    151    air: float
    152    water: float
    153    first_circle_radius: float
    154    second_circle_radius: float
    155    third_circle_radius: float
    156    width: Union[float, int]
    157    language_settings: dict
    158    chart_colors_settings: dict
    159    planets_settings: dict
    160    aspects_settings: dict
    161    user: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel]
    162    available_planets_setting: List[KerykeionSettingsCelestialPointModel]
    163    height: float
    164    location: str
    165    geolat: float
    166    geolon: float
    167    template: str
    168
    169    def __init__(
    170        self,
    171        first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel],
    172        chart_type: ChartType = "Natal",
    173        second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] = None,
    174        new_output_directory: Union[str, None] = None,
    175        new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None,
    176        theme: Union[KerykeionChartTheme, None] = "classic",
    177        double_chart_aspect_grid_type: Literal["list", "table"] = "list",
    178        chart_language: KerykeionChartLanguage = "EN",
    179        active_points: List[Union[Planet, AxialCusps]] = DEFAULT_ACTIVE_POINTS,
    180    active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
    181    ):
    182        """
    183        Initialize the chart generator with subject data and configuration options.
    184
    185        Args:
    186            first_obj (AstrologicalSubject, AstrologicalSubjectModel, or CompositeSubjectModel):
    187                Primary astrological subject instance.
    188            chart_type (ChartType, optional):
    189                Type of chart to generate (e.g., 'Natal', 'Transit').
    190            second_obj (AstrologicalSubject or AstrologicalSubjectModel, optional):
    191                Secondary subject for Transit or Synastry charts.
    192            new_output_directory (str or Path, optional):
    193                Base directory to save generated SVG files.
    194            new_settings_file (Path, dict, or KerykeionSettingsModel, optional):
    195                Custom settings source for chart colors, fonts, and aspects.
    196            theme (KerykeionChartTheme or None, optional):
    197                CSS theme to apply; None for default styling.
    198            double_chart_aspect_grid_type (Literal['list','table'], optional):
    199                Layout style for double-chart aspect grids ('list' or 'table').
    200            chart_language (KerykeionChartLanguage, optional):
    201                Language code for chart labels (e.g., 'EN', 'IT').
    202            active_points (List[Planet or AxialCusps], optional):
    203                Celestial points to include in the chart visualization.
    204            active_aspects (List[ActiveAspect], optional):
    205                Aspects to calculate, each defined by name and orb.
    206        """
    207        home_directory = Path.home()
    208        self.new_settings_file = new_settings_file
    209        self.chart_language = chart_language
    210        self.active_points = active_points
    211        self.active_aspects = active_aspects
    212
    213        if new_output_directory:
    214            self.output_directory = Path(new_output_directory)
    215        else:
    216            self.output_directory = home_directory
    217
    218        self.parse_json_settings(new_settings_file)
    219        self.chart_type = chart_type
    220
    221        # Kerykeion instance
    222        self.user = first_obj
    223
    224        self.available_planets_setting = []
    225        for body in self.planets_settings:
    226            if body["name"] not in active_points:
    227                continue
    228            else:
    229                body["is_active"] = True
    230
    231            self.available_planets_setting.append(body)
    232
    233        # Available bodies
    234        available_celestial_points_names = []
    235        for body in self.available_planets_setting:
    236            available_celestial_points_names.append(body["name"].lower())
    237
    238        self.available_kerykeion_celestial_points: list[KerykeionPointModel] = []
    239        for body in available_celestial_points_names:
    240            self.available_kerykeion_celestial_points.append(self.user.get(body))
    241
    242        # Makes the sign number list.
    243        if self.chart_type == "Natal" or self.chart_type == "ExternalNatal":
    244            natal_aspects_instance = NatalAspects(
    245                self.user, new_settings_file=self.new_settings_file,
    246                active_points=active_points,
    247                active_aspects=active_aspects,
    248            )
    249            self.aspects_list = natal_aspects_instance.relevant_aspects
    250
    251        elif self.chart_type == "Transit" or self.chart_type == "Synastry":
    252            if not second_obj:
    253                raise KerykeionException("Second object is required for Transit or Synastry charts.")
    254
    255            # Kerykeion instance
    256            self.t_user = second_obj
    257
    258            # Aspects
    259            if self.chart_type == "Transit":
    260                synastry_aspects_instance = SynastryAspects(
    261                    self.t_user,
    262                    self.user,
    263                    new_settings_file=self.new_settings_file,
    264                    active_points=active_points,
    265                    active_aspects=active_aspects,
    266                )
    267
    268            else:
    269                synastry_aspects_instance = SynastryAspects(
    270                    self.user,
    271                    self.t_user,
    272                    new_settings_file=self.new_settings_file,
    273                    active_points=active_points,
    274                    active_aspects=active_aspects,
    275                )
    276
    277            self.aspects_list = synastry_aspects_instance.relevant_aspects
    278
    279            self.t_available_kerykeion_celestial_points = []
    280            for body in available_celestial_points_names:
    281                self.t_available_kerykeion_celestial_points.append(self.t_user.get(body))
    282
    283        elif self.chart_type == "Composite":
    284            if not isinstance(first_obj, CompositeSubjectModel):
    285                raise KerykeionException("First object must be a CompositeSubjectModel instance.")
    286
    287            self.aspects_list = NatalAspects(self.user, new_settings_file=self.new_settings_file, active_points=active_points).relevant_aspects
    288
    289        # Double chart aspect grid type
    290        self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
    291
    292        # screen size
    293        self.height = self._DEFAULT_HEIGHT
    294        if self.chart_type == "Synastry" or self.chart_type == "Transit":
    295            self.width = self._DEFAULT_FULL_WIDTH
    296        elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit":
    297            self.width = self._DEFAULT_FULL_WIDTH_WITH_TABLE
    298        else:
    299            self.width = self._DEFAULT_NATAL_WIDTH
    300
    301        if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]:
    302            self.location = self.user.city
    303            self.geolat = self.user.lat
    304            self.geolon =  self.user.lng
    305
    306        elif self.chart_type == "Composite":
    307            self.location = ""
    308            self.geolat = (self.user.first_subject.lat + self.user.second_subject.lat) / 2
    309            self.geolon = (self.user.first_subject.lng + self.user.second_subject.lng) / 2
    310
    311        elif self.chart_type in ["Transit"]:
    312            self.location = self.t_user.city
    313            self.geolat = self.t_user.lat
    314            self.geolon = self.t_user.lng
    315            self.t_name = self.language_settings["transit_name"]
    316
    317        # Default radius for the chart
    318        self.main_radius = 240
    319
    320        # Set circle radii based on chart type
    321        if self.chart_type == "ExternalNatal":
    322            self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 56, 92, 112
    323        else:
    324            self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 0, 36, 120
    325
    326        # Initialize element points
    327        self.fire = 0.0
    328        self.earth = 0.0
    329        self.air = 0.0
    330        self.water = 0.0
    331
    332        # Calculate element points from planets
    333        self._calculate_elements_points_from_planets()
    334
    335        # Set up theme
    336        if theme not in get_args(KerykeionChartTheme) and theme is not None:
    337            raise KerykeionException(f"Theme {theme} is not available. Set None for default theme.")
    338
    339        self.set_up_theme(theme)
    340
    341    def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None:
    342        """
    343        Load and apply a CSS theme for the chart visualization.
    344
    345        Args:
    346            theme (KerykeionChartTheme or None): Name of the theme to apply. If None, no CSS is applied.
    347        """
    348        if theme is None:
    349            self.color_style_tag = ""
    350            return
    351
    352        theme_dir = Path(__file__).parent / "themes"
    353
    354        with open(theme_dir / f"{theme}.css", "r") as f:
    355            self.color_style_tag = f.read()
    356
    357    def set_output_directory(self, dir_path: Path) -> None:
    358        """
    359        Set the directory where generated SVG files will be saved.
    360
    361        Args:
    362            dir_path (Path): Target directory for SVG output.
    363        """
    364        self.output_directory = dir_path
    365        logging.info(f"Output direcotry set to: {self.output_directory}")
    366
    367    def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None:
    368        """
    369        Load and parse chart configuration settings.
    370
    371        Args:
    372            settings_file_or_dict (Path, dict, or KerykeionSettingsModel):
    373                Source for custom chart settings.
    374        """
    375        settings = get_settings(settings_file_or_dict)
    376
    377        self.language_settings = settings["language_settings"][self.chart_language]
    378        self.chart_colors_settings = settings["chart_colors"]
    379        self.planets_settings = settings["celestial_points"]
    380        self.aspects_settings = settings["aspects"]
    381
    382    def _draw_zodiac_circle_slices(self, r):
    383        """
    384        Draw zodiac circle slices for each sign.
    385
    386        Args:
    387            r (float): Outer radius of the zodiac ring.
    388
    389        Returns:
    390            str: Concatenated SVG elements for zodiac slices.
    391        """
    392        sings = get_args(Sign)
    393        output = ""
    394        for i, sing in enumerate(sings):
    395            output += draw_zodiac_slice(
    396                c1=self.first_circle_radius,
    397                chart_type=self.chart_type,
    398                seventh_house_degree_ut=self.user.seventh_house.abs_pos,
    399                num=i,
    400                r=r,
    401                style=f'fill:{self.chart_colors_settings[f"zodiac_bg_{i}"]}; fill-opacity: 0.5;',
    402                type=sing,
    403            )
    404
    405        return output
    406
    407    def _calculate_elements_points_from_planets(self):
    408        """
    409        Compute elemental point totals based on active planetary positions.
    410
    411        Iterates over each active planet to determine its zodiac element and adds extra points
    412        if the planet is in a related sign. Updates self.fire, self.earth, self.air, and self.water.
    413
    414        Returns:
    415            None
    416        """
    417
    418        ZODIAC = (
    419            {"name": "Ari", "element": "fire"},
    420            {"name": "Tau", "element": "earth"},
    421            {"name": "Gem", "element": "air"},
    422            {"name": "Can", "element": "water"},
    423            {"name": "Leo", "element": "fire"},
    424            {"name": "Vir", "element": "earth"},
    425            {"name": "Lib", "element": "air"},
    426            {"name": "Sco", "element": "water"},
    427            {"name": "Sag", "element": "fire"},
    428            {"name": "Cap", "element": "earth"},
    429            {"name": "Aqu", "element": "air"},
    430            {"name": "Pis", "element": "water"},
    431        )
    432
    433        # Available bodies
    434        available_celestial_points_names = []
    435        for body in self.available_planets_setting:
    436            available_celestial_points_names.append(body["name"].lower())
    437
    438        # Make list of the points sign
    439        points_sign = []
    440        for planet in available_celestial_points_names:
    441            points_sign.append(self.user.get(planet).sign_num)
    442
    443        for i in range(len(self.available_planets_setting)):
    444            # element: get extra points if planet is in own zodiac sign.
    445            related_zodiac_signs = self.available_planets_setting[i]["related_zodiac_signs"]
    446            cz = points_sign[i]
    447            extra_points = 0
    448            if related_zodiac_signs != []:
    449                for e in range(len(related_zodiac_signs)):
    450                    if int(related_zodiac_signs[e]) == int(cz):
    451                        extra_points = self._PLANET_IN_ZODIAC_EXTRA_POINTS
    452
    453            ele = ZODIAC[points_sign[i]]["element"]
    454            if ele == "fire":
    455                self.fire = self.fire + self.available_planets_setting[i]["element_points"] + extra_points
    456
    457            elif ele == "earth":
    458                self.earth = self.earth + self.available_planets_setting[i]["element_points"] + extra_points
    459
    460            elif ele == "air":
    461                self.air = self.air + self.available_planets_setting[i]["element_points"] + extra_points
    462
    463            elif ele == "water":
    464                self.water = self.water + self.available_planets_setting[i]["element_points"] + extra_points
    465
    466    def _draw_all_aspects_lines(self, r, ar):
    467        """
    468        Render SVG lines for all aspects in the chart.
    469
    470        Args:
    471            r (float): Radius at which aspect lines originate.
    472            ar (float): Radius at which aspect lines terminate.
    473
    474        Returns:
    475            str: SVG markup for all aspect lines.
    476        """
    477        out = ""
    478        for aspect in self.aspects_list:
    479            aspect_name = aspect["aspect"]
    480            aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None)
    481            if aspect_color:
    482                out += draw_aspect_line(
    483                    r=r,
    484                    ar=ar,
    485                    aspect=aspect,
    486                    color=aspect_color,
    487                    seventh_house_degree_ut=self.user.seventh_house.abs_pos
    488                )
    489        return out
    490
    491    def _draw_all_transit_aspects_lines(self, r, ar):
    492        """
    493        Render SVG lines for all transit aspects in the chart.
    494
    495        Args:
    496            r (float): Radius at which transit aspect lines originate.
    497            ar (float): Radius at which transit aspect lines terminate.
    498
    499        Returns:
    500            str: SVG markup for all transit aspect lines.
    501        """
    502        out = ""
    503        for aspect in self.aspects_list:
    504            aspect_name = aspect["aspect"]
    505            aspect_color = next((a["color"] for a in self.aspects_settings if a["name"] == aspect_name), None)
    506            if aspect_color:
    507                out += draw_aspect_line(
    508                    r=r,
    509                    ar=ar,
    510                    aspect=aspect,
    511                    color=aspect_color,
    512                    seventh_house_degree_ut=self.user.seventh_house.abs_pos
    513                )
    514        return out
    515
    516    def _create_template_dictionary(self) -> ChartTemplateDictionary:
    517        """
    518        Assemble chart data and rendering instructions into a template dictionary.
    519
    520        Gathers styling, dimensions, and SVG fragments for chart components based on
    521        chart type and subjects.
    522
    523        Returns:
    524            ChartTemplateDictionary: Populated structure of template variables.
    525        """
    526        # Initialize template dictionary
    527        template_dict: dict = {}
    528
    529        # Set the color style tag
    530        template_dict["color_style_tag"] = self.color_style_tag
    531
    532        # Set chart dimensions
    533        template_dict["chart_height"] = self.height
    534        template_dict["chart_width"] = self.width
    535
    536        # Set viewbox based on chart type
    537        if self.chart_type in ["Natal", "ExternalNatal", "Composite"]:
    538            template_dict['viewbox'] = self._BASIC_CHART_VIEWBOX
    539        elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit":
    540            template_dict['viewbox'] = self._TRANSIT_CHART_WITH_TABLE_VIWBOX
    541        else:
    542            template_dict['viewbox'] = self._WIDE_CHART_VIEWBOX
    543
    544        # Generate rings and circles based on chart type
    545        if self.chart_type in ["Transit", "Synastry"]:
    546            template_dict["transitRing"] = draw_transit_ring(self.main_radius, self.chart_colors_settings["paper_1"], self.chart_colors_settings["zodiac_transit_ring_3"])
    547            template_dict["degreeRing"] = draw_transit_ring_degree_steps(self.main_radius, self.user.seventh_house.abs_pos)
    548            template_dict["first_circle"] = draw_first_circle(self.main_radius, self.chart_colors_settings["zodiac_transit_ring_2"], self.chart_type)
    549            template_dict["second_circle"] = draw_second_circle(self.main_radius, self.chart_colors_settings['zodiac_transit_ring_1'], self.chart_colors_settings['paper_1'], self.chart_type)
    550            template_dict['third_circle'] = draw_third_circle(self.main_radius, self.chart_colors_settings['zodiac_transit_ring_0'], self.chart_colors_settings['paper_1'], self.chart_type, self.third_circle_radius)
    551
    552            if self.double_chart_aspect_grid_type == "list":
    553                title = ""
    554                if self.chart_type == "Synastry":
    555                    title = self.language_settings.get("couple_aspects", "Couple Aspects")
    556                else:
    557                    title = self.language_settings.get("transit_aspects", "Transit Aspects")
    558
    559                template_dict["makeAspectGrid"] = draw_transit_aspect_list(title, self.aspects_list, self.planets_settings, self.aspects_settings)
    560            else:
    561                template_dict["makeAspectGrid"] = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, 550, 450)
    562
    563            template_dict["makeAspects"] = self._draw_all_transit_aspects_lines(self.main_radius, self.main_radius - 160)
    564        else:
    565            template_dict["transitRing"] = ""
    566            template_dict["degreeRing"] = draw_degree_ring(self.main_radius, self.first_circle_radius, self.user.seventh_house.abs_pos, self.chart_colors_settings["paper_0"])
    567            template_dict['first_circle'] = draw_first_circle(self.main_radius, self.chart_colors_settings["zodiac_radix_ring_2"], self.chart_type, self.first_circle_radius)
    568            template_dict["second_circle"] = draw_second_circle(self.main_radius, self.chart_colors_settings["zodiac_radix_ring_1"], self.chart_colors_settings["paper_1"], self.chart_type, self.second_circle_radius)
    569            template_dict['third_circle'] = draw_third_circle(self.main_radius, self.chart_colors_settings["zodiac_radix_ring_0"], self.chart_colors_settings["paper_1"], self.chart_type, self.third_circle_radius)
    570            template_dict["makeAspectGrid"] = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list)
    571
    572            template_dict["makeAspects"] = self._draw_all_aspects_lines(self.main_radius, self.main_radius - self.third_circle_radius)
    573
    574        # Set chart title
    575        if self.chart_type == "Synastry":
    576            template_dict["stringTitle"] = f"{self.user.name} {self.language_settings['and_word']} {self.t_user.name}"
    577        elif self.chart_type == "Transit":
    578            template_dict["stringTitle"] = f"{self.language_settings['transits']} {self.t_user.day}/{self.t_user.month}/{self.t_user.year}"
    579        elif self.chart_type in ["Natal", "ExternalNatal"]:
    580            template_dict["stringTitle"] = self.user.name
    581        elif self.chart_type == "Composite":
    582            template_dict["stringTitle"] = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}"
    583
    584        # Zodiac Type Info
    585        if self.user.zodiac_type == 'Tropic':
    586            zodiac_info = f"{self.language_settings.get('zodiac', 'Zodiac')}: {self.language_settings.get('tropical', 'Tropical')}"
    587        else:
    588            mode_const = "SIDM_" + self.user.sidereal_mode # type: ignore
    589            mode_name = swe.get_ayanamsa_name(getattr(swe, mode_const))
    590            zodiac_info = f"{self.language_settings.get('ayanamsa', 'Ayanamsa')}: {mode_name}"
    591
    592        template_dict["bottom_left_0"] = f"{self.language_settings.get('houses_system_' + self.user.houses_system_identifier, self.user.houses_system_name)} {self.language_settings.get('houses', 'Houses')}"
    593        template_dict["bottom_left_1"] = zodiac_info
    594
    595        if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]:
    596            template_dict["bottom_left_2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")} {self.language_settings.get("day", "Day").lower()}: {self.user.lunar_phase.get("moon_phase", "")}'
    597            template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get(self.user.lunar_phase.moon_phase_name.lower().replace(" ", "_"), self.user.lunar_phase.moon_phase_name)}'
    598            template_dict["bottom_left_4"] = f'{self.language_settings.get(self.user.perspective_type.lower().replace(" ", "_"), self.user.perspective_type)}'
    599        elif self.chart_type == "Transit":
    600            template_dict["bottom_left_2"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.language_settings.get("day", "Day")} {self.t_user.lunar_phase.get("moon_phase", "")}'
    601            template_dict["bottom_left_3"] = f'{self.language_settings.get("lunar_phase", "Lunar Phase")}: {self.t_user.lunar_phase.moon_phase_name}'
    602            template_dict["bottom_left_4"] = f'{self.language_settings.get(self.t_user.perspective_type.lower().replace(" ", "_"), self.t_user.perspective_type)}'
    603        elif self.chart_type == "Composite":
    604            template_dict["bottom_left_2"] = f'{self.user.first_subject.perspective_type}'
    605            template_dict["bottom_left_3"] = f'{self.language_settings.get("composite_chart", "Composite Chart")} - {self.language_settings.get("midpoints", "Midpoints")}'
    606            template_dict["bottom_left_4"] = ""
    607
    608        # Draw moon phase
    609        moon_phase_dict = calculate_moon_phase_chart_params(
    610            self.user.lunar_phase["degrees_between_s_m"],
    611            self.geolat
    612        )
    613
    614        template_dict["lunar_phase_rotate"] = moon_phase_dict["lunar_phase_rotate"]
    615        template_dict["lunar_phase_circle_center_x"] = moon_phase_dict["circle_center_x"]
    616        template_dict["lunar_phase_circle_radius"] = moon_phase_dict["circle_radius"]
    617
    618        if self.chart_type == "Composite":
    619            template_dict["top_left_1"] = f"{datetime.fromisoformat(self.user.first_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}"
    620        # Set location string
    621        elif len(self.location) > 35:
    622            split_location = self.location.split(",")
    623            if len(split_location) > 1:
    624                template_dict["top_left_1"] = split_location[0] + ", " + split_location[-1]
    625                if len(template_dict["top_left_1"]) > 35:
    626                    template_dict["top_left_1"] = template_dict["top_left_1"][:35] + "..."
    627            else:
    628                template_dict["top_left_1"] = self.location[:35] + "..."
    629        else:
    630            template_dict["top_left_1"] = self.location
    631
    632        # Set chart name
    633        if self.chart_type in ["Synastry", "Transit"]:
    634            template_dict["top_left_0"] = f"{self.user.name}:"
    635        elif self.chart_type in ["Natal", "ExternalNatal"]:
    636            template_dict["top_left_0"] = f'{self.language_settings["info"]}:'
    637        elif self.chart_type == "Composite":
    638            template_dict["top_left_0"] = f'{self.user.first_subject.name}'
    639
    640        # Set additional information for Synastry chart type
    641        if self.chart_type == "Synastry":
    642            template_dict["top_left_3"] = f"{self.t_user.name}: "
    643            template_dict["top_left_4"] = self.t_user.city
    644            template_dict["top_left_5"] = f"{self.t_user.year}-{self.t_user.month}-{self.t_user.day} {self.t_user.hour:02d}:{self.t_user.minute:02d}"
    645        elif self.chart_type == "Composite":
    646            template_dict["top_left_3"] = self.user.second_subject.name
    647            template_dict["top_left_4"] = f"{datetime.fromisoformat(self.user.second_subject.iso_formatted_local_datetime).strftime('%Y-%m-%d %H:%M')}"
    648            latitude_string = convert_latitude_coordinate_to_string(self.user.second_subject.lat, self.language_settings['north_letter'], self.language_settings['south_letter'])
    649            longitude_string = convert_longitude_coordinate_to_string(self.user.second_subject.lng, self.language_settings['east_letter'], self.language_settings['west_letter'])
    650            template_dict["top_left_5"] = f"{latitude_string} / {longitude_string}"
    651        else:
    652            latitude_string = convert_latitude_coordinate_to_string(self.geolat, self.language_settings['north'], self.language_settings['south'])
    653            longitude_string = convert_longitude_coordinate_to_string(self.geolon, self.language_settings['east'], self.language_settings['west'])
    654            template_dict["top_left_3"] = f"{self.language_settings['latitude']}: {latitude_string}"
    655            template_dict["top_left_4"] = f"{self.language_settings['longitude']}: {longitude_string}"
    656            template_dict["top_left_5"] = f"{self.language_settings['type']}: {self.language_settings.get(self.chart_type, self.chart_type)}"
    657
    658
    659        # Set paper colors
    660        template_dict["paper_color_0"] = self.chart_colors_settings["paper_0"]
    661        template_dict["paper_color_1"] = self.chart_colors_settings["paper_1"]
    662
    663        # Set planet colors
    664        for planet in self.planets_settings:
    665            planet_id = planet["id"]
    666            template_dict[f"planets_color_{planet_id}"] = planet["color"] # type: ignore
    667
    668        # Set zodiac colors
    669        for i in range(12):
    670            template_dict[f"zodiac_color_{i}"] = self.chart_colors_settings[f"zodiac_icon_{i}"] # type: ignore
    671
    672        # Set orb colors
    673        for aspect in self.aspects_settings:
    674            template_dict[f"orb_color_{aspect['degree']}"] = aspect['color'] # type: ignore
    675
    676        # Drawing functions
    677        template_dict["makeZodiac"] = self._draw_zodiac_circle_slices(self.main_radius)
    678
    679        first_subject_houses_list = get_houses_list(self.user)
    680
    681        # Draw houses grid and cusps
    682        if self.chart_type in ["Transit", "Synastry"]:
    683            second_subject_houses_list = get_houses_list(self.t_user)
    684
    685            template_dict["makeHousesGrid"] = draw_house_grid(
    686                main_subject_houses_list=first_subject_houses_list,
    687                secondary_subject_houses_list=second_subject_houses_list,
    688                chart_type=self.chart_type,
    689                text_color=self.chart_colors_settings["paper_0"],
    690                house_cusp_generale_name_label=self.language_settings["cusp"]
    691            )
    692
    693            template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
    694                r=self.main_radius,
    695                first_subject_houses_list=first_subject_houses_list,
    696                standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
    697                first_house_color=self.planets_settings[12]["color"],
    698                tenth_house_color=self.planets_settings[13]["color"],
    699                seventh_house_color=self.planets_settings[14]["color"],
    700                fourth_house_color=self.planets_settings[15]["color"],
    701                c1=self.first_circle_radius,
    702                c3=self.third_circle_radius,
    703                chart_type=self.chart_type,
    704                second_subject_houses_list=second_subject_houses_list,
    705                transit_house_cusp_color=self.chart_colors_settings["houses_transit_line"],
    706            )
    707
    708        else:
    709            template_dict["makeHousesGrid"] = draw_house_grid(
    710                main_subject_houses_list=first_subject_houses_list,
    711                chart_type=self.chart_type,
    712                text_color=self.chart_colors_settings["paper_0"],
    713                house_cusp_generale_name_label=self.language_settings["cusp"]
    714            )
    715
    716            template_dict["makeHouses"] = draw_houses_cusps_and_text_number(
    717                r=self.main_radius,
    718                first_subject_houses_list=first_subject_houses_list,
    719                standard_house_cusp_color=self.chart_colors_settings["houses_radix_line"],
    720                first_house_color=self.planets_settings[12]["color"],
    721                tenth_house_color=self.planets_settings[13]["color"],
    722                seventh_house_color=self.planets_settings[14]["color"],
    723                fourth_house_color=self.planets_settings[15]["color"],
    724                c1=self.first_circle_radius,
    725                c3=self.third_circle_radius,
    726                chart_type=self.chart_type,
    727            )
    728
    729        # Draw planets
    730        if self.chart_type in ["Transit", "Synastry"]:
    731            template_dict["makePlanets"] = draw_planets(
    732                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
    733                available_planets_setting=self.available_planets_setting,
    734                second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
    735                radius=self.main_radius,
    736                main_subject_first_house_degree_ut=self.user.first_house.abs_pos,
    737                main_subject_seventh_house_degree_ut=self.user.seventh_house.abs_pos,
    738                chart_type=self.chart_type,
    739                third_circle_radius=self.third_circle_radius,
    740            )
    741        else:
    742            template_dict["makePlanets"] = draw_planets(
    743                available_planets_setting=self.available_planets_setting,
    744                chart_type=self.chart_type,
    745                radius=self.main_radius,
    746                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
    747                third_circle_radius=self.third_circle_radius,
    748                main_subject_first_house_degree_ut=self.user.first_house.abs_pos,
    749                main_subject_seventh_house_degree_ut=self.user.seventh_house.abs_pos
    750            )
    751
    752        # Draw elements percentages
    753        total = self.fire + self.water + self.earth + self.air
    754
    755        fire_percentage = int(round(100 * self.fire / total))
    756        earth_percentage = int(round(100 * self.earth / total))
    757        air_percentage = int(round(100 * self.air / total))
    758        water_percentage = int(round(100 * self.water / total))
    759
    760        template_dict["fire_string"] = f"{self.language_settings['fire']} {fire_percentage}%"
    761        template_dict["earth_string"] = f"{self.language_settings['earth']} {earth_percentage}%"
    762        template_dict["air_string"] = f"{self.language_settings['air']} {air_percentage}%"
    763        template_dict["water_string"] = f"{self.language_settings['water']} {water_percentage}%"
    764
    765        # Draw planet grid
    766        if self.chart_type in ["Transit", "Synastry"]:
    767            if self.chart_type == "Transit":
    768                second_subject_table_name = self.language_settings["transit_name"]
    769            else:
    770                second_subject_table_name = self.t_user.name
    771
    772            template_dict["makePlanetGrid"] = draw_planet_grid(
    773                planets_and_houses_grid_title=self.language_settings["planets_and_house"],
    774                subject_name=self.user.name,
    775                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
    776                chart_type=self.chart_type,
    777                text_color=self.chart_colors_settings["paper_0"],
    778                celestial_point_language=self.language_settings["celestial_points"],
    779                second_subject_name=second_subject_table_name,
    780                second_subject_available_kerykeion_celestial_points=self.t_available_kerykeion_celestial_points,
    781            )
    782        else:
    783            if self.chart_type == "Composite":
    784                subject_name = f"{self.user.first_subject.name} {self.language_settings['and_word']} {self.user.second_subject.name}"
    785            else:
    786                subject_name = self.user.name
    787
    788            template_dict["makePlanetGrid"] = draw_planet_grid(
    789                planets_and_houses_grid_title=self.language_settings["planets_and_house"],
    790                subject_name=subject_name,
    791                available_kerykeion_celestial_points=self.available_kerykeion_celestial_points,
    792                chart_type=self.chart_type,
    793                text_color=self.chart_colors_settings["paper_0"],
    794                celestial_point_language=self.language_settings["celestial_points"],
    795            )
    796
    797        # Set date time string
    798        if self.chart_type in ["Composite"]:
    799            # First Subject Latitude and Longitude
    800            latitude = convert_latitude_coordinate_to_string(self.user.first_subject.lat, self.language_settings["north_letter"], self.language_settings["south_letter"])
    801            longitude = convert_longitude_coordinate_to_string(self.user.first_subject.lng, self.language_settings["east_letter"], self.language_settings["west_letter"])
    802            template_dict["top_left_2"] = f"{latitude} {longitude}"
    803        else:
    804            dt = datetime.fromisoformat(self.user.iso_formatted_local_datetime)
    805            custom_format = dt.strftime('%Y-%m-%d %H:%M [%z]')
    806            custom_format = custom_format[:-3] + ':' + custom_format[-3:]
    807            template_dict["top_left_2"] = f"{custom_format}"
    808
    809        return ChartTemplateDictionary(**template_dict)
    810
    811    def makeTemplate(self, minify: bool = False, remove_css_variables = False) -> str:
    812        """
    813        Render the full chart SVG as a string.
    814
    815        Reads the XML template, substitutes variables, and optionally inlines CSS
    816        variables and minifies the output.
    817
    818        Args:
    819            minify (bool): Remove whitespace and quotes for compactness.
    820            remove_css_variables (bool): Embed CSS variable definitions.
    821
    822        Returns:
    823            str: SVG markup as a string.
    824        """
    825        td = self._create_template_dictionary()
    826
    827        DATA_DIR = Path(__file__).parent
    828        xml_svg = DATA_DIR / "templates" / "chart.xml"
    829
    830        # read template
    831        with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f:
    832            template = Template(f.read()).substitute(td)
    833
    834        # return filename
    835
    836        logging.debug(f"Template dictionary keys: {td.keys()}")
    837
    838        self._create_template_dictionary()
    839
    840        if remove_css_variables:
    841            template = inline_css_variables_in_svg(template)
    842
    843        if minify:
    844            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
    845
    846        else:
    847            template = template.replace('"', "'")
    848
    849        return template
    850
    851    def makeSVG(self, minify: bool = False, remove_css_variables = False):
    852        """
    853        Generate and save the full chart SVG to disk.
    854
    855        Calls makeTemplate to render the SVG, then writes a file named
    856        "{subject.name} - {chart_type} Chart.svg" in the output directory.
    857
    858        Args:
    859            minify (bool): Pass-through to makeTemplate for compact output.
    860            remove_css_variables (bool): Pass-through to makeTemplate to embed CSS variables.
    861
    862        Returns:
    863            None
    864        """
    865
    866        self.template = self.makeTemplate(minify, remove_css_variables)
    867
    868        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg"
    869
    870        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
    871            output_file.write(self.template)
    872
    873        print(f"SVG Generated Correctly in: {chartname}")
    874
    875    def makeWheelOnlyTemplate(self, minify: bool = False, remove_css_variables = False):
    876        """
    877        Render the wheel-only chart SVG as a string.
    878
    879        Reads the wheel-only XML template, substitutes chart data, and applies optional
    880        CSS inlining and minification.
    881
    882        Args:
    883            minify (bool): Remove whitespace and quotes for compactness.
    884            remove_css_variables (bool): Embed CSS variable definitions.
    885
    886        Returns:
    887            str: SVG markup for the chart wheel only.
    888        """
    889
    890        with open(Path(__file__).parent / "templates" / "wheel_only.xml", "r", encoding="utf-8", errors="ignore") as f:
    891            template = f.read()
    892
    893        template_dict = self._create_template_dictionary()
    894        template = Template(template).substitute(template_dict)
    895
    896        if remove_css_variables:
    897            template = inline_css_variables_in_svg(template)
    898
    899        if minify:
    900            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
    901
    902        else:
    903            template = template.replace('"', "'")
    904
    905        return template
    906
    907    def makeWheelOnlySVG(self, minify: bool = False, remove_css_variables = False):
    908        """
    909        Generate and save wheel-only chart SVG to disk.
    910
    911        Calls makeWheelOnlyTemplate and writes a file named
    912        "{subject.name} - {chart_type} Chart - Wheel Only.svg" in the output directory.
    913
    914        Args:
    915            minify (bool): Pass-through to makeWheelOnlyTemplate for compact output.
    916            remove_css_variables (bool): Pass-through to makeWheelOnlyTemplate to embed CSS variables.
    917
    918        Returns:
    919            None
    920        """
    921
    922        template = self.makeWheelOnlyTemplate(minify, remove_css_variables)
    923        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Wheel Only.svg"
    924
    925        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
    926            output_file.write(template)
    927
    928        print(f"SVG Generated Correctly in: {chartname}")
    929
    930    def makeAspectGridOnlyTemplate(self, minify: bool = False, remove_css_variables = False):
    931        """
    932        Render the aspect-grid-only chart SVG as a string.
    933
    934        Reads the aspect-grid XML template, generates the aspect grid based on chart type,
    935        and applies optional CSS inlining and minification.
    936
    937        Args:
    938            minify (bool): Remove whitespace and quotes for compactness.
    939            remove_css_variables (bool): Embed CSS variable definitions.
    940
    941        Returns:
    942            str: SVG markup for the aspect grid only.
    943        """
    944
    945        with open(Path(__file__).parent / "templates" / "aspect_grid_only.xml", "r", encoding="utf-8", errors="ignore") as f:
    946            template = f.read()
    947
    948        template_dict = self._create_template_dictionary()
    949
    950        if self.chart_type in ["Transit", "Synastry"]:
    951            aspects_grid = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list)
    952        else:
    953            aspects_grid = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, x_start=50, y_start=250)
    954
    955        template = Template(template).substitute({**template_dict, "makeAspectGrid": aspects_grid})
    956
    957        if remove_css_variables:
    958            template = inline_css_variables_in_svg(template)
    959
    960        if minify:
    961            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
    962
    963        else:
    964            template = template.replace('"', "'")
    965
    966        return template
    967
    968    def makeAspectGridOnlySVG(self, minify: bool = False, remove_css_variables = False):
    969        """
    970        Generate and save aspect-grid-only chart SVG to disk.
    971
    972        Calls makeAspectGridOnlyTemplate and writes a file named
    973        "{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the output directory.
    974
    975        Args:
    976            minify (bool): Pass-through to makeAspectGridOnlyTemplate for compact output.
    977            remove_css_variables (bool): Pass-through to makeAspectGridOnlyTemplate to embed CSS variables.
    978
    979        Returns:
    980            None
    981        """
    982
    983        template = self.makeAspectGridOnlyTemplate(minify, remove_css_variables)
    984        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Aspect Grid Only.svg"
    985
    986        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
    987            output_file.write(template)
    988
    989        print(f"SVG Generated Correctly in: {chartname}")

KerykeionChartSVG generates astrological chart visualizations as SVG files.

This class supports creating full chart SVGs, wheel-only SVGs, and aspect-grid-only SVGs for various chart types including Natal, ExternalNatal, Transit, Synastry, and Composite. Charts are rendered using XML templates and drawing utilities, with customizable themes, language, active points, and aspects. The rendered SVGs can be saved to a specified output directory or, by default, to the user's home directory.

NOTE: The generated SVG files are optimized for web use, opening in browsers. If you want to use them in other applications, you might need to adjust the SVG settings or styles.

Args: first_obj (AstrologicalSubject | AstrologicalSubjectModel | CompositeSubjectModel): The primary astrological subject for the chart. chart_type (ChartType, optional): The type of chart to generate ('Natal', 'ExternalNatal', 'Transit', 'Synastry', 'Composite'). Defaults to 'Natal'. second_obj (AstrologicalSubject | AstrologicalSubjectModel, optional): The secondary subject for Transit or Synastry charts. Not required for Natal or Composite. new_output_directory (str | Path, optional): Directory to write generated SVG files. Defaults to the user's home directory. new_settings_file (Path | dict | KerykeionSettingsModel, optional): Path or settings object to override default chart configuration (colors, fonts, aspects). theme (KerykeionChartTheme, optional): CSS theme for the chart. If None, no default styles are applied. Defaults to 'classic'. double_chart_aspect_grid_type (Literal['list', 'table'], optional): Specifies rendering style for double-chart aspect grids. Defaults to 'list'. chart_language (KerykeionChartLanguage, optional): Language code for chart labels. Defaults to 'EN'. active_points (list[Planet | AxialCusps], optional): List of celestial points and angles to include. Defaults to DEFAULT_ACTIVE_POINTS. Example: ["Sun", "Moon", "Mercury", "Venus"]

    active_aspects (list[ActiveAspect], optional):
        List of aspects (name and orb) to calculate. Defaults to DEFAULT_ACTIVE_ASPECTS.
        Example:
        [
            {"name": "conjunction", "orb": 10},
            {"name": "opposition", "orb": 10},
            {"name": "trine", "orb": 8},
            {"name": "sextile", "orb": 6},
            {"name": "square", "orb": 5},
            {"name": "quintile", "orb": 1},
        ]

Public Methods: makeTemplate(minify=False, remove_css_variables=False) -> str: Render the full chart SVG as a string without writing to disk. Use `minify=True` to remove whitespace and quotes, and `remove_css_variables=True` to embed CSS vars.

    makeSVG(minify=False, remove_css_variables=False) -> None:
        Generate and write the full chart SVG file to the output directory.
        Filenames follow the pattern:
        '{subject.name} - {chart_type} Chart.svg'.

    makeWheelOnlyTemplate(minify=False, remove_css_variables=False) -> str:
        Render only the chart wheel (no aspect grid) as an SVG string.

    makeWheelOnlySVG(minify=False, remove_css_variables=False) -> None:
        Generate and write the wheel-only SVG file:
        '{subject.name} - {chart_type} Chart - Wheel Only.svg'.

    makeAspectGridOnlyTemplate(minify=False, remove_css_variables=False) -> str:
        Render only the aspect grid as an SVG string.

    makeAspectGridOnlySVG(minify=False, remove_css_variables=False) -> None:
        Generate and write the aspect-grid-only SVG file:
        '{subject.name} - {chart_type} Chart - Aspect Grid Only.svg'.

KerykeionChartSVG( first_obj: Union[kerykeion.astrological_subject.AstrologicalSubject, kerykeion.kr_types.kr_models.AstrologicalSubjectModel, kerykeion.kr_types.kr_models.CompositeSubjectModel], chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit', 'Composite'] = 'Natal', second_obj: Union[kerykeion.astrological_subject.AstrologicalSubject, kerykeion.kr_types.kr_models.AstrologicalSubjectModel, NoneType] = None, new_output_directory: Optional[str] = None, new_settings_file: Union[pathlib._local.Path, NoneType, kerykeion.kr_types.settings_models.KerykeionSettingsModel, dict] = None, theme: Optional[Literal['light', 'dark', 'dark-high-contrast', 'classic']] = 'classic', double_chart_aspect_grid_type: Literal['list', 'table'] = 'list', chart_language: Literal['EN', 'FR', 'PT', 'IT', 'CN', 'ES', 'RU', 'TR', 'DE', 'HI'] = 'EN', active_points: List[Union[Literal['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', 'Mean_Node', 'True_Node', 'Mean_South_Node', 'True_South_Node', 'Chiron', 'Mean_Lilith'], Literal['Ascendant', 'Medium_Coeli', 'Descendant', 'Imum_Coeli']]] = ['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', 'Mean_Node', 'Chiron', 'Ascendant', 'Medium_Coeli', 'Mean_Lilith', 'Mean_South_Node'], active_aspects: List[kerykeion.kr_types.kr_models.ActiveAspect] = [{'name': 'conjunction', 'orb': 10}, {'name': 'opposition', 'orb': 10}, {'name': 'trine', 'orb': 8}, {'name': 'sextile', 'orb': 6}, {'name': 'square', 'orb': 5}, {'name': 'quintile', 'orb': 1}]) View Source

    169    def __init__(
    170        self,
    171        first_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel],
    172        chart_type: ChartType = "Natal",
    173        second_obj: Union[AstrologicalSubject, AstrologicalSubjectModel, None] = None,
    174        new_output_directory: Union[str, None] = None,
    175        new_settings_file: Union[Path, None, KerykeionSettingsModel, dict] = None,
    176        theme: Union[KerykeionChartTheme, None] = "classic",
    177        double_chart_aspect_grid_type: Literal["list", "table"] = "list",
    178        chart_language: KerykeionChartLanguage = "EN",
    179        active_points: List[Union[Planet, AxialCusps]] = DEFAULT_ACTIVE_POINTS,
    180    active_aspects: List[ActiveAspect] = DEFAULT_ACTIVE_ASPECTS,
    181    ):
    182        """
    183        Initialize the chart generator with subject data and configuration options.
    184
    185        Args:
    186            first_obj (AstrologicalSubject, AstrologicalSubjectModel, or CompositeSubjectModel):
    187                Primary astrological subject instance.
    188            chart_type (ChartType, optional):
    189                Type of chart to generate (e.g., 'Natal', 'Transit').
    190            second_obj (AstrologicalSubject or AstrologicalSubjectModel, optional):
    191                Secondary subject for Transit or Synastry charts.
    192            new_output_directory (str or Path, optional):
    193                Base directory to save generated SVG files.
    194            new_settings_file (Path, dict, or KerykeionSettingsModel, optional):
    195                Custom settings source for chart colors, fonts, and aspects.
    196            theme (KerykeionChartTheme or None, optional):
    197                CSS theme to apply; None for default styling.
    198            double_chart_aspect_grid_type (Literal['list','table'], optional):
    199                Layout style for double-chart aspect grids ('list' or 'table').
    200            chart_language (KerykeionChartLanguage, optional):
    201                Language code for chart labels (e.g., 'EN', 'IT').
    202            active_points (List[Planet or AxialCusps], optional):
    203                Celestial points to include in the chart visualization.
    204            active_aspects (List[ActiveAspect], optional):
    205                Aspects to calculate, each defined by name and orb.
    206        """
    207        home_directory = Path.home()
    208        self.new_settings_file = new_settings_file
    209        self.chart_language = chart_language
    210        self.active_points = active_points
    211        self.active_aspects = active_aspects
    212
    213        if new_output_directory:
    214            self.output_directory = Path(new_output_directory)
    215        else:
    216            self.output_directory = home_directory
    217
    218        self.parse_json_settings(new_settings_file)
    219        self.chart_type = chart_type
    220
    221        # Kerykeion instance
    222        self.user = first_obj
    223
    224        self.available_planets_setting = []
    225        for body in self.planets_settings:
    226            if body["name"] not in active_points:
    227                continue
    228            else:
    229                body["is_active"] = True
    230
    231            self.available_planets_setting.append(body)
    232
    233        # Available bodies
    234        available_celestial_points_names = []
    235        for body in self.available_planets_setting:
    236            available_celestial_points_names.append(body["name"].lower())
    237
    238        self.available_kerykeion_celestial_points: list[KerykeionPointModel] = []
    239        for body in available_celestial_points_names:
    240            self.available_kerykeion_celestial_points.append(self.user.get(body))
    241
    242        # Makes the sign number list.
    243        if self.chart_type == "Natal" or self.chart_type == "ExternalNatal":
    244            natal_aspects_instance = NatalAspects(
    245                self.user, new_settings_file=self.new_settings_file,
    246                active_points=active_points,
    247                active_aspects=active_aspects,
    248            )
    249            self.aspects_list = natal_aspects_instance.relevant_aspects
    250
    251        elif self.chart_type == "Transit" or self.chart_type == "Synastry":
    252            if not second_obj:
    253                raise KerykeionException("Second object is required for Transit or Synastry charts.")
    254
    255            # Kerykeion instance
    256            self.t_user = second_obj
    257
    258            # Aspects
    259            if self.chart_type == "Transit":
    260                synastry_aspects_instance = SynastryAspects(
    261                    self.t_user,
    262                    self.user,
    263                    new_settings_file=self.new_settings_file,
    264                    active_points=active_points,
    265                    active_aspects=active_aspects,
    266                )
    267
    268            else:
    269                synastry_aspects_instance = SynastryAspects(
    270                    self.user,
    271                    self.t_user,
    272                    new_settings_file=self.new_settings_file,
    273                    active_points=active_points,
    274                    active_aspects=active_aspects,
    275                )
    276
    277            self.aspects_list = synastry_aspects_instance.relevant_aspects
    278
    279            self.t_available_kerykeion_celestial_points = []
    280            for body in available_celestial_points_names:
    281                self.t_available_kerykeion_celestial_points.append(self.t_user.get(body))
    282
    283        elif self.chart_type == "Composite":
    284            if not isinstance(first_obj, CompositeSubjectModel):
    285                raise KerykeionException("First object must be a CompositeSubjectModel instance.")
    286
    287            self.aspects_list = NatalAspects(self.user, new_settings_file=self.new_settings_file, active_points=active_points).relevant_aspects
    288
    289        # Double chart aspect grid type
    290        self.double_chart_aspect_grid_type = double_chart_aspect_grid_type
    291
    292        # screen size
    293        self.height = self._DEFAULT_HEIGHT
    294        if self.chart_type == "Synastry" or self.chart_type == "Transit":
    295            self.width = self._DEFAULT_FULL_WIDTH
    296        elif self.double_chart_aspect_grid_type == "table" and self.chart_type == "Transit":
    297            self.width = self._DEFAULT_FULL_WIDTH_WITH_TABLE
    298        else:
    299            self.width = self._DEFAULT_NATAL_WIDTH
    300
    301        if self.chart_type in ["Natal", "ExternalNatal", "Synastry"]:
    302            self.location = self.user.city
    303            self.geolat = self.user.lat
    304            self.geolon =  self.user.lng
    305
    306        elif self.chart_type == "Composite":
    307            self.location = ""
    308            self.geolat = (self.user.first_subject.lat + self.user.second_subject.lat) / 2
    309            self.geolon = (self.user.first_subject.lng + self.user.second_subject.lng) / 2
    310
    311        elif self.chart_type in ["Transit"]:
    312            self.location = self.t_user.city
    313            self.geolat = self.t_user.lat
    314            self.geolon = self.t_user.lng
    315            self.t_name = self.language_settings["transit_name"]
    316
    317        # Default radius for the chart
    318        self.main_radius = 240
    319
    320        # Set circle radii based on chart type
    321        if self.chart_type == "ExternalNatal":
    322            self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 56, 92, 112
    323        else:
    324            self.first_circle_radius, self.second_circle_radius, self.third_circle_radius = 0, 36, 120
    325
    326        # Initialize element points
    327        self.fire = 0.0
    328        self.earth = 0.0
    329        self.air = 0.0
    330        self.water = 0.0
    331
    332        # Calculate element points from planets
    333        self._calculate_elements_points_from_planets()
    334
    335        # Set up theme
    336        if theme not in get_args(KerykeionChartTheme) and theme is not None:
    337            raise KerykeionException(f"Theme {theme} is not available. Set None for default theme.")
    338
    339        self.set_up_theme(theme)

Initialize the chart generator with subject data and configuration options.

Args: first_obj (AstrologicalSubject, AstrologicalSubjectModel, or CompositeSubjectModel): Primary astrological subject instance. chart_type (ChartType, optional): Type of chart to generate (e.g., 'Natal', 'Transit'). second_obj (AstrologicalSubject or AstrologicalSubjectModel, optional): Secondary subject for Transit or Synastry charts. new_output_directory (str or Path, optional): Base directory to save generated SVG files. new_settings_file (Path, dict, or KerykeionSettingsModel, optional): Custom settings source for chart colors, fonts, and aspects. theme (KerykeionChartTheme or None, optional): CSS theme to apply; None for default styling. double_chart_aspect_grid_type (Literal['list','table'], optional): Layout style for double-chart aspect grids ('list' or 'table'). chart_language (KerykeionChartLanguage, optional): Language code for chart labels (e.g., 'EN', 'IT'). active_points (List[Planet or AxialCusps], optional): Celestial points to include in the chart visualization. active_aspects (List[ActiveAspect], optional): Aspects to calculate, each defined by name and orb.

first_obj: Union[kerykeion.astrological_subject.AstrologicalSubject, kerykeion.kr_types.kr_models.AstrologicalSubjectModel]
second_obj: Union[kerykeion.astrological_subject.AstrologicalSubject, kerykeion.kr_types.kr_models.AstrologicalSubjectModel, NoneType]
chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit', 'Composite']
new_output_directory: Optional[pathlib._local.Path]
new_settings_file: Union[pathlib._local.Path, NoneType, kerykeion.kr_types.settings_models.KerykeionSettingsModel, dict]
output_directory: pathlib._local.Path
theme: Optional[Literal['light', 'dark', 'dark-high-contrast', 'classic']]
double_chart_aspect_grid_type: Literal['list', 'table']
chart_language: Literal['EN', 'FR', 'PT', 'IT', 'CN', 'ES', 'RU', 'TR', 'DE', 'HI']
active_points: List[Union[Literal['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', 'Mean_Node', 'True_Node', 'Mean_South_Node', 'True_South_Node', 'Chiron', 'Mean_Lilith'], Literal['Ascendant', 'Medium_Coeli', 'Descendant', 'Imum_Coeli']]]
active_aspects: List[kerykeion.kr_types.kr_models.ActiveAspect]
fire: float
earth: float
air: float
water: float
first_circle_radius: float
second_circle_radius: float
third_circle_radius: float
width: Union[float, int]
language_settings: dict
chart_colors_settings: dict
planets_settings: dict
aspects_settings: dict
user: Union[kerykeion.astrological_subject.AstrologicalSubject, kerykeion.kr_types.kr_models.AstrologicalSubjectModel, kerykeion.kr_types.kr_models.CompositeSubjectModel]
available_planets_setting: List[kerykeion.kr_types.settings_models.KerykeionSettingsCelestialPointModel]
height: float
location: str
geolat: float
geolon: float
template: str
available_kerykeion_celestial_points: list[kerykeion.kr_types.kr_models.KerykeionPointModel]
main_radius
def set_up_theme( self, theme: Optional[Literal['light', 'dark', 'dark-high-contrast', 'classic']] = None) -> None: View Source

    341    def set_up_theme(self, theme: Union[KerykeionChartTheme, None] = None) -> None:
    342        """
    343        Load and apply a CSS theme for the chart visualization.
    344
    345        Args:
    346            theme (KerykeionChartTheme or None): Name of the theme to apply. If None, no CSS is applied.
    347        """
    348        if theme is None:
    349            self.color_style_tag = ""
    350            return
    351
    352        theme_dir = Path(__file__).parent / "themes"
    353
    354        with open(theme_dir / f"{theme}.css", "r") as f:
    355            self.color_style_tag = f.read()

Load and apply a CSS theme for the chart visualization.

Args: theme (KerykeionChartTheme or None): Name of the theme to apply. If None, no CSS is applied.

def set_output_directory(self, dir_path: pathlib._local.Path) -> None: View Source

    357    def set_output_directory(self, dir_path: Path) -> None:
    358        """
    359        Set the directory where generated SVG files will be saved.
    360
    361        Args:
    362            dir_path (Path): Target directory for SVG output.
    363        """
    364        self.output_directory = dir_path
    365        logging.info(f"Output direcotry set to: {self.output_directory}")

Set the directory where generated SVG files will be saved.

Args: dir_path (Path): Target directory for SVG output.

def parse_json_settings( self, settings_file_or_dict: Union[pathlib._local.Path, dict, kerykeion.kr_types.settings_models.KerykeionSettingsModel, NoneType]) -> None: View Source

    367    def parse_json_settings(self, settings_file_or_dict: Union[Path, dict, KerykeionSettingsModel, None]) -> None:
    368        """
    369        Load and parse chart configuration settings.
    370
    371        Args:
    372            settings_file_or_dict (Path, dict, or KerykeionSettingsModel):
    373                Source for custom chart settings.
    374        """
    375        settings = get_settings(settings_file_or_dict)
    376
    377        self.language_settings = settings["language_settings"][self.chart_language]
    378        self.chart_colors_settings = settings["chart_colors"]
    379        self.planets_settings = settings["celestial_points"]
    380        self.aspects_settings = settings["aspects"]

Load and parse chart configuration settings.

Args: settings_file_or_dict (Path, dict, or KerykeionSettingsModel): Source for custom chart settings.

def makeTemplate(self, minify: bool = False, remove_css_variables=False) -> str: View Source

    811    def makeTemplate(self, minify: bool = False, remove_css_variables = False) -> str:
    812        """
    813        Render the full chart SVG as a string.
    814
    815        Reads the XML template, substitutes variables, and optionally inlines CSS
    816        variables and minifies the output.
    817
    818        Args:
    819            minify (bool): Remove whitespace and quotes for compactness.
    820            remove_css_variables (bool): Embed CSS variable definitions.
    821
    822        Returns:
    823            str: SVG markup as a string.
    824        """
    825        td = self._create_template_dictionary()
    826
    827        DATA_DIR = Path(__file__).parent
    828        xml_svg = DATA_DIR / "templates" / "chart.xml"
    829
    830        # read template
    831        with open(xml_svg, "r", encoding="utf-8", errors="ignore") as f:
    832            template = Template(f.read()).substitute(td)
    833
    834        # return filename
    835
    836        logging.debug(f"Template dictionary keys: {td.keys()}")
    837
    838        self._create_template_dictionary()
    839
    840        if remove_css_variables:
    841            template = inline_css_variables_in_svg(template)
    842
    843        if minify:
    844            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
    845
    846        else:
    847            template = template.replace('"', "'")
    848
    849        return template

Render the full chart SVG as a string.

Reads the XML template, substitutes variables, and optionally inlines CSS variables and minifies the output.

Args: minify (bool): Remove whitespace and quotes for compactness. remove_css_variables (bool): Embed CSS variable definitions.

Returns: str: SVG markup as a string.

def makeSVG(self, minify: bool = False, remove_css_variables=False): View Source

    851    def makeSVG(self, minify: bool = False, remove_css_variables = False):
    852        """
    853        Generate and save the full chart SVG to disk.
    854
    855        Calls makeTemplate to render the SVG, then writes a file named
    856        "{subject.name} - {chart_type} Chart.svg" in the output directory.
    857
    858        Args:
    859            minify (bool): Pass-through to makeTemplate for compact output.
    860            remove_css_variables (bool): Pass-through to makeTemplate to embed CSS variables.
    861
    862        Returns:
    863            None
    864        """
    865
    866        self.template = self.makeTemplate(minify, remove_css_variables)
    867
    868        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg"
    869
    870        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
    871            output_file.write(self.template)
    872
    873        print(f"SVG Generated Correctly in: {chartname}")

Generate and save the full chart SVG to disk.

Calls makeTemplate to render the SVG, then writes a file named "{subject.name} - {chart_type} Chart.svg" in the output directory.

Args: minify (bool): Pass-through to makeTemplate for compact output. remove_css_variables (bool): Pass-through to makeTemplate to embed CSS variables.

Returns: None

def makeWheelOnlyTemplate(self, minify: bool = False, remove_css_variables=False): View Source

    875    def makeWheelOnlyTemplate(self, minify: bool = False, remove_css_variables = False):
    876        """
    877        Render the wheel-only chart SVG as a string.
    878
    879        Reads the wheel-only XML template, substitutes chart data, and applies optional
    880        CSS inlining and minification.
    881
    882        Args:
    883            minify (bool): Remove whitespace and quotes for compactness.
    884            remove_css_variables (bool): Embed CSS variable definitions.
    885
    886        Returns:
    887            str: SVG markup for the chart wheel only.
    888        """
    889
    890        with open(Path(__file__).parent / "templates" / "wheel_only.xml", "r", encoding="utf-8", errors="ignore") as f:
    891            template = f.read()
    892
    893        template_dict = self._create_template_dictionary()
    894        template = Template(template).substitute(template_dict)
    895
    896        if remove_css_variables:
    897            template = inline_css_variables_in_svg(template)
    898
    899        if minify:
    900            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
    901
    902        else:
    903            template = template.replace('"', "'")
    904
    905        return template

Render the wheel-only chart SVG as a string.

Reads the wheel-only XML template, substitutes chart data, and applies optional CSS inlining and minification.

Args: minify (bool): Remove whitespace and quotes for compactness. remove_css_variables (bool): Embed CSS variable definitions.

Returns: str: SVG markup for the chart wheel only.

def makeWheelOnlySVG(self, minify: bool = False, remove_css_variables=False): View Source

    907    def makeWheelOnlySVG(self, minify: bool = False, remove_css_variables = False):
    908        """
    909        Generate and save wheel-only chart SVG to disk.
    910
    911        Calls makeWheelOnlyTemplate and writes a file named
    912        "{subject.name} - {chart_type} Chart - Wheel Only.svg" in the output directory.
    913
    914        Args:
    915            minify (bool): Pass-through to makeWheelOnlyTemplate for compact output.
    916            remove_css_variables (bool): Pass-through to makeWheelOnlyTemplate to embed CSS variables.
    917
    918        Returns:
    919            None
    920        """
    921
    922        template = self.makeWheelOnlyTemplate(minify, remove_css_variables)
    923        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Wheel Only.svg"
    924
    925        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
    926            output_file.write(template)
    927
    928        print(f"SVG Generated Correctly in: {chartname}")

Generate and save wheel-only chart SVG to disk.

Calls makeWheelOnlyTemplate and writes a file named "{subject.name} - {chart_type} Chart - Wheel Only.svg" in the output directory.

Args: minify (bool): Pass-through to makeWheelOnlyTemplate for compact output. remove_css_variables (bool): Pass-through to makeWheelOnlyTemplate to embed CSS variables.

Returns: None

def makeAspectGridOnlyTemplate(self, minify: bool = False, remove_css_variables=False): View Source

    930    def makeAspectGridOnlyTemplate(self, minify: bool = False, remove_css_variables = False):
    931        """
    932        Render the aspect-grid-only chart SVG as a string.
    933
    934        Reads the aspect-grid XML template, generates the aspect grid based on chart type,
    935        and applies optional CSS inlining and minification.
    936
    937        Args:
    938            minify (bool): Remove whitespace and quotes for compactness.
    939            remove_css_variables (bool): Embed CSS variable definitions.
    940
    941        Returns:
    942            str: SVG markup for the aspect grid only.
    943        """
    944
    945        with open(Path(__file__).parent / "templates" / "aspect_grid_only.xml", "r", encoding="utf-8", errors="ignore") as f:
    946            template = f.read()
    947
    948        template_dict = self._create_template_dictionary()
    949
    950        if self.chart_type in ["Transit", "Synastry"]:
    951            aspects_grid = draw_transit_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list)
    952        else:
    953            aspects_grid = draw_aspect_grid(self.chart_colors_settings['paper_0'], self.available_planets_setting, self.aspects_list, x_start=50, y_start=250)
    954
    955        template = Template(template).substitute({**template_dict, "makeAspectGrid": aspects_grid})
    956
    957        if remove_css_variables:
    958            template = inline_css_variables_in_svg(template)
    959
    960        if minify:
    961            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
    962
    963        else:
    964            template = template.replace('"', "'")
    965
    966        return template

Render the aspect-grid-only chart SVG as a string.

Reads the aspect-grid XML template, generates the aspect grid based on chart type, and applies optional CSS inlining and minification.

Args: minify (bool): Remove whitespace and quotes for compactness. remove_css_variables (bool): Embed CSS variable definitions.

Returns: str: SVG markup for the aspect grid only.

def makeAspectGridOnlySVG(self, minify: bool = False, remove_css_variables=False): View Source

    968    def makeAspectGridOnlySVG(self, minify: bool = False, remove_css_variables = False):
    969        """
    970        Generate and save aspect-grid-only chart SVG to disk.
    971
    972        Calls makeAspectGridOnlyTemplate and writes a file named
    973        "{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the output directory.
    974
    975        Args:
    976            minify (bool): Pass-through to makeAspectGridOnlyTemplate for compact output.
    977            remove_css_variables (bool): Pass-through to makeAspectGridOnlyTemplate to embed CSS variables.
    978
    979        Returns:
    980            None
    981        """
    982
    983        template = self.makeAspectGridOnlyTemplate(minify, remove_css_variables)
    984        chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart - Aspect Grid Only.svg"
    985
    986        with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
    987            output_file.write(template)
    988
    989        print(f"SVG Generated Correctly in: {chartname}")

Generate and save aspect-grid-only chart SVG to disk.

Calls makeAspectGridOnlyTemplate and writes a file named "{subject.name} - {chart_type} Chart - Aspect Grid Only.svg" in the output directory.

Args: minify (bool): Pass-through to makeAspectGridOnlyTemplate for compact output. remove_css_variables (bool): Pass-through to makeAspectGridOnlyTemplate to embed CSS variables.

Returns: None