kerykeion.charts.kerykeion_chart_svg

This is part of Kerykeion (C) 2024 Giacomo Battaglia

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

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

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

KerykeionChartSVG( first_obj: kerykeion.astrological_subject.AstrologicalSubject, chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit'] = 'Natal', second_obj: Optional[kerykeion.astrological_subject.AstrologicalSubject] = None, new_output_directory: Optional[str] = None, new_settings_file: Optional[pathlib.Path] = None)
106    def __init__(
107        self,
108        first_obj: AstrologicalSubject,
109        chart_type: ChartType = "Natal",
110        second_obj: Union[AstrologicalSubject, None] = None,
111        new_output_directory: Union[str, None] = None,
112        new_settings_file: Union[Path, None] = None,
113    ):
114        # Directories:
115        DATA_DIR = Path(__file__).parent
116        self.homedir = Path.home()
117        self.new_settings_file = new_settings_file
118
119        if new_output_directory:
120            self.output_directory = Path(new_output_directory)
121        else:
122            self.output_directory = self.homedir
123
124        self.xml_svg = DATA_DIR / "templates/chart.xml"
125
126        self.parse_json_settings(new_settings_file)
127        self.chart_type = chart_type
128
129        # Kerykeion instance
130        self.user = first_obj
131
132        self.available_planets_setting = []
133        for body in self.planets_settings:
134            if body['is_active'] == False:
135                continue
136
137            self.available_planets_setting.append(body)
138
139        # House cusp points are excluded from the transit ring.
140        self.transit_ring_exclude_points_names = [
141            "First_House",
142            "Second_House",
143            "Third_House",
144            "Fourth_House",
145            "Fifth_House",
146            "Sixth_House",
147            "Seventh_House",
148            "Eighth_House",
149            "Ninth_House",
150            "Tenth_House",
151            "Eleventh_House",
152            "Twelfth_House"
153        ]
154
155        # Available bodies
156        available_celestial_points = []
157        for body in self.available_planets_setting:
158            available_celestial_points.append(body["name"].lower())
159        
160        # Make a list for the absolute degrees of the points of the graphic.
161        self.points_deg_ut = []
162        for planet in available_celestial_points:
163            self.points_deg_ut.append(self.user.get(planet).abs_pos)
164
165        # Make a list of the relative degrees of the points in the graphic.
166        self.points_deg = []
167        for planet in available_celestial_points:
168            self.points_deg.append(self.user.get(planet).position)
169
170        # Make list of the points sign
171        self.points_sign = []
172        for planet in available_celestial_points:
173            self.points_sign.append(self.user.get(planet).sign_num)
174
175        # Make a list of points if they are retrograde or not.
176        self.points_retrograde = []
177        for planet in available_celestial_points:
178            self.points_retrograde.append(self.user.get(planet).retrograde)
179
180        # Makes the sign number list.
181
182        self.houses_sign_graph = []
183        for h in self.user.houses_list:
184            self.houses_sign_graph.append(h["sign_num"])
185
186        if self.chart_type == "Natal" or self.chart_type == "ExternalNatal":
187            natal_aspects_instance = NatalAspects(self.user, new_settings_file=self.new_settings_file)
188            self.aspects_list = natal_aspects_instance.relevant_aspects
189
190        # TODO: If not second should exit
191        if self.chart_type == "Transit" or self.chart_type == "Synastry":
192            if not second_obj:
193                raise KerykeionException("Second object is required for Transit or Synastry charts.")
194
195            # Kerykeion instance
196            self.t_user = second_obj
197
198            # Make a list for the absolute degrees of the points of the graphic.
199            self.t_points_deg_ut = []
200            for planet in available_celestial_points:            
201                self.t_points_deg_ut.append(self.t_user.get(planet).abs_pos)
202
203            # Make a list of the relative degrees of the points in the graphic.
204            self.t_points_deg = []
205            for planet in available_celestial_points:
206                self.t_points_deg.append(self.t_user.get(planet).position)
207
208            # Make list of the poits sign.
209            self.t_points_sign = []
210            for planet in available_celestial_points:
211                self.t_points_sign.append(self.t_user.get(planet).sign_num)
212
213            # Make a list of poits if they are retrograde or not.
214            self.t_points_retrograde = []
215            for planet in available_celestial_points:
216                self.t_points_retrograde.append(self.t_user.get(planet).retrograde)
217
218            self.t_houses_sign_graph = []
219            for h in self.t_user.houses_list:
220                self.t_houses_sign_graph.append(h["sign_num"])
221
222        # screen size
223        self.height = self._DEFAULT_HEIGHT
224        if self.chart_type == "Synastry" or self.chart_type == "Transit":
225            self.width = self._DEFAULT_FULL_WIDTH
226        else:
227            self.width = self._DEFAULT_NATAL_WIDTH
228
229        # default location
230        self.location = self.user.city
231        self.geolat = self.user.lat
232        self.geolon =  self.user.lng
233        
234        logging.info(f"{self.user.name} birth location: {self.location}, {self.geolat}, {self.geolon}")
235
236        if self.chart_type == "Transit":
237            self.t_name = self.language_settings["transit_name"]
238
239        # configuration
240        # ZOOM 1 = 100%
241        self.zoom = 1
242
243        self.zodiac = (
244            {"name": "aries", "element": "fire"},
245            {"name": "taurus", "element": "earth"},
246            {"name": "gemini", "element": "air"},
247            {"name": "cancer", "element": "water"},
248            {"name": "leo", "element": "fire"},
249            {"name": "virgo", "element": "earth"},
250            {"name": "libra", "element": "air"},
251            {"name": "scorpio", "element": "water"},
252            {"name": "sagittarius", "element": "fire"},
253            {"name": "capricorn", "element": "earth"},
254            {"name": "aquarius", "element": "air"},
255            {"name": "pisces", "element": "water"},
256        )
257
258        self.template = None
chart_type: Literal['Natal', 'ExternalNatal', 'Synastry', 'Transit']
new_output_directory: Optional[pathlib.Path]
new_settings_file: Optional[pathlib.Path]
output_directory: pathlib.Path
fire: float
earth: float
air: float
water: float
c1: float
c2: float
c3: float
homedir: pathlib.Path
xml_svg: pathlib.Path
width: Union[float, int]
language_settings: dict
chart_colors_settings: dict
planets_settings: dict
aspects_settings: dict
planet_in_zodiac_extra_points: int
chart_settings: dict
transit_ring_exclude_points_names: List[str]
points_deg_ut: list
points_deg: list
points_sign: list
points_retrograde: list
houses_sign_graph: list
t_points_deg_ut: list
t_points_deg: list
t_points_sign: list
t_points_retrograde: list
t_houses_sign_graph: list
height: float
location: str
geolat: float
geolon: float
zoom: int
zodiac: tuple
template: str
def set_output_directory(self, dir_path: pathlib.Path) -> None:
260    def set_output_directory(self, dir_path: Path) -> None:
261        """
262        Sets the output direcotry and returns it's path.
263        """
264        self.output_directory = dir_path
265        logging.info(f"Output direcotry set to: {self.output_directory}")

Sets the output direcotry and returns it's path.

def parse_json_settings(self, settings_file):
267    def parse_json_settings(self, settings_file):
268        """
269        Parse the settings file.
270        """
271        settings = get_settings(settings_file)
272
273        language = settings["general_settings"]["language"]
274        self.language_settings = settings["language_settings"].get(language, "EN")
275        self.chart_colors_settings = settings["chart_colors"]
276        self.planets_settings = settings["celestial_points"]
277        self.aspects_settings = settings["aspects"]
278        self.planet_in_zodiac_extra_points = settings["general_settings"]["planet_in_zodiac_extra_points"]
279        self.chart_settings = settings["chart_settings"]

Parse the settings file.

def makeTemplate(self, minify: bool = False) -> str:
1391    def makeTemplate(self, minify: bool = False) -> str:
1392        """Creates the template for the SVG file"""
1393        td = self._createTemplateDictionary()
1394
1395        # read template
1396        with open(self.xml_svg, "r", encoding="utf-8", errors="ignore") as f:
1397            template = Template(f.read()).substitute(td)
1398
1399        # return filename
1400
1401        logging.debug(f"Template dictionary keys: {td.keys()}")
1402
1403        self._createTemplateDictionary()
1404
1405        if minify:
1406            template = scourString(template).replace('"', "'").replace("\n", "").replace("\t","").replace("    ", "").replace("  ", "")
1407
1408        else:
1409            template = template.replace('"', "'")
1410
1411        return template

Creates the template for the SVG file

def makeSVG(self, minify: bool = False):
1413    def makeSVG(self, minify: bool = False):
1414        """Prints out the SVG file in the specifide folder"""
1415
1416        if not (self.template):
1417            self.template = self.makeTemplate(minify)
1418
1419        self.chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg"
1420
1421        with open(self.chartname, "w", encoding="utf-8", errors="ignore") as output_file:
1422            output_file.write(self.template)
1423
1424        logging.info(f"SVG Generated Correctly in: {self.chartname}")

Prints out the SVG file in the specifide folder