diff --git a/src/utils/export.py b/src/utils/export.py new file mode 100644 index 0000000..83cae73 --- /dev/null +++ b/src/utils/export.py @@ -0,0 +1,102 @@ +"""High-resolution chart export utilities.""" + +import os +from pathlib import Path +from typing import Optional + +import matplotlib.figure +from src.utils.styling import EXPORT_DPI + + +def ensure_output_dir(path: str) -> Path: + """Ensure output directory exists and return Path.""" + p = Path(path) + p.mkdir(parents=True, exist_ok=True) + return p + + +def save_chart( + fig: matplotlib.figure.Figure, + filename: str, + output_dir: str = "output/charts", + dpi: int = EXPORT_DPI, + bbox_inches: str = "default", +) -> str: + """Save a matplotlib figure as high-resolution PNG. + + Args: + fig: matplotlib Figure to save + filename: Output filename (e.g., '01_shiller_cape.png') + output_dir: Base output directory + dpi: Resolution (default 300) + bbox_inches: Bbox mode for tight layout + + Returns: + Full path to saved file + """ + output_path = ensure_output_dir(output_dir) / filename + fig.savefig( + str(output_path), + dpi=dpi, + bbox_inches=bbox_inches, + facecolor=fig.get_facecolor(), + edgecolor="none", + ) + return str(output_path) + + +def save_chart_tight( + fig: matplotlib.figure.Figure, + filename: str, + output_dir: str = "output/charts", + dpi: int = EXPORT_DPI, +) -> str: + """Save chart with tight layout to prevent label clipping.""" + return save_chart(fig, filename, output_dir, dpi, bbox_inches="tight") + + +def save_combined_chart( + fig: matplotlib.figure.Figure, + filename: str, + dpi: int = EXPORT_DPI, +) -> str: + """Save a combined/multi-panel dashboard chart.""" + return save_chart(fig, filename, "output/combined", dpi, bbox_inches="tight") + + +def list_output_charts(output_dir: str = "output/charts") -> list[str]: + """List all PNG files in output directory.""" + p = Path(output_dir) + if not p.exists(): + return [] + return sorted([f.name for f in p.glob("*.png")]) + + +def get_chart_metadata(filepath: str) -> dict: + """Get basic metadata about a saved chart file.""" + from PIL import Image # Try PIL first, fallback below + + p = Path(filepath) + if not p.exists(): + return {"exists": False} + + stat = p.stat() + size_mb = stat.st_size / (1024 * 1024) + + # Try to get DPI from PNG + try: + # Use matplotlib to read back (works without PIL) + import matplotlib.image as mpimg + img = mpimg.imread(str(p)) + return { + "exists": True, + "size_mb": round(size_mb, 2), + "shape": img.shape if hasattr(img, "shape") else "unknown", + "path": str(p), + } + except Exception: + return { + "exists": True, + "size_mb": round(size_mb, 2), + "path": str(p), + }