diff --git a/classes/index.html b/classes/index.html index 19ea230..20fd44a 100644 --- a/classes/index.html +++ b/classes/index.html @@ -506,6 +506,15 @@ + + +
  • + + + ModalityMember + + +
  • @@ -611,15 +620,6 @@ -
  • - -
  • - - - QualityMember - - -
  • @@ -985,6 +985,15 @@ +
  • + +
  • + + + ModalityMember + + +
  • @@ -1090,15 +1099,6 @@ -
  • - -
  • - - - QualityMember - - -
  • @@ -1382,19 +1382,19 @@

    Source code in natal/const.py -
    29
    +                
    class AspectMember(Body):
    -    """
    -    Represents an aspect in raw data.
    -    (conjunction, opposition, trine, square, sextile)
    -    """
    -
    -    ...
    +34
    class AspectMember(Body):
    +    """
    +    Represents an aspect in raw data.
    +    (conjunction, opposition, trine, square, sextile)
    +    """
    +
    +    ...
     
    @@ -1503,7 +1503,8 @@

    Source code in natal/const.py -
     9
    +                
     8
    + 9
     10
     11
     12
    @@ -1511,17 +1512,16 @@ 

    14 15 16 -17 -18

    class Body(DotDict):
    -    """
    -    Represents a celestial body in raw data.
    -    Base class for all members.
    -    """
    -
    -    name: str
    -    symbol: str
    -    value: int
    -    color: str
    +17
    class Body(DotDict):
    +    """
    +    Represents a celestial body in raw data.
    +    Base class for all members.
    +    """
    +
    +    name: str
    +    symbol: str
    +    value: int
    +    color: str
     
    @@ -1630,19 +1630,19 @@

    Source code in natal/const.py -
    38
    +                
    class ElementMember(Body):
    -    """
    -    Represents an element in raw data.
    -    (fire, earth, air, water)
    -    """
    -
    -    ...
    +43
    class ElementMember(Body):
    +    """
    +    Represents an element in raw data.
    +    (fire, earth, air, water)
    +    """
    +
    +    ...
     
    @@ -1751,19 +1751,19 @@

    Source code in natal/const.py -
    73
    +                
    class ExtraMember(Body):
    -    """
    -    Represents an extra celestial body in raw data.
    -    (e.g. asteroids, nodes)
    -    """
    -
    -    ...
    +78
    class ExtraMember(Body):
    +    """
    +    Represents an extra celestial body in raw data.
    +    (e.g. asteroids, nodes)
    +    """
    +
    +    ...
     
    @@ -1881,17 +1881,79 @@

    Source code in natal/const.py -
    65
    +                
    class HouseMember(Body):
    -    """
    -    Represents a house in raw data.
    -    """
    -
    -    ...
    +69
    class HouseMember(Body):
    +    """
    +    Represents a house in raw data.
    +    """
    +
    +    ...
    +
    + + + + +
    + + + + + + + + + + + +
    + +
    + + + +
    + + + +

    + ModalityMember + + +

    + + +
    +

    + Bases: Body

    + + +

    Represents a modality in raw data. +(cardinal, fixed, mutable)

    + + + + + + +
    + Source code in natal/const.py +
    46
    +47
    +48
    +49
    +50
    +51
    +52
    class ModalityMember(Body):
    +    """
    +    Represents a modality in raw data.
    +    (cardinal, fixed, mutable)
    +    """
    +
    +    ...
     
    @@ -2129,7 +2191,7 @@

    @property def sign(self) -> SignMember: """ - Return sign name, symbol, element, quality, and polarity. + Return sign name, symbol, element, modality, and polarity. Returns: SignMember: The sign member. @@ -2367,7 +2429,7 @@

    -

    Return sign name, symbol, element, quality, and polarity.

    +

    Return sign name, symbol, element, modality, and polarity.

    Returns:

    @@ -2577,17 +2639,17 @@

    Source code in natal/const.py -
    21
    +                
    class PlanetMember(Body):
    -    """
    -    Represents a planet in raw data.
    -    """
    -
    -    ...
    +25
    class PlanetMember(Body):
    +    """
    +    Represents a planet in raw data.
    +    """
    +
    +    ...
     
    @@ -2637,81 +2699,19 @@

    Source code in natal/const.py -
    56
    +                
    class PolarityMember(Body):
    -    """
    -    Represents a polarity in raw data.
    -    (positive, negative)
    -    """
    -
    -    ...
    -
    - - - - -
    - - - - - - - - - - - -
    - -
    - - - -
    - - - -

    - QualityMember - - -

    - - -
    -

    - Bases: Body

    - - -

    Represents a quality in raw data. -(cardinal, fixed, mutable)

    - - - - - - -
    - Source code in natal/const.py -
    47
    -48
    -49
    -50
    -51
    -52
    -53
    class QualityMember(Body):
    -    """
    -    Represents a quality in raw data.
    -    (cardinal, fixed, mutable)
    -    """
    -
    -    ...
    +61
    class PolarityMember(Body):
    +    """
    +    Represents a polarity in raw data.
    +    (positive, negative)
    +    """
    +
    +    ...
     
    @@ -2819,7 +2819,8 @@

    Source code in natal/const.py -
     90
    +                
     89
    + 90
      91
      92
      93
    @@ -2831,21 +2832,20 @@ 

    99 100 101 -102 -103

    class SignMember(Body):
    -    """
    -    Represents a zodiac sign in raw data.
    -    """
    -
    -    ruler: str
    -    detriment: str
    -    exaltation: str
    -    fall: str
    -    classic_ruler: str
    -    classic_detriment: str
    -    quality: str
    -    element: str
    -    polarity: str
    +102
    class SignMember(Body):
    +    """
    +    Represents a zodiac sign in raw data.
    +    """
    +
    +    ruler: str
    +    detriment: str
    +    exaltation: str
    +    fall: str
    +    classic_ruler: str
    +    classic_detriment: str
    +    modality: str
    +    element: str
    +    polarity: str
     
    @@ -2953,17 +2953,17 @@

    Source code in natal/const.py -
    82
    +                
    class VertexMember(Body):
    -    """
    -    Represents a vertex in raw data (asc, ic, dsc, mc).
    -    """
    -
    -    ...
    +86
    class VertexMember(Body):
    +    """
    +    Represents a vertex in raw data (asc, ic, dsc, mc).
    +    """
    +
    +    ...
     
    @@ -3073,7 +3073,8 @@

    Source code in natal/const.py -
    109
    +              
    108
    +109
     110
     111
     112
    @@ -3085,21 +3086,20 @@ 

    118 119 120 -121 -122

    def get_member(raw_data: dict, name: str) -> DotDict:
    -    """
    -    Get a member from raw data by name.
    -
    -    Args:
    -        raw_data (dict): The raw data dictionary.
    -        name (str): The name of the member.
    -
    -    Returns:
    -        DotDict: The member as a DotDict.
    -    """
    -    idx = raw_data["name"].index(name)
    -    member = {key: raw_data[key][idx] for key in raw_data.keys()}
    -    return DotDict(**member)
    +121
    def get_member(raw_data: dict, name: str) -> DotDict:
    +    """
    +    Get a member from raw data by name.
    +
    +    Args:
    +        raw_data (dict): The raw data dictionary.
    +        name (str): The name of the member.
    +
    +    Returns:
    +        DotDict: The member as a DotDict.
    +    """
    +    idx = raw_data["name"].index(name)
    +    member = {key: raw_data[key][idx] for key in raw_data.keys()}
    +    return DotDict(**member)
     
    @@ -3175,7 +3175,8 @@

    Source code in natal/const.py -
    125
    +              
    124
    +125
     126
     127
     128
    @@ -3184,18 +3185,17 @@ 

    131 132 133 -134 -135

    def get_members(raw_data: dict) -> list[DotDict]:
    -    """
    -    Get all members from raw data.
    -
    -    Args:
    -        raw_data (dict): The raw data dictionary.
    -
    -    Returns:
    -        list[DotDict]: A list of members as DotDicts.
    -    """
    -    return [get_member(raw_data, name) for name in raw_data["name"]]
    +134
    def get_members(raw_data: dict) -> list[DotDict]:
    +    """
    +    Get all members from raw data.
    +
    +    Args:
    +        raw_data (dict): The raw data dictionary.
    +
    +    Returns:
    +        list[DotDict]: A list of members as DotDicts.
    +    """
    +    return [get_member(raw_data, name) for name in raw_data["name"]]
     
    diff --git a/const/index.html b/const/index.html index 486767e..cd521ac 100644 --- a/const/index.html +++ b/const/index.html @@ -506,27 +506,27 @@
  • - + - PlanetMember + ModalityMember
  • - + - PolarityMember + PlanetMember
  • - + - QualityMember + PolarityMember @@ -793,27 +793,27 @@
  • - + - PlanetMember + ModalityMember
  • - + - PolarityMember + PlanetMember
  • - + - QualityMember + PolarityMember @@ -936,19 +936,19 @@

    Source code in natal/const.py -
    29
    +                
    class AspectMember(Body):
    -    """
    -    Represents an aspect in raw data.
    -    (conjunction, opposition, trine, square, sextile)
    -    """
    -
    -    ...
    +34
    class AspectMember(Body):
    +    """
    +    Represents an aspect in raw data.
    +    (conjunction, opposition, trine, square, sextile)
    +    """
    +
    +    ...
     
    @@ -998,7 +998,8 @@

    Source code in natal/const.py -
     9
    +                
     8
    + 9
     10
     11
     12
    @@ -1006,17 +1007,16 @@ 

    14 15 16 -17 -18

    class Body(DotDict):
    -    """
    -    Represents a celestial body in raw data.
    -    Base class for all members.
    -    """
    -
    -    name: str
    -    symbol: str
    -    value: int
    -    color: str
    +17
    class Body(DotDict):
    +    """
    +    Represents a celestial body in raw data.
    +    Base class for all members.
    +    """
    +
    +    name: str
    +    symbol: str
    +    value: int
    +    color: str
     
    @@ -1066,19 +1066,19 @@

    Source code in natal/const.py -
    38
    +                
    class ElementMember(Body):
    -    """
    -    Represents an element in raw data.
    -    (fire, earth, air, water)
    -    """
    -
    -    ...
    +43
    class ElementMember(Body):
    +    """
    +    Represents an element in raw data.
    +    (fire, earth, air, water)
    +    """
    +
    +    ...
     
    @@ -1128,19 +1128,19 @@

    Source code in natal/const.py -
    73
    +                
    class ExtraMember(Body):
    -    """
    -    Represents an extra celestial body in raw data.
    -    (e.g. asteroids, nodes)
    -    """
    -
    -    ...
    +78
    class ExtraMember(Body):
    +    """
    +    Represents an extra celestial body in raw data.
    +    (e.g. asteroids, nodes)
    +    """
    +
    +    ...
     
    @@ -1189,17 +1189,17 @@

    Source code in natal/const.py -
    65
    +                
    class HouseMember(Body):
    -    """
    -    Represents a house in raw data.
    -    """
    -
    -    ...
    +69
    class HouseMember(Body):
    +    """
    +    Represents a house in raw data.
    +    """
    +
    +    ...
     
    @@ -1227,8 +1227,8 @@

    -

    - PlanetMember +

    + ModalityMember

    @@ -1239,7 +1239,8 @@

    Bases: Body

    -

    Represents a planet in raw data.

    +

    Represents a modality in raw data. +(cardinal, fixed, mutable)

    @@ -1248,17 +1249,19 @@

    Source code in natal/const.py -
    21
    -22
    -23
    -24
    -25
    -26
    class PlanetMember(Body):
    -    """
    -    Represents a planet in raw data.
    -    """
    -
    -    ...
    +                
    46
    +47
    +48
    +49
    +50
    +51
    +52
    class ModalityMember(Body):
    +    """
    +    Represents a modality in raw data.
    +    (cardinal, fixed, mutable)
    +    """
    +
    +    ...
     
    @@ -1286,8 +1289,8 @@

    -

    - PolarityMember +

    + PlanetMember

    @@ -1298,8 +1301,7 @@

    Bases: Body

    -

    Represents a polarity in raw data. -(positive, negative)

    +

    Represents a planet in raw data.

    @@ -1308,19 +1310,17 @@

    Source code in natal/const.py -
    56
    -57
    -58
    -59
    -60
    -61
    -62
    class PolarityMember(Body):
    -    """
    -    Represents a polarity in raw data.
    -    (positive, negative)
    -    """
    -
    -    ...
    +                
    20
    +21
    +22
    +23
    +24
    +25
    class PlanetMember(Body):
    +    """
    +    Represents a planet in raw data.
    +    """
    +
    +    ...
     
    @@ -1348,8 +1348,8 @@

    -

    - QualityMember +

    + PolarityMember

    @@ -1360,8 +1360,8 @@

    Bases: Body

    -

    Represents a quality in raw data. -(cardinal, fixed, mutable)

    +

    Represents a polarity in raw data. +(positive, negative)

    @@ -1370,19 +1370,19 @@

    Source code in natal/const.py -
    47
    -48
    -49
    -50
    -51
    -52
    -53
    class QualityMember(Body):
    -    """
    -    Represents a quality in raw data.
    -    (cardinal, fixed, mutable)
    -    """
    -
    -    ...
    +                
    55
    +56
    +57
    +58
    +59
    +60
    +61
    class PolarityMember(Body):
    +    """
    +    Represents a polarity in raw data.
    +    (positive, negative)
    +    """
    +
    +    ...
     
    @@ -1431,7 +1431,8 @@

    Source code in natal/const.py -
     90
    +                
     89
    + 90
      91
      92
      93
    @@ -1443,21 +1444,20 @@ 

    99 100 101 -102 -103

    class SignMember(Body):
    -    """
    -    Represents a zodiac sign in raw data.
    -    """
    -
    -    ruler: str
    -    detriment: str
    -    exaltation: str
    -    fall: str
    -    classic_ruler: str
    -    classic_detriment: str
    -    quality: str
    -    element: str
    -    polarity: str
    +102
    class SignMember(Body):
    +    """
    +    Represents a zodiac sign in raw data.
    +    """
    +
    +    ruler: str
    +    detriment: str
    +    exaltation: str
    +    fall: str
    +    classic_ruler: str
    +    classic_detriment: str
    +    modality: str
    +    element: str
    +    polarity: str
     
    @@ -1506,17 +1506,17 @@

    Source code in natal/const.py -
    82
    +                
    class VertexMember(Body):
    -    """
    -    Represents a vertex in raw data (asc, ic, dsc, mc).
    -    """
    -
    -    ...
    +86
    class VertexMember(Body):
    +    """
    +    Represents a vertex in raw data (asc, ic, dsc, mc).
    +    """
    +
    +    ...
     
    @@ -1626,7 +1626,8 @@

    Source code in natal/const.py -
    109
    +              
    108
    +109
     110
     111
     112
    @@ -1638,21 +1639,20 @@ 

    118 119 120 -121 -122

    def get_member(raw_data: dict, name: str) -> DotDict:
    -    """
    -    Get a member from raw data by name.
    -
    -    Args:
    -        raw_data (dict): The raw data dictionary.
    -        name (str): The name of the member.
    -
    -    Returns:
    -        DotDict: The member as a DotDict.
    -    """
    -    idx = raw_data["name"].index(name)
    -    member = {key: raw_data[key][idx] for key in raw_data.keys()}
    -    return DotDict(**member)
    +121
    def get_member(raw_data: dict, name: str) -> DotDict:
    +    """
    +    Get a member from raw data by name.
    +
    +    Args:
    +        raw_data (dict): The raw data dictionary.
    +        name (str): The name of the member.
    +
    +    Returns:
    +        DotDict: The member as a DotDict.
    +    """
    +    idx = raw_data["name"].index(name)
    +    member = {key: raw_data[key][idx] for key in raw_data.keys()}
    +    return DotDict(**member)
     
    @@ -1728,7 +1728,8 @@

    Source code in natal/const.py -
    125
    +              
    124
    +125
     126
     127
     128
    @@ -1737,18 +1738,17 @@ 

    131 132 133 -134 -135

    def get_members(raw_data: dict) -> list[DotDict]:
    -    """
    -    Get all members from raw data.
    -
    -    Args:
    -        raw_data (dict): The raw data dictionary.
    -
    -    Returns:
    -        list[DotDict]: A list of members as DotDicts.
    -    """
    -    return [get_member(raw_data, name) for name in raw_data["name"]]
    +134
    def get_members(raw_data: dict) -> list[DotDict]:
    +    """
    +    Get all members from raw data.
    +
    +    Args:
    +        raw_data (dict): The raw data dictionary.
    +
    +    Returns:
    +        list[DotDict]: A list of members as DotDicts.
    +    """
    +    return [get_member(raw_data, name) for name in raw_data["name"]]
     
    diff --git a/data/index.html b/data/index.html index a6ef1d2..8e11c79 100644 --- a/data/index.html +++ b/data/index.html @@ -695,27 +695,27 @@
  • - + - PlanetMember + ModalityMember
  • - + - PolarityMember + PlanetMember
  • - + - QualityMember + PolarityMember @@ -1129,27 +1129,27 @@
  • - + - PlanetMember + ModalityMember
  • - + - PolarityMember + PlanetMember
  • - + - QualityMember + PolarityMember @@ -1270,19 +1270,19 @@

    Source code in natal/const.py -
    29
    +                
    class AspectMember(Body):
    -    """
    -    Represents an aspect in raw data.
    -    (conjunction, opposition, trine, square, sextile)
    -    """
    -
    -    ...
    +34
    class AspectMember(Body):
    +    """
    +    Represents an aspect in raw data.
    +    (conjunction, opposition, trine, square, sextile)
    +    """
    +
    +    ...
     
    @@ -1792,7 +1792,7 @@

    op += f"{e.name}: {e.signed_dms}\n" op += "Signs:\n" for e in self.signs: - op += f"{e.name}: degree={e.degree:.2f}, ruler={e.ruler}, color={e.color}, quality={e.quality}, element={e.element}, polarity={e.polarity}\n" + op += f"{e.name}: degree={e.degree:.2f}, ruler={e.ruler}, color={e.color}, modality={e.modality}, element={e.element}, polarity={e.polarity}\n" op += "Aspects:\n" for e in self.aspects: op += f"{e.body1.name} {e.aspect_member.symbol} {e.body2.name}: {e.aspect_member.color}\n" @@ -2228,7 +2228,7 @@

    op += f"{e.name}: {e.signed_dms}\n" op += "Signs:\n" for e in self.signs: - op += f"{e.name}: degree={e.degree:.2f}, ruler={e.ruler}, color={e.color}, quality={e.quality}, element={e.element}, polarity={e.polarity}\n" + op += f"{e.name}: degree={e.degree:.2f}, ruler={e.ruler}, color={e.color}, modality={e.modality}, element={e.element}, polarity={e.polarity}\n" op += "Aspects:\n" for e in self.aspects: op += f"{e.body1.name} {e.aspect_member.symbol} {e.body2.name}: {e.aspect_member.color}\n" @@ -3226,19 +3226,19 @@

    Source code in natal/const.py -
    38
    +                
    class ElementMember(Body):
    -    """
    -    Represents an element in raw data.
    -    (fire, earth, air, water)
    -    """
    -
    -    ...
    +43
    class ElementMember(Body):
    +    """
    +    Represents an element in raw data.
    +    (fire, earth, air, water)
    +    """
    +
    +    ...
     
    @@ -3288,19 +3288,19 @@

    Source code in natal/const.py -
    73
    +                
    class ExtraMember(Body):
    -    """
    -    Represents an extra celestial body in raw data.
    -    (e.g. asteroids, nodes)
    -    """
    -
    -    ...
    +78
    class ExtraMember(Body):
    +    """
    +    Represents an extra celestial body in raw data.
    +    (e.g. asteroids, nodes)
    +    """
    +
    +    ...
     
    @@ -3349,17 +3349,17 @@

    Source code in natal/const.py -
    65
    +                
    class HouseMember(Body):
    -    """
    -    Represents a house in raw data.
    -    """
    -
    -    ...
    +69
    class HouseMember(Body):
    +    """
    +    Represents a house in raw data.
    +    """
    +
    +    ...
     
    @@ -3387,8 +3387,8 @@

    -

    - PlanetMember +

    + ModalityMember

    @@ -3399,7 +3399,8 @@

    Bases: Body

    -

    Represents a planet in raw data.

    +

    Represents a modality in raw data. +(cardinal, fixed, mutable)

    @@ -3408,17 +3409,19 @@

    Source code in natal/const.py -
    21
    -22
    -23
    -24
    -25
    -26
    class PlanetMember(Body):
    -    """
    -    Represents a planet in raw data.
    -    """
    -
    -    ...
    +                
    46
    +47
    +48
    +49
    +50
    +51
    +52
    class ModalityMember(Body):
    +    """
    +    Represents a modality in raw data.
    +    (cardinal, fixed, mutable)
    +    """
    +
    +    ...
     
    @@ -3446,8 +3449,8 @@

    -

    - PolarityMember +

    + PlanetMember

    @@ -3458,8 +3461,7 @@

    Bases: Body

    -

    Represents a polarity in raw data. -(positive, negative)

    +

    Represents a planet in raw data.

    @@ -3468,19 +3470,17 @@

    Source code in natal/const.py -
    56
    -57
    -58
    -59
    -60
    -61
    -62
    class PolarityMember(Body):
    -    """
    -    Represents a polarity in raw data.
    -    (positive, negative)
    -    """
    -
    -    ...
    +                
    20
    +21
    +22
    +23
    +24
    +25
    class PlanetMember(Body):
    +    """
    +    Represents a planet in raw data.
    +    """
    +
    +    ...
     
    @@ -3508,8 +3508,8 @@

    -

    - QualityMember +

    + PolarityMember

    @@ -3520,8 +3520,8 @@

    Bases: Body

    -

    Represents a quality in raw data. -(cardinal, fixed, mutable)

    +

    Represents a polarity in raw data. +(positive, negative)

    @@ -3530,19 +3530,19 @@

    Source code in natal/const.py -
    47
    -48
    -49
    -50
    -51
    -52
    -53
    class QualityMember(Body):
    -    """
    -    Represents a quality in raw data.
    -    (cardinal, fixed, mutable)
    -    """
    -
    -    ...
    +                
    55
    +56
    +57
    +58
    +59
    +60
    +61
    class PolarityMember(Body):
    +    """
    +    Represents a polarity in raw data.
    +    (positive, negative)
    +    """
    +
    +    ...
     
    @@ -3591,7 +3591,8 @@

    Source code in natal/const.py -
     90
    +                
     89
    + 90
      91
      92
      93
    @@ -3603,21 +3604,20 @@ 

    99 100 101 -102 -103

    class SignMember(Body):
    -    """
    -    Represents a zodiac sign in raw data.
    -    """
    -
    -    ruler: str
    -    detriment: str
    -    exaltation: str
    -    fall: str
    -    classic_ruler: str
    -    classic_detriment: str
    -    quality: str
    -    element: str
    -    polarity: str
    +102
    class SignMember(Body):
    +    """
    +    Represents a zodiac sign in raw data.
    +    """
    +
    +    ruler: str
    +    detriment: str
    +    exaltation: str
    +    fall: str
    +    classic_ruler: str
    +    classic_detriment: str
    +    modality: str
    +    element: str
    +    polarity: str
     
    @@ -3666,17 +3666,17 @@

    Source code in natal/const.py -
    82
    +                
    class VertexMember(Body):
    -    """
    -    Represents a vertex in raw data (asc, ic, dsc, mc).
    -    """
    -
    -    ...
    +86
    class VertexMember(Body):
    +    """
    +    Represents a vertex in raw data (asc, ic, dsc, mc).
    +    """
    +
    +    ...
     
    @@ -3786,7 +3786,8 @@

    Source code in natal/const.py -
    109
    +              
    108
    +109
     110
     111
     112
    @@ -3798,21 +3799,20 @@ 

    118 119 120 -121 -122

    def get_member(raw_data: dict, name: str) -> DotDict:
    -    """
    -    Get a member from raw data by name.
    -
    -    Args:
    -        raw_data (dict): The raw data dictionary.
    -        name (str): The name of the member.
    -
    -    Returns:
    -        DotDict: The member as a DotDict.
    -    """
    -    idx = raw_data["name"].index(name)
    -    member = {key: raw_data[key][idx] for key in raw_data.keys()}
    -    return DotDict(**member)
    +121
    def get_member(raw_data: dict, name: str) -> DotDict:
    +    """
    +    Get a member from raw data by name.
    +
    +    Args:
    +        raw_data (dict): The raw data dictionary.
    +        name (str): The name of the member.
    +
    +    Returns:
    +        DotDict: The member as a DotDict.
    +    """
    +    idx = raw_data["name"].index(name)
    +    member = {key: raw_data[key][idx] for key in raw_data.keys()}
    +    return DotDict(**member)
     
    @@ -3888,7 +3888,8 @@

    Source code in natal/const.py -
    diff --git a/search/search_index.json b/search/search_index.json index 9bd658c..3ef6e43 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Natal","text":"

    create Natal Chart with ease

    "},{"location":"#features","title":"Features","text":"
    • SVG natal chart generation in pure python
    • composite chart (transit/synastry/sun return ... etc) generation
    • highly configurable
      • all planets, asteroids, vertices can be enabled / disabled
      • orbs for each aspect
      • light, dark, or mono theme
      • light / dark theme color definitions
      • chart stroke, opacity, font, spacing between planets ...etc
    • high precision astrological data with Swiss Ephemeris
    • timezone, latitude and longitude database from GeoNames
      • auto aware of daylight saving for a given time and location
    • natal chart data statistics
      • element, quality, and polarity counts
      • planets in each houses
      • quadrant and hemisphere distribution
      • aspect pair counts
      • composite chart aspects
      • aspects cross reference table
      • generate report in markdown or html
    • thoroughly tested with pytest
    "},{"location":"#sample-charts","title":"Sample Charts","text":"
    • default dark theme
    • default light theme
    • mono theme
    "},{"location":"#quick-start","title":"Quick Start","text":"
    • installation

    pip install natal

    • generate a chart
    from natal import Data, Chart\n\n# create chart data object\nmimi = Data(\n  name = \"MiMi\",\n  city = \"Taipei\",\n  dt = \"1980-04-20 14:30\"\n)\n\n# return natal chart in SVG string\nChart(mimi, width=600).svg\n\n# create transit data object\ncurrent = Data(\n  name = \"Current\",\n  city = \"Taipei\",\n  dt = datetime.now()\n)\n\n# create a transit chart\ntransit_chart = Chart(\n    data1=mimi, \n    data2=current, \n    width=600\n)\n\n# view the composite chart in jupyter notebook\nfrom IPython.display import HTML\n\nHTML(transit_chart.svg)\n

    following SVG chart will be produced:

    "},{"location":"#data-object","title":"Data Object","text":"
    ## -- retrieve natal chart properties -- ##\n\nmimi.planets     # list[Planet]\nmimi.houses      # list[House]\nmimi.extras      # list[Extra]\nmimi.vertices    # list[Vertex]\nmimi.signs       # list[Sign]\nmimi.aspects     # list[Aspect]\nmimi.quadrants   # list[list[Aspectable]]\n\n# Planet object \nsun = mimi.planets[0]\n\nsun.degree # 30.33039116987769\nsun.normalized_degree # 230.62043431588035 # degree relative to Asc\nsun.color # fire\nsun.speed # 0.9761994105153413\nsun.retro # False\nsun.dms # 00\u00b019'\nsun.signed_dms # 00\u00b0\u264919'\nsun.signed_deg # 0\nsun.sign.name # taurus\nsun.sign.symbol # \u2649\nsun.sign.value # 2\nsun.sign.color # earth\nsun.sign.ruler # venus\nsun.sign.classic_ruler # venus\nsun.sign.element # earth\nsun.sign.quality # fixed\nsun.sign.polarity # negative\n\n# Aspect object\naspect = mimi.aspects[0]\n\naspect.body1 # sun Planet obj \naspect.body2 # mars Planet obj\naspect.aspect_member # AspectMember(name='trine', symbol='\u25b3', value=120, color='air')\naspect.applying # False\naspect.orb # 3.3424\n
    "},{"location":"#stats","title":"Stats","text":"
    • statistics of Data object in tabular form
    from natal import Data, Stats\n\nmimi = Data(\n    name = \"MiMi\",\n    city = \"Taipei\",\n    dt = \"1980-04-20 14:30\"\n)\n\ntransit = Data(\n    name = \"Transit\",\n    city = \"Taipei\",\n    dt = \"2024-10-10 12:00\"\n)\n\nstats = Stats(data1=mimi, data2=transit)\n\nprint(stats.full_report(kind=\"markdown\"))\n
    • following markdown report will be produced:
    # Element Distribution (MiMi)\n\n| element   |  count  | bodies                                       |\n|-----------|---------|----------------------------------------------|\n| earth     |    4    | sun \u2649, jupiter \u264d, saturn \u264d, asc \u264d        |\n| water     |    2    | moon \u264b, uranus \u264f                           |\n| fire      |    4    | mercury \u2648, mars \u264c, neptune \u2650, asc_node \u264c |\n| air       |    3    | venus \u264a, pluto \u264e, mc \u264a                    |\n\n\n# Quality Distribution (MiMi)\n\n| quality   |  count  | bodies                                                     |\n|-----------|---------|------------------------------------------------------------|\n| fixed     |    4    | sun \u2649, mars \u264c, uranus \u264f, asc_node \u264c                    |\n| cardinal  |    3    | moon \u264b, mercury \u2648, pluto \u264e                              |\n| mutable   |    6    | venus \u264a, jupiter \u264d, saturn \u264d, neptune \u2650, asc \u264d, mc \u264a |\n\n\n# Polarity Distribution (MiMi)\n\n| polarity   |  count  | bodies                                                                  |\n|------------|---------|-------------------------------------------------------------------------|\n| negative   |    6    | sun \u2649, moon \u264b, jupiter \u264d, saturn \u264d, uranus \u264f, asc \u264d               |\n| positive   |    7    | mercury \u2648, venus \u264a, mars \u264c, neptune \u2650, pluto \u264e, asc_node \u264c, mc \u264a |\n\n\n# Celestial Bodies (MiMi)\n\n| body     | sign      |  house  |\n|----------|-----------|---------|\n| sun      | 00\u00b0\u264919'  |    8    |\n| moon     | 08\u00b0\u264b29'  |   10    |\n| mercury  | 08\u00b0\u264828'  |    8    |\n| venus    | 15\u00b0\u264a12'  |   10    |\n| mars     | 26\u00b0\u264c59'  |   12    |\n| jupiter  | 00\u00b0\u264d17'\u211e |   12    |\n| saturn   | 21\u00b0\u264d03'\u211e |    1    |\n| uranus   | 24\u00b0\u264f31'\u211e |    3    |\n| neptune  | 22\u00b0\u265029'\u211e |    4    |\n| pluto    | 20\u00b0\u264e06'\u211e |    2    |\n| asc_node | 26\u00b0\u264c03'\u211e |   12    |\n| asc      | 09\u00b0\u264d42'  |    1    |\n| mc       | 09\u00b0\u264a13'  |   10    |\n\n\n# Houses (MiMi)\n\n|  house  | sign     | ruler   | ruler sign   |  ruler house  |\n|---------|----------|---------|--------------|---------------|\n|    1    | 09\u00b0\u264d41' | mercury | \u2648           |       8       |\n|    2    | 07\u00b0\u264e13' | venus   | \u264a           |      10       |\n|    3    | 07\u00b0\u264f38' | pluto   | \u264e           |       2       |\n|    4    | 09\u00b0\u265013' | jupiter | \u264d           |      12       |\n|    5    | 10\u00b0\u265125' | saturn  | \u264d           |       1       |\n|    6    | 10\u00b0\u265244' | uranus  | \u264f           |       3       |\n|    7    | 09\u00b0\u265341' | neptune | \u2650           |       4       |\n|    8    | 07\u00b0\u264813' | mars    | \u264c           |      12       |\n|    9    | 07\u00b0\u264938' | venus   | \u264a           |      10       |\n|   10    | 09\u00b0\u264a13' | mercury | \u2648           |       8       |\n|   11    | 10\u00b0\u264b25' | moon    | \u264b           |      10       |\n|   12    | 10\u00b0\u264c44' | sun     | \u2649           |       8       |\n\n\n# Quadrants (MiMi)\n\n| quadrant   |  count  | bodies                               |\n|------------|---------|--------------------------------------|\n| 1st \u25f5      |    3    | saturn, uranus, pluto                |\n| 2nd \u25f6      |    1    | neptune                              |\n| 3rd \u25f7      |    2    | sun, mercury                         |\n| 4th \u25f4      |    5    | moon, venus, mars, jupiter, asc_node |\n\n\n# Hemispheres (MiMi)\n\n| hemisphere   |  count  | bodies                                                      |\n|--------------|---------|-------------------------------------------------------------|\n| \u2190            |    8    | saturn, uranus, pluto, moon, venus, mars, jupiter, asc_node |\n| \u2192            |    3    | neptune, sun, mercury                                       |\n| \u2191            |    7    | sun, mercury, moon, venus, mars, jupiter, asc_node          |\n| \u2193            |    4    | saturn, uranus, pluto, neptune                              |\n\n\n# Celestial Bodies of Transit in MiMi's chart\n\n| Transit   | sign      |  house  |\n|-----------|-----------|---------|\n| sun       | 17\u00b0\u264e20'  |    2    |\n| moon      | 09\u00b0\u265149'  |    4    |\n| mercury   | 24\u00b0\u264e04'  |    2    |\n| venus     | 20\u00b0\u264f44'  |    3    |\n| mars      | 19\u00b0\u264b29'  |   11    |\n| jupiter   | 21\u00b0\u264a20'\u211e |   10    |\n| saturn    | 13\u00b0\u265347'\u211e |    7    |\n| uranus    | 26\u00b0\u264939'\u211e |    9    |\n| neptune   | 27\u00b0\u265359'\u211e |    7    |\n| pluto     | 29\u00b0\u265138'\u211e |    5    |\n| asc_node  | 05\u00b0\u264852'\u211e |    7    |\n| asc       | 08\u00b0\u265129'  |    4    |\n| mc        | 22\u00b0\u264e28'  |    2    |\n\n\n# Aspects of Transit vs MiMi\n\n| Transit   |  aspect  | MiMi     |  phase  | orb    |\n|-----------|----------|----------|---------|--------|\n| sun       |    \u25b3     | venus    |   \u2190 \u2192   | 2\u00b0 08' |\n| sun       |    \u260c     | pluto    |   \u2192 \u2190   | 2\u00b0 46' |\n| moon      |    \u260d     | moon     |   \u2192 \u2190   | 1\u00b0 20' |\n| moon      |    \u25a1     | mercury  |   \u2190 \u2192   | 1\u00b0 21' |\n| moon      |    \u25b3     | asc      |   \u2190 \u2192   | 0\u00b0 07' |\n| mercury   |    \u26b9     | mars     |   \u2192 \u2190   | 2\u00b0 55' |\n| mercury   |    \u26b9     | neptune  |   \u2190 \u2192   | 1\u00b0 35' |\n| mercury   |    \u260c     | pluto    |   \u2190 \u2192   | 3\u00b0 58' |\n| mercury   |    \u26b9     | asc_node |   \u2192 \u2190   | 1\u00b0 59' |\n| venus     |    \u26b9     | saturn   |   \u2192 \u2190   | 0\u00b0 19' |\n| venus     |    \u260c     | uranus   |   \u2192 \u2190   | 3\u00b0 47' |\n| venus     |    \u25a1     | asc_node |   \u2192 \u2190   | 5\u00b0 19' |\n| mars      |    \u26b9     | saturn   |   \u2192 \u2190   | 1\u00b0 34' |\n| mars      |    \u25b3     | uranus   |   \u2192 \u2190   | 5\u00b0 02' |\n| mars      |    \u25a1     | pluto    |   \u2192 \u2190   | 0\u00b0 38' |\n| jupiter   |    \u260c     | venus    |   \u2192 \u2190   | 6\u00b0 08' |\n| jupiter   |    \u25a1     | saturn   |   \u2190 \u2192   | 0\u00b0 17' |\n| jupiter   |    \u260d     | neptune  |   \u2192 \u2190   | 1\u00b0 09' |\n| jupiter   |    \u25b3     | pluto    |   \u2190 \u2192   | 1\u00b0 13' |\n| jupiter   |    \u26b9     | asc_node |   \u2192 \u2190   | 4\u00b0 43' |\n| saturn    |    \u25b3     | moon     |   \u2192 \u2190   | 5\u00b0 18' |\n| saturn    |    \u25a1     | venus    |   \u2190 \u2192   | 1\u00b0 25' |\n| saturn    |    \u260d     | asc      |   \u2192 \u2190   | 4\u00b0 05' |\n| saturn    |    \u25a1     | mc       |   \u2192 \u2190   | 4\u00b0 34' |\n| uranus    |    \u25a1     | mars     |   \u2190 \u2192   | 0\u00b0 20' |\n| uranus    |    \u25a1     | jupiter  |   \u2190 \u2192   | 3\u00b0 38' |\n| uranus    |    \u25b3     | saturn   |   \u2190 \u2192   | 5\u00b0 36' |\n| uranus    |    \u260d     | uranus   |   \u2190 \u2192   | 2\u00b0 08' |\n| uranus    |    \u25a1     | asc_node |   \u2190 \u2192   | 0\u00b0 36' |\n| neptune   |    \u25b3     | uranus   |   \u2190 \u2192   | 3\u00b0 28' |\n| neptune   |    \u25a1     | neptune  |   \u2192 \u2190   | 5\u00b0 31' |\n| pluto     |    \u25a1     | sun      |   \u2190 \u2192   | 0\u00b0 41' |\n| asc_node  |    \u25a1     | moon     |   \u2190 \u2192   | 2\u00b0 36' |\n| asc_node  |    \u260c     | mercury  |   \u2190 \u2192   | 2\u00b0 35' |\n| asc_node  |    \u26b9     | mc       |   \u2190 \u2192   | 3\u00b0 20' |\n| asc       |    \u260d     | moon     |   \u2192 \u2190   | 0\u00b0 01' |\n| asc       |    \u25a1     | mercury  |   \u2192 \u2190   | 0\u00b0 02' |\n| asc       |    \u25b3     | asc      |   \u2192 \u2190   | 1\u00b0 13' |\n| mc        |    \u26b9     | mars     |   \u2190 \u2192   | 4\u00b0 31' |\n| mc        |    \u26b9     | neptune  |   \u2192 \u2190   | 0\u00b0 01' |\n| mc        |    \u260c     | pluto    |   \u2190 \u2192   | 2\u00b0 22' |\n| mc        |    \u26b9     | asc_node |   \u2192 \u2190   | 3\u00b0 35' |\n\n\n# Aspect Cross Reference of Transit(cols) vs MiMi(rows)\n\n|     |  \u2609  |  \u263d  |  \u263f  |  \u2640  |  \u2642  |  \u2643  |  \u2644  |  \u2645  |  \u2646  |  \u2647  |  \u260a  |  Asc  |  MC  |  Total  |\n|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-------|------|---------|\n|  \u2609  |     |     |     |     |     |     |     |     |     |  \u25a1  |     |       |      |    1    |\n|  \u263d  |     |  \u260d  |     |     |     |     |  \u25b3  |     |     |     |  \u25a1  |   \u260d   |      |    4    |\n|  \u263f  |     |  \u25a1  |     |     |     |     |     |     |     |     |  \u260c  |   \u25a1   |      |    3    |\n|  \u2640  |  \u25b3  |     |     |     |     |  \u260c  |  \u25a1  |     |     |     |     |       |      |    3    |\n|  \u2642  |     |     |  \u26b9  |     |     |     |     |  \u25a1  |     |     |     |       |  \u26b9   |    3    |\n|  \u2643  |     |     |     |     |     |     |     |  \u25a1  |     |     |     |       |      |    1    |\n|  \u2644  |     |     |     |  \u26b9  |  \u26b9  |  \u25a1  |     |  \u25b3  |     |     |     |       |      |    4    |\n|  \u2645  |     |     |     |  \u260c  |  \u25b3  |     |     |  \u260d  |  \u25b3  |     |     |       |      |    4    |\n|  \u2646  |     |     |  \u26b9  |     |     |  \u260d  |     |     |  \u25a1  |     |     |       |  \u26b9   |    4    |\n|  \u2647  |  \u260c  |     |  \u260c  |     |  \u25a1  |  \u25b3  |     |     |     |     |     |       |  \u260c   |    5    |\n|  \u260a  |     |     |  \u26b9  |  \u25a1  |     |  \u26b9  |     |  \u25a1  |     |     |     |       |  \u26b9   |    5    |\n| Asc |     |  \u25b3  |     |     |     |     |  \u260d  |     |     |     |     |   \u25b3   |      |    3    |\n| MC  |     |     |     |     |     |     |  \u25a1  |     |     |     |  \u26b9  |       |      |    2    |\n
    • see demo.ipynb for the HTML output
    "},{"location":"#configuration","title":"Configuration","text":"
    • create a Config object and assign it to Data object
    • it will override the default settings in config.py
    • a sample config as follow:
    from natal.config import Display, Config, Orb\n\n# adjust which celestial bodies to display\ndisplay = Display(\n    mc = False,\n    asc_node = False,\n    chiron = True\n)\n\n# adjust orbs for each aspect\norb = Orb(\n    conjunction = 8,\n    opposition = 8,\n    trine = 6,\n    square = 6,\n    sextile = 6\n)\n\n# the complete config object\nconfig = Config(\n    theme_type = \"light\", # or \"dark\", \"mono\"\n    display = display,\n    orb = orb\n)\n\n# create data object with the config\ndata = Data(\n    name = \"MiMi\",\n    city = \"Taipei\",\n    dt = \"1980-04-20 14:30\",\n    config = config,\n)\n

    read the docs for complete references

    "},{"location":"#tech-stack","title":"Tech Stack","text":"
    • tagit for creating and manipulating SVG
    • pyswisseph python extension to the Swiss Ephemeris
    • mkdocs-material for docs site generation
    "},{"location":"chart/","title":"Chart","text":""},{"location":"chart/#natal.chart","title":"natal.chart","text":"

    This module provides the Chart class for generating SVG representations of natal charts. It includes functionality for creating sign wheels, house wheels, body placements, and aspect lines for both single and composite charts.

    "},{"location":"chart/#natal.chart.Chart","title":"Chart","text":"

    Bases: DotDict

    SVG representation of a natal chart.

    This class generates the visual components of an astrological chart, including sign wheels, house wheels, planet placements, and aspect lines. It supports both single and composite charts.

    Source code in natal/chart.py
    class Chart(DotDict):\n    \"\"\"SVG representation of a natal chart.\n\n    This class generates the visual components of an astrological chart,\n    including sign wheels, house wheels, planet placements, and aspect lines.\n    It supports both single and composite charts.\n    \"\"\"\n\n    def __init__(\n        self,\n        data1: Data,\n        width: int,\n        height: int | None = None,\n        data2: Data | None = None,\n    ) -> None:\n        \"\"\"Initialize a Chart object.\n\n        Args:\n            data1: Primary chart data\n            width: Width of the SVG\n            height: Height of the SVG. If None, set to width\n            data2: Secondary chart data for composite charts\n\n        Returns:\n            None\n        \"\"\"\n        self.data1 = data1\n        self.data2 = data2\n        self.width = width\n        self.height = height\n        if self.height is None:\n            self.height = self.width\n        self.cx = self.width / 2\n        self.cy = self.height / 2\n\n        self.config = self.data1.config\n        margin = min(self.width, self.height) * self.config.chart.margin_factor\n        self.max_radius = min(self.width - margin, self.height - margin) // 2\n        self.margin = margin\n        self.ring_thickness = (\n            self.max_radius * self.config.chart.ring_thickness_fraction\n        )\n        self.font_size = self.ring_thickness * self.config.chart.font_size_fraction\n        self.scale_adjustment = self.width / self.config.chart.scale_adj_factor\n        self.pos_adjustment = self.font_size / self.config.chart.pos_adj_factor\n\n    def svg_root(self, content: str | list[str]) -> str:\n        \"\"\"Generate an SVG root element with sensible defaults.\n\n        Args:\n            content: The content to be included in the SVG root\n\n        Returns:\n            An SVG root element as a string\n        \"\"\"\n        return svg(\n            content,\n            height=self.height,\n            width=self.width,\n            font_family=self.config.chart.font,\n            version=\"1.1\",\n            xmlns=\"http://www.w3.org/2000/svg\",\n        )\n\n    def sector(\n        self,\n        radius: int,\n        start_deg: float,\n        end_deg: float,\n        fill: str = \"white\",\n        stroke_color: str = \"black\",\n        stroke_width: float = 1,\n        stroke_opacity: float = 1,\n    ) -> str:\n        \"\"\"Create a sector shape in SVG format.\n\n        Args:\n            radius: Radius of the sector\n            start_deg: Starting angle in degrees\n            end_deg: Ending angle in degrees\n            fill: Fill color of the sector\n            stroke_color: Stroke color of the sector\n            stroke_width: Width of the stroke\n            stroke_opacity: Opacity of the stroke\n\n        Returns:\n            An SVG path element representing the sector\n        \"\"\"\n        start_rad = radians(start_deg)\n        end_rad = radians(end_deg)\n        start_x = self.cx - radius * cos(start_rad)\n        start_y = self.cy + radius * sin(start_rad)\n        end_x = self.cx - radius * cos(end_rad)\n        end_y = self.cy + radius * sin(end_rad)\n\n        start_x, start_y, end_x, end_y = [\n            round(val, 2) for val in (start_x, start_y, end_x, end_y)\n        ]\n\n        path_data = \" \".join(\n            (\n                \"M{} {}\".format(self.cx, self.cy),\n                \"L{} {}\".format(start_x, start_y),\n                \"A{} {} 0 0 0 {} {}\".format(radius, radius, end_x, end_y),\n                \"Z\",\n            )\n        )\n        return path(\n            \"\",\n            d=path_data,\n            fill=fill,\n            stroke=stroke_color,\n            stroke_width=stroke_width,\n            stroke_opacity=stroke_opacity,\n        )\n\n    def background(self, radius: float, **kwargs) -> str:\n        \"\"\"Create a background circle for the chart.\n\n        Args:\n            radius: Radius of the background circle\n            **kwargs: Additional attributes for the circle element\n\n        Returns:\n            An SVG circle element representing the background\n        \"\"\"\n        return circle(cx=self.cx, cy=self.cy, r=radius, **kwargs)\n\n    def sign_wheel(self) -> list[str]:\n        \"\"\"Generate the zodiac sign wheel.\n\n        Returns:\n            A list of SVG elements representing the sign wheel\n        \"\"\"\n        radius = self.max_radius\n\n        wheel = [self.background(radius=radius, fill=self.config.theme.background)]\n        for i in range(12):\n            start_deg = self.data1.signs[i].normalized_degree\n            end_deg = start_deg + 30\n            wheel.append(\n                self.sector(\n                    radius=radius,\n                    start_deg=start_deg,\n                    end_deg=end_deg,\n                    fill=self.bg_colors[i],\n                    stroke_color=self.config.theme.foreground,\n                    stroke_width=self.config.chart.stroke_width,\n                )\n            )\n        return wheel\n\n    def sign_wheel_symbols(self) -> list[str]:\n        \"\"\"Generate the zodiac sign symbols for the sign wheel.\n\n        Returns:\n            A list of SVG elements representing the zodiac sign symbols\n        \"\"\"\n\n        wheel = []\n        for i in range(12):\n            start_deg = self.data1.signs[i].normalized_degree\n            symbol_radius = self.max_radius - (self.ring_thickness / 2)\n            symbol_angle = radians(start_deg + 15)  # Center of the sector\n            symbol_x = self.cx - symbol_radius * cos(symbol_angle) - self.pos_adjustment\n            symbol_y = self.cy + symbol_radius * sin(symbol_angle) - self.pos_adjustment\n            wheel.append(\n                g(\n                    [\n                        circle(\n                            cx=10,\n                            cy=10,\n                            r=12,\n                            stroke=\"none\",\n                            # fill=\"red\",\n                            fill=self.bg_colors[i],\n                        ),\n                        svg_paths[SIGN_MEMBERS[i].name],\n                    ],\n                    stroke=self.config.theme[SIGN_MEMBERS[i].color],\n                    stroke_width=self.config.chart.stroke_width * 1.5,\n                    fill=\"none\",\n                    transform=f\"translate({symbol_x}, {symbol_y}) scale({self.scale_adjustment})\",\n                )\n            )\n        return wheel\n\n    def house_wheel(self) -> list[str]:\n        \"\"\"Generate the house wheel.\n\n        Returns:\n            A list of SVG elements representing the house wheel\n        \"\"\"\n        radius = self.max_radius - self.ring_thickness\n        wheel = [self.background(radius, fill=self.config.theme.background)]\n\n        for i, (start_deg, end_deg) in enumerate(self.house_vertices):\n            wheel.append(\n                self.sector(\n                    radius=radius,\n                    start_deg=start_deg,\n                    end_deg=end_deg,\n                    fill=self.bg_colors[i],\n                    stroke_color=self.config.theme.foreground,\n                    stroke_width=self.config.chart.stroke_width,\n                )\n            )\n\n            # Add house number\n            number_width = self.font_size * 0.8\n            number_radius = radius - (self.ring_thickness / 2)\n            number_angle = radians(\n                start_deg + ((end_deg - start_deg) % 360) / 2\n            )  # Center of the house\n            number_x = self.cx - number_radius * cos(number_angle)\n            number_y = self.cy + number_radius * sin(number_angle)\n            wheel.append(\n                text(\n                    str(i + 1),  # House numbers start from 1\n                    x=number_x,\n                    y=number_y,\n                    fill=getattr(self.config.theme, SIGN_MEMBERS[i].color),\n                    font_size=number_width,\n                    text_anchor=\"middle\",\n                    dominant_baseline=\"central\",\n                )\n            )\n\n        return wheel\n\n    def vertex_wheel(self) -> list[str]:\n        \"\"\"Generate vertex lines for the chart.\n\n        Returns:\n            A list of SVG elements representing vertex lines\n        \"\"\"\n        vertex_radius = self.max_radius + self.margin // 2\n        house_radius = self.max_radius - 2 * self.ring_thickness\n        body_radius = self.max_radius - 3 * self.ring_thickness\n\n        lines = [\n            self.background(\n                house_radius,\n                fill=self.config.theme.background,\n                stroke=self.config.theme.foreground,\n                stroke_width=self.config.chart.stroke_width,\n            ),\n            self.background(\n                body_radius,\n                fill=\"#88888800\",  # transparent\n                stroke=self.config.theme.dim,\n                stroke_width=self.config.chart.stroke_width,\n            ),\n        ]\n        for house in self.data1.houses:\n            radius = house_radius\n            stroke_width = self.config.chart.stroke_width\n            stroke_color = self.config.theme.dim\n\n            if house.value in [1, 4, 7, 10]:\n                radius = vertex_radius\n                stroke_color = self.config.theme.foreground\n\n            angle = radians(house.normalized_degree)\n            end_x = self.cx - radius * cos(angle)\n            end_y = self.cy + radius * sin(angle)\n\n            lines.append(\n                line(\n                    x1=self.cx,\n                    y1=self.cy,\n                    x2=end_x,\n                    y2=end_y,\n                    stroke=stroke_color,\n                    stroke_width=stroke_width,\n                    stroke_opacity=self.config.chart.stroke_opacity,\n                )\n            )\n\n        return lines\n\n    def outer_body_wheel(self) -> list[str]:\n        \"\"\"Generate the outer body wheel for single or composite charts.\n\n        Returns:\n            A list of SVG elements representing the outer body wheel\n        \"\"\"\n        radius = self.max_radius - 3 * self.ring_thickness\n        data = self.data2 or self.data1\n        return self.body_wheel(radius, data, self.config.chart.outer_min_degree)\n\n    def inner_body_wheel(self) -> list[str] | None:\n        \"\"\"Generate the inner body wheel for composite charts.\n\n        Returns:\n            A list of SVG elements representing the inner body wheel, or None for single charts\n        \"\"\"\n        if self.data2 is None:\n            return\n        radius = self.max_radius - 4 * self.ring_thickness\n        data = self.data1\n        return self.body_wheel(radius, data, self.config.chart.inner_min_degree)\n\n    def outer_aspect(self) -> list[str]:\n        \"\"\"Generate aspect lines for the outer wheel in single charts.\n\n        Returns:\n            A list of SVG elements representing aspect lines\n        \"\"\"\n        if self.data2 is not None:\n            return []\n        radius = self.max_radius - 3 * self.ring_thickness\n        aspects = self.data1.aspects\n        return self.aspect_lines(radius, aspects)\n\n    def inner_aspect(self) -> list[str]:\n        \"\"\"Generate aspect lines for the inner wheel in composite charts.\n\n        Returns:\n            A list of SVG elements representing aspect lines\n        \"\"\"\n        if self.data2 is None:\n            return []\n        radius = self.max_radius - 4 * self.ring_thickness\n        aspects = self.data1.calculate_aspects(\n            self.data1.composite_aspects_pairs(self.data2)\n        )\n        return self.aspect_lines(radius, aspects)\n\n    @property\n    def svg(self) -> str:\n        \"\"\"Generate the SVG representation of the chart.\n\n        Returns:\n            str: SVG content.\n        \"\"\"\n        return self.svg_root(\n            [\n                self.sign_wheel(),\n                self.house_wheel(),\n                self.vertex_wheel(),\n                self.sign_wheel_symbols(),\n                self.outer_body_wheel(),\n                self.inner_body_wheel(),\n                self.outer_aspect(),\n                self.inner_aspect(),\n            ]\n        )\n\n    # utils ======================================================\n\n    def adjusted_degrees(self, degrees: list[float], min_degree: float) -> list[float]:\n        \"\"\"Adjust spacing between celestial bodies to avoid overlap.\n\n        Args:\n            degrees: Sorted normalized degrees of celestial bodies\n            min_degree: Minimum allowed degree separation\n\n        Returns:\n            Adjusted degrees of celestial bodies\n        \"\"\"\n        step = min_degree + 0.1  # prevent overlap for float precision\n        n = len(degrees)\n\n        fwd_degs = degrees.copy()\n        bwd_degs = degrees[::-1]\n\n        # Forward adjustment\n        changed = True\n        while changed:\n            changed = False\n            for i in range(n):\n                prev_deg = fwd_degs[-1] - 360 if i == 0 else fwd_degs[i - 1]\n                delta = fwd_degs[i] - prev_deg\n                diff = min(delta, 360 - delta)\n                if (fwd_degs[i] < prev_deg) or (diff < min_degree):\n                    fwd_degs[i] = prev_deg + step\n                    changed = True\n\n        # Backward adjustment\n        changed = True\n        while changed:\n            changed = False\n            for i in range(n):\n                prev_deg = bwd_degs[-1] + 360 if i == 0 else bwd_degs[i - 1]\n                delta = prev_deg - bwd_degs[i]\n                diff = min(delta, 360 - delta)\n                if (prev_deg < bwd_degs[i]) or (diff < min_degree):\n                    bwd_degs[i] = prev_deg - step\n                    changed = True\n\n        bwd_degs.reverse()\n\n        # average forward and backward adjustments\n        avg_adj = []\n        for fwd, bwd in zip(fwd_degs, bwd_degs):\n            fwd %= 360\n            bwd %= 360\n            if abs(fwd - bwd) < 180:\n                avg = (fwd + bwd) / 2\n            else:\n                avg = ((fwd + bwd + 360) / 2) % 360\n            avg_adj.append(avg)\n\n        return avg_adj\n\n    def body_wheel(self, wheel_radius: float, data: Data, min_degree: float):\n        \"\"\"Generate elements for both inner and outer body wheels.\n\n        Args:\n            wheel_radius: Radius of the wheel\n            data: Chart data to use\n            min_degree: Minimum degree separation between bodies\n\n        Returns:\n            A list of SVG elements representing the body wheel\n        \"\"\"\n\n        def norm_deg(x):\n            return self.data1.normalize(x.degree)\n\n        sorted_norm_bodies = sorted(data.aspectables, key=norm_deg)\n        sorted_norm_degs = [norm_deg(b) for b in sorted_norm_bodies]\n\n        # Calculate adjusted positions\n        adj_norm_degs = (\n            self.adjusted_degrees(sorted_norm_degs, min_degree)\n            if len(sorted_norm_bodies) > 1\n            else sorted_norm_degs\n        )\n        # for tests only\n        self.adj_degs_len = len(adj_norm_degs)\n\n        output = []\n        for body, adj_deg in zip(sorted_norm_bodies, adj_norm_degs):\n            g_opt = {\n                \"fill\": \"none\",\n                \"stroke\": self.config.theme[body.color],\n                \"stroke_width\": self.config.chart.stroke_width * 1.5,\n            }\n\n            # special handling for asc, ic, dsc and mc\n            if body.name in VERTEX_NAMES:\n                g_opt[\"fill\"] = self.config.theme[body.color]\n                g_opt[\"stroke\"] = \"none\"\n\n            symbol_radius = wheel_radius + (self.ring_thickness / 2)\n\n            # Use original angle for line start position\n            original_angle = radians(self.data1.normalize(body.degree))\n            degree_x = self.cx - wheel_radius * cos(original_angle)\n            degree_y = self.cy + wheel_radius * sin(original_angle)\n\n            # Use adjusted angle for symbol position\n            adjusted_angle = radians(adj_deg)\n            symbol_x = self.cx - symbol_radius * cos(adjusted_angle)\n            symbol_y = self.cy + symbol_radius * sin(adjusted_angle)\n\n            # Add line connecting to the inner circle\n            inner_radius = wheel_radius - self.ring_thickness\n            inner_x = self.cx - inner_radius * cos(original_angle)\n            inner_y = self.cy + inner_radius * sin(original_angle)\n\n            output.extend(\n                [\n                    line(\n                        x1=degree_x,\n                        y1=degree_y,\n                        x2=symbol_x,\n                        y2=symbol_y,\n                        stroke=self.config.theme[body.color],\n                        stroke_width=self.config.chart.stroke_width / 2,\n                    ),\n                    circle(\n                        cx=symbol_x,\n                        cy=symbol_y,\n                        r=self.font_size / 2,\n                        # fill=\"red\",  # for testing only\n                        fill=self.config.theme.background,\n                    ),\n                    line(\n                        x1=degree_x,\n                        y1=degree_y,\n                        x2=inner_x,\n                        y2=inner_y,\n                        stroke=self.config.theme.dim,\n                        stroke_width=self.config.chart.stroke_width / 2,\n                        stroke_dasharray=self.ring_thickness / 11,\n                    ),\n                    g(\n                        svg_paths[body.name],\n                        transform=f\"translate({symbol_x - self.pos_adjustment}, {symbol_y - self.pos_adjustment}) scale({self.scale_adjustment})\",\n                        **g_opt,\n                    ),\n                ]\n            )\n        return output\n\n    def aspect_lines(self, radius: float, aspects: list[Aspect]) -> list[str]:\n        \"\"\"Draw aspect lines between aspectable celestial bodies.\n\n        Args:\n            radius: Radius of the aspect wheel\n            aspects: List of aspects to draw\n\n        Returns:\n            A list of SVG elements representing aspect lines\n        \"\"\"\n        bg = [\n            self.background(\n                radius,\n                fill=self.config.theme.background,\n                stroke=self.config.theme.dim,\n                stroke_width=self.config.chart.stroke_width,\n            )\n        ]\n        aspect_lines = []\n        for aspect in aspects:\n            start_angle = radians(self.data1.normalize(aspect.body1.degree))\n            end_angle = radians(self.data1.normalize(aspect.body2.degree))\n            orb_config = self.config.orb[aspect.aspect_member.name]\n            if not orb_config:\n                continue\n            orb_fraction = 1 - aspect.orb / orb_config\n            opacity_factor = (\n                1 if aspect.aspect_member.name == \"conjunction\" else orb_fraction\n            )\n            aspect_lines.append(\n                line(\n                    x1=self.cx - radius * cos(start_angle),\n                    y1=self.cy + radius * sin(start_angle),\n                    x2=self.cx - radius * cos(end_angle),\n                    y2=self.cy + radius * sin(end_angle),\n                    stroke=self.config.theme[aspect.aspect_member.color],\n                    stroke_width=self.config.chart.stroke_width / 2,\n                    stroke_opacity=self.config.chart.stroke_opacity * opacity_factor,\n                )\n            )\n\n        self.aspect_lines_len = len(aspect_lines)  # for test only\n        return bg + aspect_lines\n\n    @cached_property\n    def house_vertices(self) -> list[tuple[float, float]]:\n        \"\"\"Calculate the vertices (start and end degrees) of each house.\n\n        Returns:\n            A list of tuples containing start and end degrees for each house\n        \"\"\"\n        vertices = []\n        for i in range(12):\n            next_i = (i + 1) % 12\n            start_deg = self.data1.houses[i].normalized_degree\n            end_deg = self.data1.houses[next_i].normalized_degree\n            # Handle the case where end_deg is less than start_deg (crosses 0\u00b0)\n            if end_deg < start_deg:\n                end_deg += 360\n            vertices.append((start_deg, end_deg))\n\n        return vertices\n\n    @cached_property\n    def bg_colors(self) -> list[str]:\n        \"\"\"Get the background colors for each house.\n\n        Returns:\n            A list of hex color strings for house backgrounds\n        \"\"\"\n\n        def hex_to_rgb(hex_value):\n            hex_value = hex_value.lstrip(\"#\")\n            return tuple(int(hex_value[i : i + 2], 16) for i in (0, 2, 4))\n\n        def rgb_to_hex(rgb):\n            return \"#\" + \"\".join(f\"{i:02x}\" for i in rgb)\n\n        trans = self.config.theme.transparency\n        output = []\n        for i in range(4):\n            hex_color = getattr(self.config.theme, SIGN_MEMBERS[i].color)\n            rgb_color = hex_to_rgb(hex_color)\n            rgb_bg = hex_to_rgb(self.config.theme.background)\n            # blend the color with the background\n            blended_rgb = tuple(\n                int(trans * rgb_color[i] + (1 - trans) * rgb_bg[i]) for i in range(3)\n            )\n            output.append(rgb_to_hex(blended_rgb))\n\n        return output * 4\n
    "},{"location":"chart/#natal.chart.Chart.bg_colors","title":"bg_colors: list[str]cachedproperty","text":"

    Get the background colors for each house.

    Returns:

    Type Description list[str]

    A list of hex color strings for house backgrounds

    "},{"location":"chart/#natal.chart.Chart.house_vertices","title":"house_vertices: list[tuple[float, float]]cachedproperty","text":"

    Calculate the vertices (start and end degrees) of each house.

    Returns:

    Type Description list[tuple[float, float]]

    A list of tuples containing start and end degrees for each house

    "},{"location":"chart/#natal.chart.Chart.svg","title":"svg: strproperty","text":"

    Generate the SVG representation of the chart.

    Returns:

    Name Type Description strstr

    SVG content.

    "},{"location":"chart/#natal.chart.Chart.__init__","title":"__init__(data1: Data, width: int, height: int | None = None, data2: Data | None = None) -> None","text":"

    Initialize a Chart object.

    Parameters:

    Name Type Description Default data1Data

    Primary chart data

    required widthint

    Width of the SVG

    required heightint | None

    Height of the SVG. If None, set to width

    Nonedata2Data | None

    Secondary chart data for composite charts

    None

    Returns:

    Type Description None

    None

    Source code in natal/chart.py
    def __init__(\n    self,\n    data1: Data,\n    width: int,\n    height: int | None = None,\n    data2: Data | None = None,\n) -> None:\n    \"\"\"Initialize a Chart object.\n\n    Args:\n        data1: Primary chart data\n        width: Width of the SVG\n        height: Height of the SVG. If None, set to width\n        data2: Secondary chart data for composite charts\n\n    Returns:\n        None\n    \"\"\"\n    self.data1 = data1\n    self.data2 = data2\n    self.width = width\n    self.height = height\n    if self.height is None:\n        self.height = self.width\n    self.cx = self.width / 2\n    self.cy = self.height / 2\n\n    self.config = self.data1.config\n    margin = min(self.width, self.height) * self.config.chart.margin_factor\n    self.max_radius = min(self.width - margin, self.height - margin) // 2\n    self.margin = margin\n    self.ring_thickness = (\n        self.max_radius * self.config.chart.ring_thickness_fraction\n    )\n    self.font_size = self.ring_thickness * self.config.chart.font_size_fraction\n    self.scale_adjustment = self.width / self.config.chart.scale_adj_factor\n    self.pos_adjustment = self.font_size / self.config.chart.pos_adj_factor\n
    "},{"location":"chart/#natal.chart.Chart.adjusted_degrees","title":"adjusted_degrees(degrees: list[float], min_degree: float) -> list[float]","text":"

    Adjust spacing between celestial bodies to avoid overlap.

    Parameters:

    Name Type Description Default degreeslist[float]

    Sorted normalized degrees of celestial bodies

    required min_degreefloat

    Minimum allowed degree separation

    required

    Returns:

    Type Description list[float]

    Adjusted degrees of celestial bodies

    Source code in natal/chart.py
    def adjusted_degrees(self, degrees: list[float], min_degree: float) -> list[float]:\n    \"\"\"Adjust spacing between celestial bodies to avoid overlap.\n\n    Args:\n        degrees: Sorted normalized degrees of celestial bodies\n        min_degree: Minimum allowed degree separation\n\n    Returns:\n        Adjusted degrees of celestial bodies\n    \"\"\"\n    step = min_degree + 0.1  # prevent overlap for float precision\n    n = len(degrees)\n\n    fwd_degs = degrees.copy()\n    bwd_degs = degrees[::-1]\n\n    # Forward adjustment\n    changed = True\n    while changed:\n        changed = False\n        for i in range(n):\n            prev_deg = fwd_degs[-1] - 360 if i == 0 else fwd_degs[i - 1]\n            delta = fwd_degs[i] - prev_deg\n            diff = min(delta, 360 - delta)\n            if (fwd_degs[i] < prev_deg) or (diff < min_degree):\n                fwd_degs[i] = prev_deg + step\n                changed = True\n\n    # Backward adjustment\n    changed = True\n    while changed:\n        changed = False\n        for i in range(n):\n            prev_deg = bwd_degs[-1] + 360 if i == 0 else bwd_degs[i - 1]\n            delta = prev_deg - bwd_degs[i]\n            diff = min(delta, 360 - delta)\n            if (prev_deg < bwd_degs[i]) or (diff < min_degree):\n                bwd_degs[i] = prev_deg - step\n                changed = True\n\n    bwd_degs.reverse()\n\n    # average forward and backward adjustments\n    avg_adj = []\n    for fwd, bwd in zip(fwd_degs, bwd_degs):\n        fwd %= 360\n        bwd %= 360\n        if abs(fwd - bwd) < 180:\n            avg = (fwd + bwd) / 2\n        else:\n            avg = ((fwd + bwd + 360) / 2) % 360\n        avg_adj.append(avg)\n\n    return avg_adj\n
    "},{"location":"chart/#natal.chart.Chart.aspect_lines","title":"aspect_lines(radius: float, aspects: list[Aspect]) -> list[str]","text":"

    Draw aspect lines between aspectable celestial bodies.

    Parameters:

    Name Type Description Default radiusfloat

    Radius of the aspect wheel

    required aspectslist[Aspect]

    List of aspects to draw

    required

    Returns:

    Type Description list[str]

    A list of SVG elements representing aspect lines

    Source code in natal/chart.py
    def aspect_lines(self, radius: float, aspects: list[Aspect]) -> list[str]:\n    \"\"\"Draw aspect lines between aspectable celestial bodies.\n\n    Args:\n        radius: Radius of the aspect wheel\n        aspects: List of aspects to draw\n\n    Returns:\n        A list of SVG elements representing aspect lines\n    \"\"\"\n    bg = [\n        self.background(\n            radius,\n            fill=self.config.theme.background,\n            stroke=self.config.theme.dim,\n            stroke_width=self.config.chart.stroke_width,\n        )\n    ]\n    aspect_lines = []\n    for aspect in aspects:\n        start_angle = radians(self.data1.normalize(aspect.body1.degree))\n        end_angle = radians(self.data1.normalize(aspect.body2.degree))\n        orb_config = self.config.orb[aspect.aspect_member.name]\n        if not orb_config:\n            continue\n        orb_fraction = 1 - aspect.orb / orb_config\n        opacity_factor = (\n            1 if aspect.aspect_member.name == \"conjunction\" else orb_fraction\n        )\n        aspect_lines.append(\n            line(\n                x1=self.cx - radius * cos(start_angle),\n                y1=self.cy + radius * sin(start_angle),\n                x2=self.cx - radius * cos(end_angle),\n                y2=self.cy + radius * sin(end_angle),\n                stroke=self.config.theme[aspect.aspect_member.color],\n                stroke_width=self.config.chart.stroke_width / 2,\n                stroke_opacity=self.config.chart.stroke_opacity * opacity_factor,\n            )\n        )\n\n    self.aspect_lines_len = len(aspect_lines)  # for test only\n    return bg + aspect_lines\n
    "},{"location":"chart/#natal.chart.Chart.background","title":"background(radius: float, **kwargs) -> str","text":"

    Create a background circle for the chart.

    Parameters:

    Name Type Description Default radiusfloat

    Radius of the background circle

    required **kwargs

    Additional attributes for the circle element

    {}

    Returns:

    Type Description str

    An SVG circle element representing the background

    Source code in natal/chart.py
    def background(self, radius: float, **kwargs) -> str:\n    \"\"\"Create a background circle for the chart.\n\n    Args:\n        radius: Radius of the background circle\n        **kwargs: Additional attributes for the circle element\n\n    Returns:\n        An SVG circle element representing the background\n    \"\"\"\n    return circle(cx=self.cx, cy=self.cy, r=radius, **kwargs)\n
    "},{"location":"chart/#natal.chart.Chart.body_wheel","title":"body_wheel(wheel_radius: float, data: Data, min_degree: float)","text":"

    Generate elements for both inner and outer body wheels.

    Parameters:

    Name Type Description Default wheel_radiusfloat

    Radius of the wheel

    required dataData

    Chart data to use

    required min_degreefloat

    Minimum degree separation between bodies

    required

    Returns:

    Type Description

    A list of SVG elements representing the body wheel

    Source code in natal/chart.py
    def body_wheel(self, wheel_radius: float, data: Data, min_degree: float):\n    \"\"\"Generate elements for both inner and outer body wheels.\n\n    Args:\n        wheel_radius: Radius of the wheel\n        data: Chart data to use\n        min_degree: Minimum degree separation between bodies\n\n    Returns:\n        A list of SVG elements representing the body wheel\n    \"\"\"\n\n    def norm_deg(x):\n        return self.data1.normalize(x.degree)\n\n    sorted_norm_bodies = sorted(data.aspectables, key=norm_deg)\n    sorted_norm_degs = [norm_deg(b) for b in sorted_norm_bodies]\n\n    # Calculate adjusted positions\n    adj_norm_degs = (\n        self.adjusted_degrees(sorted_norm_degs, min_degree)\n        if len(sorted_norm_bodies) > 1\n        else sorted_norm_degs\n    )\n    # for tests only\n    self.adj_degs_len = len(adj_norm_degs)\n\n    output = []\n    for body, adj_deg in zip(sorted_norm_bodies, adj_norm_degs):\n        g_opt = {\n            \"fill\": \"none\",\n            \"stroke\": self.config.theme[body.color],\n            \"stroke_width\": self.config.chart.stroke_width * 1.5,\n        }\n\n        # special handling for asc, ic, dsc and mc\n        if body.name in VERTEX_NAMES:\n            g_opt[\"fill\"] = self.config.theme[body.color]\n            g_opt[\"stroke\"] = \"none\"\n\n        symbol_radius = wheel_radius + (self.ring_thickness / 2)\n\n        # Use original angle for line start position\n        original_angle = radians(self.data1.normalize(body.degree))\n        degree_x = self.cx - wheel_radius * cos(original_angle)\n        degree_y = self.cy + wheel_radius * sin(original_angle)\n\n        # Use adjusted angle for symbol position\n        adjusted_angle = radians(adj_deg)\n        symbol_x = self.cx - symbol_radius * cos(adjusted_angle)\n        symbol_y = self.cy + symbol_radius * sin(adjusted_angle)\n\n        # Add line connecting to the inner circle\n        inner_radius = wheel_radius - self.ring_thickness\n        inner_x = self.cx - inner_radius * cos(original_angle)\n        inner_y = self.cy + inner_radius * sin(original_angle)\n\n        output.extend(\n            [\n                line(\n                    x1=degree_x,\n                    y1=degree_y,\n                    x2=symbol_x,\n                    y2=symbol_y,\n                    stroke=self.config.theme[body.color],\n                    stroke_width=self.config.chart.stroke_width / 2,\n                ),\n                circle(\n                    cx=symbol_x,\n                    cy=symbol_y,\n                    r=self.font_size / 2,\n                    # fill=\"red\",  # for testing only\n                    fill=self.config.theme.background,\n                ),\n                line(\n                    x1=degree_x,\n                    y1=degree_y,\n                    x2=inner_x,\n                    y2=inner_y,\n                    stroke=self.config.theme.dim,\n                    stroke_width=self.config.chart.stroke_width / 2,\n                    stroke_dasharray=self.ring_thickness / 11,\n                ),\n                g(\n                    svg_paths[body.name],\n                    transform=f\"translate({symbol_x - self.pos_adjustment}, {symbol_y - self.pos_adjustment}) scale({self.scale_adjustment})\",\n                    **g_opt,\n                ),\n            ]\n        )\n    return output\n
    "},{"location":"chart/#natal.chart.Chart.house_wheel","title":"house_wheel() -> list[str]","text":"

    Generate the house wheel.

    Returns:

    Type Description list[str]

    A list of SVG elements representing the house wheel

    Source code in natal/chart.py
    def house_wheel(self) -> list[str]:\n    \"\"\"Generate the house wheel.\n\n    Returns:\n        A list of SVG elements representing the house wheel\n    \"\"\"\n    radius = self.max_radius - self.ring_thickness\n    wheel = [self.background(radius, fill=self.config.theme.background)]\n\n    for i, (start_deg, end_deg) in enumerate(self.house_vertices):\n        wheel.append(\n            self.sector(\n                radius=radius,\n                start_deg=start_deg,\n                end_deg=end_deg,\n                fill=self.bg_colors[i],\n                stroke_color=self.config.theme.foreground,\n                stroke_width=self.config.chart.stroke_width,\n            )\n        )\n\n        # Add house number\n        number_width = self.font_size * 0.8\n        number_radius = radius - (self.ring_thickness / 2)\n        number_angle = radians(\n            start_deg + ((end_deg - start_deg) % 360) / 2\n        )  # Center of the house\n        number_x = self.cx - number_radius * cos(number_angle)\n        number_y = self.cy + number_radius * sin(number_angle)\n        wheel.append(\n            text(\n                str(i + 1),  # House numbers start from 1\n                x=number_x,\n                y=number_y,\n                fill=getattr(self.config.theme, SIGN_MEMBERS[i].color),\n                font_size=number_width,\n                text_anchor=\"middle\",\n                dominant_baseline=\"central\",\n            )\n        )\n\n    return wheel\n
    "},{"location":"chart/#natal.chart.Chart.inner_aspect","title":"inner_aspect() -> list[str]","text":"

    Generate aspect lines for the inner wheel in composite charts.

    Returns:

    Type Description list[str]

    A list of SVG elements representing aspect lines

    Source code in natal/chart.py
    def inner_aspect(self) -> list[str]:\n    \"\"\"Generate aspect lines for the inner wheel in composite charts.\n\n    Returns:\n        A list of SVG elements representing aspect lines\n    \"\"\"\n    if self.data2 is None:\n        return []\n    radius = self.max_radius - 4 * self.ring_thickness\n    aspects = self.data1.calculate_aspects(\n        self.data1.composite_aspects_pairs(self.data2)\n    )\n    return self.aspect_lines(radius, aspects)\n
    "},{"location":"chart/#natal.chart.Chart.inner_body_wheel","title":"inner_body_wheel() -> list[str] | None","text":"

    Generate the inner body wheel for composite charts.

    Returns:

    Type Description list[str] | None

    A list of SVG elements representing the inner body wheel, or None for single charts

    Source code in natal/chart.py
    def inner_body_wheel(self) -> list[str] | None:\n    \"\"\"Generate the inner body wheel for composite charts.\n\n    Returns:\n        A list of SVG elements representing the inner body wheel, or None for single charts\n    \"\"\"\n    if self.data2 is None:\n        return\n    radius = self.max_radius - 4 * self.ring_thickness\n    data = self.data1\n    return self.body_wheel(radius, data, self.config.chart.inner_min_degree)\n
    "},{"location":"chart/#natal.chart.Chart.outer_aspect","title":"outer_aspect() -> list[str]","text":"

    Generate aspect lines for the outer wheel in single charts.

    Returns:

    Type Description list[str]

    A list of SVG elements representing aspect lines

    Source code in natal/chart.py
    def outer_aspect(self) -> list[str]:\n    \"\"\"Generate aspect lines for the outer wheel in single charts.\n\n    Returns:\n        A list of SVG elements representing aspect lines\n    \"\"\"\n    if self.data2 is not None:\n        return []\n    radius = self.max_radius - 3 * self.ring_thickness\n    aspects = self.data1.aspects\n    return self.aspect_lines(radius, aspects)\n
    "},{"location":"chart/#natal.chart.Chart.outer_body_wheel","title":"outer_body_wheel() -> list[str]","text":"

    Generate the outer body wheel for single or composite charts.

    Returns:

    Type Description list[str]

    A list of SVG elements representing the outer body wheel

    Source code in natal/chart.py
    def outer_body_wheel(self) -> list[str]:\n    \"\"\"Generate the outer body wheel for single or composite charts.\n\n    Returns:\n        A list of SVG elements representing the outer body wheel\n    \"\"\"\n    radius = self.max_radius - 3 * self.ring_thickness\n    data = self.data2 or self.data1\n    return self.body_wheel(radius, data, self.config.chart.outer_min_degree)\n
    "},{"location":"chart/#natal.chart.Chart.sector","title":"sector(radius: int, start_deg: float, end_deg: float, fill: str = 'white', stroke_color: str = 'black', stroke_width: float = 1, stroke_opacity: float = 1) -> str","text":"

    Create a sector shape in SVG format.

    Parameters:

    Name Type Description Default radiusint

    Radius of the sector

    required start_degfloat

    Starting angle in degrees

    required end_degfloat

    Ending angle in degrees

    required fillstr

    Fill color of the sector

    'white'stroke_colorstr

    Stroke color of the sector

    'black'stroke_widthfloat

    Width of the stroke

    1stroke_opacityfloat

    Opacity of the stroke

    1

    Returns:

    Type Description str

    An SVG path element representing the sector

    Source code in natal/chart.py
    def sector(\n    self,\n    radius: int,\n    start_deg: float,\n    end_deg: float,\n    fill: str = \"white\",\n    stroke_color: str = \"black\",\n    stroke_width: float = 1,\n    stroke_opacity: float = 1,\n) -> str:\n    \"\"\"Create a sector shape in SVG format.\n\n    Args:\n        radius: Radius of the sector\n        start_deg: Starting angle in degrees\n        end_deg: Ending angle in degrees\n        fill: Fill color of the sector\n        stroke_color: Stroke color of the sector\n        stroke_width: Width of the stroke\n        stroke_opacity: Opacity of the stroke\n\n    Returns:\n        An SVG path element representing the sector\n    \"\"\"\n    start_rad = radians(start_deg)\n    end_rad = radians(end_deg)\n    start_x = self.cx - radius * cos(start_rad)\n    start_y = self.cy + radius * sin(start_rad)\n    end_x = self.cx - radius * cos(end_rad)\n    end_y = self.cy + radius * sin(end_rad)\n\n    start_x, start_y, end_x, end_y = [\n        round(val, 2) for val in (start_x, start_y, end_x, end_y)\n    ]\n\n    path_data = \" \".join(\n        (\n            \"M{} {}\".format(self.cx, self.cy),\n            \"L{} {}\".format(start_x, start_y),\n            \"A{} {} 0 0 0 {} {}\".format(radius, radius, end_x, end_y),\n            \"Z\",\n        )\n    )\n    return path(\n        \"\",\n        d=path_data,\n        fill=fill,\n        stroke=stroke_color,\n        stroke_width=stroke_width,\n        stroke_opacity=stroke_opacity,\n    )\n
    "},{"location":"chart/#natal.chart.Chart.sign_wheel","title":"sign_wheel() -> list[str]","text":"

    Generate the zodiac sign wheel.

    Returns:

    Type Description list[str]

    A list of SVG elements representing the sign wheel

    Source code in natal/chart.py
    def sign_wheel(self) -> list[str]:\n    \"\"\"Generate the zodiac sign wheel.\n\n    Returns:\n        A list of SVG elements representing the sign wheel\n    \"\"\"\n    radius = self.max_radius\n\n    wheel = [self.background(radius=radius, fill=self.config.theme.background)]\n    for i in range(12):\n        start_deg = self.data1.signs[i].normalized_degree\n        end_deg = start_deg + 30\n        wheel.append(\n            self.sector(\n                radius=radius,\n                start_deg=start_deg,\n                end_deg=end_deg,\n                fill=self.bg_colors[i],\n                stroke_color=self.config.theme.foreground,\n                stroke_width=self.config.chart.stroke_width,\n            )\n        )\n    return wheel\n
    "},{"location":"chart/#natal.chart.Chart.sign_wheel_symbols","title":"sign_wheel_symbols() -> list[str]","text":"

    Generate the zodiac sign symbols for the sign wheel.

    Returns:

    Type Description list[str]

    A list of SVG elements representing the zodiac sign symbols

    Source code in natal/chart.py
    def sign_wheel_symbols(self) -> list[str]:\n    \"\"\"Generate the zodiac sign symbols for the sign wheel.\n\n    Returns:\n        A list of SVG elements representing the zodiac sign symbols\n    \"\"\"\n\n    wheel = []\n    for i in range(12):\n        start_deg = self.data1.signs[i].normalized_degree\n        symbol_radius = self.max_radius - (self.ring_thickness / 2)\n        symbol_angle = radians(start_deg + 15)  # Center of the sector\n        symbol_x = self.cx - symbol_radius * cos(symbol_angle) - self.pos_adjustment\n        symbol_y = self.cy + symbol_radius * sin(symbol_angle) - self.pos_adjustment\n        wheel.append(\n            g(\n                [\n                    circle(\n                        cx=10,\n                        cy=10,\n                        r=12,\n                        stroke=\"none\",\n                        # fill=\"red\",\n                        fill=self.bg_colors[i],\n                    ),\n                    svg_paths[SIGN_MEMBERS[i].name],\n                ],\n                stroke=self.config.theme[SIGN_MEMBERS[i].color],\n                stroke_width=self.config.chart.stroke_width * 1.5,\n                fill=\"none\",\n                transform=f\"translate({symbol_x}, {symbol_y}) scale({self.scale_adjustment})\",\n            )\n        )\n    return wheel\n
    "},{"location":"chart/#natal.chart.Chart.svg_root","title":"svg_root(content: str | list[str]) -> str","text":"

    Generate an SVG root element with sensible defaults.

    Parameters:

    Name Type Description Default contentstr | list[str]

    The content to be included in the SVG root

    required

    Returns:

    Type Description str

    An SVG root element as a string

    Source code in natal/chart.py
    def svg_root(self, content: str | list[str]) -> str:\n    \"\"\"Generate an SVG root element with sensible defaults.\n\n    Args:\n        content: The content to be included in the SVG root\n\n    Returns:\n        An SVG root element as a string\n    \"\"\"\n    return svg(\n        content,\n        height=self.height,\n        width=self.width,\n        font_family=self.config.chart.font,\n        version=\"1.1\",\n        xmlns=\"http://www.w3.org/2000/svg\",\n    )\n
    "},{"location":"chart/#natal.chart.Chart.vertex_wheel","title":"vertex_wheel() -> list[str]","text":"

    Generate vertex lines for the chart.

    Returns:

    Type Description list[str]

    A list of SVG elements representing vertex lines

    Source code in natal/chart.py
    def vertex_wheel(self) -> list[str]:\n    \"\"\"Generate vertex lines for the chart.\n\n    Returns:\n        A list of SVG elements representing vertex lines\n    \"\"\"\n    vertex_radius = self.max_radius + self.margin // 2\n    house_radius = self.max_radius - 2 * self.ring_thickness\n    body_radius = self.max_radius - 3 * self.ring_thickness\n\n    lines = [\n        self.background(\n            house_radius,\n            fill=self.config.theme.background,\n            stroke=self.config.theme.foreground,\n            stroke_width=self.config.chart.stroke_width,\n        ),\n        self.background(\n            body_radius,\n            fill=\"#88888800\",  # transparent\n            stroke=self.config.theme.dim,\n            stroke_width=self.config.chart.stroke_width,\n        ),\n    ]\n    for house in self.data1.houses:\n        radius = house_radius\n        stroke_width = self.config.chart.stroke_width\n        stroke_color = self.config.theme.dim\n\n        if house.value in [1, 4, 7, 10]:\n            radius = vertex_radius\n            stroke_color = self.config.theme.foreground\n\n        angle = radians(house.normalized_degree)\n        end_x = self.cx - radius * cos(angle)\n        end_y = self.cy + radius * sin(angle)\n\n        lines.append(\n            line(\n                x1=self.cx,\n                y1=self.cy,\n                x2=end_x,\n                y2=end_y,\n                stroke=stroke_color,\n                stroke_width=stroke_width,\n                stroke_opacity=self.config.chart.stroke_opacity,\n            )\n        )\n\n    return lines\n
    "},{"location":"classes/","title":"Classes","text":""},{"location":"classes/#natal.classes","title":"natal.classes","text":""},{"location":"classes/#natal.classes.Aspect","title":"Aspect","text":"

    Bases: DotDict

    An aspect between two celestial bodies.

    Attributes:

    Name Type Description body1Aspectable

    First body in aspect

    body2Aspectable

    Second body in aspect

    aspect_memberAspectMember

    Type of aspect

    applyingbool | None

    Whether aspect is applying

    orbfloat | None

    Orb in degrees from exact aspect

    Source code in natal/classes.py
    class Aspect(DotDict):\n    \"\"\"An aspect between two celestial bodies.\n\n    Attributes:\n        body1 (Aspectable): First body in aspect\n        body2 (Aspectable): Second body in aspect  \n        aspect_member (AspectMember): Type of aspect\n        applying (bool | None): Whether aspect is applying\n        orb (float | None): Orb in degrees from exact aspect\n    \"\"\"\n\n    body1: Aspectable\n    body2: Aspectable\n    aspect_member: AspectMember\n    applying: bool | None = None\n    orb: float | None = None\n
    "},{"location":"classes/#natal.classes.AspectMember","title":"AspectMember","text":"

    Bases: Body

    Represents an aspect in raw data. (conjunction, opposition, trine, square, sextile)

    Source code in natal/const.py
    class AspectMember(Body):\n    \"\"\"\n    Represents an aspect in raw data.\n    (conjunction, opposition, trine, square, sextile)\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.Aspectable","title":"Aspectable","text":"

    Bases: MovableBody

    Represents a celestial body that can form aspects.

    Source code in natal/classes.py
    class Aspectable(MovableBody):\n    \"\"\"\n    Represents a celestial body that can form aspects.\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.Body","title":"Body","text":"

    Bases: DotDict

    Represents a celestial body in raw data. Base class for all members.

    Source code in natal/const.py
    class Body(DotDict):\n    \"\"\"\n    Represents a celestial body in raw data.\n    Base class for all members.\n    \"\"\"\n\n    name: str\n    symbol: str\n    value: int\n    color: str\n
    "},{"location":"classes/#natal.classes.DotDict","title":"DotDict","text":"

    Bases: SimpleNamespace, Dictable

    Extends SimpleNamespace to allow for unpacking and subscript notation access.

    Source code in natal/config.py
    class DotDict(SimpleNamespace, Dictable):\n    \"\"\"\n    Extends SimpleNamespace to allow for unpacking and subscript notation access.\n    \"\"\"\n\n    pass\n
    "},{"location":"classes/#natal.classes.ElementMember","title":"ElementMember","text":"

    Bases: Body

    Represents an element in raw data. (fire, earth, air, water)

    Source code in natal/const.py
    class ElementMember(Body):\n    \"\"\"\n    Represents an element in raw data.\n    (fire, earth, air, water)\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.Extra","title":"Extra","text":"

    Bases: MovableBody

    Represents an extra celestial body (e.g. Moon's Node and Asteroids).

    Source code in natal/classes.py
    class Extra(MovableBody):\n    \"\"\"\n    Represents an extra celestial body (e.g. Moon's Node and Asteroids).\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.ExtraMember","title":"ExtraMember","text":"

    Bases: Body

    Represents an extra celestial body in raw data. (e.g. asteroids, nodes)

    Source code in natal/const.py
    class ExtraMember(Body):\n    \"\"\"\n    Represents an extra celestial body in raw data.\n    (e.g. asteroids, nodes)\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.House","title":"House","text":"

    Bases: MovableBody

    Represents a house.

    Source code in natal/classes.py
    class House(MovableBody):\n    \"\"\"\n    Represents a house.\n    \"\"\"\n\n    ruler: str = None\n    ruler_sign: str = None\n    ruler_house: int = None\n    classic_ruler: str = None\n    classic_ruler_sign: str = None\n    classic_ruler_house: int = None\n
    "},{"location":"classes/#natal.classes.HouseMember","title":"HouseMember","text":"

    Bases: Body

    Represents a house in raw data.

    Source code in natal/const.py
    class HouseMember(Body):\n    \"\"\"\n    Represents a house in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.MovableBody","title":"MovableBody","text":"

    Bases: Body

    A celestial body that can move and have aspects.

    Attributes:

    Name Type Description degreefloat

    Position in degrees (0-360)

    speedfloat

    Movement speed (negative for retrograde)

    normalized_degreefloat

    Position relative to Ascendant

    Source code in natal/classes.py
    class MovableBody(Body):\n    \"\"\"A celestial body that can move and have aspects.\n\n    Attributes:\n        degree (float): Position in degrees (0-360)\n        speed (float): Movement speed (negative for retrograde)\n        normalized_degree (float): Position relative to Ascendant\n    \"\"\"\n\n    degree: float = 0\n    speed: float = 0\n    normalized_degree: float = 0\n\n    @property\n    def signed_deg(self) -> int:\n        \"\"\"Get degree within current sign.\n\n        Returns:\n            int: Degree position within sign (0-29)\n        \"\"\"\n        return int(self.degree % 30)\n\n    @property\n    def minute(self) -> int:\n        \"\"\"Get arc minutes of position.\n\n        Returns:\n            int: Arc minutes of position (0-59)\n        \"\"\"\n        minutes = (self.degree % 30 - self.signed_deg) * 60\n        return floor(minutes)\n\n    @property\n    def retro(self) -> bool:\n        \"\"\"\n        Retrograde status.\n\n        Returns:\n            bool: True if retrograde, False otherwise.\n        \"\"\"\n        return self.speed < 0\n\n    @property\n    def rx(self) -> str:\n        \"\"\"\n        Retrograde symbol.\n\n        Returns:\n            str: The retrograde symbol if retrograde, empty string otherwise.\n        \"\"\"\n        return \"\u211e\" if self.retro else \"\"\n\n    @property\n    def sign(self) -> SignMember:\n        \"\"\"\n        Return sign name, symbol, element, quality, and polarity.\n\n        Returns:\n            SignMember: The sign member.\n        \"\"\"\n        idx = int(self.degree // 30)\n        return SIGN_MEMBERS[idx]\n\n    @property\n    def dms(self) -> str:\n        \"\"\"\n        Degree Minute Second representation of the position.\n\n        Returns:\n            str: The DMS representation.\n        \"\"\"\n        op = [f\"{self.signed_deg:02d}\u00b0\", f\"{self.minute:02d}'\"]\n        if self.rx:\n            op.append(self.rx)\n        return \"\".join(op)\n\n    @property\n    def signed_dms(self) -> str:\n        \"\"\"\n        Degree Minute representation with sign.\n\n        Returns:\n            str: The signed DMS representation.\n        \"\"\"\n        op = [f\"{self.signed_deg:02d}\u00b0\", self.sign.symbol, f\"{self.minute:02d}'\"]\n        if self.rx:\n            op.append(self.rx)\n        return \"\".join(op)\n
    "},{"location":"classes/#natal.classes.MovableBody.dms","title":"dms: strproperty","text":"

    Degree Minute Second representation of the position.

    Returns:

    Name Type Description strstr

    The DMS representation.

    "},{"location":"classes/#natal.classes.MovableBody.minute","title":"minute: intproperty","text":"

    Get arc minutes of position.

    Returns:

    Name Type Description intint

    Arc minutes of position (0-59)

    "},{"location":"classes/#natal.classes.MovableBody.retro","title":"retro: boolproperty","text":"

    Retrograde status.

    Returns:

    Name Type Description boolbool

    True if retrograde, False otherwise.

    "},{"location":"classes/#natal.classes.MovableBody.rx","title":"rx: strproperty","text":"

    Retrograde symbol.

    Returns:

    Name Type Description strstr

    The retrograde symbol if retrograde, empty string otherwise.

    "},{"location":"classes/#natal.classes.MovableBody.sign","title":"sign: SignMemberproperty","text":"

    Return sign name, symbol, element, quality, and polarity.

    Returns:

    Name Type Description SignMemberSignMember

    The sign member.

    "},{"location":"classes/#natal.classes.MovableBody.signed_deg","title":"signed_deg: intproperty","text":"

    Get degree within current sign.

    Returns:

    Name Type Description intint

    Degree position within sign (0-29)

    "},{"location":"classes/#natal.classes.MovableBody.signed_dms","title":"signed_dms: strproperty","text":"

    Degree Minute representation with sign.

    Returns:

    Name Type Description strstr

    The signed DMS representation.

    "},{"location":"classes/#natal.classes.Planet","title":"Planet","text":"

    Bases: MovableBody

    Represents a planet.

    Source code in natal/classes.py
    class Planet(MovableBody):\n    \"\"\"\n    Represents a planet.\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.PlanetMember","title":"PlanetMember","text":"

    Bases: Body

    Represents a planet in raw data.

    Source code in natal/const.py
    class PlanetMember(Body):\n    \"\"\"\n    Represents a planet in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.PolarityMember","title":"PolarityMember","text":"

    Bases: Body

    Represents a polarity in raw data. (positive, negative)

    Source code in natal/const.py
    class PolarityMember(Body):\n    \"\"\"\n    Represents a polarity in raw data.\n    (positive, negative)\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.QualityMember","title":"QualityMember","text":"

    Bases: Body

    Represents a quality in raw data. (cardinal, fixed, mutable)

    Source code in natal/const.py
    class QualityMember(Body):\n    \"\"\"\n    Represents a quality in raw data.\n    (cardinal, fixed, mutable)\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.Sign","title":"Sign","text":"

    Bases: SignMember

    alias to SignMember

    Source code in natal/classes.py
    class Sign(SignMember):\n    \"\"\"\n    alias to SignMember\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.SignMember","title":"SignMember","text":"

    Bases: Body

    Represents a zodiac sign in raw data.

    Source code in natal/const.py
    class SignMember(Body):\n    \"\"\"\n    Represents a zodiac sign in raw data.\n    \"\"\"\n\n    ruler: str\n    detriment: str\n    exaltation: str\n    fall: str\n    classic_ruler: str\n    classic_detriment: str\n    quality: str\n    element: str\n    polarity: str\n
    "},{"location":"classes/#natal.classes.Vertex","title":"Vertex","text":"

    Bases: MovableBody

    Represents a vertex (Asc, Dsc, MC, IC).

    Source code in natal/classes.py
    class Vertex(MovableBody):\n    \"\"\"\n    Represents a vertex (Asc, Dsc, MC, IC).\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.VertexMember","title":"VertexMember","text":"

    Bases: Body

    Represents a vertex in raw data (asc, ic, dsc, mc).

    Source code in natal/const.py
    class VertexMember(Body):\n    \"\"\"\n    Represents a vertex in raw data (asc, ic, dsc, mc).\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.get_member","title":"get_member(raw_data: dict, name: str) -> DotDict","text":"

    Get a member from raw data by name.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required namestr

    The name of the member.

    required

    Returns:

    Name Type Description DotDictDotDict

    The member as a DotDict.

    Source code in natal/const.py
    def get_member(raw_data: dict, name: str) -> DotDict:\n    \"\"\"\n    Get a member from raw data by name.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n        name (str): The name of the member.\n\n    Returns:\n        DotDict: The member as a DotDict.\n    \"\"\"\n    idx = raw_data[\"name\"].index(name)\n    member = {key: raw_data[key][idx] for key in raw_data.keys()}\n    return DotDict(**member)\n
    "},{"location":"classes/#natal.classes.get_members","title":"get_members(raw_data: dict) -> list[DotDict]","text":"

    Get all members from raw data.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required

    Returns:

    Type Description list[DotDict]

    list[DotDict]: A list of members as DotDicts.

    Source code in natal/const.py
    def get_members(raw_data: dict) -> list[DotDict]:\n    \"\"\"\n    Get all members from raw data.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n\n    Returns:\n        list[DotDict]: A list of members as DotDicts.\n    \"\"\"\n    return [get_member(raw_data, name) for name in raw_data[\"name\"]]\n
    "},{"location":"config/","title":"Config","text":""},{"location":"config/#natal.config","title":"natal.config","text":""},{"location":"config/#natal.config.Chart","title":"Chart","text":"

    Bases: ModelDict

    Chart configuration settings.

    Source code in natal/config.py
    class Chart(ModelDict):\n    \"\"\"\n    Chart configuration settings.\n    \"\"\"\n\n    stroke_width: int = 1\n    stroke_opacity: float = 1\n    font: str = \"sans-serif\"\n    font_size_fraction: float = 0.55\n    inner_min_degree: float = 9\n    outer_min_degree: float = 8\n    margin_factor: float = 0.04\n    ring_thickness_fraction: float = 0.15\n    # hard-coded 2.2 and 600 due to the original symbol svg size = 20x20\n    scale_adj_factor: float = 600\n    pos_adj_factor: float = 2.2\n
    "},{"location":"config/#natal.config.Config","title":"Config","text":"

    Bases: ModelDict

    Package configuration model.

    Source code in natal/config.py
    class Config(ModelDict):\n    \"\"\"\n    Package configuration model.\n    \"\"\"\n\n    theme_type: ThemeType = \"dark\"\n    house_sys: HouseSys = HouseSys.Placidus\n    orb: Orb = Orb()\n    light_theme: LightTheme = LightTheme()\n    dark_theme: DarkTheme = DarkTheme()\n    display: Display = Display()\n    chart: Chart = Chart()\n\n    @property\n    def theme(self) -> Theme:\n        \"\"\"\n        Return theme colors based on the theme type.\n\n        Returns:\n            Theme: The theme colors.\n        \"\"\"\n        match self.theme_type:\n            case \"light\":\n                return self.light_theme\n            case \"dark\":\n                return self.dark_theme\n            case \"mono\":\n                kwargs = {key: \"#888888\" for key in self.light_theme.model_dump()}\n                kwargs[\"background\"] = \"#FFFFFF\"\n                kwargs[\"transparency\"] = 0\n                return Theme(**kwargs)\n
    "},{"location":"config/#natal.config.Config.theme","title":"theme: Themeproperty","text":"

    Return theme colors based on the theme type.

    Returns:

    Name Type Description ThemeTheme

    The theme colors.

    "},{"location":"config/#natal.config.DarkTheme","title":"DarkTheme","text":"

    Bases: Theme

    Default dark colors.

    Source code in natal/config.py
    class DarkTheme(Theme):\n    \"\"\"\n    Default dark colors.\n    \"\"\"\n\n    foreground: str = \"#F7F3F0\"\n    background: str = \"#343a40\"\n    dim: str = \"#515860\"\n
    "},{"location":"config/#natal.config.Dictable","title":"Dictable","text":"

    Bases: Mapping

    Protocols for subclasses to behave like a dict.

    Source code in natal/config.py
    class Dictable(Mapping):\n    \"\"\"\n    Protocols for subclasses to behave like a dict.\n    \"\"\"\n\n    def __getitem__(self, key: str):\n        return getattr(self, key)\n\n    def __setitem__(self, key: str, value: Any) -> None:\n        setattr(self, key, value)\n\n    def __iter__(self) -> Iterator[str]:\n        return iter(self.__dict__)\n\n    def __len__(self) -> int:\n        return len(self.__dict__)\n\n    def update(self, other: Mapping[str, Any] | None = None, **kwargs) -> None:\n        \"\"\"\n        Update the attributes with elements from another mapping or from key/value pairs.\n\n        Args:\n            other (Mapping[str, Any] | None): A mapping object to update from.\n            **kwargs: Additional key/value pairs to update with.\n        \"\"\"\n        if other is not None:\n            for key, value in other.items():\n                setattr(self, key, value)\n        for key, value in kwargs.items():\n            setattr(self, key, value)\n
    "},{"location":"config/#natal.config.Dictable.update","title":"update(other: Mapping[str, Any] | None = None, **kwargs) -> None","text":"

    Update the attributes with elements from another mapping or from key/value pairs.

    Parameters:

    Name Type Description Default otherMapping[str, Any] | None

    A mapping object to update from.

    None**kwargs

    Additional key/value pairs to update with.

    {} Source code in natal/config.py
    def update(self, other: Mapping[str, Any] | None = None, **kwargs) -> None:\n    \"\"\"\n    Update the attributes with elements from another mapping or from key/value pairs.\n\n    Args:\n        other (Mapping[str, Any] | None): A mapping object to update from.\n        **kwargs: Additional key/value pairs to update with.\n    \"\"\"\n    if other is not None:\n        for key, value in other.items():\n            setattr(self, key, value)\n    for key, value in kwargs.items():\n        setattr(self, key, value)\n
    "},{"location":"config/#natal.config.Display","title":"Display","text":"

    Bases: ModelDict

    Display settings for celestial bodies.

    Source code in natal/config.py
    class Display(ModelDict):\n    \"\"\"\n    Display settings for celestial bodies.\n    \"\"\"\n\n    sun: bool = True\n    moon: bool = True\n    mercury: bool = True\n    venus: bool = True\n    mars: bool = True\n    jupiter: bool = True\n    saturn: bool = True\n    uranus: bool = True\n    neptune: bool = True\n    pluto: bool = True\n    asc_node: bool = True\n    chiron: bool = False\n    ceres: bool = False\n    pallas: bool = False\n    juno: bool = False\n    vesta: bool = False\n    asc: bool = True\n    ic: bool = False\n    dsc: bool = False\n    mc: bool = True\n
    "},{"location":"config/#natal.config.DotDict","title":"DotDict","text":"

    Bases: SimpleNamespace, Dictable

    Extends SimpleNamespace to allow for unpacking and subscript notation access.

    Source code in natal/config.py
    class DotDict(SimpleNamespace, Dictable):\n    \"\"\"\n    Extends SimpleNamespace to allow for unpacking and subscript notation access.\n    \"\"\"\n\n    pass\n
    "},{"location":"config/#natal.config.LightTheme","title":"LightTheme","text":"

    Bases: Theme

    Default light colors.

    Source code in natal/config.py
    class LightTheme(Theme):\n    \"\"\"\n    Default light colors.\n    \"\"\"\n\n    foreground: str = \"#758492\"\n    background: str = \"#FFFDF1\"\n    dim: str = \"#A4BACD\"\n
    "},{"location":"config/#natal.config.ModelDict","title":"ModelDict","text":"

    Bases: BaseModel, Dictable

    Extends BaseModel to allow for unpacking and subscript notation access.

    Source code in natal/config.py
    class ModelDict(BaseModel, Dictable):\n    \"\"\"\n    Extends BaseModel to allow for unpacking and subscript notation access.\n    \"\"\"\n\n    # override to return keys, otherwise BaseModel.__iter__ returns key value pairs\n    def __iter__(self) -> Iterator[str]:\n        return iter(self.__dict__)\n
    "},{"location":"config/#natal.config.Orb","title":"Orb","text":"

    Bases: ModelDict

    default orb for natal chart

    Source code in natal/config.py
    class Orb(ModelDict):\n    \"\"\"default orb for natal chart\"\"\"\n\n    conjunction: int = 7\n    opposition: int = 6\n    trine: int = 6\n    square: int = 6\n    sextile: int = 5\n
    "},{"location":"config/#natal.config.Theme","title":"Theme","text":"

    Bases: ModelDict

    Default colors for the chart.

    Source code in natal/config.py
    class Theme(ModelDict):\n    \"\"\"\n    Default colors for the chart.\n    \"\"\"\n\n    fire: str = \"#ef476f\"  # fire, square, Asc\n    earth: str = \"#ffd166\"  # earth, MC\n    air: str = \"#06d6a0\"  # air, trine\n    water: str = \"#81bce7\"  # water, opposition\n    points: str = \"#118ab2\"  # lunar nodes, sextile\n    asteroids: str = \"#AA96DA\"  # asteroids\n    positive: str = \"#FFC0CB\"  # positive\n    negative: str = \"#AD8B73\"  # negative\n    others: str = \"#FFA500\"  # conjunction\n    transparency: float = 0.1\n    foreground: str\n    background: str\n    dim: str\n
    "},{"location":"const/","title":"Constants","text":""},{"location":"const/#natal.const","title":"natal.const","text":"

    Constants and utility functions for the natal package.

    "},{"location":"const/#natal.const.AspectMember","title":"AspectMember","text":"

    Bases: Body

    Represents an aspect in raw data. (conjunction, opposition, trine, square, sextile)

    Source code in natal/const.py
    class AspectMember(Body):\n    \"\"\"\n    Represents an aspect in raw data.\n    (conjunction, opposition, trine, square, sextile)\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.Body","title":"Body","text":"

    Bases: DotDict

    Represents a celestial body in raw data. Base class for all members.

    Source code in natal/const.py
    class Body(DotDict):\n    \"\"\"\n    Represents a celestial body in raw data.\n    Base class for all members.\n    \"\"\"\n\n    name: str\n    symbol: str\n    value: int\n    color: str\n
    "},{"location":"const/#natal.const.ElementMember","title":"ElementMember","text":"

    Bases: Body

    Represents an element in raw data. (fire, earth, air, water)

    Source code in natal/const.py
    class ElementMember(Body):\n    \"\"\"\n    Represents an element in raw data.\n    (fire, earth, air, water)\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.ExtraMember","title":"ExtraMember","text":"

    Bases: Body

    Represents an extra celestial body in raw data. (e.g. asteroids, nodes)

    Source code in natal/const.py
    class ExtraMember(Body):\n    \"\"\"\n    Represents an extra celestial body in raw data.\n    (e.g. asteroids, nodes)\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.HouseMember","title":"HouseMember","text":"

    Bases: Body

    Represents a house in raw data.

    Source code in natal/const.py
    class HouseMember(Body):\n    \"\"\"\n    Represents a house in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.PlanetMember","title":"PlanetMember","text":"

    Bases: Body

    Represents a planet in raw data.

    Source code in natal/const.py
    class PlanetMember(Body):\n    \"\"\"\n    Represents a planet in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.PolarityMember","title":"PolarityMember","text":"

    Bases: Body

    Represents a polarity in raw data. (positive, negative)

    Source code in natal/const.py
    class PolarityMember(Body):\n    \"\"\"\n    Represents a polarity in raw data.\n    (positive, negative)\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.QualityMember","title":"QualityMember","text":"

    Bases: Body

    Represents a quality in raw data. (cardinal, fixed, mutable)

    Source code in natal/const.py
    class QualityMember(Body):\n    \"\"\"\n    Represents a quality in raw data.\n    (cardinal, fixed, mutable)\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.SignMember","title":"SignMember","text":"

    Bases: Body

    Represents a zodiac sign in raw data.

    Source code in natal/const.py
    class SignMember(Body):\n    \"\"\"\n    Represents a zodiac sign in raw data.\n    \"\"\"\n\n    ruler: str\n    detriment: str\n    exaltation: str\n    fall: str\n    classic_ruler: str\n    classic_detriment: str\n    quality: str\n    element: str\n    polarity: str\n
    "},{"location":"const/#natal.const.VertexMember","title":"VertexMember","text":"

    Bases: Body

    Represents a vertex in raw data (asc, ic, dsc, mc).

    Source code in natal/const.py
    class VertexMember(Body):\n    \"\"\"\n    Represents a vertex in raw data (asc, ic, dsc, mc).\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.get_member","title":"get_member(raw_data: dict, name: str) -> DotDict","text":"

    Get a member from raw data by name.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required namestr

    The name of the member.

    required

    Returns:

    Name Type Description DotDictDotDict

    The member as a DotDict.

    Source code in natal/const.py
    def get_member(raw_data: dict, name: str) -> DotDict:\n    \"\"\"\n    Get a member from raw data by name.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n        name (str): The name of the member.\n\n    Returns:\n        DotDict: The member as a DotDict.\n    \"\"\"\n    idx = raw_data[\"name\"].index(name)\n    member = {key: raw_data[key][idx] for key in raw_data.keys()}\n    return DotDict(**member)\n
    "},{"location":"const/#natal.const.get_members","title":"get_members(raw_data: dict) -> list[DotDict]","text":"

    Get all members from raw data.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required

    Returns:

    Type Description list[DotDict]

    list[DotDict]: A list of members as DotDicts.

    Source code in natal/const.py
    def get_members(raw_data: dict) -> list[DotDict]:\n    \"\"\"\n    Get all members from raw data.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n\n    Returns:\n        list[DotDict]: A list of members as DotDicts.\n    \"\"\"\n    return [get_member(raw_data, name) for name in raw_data[\"name\"]]\n
    "},{"location":"data/","title":"Data","text":""},{"location":"data/#natal.data","title":"natal.data","text":""},{"location":"data/#natal.data.AspectMember","title":"AspectMember","text":"

    Bases: Body

    Represents an aspect in raw data. (conjunction, opposition, trine, square, sextile)

    Source code in natal/const.py
    class AspectMember(Body):\n    \"\"\"\n    Represents an aspect in raw data.\n    (conjunction, opposition, trine, square, sextile)\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.Data","title":"Data","text":"

    Bases: DotDict

    Data object for a natal chart.

    Source code in natal/data.py
    class Data(DotDict):\n    \"\"\"\n    Data object for a natal chart.\n    \"\"\"\n\n    cities = pd.read_csv(data_folder / \"cities.csv.gz\")\n\n    def __init__(\n        self,\n        name: str,\n        city: str,\n        dt: datetime | str,\n        config: Config = Config(),\n    ) -> None:\n        \"\"\"Initialize a natal chart data object.\n\n        Args:\n            name (str): The name for this chart\n            city (str): City name to lookup coordinates\n            dt (datetime | str): Date and time as datetime object or string\n            config (Config): Configuration settings\n        \"\"\"\n        self.name = name\n        self.city = city\n        if isinstance(dt, str):\n            dt = str_to_dt(dt)\n        self.dt = dt\n        self.config = config\n        self.lat: float = None\n        self.lon: float = None\n        self.timezone: str = None\n        self.house_sys = config.house_sys\n        self.houses: list[House] = []\n        self.planets: list[Planet] = []\n        self.extras: list[Extra] = []\n        self.vertices: list[Vertex] = []\n        self.signs: list[Sign] = []\n        self.aspects: list[Aspect] = []\n        self.quadrants: list[list[Aspectable]] = []\n        self.set_lat_lon()\n        self.set_houses_vertices()\n        self.set_movable_bodies()\n        self.set_aspectable()\n        self.set_signs()\n        self.set_normalized_degrees()\n        self.set_aspects()\n        self.set_rulers()\n        self.set_quadrants()\n\n    @property\n    def julian_day(self) -> float:\n        \"\"\"Convert dt to UTC and return Julian day.\n\n        Returns:\n            float: The Julian day number\n        \"\"\"\n        local_tz = ZoneInfo(self.timezone)\n        local_dt = self.dt.replace(tzinfo=local_tz)\n        utc_dt = local_dt.astimezone(ZoneInfo(\"UTC\"))\n        return swe.date_conversion(\n            utc_dt.year, utc_dt.month, utc_dt.day, utc_dt.hour + utc_dt.minute / 60\n        )[1]\n\n    def set_lat_lon(self) -> None:\n        \"\"\"Set the geographical information of a city.\"\"\"\n        info = self.cities[self.cities[\"name\"].str.lower() == self.city.lower()].iloc[0]\n        self.lat = float(info[\"lat\"])\n        self.lon = float(info[\"lon\"])\n        self.timezone = info[\"timezone\"]\n\n    def set_movable_bodies(self) -> None:\n        \"\"\"Set the positions of the planets and other celestial bodies.\"\"\"\n        self.planets = self.set_positions(PLANET_MEMBERS)\n        self.extras = self.set_positions(EXTRA_MEMBERS)\n\n    def set_houses_vertices(self) -> None:\n        \"\"\"Calculate the cusps of the houses and set the vertices.\"\"\"\n        cusps, (asc_deg, mc_deg, *_) = swe.houses(\n            self.julian_day,\n            self.lat,\n            self.lon,\n            self.house_sys.encode(),\n        )\n\n        for house, cusp in zip(HOUSE_MEMBERS, cusps):\n            house_body = House(\n                **house,\n                degree=floor(cusp * 100) / 100,\n            )\n            self.houses.append(house_body)\n\n        self.vertices = [\n            Vertex(degree=asc_deg, **VERTEX_MEMBERS[0]),\n            Vertex(degree=(mc_deg + 180) % 360, **VERTEX_MEMBERS[1]),\n            Vertex(degree=(asc_deg + 180) % 360, **VERTEX_MEMBERS[2]),\n            Vertex(degree=mc_deg, **VERTEX_MEMBERS[3]),\n        ]\n\n        for v in self.vertices:\n            setattr(self, v.name, v)\n\n    def set_aspectable(self) -> None:\n        \"\"\"Set the aspectable celestial bodies based on the display configuration.\"\"\"\n        self.aspectables = [\n            body\n            for body in (self.planets + self.extras + self.vertices)\n            if self.config.display[body.name]\n        ]\n\n    def set_signs(self) -> None:\n        \"\"\"Set the signs of the zodiac.\"\"\"\n        for i, sign_member in enumerate(SIGN_MEMBERS):\n            sign = Sign(\n                **sign_member,\n                degree=i * 30,\n            )\n            self.signs.append(sign)\n\n    def set_aspects(self) -> None:\n        \"\"\"Set the aspects between the aspectable celestial bodies.\"\"\"\n        body_pairs = pairs(self.aspectables)\n        self.aspects = self.calculate_aspects(body_pairs)\n\n    def set_normalized_degrees(self) -> None:\n        \"\"\"Normalize the positions of celestial bodies relative to the first house.\"\"\"\n        bodies = self.signs + self.planets + self.extras + self.vertices + self.houses\n        for body in bodies:\n            body.normalized_degree = self.normalize(body.degree)\n\n    def set_rulers(self) -> None:\n        \"\"\"Set the rulers for each house.\"\"\"\n        for house in self.houses:\n            ruler = getattr(self, house.sign.ruler)\n            classic_ruler = getattr(self, house.sign.classic_ruler)\n            house.ruler = ruler.name\n            house.ruler_sign = f\"{ruler.sign.symbol}\"\n            house.ruler_house = self.house_of(ruler)\n            house.classic_ruler = classic_ruler.name\n            house.classic_ruler_sign = (\n                f\"{classic_ruler.sign.symbol} {classic_ruler.sign.name}\"\n            )\n            house.classic_ruler_house = self.house_of(classic_ruler)\n\n    def set_quadrants(self) -> None:\n        \"\"\"Set the distribution of celestial bodies in quadrants.\"\"\"\n        bodies = [b for b in self.aspectables if b not in self.vertices]\n        _, ic, dsc, mc = [v.normalized_degree for v in self.vertices]\n\n        first = [b for b in bodies if b.normalized_degree < ic]\n        second = [b for b in bodies if ic <= b.normalized_degree < dsc]\n        third = [b for b in bodies if dsc <= b.normalized_degree < mc]\n        fourth = [b for b in bodies if mc <= b.normalized_degree]\n        self.quadrants = [first, second, third, fourth]\n\n    def __str__(self) -> str:\n        \"\"\"Get string representation of the Data object.\n\n        Returns:\n            str: Formatted string showing chart data\n        \"\"\"\n        op = \"\"\n        op += f\"Name: {self.name}\\n\"\n        op += f\"City: {self.city}\\n\"\n        op += f\"Date: {self.dt}\\n\"\n        op += f\"Latitude: {self.lat}\\n\"\n        op += f\"Longitude: {self.lon}\\n\"\n        op += f\"House System: {self.house_sys}\\n\"\n        op += \"Planets:\\n\"\n        for e in self.planets:\n            op += f\"{e.name}: {e.signed_dms}\\n\"\n        op += \"Extras:\\n\"\n        for e in self.extras:\n            op += f\"{e.name}: {e.signed_dms}\\n\"\n        op += f\"Asc: {self.asc.signed_dms}\\n\"\n        op += f\"MC: {self.mc.signed_dms}\\n\"\n        op += \"Houses:\\n\"\n        for e in self.houses:\n            op += f\"{e.name}: {e.signed_dms}\\n\"\n        op += \"Signs:\\n\"\n        for e in self.signs:\n            op += f\"{e.name}: degree={e.degree:.2f}, ruler={e.ruler}, color={e.color}, quality={e.quality}, element={e.element}, polarity={e.polarity}\\n\"\n        op += \"Aspects:\\n\"\n        for e in self.aspects:\n            op += f\"{e.body1.name} {e.aspect_member.symbol} {e.body2.name}: {e.aspect_member.color}\\n\"\n        return op\n\n    # utils ===============================\n\n    def set_positions(self, bodies: list[Body]) -> list[Aspectable]:\n        \"\"\"Set the positions of celestial bodies.\n\n        Args:\n            bodies (list[Body]): List of celestial body definitions\n\n        Returns:\n            list[Aspectable]: List of aspectable bodies with positions set\n        \"\"\"\n        output = []\n        for body in bodies:\n            ((lon, _, _, speed, *_), _) = swe.calc_ut(self.julian_day, body.value)\n            pos = Aspectable(\n                **body,\n                degree=lon,\n                speed=speed,\n            )\n            setattr(self, body.name, pos)\n            output.append(pos)\n        return output\n\n    def house_of(self, body: Body) -> int:\n        \"\"\"Get the house number containing a celestial body.\n\n        Args:\n            body (Body): The celestial body to locate\n\n        Returns:\n            int: House number (1-12) containing the body\n        \"\"\"\n        sorted_houses = sorted(self.houses, key=lambda x: x.degree, reverse=True)\n        for house in sorted_houses:\n            if body.degree >= house.degree:\n                return house.value\n        return sorted_houses[0].value\n\n    def normalize(self, degree: float) -> float:\n        \"\"\"Normalize a degree relative to the Ascendant.\n\n        Args:\n            degree (float): The degree to normalize\n\n        Returns:\n            float: Normalized degree (0-360)\n        \"\"\"\n        return (degree - self.asc.degree + 360) % 360\n\n    def calculate_aspects(self, body_pairs: BodyPairs) -> list[Aspect]:\n        \"\"\"Calculate aspects between pairs of celestial bodies.\n\n        Args:\n            body_pairs (BodyPairs): Pairs of bodies to check for aspects\n\n        Returns:\n            list[Aspect]: List of aspects found between the bodies\n        \"\"\"\n        output = []\n        for b1, b2 in body_pairs:\n            sorted_bodies = sorted([b1, b2], key=lambda x: x.degree)\n            org_angle = sorted_bodies[1].degree - sorted_bodies[0].degree\n            # get the smaller angle\n            angle = 360 - org_angle if org_angle > 180 else org_angle\n            for aspect_member in ASPECT_MEMBERS:\n                orb_val = self.config.orb[aspect_member.name]\n                if not orb_val:\n                    continue\n                max_orb = aspect_member.value + orb_val\n                min_orb = aspect_member.value - orb_val\n                if min_orb <= angle <= max_orb:\n                    applying = sorted_bodies[0].speed > sorted_bodies[1].speed\n                    if angle < aspect_member.value:\n                        applying = not applying\n                    applying = not applying if org_angle > 180 else applying\n                    output.append(\n                        Aspect(\n                            body1=b1,\n                            body2=b2,\n                            aspect_member=aspect_member,\n                            applying=applying,\n                            orb=abs(angle - aspect_member.value),\n                        )\n                    )\n        return output\n\n    def composite_aspects_pairs(self, data2: Self) -> BodyPairs:\n        \"\"\"Generate pairs of aspectable bodies for composite chart.\n\n        Args:\n            data2 (Self): Second chart data to compare against\n\n        Returns:\n            BodyPairs: Pairs of bodies to check for aspects\n        \"\"\"\n        return itertools.product(self.aspectables, data2.aspectables)\n
    "},{"location":"data/#natal.data.Data.julian_day","title":"julian_day: floatproperty","text":"

    Convert dt to UTC and return Julian day.

    Returns:

    Name Type Description floatfloat

    The Julian day number

    "},{"location":"data/#natal.data.Data.__init__","title":"__init__(name: str, city: str, dt: datetime | str, config: Config = Config()) -> None","text":"

    Initialize a natal chart data object.

    Parameters:

    Name Type Description Default namestr

    The name for this chart

    required citystr

    City name to lookup coordinates

    required dtdatetime | str

    Date and time as datetime object or string

    required configConfig

    Configuration settings

    Config() Source code in natal/data.py
    def __init__(\n    self,\n    name: str,\n    city: str,\n    dt: datetime | str,\n    config: Config = Config(),\n) -> None:\n    \"\"\"Initialize a natal chart data object.\n\n    Args:\n        name (str): The name for this chart\n        city (str): City name to lookup coordinates\n        dt (datetime | str): Date and time as datetime object or string\n        config (Config): Configuration settings\n    \"\"\"\n    self.name = name\n    self.city = city\n    if isinstance(dt, str):\n        dt = str_to_dt(dt)\n    self.dt = dt\n    self.config = config\n    self.lat: float = None\n    self.lon: float = None\n    self.timezone: str = None\n    self.house_sys = config.house_sys\n    self.houses: list[House] = []\n    self.planets: list[Planet] = []\n    self.extras: list[Extra] = []\n    self.vertices: list[Vertex] = []\n    self.signs: list[Sign] = []\n    self.aspects: list[Aspect] = []\n    self.quadrants: list[list[Aspectable]] = []\n    self.set_lat_lon()\n    self.set_houses_vertices()\n    self.set_movable_bodies()\n    self.set_aspectable()\n    self.set_signs()\n    self.set_normalized_degrees()\n    self.set_aspects()\n    self.set_rulers()\n    self.set_quadrants()\n
    "},{"location":"data/#natal.data.Data.__str__","title":"__str__() -> str","text":"

    Get string representation of the Data object.

    Returns:

    Name Type Description strstr

    Formatted string showing chart data

    Source code in natal/data.py
    def __str__(self) -> str:\n    \"\"\"Get string representation of the Data object.\n\n    Returns:\n        str: Formatted string showing chart data\n    \"\"\"\n    op = \"\"\n    op += f\"Name: {self.name}\\n\"\n    op += f\"City: {self.city}\\n\"\n    op += f\"Date: {self.dt}\\n\"\n    op += f\"Latitude: {self.lat}\\n\"\n    op += f\"Longitude: {self.lon}\\n\"\n    op += f\"House System: {self.house_sys}\\n\"\n    op += \"Planets:\\n\"\n    for e in self.planets:\n        op += f\"{e.name}: {e.signed_dms}\\n\"\n    op += \"Extras:\\n\"\n    for e in self.extras:\n        op += f\"{e.name}: {e.signed_dms}\\n\"\n    op += f\"Asc: {self.asc.signed_dms}\\n\"\n    op += f\"MC: {self.mc.signed_dms}\\n\"\n    op += \"Houses:\\n\"\n    for e in self.houses:\n        op += f\"{e.name}: {e.signed_dms}\\n\"\n    op += \"Signs:\\n\"\n    for e in self.signs:\n        op += f\"{e.name}: degree={e.degree:.2f}, ruler={e.ruler}, color={e.color}, quality={e.quality}, element={e.element}, polarity={e.polarity}\\n\"\n    op += \"Aspects:\\n\"\n    for e in self.aspects:\n        op += f\"{e.body1.name} {e.aspect_member.symbol} {e.body2.name}: {e.aspect_member.color}\\n\"\n    return op\n
    "},{"location":"data/#natal.data.Data.calculate_aspects","title":"calculate_aspects(body_pairs: BodyPairs) -> list[Aspect]","text":"

    Calculate aspects between pairs of celestial bodies.

    Parameters:

    Name Type Description Default body_pairsBodyPairs

    Pairs of bodies to check for aspects

    required

    Returns:

    Type Description list[Aspect]

    list[Aspect]: List of aspects found between the bodies

    Source code in natal/data.py
    def calculate_aspects(self, body_pairs: BodyPairs) -> list[Aspect]:\n    \"\"\"Calculate aspects between pairs of celestial bodies.\n\n    Args:\n        body_pairs (BodyPairs): Pairs of bodies to check for aspects\n\n    Returns:\n        list[Aspect]: List of aspects found between the bodies\n    \"\"\"\n    output = []\n    for b1, b2 in body_pairs:\n        sorted_bodies = sorted([b1, b2], key=lambda x: x.degree)\n        org_angle = sorted_bodies[1].degree - sorted_bodies[0].degree\n        # get the smaller angle\n        angle = 360 - org_angle if org_angle > 180 else org_angle\n        for aspect_member in ASPECT_MEMBERS:\n            orb_val = self.config.orb[aspect_member.name]\n            if not orb_val:\n                continue\n            max_orb = aspect_member.value + orb_val\n            min_orb = aspect_member.value - orb_val\n            if min_orb <= angle <= max_orb:\n                applying = sorted_bodies[0].speed > sorted_bodies[1].speed\n                if angle < aspect_member.value:\n                    applying = not applying\n                applying = not applying if org_angle > 180 else applying\n                output.append(\n                    Aspect(\n                        body1=b1,\n                        body2=b2,\n                        aspect_member=aspect_member,\n                        applying=applying,\n                        orb=abs(angle - aspect_member.value),\n                    )\n                )\n    return output\n
    "},{"location":"data/#natal.data.Data.composite_aspects_pairs","title":"composite_aspects_pairs(data2: Self) -> BodyPairs","text":"

    Generate pairs of aspectable bodies for composite chart.

    Parameters:

    Name Type Description Default data2Self

    Second chart data to compare against

    required

    Returns:

    Name Type Description BodyPairsBodyPairs

    Pairs of bodies to check for aspects

    Source code in natal/data.py
    def composite_aspects_pairs(self, data2: Self) -> BodyPairs:\n    \"\"\"Generate pairs of aspectable bodies for composite chart.\n\n    Args:\n        data2 (Self): Second chart data to compare against\n\n    Returns:\n        BodyPairs: Pairs of bodies to check for aspects\n    \"\"\"\n    return itertools.product(self.aspectables, data2.aspectables)\n
    "},{"location":"data/#natal.data.Data.house_of","title":"house_of(body: Body) -> int","text":"

    Get the house number containing a celestial body.

    Parameters:

    Name Type Description Default bodyBody

    The celestial body to locate

    required

    Returns:

    Name Type Description intint

    House number (1-12) containing the body

    Source code in natal/data.py
    def house_of(self, body: Body) -> int:\n    \"\"\"Get the house number containing a celestial body.\n\n    Args:\n        body (Body): The celestial body to locate\n\n    Returns:\n        int: House number (1-12) containing the body\n    \"\"\"\n    sorted_houses = sorted(self.houses, key=lambda x: x.degree, reverse=True)\n    for house in sorted_houses:\n        if body.degree >= house.degree:\n            return house.value\n    return sorted_houses[0].value\n
    "},{"location":"data/#natal.data.Data.normalize","title":"normalize(degree: float) -> float","text":"

    Normalize a degree relative to the Ascendant.

    Parameters:

    Name Type Description Default degreefloat

    The degree to normalize

    required

    Returns:

    Name Type Description floatfloat

    Normalized degree (0-360)

    Source code in natal/data.py
    def normalize(self, degree: float) -> float:\n    \"\"\"Normalize a degree relative to the Ascendant.\n\n    Args:\n        degree (float): The degree to normalize\n\n    Returns:\n        float: Normalized degree (0-360)\n    \"\"\"\n    return (degree - self.asc.degree + 360) % 360\n
    "},{"location":"data/#natal.data.Data.set_aspectable","title":"set_aspectable() -> None","text":"

    Set the aspectable celestial bodies based on the display configuration.

    Source code in natal/data.py
    def set_aspectable(self) -> None:\n    \"\"\"Set the aspectable celestial bodies based on the display configuration.\"\"\"\n    self.aspectables = [\n        body\n        for body in (self.planets + self.extras + self.vertices)\n        if self.config.display[body.name]\n    ]\n
    "},{"location":"data/#natal.data.Data.set_aspects","title":"set_aspects() -> None","text":"

    Set the aspects between the aspectable celestial bodies.

    Source code in natal/data.py
    def set_aspects(self) -> None:\n    \"\"\"Set the aspects between the aspectable celestial bodies.\"\"\"\n    body_pairs = pairs(self.aspectables)\n    self.aspects = self.calculate_aspects(body_pairs)\n
    "},{"location":"data/#natal.data.Data.set_houses_vertices","title":"set_houses_vertices() -> None","text":"

    Calculate the cusps of the houses and set the vertices.

    Source code in natal/data.py
    def set_houses_vertices(self) -> None:\n    \"\"\"Calculate the cusps of the houses and set the vertices.\"\"\"\n    cusps, (asc_deg, mc_deg, *_) = swe.houses(\n        self.julian_day,\n        self.lat,\n        self.lon,\n        self.house_sys.encode(),\n    )\n\n    for house, cusp in zip(HOUSE_MEMBERS, cusps):\n        house_body = House(\n            **house,\n            degree=floor(cusp * 100) / 100,\n        )\n        self.houses.append(house_body)\n\n    self.vertices = [\n        Vertex(degree=asc_deg, **VERTEX_MEMBERS[0]),\n        Vertex(degree=(mc_deg + 180) % 360, **VERTEX_MEMBERS[1]),\n        Vertex(degree=(asc_deg + 180) % 360, **VERTEX_MEMBERS[2]),\n        Vertex(degree=mc_deg, **VERTEX_MEMBERS[3]),\n    ]\n\n    for v in self.vertices:\n        setattr(self, v.name, v)\n
    "},{"location":"data/#natal.data.Data.set_lat_lon","title":"set_lat_lon() -> None","text":"

    Set the geographical information of a city.

    Source code in natal/data.py
    def set_lat_lon(self) -> None:\n    \"\"\"Set the geographical information of a city.\"\"\"\n    info = self.cities[self.cities[\"name\"].str.lower() == self.city.lower()].iloc[0]\n    self.lat = float(info[\"lat\"])\n    self.lon = float(info[\"lon\"])\n    self.timezone = info[\"timezone\"]\n
    "},{"location":"data/#natal.data.Data.set_movable_bodies","title":"set_movable_bodies() -> None","text":"

    Set the positions of the planets and other celestial bodies.

    Source code in natal/data.py
    def set_movable_bodies(self) -> None:\n    \"\"\"Set the positions of the planets and other celestial bodies.\"\"\"\n    self.planets = self.set_positions(PLANET_MEMBERS)\n    self.extras = self.set_positions(EXTRA_MEMBERS)\n
    "},{"location":"data/#natal.data.Data.set_normalized_degrees","title":"set_normalized_degrees() -> None","text":"

    Normalize the positions of celestial bodies relative to the first house.

    Source code in natal/data.py
    def set_normalized_degrees(self) -> None:\n    \"\"\"Normalize the positions of celestial bodies relative to the first house.\"\"\"\n    bodies = self.signs + self.planets + self.extras + self.vertices + self.houses\n    for body in bodies:\n        body.normalized_degree = self.normalize(body.degree)\n
    "},{"location":"data/#natal.data.Data.set_positions","title":"set_positions(bodies: list[Body]) -> list[Aspectable]","text":"

    Set the positions of celestial bodies.

    Parameters:

    Name Type Description Default bodieslist[Body]

    List of celestial body definitions

    required

    Returns:

    Type Description list[Aspectable]

    list[Aspectable]: List of aspectable bodies with positions set

    Source code in natal/data.py
    def set_positions(self, bodies: list[Body]) -> list[Aspectable]:\n    \"\"\"Set the positions of celestial bodies.\n\n    Args:\n        bodies (list[Body]): List of celestial body definitions\n\n    Returns:\n        list[Aspectable]: List of aspectable bodies with positions set\n    \"\"\"\n    output = []\n    for body in bodies:\n        ((lon, _, _, speed, *_), _) = swe.calc_ut(self.julian_day, body.value)\n        pos = Aspectable(\n            **body,\n            degree=lon,\n            speed=speed,\n        )\n        setattr(self, body.name, pos)\n        output.append(pos)\n    return output\n
    "},{"location":"data/#natal.data.Data.set_quadrants","title":"set_quadrants() -> None","text":"

    Set the distribution of celestial bodies in quadrants.

    Source code in natal/data.py
    def set_quadrants(self) -> None:\n    \"\"\"Set the distribution of celestial bodies in quadrants.\"\"\"\n    bodies = [b for b in self.aspectables if b not in self.vertices]\n    _, ic, dsc, mc = [v.normalized_degree for v in self.vertices]\n\n    first = [b for b in bodies if b.normalized_degree < ic]\n    second = [b for b in bodies if ic <= b.normalized_degree < dsc]\n    third = [b for b in bodies if dsc <= b.normalized_degree < mc]\n    fourth = [b for b in bodies if mc <= b.normalized_degree]\n    self.quadrants = [first, second, third, fourth]\n
    "},{"location":"data/#natal.data.Data.set_rulers","title":"set_rulers() -> None","text":"

    Set the rulers for each house.

    Source code in natal/data.py
    def set_rulers(self) -> None:\n    \"\"\"Set the rulers for each house.\"\"\"\n    for house in self.houses:\n        ruler = getattr(self, house.sign.ruler)\n        classic_ruler = getattr(self, house.sign.classic_ruler)\n        house.ruler = ruler.name\n        house.ruler_sign = f\"{ruler.sign.symbol}\"\n        house.ruler_house = self.house_of(ruler)\n        house.classic_ruler = classic_ruler.name\n        house.classic_ruler_sign = (\n            f\"{classic_ruler.sign.symbol} {classic_ruler.sign.name}\"\n        )\n        house.classic_ruler_house = self.house_of(classic_ruler)\n
    "},{"location":"data/#natal.data.Data.set_signs","title":"set_signs() -> None","text":"

    Set the signs of the zodiac.

    Source code in natal/data.py
    def set_signs(self) -> None:\n    \"\"\"Set the signs of the zodiac.\"\"\"\n    for i, sign_member in enumerate(SIGN_MEMBERS):\n        sign = Sign(\n            **sign_member,\n            degree=i * 30,\n        )\n        self.signs.append(sign)\n
    "},{"location":"data/#natal.data.DotDict","title":"DotDict","text":"

    Bases: SimpleNamespace, Dictable

    Extends SimpleNamespace to allow for unpacking and subscript notation access.

    Source code in natal/config.py
    class DotDict(SimpleNamespace, Dictable):\n    \"\"\"\n    Extends SimpleNamespace to allow for unpacking and subscript notation access.\n    \"\"\"\n\n    pass\n
    "},{"location":"data/#natal.data.ElementMember","title":"ElementMember","text":"

    Bases: Body

    Represents an element in raw data. (fire, earth, air, water)

    Source code in natal/const.py
    class ElementMember(Body):\n    \"\"\"\n    Represents an element in raw data.\n    (fire, earth, air, water)\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.ExtraMember","title":"ExtraMember","text":"

    Bases: Body

    Represents an extra celestial body in raw data. (e.g. asteroids, nodes)

    Source code in natal/const.py
    class ExtraMember(Body):\n    \"\"\"\n    Represents an extra celestial body in raw data.\n    (e.g. asteroids, nodes)\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.HouseMember","title":"HouseMember","text":"

    Bases: Body

    Represents a house in raw data.

    Source code in natal/const.py
    class HouseMember(Body):\n    \"\"\"\n    Represents a house in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.PlanetMember","title":"PlanetMember","text":"

    Bases: Body

    Represents a planet in raw data.

    Source code in natal/const.py
    class PlanetMember(Body):\n    \"\"\"\n    Represents a planet in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.PolarityMember","title":"PolarityMember","text":"

    Bases: Body

    Represents a polarity in raw data. (positive, negative)

    Source code in natal/const.py
    class PolarityMember(Body):\n    \"\"\"\n    Represents a polarity in raw data.\n    (positive, negative)\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.QualityMember","title":"QualityMember","text":"

    Bases: Body

    Represents a quality in raw data. (cardinal, fixed, mutable)

    Source code in natal/const.py
    class QualityMember(Body):\n    \"\"\"\n    Represents a quality in raw data.\n    (cardinal, fixed, mutable)\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.SignMember","title":"SignMember","text":"

    Bases: Body

    Represents a zodiac sign in raw data.

    Source code in natal/const.py
    class SignMember(Body):\n    \"\"\"\n    Represents a zodiac sign in raw data.\n    \"\"\"\n\n    ruler: str\n    detriment: str\n    exaltation: str\n    fall: str\n    classic_ruler: str\n    classic_detriment: str\n    quality: str\n    element: str\n    polarity: str\n
    "},{"location":"data/#natal.data.VertexMember","title":"VertexMember","text":"

    Bases: Body

    Represents a vertex in raw data (asc, ic, dsc, mc).

    Source code in natal/const.py
    class VertexMember(Body):\n    \"\"\"\n    Represents a vertex in raw data (asc, ic, dsc, mc).\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.get_member","title":"get_member(raw_data: dict, name: str) -> DotDict","text":"

    Get a member from raw data by name.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required namestr

    The name of the member.

    required

    Returns:

    Name Type Description DotDictDotDict

    The member as a DotDict.

    Source code in natal/const.py
    def get_member(raw_data: dict, name: str) -> DotDict:\n    \"\"\"\n    Get a member from raw data by name.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n        name (str): The name of the member.\n\n    Returns:\n        DotDict: The member as a DotDict.\n    \"\"\"\n    idx = raw_data[\"name\"].index(name)\n    member = {key: raw_data[key][idx] for key in raw_data.keys()}\n    return DotDict(**member)\n
    "},{"location":"data/#natal.data.get_members","title":"get_members(raw_data: dict) -> list[DotDict]","text":"

    Get all members from raw data.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required

    Returns:

    Type Description list[DotDict]

    list[DotDict]: A list of members as DotDicts.

    Source code in natal/const.py
    def get_members(raw_data: dict) -> list[DotDict]:\n    \"\"\"\n    Get all members from raw data.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n\n    Returns:\n        list[DotDict]: A list of members as DotDicts.\n    \"\"\"\n    return [get_member(raw_data, name) for name in raw_data[\"name\"]]\n
    "},{"location":"license/","title":"License","text":"

    MIT License

    Copyright (c) 2022 Kelvin Ng

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    "},{"location":"report/","title":"Report","text":""},{"location":"report/#natal.report","title":"natal.report","text":"

    This module generates a detailed astrological report in PDF format. It includes information about birth data, elements, modalities, polarities, hemispheres, quadrants, signs, houses, and celestial bodies. The report is created using the natal astrology library and rendered as an HTML document, which is then converted to a PDF.

    Classes:

    Name Description Report

    Generates the astrological report.

    "},{"location":"report/#natal.report.Report","title":"Report","text":"

    Generates an astrological report based on provided data.

    Attributes:

    Name Type Description data1Data

    The primary data for the report.

    data2Data

    The secondary data for the report, if any.

    Source code in natal/report.py
    class Report:\n    \"\"\"\n    Generates an astrological report based on provided data.\n\n    Attributes:\n        data1: The primary data for the report.\n        data2: The secondary data for the report, if any.\n    \"\"\"\n\n    def __init__(self, data1: Data, data2: Data | None = None):\n        \"\"\"\n        Initializes the Report with the given data.\n\n        Args:\n            data1: The primary data for the report.\n            data2: The secondary data for the report, if any.\n        \"\"\"\n        self.data1: Data = data1\n        self.data2: Data = data2\n\n    @property\n    def basic_info(self) -> Grid:\n        \"\"\"\n        Generates basic information about the provided data.\n\n        Returns:\n            A grid containing the name, city, and birth date/time.\n        \"\"\"\n        time_fmt = \"%Y-%m-%d %H:%M\"\n        dt1 = self.data1.dt.strftime(time_fmt)\n        output = [[\"name\", \"city\", \"birth\"]]\n        output.append([self.data1.name, self.data1.city, dt1])\n        if self.data2:\n            dt2 = self.data2.dt.strftime(time_fmt)\n            output.append([self.data2.name, self.data2.city, dt2])\n        return list(zip(*output))\n\n    @property\n    def element_vs_quality(self) -> Grid:\n        \"\"\"\n        Generates a grid comparing elements and qualities.\n\n        Returns:\n            A grid comparing elements and qualities.\n        \"\"\"\n        aspectable1 = self.data1.aspectables\n        element_symbols = [svg_of(ele.name) for ele in ELEMENTS]\n        grid = [[\"\"] + element_symbols + [\"sum\"]]\n        element_count = defaultdict(int)\n        for quality in QUALITY_MEMBERS:\n            row = [svg_of(quality.name)]\n            quality_count = 0\n            for element in ELEMENTS:\n                count = 0\n                symbols = \"\"\n                for body in aspectable1:\n                    if (\n                        body.sign.element == element.name\n                        and body.sign.quality == quality.name\n                    ):\n                        symbols += svg_of(body.name)\n                        count += 1\n                        element_count[element.name] += 1\n                row.append(symbols)\n                quality_count += count\n            row.append(quality_count)\n            grid.append(row)\n        grid.append(\n            [\"sum\"] + list(element_count.values()) + [sum(element_count.values())]\n        )\n        grid.append(\n            [\n                \"\u25d0\",\n                f\"null:{element_count['fire'] + element_count['air']} pos\",\n                f\"null:{element_count['water'] + element_count['earth']} neg\",\n                \"\",\n            ]\n        )\n        return grid\n\n    @property\n    def quadrants_vs_hemisphere(self) -> Grid:\n        \"\"\"\n        Generates a grid comparing quadrants and hemispheres.\n\n        Returns:\n            A grid comparing quadrants and hemispheres.\n        \"\"\"\n        q = self.data1.quadrants\n        first_q = [svg_of(body.name) for body in q[0]]\n        second_q = [svg_of(body.name) for body in q[1]]\n        third_q = [svg_of(body.name) for body in q[2]]\n        forth_q = [svg_of(body.name) for body in q[3]]\n        hemi_symbols = [\"\u2190\", \"\u2192\", \"\u2191\", \"\u2193\"]\n        grid = [[\"\"] + hemi_symbols[:2] + [\"sum\"]]\n        grid += [[\"\u2191\"] + [forth_q, third_q] + [len(q[3] + q[2])]]\n        grid += [[\"\u2193\"] + [first_q, second_q] + [len(q[3] + q[2])]]\n        grid += [\n            [\"sum\"]\n            + [len(q[3] + q[0]), len(q[1] + q[2])]\n            + [len(q[0] + q[1] + q[2] + q[3])]\n        ]\n        return grid\n\n    @property\n    def signs(self) -> Grid:\n        \"\"\"\n        Generates a grid of signs and their corresponding bodies.\n\n        Returns:\n            A grid of signs and their corresponding bodies\n        \"\"\"\n        grid = [[\"sign\", \"bodies\", \"sum\"]]\n        for sign in SIGN_MEMBERS:\n            bodies = [\n                svg_of(b.name)\n                for b in self.data1.aspectables\n                if b.sign.name == sign.name\n            ]\n            grid.append([svg_of(sign.name), \"\".join(bodies), len(bodies) or \"\"])\n        return grid\n\n    @property\n    def houses(self) -> Grid:\n        \"\"\"\n        Generates a grid of houses and their corresponding bodies.\n\n        Returns:\n            A grid of houses and their corresponding bodies.\n        \"\"\"\n        grid = [[\"house\", \"cusp\", \"bodies\", \"sum\"]]\n        for hse in self.data1.houses:\n            bodies = [\n                svg_of(b.name)\n                for b in self.data1.aspectables\n                if self.data1.house_of(b) == hse.value\n            ]\n            grid.append(\n                [\n                    hse.value,\n                    f\"{hse.signed_deg:02d}\u00b0 {svg_of(hse.sign.name)} {hse.minute:02d}'\",\n                    \"\".join(bodies),\n                    len(bodies) or \"\",\n                ]\n            )\n        return grid\n\n    @property\n    def celestial_body1(self) -> Grid:\n        \"\"\"\n        Generates a grid of celestial bodies for the primary data.\n\n        Returns:\n            Grid: A grid of celestial bodies for the primary data.\n        \"\"\"\n        return self.celestial_body(self.data1)\n\n    @property\n    def celestial_body2(self) -> Grid:\n        \"\"\"\n        Generates a grid of celestial bodies for the secondary data.\n\n        Returns:\n            Grid: A grid of celestial bodies for the secondary data.\n        \"\"\"\n        return self.celestial_body(self.data2)\n\n    def celestial_body(self, data: Data) -> Grid:\n        \"\"\"\n        Generates a grid of celestial bodies for the given data.\n\n        Args:\n            data: The data for which to generate the grid.\n\n        Returns:\n            A grid of celestial bodies for the given data.\n        \"\"\"\n        grid = [(\"body\", \"sign\", \"house\", \"dignity\")]\n        for body in data.aspectables:\n            grid.append(\n                (\n                    svg_of(body.name),\n                    f\"{body.signed_deg:02d}\u00b0 {svg_of(body.sign.name)} {body.minute:02d}'\",\n                    self.data1.house_of(body),\n                    svg_of(dignity_of(body)),\n                )\n            )\n        return grid\n\n    @property\n    def cross_ref(self) -> StatData:\n        \"\"\"\n        Generates cross-reference statistics between the primary and secondary data.\n\n        Returns:\n            StatData: Cross-reference statistics between the primary and secondary data.\n        \"\"\"\n        stats = Stats(self.data1, self.data2)\n        grid = stats.cross_ref.grid\n        for row in range(len(grid)):\n            for col in range(len(grid[0])):\n                cell = grid[row][col]\n                if name := symbol_name_map.get(cell):\n                    grid[row][col] = svg_of(name)\n        return StatData(stats.cross_ref.title, grid)\n\n    @property\n    def full_report(self) -> str:\n        \"\"\"\n        Generates the full astrological report as an HTML string.\n\n        Returns:\n            str: The full astrological report as an HTML string.\n        \"\"\"\n        chart = Chart(self.data1, width=400, data2=self.data2)\n        row1 = div(\n            section(\"Birth Info\", self.basic_info)\n            + section(\"Elements, Modality & Polarity\", self.element_vs_quality)\n            + section(\"Hemisphere & Quadrants\", self.quadrants_vs_hemisphere),\n            class_=\"info_col\",\n        ) + div(chart.svg, class_=\"chart\")\n\n        row2 = section(f\"{self.data1.name}'s Celestial Bodies\", self.celestial_body1)\n\n        if self.data2:\n            row2 += section(\n                f\"{self.data2.name}'s Celestial Bodies\", self.celestial_body2\n            )\n        row2 += section(self.cross_ref.title, self.cross_ref.grid)\n        row3 = section(\"Signs\", self.signs) + section(\"Houses\", self.houses)\n        css = Path(__file__).parent / \"report.css\"\n        html = style(css.read_text()) + main(\n            div(row1, class_=\"row1\")\n            + div(row2, class_=\"row2\")\n            + div(row3, class_=\"row3\")\n        )\n        return html\n\n    def create_pdf(self, html: str) -> BytesIO:\n        \"\"\"\n        Creates a PDF from the given HTML string.\n\n        Args:\n            html: The HTML string to convert to PDF.\n\n        Returns:\n            A BytesIO object containing the PDF data.\n        \"\"\"\n        fp = BytesIO()\n        HTML(string=html).write_pdf(fp)\n        return fp\n
    "},{"location":"report/#natal.report.Report.basic_info","title":"basic_info: Gridproperty","text":"

    Generates basic information about the provided data.

    Returns:

    Type Description Grid

    A grid containing the name, city, and birth date/time.

    "},{"location":"report/#natal.report.Report.celestial_body1","title":"celestial_body1: Gridproperty","text":"

    Generates a grid of celestial bodies for the primary data.

    Returns:

    Name Type Description GridGrid

    A grid of celestial bodies for the primary data.

    "},{"location":"report/#natal.report.Report.celestial_body2","title":"celestial_body2: Gridproperty","text":"

    Generates a grid of celestial bodies for the secondary data.

    Returns:

    Name Type Description GridGrid

    A grid of celestial bodies for the secondary data.

    "},{"location":"report/#natal.report.Report.cross_ref","title":"cross_ref: StatDataproperty","text":"

    Generates cross-reference statistics between the primary and secondary data.

    Returns:

    Name Type Description StatDataStatData

    Cross-reference statistics between the primary and secondary data.

    "},{"location":"report/#natal.report.Report.element_vs_quality","title":"element_vs_quality: Gridproperty","text":"

    Generates a grid comparing elements and qualities.

    Returns:

    Type Description Grid

    A grid comparing elements and qualities.

    "},{"location":"report/#natal.report.Report.full_report","title":"full_report: strproperty","text":"

    Generates the full astrological report as an HTML string.

    Returns:

    Name Type Description strstr

    The full astrological report as an HTML string.

    "},{"location":"report/#natal.report.Report.houses","title":"houses: Gridproperty","text":"

    Generates a grid of houses and their corresponding bodies.

    Returns:

    Type Description Grid

    A grid of houses and their corresponding bodies.

    "},{"location":"report/#natal.report.Report.quadrants_vs_hemisphere","title":"quadrants_vs_hemisphere: Gridproperty","text":"

    Generates a grid comparing quadrants and hemispheres.

    Returns:

    Type Description Grid

    A grid comparing quadrants and hemispheres.

    "},{"location":"report/#natal.report.Report.signs","title":"signs: Gridproperty","text":"

    Generates a grid of signs and their corresponding bodies.

    Returns:

    Type Description Grid

    A grid of signs and their corresponding bodies

    "},{"location":"report/#natal.report.Report.__init__","title":"__init__(data1: Data, data2: Data | None = None)","text":"

    Initializes the Report with the given data.

    Parameters:

    Name Type Description Default data1Data

    The primary data for the report.

    required data2Data | None

    The secondary data for the report, if any.

    None Source code in natal/report.py
    def __init__(self, data1: Data, data2: Data | None = None):\n    \"\"\"\n    Initializes the Report with the given data.\n\n    Args:\n        data1: The primary data for the report.\n        data2: The secondary data for the report, if any.\n    \"\"\"\n    self.data1: Data = data1\n    self.data2: Data = data2\n
    "},{"location":"report/#natal.report.Report.celestial_body","title":"celestial_body(data: Data) -> Grid","text":"

    Generates a grid of celestial bodies for the given data.

    Parameters:

    Name Type Description Default dataData

    The data for which to generate the grid.

    required

    Returns:

    Type Description Grid

    A grid of celestial bodies for the given data.

    Source code in natal/report.py
    def celestial_body(self, data: Data) -> Grid:\n    \"\"\"\n    Generates a grid of celestial bodies for the given data.\n\n    Args:\n        data: The data for which to generate the grid.\n\n    Returns:\n        A grid of celestial bodies for the given data.\n    \"\"\"\n    grid = [(\"body\", \"sign\", \"house\", \"dignity\")]\n    for body in data.aspectables:\n        grid.append(\n            (\n                svg_of(body.name),\n                f\"{body.signed_deg:02d}\u00b0 {svg_of(body.sign.name)} {body.minute:02d}'\",\n                self.data1.house_of(body),\n                svg_of(dignity_of(body)),\n            )\n        )\n    return grid\n
    "},{"location":"report/#natal.report.Report.create_pdf","title":"create_pdf(html: str) -> BytesIO","text":"

    Creates a PDF from the given HTML string.

    Parameters:

    Name Type Description Default htmlstr

    The HTML string to convert to PDF.

    required

    Returns:

    Type Description BytesIO

    A BytesIO object containing the PDF data.

    Source code in natal/report.py
    def create_pdf(self, html: str) -> BytesIO:\n    \"\"\"\n    Creates a PDF from the given HTML string.\n\n    Args:\n        html: The HTML string to convert to PDF.\n\n    Returns:\n        A BytesIO object containing the PDF data.\n    \"\"\"\n    fp = BytesIO()\n    HTML(string=html).write_pdf(fp)\n    return fp\n
    "},{"location":"report/#natal.report.html_table_of","title":"html_table_of(grid: Grid) -> str","text":"

    Converts a grid of data into an HTML table.

    "},{"location":"report/#natal.report.html_table_of--arguments","title":"Arguments","text":"
    • grid - The grid of data to convert
    "},{"location":"report/#natal.report.html_table_of--returns","title":"Returns","text":"

    String containing the HTML table

    Source code in natal/report.py
    def html_table_of(grid: Grid) -> str:\n    \"\"\"\n    Converts a grid of data into an HTML table.\n\n    # Arguments\n    * grid - The grid of data to convert\n\n    # Returns\n    String containing the HTML table\n    \"\"\"\n    rows = []\n    for row in grid:\n        cells = []\n        for cell in row:\n            if isinstance(cell, str) and cell.startswith(\"null:\"):\n                cells.append(td(cell.split(\":\")[1], colspan=2))\n            else:\n                cells.append(td(cell))\n        rows.append(tr(cells))\n    return table(rows)\n
    "},{"location":"report/#natal.report.section","title":"section(title: str, grid: Grid) -> str","text":"

    Creates an HTML section with a title and a table of data.

    Parameters:

    Name Type Description Default titlestr

    The title of the section.

    required gridGrid

    The grid of data to include in the section.

    required

    Returns:

    Type Description str

    The HTML section as a string.

    Source code in natal/report.py
    def section(title: str, grid: Grid) -> str:\n    \"\"\"\n    Creates an HTML section with a title and a table of data.\n\n    Args:\n        title: The title of the section.\n        grid: The grid of data to include in the section.\n\n    Returns:\n        The HTML section as a string.\n    \"\"\"\n    return div(\n        div(title, class_=\"title\") + html_table_of(grid),\n        class_=\"section\",\n    )\n
    "},{"location":"report/#natal.report.svg_of","title":"svg_of(name: str, scale: float = 0.5) -> str","text":"

    Generates an SVG representation of a given symbol name.

    Parameters:

    Name Type Description Default namestr

    The name of the symbol.

    required scalefloat

    The scale of the SVG. Defaults to 0.5.

    0.5

    Returns:

    Type Description str

    The SVG representation of the symbol.

    Source code in natal/report.py
    def svg_of(name: str, scale: float = 0.5) -> str:\n    \"\"\"\n    Generates an SVG representation of a given symbol name.\n\n    Args:\n        name: The name of the symbol.\n        scale: The scale of the SVG. Defaults to 0.5.\n\n    Returns:\n        The SVG representation of the symbol.\n    \"\"\"\n    if not name:\n        return \"\"\n    stroke = TEXT_COLOR\n    fill = \"none\"\n    if name in [\"mc\", \"asc\", \"dsc\", \"ic\"]:\n        stroke = \"none\"\n        fill = TEXT_COLOR\n\n    return svg(\n        (Path(__file__).parent / \"svg_paths\" / f\"{name}.svg\").read_text(),\n        fill=fill,\n        stroke=stroke,\n        stroke_width=3 * scale,\n        version=\"1.1\",\n        width=f\"{20 * scale}px\",\n        height=f\"{20 * scale}px\",\n        transform=f\"scale({scale})\",\n        xmlns=\"http://www.w3.org/2000/svg\",\n    )\n
    "},{"location":"stats/","title":"Stats","text":""},{"location":"stats/#natal.stats","title":"natal.stats","text":"

    This module provides statistical analysis for natal charts.

    It contains the Stats class, which calculates and presents various astrological statistics for a single natal chart or a comparison between two charts.

    "},{"location":"stats/#natal.stats.StatData","title":"StatData","text":"

    Bases: NamedTuple

    A named tuple representing statistical data with a title and grid.

    Attributes:

    Name Type Description titlestr

    The title of the statistical data.

    gridGrid

    A grid containing the statistical information.

    Source code in natal/stats.py
    class StatData(NamedTuple):\n    \"\"\"\n    A named tuple representing statistical data with a title and grid.\n\n    Attributes:\n        title (str): The title of the statistical data.\n        grid (Grid): A grid containing the statistical information.\n    \"\"\"\n    title: str\n    grid: Grid\n
    "},{"location":"stats/#natal.stats.Stats","title":"Stats","text":"

    Statistics for a natal chart data.

    This class calculates and presents various astrological statistics for a single natal chart or a comparison between two charts.

    Attributes:

    Name Type Description data1Data

    The primary natal chart data.

    data2Data | None

    The secondary natal chart data for comparisons (optional).

    Source code in natal/stats.py
    class Stats:\n    \"\"\"\n    Statistics for a natal chart data.\n\n    This class calculates and presents various astrological statistics for a single natal chart\n    or a comparison between two charts.\n\n    Attributes:\n        data1 (Data): The primary natal chart data.\n        data2 (Data | None): The secondary natal chart data for comparisons (optional).\n    \"\"\"\n\n    data1: Data\n    data2: Data | None = None\n\n    def __init__(self, data1: Data, data2: Data | None = None) -> None:\n        \"\"\"\n        Initialize the Stats object with one or two natal chart data sets.\n\n        Args:\n            data1 (Data): The primary natal chart data.\n            data2 (Data, optional): The secondary natal chart data for comparisons. Defaults to None.\n        \"\"\"\n        self.data1 = data1\n        self.data2 = data2\n        if self.data2:\n            self.composite_pairs = data2.composite_aspects_pairs(self.data1)\n            self.composite_aspects = data1.calculate_aspects(self.composite_pairs)\n\n    # data grids =================================================================\n\n    def distribution(self, kind: DistKind) -> StatData:\n        \"\"\"\n        Generate distribution statistics for elements, qualities, or polarities.\n\n        Args:\n            kind (DistKind): The type of distribution to calculate. \n                Must be one of \"element\", \"quality\", or \"polarity\".\n\n        Returns:\n            StatData: A named tuple containing the title and grid of distribution data, \n                      where the grid includes the distribution type, count, and bodies.\n        \"\"\"\n        title = f\"{kind.capitalize()} Distribution ({self.data1.name})\"\n        bodies = defaultdict(lambda: [0, []])\n        for body in self.data1.aspectables:\n            key = body.sign[kind]\n            bodies[key][0] += 1  # act as a counter\n            bodies[key][1].append(f\"{body.name} {body.sign.symbol}\")\n        grid = [(kind, \"sum\", \"bodies\")]\n        data = [(key, val[0], \", \".join(val[1])) for key, val in bodies.items()]\n        grid.extend(data)\n        return StatData(title, grid)\n\n    @property\n    def celestial_body(self) -> StatData:\n        \"\"\"\n        Generate a grid of celestial body positions for the primary chart.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of celestial body data, \n                      where the grid includes body name, sign, house, and dignity.\n        \"\"\"\n        title = f\"Celestial Bodies ({self.data1.name})\"\n        grid = [(\"body\", \"sign\", \"house\", \"dignity\")]\n        for body in self.data1.aspectables:\n            grid.append(\n                (\n                    body.name,\n                    body.signed_dms,\n                    self.data1.house_of(body),\n                    dignity_of(body),\n                )\n            )\n        return StatData(title, grid)\n\n    @property\n    def data2_celestial_body(self) -> StatData:\n        \"\"\"\n        Generate a grid of celestial body positions for the secondary chart.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of celestial body data \n                      for the secondary chart, showing its bodies in the primary chart's context.\n                      The grid includes body name, sign, house, and dignity.\n\n        Raises:\n            AttributeError: If no secondary chart (data2) is available.\n        \"\"\"\n        if not self.data2:\n            raise AttributeError(\"No secondary chart available\")\n\n        title = f\"Celestial Bodies of {self.data2.name} in {self.data1.name}'s chart\"\n        grid = [(self.data2.name, \"sign\", \"house\", \"dignity\")]\n        for body in self.data2.aspectables:\n            grid.append(\n                (\n                    body.name,\n                    body.signed_dms,\n                    self.data1.house_of(body),\n                    dignity_of(body),\n                )\n            )\n        return StatData(title, grid)\n\n    @property\n    def house(self) -> StatData:\n        \"\"\"\n        Generate a grid of house data for the primary chart.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of house data, \n                      where the grid includes house number, cusp, ruler, ruler sign, and ruler house.\n        \"\"\"\n        title = f\"Houses ({self.data1.name})\"\n        grid = [(\"house\", \"cusp\", \"ruler\", \"ruler sign\", \"ruler house\")]\n        for house in self.data1.houses:\n            grid.append(\n                (\n                    house.value,\n                    house.signed_dms,\n                    house.ruler,\n                    house.ruler_sign,\n                    house.ruler_house,\n                )\n            )\n        return StatData(title, grid)\n\n    @property\n    def quadrant(self) -> StatData:\n        \"\"\"\n        Generate a grid of celestial body distribution in quadrants.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of quadrant distribution data, \n                      where the grid includes quadrant name, body count, and body names.\n        \"\"\"\n        title = f\"Quadrants ({self.data1.name})\"\n        quad_names = [\"1st \u25f5\", \"2nd \u25f6\", \"3rd \u25f7\", \"4th \u25f4\"]\n        quadrants = defaultdict(lambda: [0, []])\n        for i, quad in enumerate(self.data1.quadrants):\n            if quad:\n                for body in quad:\n                    quadrants[i][0] += 1  # act as a counter\n                    quadrants[i][1].append(f\"{body.name}\")\n            else:\n                # no celestial body in this quadrant\n                quadrants[i][0] = 0\n        grid = [(\"quadrant\", \"sum\", \"bodies\")]\n        data = [\n            (quad_names[quad_no], val[0], \", \".join(val[1]))\n            for quad_no, val in quadrants.items()\n        ]\n        return StatData(title, grid + data)\n\n    @property\n    def hemisphere(self) -> StatData:\n        \"\"\"\n        Generate a grid of celestial body distribution in hemispheres.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of hemisphere distribution data, \n                      where the grid includes hemisphere direction, body count, and body names.\n        \"\"\"\n        title = f\"Hemispheres ({self.data1.name})\"\n        grid = [(\"hemisphere\", \"sum\", \"bodies\")]\n        data = self.quadrant.grid[1:]\n        formatter: Callable[[int, int], str] = lambda a, b: (data[a][2] + \", \" + data[b][2]).strip(\" ,\")\n        left = (\"\u2190\", data[0][1] + data[3][1], formatter(0, 3))\n        right = (\"\u2192\", data[1][1] + data[2][1], formatter(1, 2))\n        top = (\"\u2191\", data[2][1] + data[3][1], formatter(2, 3))\n        bottom = (\"\u2193\", data[0][1] + data[1][1], formatter(0, 1))\n        return StatData(title, grid + [left, right, top, bottom])\n\n    @property\n    def aspect(self) -> StatData:\n        \"\"\"\n        Generate a grid of aspects for the primary chart.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of aspect data, \n                      where the grid includes body 1, aspect type, body 2, phase, and orb.\n        \"\"\"\n        title = f\"Aspects ({self.data1.name})\"\n        headers = [\"body 1\", \"aspect\", \"body 2\", \"phase\", \"orb\"]\n        return StatData(title, _aspect_grid(self.data1.aspects, headers))\n\n    @property\n    def composite_aspect(self) -> StatData:\n        \"\"\"\n        Generate a grid of composite aspects between two charts.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of composite aspect data, \n                      where the grid includes body names from both charts, aspect type, phase, and orb.\n\n        Raises:\n            AttributeError: If no secondary chart (data2) is available.\n        \"\"\"\n        if not self.data2:\n            raise AttributeError(\"No secondary chart available for composite aspects\")\n\n        title = f\"Aspects of {self.data2.name} vs {self.data1.name}\"\n        headers = [self.data2.name, \"aspect\", self.data1.name, \"phase\", \"orb\"]\n        return StatData(title, _aspect_grid(self.composite_aspects, headers))\n\n    @property\n    def cross_ref(self) -> StatData:\n        \"\"\"\n        Generate a grid for aspect cross-reference between charts or within a single chart.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of aspect cross-reference data, \n                      where the grid shows aspect connections between bodies, with a sum column.\n        \"\"\"\n        name = (\n            f\"{self.data2.name}(cols) vs {self.data1.name}(rows)\"\n            if self.data2\n            else self.data1.name\n        )\n        title = f\"Aspect Cross Reference of {name}\"\n        aspectable1 = self.data1.aspectables\n        aspectable2 = self.data2.aspectables if self.data2 else self.data1.aspectables\n        aspects = self.composite_aspects if self.data2 else self.data1.aspects\n        body_symbols = [body.symbol for body in aspectable2]\n        grid: list[list[str]] = [[\"\"] + body_symbols + [\"sum\"]]\n        for body1 in aspectable1:\n            row = [body1.symbol]\n            aspect_count = 0\n            for body2 in aspectable2:\n                aspect = next(\n                    (\n                        asp\n                        for asp in aspects\n                        if (asp.body1 == body1 and asp.body2 == body2)\n                        or (asp.body1 == body2 and asp.body2 == body1)\n                    ),\n                    None,\n                )\n                if aspect:\n                    row.append(aspect.aspect_member.symbol)\n                    aspect_count += 1\n                else:\n                    row.append(\"\")\n\n            row.append(str(aspect_count))  # Add sum to the end of the row\n            grid.append(row)\n        return StatData(title, grid)\n\n    def full_report(self, kind: ReportKind) -> str:\n        \"\"\"\n        Generate a full report containing all statistical tables.\n\n        Args:\n            kind (ReportKind): The format of the report, either \"markdown\" or \"html\".\n\n        Returns:\n            str: A formatted string containing the full statistical report with various tables.\n        \"\"\"\n        output = \"\\n\"\n        for dist in DistKind.__args__:\n            output += self.table_of(\"distribution\", kind, dist)\n        output += self.table_of(\"celestial_body\", kind)\n        output += self.table_of(\"house\", kind)\n        output += self.table_of(\"quadrant\", kind)\n        output += self.table_of(\"hemisphere\", kind)\n        if self.data2:\n            output += self.table_of(\"data2_celestial_body\", kind)\n            output += self.table_of(\n                \"composite_aspect\", kind, colalign=(\"left\", \"center\", \"left\", \"center\")\n            )\n        else:\n            output += self.table_of(\"aspect\", kind)\n        output += self.table_of(\"cross_ref\", kind, stralign=\"center\")\n        return output\n\n    def table_of(\n        self, \n        fn_name: str, \n        kind: ReportKind, \n        *fn_args: object, \n        **markdown_options: object\n    ) -> str:\n        \"\"\"\n        Format a table with a title.\n\n        Args:\n            fn_name (str): The name of the method to call (e.g., \"distribution\", \"celestial_body\").\n            kind (ReportKind): The kind of report to generate (\"markdown\" or \"html\").\n            *fn_args: Variable positional arguments passed to the method.\n            **markdown_options: Additional keyword arguments for tabulate formatting.\n\n        Returns:\n            str: A formatted string containing the titled table in the specified format.\n        \"\"\"\n        stat = getattr(self, fn_name)\n        if fn_args:\n            stat = stat(*fn_args)\n        base_option = dict(headers=\"firstrow\", numalign=\"center\")\n\n        if kind == \"markdown\":\n            options = base_option | {\"tablefmt\": \"github\"} | markdown_options\n            output = f\"# {stat.title}\\n\\n\"\n            output += tabulate(stat.grid, **options)\n            output += \"\\n\\n\\n\"\n            return output\n        elif kind == \"html\":\n            options = base_option | {\"tablefmt\": \"html\"}\n            tb = tabulate(stat.grid, **options)\n            output = div([h4(stat.title), tb], class_=f\"tabulate {fn_name}\")\n            return str(output)\n
    "},{"location":"stats/#natal.stats.Stats.aspect","title":"aspect: StatDataproperty","text":"

    Generate a grid of aspects for the primary chart.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of aspect data, where the grid includes body 1, aspect type, body 2, phase, and orb.

    "},{"location":"stats/#natal.stats.Stats.celestial_body","title":"celestial_body: StatDataproperty","text":"

    Generate a grid of celestial body positions for the primary chart.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of celestial body data, where the grid includes body name, sign, house, and dignity.

    "},{"location":"stats/#natal.stats.Stats.composite_aspect","title":"composite_aspect: StatDataproperty","text":"

    Generate a grid of composite aspects between two charts.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of composite aspect data, where the grid includes body names from both charts, aspect type, phase, and orb.

    Raises:

    Type Description AttributeError

    If no secondary chart (data2) is available.

    "},{"location":"stats/#natal.stats.Stats.cross_ref","title":"cross_ref: StatDataproperty","text":"

    Generate a grid for aspect cross-reference between charts or within a single chart.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of aspect cross-reference data, where the grid shows aspect connections between bodies, with a sum column.

    "},{"location":"stats/#natal.stats.Stats.data2_celestial_body","title":"data2_celestial_body: StatDataproperty","text":"

    Generate a grid of celestial body positions for the secondary chart.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of celestial body data for the secondary chart, showing its bodies in the primary chart's context. The grid includes body name, sign, house, and dignity.

    Raises:

    Type Description AttributeError

    If no secondary chart (data2) is available.

    "},{"location":"stats/#natal.stats.Stats.hemisphere","title":"hemisphere: StatDataproperty","text":"

    Generate a grid of celestial body distribution in hemispheres.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of hemisphere distribution data, where the grid includes hemisphere direction, body count, and body names.

    "},{"location":"stats/#natal.stats.Stats.house","title":"house: StatDataproperty","text":"

    Generate a grid of house data for the primary chart.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of house data, where the grid includes house number, cusp, ruler, ruler sign, and ruler house.

    "},{"location":"stats/#natal.stats.Stats.quadrant","title":"quadrant: StatDataproperty","text":"

    Generate a grid of celestial body distribution in quadrants.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of quadrant distribution data, where the grid includes quadrant name, body count, and body names.

    "},{"location":"stats/#natal.stats.Stats.__init__","title":"__init__(data1: Data, data2: Data | None = None) -> None","text":"

    Initialize the Stats object with one or two natal chart data sets.

    Parameters:

    Name Type Description Default data1Data

    The primary natal chart data.

    required data2Data

    The secondary natal chart data for comparisons. Defaults to None.

    None Source code in natal/stats.py
    def __init__(self, data1: Data, data2: Data | None = None) -> None:\n    \"\"\"\n    Initialize the Stats object with one or two natal chart data sets.\n\n    Args:\n        data1 (Data): The primary natal chart data.\n        data2 (Data, optional): The secondary natal chart data for comparisons. Defaults to None.\n    \"\"\"\n    self.data1 = data1\n    self.data2 = data2\n    if self.data2:\n        self.composite_pairs = data2.composite_aspects_pairs(self.data1)\n        self.composite_aspects = data1.calculate_aspects(self.composite_pairs)\n
    "},{"location":"stats/#natal.stats.Stats.distribution","title":"distribution(kind: DistKind) -> StatData","text":"

    Generate distribution statistics for elements, qualities, or polarities.

    Parameters:

    Name Type Description Default kindDistKind

    The type of distribution to calculate. Must be one of \"element\", \"quality\", or \"polarity\".

    required

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of distribution data, where the grid includes the distribution type, count, and bodies.

    Source code in natal/stats.py
    def distribution(self, kind: DistKind) -> StatData:\n    \"\"\"\n    Generate distribution statistics for elements, qualities, or polarities.\n\n    Args:\n        kind (DistKind): The type of distribution to calculate. \n            Must be one of \"element\", \"quality\", or \"polarity\".\n\n    Returns:\n        StatData: A named tuple containing the title and grid of distribution data, \n                  where the grid includes the distribution type, count, and bodies.\n    \"\"\"\n    title = f\"{kind.capitalize()} Distribution ({self.data1.name})\"\n    bodies = defaultdict(lambda: [0, []])\n    for body in self.data1.aspectables:\n        key = body.sign[kind]\n        bodies[key][0] += 1  # act as a counter\n        bodies[key][1].append(f\"{body.name} {body.sign.symbol}\")\n    grid = [(kind, \"sum\", \"bodies\")]\n    data = [(key, val[0], \", \".join(val[1])) for key, val in bodies.items()]\n    grid.extend(data)\n    return StatData(title, grid)\n
    "},{"location":"stats/#natal.stats.Stats.full_report","title":"full_report(kind: ReportKind) -> str","text":"

    Generate a full report containing all statistical tables.

    Parameters:

    Name Type Description Default kindReportKind

    The format of the report, either \"markdown\" or \"html\".

    required

    Returns:

    Name Type Description strstr

    A formatted string containing the full statistical report with various tables.

    Source code in natal/stats.py
    def full_report(self, kind: ReportKind) -> str:\n    \"\"\"\n    Generate a full report containing all statistical tables.\n\n    Args:\n        kind (ReportKind): The format of the report, either \"markdown\" or \"html\".\n\n    Returns:\n        str: A formatted string containing the full statistical report with various tables.\n    \"\"\"\n    output = \"\\n\"\n    for dist in DistKind.__args__:\n        output += self.table_of(\"distribution\", kind, dist)\n    output += self.table_of(\"celestial_body\", kind)\n    output += self.table_of(\"house\", kind)\n    output += self.table_of(\"quadrant\", kind)\n    output += self.table_of(\"hemisphere\", kind)\n    if self.data2:\n        output += self.table_of(\"data2_celestial_body\", kind)\n        output += self.table_of(\n            \"composite_aspect\", kind, colalign=(\"left\", \"center\", \"left\", \"center\")\n        )\n    else:\n        output += self.table_of(\"aspect\", kind)\n    output += self.table_of(\"cross_ref\", kind, stralign=\"center\")\n    return output\n
    "},{"location":"stats/#natal.stats.Stats.table_of","title":"table_of(fn_name: str, kind: ReportKind, *fn_args: object, **markdown_options: object) -> str","text":"

    Format a table with a title.

    Parameters:

    Name Type Description Default fn_namestr

    The name of the method to call (e.g., \"distribution\", \"celestial_body\").

    required kindReportKind

    The kind of report to generate (\"markdown\" or \"html\").

    required *fn_argsobject

    Variable positional arguments passed to the method.

    ()**markdown_optionsobject

    Additional keyword arguments for tabulate formatting.

    {}

    Returns:

    Name Type Description strstr

    A formatted string containing the titled table in the specified format.

    Source code in natal/stats.py
    def table_of(\n    self, \n    fn_name: str, \n    kind: ReportKind, \n    *fn_args: object, \n    **markdown_options: object\n) -> str:\n    \"\"\"\n    Format a table with a title.\n\n    Args:\n        fn_name (str): The name of the method to call (e.g., \"distribution\", \"celestial_body\").\n        kind (ReportKind): The kind of report to generate (\"markdown\" or \"html\").\n        *fn_args: Variable positional arguments passed to the method.\n        **markdown_options: Additional keyword arguments for tabulate formatting.\n\n    Returns:\n        str: A formatted string containing the titled table in the specified format.\n    \"\"\"\n    stat = getattr(self, fn_name)\n    if fn_args:\n        stat = stat(*fn_args)\n    base_option = dict(headers=\"firstrow\", numalign=\"center\")\n\n    if kind == \"markdown\":\n        options = base_option | {\"tablefmt\": \"github\"} | markdown_options\n        output = f\"# {stat.title}\\n\\n\"\n        output += tabulate(stat.grid, **options)\n        output += \"\\n\\n\\n\"\n        return output\n    elif kind == \"html\":\n        options = base_option | {\"tablefmt\": \"html\"}\n        tb = tabulate(stat.grid, **options)\n        output = div([h4(stat.title), tb], class_=f\"tabulate {fn_name}\")\n        return str(output)\n
    "},{"location":"stats/#natal.stats.dignity_of","title":"dignity_of(body: Aspectable) -> str","text":"

    Get the dignity of a celestial body.

    Parameters:

    Name Type Description Default bodyAspectable

    The celestial body to check for dignity.

    required

    Returns:

    Name Type Description strstr

    The dignity of the celestial body. Possible values are \"domicile\", \"detriment\", \"exaltation\", \"fall\", or an empty string.

    Source code in natal/stats.py
    def dignity_of(body: Aspectable) -> str:\n    \"\"\"\n    Get the dignity of a celestial body.\n\n    Args:\n        body (Aspectable): The celestial body to check for dignity.\n\n    Returns:\n        str: The dignity of the celestial body. \n             Possible values are \"domicile\", \"detriment\", \"exaltation\", \"fall\", or an empty string.\n    \"\"\"\n    if body.name == (body.sign.classic_ruler or body.sign.ruler):\n        return \"domicile\"\n    if body.name == (body.sign.classic_detriment or body.sign.detriment):\n        return \"detriment\"\n    if body.name == body.sign.exaltation:\n        return \"exaltation\"\n    if body.name == body.sign.fall:\n        return \"fall\"\n    return \"\"\n
    "},{"location":"utils/","title":"Utils","text":""},{"location":"utils/#natal.utils","title":"natal.utils","text":"

    utility functions for natal

    "},{"location":"utils/#natal.utils.color_hex","title":"color_hex(name: str, config: Config = Config()) -> str","text":"

    Get color hex code from name and config.

    Parameters:

    Name Type Description Default namestr

    Color name to look up

    required configConfig

    Config containing color definitions

    Config()

    Returns:

    Name Type Description strstr

    Hex color code string

    Source code in natal/utils.py
    def color_hex(name: str, config: Config = Config()) -> str:\n    \"\"\"Get color hex code from name and config.\n\n    Args:\n        name (str): Color name to look up\n        config (Config): Config containing color definitions\n\n    Returns:\n        str: Hex color code string\n    \"\"\"\n    return getattr(config.colors, name)\n
    "},{"location":"utils/#natal.utils.member_of","title":"member_of(const: list[T], name: str) -> T","text":"

    Get a member from a list of constants by name.

    Parameters:

    Name Type Description Default constlist[T]

    List of constant definitions

    required namestr

    Name to look up

    required

    Returns:

    Name Type Description TT

    Matching constant member

    Source code in natal/utils.py
    def member_of[T](const: list[T], name: str) -> T:\n    \"\"\"Get a member from a list of constants by name.\n\n    Args:\n        const (list[T]): List of constant definitions\n        name (str): Name to look up\n\n    Returns:\n        T: Matching constant member\n    \"\"\"\n    idx: int = const[\"name\"].index(name)\n    return {prop: const[prop][idx] for prop in const.model_fields}\n
    "},{"location":"utils/#natal.utils.pairs","title":"pairs(iterable: Iterable[T]) -> list[tuple[T, T]]","text":"

    Generate unique pairs of elements from an iterable.

    Parameters:

    Name Type Description Default iterableIterable[T]

    Source of elements to pair

    required

    Returns:

    Type Description list[tuple[T, T]]

    list[tuple[T, T]]: List of element pairs as tuples

    Source code in natal/utils.py
    def pairs[T](iterable: Iterable[T]) -> list[tuple[T, T]]:\n    \"\"\"Generate unique pairs of elements from an iterable.\n\n    Args:\n        iterable (Iterable[T]): Source of elements to pair\n\n    Returns:\n        list[tuple[T, T]]: List of element pairs as tuples\n    \"\"\"\n    output = []\n    for i in range(len(iterable)):\n        for j in range(i + 1, len(iterable)):\n            output.append((iterable[i], iterable[j]))\n    return output\n
    "},{"location":"utils/#natal.utils.str_to_dt","title":"str_to_dt(dt_str: str) -> datetime","text":"

    Convert string to datetime object.

    Parameters:

    Name Type Description Default dt_strstr

    Datetime string in format \"YYYY-MM-DD HH:MM\"

    required

    Returns:

    Name Type Description datetimedatetime

    Parsed datetime object

    Source code in natal/utils.py
    def str_to_dt(dt_str: str) -> datetime:\n    \"\"\"Convert string to datetime object.\n\n    Args:\n        dt_str (str): Datetime string in format \"YYYY-MM-DD HH:MM\"\n\n    Returns:\n        datetime: Parsed datetime object\n    \"\"\"\n    return datetime.strptime(dt_str, \"%Y-%m-%d %H:%M\")\n
    "}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Natal","text":"

    create Natal Chart with ease

    "},{"location":"#features","title":"Features","text":"
    • SVG natal chart generation in pure python
    • composite chart (transit/synastry/sun return ... etc) generation
    • highly configurable
      • all planets, asteroids, vertices can be enabled / disabled
      • orbs for each aspect
      • light, dark, or mono theme
      • light / dark theme color definitions
      • chart stroke, opacity, font, spacing between planets ...etc
    • high precision astrological data with Swiss Ephemeris
    • timezone, latitude and longitude database from GeoNames
      • auto aware of daylight saving for a given time and location
    • natal chart data statistics
      • element, modality, and polarity counts
      • planets in each houses
      • quadrant and hemisphere distribution
      • aspect pair counts
      • composite chart aspects
      • aspects cross reference table
      • generate report in markdown or html
    • thoroughly tested with pytest
    "},{"location":"#sample-charts","title":"Sample Charts","text":"
    • default dark theme
    • default light theme
    • mono theme
    "},{"location":"#quick-start","title":"Quick Start","text":"
    • installation

    pip install natal

    • generate a chart
    from natal import Data, Chart\n\n# create chart data object\nmimi = Data(\n  name = \"MiMi\",\n  city = \"Taipei\",\n  dt = \"1980-04-20 14:30\"\n)\n\n# return natal chart in SVG string\nChart(mimi, width=600).svg\n\n# create transit data object\ncurrent = Data(\n  name = \"Current\",\n  city = \"Taipei\",\n  dt = datetime.now()\n)\n\n# create a transit chart\ntransit_chart = Chart(\n    data1=mimi, \n    data2=current, \n    width=600\n)\n\n# view the composite chart in jupyter notebook\nfrom IPython.display import HTML\n\nHTML(transit_chart.svg)\n

    following SVG chart will be produced:

    "},{"location":"#data-object","title":"Data Object","text":"
    ## -- retrieve natal chart properties -- ##\n\nmimi.planets     # list[Planet]\nmimi.houses      # list[House]\nmimi.extras      # list[Extra]\nmimi.vertices    # list[Vertex]\nmimi.signs       # list[Sign]\nmimi.aspects     # list[Aspect]\nmimi.quadrants   # list[list[Aspectable]]\n\n# Planet object \nsun = mimi.planets[0]\n\nsun.degree # 30.33039116987769\nsun.normalized_degree # 230.62043431588035 # degree relative to Asc\nsun.color # fire\nsun.speed # 0.9761994105153413\nsun.retro # False\nsun.dms # 00\u00b019'\nsun.signed_dms # 00\u00b0\u264919'\nsun.signed_deg # 0\nsun.sign.name # taurus\nsun.sign.symbol # \u2649\nsun.sign.value # 2\nsun.sign.color # earth\nsun.sign.ruler # venus\nsun.sign.classic_ruler # venus\nsun.sign.element # earth\nsun.sign.modality # fixed\nsun.sign.polarity # negative\n\n# Aspect object\naspect = mimi.aspects[0]\n\naspect.body1 # sun Planet obj \naspect.body2 # mars Planet obj\naspect.aspect_member # AspectMember(name='trine', symbol='\u25b3', value=120, color='air')\naspect.applying # False\naspect.orb # 3.3424\n
    "},{"location":"#stats","title":"Stats","text":"
    • statistics of Data object in tabular form
    from natal import Data, Stats\n\nmimi = Data(\n    name = \"MiMi\",\n    city = \"Taipei\",\n    dt = \"1980-04-20 14:30\"\n)\n\ntransit = Data(\n    name = \"Transit\",\n    city = \"Taipei\",\n    dt = \"2024-10-10 12:00\"\n)\n\nstats = Stats(data1=mimi, data2=transit)\n\nprint(stats.full_report(kind=\"markdown\"))\n
    • following markdown report will be produced:
    # Element Distribution (MiMi)\n\n| element   |  count  | bodies                                       |\n|-----------|---------|----------------------------------------------|\n| earth     |    4    | sun \u2649, jupiter \u264d, saturn \u264d, asc \u264d        |\n| water     |    2    | moon \u264b, uranus \u264f                           |\n| fire      |    4    | mercury \u2648, mars \u264c, neptune \u2650, asc_node \u264c |\n| air       |    3    | venus \u264a, pluto \u264e, mc \u264a                    |\n\n\n# Modality Distribution (MiMi)\n\n| modality  |  count  | bodies                                                     |\n|-----------|---------|------------------------------------------------------------|\n| fixed     |    4    | sun \u2649, mars \u264c, uranus \u264f, asc_node \u264c                    |\n| cardinal  |    3    | moon \u264b, mercury \u2648, pluto \u264e                              |\n| mutable   |    6    | venus \u264a, jupiter \u264d, saturn \u264d, neptune \u2650, asc \u264d, mc \u264a |\n\n\n# Polarity Distribution (MiMi)\n\n| polarity   |  count  | bodies                                                                  |\n|------------|---------|-------------------------------------------------------------------------|\n| negative   |    6    | sun \u2649, moon \u264b, jupiter \u264d, saturn \u264d, uranus \u264f, asc \u264d               |\n| positive   |    7    | mercury \u2648, venus \u264a, mars \u264c, neptune \u2650, pluto \u264e, asc_node \u264c, mc \u264a |\n\n\n# Celestial Bodies (MiMi)\n\n| body     | sign      |  house  |\n|----------|-----------|---------|\n| sun      | 00\u00b0\u264919'  |    8    |\n| moon     | 08\u00b0\u264b29'  |   10    |\n| mercury  | 08\u00b0\u264828'  |    8    |\n| venus    | 15\u00b0\u264a12'  |   10    |\n| mars     | 26\u00b0\u264c59'  |   12    |\n| jupiter  | 00\u00b0\u264d17'\u211e |   12    |\n| saturn   | 21\u00b0\u264d03'\u211e |    1    |\n| uranus   | 24\u00b0\u264f31'\u211e |    3    |\n| neptune  | 22\u00b0\u265029'\u211e |    4    |\n| pluto    | 20\u00b0\u264e06'\u211e |    2    |\n| asc_node | 26\u00b0\u264c03'\u211e |   12    |\n| asc      | 09\u00b0\u264d42'  |    1    |\n| mc       | 09\u00b0\u264a13'  |   10    |\n\n\n# Houses (MiMi)\n\n|  house  | sign     | ruler   | ruler sign   |  ruler house  |\n|---------|----------|---------|--------------|---------------|\n|    1    | 09\u00b0\u264d41' | mercury | \u2648           |       8       |\n|    2    | 07\u00b0\u264e13' | venus   | \u264a           |      10       |\n|    3    | 07\u00b0\u264f38' | pluto   | \u264e           |       2       |\n|    4    | 09\u00b0\u265013' | jupiter | \u264d           |      12       |\n|    5    | 10\u00b0\u265125' | saturn  | \u264d           |       1       |\n|    6    | 10\u00b0\u265244' | uranus  | \u264f           |       3       |\n|    7    | 09\u00b0\u265341' | neptune | \u2650           |       4       |\n|    8    | 07\u00b0\u264813' | mars    | \u264c           |      12       |\n|    9    | 07\u00b0\u264938' | venus   | \u264a           |      10       |\n|   10    | 09\u00b0\u264a13' | mercury | \u2648           |       8       |\n|   11    | 10\u00b0\u264b25' | moon    | \u264b           |      10       |\n|   12    | 10\u00b0\u264c44' | sun     | \u2649           |       8       |\n\n\n# Quadrants (MiMi)\n\n| quadrant   |  count  | bodies                               |\n|------------|---------|--------------------------------------|\n| 1st \u25f5      |    3    | saturn, uranus, pluto                |\n| 2nd \u25f6      |    1    | neptune                              |\n| 3rd \u25f7      |    2    | sun, mercury                         |\n| 4th \u25f4      |    5    | moon, venus, mars, jupiter, asc_node |\n\n\n# Hemispheres (MiMi)\n\n| hemisphere   |  count  | bodies                                                      |\n|--------------|---------|-------------------------------------------------------------|\n| \u2190            |    8    | saturn, uranus, pluto, moon, venus, mars, jupiter, asc_node |\n| \u2192            |    3    | neptune, sun, mercury                                       |\n| \u2191            |    7    | sun, mercury, moon, venus, mars, jupiter, asc_node          |\n| \u2193            |    4    | saturn, uranus, pluto, neptune                              |\n\n\n# Celestial Bodies of Transit in MiMi's chart\n\n| Transit   | sign      |  house  |\n|-----------|-----------|---------|\n| sun       | 17\u00b0\u264e20'  |    2    |\n| moon      | 09\u00b0\u265149'  |    4    |\n| mercury   | 24\u00b0\u264e04'  |    2    |\n| venus     | 20\u00b0\u264f44'  |    3    |\n| mars      | 19\u00b0\u264b29'  |   11    |\n| jupiter   | 21\u00b0\u264a20'\u211e |   10    |\n| saturn    | 13\u00b0\u265347'\u211e |    7    |\n| uranus    | 26\u00b0\u264939'\u211e |    9    |\n| neptune   | 27\u00b0\u265359'\u211e |    7    |\n| pluto     | 29\u00b0\u265138'\u211e |    5    |\n| asc_node  | 05\u00b0\u264852'\u211e |    7    |\n| asc       | 08\u00b0\u265129'  |    4    |\n| mc        | 22\u00b0\u264e28'  |    2    |\n\n\n# Aspects of Transit vs MiMi\n\n| Transit   |  aspect  | MiMi     |  phase  | orb    |\n|-----------|----------|----------|---------|--------|\n| sun       |    \u25b3     | venus    |   \u2190 \u2192   | 2\u00b0 08' |\n| sun       |    \u260c     | pluto    |   \u2192 \u2190   | 2\u00b0 46' |\n| moon      |    \u260d     | moon     |   \u2192 \u2190   | 1\u00b0 20' |\n| moon      |    \u25a1     | mercury  |   \u2190 \u2192   | 1\u00b0 21' |\n| moon      |    \u25b3     | asc      |   \u2190 \u2192   | 0\u00b0 07' |\n| mercury   |    \u26b9     | mars     |   \u2192 \u2190   | 2\u00b0 55' |\n| mercury   |    \u26b9     | neptune  |   \u2190 \u2192   | 1\u00b0 35' |\n| mercury   |    \u260c     | pluto    |   \u2190 \u2192   | 3\u00b0 58' |\n| mercury   |    \u26b9     | asc_node |   \u2192 \u2190   | 1\u00b0 59' |\n| venus     |    \u26b9     | saturn   |   \u2192 \u2190   | 0\u00b0 19' |\n| venus     |    \u260c     | uranus   |   \u2192 \u2190   | 3\u00b0 47' |\n| venus     |    \u25a1     | asc_node |   \u2192 \u2190   | 5\u00b0 19' |\n| mars      |    \u26b9     | saturn   |   \u2192 \u2190   | 1\u00b0 34' |\n| mars      |    \u25b3     | uranus   |   \u2192 \u2190   | 5\u00b0 02' |\n| mars      |    \u25a1     | pluto    |   \u2192 \u2190   | 0\u00b0 38' |\n| jupiter   |    \u260c     | venus    |   \u2192 \u2190   | 6\u00b0 08' |\n| jupiter   |    \u25a1     | saturn   |   \u2190 \u2192   | 0\u00b0 17' |\n| jupiter   |    \u260d     | neptune  |   \u2192 \u2190   | 1\u00b0 09' |\n| jupiter   |    \u25b3     | pluto    |   \u2190 \u2192   | 1\u00b0 13' |\n| jupiter   |    \u26b9     | asc_node |   \u2192 \u2190   | 4\u00b0 43' |\n| saturn    |    \u25b3     | moon     |   \u2192 \u2190   | 5\u00b0 18' |\n| saturn    |    \u25a1     | venus    |   \u2190 \u2192   | 1\u00b0 25' |\n| saturn    |    \u260d     | asc      |   \u2192 \u2190   | 4\u00b0 05' |\n| saturn    |    \u25a1     | mc       |   \u2192 \u2190   | 4\u00b0 34' |\n| uranus    |    \u25a1     | mars     |   \u2190 \u2192   | 0\u00b0 20' |\n| uranus    |    \u25a1     | jupiter  |   \u2190 \u2192   | 3\u00b0 38' |\n| uranus    |    \u25b3     | saturn   |   \u2190 \u2192   | 5\u00b0 36' |\n| uranus    |    \u260d     | uranus   |   \u2190 \u2192   | 2\u00b0 08' |\n| uranus    |    \u25a1     | asc_node |   \u2190 \u2192   | 0\u00b0 36' |\n| neptune   |    \u25b3     | uranus   |   \u2190 \u2192   | 3\u00b0 28' |\n| neptune   |    \u25a1     | neptune  |   \u2192 \u2190   | 5\u00b0 31' |\n| pluto     |    \u25a1     | sun      |   \u2190 \u2192   | 0\u00b0 41' |\n| asc_node  |    \u25a1     | moon     |   \u2190 \u2192   | 2\u00b0 36' |\n| asc_node  |    \u260c     | mercury  |   \u2190 \u2192   | 2\u00b0 35' |\n| asc_node  |    \u26b9     | mc       |   \u2190 \u2192   | 3\u00b0 20' |\n| asc       |    \u260d     | moon     |   \u2192 \u2190   | 0\u00b0 01' |\n| asc       |    \u25a1     | mercury  |   \u2192 \u2190   | 0\u00b0 02' |\n| asc       |    \u25b3     | asc      |   \u2192 \u2190   | 1\u00b0 13' |\n| mc        |    \u26b9     | mars     |   \u2190 \u2192   | 4\u00b0 31' |\n| mc        |    \u26b9     | neptune  |   \u2192 \u2190   | 0\u00b0 01' |\n| mc        |    \u260c     | pluto    |   \u2190 \u2192   | 2\u00b0 22' |\n| mc        |    \u26b9     | asc_node |   \u2192 \u2190   | 3\u00b0 35' |\n\n\n# Aspect Cross Reference of Transit(cols) vs MiMi(rows)\n\n|     |  \u2609  |  \u263d  |  \u263f  |  \u2640  |  \u2642  |  \u2643  |  \u2644  |  \u2645  |  \u2646  |  \u2647  |  \u260a  |  Asc  |  MC  |  Total  |\n|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-------|------|---------|\n|  \u2609  |     |     |     |     |     |     |     |     |     |  \u25a1  |     |       |      |    1    |\n|  \u263d  |     |  \u260d  |     |     |     |     |  \u25b3  |     |     |     |  \u25a1  |   \u260d   |      |    4    |\n|  \u263f  |     |  \u25a1  |     |     |     |     |     |     |     |     |  \u260c  |   \u25a1   |      |    3    |\n|  \u2640  |  \u25b3  |     |     |     |     |  \u260c  |  \u25a1  |     |     |     |     |       |      |    3    |\n|  \u2642  |     |     |  \u26b9  |     |     |     |     |  \u25a1  |     |     |     |       |  \u26b9   |    3    |\n|  \u2643  |     |     |     |     |     |     |     |  \u25a1  |     |     |     |       |      |    1    |\n|  \u2644  |     |     |     |  \u26b9  |  \u26b9  |  \u25a1  |     |  \u25b3  |     |     |     |       |      |    4    |\n|  \u2645  |     |     |     |  \u260c  |  \u25b3  |     |     |  \u260d  |  \u25b3  |     |     |       |      |    4    |\n|  \u2646  |     |     |  \u26b9  |     |     |  \u260d  |     |     |  \u25a1  |     |     |       |  \u26b9   |    4    |\n|  \u2647  |  \u260c  |     |  \u260c  |     |  \u25a1  |  \u25b3  |     |     |     |     |     |       |  \u260c   |    5    |\n|  \u260a  |     |     |  \u26b9  |  \u25a1  |     |  \u26b9  |     |  \u25a1  |     |     |     |       |  \u26b9   |    5    |\n| Asc |     |  \u25b3  |     |     |     |     |  \u260d  |     |     |     |     |   \u25b3   |      |    3    |\n| MC  |     |     |     |     |     |     |  \u25a1  |     |     |     |  \u26b9  |       |      |    2    |\n
    • see demo.ipynb for the HTML output
    "},{"location":"#configuration","title":"Configuration","text":"
    • create a Config object and assign it to Data object
    • it will override the default settings in config.py
    • a sample config as follow:
    from natal.config import Display, Config, Orb\n\n# adjust which celestial bodies to display\ndisplay = Display(\n    mc = False,\n    asc_node = False,\n    chiron = True\n)\n\n# adjust orbs for each aspect\norb = Orb(\n    conjunction = 8,\n    opposition = 8,\n    trine = 6,\n    square = 6,\n    sextile = 6\n)\n\n# the complete config object\nconfig = Config(\n    theme_type = \"light\", # or \"dark\", \"mono\"\n    display = display,\n    orb = orb\n)\n\n# create data object with the config\ndata = Data(\n    name = \"MiMi\",\n    city = \"Taipei\",\n    dt = \"1980-04-20 14:30\",\n    config = config,\n)\n

    read the docs for complete references

    "},{"location":"#tech-stack","title":"Tech Stack","text":"
    • tagit for creating and manipulating SVG
    • pyswisseph python extension to the Swiss Ephemeris
    • mkdocs-material for docs site generation
    "},{"location":"chart/","title":"Chart","text":""},{"location":"chart/#natal.chart","title":"natal.chart","text":"

    This module provides the Chart class for generating SVG representations of natal charts. It includes functionality for creating sign wheels, house wheels, body placements, and aspect lines for both single and composite charts.

    "},{"location":"chart/#natal.chart.Chart","title":"Chart","text":"

    Bases: DotDict

    SVG representation of a natal chart.

    This class generates the visual components of an astrological chart, including sign wheels, house wheels, planet placements, and aspect lines. It supports both single and composite charts.

    Source code in natal/chart.py
    class Chart(DotDict):\n    \"\"\"SVG representation of a natal chart.\n\n    This class generates the visual components of an astrological chart,\n    including sign wheels, house wheels, planet placements, and aspect lines.\n    It supports both single and composite charts.\n    \"\"\"\n\n    def __init__(\n        self,\n        data1: Data,\n        width: int,\n        height: int | None = None,\n        data2: Data | None = None,\n    ) -> None:\n        \"\"\"Initialize a Chart object.\n\n        Args:\n            data1: Primary chart data\n            width: Width of the SVG\n            height: Height of the SVG. If None, set to width\n            data2: Secondary chart data for composite charts\n\n        Returns:\n            None\n        \"\"\"\n        self.data1 = data1\n        self.data2 = data2\n        self.width = width\n        self.height = height\n        if self.height is None:\n            self.height = self.width\n        self.cx = self.width / 2\n        self.cy = self.height / 2\n\n        self.config = self.data1.config\n        margin = min(self.width, self.height) * self.config.chart.margin_factor\n        self.max_radius = min(self.width - margin, self.height - margin) // 2\n        self.margin = margin\n        self.ring_thickness = (\n            self.max_radius * self.config.chart.ring_thickness_fraction\n        )\n        self.font_size = self.ring_thickness * self.config.chart.font_size_fraction\n        self.scale_adjustment = self.width / self.config.chart.scale_adj_factor\n        self.pos_adjustment = self.font_size / self.config.chart.pos_adj_factor\n\n    def svg_root(self, content: str | list[str]) -> str:\n        \"\"\"Generate an SVG root element with sensible defaults.\n\n        Args:\n            content: The content to be included in the SVG root\n\n        Returns:\n            An SVG root element as a string\n        \"\"\"\n        return svg(\n            content,\n            height=self.height,\n            width=self.width,\n            font_family=self.config.chart.font,\n            version=\"1.1\",\n            xmlns=\"http://www.w3.org/2000/svg\",\n        )\n\n    def sector(\n        self,\n        radius: int,\n        start_deg: float,\n        end_deg: float,\n        fill: str = \"white\",\n        stroke_color: str = \"black\",\n        stroke_width: float = 1,\n        stroke_opacity: float = 1,\n    ) -> str:\n        \"\"\"Create a sector shape in SVG format.\n\n        Args:\n            radius: Radius of the sector\n            start_deg: Starting angle in degrees\n            end_deg: Ending angle in degrees\n            fill: Fill color of the sector\n            stroke_color: Stroke color of the sector\n            stroke_width: Width of the stroke\n            stroke_opacity: Opacity of the stroke\n\n        Returns:\n            An SVG path element representing the sector\n        \"\"\"\n        start_rad = radians(start_deg)\n        end_rad = radians(end_deg)\n        start_x = self.cx - radius * cos(start_rad)\n        start_y = self.cy + radius * sin(start_rad)\n        end_x = self.cx - radius * cos(end_rad)\n        end_y = self.cy + radius * sin(end_rad)\n\n        start_x, start_y, end_x, end_y = [\n            round(val, 2) for val in (start_x, start_y, end_x, end_y)\n        ]\n\n        path_data = \" \".join(\n            (\n                \"M{} {}\".format(self.cx, self.cy),\n                \"L{} {}\".format(start_x, start_y),\n                \"A{} {} 0 0 0 {} {}\".format(radius, radius, end_x, end_y),\n                \"Z\",\n            )\n        )\n        return path(\n            \"\",\n            d=path_data,\n            fill=fill,\n            stroke=stroke_color,\n            stroke_width=stroke_width,\n            stroke_opacity=stroke_opacity,\n        )\n\n    def background(self, radius: float, **kwargs) -> str:\n        \"\"\"Create a background circle for the chart.\n\n        Args:\n            radius: Radius of the background circle\n            **kwargs: Additional attributes for the circle element\n\n        Returns:\n            An SVG circle element representing the background\n        \"\"\"\n        return circle(cx=self.cx, cy=self.cy, r=radius, **kwargs)\n\n    def sign_wheel(self) -> list[str]:\n        \"\"\"Generate the zodiac sign wheel.\n\n        Returns:\n            A list of SVG elements representing the sign wheel\n        \"\"\"\n        radius = self.max_radius\n\n        wheel = [self.background(radius=radius, fill=self.config.theme.background)]\n        for i in range(12):\n            start_deg = self.data1.signs[i].normalized_degree\n            end_deg = start_deg + 30\n            wheel.append(\n                self.sector(\n                    radius=radius,\n                    start_deg=start_deg,\n                    end_deg=end_deg,\n                    fill=self.bg_colors[i],\n                    stroke_color=self.config.theme.foreground,\n                    stroke_width=self.config.chart.stroke_width,\n                )\n            )\n        return wheel\n\n    def sign_wheel_symbols(self) -> list[str]:\n        \"\"\"Generate the zodiac sign symbols for the sign wheel.\n\n        Returns:\n            A list of SVG elements representing the zodiac sign symbols\n        \"\"\"\n\n        wheel = []\n        for i in range(12):\n            start_deg = self.data1.signs[i].normalized_degree\n            symbol_radius = self.max_radius - (self.ring_thickness / 2)\n            symbol_angle = radians(start_deg + 15)  # Center of the sector\n            symbol_x = self.cx - symbol_radius * cos(symbol_angle) - self.pos_adjustment\n            symbol_y = self.cy + symbol_radius * sin(symbol_angle) - self.pos_adjustment\n            wheel.append(\n                g(\n                    [\n                        circle(\n                            cx=10,\n                            cy=10,\n                            r=12,\n                            stroke=\"none\",\n                            # fill=\"red\",\n                            fill=self.bg_colors[i],\n                        ),\n                        svg_paths[SIGN_MEMBERS[i].name],\n                    ],\n                    stroke=self.config.theme[SIGN_MEMBERS[i].color],\n                    stroke_width=self.config.chart.stroke_width * 1.5,\n                    fill=\"none\",\n                    transform=f\"translate({symbol_x}, {symbol_y}) scale({self.scale_adjustment})\",\n                )\n            )\n        return wheel\n\n    def house_wheel(self) -> list[str]:\n        \"\"\"Generate the house wheel.\n\n        Returns:\n            A list of SVG elements representing the house wheel\n        \"\"\"\n        radius = self.max_radius - self.ring_thickness\n        wheel = [self.background(radius, fill=self.config.theme.background)]\n\n        for i, (start_deg, end_deg) in enumerate(self.house_vertices):\n            wheel.append(\n                self.sector(\n                    radius=radius,\n                    start_deg=start_deg,\n                    end_deg=end_deg,\n                    fill=self.bg_colors[i],\n                    stroke_color=self.config.theme.foreground,\n                    stroke_width=self.config.chart.stroke_width,\n                )\n            )\n\n            # Add house number\n            number_width = self.font_size * 0.8\n            number_radius = radius - (self.ring_thickness / 2)\n            number_angle = radians(\n                start_deg + ((end_deg - start_deg) % 360) / 2\n            )  # Center of the house\n            number_x = self.cx - number_radius * cos(number_angle)\n            number_y = self.cy + number_radius * sin(number_angle)\n            wheel.append(\n                text(\n                    str(i + 1),  # House numbers start from 1\n                    x=number_x,\n                    y=number_y,\n                    fill=getattr(self.config.theme, SIGN_MEMBERS[i].color),\n                    font_size=number_width,\n                    text_anchor=\"middle\",\n                    dominant_baseline=\"central\",\n                )\n            )\n\n        return wheel\n\n    def vertex_wheel(self) -> list[str]:\n        \"\"\"Generate vertex lines for the chart.\n\n        Returns:\n            A list of SVG elements representing vertex lines\n        \"\"\"\n        vertex_radius = self.max_radius + self.margin // 2\n        house_radius = self.max_radius - 2 * self.ring_thickness\n        body_radius = self.max_radius - 3 * self.ring_thickness\n\n        lines = [\n            self.background(\n                house_radius,\n                fill=self.config.theme.background,\n                stroke=self.config.theme.foreground,\n                stroke_width=self.config.chart.stroke_width,\n            ),\n            self.background(\n                body_radius,\n                fill=\"#88888800\",  # transparent\n                stroke=self.config.theme.dim,\n                stroke_width=self.config.chart.stroke_width,\n            ),\n        ]\n        for house in self.data1.houses:\n            radius = house_radius\n            stroke_width = self.config.chart.stroke_width\n            stroke_color = self.config.theme.dim\n\n            if house.value in [1, 4, 7, 10]:\n                radius = vertex_radius\n                stroke_color = self.config.theme.foreground\n\n            angle = radians(house.normalized_degree)\n            end_x = self.cx - radius * cos(angle)\n            end_y = self.cy + radius * sin(angle)\n\n            lines.append(\n                line(\n                    x1=self.cx,\n                    y1=self.cy,\n                    x2=end_x,\n                    y2=end_y,\n                    stroke=stroke_color,\n                    stroke_width=stroke_width,\n                    stroke_opacity=self.config.chart.stroke_opacity,\n                )\n            )\n\n        return lines\n\n    def outer_body_wheel(self) -> list[str]:\n        \"\"\"Generate the outer body wheel for single or composite charts.\n\n        Returns:\n            A list of SVG elements representing the outer body wheel\n        \"\"\"\n        radius = self.max_radius - 3 * self.ring_thickness\n        data = self.data2 or self.data1\n        return self.body_wheel(radius, data, self.config.chart.outer_min_degree)\n\n    def inner_body_wheel(self) -> list[str] | None:\n        \"\"\"Generate the inner body wheel for composite charts.\n\n        Returns:\n            A list of SVG elements representing the inner body wheel, or None for single charts\n        \"\"\"\n        if self.data2 is None:\n            return\n        radius = self.max_radius - 4 * self.ring_thickness\n        data = self.data1\n        return self.body_wheel(radius, data, self.config.chart.inner_min_degree)\n\n    def outer_aspect(self) -> list[str]:\n        \"\"\"Generate aspect lines for the outer wheel in single charts.\n\n        Returns:\n            A list of SVG elements representing aspect lines\n        \"\"\"\n        if self.data2 is not None:\n            return []\n        radius = self.max_radius - 3 * self.ring_thickness\n        aspects = self.data1.aspects\n        return self.aspect_lines(radius, aspects)\n\n    def inner_aspect(self) -> list[str]:\n        \"\"\"Generate aspect lines for the inner wheel in composite charts.\n\n        Returns:\n            A list of SVG elements representing aspect lines\n        \"\"\"\n        if self.data2 is None:\n            return []\n        radius = self.max_radius - 4 * self.ring_thickness\n        aspects = self.data1.calculate_aspects(\n            self.data1.composite_aspects_pairs(self.data2)\n        )\n        return self.aspect_lines(radius, aspects)\n\n    @property\n    def svg(self) -> str:\n        \"\"\"Generate the SVG representation of the chart.\n\n        Returns:\n            str: SVG content.\n        \"\"\"\n        return self.svg_root(\n            [\n                self.sign_wheel(),\n                self.house_wheel(),\n                self.vertex_wheel(),\n                self.sign_wheel_symbols(),\n                self.outer_body_wheel(),\n                self.inner_body_wheel(),\n                self.outer_aspect(),\n                self.inner_aspect(),\n            ]\n        )\n\n    # utils ======================================================\n\n    def adjusted_degrees(self, degrees: list[float], min_degree: float) -> list[float]:\n        \"\"\"Adjust spacing between celestial bodies to avoid overlap.\n\n        Args:\n            degrees: Sorted normalized degrees of celestial bodies\n            min_degree: Minimum allowed degree separation\n\n        Returns:\n            Adjusted degrees of celestial bodies\n        \"\"\"\n        step = min_degree + 0.1  # prevent overlap for float precision\n        n = len(degrees)\n\n        fwd_degs = degrees.copy()\n        bwd_degs = degrees[::-1]\n\n        # Forward adjustment\n        changed = True\n        while changed:\n            changed = False\n            for i in range(n):\n                prev_deg = fwd_degs[-1] - 360 if i == 0 else fwd_degs[i - 1]\n                delta = fwd_degs[i] - prev_deg\n                diff = min(delta, 360 - delta)\n                if (fwd_degs[i] < prev_deg) or (diff < min_degree):\n                    fwd_degs[i] = prev_deg + step\n                    changed = True\n\n        # Backward adjustment\n        changed = True\n        while changed:\n            changed = False\n            for i in range(n):\n                prev_deg = bwd_degs[-1] + 360 if i == 0 else bwd_degs[i - 1]\n                delta = prev_deg - bwd_degs[i]\n                diff = min(delta, 360 - delta)\n                if (prev_deg < bwd_degs[i]) or (diff < min_degree):\n                    bwd_degs[i] = prev_deg - step\n                    changed = True\n\n        bwd_degs.reverse()\n\n        # average forward and backward adjustments\n        avg_adj = []\n        for fwd, bwd in zip(fwd_degs, bwd_degs):\n            fwd %= 360\n            bwd %= 360\n            if abs(fwd - bwd) < 180:\n                avg = (fwd + bwd) / 2\n            else:\n                avg = ((fwd + bwd + 360) / 2) % 360\n            avg_adj.append(avg)\n\n        return avg_adj\n\n    def body_wheel(self, wheel_radius: float, data: Data, min_degree: float):\n        \"\"\"Generate elements for both inner and outer body wheels.\n\n        Args:\n            wheel_radius: Radius of the wheel\n            data: Chart data to use\n            min_degree: Minimum degree separation between bodies\n\n        Returns:\n            A list of SVG elements representing the body wheel\n        \"\"\"\n\n        def norm_deg(x):\n            return self.data1.normalize(x.degree)\n\n        sorted_norm_bodies = sorted(data.aspectables, key=norm_deg)\n        sorted_norm_degs = [norm_deg(b) for b in sorted_norm_bodies]\n\n        # Calculate adjusted positions\n        adj_norm_degs = (\n            self.adjusted_degrees(sorted_norm_degs, min_degree)\n            if len(sorted_norm_bodies) > 1\n            else sorted_norm_degs\n        )\n        # for tests only\n        self.adj_degs_len = len(adj_norm_degs)\n\n        output = []\n        for body, adj_deg in zip(sorted_norm_bodies, adj_norm_degs):\n            g_opt = {\n                \"fill\": \"none\",\n                \"stroke\": self.config.theme[body.color],\n                \"stroke_width\": self.config.chart.stroke_width * 1.5,\n            }\n\n            # special handling for asc, ic, dsc and mc\n            if body.name in VERTEX_NAMES:\n                g_opt[\"fill\"] = self.config.theme[body.color]\n                g_opt[\"stroke\"] = \"none\"\n\n            symbol_radius = wheel_radius + (self.ring_thickness / 2)\n\n            # Use original angle for line start position\n            original_angle = radians(self.data1.normalize(body.degree))\n            degree_x = self.cx - wheel_radius * cos(original_angle)\n            degree_y = self.cy + wheel_radius * sin(original_angle)\n\n            # Use adjusted angle for symbol position\n            adjusted_angle = radians(adj_deg)\n            symbol_x = self.cx - symbol_radius * cos(adjusted_angle)\n            symbol_y = self.cy + symbol_radius * sin(adjusted_angle)\n\n            # Add line connecting to the inner circle\n            inner_radius = wheel_radius - self.ring_thickness\n            inner_x = self.cx - inner_radius * cos(original_angle)\n            inner_y = self.cy + inner_radius * sin(original_angle)\n\n            output.extend(\n                [\n                    line(\n                        x1=degree_x,\n                        y1=degree_y,\n                        x2=symbol_x,\n                        y2=symbol_y,\n                        stroke=self.config.theme[body.color],\n                        stroke_width=self.config.chart.stroke_width / 2,\n                    ),\n                    circle(\n                        cx=symbol_x,\n                        cy=symbol_y,\n                        r=self.font_size / 2,\n                        # fill=\"red\",  # for testing only\n                        fill=self.config.theme.background,\n                    ),\n                    line(\n                        x1=degree_x,\n                        y1=degree_y,\n                        x2=inner_x,\n                        y2=inner_y,\n                        stroke=self.config.theme.dim,\n                        stroke_width=self.config.chart.stroke_width / 2,\n                        stroke_dasharray=self.ring_thickness / 11,\n                    ),\n                    g(\n                        svg_paths[body.name],\n                        transform=f\"translate({symbol_x - self.pos_adjustment}, {symbol_y - self.pos_adjustment}) scale({self.scale_adjustment})\",\n                        **g_opt,\n                    ),\n                ]\n            )\n        return output\n\n    def aspect_lines(self, radius: float, aspects: list[Aspect]) -> list[str]:\n        \"\"\"Draw aspect lines between aspectable celestial bodies.\n\n        Args:\n            radius: Radius of the aspect wheel\n            aspects: List of aspects to draw\n\n        Returns:\n            A list of SVG elements representing aspect lines\n        \"\"\"\n        bg = [\n            self.background(\n                radius,\n                fill=self.config.theme.background,\n                stroke=self.config.theme.dim,\n                stroke_width=self.config.chart.stroke_width,\n            )\n        ]\n        aspect_lines = []\n        for aspect in aspects:\n            start_angle = radians(self.data1.normalize(aspect.body1.degree))\n            end_angle = radians(self.data1.normalize(aspect.body2.degree))\n            orb_config = self.config.orb[aspect.aspect_member.name]\n            if not orb_config:\n                continue\n            orb_fraction = 1 - aspect.orb / orb_config\n            opacity_factor = (\n                1 if aspect.aspect_member.name == \"conjunction\" else orb_fraction\n            )\n            aspect_lines.append(\n                line(\n                    x1=self.cx - radius * cos(start_angle),\n                    y1=self.cy + radius * sin(start_angle),\n                    x2=self.cx - radius * cos(end_angle),\n                    y2=self.cy + radius * sin(end_angle),\n                    stroke=self.config.theme[aspect.aspect_member.color],\n                    stroke_width=self.config.chart.stroke_width / 2,\n                    stroke_opacity=self.config.chart.stroke_opacity * opacity_factor,\n                )\n            )\n\n        self.aspect_lines_len = len(aspect_lines)  # for test only\n        return bg + aspect_lines\n\n    @cached_property\n    def house_vertices(self) -> list[tuple[float, float]]:\n        \"\"\"Calculate the vertices (start and end degrees) of each house.\n\n        Returns:\n            A list of tuples containing start and end degrees for each house\n        \"\"\"\n        vertices = []\n        for i in range(12):\n            next_i = (i + 1) % 12\n            start_deg = self.data1.houses[i].normalized_degree\n            end_deg = self.data1.houses[next_i].normalized_degree\n            # Handle the case where end_deg is less than start_deg (crosses 0\u00b0)\n            if end_deg < start_deg:\n                end_deg += 360\n            vertices.append((start_deg, end_deg))\n\n        return vertices\n\n    @cached_property\n    def bg_colors(self) -> list[str]:\n        \"\"\"Get the background colors for each house.\n\n        Returns:\n            A list of hex color strings for house backgrounds\n        \"\"\"\n\n        def hex_to_rgb(hex_value):\n            hex_value = hex_value.lstrip(\"#\")\n            return tuple(int(hex_value[i : i + 2], 16) for i in (0, 2, 4))\n\n        def rgb_to_hex(rgb):\n            return \"#\" + \"\".join(f\"{i:02x}\" for i in rgb)\n\n        trans = self.config.theme.transparency\n        output = []\n        for i in range(4):\n            hex_color = getattr(self.config.theme, SIGN_MEMBERS[i].color)\n            rgb_color = hex_to_rgb(hex_color)\n            rgb_bg = hex_to_rgb(self.config.theme.background)\n            # blend the color with the background\n            blended_rgb = tuple(\n                int(trans * rgb_color[i] + (1 - trans) * rgb_bg[i]) for i in range(3)\n            )\n            output.append(rgb_to_hex(blended_rgb))\n\n        return output * 4\n
    "},{"location":"chart/#natal.chart.Chart.bg_colors","title":"bg_colors: list[str]cachedproperty","text":"

    Get the background colors for each house.

    Returns:

    Type Description list[str]

    A list of hex color strings for house backgrounds

    "},{"location":"chart/#natal.chart.Chart.house_vertices","title":"house_vertices: list[tuple[float, float]]cachedproperty","text":"

    Calculate the vertices (start and end degrees) of each house.

    Returns:

    Type Description list[tuple[float, float]]

    A list of tuples containing start and end degrees for each house

    "},{"location":"chart/#natal.chart.Chart.svg","title":"svg: strproperty","text":"

    Generate the SVG representation of the chart.

    Returns:

    Name Type Description strstr

    SVG content.

    "},{"location":"chart/#natal.chart.Chart.__init__","title":"__init__(data1: Data, width: int, height: int | None = None, data2: Data | None = None) -> None","text":"

    Initialize a Chart object.

    Parameters:

    Name Type Description Default data1Data

    Primary chart data

    required widthint

    Width of the SVG

    required heightint | None

    Height of the SVG. If None, set to width

    Nonedata2Data | None

    Secondary chart data for composite charts

    None

    Returns:

    Type Description None

    None

    Source code in natal/chart.py
    def __init__(\n    self,\n    data1: Data,\n    width: int,\n    height: int | None = None,\n    data2: Data | None = None,\n) -> None:\n    \"\"\"Initialize a Chart object.\n\n    Args:\n        data1: Primary chart data\n        width: Width of the SVG\n        height: Height of the SVG. If None, set to width\n        data2: Secondary chart data for composite charts\n\n    Returns:\n        None\n    \"\"\"\n    self.data1 = data1\n    self.data2 = data2\n    self.width = width\n    self.height = height\n    if self.height is None:\n        self.height = self.width\n    self.cx = self.width / 2\n    self.cy = self.height / 2\n\n    self.config = self.data1.config\n    margin = min(self.width, self.height) * self.config.chart.margin_factor\n    self.max_radius = min(self.width - margin, self.height - margin) // 2\n    self.margin = margin\n    self.ring_thickness = (\n        self.max_radius * self.config.chart.ring_thickness_fraction\n    )\n    self.font_size = self.ring_thickness * self.config.chart.font_size_fraction\n    self.scale_adjustment = self.width / self.config.chart.scale_adj_factor\n    self.pos_adjustment = self.font_size / self.config.chart.pos_adj_factor\n
    "},{"location":"chart/#natal.chart.Chart.adjusted_degrees","title":"adjusted_degrees(degrees: list[float], min_degree: float) -> list[float]","text":"

    Adjust spacing between celestial bodies to avoid overlap.

    Parameters:

    Name Type Description Default degreeslist[float]

    Sorted normalized degrees of celestial bodies

    required min_degreefloat

    Minimum allowed degree separation

    required

    Returns:

    Type Description list[float]

    Adjusted degrees of celestial bodies

    Source code in natal/chart.py
    def adjusted_degrees(self, degrees: list[float], min_degree: float) -> list[float]:\n    \"\"\"Adjust spacing between celestial bodies to avoid overlap.\n\n    Args:\n        degrees: Sorted normalized degrees of celestial bodies\n        min_degree: Minimum allowed degree separation\n\n    Returns:\n        Adjusted degrees of celestial bodies\n    \"\"\"\n    step = min_degree + 0.1  # prevent overlap for float precision\n    n = len(degrees)\n\n    fwd_degs = degrees.copy()\n    bwd_degs = degrees[::-1]\n\n    # Forward adjustment\n    changed = True\n    while changed:\n        changed = False\n        for i in range(n):\n            prev_deg = fwd_degs[-1] - 360 if i == 0 else fwd_degs[i - 1]\n            delta = fwd_degs[i] - prev_deg\n            diff = min(delta, 360 - delta)\n            if (fwd_degs[i] < prev_deg) or (diff < min_degree):\n                fwd_degs[i] = prev_deg + step\n                changed = True\n\n    # Backward adjustment\n    changed = True\n    while changed:\n        changed = False\n        for i in range(n):\n            prev_deg = bwd_degs[-1] + 360 if i == 0 else bwd_degs[i - 1]\n            delta = prev_deg - bwd_degs[i]\n            diff = min(delta, 360 - delta)\n            if (prev_deg < bwd_degs[i]) or (diff < min_degree):\n                bwd_degs[i] = prev_deg - step\n                changed = True\n\n    bwd_degs.reverse()\n\n    # average forward and backward adjustments\n    avg_adj = []\n    for fwd, bwd in zip(fwd_degs, bwd_degs):\n        fwd %= 360\n        bwd %= 360\n        if abs(fwd - bwd) < 180:\n            avg = (fwd + bwd) / 2\n        else:\n            avg = ((fwd + bwd + 360) / 2) % 360\n        avg_adj.append(avg)\n\n    return avg_adj\n
    "},{"location":"chart/#natal.chart.Chart.aspect_lines","title":"aspect_lines(radius: float, aspects: list[Aspect]) -> list[str]","text":"

    Draw aspect lines between aspectable celestial bodies.

    Parameters:

    Name Type Description Default radiusfloat

    Radius of the aspect wheel

    required aspectslist[Aspect]

    List of aspects to draw

    required

    Returns:

    Type Description list[str]

    A list of SVG elements representing aspect lines

    Source code in natal/chart.py
    def aspect_lines(self, radius: float, aspects: list[Aspect]) -> list[str]:\n    \"\"\"Draw aspect lines between aspectable celestial bodies.\n\n    Args:\n        radius: Radius of the aspect wheel\n        aspects: List of aspects to draw\n\n    Returns:\n        A list of SVG elements representing aspect lines\n    \"\"\"\n    bg = [\n        self.background(\n            radius,\n            fill=self.config.theme.background,\n            stroke=self.config.theme.dim,\n            stroke_width=self.config.chart.stroke_width,\n        )\n    ]\n    aspect_lines = []\n    for aspect in aspects:\n        start_angle = radians(self.data1.normalize(aspect.body1.degree))\n        end_angle = radians(self.data1.normalize(aspect.body2.degree))\n        orb_config = self.config.orb[aspect.aspect_member.name]\n        if not orb_config:\n            continue\n        orb_fraction = 1 - aspect.orb / orb_config\n        opacity_factor = (\n            1 if aspect.aspect_member.name == \"conjunction\" else orb_fraction\n        )\n        aspect_lines.append(\n            line(\n                x1=self.cx - radius * cos(start_angle),\n                y1=self.cy + radius * sin(start_angle),\n                x2=self.cx - radius * cos(end_angle),\n                y2=self.cy + radius * sin(end_angle),\n                stroke=self.config.theme[aspect.aspect_member.color],\n                stroke_width=self.config.chart.stroke_width / 2,\n                stroke_opacity=self.config.chart.stroke_opacity * opacity_factor,\n            )\n        )\n\n    self.aspect_lines_len = len(aspect_lines)  # for test only\n    return bg + aspect_lines\n
    "},{"location":"chart/#natal.chart.Chart.background","title":"background(radius: float, **kwargs) -> str","text":"

    Create a background circle for the chart.

    Parameters:

    Name Type Description Default radiusfloat

    Radius of the background circle

    required **kwargs

    Additional attributes for the circle element

    {}

    Returns:

    Type Description str

    An SVG circle element representing the background

    Source code in natal/chart.py
    def background(self, radius: float, **kwargs) -> str:\n    \"\"\"Create a background circle for the chart.\n\n    Args:\n        radius: Radius of the background circle\n        **kwargs: Additional attributes for the circle element\n\n    Returns:\n        An SVG circle element representing the background\n    \"\"\"\n    return circle(cx=self.cx, cy=self.cy, r=radius, **kwargs)\n
    "},{"location":"chart/#natal.chart.Chart.body_wheel","title":"body_wheel(wheel_radius: float, data: Data, min_degree: float)","text":"

    Generate elements for both inner and outer body wheels.

    Parameters:

    Name Type Description Default wheel_radiusfloat

    Radius of the wheel

    required dataData

    Chart data to use

    required min_degreefloat

    Minimum degree separation between bodies

    required

    Returns:

    Type Description

    A list of SVG elements representing the body wheel

    Source code in natal/chart.py
    def body_wheel(self, wheel_radius: float, data: Data, min_degree: float):\n    \"\"\"Generate elements for both inner and outer body wheels.\n\n    Args:\n        wheel_radius: Radius of the wheel\n        data: Chart data to use\n        min_degree: Minimum degree separation between bodies\n\n    Returns:\n        A list of SVG elements representing the body wheel\n    \"\"\"\n\n    def norm_deg(x):\n        return self.data1.normalize(x.degree)\n\n    sorted_norm_bodies = sorted(data.aspectables, key=norm_deg)\n    sorted_norm_degs = [norm_deg(b) for b in sorted_norm_bodies]\n\n    # Calculate adjusted positions\n    adj_norm_degs = (\n        self.adjusted_degrees(sorted_norm_degs, min_degree)\n        if len(sorted_norm_bodies) > 1\n        else sorted_norm_degs\n    )\n    # for tests only\n    self.adj_degs_len = len(adj_norm_degs)\n\n    output = []\n    for body, adj_deg in zip(sorted_norm_bodies, adj_norm_degs):\n        g_opt = {\n            \"fill\": \"none\",\n            \"stroke\": self.config.theme[body.color],\n            \"stroke_width\": self.config.chart.stroke_width * 1.5,\n        }\n\n        # special handling for asc, ic, dsc and mc\n        if body.name in VERTEX_NAMES:\n            g_opt[\"fill\"] = self.config.theme[body.color]\n            g_opt[\"stroke\"] = \"none\"\n\n        symbol_radius = wheel_radius + (self.ring_thickness / 2)\n\n        # Use original angle for line start position\n        original_angle = radians(self.data1.normalize(body.degree))\n        degree_x = self.cx - wheel_radius * cos(original_angle)\n        degree_y = self.cy + wheel_radius * sin(original_angle)\n\n        # Use adjusted angle for symbol position\n        adjusted_angle = radians(adj_deg)\n        symbol_x = self.cx - symbol_radius * cos(adjusted_angle)\n        symbol_y = self.cy + symbol_radius * sin(adjusted_angle)\n\n        # Add line connecting to the inner circle\n        inner_radius = wheel_radius - self.ring_thickness\n        inner_x = self.cx - inner_radius * cos(original_angle)\n        inner_y = self.cy + inner_radius * sin(original_angle)\n\n        output.extend(\n            [\n                line(\n                    x1=degree_x,\n                    y1=degree_y,\n                    x2=symbol_x,\n                    y2=symbol_y,\n                    stroke=self.config.theme[body.color],\n                    stroke_width=self.config.chart.stroke_width / 2,\n                ),\n                circle(\n                    cx=symbol_x,\n                    cy=symbol_y,\n                    r=self.font_size / 2,\n                    # fill=\"red\",  # for testing only\n                    fill=self.config.theme.background,\n                ),\n                line(\n                    x1=degree_x,\n                    y1=degree_y,\n                    x2=inner_x,\n                    y2=inner_y,\n                    stroke=self.config.theme.dim,\n                    stroke_width=self.config.chart.stroke_width / 2,\n                    stroke_dasharray=self.ring_thickness / 11,\n                ),\n                g(\n                    svg_paths[body.name],\n                    transform=f\"translate({symbol_x - self.pos_adjustment}, {symbol_y - self.pos_adjustment}) scale({self.scale_adjustment})\",\n                    **g_opt,\n                ),\n            ]\n        )\n    return output\n
    "},{"location":"chart/#natal.chart.Chart.house_wheel","title":"house_wheel() -> list[str]","text":"

    Generate the house wheel.

    Returns:

    Type Description list[str]

    A list of SVG elements representing the house wheel

    Source code in natal/chart.py
    def house_wheel(self) -> list[str]:\n    \"\"\"Generate the house wheel.\n\n    Returns:\n        A list of SVG elements representing the house wheel\n    \"\"\"\n    radius = self.max_radius - self.ring_thickness\n    wheel = [self.background(radius, fill=self.config.theme.background)]\n\n    for i, (start_deg, end_deg) in enumerate(self.house_vertices):\n        wheel.append(\n            self.sector(\n                radius=radius,\n                start_deg=start_deg,\n                end_deg=end_deg,\n                fill=self.bg_colors[i],\n                stroke_color=self.config.theme.foreground,\n                stroke_width=self.config.chart.stroke_width,\n            )\n        )\n\n        # Add house number\n        number_width = self.font_size * 0.8\n        number_radius = radius - (self.ring_thickness / 2)\n        number_angle = radians(\n            start_deg + ((end_deg - start_deg) % 360) / 2\n        )  # Center of the house\n        number_x = self.cx - number_radius * cos(number_angle)\n        number_y = self.cy + number_radius * sin(number_angle)\n        wheel.append(\n            text(\n                str(i + 1),  # House numbers start from 1\n                x=number_x,\n                y=number_y,\n                fill=getattr(self.config.theme, SIGN_MEMBERS[i].color),\n                font_size=number_width,\n                text_anchor=\"middle\",\n                dominant_baseline=\"central\",\n            )\n        )\n\n    return wheel\n
    "},{"location":"chart/#natal.chart.Chart.inner_aspect","title":"inner_aspect() -> list[str]","text":"

    Generate aspect lines for the inner wheel in composite charts.

    Returns:

    Type Description list[str]

    A list of SVG elements representing aspect lines

    Source code in natal/chart.py
    def inner_aspect(self) -> list[str]:\n    \"\"\"Generate aspect lines for the inner wheel in composite charts.\n\n    Returns:\n        A list of SVG elements representing aspect lines\n    \"\"\"\n    if self.data2 is None:\n        return []\n    radius = self.max_radius - 4 * self.ring_thickness\n    aspects = self.data1.calculate_aspects(\n        self.data1.composite_aspects_pairs(self.data2)\n    )\n    return self.aspect_lines(radius, aspects)\n
    "},{"location":"chart/#natal.chart.Chart.inner_body_wheel","title":"inner_body_wheel() -> list[str] | None","text":"

    Generate the inner body wheel for composite charts.

    Returns:

    Type Description list[str] | None

    A list of SVG elements representing the inner body wheel, or None for single charts

    Source code in natal/chart.py
    def inner_body_wheel(self) -> list[str] | None:\n    \"\"\"Generate the inner body wheel for composite charts.\n\n    Returns:\n        A list of SVG elements representing the inner body wheel, or None for single charts\n    \"\"\"\n    if self.data2 is None:\n        return\n    radius = self.max_radius - 4 * self.ring_thickness\n    data = self.data1\n    return self.body_wheel(radius, data, self.config.chart.inner_min_degree)\n
    "},{"location":"chart/#natal.chart.Chart.outer_aspect","title":"outer_aspect() -> list[str]","text":"

    Generate aspect lines for the outer wheel in single charts.

    Returns:

    Type Description list[str]

    A list of SVG elements representing aspect lines

    Source code in natal/chart.py
    def outer_aspect(self) -> list[str]:\n    \"\"\"Generate aspect lines for the outer wheel in single charts.\n\n    Returns:\n        A list of SVG elements representing aspect lines\n    \"\"\"\n    if self.data2 is not None:\n        return []\n    radius = self.max_radius - 3 * self.ring_thickness\n    aspects = self.data1.aspects\n    return self.aspect_lines(radius, aspects)\n
    "},{"location":"chart/#natal.chart.Chart.outer_body_wheel","title":"outer_body_wheel() -> list[str]","text":"

    Generate the outer body wheel for single or composite charts.

    Returns:

    Type Description list[str]

    A list of SVG elements representing the outer body wheel

    Source code in natal/chart.py
    def outer_body_wheel(self) -> list[str]:\n    \"\"\"Generate the outer body wheel for single or composite charts.\n\n    Returns:\n        A list of SVG elements representing the outer body wheel\n    \"\"\"\n    radius = self.max_radius - 3 * self.ring_thickness\n    data = self.data2 or self.data1\n    return self.body_wheel(radius, data, self.config.chart.outer_min_degree)\n
    "},{"location":"chart/#natal.chart.Chart.sector","title":"sector(radius: int, start_deg: float, end_deg: float, fill: str = 'white', stroke_color: str = 'black', stroke_width: float = 1, stroke_opacity: float = 1) -> str","text":"

    Create a sector shape in SVG format.

    Parameters:

    Name Type Description Default radiusint

    Radius of the sector

    required start_degfloat

    Starting angle in degrees

    required end_degfloat

    Ending angle in degrees

    required fillstr

    Fill color of the sector

    'white'stroke_colorstr

    Stroke color of the sector

    'black'stroke_widthfloat

    Width of the stroke

    1stroke_opacityfloat

    Opacity of the stroke

    1

    Returns:

    Type Description str

    An SVG path element representing the sector

    Source code in natal/chart.py
    def sector(\n    self,\n    radius: int,\n    start_deg: float,\n    end_deg: float,\n    fill: str = \"white\",\n    stroke_color: str = \"black\",\n    stroke_width: float = 1,\n    stroke_opacity: float = 1,\n) -> str:\n    \"\"\"Create a sector shape in SVG format.\n\n    Args:\n        radius: Radius of the sector\n        start_deg: Starting angle in degrees\n        end_deg: Ending angle in degrees\n        fill: Fill color of the sector\n        stroke_color: Stroke color of the sector\n        stroke_width: Width of the stroke\n        stroke_opacity: Opacity of the stroke\n\n    Returns:\n        An SVG path element representing the sector\n    \"\"\"\n    start_rad = radians(start_deg)\n    end_rad = radians(end_deg)\n    start_x = self.cx - radius * cos(start_rad)\n    start_y = self.cy + radius * sin(start_rad)\n    end_x = self.cx - radius * cos(end_rad)\n    end_y = self.cy + radius * sin(end_rad)\n\n    start_x, start_y, end_x, end_y = [\n        round(val, 2) for val in (start_x, start_y, end_x, end_y)\n    ]\n\n    path_data = \" \".join(\n        (\n            \"M{} {}\".format(self.cx, self.cy),\n            \"L{} {}\".format(start_x, start_y),\n            \"A{} {} 0 0 0 {} {}\".format(radius, radius, end_x, end_y),\n            \"Z\",\n        )\n    )\n    return path(\n        \"\",\n        d=path_data,\n        fill=fill,\n        stroke=stroke_color,\n        stroke_width=stroke_width,\n        stroke_opacity=stroke_opacity,\n    )\n
    "},{"location":"chart/#natal.chart.Chart.sign_wheel","title":"sign_wheel() -> list[str]","text":"

    Generate the zodiac sign wheel.

    Returns:

    Type Description list[str]

    A list of SVG elements representing the sign wheel

    Source code in natal/chart.py
    def sign_wheel(self) -> list[str]:\n    \"\"\"Generate the zodiac sign wheel.\n\n    Returns:\n        A list of SVG elements representing the sign wheel\n    \"\"\"\n    radius = self.max_radius\n\n    wheel = [self.background(radius=radius, fill=self.config.theme.background)]\n    for i in range(12):\n        start_deg = self.data1.signs[i].normalized_degree\n        end_deg = start_deg + 30\n        wheel.append(\n            self.sector(\n                radius=radius,\n                start_deg=start_deg,\n                end_deg=end_deg,\n                fill=self.bg_colors[i],\n                stroke_color=self.config.theme.foreground,\n                stroke_width=self.config.chart.stroke_width,\n            )\n        )\n    return wheel\n
    "},{"location":"chart/#natal.chart.Chart.sign_wheel_symbols","title":"sign_wheel_symbols() -> list[str]","text":"

    Generate the zodiac sign symbols for the sign wheel.

    Returns:

    Type Description list[str]

    A list of SVG elements representing the zodiac sign symbols

    Source code in natal/chart.py
    def sign_wheel_symbols(self) -> list[str]:\n    \"\"\"Generate the zodiac sign symbols for the sign wheel.\n\n    Returns:\n        A list of SVG elements representing the zodiac sign symbols\n    \"\"\"\n\n    wheel = []\n    for i in range(12):\n        start_deg = self.data1.signs[i].normalized_degree\n        symbol_radius = self.max_radius - (self.ring_thickness / 2)\n        symbol_angle = radians(start_deg + 15)  # Center of the sector\n        symbol_x = self.cx - symbol_radius * cos(symbol_angle) - self.pos_adjustment\n        symbol_y = self.cy + symbol_radius * sin(symbol_angle) - self.pos_adjustment\n        wheel.append(\n            g(\n                [\n                    circle(\n                        cx=10,\n                        cy=10,\n                        r=12,\n                        stroke=\"none\",\n                        # fill=\"red\",\n                        fill=self.bg_colors[i],\n                    ),\n                    svg_paths[SIGN_MEMBERS[i].name],\n                ],\n                stroke=self.config.theme[SIGN_MEMBERS[i].color],\n                stroke_width=self.config.chart.stroke_width * 1.5,\n                fill=\"none\",\n                transform=f\"translate({symbol_x}, {symbol_y}) scale({self.scale_adjustment})\",\n            )\n        )\n    return wheel\n
    "},{"location":"chart/#natal.chart.Chart.svg_root","title":"svg_root(content: str | list[str]) -> str","text":"

    Generate an SVG root element with sensible defaults.

    Parameters:

    Name Type Description Default contentstr | list[str]

    The content to be included in the SVG root

    required

    Returns:

    Type Description str

    An SVG root element as a string

    Source code in natal/chart.py
    def svg_root(self, content: str | list[str]) -> str:\n    \"\"\"Generate an SVG root element with sensible defaults.\n\n    Args:\n        content: The content to be included in the SVG root\n\n    Returns:\n        An SVG root element as a string\n    \"\"\"\n    return svg(\n        content,\n        height=self.height,\n        width=self.width,\n        font_family=self.config.chart.font,\n        version=\"1.1\",\n        xmlns=\"http://www.w3.org/2000/svg\",\n    )\n
    "},{"location":"chart/#natal.chart.Chart.vertex_wheel","title":"vertex_wheel() -> list[str]","text":"

    Generate vertex lines for the chart.

    Returns:

    Type Description list[str]

    A list of SVG elements representing vertex lines

    Source code in natal/chart.py
    def vertex_wheel(self) -> list[str]:\n    \"\"\"Generate vertex lines for the chart.\n\n    Returns:\n        A list of SVG elements representing vertex lines\n    \"\"\"\n    vertex_radius = self.max_radius + self.margin // 2\n    house_radius = self.max_radius - 2 * self.ring_thickness\n    body_radius = self.max_radius - 3 * self.ring_thickness\n\n    lines = [\n        self.background(\n            house_radius,\n            fill=self.config.theme.background,\n            stroke=self.config.theme.foreground,\n            stroke_width=self.config.chart.stroke_width,\n        ),\n        self.background(\n            body_radius,\n            fill=\"#88888800\",  # transparent\n            stroke=self.config.theme.dim,\n            stroke_width=self.config.chart.stroke_width,\n        ),\n    ]\n    for house in self.data1.houses:\n        radius = house_radius\n        stroke_width = self.config.chart.stroke_width\n        stroke_color = self.config.theme.dim\n\n        if house.value in [1, 4, 7, 10]:\n            radius = vertex_radius\n            stroke_color = self.config.theme.foreground\n\n        angle = radians(house.normalized_degree)\n        end_x = self.cx - radius * cos(angle)\n        end_y = self.cy + radius * sin(angle)\n\n        lines.append(\n            line(\n                x1=self.cx,\n                y1=self.cy,\n                x2=end_x,\n                y2=end_y,\n                stroke=stroke_color,\n                stroke_width=stroke_width,\n                stroke_opacity=self.config.chart.stroke_opacity,\n            )\n        )\n\n    return lines\n
    "},{"location":"classes/","title":"Classes","text":""},{"location":"classes/#natal.classes","title":"natal.classes","text":""},{"location":"classes/#natal.classes.Aspect","title":"Aspect","text":"

    Bases: DotDict

    An aspect between two celestial bodies.

    Attributes:

    Name Type Description body1Aspectable

    First body in aspect

    body2Aspectable

    Second body in aspect

    aspect_memberAspectMember

    Type of aspect

    applyingbool | None

    Whether aspect is applying

    orbfloat | None

    Orb in degrees from exact aspect

    Source code in natal/classes.py
    class Aspect(DotDict):\n    \"\"\"An aspect between two celestial bodies.\n\n    Attributes:\n        body1 (Aspectable): First body in aspect\n        body2 (Aspectable): Second body in aspect  \n        aspect_member (AspectMember): Type of aspect\n        applying (bool | None): Whether aspect is applying\n        orb (float | None): Orb in degrees from exact aspect\n    \"\"\"\n\n    body1: Aspectable\n    body2: Aspectable\n    aspect_member: AspectMember\n    applying: bool | None = None\n    orb: float | None = None\n
    "},{"location":"classes/#natal.classes.AspectMember","title":"AspectMember","text":"

    Bases: Body

    Represents an aspect in raw data. (conjunction, opposition, trine, square, sextile)

    Source code in natal/const.py
    class AspectMember(Body):\n    \"\"\"\n    Represents an aspect in raw data.\n    (conjunction, opposition, trine, square, sextile)\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.Aspectable","title":"Aspectable","text":"

    Bases: MovableBody

    Represents a celestial body that can form aspects.

    Source code in natal/classes.py
    class Aspectable(MovableBody):\n    \"\"\"\n    Represents a celestial body that can form aspects.\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.Body","title":"Body","text":"

    Bases: DotDict

    Represents a celestial body in raw data. Base class for all members.

    Source code in natal/const.py
    class Body(DotDict):\n    \"\"\"\n    Represents a celestial body in raw data.\n    Base class for all members.\n    \"\"\"\n\n    name: str\n    symbol: str\n    value: int\n    color: str\n
    "},{"location":"classes/#natal.classes.DotDict","title":"DotDict","text":"

    Bases: SimpleNamespace, Dictable

    Extends SimpleNamespace to allow for unpacking and subscript notation access.

    Source code in natal/config.py
    class DotDict(SimpleNamespace, Dictable):\n    \"\"\"\n    Extends SimpleNamespace to allow for unpacking and subscript notation access.\n    \"\"\"\n\n    pass\n
    "},{"location":"classes/#natal.classes.ElementMember","title":"ElementMember","text":"

    Bases: Body

    Represents an element in raw data. (fire, earth, air, water)

    Source code in natal/const.py
    class ElementMember(Body):\n    \"\"\"\n    Represents an element in raw data.\n    (fire, earth, air, water)\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.Extra","title":"Extra","text":"

    Bases: MovableBody

    Represents an extra celestial body (e.g. Moon's Node and Asteroids).

    Source code in natal/classes.py
    class Extra(MovableBody):\n    \"\"\"\n    Represents an extra celestial body (e.g. Moon's Node and Asteroids).\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.ExtraMember","title":"ExtraMember","text":"

    Bases: Body

    Represents an extra celestial body in raw data. (e.g. asteroids, nodes)

    Source code in natal/const.py
    class ExtraMember(Body):\n    \"\"\"\n    Represents an extra celestial body in raw data.\n    (e.g. asteroids, nodes)\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.House","title":"House","text":"

    Bases: MovableBody

    Represents a house.

    Source code in natal/classes.py
    class House(MovableBody):\n    \"\"\"\n    Represents a house.\n    \"\"\"\n\n    ruler: str = None\n    ruler_sign: str = None\n    ruler_house: int = None\n    classic_ruler: str = None\n    classic_ruler_sign: str = None\n    classic_ruler_house: int = None\n
    "},{"location":"classes/#natal.classes.HouseMember","title":"HouseMember","text":"

    Bases: Body

    Represents a house in raw data.

    Source code in natal/const.py
    class HouseMember(Body):\n    \"\"\"\n    Represents a house in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.ModalityMember","title":"ModalityMember","text":"

    Bases: Body

    Represents a modality in raw data. (cardinal, fixed, mutable)

    Source code in natal/const.py
    class ModalityMember(Body):\n    \"\"\"\n    Represents a modality in raw data.\n    (cardinal, fixed, mutable)\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.MovableBody","title":"MovableBody","text":"

    Bases: Body

    A celestial body that can move and have aspects.

    Attributes:

    Name Type Description degreefloat

    Position in degrees (0-360)

    speedfloat

    Movement speed (negative for retrograde)

    normalized_degreefloat

    Position relative to Ascendant

    Source code in natal/classes.py
    class MovableBody(Body):\n    \"\"\"A celestial body that can move and have aspects.\n\n    Attributes:\n        degree (float): Position in degrees (0-360)\n        speed (float): Movement speed (negative for retrograde)\n        normalized_degree (float): Position relative to Ascendant\n    \"\"\"\n\n    degree: float = 0\n    speed: float = 0\n    normalized_degree: float = 0\n\n    @property\n    def signed_deg(self) -> int:\n        \"\"\"Get degree within current sign.\n\n        Returns:\n            int: Degree position within sign (0-29)\n        \"\"\"\n        return int(self.degree % 30)\n\n    @property\n    def minute(self) -> int:\n        \"\"\"Get arc minutes of position.\n\n        Returns:\n            int: Arc minutes of position (0-59)\n        \"\"\"\n        minutes = (self.degree % 30 - self.signed_deg) * 60\n        return floor(minutes)\n\n    @property\n    def retro(self) -> bool:\n        \"\"\"\n        Retrograde status.\n\n        Returns:\n            bool: True if retrograde, False otherwise.\n        \"\"\"\n        return self.speed < 0\n\n    @property\n    def rx(self) -> str:\n        \"\"\"\n        Retrograde symbol.\n\n        Returns:\n            str: The retrograde symbol if retrograde, empty string otherwise.\n        \"\"\"\n        return \"\u211e\" if self.retro else \"\"\n\n    @property\n    def sign(self) -> SignMember:\n        \"\"\"\n        Return sign name, symbol, element, modality, and polarity.\n\n        Returns:\n            SignMember: The sign member.\n        \"\"\"\n        idx = int(self.degree // 30)\n        return SIGN_MEMBERS[idx]\n\n    @property\n    def dms(self) -> str:\n        \"\"\"\n        Degree Minute Second representation of the position.\n\n        Returns:\n            str: The DMS representation.\n        \"\"\"\n        op = [f\"{self.signed_deg:02d}\u00b0\", f\"{self.minute:02d}'\"]\n        if self.rx:\n            op.append(self.rx)\n        return \"\".join(op)\n\n    @property\n    def signed_dms(self) -> str:\n        \"\"\"\n        Degree Minute representation with sign.\n\n        Returns:\n            str: The signed DMS representation.\n        \"\"\"\n        op = [f\"{self.signed_deg:02d}\u00b0\", self.sign.symbol, f\"{self.minute:02d}'\"]\n        if self.rx:\n            op.append(self.rx)\n        return \"\".join(op)\n
    "},{"location":"classes/#natal.classes.MovableBody.dms","title":"dms: strproperty","text":"

    Degree Minute Second representation of the position.

    Returns:

    Name Type Description strstr

    The DMS representation.

    "},{"location":"classes/#natal.classes.MovableBody.minute","title":"minute: intproperty","text":"

    Get arc minutes of position.

    Returns:

    Name Type Description intint

    Arc minutes of position (0-59)

    "},{"location":"classes/#natal.classes.MovableBody.retro","title":"retro: boolproperty","text":"

    Retrograde status.

    Returns:

    Name Type Description boolbool

    True if retrograde, False otherwise.

    "},{"location":"classes/#natal.classes.MovableBody.rx","title":"rx: strproperty","text":"

    Retrograde symbol.

    Returns:

    Name Type Description strstr

    The retrograde symbol if retrograde, empty string otherwise.

    "},{"location":"classes/#natal.classes.MovableBody.sign","title":"sign: SignMemberproperty","text":"

    Return sign name, symbol, element, modality, and polarity.

    Returns:

    Name Type Description SignMemberSignMember

    The sign member.

    "},{"location":"classes/#natal.classes.MovableBody.signed_deg","title":"signed_deg: intproperty","text":"

    Get degree within current sign.

    Returns:

    Name Type Description intint

    Degree position within sign (0-29)

    "},{"location":"classes/#natal.classes.MovableBody.signed_dms","title":"signed_dms: strproperty","text":"

    Degree Minute representation with sign.

    Returns:

    Name Type Description strstr

    The signed DMS representation.

    "},{"location":"classes/#natal.classes.Planet","title":"Planet","text":"

    Bases: MovableBody

    Represents a planet.

    Source code in natal/classes.py
    class Planet(MovableBody):\n    \"\"\"\n    Represents a planet.\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.PlanetMember","title":"PlanetMember","text":"

    Bases: Body

    Represents a planet in raw data.

    Source code in natal/const.py
    class PlanetMember(Body):\n    \"\"\"\n    Represents a planet in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.PolarityMember","title":"PolarityMember","text":"

    Bases: Body

    Represents a polarity in raw data. (positive, negative)

    Source code in natal/const.py
    class PolarityMember(Body):\n    \"\"\"\n    Represents a polarity in raw data.\n    (positive, negative)\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.Sign","title":"Sign","text":"

    Bases: SignMember

    alias to SignMember

    Source code in natal/classes.py
    class Sign(SignMember):\n    \"\"\"\n    alias to SignMember\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.SignMember","title":"SignMember","text":"

    Bases: Body

    Represents a zodiac sign in raw data.

    Source code in natal/const.py
    class SignMember(Body):\n    \"\"\"\n    Represents a zodiac sign in raw data.\n    \"\"\"\n\n    ruler: str\n    detriment: str\n    exaltation: str\n    fall: str\n    classic_ruler: str\n    classic_detriment: str\n    modality: str\n    element: str\n    polarity: str\n
    "},{"location":"classes/#natal.classes.Vertex","title":"Vertex","text":"

    Bases: MovableBody

    Represents a vertex (Asc, Dsc, MC, IC).

    Source code in natal/classes.py
    class Vertex(MovableBody):\n    \"\"\"\n    Represents a vertex (Asc, Dsc, MC, IC).\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.VertexMember","title":"VertexMember","text":"

    Bases: Body

    Represents a vertex in raw data (asc, ic, dsc, mc).

    Source code in natal/const.py
    class VertexMember(Body):\n    \"\"\"\n    Represents a vertex in raw data (asc, ic, dsc, mc).\n    \"\"\"\n\n    ...\n
    "},{"location":"classes/#natal.classes.get_member","title":"get_member(raw_data: dict, name: str) -> DotDict","text":"

    Get a member from raw data by name.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required namestr

    The name of the member.

    required

    Returns:

    Name Type Description DotDictDotDict

    The member as a DotDict.

    Source code in natal/const.py
    def get_member(raw_data: dict, name: str) -> DotDict:\n    \"\"\"\n    Get a member from raw data by name.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n        name (str): The name of the member.\n\n    Returns:\n        DotDict: The member as a DotDict.\n    \"\"\"\n    idx = raw_data[\"name\"].index(name)\n    member = {key: raw_data[key][idx] for key in raw_data.keys()}\n    return DotDict(**member)\n
    "},{"location":"classes/#natal.classes.get_members","title":"get_members(raw_data: dict) -> list[DotDict]","text":"

    Get all members from raw data.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required

    Returns:

    Type Description list[DotDict]

    list[DotDict]: A list of members as DotDicts.

    Source code in natal/const.py
    def get_members(raw_data: dict) -> list[DotDict]:\n    \"\"\"\n    Get all members from raw data.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n\n    Returns:\n        list[DotDict]: A list of members as DotDicts.\n    \"\"\"\n    return [get_member(raw_data, name) for name in raw_data[\"name\"]]\n
    "},{"location":"config/","title":"Config","text":""},{"location":"config/#natal.config","title":"natal.config","text":""},{"location":"config/#natal.config.Chart","title":"Chart","text":"

    Bases: ModelDict

    Chart configuration settings.

    Source code in natal/config.py
    class Chart(ModelDict):\n    \"\"\"\n    Chart configuration settings.\n    \"\"\"\n\n    stroke_width: int = 1\n    stroke_opacity: float = 1\n    font: str = \"sans-serif\"\n    font_size_fraction: float = 0.55\n    inner_min_degree: float = 9\n    outer_min_degree: float = 8\n    margin_factor: float = 0.04\n    ring_thickness_fraction: float = 0.15\n    # hard-coded 2.2 and 600 due to the original symbol svg size = 20x20\n    scale_adj_factor: float = 600\n    pos_adj_factor: float = 2.2\n
    "},{"location":"config/#natal.config.Config","title":"Config","text":"

    Bases: ModelDict

    Package configuration model.

    Source code in natal/config.py
    class Config(ModelDict):\n    \"\"\"\n    Package configuration model.\n    \"\"\"\n\n    theme_type: ThemeType = \"dark\"\n    house_sys: HouseSys = HouseSys.Placidus\n    orb: Orb = Orb()\n    light_theme: LightTheme = LightTheme()\n    dark_theme: DarkTheme = DarkTheme()\n    display: Display = Display()\n    chart: Chart = Chart()\n\n    @property\n    def theme(self) -> Theme:\n        \"\"\"\n        Return theme colors based on the theme type.\n\n        Returns:\n            Theme: The theme colors.\n        \"\"\"\n        match self.theme_type:\n            case \"light\":\n                return self.light_theme\n            case \"dark\":\n                return self.dark_theme\n            case \"mono\":\n                kwargs = {key: \"#888888\" for key in self.light_theme.model_dump()}\n                kwargs[\"background\"] = \"#FFFFFF\"\n                kwargs[\"transparency\"] = 0\n                return Theme(**kwargs)\n
    "},{"location":"config/#natal.config.Config.theme","title":"theme: Themeproperty","text":"

    Return theme colors based on the theme type.

    Returns:

    Name Type Description ThemeTheme

    The theme colors.

    "},{"location":"config/#natal.config.DarkTheme","title":"DarkTheme","text":"

    Bases: Theme

    Default dark colors.

    Source code in natal/config.py
    class DarkTheme(Theme):\n    \"\"\"\n    Default dark colors.\n    \"\"\"\n\n    foreground: str = \"#F7F3F0\"\n    background: str = \"#343a40\"\n    dim: str = \"#515860\"\n
    "},{"location":"config/#natal.config.Dictable","title":"Dictable","text":"

    Bases: Mapping

    Protocols for subclasses to behave like a dict.

    Source code in natal/config.py
    class Dictable(Mapping):\n    \"\"\"\n    Protocols for subclasses to behave like a dict.\n    \"\"\"\n\n    def __getitem__(self, key: str):\n        return getattr(self, key)\n\n    def __setitem__(self, key: str, value: Any) -> None:\n        setattr(self, key, value)\n\n    def __iter__(self) -> Iterator[str]:\n        return iter(self.__dict__)\n\n    def __len__(self) -> int:\n        return len(self.__dict__)\n\n    def update(self, other: Mapping[str, Any] | None = None, **kwargs) -> None:\n        \"\"\"\n        Update the attributes with elements from another mapping or from key/value pairs.\n\n        Args:\n            other (Mapping[str, Any] | None): A mapping object to update from.\n            **kwargs: Additional key/value pairs to update with.\n        \"\"\"\n        if other is not None:\n            for key, value in other.items():\n                setattr(self, key, value)\n        for key, value in kwargs.items():\n            setattr(self, key, value)\n
    "},{"location":"config/#natal.config.Dictable.update","title":"update(other: Mapping[str, Any] | None = None, **kwargs) -> None","text":"

    Update the attributes with elements from another mapping or from key/value pairs.

    Parameters:

    Name Type Description Default otherMapping[str, Any] | None

    A mapping object to update from.

    None**kwargs

    Additional key/value pairs to update with.

    {} Source code in natal/config.py
    def update(self, other: Mapping[str, Any] | None = None, **kwargs) -> None:\n    \"\"\"\n    Update the attributes with elements from another mapping or from key/value pairs.\n\n    Args:\n        other (Mapping[str, Any] | None): A mapping object to update from.\n        **kwargs: Additional key/value pairs to update with.\n    \"\"\"\n    if other is not None:\n        for key, value in other.items():\n            setattr(self, key, value)\n    for key, value in kwargs.items():\n        setattr(self, key, value)\n
    "},{"location":"config/#natal.config.Display","title":"Display","text":"

    Bases: ModelDict

    Display settings for celestial bodies.

    Source code in natal/config.py
    class Display(ModelDict):\n    \"\"\"\n    Display settings for celestial bodies.\n    \"\"\"\n\n    sun: bool = True\n    moon: bool = True\n    mercury: bool = True\n    venus: bool = True\n    mars: bool = True\n    jupiter: bool = True\n    saturn: bool = True\n    uranus: bool = True\n    neptune: bool = True\n    pluto: bool = True\n    asc_node: bool = True\n    chiron: bool = False\n    ceres: bool = False\n    pallas: bool = False\n    juno: bool = False\n    vesta: bool = False\n    asc: bool = True\n    ic: bool = False\n    dsc: bool = False\n    mc: bool = True\n
    "},{"location":"config/#natal.config.DotDict","title":"DotDict","text":"

    Bases: SimpleNamespace, Dictable

    Extends SimpleNamespace to allow for unpacking and subscript notation access.

    Source code in natal/config.py
    class DotDict(SimpleNamespace, Dictable):\n    \"\"\"\n    Extends SimpleNamespace to allow for unpacking and subscript notation access.\n    \"\"\"\n\n    pass\n
    "},{"location":"config/#natal.config.LightTheme","title":"LightTheme","text":"

    Bases: Theme

    Default light colors.

    Source code in natal/config.py
    class LightTheme(Theme):\n    \"\"\"\n    Default light colors.\n    \"\"\"\n\n    foreground: str = \"#758492\"\n    background: str = \"#FFFDF1\"\n    dim: str = \"#A4BACD\"\n
    "},{"location":"config/#natal.config.ModelDict","title":"ModelDict","text":"

    Bases: BaseModel, Dictable

    Extends BaseModel to allow for unpacking and subscript notation access.

    Source code in natal/config.py
    class ModelDict(BaseModel, Dictable):\n    \"\"\"\n    Extends BaseModel to allow for unpacking and subscript notation access.\n    \"\"\"\n\n    # override to return keys, otherwise BaseModel.__iter__ returns key value pairs\n    def __iter__(self) -> Iterator[str]:\n        return iter(self.__dict__)\n
    "},{"location":"config/#natal.config.Orb","title":"Orb","text":"

    Bases: ModelDict

    default orb for natal chart

    Source code in natal/config.py
    class Orb(ModelDict):\n    \"\"\"default orb for natal chart\"\"\"\n\n    conjunction: int = 7\n    opposition: int = 6\n    trine: int = 6\n    square: int = 6\n    sextile: int = 5\n
    "},{"location":"config/#natal.config.Theme","title":"Theme","text":"

    Bases: ModelDict

    Default colors for the chart.

    Source code in natal/config.py
    class Theme(ModelDict):\n    \"\"\"\n    Default colors for the chart.\n    \"\"\"\n\n    fire: str = \"#ef476f\"  # fire, square, Asc\n    earth: str = \"#ffd166\"  # earth, MC\n    air: str = \"#06d6a0\"  # air, trine\n    water: str = \"#81bce7\"  # water, opposition\n    points: str = \"#118ab2\"  # lunar nodes, sextile\n    asteroids: str = \"#AA96DA\"  # asteroids\n    positive: str = \"#FFC0CB\"  # positive\n    negative: str = \"#AD8B73\"  # negative\n    others: str = \"#FFA500\"  # conjunction\n    transparency: float = 0.1\n    foreground: str\n    background: str\n    dim: str\n
    "},{"location":"const/","title":"Constants","text":""},{"location":"const/#natal.const","title":"natal.const","text":"

    Constants and utility functions for the natal package.

    "},{"location":"const/#natal.const.AspectMember","title":"AspectMember","text":"

    Bases: Body

    Represents an aspect in raw data. (conjunction, opposition, trine, square, sextile)

    Source code in natal/const.py
    class AspectMember(Body):\n    \"\"\"\n    Represents an aspect in raw data.\n    (conjunction, opposition, trine, square, sextile)\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.Body","title":"Body","text":"

    Bases: DotDict

    Represents a celestial body in raw data. Base class for all members.

    Source code in natal/const.py
    class Body(DotDict):\n    \"\"\"\n    Represents a celestial body in raw data.\n    Base class for all members.\n    \"\"\"\n\n    name: str\n    symbol: str\n    value: int\n    color: str\n
    "},{"location":"const/#natal.const.ElementMember","title":"ElementMember","text":"

    Bases: Body

    Represents an element in raw data. (fire, earth, air, water)

    Source code in natal/const.py
    class ElementMember(Body):\n    \"\"\"\n    Represents an element in raw data.\n    (fire, earth, air, water)\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.ExtraMember","title":"ExtraMember","text":"

    Bases: Body

    Represents an extra celestial body in raw data. (e.g. asteroids, nodes)

    Source code in natal/const.py
    class ExtraMember(Body):\n    \"\"\"\n    Represents an extra celestial body in raw data.\n    (e.g. asteroids, nodes)\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.HouseMember","title":"HouseMember","text":"

    Bases: Body

    Represents a house in raw data.

    Source code in natal/const.py
    class HouseMember(Body):\n    \"\"\"\n    Represents a house in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.ModalityMember","title":"ModalityMember","text":"

    Bases: Body

    Represents a modality in raw data. (cardinal, fixed, mutable)

    Source code in natal/const.py
    class ModalityMember(Body):\n    \"\"\"\n    Represents a modality in raw data.\n    (cardinal, fixed, mutable)\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.PlanetMember","title":"PlanetMember","text":"

    Bases: Body

    Represents a planet in raw data.

    Source code in natal/const.py
    class PlanetMember(Body):\n    \"\"\"\n    Represents a planet in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.PolarityMember","title":"PolarityMember","text":"

    Bases: Body

    Represents a polarity in raw data. (positive, negative)

    Source code in natal/const.py
    class PolarityMember(Body):\n    \"\"\"\n    Represents a polarity in raw data.\n    (positive, negative)\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.SignMember","title":"SignMember","text":"

    Bases: Body

    Represents a zodiac sign in raw data.

    Source code in natal/const.py
    class SignMember(Body):\n    \"\"\"\n    Represents a zodiac sign in raw data.\n    \"\"\"\n\n    ruler: str\n    detriment: str\n    exaltation: str\n    fall: str\n    classic_ruler: str\n    classic_detriment: str\n    modality: str\n    element: str\n    polarity: str\n
    "},{"location":"const/#natal.const.VertexMember","title":"VertexMember","text":"

    Bases: Body

    Represents a vertex in raw data (asc, ic, dsc, mc).

    Source code in natal/const.py
    class VertexMember(Body):\n    \"\"\"\n    Represents a vertex in raw data (asc, ic, dsc, mc).\n    \"\"\"\n\n    ...\n
    "},{"location":"const/#natal.const.get_member","title":"get_member(raw_data: dict, name: str) -> DotDict","text":"

    Get a member from raw data by name.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required namestr

    The name of the member.

    required

    Returns:

    Name Type Description DotDictDotDict

    The member as a DotDict.

    Source code in natal/const.py
    def get_member(raw_data: dict, name: str) -> DotDict:\n    \"\"\"\n    Get a member from raw data by name.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n        name (str): The name of the member.\n\n    Returns:\n        DotDict: The member as a DotDict.\n    \"\"\"\n    idx = raw_data[\"name\"].index(name)\n    member = {key: raw_data[key][idx] for key in raw_data.keys()}\n    return DotDict(**member)\n
    "},{"location":"const/#natal.const.get_members","title":"get_members(raw_data: dict) -> list[DotDict]","text":"

    Get all members from raw data.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required

    Returns:

    Type Description list[DotDict]

    list[DotDict]: A list of members as DotDicts.

    Source code in natal/const.py
    def get_members(raw_data: dict) -> list[DotDict]:\n    \"\"\"\n    Get all members from raw data.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n\n    Returns:\n        list[DotDict]: A list of members as DotDicts.\n    \"\"\"\n    return [get_member(raw_data, name) for name in raw_data[\"name\"]]\n
    "},{"location":"data/","title":"Data","text":""},{"location":"data/#natal.data","title":"natal.data","text":""},{"location":"data/#natal.data.AspectMember","title":"AspectMember","text":"

    Bases: Body

    Represents an aspect in raw data. (conjunction, opposition, trine, square, sextile)

    Source code in natal/const.py
    class AspectMember(Body):\n    \"\"\"\n    Represents an aspect in raw data.\n    (conjunction, opposition, trine, square, sextile)\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.Data","title":"Data","text":"

    Bases: DotDict

    Data object for a natal chart.

    Source code in natal/data.py
    class Data(DotDict):\n    \"\"\"\n    Data object for a natal chart.\n    \"\"\"\n\n    cities = pd.read_csv(data_folder / \"cities.csv.gz\")\n\n    def __init__(\n        self,\n        name: str,\n        city: str,\n        dt: datetime | str,\n        config: Config = Config(),\n    ) -> None:\n        \"\"\"Initialize a natal chart data object.\n\n        Args:\n            name (str): The name for this chart\n            city (str): City name to lookup coordinates\n            dt (datetime | str): Date and time as datetime object or string\n            config (Config): Configuration settings\n        \"\"\"\n        self.name = name\n        self.city = city\n        if isinstance(dt, str):\n            dt = str_to_dt(dt)\n        self.dt = dt\n        self.config = config\n        self.lat: float = None\n        self.lon: float = None\n        self.timezone: str = None\n        self.house_sys = config.house_sys\n        self.houses: list[House] = []\n        self.planets: list[Planet] = []\n        self.extras: list[Extra] = []\n        self.vertices: list[Vertex] = []\n        self.signs: list[Sign] = []\n        self.aspects: list[Aspect] = []\n        self.quadrants: list[list[Aspectable]] = []\n        self.set_lat_lon()\n        self.set_houses_vertices()\n        self.set_movable_bodies()\n        self.set_aspectable()\n        self.set_signs()\n        self.set_normalized_degrees()\n        self.set_aspects()\n        self.set_rulers()\n        self.set_quadrants()\n\n    @property\n    def julian_day(self) -> float:\n        \"\"\"Convert dt to UTC and return Julian day.\n\n        Returns:\n            float: The Julian day number\n        \"\"\"\n        local_tz = ZoneInfo(self.timezone)\n        local_dt = self.dt.replace(tzinfo=local_tz)\n        utc_dt = local_dt.astimezone(ZoneInfo(\"UTC\"))\n        return swe.date_conversion(\n            utc_dt.year, utc_dt.month, utc_dt.day, utc_dt.hour + utc_dt.minute / 60\n        )[1]\n\n    def set_lat_lon(self) -> None:\n        \"\"\"Set the geographical information of a city.\"\"\"\n        info = self.cities[self.cities[\"name\"].str.lower() == self.city.lower()].iloc[0]\n        self.lat = float(info[\"lat\"])\n        self.lon = float(info[\"lon\"])\n        self.timezone = info[\"timezone\"]\n\n    def set_movable_bodies(self) -> None:\n        \"\"\"Set the positions of the planets and other celestial bodies.\"\"\"\n        self.planets = self.set_positions(PLANET_MEMBERS)\n        self.extras = self.set_positions(EXTRA_MEMBERS)\n\n    def set_houses_vertices(self) -> None:\n        \"\"\"Calculate the cusps of the houses and set the vertices.\"\"\"\n        cusps, (asc_deg, mc_deg, *_) = swe.houses(\n            self.julian_day,\n            self.lat,\n            self.lon,\n            self.house_sys.encode(),\n        )\n\n        for house, cusp in zip(HOUSE_MEMBERS, cusps):\n            house_body = House(\n                **house,\n                degree=floor(cusp * 100) / 100,\n            )\n            self.houses.append(house_body)\n\n        self.vertices = [\n            Vertex(degree=asc_deg, **VERTEX_MEMBERS[0]),\n            Vertex(degree=(mc_deg + 180) % 360, **VERTEX_MEMBERS[1]),\n            Vertex(degree=(asc_deg + 180) % 360, **VERTEX_MEMBERS[2]),\n            Vertex(degree=mc_deg, **VERTEX_MEMBERS[3]),\n        ]\n\n        for v in self.vertices:\n            setattr(self, v.name, v)\n\n    def set_aspectable(self) -> None:\n        \"\"\"Set the aspectable celestial bodies based on the display configuration.\"\"\"\n        self.aspectables = [\n            body\n            for body in (self.planets + self.extras + self.vertices)\n            if self.config.display[body.name]\n        ]\n\n    def set_signs(self) -> None:\n        \"\"\"Set the signs of the zodiac.\"\"\"\n        for i, sign_member in enumerate(SIGN_MEMBERS):\n            sign = Sign(\n                **sign_member,\n                degree=i * 30,\n            )\n            self.signs.append(sign)\n\n    def set_aspects(self) -> None:\n        \"\"\"Set the aspects between the aspectable celestial bodies.\"\"\"\n        body_pairs = pairs(self.aspectables)\n        self.aspects = self.calculate_aspects(body_pairs)\n\n    def set_normalized_degrees(self) -> None:\n        \"\"\"Normalize the positions of celestial bodies relative to the first house.\"\"\"\n        bodies = self.signs + self.planets + self.extras + self.vertices + self.houses\n        for body in bodies:\n            body.normalized_degree = self.normalize(body.degree)\n\n    def set_rulers(self) -> None:\n        \"\"\"Set the rulers for each house.\"\"\"\n        for house in self.houses:\n            ruler = getattr(self, house.sign.ruler)\n            classic_ruler = getattr(self, house.sign.classic_ruler)\n            house.ruler = ruler.name\n            house.ruler_sign = f\"{ruler.sign.symbol}\"\n            house.ruler_house = self.house_of(ruler)\n            house.classic_ruler = classic_ruler.name\n            house.classic_ruler_sign = (\n                f\"{classic_ruler.sign.symbol} {classic_ruler.sign.name}\"\n            )\n            house.classic_ruler_house = self.house_of(classic_ruler)\n\n    def set_quadrants(self) -> None:\n        \"\"\"Set the distribution of celestial bodies in quadrants.\"\"\"\n        bodies = [b for b in self.aspectables if b not in self.vertices]\n        _, ic, dsc, mc = [v.normalized_degree for v in self.vertices]\n\n        first = [b for b in bodies if b.normalized_degree < ic]\n        second = [b for b in bodies if ic <= b.normalized_degree < dsc]\n        third = [b for b in bodies if dsc <= b.normalized_degree < mc]\n        fourth = [b for b in bodies if mc <= b.normalized_degree]\n        self.quadrants = [first, second, third, fourth]\n\n    def __str__(self) -> str:\n        \"\"\"Get string representation of the Data object.\n\n        Returns:\n            str: Formatted string showing chart data\n        \"\"\"\n        op = \"\"\n        op += f\"Name: {self.name}\\n\"\n        op += f\"City: {self.city}\\n\"\n        op += f\"Date: {self.dt}\\n\"\n        op += f\"Latitude: {self.lat}\\n\"\n        op += f\"Longitude: {self.lon}\\n\"\n        op += f\"House System: {self.house_sys}\\n\"\n        op += \"Planets:\\n\"\n        for e in self.planets:\n            op += f\"{e.name}: {e.signed_dms}\\n\"\n        op += \"Extras:\\n\"\n        for e in self.extras:\n            op += f\"{e.name}: {e.signed_dms}\\n\"\n        op += f\"Asc: {self.asc.signed_dms}\\n\"\n        op += f\"MC: {self.mc.signed_dms}\\n\"\n        op += \"Houses:\\n\"\n        for e in self.houses:\n            op += f\"{e.name}: {e.signed_dms}\\n\"\n        op += \"Signs:\\n\"\n        for e in self.signs:\n            op += f\"{e.name}: degree={e.degree:.2f}, ruler={e.ruler}, color={e.color}, modality={e.modality}, element={e.element}, polarity={e.polarity}\\n\"\n        op += \"Aspects:\\n\"\n        for e in self.aspects:\n            op += f\"{e.body1.name} {e.aspect_member.symbol} {e.body2.name}: {e.aspect_member.color}\\n\"\n        return op\n\n    # utils ===============================\n\n    def set_positions(self, bodies: list[Body]) -> list[Aspectable]:\n        \"\"\"Set the positions of celestial bodies.\n\n        Args:\n            bodies (list[Body]): List of celestial body definitions\n\n        Returns:\n            list[Aspectable]: List of aspectable bodies with positions set\n        \"\"\"\n        output = []\n        for body in bodies:\n            ((lon, _, _, speed, *_), _) = swe.calc_ut(self.julian_day, body.value)\n            pos = Aspectable(\n                **body,\n                degree=lon,\n                speed=speed,\n            )\n            setattr(self, body.name, pos)\n            output.append(pos)\n        return output\n\n    def house_of(self, body: Body) -> int:\n        \"\"\"Get the house number containing a celestial body.\n\n        Args:\n            body (Body): The celestial body to locate\n\n        Returns:\n            int: House number (1-12) containing the body\n        \"\"\"\n        sorted_houses = sorted(self.houses, key=lambda x: x.degree, reverse=True)\n        for house in sorted_houses:\n            if body.degree >= house.degree:\n                return house.value\n        return sorted_houses[0].value\n\n    def normalize(self, degree: float) -> float:\n        \"\"\"Normalize a degree relative to the Ascendant.\n\n        Args:\n            degree (float): The degree to normalize\n\n        Returns:\n            float: Normalized degree (0-360)\n        \"\"\"\n        return (degree - self.asc.degree + 360) % 360\n\n    def calculate_aspects(self, body_pairs: BodyPairs) -> list[Aspect]:\n        \"\"\"Calculate aspects between pairs of celestial bodies.\n\n        Args:\n            body_pairs (BodyPairs): Pairs of bodies to check for aspects\n\n        Returns:\n            list[Aspect]: List of aspects found between the bodies\n        \"\"\"\n        output = []\n        for b1, b2 in body_pairs:\n            sorted_bodies = sorted([b1, b2], key=lambda x: x.degree)\n            org_angle = sorted_bodies[1].degree - sorted_bodies[0].degree\n            # get the smaller angle\n            angle = 360 - org_angle if org_angle > 180 else org_angle\n            for aspect_member in ASPECT_MEMBERS:\n                orb_val = self.config.orb[aspect_member.name]\n                if not orb_val:\n                    continue\n                max_orb = aspect_member.value + orb_val\n                min_orb = aspect_member.value - orb_val\n                if min_orb <= angle <= max_orb:\n                    applying = sorted_bodies[0].speed > sorted_bodies[1].speed\n                    if angle < aspect_member.value:\n                        applying = not applying\n                    applying = not applying if org_angle > 180 else applying\n                    output.append(\n                        Aspect(\n                            body1=b1,\n                            body2=b2,\n                            aspect_member=aspect_member,\n                            applying=applying,\n                            orb=abs(angle - aspect_member.value),\n                        )\n                    )\n        return output\n\n    def composite_aspects_pairs(self, data2: Self) -> BodyPairs:\n        \"\"\"Generate pairs of aspectable bodies for composite chart.\n\n        Args:\n            data2 (Self): Second chart data to compare against\n\n        Returns:\n            BodyPairs: Pairs of bodies to check for aspects\n        \"\"\"\n        return itertools.product(self.aspectables, data2.aspectables)\n
    "},{"location":"data/#natal.data.Data.julian_day","title":"julian_day: floatproperty","text":"

    Convert dt to UTC and return Julian day.

    Returns:

    Name Type Description floatfloat

    The Julian day number

    "},{"location":"data/#natal.data.Data.__init__","title":"__init__(name: str, city: str, dt: datetime | str, config: Config = Config()) -> None","text":"

    Initialize a natal chart data object.

    Parameters:

    Name Type Description Default namestr

    The name for this chart

    required citystr

    City name to lookup coordinates

    required dtdatetime | str

    Date and time as datetime object or string

    required configConfig

    Configuration settings

    Config() Source code in natal/data.py
    def __init__(\n    self,\n    name: str,\n    city: str,\n    dt: datetime | str,\n    config: Config = Config(),\n) -> None:\n    \"\"\"Initialize a natal chart data object.\n\n    Args:\n        name (str): The name for this chart\n        city (str): City name to lookup coordinates\n        dt (datetime | str): Date and time as datetime object or string\n        config (Config): Configuration settings\n    \"\"\"\n    self.name = name\n    self.city = city\n    if isinstance(dt, str):\n        dt = str_to_dt(dt)\n    self.dt = dt\n    self.config = config\n    self.lat: float = None\n    self.lon: float = None\n    self.timezone: str = None\n    self.house_sys = config.house_sys\n    self.houses: list[House] = []\n    self.planets: list[Planet] = []\n    self.extras: list[Extra] = []\n    self.vertices: list[Vertex] = []\n    self.signs: list[Sign] = []\n    self.aspects: list[Aspect] = []\n    self.quadrants: list[list[Aspectable]] = []\n    self.set_lat_lon()\n    self.set_houses_vertices()\n    self.set_movable_bodies()\n    self.set_aspectable()\n    self.set_signs()\n    self.set_normalized_degrees()\n    self.set_aspects()\n    self.set_rulers()\n    self.set_quadrants()\n
    "},{"location":"data/#natal.data.Data.__str__","title":"__str__() -> str","text":"

    Get string representation of the Data object.

    Returns:

    Name Type Description strstr

    Formatted string showing chart data

    Source code in natal/data.py
    def __str__(self) -> str:\n    \"\"\"Get string representation of the Data object.\n\n    Returns:\n        str: Formatted string showing chart data\n    \"\"\"\n    op = \"\"\n    op += f\"Name: {self.name}\\n\"\n    op += f\"City: {self.city}\\n\"\n    op += f\"Date: {self.dt}\\n\"\n    op += f\"Latitude: {self.lat}\\n\"\n    op += f\"Longitude: {self.lon}\\n\"\n    op += f\"House System: {self.house_sys}\\n\"\n    op += \"Planets:\\n\"\n    for e in self.planets:\n        op += f\"{e.name}: {e.signed_dms}\\n\"\n    op += \"Extras:\\n\"\n    for e in self.extras:\n        op += f\"{e.name}: {e.signed_dms}\\n\"\n    op += f\"Asc: {self.asc.signed_dms}\\n\"\n    op += f\"MC: {self.mc.signed_dms}\\n\"\n    op += \"Houses:\\n\"\n    for e in self.houses:\n        op += f\"{e.name}: {e.signed_dms}\\n\"\n    op += \"Signs:\\n\"\n    for e in self.signs:\n        op += f\"{e.name}: degree={e.degree:.2f}, ruler={e.ruler}, color={e.color}, modality={e.modality}, element={e.element}, polarity={e.polarity}\\n\"\n    op += \"Aspects:\\n\"\n    for e in self.aspects:\n        op += f\"{e.body1.name} {e.aspect_member.symbol} {e.body2.name}: {e.aspect_member.color}\\n\"\n    return op\n
    "},{"location":"data/#natal.data.Data.calculate_aspects","title":"calculate_aspects(body_pairs: BodyPairs) -> list[Aspect]","text":"

    Calculate aspects between pairs of celestial bodies.

    Parameters:

    Name Type Description Default body_pairsBodyPairs

    Pairs of bodies to check for aspects

    required

    Returns:

    Type Description list[Aspect]

    list[Aspect]: List of aspects found between the bodies

    Source code in natal/data.py
    def calculate_aspects(self, body_pairs: BodyPairs) -> list[Aspect]:\n    \"\"\"Calculate aspects between pairs of celestial bodies.\n\n    Args:\n        body_pairs (BodyPairs): Pairs of bodies to check for aspects\n\n    Returns:\n        list[Aspect]: List of aspects found between the bodies\n    \"\"\"\n    output = []\n    for b1, b2 in body_pairs:\n        sorted_bodies = sorted([b1, b2], key=lambda x: x.degree)\n        org_angle = sorted_bodies[1].degree - sorted_bodies[0].degree\n        # get the smaller angle\n        angle = 360 - org_angle if org_angle > 180 else org_angle\n        for aspect_member in ASPECT_MEMBERS:\n            orb_val = self.config.orb[aspect_member.name]\n            if not orb_val:\n                continue\n            max_orb = aspect_member.value + orb_val\n            min_orb = aspect_member.value - orb_val\n            if min_orb <= angle <= max_orb:\n                applying = sorted_bodies[0].speed > sorted_bodies[1].speed\n                if angle < aspect_member.value:\n                    applying = not applying\n                applying = not applying if org_angle > 180 else applying\n                output.append(\n                    Aspect(\n                        body1=b1,\n                        body2=b2,\n                        aspect_member=aspect_member,\n                        applying=applying,\n                        orb=abs(angle - aspect_member.value),\n                    )\n                )\n    return output\n
    "},{"location":"data/#natal.data.Data.composite_aspects_pairs","title":"composite_aspects_pairs(data2: Self) -> BodyPairs","text":"

    Generate pairs of aspectable bodies for composite chart.

    Parameters:

    Name Type Description Default data2Self

    Second chart data to compare against

    required

    Returns:

    Name Type Description BodyPairsBodyPairs

    Pairs of bodies to check for aspects

    Source code in natal/data.py
    def composite_aspects_pairs(self, data2: Self) -> BodyPairs:\n    \"\"\"Generate pairs of aspectable bodies for composite chart.\n\n    Args:\n        data2 (Self): Second chart data to compare against\n\n    Returns:\n        BodyPairs: Pairs of bodies to check for aspects\n    \"\"\"\n    return itertools.product(self.aspectables, data2.aspectables)\n
    "},{"location":"data/#natal.data.Data.house_of","title":"house_of(body: Body) -> int","text":"

    Get the house number containing a celestial body.

    Parameters:

    Name Type Description Default bodyBody

    The celestial body to locate

    required

    Returns:

    Name Type Description intint

    House number (1-12) containing the body

    Source code in natal/data.py
    def house_of(self, body: Body) -> int:\n    \"\"\"Get the house number containing a celestial body.\n\n    Args:\n        body (Body): The celestial body to locate\n\n    Returns:\n        int: House number (1-12) containing the body\n    \"\"\"\n    sorted_houses = sorted(self.houses, key=lambda x: x.degree, reverse=True)\n    for house in sorted_houses:\n        if body.degree >= house.degree:\n            return house.value\n    return sorted_houses[0].value\n
    "},{"location":"data/#natal.data.Data.normalize","title":"normalize(degree: float) -> float","text":"

    Normalize a degree relative to the Ascendant.

    Parameters:

    Name Type Description Default degreefloat

    The degree to normalize

    required

    Returns:

    Name Type Description floatfloat

    Normalized degree (0-360)

    Source code in natal/data.py
    def normalize(self, degree: float) -> float:\n    \"\"\"Normalize a degree relative to the Ascendant.\n\n    Args:\n        degree (float): The degree to normalize\n\n    Returns:\n        float: Normalized degree (0-360)\n    \"\"\"\n    return (degree - self.asc.degree + 360) % 360\n
    "},{"location":"data/#natal.data.Data.set_aspectable","title":"set_aspectable() -> None","text":"

    Set the aspectable celestial bodies based on the display configuration.

    Source code in natal/data.py
    def set_aspectable(self) -> None:\n    \"\"\"Set the aspectable celestial bodies based on the display configuration.\"\"\"\n    self.aspectables = [\n        body\n        for body in (self.planets + self.extras + self.vertices)\n        if self.config.display[body.name]\n    ]\n
    "},{"location":"data/#natal.data.Data.set_aspects","title":"set_aspects() -> None","text":"

    Set the aspects between the aspectable celestial bodies.

    Source code in natal/data.py
    def set_aspects(self) -> None:\n    \"\"\"Set the aspects between the aspectable celestial bodies.\"\"\"\n    body_pairs = pairs(self.aspectables)\n    self.aspects = self.calculate_aspects(body_pairs)\n
    "},{"location":"data/#natal.data.Data.set_houses_vertices","title":"set_houses_vertices() -> None","text":"

    Calculate the cusps of the houses and set the vertices.

    Source code in natal/data.py
    def set_houses_vertices(self) -> None:\n    \"\"\"Calculate the cusps of the houses and set the vertices.\"\"\"\n    cusps, (asc_deg, mc_deg, *_) = swe.houses(\n        self.julian_day,\n        self.lat,\n        self.lon,\n        self.house_sys.encode(),\n    )\n\n    for house, cusp in zip(HOUSE_MEMBERS, cusps):\n        house_body = House(\n            **house,\n            degree=floor(cusp * 100) / 100,\n        )\n        self.houses.append(house_body)\n\n    self.vertices = [\n        Vertex(degree=asc_deg, **VERTEX_MEMBERS[0]),\n        Vertex(degree=(mc_deg + 180) % 360, **VERTEX_MEMBERS[1]),\n        Vertex(degree=(asc_deg + 180) % 360, **VERTEX_MEMBERS[2]),\n        Vertex(degree=mc_deg, **VERTEX_MEMBERS[3]),\n    ]\n\n    for v in self.vertices:\n        setattr(self, v.name, v)\n
    "},{"location":"data/#natal.data.Data.set_lat_lon","title":"set_lat_lon() -> None","text":"

    Set the geographical information of a city.

    Source code in natal/data.py
    def set_lat_lon(self) -> None:\n    \"\"\"Set the geographical information of a city.\"\"\"\n    info = self.cities[self.cities[\"name\"].str.lower() == self.city.lower()].iloc[0]\n    self.lat = float(info[\"lat\"])\n    self.lon = float(info[\"lon\"])\n    self.timezone = info[\"timezone\"]\n
    "},{"location":"data/#natal.data.Data.set_movable_bodies","title":"set_movable_bodies() -> None","text":"

    Set the positions of the planets and other celestial bodies.

    Source code in natal/data.py
    def set_movable_bodies(self) -> None:\n    \"\"\"Set the positions of the planets and other celestial bodies.\"\"\"\n    self.planets = self.set_positions(PLANET_MEMBERS)\n    self.extras = self.set_positions(EXTRA_MEMBERS)\n
    "},{"location":"data/#natal.data.Data.set_normalized_degrees","title":"set_normalized_degrees() -> None","text":"

    Normalize the positions of celestial bodies relative to the first house.

    Source code in natal/data.py
    def set_normalized_degrees(self) -> None:\n    \"\"\"Normalize the positions of celestial bodies relative to the first house.\"\"\"\n    bodies = self.signs + self.planets + self.extras + self.vertices + self.houses\n    for body in bodies:\n        body.normalized_degree = self.normalize(body.degree)\n
    "},{"location":"data/#natal.data.Data.set_positions","title":"set_positions(bodies: list[Body]) -> list[Aspectable]","text":"

    Set the positions of celestial bodies.

    Parameters:

    Name Type Description Default bodieslist[Body]

    List of celestial body definitions

    required

    Returns:

    Type Description list[Aspectable]

    list[Aspectable]: List of aspectable bodies with positions set

    Source code in natal/data.py
    def set_positions(self, bodies: list[Body]) -> list[Aspectable]:\n    \"\"\"Set the positions of celestial bodies.\n\n    Args:\n        bodies (list[Body]): List of celestial body definitions\n\n    Returns:\n        list[Aspectable]: List of aspectable bodies with positions set\n    \"\"\"\n    output = []\n    for body in bodies:\n        ((lon, _, _, speed, *_), _) = swe.calc_ut(self.julian_day, body.value)\n        pos = Aspectable(\n            **body,\n            degree=lon,\n            speed=speed,\n        )\n        setattr(self, body.name, pos)\n        output.append(pos)\n    return output\n
    "},{"location":"data/#natal.data.Data.set_quadrants","title":"set_quadrants() -> None","text":"

    Set the distribution of celestial bodies in quadrants.

    Source code in natal/data.py
    def set_quadrants(self) -> None:\n    \"\"\"Set the distribution of celestial bodies in quadrants.\"\"\"\n    bodies = [b for b in self.aspectables if b not in self.vertices]\n    _, ic, dsc, mc = [v.normalized_degree for v in self.vertices]\n\n    first = [b for b in bodies if b.normalized_degree < ic]\n    second = [b for b in bodies if ic <= b.normalized_degree < dsc]\n    third = [b for b in bodies if dsc <= b.normalized_degree < mc]\n    fourth = [b for b in bodies if mc <= b.normalized_degree]\n    self.quadrants = [first, second, third, fourth]\n
    "},{"location":"data/#natal.data.Data.set_rulers","title":"set_rulers() -> None","text":"

    Set the rulers for each house.

    Source code in natal/data.py
    def set_rulers(self) -> None:\n    \"\"\"Set the rulers for each house.\"\"\"\n    for house in self.houses:\n        ruler = getattr(self, house.sign.ruler)\n        classic_ruler = getattr(self, house.sign.classic_ruler)\n        house.ruler = ruler.name\n        house.ruler_sign = f\"{ruler.sign.symbol}\"\n        house.ruler_house = self.house_of(ruler)\n        house.classic_ruler = classic_ruler.name\n        house.classic_ruler_sign = (\n            f\"{classic_ruler.sign.symbol} {classic_ruler.sign.name}\"\n        )\n        house.classic_ruler_house = self.house_of(classic_ruler)\n
    "},{"location":"data/#natal.data.Data.set_signs","title":"set_signs() -> None","text":"

    Set the signs of the zodiac.

    Source code in natal/data.py
    def set_signs(self) -> None:\n    \"\"\"Set the signs of the zodiac.\"\"\"\n    for i, sign_member in enumerate(SIGN_MEMBERS):\n        sign = Sign(\n            **sign_member,\n            degree=i * 30,\n        )\n        self.signs.append(sign)\n
    "},{"location":"data/#natal.data.DotDict","title":"DotDict","text":"

    Bases: SimpleNamespace, Dictable

    Extends SimpleNamespace to allow for unpacking and subscript notation access.

    Source code in natal/config.py
    class DotDict(SimpleNamespace, Dictable):\n    \"\"\"\n    Extends SimpleNamespace to allow for unpacking and subscript notation access.\n    \"\"\"\n\n    pass\n
    "},{"location":"data/#natal.data.ElementMember","title":"ElementMember","text":"

    Bases: Body

    Represents an element in raw data. (fire, earth, air, water)

    Source code in natal/const.py
    class ElementMember(Body):\n    \"\"\"\n    Represents an element in raw data.\n    (fire, earth, air, water)\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.ExtraMember","title":"ExtraMember","text":"

    Bases: Body

    Represents an extra celestial body in raw data. (e.g. asteroids, nodes)

    Source code in natal/const.py
    class ExtraMember(Body):\n    \"\"\"\n    Represents an extra celestial body in raw data.\n    (e.g. asteroids, nodes)\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.HouseMember","title":"HouseMember","text":"

    Bases: Body

    Represents a house in raw data.

    Source code in natal/const.py
    class HouseMember(Body):\n    \"\"\"\n    Represents a house in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.ModalityMember","title":"ModalityMember","text":"

    Bases: Body

    Represents a modality in raw data. (cardinal, fixed, mutable)

    Source code in natal/const.py
    class ModalityMember(Body):\n    \"\"\"\n    Represents a modality in raw data.\n    (cardinal, fixed, mutable)\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.PlanetMember","title":"PlanetMember","text":"

    Bases: Body

    Represents a planet in raw data.

    Source code in natal/const.py
    class PlanetMember(Body):\n    \"\"\"\n    Represents a planet in raw data.\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.PolarityMember","title":"PolarityMember","text":"

    Bases: Body

    Represents a polarity in raw data. (positive, negative)

    Source code in natal/const.py
    class PolarityMember(Body):\n    \"\"\"\n    Represents a polarity in raw data.\n    (positive, negative)\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.SignMember","title":"SignMember","text":"

    Bases: Body

    Represents a zodiac sign in raw data.

    Source code in natal/const.py
    class SignMember(Body):\n    \"\"\"\n    Represents a zodiac sign in raw data.\n    \"\"\"\n\n    ruler: str\n    detriment: str\n    exaltation: str\n    fall: str\n    classic_ruler: str\n    classic_detriment: str\n    modality: str\n    element: str\n    polarity: str\n
    "},{"location":"data/#natal.data.VertexMember","title":"VertexMember","text":"

    Bases: Body

    Represents a vertex in raw data (asc, ic, dsc, mc).

    Source code in natal/const.py
    class VertexMember(Body):\n    \"\"\"\n    Represents a vertex in raw data (asc, ic, dsc, mc).\n    \"\"\"\n\n    ...\n
    "},{"location":"data/#natal.data.get_member","title":"get_member(raw_data: dict, name: str) -> DotDict","text":"

    Get a member from raw data by name.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required namestr

    The name of the member.

    required

    Returns:

    Name Type Description DotDictDotDict

    The member as a DotDict.

    Source code in natal/const.py
    def get_member(raw_data: dict, name: str) -> DotDict:\n    \"\"\"\n    Get a member from raw data by name.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n        name (str): The name of the member.\n\n    Returns:\n        DotDict: The member as a DotDict.\n    \"\"\"\n    idx = raw_data[\"name\"].index(name)\n    member = {key: raw_data[key][idx] for key in raw_data.keys()}\n    return DotDict(**member)\n
    "},{"location":"data/#natal.data.get_members","title":"get_members(raw_data: dict) -> list[DotDict]","text":"

    Get all members from raw data.

    Parameters:

    Name Type Description Default raw_datadict

    The raw data dictionary.

    required

    Returns:

    Type Description list[DotDict]

    list[DotDict]: A list of members as DotDicts.

    Source code in natal/const.py
    def get_members(raw_data: dict) -> list[DotDict]:\n    \"\"\"\n    Get all members from raw data.\n\n    Args:\n        raw_data (dict): The raw data dictionary.\n\n    Returns:\n        list[DotDict]: A list of members as DotDicts.\n    \"\"\"\n    return [get_member(raw_data, name) for name in raw_data[\"name\"]]\n
    "},{"location":"license/","title":"License","text":"

    MIT License

    Copyright (c) 2022 Kelvin Ng

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    "},{"location":"report/","title":"Report","text":""},{"location":"report/#natal.report","title":"natal.report","text":"

    This module generates a detailed astrological report in PDF format. It includes information about birth data, elements, modalities, polarities, hemispheres, quadrants, signs, houses, and celestial bodies. The report is created using the natal astrology library and rendered as an HTML document, which is then converted to a PDF.

    Classes:

    Name Description Report

    Generates the astrological report.

    "},{"location":"report/#natal.report.Report","title":"Report","text":"

    Generates an astrological report based on provided data.

    Attributes:

    Name Type Description data1Data

    The primary data for the report.

    data2Data

    The secondary data for the report, if any.

    Source code in natal/report.py
    class Report:\n    \"\"\"\n    Generates an astrological report based on provided data.\n\n    Attributes:\n        data1: The primary data for the report.\n        data2: The secondary data for the report, if any.\n    \"\"\"\n\n    def __init__(self, data1: Data, data2: Data | None = None):\n        \"\"\"\n        Initializes the Report with the given data.\n\n        Args:\n            data1: The primary data for the report.\n            data2: The secondary data for the report, if any.\n        \"\"\"\n        self.data1: Data = data1\n        self.data2: Data = data2\n\n    @property\n    def basic_info(self) -> Grid:\n        \"\"\"\n        Generates basic information about the provided data.\n\n        Returns:\n            A grid containing the name, city, and birth date/time.\n        \"\"\"\n        time_fmt = \"%Y-%m-%d %H:%M\"\n        dt1 = self.data1.dt.strftime(time_fmt)\n        output = [[\"name\", \"city\", \"birth\"]]\n        output.append([self.data1.name, self.data1.city, dt1])\n        if self.data2:\n            dt2 = self.data2.dt.strftime(time_fmt)\n            output.append([self.data2.name, self.data2.city, dt2])\n        return list(zip(*output))\n\n    @property\n    def element_vs_modality(self) -> Grid:\n        \"\"\"\n        Generates a grid comparing elements and modalities.\n\n        Returns:\n            A grid comparing elements and modalities.\n        \"\"\"\n        aspectable1 = self.data1.aspectables\n        element_symbols = [svg_of(ele.name) for ele in ELEMENTS]\n        grid = [[\"\"] + element_symbols + [\"sum\"]]\n        element_count = defaultdict(int)\n        for modality in MODALITY_MEMBERS:\n            row = [svg_of(modality.name)]\n            modality_count = 0\n            for element in ELEMENTS:\n                count = 0\n                symbols = \"\"\n                for body in aspectable1:\n                    if (\n                        body.sign.element == element.name\n                        and body.sign.modality == modality.name\n                    ):\n                        symbols += svg_of(body.name)\n                        count += 1\n                        element_count[element.name] += 1\n                row.append(symbols)\n                modality_count += count\n            row.append(modality_count)\n            grid.append(row)\n        grid.append(\n            [\"sum\"] + list(element_count.values()) + [sum(element_count.values())]\n        )\n        grid.append(\n            [\n                \"\u25d0\",\n                f\"null:{element_count['fire'] + element_count['air']} pos\",\n                f\"null:{element_count['water'] + element_count['earth']} neg\",\n                \"\",\n            ]\n        )\n        return grid\n\n    @property\n    def quadrants_vs_hemisphere(self) -> Grid:\n        \"\"\"\n        Generates a grid comparing quadrants and hemispheres.\n\n        Returns:\n            A grid comparing quadrants and hemispheres.\n        \"\"\"\n        q = self.data1.quadrants\n        first_q = [svg_of(body.name) for body in q[0]]\n        second_q = [svg_of(body.name) for body in q[1]]\n        third_q = [svg_of(body.name) for body in q[2]]\n        forth_q = [svg_of(body.name) for body in q[3]]\n        hemi_symbols = [\"\u2190\", \"\u2192\", \"\u2191\", \"\u2193\"]\n        grid = [[\"\"] + hemi_symbols[:2] + [\"sum\"]]\n        grid += [[\"\u2191\"] + [forth_q, third_q] + [len(q[3] + q[2])]]\n        grid += [[\"\u2193\"] + [first_q, second_q] + [len(q[3] + q[2])]]\n        grid += [\n            [\"sum\"]\n            + [len(q[3] + q[0]), len(q[1] + q[2])]\n            + [len(q[0] + q[1] + q[2] + q[3])]\n        ]\n        return grid\n\n    @property\n    def signs(self) -> Grid:\n        \"\"\"\n        Generates a grid of signs and their corresponding bodies.\n\n        Returns:\n            A grid of signs and their corresponding bodies\n        \"\"\"\n        grid = [[\"sign\", \"bodies\", \"sum\"]]\n        for sign in SIGN_MEMBERS:\n            bodies = [\n                svg_of(b.name)\n                for b in self.data1.aspectables\n                if b.sign.name == sign.name\n            ]\n            grid.append([svg_of(sign.name), \"\".join(bodies), len(bodies) or \"\"])\n        return grid\n\n    @property\n    def houses(self) -> Grid:\n        \"\"\"\n        Generates a grid of houses and their corresponding bodies.\n\n        Returns:\n            A grid of houses and their corresponding bodies.\n        \"\"\"\n        grid = [[\"house\", \"cusp\", \"bodies\", \"sum\"]]\n        for hse in self.data1.houses:\n            bodies = [\n                svg_of(b.name)\n                for b in self.data1.aspectables\n                if self.data1.house_of(b) == hse.value\n            ]\n            grid.append(\n                [\n                    hse.value,\n                    f\"{hse.signed_deg:02d}\u00b0 {svg_of(hse.sign.name)} {hse.minute:02d}'\",\n                    \"\".join(bodies),\n                    len(bodies) or \"\",\n                ]\n            )\n        return grid\n\n    @property\n    def celestial_body1(self) -> Grid:\n        \"\"\"\n        Generates a grid of celestial bodies for the primary data.\n\n        Returns:\n            Grid: A grid of celestial bodies for the primary data.\n        \"\"\"\n        return self.celestial_body(self.data1)\n\n    @property\n    def celestial_body2(self) -> Grid:\n        \"\"\"\n        Generates a grid of celestial bodies for the secondary data.\n\n        Returns:\n            Grid: A grid of celestial bodies for the secondary data.\n        \"\"\"\n        return self.celestial_body(self.data2)\n\n    def celestial_body(self, data: Data) -> Grid:\n        \"\"\"\n        Generates a grid of celestial bodies for the given data.\n\n        Args:\n            data: The data for which to generate the grid.\n\n        Returns:\n            A grid of celestial bodies for the given data.\n        \"\"\"\n        grid = [(\"body\", \"sign\", \"house\", \"dignity\")]\n        for body in data.aspectables:\n            grid.append(\n                (\n                    svg_of(body.name),\n                    f\"{body.signed_deg:02d}\u00b0 {svg_of(body.sign.name)} {body.minute:02d}'\",\n                    self.data1.house_of(body),\n                    svg_of(dignity_of(body)),\n                )\n            )\n        return grid\n\n    @property\n    def cross_ref(self) -> StatData:\n        \"\"\"\n        Generates cross-reference statistics between the primary and secondary data.\n\n        Returns:\n            StatData: Cross-reference statistics between the primary and secondary data.\n        \"\"\"\n        stats = Stats(self.data1, self.data2)\n        grid = stats.cross_ref.grid\n        for row in range(len(grid)):\n            for col in range(len(grid[0])):\n                cell = grid[row][col]\n                if name := symbol_name_map.get(cell):\n                    grid[row][col] = svg_of(name)\n        return StatData(stats.cross_ref.title, grid)\n\n    @property\n    def full_report(self) -> str:\n        \"\"\"\n        Generates the full astrological report as an HTML string.\n\n        Returns:\n            str: The full astrological report as an HTML string.\n        \"\"\"\n        chart = Chart(self.data1, width=400, data2=self.data2)\n        row1 = div(\n            section(\"Birth Info\", self.basic_info)\n            + section(\"Elements, Modality & Polarity\", self.element_vs_modality)\n            + section(\"Hemisphere & Quadrants\", self.quadrants_vs_hemisphere),\n            class_=\"info_col\",\n        ) + div(chart.svg, class_=\"chart\")\n\n        row2 = section(f\"{self.data1.name}'s Celestial Bodies\", self.celestial_body1)\n\n        if self.data2:\n            row2 += section(\n                f\"{self.data2.name}'s Celestial Bodies\", self.celestial_body2\n            )\n        row2 += section(self.cross_ref.title, self.cross_ref.grid)\n        row3 = section(\"Signs\", self.signs) + section(\"Houses\", self.houses)\n        css = Path(__file__).parent / \"report.css\"\n        html = style(css.read_text()) + main(\n            div(row1, class_=\"row1\")\n            + div(row2, class_=\"row2\")\n            + div(row3, class_=\"row3\")\n        )\n        return html\n\n    def create_pdf(self, html: str) -> BytesIO:\n        \"\"\"\n        Creates a PDF from the given HTML string.\n\n        Args:\n            html: The HTML string to convert to PDF.\n\n        Returns:\n            A BytesIO object containing the PDF data.\n        \"\"\"\n        fp = BytesIO()\n        HTML(string=html).write_pdf(fp)\n        return fp\n
    "},{"location":"report/#natal.report.Report.basic_info","title":"basic_info: Gridproperty","text":"

    Generates basic information about the provided data.

    Returns:

    Type Description Grid

    A grid containing the name, city, and birth date/time.

    "},{"location":"report/#natal.report.Report.celestial_body1","title":"celestial_body1: Gridproperty","text":"

    Generates a grid of celestial bodies for the primary data.

    Returns:

    Name Type Description GridGrid

    A grid of celestial bodies for the primary data.

    "},{"location":"report/#natal.report.Report.celestial_body2","title":"celestial_body2: Gridproperty","text":"

    Generates a grid of celestial bodies for the secondary data.

    Returns:

    Name Type Description GridGrid

    A grid of celestial bodies for the secondary data.

    "},{"location":"report/#natal.report.Report.cross_ref","title":"cross_ref: StatDataproperty","text":"

    Generates cross-reference statistics between the primary and secondary data.

    Returns:

    Name Type Description StatDataStatData

    Cross-reference statistics between the primary and secondary data.

    "},{"location":"report/#natal.report.Report.element_vs_modality","title":"element_vs_modality: Gridproperty","text":"

    Generates a grid comparing elements and modalities.

    Returns:

    Type Description Grid

    A grid comparing elements and modalities.

    "},{"location":"report/#natal.report.Report.full_report","title":"full_report: strproperty","text":"

    Generates the full astrological report as an HTML string.

    Returns:

    Name Type Description strstr

    The full astrological report as an HTML string.

    "},{"location":"report/#natal.report.Report.houses","title":"houses: Gridproperty","text":"

    Generates a grid of houses and their corresponding bodies.

    Returns:

    Type Description Grid

    A grid of houses and their corresponding bodies.

    "},{"location":"report/#natal.report.Report.quadrants_vs_hemisphere","title":"quadrants_vs_hemisphere: Gridproperty","text":"

    Generates a grid comparing quadrants and hemispheres.

    Returns:

    Type Description Grid

    A grid comparing quadrants and hemispheres.

    "},{"location":"report/#natal.report.Report.signs","title":"signs: Gridproperty","text":"

    Generates a grid of signs and their corresponding bodies.

    Returns:

    Type Description Grid

    A grid of signs and their corresponding bodies

    "},{"location":"report/#natal.report.Report.__init__","title":"__init__(data1: Data, data2: Data | None = None)","text":"

    Initializes the Report with the given data.

    Parameters:

    Name Type Description Default data1Data

    The primary data for the report.

    required data2Data | None

    The secondary data for the report, if any.

    None Source code in natal/report.py
    def __init__(self, data1: Data, data2: Data | None = None):\n    \"\"\"\n    Initializes the Report with the given data.\n\n    Args:\n        data1: The primary data for the report.\n        data2: The secondary data for the report, if any.\n    \"\"\"\n    self.data1: Data = data1\n    self.data2: Data = data2\n
    "},{"location":"report/#natal.report.Report.celestial_body","title":"celestial_body(data: Data) -> Grid","text":"

    Generates a grid of celestial bodies for the given data.

    Parameters:

    Name Type Description Default dataData

    The data for which to generate the grid.

    required

    Returns:

    Type Description Grid

    A grid of celestial bodies for the given data.

    Source code in natal/report.py
    def celestial_body(self, data: Data) -> Grid:\n    \"\"\"\n    Generates a grid of celestial bodies for the given data.\n\n    Args:\n        data: The data for which to generate the grid.\n\n    Returns:\n        A grid of celestial bodies for the given data.\n    \"\"\"\n    grid = [(\"body\", \"sign\", \"house\", \"dignity\")]\n    for body in data.aspectables:\n        grid.append(\n            (\n                svg_of(body.name),\n                f\"{body.signed_deg:02d}\u00b0 {svg_of(body.sign.name)} {body.minute:02d}'\",\n                self.data1.house_of(body),\n                svg_of(dignity_of(body)),\n            )\n        )\n    return grid\n
    "},{"location":"report/#natal.report.Report.create_pdf","title":"create_pdf(html: str) -> BytesIO","text":"

    Creates a PDF from the given HTML string.

    Parameters:

    Name Type Description Default htmlstr

    The HTML string to convert to PDF.

    required

    Returns:

    Type Description BytesIO

    A BytesIO object containing the PDF data.

    Source code in natal/report.py
    def create_pdf(self, html: str) -> BytesIO:\n    \"\"\"\n    Creates a PDF from the given HTML string.\n\n    Args:\n        html: The HTML string to convert to PDF.\n\n    Returns:\n        A BytesIO object containing the PDF data.\n    \"\"\"\n    fp = BytesIO()\n    HTML(string=html).write_pdf(fp)\n    return fp\n
    "},{"location":"report/#natal.report.html_table_of","title":"html_table_of(grid: Grid) -> str","text":"

    Converts a grid of data into an HTML table.

    "},{"location":"report/#natal.report.html_table_of--arguments","title":"Arguments","text":"
    • grid - The grid of data to convert
    "},{"location":"report/#natal.report.html_table_of--returns","title":"Returns","text":"

    String containing the HTML table

    Source code in natal/report.py
    def html_table_of(grid: Grid) -> str:\n    \"\"\"\n    Converts a grid of data into an HTML table.\n\n    # Arguments\n    * grid - The grid of data to convert\n\n    # Returns\n    String containing the HTML table\n    \"\"\"\n    rows = []\n    for row in grid:\n        cells = []\n        for cell in row:\n            if isinstance(cell, str) and cell.startswith(\"null:\"):\n                cells.append(td(cell.split(\":\")[1], colspan=2))\n            else:\n                cells.append(td(cell))\n        rows.append(tr(cells))\n    return table(rows)\n
    "},{"location":"report/#natal.report.section","title":"section(title: str, grid: Grid) -> str","text":"

    Creates an HTML section with a title and a table of data.

    Parameters:

    Name Type Description Default titlestr

    The title of the section.

    required gridGrid

    The grid of data to include in the section.

    required

    Returns:

    Type Description str

    The HTML section as a string.

    Source code in natal/report.py
    def section(title: str, grid: Grid) -> str:\n    \"\"\"\n    Creates an HTML section with a title and a table of data.\n\n    Args:\n        title: The title of the section.\n        grid: The grid of data to include in the section.\n\n    Returns:\n        The HTML section as a string.\n    \"\"\"\n    return div(\n        div(title, class_=\"title\") + html_table_of(grid),\n        class_=\"section\",\n    )\n
    "},{"location":"report/#natal.report.svg_of","title":"svg_of(name: str, scale: float = 0.5) -> str","text":"

    Generates an SVG representation of a given symbol name.

    Parameters:

    Name Type Description Default namestr

    The name of the symbol.

    required scalefloat

    The scale of the SVG. Defaults to 0.5.

    0.5

    Returns:

    Type Description str

    The SVG representation of the symbol.

    Source code in natal/report.py
    def svg_of(name: str, scale: float = 0.5) -> str:\n    \"\"\"\n    Generates an SVG representation of a given symbol name.\n\n    Args:\n        name: The name of the symbol.\n        scale: The scale of the SVG. Defaults to 0.5.\n\n    Returns:\n        The SVG representation of the symbol.\n    \"\"\"\n    if not name:\n        return \"\"\n    stroke = TEXT_COLOR\n    fill = \"none\"\n    if name in [\"mc\", \"asc\", \"dsc\", \"ic\"]:\n        stroke = \"none\"\n        fill = TEXT_COLOR\n\n    return svg(\n        (Path(__file__).parent / \"svg_paths\" / f\"{name}.svg\").read_text(),\n        fill=fill,\n        stroke=stroke,\n        stroke_width=3 * scale,\n        version=\"1.1\",\n        width=f\"{20 * scale}px\",\n        height=f\"{20 * scale}px\",\n        transform=f\"scale({scale})\",\n        xmlns=\"http://www.w3.org/2000/svg\",\n    )\n
    "},{"location":"stats/","title":"Stats","text":""},{"location":"stats/#natal.stats","title":"natal.stats","text":"

    This module provides statistical analysis for natal charts.

    It contains the Stats class, which calculates and presents various astrological statistics for a single natal chart or a comparison between two charts.

    "},{"location":"stats/#natal.stats.StatData","title":"StatData","text":"

    Bases: NamedTuple

    A named tuple representing statistical data with a title and grid.

    Attributes:

    Name Type Description titlestr

    The title of the statistical data.

    gridGrid

    A grid containing the statistical information.

    Source code in natal/stats.py
    class StatData(NamedTuple):\n    \"\"\"\n    A named tuple representing statistical data with a title and grid.\n\n    Attributes:\n        title (str): The title of the statistical data.\n        grid (Grid): A grid containing the statistical information.\n    \"\"\"\n    title: str\n    grid: Grid\n
    "},{"location":"stats/#natal.stats.Stats","title":"Stats","text":"

    Statistics for a natal chart data.

    This class calculates and presents various astrological statistics for a single natal chart or a comparison between two charts.

    Attributes:

    Name Type Description data1Data

    The primary natal chart data.

    data2Data | None

    The secondary natal chart data for comparisons (optional).

    Source code in natal/stats.py
    class Stats:\n    \"\"\"\n    Statistics for a natal chart data.\n\n    This class calculates and presents various astrological statistics for a single natal chart\n    or a comparison between two charts.\n\n    Attributes:\n        data1 (Data): The primary natal chart data.\n        data2 (Data | None): The secondary natal chart data for comparisons (optional).\n    \"\"\"\n\n    data1: Data\n    data2: Data | None = None\n\n    def __init__(self, data1: Data, data2: Data | None = None) -> None:\n        \"\"\"\n        Initialize the Stats object with one or two natal chart data sets.\n\n        Args:\n            data1 (Data): The primary natal chart data.\n            data2 (Data, optional): The secondary natal chart data for comparisons. Defaults to None.\n        \"\"\"\n        self.data1 = data1\n        self.data2 = data2\n        if self.data2:\n            self.composite_pairs = data2.composite_aspects_pairs(self.data1)\n            self.composite_aspects = data1.calculate_aspects(self.composite_pairs)\n\n    # data grids =================================================================\n\n    def distribution(self, kind: DistKind) -> StatData:\n        \"\"\"\n        Generate distribution statistics for elements, modalities, or polarities.\n\n        Args:\n            kind (DistKind): The type of distribution to calculate. \n                Must be one of \"element\", \"modality\", or \"polarity\".\n\n        Returns:\n            StatData: A named tuple containing the title and grid of distribution data, \n                      where the grid includes the distribution type, count, and bodies.\n        \"\"\"\n        title = f\"{kind.capitalize()} Distribution ({self.data1.name})\"\n        bodies = defaultdict(lambda: [0, []])\n        for body in self.data1.aspectables:\n            key = body.sign[kind]\n            bodies[key][0] += 1  # act as a counter\n            bodies[key][1].append(f\"{body.name} {body.sign.symbol}\")\n        grid = [(kind, \"sum\", \"bodies\")]\n        data = [(key, val[0], \", \".join(val[1])) for key, val in bodies.items()]\n        grid.extend(data)\n        return StatData(title, grid)\n\n    @property\n    def celestial_body(self) -> StatData:\n        \"\"\"\n        Generate a grid of celestial body positions for the primary chart.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of celestial body data, \n                      where the grid includes body name, sign, house, and dignity.\n        \"\"\"\n        title = f\"Celestial Bodies ({self.data1.name})\"\n        grid = [(\"body\", \"sign\", \"house\", \"dignity\")]\n        for body in self.data1.aspectables:\n            grid.append(\n                (\n                    body.name,\n                    body.signed_dms,\n                    self.data1.house_of(body),\n                    dignity_of(body),\n                )\n            )\n        return StatData(title, grid)\n\n    @property\n    def data2_celestial_body(self) -> StatData:\n        \"\"\"\n        Generate a grid of celestial body positions for the secondary chart.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of celestial body data \n                      for the secondary chart, showing its bodies in the primary chart's context.\n                      The grid includes body name, sign, house, and dignity.\n\n        Raises:\n            AttributeError: If no secondary chart (data2) is available.\n        \"\"\"\n        if not self.data2:\n            raise AttributeError(\"No secondary chart available\")\n\n        title = f\"Celestial Bodies of {self.data2.name} in {self.data1.name}'s chart\"\n        grid = [(self.data2.name, \"sign\", \"house\", \"dignity\")]\n        for body in self.data2.aspectables:\n            grid.append(\n                (\n                    body.name,\n                    body.signed_dms,\n                    self.data1.house_of(body),\n                    dignity_of(body),\n                )\n            )\n        return StatData(title, grid)\n\n    @property\n    def house(self) -> StatData:\n        \"\"\"\n        Generate a grid of house data for the primary chart.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of house data, \n                      where the grid includes house number, cusp, ruler, ruler sign, and ruler house.\n        \"\"\"\n        title = f\"Houses ({self.data1.name})\"\n        grid = [(\"house\", \"cusp\", \"ruler\", \"ruler sign\", \"ruler house\")]\n        for house in self.data1.houses:\n            grid.append(\n                (\n                    house.value,\n                    house.signed_dms,\n                    house.ruler,\n                    house.ruler_sign,\n                    house.ruler_house,\n                )\n            )\n        return StatData(title, grid)\n\n    @property\n    def quadrant(self) -> StatData:\n        \"\"\"\n        Generate a grid of celestial body distribution in quadrants.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of quadrant distribution data, \n                      where the grid includes quadrant name, body count, and body names.\n        \"\"\"\n        title = f\"Quadrants ({self.data1.name})\"\n        quad_names = [\"1st \u25f5\", \"2nd \u25f6\", \"3rd \u25f7\", \"4th \u25f4\"]\n        quadrants = defaultdict(lambda: [0, []])\n        for i, quad in enumerate(self.data1.quadrants):\n            if quad:\n                for body in quad:\n                    quadrants[i][0] += 1  # act as a counter\n                    quadrants[i][1].append(f\"{body.name}\")\n            else:\n                # no celestial body in this quadrant\n                quadrants[i][0] = 0\n        grid = [(\"quadrant\", \"sum\", \"bodies\")]\n        data = [\n            (quad_names[quad_no], val[0], \", \".join(val[1]))\n            for quad_no, val in quadrants.items()\n        ]\n        return StatData(title, grid + data)\n\n    @property\n    def hemisphere(self) -> StatData:\n        \"\"\"\n        Generate a grid of celestial body distribution in hemispheres.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of hemisphere distribution data, \n                      where the grid includes hemisphere direction, body count, and body names.\n        \"\"\"\n        title = f\"Hemispheres ({self.data1.name})\"\n        grid = [(\"hemisphere\", \"sum\", \"bodies\")]\n        data = self.quadrant.grid[1:]\n        formatter: Callable[[int, int], str] = lambda a, b: (data[a][2] + \", \" + data[b][2]).strip(\" ,\")\n        left = (\"\u2190\", data[0][1] + data[3][1], formatter(0, 3))\n        right = (\"\u2192\", data[1][1] + data[2][1], formatter(1, 2))\n        top = (\"\u2191\", data[2][1] + data[3][1], formatter(2, 3))\n        bottom = (\"\u2193\", data[0][1] + data[1][1], formatter(0, 1))\n        return StatData(title, grid + [left, right, top, bottom])\n\n    @property\n    def aspect(self) -> StatData:\n        \"\"\"\n        Generate a grid of aspects for the primary chart.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of aspect data, \n                      where the grid includes body 1, aspect type, body 2, phase, and orb.\n        \"\"\"\n        title = f\"Aspects ({self.data1.name})\"\n        headers = [\"body 1\", \"aspect\", \"body 2\", \"phase\", \"orb\"]\n        return StatData(title, _aspect_grid(self.data1.aspects, headers))\n\n    @property\n    def composite_aspect(self) -> StatData:\n        \"\"\"\n        Generate a grid of composite aspects between two charts.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of composite aspect data, \n                      where the grid includes body names from both charts, aspect type, phase, and orb.\n\n        Raises:\n            AttributeError: If no secondary chart (data2) is available.\n        \"\"\"\n        if not self.data2:\n            raise AttributeError(\"No secondary chart available for composite aspects\")\n\n        title = f\"Aspects of {self.data2.name} vs {self.data1.name}\"\n        headers = [self.data2.name, \"aspect\", self.data1.name, \"phase\", \"orb\"]\n        return StatData(title, _aspect_grid(self.composite_aspects, headers))\n\n    @property\n    def cross_ref(self) -> StatData:\n        \"\"\"\n        Generate a grid for aspect cross-reference between charts or within a single chart.\n\n        Returns:\n            StatData: A named tuple containing the title and grid of aspect cross-reference data, \n                      where the grid shows aspect connections between bodies, with a sum column.\n        \"\"\"\n        name = (\n            f\"{self.data2.name}(cols) vs {self.data1.name}(rows)\"\n            if self.data2\n            else self.data1.name\n        )\n        title = f\"Aspect Cross Reference of {name}\"\n        aspectable1 = self.data1.aspectables\n        aspectable2 = self.data2.aspectables if self.data2 else self.data1.aspectables\n        aspects = self.composite_aspects if self.data2 else self.data1.aspects\n        body_symbols = [body.symbol for body in aspectable2]\n        grid: list[list[str]] = [[\"\"] + body_symbols + [\"sum\"]]\n        for body1 in aspectable1:\n            row = [body1.symbol]\n            aspect_count = 0\n            for body2 in aspectable2:\n                aspect = next(\n                    (\n                        asp\n                        for asp in aspects\n                        if (asp.body1 == body1 and asp.body2 == body2)\n                        or (asp.body1 == body2 and asp.body2 == body1)\n                    ),\n                    None,\n                )\n                if aspect:\n                    row.append(aspect.aspect_member.symbol)\n                    aspect_count += 1\n                else:\n                    row.append(\"\")\n\n            row.append(str(aspect_count))  # Add sum to the end of the row\n            grid.append(row)\n        return StatData(title, grid)\n\n    def full_report(self, kind: ReportKind) -> str:\n        \"\"\"\n        Generate a full report containing all statistical tables.\n\n        Args:\n            kind (ReportKind): The format of the report, either \"markdown\" or \"html\".\n\n        Returns:\n            str: A formatted string containing the full statistical report with various tables.\n        \"\"\"\n        output = \"\\n\"\n        for dist in DistKind.__args__:\n            output += self.table_of(\"distribution\", kind, dist)\n        output += self.table_of(\"celestial_body\", kind)\n        output += self.table_of(\"house\", kind)\n        output += self.table_of(\"quadrant\", kind)\n        output += self.table_of(\"hemisphere\", kind)\n        if self.data2:\n            output += self.table_of(\"data2_celestial_body\", kind)\n            output += self.table_of(\n                \"composite_aspect\", kind, colalign=(\"left\", \"center\", \"left\", \"center\")\n            )\n        else:\n            output += self.table_of(\"aspect\", kind)\n        output += self.table_of(\"cross_ref\", kind, stralign=\"center\")\n        return output\n\n    def table_of(\n        self, \n        fn_name: str, \n        kind: ReportKind, \n        *fn_args: object, \n        **markdown_options: object\n    ) -> str:\n        \"\"\"\n        Format a table with a title.\n\n        Args:\n            fn_name (str): The name of the method to call (e.g., \"distribution\", \"celestial_body\").\n            kind (ReportKind): The kind of report to generate (\"markdown\" or \"html\").\n            *fn_args: Variable positional arguments passed to the method.\n            **markdown_options: Additional keyword arguments for tabulate formatting.\n\n        Returns:\n            str: A formatted string containing the titled table in the specified format.\n        \"\"\"\n        stat = getattr(self, fn_name)\n        if fn_args:\n            stat = stat(*fn_args)\n        base_option = dict(headers=\"firstrow\", numalign=\"center\")\n\n        if kind == \"markdown\":\n            options = base_option | {\"tablefmt\": \"github\"} | markdown_options\n            output = f\"# {stat.title}\\n\\n\"\n            output += tabulate(stat.grid, **options)\n            output += \"\\n\\n\\n\"\n            return output\n        elif kind == \"html\":\n            options = base_option | {\"tablefmt\": \"html\"}\n            tb = tabulate(stat.grid, **options)\n            output = div([h4(stat.title), tb], class_=f\"tabulate {fn_name}\")\n            return str(output)\n
    "},{"location":"stats/#natal.stats.Stats.aspect","title":"aspect: StatDataproperty","text":"

    Generate a grid of aspects for the primary chart.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of aspect data, where the grid includes body 1, aspect type, body 2, phase, and orb.

    "},{"location":"stats/#natal.stats.Stats.celestial_body","title":"celestial_body: StatDataproperty","text":"

    Generate a grid of celestial body positions for the primary chart.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of celestial body data, where the grid includes body name, sign, house, and dignity.

    "},{"location":"stats/#natal.stats.Stats.composite_aspect","title":"composite_aspect: StatDataproperty","text":"

    Generate a grid of composite aspects between two charts.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of composite aspect data, where the grid includes body names from both charts, aspect type, phase, and orb.

    Raises:

    Type Description AttributeError

    If no secondary chart (data2) is available.

    "},{"location":"stats/#natal.stats.Stats.cross_ref","title":"cross_ref: StatDataproperty","text":"

    Generate a grid for aspect cross-reference between charts or within a single chart.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of aspect cross-reference data, where the grid shows aspect connections between bodies, with a sum column.

    "},{"location":"stats/#natal.stats.Stats.data2_celestial_body","title":"data2_celestial_body: StatDataproperty","text":"

    Generate a grid of celestial body positions for the secondary chart.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of celestial body data for the secondary chart, showing its bodies in the primary chart's context. The grid includes body name, sign, house, and dignity.

    Raises:

    Type Description AttributeError

    If no secondary chart (data2) is available.

    "},{"location":"stats/#natal.stats.Stats.hemisphere","title":"hemisphere: StatDataproperty","text":"

    Generate a grid of celestial body distribution in hemispheres.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of hemisphere distribution data, where the grid includes hemisphere direction, body count, and body names.

    "},{"location":"stats/#natal.stats.Stats.house","title":"house: StatDataproperty","text":"

    Generate a grid of house data for the primary chart.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of house data, where the grid includes house number, cusp, ruler, ruler sign, and ruler house.

    "},{"location":"stats/#natal.stats.Stats.quadrant","title":"quadrant: StatDataproperty","text":"

    Generate a grid of celestial body distribution in quadrants.

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of quadrant distribution data, where the grid includes quadrant name, body count, and body names.

    "},{"location":"stats/#natal.stats.Stats.__init__","title":"__init__(data1: Data, data2: Data | None = None) -> None","text":"

    Initialize the Stats object with one or two natal chart data sets.

    Parameters:

    Name Type Description Default data1Data

    The primary natal chart data.

    required data2Data

    The secondary natal chart data for comparisons. Defaults to None.

    None Source code in natal/stats.py
    def __init__(self, data1: Data, data2: Data | None = None) -> None:\n    \"\"\"\n    Initialize the Stats object with one or two natal chart data sets.\n\n    Args:\n        data1 (Data): The primary natal chart data.\n        data2 (Data, optional): The secondary natal chart data for comparisons. Defaults to None.\n    \"\"\"\n    self.data1 = data1\n    self.data2 = data2\n    if self.data2:\n        self.composite_pairs = data2.composite_aspects_pairs(self.data1)\n        self.composite_aspects = data1.calculate_aspects(self.composite_pairs)\n
    "},{"location":"stats/#natal.stats.Stats.distribution","title":"distribution(kind: DistKind) -> StatData","text":"

    Generate distribution statistics for elements, modalities, or polarities.

    Parameters:

    Name Type Description Default kindDistKind

    The type of distribution to calculate. Must be one of \"element\", \"modality\", or \"polarity\".

    required

    Returns:

    Name Type Description StatDataStatData

    A named tuple containing the title and grid of distribution data, where the grid includes the distribution type, count, and bodies.

    Source code in natal/stats.py
    def distribution(self, kind: DistKind) -> StatData:\n    \"\"\"\n    Generate distribution statistics for elements, modalities, or polarities.\n\n    Args:\n        kind (DistKind): The type of distribution to calculate. \n            Must be one of \"element\", \"modality\", or \"polarity\".\n\n    Returns:\n        StatData: A named tuple containing the title and grid of distribution data, \n                  where the grid includes the distribution type, count, and bodies.\n    \"\"\"\n    title = f\"{kind.capitalize()} Distribution ({self.data1.name})\"\n    bodies = defaultdict(lambda: [0, []])\n    for body in self.data1.aspectables:\n        key = body.sign[kind]\n        bodies[key][0] += 1  # act as a counter\n        bodies[key][1].append(f\"{body.name} {body.sign.symbol}\")\n    grid = [(kind, \"sum\", \"bodies\")]\n    data = [(key, val[0], \", \".join(val[1])) for key, val in bodies.items()]\n    grid.extend(data)\n    return StatData(title, grid)\n
    "},{"location":"stats/#natal.stats.Stats.full_report","title":"full_report(kind: ReportKind) -> str","text":"

    Generate a full report containing all statistical tables.

    Parameters:

    Name Type Description Default kindReportKind

    The format of the report, either \"markdown\" or \"html\".

    required

    Returns:

    Name Type Description strstr

    A formatted string containing the full statistical report with various tables.

    Source code in natal/stats.py
    def full_report(self, kind: ReportKind) -> str:\n    \"\"\"\n    Generate a full report containing all statistical tables.\n\n    Args:\n        kind (ReportKind): The format of the report, either \"markdown\" or \"html\".\n\n    Returns:\n        str: A formatted string containing the full statistical report with various tables.\n    \"\"\"\n    output = \"\\n\"\n    for dist in DistKind.__args__:\n        output += self.table_of(\"distribution\", kind, dist)\n    output += self.table_of(\"celestial_body\", kind)\n    output += self.table_of(\"house\", kind)\n    output += self.table_of(\"quadrant\", kind)\n    output += self.table_of(\"hemisphere\", kind)\n    if self.data2:\n        output += self.table_of(\"data2_celestial_body\", kind)\n        output += self.table_of(\n            \"composite_aspect\", kind, colalign=(\"left\", \"center\", \"left\", \"center\")\n        )\n    else:\n        output += self.table_of(\"aspect\", kind)\n    output += self.table_of(\"cross_ref\", kind, stralign=\"center\")\n    return output\n
    "},{"location":"stats/#natal.stats.Stats.table_of","title":"table_of(fn_name: str, kind: ReportKind, *fn_args: object, **markdown_options: object) -> str","text":"

    Format a table with a title.

    Parameters:

    Name Type Description Default fn_namestr

    The name of the method to call (e.g., \"distribution\", \"celestial_body\").

    required kindReportKind

    The kind of report to generate (\"markdown\" or \"html\").

    required *fn_argsobject

    Variable positional arguments passed to the method.

    ()**markdown_optionsobject

    Additional keyword arguments for tabulate formatting.

    {}

    Returns:

    Name Type Description strstr

    A formatted string containing the titled table in the specified format.

    Source code in natal/stats.py
    def table_of(\n    self, \n    fn_name: str, \n    kind: ReportKind, \n    *fn_args: object, \n    **markdown_options: object\n) -> str:\n    \"\"\"\n    Format a table with a title.\n\n    Args:\n        fn_name (str): The name of the method to call (e.g., \"distribution\", \"celestial_body\").\n        kind (ReportKind): The kind of report to generate (\"markdown\" or \"html\").\n        *fn_args: Variable positional arguments passed to the method.\n        **markdown_options: Additional keyword arguments for tabulate formatting.\n\n    Returns:\n        str: A formatted string containing the titled table in the specified format.\n    \"\"\"\n    stat = getattr(self, fn_name)\n    if fn_args:\n        stat = stat(*fn_args)\n    base_option = dict(headers=\"firstrow\", numalign=\"center\")\n\n    if kind == \"markdown\":\n        options = base_option | {\"tablefmt\": \"github\"} | markdown_options\n        output = f\"# {stat.title}\\n\\n\"\n        output += tabulate(stat.grid, **options)\n        output += \"\\n\\n\\n\"\n        return output\n    elif kind == \"html\":\n        options = base_option | {\"tablefmt\": \"html\"}\n        tb = tabulate(stat.grid, **options)\n        output = div([h4(stat.title), tb], class_=f\"tabulate {fn_name}\")\n        return str(output)\n
    "},{"location":"stats/#natal.stats.dignity_of","title":"dignity_of(body: Aspectable) -> str","text":"

    Get the dignity of a celestial body.

    Parameters:

    Name Type Description Default bodyAspectable

    The celestial body to check for dignity.

    required

    Returns:

    Name Type Description strstr

    The dignity of the celestial body. Possible values are \"domicile\", \"detriment\", \"exaltation\", \"fall\", or an empty string.

    Source code in natal/stats.py
    def dignity_of(body: Aspectable) -> str:\n    \"\"\"\n    Get the dignity of a celestial body.\n\n    Args:\n        body (Aspectable): The celestial body to check for dignity.\n\n    Returns:\n        str: The dignity of the celestial body. \n             Possible values are \"domicile\", \"detriment\", \"exaltation\", \"fall\", or an empty string.\n    \"\"\"\n    if body.name == (body.sign.classic_ruler or body.sign.ruler):\n        return \"domicile\"\n    if body.name == (body.sign.classic_detriment or body.sign.detriment):\n        return \"detriment\"\n    if body.name == body.sign.exaltation:\n        return \"exaltation\"\n    if body.name == body.sign.fall:\n        return \"fall\"\n    return \"\"\n
    "},{"location":"utils/","title":"Utils","text":""},{"location":"utils/#natal.utils","title":"natal.utils","text":"

    utility functions for natal

    "},{"location":"utils/#natal.utils.color_hex","title":"color_hex(name: str, config: Config = Config()) -> str","text":"

    Get color hex code from name and config.

    Parameters:

    Name Type Description Default namestr

    Color name to look up

    required configConfig

    Config containing color definitions

    Config()

    Returns:

    Name Type Description strstr

    Hex color code string

    Source code in natal/utils.py
    def color_hex(name: str, config: Config = Config()) -> str:\n    \"\"\"Get color hex code from name and config.\n\n    Args:\n        name (str): Color name to look up\n        config (Config): Config containing color definitions\n\n    Returns:\n        str: Hex color code string\n    \"\"\"\n    return getattr(config.colors, name)\n
    "},{"location":"utils/#natal.utils.member_of","title":"member_of(const: list[T], name: str) -> T","text":"

    Get a member from a list of constants by name.

    Parameters:

    Name Type Description Default constlist[T]

    List of constant definitions

    required namestr

    Name to look up

    required

    Returns:

    Name Type Description TT

    Matching constant member

    Source code in natal/utils.py
    def member_of[T](const: list[T], name: str) -> T:\n    \"\"\"Get a member from a list of constants by name.\n\n    Args:\n        const (list[T]): List of constant definitions\n        name (str): Name to look up\n\n    Returns:\n        T: Matching constant member\n    \"\"\"\n    idx: int = const[\"name\"].index(name)\n    return {prop: const[prop][idx] for prop in const.model_fields}\n
    "},{"location":"utils/#natal.utils.pairs","title":"pairs(iterable: Iterable[T]) -> list[tuple[T, T]]","text":"

    Generate unique pairs of elements from an iterable.

    Parameters:

    Name Type Description Default iterableIterable[T]

    Source of elements to pair

    required

    Returns:

    Type Description list[tuple[T, T]]

    list[tuple[T, T]]: List of element pairs as tuples

    Source code in natal/utils.py
    def pairs[T](iterable: Iterable[T]) -> list[tuple[T, T]]:\n    \"\"\"Generate unique pairs of elements from an iterable.\n\n    Args:\n        iterable (Iterable[T]): Source of elements to pair\n\n    Returns:\n        list[tuple[T, T]]: List of element pairs as tuples\n    \"\"\"\n    output = []\n    for i in range(len(iterable)):\n        for j in range(i + 1, len(iterable)):\n            output.append((iterable[i], iterable[j]))\n    return output\n
    "},{"location":"utils/#natal.utils.str_to_dt","title":"str_to_dt(dt_str: str) -> datetime","text":"

    Convert string to datetime object.

    Parameters:

    Name Type Description Default dt_strstr

    Datetime string in format \"YYYY-MM-DD HH:MM\"

    required

    Returns:

    Name Type Description datetimedatetime

    Parsed datetime object

    Source code in natal/utils.py
    def str_to_dt(dt_str: str) -> datetime:\n    \"\"\"Convert string to datetime object.\n\n    Args:\n        dt_str (str): Datetime string in format \"YYYY-MM-DD HH:MM\"\n\n    Returns:\n        datetime: Parsed datetime object\n    \"\"\"\n    return datetime.strptime(dt_str, \"%Y-%m-%d %H:%M\")\n
    "}]} \ No newline at end of file diff --git a/stats/index.html b/stats/index.html index 03c76e8..3650261 100644 --- a/stats/index.html +++ b/stats/index.html @@ -1485,11 +1485,11 @@

    def distribution(self, kind: DistKind) -> StatData: """ - Generate distribution statistics for elements, qualities, or polarities. + Generate distribution statistics for elements, modalities, or polarities. Args: kind (DistKind): The type of distribution to calculate. - Must be one of "element", "quality", or "polarity". + Must be one of "element", "modality", or "polarity". Returns: StatData: A named tuple containing the title and grid of distribution data, @@ -2289,7 +2289,7 @@

    -

    Generate distribution statistics for elements, qualities, or polarities.

    +

    Generate distribution statistics for elements, modalities, or polarities.

    Parameters:

    @@ -2313,7 +2313,7 @@

    125
    +              
    124
    +125
     126
     127
     128
    @@ -3897,18 +3898,17 @@ 

    131 132 133 -134 -135

    def get_members(raw_data: dict) -> list[DotDict]:
    -    """
    -    Get all members from raw data.
    -
    -    Args:
    -        raw_data (dict): The raw data dictionary.
    -
    -    Returns:
    -        list[DotDict]: A list of members as DotDicts.
    -    """
    -    return [get_member(raw_data, name) for name in raw_data["name"]]
    +134
    def get_members(raw_data: dict) -> list[DotDict]:
    +    """
    +    Get all members from raw data.
    +
    +    Args:
    +        raw_data (dict): The raw data dictionary.
    +
    +    Returns:
    +        list[DotDict]: A list of members as DotDicts.
    +    """
    +    return [get_member(raw_data, name) for name in raw_data["name"]]
     
    diff --git a/index.html b/index.html index eeffe7a..9373e9e 100644 --- a/index.html +++ b/index.html @@ -785,7 +785,7 @@

    Features

  • natal chart data statistics
      -
    • element, quality, and polarity counts
    • +
    • element, modality, and polarity counts
    • planets in each houses
    • quadrant and hemisphere distribution
    • aspect pair counts
    • @@ -879,7 +879,7 @@

      Data Object

      sun.sign.ruler # venus sun.sign.classic_ruler # venus sun.sign.element # earth -sun.sign.quality # fixed +sun.sign.modality # fixed sun.sign.polarity # negative # Aspect object @@ -926,9 +926,9 @@

      Stats

      | air | 3 | venus ♊, pluto ♎, mc ♊ | -# Quality Distribution (MiMi) +# Modality Distribution (MiMi) -| quality | count | bodies | +| modality | count | bodies | |-----------|---------|------------------------------------------------------------| | fixed | 4 | sun ♉, mars ♌, uranus ♏, asc_node ♌ | | cardinal | 3 | moon ♋, mercury ♈, pluto ♎ | diff --git a/objects.inv b/objects.inv index 8c3986e..b9dbb33 100644 Binary files a/objects.inv and b/objects.inv differ diff --git a/report/index.html b/report/index.html index 1d0656a..79c6033 100644 --- a/report/index.html +++ b/report/index.html @@ -549,9 +549,9 @@
    • - + - element_vs_quality + element_vs_modality @@ -860,9 +860,9 @@
    • - + - element_vs_quality + element_vs_modality @@ -1410,34 +1410,34 @@

      return list(zip(*output)) @property - def element_vs_quality(self) -> Grid: + def element_vs_modality(self) -> Grid: """ - Generates a grid comparing elements and qualities. + Generates a grid comparing elements and modalities. Returns: - A grid comparing elements and qualities. + A grid comparing elements and modalities. """ aspectable1 = self.data1.aspectables element_symbols = [svg_of(ele.name) for ele in ELEMENTS] grid = [[""] + element_symbols + ["sum"]] element_count = defaultdict(int) - for quality in QUALITY_MEMBERS: - row = [svg_of(quality.name)] - quality_count = 0 + for modality in MODALITY_MEMBERS: + row = [svg_of(modality.name)] + modality_count = 0 for element in ELEMENTS: count = 0 symbols = "" for body in aspectable1: if ( body.sign.element == element.name - and body.sign.quality == quality.name + and body.sign.modality == modality.name ): symbols += svg_of(body.name) count += 1 element_count[element.name] += 1 row.append(symbols) - quality_count += count - row.append(quality_count) + modality_count += count + row.append(modality_count) grid.append(row) grid.append( ["sum"] + list(element_count.values()) + [sum(element_count.values())] @@ -1589,7 +1589,7 @@

      chart = Chart(self.data1, width=400, data2=self.data2) row1 = div( section("Birth Info", self.basic_info) - + section("Elements, Modality & Polarity", self.element_vs_quality) + + section("Elements, Modality & Polarity", self.element_vs_modality) + section("Hemisphere & Quadrants", self.quadrants_vs_hemisphere), class_="info_col", ) + div(chart.svg, class_="chart") @@ -1816,8 +1816,8 @@

      -

      - element_vs_quality: Grid +

      + element_vs_modality: Grid property @@ -1828,7 +1828,7 @@

      -

      Generates a grid comparing elements and qualities.

      +

      Generates a grid comparing elements and modalities.

      Returns:

      @@ -1846,7 +1846,7 @@

  • -

    A grid comparing elements and qualities.

    +

    A grid comparing elements and modalities.

    The type of distribution to calculate. -Must be one of "element", "quality", or "polarity".

    +Must be one of "element", "modality", or "polarity".

    @@ -2372,11 +2372,11 @@

    84 85

    def distribution(self, kind: DistKind) -> StatData:
         """
    -    Generate distribution statistics for elements, qualities, or polarities.
    +    Generate distribution statistics for elements, modalities, or polarities.
     
         Args:
             kind (DistKind): The type of distribution to calculate. 
    -            Must be one of "element", "quality", or "polarity".
    +            Must be one of "element", "modality", or "polarity".
     
         Returns:
             StatData: A named tuple containing the title and grid of distribution data,