Files
cs249r_book/book/tools/scripts/_archive/render_figures.py
Vijay Janapa Reddi c9d21b768b feat(binder): add render plots command for matplotlib figure gallery
Integrate figure rendering into the binder CLI so plots can be previewed
without a full Quarto build. Extracts Python code blocks with fig-* labels
from QMD files, renders them to PNG, and outputs a browsable gallery at
_output/plots/<chapter>/. Also fixes the package import chain so `binder`
works correctly as an installed entry point.

- Add book/cli/commands/render.py with RenderCommand class
- Wire into main.py with help table entry and command dispatch
- Add matplotlib>=3.7.0 to pyproject.toml dependencies
- Add book/quarto/_output/ to .gitignore
- Archive standalone render_figures.py to _archive/
2026-02-14 12:43:23 -05:00

149 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Render all Matplotlib figures from QMD files to PNG for visual inspection.
Usage:
cd book/quarto
python ../tools/scripts/testing/render_figures.py [chapter_path ...]
Examples:
# Render figures from specific chapters:
python ../tools/scripts/testing/render_figures.py contents/vol1/training/training.qmd
# Render figures from all modified chapters:
python ../tools/scripts/testing/render_figures.py \
contents/vol1/training/training.qmd \
contents/vol1/data_selection/data_selection.qmd \
contents/vol1/ml_ops/ml_ops.qmd
# Render ALL vol1 figures:
python ../tools/scripts/testing/render_figures.py contents/vol1/*/*.qmd
Output goes to /tmp/rendered_figures/<chapter>/<fig-label>.png
"""
import re
import sys
import os
import textwrap
from pathlib import Path
def extract_python_figures(qmd_path: str) -> list[dict]:
"""Extract Python code blocks that have a fig-* label from a QMD file."""
with open(qmd_path, "r") as f:
content = f.read()
# Match ```{python} ... ``` blocks
pattern = re.compile(
r"```\{python\}\s*\n(.*?)```",
re.DOTALL,
)
figures = []
for match in pattern.finditer(content):
block = match.group(1)
# Check if it has a figure label
label_match = re.search(r"#\|\s*label:\s*(fig-[\w-]+)", block)
if not label_match:
continue
label = label_match.group(1)
# Strip the #| directives to get just the Python code
lines = block.split("\n")
code_lines = [
line for line in lines if not line.strip().startswith("#|")
]
code = "\n".join(code_lines).strip()
figures.append({"label": label, "code": code})
return figures
def render_figure(code: str, output_path: str) -> bool:
"""Execute a figure's Python code and save to PNG."""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
# Replace plt.show() with savefig
modified_code = code.replace("plt.show()", f"plt.savefig('{output_path}', dpi=150, bbox_inches='tight')\nplt.close('all')")
# If code doesn't call plt.show(), add savefig at the end
if "plt.show()" not in code and "savefig" not in code:
modified_code += f"\nplt.savefig('{output_path}', dpi=150, bbox_inches='tight')\nplt.close('all')"
try:
exec(modified_code, {"__name__": "__main__"})
return True
except Exception as e:
print(f" ERROR: {e}")
return False
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
# Ensure we're in the quarto directory for imports
cwd = os.getcwd()
if not os.path.exists("mlsys"):
quarto_dir = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"..", "..", "..", "quarto",
)
if os.path.exists(os.path.join(quarto_dir, "mlsys")):
os.chdir(quarto_dir)
print(f"Changed to {quarto_dir}")
sys.path.insert(0, ".")
output_base = Path("/tmp/rendered_figures")
total, success, failed = 0, 0, 0
for qmd_path in sys.argv[1:]:
if not os.path.exists(qmd_path):
print(f"File not found: {qmd_path}")
continue
chapter_name = Path(qmd_path).stem
figures = extract_python_figures(qmd_path)
if not figures:
print(f"\n{chapter_name}: no Matplotlib figures found")
continue
print(f"\n{'='*60}")
print(f"{chapter_name}: {len(figures)} figures")
print(f"{'='*60}")
out_dir = output_base / chapter_name
out_dir.mkdir(parents=True, exist_ok=True)
for fig in figures:
total += 1
out_file = out_dir / f"{fig['label']}.png"
print(f" Rendering {fig['label']}...", end=" ", flush=True)
if render_figure(fig["code"], str(out_file)):
success += 1
print(f"OK -> {out_file}")
else:
failed += 1
print(f"\n{'='*60}")
print(f"Done: {success}/{total} succeeded, {failed} failed")
print(f"Output: {output_base}/")
print(f"{'='*60}")
if sys.platform == "darwin":
print(f"\nTo view all figures:\n open {output_base}/")
if __name__ == "__main__":
main()