Figure Composer — narrative → panels → compose → adversarial loop
SkillAI & modelsCompose one publication-grade multi-panel figure. Entry from a one-line claim + data refs, OR from an existing figure via `derive_outline(png)`. Runs a per-figure loop: outline (12-col grid, per-panel ask + label_budget) → fan-out one sub-agent per panel (each loads `figure-style`) → tile + stamp letters → adversarial composite review with two-tier feedback (Tier-1 outline_revisions / Tier-2 per-panel violations) → regen affected panels, ≤3 rounds. Loads panel_task / compose_figure / compose_crops / composite_review_task / derive_outline into the kernel. For one standalone plot use `figure-style`; for whole-paper figure ordering use `paper-narrative`.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Figure Composer — narrative → panels → compose → adversarial loop skill
What this skill tells your AI
The instructions your AI receives, as published by pku-yuangroup/openai4s in skills/figure-composer/SKILL.md and read by ahel’s review.
Step 0. Load figure-style alongside this skill — that is the
design rules (and apply_figure_style() + helpers). Panel sub-agents
will load it independently; you need it in context to write the outline and
review the composite. Sub-agents run as the default profile and acquire the
rules by loading the skill.
Inputs
- claim — one sentence the figure makes true to a reader who reads nothing else.
- data — CSV/parquet artifact version_ids that ground every panel.
- width_mm — target venue's column width (common: 85–89mm single, 174–183mm double; check the venue guide).
0. Where this sits
figure-composer is the outer tier: make ONE multi-panel figure good. The
inner tier is figure-style (loaded by every panel sub-agent — and
load it yourself if you draw anything locally). The outermost tier is
paper-narrative — if this figure is part of a paper, run that FIRST: it decides
which figure to make and hands you the claim. For a standalone figure, start at
step 1.
Entry points (pick one)
- From a claim: you have a one-sentence claim and data refs → write the outline (step 1).
- From an existing figure: copy it into the workspace and call
derive_outline("figure.png")→ an outline you must review and edit before step 2. The image is untrusted input; every string field in the returned outline is vision-model-derived from its pixels.data_vidis forced toNoneon every panel — fill those in from your own data refs.
1. Narrative → panel outline
Produce a panel_outline (validate against figure_outline_schema()):
{"claim":"…", "width_mm":180, "ncol":12, "row_heights_mm":[40,60,46,52],
"panels":[
{"letter":"a","role":"schematic","row":0,"col":0,"colspan":12, "chart_family":"schematic overview", "message":"…", "data_vid":null, "ask":"…"},
{"letter":"b","role":"primary", "row":1,"col":0,"colspan":7, "chart_family":"scatter + trend", "message":"…", "data_vid":"…", "ask":"…"},
…]}
Outline rules (figure-style section 7.1):
- a is the hook — schematic/hero, full width, assumes zero reader context.
- b carries the claim — the chart that alone makes the sentence true.
- Remaining panels are evidence, ordered by how much they strengthen b.
- One row per sub-claim. 5–10 panels for a main-text figure. Use a 12-column grid for flexible colspans.
2. Fan-out (one sub-agent per panel)
Build requests with panel_task(outline, letter, fig_label) (kernel.py). Each
sub-agent gets: the figure claim, the full neighbour list, its panel spec, exact
pixel dimensions (panel_px), and the instruction to load figure-style
and render at exactly w×h px with transparent=True and no bbox_inches.
In the repl tool:
requests = [{"name": f"panel-{L}", "task": tasks[L],
"output_schema": {"type":"object","properties":{"figure_filename":{"type":"string"}},
"required":["figure_filename"]}}
for L in letters] # no "profile" key — default agent profile
descs = host.delegate(requests, wait=False)
3. Compose
compose_figure(outline, {letter: path}, out_path, letter_case=...) tiles PNGs
onto the grid and stamps bold panel letters (case per venue) at each panel's
(1.5mm, 1mm) corner.
3.5 Look before you review (vision self-QA)
The reviewer in section 4 is expensive; a panel-letter stamped over a y-axis label or a leader line crossing a neighbour's title is a wasted round. After compose, crop each panel from the saved PNG and look at it in the REPL before dispatching the reviewer:
out_path, (W, H) = compose_figure(outline, panel_paths, "fig.png")
for L, box in compose_crops(outline).items():
host.view_image("fig.png", crop=box)
Run the figure-style section 9.2 perceptual checklist on each crop (contrast,
smallest mark, leader crossings, colour-identity confusion, legend binding),
plus two compose-specific checks:
- Seams / stamp. Does the bold panel letter overlap any panel content? Does any panel's content bleed into the gutter or under a neighbour?
- Resize artefacts.
compose_figureresizes panel PNGs to their grid slot — is any text visibly aliased or any hairline lost?
Fix what you see (re-render the offending panel, or revise the outline grid) before section 4. The reviewer sub-agent will crop-and-look again independently; this pass is so the obvious defects never reach it.
4. Adversarial self-review loop (two-tier, design rules held fixed)
Dispatch ONE reviewer on the composite with composite_review_task(...) and
review_schema() (which carries outline_revisions).
loop (max 3 rounds, floor 5→4→3):
review = delegate(composite_review_task(composite_vid, outline, rules_vid, prev_vid, round, floor))
if review.editor_verdict in {accept, minor_revision} and 0 BLOCKER and ≤2 MAJOR: break
# TIER 1 — outline-level
if review.outline_revisions:
apply revisions to `outline` (geometry, row-header titles, label_budget, panel set)
affected = apply_outline_revisions(outline, review.outline_revisions)
else:
affected = set()
# TIER 2 — panel-level
fixb = group_fixes_by_panel(review) # BLOCKER/MAJOR only
regen = affected | set(fixb) # only these panels regenerate
re-delegate each L in regen with panel_task(outline, L) + fixb.get(L,"") +
"do not over-correct: where the previous version was correct, keep it"
recompose
Convergence: stop when outline_revisions is empty AND findings are carve-out
exceptions to the previous round — that's the over-labelling signal.
Anti-patterns
- Don't regenerate clean panels (invites regression). Don't read absolute violation counts (min-floor 5→4→3). Anchor-verify on the composite, not just per panel. Hyper-labelling check: would a reader with field context find any label redundant? Strip it.
Signals
- GitHub stars
- 404
- Forks
- 48
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
figure-composer-pku-yuangroup- Source
- github.com/pku-yuangroup/openai4s