feat(utils): add high-resolution chart export utilities
This commit is contained in:
102
src/utils/export.py
Normal file
102
src/utils/export.py
Normal file
@@ -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),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user