Spaces:
Sleeping
Sleeping
File size: 7,468 Bytes
58e6885 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | """
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)} | "
f"<b>Tasks:</b> {len(tasks)} | "
f"<b class='bad'>Failed:</b> {n_failed} | "
f"<b class='bad'>Fallback PNG:</b> {n_fb} | "
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> · "
f"done {s.get('done', '?')}/{s.get('target', '?')} · "
f"ok {s['n_success']} · "
f"<span class='bad'>fail {s.get('n_fail', 0)}</span> · "
f"<span class='bad'>fallback_png {s['n_fallback_png']}</span> · "
f"<span class='warn'>empty {s.get('n_empty', 0)}</span> · "
f"mean_svg {s.get('mean_size_kb', s.get('mean_final_svg_kb', '?'))}KB · "
f"mean_shapes {s.get('mean_shapes', s.get('mean_n_shapes', '?'))} · "
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()
|