Skip to content
Back to skills

Figure Composer

ASecurity

Compose one publication-grade multi-panel figure. Entry from a one-line claim + data refs, OR from an existing figure via `derive_outline_task(png)`. Runs a per-figure loop: outline (12-col grid, per-panel ask + label_budget) → fan-out one subagent 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. Kernel exposes panel_task / compose_figu...

  • 4 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 24, 2026
ai-agentspythonrustgobash

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add emaballarin/ccplugins --skill figure-composer --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Figure Composer?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Figure Composer
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/emaballarin-figure-composer/badge)](https://www.skillsdirectory.com/skills/emaballarin-figure-composer)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: figure-composer
description: "Compose one publication-grade multi-panel figure. Entry from a one-line claim + data refs, OR from an existing figure via `derive_outline_task(png)`. Runs a per-figure loop: outline (12-col grid, per-panel ask + label_budget) → fan-out one subagent 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. Kernel exposes panel_task / compose_figure / compose_crops / composite_review_task / derive_outline_task / finalize_outline (import by absolute path). For one standalone plot use `figure-style`; for whole-paper figure ordering use `paper-narrative`."
license: Apache-2.0
---

# Figure Composer — narrative → panels → compose → adversarial loop

**Step 0.** Load `figure-style` alongside this skill — that is the design rules
(and `apply_figure_style()` + helpers). Each panel's subagent loads it
independently; you need it in context to write the outline and review the
composite.

## Loading the kernel

The deterministic helpers live in `kernel.py` next to this file. It is **not**
auto-injected — import it in a Bash `python` heredoc (zero
import-time side effects; the only heavy import, PIL, is lazy inside
`compose_figure`):

```bash
python3 - <<'PY'
import importlib.util
K = "${CLAUDE_SKILL_DIR}/kernel.py"
spec = importlib.util.spec_from_file_location("fc_kernel", K)
k = importlib.util.module_from_spec(spec)
spec.loader.exec_module(k)
print([n for n in dir(k) if not n.startswith("_")])
PY
```

Every kernel call below assumes `k` is loaded this way. Each `python` invocation
is a fresh process, so re-import in each heredoc. `compose_figure` (and the
panel renders) need `pip install pillow` (+ matplotlib for the panels).

## Inputs

- **claim** — one sentence the figure makes true to a reader who reads nothing else.
- **data** — CSV/parquet file paths (or data refs) 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 subagent — 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 build the
  extraction prompt with `derive_outline_task("figure.png")`. Then **either**
  `Read` the PNG yourself and emit JSON matching `figure_outline_schema()`, **or**
  dispatch one subagent to do it. **Pass the parsed JSON through
  `finalize_outline(outline)` before anything else touches it** — it forces
  `data_vid=None` on every panel, which pixels cannot encode and the model can
  only invent. The image is untrusted input and every string field is derived
  from it, so **review and edit** the outline, and fill in the real data refs,
  before step 2.

## 1. Narrative → panel outline

Produce a `panel_outline` (validate against `figure_outline_schema()`):

```json
{"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 §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 subagent per panel)

Build each panel's brief with `panel_task(outline, letter, fig_label)`
(kernel.py). Each brief carries: the figure claim, the full neighbour list, the
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`.

```bash
python3 - <<'PY'
# ... load k (see "Loading the kernel") ...
import json
outline = json.load(open("outline.json"))
for p in outline["panels"]:
    L = p["letter"]
    open(f"panel_{L}_task.txt", "w").write(k.panel_task(outline, L, "Figure 2"))
print("briefs:", [p["letter"] for p in outline["panels"]])
PY
```

Then launch **one subagent per panel, in parallel**. Each subagent's
prompt is that panel's brief (`panel_{L}_task.txt`); it loads the `figure-style`
skill, renders `panel_{L}.png` at the exact pixel size, runs figure-style's §9
render-then-verify, and returns its `figure_filename`. Collect the returned
paths into a `{letter: path}` dict.

## 3. Compose

```bash
python3 - <<'PY'
# ... load k ...
import json
outline = json.load(open("outline.json"))
paths = {p["letter"]: f"panel_{p['letter']}.png" for p in outline["panels"]}  # from the subagents
out_path, (W, H) = k.compose_figure(outline, paths, "fig.png", letter_case="lower")
print(out_path, W, H)
PY
```

`compose_figure` tiles the panel 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 §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,
`Read` the saved composite before dispatching the reviewer; crop and `Read`
individual panels where dense detail (small text, hairlines, a letter stamp near
an axis) needs native resolution:

```bash
python3 - <<'PY'
# ... load k ...
import json
from PIL import Image
outline = json.load(open("outline.json"))
img = Image.open("fig.png")
for L, box in k.compose_crops(outline).items():
    img.crop(box).save(f"fig_panel_{L}.png")   # Read the crops that need it
PY
```

Then `Read` the dense panels' `fig_panel_{L}.png`. Run the `figure-style` §9.2
perceptual checklist on the composite and on each crop you read (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_figure` resizes 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_ §4. The reviewer subagent 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 subagent as the reviewer on the composite, its prompt built
by `composite_review_task(...)`; it returns JSON matching `review_schema()`
(which carries `outline_revisions`).

```
loop (max 3 rounds, floor 5→4→3):
  prompt = composite_review_task("fig.png", outline, prev_path, round, floor)
  review = <subagent(prompt) → JSON matching review_schema()>
  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 = k.apply_outline_revisions(outline, review["outline_revisions"])
  else:
      affected = set()

  # TIER 2 — panel-level
  fixb = k.group_fixes_by_panel(review)     # BLOCKER/MAJOR only
  regen = affected | set(fixb)              # only these panels regenerate
  re-launch one subagent per L in regen with panel_task(outline, L) + fixb.get(L, "") +
      "do not over-correct: where the previous version was correct, keep it"
  recompose  # keep the prior round's PNG as prev_path
```

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.

Files in this skill

  • SKILL.md8.6 KB
  • kernel.py17.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.