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.
This commit is contained in:
141
core/notebook_helpers/display.py
Normal file
141
core/notebook_helpers/display.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
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))
|
||||
Reference in New Issue
Block a user