First commit of claude's rework in django + vanillajs fronted
This commit is contained in:
95
apps/tools/analyzer/pdf_report.py
Normal file
95
apps/tools/analyzer/pdf_report.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import base64
|
||||
import io
|
||||
from datetime import datetime
|
||||
|
||||
from fpdf import FPDF
|
||||
|
||||
_COL_LABEL = 80
|
||||
_COL_VALUE = 50
|
||||
_ROW_H = 7
|
||||
|
||||
|
||||
def generate_pdf(overall: dict, group_stats: list, charts: list, overview_chart: str) -> bytes:
|
||||
pdf = FPDF()
|
||||
pdf.set_auto_page_break(auto=True, margin=15)
|
||||
|
||||
pdf.add_page()
|
||||
_title_block(pdf)
|
||||
_overall_section(pdf, overall, overview_chart)
|
||||
|
||||
for stat, chart_b64 in zip(group_stats, charts):
|
||||
_group_section(pdf, stat, chart_b64)
|
||||
|
||||
return bytes(pdf.output())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _title_block(pdf: FPDF):
|
||||
pdf.set_font("Helvetica", "B", 18)
|
||||
pdf.cell(0, 12, "Ballistic Analysis Report", new_x="LMARGIN", new_y="NEXT", align="C")
|
||||
pdf.set_font("Helvetica", "", 9)
|
||||
pdf.cell(
|
||||
0, 5,
|
||||
f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
||||
new_x="LMARGIN", new_y="NEXT", align="C",
|
||||
)
|
||||
pdf.ln(8)
|
||||
|
||||
|
||||
def _overall_section(pdf: FPDF, overall: dict, overview_chart: str):
|
||||
_section_heading(pdf, "Overall Statistics")
|
||||
rows = [
|
||||
("Total shots", str(overall["count"])),
|
||||
("Min speed", f"{overall['min_speed']:.4f}"),
|
||||
("Max speed", f"{overall['max_speed']:.4f}"),
|
||||
("Mean speed", f"{overall['mean_speed']:.4f}"),
|
||||
("Std dev (speed)", f"{overall['std_speed']:.4f}" if overall["std_speed"] is not None else "n/a"),
|
||||
]
|
||||
_table(pdf, rows)
|
||||
img_bytes = base64.b64decode(overview_chart)
|
||||
pdf.image(io.BytesIO(img_bytes), x=pdf.l_margin, w=min(140, pdf.epw))
|
||||
pdf.ln(4)
|
||||
|
||||
|
||||
def _group_section(pdf: FPDF, stat: dict, chart_b64: str):
|
||||
pdf.ln(4)
|
||||
heading = (
|
||||
f"Group {stat['group_index']} - "
|
||||
f"{stat['time_start']} to {stat['time_end']} "
|
||||
f"({stat['count']} shot(s))"
|
||||
)
|
||||
_section_heading(pdf, heading)
|
||||
|
||||
rows = [
|
||||
("Min speed", f"{stat['min_speed']:.4f}"),
|
||||
("Max speed", f"{stat['max_speed']:.4f}"),
|
||||
("Mean speed", f"{stat['mean_speed']:.4f}"),
|
||||
("Std dev (speed)", f"{stat['std_speed']:.4f}" if stat["std_speed"] is not None else "n/a"),
|
||||
]
|
||||
_table(pdf, rows)
|
||||
|
||||
img_bytes = base64.b64decode(chart_b64)
|
||||
# Check remaining page space; add new page if chart won't fit
|
||||
if pdf.get_y() + 75 > pdf.page_break_trigger:
|
||||
pdf.add_page()
|
||||
pdf.image(io.BytesIO(img_bytes), x=pdf.l_margin, w=pdf.epw)
|
||||
pdf.ln(4)
|
||||
|
||||
|
||||
def _section_heading(pdf: FPDF, text: str):
|
||||
pdf.set_font("Helvetica", "B", 12)
|
||||
pdf.set_fill_color(230, 236, 255)
|
||||
pdf.cell(0, 8, text, new_x="LMARGIN", new_y="NEXT", fill=True)
|
||||
pdf.ln(2)
|
||||
|
||||
|
||||
def _table(pdf: FPDF, rows: list):
|
||||
for i, (label, value) in enumerate(rows):
|
||||
fill = i % 2 == 0
|
||||
pdf.set_fill_color(248, 249, 252) if fill else pdf.set_fill_color(255, 255, 255)
|
||||
pdf.set_font("Helvetica", "", 10)
|
||||
pdf.cell(_COL_LABEL, _ROW_H, label, border=0, fill=fill)
|
||||
pdf.set_font("Helvetica", "B", 10)
|
||||
pdf.cell(_COL_VALUE, _ROW_H, value, border=0, fill=fill, new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.ln(3)
|
||||
Reference in New Issue
Block a user