102 lines
2.7 KiB
Python
102 lines
2.7 KiB
Python
"""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: Optional[str] = None,
|
|
) -> 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 (default None, matplotlib auto)
|
|
|
|
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."""
|
|
|
|
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),
|
|
}
|