ChartPipeline / scripts /build_quality_preview.py
Ray1ee01's picture
Upload folder using huggingface_hub
58e6885 verified
Raw
History Blame Contribute Delete
7.47 kB
"""
Generate an HTML page that previews the quality-check outputs.
Each template gets a section with summary stats and a horizontally-
scrolling strip of every generated infographic SVG, plus per-card links
to chart.svg / chart.html for the per-data subfolder so failures can be
inspected. Designed to be safe to run mid-run; it just renders whatever
is currently in _tasks.csv.
Usage:
python scripts/build_quality_preview.py \\
--output-dir output/quality_check \\
--out output/quality_check/preview.html
"""
import argparse
import csv
import os
from collections import defaultdict
from pathlib import Path
from typing import Optional
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--output-dir", default="output/quality_check")
p.add_argument("--out", default=None,
help="HTML output path (default: <output-dir>/preview.html)")
return p.parse_args()
CSS = """
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: #f5f5f7; color: #1d1d1f; margin: 0; padding: 24px; }
h1 { font-size: 22px; margin: 0 0 12px; }
.toc { background: white; padding: 12px 16px; border-radius: 8px;
margin-bottom: 16px; font-size: 13px; line-height: 1.7; }
.toc a { color: #0066cc; margin-right: 12px; text-decoration: none; }
.tpl { background: white; border-radius: 8px; margin-bottom: 16px;
padding: 16px 18px; box-shadow: 0 1px 2px rgba(0,0,0,.06); }
.tpl h2 { font-size: 16px; margin: 0 0 4px; }
.tpl .meta { font-size: 12px; color: #6e6e73; margin-bottom: 10px; }
.bad { color: #d70015; }
.warn { color: #b25000; }
.good { color: #248a3d; }
.strip { display: flex; overflow-x: auto; gap: 10px; padding-bottom: 8px; }
.card { flex: 0 0 auto; width: 240px; background: #fafafa;
border: 1px solid #e5e5ea; border-radius: 6px; padding: 6px; }
.card img, .card object, .card iframe { width: 100%; height: 200px;
object-fit: contain; background: white; border: 0; display: block; }
.card .lbl { font-size: 10px; color: #6e6e73; margin-top: 4px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card.failed { border-color: #d70015; }
.card.empty { border-color: #b25000; }
.card.fb { border-color: #d70015; background: #fff5f5; }
"""
def severity_class(row):
"""Return a CSS class for the per-template summary row.
Works against the schema produced by scripts/_progress.py's
ProgressTracker._rollups (target/done/n_success/n_fail/n_fallback_png/...).
"""
if int(row.get("n_fail", 0) or 0) > 0:
return "bad"
if int(row.get("n_fallback_png", 0) or 0) > 0:
return "bad"
if int(row.get("n_empty", 0) or 0) > 0:
return "warn"
return "good"
def build_preview(out_dir: Path, html_path: Optional[Path] = None) -> Path:
"""Render preview.html from CSVs in out_dir. Returns the html path."""
out_dir = Path(out_dir)
if html_path is None:
html_path = out_dir / "preview.html"
html_path = Path(html_path)
tasks_csv = out_dir / "_tasks.csv"
summary_csv = out_dir / "_summary.csv"
if not tasks_csv.exists() or not summary_csv.exists():
raise FileNotFoundError(
f"Expected {tasks_csv} and {summary_csv} but at least one is missing"
)
tasks = list(csv.DictReader(open(tasks_csv)))
summary = list(csv.DictReader(open(summary_csv)))
# _summary.csv is already worst-first.
by_tpl = defaultdict(list)
for t in tasks:
by_tpl[t["chart_name"]].append(t)
# Make all paths relative to the HTML file's parent dir for browser display.
html_parent = html_path.parent.resolve()
def rel(p):
if not p:
return ""
p = Path(p)
if not p.is_absolute():
p = (Path.cwd() / p).resolve()
return os.path.relpath(p, html_parent)
n_failed = sum(1 for t in tasks if t["ok"] != "True")
n_fb = sum(1 for t in tasks if t["chart_svg_fallback_png"] == "True")
n_empty = sum(
1 for t in tasks
if t["ok"] == "True"
and (int(t.get("n_shapes") or 0) + int(t.get("n_text") or 0)) < 8
)
parts = [
"<!DOCTYPE html><html><head><meta charset='utf-8'>",
"<title>ChartPipeline Quality Check</title>",
f"<style>{CSS}</style></head><body>",
"<h1>Chart Template Quality Check</h1>",
f"<div class='toc'><b>Templates:</b> {len(summary)} &nbsp;|&nbsp; "
f"<b>Tasks:</b> {len(tasks)} &nbsp;|&nbsp; "
f"<b class='bad'>Failed:</b> {n_failed} &nbsp;|&nbsp; "
f"<b class='bad'>Fallback PNG:</b> {n_fb} &nbsp;|&nbsp; "
f"<b class='warn'>Empty:</b> {n_empty}<br>",
]
parts.append("<b>Jump to:</b> ")
for s in summary:
cls = severity_class(s)
parts.append(
f"<a class='{cls}' href='#{s['chart_name']}'>{s['chart_name']}</a>"
)
parts.append("</div>")
for s in summary:
chart_name = s["chart_name"]
cls = severity_class(s)
parts.append(f"<div class='tpl' id='{chart_name}'>")
parts.append(f"<h2 class='{cls}'>{chart_name}</h2>")
parts.append(
f"<div class='meta'>"
f"<b>{s.get('chart_type', '')}</b> &middot; "
f"done {s.get('done', '?')}/{s.get('target', '?')} &middot; "
f"ok {s['n_success']} &middot; "
f"<span class='bad'>fail {s.get('n_fail', 0)}</span> &middot; "
f"<span class='bad'>fallback_png {s['n_fallback_png']}</span> &middot; "
f"<span class='warn'>empty {s.get('n_empty', 0)}</span> &middot; "
f"mean_svg {s.get('mean_size_kb', s.get('mean_final_svg_kb', '?'))}KB &middot; "
f"mean_shapes {s.get('mean_shapes', s.get('mean_n_shapes', '?'))} &middot; "
f"mean_time {s['mean_elapsed_s']}s</div>"
)
parts.append("<div class='strip'>")
for t in by_tpl.get(chart_name, []):
card_cls = "card"
if t["ok"] != "True":
card_cls += " failed"
elif t["chart_svg_fallback_png"] == "True":
card_cls += " fb"
elif int(t.get("n_shapes") or 0) + int(t.get("n_text") or 0) < 8:
card_cls += " empty"
img_html = ""
if t["final_svg"]:
img_html = f"<object data='{rel(t['final_svg'])}' type='image/svg+xml'></object>"
elif t["chart_svg"]:
img_html = (
f"<div style='display:flex;align-items:center;justify-content:center;"
f"height:200px;color:#d70015;font-size:11px;'>"
f"render failed</div>"
)
label = (
f"{t['input'][:30]}<br>"
f"shapes={t['n_shapes']} t={t['elapsed_s']}s "
f"size={int(t['final_svg_bytes'])//1024}KB"
)
parts.append(
f"<div class='{card_cls}'>{img_html}"
f"<div class='lbl'>{label}</div></div>"
)
parts.append("</div></div>")
parts.append("</body></html>")
html_path.write_text("\n".join(parts))
return html_path
def main():
args = parse_args()
out_dir = Path(args.output_dir)
html_path = Path(args.out) if args.out else None
written = build_preview(out_dir, html_path)
print(f"Wrote {written}")
print(f"Open with: file://{written.resolve()}")
if __name__ == "__main__":
main()