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