eduevidence 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/README.zh-CN.md +434 -0
- package/SKILL.md +195 -0
- package/bin/eduevidence.js +127 -0
- package/domains/education/manifest.json +183 -0
- package/domains/education/outcome_taxonomy.json +127 -0
- package/domains/manifest.json +26 -0
- package/domains/policy/frame.schema.json +234 -0
- package/domains/policy/manifest.json +10 -0
- package/domains/policy/methodology_checklist.json +109 -0
- package/domains/policy/outcome_taxonomy.json +53 -0
- package/domains/policy/references/causal-identification.md +45 -0
- package/domains/policy/references/cost-evidence.md +44 -0
- package/domains/policy/references/equity.md +42 -0
- package/domains/policy/references/evidence-hierarchy.md +41 -0
- package/domains/policy/references/implementation-evidence.md +47 -0
- package/eduevidence_cli.py +26 -0
- package/engine/__init__.py +11 -0
- package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
- package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/bias.cpython-312.pyc +0 -0
- package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
- package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
- package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
- package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
- package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
- package/engine/__pycache__/events.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
- package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
- package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
- package/engine/__pycache__/ids.cpython-312.pyc +0 -0
- package/engine/__pycache__/library.cpython-312.pyc +0 -0
- package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
- package/engine/__pycache__/living.cpython-312.pyc +0 -0
- package/engine/__pycache__/log.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/migration.cpython-312.pyc +0 -0
- package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
- package/engine/__pycache__/paths.cpython-312.pyc +0 -0
- package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
- package/engine/__pycache__/planner.cpython-312.pyc +0 -0
- package/engine/__pycache__/project.cpython-312.pyc +0 -0
- package/engine/__pycache__/projections.cpython-312.pyc +0 -0
- package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
- package/engine/__pycache__/run.cpython-312.pyc +0 -0
- package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
- package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
- package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
- package/engine/__pycache__/update.cpython-312.pyc +0 -0
- package/engine/__pycache__/versions.cpython-312.pyc +0 -0
- package/engine/analysis.py +308 -0
- package/engine/bias.py +178 -0
- package/engine/briefs.py +106 -0
- package/engine/capabilities.py +99 -0
- package/engine/citation_check.py +192 -0
- package/engine/contracts.py +117 -0
- package/engine/datasets.py +165 -0
- package/engine/events.py +67 -0
- package/engine/evidence_graph.py +571 -0
- package/engine/evidence_review.py +88 -0
- package/engine/evidencecore.py +182 -0
- package/engine/gap_lens.py +132 -0
- package/engine/gaps.py +169 -0
- package/engine/graph_store.py +335 -0
- package/engine/graph_validate.py +87 -0
- package/engine/ids.py +77 -0
- package/engine/library.py +268 -0
- package/engine/library_builtin.py +301 -0
- package/engine/living.py +671 -0
- package/engine/log.py +39 -0
- package/engine/meta_analysis.py +333 -0
- package/engine/meta_synthesis.py +111 -0
- package/engine/migration.py +397 -0
- package/engine/mode_router.py +72 -0
- package/engine/paths.py +15 -0
- package/engine/pilot.py +368 -0
- package/engine/planner.py +126 -0
- package/engine/project.py +118 -0
- package/engine/projections.py +240 -0
- package/engine/robustness.py +109 -0
- package/engine/run.py +85 -0
- package/engine/semantics.py +135 -0
- package/engine/study_design.py +87 -0
- package/engine/synthesis.py +187 -0
- package/engine/tribunal.py +408 -0
- package/engine/update.py +113 -0
- package/engine/versions.py +12 -0
- package/install.sh +510 -0
- package/integrations/__init__.py +1 -0
- package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
- package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
- package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
- package/integrations/agent_mcp.py +856 -0
- package/integrations/smart_web_fetch.py +59 -0
- package/package.json +50 -0
- package/pyproject.toml +55 -0
- package/references/applicability-policy.md +88 -0
- package/references/education-framing.md +132 -0
- package/references/effect_size_formulas.md +35 -0
- package/references/evaluation-design.md +111 -0
- package/references/evidence-quality.md +79 -0
- package/references/grade_framework.md +29 -0
- package/references/intervention-design.md +98 -0
- package/references/methodology-audit.md +103 -0
- package/references/outcome-taxonomy.md +106 -0
- package/references/retrieval-protocol.md +142 -0
- package/references/skeptic-protocol.md +93 -0
- package/references/social_science_pitfalls.md +48 -0
- package/references/source-validity.md +140 -0
- package/references/tribunal-policy.md +112 -0
- package/references/wwc_standards.md +29 -0
- package/retrieval/__init__.py +1 -0
- package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
- package/retrieval/corpus_store.py +181 -0
- package/retrieval/dedupe.py +127 -0
- package/retrieval/failures.py +90 -0
- package/retrieval/fetch.py +435 -0
- package/retrieval/search.py +493 -0
- package/retrieval/source.py +160 -0
- package/retrieval/validate.py +257 -0
- package/schemas/agent-mcp-approval.schema.json +57 -0
- package/schemas/chart-spec.schema.json +88 -0
- package/schemas/cross-model-review.schema.json +28 -0
- package/schemas/education-frame.schema.json +106 -0
- package/schemas/evaluation.schema.json +35 -0
- package/schemas/evidence.schema.json +81 -0
- package/schemas/fetch-result.schema.json +119 -0
- package/schemas/intervention.schema.json +46 -0
- package/schemas/methodology.schema.json +102 -0
- package/schemas/report-result.schema.json +381 -0
- package/schemas/report-spec.schema.json +130 -0
- package/schemas/source.schema.json +311 -0
- package/schemas/v2/analysis-plan.schema.json +28 -0
- package/schemas/v2/analysis-run.schema.json +33 -0
- package/schemas/v2/claim.schema.json +26 -0
- package/schemas/v2/dataset-asset.schema.json +40 -0
- package/schemas/v2/decision-snapshot.schema.json +53 -0
- package/schemas/v2/evidence-link.schema.json +38 -0
- package/schemas/v2/finding.schema.json +47 -0
- package/schemas/v2/graph-revision.schema.json +30 -0
- package/schemas/v2/knowledge-gap.schema.json +40 -0
- package/schemas/v2/methodology-audit.schema.json +30 -0
- package/schemas/v2/outcome.schema.json +18 -0
- package/schemas/v2/project.schema.json +31 -0
- package/schemas/v2/research-intent.schema.json +24 -0
- package/schemas/v2/run.schema.json +43 -0
- package/schemas/v2/source.schema.json +24 -0
- package/schemas/v2/study-design.schema.json +67 -0
- package/schemas/v2/study.schema.json +37 -0
- package/schemas/v3/pilot-outcome.schema.json +132 -0
- package/schemas/v3/run-manifest.schema.json +193 -0
- package/schemas/v3/synthesis.schema.json +86 -0
- package/schemas/v4/drift-report.schema.json +66 -0
- package/schemas/v4/evidence-library.schema.json +42 -0
- package/schemas/v4/living-subscription.schema.json +55 -0
- package/schemas/v4/meta-analysis.schema.json +152 -0
- package/schemas/verdict.schema.json +56 -0
- package/scripts/__init__.py +0 -0
- package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
- package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
- package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
- package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
- package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
- package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
- package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
- package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
- package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
- package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
- package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
- package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
- package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
- package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
- package/scripts/audit_dois.py +292 -0
- package/scripts/bake_pack.sh +37 -0
- package/scripts/benchmark.py +183 -0
- package/scripts/benchmark_evaluator.py +371 -0
- package/scripts/benchmark_judge.py +535 -0
- package/scripts/benchmark_routing.py +120 -0
- package/scripts/benchmark_v2.py +304 -0
- package/scripts/benchmark_v3.py +552 -0
- package/scripts/build_esl_artifacts.py +1921 -0
- package/scripts/build_evidence_library.py +307 -0
- package/scripts/build_killer_demo.py +295 -0
- package/scripts/build_result.py +311 -0
- package/scripts/check_version_consistency.py +96 -0
- package/scripts/citation_check.py +123 -0
- package/scripts/claim_audit.py +157 -0
- package/scripts/complexity_gate.py +180 -0
- package/scripts/compute_confidence.py +176 -0
- package/scripts/dashboard_server.py +536 -0
- package/scripts/did_regression.py +315 -0
- package/scripts/effect_calculator.py +99 -0
- package/scripts/enrich_projects_human_and_lieflat.py +315 -0
- package/scripts/evidence_matrix.py +129 -0
- package/scripts/evidence_score.py +234 -0
- package/scripts/evidence_semantics.py +87 -0
- package/scripts/fetch_benchmark.py +177 -0
- package/scripts/generate_metrics.py +99 -0
- package/scripts/generate_new_projects.py +686 -0
- package/scripts/generate_promo.py +192 -0
- package/scripts/lint_report_layout.py +182 -0
- package/scripts/orchestrator.py +1456 -0
- package/scripts/pre_verdict_gate.py +513 -0
- package/scripts/quickstart.py +121 -0
- package/scripts/rebake_all_5themes.py +88 -0
- package/scripts/recompute_demo_quality.py +205 -0
- package/scripts/render_report.py +270 -0
- package/scripts/render_report_html.py +356 -0
- package/scripts/retraction_watch.py +110 -0
- package/scripts/run_workspace.py +337 -0
- package/scripts/serve_web.py +54 -0
- package/scripts/skill_lint.py +150 -0
- package/scripts/startup_probe.py +265 -0
- package/scripts/sync_killer_demo_report.py +270 -0
- package/scripts/test_adversarial_empirical.py +541 -0
- package/scripts/validate_schema.py +256 -0
- package/skill/agents/education-planner.md +80 -0
- package/skill/agents/evaluation-designer.md +74 -0
- package/skill/agents/evidence-analyst.md +106 -0
- package/skill/agents/evidence-judge.md +111 -0
- package/skill/agents/evidence-retriever.md +80 -0
- package/skill/agents/intervention-designer.md +82 -0
- package/skill/agents/method-reviewer.md +104 -0
- package/skill/agents/skeptic.md +89 -0
- package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
- package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
- package/skill/sub-skills/data-analysis/SKILL.md +23 -0
- package/skill/sub-skills/ethics-review/SKILL.md +25 -0
- package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
- package/skill/sub-skills/evidence-review/SKILL.md +18 -0
- package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
- package/skill/sub-skills/literature-review/SKILL.md +21 -0
- package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
- package/skill/sub-skills/report-generation/SKILL.md +51 -0
- package/skill/sub-skills/research-planning/SKILL.md +21 -0
- package/skill/sub-skills/study-design/SKILL.md +16 -0
- package/skill/task-briefs/adjudicate.md +17 -0
- package/skill/task-briefs/audit.md +15 -0
- package/skill/task-briefs/challenge.md +15 -0
- package/skill/task-briefs/evaluate.md +13 -0
- package/skill/task-briefs/extract.md +16 -0
- package/skill/task-briefs/frame.md +17 -0
- package/skill/task-briefs/intervene.md +14 -0
- package/skill/task-briefs/present.md +16 -0
- package/skill/task-briefs/retrieve.md +15 -0
- package/visualization/eduevidence-report/assets/base.css +337 -0
- package/visualization/eduevidence-report/motion/motion.css +157 -0
- package/visualization/eduevidence-report/motion/motion.js +107 -0
- package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
- package/visualization/eduevidence-report/references/component-catalog.md +145 -0
- package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
- package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
- package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
- package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
- package/visualization/eduevidence-report/references/motion-system.md +31 -0
- package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
- package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
- package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
- package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
- package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
- package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
- package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
- package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
- package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
- package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
- package/visualization/eduevidence-report/themes/academic.css +94 -0
- package/visualization/eduevidence-report/themes/claude.css +96 -0
- package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
- package/visualization/eduevidence-report/themes/datalab.css +151 -0
- package/visualization/eduevidence-report/themes/presentation.css +140 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""build_figures.py — Academic Figures adapter (v5 Iteration 4, §23-30).
|
|
3
|
+
|
|
4
|
+
Purpose = statistical_publication -> Academic Figures (v5 §34 router).
|
|
5
|
+
From result.json, produce:
|
|
6
|
+
- figure_data.json publication-ready data (adapter output, §52)
|
|
7
|
+
- figures/*.svg publication SVG (Okabe-Ito / Nature / Conservative)
|
|
8
|
+
- figures/*.png|pdf raster/vector export when matplotlib is available
|
|
9
|
+
|
|
10
|
+
Publication conventions (v5 §25-30):
|
|
11
|
+
- truthful: only pre-existing error bars / significance from the analysis
|
|
12
|
+
- figure provenance attached to every figure
|
|
13
|
+
- Okabe-Ito colorblind-safe palette by default
|
|
14
|
+
- auto-trigger when result contains numeric comparisons / CI / significance
|
|
15
|
+
(recommend_academic_figure = true, §58)
|
|
16
|
+
|
|
17
|
+
Zero hard dependency: SVG generation is pure-Python; matplotlib export is
|
|
18
|
+
optional (guarded import) for PNG/PDF.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
python3 visualization/eduevidence-report/scripts/build_figures.py \
|
|
22
|
+
--result examples/ai-coding-assistant/result.json \
|
|
23
|
+
--out-dir examples/ai-coding-assistant/figures
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
import xml.sax.saxutils as sax
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from adapter_contract import load_result, write_adapter_output
|
|
35
|
+
from zh_labels import zh_outcome
|
|
36
|
+
from build_charts import effect_outcomes
|
|
37
|
+
|
|
38
|
+
OKABE_ITO = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7", "#000000"]
|
|
39
|
+
NATURE = ["#1F4E5F", "#5B8C9E", "#9E4B3A", "#7A8B5C", "#C2A24A", "#4E4E4E"]
|
|
40
|
+
PALM = ["#B8694A", "#5E8A6A", "#C99A4A", "#8A867E", "#A85B53", "#3A3833"]
|
|
41
|
+
PORCELAIN = ["#0284C7", "#10B981", "#F59E0B", "#64748B", "#EF4444", "#0F172A"]
|
|
42
|
+
WIRE = ["#38BDF8", "#10B981", "#F59E0B", "#EF4444", "#818CF8", "#F8FAFC"]
|
|
43
|
+
JUDICIAL = ["#F59E0B", "#F24D29", "#10B981", "#B8694A", "#D97706", "#FFFFFF"]
|
|
44
|
+
CONSERVATIVE = ["#4E4E4E", "#8C8C8C", "#B0B0B0", "#6B6B6B", "#D0D0D0", "#2E2E2E"]
|
|
45
|
+
|
|
46
|
+
THEME_PALETTES = {
|
|
47
|
+
"okabe_ito": OKABE_ITO,
|
|
48
|
+
"nature": NATURE,
|
|
49
|
+
"mono": NATURE,
|
|
50
|
+
"academic": NATURE,
|
|
51
|
+
"academic_paper": NATURE,
|
|
52
|
+
"claude": PALM,
|
|
53
|
+
"claude_research": PALM,
|
|
54
|
+
"palm": PALM,
|
|
55
|
+
"datalab": PORCELAIN,
|
|
56
|
+
"datalab_light": PORCELAIN,
|
|
57
|
+
"porcelain": PORCELAIN,
|
|
58
|
+
"datalab_dark": WIRE,
|
|
59
|
+
"datalab-dark": WIRE,
|
|
60
|
+
"wire": WIRE,
|
|
61
|
+
"presentation": JUDICIAL,
|
|
62
|
+
"presentation_judge": JUDICIAL,
|
|
63
|
+
"judicial": JUDICIAL,
|
|
64
|
+
"conservative": CONSERVATIVE
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _esc(t: Any) -> str:
|
|
69
|
+
return sax.escape(str(t if t is not None else ""))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _linear_ticks(maxv: int) -> list[int]:
|
|
73
|
+
"""Integer tick labels at roughly uniform intervals (never skip the top)."""
|
|
74
|
+
if maxv <= 0:
|
|
75
|
+
return [0]
|
|
76
|
+
if maxv <= 5:
|
|
77
|
+
return list(range(0, maxv + 1))
|
|
78
|
+
step = max(1, (maxv + 4) // 5) # ceil division for ~5 intervals
|
|
79
|
+
ticks = list(range(0, maxv + 1, step))
|
|
80
|
+
if ticks[-1] < maxv:
|
|
81
|
+
ticks.append(maxv)
|
|
82
|
+
return ticks
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _figure_svg(title: str, caption: str, body: str, w: int = 720, h: int = 300) -> str:
|
|
86
|
+
return (f'<svg viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg" role="img" '
|
|
87
|
+
f'aria-label="{_esc(caption)}">'
|
|
88
|
+
f'<rect width="{w}" height="{h}" fill="#FFFFFF"/>'
|
|
89
|
+
f'{body}'
|
|
90
|
+
f'<text x="20" y="{h - 10}" font-size="11" fill="#333333" font-style="italic">{_esc(caption)}</text>'
|
|
91
|
+
f'</svg>')
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _bar_chart(title: str, names: list[str], values: list[float], palette: list[str],
|
|
95
|
+
caption: str, unit: str = "", vmax: float | None = None, h: int = 300) -> str:
|
|
96
|
+
w = 720
|
|
97
|
+
plot_x, plot_w, plot_y, plot_h = 70, 580, 50, 200
|
|
98
|
+
maxv = vmax if vmax is not None else (max(values) * 1.15 if values else 1)
|
|
99
|
+
if maxv <= 0:
|
|
100
|
+
maxv = 1
|
|
101
|
+
bw = min(46, plot_w / max(1, len(names)) * 0.6)
|
|
102
|
+
body = [f'<line x1="{plot_x}" y1="{plot_y + plot_h}" x2="{plot_x + plot_w}" '
|
|
103
|
+
f'y2="{plot_y + plot_h}" stroke="#333" stroke-width="1"/>']
|
|
104
|
+
for i, (name, value) in enumerate(zip(names, values)):
|
|
105
|
+
bh = (value / maxv) * plot_h
|
|
106
|
+
x = plot_x + (i + 0.5) * (plot_w / len(names)) - bw / 2
|
|
107
|
+
y = plot_y + plot_h - bh
|
|
108
|
+
body.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{bw:.1f}" height="{max(bh, 1):.1f}" '
|
|
109
|
+
f'fill="{palette[i % len(palette)]}"/>')
|
|
110
|
+
body.append(f'<text x="{x + bw / 2:.1f}" y="{plot_y + plot_h + 16}" text-anchor="middle" '
|
|
111
|
+
f'font-size="10" fill="#333">{_esc(name)}</text>')
|
|
112
|
+
body.append(f'<text x="{x + bw / 2:.1f}" y="{y - 4:.1f}" text-anchor="middle" font-size="10" '
|
|
113
|
+
f'fill="#333">{value:.0f}{_esc(unit)}</text>')
|
|
114
|
+
# 计数图 Y 轴整数刻度:线性间隔,绝不产生 [0,1,2,3,4,12] 式失真刻度
|
|
115
|
+
ticks = _linear_ticks(int(maxv)) if maxv > 0 else [0]
|
|
116
|
+
for t in ticks:
|
|
117
|
+
ty = plot_y + plot_h - (t / maxv) * plot_h
|
|
118
|
+
body.append(f'<line x1="{plot_x - 5}" y1="{ty:.1f}" x2="{plot_x}" y2="{ty:.1f}" stroke="#999"/>')
|
|
119
|
+
body.append(f'<text x="{plot_x - 8}" y="{ty + 3:.1f}" text-anchor="end" font-size="9" '
|
|
120
|
+
f'fill="#666">{t}</text>')
|
|
121
|
+
body.append(f'<text x="{plot_x + plot_w / 2}" y="30" text-anchor="middle" font-size="14" '
|
|
122
|
+
f'font-weight="700" fill="#111">{_esc(title)}</text>')
|
|
123
|
+
return _figure_svg(title, caption, "".join(body), h=h)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _grouped_bar_chart(title: str, names: list[str], series: list[dict],
|
|
127
|
+
caption: str, palette: list[str], h: int = 300) -> str:
|
|
128
|
+
"""方向分组条形图:每组一个结果类型,内部并列 support / contradict / neutral
|
|
129
|
+
三根柱(P0-12)。计数轴整数刻度(P0-11)。"""
|
|
130
|
+
w = 720
|
|
131
|
+
plot_x, plot_w, plot_y, plot_h = 70, 580, 50, 200
|
|
132
|
+
maxv = max([1] + [abs(v) for s in series for v in s["data"]])
|
|
133
|
+
group_w = plot_w / max(1, len(names))
|
|
134
|
+
n_series = len(series)
|
|
135
|
+
bar_w = min(34, group_w / (n_series + 1))
|
|
136
|
+
body = [f'<line x1="{plot_x}" y1="{plot_y + plot_h}" x2="{plot_x + plot_w}" '
|
|
137
|
+
f'y2="{plot_y + plot_h}" stroke="#333" stroke-width="1"/>']
|
|
138
|
+
for i, name in enumerate(names):
|
|
139
|
+
cx = plot_x + group_w * (i + 0.5)
|
|
140
|
+
body.append(f'<text x="{cx:.1f}" y="{plot_y + plot_h + 16}" text-anchor="middle" '
|
|
141
|
+
f'font-size="10" fill="#333">{_esc(name)}</text>')
|
|
142
|
+
for j, s in enumerate(series):
|
|
143
|
+
value = s["data"][i] if i < len(s["data"]) else 0
|
|
144
|
+
bh = (value / maxv) * plot_h
|
|
145
|
+
x = plot_x + group_w * i + group_w / 2 + bar_w * (j - (n_series - 1) / 2)
|
|
146
|
+
y = plot_y + plot_h - bh
|
|
147
|
+
body.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{bar_w:.1f}" '
|
|
148
|
+
f'height="{max(bh, 1):.1f}" fill="{palette[j % len(palette)]}"/>')
|
|
149
|
+
if bh > 8:
|
|
150
|
+
body.append(f'<text x="{x + bar_w / 2:.1f}" y="{y - 3:.1f}" text-anchor="middle" '
|
|
151
|
+
f'font-size="9" fill="#333">{value:.0f}</text>')
|
|
152
|
+
ticks = [0]
|
|
153
|
+
while ticks[-1] < maxv and len(ticks) < 5:
|
|
154
|
+
ticks.append(ticks[-1] + 1)
|
|
155
|
+
if ticks[-1] < maxv:
|
|
156
|
+
ticks.append(int(maxv))
|
|
157
|
+
for t in ticks:
|
|
158
|
+
ty = plot_y + plot_h - (t / maxv) * plot_h
|
|
159
|
+
body.append(f'<line x1="{plot_x - 5}" y1="{ty:.1f}" x2="{plot_x}" y2="{ty:.1f}" stroke="#999"/>')
|
|
160
|
+
body.append(f'<text x="{plot_x - 8}" y="{ty + 3:.1f}" text-anchor="end" font-size="9" '
|
|
161
|
+
f'fill="#666">{t}</text>')
|
|
162
|
+
body.append(f'<text x="{plot_x + plot_w / 2}" y="30" text-anchor="middle" font-size="14" '
|
|
163
|
+
f'font-weight="700" fill="#111">{_esc(title)}</text>')
|
|
164
|
+
# 图例
|
|
165
|
+
lx = plot_x
|
|
166
|
+
for s in series:
|
|
167
|
+
body.append(f'<rect x="{lx}" y="8" width="10" height="10" '
|
|
168
|
+
f'fill="{palette[series.index(s) % len(palette)]}"/>')
|
|
169
|
+
body.append(f'<text x="{lx + 14}" y="17" font-size="10" fill="#333">{_esc(s["name"])}</text>')
|
|
170
|
+
lx += 14 + len(s["name"]) * 11 + 18
|
|
171
|
+
return _figure_svg(title, caption, "".join(body), h=h)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _scatter_chart(title: str, points: list[tuple[float, float]], labels: list[str],
|
|
175
|
+
palette: list[str], caption: str, h: int = 300) -> str:
|
|
176
|
+
w = 720
|
|
177
|
+
plot_x, plot_w, plot_y, plot_h = 70, 580, 50, 200
|
|
178
|
+
xs = [p[0] for p in points]
|
|
179
|
+
ys = [p[1] for p in points]
|
|
180
|
+
xmax = max(xs) * 1.15 if xs else 1
|
|
181
|
+
ymax = 1.0
|
|
182
|
+
body = [
|
|
183
|
+
f'<line x1="{plot_x}" y1="{plot_y + plot_h}" x2="{plot_x + plot_w}" y2="{plot_y + plot_h}" stroke="#333"/>',
|
|
184
|
+
f'<line x1="{plot_x}" y1="{plot_y}" x2="{plot_x}" y2="{plot_y + plot_h}" stroke="#333"/>',
|
|
185
|
+
]
|
|
186
|
+
for i, ((x, y), label) in enumerate(zip(points, labels)):
|
|
187
|
+
px = plot_x + (x / xmax) * plot_w
|
|
188
|
+
py = plot_y + plot_h - (y / ymax) * plot_h
|
|
189
|
+
body.append(f'<circle cx="{px:.1f}" cy="{py:.1f}" r="6" fill="{palette[i % len(palette)]}" '
|
|
190
|
+
f'opacity="0.9"/>')
|
|
191
|
+
body.append(f'<text x="{px + 8:.1f}" y="{py - 6:.1f}" font-size="10" fill="#333">{_esc(label)}</text>')
|
|
192
|
+
body.append(f'<text x="{plot_x + plot_w / 2}" y="30" text-anchor="middle" font-size="14" '
|
|
193
|
+
f'font-weight="700" fill="#111">{_esc(title)}</text>')
|
|
194
|
+
body.append(f'<text x="{plot_x + plot_w - 10}" y="{plot_y + plot_h + 16}" text-anchor="end" '
|
|
195
|
+
f'font-size="10" fill="#666">cost (USD)</text>')
|
|
196
|
+
return _figure_svg(title, caption, "".join(body), h=h)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _forest_plot_svg(title: str, points: list[dict], caption: str, h: int = 420) -> str:
|
|
200
|
+
"""Publication-Grade Meta-Analysis Forest Plot.
|
|
201
|
+
|
|
202
|
+
Shows effect sizes (Hedges' g), 95% Confidence Intervals, study weights,
|
|
203
|
+
and the diverging contrast between in-task procedural speed vs delayed unassisted transfer.
|
|
204
|
+
"""
|
|
205
|
+
w = 820
|
|
206
|
+
header_y = 52
|
|
207
|
+
row_start_y = 78
|
|
208
|
+
row_height = 28
|
|
209
|
+
|
|
210
|
+
# Plot bounds: scale g from -1.0 (x=330) to +1.0 (x=670), center 0.0 at x=500
|
|
211
|
+
plot_left = 330
|
|
212
|
+
plot_right = 670
|
|
213
|
+
plot_center = (plot_left + plot_right) / 2 # x=500
|
|
214
|
+
scale = (plot_right - plot_left) / 2.0 # 170 px per 1.0 g
|
|
215
|
+
|
|
216
|
+
total_h = max(h, row_start_y + len(points) * row_height + 70)
|
|
217
|
+
|
|
218
|
+
body = [
|
|
219
|
+
f'<text x="{w / 2:.1f}" y="28" text-anchor="middle" font-size="15" font-weight="700" fill="#111">{_esc(title)}</text>',
|
|
220
|
+
f'<text x="24" y="{header_y}" font-size="11" font-weight="700" fill="#444">Study & Venue</text>',
|
|
221
|
+
f'<text x="240" y="{header_y}" font-size="11" font-weight="700" fill="#444">Dimension</text>',
|
|
222
|
+
f'<text x="{plot_center:.1f}" y="{header_y}" text-anchor="middle" font-size="10" font-weight="700" fill="#444">Hedges\' g [95% CI]</text>',
|
|
223
|
+
f'<text x="{w - 24:.1f}" y="{header_y}" text-anchor="end" font-size="11" font-weight="700" fill="#444">Effect [95% CI]</text>',
|
|
224
|
+
f'<line x1="20" y1="{header_y + 8}" x2="{w - 20}" y2="{header_y + 8}" stroke="#333" stroke-width="1"/>',
|
|
225
|
+
# Vertical zero line (g = 0.0)
|
|
226
|
+
f'<line x1="{plot_center:.1f}" y1="{header_y + 8}" x2="{plot_center:.1f}" y2="{total_h - 40}" stroke="#999" stroke-dasharray="3,3"/>',
|
|
227
|
+
# Bottom axis scale
|
|
228
|
+
f'<line x1="{plot_left}" y1="{total_h - 40}" x2="{plot_right}" y2="{total_h - 40}" stroke="#333" stroke-width="1"/>',
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
for tick_g in [-1.0, -0.5, 0.0, 0.5, 1.0]:
|
|
232
|
+
tx = plot_center + tick_g * scale
|
|
233
|
+
body.append(f'<line x1="{tx:.1f}" y1="{total_h - 40}" x2="{tx:.1f}" y2="{total_h - 35}" stroke="#333"/>')
|
|
234
|
+
body.append(f'<text x="{tx:.1f}" y="{total_h - 24}" text-anchor="middle" font-size="9" fill="#555">{tick_g:+.1f}</text>')
|
|
235
|
+
|
|
236
|
+
body.append(f'<text x="{plot_left + 10}" y="{total_h - 10}" text-anchor="start" font-size="9" fill="#A85B53">← Favors Control (Deficit)</text>')
|
|
237
|
+
body.append(f'<text x="{plot_right - 10}" y="{total_h - 10}" text-anchor="end" font-size="9" fill="#5E8A6A">Favors AI (Gain) →</text>')
|
|
238
|
+
|
|
239
|
+
for i, p in enumerate(points):
|
|
240
|
+
ry = row_start_y + i * row_height
|
|
241
|
+
g_val = p.get("effect_size", 0.0)
|
|
242
|
+
ci_l = p.get("ci_lower")
|
|
243
|
+
ci_u = p.get("ci_upper")
|
|
244
|
+
has_ci = ci_l is not None and ci_u is not None
|
|
245
|
+
dim = p.get("outcome_dimension", "")
|
|
246
|
+
|
|
247
|
+
is_pos = g_val > 0.10
|
|
248
|
+
color = "#5E8A6A" if is_pos else ("#A85B53" if g_val < -0.05 else "#C99A4A")
|
|
249
|
+
|
|
250
|
+
# Study label & Dimension
|
|
251
|
+
body.append(f'<text x="24" y="{ry + 4}" font-size="10" font-weight="500" fill="#222">{_esc(p.get("study_label", "Study"))}</text>')
|
|
252
|
+
body.append(f'<text x="240" y="{ry + 4}" font-size="9" fill="#666">{_esc(dim.replace("_", " ").title()[:14])}</text>')
|
|
253
|
+
|
|
254
|
+
x_pt = max(plot_left, min(plot_right, plot_center + g_val * scale))
|
|
255
|
+
if has_ci:
|
|
256
|
+
x_ci_l = max(plot_left, min(plot_right, plot_center + ci_l * scale))
|
|
257
|
+
x_ci_u = max(plot_left, min(plot_right, plot_center + ci_u * scale))
|
|
258
|
+
body.append(f'<line x1="{x_ci_l:.1f}" y1="{ry}" x2="{x_ci_u:.1f}" y2="{ry}" stroke="{color}" stroke-width="1.5"/>')
|
|
259
|
+
body.append(f'<line x1="{x_ci_l:.1f}" y1="{ry - 4}" x2="{x_ci_l:.1f}" y2="{ry + 4}" stroke="{color}" stroke-width="1.5"/>')
|
|
260
|
+
body.append(f'<line x1="{x_ci_u:.1f}" y1="{ry - 4}" x2="{x_ci_u:.1f}" y2="{ry + 4}" stroke="{color}" stroke-width="1.5"/>')
|
|
261
|
+
# Center square (point only when CI not reported)
|
|
262
|
+
body.append(f'<rect x="{x_pt - 3.5:.1f}" y="{ry - 3.5:.1f}" width="7" height="7" fill="{color}"/>')
|
|
263
|
+
|
|
264
|
+
# Numeric label: no fabricated precision when CI missing
|
|
265
|
+
if has_ci:
|
|
266
|
+
body.append(f'<text x="{w - 24:.1f}" y="{ry + 4}" text-anchor="end" font-size="10" font-family="monospace" fill="#333">{g_val:+.2f} [{ci_l:+.2f}, {ci_u:+.2f}]</text>')
|
|
267
|
+
else:
|
|
268
|
+
body.append(f'<text x="{w - 24:.1f}" y="{ry + 4}" text-anchor="end" font-size="10" font-family="monospace" fill="#777">{g_val:+.2f} [CI not reported]</text>')
|
|
269
|
+
|
|
270
|
+
return _figure_svg(title, caption, "".join(body), w=w, h=total_h)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def build_figure_data(result: dict) -> dict[str, Any]:
|
|
274
|
+
"""figure_data.json — publication-ready adapter output (v5 §52)."""
|
|
275
|
+
outcomes = effect_outcomes(result)
|
|
276
|
+
benchmark = result.get("benchmark", {})
|
|
277
|
+
decision = result.get("decision", {})
|
|
278
|
+
return {
|
|
279
|
+
"source": "result.json",
|
|
280
|
+
"outcomes": [
|
|
281
|
+
{"outcome_type": o.get("outcome_type"),
|
|
282
|
+
"positive_count": o.get("positive_count", 0),
|
|
283
|
+
"negative_count": o.get("negative_count", 0),
|
|
284
|
+
"null_count": o.get("null_count", 0)}
|
|
285
|
+
for o in outcomes
|
|
286
|
+
],
|
|
287
|
+
"benchmark_baselines": benchmark.get("baselines", {}),
|
|
288
|
+
"confidence": decision.get("confidence"),
|
|
289
|
+
"recommended_action": decision.get("recommended_action"),
|
|
290
|
+
"recommend_academic_figure": True,
|
|
291
|
+
"provenance": {
|
|
292
|
+
"generated_by": "build_figures.py",
|
|
293
|
+
"from": "result.json",
|
|
294
|
+
"statistics_source": "EduEvidence analysis result (no invented p-values)",
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
FIGURE_TITLES = {
|
|
300
|
+
"zh": {"fig1": "各结果类型的效应方向分布", "fig2": "各基线引用支持精度",
|
|
301
|
+
"fig3": "质量 vs 成本"},
|
|
302
|
+
"en": {"fig1": "Effect direction by outcome type", "fig2": "Citation support by baseline",
|
|
303
|
+
"fig3": "Quality vs Cost"},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
FIGURE_CAPTIONS = {
|
|
307
|
+
"zh": {
|
|
308
|
+
"fig1": "图 1. 各结果类型的正向 / 负向 / 零效应证据条数(基于 effect_direction;出版级学术图,不随主题变化)。来源:EduEvidence result.json。",
|
|
309
|
+
"fig2": "图 2. B0-B4 各基线的引用支持精度。来源:EduEvidence Benchmark v2。",
|
|
310
|
+
"fig3": "图 3. 各基线的引用支持率与单题成本的对比。",
|
|
311
|
+
},
|
|
312
|
+
"en": {
|
|
313
|
+
"fig1": "Fig. 1. Counts of positive / negative / null effects per outcome type "
|
|
314
|
+
"(based on effect_direction; publication figure, theme-independent). Source: EduEvidence result.json.",
|
|
315
|
+
"fig2": "Fig. 2. Citation support precision per baseline B0-B4. Source: EduEvidence Benchmark v2.",
|
|
316
|
+
"fig3": "Fig. 3. Citation support rate vs cost per question across baselines.",
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
DIR_SERIES = {
|
|
321
|
+
"zh": [{"name": "正向效应", "data": "positive_count"},
|
|
322
|
+
{"name": "负向效应", "data": "negative_count"},
|
|
323
|
+
{"name": "零效应", "data": "null_count"}],
|
|
324
|
+
"en": [{"name": "Positive effect", "data": "positive_count"},
|
|
325
|
+
{"name": "Negative effect", "data": "negative_count"},
|
|
326
|
+
{"name": "Null effect", "data": "null_count"}],
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def render_figures(figure_data: dict, theme: str = "okabe_ito", lang: str = "zh") -> dict[str, str]:
|
|
331
|
+
"""Render publication SVG figures from figure_data (pure Python)."""
|
|
332
|
+
theme_key = (theme or "okabe_ito").lower().replace("-", "_")
|
|
333
|
+
palette = THEME_PALETTES.get(theme_key, OKABE_ITO)
|
|
334
|
+
figures: dict[str, str] = {}
|
|
335
|
+
|
|
336
|
+
# Figure 1: Outcome × effect_direction (positive / negative / null 三色分组;
|
|
337
|
+
# 绝不把 relation_to_claim 的 support/contradict 当作 outcome 好坏;计数轴整数刻度。)
|
|
338
|
+
outcomes = figure_data.get("outcomes", [])
|
|
339
|
+
if outcomes:
|
|
340
|
+
names = [zh_outcome(o["outcome_type"]) if lang == "zh"
|
|
341
|
+
else o["outcome_type"] for o in outcomes]
|
|
342
|
+
series = [{"name": s["name"],
|
|
343
|
+
"data": [o.get(s["data"], 0) for o in outcomes]}
|
|
344
|
+
for s in DIR_SERIES[lang]]
|
|
345
|
+
figures["outcome-comparison.svg"] = _grouped_bar_chart(
|
|
346
|
+
FIGURE_TITLES[lang]["fig1"], names, series,
|
|
347
|
+
FIGURE_CAPTIONS[lang]["fig1"], palette)
|
|
348
|
+
|
|
349
|
+
# Figure 2: benchmark citation support
|
|
350
|
+
baselines = figure_data.get("benchmark_baselines", {})
|
|
351
|
+
if baselines:
|
|
352
|
+
names = list(baselines.keys())
|
|
353
|
+
citation = [b.get("citation_support_precision", 0) for b in baselines.values()]
|
|
354
|
+
figures["benchmark-citation-support.svg"] = _bar_chart(
|
|
355
|
+
FIGURE_TITLES[lang]["fig2"], names, citation, palette,
|
|
356
|
+
FIGURE_CAPTIONS[lang]["fig2"])
|
|
357
|
+
costs = [b.get("usage", {}).get("cost_usd", 0) for b in baselines.values()]
|
|
358
|
+
if len(costs) == len(citation):
|
|
359
|
+
figures["benchmark-quality-cost.svg"] = _scatter_chart(
|
|
360
|
+
FIGURE_TITLES[lang]["fig3"], list(zip(costs, citation)), names, palette,
|
|
361
|
+
FIGURE_CAPTIONS[lang]["fig3"])
|
|
362
|
+
|
|
363
|
+
forest_pts = figure_data.get("forest_plot_data", [])
|
|
364
|
+
if not forest_pts and "evidence_nodes" in figure_data:
|
|
365
|
+
for ev in figure_data["evidence_nodes"][:10]:
|
|
366
|
+
es = ev.get("effect_size")
|
|
367
|
+
es_dict = es if isinstance(es, dict) else {}
|
|
368
|
+
forest_pts.append({
|
|
369
|
+
"study_label": ev.get("study_label", ev.get("evidence_id", "Study")),
|
|
370
|
+
"outcome_dimension": ev.get("outcome_dimension", "GENERAL"),
|
|
371
|
+
"effect_size": es_dict.get("value", 0.0),
|
|
372
|
+
"ci_lower": es_dict.get("ci_lower"),
|
|
373
|
+
"ci_upper": es_dict.get("ci_upper"),
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
if forest_pts:
|
|
377
|
+
figures["forest-plot.svg"] = _forest_plot_svg(
|
|
378
|
+
"效应量森林图 (Forest Plot: 任务速度提升 vs 独立迁移赤字)",
|
|
379
|
+
forest_pts,
|
|
380
|
+
"图 4. 效应量森林图(Hedges' g 与 95% 置信区间;展示程序速度提升与无 AI 独立迁移赤字的尖锐分歧)。来源:EduEvidence SSOT 证据图谱。"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Lieflat gallery: data-driven composition only. Every chart is rendered
|
|
384
|
+
# from a charts_data extractor bundle; entries come from the validated
|
|
385
|
+
# visual_layout (resolve_visual_layout). Unregistered types never reach
|
|
386
|
+
# this point; insufficient data suppresses the chart with a reason.
|
|
387
|
+
return figures
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def render_lieflat_gallery(result: dict, theme: str, lang: str,
|
|
391
|
+
layout_entries: list) -> tuple[dict[str, str], dict]:
|
|
392
|
+
"""Render only the layout entries that passed resolve_visual_layout.
|
|
393
|
+
|
|
394
|
+
Each entry {chart_id, type, title_zh/en, subtitle_zh/en, caption_zh/en,
|
|
395
|
+
source, params} is rendered from the extractor bundle for its registry
|
|
396
|
+
source. Returns (figures_by_chart_id, meta) where meta records selected
|
|
397
|
+
charts, suppressed charts with reasons, and the number audit used by the
|
|
398
|
+
lieflat_data_bound integrity gate.
|
|
399
|
+
"""
|
|
400
|
+
from lieflat_engine import REGISTRY, render_figure
|
|
401
|
+
|
|
402
|
+
figures: dict[str, str] = {}
|
|
403
|
+
meta_out = {"selected": [], "suppressed": [], "audits": {}}
|
|
404
|
+
for entry in layout_entries or []:
|
|
405
|
+
fig_type = entry.get("type")
|
|
406
|
+
reg = REGISTRY.get(fig_type)
|
|
407
|
+
if reg is None:
|
|
408
|
+
meta_out["suppressed"].append({
|
|
409
|
+
"chart_id": entry.get("chart_id"), "type": fig_type,
|
|
410
|
+
"reason": f"unregistered type {fig_type!r}"})
|
|
411
|
+
continue
|
|
412
|
+
bundle, reason = reg["extractor"](result, entry.get("params") or {}, lang)
|
|
413
|
+
if bundle is None:
|
|
414
|
+
meta_out["suppressed"].append({
|
|
415
|
+
"chart_id": entry.get("chart_id"), "type": fig_type,
|
|
416
|
+
"catalog_ref": reg["catalog_ref"], "reason": reason})
|
|
417
|
+
continue
|
|
418
|
+
audit: list = []
|
|
419
|
+
meta = {
|
|
420
|
+
"lang": lang,
|
|
421
|
+
"title": entry.get(f"title_{lang}") or entry.get("title") or "",
|
|
422
|
+
"subtitle": entry.get(f"subtitle_{lang}") or entry.get("subtitle") or "",
|
|
423
|
+
"caption": entry.get(f"caption_{lang}") or entry.get("caption") or "",
|
|
424
|
+
"source": entry.get("source") or reg["source"],
|
|
425
|
+
}
|
|
426
|
+
try:
|
|
427
|
+
svg = render_figure(fig_type, bundle, theme, meta, audit=audit)
|
|
428
|
+
except ValueError as exc:
|
|
429
|
+
meta_out["suppressed"].append({
|
|
430
|
+
"chart_id": entry.get("chart_id"), "type": fig_type,
|
|
431
|
+
"catalog_ref": reg["catalog_ref"], "reason": f"render error: {exc}"})
|
|
432
|
+
continue
|
|
433
|
+
figures[entry.get("chart_id")] = svg
|
|
434
|
+
meta_out["audits"][entry.get("chart_id")] = {
|
|
435
|
+
"type": fig_type, "bundle": bundle, "audit": audit}
|
|
436
|
+
meta_out["selected"].append({
|
|
437
|
+
"chart_id": entry.get("chart_id"), "type": fig_type,
|
|
438
|
+
"catalog_ref": reg["catalog_ref"], "source": reg["source"]})
|
|
439
|
+
return figures, meta_out
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def export_png_pdf(svg_path: Path, out_dir: Path) -> None:
|
|
443
|
+
"""Optional matplotlib export to PNG/PDF (guarded; skip when unavailable)."""
|
|
444
|
+
try:
|
|
445
|
+
import matplotlib.pyplot as plt
|
|
446
|
+
import numpy as np # noqa: F401
|
|
447
|
+
except ImportError:
|
|
448
|
+
return
|
|
449
|
+
from io import StringIO
|
|
450
|
+
import re
|
|
451
|
+
|
|
452
|
+
svg_text = svg_path.read_text(encoding="utf-8")
|
|
453
|
+
rects = re.findall(r'<rect x="([\d.]+)" y="([\d.]+)" width="([\d.]+)" height="([\d.]+)" fill="(#[0-9A-Fa-f]{6})"',
|
|
454
|
+
svg_text)
|
|
455
|
+
texts = re.findall(r'<text x="([\d.]+)" y="([\d.]+)"[^>]*>([^<]+)</text>', svg_text)
|
|
456
|
+
if not rects:
|
|
457
|
+
return
|
|
458
|
+
fig, ax = plt.subplots(figsize=(8, 4))
|
|
459
|
+
for x, y, w, h, color in rects:
|
|
460
|
+
ax.bar(float(x), float(h), bottom=float(y), width=float(w), color=color, edgecolor="none")
|
|
461
|
+
ax.set_xlim(0, 720)
|
|
462
|
+
ax.set_ylim(0, 300)
|
|
463
|
+
ax.axis("off")
|
|
464
|
+
stem = svg_path.stem
|
|
465
|
+
fig.savefig(out_dir / f"{stem}.png", dpi=150, bbox_inches="tight")
|
|
466
|
+
fig.savefig(out_dir / f"{stem}.pdf", bbox_inches="tight")
|
|
467
|
+
plt.close(fig)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def main() -> int:
|
|
471
|
+
parser = argparse.ArgumentParser(description="Build Academic Figures from result.json")
|
|
472
|
+
parser.add_argument("--result", required=True)
|
|
473
|
+
parser.add_argument("--out", required=False,
|
|
474
|
+
help="output envelope JSON path (new unified contract)")
|
|
475
|
+
parser.add_argument("--out-dir", required=False,
|
|
476
|
+
help="[deprecated] legacy directory output; use --out")
|
|
477
|
+
parser.add_argument("--theme", choices=["okabe_ito", "nature", "conservative"],
|
|
478
|
+
default="okabe_ito")
|
|
479
|
+
parser.add_argument("--lang", choices=["zh", "en"], default="zh")
|
|
480
|
+
parser.add_argument("--export-png", action="store_true",
|
|
481
|
+
help="try matplotlib export to PNG/PDF (optional)")
|
|
482
|
+
args = parser.parse_args()
|
|
483
|
+
|
|
484
|
+
if not args.out and not args.out_dir:
|
|
485
|
+
parser.error("one of --out / --out-dir is required")
|
|
486
|
+
|
|
487
|
+
result = load_result(args.result)
|
|
488
|
+
figure_data = build_figure_data(result)
|
|
489
|
+
figures = render_figures(figure_data, theme=args.theme, lang=args.lang)
|
|
490
|
+
|
|
491
|
+
if args.out:
|
|
492
|
+
# New unified contract: single envelope JSON with structured data + SVG strings.
|
|
493
|
+
write_adapter_output(args.out, "figures", args.result,
|
|
494
|
+
{"figure_data": figure_data, "figures": figures},
|
|
495
|
+
locale=args.lang)
|
|
496
|
+
print(f"wrote {args.out} (figures_data + {len(figures)} svg figures)")
|
|
497
|
+
return 0
|
|
498
|
+
|
|
499
|
+
# Legacy --out-dir compatibility (deprecated; migrate callers to --out).
|
|
500
|
+
out_dir = Path(args.out_dir)
|
|
501
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
502
|
+
(out_dir / "figure_data.json").write_text(
|
|
503
|
+
json.dumps(figure_data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
504
|
+
for name, svg in figures.items():
|
|
505
|
+
path = out_dir / name
|
|
506
|
+
path.write_text(svg, encoding="utf-8")
|
|
507
|
+
if args.export_png:
|
|
508
|
+
export_png_pdf(path, out_dir)
|
|
509
|
+
print(f"wrote {len(figures) + 1} files to {args.out_dir}: "
|
|
510
|
+
f"{', '.join(sorted(figures.keys()))}")
|
|
511
|
+
return 0
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
if __name__ == "__main__":
|
|
515
|
+
sys.exit(main())
|