ChartPipeline / scripts /audit_d3_template_standardization.py
Ray1ee01's picture
Upload folder using huggingface_hub
58e6885 verified
Raw
History Blame Contribute Delete
6.59 kB
#!/usr/bin/env python3
import argparse
import csv
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TEMPLATE_ROOT = ROOT / "modules/chart_engine/template/d3-js"
CHECKS = {
"missing_chart_utils": re.compile(r"chartUtils\."),
"missing_color_resolver": re.compile(r"chartUtils\.color\.resolver"),
"missing_schema_helper": re.compile(r"chartUtils\.schema\.(?:channel|channels|channelByKey|columns)"),
"missing_format_helper": re.compile(r"chartUtils\.format\."),
"missing_text_helper": re.compile(r"chartUtils\.text\."),
"missing_legend_helper": re.compile(r"chartUtils\.legend\."),
}
ISSUE_PATTERNS = {
"direct_column_index": re.compile(
r"(?:dataColumns|data\.data\.columns|jsonData\.data\.columns)"
r"\[[^\]]+\](?:\?\.|\.)"
r"(?:name|role|unit|label|description|display_name|data_type|type)"
r"|(?:dataColumns|data\.data\.columns|jsonData\.data\.columns)"
r"\.(?:find|filter)\("
r"|(?:dataColumns|data\.data\.columns|jsonData\.data\.columns)"
r"\.filter\([^)]*\.role\s*===\s*['\"][^'\"]+['\"][^)]*\)\[[0-9]+\]\.name"
r"|(?:dataColumns|data\.data\.columns|jsonData\.data\.columns)"
r"\.find\([^)]*\.role\s*===\s*['\"][^'\"]+['\"][^)]*\)"
),
"local_format_value": re.compile(r"\b(?:const|let|var|function)\s+formatValue\b|formatValue\s*="),
"local_text_measure": re.compile(
r"\b(?:const|let|var|function)\s+(?:getTextWidth|getTextWidthCanvas|measureText)\b"
r"|\.getBBox\(|getComputedTextLength\(|\.measureText\("
),
"local_parse_date": re.compile(
r"\b(?:const|let|var|function)\s+parseDate\b|parseDate\s*="
r"|new Date\(\s*d\[[^\]]+\]\s*\)"
),
"direct_color_access": re.compile(
r"\bcolors(?:\.|\?\.)"
r"(?:other|field|available_colors|text_color|background_color)\b"
r"|\b(?:data|jsonData|dataJSON)(?:\.|\?\.)colors(?:\.|\?\.)"
r"(?:other|field|available_colors|text_color|background_color)\b"
),
"direct_color_variant": re.compile(r"\.(?:brighter|darker)\("),
"legacy_global_helper_call": re.compile(
r"(?<![.\w$])(?:layoutLegend|getTextWidth|formatValue|parseDate)\("
),
"debug_console_log": re.compile(r"console\.log\("),
"math_random": re.compile(r"Math\.random\("),
"schema_role_lookup": re.compile(
r"\.(?:find|filter)\([^\n)]*\.role\s*={2,3}\s*['\"][^'\"]+['\"]"
r"|(?:\b\w+|\])\.role\s*={2,3}\s*['\"][^'\"]+['\"]"
),
"manual_legend": re.compile(
r"\blegend(?:Group|G|Items?|Lines?|Container|Block|Offset|X|Y|Gap|Padding|Spacing|Width|Height)\b"
r"|class[\"'],\s*[\"']legend"
),
}
def has_requirements(text: str) -> bool:
return "REQUIREMENTS_BEGIN" in text and "REQUIREMENTS_END" in text
def node_check(path: Path) -> str:
proc = subprocess.run(
["node", "--check", str(path)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if proc.returncode == 0:
return ""
return proc.stderr.strip().splitlines()[0] if proc.stderr.strip() else "node --check failed"
def audit_file(path: Path, run_node_check: bool) -> dict:
text = path.read_text(encoding="utf-8")
record = {
"path": str(path.relative_to(ROOT)),
"has_requirements": has_requirements(text),
"syntax_error": node_check(path) if run_node_check else "",
}
for name, pattern in CHECKS.items():
if name.startswith("missing_"):
record[name] = not bool(pattern.search(text))
for name, pattern in ISSUE_PATTERNS.items():
record[name] = bool(pattern.search(text))
record["manual_legend_without_chart_utils_legend"] = (
record["manual_legend"] and record["missing_legend_helper"]
)
return record
def summarize(records: list[dict]) -> dict:
keys = [
"has_requirements",
"syntax_error",
*CHECKS.keys(),
*ISSUE_PATTERNS.keys(),
"manual_legend_without_chart_utils_legend",
]
summary = {"total_js": len(records)}
summary["registered_requirement_files"] = sum(1 for record in records if record["has_requirements"])
for key in keys:
if key == "syntax_error":
summary[key] = sum(1 for record in records if record[key])
elif key == "has_requirements":
continue
else:
summary[key] = sum(1 for record in records if record.get(key))
return summary
def write_csv(path: Path, records: list[dict]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fieldnames = list(records[0].keys()) if records else ["path"]
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(records)
def print_summary(summary: dict, records: list[dict], max_examples: int) -> None:
print(json.dumps(summary, indent=2, ensure_ascii=False))
for key, count in summary.items():
if key in {"total_js", "registered_requirement_files"} or not count:
continue
examples = [record["path"] for record in records if record.get(key)][:max_examples]
if examples:
print(f"\n{key} examples:")
for item in examples:
print(f" {item}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Audit D3 template usage of chartUtils standard helpers.")
parser.add_argument("--template-root", default=str(TEMPLATE_ROOT))
parser.add_argument("--output-json", default=None)
parser.add_argument("--output-csv", default=None)
parser.add_argument("--examples", type=int, default=5)
parser.add_argument("--node-check", action="store_true")
return parser.parse_args()
def main() -> None:
args = parse_args()
template_root = Path(args.template_root)
records = [
audit_file(path, run_node_check=args.node_check)
for path in sorted(template_root.rglob("*.js"))
]
summary = summarize(records)
if args.output_json:
output_path = Path(args.output_json)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(
json.dumps({"summary": summary, "records": records}, indent=2, ensure_ascii=False),
encoding="utf-8",
)
if args.output_csv:
write_csv(Path(args.output_csv), records)
print_summary(summary, records, args.examples)
if __name__ == "__main__":
main()