Files
Robert Helewka a2420ed692 refactor: restructure repo into core/app modules with per-study folders
Reorganize Palladium codebase into a modular architecture with `core/`
shared logic and `app/` Streamlit UI, separating per-study assets into
`studies/YYYYMM_<Vendor>/` folders containing notebooks, seed data, and
configuration. Update README to reflect new structure, add `.gitignore`
entries for `.env` and study exports, and refresh component documentation.
2026-05-20 22:28:12 -04:00

142 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
IPython display helpers — KPI cards, formatted summary blocks, alerts.
Functions are notebook-safe: they fall back to plain ``print`` when running
outside Jupyter / when IPython is not available.
"""
from __future__ import annotations
from typing import Any
try: # pragma: no cover IPython is a soft dep
from IPython.display import HTML, display
_IPY = True
except Exception: # pragma: no cover
_IPY = False
def _money(value: Any, default: str = "") -> str:
try:
v = float(value)
except (TypeError, ValueError):
return default
if abs(v) >= 1_000_000_000:
return f"${v/1_000_000_000:,.1f}B"
if abs(v) >= 1_000_000:
return f"${v/1_000_000:,.1f}M"
if abs(v) >= 1_000:
return f"${v/1_000:,.1f}K"
return f"${v:,.0f}"
def _pct(value: Any, default: str = "") -> str:
try:
v = float(value)
except (TypeError, ValueError):
return default
return f"{v:,.0f}%"
def _months(value: Any, default: str = "N/A") -> str:
if value is None:
return default
try:
v = float(value)
except (TypeError, ValueError):
return default
if v < 6:
return f"<6 months ({v:.1f})"
return f"{v:.1f} months"
def kpi_cards(summary: dict, *, title: str | None = None) -> Any:
"""
Render a row of KPI cards (NPV, ROI, Payback, Benefits PV).
In notebooks, returns/displays inline HTML. Outside IPython, prints a
plain text version.
"""
npv = _money(summary.get("npv"))
roi = _pct(summary.get("roi") or summary.get("roi_pct"))
payback = _months(summary.get("payback_months"))
benefits_pv = _money(summary.get("total_benefits_pv"))
costs_pv = _money(summary.get("total_costs_pv"))
if not _IPY: # pragma: no cover
print(title or "TEI Summary")
print(f" NPV: {npv} ROI: {roi} Payback: {payback}")
print(f" Benefits PV: {benefits_pv} Costs PV: {costs_pv}")
return None
title_html = (
f'<div style="font-size:1.1em;font-weight:600;margin-bottom:6px;color:#444;">'
f"{title}</div>"
if title
else ""
)
card_style = (
"flex:1;min-width:140px;padding:14px 18px;margin:4px;border-radius:8px;"
"background:#f7f9fc;border:1px solid #e3e8ee;"
)
label_style = "font-size:0.78em;color:#6b7480;text-transform:uppercase;letter-spacing:0.04em;"
value_style = "font-size:1.6em;font-weight:600;color:#1a2540;margin-top:4px;"
cards = [
("NPV", npv),
("ROI", roi),
("Payback", payback),
("Benefits PV", benefits_pv),
("Costs PV", costs_pv),
]
cards_html = "".join(
f'<div style="{card_style}">'
f'<div style="{label_style}">{label}</div>'
f'<div style="{value_style}">{value}</div>'
f"</div>"
for label, value in cards
)
html = (
f'<div>{title_html}'
f'<div style="display:flex;flex-wrap:wrap;align-items:stretch;">{cards_html}</div>'
f"</div>"
)
return display(HTML(html))
def summary_panel(summary: dict, *, title: str = "TEI Financial Summary") -> None:
"""Plain-text bordered summary block (mirrors the PDF Cash Flow Analysis)."""
width = 60
print("" * width)
print(f" {title}")
print("" * width)
print(f" Benefits PV : {_money(summary.get('total_benefits_pv')):>20}")
print(f" Costs PV : {_money(summary.get('total_costs_pv')):>20}")
print("" * width)
print(f" NPV : {_money(summary.get('npv')):>20}")
roi_val = summary.get("roi") or summary.get("roi_pct")
print(f" ROI : {_pct(roi_val):>20}")
print(f" Payback : {_months(summary.get('payback_months')):>20}")
print("" * width)
def alert(text: str, kind: str = "info") -> Any:
"""Coloured alert box for notebooks ('info', 'success', 'warning', 'error')."""
colors = {
"info": ("#0277bd", "#e1f5fe"),
"success": ("#2e7d32", "#e8f5e9"),
"warning": ("#ef6c00", "#fff3e0"),
"error": ("#c62828", "#ffebee"),
}
fg, bg = colors.get(kind, colors["info"])
if not _IPY: # pragma: no cover
print(f"[{kind.upper()}] {text}")
return None
html = (
f'<div style="padding:10px 14px;border-left:4px solid {fg};'
f'background:{bg};color:#1a1a1a;border-radius:4px;margin:6px 0;">'
f"{text}</div>"
)
return display(HTML(html))