Skip to content

Commit

Permalink
add pdf report
Browse files Browse the repository at this point in the history
  • Loading branch information
hoishing committed Nov 13, 2024
1 parent 62f0451 commit e2dad7c
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.DS_Store
try*
ref/
report.pdf

### Python ###
# Byte-compiled / optimized / DLL files
Expand Down
90 changes: 90 additions & 0 deletions natal/report.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
:root {
--color-border: white;
--color-background-alt: #f5f5f5;
--border-width: 0.1rem;
--color-text: #595959;
--text-size: 0.5rem;
}

@page {
size: A4;
margin: 2rem 2rem 1rem 2rem;
}

.row1,
.row2,
.row3 {
display: flex;
}

.row1 {
& > .chart {
flex: 1 0 auto;
/* border: 1px solid red; */
text-align: center;
padding-bottom: 0.5rem;
}
}

main {
font-family: sans-serif, serif, monospace;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}

.info_col {
/* border: 1px solid red; */
display: flex;
flex-direction: column;
& .section {
margin-bottom: 1rem;
}
}

table {
color: var(--color-text);
font-size: var(--text-size);
border-collapse: separate;
border-spacing: 0;
overflow: hidden;

& tr:nth-child(odd) {
background-color: var(--color-background-alt);
}

& td {
padding: 0.3rem;
border-right: var(--border-width) solid var(--color-border);
border-bottom: var(--border-width) solid var(--color-border);
text-align: center;

&:last-child {
border-right: none;
}
}

& tr:last-child td {
border-bottom: none;
}
}

svg {
vertical-align: bottom;
}

.title {
font-size: var(--text-size);
font-weight: bold;
color: var(--color-text);
padding: 0.3rem 0;
}

.section {
margin-right: 1rem;

&:last-child {
margin-right: 0;
}
}
268 changes: 268 additions & 0 deletions natal/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
from io import BytesIO
from collections import defaultdict
from natal import Chart, Config, Data, Stats
from natal.config import Orb
from natal.const import (
ASPECT_MEMBERS,
ELEMENT_MEMBERS,
EXTRA_MEMBERS,
PLANET_MEMBERS,
QUALITY_MEMBERS,
SIGN_MEMBERS,
VERTEX_MEMBERS,
)
from natal.stats import StatData, dignity_of
from pathlib import Path
from tagit import div, main, style, svg, table, td, tr
from typing import Iterable
from weasyprint import HTML

Grid = list[Iterable[str | int]]
ELEMENTS = [ELEMENT_MEMBERS[i] for i in (0, 2, 3, 1)]
TEXT_COLOR = "#595959"
symbol_name_map = {
asp.symbol: asp.name
for asp in (PLANET_MEMBERS + EXTRA_MEMBERS + VERTEX_MEMBERS + ASPECT_MEMBERS)
}


class Report:
def __init__(self, data1: Data, data2: Data | None = None):
self.data1 = data1
self.data2 = data2

@property
def basic_info(self) -> Grid:
time_fmt = "%Y-%m-%d %H:%M"
dt1 = self.data1.dt.strftime(time_fmt)
output = [["name", "city", "birth"]]
output.append([self.data1.name, self.data1.city, dt1])
if self.data2:
dt2 = self.data2.dt.strftime(time_fmt)
output.append([self.data2.name, self.data2.city, dt2])
return list(zip(*output))

@property
def element_vs_quality(self) -> Grid:
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 element in ELEMENTS:
count = 0
symbols = ""
for body in aspectable1:
if (
body.sign.element == element.name
and body.sign.quality == quality.name
):
symbols += svg_of(body.name)
count += 1
element_count[element.name] += 1
row.append(symbols)
quality_count += count
row.append(str(quality_count))
grid.append(row)
grid.append(
["sum"] + list(element_count.values()) + [sum(element_count.values())]
)
grid.append(
[
"◐",
f"null:{element_count['fire'] + element_count['air']} pos",
f"null:{element_count['water'] + element_count['earth']} neg",
"",
]
)
return grid

@property
def quadrants_vs_hemisphere(self) -> Grid:
q = self.data1.quadrants
first_q = [svg_of(body.name) for body in q[0]]
second_q = [svg_of(body.name) for body in q[1]]
third_q = [svg_of(body.name) for body in q[2]]
forth_q = [svg_of(body.name) for body in q[3]]
hemi_symbols = ["←", "→", "↑", "↓"]
grid = [[""] + hemi_symbols[:2] + ["sum"]]
grid += [["↑"] + [forth_q, third_q] + [len(q[3] + q[2])]]
grid += [["↓"] + [first_q, second_q] + [len(q[3] + q[2])]]
grid += [
["sum"]
+ [len(q[3] + q[0]), len(q[1] + q[2])]
+ [len(q[0] + q[1] + q[2] + q[3])]
]
return grid

@property
def signs(self) -> Grid:
grid = [["sign", "bodies", "sum"]]
for sign in SIGN_MEMBERS:
bodies = [
svg_of(b.name)
for b in self.data1.aspectables
if b.sign.name == sign.name
]
grid.append([svg_of(sign.name), "".join(bodies), len(bodies) or ""])
return grid

@property
def houses(self) -> Grid:
grid = [["house", "cusp", "bodies", "sum"]]
for hse in self.data1.houses:
bodies = [
svg_of(b.name)
for b in self.data1.aspectables
if self.data1.house_of(b) == hse.value
]
grid.append(
[
hse.value,
f"{hse.signed_deg:02d}° {svg_of(hse.sign.name)} {hse.minute:02d}'",
"".join(bodies),
len(bodies) or "",
]
)
return grid

@property
def celestial_body1(self) -> Grid:
return self.celestial_body(self.data1)

@property
def celestial_body2(self) -> Grid:
return self.celestial_body(self.data2)

def celestial_body(self, data: Data) -> Grid:
grid = [("body", "sign", "house", "dignity")]
for body in data.aspectables:
grid.append(
(
svg_of(body.name),
f"{body.signed_deg:02d}° {svg_of(body.sign.name)} {body.minute:02d}'",
self.data1.house_of(body),
svg_of(dignity_of(body)),
)
)
return grid

@property
def cross_ref(self) -> StatData:
stats = Stats(self.data1, self.data2)
grid = stats.cross_ref.grid
for row in range(len(grid)):
for col in range(len(grid[0])):
cell = grid[row][col]
if name := symbol_name_map.get(cell):
grid[row][col] = svg_of(name)
return StatData(stats.cross_ref.title, grid)

@property
def full_report(self) -> str:
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("Hemisphere & Quadrants", self.quadrants_vs_hemisphere),
class_="info_col",
) + div(chart.svg, class_="chart")

row2 = section(f"{self.data1.name}'s Celestial Bodies", self.celestial_body1)

if self.data2:
row2 += section(
f"{self.data2.name}'s Celestial Bodies", self.celestial_body2
)
row2 += section(report.cross_ref.title, report.cross_ref.grid)
row3 = section("Signs", self.signs) + section("Houses", self.houses)
css = Path(__file__).parent / "report.css"
html = style(css.read_text()) + main(
div(row1, class_="row1")
+ div(row2, class_="row2")
+ div(row3, class_="row3")
)
return html

def create_pdf(self, html: str) -> BytesIO:
fp = BytesIO()
HTML(string=html).write_pdf(fp)
return fp


# utils ======================================================================


def html_table_of(grid: Grid) -> str:
rows = []
for row in grid:
cells = []
for cell in row:
if isinstance(cell, str) and cell.startswith("null:"):
cells.append(td(cell.split(":")[1], colspan=2))
else:
cells.append(td(cell))
rows.append(tr(cells))
return table(rows)


def svg_of(name: str, scale: float = 0.5) -> str:
if not name:
return ""
stroke = TEXT_COLOR
fill = "none"
if name in ["mc", "asc", "dsc", "ic"]:
stroke = "none"
fill = TEXT_COLOR

return svg(
(Path(__file__).parent / "svg_paths" / f"{name}.svg").read_text(),
fill=fill,
stroke=stroke,
stroke_width=3 * scale,
version="1.1",
width=f"{20 * scale}px",
height=f"{20 * scale}px",
transform=f"scale({scale})",
xmlns="http://www.w3.org/2000/svg",
)


def section(title: str, grid: Grid) -> str:
return div(
div(title, class_="title") + html_table_of(grid),
class_="section",
)


# sample data ================================================================

if __name__ == "__main__":
person1 = {
"name": "Shing",
"city": "Hong Kong",
"dt": "1976-04-20 18:58",
}

person2 = {
"name": "Belle",
"city": "Hong Kong",
"dt": "2011-01-23 08:44",
}

orb = Orb(
conjunction=2,
opposition=2,
trine=2,
square=2,
sextile=1,
)

data1 = Data(**person1, config=Config(theme_type="light", orb=orb))
data2 = Data(**person2)
report = Report(data1, data2)
fp = report.create_pdf(report.full_report)
with open("report.pdf", "wb") as f:
f.write(fp.getvalue())
1 change: 1 addition & 0 deletions natal/svg_paths/detriment.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions natal/svg_paths/domicile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions natal/svg_paths/exaltation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions natal/svg_paths/fall.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "natal"
version = "0.7.3"
version = "0.7.4"
description = "create Natal Chart with ease"
license = "MIT"
repository = "https://github.com/hoishing/natal"
Expand Down

0 comments on commit e2dad7c

Please sign in to comment.